eslint-plugin-unicorn-x
Version:
More than 100 powerful ESLint rules
245 lines (214 loc) • 5.71 kB
JavaScript
const MESSAGE_ID_ERROR = 'prefer-global-this/error';
const messages = {
[MESSAGE_ID_ERROR]: 'Prefer `{{replacement}}` over `{{value}}`.',
};
const globalIdentifier = new Set(['window', 'self', 'global']);
const windowSpecificEvents = new Set([
'resize',
'blur',
'focus',
'load',
'scroll',
'scrollend',
'wheel',
'beforeunload', // Browsers might have specific behaviors on exactly `window.onbeforeunload =`
'message',
'messageerror',
'pagehide',
'pagereveal',
'pageshow',
'pageswap',
'unload',
]);
/**
Note: What kind of API should be a windows-specific interface?
1. It's directly related to window (✅ window.close())
2. It does NOT work well as globalThis.x or x (✅ window.frames, window.top)
Some constructors are occasionally related to window (like Element !== iframe.contentWindow.Element), but they don't need to mention window anyway.
Please use these criteria to decide whether an API should be added here. Context: https://github.com/sindresorhus/eslint-plugin-unicorn/pull/2410#discussion_r1695312427
*/
const windowSpecificAPIs = new Set([
// Properties and methods
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-window-object
'name',
'locationbar',
'menubar',
'personalbar',
'scrollbars',
'statusbar',
'toolbar',
'status',
'close',
'closed',
'stop',
'focus',
'blur',
'frames',
'length',
'top',
'opener',
'parent',
'frameElement',
'open',
'originAgentCluster',
'postMessage',
// Events commonly associated with "window"
...[...windowSpecificEvents].map((event) => `on${event}`),
// To add/remove/dispatch events that are commonly associated with "window"
// https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-flow
'addEventListener',
'removeEventListener',
'dispatchEvent',
// https://dom.spec.whatwg.org/#idl-index
'event', // Deprecated and quirky, best left untouched
// https://drafts.csswg.org/cssom-view/#idl-index
'screen',
'visualViewport',
'moveTo',
'moveBy',
'resizeTo',
'resizeBy',
'innerWidth',
'innerHeight',
'outerWidth',
'outerHeight',
'scrollX',
'pageXOffset',
'scrollY',
'pageYOffset',
'scroll',
'scrollTo',
'scrollBy',
'screenX',
'screenLeft',
'screenY',
'screenTop',
'screenWidth',
'screenHeight',
'devicePixelRatio',
]);
const webWorkerSpecificAPIs = new Set([
// https://html.spec.whatwg.org/multipage/workers.html#the-workerglobalscope-common-interface
'addEventListener',
'removeEventListener',
'dispatchEvent',
'self',
'location',
'navigator',
'onerror',
'onlanguagechange',
'onoffline',
'ononline',
'onrejectionhandled',
'onunhandledrejection',
// https://html.spec.whatwg.org/multipage/workers.html#dedicated-workers-and-the-dedicatedworkerglobalscope-interface
'name',
'postMessage',
'onconnect',
]);
/**
Check if the node is a window-specific API.
@param {import('estree').MemberExpression} node
@returns {boolean}
*/
const isWindowSpecificAPI = (node) => {
if (node.type !== 'MemberExpression') {
return false;
}
if (node.object.name !== 'window' || node.property.type !== 'Identifier') {
return false;
}
if (windowSpecificAPIs.has(node.property.name)) {
if (
['addEventListener', 'removeEventListener', 'dispatchEvent'].includes(
node.property.name,
) &&
node.parent.type === 'CallExpression' &&
node.parent.callee === node
) {
const argument = node.parent.arguments[0];
return (
argument &&
argument.type === 'Literal' &&
windowSpecificEvents.has(argument.value)
);
}
return true;
}
return false;
};
/**
@param {import('estree').Identifier} identifier
@returns {boolean}
*/
function isComputedMemberExpressionObject(identifier) {
return (
identifier.parent.type === 'MemberExpression' &&
identifier.parent.computed &&
identifier.parent.object === identifier
);
}
/**
Check if the node is a web worker specific API.
@param {import('estree').MemberExpression} node
@returns {boolean}
*/
const isWebWorkerSpecificAPI = (node) =>
node.type === 'MemberExpression' &&
node.object.name === 'self' &&
node.property.type === 'Identifier' &&
webWorkerSpecificAPIs.has(node.property.name);
/** @param {import('eslint').Rule.RuleContext} context */
const create = (context) => ({
*Program(program) {
const scope = context.sourceCode.getScope(program);
const references = [
// Variables declared at globals options
...scope.variables.flatMap((variable) =>
globalIdentifier.has(variable.name) ? variable.references : [],
),
// Variables not declared at globals options
...scope.through.filter((reference) =>
globalIdentifier.has(reference.identifier.name),
),
];
for (const {identifier} of references) {
if (
isComputedMemberExpressionObject(identifier) ||
isWindowSpecificAPI(identifier.parent) ||
isWebWorkerSpecificAPI(identifier.parent)
) {
continue;
}
// Skip the fix for `typeof window` and `typeof self`
const isTypeofLegacyGlobal =
identifier.parent.type === 'UnaryExpression' &&
identifier.parent.operator === 'typeof' &&
identifier.parent.argument === identifier;
const replacement = isTypeofLegacyGlobal
? 'globalThis.' + identifier.name
: 'globalThis';
yield {
node: identifier,
messageId: MESSAGE_ID_ERROR,
data: {replacement, value: identifier.name},
fix: (fixer) => fixer.replaceText(identifier, replacement),
};
}
},
});
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Prefer `globalThis` over `window`, `self`, and `global`.',
recommended: true,
},
fixable: 'code',
hasSuggestions: false,
messages,
},
};
export default config;