eslint-plugin-unicorn 55.0.0 → 56.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-unicorn",
3
- "version": "55.0.0",
3
+ "version": "56.0.1",
4
4
  "description": "More than 100 powerful ESLint rules",
5
5
  "license": "MIT",
6
6
  "repository": "sindresorhus/eslint-plugin-unicorn",
@@ -23,6 +23,7 @@
23
23
  "fix:eslint-docs": "eslint-doc-generator",
24
24
  "fix:js": "npm run lint:js -- --fix",
25
25
  "fix:markdown": "npm run lint:markdown -- --fix",
26
+ "fix:snapshots": "ava --update-snapshots",
26
27
  "integration": "node ./test/integration/test.mjs",
27
28
  "lint": "run-p --continue-on-error lint:*",
28
29
  "lint:eslint-docs": "npm run fix:eslint-docs -- --check",
@@ -51,13 +52,13 @@
51
52
  "xo"
52
53
  ],
53
54
  "dependencies": {
54
- "@babel/helper-validator-identifier": "^7.24.5",
55
+ "@babel/helper-validator-identifier": "^7.24.7",
55
56
  "@eslint-community/eslint-utils": "^4.4.0",
56
57
  "ci-info": "^4.0.0",
57
58
  "clean-regexp": "^1.0.0",
58
- "core-js-compat": "^3.37.0",
59
- "esquery": "^1.5.0",
60
- "globals": "^15.7.0",
59
+ "core-js-compat": "^3.38.1",
60
+ "esquery": "^1.6.0",
61
+ "globals": "^15.9.0",
61
62
  "indent-string": "^4.0.0",
62
63
  "is-builtin-module": "^3.2.1",
63
64
  "jsesc": "^3.0.2",
@@ -65,41 +66,41 @@
65
66
  "read-pkg-up": "^7.0.1",
66
67
  "regexp-tree": "^0.1.27",
67
68
  "regjsparser": "^0.10.0",
68
- "semver": "^7.6.1",
69
+ "semver": "^7.6.3",
69
70
  "strip-indent": "^3.0.0"
70
71
  },
71
72
  "devDependencies": {
72
- "@babel/code-frame": "^7.24.2",
73
- "@babel/core": "^7.24.5",
74
- "@babel/eslint-parser": "^7.24.5",
73
+ "@babel/code-frame": "^7.24.7",
74
+ "@babel/core": "^7.25.2",
75
+ "@babel/eslint-parser": "^7.25.1",
75
76
  "@eslint/eslintrc": "^3.1.0",
76
77
  "@lubien/fixture-beta-package": "^1.0.0-beta.1",
77
- "@typescript-eslint/parser": "^8.0.0-alpha.12",
78
+ "@typescript-eslint/parser": "^8.4.0",
78
79
  "ava": "^6.1.3",
79
- "c8": "^9.1.0",
80
+ "c8": "^10.1.2",
80
81
  "chalk": "^5.3.0",
81
82
  "enquirer": "^2.4.1",
82
- "eslint": "^9.6.0",
83
+ "eslint": "^9.10.0",
83
84
  "eslint-ava-rule-tester": "^5.0.1",
84
85
  "eslint-doc-generator": "1.7.0",
85
- "eslint-plugin-eslint-plugin": "^6.1.0",
86
+ "eslint-plugin-eslint-plugin": "^6.2.0",
86
87
  "eslint-plugin-internal-rules": "file:./scripts/internal-rules/",
87
- "eslint-remote-tester": "^4.0.0",
88
+ "eslint-remote-tester": "^4.0.1",
88
89
  "eslint-remote-tester-repositories": "^2.0.0",
89
- "espree": "^10.0.1",
90
+ "espree": "^10.1.0",
90
91
  "execa": "^8.0.1",
91
92
  "listr": "^0.14.3",
92
93
  "lodash-es": "^4.17.21",
93
- "markdownlint-cli": "^0.40.0",
94
+ "markdownlint-cli": "^0.41.0",
94
95
  "memoize": "^10.0.0",
95
- "npm-package-json-lint": "^7.1.0",
96
- "npm-run-all2": "^6.1.2",
96
+ "npm-package-json-lint": "^8.0.0",
97
+ "npm-run-all2": "^6.2.2",
97
98
  "outdent": "^0.8.0",
98
- "pretty-ms": "^9.0.0",
99
- "typescript": "^5.4.5",
100
- "vue-eslint-parser": "^9.4.2",
101
- "xo": "^0.58.0",
102
- "yaml": "^2.4.2"
99
+ "pretty-ms": "^9.1.0",
100
+ "typescript": "^5.5.4",
101
+ "vue-eslint-parser": "^9.4.3",
102
+ "xo": "^0.59.3",
103
+ "yaml": "^2.5.1"
103
104
  },
104
105
  "peerDependencies": {
105
106
  "eslint": ">=8.56.0"
@@ -175,7 +176,8 @@
175
176
  ],
176
177
  "eslint-plugin/require-meta-docs-url": "off",
177
178
  "eslint-plugin/require-meta-has-suggestions": "off",
178
- "eslint-plugin/require-meta-schema": "off"
179
+ "eslint-plugin/require-meta-schema": "off",
180
+ "eslint-plugin/require-meta-schema-description": "off"
179
181
  }
180
182
  }
181
183
  ]
