eslint 9.26.0 → 9.28.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 (41) hide show
  1. package/README.md +7 -2
  2. package/bin/eslint.js +7 -11
  3. package/conf/rule-type-list.json +2 -1
  4. package/lib/cli-engine/cli-engine.js +7 -7
  5. package/lib/cli.js +19 -16
  6. package/lib/config/config-loader.js +42 -39
  7. package/lib/config/config.js +362 -16
  8. package/lib/eslint/eslint-helpers.js +3 -1
  9. package/lib/eslint/eslint.js +31 -13
  10. package/lib/eslint/legacy-eslint.js +6 -6
  11. package/lib/languages/js/source-code/source-code.js +40 -6
  12. package/lib/linter/apply-disable-directives.js +1 -1
  13. package/lib/linter/file-context.js +11 -0
  14. package/lib/linter/linter.js +102 -140
  15. package/lib/linter/report-translator.js +2 -1
  16. package/lib/linter/{node-event-generator.js → source-code-traverser.js} +143 -87
  17. package/lib/options.js +7 -0
  18. package/lib/rule-tester/rule-tester.js +3 -3
  19. package/lib/rules/func-style.js +57 -7
  20. package/lib/rules/index.js +1 -0
  21. package/lib/rules/max-params.js +32 -7
  22. package/lib/rules/no-array-constructor.js +51 -1
  23. package/lib/rules/no-implicit-globals.js +31 -15
  24. package/lib/rules/no-magic-numbers.js +98 -5
  25. package/lib/rules/no-shadow.js +262 -6
  26. package/lib/rules/no-unassigned-vars.js +80 -0
  27. package/lib/rules/no-use-before-define.js +97 -1
  28. package/lib/rules/no-useless-escape.js +24 -2
  29. package/lib/rules/prefer-arrow-callback.js +9 -0
  30. package/lib/rules/prefer-named-capture-group.js +7 -1
  31. package/lib/services/processor-service.js +1 -1
  32. package/lib/services/suppressions-service.js +5 -3
  33. package/lib/services/warning-service.js +85 -0
  34. package/lib/shared/flags.js +1 -0
  35. package/lib/types/index.d.ts +132 -9
  36. package/lib/types/rules.d.ts +66 -3
  37. package/package.json +11 -11
  38. package/lib/config/flat-config-helpers.js +0 -128
  39. package/lib/config/rule-validator.js +0 -199
  40. package/lib/mcp/mcp-server.js +0 -66
  41. package/lib/shared/types.js +0 -229
@@ -1,6 +1,6 @@
1
1
  /**
2
- * @fileoverview The event generator for AST nodes.
3
- * @author Toru Nagashima
2
+ * @fileoverview Traverser for SourceCode objects.
3
+ * @author Nicholas C. Zakas
4
4
  */
5
5
 
6
6
  "use strict";
@@ -10,6 +10,7 @@
10
10
  //------------------------------------------------------------------------------
11
11
 
12
12
  const { parse, matches } = require("./esquery");
13
+ const vk = require("eslint-visitor-keys");
13
14
 
14
15
  //-----------------------------------------------------------------------------
15
16
  // Typedefs
@@ -17,12 +18,16 @@ const { parse, matches } = require("./esquery");
17
18
 
18
19
  /**
19
20
  * @import { ESQueryParsedSelector } from "./esquery.js";
21
+ * @import { Language, SourceCode } from "@eslint/core";
20
22
  */
21
23
 
22
24
  //-----------------------------------------------------------------------------
23
25
  // Helpers
24
26
  //-----------------------------------------------------------------------------
25
27
 
28
+ const STEP_KIND_VISIT = 1;
29
+ const STEP_KIND_CALL = 2;
30
+
26
31
  /**
27
32
  * Compares two ESQuery selectors by specificity.
28
33
  * @param {ESQueryParsedSelector} a The first selector to compare.
@@ -33,69 +38,10 @@ function compareSpecificity(a, b) {
33
38
  return a.compare(b);
34
39
  }
35
40
 
36
- //------------------------------------------------------------------------------
37
- // Public Interface
38
- //------------------------------------------------------------------------------
39
-
40
41
  /**
41
- * The event generator for AST nodes.
42
- * This implements below interface.
43
- *
44
- * ```ts
45
- * interface EventGenerator {
46
- * emitter: SafeEmitter;
47
- * enterNode(node: ASTNode): void;
48
- * leaveNode(node: ASTNode): void;
49
- * }
50
- * ```
42
+ * Helper to wrap ESQuery operations.
51
43
  */
