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
@@ -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 = new Set([
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
- function hasSideEffectOrTaggedTemplate(node, sourceCode) {
33
- if (
34
- node.type === 'TaggedTemplateExpression'
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 && hasSideEffectOrTaggedTemplate(childNode, sourceCode)) {
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 && hasSideEffectOrTaggedTemplate(child, sourceCode)) {
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 {getParenthesizedText} from './utils/index.js';
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
- // Only `hasSameObjectWithDifferentStaticProperty` is opt-in: it produces computed member access (`obj[test ? 'a' : 'b'](…)`). The other branches keep plain identifier or dot access.
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, checkComputedMemberAccess) {
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, checkComputedMemberAccess);
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, checkComputedMemberAccess) {
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, checkComputedMemberAccess)
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 {checkComputedMemberAccess} = context.options[0];
224
+ const options = context.options[0];
225
225
 
226
226
  context.on('ConditionalExpression', node => {
227
- if (!isMinimalTernary(node.consequent, node.alternate, context, checkComputedMemberAccess)) {
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',