eslint-plugin-unicorn
Version:
More than 100 powerful ESLint rules
222 lines (191 loc) • 5.6 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;