eslint-plugin-unicorn 68.0.0 → 69.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.
Files changed (53) hide show
  1. package/package.json +3 -1
  2. package/readme.md +12 -0
  3. package/rules/ast/is-empty-object-expression.js +2 -2
  4. package/rules/comment-content.js +136 -18
  5. package/rules/consistent-boolean-name.js +31 -4
  6. package/rules/consistent-conditional-object-spread.js +9 -3
  7. package/rules/consistent-tuple-labels.js +49 -0
  8. package/rules/custom-error-definition.js +26 -7
  9. package/rules/index.js +12 -0
  10. package/rules/no-chained-comparison.js +1 -1
  11. package/rules/no-computed-property-existence-check.js +6 -0
  12. package/rules/no-incorrect-template-string-interpolation.js +82 -30
  13. package/rules/no-instanceof-builtins.js +59 -13
  14. package/rules/no-invalid-well-known-symbol-methods.js +205 -0
  15. package/rules/no-late-current-target-access.js +9 -71
  16. package/rules/no-late-event-control.js +127 -0
  17. package/rules/no-nonstandard-builtin-properties.js +2 -0
  18. package/rules/no-object-methods-with-collections.js +4 -289
  19. package/rules/no-return-array-push.js +20 -5
  20. package/rules/no-top-level-side-effects.js +8 -1
  21. package/rules/no-unnecessary-global-this.js +36 -1
  22. package/rules/no-unreadable-object-destructuring.js +15 -0
  23. package/rules/no-unsafe-string-replacement.js +31 -0
  24. package/rules/no-unused-array-method-return.js +11 -2
  25. package/rules/no-useless-boolean-cast.js +58 -1
  26. package/rules/no-useless-concat.js +27 -3
  27. package/rules/no-useless-else.js +16 -2
  28. package/rules/no-useless-iterator-to-array.js +165 -115
  29. package/rules/prefer-abort-signal-timeout.js +395 -0
  30. package/rules/prefer-aggregate-error.js +478 -0
  31. package/rules/prefer-at.js +5 -6
  32. package/rules/prefer-code-point.js +58 -8
  33. package/rules/prefer-continue.js +16 -0
  34. package/rules/prefer-dom-node-replace-children.js +353 -0
  35. package/rules/prefer-error-is-error.js +181 -0
  36. package/rules/prefer-has-check.js +1 -4
  37. package/rules/prefer-hoisting-branch-code.js +9 -7
  38. package/rules/prefer-iterator-concat.js +21 -1
  39. package/rules/prefer-minimal-ternary.js +15 -11
  40. package/rules/prefer-modern-dom-apis.js +1 -45
  41. package/rules/prefer-observer-apis.js +498 -0
  42. package/rules/prefer-promise-try.js +238 -0
  43. package/rules/prefer-set-methods.js +337 -0
  44. package/rules/prefer-toggle-attribute.js +293 -0
  45. package/rules/prefer-url-search-parameters.js +411 -0
  46. package/rules/shared/late-event-handler.js +82 -0
  47. package/rules/shared/name-replacements.js +6 -0
  48. package/rules/utils/builtin-collection-type.js +267 -0
  49. package/rules/utils/index.js +6 -0
  50. package/rules/utils/is-boolean.js +3 -1
  51. package/rules/utils/is-event.js +127 -0
  52. package/rules/utils/should-report-replace-children-receiver.js +210 -0
  53. package/rules/utils/type-helpers.js +166 -8
@@ -1,11 +1,60 @@
1
1
  import {isFunction, isMethodCall} from './ast/index.js';
2
- import {isGlobalBooleanCall, isSameIdentifier} from './utils/index.js';
2
+ import {
3
+ hasOptionalChainElement,
4
+ isGlobalBooleanCall,
5
+ isNullishType,
6
+ isSameIdentifier,
7
+ unwrapTypeScriptExpression,
8
+ } from './utils/index.js';
3
9
 
4
10
  const MESSAGE_ID = 'no-useless-boolean-cast';
5
11
  const messages = {
6
12
  [MESSAGE_ID]: '`Boolean()` is unnecessary in `Array#{{method}}()` callbacks.',
7
13
  };
8
14
 
