Skip to content

Commit

Permalink
Merge branch 'main' into reduce-right
Browse files Browse the repository at this point in the history
  • Loading branch information
fregante committed Sep 29, 2024
2 parents 9d81360 + 1558cbe commit 93d298a
Show file tree
Hide file tree
Showing 7 changed files with 1,710 additions and 0 deletions.
76 changes: 76 additions & 0 deletions docs/rules/prefer-global-this.md
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)
```
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ If you don't use the preset, ensure you use the same `env` and `parserOptions` c
| [prefer-dom-node-text-content](docs/rules/prefer-dom-node-text-content.md) | Prefer `.textContent` over `.innerText`. || | 💡 |
| [prefer-event-target](docs/rules/prefer-event-target.md) | Prefer `EventTarget` over `EventEmitter`. || | |
| [prefer-export-from](docs/rules/prefer-export-from.md) | Prefer `export…from` when re-exporting. || 🔧 | 💡 |
| [prefer-global-this](docs/rules/prefer-global-this.md) | Prefer `globalThis` over `window`, `self`, and `global`. || 🔧 | |
| [prefer-includes](docs/rules/prefer-includes.md) | Prefer `.includes()` over `.indexOf()`, `.lastIndexOf()`, and `Array#some()` when checking for existence or non-existence. || 🔧 | 💡 |
| [prefer-json-parse-buffer](docs/rules/prefer-json-parse-buffer.md) | Prefer reading a JSON file as a buffer. | | 🔧 | |
| [prefer-keyboard-event-key](docs/rules/prefer-keyboard-event-key.md) | Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`. || 🔧 | |
Expand Down
210 changes: 210 additions & 0 deletions rules/prefer-global-this.js
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,
},
};
1 change: 1 addition & 0 deletions test/package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const RULES_WITHOUT_PASS_FAIL_SECTIONS = new Set([
'prefer-modern-math-apis',
'prefer-math-min-max',
'consistent-existence-index-check',
'prefer-global-this',
]);

test('Every rule is defined in index file in alphabetical order', t => {
Expand Down
Loading

0 comments on commit 93d298a

Please sign in to comment.