52
- class NodeEventGenerator {
53
- /**
54
- * The emitter to use during traversal.
55
- * @type {SafeEmitter}
56
- */
57
- emitter;
58
-
59
- /**
60
- * The options for `esquery` to use during matching.
61
- * @type {ESQueryOptions}
62
- */
63
- esqueryOptions;
64
-
65
- /**
66
- * The ancestry of the currently visited node.
67
- * @type {ASTNode[]}
68
- */
69
- currentAncestry = [];
70
-
71
- /**
72
- * A map of node type to selectors targeting that node type on the
73
- * enter phase of traversal.
74
- * @type {Map<string, ESQueryParsedSelector[]>}
75
- */
76
- enterSelectorsByNodeType = new Map();
77
-
78
- /**
79
- * A map of node type to selectors targeting that node type on the
80
- * exit phase of traversal.
81
- * @type {Map<string, ESQueryParsedSelector[]>}
82
- */
83
- exitSelectorsByNodeType = new Map();
84
-
85
- /**
86
- * An array of selectors that match any node type on the
87
- * enter phase of traversal.
88
- * @type {ESQueryParsedSelector[]}
89
- */
90
- anyTypeEnterSelectors = [];
91
-
92
- /**
93
- * An array of selectors that match any node type on the
94
- * exit phase of traversal.
95
- * @type {ESQueryParsedSelector[]}
96
- */
97
- anyTypeExitSelectors = [];
98
-
44
+ class ESQueryHelper {
99
45
  /**
100
46
  * @param {SafeEmitter} emitter
101
47
  * An SafeEmitter which is the destination of events. This emitter must already
@@ -105,9 +51,46 @@ class NodeEventGenerator {
105
51
  * @returns {NodeEventGenerator} new instance
106
52
  */
107
53
  constructor(emitter, esqueryOptions) {
54
+ /**
55
+ * The emitter to use during traversal.
56
+ * @type {SafeEmitter}
57
+ */
108
58
  this.emitter = emitter;
59
+
60
+ /**
61
+ * The options for `esquery` to use during matching.
62
+ * @type {ESQueryOptions}
63
+ */
109
64
  this.esqueryOptions = esqueryOptions;
110
65
 
66
+ /**
67
+ * A map of node type to selectors targeting that node type on the
68
+ * enter phase of traversal.
69
+ * @type {Map<string, ESQueryParsedSelector[]>}
70
+ */
71
+ this.enterSelectorsByNodeType = new Map();
72
+
73
+ /**
74
+ * A map of node type to selectors targeting that node type on the
75
+ * exit phase of traversal.
76
+ * @type {Map<string, ESQueryParsedSelector[]>}
77
+ */
78
+ this.exitSelectorsByNodeType = new Map();
79
+
80
+ /**
81
+ * An array of selectors that match any node type on the
82
+ * enter phase of traversal.
83
+ * @type {ESQueryParsedSelector[]}
84
+ */
85
+ this.anyTypeEnterSelectors = [];
86
+
87
+ /**
88
+ * An array of selectors that match any node type on the
89
+ * exit phase of traversal.
90
+ * @type {ESQueryParsedSelector[]}
91
+ */
92
+ this.anyTypeExitSelectors = [];
93
+
111
94
  emitter.eventNames().forEach(rawSelector => {
112
95
  const selector = parse(rawSelector);
113
96
 
@@ -156,18 +139,12 @@ class NodeEventGenerator {
156
139
  /**
157
140
  * Checks a selector against a node, and emits it if it matches
158
141
  * @param {ASTNode} node The node to check
142
+ * @param {ASTNode[]} ancestry The ancestry of the node being checked.
159
143
  * @param {ESQueryParsedSelector} selector An AST selector descriptor
160
144
  * @returns {void}
161
145
  */
162
- applySelector(node, selector) {
163
- if (
164
- matches(
165
- node,
166
- selector.root,
167
- this.currentAncestry,
168
- this.esqueryOptions,
169
- )
170
- ) {
146
+ #applySelector(node, ancestry, selector) {
147
+ if (matches(node, selector.root, ancestry, this.esqueryOptions)) {
171
148
  this.emitter.emit(selector.source, node);
172
149
  }
173
150
  }
@@ -175,10 +152,11 @@ class NodeEventGenerator {
175
152
  /**
176
153
  * Applies all appropriate selectors to a node, in specificity order
177
154
  * @param {ASTNode} node The node to check
155
+ * @param {ASTNode[]} ancestry The ancestry of the node being checked.
178
156
  * @param {boolean} isExit `false` if the node is currently being entered, `true` if it's currently being exited
179
157
  * @returns {void}
180
158
  */
181
- applySelectors(node, isExit) {
159
+ applySelectors(node, ancestry, isExit) {
182
160
  const nodeTypeKey = this.esqueryOptions?.nodeTypeKey || "type";
183
161
 
184
162
  /*
@@ -218,39 +196,117 @@ class NodeEventGenerator {
218
196
  selectorsByNodeType[selectorsByNodeTypeIndex],
219
197
  ) < 0)
220
198
  ) {
221
- this.applySelector(
199
+ this.#applySelector(
222
200
  node,
201
+ ancestry,
223
202
  anyTypeSelectors[anyTypeSelectorsIndex++],
224
203
  );
225
204
  } else {
226
205
  // otherwise apply the node type selector
227
- this.applySelector(
206
+ this.#applySelector(
228
207
  node,
208
+ ancestry,
229
209
  selectorsByNodeType[selectorsByNodeTypeIndex++],
230
210
  );
231
211
  }
232
212
  }
233
213
  }
214
+ }
215
+
216
+ //------------------------------------------------------------------------------
217
+ // Public Interface
218
+ //------------------------------------------------------------------------------
234
219
 
220
+ /**
221
+ * Traverses source code and ensures that visitor methods are called when
222
+ * entering and leaving each node.
223
+ */
224
+ class SourceCodeTraverser {
235
225
  /**
236
- * Emits an event of entering AST node.
237
- * @param {ASTNode} node A node which was entered.
238
- * @returns {void}
226
+ * The language of the source code being traversed.
227
+ * @type {Language}
228
+ */
229
+ #language;
230
+
231
+ /**
232
+ * Map of languages to instances of this class.
233
+ * @type {WeakMap<Language, SourceCodeTraverser>}
234
+ */
235
+ static instances = new WeakMap();
236
+
237
+ /**
238
+ * Creates a new instance.
239
+ * @param {Language} language The language of the source code being traversed.
239
240
  */
240
- enterNode(node) {
241
- this.applySelectors(node, false);
242
- this.currentAncestry.unshift(node);
241
+ constructor(language) {
242
+ this.#language = language;
243
+ }
244
+
245
+ static getInstance(language) {
246
+ if (!this.instances.has(language)) {
247
+ this.instances.set(language, new this(language));
248
+ }
249
+
250
+ return this.instances.get(language);
243
251
  }
244
252
 
245
253
  /**
246
- * Emits an event of leaving AST node.
247
- * @param {ASTNode} node A node which was left.
254
+ * Traverses the given source code synchronously.
255
+ * @param {SourceCode} sourceCode The source code to traverse.
256
+ * @param {SafeEmitter} emitter The emitter to use for events.
257
+ * @param {Object} options Options for traversal.
258
+ * @param {ReturnType<SourceCode["traverse"]>} options.steps The steps to take during traversal.
248
259
  * @returns {void}
260
+ * @throws {Error} If an error occurs during traversal.
249
261
  */
250
- leaveNode(node) {
251
- this.currentAncestry.shift();
252
- this.applySelectors(node, true);
262
+ traverseSync(sourceCode, emitter, { steps } = {}) {
263
+ const esquery = new ESQueryHelper(emitter, {
264
+ visitorKeys: sourceCode.visitorKeys ?? this.#language.visitorKeys,
265
+ fallback: vk.getKeys,
266
+ matchClass: this.#language.matchesSelectorClass ?? (() => false),
267
+ nodeTypeKey: this.#language.nodeTypeKey,
268
+ });
269
+
270
+ const currentAncestry = [];
271
+
272
+ for (const step of steps ?? sourceCode.traverse()) {
273
+ switch (step.kind) {
274
+ case STEP_KIND_VISIT: {
275
+ try {
276
+ if (step.phase === 1) {
277
+ esquery.applySelectors(
278
+ step.target,
279
+ currentAncestry,
280
+ false,
281
+ );
282
+ currentAncestry.unshift(step.target);
283
+ } else {
284
+ currentAncestry.shift();
285
+ esquery.applySelectors(
286
+ step.target,
287
+ currentAncestry,
288
+ true,
289
+ );
290
+ }
291
+ } catch (err) {
292
+ err.currentNode = step.target;
293
+ throw err;
294
+ }
295
+ break;
296
+ }
297
+
298
+ case STEP_KIND_CALL: {
299
+ emitter.emit(step.target, ...step.args);
300
+ break;
301
+ }
302
+
303
+ default:
304
+ throw new Error(
305
+ `Invalid traversal step found: "${step.kind}".`,
306
+ );
307
+ }
308
+ }
253
309
  }
254
310
  }
255
311
 
256
- module.exports = NodeEventGenerator;
312
+ module.exports = { SourceCodeTraverser };
package/lib/options.js CHANGED
@@ -66,6 +66,7 @@ const optionator = require("optionator");
66
66
  * @property {string[]} [suppressRule] Suppress specific rules
67
67
  * @property {string} [suppressionsLocation] Path to the suppressions file or directory
68
68
  * @property {boolean} [pruneSuppressions] Prune unused suppressions
69
+ * @property {boolean} [passOnUnprunedSuppressions] Ignore unused suppressions
69
70
  */
70
71
 
71
72
  //------------------------------------------------------------------------------
@@ -449,6 +450,12 @@ module.exports = function (usingFlatConfig) {
449
450
  default: "false",
450
451
  description: "Prune unused suppressions",
451
452
  },
453
+ {
454
+ option: "pass-on-unpruned-suppressions",
455
+ type: "Boolean",
456
+ default: "false",
457
+ description: "Ignore unused suppressions",
458
+ },
452
459
  {
453
460
  heading: "Miscellaneous",
454
461
  },
@@ -15,7 +15,7 @@ const assert = require("node:assert"),
15
15
  path = require("node:path"),
16
16
  equal = require("fast-deep-equal"),
17
17
  Traverser = require("../shared/traverser"),
18
- { getRuleOptionsSchema } = require("../config/flat-config-helpers"),
18
+ { Config } = require("../config/config"),
19
19
  { Linter, SourceCodeFixer } = require("../linter"),
20
20
  { interpolate, getPlaceholderMatcher } = require("../linter/interpolate"),
21
21
  stringify = require("json-stable-stringify-without-jsonify");
@@ -41,7 +41,7 @@ const { SourceCode } = require("../languages/js/source-code");
41
41
 
42
42
  /** @import { LanguageOptions, RuleDefinition } from "@eslint/core" */
43
43
 
44
- /** @typedef {import("../shared/types").Parser} Parser */
44
+ /** @typedef {import("../types").Linter.Parser} Parser */
45
45
 
46
46
  /**
47
47
  * A test case that is expected to pass lint.
@@ -767,7 +767,7 @@ class RuleTester {
767
767
  let schema;
768
768
 
769
769
  try {
770
- schema = getRuleOptionsSchema(rule);
770
+ schema = Config.getRuleOptionsSchema(rule);
771
771
  } catch (err) {
772
772
  err.message += metaSchemaDescription;
773
773
  throw err;
@@ -11,12 +11,15 @@
11
11
  /** @type {import('../types').Rule.RuleModule} */
12
12
  module.exports = {
13
13
  meta: {
14
+ dialects: ["javascript", "typescript"],
15
+ language: "javascript",
14
16
  type: "suggestion",
15
17
 
16
18
  defaultOptions: [
17
19
  "expression",
18
20
  {
19
21
  allowArrowFunctions: false,
22
+ allowTypeAnnotation: false,
20
23
  overrides: {},
21
24
  },
22
25
  ],
@@ -39,6 +42,9 @@ module.exports = {
39
42
  allowArrowFunctions: {
40
43
  type: "boolean",
41
44
  },
45
+ allowTypeAnnotation: {
46
+ type: "boolean",
47
+ },
42
48
  overrides: {
43
49
  type: "object",
44
50
  properties: {
@@ -60,11 +66,49 @@ module.exports = {
60
66
  },
61
67
 
62
68
  create(context) {
63
- const [style, { allowArrowFunctions, overrides }] = context.options;
69
+ const [style, { allowArrowFunctions, allowTypeAnnotation, overrides }] =
70
+ context.options;
64
71
  const enforceDeclarations = style === "declaration";
65
72
  const { namedExports: exportFunctionStyle } = overrides;
66
73
  const stack = [];
67
74
 
75
+ /**
76
+ * Checks if a function declaration is part of an overloaded function
77
+ * @param {ASTNode} node The function declaration node to check
78
+ * @returns {boolean} True if the function is overloaded
79
+ */
80
+ function isOverloadedFunction(node) {
81
+ const functionName = node.id.name;
82
+
83
+ if (node.parent.type === "ExportNamedDeclaration") {
84
+ return node.parent.parent.body.some(
85
+ member =>
86
+ member.type === "ExportNamedDeclaration" &&
87
+ member.declaration?.type === "TSDeclareFunction" &&
88
+ member.declaration.id.name === functionName,
89
+ );
90
+ }
91
+
92
+ if (node.parent.type === "SwitchCase") {
93
+ return node.parent.parent.cases.some(switchCase =>
94
+ switchCase.consequent.some(
95
+ member =>
96
+ member.type === "TSDeclareFunction" &&
97
+ member.id.name === functionName,
98
+ ),
99
+ );
100
+ }
101
+
102
+ return (
103
+ Array.isArray(node.parent.body) &&
104
+ node.parent.body.some(
105
+ member =>
106
+ member.type === "TSDeclareFunction" &&
107
+ member.id.name === functionName,
108
+ )
109
+ );
110
+ }
111
+
68
112
  const nodesToCheck = {
69
113
  FunctionDeclaration(node) {
70
114
  stack.push(false);
@@ -73,14 +117,16 @@ module.exports = {
73
117
  !enforceDeclarations &&
74
118
  node.parent.type !== "ExportDefaultDeclaration" &&
75
119
  (typeof exportFunctionStyle === "undefined" ||
76
- node.parent.type !== "ExportNamedDeclaration")
120
+ node.parent.type !== "ExportNamedDeclaration") &&
121
+ !isOverloadedFunction(node)
77
122
  ) {
78
123
  context.report({ node, messageId: "expression" });
79
124
  }
80
125
 
81
126
  if (
82
127
  node.parent.type === "ExportNamedDeclaration" &&
83
- exportFunctionStyle === "expression"
128
+ exportFunctionStyle === "expression" &&
129
+ !isOverloadedFunction(node)
84
130
  ) {
85
131
  context.report({ node, messageId: "expression" });
86
132
  }
@@ -97,7 +143,8 @@ module.exports = {
97
143
  node.parent.type === "VariableDeclarator" &&
98
144
  (typeof exportFunctionStyle === "undefined" ||
99
145
  node.parent.parent.parent.type !==
100
- "ExportNamedDeclaration")
146
+ "ExportNamedDeclaration") &&
147
+ !(allowTypeAnnotation && node.parent.id.typeAnnotation)
101
148
  ) {
102
149
  context.report({
103
150
  node: node.parent,
@@ -109,7 +156,8 @@ module.exports = {
109
156
  node.parent.type === "VariableDeclarator" &&
110
157
  node.parent.parent.parent.type ===
111
158
  "ExportNamedDeclaration" &&
112
- exportFunctionStyle === "declaration"
159
+ exportFunctionStyle === "declaration" &&
160
+ !(allowTypeAnnotation && node.parent.id.typeAnnotation)
113
161
  ) {
114
162
  context.report({
115
163
  node: node.parent,
@@ -144,7 +192,8 @@ module.exports = {
144
192
  enforceDeclarations &&
145
193
  (typeof exportFunctionStyle === "undefined" ||
146
194
  node.parent.parent.parent.type !==
147
- "ExportNamedDeclaration")
195
+ "ExportNamedDeclaration") &&
196
+ !(allowTypeAnnotation && node.parent.id.typeAnnotation)
148
197
  ) {
149
198
  context.report({
150
199
  node: node.parent,
@@ -155,7 +204,8 @@ module.exports = {
155
204
  if (
156
205
  node.parent.parent.parent.type ===
157
206
  "ExportNamedDeclaration" &&
158
- exportFunctionStyle === "declaration"
207
+ exportFunctionStyle === "declaration" &&
208
+ !(allowTypeAnnotation && node.parent.id.typeAnnotation)
159
209
  ) {
160
210
  context.report({
161
211
  node: node.parent,
@@ -225,6 +225,7 @@ module.exports = new LazyLoadingRuleMap(
225
225
  "no-this-before-super": () => require("./no-this-before-super"),
226
226
  "no-throw-literal": () => require("./no-throw-literal"),
227
227
  "no-trailing-spaces": () => require("./no-trailing-spaces"),
228
+ "no-unassigned-vars": () => require("./no-unassigned-vars"),
228
229
  "no-undef": () => require("./no-undef"),
229
230
  "no-undef-init": () => require("./no-undef-init"),
230
231
  "no-undefined": () => require("./no-undefined"),
@@ -20,6 +20,8 @@ const { upperCaseFirst } = require("../shared/string-utils");
20
20
  module.exports = {
21
21
  meta: {
22
22
  type: "suggestion",
23
+ dialects: ["typescript", "javascript"],
24
+ language: "javascript",
23
25
 
24
26
  docs: {
25
27
  description:
@@ -46,6 +48,11 @@ module.exports = {
46
48
  type: "integer",
47
49
  minimum: 0,
48
50
  },
51
+ countVoidThis: {
52
+ type: "boolean",
53
+ description:
54
+ "Whether to count a `this` declaration when the type is `void`.",
55
+ },
49
56
  },
50
57
  additionalProperties: false,
51
58
  },
@@ -61,12 +68,16 @@ module.exports = {
61
68
  const sourceCode = context.sourceCode;
62
69
  const option = context.options[0];
63
70
  let numParams = 3;
71
+ let countVoidThis = false;
64
72
 
65
- if (
66
- typeof option === "object" &&
67
- (Object.hasOwn(option, "maximum") || Object.hasOwn(option, "max"))
68
- ) {
69
- numParams = option.maximum || option.max;
73
+ if (typeof option === "object") {
74
+ if (
75
+ Object.hasOwn(option, "maximum") ||
76
+ Object.hasOwn(option, "max")
77
+ ) {
78
+ numParams = option.maximum || option.max;
79
+ }
80
+ countVoidThis = option.countVoidThis;
70
81
  }
71
82
  if (typeof option === "number") {
72
83
  numParams = option;
@@ -79,7 +90,19 @@ module.exports = {
79
90
  * @private
80
91
  */
81
92
  function checkFunction(node) {
82
- if (node.params.length > numParams) {
93
+ const hasVoidThisParam =
94
+ node.params.length > 0 &&
95
+ node.params[0].type === "Identifier" &&
96
+ node.params[0].name === "this" &&
97
+ node.params[0].typeAnnotation?.typeAnnotation.type ===
98
+ "TSVoidKeyword";
99
+
100
+ const effectiveParamCount =
101
+ hasVoidThisParam && !countVoidThis
102
+ ? node.params.length - 1
103
+ : node.params.length;
104
+
105
+ if (effectiveParamCount > numParams) {
83
106
  context.report({
84
107
  loc: astUtils.getFunctionHeadLoc(node, sourceCode),
85
108
  node,
@@ -88,7 +111,7 @@ module.exports = {
88
111
  name: upperCaseFirst(
89
112
  astUtils.getFunctionNameWithKind(node),
90
113
  ),
91
- count: node.params.length,
114
+ count: effectiveParamCount,
92
115
  max: numParams,
93
116
  },
94
117
  });
@@ -99,6 +122,8 @@ module.exports = {
99
122
  FunctionDeclaration: checkFunction,
100
123
  ArrowFunctionExpression: checkFunction,
101
124
  FunctionExpression: checkFunction,
125
+ TSDeclareFunction: checkFunction,
126
+ TSFunctionType: checkFunction,
102
127
  };
103
128
  },
104
129
  };
@@ -34,6 +34,8 @@ module.exports = {
34
34
  url: "https://eslint.org/docs/latest/rules/no-array-constructor",
35
35
  },
36
36
 
37
+ fixable: "code",
38
+
37
39
  hasSuggestions: true,
38
40
 
39
41
  schema: [],
@@ -49,6 +51,30 @@ module.exports = {
49
51
  create(context) {
50
52
  const sourceCode = context.sourceCode;
51
53
 
54
+ /**
55
+ * Checks if there are comments in Array constructor expressions.
56
+ * @param {ASTNode} node A CallExpression or NewExpression node.
57
+ * @returns {boolean} True if there are comments, false otherwise.
58
+ */
59
+ function hasCommentsInArrayConstructor(node) {
60
+ const firstToken = sourceCode.getFirstToken(node);
61
+ const lastToken = sourceCode.getLastToken(node);
62
+
63
+ let lastRelevantToken = sourceCode.getLastToken(node.callee);
64
+
65
+ while (
66
+ lastRelevantToken !== lastToken &&
67
+ !isOpeningParenToken(lastRelevantToken)
68
+ ) {
69
+ lastRelevantToken = sourceCode.getTokenAfter(lastRelevantToken);
70
+ }
71
+
72
+ return sourceCode.commentsExistBetween(
73
+ firstToken,
74
+ lastRelevantToken,
75
+ );
76
+ }
77
+
52
78
  /**
53
79
  * Gets the text between the calling parentheses of a CallExpression or NewExpression.
54
80
  * @param {ASTNode} node A CallExpression or NewExpression node.
@@ -107,6 +133,17 @@ module.exports = {
107
133
  let fixText;
108
134
  let messageId;
109
135
 
136
+ const nonSpreadCount = node.arguments.reduce(
137
+ (count, arg) =>
138
+ arg.type !== "SpreadElement" ? count + 1 : count,
139
+ 0,
140
+ );
141
+
142
+ const shouldSuggest =
143
+ node.optional ||
144
+ (node.arguments.length > 0 && nonSpreadCount < 2) ||
145
+ hasCommentsInArrayConstructor(node);
146
+
110
147
  /*
111
148
  * Check if the suggested change should include a preceding semicolon or not.
112
149
  * Due to JavaScript's ASI rules, a missing semicolon may be inserted automatically
@@ -127,10 +164,23 @@ module.exports = {
127
164
  context.report({
128
165
  node,
129
166
  messageId: "preferLiteral",
167
+ fix(fixer) {
168
+ if (shouldSuggest) {
169
+ return null;
170
+ }
171
+
172
+ return fixer.replaceText(node, fixText);
173
+ },
130
174
  suggest: [
131
175
  {
132
176
  messageId,
133
- fix: fixer => fixer.replaceText(node, fixText),
177
+ fix(fixer) {
178
+ if (shouldSuggest) {
179
+ return fixer.replaceText(node, fixText);
180
+ }
181
+
182
+ return null;
183
+ },
134
184
  },
135
185
  ],
136
186
  });