eslint-plugin-unicorn 53.0.0 β†’ 54.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.
package/index.js CHANGED
@@ -47,9 +47,12 @@ const allRules = Object.fromEntries(
47
47
  ]),
48
48
  );
49
49
 
50
- const createConfig = (rules, isLegacyConfig = false) => ({
51
- ...(isLegacyConfig ? legacyConfigBase : flatConfigBase),
52
- plugins: isLegacyConfig ? ['unicorn'] : {unicorn},
50
+ const createConfig = (rules, flatConfigName = false) => ({
51
+ ...(
52
+ flatConfigName
53
+ ? {...flatConfigBase, name: flatConfigName, plugins: {unicorn}}
54
+ : {...legacyConfigBase, plugins: ['unicorn']}
55
+ ),
53
56
  rules: {...externalRules, ...rules},
54
57
  });
55
58
 
@@ -65,10 +68,10 @@ const unicorn = {
65
68
  };
66
69
 
67
70
  const configs = {
68
- recommended: createConfig(recommendedRules, /* isLegacyConfig */ true),
69
- all: createConfig(allRules, /* isLegacyConfig */ true),
70
- 'flat/recommended': createConfig(recommendedRules),
71
- 'flat/all': createConfig(allRules),
71
+ recommended: createConfig(recommendedRules),
72
+ all: createConfig(allRules),
73
+ 'flat/recommended': createConfig(recommendedRules, 'unicorn/flat/recommended'),
74
+ 'flat/all': createConfig(allRules, 'unicorn/flat/all'),
72
75
  };
73
76
 
74
77
  module.exports = {...unicorn, configs};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-unicorn",
3
- "version": "53.0.0",
3
+ "version": "54.0.0",
4
4
  "description": "More than 100 powerful ESLint rules",
5
5
  "license": "MIT",
6
6
  "repository": "sindresorhus/eslint-plugin-unicorn",
@@ -29,7 +29,7 @@
29
29
  "lint:markdown": "markdownlint \"**/*.md\"",
30
30
  "lint:package-json": "npmPkgJsonLint .",
31
31
  "run-rules-on-codebase": "node ./test/run-rules-on-codebase/lint.mjs",
32
- "smoke": "eslint-remote-tester --config ./test/smoke/eslint-remote-tester.config.js",
32
+ "smoke": "eslint-remote-tester --config ./test/smoke/eslint-remote-tester.config.mjs",
33
33
  "test": "npm-run-all --continue-on-error lint test:*",
34
34
  "test:js": "c8 ava"
35
35
  },
@@ -71,7 +71,7 @@
71
71
  "@babel/core": "^7.24.5",
72
72
  "@babel/eslint-parser": "^7.24.5",
73
73
  "@lubien/fixture-beta-package": "^1.0.0-beta.1",
74
- "@typescript-eslint/parser": "^7.8.0",
74
+ "@typescript-eslint/parser": "^8.0.0-alpha.12",
75
75
  "ava": "^6.1.3",
76
76
  "c8": "^9.1.0",
77
77
  "chalk": "^5.3.0",
@@ -81,8 +81,8 @@
81
81
  "eslint-doc-generator": "1.7.0",
82
82
  "eslint-plugin-eslint-plugin": "^6.1.0",
83
83
  "eslint-plugin-internal-rules": "file:./scripts/internal-rules/",
84
- "eslint-remote-tester": "^3.0.1",
85
- "eslint-remote-tester-repositories": "^1.0.1",
84
+ "eslint-remote-tester": "^4.0.0",
85
+ "eslint-remote-tester-repositories": "^2.0.0",
86
86
  "espree": "^10.0.1",
87
87
  "execa": "^8.0.1",
88
88
  "listr": "^0.14.3",
package/readme.md CHANGED
@@ -145,6 +145,7 @@ If you don't use the preset, ensure you use the same `env` and `parserOptions` c
145
145
  | [no-lonely-if](docs/rules/no-lonely-if.md) | Disallow `if` statements as the only statement in `if` blocks without `else`. | βœ… | πŸ”§ | |
146
146
  | [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
147
  | [no-negated-condition](docs/rules/no-negated-condition.md) | Disallow negated conditions. | βœ… | πŸ”§ | |
148
+ | [no-negation-in-equality-check](docs/rules/no-negation-in-equality-check.md) | Disallow negated expression in equality check. | βœ… | | πŸ’‘ |
148
149
  | [no-nested-ternary](docs/rules/no-nested-ternary.md) | Disallow nested ternary expressions. | βœ… | πŸ”§ | |
149
150
  | [no-new-array](docs/rules/no-new-array.md) | Disallow `new Array()`. | βœ… | πŸ”§ | πŸ’‘ |
150
151
  | [no-new-buffer](docs/rules/no-new-buffer.md) | Enforce the use of `Buffer.from()` and `Buffer.alloc()` instead of the deprecated `new Buffer()`. | βœ… | πŸ”§ | πŸ’‘ |
@@ -175,7 +176,7 @@ If you don't use the preset, ensure you use the same `env` and `parserOptions` c
175
176
  | [prefer-array-flat](docs/rules/prefer-array-flat.md) | Prefer `Array#flat()` over legacy techniques to flatten arrays. | βœ… | πŸ”§ | |
176
177
  | [prefer-array-flat-map](docs/rules/prefer-array-flat-map.md) | Prefer `.flatMap(…)` over `.map(…).flat()`. | βœ… | πŸ”§ | |
177
178
  | [prefer-array-index-of](docs/rules/prefer-array-index-of.md) | Prefer `Array#{indexOf,lastIndexOf}()` over `Array#{findIndex,findLastIndex}()` when looking for the index of an item. | βœ… | πŸ”§ | πŸ’‘ |
178
- | [prefer-array-some](docs/rules/prefer-array-some.md) | Prefer `.some(…)` over `.filter(…).length` check and `.{find,findLast}(…)`. | βœ… | πŸ”§ | πŸ’‘ |
179
+ | [prefer-array-some](docs/rules/prefer-array-some.md) | Prefer `.some(…)` over `.filter(…).length` check and `.{find,findLast,findIndex,findLastIndex}(…)`. | βœ… | πŸ”§ | πŸ’‘ |
179
180
  | [prefer-at](docs/rules/prefer-at.md) | Prefer `.at()` method for index access and `String#charAt()`. | βœ… | πŸ”§ | πŸ’‘ |
180
181
  | [prefer-blob-reading-methods](docs/rules/prefer-blob-reading-methods.md) | Prefer `Blob#arrayBuffer()` over `FileReader#readAsArrayBuffer(…)` and `Blob#text()` over `FileReader#readAsText(…)`. | βœ… | | |
181
182
  | [prefer-code-point](docs/rules/prefer-code-point.md) | Prefer `String#codePointAt(…)` over `String#charCodeAt(…)` and `String.fromCodePoint(…)` over `String.fromCharCode(…)`. | βœ… | | πŸ’‘ |
@@ -187,7 +188,7 @@ If you don't use the preset, ensure you use the same `env` and `parserOptions` c
187
188
  | [prefer-dom-node-text-content](docs/rules/prefer-dom-node-text-content.md) | Prefer `.textContent` over `.innerText`. | βœ… | | πŸ’‘ |
188
189
  | [prefer-event-target](docs/rules/prefer-event-target.md) | Prefer `EventTarget` over `EventEmitter`. | βœ… | | |
189
190
  | [prefer-export-from](docs/rules/prefer-export-from.md) | Prefer `export…from` when re-exporting. | βœ… | πŸ”§ | πŸ’‘ |
190
- | [prefer-includes](docs/rules/prefer-includes.md) | Prefer `.includes()` over `.indexOf()` and `Array#some()` when checking for existence or non-existence. | βœ… | πŸ”§ | πŸ’‘ |
191
+ | [prefer-includes](docs/rules/prefer-includes.md) | Prefer `.includes()` over `.indexOf()`, `.lastIndexOf()`, and `Array#some()` when checking for existence or non-existence. | βœ… | πŸ”§ | πŸ’‘ |
191
192
  | [prefer-json-parse-buffer](docs/rules/prefer-json-parse-buffer.md) | Prefer reading a JSON file as a buffer. | | πŸ”§ | |
192
193
  | [prefer-keyboard-event-key](docs/rules/prefer-keyboard-event-key.md) | Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`. | βœ… | πŸ”§ | |
193
194
  | [prefer-logical-operator-over-ternary](docs/rules/prefer-logical-operator-over-ternary.md) | Prefer using a logical operator over a ternary. | βœ… | | πŸ’‘ |
@@ -120,11 +120,18 @@ function isNotReference(node) {
120
120
  return parent.parameters.includes(node);
121
121
  }
122
122
 
123
+ // `@typescript-eslint/parse` v7
123
124
  // `type Foo = { [Identifier in keyof string]: number; };`
124
125
  case 'TSTypeParameter': {
125
126
  return parent.name === node;
126
127
  }
127
128
 
129
+ // `@typescript-eslint/parse` v8
130
+ // `type Foo = { [Identifier in keyof string]: number; };`
131
+ case 'TSMappedType': {
132
+ return parent.key === node;
133
+ }
134
+
128
135
  // `type Identifier = Foo`
129
136
  case 'TSTypeAliasDeclaration': {
130
137
  return parent.id === node;
@@ -1,8 +1,10 @@
1
1
  'use strict';
2
2
  const {isParenthesized} = require('../utils/parentheses.js');
3
3
  const shouldAddParenthesesToNewExpressionCallee = require('../utils/should-add-parentheses-to-new-expression-callee.js');
4
+ const fixSpaceAroundKeyword = require('./fix-space-around-keywords.js');
4
5
 
5
6
  function * switchCallExpressionToNewExpression(node, sourceCode, fixer) {
7
+ yield * fixSpaceAroundKeyword(fixer, node, sourceCode);
6
8
  yield fixer.insertTextBefore(node, 'new ');
7
9
 
8
10
  const {callee} = node;
@@ -0,0 +1,104 @@
1
+ 'use strict';
2
+ const {
3
+ fixSpaceAroundKeyword,
4
+ addParenthesizesToReturnOrThrowExpression,
5
+ } = require('./fix/index.js');
6
+ const {
7
+ needsSemicolon,
8
+ isParenthesized,
9
+ isOnSameLine,
10
+ } = require('./utils/index.js');
11
+
12
+ const MESSAGE_ID_ERROR = 'no-negation-in-equality-check/error';
13
+ const MESSAGE_ID_SUGGESTION = 'no-negation-in-equality-check/suggestion';
14
+ const messages = {
15
+ [MESSAGE_ID_ERROR]: 'Negated expression in not allowed in equality check.',
16
+ [MESSAGE_ID_SUGGESTION]: 'Switch to \'{{operator}}\' check.',
17
+ };
18
+
19
+ const EQUALITY_OPERATORS = new Set([
20
+ '===',
21
+ '!==',
22
+ '==',
23
+ '!=',
24
+ ]);
25
+
26
+ const isEqualityCheck = node => node.type === 'BinaryExpression' && EQUALITY_OPERATORS.has(node.operator);
27
+ const isNegatedExpression = node => node.type === 'UnaryExpression' && node.prefix && node.operator === '!';
28
+
29
+ /** @param {import('eslint').Rule.RuleContext} context */
30
+ const create = context => ({
31
+ BinaryExpression(binaryExpression) {
32
+ const {operator, left} = binaryExpression;
33
+
34
+ if (
35
+ !isEqualityCheck(binaryExpression)
36
+ || !isNegatedExpression(left)
37
+ ) {
38
+ return;
39
+ }
40
+
41
+ const {sourceCode} = context;
42
+ const bangToken = sourceCode.getFirstToken(left);
43
+ const negatedOperator = `${operator.startsWith('!') ? '=' : '!'}${operator.slice(1)}`;
44
+
45
+ return {
46
+ node: bangToken,
47
+ messageId: MESSAGE_ID_ERROR,
48
+ /** @param {import('eslint').Rule.RuleFixer} fixer */
49
+ suggest: [
50
+ {
51
+ messageId: MESSAGE_ID_SUGGESTION,
52
+ data: {
53
+ operator: negatedOperator,
54
+ },
55
+ /** @param {import('eslint').Rule.RuleFixer} fixer */
56
+ * fix(fixer) {
57
+ yield * fixSpaceAroundKeyword(fixer, binaryExpression, sourceCode);
58
+
59
+ const tokenAfterBang = sourceCode.getTokenAfter(bangToken);
60
+
61
+ const {parent} = binaryExpression;
62
+ if (
63
+ (parent.type === 'ReturnStatement' || parent.type === 'ThrowStatement')
64
+ && !isParenthesized(binaryExpression, sourceCode)
65
+ ) {
66
+ const returnToken = sourceCode.getFirstToken(parent);
67
+ if (!isOnSameLine(returnToken, tokenAfterBang)) {
68
+ yield * addParenthesizesToReturnOrThrowExpression(fixer, parent, sourceCode);
69
+ }
70
+ }
71
+
72
+ yield fixer.remove(bangToken);
73
+
74
+ const previousToken = sourceCode.getTokenBefore(bangToken);
75
+ if (needsSemicolon(previousToken, sourceCode, tokenAfterBang.value)) {
76
+ yield fixer.insertTextAfter(bangToken, ';');
77
+ }
78
+
79
+ const operatorToken = sourceCode.getTokenAfter(
80
+ left,
81
+ token => token.type === 'Punctuator' && token.value === operator,
82
+ );
83
+ yield fixer.replaceText(operatorToken, negatedOperator);
84
+ },
85
+ },
86
+ ],
87
+ };
88
+ },
89
+ });
90
+
91
+ /** @type {import('eslint').Rule.RuleModule} */
92
+ module.exports = {
93
+ create,
94
+ meta: {
95
+ type: 'problem',
96
+ docs: {
97
+ description: 'Disallow negated expression in equality check.',
98
+ recommended: true,
99
+ },
100
+
101
+ hasSuggestions: true,
102
+ messages,
103
+ },
104
+ };
@@ -180,7 +180,7 @@ const create = context => {
180
180
  const {
181
181
  checkFromLast,
182
182
  } = {
183
- checkFromLast: false,
183
+ checkFromLast: true,
184
184
  ...context.options[0],
185
185
  };
186
186
 
@@ -428,8 +428,8 @@ const schema = [
428
428
  properties: {
429
429
  checkFromLast: {
430
430
  type: 'boolean',
431
- // TODO: Change default value to `true`, or remove the option when targeting Node.js 18.
432
- default: false,
431
+ // TODO: Remove the option at some point.
432
+ default: true,
433
433
  },
434
434
  },
435
435
  },
@@ -46,7 +46,6 @@ const arrayFlatMap = {
46
46
  },
47
47
  getArrayNode: node => node.callee.object,
48
48
  description: 'Array#flatMap()',
49
- recommended: true,
50
49
  };
51
50
 
52
51
  // `array.reduce((a, b) => a.concat(b), [])`
@@ -100,7 +99,6 @@ const arrayReduce = {
100
99
  },
101
100
  getArrayNode: node => node.callee.object,
102
101
  description: 'Array#reduce()',
103
- recommended: true,
104
102
  };
105
103
 
106
104
  // `[].concat(maybeArray)`
@@ -121,7 +119,6 @@ const emptyArrayConcat = {
121
119
  return argumentNode.type === 'SpreadElement' ? argumentNode.argument : argumentNode;
122
120
  },
123
121
  description: '[].concat()',
124
- recommended: true,
125
122
  shouldSwitchToArray: node => node.arguments[0].type !== 'SpreadElement',
126
123
  };
127
124
 
@@ -157,7 +154,6 @@ const arrayPrototypeConcat = {
157
154
  return argumentNode.type === 'SpreadElement' ? argumentNode.argument : argumentNode;
158
155
  },
159
156
  description: 'Array.prototype.concat()',
160
- recommended: true,
161
157
  shouldSwitchToArray: node => node.arguments[1].type !== 'SpreadElement' && node.callee.property.name === 'call',
162
158
  };
163
159
 
@@ -40,10 +40,14 @@ const isCheckingUndefined = node =>
40
40
  && isLiteral(node.parent.right, null)
41
41
  )
42
42
  );
43
+ const isNegativeOne = node => node.type === 'UnaryExpression' && node.operator === '-' && node.argument && node.argument.type === 'Literal' && node.argument.value === 1;
44
+ const isLiteralZero = node => isLiteral(node, 0);
43
45
 
44
46
  /** @param {import('eslint').Rule.RuleContext} context */
45
- const create = context => ({
46
- CallExpression(callExpression) {
47
+ const create = context => {
48
+ // `.find(…)`
49
+ // `.findLast(…)`
50
+ context.on('CallExpression', callExpression => {
47
51
  if (!isMethodCall(callExpression, {
48
52
  methods: ['find', 'findLast'],
49
53
  minimumArguments: 1,
@@ -86,8 +90,61 @@ const create = context => ({
86
90
  },
87
91
  ],
88
92
  };
89
- },
90
- BinaryExpression(binaryExpression) {
93
+ });
94
+
95
+ // These operators also used in `prefer-includes`, try to reuse the code in future
96
+ // `.{findIndex,findLastIndex}(…) !== -1`
97
+ // `.{findIndex,findLastIndex}(…) != -1`
98
+ // `.{findIndex,findLastIndex}(…) > -1`
99
+ // `.{findIndex,findLastIndex}(…) === -1`
100
+ // `.{findIndex,findLastIndex}(…) == -1`
101
+ // `.{findIndex,findLastIndex}(…) >= 0`
102
+ // `.{findIndex,findLastIndex}(…) < 0`
103
+ context.on('BinaryExpression', binaryExpression => {
104
+ const {left, right, operator} = binaryExpression;
105
+
106
+ if (!(
107
+ isMethodCall(left, {
108
+ methods: ['findIndex', 'findLastIndex'],
109
+ argumentsLength: 1,
110
+ optionalCall: false,
111
+ optionalMember: false,
112
+ })
113
+ && (
114
+ (['!==', '!=', '>', '===', '=='].includes(operator) && isNegativeOne(right))
115
+ || (['>=', '<'].includes(operator) && isLiteralZero(right))
116
+ )
117
+ )) {
118
+ return;
119
+ }
120
+
121
+ const methodNode = left.callee.property;
122
+ return {
123
+ node: methodNode,
124
+ messageId: ERROR_ID_ARRAY_SOME,
125
+ data: {method: methodNode.name},
126
+ * fix(fixer) {
127
+ if (['===', '==', '<'].includes(operator)) {
128
+ yield fixer.insertTextBefore(binaryExpression, '!');
129
+ }
130
+
131
+ yield fixer.replaceText(methodNode, 'some');
132
+
133
+ const operatorToken = context.sourceCode.getTokenAfter(
134
+ left,
135
+ token => token.type === 'Punctuator' && token.value === operator,
136
+ );
137
+ const [start] = operatorToken.range;
138
+ const [, end] = binaryExpression.range;
139
+
140
+ yield fixer.removeRange([start, end]);
141
+ },
142
+ };
143
+ });
144
+
145
+ // `.filter(…).length > 0`
146
+ // `.filter(…).length !== 0`
147
+ context.on('BinaryExpression', binaryExpression => {
91
148
  if (!(
92
149
  // We assume the user already follows `unicorn/explicit-length-check`. These are allowed in that rule.
93
150
  (binaryExpression.operator === '>' || binaryExpression.operator === '!==')
@@ -139,8 +196,8 @@ const create = context => ({
139
196
  // The `BinaryExpression` always ends with a number or `)`, no need check for ASI
140
197
  },
141
198
  };
142
- },
143
- });
199
+ });
200
+ };
144
201
 
145
202
  /** @type {import('eslint').Rule.RuleModule} */
146
203
  module.exports = {
@@ -148,7 +205,7 @@ module.exports = {
148
205
  meta: {
149
206
  type: 'suggestion',
150
207
  docs: {
151
- description: 'Prefer `.some(…)` over `.filter(…).length` check and `.{find,findLast}(…)`.',
208
+ description: 'Prefer `.some(…)` over `.filter(…).length` check and `.{find,findLast,findIndex,findLastIndex}(…)`.',
152
209
  recommended: true,
153
210
  },
154
211
  fixable: 'code',
@@ -5,9 +5,9 @@ const {isLiteral} = require('./ast/index.js');
5
5
 
6
6
  const MESSAGE_ID = 'prefer-includes';
7
7
  const messages = {
8
- [MESSAGE_ID]: 'Use `.includes()`, rather than `.indexOf()`, when checking for existence.',
8
+ [MESSAGE_ID]: 'Use `.includes()`, rather than `.{{method}}()`, when checking for existence.',
9
9
  };
10
- // Ignore {_,lodash,underscore}.indexOf
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
13
  const isNegativeOne = node => node.type === 'UnaryExpression' && node.operator === '-' && node.argument && node.argument.type === 'Literal' && node.argument.value === 1;
@@ -30,6 +30,9 @@ const getProblem = (context, node, target, argumentsNodes) => {
30
30
  return {
31
31
  node: memberExpressionNode.property,
32
32
  messageId: MESSAGE_ID,
33
+ data: {
34
+ method: node.left.callee.property.name,
35
+ },
33
36
  fix(fixer) {
34
37
  const replacement = `${isNegativeResult(node) ? '!' : ''}${targetSource}.includes(${argumentsSource.join(', ')})`;
35
38
  return fixer.replaceText(node, replacement);
@@ -49,7 +52,7 @@ const create = context => {
49
52
  context.on('BinaryExpression', node => {
50
53
  const {left, right, operator} = node;
51
54
 
52
- if (!isMethodNamed(left, 'indexOf')) {
55
+ if (!isMethodNamed(left, 'indexOf') && !isMethodNamed(left, 'lastIndexOf')) {
53
56
  return;
54
57
  }
55
58
 
@@ -86,7 +89,7 @@ module.exports = {
86
89
  meta: {
87
90
  type: 'suggestion',
88
91
  docs: {
89
- description: 'Prefer `.includes()` over `.indexOf()` and `Array#some()` when checking for existence or non-existence.',
92
+ description: 'Prefer `.includes()` over `.indexOf()`, `.lastIndexOf()`, and `Array#some()` when checking for existence or non-existence.',
90
93
  recommended: true,
91
94
  },
92
95
  fixable: 'code',
@@ -154,7 +154,6 @@ const create = context => {
154
154
  data: {
155
155
  replacement: `Math.${replacementMethod}(…)`,
156
156
  description: 'Math.sqrt(…)',
157
- recommended: true,
158
157
  },
159
158
  * fix(fixer) {
160
159
  const {sourceCode} = context;
@@ -47,6 +47,7 @@ const create = context => {
47
47
  )
48
48
  || (node.parent.type === 'Property' && !node.parent.computed && node.parent.key === node)
49
49
  || (node.parent.type === 'JSXAttribute' && node.parent.value === node)
50
+ || (node.parent.type === 'TSEnumMember' && (node.parent.initializer === node || node.parent.id === node))
50
51
  ) {
51
52
  return;
52
53
  }
@@ -61,7 +61,6 @@ const create = context => {
61
61
  messageId: MESSAGE_ID_ERROR,
62
62
  data: {
63
63
  description: 'JSON.parse(JSON.stringify(…))',
64
- recommended: true,
65
64
  },
66
65
  suggest: [
67
66
  {