eslint-plugin-unicorn 54.0.0 → 56.0.0

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.
@@ -1,10 +1,8 @@
1
1
  'use strict';
2
- const eslintrc = require('@eslint/eslintrc');
3
-
4
- const {globals} = eslintrc.Legacy.environments.get('es2024');
2
+ const globals = require('globals');
5
3
 
6
4
  module.exports = {
7
5
  languageOptions: {
8
- globals,
6
+ globals: globals.builtin,
9
7
  },
10
8
  };
package/index.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type {ESLint, Linter} from 'eslint';
2
+
3
+ declare const eslintPluginUnicorn: ESLint.Plugin & {
4
+ configs: {
5
+ recommended: Linter.Config;
6
+ all: Linter.Config;
7
+ 'flat/all': Linter.FlatConfig;
8
+ 'flat/recommended': Linter.FlatConfig;
9
+ };
10
+ };
11
+
12
+ export = eslintPluginUnicorn;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-unicorn",
3
- "version": "54.0.0",
3
+ "version": "56.0.0",
4
4
  "description": "More than 100 powerful ESLint rules",
5
5
  "license": "MIT",
6
6
  "repository": "sindresorhus/eslint-plugin-unicorn",
@@ -11,6 +11,7 @@
11
11
  "url": "https://sindresorhus.com"
12
12
  },
13
13
  "main": "index.js",
14
+ "types": "index.d.ts",
14
15
  "sideEffects": false,
15
16
  "engines": {
16
17
  "node": ">=18.18"
@@ -28,13 +29,14 @@
28
29
  "lint:js": "xo",
29
30
  "lint:markdown": "markdownlint \"**/*.md\"",
30
31
  "lint:package-json": "npmPkgJsonLint .",
31
- "run-rules-on-codebase": "node ./test/run-rules-on-codebase/lint.mjs",
32
+ "run-rules-on-codebase": "eslint --config=./eslint.dogfooding.config.mjs",
32
33
  "smoke": "eslint-remote-tester --config ./test/smoke/eslint-remote-tester.config.mjs",
33
34
  "test": "npm-run-all --continue-on-error lint test:*",
34
35
  "test:js": "c8 ava"
35
36
  },
36
37
  "files": [
37
38
  "index.js",
39
+ "index.d.ts",
38
40
  "rules",
39
41
  "configs"
40
42
  ],
@@ -49,13 +51,13 @@
49
51
  "xo"
50
52
  ],
51
53
  "dependencies": {
52
- "@babel/helper-validator-identifier": "^7.24.5",
54
+ "@babel/helper-validator-identifier": "^7.24.7",
53
55
  "@eslint-community/eslint-utils": "^4.4.0",
54
- "@eslint/eslintrc": "^3.0.2",
55
56
  "ci-info": "^4.0.0",
56
57
  "clean-regexp": "^1.0.0",
57
- "core-js-compat": "^3.37.0",
58
- "esquery": "^1.5.0",
58
+ "core-js-compat": "^3.38.1",
59
+ "esquery": "^1.6.0",
60
+ "globals": "^15.9.0",
59
61
  "indent-string": "^4.0.0",
60
62
  "is-builtin-module": "^3.2.1",
61
63
  "jsesc": "^3.0.2",
@@ -63,40 +65,41 @@
63
65
  "read-pkg-up": "^7.0.1",
64
66
  "regexp-tree": "^0.1.27",
65
67
  "regjsparser": "^0.10.0",
66
- "semver": "^7.6.1",
68
+ "semver": "^7.6.3",
67
69
  "strip-indent": "^3.0.0"
68
70
  },
