eslint 9.0.0-rc.0 → 9.1.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/lib/options.js CHANGED
@@ -38,7 +38,7 @@ const optionator = require("optionator");
38
38
  * @property {boolean} [help] Show help
39
39
  * @property {boolean} ignore Disable use of ignore files and patterns
40
40
  * @property {string} [ignorePath] Specify path of ignore file
41
- * @property {string[]} [ignorePattern] Pattern of files to ignore (in addition to those in .eslintignore)
41
+ * @property {string[]} [ignorePattern] Patterns of files to ignore. In eslintrc mode, these are in addition to `.eslintignore`
42
42
  * @property {boolean} init Run config initialization wizard
43
43
  * @property {boolean} inlineConfig Prevent comments from changing config or rules
44
44
  * @property {number} maxWarnings Number of warnings to trigger nonzero exit code
@@ -60,6 +60,7 @@ const optionator = require("optionator");
60
60
  * @property {boolean} [passOnNoPatterns=false] When set to true, missing patterns cause
61
61
  * the linting operation to short circuit and not report any failures.
62
62
  * @property {string[]} _ Positional filenames or patterns
63
+ * @property {boolean} [stats] Report additional statistics
63
64
  */
64
65
 
65
66
  //------------------------------------------------------------------------------
@@ -103,6 +104,16 @@ module.exports = function(usingFlatConfig) {
103
104
  };
104
105
  }
105
106
 
107
+ let inspectConfigFlag;
108
+
109
+ if (usingFlatConfig) {
110
+ inspectConfigFlag = {
111
+ option: "inspect-config",
112
+ type: "Boolean",
113
+ description: "Open the config inspector with the current configuration"
114
+ };
115
+ }
116
+
106
117
  let extFlag;
107
118
 
108
119
  if (!usingFlatConfig) {
@@ -143,6 +154,17 @@ module.exports = function(usingFlatConfig) {
143
154
  };
144
155
  }
145
156
 
157
+ let statsFlag;
158
+
159
+ if (usingFlatConfig) {
160
+ statsFlag = {
161
+ option: "stats",
162
+ type: "Boolean",
163
+ default: "false",
164
+ description: "Add statistics to the lint report"
165
+ };
166
+ }
167
+
146
168
  let warnIgnoredFlag;
147
169
 
148
170
  if (usingFlatConfig) {
@@ -173,6 +195,7 @@ module.exports = function(usingFlatConfig) {
173
195
  ? "Use this configuration instead of eslint.config.js, eslint.config.mjs, or eslint.config.cjs"
174
196
  : "Use this configuration, overriding .eslintrc.* config options if present"
175
197
  },
198
+ inspectConfigFlag,
176
199
  envFlag,
177
200
  extFlag,
178
201
  {
@@ -238,7 +261,7 @@ module.exports = function(usingFlatConfig) {
238
261
  {
239
262
  option: "ignore-pattern",
240
263
  type: "[String]",
241
- description: "Pattern of files to ignore (in addition to those in .eslintignore)",
264
+ description: `Patterns of files to ignore${usingFlatConfig ? "" : " (in addition to those in .eslintignore)"}`,
242
265
  concatRepeatedArrays: [true, {
243
266
  oneValuePerFlag: true
244
267
  }]
@@ -400,7 +423,8 @@ module.exports = function(usingFlatConfig) {
400
423
  option: "print-config",
401
424
  type: "path::String",
402
425
  description: "Print the configuration for the given file"
403
- }
426
+ },
427
+ statsFlag
404
428
  ].filter(value => !!value)
405
429
  });
406
430
  };
@@ -136,6 +136,15 @@ const suggestionObjectParameters = new Set([
136
136
  ]);
137
137
  const friendlySuggestionObjectParameterList = `[${[...suggestionObjectParameters].map(key => `'${key}'`).join(", ")}]`;
138
138
 
