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.
- package/package.json +3 -1
- package/readme.md +12 -0
- package/rules/ast/is-empty-object-expression.js +2 -2
- package/rules/comment-content.js +136 -18
- package/rules/consistent-boolean-name.js +31 -4
- package/rules/consistent-conditional-object-spread.js +9 -3
- package/rules/consistent-tuple-labels.js +49 -0
- package/rules/custom-error-definition.js +26 -7
- package/rules/index.js +12 -0
- package/rules/no-chained-comparison.js +1 -1
- package/rules/no-computed-property-existence-check.js +6 -0
- package/rules/no-incorrect-template-string-interpolation.js +82 -30
- package/rules/no-instanceof-builtins.js +59 -13
- package/rules/no-invalid-well-known-symbol-methods.js +205 -0
- package/rules/no-late-current-target-access.js +9 -71
- package/rules/no-late-event-control.js +127 -0
- package/rules/no-nonstandard-builtin-properties.js +2 -0
- package/rules/no-object-methods-with-collections.js +4 -289
- package/rules/no-return-array-push.js +20 -5
- package/rules/no-top-level-side-effects.js +8 -1
- package/rules/no-unnecessary-global-this.js +36 -1
- package/rules/no-unreadable-object-destructuring.js +15 -0
- package/rules/no-unsafe-string-replacement.js +31 -0
- package/rules/no-unused-array-method-return.js +11 -2
- package/rules/no-useless-boolean-cast.js +58 -1
- package/rules/no-useless-concat.js +27 -3
- package/rules/no-useless-else.js +16 -2
- package/rules/no-useless-iterator-to-array.js +165 -115
- package/rules/prefer-abort-signal-timeout.js +395 -0
- package/rules/prefer-aggregate-error.js +478 -0
- package/rules/prefer-at.js +5 -6
- package/rules/prefer-code-point.js +58 -8
- package/rules/prefer-continue.js +16 -0
- package/rules/prefer-dom-node-replace-children.js +353 -0
- package/rules/prefer-error-is-error.js +181 -0
- package/rules/prefer-has-check.js +1 -4
- package/rules/prefer-hoisting-branch-code.js +9 -7
- package/rules/prefer-iterator-concat.js +21 -1
- package/rules/prefer-minimal-ternary.js +15 -11
- package/rules/prefer-modern-dom-apis.js +1 -45
- package/rules/prefer-observer-apis.js +498 -0
- package/rules/prefer-promise-try.js +238 -0
- package/rules/prefer-set-methods.js +337 -0
- package/rules/prefer-toggle-attribute.js +293 -0
- package/rules/prefer-url-search-parameters.js +411 -0
- package/rules/shared/late-event-handler.js +82 -0
- package/rules/shared/name-replacements.js +6 -0
- package/rules/utils/builtin-collection-type.js +267 -0
- package/rules/utils/index.js +6 -0
- package/rules/utils/is-boolean.js +3 -1
- package/rules/utils/is-event.js +127 -0
- package/rules/utils/should-report-replace-children-receiver.js +210 -0
- package/rules/utils/type-helpers.js +166 -8
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import {findVariable, getStaticValue} from '@eslint-community/eslint-utils';
|
|
2
|
+
import {
|
|
3
|
+
getStaticStringValue,
|
|
4
|
+
isCallExpression,
|
|
5
|
+
isMemberExpression,
|
|
6
|
+
isMethodCall,
|
|
7
|
+
} from './ast/index.js';
|
|
8
|
+
import {
|
|
9
|
+
isKnownNonDomNode,
|
|
10
|
+
isGlobalIdentifier,
|
|
11
|
+
isNodeValueNotDomNode,
|
|
12
|
+
isSameReference,
|
|
13
|
+
isValueNotUsable,
|
|
14
|
+
mayBeHtmlTemplateElement,
|
|
15
|
+
needsSemicolon,
|
|
16
|
+
shouldAddParenthesesToMemberExpressionObject,
|
|
17
|
+
shouldReportReplaceChildrenReceiver,
|
|
18
|
+
unwrapTypeScriptExpression,
|
|
19
|
+
wouldRemoveComments,
|
|
20
|
+
} from './utils/index.js';
|
|
21
|
+
|
|
22
|
+
const MESSAGE_ID = 'prefer-dom-node-replace-children';
|
|
23
|
+
const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
|
|
24
|
+
const globalObjectNames = new Set([
|
|
25
|
+
'frames',
|
|
26
|
+
'globalThis',
|
|
27
|
+
'parent',
|
|
28
|
+
'self',
|
|
29
|
+
'top',
|
|
30
|
+
'window',
|
|
31
|
+
]);
|
|
32
|
+
const messages = {
|
|
33
|
+
[MESSAGE_ID]: 'Prefer `{{replacement}}` over manually emptying DOM children.',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const getStaticString = node => getStaticStringValue(unwrapTypeScriptExpression(node));
|
|
37
|
+
|
|
38
|
+
const getStaticStringValueFromScope = (node, context) => {
|
|
39
|
+
node = unwrapTypeScriptExpression(node);
|
|
40
|
+
|
|
41
|
+
const string = getStaticStringValue(node);
|
|
42
|
+
if (string !== undefined) {
|
|
43
|
+
return string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const result = getStaticValue(node, context.sourceCode.getScope(node));
|
|
47
|
+
return typeof result?.value === 'string' ? result.value : undefined;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const getConstIdentifierInitializer = (node, context, visitedVariables) => {
|
|
51
|
+
if (node.type !== 'Identifier') {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const variable = findVariable(context.sourceCode.getScope(node), node);
|
|
56
|
+
if (
|
|
57
|
+
!variable
|
|
58
|
+
|| visitedVariables.has(variable)
|
|
59
|
+
|| variable.defs.length !== 1
|
|
60
|
+
) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const [definition] = variable.defs;
|
|
65
|
+
if (
|
|
66
|
+
definition.type !== 'Variable'
|
|
67
|
+
|| definition.parent.kind !== 'const'
|
|
68
|
+
|| definition.node.id !== definition.name
|
|
69
|
+
) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
visitedVariables.add(variable);
|
|
74
|
+
return definition.node.init;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const getStaticPropertyName = memberExpression => {
|
|
78
|
+
const {property} = memberExpression;
|
|
79
|
+
|
|
80
|
+
if (
|
|
81
|
+
!memberExpression.computed
|
|
82
|
+
&& property.type === 'Identifier'
|
|
83
|
+
) {
|
|
84
|
+
return property.name;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return getStaticString(property);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const isInnerHTMLMemberExpression = node =>
|
|
91
|
+
isMemberExpression(node)
|
|
92
|
+
&& getStaticPropertyName(node) === 'innerHTML';
|
|
93
|
+
|
|
94
|
+
const isEmptyString = node => getStaticString(node) === '';
|
|
95
|
+
|
|
96
|
+
const isStaticMethodCall = (node, method, options) =>
|
|
97
|
+
isCallExpression(node, {
|
|
98
|
+
...options,
|
|
99
|
+
optional: false,
|
|
100
|
+
})
|
|
101
|
+
&& isMemberExpression(node.callee, {optional: false})
|
|
102
|
+
&& getStaticPropertyName(node.callee) === method;
|
|
103
|
+
|
|
104
|
+
const isGlobalDocument = (node, context, visitedVariables = new Set()) => {
|
|
105
|
+
node = unwrapTypeScriptExpression(node);
|
|
106
|
+
|
|
107
|
+
const initializer = getConstIdentifierInitializer(node, context, visitedVariables);
|
|
108
|
+
if (initializer) {
|
|
109
|
+
return isGlobalDocument(initializer, context, visitedVariables);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (
|
|
113
|
+
node.type === 'Identifier'
|
|
114
|
+
&& node.name === 'document'
|
|
115
|
+
) {
|
|
116
|
+
return isGlobalIdentifier(node, context);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return isMemberExpression(node, {optional: false})
|
|
120
|
+
&& getStaticPropertyName(node) === 'document'
|
|
121
|
+
&& node.object.type === 'Identifier'
|
|
122
|
+
&& globalObjectNames.has(node.object.name)
|
|
123
|
+
&& isGlobalIdentifier(node.object, context);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const isUnknownOrHtmlNamespace = (node, context) => {
|
|
127
|
+
node = unwrapTypeScriptExpression(node);
|
|
128
|
+
|
|
129
|
+
const string = getStaticStringValue(node);
|
|
130
|
+
if (string !== undefined) {
|
|
131
|
+
return string === HTML_NAMESPACE;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const result = getStaticValue(node, context.sourceCode.getScope(node));
|
|
135
|
+
return !result || result.value === HTML_NAMESPACE;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const mayCreateHtmlTemplateElement = (node, context, visitedVariables = new Set()) => {
|
|
139
|
+
node = unwrapTypeScriptExpression(node);
|
|
140
|
+
|
|
141
|
+
const initializer = getConstIdentifierInitializer(node, context, visitedVariables);
|
|
142
|
+
if (initializer) {
|
|
143
|
+
return mayCreateHtmlTemplateElement(initializer, context, visitedVariables);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (
|
|
147
|
+
isStaticMethodCall(node, 'createElement', {
|
|
148
|
+
minimumArguments: 1,
|
|
149
|
+
maximumArguments: 2,
|
|
150
|
+
})
|
|
151
|
+
) {
|
|
152
|
+
return getStaticStringValueFromScope(node.arguments[0], context)?.toLowerCase() === 'template';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (
|
|
156
|
+
!isStaticMethodCall(node, 'createElementNS', {
|
|
157
|
+
minimumArguments: 2,
|
|
158
|
+
maximumArguments: 3,
|
|
159
|
+
})
|
|
160
|
+
|| getStaticStringValueFromScope(node.arguments[1], context)?.toLowerCase() !== 'template'
|
|
161
|
+
) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return isUnknownOrHtmlNamespace(node.arguments[0], context);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const getOnlyBodyStatement = node => {
|
|
169
|
+
if (node.body.type !== 'BlockStatement') {
|
|
170
|
+
return node.body;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return node.body.body.length === 1
|
|
174
|
+
? node.body.body[0]
|
|
175
|
+
: undefined;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const getChildNodeMemberExpression = node => {
|
|
179
|
+
if (
|
|
180
|
+
isMemberExpression(node, {
|
|
181
|
+
properties: ['firstChild', 'lastChild'],
|
|
182
|
+
optional: false,
|
|
183
|
+
})
|
|
184
|
+
) {
|
|
185
|
+
return node;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const containsChainExpression = (node, sourceCode) => {
|
|
190
|
+
if (node.type === 'ChainExpression') {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const keys = sourceCode.visitorKeys[node.type] ?? [];
|
|
195
|
+
for (const key of keys) {
|
|
196
|
+
const child = node[key];
|
|
197
|
+
if (Array.isArray(child)) {
|
|
198
|
+
for (const childNode of child) {
|
|
199
|
+
if (childNode && containsChainExpression(childNode, sourceCode)) {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (child && containsChainExpression(child, sourceCode)) {
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return false;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const getParentNodeText = (parentNode, context) => {
|
|
216
|
+
const {sourceCode} = context;
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
parentNode.type !== 'Super'
|
|
220
|
+
&& shouldAddParenthesesToMemberExpressionObject(parentNode, context)
|
|
221
|
+
)
|
|
222
|
+
? `(${sourceCode.getText(parentNode)})`
|
|
223
|
+
: sourceCode.getText(parentNode);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const getReplaceChildrenStatement = (node, parentNode, context) => {
|
|
227
|
+
const parentNodeText = getParentNodeText(parentNode, context);
|
|
228
|
+
return `${needsSemicolon(context.sourceCode.getTokenBefore(node), context, parentNodeText) ? ';' : ''}${parentNodeText}.replaceChildren();`;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const shouldSkipParentNode = (parentNode, context, options) => {
|
|
232
|
+
const {sourceCode} = context;
|
|
233
|
+
|
|
234
|
+
return isNodeValueNotDomNode(parentNode)
|
|
235
|
+
|| isKnownNonDomNode(parentNode, context, {
|
|
236
|
+
allowNullishInMixedUnion: true,
|
|
237
|
+
treatMixedUnionAsNonTarget: true,
|
|
238
|
+
})
|
|
239
|
+
|| containsChainExpression(parentNode, sourceCode)
|
|
240
|
+
|| !shouldReportReplaceChildrenReceiver(context, parentNode, options);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const shouldSkipInnerHTMLParentNode = (parentNode, context) =>
|
|
244
|
+
shouldSkipParentNode(parentNode, context, {checkInnerHTML: true});
|
|
245
|
+
|
|
246
|
+
const getInnerHTMLProblem = (context, node) => {
|
|
247
|
+
if (
|
|
248
|
+
node.operator !== '='
|
|
249
|
+
|| !isInnerHTMLMemberExpression(node.left)
|
|
250
|
+
|| !isEmptyString(node.right)
|
|
251
|
+
) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const parentNode = node.left.object;
|
|
256
|
+
if (
|
|
257
|
+
shouldSkipInnerHTMLParentNode(parentNode, context)
|
|
258
|
+
|| isGlobalDocument(parentNode, context)
|
|
259
|
+
|| mayCreateHtmlTemplateElement(parentNode, context)
|
|
260
|
+
|| mayBeHtmlTemplateElement(context, parentNode)
|
|
261
|
+
) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const replacement = getReplaceChildrenStatement(node.parent, parentNode, context);
|
|
266
|
+
const fix = (
|
|
267
|
+
node.parent.type === 'ExpressionStatement'
|
|
268
|
+
&& isValueNotUsable(node)
|
|
269
|
+
&& !wouldRemoveComments(context, node.parent, [parentNode])
|
|
270
|
+
)
|
|
271
|
+
? fixer => fixer.replaceText(node.parent, replacement)
|
|
272
|
+
: undefined;
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
node: node.left.property,
|
|
276
|
+
messageId: MESSAGE_ID,
|
|
277
|
+
data: {
|
|
278
|
+
replacement: `${getParentNodeText(parentNode, context)}.replaceChildren()`,
|
|
279
|
+
},
|
|
280
|
+
fix,
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const getRemoveChildLoopProblem = (context, node) => {
|
|
285
|
+
const childNode = getChildNodeMemberExpression(node.test);
|
|
286
|
+
if (!childNode) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const bodyStatement = getOnlyBodyStatement(node);
|
|
291
|
+
if (bodyStatement?.type !== 'ExpressionStatement') {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const {expression} = bodyStatement;
|
|
296
|
+
if (
|
|
297
|
+
!isMethodCall(expression, {
|
|
298
|
+
method: 'removeChild',
|
|
299
|
+
argumentsLength: 1,
|
|
300
|
+
optionalCall: false,
|
|
301
|
+
optionalMember: false,
|
|
302
|
+
})
|
|
303
|
+
|| !isSameReference(childNode.object, expression.callee.object)
|
|
304
|
+
|| !isSameReference(childNode, expression.arguments[0])
|
|
305
|
+
) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const parentNode = childNode.object;
|
|
310
|
+
if (shouldSkipParentNode(parentNode, context)) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const replacement = getReplaceChildrenStatement(node, parentNode, context);
|
|
315
|
+
const fix = wouldRemoveComments(context, node, [parentNode])
|
|
316
|
+
? undefined
|
|
317
|
+
: fixer => fixer.replaceText(node, replacement);
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
node,
|
|
321
|
+
messageId: MESSAGE_ID,
|
|
322
|
+
data: {
|
|
323
|
+
replacement: `${getParentNodeText(parentNode, context)}.replaceChildren()`,
|
|
324
|
+
},
|
|
325
|
+
fix,
|
|
326
|
+
};
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
330
|
+
const create = context => {
|
|
331
|
+
context.on('AssignmentExpression', node => getInnerHTMLProblem(context, node));
|
|
332
|
+
context.on('WhileStatement', node => getRemoveChildLoopProblem(context, node));
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
336
|
+
const config = {
|
|
337
|
+
create,
|
|
338
|
+
meta: {
|
|
339
|
+
type: 'suggestion',
|
|
340
|
+
docs: {
|
|
341
|
+
description: 'Prefer `.replaceChildren()` when emptying DOM children.',
|
|
342
|
+
recommended: 'unopinionated',
|
|
343
|
+
},
|
|
344
|
+
fixable: 'code',
|
|
345
|
+
schema: [],
|
|
346
|
+
messages,
|
|
347
|
+
languages: [
|
|
348
|
+
'js/js',
|
|
349
|
+
],
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
export default config;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import {isMemberExpression, isMethodCall} from './ast/index.js';
|
|
2
|
+
import {isTypeImportSpecifier} from './utils/index.js';
|
|
3
|
+
|
|
4
|
+
const MESSAGE_ID = 'prefer-error-is-error';
|
|
5
|
+
const messages = {
|
|
6
|
+
[MESSAGE_ID]: 'Prefer `Error.isError(…)`.',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const isTypeImport = definition =>
|
|
10
|
+
definition.type === 'ImportBinding'
|
|
11
|
+
&& isTypeImportSpecifier(definition.node);
|
|
12
|
+
|
|
13
|
+
const isTypeOnlyDefinition = definition =>
|
|
14
|
+
definition.type === 'Type'
|
|
15
|
+
|| isTypeImport(definition);
|
|
16
|
+
|
|
17
|
+
function isValueShadowed(node, name, context) {
|
|
18
|
+
let scope = context.sourceCode.getScope(node);
|
|
19
|
+
|
|
20
|
+
while (scope) {
|
|
21
|
+
const variable = scope.set.get(name);
|
|
22
|
+
|
|
23
|
+
if (variable?.defs.some(definition => !isTypeOnlyDefinition(definition))) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
scope = scope.upper;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const isGlobalError = (node, context) =>
|
|
34
|
+
node.type === 'Identifier'
|
|
35
|
+
&& node.name === 'Error'
|
|
36
|
+
&& !isValueShadowed(node, 'Error', context);
|
|
37
|
+
|
|
38
|
+
const isGlobalObject = (node, context) =>
|
|
39
|
+
node.type === 'Identifier'
|
|
40
|
+
&& node.name === 'Object'
|
|
41
|
+
&& !isValueShadowed(node, 'Object', context);
|
|
42
|
+
|
|
43
|
+
const isErrorTagLiteral = node =>
|
|
44
|
+
node.type === 'Literal'
|
|
45
|
+
&& node.value === '[object Error]';
|
|
46
|
+
|
|
47
|
+
const supportedComparisonOperators = new Set([
|
|
48
|
+
'===',
|
|
49
|
+
'!==',
|
|
50
|
+
'==',
|
|
51
|
+
'!=',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
const isObjectPrototypeToString = (node, context) =>
|
|
55
|
+
isMemberExpression(node, {property: 'toString', optional: false})
|
|
56
|
+
&& isMemberExpression(node.object, {property: 'prototype', optional: false})
|
|
57
|
+
&& isGlobalObject(node.object.object, context);
|
|
58
|
+
|
|
59
|
+
const isEmptyObjectToString = node =>
|
|
60
|
+
isMemberExpression(node, {property: 'toString', optional: false})
|
|
61
|
+
&& node.object.type === 'ObjectExpression'
|
|
62
|
+
&& node.object.properties.length === 0;
|
|
63
|
+
|
|
64
|
+
const getToStringCallArgument = (node, context) => {
|
|
65
|
+
if (
|
|
66
|
+
!isMethodCall(node, {
|
|
67
|
+
method: 'call',
|
|
68
|
+
argumentsLength: 1,
|
|
69
|
+
optionalCall: false,
|
|
70
|
+
optionalMember: false,
|
|
71
|
+
})
|
|
72
|
+
) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const method = node.callee.object;
|
|
77
|
+
if (
|
|
78
|
+
isObjectPrototypeToString(method, context)
|
|
79
|
+
|| isEmptyObjectToString(method)
|
|
80
|
+
) {
|
|
81
|
+
return node.arguments[0];
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const getErrorTagComparison = (node, context) => {
|
|
86
|
+
if (!supportedComparisonOperators.has(node.operator)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (isErrorTagLiteral(node.right)) {
|
|
91
|
+
const argument = getToStringCallArgument(node.left, context);
|
|
92
|
+
if (argument) {
|
|
93
|
+
return argument;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (isErrorTagLiteral(node.left)) {
|
|
98
|
+
const argument = getToStringCallArgument(node.right, context);
|
|
99
|
+
if (argument) {
|
|
100
|
+
return argument;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const hasComments = (node, sourceCode) =>
|
|
106
|
+
sourceCode.getCommentsInside(node).length > 0;
|
|
107
|
+
|
|
108
|
+
const getArgumentText = (node, sourceCode) => {
|
|
109
|
+
const text = sourceCode.getText(node);
|
|
110
|
+
return node.type === 'SequenceExpression' ? `(${text})` : text;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const createFix = ({node, argument, negate}, context) =>
|
|
114
|
+
fixer => fixer.replaceText(
|
|
115
|
+
node,
|
|
116
|
+
`${negate ? '!' : ''}Error.isError(${getArgumentText(argument, context.sourceCode)})`,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
/** @param {import('eslint').Rule.RuleContext} context */
|
|
120
|
+
const create = context => {
|
|
121
|
+
context.on('BinaryExpression', node => {
|
|
122
|
+
if (hasComments(node, context.sourceCode)) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (
|
|
127
|
+
node.operator === 'instanceof'
|
|
128
|
+
&& isGlobalError(node.right, context)
|
|
129
|
+
) {
|
|
130
|
+
return {
|
|
131
|
+
node,
|
|
132
|
+
messageId: MESSAGE_ID,
|
|
133
|
+
fix: createFix({
|
|
134
|
+
node,
|
|
135
|
+
argument: node.left,
|
|
136
|
+
negate: false,
|
|
137
|
+
}, context),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const argument = getErrorTagComparison(node, context);
|
|
142
|
+
if (
|
|
143
|
+
!argument
|
|
144
|
+
|| isValueShadowed(node, 'Error', context)
|
|
145
|
+
) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
node,
|
|
151
|
+
messageId: MESSAGE_ID,
|
|
152
|
+
fix: createFix({
|
|
153
|
+
node,
|
|
154
|
+
argument,
|
|
155
|
+
negate: node.operator === '!==' || node.operator === '!=',
|
|
156
|
+
}, context),
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
162
|
+
const config = {
|
|
163
|
+
create,
|
|
164
|
+
meta: {
|
|
165
|
+
type: 'suggestion',
|
|
166
|
+
docs: {
|
|
167
|
+
description: 'Prefer `Error.isError()` when checking for errors.',
|
|
168
|
+
// eslint-disable-next-line no-warning-comments
|
|
169
|
+
// TODO: Enable in the `recommended` config when `Error.isError()` is Baseline and the project targets Node.js >=24.
|
|
170
|
+
recommended: false,
|
|
171
|
+
},
|
|
172
|
+
fixable: 'code',
|
|
173
|
+
schema: [],
|
|
174
|
+
messages,
|
|
175
|
+
languages: [
|
|
176
|
+
'js/js',
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export default config;
|
|
@@ -39,10 +39,7 @@ const definitelyTruthyBuiltinTypeNames = new Set([
|
|
|
39
39
|
'ReadonlyArray',
|
|
40
40
|
]);
|
|
41
41
|
|
|
42
|
-
const constructibleCollectionTypeNames =
|
|
43
|
-
...mapConstructorTypeNames,
|
|
44
|
-
...nullSentinelTypeNames,
|
|
45
|
-
]);
|
|
42
|
+
const constructibleCollectionTypeNames = mapConstructorTypeNames.union(nullSentinelTypeNames);
|
|
46
43
|
|
|
47
44
|
const typeReferenceDefinitionTypes = new Set([
|
|
48
45
|
'ClassName',
|
|
@@ -29,11 +29,9 @@ const isElseIfStatement = node =>
|
|
|
29
29
|
node.parent.type === 'IfStatement'
|
|
30
30
|
&& node.parent.alternate === node;
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|| hasSideEffect(node, sourceCode)
|
|
36
|
-
) {
|
|
32
|
+
// `hasSideEffect` does not treat a tagged template as a call, so check for one separately.
|
|
33
|
+
function containsTaggedTemplate(node, sourceCode) {
|
|
34
|
+
if (node.type === 'TaggedTemplateExpression') {
|
|
37
35
|
return true;
|
|
38
36
|
}
|
|
39
37
|
|
|
@@ -42,7 +40,7 @@ function hasSideEffectOrTaggedTemplate(node, sourceCode) {
|
|
|
42
40
|
const child = node[key];
|
|
43
41
|
if (Array.isArray(child)) {
|
|
44
42
|
for (const childNode of child) {
|
|
45
|
-
if (childNode &&
|
|
43
|
+
if (childNode && containsTaggedTemplate(childNode, sourceCode)) {
|
|
46
44
|
return true;
|
|
47
45
|
}
|
|
48
46
|
}
|
|
@@ -50,7 +48,7 @@ function hasSideEffectOrTaggedTemplate(node, sourceCode) {
|
|
|
50
48
|
continue;
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
if (child &&
|
|
51
|
+
if (child && containsTaggedTemplate(child, sourceCode)) {
|
|
54
52
|
return true;
|
|
55
53
|
}
|
|
56
54
|
}
|
|
@@ -58,6 +56,10 @@ function hasSideEffectOrTaggedTemplate(node, sourceCode) {
|
|
|
58
56
|
return false;
|
|
59
57
|
}
|
|
60
58
|
|
|
59
|
+
const hasSideEffectOrTaggedTemplate = (node, sourceCode) =>
|
|
60
|
+
hasSideEffect(node, sourceCode)
|
|
61
|
+
|| containsTaggedTemplate(node, sourceCode);
|
|
62
|
+
|
|
61
63
|
// Tokens of a statement, ignoring a trailing semicolon so ASI differences don't matter.
|
|
62
64
|
const getStatementTokens = (node, sourceCode) => {
|
|
63
65
|
const tokens = sourceCode.getTokens(node);
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import typedArray from './shared/typed-array.js';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getParenthesizedText,
|
|
4
|
+
isBuiltinSet,
|
|
5
|
+
isGlobalIdentifier,
|
|
6
|
+
} from './utils/index.js';
|
|
3
7
|
import {
|
|
4
8
|
isMethodCall,
|
|
5
9
|
isNewExpression,
|
|
@@ -89,6 +93,21 @@ const isToArrayCall = node => isMethodCall(node, {
|
|
|
89
93
|
const hasToArraySpreadElement = arrayExpression =>
|
|
90
94
|
arrayExpression.elements.some(element => isToArrayCall(element.argument));
|
|
91
95
|
|
|
96
|
+
const isKnownSetUnionCase = (arrayExpression, context) => {
|
|
97
|
+
const {parent} = arrayExpression;
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
isNewExpression(parent, {
|
|
101
|
+
name: 'Set',
|
|
102
|
+
argumentsLength: 1,
|
|
103
|
+
})
|
|
104
|
+
&& parent.arguments[0] === arrayExpression
|
|
105
|
+
&& isGlobalIdentifier(parent.callee, context)
|
|
106
|
+
&& context.sourceCode.getCommentsInside(parent).length === 0
|
|
107
|
+
&& arrayExpression.elements.every(element => isBuiltinSet(element.argument, context))
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
92
111
|
const isSuggestionOnlyParent = parent =>
|
|
93
112
|
(parent.type === 'ForOfStatement' || (parent.type === 'YieldExpression' && parent.delegate))
|
|
94
113
|
|| (
|
|
@@ -119,6 +138,7 @@ const create = context => {
|
|
|
119
138
|
!isSpreadArray(node)
|
|
120
139
|
|| !isInIterableAcceptingParent(node)
|
|
121
140
|
|| hasToArraySpreadElement(node)
|
|
141
|
+
|| isKnownSetUnionCase(node, context)
|
|
122
142
|
) {
|
|
123
143
|
return;
|
|
124
144
|
}
|
|
@@ -89,11 +89,11 @@ function hasSameStaticPropertyWithDifferentObject(left, right, context) {
|
|
|
89
89
|
&& !isSameSourceText(left.object, right.object, sourceCode);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
function hasMinimalCalleeDifference(left, right, context, checkComputedMemberAccess) {
|
|
93
|
-
//
|
|
94
|
-
return isDifferentIdentifier(left, right)
|
|
92
|
+
function hasMinimalCalleeDifference(left, right, context, {checkVaryingCallee, checkComputedMemberAccess}) {
|
|
93
|
+
// All callee-varying call cases push the ternary in front of the call (`(test ? a : b)()`), hiding the call site, so they are opt-in. `checkVaryingCallee` covers plain identifier and dot access; `checkComputedMemberAccess` additionally produces computed member access (`obj[test ? 'a' : 'b'](…)`).
|
|
94
|
+
return (checkVaryingCallee && isDifferentIdentifier(left, right))
|
|
95
95
|
|| (checkComputedMemberAccess && hasSameObjectWithDifferentStaticProperty(left, right, context))
|
|
96
|
-
|| hasSameStaticPropertyWithDifferentObject(left, right, context);
|
|
96
|
+
|| (checkVaryingCallee && hasSameStaticPropertyWithDifferentObject(left, right, context));
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
function hasOneMinimalItemDifference(leftItems, rightItems, sourceCode) {
|
|
@@ -125,7 +125,7 @@ function hasSameItems(leftItems, rightItems, sourceCode) {
|
|
|
125
125
|
&& leftItems.every((leftItem, index) => isSameSourceText(leftItem, rightItems[index], sourceCode));
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
function isMinimalCallExpression(left, right, context,
|
|
128
|
+
function isMinimalCallExpression(left, right, context, options) {
|
|
129
129
|
const {sourceCode} = context;
|
|
130
130
|
|
|
131
131
|
if (
|
|
@@ -148,7 +148,7 @@ function isMinimalCallExpression(left, right, context, checkComputedMemberAccess
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
return hasSameItems(left.arguments, right.arguments, sourceCode)
|
|
151
|
-
&& hasMinimalCalleeDifference(left.callee, right.callee, context,
|
|
151
|
+
&& hasMinimalCalleeDifference(left.callee, right.callee, context, options);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
function isPrivateBrandCheck(node) {
|
|
@@ -205,7 +205,7 @@ function isMinimalMemberExpression(left, right, context) {
|
|
|
205
205
|
|| hasSameObjectWithDifferentDynamicKey(left, right, context);
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
function isMinimalTernary(consequent, alternate, context,
|
|
208
|
+
function isMinimalTernary(consequent, alternate, context, options) {
|
|
209
209
|
if (
|
|
210
210
|
consequent.type !== alternate.type
|
|
211
211
|
|| isIgnoredExpression(consequent)
|
|
@@ -214,17 +214,17 @@ function isMinimalTernary(consequent, alternate, context, checkComputedMemberAcc
|
|
|
214
214
|
return false;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
return isMinimalCallExpression(consequent, alternate, context,
|
|
217
|
+
return isMinimalCallExpression(consequent, alternate, context, options)
|
|
218
218
|
|| isMinimalBinaryExpression(consequent, alternate, context)
|
|
219
219
|
|| isMinimalMemberExpression(consequent, alternate, context);
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
/** @param {import('eslint').Rule.RuleContext} context */
|
|
223
223
|
const create = context => {
|
|
224
|
-
const
|
|
224
|
+
const options = context.options[0];
|
|
225
225
|
|
|
226
226
|
context.on('ConditionalExpression', node => {
|
|
227
|
-
if (!isMinimalTernary(node.consequent, node.alternate, context,
|
|
227
|
+
if (!isMinimalTernary(node.consequent, node.alternate, context, options)) {
|
|
228
228
|
return;
|
|
229
229
|
}
|
|
230
230
|
|
|
@@ -249,6 +249,10 @@ const config = {
|
|
|
249
249
|
type: 'object',
|
|
250
250
|
additionalProperties: false,
|
|
251
251
|
properties: {
|
|
252
|
+
checkVaryingCallee: {
|
|
253
|
+
type: 'boolean',
|
|
254
|
+
description: 'Also report call ternaries that differ only by the callee, whose minimization moves the ternary in front of the call.',
|
|
255
|
+
},
|
|
252
256
|
checkComputedMemberAccess: {
|
|
253
257
|
type: 'boolean',
|
|
254
258
|
description: 'Also report method-call ternaries that differ only by the method name, whose minimization requires computed member access.',
|
|
@@ -256,7 +260,7 @@ const config = {
|
|
|
256
260
|
},
|
|
257
261
|
},
|
|
258
262
|
],
|
|
259
|
-
defaultOptions: [{checkComputedMemberAccess: false}],
|
|
263
|
+
defaultOptions: [{checkVaryingCallee: false, checkComputedMemberAccess: false}],
|
|
260
264
|
messages,
|
|
261
265
|
languages: [
|
|
262
266
|
'js/js',
|