eslint 8.47.0 → 8.49.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.
@@ -48,7 +48,8 @@ const
48
48
  equal = require("fast-deep-equal"),
49
49
  Traverser = require("../../lib/shared/traverser"),
50
50
  { getRuleOptionsSchema, validate } = require("../shared/config-validator"),
51
- { Linter, SourceCodeFixer, interpolate } = require("../linter");
51
+ { Linter, SourceCodeFixer, interpolate } = require("../linter"),
52
+ CodePath = require("../linter/code-path-analysis/code-path");
52
53
 
53
54
  const ajv = require("../shared/ajv")({ strictDefaults: true });
54
55
 
@@ -62,6 +63,7 @@ const { SourceCode } = require("../source-code");
62
63
  //------------------------------------------------------------------------------
63
64
 
64
65
  /** @typedef {import("../shared/types").Parser} Parser */
66
+ /** @typedef {import("../shared/types").Rule} Rule */
65
67
 
66
68
 
67
69
  /**
@@ -163,6 +165,30 @@ const friendlySuggestionObjectParameterList = `[${[...suggestionObjectParameters
163
165
 
164
166
  const hasOwnProperty = Function.call.bind(Object.hasOwnProperty);
165
167
 
168
+ const DEPRECATED_SOURCECODE_PASSTHROUGHS = {
169
+ getSource: "getText",
170
+ getSourceLines: "getLines",
171
+ getAllComments: "getAllComments",
172
+ getNodeByRangeIndex: "getNodeByRangeIndex",
173
+
174
+ // getComments: "getComments", -- already handled by a separate error
175
+ getCommentsBefore: "getCommentsBefore",
176
+ getCommentsAfter: "getCommentsAfter",
177
+ getCommentsInside: "getCommentsInside",
178
+ getJSDocComment: "getJSDocComment",
179
+ getFirstToken: "getFirstToken",
180
+ getFirstTokens: "getFirstTokens",
181
+ getLastToken: "getLastToken",
182
+ getLastTokens: "getLastTokens",
183
+ getTokenAfter: "getTokenAfter",
184
+ getTokenBefore: "getTokenBefore",
185
+ getTokenByRangeStart: "getTokenByRangeStart",
186
+ getTokens: "getTokens",
187
+ getTokensAfter: "getTokensAfter",
188
+ getTokensBefore: "getTokensBefore",
189
+ getTokensBetween: "getTokensBetween"
190
+ };
191
+
166
192
  /**
167
193
  * Clones a given value deeply.
168
194
  * Note: This ignores `parent` property.
@@ -334,6 +360,37 @@ function emitMissingSchemaWarning(ruleName) {
334
360
  }
335
361
  }
336
362
 
363
+ /**
364
+ * Emit a deprecation warning if a rule uses a deprecated `context` method.
365
+ * @param {string} ruleName Name of the rule.
366
+ * @param {string} methodName The name of the method on `context` that was used.
367
+ * @returns {void}
368
+ */
369
+ function emitDeprecatedContextMethodWarning(ruleName, methodName) {
370
+ if (!emitDeprecatedContextMethodWarning[`warned-${ruleName}-${methodName}`]) {
371
+ emitDeprecatedContextMethodWarning[`warned-${ruleName}-${methodName}`] = true;
372
+ process.emitWarning(
373
+ `"${ruleName}" rule is using \`context.${methodName}()\`, which is deprecated and will be removed in ESLint v9. Please use \`sourceCode.${DEPRECATED_SOURCECODE_PASSTHROUGHS[methodName]}()\` instead.`,
374
+ "DeprecationWarning"
375
+ );
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Emit a deprecation warning if rule uses CodePath#currentSegments.
381
+ * @param {string} ruleName Name of the rule.
382
+ * @returns {void}
383
+ */
384
+ function emitCodePathCurrentSegmentsWarning(ruleName) {
385
+ if (!emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`]) {
386
+ emitCodePathCurrentSegmentsWarning[`warned-${ruleName}`] = true;
387
+ process.emitWarning(
388
+ `"${ruleName}" rule uses CodePath#currentSegments and will stop working in ESLint v9. Please read the documentation for how to update your code: https://eslint.org/docs/latest/extend/code-path-analysis#usage-examples`,
389
+ "DeprecationWarning"
390
+ );
391
+ }
392
+ }
393
+
337
394
  //------------------------------------------------------------------------------
338
395
  // Public Interface
339
396
  //------------------------------------------------------------------------------
@@ -508,17 +565,20 @@ class RuleTester {
508
565
  /**
509
566
  * Define a rule for one particular run of tests.
510
567
  * @param {string} name The name of the rule to define.
511
- * @param {Function} rule The rule definition.
568
+ * @param {Function | Rule} rule The rule definition.
512
569
  * @returns {void}
513
570
  */
514
571
  defineRule(name, rule) {
572
+ if (typeof rule === "function") {
573
+ emitLegacyRuleAPIWarning(name);
574
+ }
515
575
  this.rules[name] = rule;
516
576
  }
517
577
 
518
578
  /**
519
579
  * Adds a new rule test to execute.
520
580
  * @param {string} ruleName The name of the rule to run.
521
- * @param {Function} rule The rule to test.
581
+ * @param {Function | Rule} rule The rule to test.
522
582
  * @param {{
523
583
  * valid: (ValidTestCase | string)[],
524
584
  * invalid: InvalidTestCase[]
@@ -562,7 +622,27 @@ class RuleTester {
562
622
  freezeDeeply(context.settings);
563
623
  freezeDeeply(context.parserOptions);
564
624
 
565
- return (typeof rule === "function" ? rule : rule.create)(context);
625
+ const newContext = Object.freeze(
626
+ Object.create(
627
+ context,
628
+ Object.fromEntries(Object.keys(DEPRECATED_SOURCECODE_PASSTHROUGHS).map(methodName => [
629
+ methodName,
630
+ {
631
+ value(...args) {
632
+
633
+ // emit deprecation warning
634
+ emitDeprecatedContextMethodWarning(ruleName, methodName);
635
+
636
+ // call the original method
637
+ return context[methodName].call(this, ...args);
638
+ },
639
+ enumerable: true
640
+ }
641
+ ]))
642
+ )
643
+ );
644
+
645
+ return (typeof rule === "function" ? rule : rule.create)(newContext);
566
646
  }
567
647
  }));
568
648
 
@@ -682,13 +762,22 @@ class RuleTester {
682
762
 
683
763
  // Verify the code.
684
764
  const { getComments } = SourceCode.prototype;
765
+ const originalCurrentSegments = Object.getOwnPropertyDescriptor(CodePath.prototype, "currentSegments");
685
766
  let messages;
686
767
 
687
768
  try {
688
769
  SourceCode.prototype.getComments = getCommentsDeprecation;
770
+ Object.defineProperty(CodePath.prototype, "currentSegments", {
771
+ get() {
772
+ emitCodePathCurrentSegmentsWarning(ruleName);
773
+ return originalCurrentSegments.get.call(this);
774
+ }
775
+ });
776
+
689
777
  messages = linter.verify(code, config, filename);
690
778
  } finally {
691
779
  SourceCode.prototype.getComments = getComments;
780
+ Object.defineProperty(CodePath.prototype, "currentSegments", originalCurrentSegments);
692
781
  }
693
782
 
694
783
  const fatalErrorMessage = messages.find(m => m.fatal);
@@ -1021,29 +1110,35 @@ class RuleTester {
1021
1110
  /*
1022
1111
  * This creates a mocha test suite and pipes all supplied info through
1023
1112
  * one of the templates above.
1113
+ * The test suites for valid/invalid are created conditionally as
1114
+ * test runners (eg. vitest) fail for empty test suites.
1024
1115
  */
1025
1116
  this.constructor.describe(ruleName, () => {
1026
- this.constructor.describe("valid", () => {
1027
- test.valid.forEach(valid => {
1028
- this.constructor[valid.only ? "itOnly" : "it"](
1029
- sanitize(typeof valid === "object" ? valid.name || valid.code : valid),
1030
- () => {
1031
- testValidTemplate(valid);
1032
- }
1033
- );
1117
+ if (test.valid.length > 0) {
1118
+ this.constructor.describe("valid", () => {
1119
+ test.valid.forEach(valid => {
1120
+ this.constructor[valid.only ? "itOnly" : "it"](
1121
+ sanitize(typeof valid === "object" ? valid.name || valid.code : valid),
1122
+ () => {
1123
+ testValidTemplate(valid);
1124
+ }
1125
+ );
1126
+ });
1034
1127
  });
1035
- });
1128
+ }
1036
1129
 
1037
- this.constructor.describe("invalid", () => {
1038
- test.invalid.forEach(invalid => {
1039
- this.constructor[invalid.only ? "itOnly" : "it"](
1040
- sanitize(invalid.name || invalid.code),
1041
- () => {
1042
- testInvalidTemplate(invalid);
1043
- }
1044
- );
1130
+ if (test.invalid.length > 0) {
1131
+ this.constructor.describe("invalid", () => {
1132
+ test.invalid.forEach(invalid => {
1133
+ this.constructor[invalid.only ? "itOnly" : "it"](
1134
+ sanitize(invalid.name || invalid.code),
1135
+ () => {
1136
+ testInvalidTemplate(invalid);
1137
+ }
1138
+ );
1139
+ });
1045
1140
  });
1046
- });
1141
+ }
1047
1142
  });
1048
1143
  }
1049
1144
  }
@@ -18,15 +18,6 @@ const astUtils = require("./utils/ast-utils");
18
18
  const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/u;
19
19
  const TARGET_METHODS = /^(?:every|filter|find(?:Last)?(?:Index)?|flatMap|forEach|map|reduce(?:Right)?|some|sort|toSorted)$/u;
20
20
 
21
- /**
22
- * Checks a given code path segment is reachable.
23
- * @param {CodePathSegment} segment A segment to check.
24
- * @returns {boolean} `true` if the segment is reachable.
25
- */
26
- function isReachable(segment) {
27
- return segment.reachable;
28
- }
29
-
30
21
  /**
31
22
  * Checks a given node is a member access which has the specified name's
32
23
  * property.
@@ -38,6 +29,22 @@ function isTargetMethod(node) {
38
29
  return astUtils.isSpecificMemberAccess(node, null, TARGET_METHODS);
39
30
  }
40
31
 
32
+ /**
33
+ * Checks all segments in a set and returns true if any are reachable.
34
+ * @param {Set<CodePathSegment>} segments The segments to check.
35
+ * @returns {boolean} True if any segment is reachable; false otherwise.
36
+ */
37
+ function isAnySegmentReachable(segments) {
38
+
39
+ for (const segment of segments) {
40
+ if (segment.reachable) {
41
+ return true;
42
+ }
43
+ }
44
+
45
+ return false;
46
+ }
47
+
41
48
  /**
42
49
  * Returns a human-legible description of an array method
43
50
  * @param {string} arrayMethodName A method name to fully qualify
@@ -205,7 +212,7 @@ module.exports = {
205
212
  messageId = "expectedNoReturnValue";
206
213
  }
207
214
  } else {
208
- if (node.body.type === "BlockStatement" && funcInfo.codePath.currentSegments.some(isReachable)) {
215
+ if (node.body.type === "BlockStatement" && isAnySegmentReachable(funcInfo.currentSegments)) {
209
216
  messageId = funcInfo.hasReturn ? "expectedAtEnd" : "expectedInside";
210
217
  }
211
218
  }
@@ -242,7 +249,8 @@ module.exports = {
242
249
  methodName &&
243
250
  !node.async &&
244
251
  !node.generator,
245
- node
252
+ node,
253
+ currentSegments: new Set()
246
254
  };
247
255
  },
248
256
 
@@ -251,6 +259,23 @@ module.exports = {
251
259
  funcInfo = funcInfo.upper;
252
260
  },
253
261
 
262
+ onUnreachableCodePathSegmentStart(segment) {
263
+ funcInfo.currentSegments.add(segment);
264
+ },
265
+
266
+ onUnreachableCodePathSegmentEnd(segment) {
267
+ funcInfo.currentSegments.delete(segment);
268
+ },
269
+
270
+ onCodePathSegmentStart(segment) {
271
+ funcInfo.currentSegments.add(segment);
272
+ },
273
+
274
+ onCodePathSegmentEnd(segment) {
275
+ funcInfo.currentSegments.delete(segment);
276
+ },
277
+
278
+
254
279
  // Checks the return statement is valid.
255
280
  ReturnStatement(node) {
256
281
 
@@ -16,12 +16,19 @@ const { upperCaseFirst } = require("../shared/string-utils");
16
16
  //------------------------------------------------------------------------------
17
17
 
18
18
  /**
19
- * Checks whether or not a given code path segment is unreachable.
20
- * @param {CodePathSegment} segment A CodePathSegment to check.
21
- * @returns {boolean} `true` if the segment is unreachable.
19
+ * Checks all segments in a set and returns true if all are unreachable.
20
+ * @param {Set<CodePathSegment>} segments The segments to check.
21
+ * @returns {boolean} True if all segments are unreachable; false otherwise.
22
22
  */
23
- function isUnreachable(segment) {
24
- return !segment.reachable;
23
+ function areAllSegmentsUnreachable(segments) {
24
+
25
+ for (const segment of segments) {
26
+ if (segment.reachable) {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ return true;
25
32
  }
26
33
 
27
34
  /**
@@ -88,7 +95,7 @@ module.exports = {
88
95
  * When unreachable, all paths are returned or thrown.
89
96
  */
90
97
  if (!funcInfo.hasReturnValue ||
91
- funcInfo.codePath.currentSegments.every(isUnreachable) ||
98
+ areAllSegmentsUnreachable(funcInfo.currentSegments) ||
92
99
  astUtils.isES5Constructor(node) ||
93
100
  isClassConstructor(node)
94
101
  ) {
@@ -141,13 +148,31 @@ module.exports = {
141
148
  hasReturn: false,
142
149
  hasReturnValue: false,
143
150
  messageId: "",
144
- node
151
+ node,
152
+ currentSegments: new Set()
145
153
  };
146
154
  },
147
155
  onCodePathEnd() {
148
156
  funcInfo = funcInfo.upper;
149
157
  },
150
158
 
159
+ onUnreachableCodePathSegmentStart(segment) {
160
+ funcInfo.currentSegments.add(segment);
161
+ },
162
+
163
+ onUnreachableCodePathSegmentEnd(segment) {
164
+ funcInfo.currentSegments.delete(segment);
165
+ },
166
+
167
+ onCodePathSegmentStart(segment) {
168
+ funcInfo.currentSegments.add(segment);
169
+ },
170
+
171
+ onCodePathSegmentEnd(segment) {
172
+ funcInfo.currentSegments.delete(segment);
173
+ },
174
+
175
+
151
176
  // Reports a given return statement if it's inconsistent.
152
177
  ReturnStatement(node) {
153
178
  const argument = node.argument;
@@ -10,12 +10,19 @@
10
10
  //------------------------------------------------------------------------------
11
11
 
12
12
  /**
13
- * Checks whether a given code path segment is reachable or not.
14
- * @param {CodePathSegment} segment A code path segment to check.
15
- * @returns {boolean} `true` if the segment is reachable.
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
16
  */
17
- function isReachable(segment) {
18
- return segment.reachable;
17
+ function isAnySegmentReachable(segments) {
18
+
19
+ for (const segment of segments) {
20
+ if (segment.reachable) {
21
+ return true;
22
+ }
23
+ }
24
+
25
+ return false;
19
26
  }
20
27
 
21
28
  /**
@@ -210,7 +217,8 @@ module.exports = {
210
217
  isConstructor: true,
211
218
  hasExtends: Boolean(superClass),
212
219
  superIsConstructor: isPossibleConstructor(superClass),
213
- codePath
220
+ codePath,
221
+ currentSegments: new Set()
214
222
  };
215
223
  } else {
216
224
  funcInfo = {
@@ -218,7 +226,8 @@ module.exports = {
218
226
  isConstructor: false,
219
227
  hasExtends: false,
220
228
  superIsConstructor: false,
221
- codePath
229
+ codePath,
230
+ currentSegments: new Set()
222
231
  };
223
232
  }
224
233
  },
@@ -261,6 +270,9 @@ module.exports = {
261
270
  * @returns {void}
262
271
  */
263
272
  onCodePathSegmentStart(segment) {
273
+
274
+ funcInfo.currentSegments.add(segment);
275
+
264
276
  if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
265
277
  return;
266
278
  }
@@ -281,6 +293,19 @@ module.exports = {
281
293
  }
282
294
  },
283
295
 
296
+ onUnreachableCodePathSegmentStart(segment) {
297
+ funcInfo.currentSegments.add(segment);
298
+ },
299
+
300
+ onUnreachableCodePathSegmentEnd(segment) {
301
+ funcInfo.currentSegments.delete(segment);
302
+ },
303
+
304
+ onCodePathSegmentEnd(segment) {
305
+ funcInfo.currentSegments.delete(segment);
306
+ },
307
+
308
+
284
309
  /**
285
310
  * Update information of the code path segment when a code path was
286
311
  * looped.
@@ -344,12 +369,11 @@ module.exports = {
344
369
 
345
370
  // Reports if needed.
346
371
  if (funcInfo.hasExtends) {
347
- const segments = funcInfo.codePath.currentSegments;
372
+ const segments = funcInfo.currentSegments;
348
373
  let duplicate = false;
349
374
  let info = null;
350
375
 
351
- for (let i = 0; i < segments.length; ++i) {
352
- const segment = segments[i];
376
+ for (const segment of segments) {
353
377
 
354
378
  if (segment.reachable) {
355
379
  info = segInfoMap[segment.id];
@@ -374,7 +398,7 @@ module.exports = {
374
398
  info.validNodes.push(node);
375
399
  }
376
400
  }
377
- } else if (funcInfo.codePath.currentSegments.some(isReachable)) {
401
+ } else if (isAnySegmentReachable(funcInfo.currentSegments)) {
378
402
  context.report({
379
403
  messageId: "unexpected",
380
404
  node
@@ -398,10 +422,9 @@ module.exports = {
398
422
  }
399
423
 
400
424
  // Returning argument is a substitute of 'super()'.
401
- const segments = funcInfo.codePath.currentSegments;
425
+ const segments = funcInfo.currentSegments;
402
426
 
403
- for (let i = 0; i < segments.length; ++i) {
404
- const segment = segments[i];
427
+ for (const segment of segments) {
405
428
 
406
429
  if (segment.reachable) {
407
430
  const info = segInfoMap[segment.id];
@@ -5,6 +5,12 @@
5
5
 
6
6
  "use strict";
7
7
 
8
+ //------------------------------------------------------------------------------
9
+ // Requirements
10
+ //------------------------------------------------------------------------------
11
+
12
+ const { getStaticValue } = require("@eslint-community/eslint-utils");
13
+
8
14
  //------------------------------------------------------------------------------
9
15
  // Rule Definition
10
16
  //------------------------------------------------------------------------------
@@ -29,6 +35,7 @@ module.exports = {
29
35
  },
30
36
 
31
37
  create(context) {
38
+ const { sourceCode } = context;
32
39
 
33
40
  /**
34
41
  * report an error.
@@ -46,17 +53,17 @@ module.exports = {
46
53
  * check the right side of the assignment
47
54
  * @param {ASTNode} update UpdateExpression to check
48
55
  * @param {int} dir expected direction that could either be turned around or invalidated
49
- * @returns {int} return dir, the negated dir or zero if it's not clear for identifiers
56
+ * @returns {int} return dir, the negated dir, or zero if the counter does not change or the direction is not clear
50
57
  */
51
58
  function getRightDirection(update, dir) {
52
- if (update.right.type === "UnaryExpression") {
53
- if (update.right.operator === "-") {
54
- return -dir;
55
- }
56
- } else if (update.right.type === "Identifier") {
57
- return 0;
59
+ const staticValue = getStaticValue(update.right, sourceCode.getScope(update));
60
+
61
+ if (staticValue && ["bigint", "boolean", "number"].includes(typeof staticValue.value)) {
62
+ const sign = Math.sign(Number(staticValue.value)) || 0; // convert NaN to 0
63
+
64
+ return dir * sign;
58
65
  }
59
- return dir;
66
+ return 0;
60
67
  }
61
68
 
62
69
  /**
@@ -14,15 +14,23 @@ const astUtils = require("./utils/ast-utils");
14
14
  //------------------------------------------------------------------------------
15
15
  // Helpers
16
16
  //------------------------------------------------------------------------------
17
+
17
18
  const TARGET_NODE_TYPE = /^(?:Arrow)?FunctionExpression$/u;
18
19
 
19
20
  /**
20
- * Checks a given code path segment is reachable.
21
- * @param {CodePathSegment} segment A segment to check.
22
- * @returns {boolean} `true` if the segment is reachable.
21
+ * Checks all segments in a set and returns true if any are reachable.
22
+ * @param {Set<CodePathSegment>} segments The segments to check.
23
+ * @returns {boolean} True if any segment is reachable; false otherwise.
23
24
  */
24
- function isReachable(segment) {
25
- return segment.reachable;
25
+ function isAnySegmentReachable(segments) {
26
+
27
+ for (const segment of segments) {
28
+ if (segment.reachable) {
29
+ return true;
30
+ }
31
+ }
32
+
33
+ return false;
26
34
  }
27
35
 
28
36
  //------------------------------------------------------------------------------
@@ -71,7 +79,8 @@ module.exports = {
71
79
  codePath: null,
72
80
  hasReturn: false,
73
81
  shouldCheck: false,
74
- node: null
82
+ node: null,
83
+ currentSegments: []
75
84
  };
76
85
 
77
86
  /**
@@ -85,7 +94,7 @@ module.exports = {
85
94
  */
86
95
  function checkLastSegment(node) {
87
96
  if (funcInfo.shouldCheck &&
88
- funcInfo.codePath.currentSegments.some(isReachable)
97
+ isAnySegmentReachable(funcInfo.currentSegments)
89
98
  ) {
90
99
  context.report({
91
100
  node,
@@ -144,7 +153,8 @@ module.exports = {
144
153
  codePath,
145
154
  hasReturn: false,
146
155
  shouldCheck: isGetter(node),
147
- node
156
+ node,
157
+ currentSegments: new Set()
148
158
  };
149
159
  },
150
160
 
@@ -152,6 +162,21 @@ module.exports = {
152
162
  onCodePathEnd() {
153
163
  funcInfo = funcInfo.upper;
154
164
  },
165
+ onUnreachableCodePathSegmentStart(segment) {
166
+ funcInfo.currentSegments.add(segment);
167
+ },
168
+
169
+ onUnreachableCodePathSegmentEnd(segment) {
170
+ funcInfo.currentSegments.delete(segment);
171
+ },
172
+
173
+ onCodePathSegmentStart(segment) {
174
+ funcInfo.currentSegments.add(segment);
175
+ },
176
+
177
+ onCodePathSegmentEnd(segment) {
178
+ funcInfo.currentSegments.delete(segment);
179
+ },
155
180
 
156
181
  // Checks the return statement is valid.
157
182
  ReturnStatement(node) {