139
+ /*
140
+ * Ignored test case properties when checking for test case duplicates.
141
+ */
142
+ const duplicationIgnoredParameters = new Set([
143
+ "name",
144
+ "errors",
145
+ "output"
146
+ ]);
147
+
139
148
  const forbiddenMethods = [
140
149
  "applyInlineConfig",
141
150
  "applyLanguageOptions",
@@ -848,7 +857,7 @@ class RuleTester {
848
857
 
849
858
  /**
850
859
  * Check if this test case is a duplicate of one we have seen before.
851
- * @param {Object} item test case object
860
+ * @param {string|Object} item test case object
852
861
  * @param {Set<string>} seenTestCases set of serialized test cases we have seen so far (managed by this function)
853
862
  * @returns {void}
854
863
  * @private
@@ -863,7 +872,14 @@ class RuleTester {
863
872
  return;
864
873
  }
865
874
 
866
- const serializedTestCase = stringify(item);
875
+ const normalizedItem = typeof item === "string" ? { code: item } : item;
876
+ const serializedTestCase = stringify(normalizedItem, {
877
+ replacer(key, value) {
878
+
879
+ // "this" is the currently stringified object --> only ignore top-level properties
880
+ return (normalizedItem !== this || !duplicationIgnoredParameters.has(key)) ? value : void 0;
881
+ }
882
+ });
867
883
 
868
884
  assert(
869
885
  !seenTestCases.has(serializedTestCase),
@@ -47,11 +47,9 @@ module.exports = {
47
47
  },
48
48
  allow: {
49
49
  type: "array",
50
- items: [
51
- {
52
- type: "string"
53
- }
54
- ],
50
+ items: {
51
+ type: "string"
52
+ },
55
53
  minItems: 0,
56
54
  uniqueItems: true
57
55
  }
@@ -9,22 +9,6 @@
9
9
  // Helpers
10
10
  //------------------------------------------------------------------------------
11
11
 
12
- /**
13
- * Checks all segments in a set and returns true if any are reachable.
14
- * @param {Set<CodePathSegment>} segments The segments to check.
15
- * @returns {boolean} True if any segment is reachable; false otherwise.
16
- */
17
- function isAnySegmentReachable(segments) {
18
-
19
- for (const segment of segments) {
20
- if (segment.reachable) {
21
- return true;
22
- }
23
- }
24
-
25
- return false;
26
- }
27
-
28
12
  /**
29
13
  * Checks whether or not a given node is a constructor.
30
14
  * @param {ASTNode} node A node to check. This node type is one of
@@ -165,8 +149,7 @@ module.exports = {
165
149
  missingAll: "Expected to call 'super()'.",
166
150
 
167
151
  duplicate: "Unexpected duplicate 'super()'.",
168
- badSuper: "Unexpected 'super()' because 'super' is not a constructor.",
169
- unexpected: "Unexpected 'super()'."
152
+ badSuper: "Unexpected 'super()' because 'super' is not a constructor."
170
153
  }
171
154
  },
172
155
 
@@ -186,7 +169,7 @@ module.exports = {
186
169
  /**
187
170
  * @type {Record<string, SegmentInfo>}
188
171
  */
189
- let segInfoMap = Object.create(null);
172
+ const segInfoMap = Object.create(null);
190
173
 
191
174
  /**
192
175
  * Gets the flag which shows `super()` is called in some paths.
@@ -194,7 +177,7 @@ module.exports = {
194
177
  * @returns {boolean} The flag which shows `super()` is called in some paths
195
178
  */
196
179
  function isCalledInSomePath(segment) {
197
- return segment.reachable && segInfoMap[segment.id]?.calledInSomePaths;
180
+ return segment.reachable && segInfoMap[segment.id].calledInSomePaths;
198
181
  }
199
182
 
200
183
  /**
@@ -212,17 +195,6 @@ module.exports = {
212
195
  * @returns {boolean} The flag which shows `super()` is called in all paths.
213
196
  */
214
197
  function isCalledInEveryPath(segment) {
215
-
216
- /*
217
- * If specific segment is the looped segment of the current segment,
218
- * skip the segment.
219
- * If not skipped, this never becomes true after a loop.
220
- */
221
- if (segment.nextSegments.length === 1 &&
222
- segment.nextSegments[0]?.isLoopedPrevSegment(segment)) {
223
- return true;
224
- }
225
-
226
198
  return segment.reachable && segInfoMap[segment.id].calledInEveryPaths;
227
199
  }
228
200
 
@@ -279,9 +251,9 @@ module.exports = {
279
251
  }
280
252
 
281
253
  // Reports if `super()` lacked.
282
- const seenSegments = codePath.returnedSegments.filter(hasSegmentBeenSeen);
283
- const calledInEveryPaths = seenSegments.every(isCalledInEveryPath);
284
- const calledInSomePaths = seenSegments.some(isCalledInSomePath);
254
+ const returnedSegments = codePath.returnedSegments;
255
+ const calledInEveryPaths = returnedSegments.every(isCalledInEveryPath);
256
+ const calledInSomePaths = returnedSegments.some(isCalledInSomePath);
285
257
 
286
258
  if (!calledInEveryPaths) {
287
259
  context.report({
@@ -296,28 +268,38 @@ module.exports = {
296
268
  /**
297
269
  * Initialize information of a given code path segment.
298
270
  * @param {CodePathSegment} segment A code path segment to initialize.
271
+ * @param {CodePathSegment} node Node that starts the segment.
299
272
  * @returns {void}
300
273
  */
301
- onCodePathSegmentStart(segment) {
274
+ onCodePathSegmentStart(segment, node) {
302
275
 
303
276
  funcInfo.currentSegments.add(segment);
304
277
 
305
- if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
278
+ if (!(funcInfo.isConstructor && funcInfo.hasExtends)) {
306
279
  return;
307
280
  }
308
281
 
309
282
  // Initialize info.
310
283
  const info = segInfoMap[segment.id] = new SegmentInfo();
311
284
 
312
- // When there are previous segments, aggregates these.
313
- const prevSegments = segment.prevSegments;
314
-
315
- if (prevSegments.length > 0) {
316
- const seenPrevSegments = prevSegments.filter(hasSegmentBeenSeen);
285
+ const seenPrevSegments = segment.prevSegments.filter(hasSegmentBeenSeen);
317
286
 
287
+ // When there are previous segments, aggregates these.
288
+ if (seenPrevSegments.length > 0) {
318
289
  info.calledInSomePaths = seenPrevSegments.some(isCalledInSomePath);
319
290
  info.calledInEveryPaths = seenPrevSegments.every(isCalledInEveryPath);
320
291
  }
292
+
293
+ /*
294
+ * ForStatement > *.update segments are a special case as they are created in advance,
295
+ * without seen previous segments. Since they logically don't affect `calledInEveryPaths`
296
+ * calculations, and they can never be a lone previous segment of another one, we'll set
297
+ * their `calledInEveryPaths` to `true` to effectively ignore them in those calculations.
298
+ * .
299
+ */
300
+ if (node.parent && node.parent.type === "ForStatement" && node.parent.update === node) {
301
+ info.calledInEveryPaths = true;
302
+ }
321
303
  },
322
304
 
323
305
  onUnreachableCodePathSegmentStart(segment) {
@@ -343,25 +325,30 @@ module.exports = {
343
325
  * @returns {void}
344
326
  */
345
327
  onCodePathSegmentLoop(fromSegment, toSegment) {
346
- if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
328
+ if (!(funcInfo.isConstructor && funcInfo.hasExtends)) {
347
329
  return;
348
330
  }
349
331
 
350
- // Update information inside of the loop.
351
- const isRealLoop = toSegment.prevSegments.length >= 2;
352
-
353
332
  funcInfo.codePath.traverseSegments(
354
333
  { first: toSegment, last: fromSegment },
355
- segment => {
356
- const info = segInfoMap[segment.id] ?? new SegmentInfo();
334
+ (segment, controller) => {
335
+ const info = segInfoMap[segment.id];
336
+
337
+ // skip segments after the loop
338
+ if (!info) {
339
+ controller.skip();
340
+ return;
341
+ }
342
+
357
343
  const seenPrevSegments = segment.prevSegments.filter(hasSegmentBeenSeen);
344
+ const calledInSomePreviousPaths = seenPrevSegments.some(isCalledInSomePath);
345
+ const calledInEveryPreviousPaths = seenPrevSegments.every(isCalledInEveryPath);
358
346
 
359
- // Updates flags.
360
- info.calledInSomePaths = seenPrevSegments.some(isCalledInSomePath);
361
- info.calledInEveryPaths = seenPrevSegments.every(isCalledInEveryPath);
347
+ info.calledInSomePaths ||= calledInSomePreviousPaths;
348
+ info.calledInEveryPaths ||= calledInEveryPreviousPaths;
362
349
 
363
350
  // If flags become true anew, reports the valid nodes.
364
- if (info.calledInSomePaths || isRealLoop) {
351
+ if (calledInSomePreviousPaths) {
365
352
  const nodes = info.validNodes;
366
353
 
367
354
  info.validNodes = [];
@@ -375,9 +362,6 @@ module.exports = {
375
362
  });
376
363
  }
377
364
  }
378
-
379
- // save just in case we created a new SegmentInfo object
380
- segInfoMap[segment.id] = info;
381
365
  }
382
366
  );
383
367
  },
@@ -388,7 +372,7 @@ module.exports = {
388
372
  * @returns {void}
389
373
  */
390
374
  "CallExpression:exit"(node) {
391
- if (!(funcInfo && funcInfo.isConstructor)) {
375
+ if (!(funcInfo.isConstructor && funcInfo.hasExtends)) {
392
376
  return;
393
377
  }
394
378
 
@@ -398,41 +382,34 @@ module.exports = {
398
382
  }
399
383
 
400
384
  // Reports if needed.
401
- if (funcInfo.hasExtends) {
402
- const segments = funcInfo.currentSegments;
403
- let duplicate = false;
404
- let info = null;
385
+ const segments = funcInfo.currentSegments;
386
+ let duplicate = false;
387
+ let info = null;
405
388
 
406
- for (const segment of segments) {
389
+ for (const segment of segments) {
407
390
 
408
- if (segment.reachable) {
409
- info = segInfoMap[segment.id];
391
+ if (segment.reachable) {
392
+ info = segInfoMap[segment.id];
410
393
 
411
- duplicate = duplicate || info.calledInSomePaths;
412
- info.calledInSomePaths = info.calledInEveryPaths = true;
413
- }
394
+ duplicate = duplicate || info.calledInSomePaths;
395
+ info.calledInSomePaths = info.calledInEveryPaths = true;
414
396
  }
397
+ }
415
398
 
416
- if (info) {
417
- if (duplicate) {
418
- context.report({
419
- messageId: "duplicate",
420
- node
421
- });
422
- } else if (!funcInfo.superIsConstructor) {
423
- context.report({
424
- messageId: "badSuper",
425
- node
426
- });
427
- } else {
428
- info.validNodes.push(node);
429
- }
399
+ if (info) {
400
+ if (duplicate) {
401
+ context.report({
402
+ messageId: "duplicate",
403
+ node
404
+ });
405
+ } else if (!funcInfo.superIsConstructor) {
406
+ context.report({
407
+ messageId: "badSuper",
408
+ node
409
+ });
410
+ } else {
411
+ info.validNodes.push(node);
430
412
  }
431
- } else if (isAnySegmentReachable(funcInfo.currentSegments)) {
432
- context.report({
433
- messageId: "unexpected",
434
- node
435
- });
436
413
  }
437
414
  },
438
415
 
@@ -442,7 +419,7 @@ module.exports = {
442
419
  * @returns {void}
443
420
  */
444
421
  ReturnStatement(node) {
445
- if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
422
+ if (!(funcInfo.isConstructor && funcInfo.hasExtends)) {
446
423
  return;
447
424
  }
448
425
 
@@ -462,14 +439,6 @@ module.exports = {
462
439
  info.calledInSomePaths = info.calledInEveryPaths = true;
463
440
  }
464
441
  }
465
- },
466
-
467
- /**
468
- * Resets state.
469
- * @returns {void}
470
- */
471
- "Program:exit"() {
472
- segInfoMap = Object.create(null);
473
442
  }
474
443
  };
475
444
  }
@@ -31,8 +31,7 @@ module.exports = {
31
31
  type: "object",
32
32
  properties: {
33
33
  checkLoops: {
34
- type: "boolean",
35
- default: true
34
+ enum: ["all", "allExceptWhileTrue", "none", true, false]
36
35
  }
37
36
  },
38
37
  additionalProperties: false
@@ -45,11 +44,17 @@ module.exports = {
45
44
  },
46
45
 
47
46
  create(context) {
48
- const options = context.options[0] || {},
49
- checkLoops = options.checkLoops !== false,
50
- loopSetStack = [];
47
+ const options = context.options[0] || {};
48
+ let checkLoops = options.checkLoops ?? "allExceptWhileTrue";
49
+ const loopSetStack = [];
51
50
  const sourceCode = context.sourceCode;
52
51
 
52
+ if (options.checkLoops === true) {
53
+ checkLoops = "all";
54
+ } else if (options.checkLoops === false) {
55
+ checkLoops = "none";
56
+ }
57
+
53
58
  let loopsInCurrentScope = new Set();
54
59
 
55
60
  //--------------------------------------------------------------------------
@@ -120,7 +125,7 @@ module.exports = {
120
125
  * @private
121
126
  */
122
127
  function checkLoop(node) {
123
- if (checkLoops) {
128
+ if (checkLoops === "all" || checkLoops === "allExceptWhileTrue") {
124
129
  trackConstantConditionLoop(node);
125
130
  }
126
131
  }
@@ -132,7 +137,13 @@ module.exports = {
132
137
  return {
133
138
  ConditionalExpression: reportIfConstant,
134
139
  IfStatement: reportIfConstant,
135
- WhileStatement: checkLoop,
140
+ WhileStatement(node) {
141
+ if (node.test.type === "Literal" && node.test.value === true && checkLoops === "allExceptWhileTrue") {
142
+ return;
143
+ }
144
+
145
+ checkLoop(node);
146
+ },
136
147
  "WhileStatement:exit": checkConstantConditionLoopInSet,
137
148
  DoWhileStatement: checkLoop,
138
149
  "DoWhileStatement:exit": checkConstantConditionLoopInSet,
@@ -117,7 +117,7 @@ module.exports = {
117
117
  };
118
118
 
119
119
  ruleDef.VariableDeclaration = function(node) {
120
- if (node.kind === "let" || node.kind === "const") {
120
+ if (node.kind !== "var") {
121
121
  markLoneBlock(node);
122
122
  }
123
123
  };