266 lines
6.6 KiB
JavaScript
266 lines
6.6 KiB
JavaScript
import {isStringLiteral} from './ast/index.js';
|
|
|
|
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',
|
|
'navigation',
|
|
|
|
// 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',
|
|
]);
|
|
|
|
const environmentSpecificApisByGlobalIdentifier = new Map([
|
|
['window', windowSpecificApis],
|
|
['self', webWorkerSpecificApis],
|
|
]);
|
|
|
|
function getStaticPropertyName(node) {
|
|
if (isStringLiteral(node)) {
|
|
return node.value;
|
|
}
|
|
|
|
if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
|
|
return node.quasis[0].value.cooked;
|
|
}
|
|
}
|
|
|
|
/**
|
|
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 identifier is used in an existence check for a known environment-specific API.
|
|
|
|
@param {import('estree').Identifier} identifier
|
|
@returns {boolean}
|
|
*/
|
|
function isKnownSpecificApiExistenceCheck(identifier) {
|
|
const specificApis = environmentSpecificApisByGlobalIdentifier.get(identifier.name);
|
|
if (!specificApis) {
|
|
return false;
|
|
}
|
|
|
|
const {parent} = identifier;
|
|
if (parent.type !== 'BinaryExpression' || parent.operator !== 'in' || parent.right !== identifier) {
|
|
return false;
|
|
}
|
|
|
|
const propertyName = getStaticPropertyName(parent.left);
|
|
if (typeof propertyName !== 'string') {
|
|
return false;
|
|
}
|
|
|
|
return specificApis.has(propertyName);
|
|
}
|
|
|
|
/**
|
|
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 => {
|
|
context.on('Program', function * (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)
|
|
|| isKnownSpecificApiExistenceCheck(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: 'unopinionated',
|
|
},
|
|
fixable: 'code',
|
|
hasSuggestions: false,
|
|
messages,
|
|
},
|
|
};
|
|
|
|
export default config;
|