-
-
Notifications
You must be signed in to change notification settings - Fork 384
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com> Co-authored-by: Federico Brigante <me@fregante.com> Co-authored-by: fisker <lionkay@gmail.com>
- Loading branch information
1 parent
a5d5562
commit 1558cbe
Showing
7 changed files
with
1,710 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
# Prefer `globalThis` over `window`, `self`, and `global` | ||
|
||
💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#preset-configs-eslintconfigjs). | ||
|
||
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). | ||
|
||
<!-- end auto-generated rule header --> | ||
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` --> | ||
|
||
This rule will enforce the use of `globalThis` over `window`, `self`, and `global`. | ||
|
||
However, there are several exceptions that remain permitted: | ||
|
||
1. Certain window/WebWorker-specific APIs, such as `window.innerHeight` and `self.postMessage` | ||
2. Window-specific events, such as `window.addEventListener('resize')` | ||
|
||
The complete list of permitted APIs can be found in the rule's [source code](../../rules/prefer-global-this.js). | ||
|
||
## Examples | ||
|
||
```js | ||
window; // ❌ | ||
globalThis; // ✅ | ||
``` | ||
|
||
```js | ||
window.foo; // ❌ | ||
globalThis.foo; // ✅ | ||
``` | ||
|
||
```js | ||
window[foo]; // ❌ | ||
globalThis[foo]; // ✅ | ||
``` | ||
|
||
```js | ||
global; // ❌ | ||
globalThis; // ✅ | ||
``` | ||
|
||
```js | ||
global.foo; // ❌ | ||
globalThis.foo; // ✅ | ||
``` | ||
|
||
```js | ||
const {foo} = window; // ❌ | ||
const {foo} = globalThis; // ✅ | ||
``` | ||
|
||
```js | ||
window.location; // ❌ | ||
globalThis.location; // ✅ | ||
|
||
window.innerWidth; // ✅ (Window specific API) | ||
window.innerHeight; // ✅ (Window specific API) | ||
``` | ||
|
||
```js | ||
window.navigator; // ❌ | ||
globalThis.navigator; // ✅ | ||
``` | ||
|
||
```js | ||
self.postMessage('Hello'); // ✅ (Web Worker specific API) | ||
self.onmessage = () => {}; // ✅ (Web Worker specific API) | ||
``` | ||
|
||
```js | ||
window.addEventListener('click', () => {}); // ❌ | ||
globalThis.addEventListener('click', () => {}); // ✅ | ||
|
||
window.addEventListener('resize', () => {}); // ✅ (Window specific event) | ||
window.addEventListener('load', () => {}); // ✅ (Window specific event) | ||
window.addEventListener('unload', () => {}); // ✅ (Window specific event) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
'use strict'; | ||
|
||
const MESSAGE_ID_ERROR = 'prefer-global-this/error'; | ||
const messages = { | ||
[MESSAGE_ID_ERROR]: 'Prefer `globalThis` 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', | ||
'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; | ||
} | ||
|
||
yield { | ||
node: identifier, | ||
messageId: MESSAGE_ID_ERROR, | ||
data: {value: identifier.name}, | ||
fix: fixer => fixer.replaceText(identifier, 'globalThis'), | ||
}; | ||
} | ||
}, | ||
}); | ||
|
||
/** @type {import('eslint').Rule.RuleModule} */ | ||
module.exports = { | ||
create, | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
description: 'Prefer `globalThis` over `window`, `self`, and `global`.', | ||
recommended: true, | ||
}, | ||
fixable: 'code', | ||
hasSuggestions: false, | ||
messages, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.