package/readme.md CHANGED
@@ -72,7 +72,7 @@ module.exports = [
72
72
 
73
73
  ## Usage (legacy: `.eslintrc.*` or `package.json`)
74
74
 
75
- Use a [preset config](#preset-configs-eslintrc-or-packagejson) or configure each rule in `package.json`.
75
+ Use a [preset config](#legacy-preset-configs-eslintrc-or-packagejson) or configure each rule in `package.json`.
76
76
 
77
77
  If you don't use the preset, ensure you use the same `env` and `parserOptions` config as below.
78
78
 
@@ -110,10 +110,11 @@ If you don't use the preset, ensure you use the same `env` and `parserOptions` c
110
110
 
111
111
  | Name                                    | Description | 💼 | 🔧 | 💡 |
112
112
  | :----------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :- | :- | :- |
113
- | [better-regex](docs/rules/better-regex.md) | Improve regexes by making them shorter, consistent, and safer. || 🔧 | |
113
+ | [better-regex](docs/rules/better-regex.md) | Improve regexes by making them shorter, consistent, and safer. | | 🔧 | |
114
114
  | [catch-error-name](docs/rules/catch-error-name.md) | Enforce a specific parameter name in catch clauses. | ✅ | 🔧 | |
115
115
  | [consistent-destructuring](docs/rules/consistent-destructuring.md) | Use destructured variables over properties. | | 🔧 | 💡 |
116
116
  | [consistent-empty-array-spread](docs/rules/consistent-empty-array-spread.md) | Prefer consistent types when spreading a ternary in an array literal. | ✅ | 🔧 | |
117
+ | [consistent-existence-index-check](docs/rules/consistent-existence-index-check.md) | Enforce consistent style for element existence checks with `indexOf()`, `lastIndexOf()`, `findIndex()`, and `findLastIndex()`. | ✅ | 🔧 | |
117
118
  | [consistent-function-scoping](docs/rules/consistent-function-scoping.md) | Move function definitions to the highest possible scope. | ✅ | | |
118
119
  | [custom-error-definition](docs/rules/custom-error-definition.md) | Enforce correct `Error` subclassing. | | 🔧 | |
119
120
  | [empty-brace-spaces](docs/rules/empty-brace-spaces.md) | Enforce no spaces between braces. | ✅ | 🔧 | |
@@ -189,10 +190,12 @@ If you don't use the preset, ensure you use the same `env` and `parserOptions` c
189
190
  | [prefer-dom-node-text-content](docs/rules/prefer-dom-node-text-content.md) | Prefer `.textContent` over `.innerText`. | ✅ | | 💡 |
190
191
  | [prefer-event-target](docs/rules/prefer-event-target.md) | Prefer `EventTarget` over `EventEmitter`. | ✅ | | |
191
192
  | [prefer-export-from](docs/rules/prefer-export-from.md) | Prefer `export…from` when re-exporting. | ✅ | 🔧 | 💡 |
193
+ | [prefer-global-this](docs/rules/prefer-global-this.md) | Prefer `globalThis` over `window`, `self`, and `global`. | ✅ | 🔧 | |
192
194
  | [prefer-includes](docs/rules/prefer-includes.md) | Prefer `.includes()` over `.indexOf()`, `.lastIndexOf()`, and `Array#some()` when checking for existence or non-existence. | ✅ | 🔧 | 💡 |
193
195
  | [prefer-json-parse-buffer](docs/rules/prefer-json-parse-buffer.md) | Prefer reading a JSON file as a buffer. | | 🔧 | |
194
196
  | [prefer-keyboard-event-key](docs/rules/prefer-keyboard-event-key.md) | Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`. | ✅ | 🔧 | |
195
197
  | [prefer-logical-operator-over-ternary](docs/rules/prefer-logical-operator-over-ternary.md) | Prefer using a logical operator over a ternary. | ✅ | | 💡 |
198
+ | [prefer-math-min-max](docs/rules/prefer-math-min-max.md) | Prefer `Math.min()` and `Math.max()` over ternaries for simple comparisons. | ✅ | 🔧 | |
196
199
  | [prefer-math-trunc](docs/rules/prefer-math-trunc.md) | Enforce the use of `Math.trunc` instead of bitwise operators. | ✅ | 🔧 | 💡 |
197
200
  | [prefer-modern-dom-apis](docs/rules/prefer-modern-dom-apis.md) | Prefer `.before()` over `.insertBefore()`, `.replaceWith()` over `.replaceChild()`, prefer one of `.before()`, `.after()`, `.append()` or `.prepend()` over `insertAdjacentText()` and `insertAdjacentElement()`. | ✅ | 🔧 | |
198
201
  | [prefer-modern-math-apis](docs/rules/prefer-modern-math-apis.md) | Prefer modern `Math` APIs over legacy patterns. | ✅ | 🔧 | |
@@ -204,7 +207,7 @@ If you don't use the preset, ensure you use the same `env` and `parserOptions` c
204
207
  | [prefer-object-from-entries](docs/rules/prefer-object-from-entries.md) | Prefer using `Object.fromEntries(…)` to transform a list of key-value pairs into an object. | ✅ | 🔧 | |
205
208
  | [prefer-optional-catch-binding](docs/rules/prefer-optional-catch-binding.md) | Prefer omitting the `catch` binding parameter. | ✅ | 🔧 | |
206
209
  | [prefer-prototype-methods](docs/rules/prefer-prototype-methods.md) | Prefer borrowing methods from the prototype instead of the instance. | ✅ | 🔧 | |
207
- | [prefer-query-selector](docs/rules/prefer-query-selector.md) | Prefer `.querySelector()` over `.getElementById()`, `.querySelectorAll()` over `.getElementsByClassName()` and `.getElementsByTagName()`. | ✅ | 🔧 | |
210
+ | [prefer-query-selector](docs/rules/prefer-query-selector.md) | Prefer `.querySelector()` over `.getElementById()`, `.querySelectorAll()` over `.getElementsByClassName()` and `.getElementsByTagName()` and `.getElementsByName()`. | ✅ | 🔧 | |
208
211
  | [prefer-reflect-apply](docs/rules/prefer-reflect-apply.md) | Prefer `Reflect.apply()` over `Function#apply()`. | ✅ | 🔧 | |
209
212
  | [prefer-regexp-test](docs/rules/prefer-regexp-test.md) | Prefer `RegExp#test()` over `String#match()` and `RegExp#exec()`. | ✅ | 🔧 | 💡 |
210
213
  | [prefer-set-has](docs/rules/prefer-set-has.md) | Prefer `Set#has()` over `Array#includes()` when checking for existence or non-existence. | ✅ | 🔧 | 💡 |
@@ -239,13 +242,13 @@ See [docs/deprecated-rules.md](docs/deprecated-rules.md)
239
242
 
240
243
  ## Preset configs (`eslint.config.js`)
241
244
 
242
- See the [ESLint docs](https://eslint.org/docs/latest/user-guide/configuring/configuration-files-new) for more information about extending config files.
245
+ See the [ESLint docs](https://eslint.org/docs/latest/use/configure/configuration-files) for more information about extending config files.
243
246
 
244
- **Note**: Preset configs will also enable the correct [language options](https://eslint.org/docs/latest/use/configure/configuration-files-new#configuring-language-options).
247
+ **Note**: Preset configs will also enable the correct [language options](https://eslint.org/docs/latest/use/configure/language-options).
245
248
 
246
249
  ### Recommended config
247
250
 
248
- This plugin exports a [`recommended` config](configs/recommended.js) that enforces good practices.
251
+ This plugin exports a `recommended` config that enforces good practices.
249
252
 
250
253
  #### ES Module (Recommended)
251
254
 
@@ -282,7 +285,7 @@ module.exports = [
282
285
 
283
286
  ### All config
284
287
 
285
- This plugin exports an [`all` config](configs/all.js) that makes use of all rules (except for deprecated ones).
288
+ This plugin exports an `all` that makes use of all rules (except for deprecated ones).
286
289
 
287
290
  #### ES Module (Recommended)
288
291
 
@@ -317,15 +320,15 @@ module.exports = [
317
320
  ];
318
321
  ```
319
322
 
320
- ## Preset configs (`.eslintrc.*` or `package.json`)
323
+ ## Legacy preset configs (`.eslintrc.*` or `package.json`)
321
324
 
322
- See the [ESLint docs](https://eslint.org/docs/user-guide/configuring/configuration-files#extending-configuration-files) for more information about extending config files.
325
+ See the [ESLint docs](https://eslint.org/docs/latest/use/configure/configuration-files-deprecated) for more information about extending deprecated legacy config files.
323
326
 
324
- **Note**: Preset configs will also enable the correct [parser options](https://eslint.org/docs/user-guide/configuring/language-options#specifying-parser-options) and [environment](https://eslint.org/docs/user-guide/configuring/language-options#specifying-environments).
327
+ **Note**: Preset configs will also enable the correct [parser options](https://eslint.org/docs/latest/use/configure/parser-deprecated) and [environment](https://eslint.org/docs/latest/use/configure/language-options-deprecated).
325
328
 
326
- ### Recommended config
329
+ ### Recommended legacy config
327
330
 
328
- This plugin exports a [`recommended` config](configs/recommended.js) that enforces good practices.
331
+ This plugin exports a `recommended` legacy config that enforces good practices.
329
332
 
330
333
  ```json
331
334
  {
@@ -339,9 +342,9 @@ This plugin exports a [`recommended` config](configs/recommended.js) that enforc
339
342
  }
340
343
  ```
341
344
 
342
- ### All config
345
+ ### All legacy config
343
346
 
344
- This plugin exports an [`all` config](configs/all.js) that makes use of all rules (except for deprecated ones).
347
+ This plugin exports an `all` legacy config that makes use of all rules (except for deprecated ones).
345
348
 
346
349
  ```json
347
350
  {
@@ -31,6 +31,7 @@ module.exports = {
31
31
  isFunction: require('./is-function.js'),
32
32
  isMemberExpression: require('./is-member-expression.js'),
33
33
  isMethodCall: require('./is-method-call.js'),
34
+ isNegativeOne: require('./is-negative-one.js'),
34
35
  isNewExpression,
35
36
  isReferenceIdentifier: require('./is-reference-identifier.js'),
36
37
  isStaticRequire: require('./is-static-require.js'),
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+
3
+ const {isNumberLiteral} = require('./literal.js');
4
+
5
+ function isNegativeOne(node) {
6
+ return node?.type === 'UnaryExpression'
7
+ && node.operator === '-'
8
+ && isNumberLiteral(node.argument)
9
+ && node.argument.value === 1;
10
+ }
11
+
12
+ module.exports = isNegativeOne;
@@ -136,7 +136,7 @@ module.exports = {
136
136
  type: 'suggestion',
137
137
  docs: {
138
138
  description: 'Improve regexes by making them shorter, consistent, and safer.',
139
- recommended: true,
139
+ recommended: false,
140
140
  },
141
141
  fixable: 'code',
142
142
  schema,
@@ -0,0 +1,133 @@
1
+ 'use strict';
2
+ const toLocation = require('./utils/to-location.js');
3
+ const {isMethodCall, isNegativeOne, isNumberLiteral} = require('./ast/index.js');
4
+
5
+ const MESSAGE_ID = 'consistent-existence-index-check';
6
+ const messages = {
7
+ [MESSAGE_ID]: 'Prefer `{{replacementOperator}} {{replacementValue}}` over `{{originalOperator}} {{originalValue}}` to check {{existenceOrNonExistence}}.',
8
+ };
9
+
10
+ const isZero = node => isNumberLiteral(node) && node.value === 0;
11
+
12
+ /**
13
+ @param {parent: import('estree').BinaryExpression} binaryExpression
14
+ @returns {{
15
+ replacementOperator: string,
16
+ replacementValue: string,
17
+ originalOperator: string,
18
+ originalValue: string,
19
+ } | undefined}
20
+ */
21
+ function getReplacement(binaryExpression) {
22
+ const {operator, right} = binaryExpression;
23
+
24
+ if (operator === '<' && isZero(right)) {
25
+ return {
26
+ replacementOperator: '===',
27
+ replacementValue: '-1',
28
+ originalOperator: operator,
29
+ originalValue: '0',
30
+ };
31
+ }
32
+
33
+ if (operator === '>' && isNegativeOne(right)) {
34
+ return {
35
+ replacementOperator: '!==',
36
+ replacementValue: '-1',
37
+ originalOperator: operator,
38
+ originalValue: '-1',
39
+ };
40
+ }
41
+
42
+ if (operator === '>=' && isZero(right)) {
43
+ return {
44
+ replacementOperator: '!==',
45
+ replacementValue: '-1',
46
+ originalOperator: operator,
47
+ originalValue: '0',
48
+ };
49
+ }
50
+ }
51
+
52
+ /** @param {import('eslint').Rule.RuleContext} context */
53
+ const create = context => ({
54
+ /** @param {import('estree').VariableDeclarator} variableDeclarator */
55
+ * VariableDeclarator(variableDeclarator) {
56
+ if (!(
57
+ variableDeclarator.parent.type === 'VariableDeclaration'
58
+ && variableDeclarator.parent.kind === 'const'
59
+ && variableDeclarator.id.type === 'Identifier'
60
+ && isMethodCall(variableDeclarator.init, {methods: ['indexOf', 'lastIndexOf', 'findIndex', 'findLastIndex']})
61
+ )) {
62
+ return;
63
+ }
64
+
65
+ const variableIdentifier = variableDeclarator.id;
66
+ const variables = context.sourceCode.getDeclaredVariables(variableDeclarator);
67
+ const [variable] = variables;
68
+
69
+ // Just for safety
70
+ if (
71
+ variables.length !== 1
72
+ || variable.identifiers.length !== 1
73
+ || variable.identifiers[0] !== variableIdentifier
74
+ ) {
75
+ return;
76
+ }
77
+
78
+ for (const {identifier} of variable.references) {
79
+ /** @type {{parent: import('estree').BinaryExpression}} */
80
+ const binaryExpression = identifier.parent;
81
+
82
+ if (binaryExpression.type !== 'BinaryExpression' || binaryExpression.left !== identifier) {
83
+ continue;
84
+ }
85
+
86
+ const replacement = getReplacement(binaryExpression);
87
+
88
+ if (!replacement) {
89
+ return;
90
+ }
91
+
92
+ const {left, operator, right} = binaryExpression;
93
+ const {sourceCode} = context;
94
+
95
+ const operatorToken = sourceCode.getTokenAfter(
96
+ left,
97
+ token => token.type === 'Punctuator' && token.value === operator,
98
+ );
99
+
100
+ yield {
101
+ node: binaryExpression,
102
+ loc: toLocation([operatorToken.range[0], right.range[1]], sourceCode),
103
+ messageId: MESSAGE_ID,
104
+ data: {
105
+ ...replacement,
106
+ existenceOrNonExistence: `${replacement.replacementOperator === '===' ? 'non-' : ''}existence`,
107
+ },
108
+ * fix(fixer) {
109
+ yield fixer.replaceText(operatorToken, replacement.replacementOperator);
110
+
111
+ if (replacement.replacementValue !== replacement.originalValue) {
112
+ yield fixer.replaceText(right, replacement.replacementValue);
113
+ }
114
+ },
115
+ };
116
+ }
117
+ },
118
+ });
119
+
120
+ /** @type {import('eslint').Rule.RuleModule} */
121
+ module.exports = {
122
+ create,
123
+ meta: {
124
+ type: 'problem',
125
+ docs: {
126
+ description:
127
+ 'Enforce consistent style for element existence checks with `indexOf()`, `lastIndexOf()`, `findIndex()`, and `findLastIndex()`.',
128
+ recommended: true,
129
+ },
130
+ fixable: 'code',
131
+ messages,
132
+ },
133
+ };
@@ -271,13 +271,17 @@ function semverComparisonForOperator(operator) {
271
271
  }[operator];
272
272
  }
273
273
 
274
+ const DEFAULT_OPTIONS = {
275
+ terms: ['todo', 'fixme', 'xxx'],
276
+ ignore: [],
277
+ ignoreDatesOnPullRequests: true,
278
+ allowWarningComments: true,
279
+ };
280
+
274
281
  /** @param {import('eslint').Rule.RuleContext} context */
275
282
  const create = context => {
276
283
  const options = {
277
- terms: ['todo', 'fixme', 'xxx'],
278
- ignore: [],
279
- ignoreDatesOnPullRequests: true,
280
- allowWarningComments: true,
284
+ ...DEFAULT_OPTIONS,
281
285
  date: new Date().toISOString().slice(0, 10),
282
286
  ...context.options[0],
283
287
  };
@@ -564,7 +568,7 @@ const schema = [
564
568
  },
565
569
  allowWarningComments: {
566
570
  type: 'boolean',
567
- default: false,
571
+ default: true,
568
572
  },
569
573
  date: {
570
574
  type: 'string',
@@ -584,6 +588,7 @@ module.exports = {
584
588
  recommended: true,
585
589
  },
586
590
  schema,
591
+ defaultOptions: [{...DEFAULT_OPTIONS}],
587
592
  messages,
588
593
  },
589
594
  };
@@ -2,9 +2,11 @@
2
2
  const {isMethodCall} = require('./ast/index.js');
3
3
  const {isNodeValueNotFunction, isArrayPrototypeProperty} = require('./utils/index.js');
4
4
 
5
- const MESSAGE_ID = 'no-reduce';
5
+ const MESSAGE_ID_REDUCE = 'reduce';
6
+ const MESSAGE_ID_REDUCE_RIGHT = 'reduceRight';
6
7
  const messages = {
7
- [MESSAGE_ID]: '`Array#{{method}}()` is not allowed',
8
+ [MESSAGE_ID_REDUCE]: '`Array#reduce()` is not allowed. Prefer other types of loop for readability.',
9
+ [MESSAGE_ID_REDUCE_RIGHT]: '`Array#reduceRight()` is not allowed. Prefer other types of loop for readability. You may want to call `Array#toReversed()` before looping it.',
8
10
  };
9
11
 
10
12
  const cases = [
@@ -104,8 +106,7 @@ const create = context => {
104
106
  const methodNode = getMethodNode(callExpression);
105
107
  yield {
106
108
  node: methodNode,
107
- messageId: MESSAGE_ID,
108
- data: {method: methodNode.name},
109
+ messageId: methodNode.name,
109
110
  };
110
111
  }
111
112
  },
@@ -263,7 +263,7 @@ const getReferencesInChildScopes = (scope, name) =>
263
263
  /** @param {import('eslint').Rule.RuleContext} context */
264
264
  const create = context => {
265
265
  const {sourceCode} = context;
266
- const {scopeManager, text: sourceCodeText} = sourceCode;
266
+ const {scopeManager} = sourceCode;
267
267
 
268
268
  return {
269
269
  ForStatement(node) {
@@ -339,12 +339,12 @@ const create = context => {
339
339
  const elementIdentifierName = elementNode?.id.name;
340
340
  const elementVariable = elementIdentifierName && resolveIdentifierName(elementIdentifierName, bodyScope);
341
341
 
342
- const shouldFix = !someVariablesLeakOutOfTheLoop(node, [indexVariable, elementVariable].filter(Boolean), forScope);
342
+ const shouldFix = !someVariablesLeakOutOfTheLoop(node, [indexVariable, elementVariable].filter(Boolean), forScope)
343
+ && !elementNode?.id.typeAnnotation;
343
344
 
344
345
  if (shouldFix) {
345
346
  problem.fix = function * (fixer) {
346
347
  const shouldGenerateIndex = isIndexVariableUsedElsewhereInTheLoopBody(indexVariable, bodyScope, arrayIdentifierName);
347
-
348
348
  const index = indexIdentifierName;
349
349
  const element = elementIdentifierName
350
350
  || avoidCapture(singular(arrayIdentifierName) || defaultElementName, getScopes(bodyScope));
@@ -353,7 +353,6 @@ const create = context => {
353
353
  let declarationElement = element;
354
354
  let declarationType = 'const';
355
355
  let removeDeclaration = true;
356
- let typeAnnotation;
357
356
 
358
357
  if (elementNode) {
359
358
  if (elementNode.id.type === 'ObjectPattern' || elementNode.id.type === 'ArrayPattern') {
@@ -362,26 +361,13 @@ const create = context => {
362
361
 
363
362
  if (removeDeclaration) {
364
363
  declarationType = element.type === 'VariableDeclarator' ? elementNode.kind : elementNode.parent.kind;
365
- if (elementNode.id.typeAnnotation && shouldGenerateIndex) {
366
- declarationElement = sourceCodeText.slice(elementNode.id.range[0], elementNode.id.typeAnnotation.range[0]).trim();
367
- typeAnnotation = sourceCode.getText(
368
- elementNode.id.typeAnnotation,
369
- -1, // Skip leading `:`
370
- ).trim();
371
- } else {
372
- declarationElement = sourceCode.getText(elementNode.id);
373
- }
364
+ declarationElement = sourceCode.getText(elementNode.id);
374
365
  }
375
366
  }
376
367
 
377
368
  const parts = [declarationType];
378
369
  if (shouldGenerateIndex) {
379
- parts.push(` [${index}, ${declarationElement}]`);
380
- if (typeAnnotation) {
381
- parts.push(`: [number, ${typeAnnotation}]`);
382
- }
383
-
384
- parts.push(` of ${array}.entries()`);
370
+ parts.push(` [${index}, ${declarationElement}] of ${array}.entries()`);
385
371
  } else {
386
372
  parts.push(` ${declarationElement} of ${array}`);
387
373
  }
@@ -12,7 +12,7 @@ const {
12
12
  const MESSAGE_ID_ERROR = 'no-negation-in-equality-check/error';
13
13
  const MESSAGE_ID_SUGGESTION = 'no-negation-in-equality-check/suggestion';
14
14
  const messages = {
15
- [MESSAGE_ID_ERROR]: 'Negated expression in not allowed in equality check.',
15
+ [MESSAGE_ID_ERROR]: 'Negated expression is not allowed in equality check.',
16
16
  [MESSAGE_ID_SUGGESTION]: 'Switch to \'{{operator}}\' check.',
17
17
  };
18
18
 
@@ -9,7 +9,7 @@ const MESSAGE_ID_LENGTH = 'array-length';
9
9
  const MESSAGE_ID_ONLY_ELEMENT = 'only-element';
10
10
  const MESSAGE_ID_SPREAD = 'spread';
11
11
  const messages = {
12
- [MESSAGE_ID_ERROR]: 'Do not use `new Array()`.',
12
+ [MESSAGE_ID_ERROR]: '`new Array()` is unclear in intent; use either `[x]` or `Array.from({length: x})`',
13
13
  [MESSAGE_ID_LENGTH]: 'The argument is the length of array.',
14
14
  [MESSAGE_ID_ONLY_ELEMENT]: 'The argument is the only element of array.',
15
15
  [MESSAGE_ID_SPREAD]: 'Spread the argument.',
@@ -62,6 +62,8 @@ const shouldIgnore = node => {
62
62
  || name === 'createContext'
63
63
  // `setState(undefined)`
64
64
  || /^set[A-Z]/.test(name)
65
+ // React 19 useRef
66
+ || name === 'useRef'
65
67
 
66
68
  // https://vuejs.org/api/reactivity-core.html#ref
67
69
  || name === 'ref';
@@ -0,0 +1,210 @@
1
+ 'use strict';
2
+
3
+ const MESSAGE_ID_ERROR = 'prefer-global-this/error';
4
+ const messages = {
5
+ [MESSAGE_ID_ERROR]: 'Prefer `globalThis` over `{{value}}`.',
6
+ };
7
+
8
+ const globalIdentifier = new Set(['window', 'self', 'global']);
9
+
10
+ const windowSpecificEvents = new Set([
11
+ 'resize',
12
+ 'blur',
13
+ 'focus',
14
+ 'load',
15
+ 'scroll',
16
+ 'scrollend',
17
+ 'wheel',
18
+ 'beforeunload', // Browsers might have specific behaviors on exactly `window.onbeforeunload =`
19
+ 'message',
20
+ 'messageerror',
21
+ 'pagehide',
22
+ 'pagereveal',
23
+ 'pageshow',
24
+ 'pageswap',
25
+ 'unload',
26
+ ]);
27
+
28
+ /**
29
+ Note: What kind of API should be a windows-specific interface?
30
+
31
+ 1. It's directly related to window (✅ window.close())
32
+ 2. It does NOT work well as globalThis.x or x (✅ window.frames, window.top)
33
+
34
+ Some constructors are occasionally related to window (like Element !== iframe.contentWindow.Element), but they don't need to mention window anyway.
35
+
36
+ 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
37
+ */
38
+ const windowSpecificAPIs = new Set([
39
+ // Properties and methods
40
+ // https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-window-object
41
+ 'name',
42
+ 'locationbar',
43
+ 'menubar',
44
+ 'personalbar',
45
+ 'scrollbars',
46
+ 'statusbar',
47
+ 'toolbar',
48
+ 'status',
49
+ 'close',
50
+ 'closed',
51
+ 'stop',
52
+ 'focus',
53
+ 'blur',
54
+ 'frames',
55
+ 'length',
56
+ 'top',
57
+ 'opener',
58
+ 'parent',
59
+ 'frameElement',
60
+ 'open',
61
+ 'originAgentCluster',
62
+ 'postMessage',
63
+
64
+ // Events commonly associated with "window"
65
+ ...[...windowSpecificEvents].map(event => `on${event}`),
66
+
67
+ // To add/remove/dispatch events that are commonly associated with "window"
68
+ // https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-flow
69
+ 'addEventListener',
70
+ 'removeEventListener',
71
+ 'dispatchEvent',
72
+
73
+ // https://dom.spec.whatwg.org/#idl-index
74
+ 'event', // Deprecated and quirky, best left untouched
75
+
76
+ // https://drafts.csswg.org/cssom-view/#idl-index
77
+ 'screen',
78
+ 'visualViewport',
79
+ 'moveTo',
80
+ 'moveBy',
81
+ 'resizeTo',
82
+ 'resizeBy',
83
+ 'innerWidth',
84
+ 'innerHeight',
85
+ 'scrollX',
86
+ 'pageXOffset',
87
+ 'scrollY',
88
+ 'pageYOffset',
89
+ 'scroll',
90
+ 'scrollTo',
91
+ 'scrollBy',
92
+ 'screenX',
93
+ 'screenLeft',
94
+ 'screenY',
95
+ 'screenTop',
96
+ 'screenWidth',
97
+ 'screenHeight',
98
+ 'devicePixelRatio',
99
+ ]);
100
+
101
+ const webWorkerSpecificAPIs = new Set([
102
+ // https://html.spec.whatwg.org/multipage/workers.html#the-workerglobalscope-common-interface
103
+ 'addEventListener',
104
+ 'removeEventListener',
105
+ 'dispatchEvent',
106
+
107
+ 'self',
108
+ 'location',
109
+ 'navigator',
110
+ 'onerror',
111
+ 'onlanguagechange',
112
+ 'onoffline',
113
+ 'ononline',
114
+ 'onrejectionhandled',
115
+ 'onunhandledrejection',
116
+
117
+ // https://html.spec.whatwg.org/multipage/workers.html#dedicated-workers-and-the-dedicatedworkerglobalscope-interface
118
+ 'name',
119
+ 'postMessage',
120
+ 'onconnect',
121
+ ]);
122
+
123
+ /**
124
+ Check if the node is a window-specific API.
125
+
126
+ @param {import('estree').MemberExpression} node
127
+ @returns {boolean}
128
+ */
129
+ const isWindowSpecificAPI = node => {
130
+ if (node.type !== 'MemberExpression') {
131
+ return false;
132
+ }
133
+
134
+ if (node.object.name !== 'window' || node.property.type !== 'Identifier') {
135
+ return false;
136
+ }
137
+
138
+ if (windowSpecificAPIs.has(node.property.name)) {
139
+ if (['addEventListener', 'removeEventListener', 'dispatchEvent'].includes(node.property.name) && node.parent.type === 'CallExpression' && node.parent.callee === node) {
140
+ const argument = node.parent.arguments[0];
141
+ return argument && argument.type === 'Literal' && windowSpecificEvents.has(argument.value);
142
+ }
143
+
144
+ return true;
145
+ }
146
+
147
+ return false;
148
+ };
149
+
150
+ /**
151
+ @param {import('estree').Identifier} identifier
152
+ @returns {boolean}
153
+ */
154
+ function isComputedMemberExpressionObject(identifier) {
155
+ return identifier.parent.type === 'MemberExpression' && identifier.parent.computed && identifier.parent.object === identifier;
156
+ }
157
+
158
+ /**
159
+ Check if the node is a web worker specific API.
160
+
161
+ @param {import('estree').MemberExpression} node
162
+ @returns {boolean}
163
+ */
164
+ const isWebWorkerSpecificAPI = node => node.type === 'MemberExpression' && node.object.name === 'self' && node.property.type === 'Identifier' && webWorkerSpecificAPIs.has(node.property.name);
165
+
166
+ /** @param {import('eslint').Rule.RuleContext} context */
167
+ const create = context => ({
168
+ * Program(program) {
169
+ const scope = context.sourceCode.getScope(program);
170
+
171
+ const references = [
172
+ // Variables declared at globals options
173
+ ...scope.variables.flatMap(variable => globalIdentifier.has(variable.name) ? variable.references : []),
174
+ // Variables not declared at globals options
175
+ ...scope.through.filter(reference => globalIdentifier.has(reference.identifier.name)),
176
+ ];
177
+
178
+ for (const {identifier} of references) {
179
+ if (
180
+ isComputedMemberExpressionObject(identifier)
181
+ || isWindowSpecificAPI(identifier.parent)
182
+ || isWebWorkerSpecificAPI(identifier.parent)
183
+ ) {
184
+ continue;
185
+ }
186
+
187
+ yield {
188
+ node: identifier,
189
+ messageId: MESSAGE_ID_ERROR,
190
+ data: {value: identifier.name},
191
+ fix: fixer => fixer.replaceText(identifier, 'globalThis'),
192
+ };
193
+ }
194
+ },
195
+ });
196
+
197
+ /** @type {import('eslint').Rule.RuleModule} */
198
+ module.exports = {
199
+ create,
200
+ meta: {
201
+ type: 'suggestion',
202
+ docs: {
203
+ description: 'Prefer `globalThis` over `window`, `self`, and `global`.',
204
+ recommended: true,
205
+ },
206
+ fixable: 'code',
207
+ hasSuggestions: false,
208
+ messages,
209
+ },
210
+ };
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
  const isMethodNamed = require('./utils/is-method-named.js');
3
3
  const simpleArraySearchRule = require('./shared/simple-array-search-rule.js');
4
- const {isLiteral} = require('./ast/index.js');
4
+ const {isLiteral, isNegativeOne} = require('./ast/index.js');
5
5
 
6
6
  const MESSAGE_ID = 'prefer-includes';
7
7
  const messages = {
@@ -10,7 +10,6 @@ const messages = {
10
10
  // Ignore `{_,lodash,underscore}.{indexOf,lastIndexOf}`
11
11
  const ignoredVariables = new Set(['_', 'lodash', 'underscore']);
12
12
  const isIgnoredTarget = node => node.type === 'Identifier' && ignoredVariables.has(node.name);
13
- const isNegativeOne = node => node.type === 'UnaryExpression' && node.operator === '-' && node.argument && node.argument.type === 'Literal' && node.argument.value === 1;
14
13
  const isLiteralZero = node => isLiteral(node, 0);
15
14
  const isNegativeResult = node => ['===', '==', '<'].includes(node.operator);
16
15
 
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+ const {fixSpaceAroundKeyword} = require('./fix/index.js');
3
+
4
+ const MESSAGE_ID = 'prefer-math-min-max';
5
+ const messages = {
6
+ [MESSAGE_ID]: 'Prefer `Math.{{method}}()` to simplify ternary expressions.',
7
+ };
8
+
9
+ /** @param {import('eslint').Rule.RuleContext} context */
10
+ const create = context => ({
11
+ /** @param {import('estree').ConditionalExpression} conditionalExpression */
12
+ ConditionalExpression(conditionalExpression) {
13
+ const {test, consequent, alternate} = conditionalExpression;
14
+
15
+ if (test.type !== 'BinaryExpression') {
16
+ return;
17
+ }
18
+
19
+ const {operator, left, right} = test;
20
+ const [leftText, rightText, alternateText, consequentText] = [left, right, alternate, consequent].map(node => context.sourceCode.getText(node));
21
+
22
+ const isGreaterOrEqual = operator === '>' || operator === '>=';
23
+ const isLessOrEqual = operator === '<' || operator === '<=';
24
+
25
+ let method;
26
+
27
+ // Prefer `Math.min()`
28
+ if (
29
+ // `height > 50 ? 50 : height`
30
+ (isGreaterOrEqual && leftText === alternateText && rightText === consequentText)
31
+ // `height < 50 ? height : 50`
32
+ || (isLessOrEqual && leftText === consequentText && rightText === alternateText)
33
+ ) {
34
+ method = 'min';
35
+ } else if (
36
+ // `height > 50 ? height : 50`
37
+ (isGreaterOrEqual && leftText === consequentText && rightText === alternateText)
38
+ // `height < 50 ? 50 : height`
39
+ || (isLessOrEqual && leftText === alternateText && rightText === consequentText)
40
+ ) {
41
+ method = 'max';
42
+ }
43
+
44
+ if (!method) {
45
+ return;
46
+ }
47
+
48
+ return {
49
+ node: conditionalExpression,
50
+ messageId: MESSAGE_ID,
51
+ data: {method},
52
+ /** @param {import('eslint').Rule.RuleFixer} fixer */
53
+ * fix(fixer) {
54
+ const {sourceCode} = context;
55
+
56
+ yield * fixSpaceAroundKeyword(fixer, conditionalExpression, sourceCode);
57
+
58
+ const argumentsText = [left, right]
59
+ .map(node => node.type === 'SequenceExpression' ? `(${sourceCode.getText(node)})` : sourceCode.getText(node))
60
+ .join(', ');
61
+
62
+ yield fixer.replaceText(conditionalExpression, `Math.${method}(${argumentsText})`);
63
+ },
64
+ };
65
+ },
66
+ });
67
+
68
+ /** @type {import('eslint').Rule.RuleModule} */
69
+ module.exports = {
70
+ create,
71
+ meta: {
72
+ type: 'problem',
73
+ docs: {
74
+ description: 'Prefer `Math.min()` and `Math.max()` over ternaries for simple comparisons.',
75
+ recommended: true,
76
+ },
77
+ fixable: 'code',
78
+ messages,
79
+ },
80
+ };
@@ -11,10 +11,12 @@ const disallowedIdentifierNames = new Map([
11
11
  ['getElementById', 'querySelector'],
12
12
  ['getElementsByClassName', 'querySelectorAll'],
13
13
  ['getElementsByTagName', 'querySelectorAll'],
14
+ ['getElementsByName', 'querySelectorAll'],
14
15
  ]);
15
16
 
16
17
  const getReplacementForId = value => `#${value}`;
17
18
  const getReplacementForClass = value => value.match(/\S+/g).map(className => `.${className}`).join('');
19
+ const getReplacementForName = (value, originQuote) => `[name=${wrapQuoted(value, originQuote)}]`;
18
20
 
19
21
  const getQuotedReplacement = (node, value) => {
20
22
  const leftQuote = node.raw.charAt(0);
@@ -22,6 +24,24 @@ const getQuotedReplacement = (node, value) => {
22
24
  return `${leftQuote}${value}${rightQuote}`;
23
25
  };
24
26
 
27
+ const wrapQuoted = (value, originalQuote) => {
28
+ switch (originalQuote) {
29
+ case '\'': {
30
+ return `"${value}"`;
31
+ }
32
+
33
+ case '"': {
34
+ return `'${value}'`;
35
+ }
36
+
37
+ case '`': {
38
+ return `'${value}'`;
39
+ }
40
+
41
+ // No default
42
+ }
43
+ };
44
+
25
45
  function * getLiteralFix(fixer, node, identifierName) {
26
46
  let replacement = node.raw;
27
47
  if (identifierName === 'getElementById') {
@@ -32,6 +52,11 @@ function * getLiteralFix(fixer, node, identifierName) {
32
52
  replacement = getQuotedReplacement(node, getReplacementForClass(node.value));
33
53
  }
34
54
 
55
+ if (identifierName === 'getElementsByName') {
56
+ const quoted = node.raw.charAt(0);
57
+ replacement = getQuotedReplacement(node, getReplacementForName(node.value, quoted));
58
+ }
59
+
35
60
  yield fixer.replaceText(node, replacement);
36
61
  }
37
62
 
@@ -53,6 +78,14 @@ function * getTemplateLiteralFix(fixer, node, identifierName) {
53
78
  getReplacementForClass(templateElement.value.cooked),
54
79
  );
55
80
  }
81
+
82
+ if (identifierName === 'getElementsByName') {
83
+ const quoted = node.raw ? node.raw.charAt(0) : '"';
84
+ yield fixer.replaceText(
85
+ templateElement,
86
+ getReplacementForName(templateElement.value.cooked, quoted),
87
+ );
88
+ }
56
89
  }
57
90
  }
58
91
 
@@ -91,7 +124,7 @@ const create = () => ({
91
124
  CallExpression(node) {
92
125
  if (
93
126
  !isMethodCall(node, {
94
- methods: ['getElementById', 'getElementsByClassName', 'getElementsByTagName'],
127
+ methods: ['getElementById', 'getElementsByClassName', 'getElementsByTagName', 'getElementsByName'],
95
128
  argumentsLength: 1,
96
129
  optionalCall: false,
97
130
  optionalMember: false,
@@ -127,7 +160,7 @@ module.exports = {
127
160
  meta: {
128
161
  type: 'suggestion',
129
162
  docs: {
130
- description: 'Prefer `.querySelector()` over `.getElementById()`, `.querySelectorAll()` over `.getElementsByClassName()` and `.getElementsByTagName()`.',
163
+ description: 'Prefer `.querySelector()` over `.getElementById()`, `.querySelectorAll()` over `.getElementsByClassName()` and `.getElementsByTagName()` and `.getElementsByName()`.',
131
164
  recommended: true,
132
165
  },
133
166
  fixable: 'code',
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
  const {getStaticValue} = require('@eslint-community/eslint-utils');
3
3
  const {getParenthesizedText, getParenthesizedRange} = require('./utils/parentheses.js');
4
- const isNumber = require('./utils/is-number.js');
5
4
  const {replaceArgument} = require('./fix/index.js');
6
5
  const {isNumberLiteral, isMethodCall} = require('./ast/index.js');
7
6
 
@@ -64,13 +63,6 @@ function * fixSubstrArguments({node, fixer, context, abort}) {
64
63
  return;
65
64
  }
66
65
 
67
- if (argumentNodes.every(node => isNumber(node, scope))) {
68
- const firstArgumentText = getParenthesizedText(firstArgument, sourceCode);
69
-
70
- yield fixer.insertTextBeforeRange(secondArgumentRange, `${firstArgumentText} + `);
71
- return;
72
- }
73
-
74
66
  return abort();
75
67
  }
76
68
 
@@ -4,8 +4,8 @@
4
4
  Finds a variable named `name` in the scope `scope` (or it's parents).
5
5
 
6
6
  @param {string} name - The variable name to be resolve.
7
- @param {Scope} scope - The scope to look for the variable in.
8
- @returns {Variable?} - The found variable, if any.
7
+ @param {import('eslint').Scope.Scope} scope - The scope to look for the variable in.
8
+ @returns {import('eslint').Scope.Variable | void} - The found variable, if any.
9
9
  */
10
10
  module.exports = function resolveVariableName(name, scope) {
11
11
  while (scope) {