69
71
  "devDependencies": {
70
- "@babel/code-frame": "^7.24.2",
71
- "@babel/core": "^7.24.5",
72
- "@babel/eslint-parser": "^7.24.5",
72
+ "@babel/code-frame": "^7.24.7",
73
+ "@babel/core": "^7.25.2",
74
+ "@babel/eslint-parser": "^7.25.1",
75
+ "@eslint/eslintrc": "^3.1.0",
73
76
  "@lubien/fixture-beta-package": "^1.0.0-beta.1",
74
- "@typescript-eslint/parser": "^8.0.0-alpha.12",
77
+ "@typescript-eslint/parser": "^8.4.0",
75
78
  "ava": "^6.1.3",
76
- "c8": "^9.1.0",
79
+ "c8": "^10.1.2",
77
80
  "chalk": "^5.3.0",
78
81
  "enquirer": "^2.4.1",
79
- "eslint": "^9.2.0",
82
+ "eslint": "^9.10.0",
80
83
  "eslint-ava-rule-tester": "^5.0.1",
81
84
  "eslint-doc-generator": "1.7.0",
82
- "eslint-plugin-eslint-plugin": "^6.1.0",
85
+ "eslint-plugin-eslint-plugin": "^6.2.0",
83
86
  "eslint-plugin-internal-rules": "file:./scripts/internal-rules/",
84
- "eslint-remote-tester": "^4.0.0",
87
+ "eslint-remote-tester": "^4.0.1",
85
88
  "eslint-remote-tester-repositories": "^2.0.0",
86
- "espree": "^10.0.1",
89
+ "espree": "^10.1.0",
87
90
  "execa": "^8.0.1",
88
91
  "listr": "^0.14.3",
89
92
  "lodash-es": "^4.17.21",
90
- "markdownlint-cli": "^0.40.0",
93
+ "markdownlint-cli": "^0.41.0",
91
94
  "memoize": "^10.0.0",
92
- "npm-package-json-lint": "^7.1.0",
93
- "npm-run-all2": "^6.1.2",
95
+ "npm-package-json-lint": "^8.0.0",
96
+ "npm-run-all2": "^6.2.2",
94
97
  "outdent": "^0.8.0",
95
- "pretty-ms": "^9.0.0",
96
- "typescript": "^5.4.5",
97
- "vue-eslint-parser": "^9.4.2",
98
- "xo": "^0.58.0",
99
- "yaml": "^2.4.2"
98
+ "pretty-ms": "^9.1.0",
99
+ "typescript": "^5.5.4",
100
+ "vue-eslint-parser": "^9.4.3",
101
+ "xo": "^0.59.3",
102
+ "yaml": "^2.5.1"
100
103
  },