15
+ const isNullishOrVoidType = type =>
16
+ type.intrinsicName === 'void' || isNullishType(type);
17
+
18
+ // When type information is available, the `Boolean()` cast is meaningful if the argument's type includes `null`/`undefined`/`void`, since removing it would widen the predicate's return type. Returns `false` when type information is unavailable, so it only ever keeps more casts than the syntactic check alone, never fewer.
19
+ function hasNullishOrVoidType(node, context) {
20
+ const {parserServices} = context.sourceCode;
21
+ if (!parserServices?.program) {
22
+ return false;
23
+ }
24
+
25
+ try {
26
+ const type = parserServices.getTypeAtLocation(node);
27
+ const types = type.isUnion() ? type.types : [type];
28
+ return types.some(type => isNullishOrVoidType(type));
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ function isOptionalChainResult(node) {
35
+ node = unwrapTypeScriptExpression(node);
36
+
37
+ if (node?.type === 'ChainExpression') {
38
+ return hasOptionalChainElement(node);
39
+ }
40
+
41
+ if (node?.type === 'LogicalExpression') {
42
+ return node.operator === '&&'
43
+ ? isOptionalChainResult(node.left) || isOptionalChainResult(node.right)
44
+ : isOptionalChainResult(node.right);
45
+ }
46
+
47
+ if (node?.type === 'ConditionalExpression') {
48
+ return isOptionalChainResult(node.consequent) || isOptionalChainResult(node.alternate);
49
+ }
50
+
51
+ if (node?.type === 'SequenceExpression') {
52
+ return isOptionalChainResult(node.expressions.at(-1));
53
+ }
54
+
55
+ return false;
56
+ }
57
+
9
58
  const predicateMethods = [
10
59
  'every',
11
60
  'filter',
@@ -79,6 +128,14 @@ const create = context => {
79
128
  return;
80
129
  }
81
130
 
131
+ // `Boolean()` normalizes a possibly-`undefined` value, so it is not useless. Detect this syntactically via optional chaining, and, when type information is available, via the resolved type.
132
+ if (
133
+ isOptionalChainResult(argument)
134
+ || hasNullishOrVoidType(argument, context)
135
+ ) {
136
+ return;
137
+ }
138
+
82
139
  return {
83
140
  node: booleanCall,
84
141
  messageId: MESSAGE_ID,
@@ -30,6 +30,11 @@ function getStringValue(node) {
30
30
  // Legacy octal (`\1`, `\012`) and `\8`/`\9` escapes are valid in sloppy-mode string literals but are syntax errors inside template literals.
31
31
  const hasTemplateIncompatibleEscape = raw => /(?<=(?:^|[^\\])(?:\\\\)*)\\(?:[1-9]|0\d)/v.test(raw);
32
32
 
33
+ // Whether the string contains a `${…}` placeholder. The regex mirrors ESLint's `no-template-curly-in-string`, which we defer to, so an empty `${}` is intentionally not matched.
34
+ const hasTemplatePlaceholder = string => /\$\{[^}]+\}/u.test(string);
35
+
36
+ const formsTemplatePlaceholderBoundary = (leftRaw, rightRaw) => leftRaw.endsWith('$') && rightRaw.startsWith('{');
37
+
33
38
  // The raw inner content of a literal as it would appear inside a template literal.
34
39
  function toTemplateElementRaw(node, sourceCode) {
35
40
  if (node.type === 'TemplateLiteral') {
@@ -69,9 +74,28 @@ const create = context => {
69
74
 
70
75
  const leftValue = getStringValue(left);
71
76
  const rightValue = getStringValue(right);
72
- const replacement = leftValue === undefined || rightValue === undefined
73
- ? `\`${toTemplateElementRaw(left, sourceCode)}${toTemplateElementRaw(right, sourceCode)}\``
74
- : escapeString(leftValue + rightValue);
77
+
78
+ // Merging into a single string that contains a `${…}` placeholder produces an ambiguous template-like literal that `no-template-curly-in-string` flags. The split is likely intentional, so leave it alone.
79
+ if (
80
+ leftValue !== undefined
81
+ && rightValue !== undefined
82
+ && hasTemplatePlaceholder(leftValue + rightValue)
83
+ ) {
84
+ return;
85
+ }
86
+
87
+ let replacement;
88
+ if (leftValue === undefined || rightValue === undefined) {
89
+ const leftRaw = toTemplateElementRaw(left, sourceCode);
90
+ const rightRaw = toTemplateElementRaw(right, sourceCode);
91
+ if (formsTemplatePlaceholderBoundary(leftRaw, rightRaw)) {
92
+ return;
93
+ }
94
+
95
+ replacement = `\`${leftRaw}${rightRaw}\``;
96
+ } else {
97
+ replacement = escapeString(leftValue + rightValue);
98
+ }
75
99
 
76
100
  const operatorToken = sourceCode.getTokenBefore(right, token => token.type === 'Punctuator' && token.value === '+');
77
101
 
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  hasCommentInRange,
3
3
  hasDirectBlockScopedDeclaration,
4
- hasMultilineToken,
5
4
  needsSemicolon,
6
5
  trackBranchExits,
7
6
  } from './utils/index.js';
@@ -25,6 +24,7 @@ const statementListParentTypes = new Set([
25
24
  const asiHazardCharacters = new Set([
26
25
  '[',
27
26
  '(',
27
+ '<',
28
28
  '/',
29
29
  '`',
30
30
  '+',
@@ -38,6 +38,20 @@ const getLineIndent = (sourceCode, index) => {
38
38
 
39
39
  const startsWithAsiHazard = token => asiHazardCharacters.has(token.value[0]);
40
40
 
41
+ const isJsxChildTextToken = (token, sourceCode) =>
42
+ token.type === 'JSXText'
43
+ && sourceCode.getNodeByRangeIndex(sourceCode.getRange(token)[0]).type === 'JSXText';
44
+
45
+ // A multiline token's internal whitespace can be meaningful, so it can't be safely reindented.
46
+ // JSX child text is safe because JSX collapses line-leading and line-trailing whitespace at compile time.
47
+ const hasReindentUnsafeMultilineToken = (node, context) => {
48
+ const {sourceCode} = context;
49
+ return sourceCode.getTokens(node).some(token =>
50
+ !isJsxChildTextToken(token, sourceCode)
51
+ && sourceCode.getLoc(token).start.line !== sourceCode.getLoc(token).end.line,
52
+ );
53
+ };
54
+
41
55
  const getBlockBodyText = (blockStatement, ifStatement, sourceCode) => {
42
56
  const openingBrace = sourceCode.getFirstToken(blockStatement);
43
57
  const closingBrace = sourceCode.getLastToken(blockStatement);
@@ -156,7 +170,7 @@ const fix = (ifStatement, context) => fixer => {
156
170
  hasDirectBlockScopedDeclaration(alternate)
157
171
  || (
158
172
  alternate.type === 'BlockStatement'
159
- && hasMultilineToken(alternate, context)
173
+ && hasReindentUnsafeMultilineToken(alternate, context)
160
174
  )
161
175
  || !isSafeToMoveAlternate(ifStatement, context)
162
176
  ) {
@@ -15,7 +15,7 @@ const MESSAGE_ID_SUGGESTION_ITERATOR_METHOD = 'iterator-method/suggestion';
15
15
 
16
16
  const messages = {
17
17
  [MESSAGE_ID_ITERABLE_ACCEPTING]: '`{{description}}` accepts an iterable, `.toArray()` is unnecessary.',
18
- [MESSAGE_ID_FOR_OF]: '`for…of` can iterate over an iterable, `.toArray()` is unnecessary.',
18
+ [MESSAGE_ID_FOR_OF]: '`for…of`/`for await…of` can iterate over an iterable, `.toArray()` is unnecessary.',
19
19
  [MESSAGE_ID_YIELD_STAR]: '`yield*` can delegate to an iterable, `.toArray()` is unnecessary.',
20
20
  [MESSAGE_ID_SPREAD]: 'Spread works on iterables, `.toArray()` is unnecessary.',
21
21
  [MESSAGE_ID_ITERATOR_METHOD]: '`Iterator` has a `.{{method}}()` method, `.toArray()` is unnecessary.',
@@ -45,6 +45,154 @@ const isToArrayCall = node => isMethodCall(node, {
45
45
  optionalMember: false,
46
46
  });
47
47
 
48
+ const getRemoveToArrayFix = (toArrayCall, context) => {
49
+ if (context.sourceCode.getCommentsInside(toArrayCall).length > 0) {
50
+ return;
51
+ }
52
+
53
+ return fixer => removeMethodCall(fixer, toArrayCall, context);
54
+ };
55
+
56
+ const getRemoveToArraySuggestion = (toArrayCall, context, messageId, data) => {
57
+ const fix = getRemoveToArrayFix(toArrayCall, context);
58
+ if (!fix) {
59
+ return;
60
+ }
61
+
62
+ return {
63
+ messageId,
64
+ ...(data && {data}),
65
+ fix,
66
+ };
67
+ };
68
+
69
+ const getArrayFromProblem = (node, context) => {
70
+ if (
71
+ !isMethodCall(node, {
72
+ objects: ['Array', ...typedArray],
73
+ method: 'from',
74
+ minimumArguments: 1,
75
+ optionalCall: false,
76
+ optionalMember: false,
77
+ })
78
+ || !isToArrayCall(node.arguments[0])
79
+ ) {
80
+ return;
81
+ }
82
+
83
+ const toArrayCall = node.arguments[0];
84
+ const fix = getRemoveToArrayFix(toArrayCall, context);
85
+
86
+ return {
87
+ node: toArrayCall.callee.property,
88
+ messageId: MESSAGE_ID_ITERABLE_ACCEPTING,
89
+ data: {description: `${node.callee.object.name}.${node.callee.property.name}(…)`},
90
+ ...(fix && {fix}),
91
+ };
92
+ };
93
+
94
+ const getObjectFromEntriesProblem = (node, context) => {
95
+ if (
96
+ !isMethodCall(node, {
97
+ object: 'Object',
98
+ method: 'fromEntries',
99
+ minimumArguments: 1,
100
+ optionalCall: false,
101
+ optionalMember: false,
102
+ })
103
+ || !isToArrayCall(node.arguments[0])
104
+ ) {
105
+ return;
106
+ }
107
+
108
+ const toArrayCall = node.arguments[0];
109
+ const fix = getRemoveToArrayFix(toArrayCall, context);
110
+
111
+ return {
112
+ node: toArrayCall.callee.property,
113
+ messageId: MESSAGE_ID_ITERABLE_ACCEPTING,
114
+ data: {description: `${node.callee.object.name}.${node.callee.property.name}(…)`},
115
+ ...(fix && {fix}),
116
+ };
117
+ };
118
+
119
+ const getPromiseProblem = (node, context) => {
120
+ // Suggestion only — passing an iterator directly can change a sync throw
121
+ // into an async rejection when iteration fails.
122
+ if (
123
+ !isMethodCall(node, {
124
+ object: 'Promise',
125
+ methods: ['all', 'allSettled', 'any', 'race'],
126
+ argumentsLength: 1,
127
+ optionalCall: false,
128
+ optionalMember: false,
129
+ })
130
+ || !isToArrayCall(node.arguments[0])
131
+ ) {
132
+ return;
133
+ }
134
+
135
+ const toArrayCall = node.arguments[0];
136
+ const suggestion = getRemoveToArraySuggestion(toArrayCall, context, MESSAGE_ID_SUGGESTION_ITERABLE_ACCEPTING);
137
+
138
+ return {
139
+ node: toArrayCall.callee.property,
140
+ messageId: MESSAGE_ID_ITERABLE_ACCEPTING,
141
+ data: {description: `${node.callee.object.name}.${node.callee.property.name}(…)`},
142
+ ...(suggestion && {suggest: [suggestion]}),
143
+ };
144
+ };
145
+
146
+ const getIteratorMethodProblem = (node, context) => {
147
+ if (
148
+ !(
149
+ isMethodCall(node, {
150
+ methods: callbackOnlyIteratorMethods,
151
+ maximumArguments: 1,
152
+ optionalCall: false,
153
+ optionalMember: false,
154
+ })
155
+ || isMethodCall(node, {
156
+ method: reduceMethod,
157
+ argumentsLength: 2,
158
+ optionalCall: false,
159
+ optionalMember: false,
160
+ })
161
+ )
162
+ || !isToArrayCall(node.callee.object)
163
+ ) {
164
+ return;
165
+ }
166
+
167
+ // If the callback is a function/arrow with enough parameters to reference
168
+ // the `array` argument, `.toArray()` may be intentional.
169
+ const callback = node.arguments[0];
170
+ const method = node.callee.property.name;
171
+ const isReduceCall = method === reduceMethod;
172
+ const arrayParameterIndex = isReduceCall ? 3 : 2;
173
+ if (
174
+ callback
175
+ && (callback.type === 'ArrowFunctionExpression' || callback.type === 'FunctionExpression')
176
+ && (
177
+ callback.params.length > arrayParameterIndex
178
+ // A rest parameter can capture the trailing `array` argument too.
179
+ || callback.params.at(-1)?.type === 'RestElement'
180
+ )
181
+ ) {
182
+ return;
183
+ }
184
+
185
+ const toArrayCall = node.callee.object;
186
+ const suggestion = getRemoveToArraySuggestion(toArrayCall, context, MESSAGE_ID_SUGGESTION_ITERATOR_METHOD, {method});
187
+
188
+ return {
189
+ node: toArrayCall.callee.property,
190
+ messageId: MESSAGE_ID_ITERATOR_METHOD,
191
+ data: {method},
192
+ ...(suggestion && {suggest: [suggestion]}),
193
+ };
194
+ };
195
+
48
196
  /** @param {import('eslint').Rule.RuleContext} context */
49
197
  const create = context => {
50
198
  // Case 1: `new Set(iterator.toArray())`, `new Map(iterator.toArray())`, etc.
@@ -60,126 +208,22 @@ const create = context => {
60
208
  }
61
209
 
62
210
  const toArrayCall = node.arguments[0];
211
+ const fix = getRemoveToArrayFix(toArrayCall, context);
63
212
 
64
213
  return {
65
214
  node: toArrayCall.callee.property,
66
215
  messageId: MESSAGE_ID_ITERABLE_ACCEPTING,
67
216
  data: {description: `new ${node.callee.name}(…)`},
68
- fix: fixer => removeMethodCall(fixer, toArrayCall, context),
217
+ ...(fix && {fix}),
69
218
  };
70
219
  });
71
220
 
72
221
  // Case 2: Call expressions — static methods and iterator prototype methods.
73
- context.on('CallExpression', node => {
74
- // Case 2a: `Array.from(iterator.toArray())`, `TypedArray.from(…)`, `Object.fromEntries(…)`
75
- // Autofix — these methods materialize their first argument into an array/object
76
- // regardless of extra arguments (e.g. mapFn), so .toArray() is always unnecessary.
77
- if (
78
- (
79
- isMethodCall(node, {
80
- objects: ['Array', ...typedArray],
81
- method: 'from',
82
- minimumArguments: 1,
83
- optionalCall: false,
84
- optionalMember: false,
85
- })
86
- || isMethodCall(node, {
87
- object: 'Object',
88
- method: 'fromEntries',
89
- minimumArguments: 1,
90
- optionalCall: false,
91
- optionalMember: false,
92
- })
93
- )
94
- && isToArrayCall(node.arguments[0])
95
- ) {
96
- const toArrayCall = node.arguments[0];
97
-
98
- return {
99
- node: toArrayCall.callee.property,
100
- messageId: MESSAGE_ID_ITERABLE_ACCEPTING,
101
- data: {description: `${node.callee.object.name}.${node.callee.property.name}(…)`},
102
- fix: fixer => removeMethodCall(fixer, toArrayCall, context),
103
- };
104
- }
105
-
106
- // Case 2b: `Promise.all(iterator.toArray())`, etc.
107
- // Suggestion only — passing an iterator directly can change a sync throw
108
- // into an async rejection when iteration fails.
109
- if (
110
- isMethodCall(node, {
111
- object: 'Promise',
112
- methods: ['all', 'allSettled', 'any', 'race'],
113
- argumentsLength: 1,
114
- optionalCall: false,
115
- optionalMember: false,
116
- })
117
- && isToArrayCall(node.arguments[0])
118
- ) {
119
- const toArrayCall = node.arguments[0];
120
-
121
- return {
122
- node: toArrayCall.callee.property,
123
- messageId: MESSAGE_ID_ITERABLE_ACCEPTING,
124
- data: {description: `${node.callee.object.name}.${node.callee.property.name}(…)`},
125
- suggest: [
126
- {
127
- messageId: MESSAGE_ID_SUGGESTION_ITERABLE_ACCEPTING,
128
- fix: fixer => removeMethodCall(fixer, toArrayCall, context),
129
- },
130
- ],
131
- };
132
- }
133
-
134
- // Case 2c: `iterator.toArray().every(fn)`, `.find(fn)`, `.forEach(fn)`, `.some(fn)`, `.reduce(fn, init)`
135
- // Suggestion only — Array callbacks receive a 3rd `array` argument
136
- // (and `reduce` a 4th) that Iterator callbacks do not.
137
- if (
138
- (
139
- isMethodCall(node, {
140
- methods: callbackOnlyIteratorMethods,
141
- maximumArguments: 1,
142
- optionalCall: false,
143
- optionalMember: false,
144
- })
145
- || isMethodCall(node, {
146
- method: reduceMethod,
147
- argumentsLength: 2,
148
- optionalCall: false,
149
- optionalMember: false,
150
- })
151
- )
152
- && isToArrayCall(node.callee.object)
153
- ) {
154
- // If the callback is a function/arrow with enough parameters to reference
155
- // the `array` argument, `.toArray()` may be intentional.
156
- const callback = node.arguments[0];
157
- const isReduceCall = node.callee.property.name === reduceMethod;
158
- const arrayParameterIndex = isReduceCall ? 3 : 2;
159
- if (
160
- callback
161
- && (callback.type === 'ArrowFunctionExpression' || callback.type === 'FunctionExpression')
162
- && callback.params.length > arrayParameterIndex
163
- ) {
164
- return;
165
- }
166
-
167
- const callerObject = node.callee.object;
168
-
169
- return {
170
- node: callerObject.callee.property,
171
- messageId: MESSAGE_ID_ITERATOR_METHOD,
172
- data: {method: node.callee.property.name},
173
- suggest: [
174
- {
175
- messageId: MESSAGE_ID_SUGGESTION_ITERATOR_METHOD,
176
- data: {method: node.callee.property.name},
177
- fix: fixer => removeMethodCall(fixer, callerObject, context),
178
- },
179
- ],
180
- };
181
- }
182
- });
222
+ context.on('CallExpression', node =>
223
+ getArrayFromProblem(node, context)
224
+ ?? getObjectFromEntriesProblem(node, context)
225
+ ?? getPromiseProblem(node, context)
226
+ ?? getIteratorMethodProblem(node, context));
183
227
 
184
228
  // Case 3: `for (const x of iterator.toArray())`
185
229
  context.on('ForOfStatement', node => {
@@ -187,10 +231,12 @@ const create = context => {
187
231
  return;
188
232
  }
189
233
 
234
+ const fix = getRemoveToArrayFix(node.right, context);
235
+
190
236
  return {
191
237
  node: node.right.callee.property,
192
238
  messageId: MESSAGE_ID_FOR_OF,
193
- fix: fixer => removeMethodCall(fixer, node.right, context),
239
+ ...(fix && {fix}),
194
240
  };
195
241
  });
196
242
 
@@ -200,10 +246,12 @@ const create = context => {
200
246
  return;
201
247
  }
202
248
 
249
+ const fix = getRemoveToArrayFix(node.argument, context);
250
+
203
251
  return {
204
252
  node: node.argument.callee.property,
205
253
  messageId: MESSAGE_ID_YIELD_STAR,
206
- fix: fixer => removeMethodCall(fixer, node.argument, context),
254
+ ...(fix && {fix}),
207
255
  };
208
256
  });
209
257
 
@@ -223,10 +271,12 @@ const create = context => {
223
271
  return;
224
272
  }
225
273
 
274
+ const fix = getRemoveToArrayFix(node.argument, context);
275
+
226
276
  return {
227
277
  node: node.argument.callee.property,
228
278
  messageId: MESSAGE_ID_SPREAD,
229
- fix: fixer => removeMethodCall(fixer, node.argument, context),
279
+ ...(fix && {fix}),
230
280
  };
231
281
  });
232
282
  };