101
104
  "peerDependencies": {
102
105
  "eslint": ">=8.56.0"
package/readme.md CHANGED
@@ -27,12 +27,12 @@ If you don't use the preset, ensure you use the same `languageOptions` config as
27
27
 
28
28
  ```js
29
29
  import eslintPluginUnicorn from 'eslint-plugin-unicorn';
30
- import * as eslintrc from '@eslint/eslintrc';
30
+ import globals from 'globals';
31
31
 
32
32
  export default [
33
33
  {
34
34
  languageOptions: {
35
- globals: eslintrc.Legacy.environments.get('es2024'),
35
+ globals: globals.builtin,
36
36
  },
37
37
  plugins: {
38
38
  unicorn: eslintPluginUnicorn,
@@ -51,12 +51,12 @@ export default [
51
51
  ```js
52
52
  'use strict';
53
53
  const eslintPluginUnicorn = require('eslint-plugin-unicorn');
54
- const eslintrc = require('@eslint/eslintrc');
54
+ const globals = require('globals');
55
55
 
56
56
  module.exports = [
57
57
  {
58
58
  languageOptions: {
59
- globals: eslintrc.Legacy.environments.get('es2024'),
59
+ globals: globals.builtin,
60
60
  },
61
61
  plugins: {
62
62
  unicorn: eslintPluginUnicorn,
@@ -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. | ✅ | 🔧 | |
@@ -142,6 +143,7 @@ If you don't use the preset, ensure you use the same `env` and `parserOptions` c
142
143
  | [no-invalid-fetch-options](docs/rules/no-invalid-fetch-options.md) | Disallow invalid options in `fetch()` and `new Request()`. | ✅ | | |
143
144
  | [no-invalid-remove-event-listener](docs/rules/no-invalid-remove-event-listener.md) | Prevent calling `EventTarget#removeEventListener()` with the result of an expression. | ✅ | | |
144
145
  | [no-keyword-prefix](docs/rules/no-keyword-prefix.md) | Disallow identifiers starting with `new` or `class`. | | | |
146
+ | [no-length-as-slice-end](docs/rules/no-length-as-slice-end.md) | Disallow using `.length` as the `end` argument of `{Array,String,TypedArray}#slice()`. | ✅ | 🔧 | |
145
147
  | [no-lonely-if](docs/rules/no-lonely-if.md) | Disallow `if` statements as the only statement in `if` blocks without `else`. | ✅ | 🔧 | |
146
148
  | [no-magic-array-flat-depth](docs/rules/no-magic-array-flat-depth.md) | Disallow a magic number as the `depth` argument in `Array#flat(…).` | ✅ | | |
147
149
  | [no-negated-condition](docs/rules/no-negated-condition.md) | Disallow negated conditions. | ✅ | 🔧 | |
@@ -188,10 +190,12 @@ If you don't use the preset, ensure you use the same `env` and `parserOptions` c
188
190
  | [prefer-dom-node-text-content](docs/rules/prefer-dom-node-text-content.md) | Prefer `.textContent` over `.innerText`. | ✅ | | 💡 |
189
191
  | [prefer-event-target](docs/rules/prefer-event-target.md) | Prefer `EventTarget` over `EventEmitter`. | ✅ | | |
190
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`. | ✅ | 🔧 | |
191
194
  | [prefer-includes](docs/rules/prefer-includes.md) | Prefer `.includes()` over `.indexOf()`, `.lastIndexOf()`, and `Array#some()` when checking for existence or non-existence. | ✅ | 🔧 | 💡 |
192
195
  | [prefer-json-parse-buffer](docs/rules/prefer-json-parse-buffer.md) | Prefer reading a JSON file as a buffer. | | 🔧 | |
193
196
  | [prefer-keyboard-event-key](docs/rules/prefer-keyboard-event-key.md) | Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`. | ✅ | 🔧 | |
194
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. | ✅ | 🔧 | |
195
199
  | [prefer-math-trunc](docs/rules/prefer-math-trunc.md) | Enforce the use of `Math.trunc` instead of bitwise operators. | ✅ | 🔧 | 💡 |
196
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()`. | ✅ | 🔧 | |
197
201
  | [prefer-modern-math-apis](docs/rules/prefer-modern-math-apis.md) | Prefer modern `Math` APIs over legacy patterns. | ✅ | 🔧 | |
@@ -203,7 +207,7 @@ If you don't use the preset, ensure you use the same `env` and `parserOptions` c
203
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. | ✅ | 🔧 | |
204
208
  | [prefer-optional-catch-binding](docs/rules/prefer-optional-catch-binding.md) | Prefer omitting the `catch` binding parameter. | ✅ | 🔧 | |
205
209
  | [prefer-prototype-methods](docs/rules/prefer-prototype-methods.md) | Prefer borrowing methods from the prototype instead of the instance. | ✅ | 🔧 | |
206
- | [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()`. | ✅ | 🔧 | |
207
211
  | [prefer-reflect-apply](docs/rules/prefer-reflect-apply.md) | Prefer `Reflect.apply()` over `Function#apply()`. | ✅ | 🔧 | |
208
212
  | [prefer-regexp-test](docs/rules/prefer-regexp-test.md) | Prefer `RegExp#test()` over `String#match()` and `RegExp#exec()`. | ✅ | 🔧 | 💡 |
209
213
  | [prefer-set-has](docs/rules/prefer-set-has.md) | Prefer `Set#has()` over `Array#includes()` when checking for existence or non-existence. | ✅ | 🔧 | 💡 |
@@ -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
+ };
@@ -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
  }
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+ const {isMethodCall, isMemberExpression} = require('./ast/index.js');
3
+ const {removeArgument} = require('./fix/index.js');
4
+ const {isSameReference} = require('./utils/index.js');
5
+
6
+ const MESSAGE_ID = 'no-length-as-slice-end';
7
+ const messages = {
8
+ [MESSAGE_ID]: 'Passing `….length` as the `end` argument is unnecessary.',
9
+ };
10
+
11
+ /** @param {import('eslint').Rule.RuleContext} context */
12
+ const create = context => {
13
+ context.on('CallExpression', callExpression => {
14
+ if (!isMethodCall(callExpression, {
15
+ method: 'slice',
16
+ argumentsLength: 2,
17
+ optionalCall: false,
18
+ })) {
19
+ return;
20
+ }
21
+
22
+ const secondArgument = callExpression.arguments[1];
23
+ const node = secondArgument.type === 'ChainExpression' ? secondArgument.expression : secondArgument;
24
+
25
+ if (
26
+ !isMemberExpression(node, {property: 'length', computed: false})
27
+ || !isSameReference(callExpression.callee.object, node.object)
28
+ ) {
29
+ return;
30
+ }
31
+
32
+ return {
33
+ node,
34
+ messageId: MESSAGE_ID,
35
+ /** @param {import('eslint').Rule.RuleFixer} fixer */
36
+ fix: fixer => removeArgument(fixer, secondArgument, context.sourceCode),
37
+ };
38
+ });
39
+ };
40
+
41
+ /** @type {import('eslint').Rule.RuleModule} */
42
+ module.exports = {
43
+ create,
44
+ meta: {
45
+ type: 'suggestion',
46
+ docs: {
47
+ description: 'Disallow using `.length` as the `end` argument of `{Array,String,TypedArray}#slice()`.',
48
+ recommended: true,
49
+ },
50
+ fixable: 'code',
51
+ messages,
52
+ },
53
+ };
@@ -31,10 +31,11 @@ const create = context => ({
31
31
  BinaryExpression(binaryExpression) {
32
32
  const {operator, left} = binaryExpression;
33
33
 
34
- if (
35
- !isEqualityCheck(binaryExpression)
36
- || !isNegatedExpression(left)
37
- ) {
34
+ if (!(
35
+ isEqualityCheck(binaryExpression)
36
+ && isNegatedExpression(left)
37
+ && !isNegatedExpression(left.argument)
38
+ )) {
38
39
  return;
39
40
  }
40
41
 
@@ -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.',
@@ -2,7 +2,10 @@
2
2
  const {
3
3
  isCommaToken,
4
4
  } = require('@eslint-community/eslint-utils');
5
- const {isMethodCall} = require('./ast/index.js');
5
+ const {
6
+ isMethodCall,
7
+ isExpressionStatement,
8
+ } = require('./ast/index.js');
6
9
  const {
7
10
  getParenthesizedText,
8
11
  isParenthesized,
@@ -77,8 +80,8 @@ const unwrapNonAwaitedCallExpression = (callExpression, sourceCode) => fixer =>
77
80
  const switchToPromiseResolve = (callExpression, sourceCode) => function * (fixer) {
78
81
  /*
79
82
  ```
80
- Promise.all([promise,])
81
- // ^^^ methodNameNode
83
+ Promise.race([promise,])
84
+ // ^^^^ methodNameNode
82
85
  ```
83
86
  */
84
87
  const methodNameNode = callExpression.callee.property;
@@ -87,16 +90,16 @@ const switchToPromiseResolve = (callExpression, sourceCode) => function * (fixer
87
90
  const [arrayExpression] = callExpression.arguments;
88
91
  /*
89
92
  ```
90
- Promise.all([promise,])
91
- // ^ openingBracketToken
93
+ Promise.race([promise,])
94
+ // ^ openingBracketToken
92
95
  ```
93
96
  */
94
97
  const openingBracketToken = sourceCode.getFirstToken(arrayExpression);
95
98
  /*
96
99
  ```
97
- Promise.all([promise,])
98
- // ^ penultimateToken
99
- // ^ closingBracketToken
100
+ Promise.race([promise,])
101
+ // ^ penultimateToken
102
+ // ^ closingBracketToken
100
103
  ```
101
104
  */
102
105
  const [
@@ -119,11 +122,13 @@ const create = context => ({
119
122
  return;
120
123
  }
121
124
 
125
+ const methodName = callExpression.callee.property.name;
126
+
122
127
  const problem = {
123
128
  node: callExpression.arguments[0],
124
129
  messageId: MESSAGE_ID_ERROR,
125
130
  data: {
126
- method: callExpression.callee.property.name,
131
+ method: methodName,
127
132
  },
128
133
  };
129
134
 
@@ -132,11 +137,19 @@ const create = context => ({
132
137
  if (
133
138
  callExpression.parent.type === 'AwaitExpression'
134
139
  && callExpression.parent.argument === callExpression
140
+ && (
141
+ methodName !== 'all'
142
+ || isExpressionStatement(callExpression.parent.parent)
143
+ )
135
144
  ) {
136
145
  problem.fix = unwrapAwaitedCallExpression(callExpression, sourceCode);
137
146
  return problem;
138
147
  }
139
148
 
149
+ if (methodName === 'all') {
150
+ return problem;
151
+ }
152
+
140
153
  problem.suggest = [
141
154
  {
142
155
  messageId: MESSAGE_ID_SUGGESTION_UNWRAP,
@@ -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
+ };
@@ -32,8 +32,8 @@ const create = () => ({
32
32
  if (
33
33
  typeof value !== 'string'
34
34
  || value.startsWith('node:')
35
+ || /^bun(?::|$)/.test(value)
35
36
  || !isBuiltinModule(value)
36
- || !isBuiltinModule(`node:${value}`)
37
37
  ) {
38
38
  return;
39
39
  }
@@ -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) {