eslint 10.2.0 → 10.3.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/README.md CHANGED
@@ -360,7 +360,7 @@ to get your logo on our READMEs and [website](https://eslint.org/sponsors).
360
360
  <p><a href="https://automattic.com"><img src="https://images.opencollective.com/automattic/d0ef3e1/logo.png" alt="Automattic" height="128"></a></p><h3>Gold Sponsors</h3>
361
361
  <p><a href="https://qlty.sh/"><img src="https://images.opencollective.com/qltysh/33d157d/logo.png" alt="Qlty Software" height="96"></a></p><h3>Silver Sponsors</h3>
362
362
  <p><a href="https://vite.dev/"><img src="https://images.opencollective.com/vite/d472863/logo.png" alt="Vite" height="64"></a> <a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/2d6c3b6/logo.png" alt="Liftoff" height="64"></a> <a href="https://stackblitz.com"><img src="https://avatars.githubusercontent.com/u/28635252" alt="StackBlitz" height="64"></a></p><h3>Bronze Sponsors</h3>
363
- <p><a href="https://cybozu.co.jp/"><img src="https://images.opencollective.com/cybozu/933e46d/logo.png" alt="Cybozu" height="32"></a> <a href="https://opensource.sap.com"><img src="https://avatars.githubusercontent.com/u/2531208" alt="SAP" height="32"></a> <a href="https://www.crawljobs.com/"><img src="https://images.opencollective.com/crawljobs-poland/fa43a17/logo.png" alt="CrawlJobs" height="32"></a> <a href="#"><img src="https://images.opencollective.com/aeriusventilations-org/avatar.png" alt="aeriusventilation's Org" height="32"></a> <a href="https://depot.dev"><img src="https://images.opencollective.com/depot/39125a1/logo.png" alt="Depot" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://www.gitbook.com"><img src="https://avatars.githubusercontent.com/u/7111340" alt="GitBook" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774" alt="HeroCoders" height="32"></a> <a href="https://www.lambdatest.com"><img src="https://avatars.githubusercontent.com/u/171592363" alt="TestMu AI Open Source Office (Formerly LambdaTest)" height="32"></a></p>
363
+ <p><a href="https://cybozu.co.jp/"><img src="https://images.opencollective.com/cybozu/933e46d/logo.png" alt="Cybozu" height="32"></a> <a href="https://opensource.sap.com"><img src="https://avatars.githubusercontent.com/u/2531208" alt="SAP" height="32"></a> <a href="https://www.crawljobs.com/"><img src="https://images.opencollective.com/crawljobs-poland/fa43a17/logo.png" alt="CrawlJobs" height="32"></a> <a href="https://depot.dev"><img src="https://images.opencollective.com/depot/39125a1/logo.png" alt="Depot" height="32"></a> <a href="https://icons8.com/"><img src="https://images.opencollective.com/icons8/7fa1641/logo.png" alt="Icons8" height="32"></a> <a href="https://discord.com"><img src="https://images.opencollective.com/discordapp/f9645d9/logo.png" alt="Discord" height="32"></a> <a href="https://www.gitbook.com"><img src="https://avatars.githubusercontent.com/u/7111340" alt="GitBook" height="32"></a> <a href="https://herocoders.com"><img src="https://avatars.githubusercontent.com/u/37549774" alt="HeroCoders" height="32"></a> <a href="https://citadel.co.jp"><img src="https://avatars.githubusercontent.com/u/75781367" alt="Citadel AI" height="32"></a> <a href="https://www.lambdatest.com"><img src="https://avatars.githubusercontent.com/u/171592363" alt="TestMu AI Open Source Office (Formerly LambdaTest)" height="32"></a></p>
364
364
  <h3>Technology Sponsors</h3>
365
365
  Technology sponsors allow us to use their products and services for free as part of a contribution to the open source ecosystem and our work.
366
366
  <p><a href="https://netlify.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/netlify-icon.svg" alt="Netlify" height="32"></a> <a href="https://algolia.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/algolia-icon.svg" alt="Algolia" height="32"></a> <a href="https://1password.com"><img src="https://raw.githubusercontent.com/eslint/eslint.org/main/src/assets/images/techsponsors/1password-icon.svg" alt="1Password" height="32"></a></p>
@@ -230,7 +230,7 @@ async function loadConfigFile(filePath, hasUnstableNativeNodeJsTSConfigFlag) {
230
230
  * the require cache only if the file has been changed.
231
231
  */
232
232
  if (importedConfigFileModificationTime.get(filePath) !== mtime) {
233
- delete require.cache[filePath];
233
+ delete require.cache?.[filePath];
234
234
  }
235
235
 
236
236
  const isTS = isFileTS(filePath);
@@ -224,15 +224,16 @@ function assertIsRuleSeverity(ruleId, value) {
224
224
  }
225
225
 
226
226
  /**
227
- * Validates that a given string is the form pluginName/objectName.
227
+ * Validates that a given string matches the "pluginName/memberPlaceholder" pattern.
228
228
  * @param {string} value The string to check.
229
+ * @param {string} memberPlaceholder The placeholder for the member portion of the expected format in the error message.
229
230
  * @returns {void}
230
- * @throws {TypeError} If the string isn't in the correct format.
231
+ * @throws {TypeError} If the string doesn't match the expected pattern.
231
232
  */
232
- function assertIsPluginMemberName(value) {
233
+ function assertIsPluginMemberName(value, memberPlaceholder) {
233
234
  if (!/[\w\-@$]+(?:\/[\w\-$]+)+$/iu.test(value)) {
234
235
  throw new TypeError(
235
- `Expected string in the form "pluginName/objectName" but found "${value}".`,
236
+ `Expected string in the form "pluginName/${memberPlaceholder}" but found "${value}".`,
236
237
  );
237
238
  }
238
239
  }
@@ -375,7 +376,9 @@ const languageOptionsSchema = {
375
376
  /** @type {ObjectPropertySchema} */
376
377
  const languageSchema = {
377
378
  merge: "replace",
378
- validate: assertIsPluginMemberName,
379
+ validate(value) {
380
+ assertIsPluginMemberName(value, "languageName");
381
+ },
379
382
  };
380
383
 
381
384
  /** @type {ObjectPropertySchema} */
@@ -430,7 +433,7 @@ const processorSchema = {
430
433
  merge: "replace",
431
434
  validate(value) {
432
435
  if (typeof value === "string") {
433
- assertIsPluginMemberName(value);
436
+ assertIsPluginMemberName(value, "processorName");
434
437
  } else if (value && typeof value === "object") {
435
438
  if (
436
439
  typeof value.preprocess !== "function" ||
@@ -31,7 +31,7 @@ function isCaseNode(node) {
31
31
 
32
32
  /**
33
33
  * Checks if a given node appears as the value of a PropertyDefinition node.
34
- * @param {ASTNode} node THe node to check.
34
+ * @param {ASTNode} node The node to check.
35
35
  * @returns {boolean} `true` if the node is a PropertyDefinition value,
36
36
  * false if not.
37
37
  */
@@ -77,8 +77,8 @@ function getLabel(node) {
77
77
  }
78
78
 
79
79
  /**
80
- * Checks whether or not a given logical expression node goes different path
81
- * between the `true` case and the `false` case.
80
+ * Checks whether a given logical expression node takes different paths for the
81
+ * `true` and `false` cases.
82
82
  * @param {ASTNode} node A node to check.
83
83
  * @returns {boolean} `true` if the node is a test of a choice statement.
84
84
  */
@@ -107,8 +107,8 @@ function isForkingByTrueOrFalse(node) {
107
107
  /**
108
108
  * Gets the boolean value of a given literal node.
109
109
  *
110
- * This is used to detect infinity loops (e.g. `while (true) {}`).
111
- * Statements preceded by an infinity loop are unreachable if the loop didn't
110
+ * This is used to detect infinite loops (e.g. `while (true) {}`).
111
+ * Statements preceded by an infinite loop are unreachable if the loop didn't
112
112
  * have any `break` statement.
113
113
  * @param {ASTNode} node A node to get.
114
114
  * @returns {boolean|undefined} a boolean value if the node is a Literal node,
@@ -221,8 +221,8 @@ function forwardCurrentToHead(analyzer, node) {
221
221
  }
222
222
 
223
223
  /**
224
- * Updates the current segment with empty.
225
- * This is called at the last of functions or the program.
224
+ * Updates the current segment with an empty array.
225
+ * This is called when a code path ends.
226
226
  * @param {CodePathAnalyzer} analyzer The instance.
227
227
  * @param {ASTNode} node The current AST node.
228
228
  * @returns {void}
@@ -504,7 +504,7 @@ function processCodePathToEnter(analyzer, node) {
504
504
 
505
505
  case "SwitchCase":
506
506
  /*
507
- * Fork if this node is after the 2st node in `cases`.
507
+ * Fork if this node is after the 1st node in `cases`.
508
508
  * It's similar to `else` blocks.
509
509
  * The next `test` node is processed in this path.
510
510
  */
@@ -626,10 +626,13 @@ function processCodePathToExit(analyzer, node) {
626
626
  case "ImportExpression":
627
627
  case "MemberExpression":
628
628
  case "NewExpression":
629
- case "YieldExpression":
630
629
  state.makeFirstThrowablePathInTryBlock();
631
630
  break;
632
631
 
632
+ case "YieldExpression":
633
+ state.makeYield();
634
+ break;
635
+
633
636
  case "WhileStatement":
634
637
  case "DoWhileStatement":
635
638
  case "ForStatement":
@@ -724,7 +727,7 @@ function postprocess(analyzer, node) {
724
727
  * a = () => {}
725
728
  * }
726
729
  *
727
- * In this case, The ArrowFunctionExpression code path is closed first
730
+ * In this case, the ArrowFunctionExpression code path is closed first,
728
731
  * and then we need to close the code path for the PropertyDefinition
729
732
  * value.
730
733
  */
@@ -569,11 +569,10 @@ class TryContext {
569
569
  this.position = "try";
570
570
 
571
571
  /**
572
- * If the `try` statement has a `finally` block, this affects how a
573
- * `return` statement behaves in the `try` block. Without `finally`,
574
- * `return` behaves as usual and doesn't require a fork; with `finally`,
575
- * `return` forks into the `finally` block, so we need a fork context
576
- * to track it.
572
+ * If the `try` statement has a `finally` block, this affects how return-like
573
+ * leaving paths behave in the `try` block. Without `finally`, they behave as
574
+ * usual and don't require a fork; with `finally`, they fork into the
575
+ * `finally` block, so we need a fork context to track them.
577
576
  * @type {ForkContext|null}
578
577
  */
579
578
  this.returnedForkContext = hasFinalizer
@@ -1755,6 +1754,23 @@ class CodePathState {
1755
1754
  this.forkContext.add(segments);
1756
1755
  }
1757
1756
 
1757
+ /**
1758
+ * Records abrupt resumption paths from a suspended `yield` expression,
1759
+ * then splits normal post-`yield` continuation into a fresh segment.
1760
+ * @returns {void}
1761
+ */
1762
+ makeYield() {
1763
+ const forkContext = this.forkContext;
1764
+ const leavingSegments = forkContext.head;
1765
+
1766
+ if (forkContext.reachable) {
1767
+ getReturnContext(this).returnedForkContext.add(leavingSegments);
1768
+ getThrowContext(this).thrownForkContext.add(leavingSegments);
1769
+
1770
+ forkContext.replaceHead(forkContext.makeNext(-1, -1));
1771
+ }
1772
+ }
1773
+
1758
1774
  /**
1759
1775
  * Makes a code path segment from the first throwable node to the `catch`
1760
1776
  * block or the `finally` block.
@@ -100,9 +100,10 @@ class CodePath {
100
100
  * Final code path segments that represent normal completion of the code path.
101
101
  * For functions, this means both explicit `return` statements and implicit returns,
102
102
  * such as the last reachable segment in a function that does not have an
103
- * explicit `return` as this implicitly returns `undefined`. For scripts,
104
- * modules, class field initializers, and class static blocks, this means
105
- * all lines of code have been executed.
103
+ * explicit `return` as this implicitly returns `undefined`, as well as
104
+ * return-like exits from suspended `yield` expressions. For scripts, modules,
105
+ * class field initializers, and class static blocks, this means all lines of
106
+ * code have been executed.
106
107
  * These segments are also present in `finalSegments`.
107
108
  * This is a passthrough to the underlying `CodePathState`.
108
109
  * @type {CodePathSegment[]}
@@ -112,7 +113,8 @@ class CodePath {
112
113
  }
113
114
 
114
115
  /**
115
- * Final code path segments that represent `throw` statements.
116
+ * Final code path segments that represent `throw` statements and throw-like
117
+ * exits from suspended `yield` expressions.
116
118
  * This is a passthrough to the underlying `CodePathState`.
117
119
  * These segments are also present in `finalSegments`.
118
120
  * @type {CodePathSegment[]}
@@ -32,10 +32,9 @@ class IdGenerator {
32
32
  next() {
33
33
  this.n = (1 + this.n) | 0;
34
34
 
35
- /* c8 ignore start */
36
35
  if (this.n < 0) {
37
36
  this.n = 1;
38
- } /* c8 ignore stop */
37
+ }
39
38
 
40
39
  return this.prefix + this.n;
41
40
  }
@@ -28,12 +28,18 @@ module.exports = {
28
28
  },
29
29
 
30
30
  create(context) {
31
+ const sourceCode = context.sourceCode;
32
+
31
33
  return {
32
34
  "NewExpression[callee.name='Promise'][arguments.0.async=true]"(
33
35
  node,
34
36
  ) {
37
+ if (!sourceCode.isGlobalReference(node.callee)) {
38
+ return;
39
+ }
40
+
35
41
  context.report({
36
- node: context.sourceCode.getFirstToken(
42
+ node: sourceCode.getFirstToken(
37
43
  node.arguments[0],
38
44
  token => token.value === "async",
39
45
  ),
@@ -612,7 +612,7 @@ module.exports = {
612
612
 
613
613
  /**
614
614
  * Checks the current context for shadowed variables.
615
- * @param {Scope} scope Fixme
615
+ * @param {Scope} scope The scope to check for shadowed variables.
616
616
  * @returns {void}
617
617
  */
618
618
  function checkForShadows(scope) {
@@ -5,6 +5,12 @@
5
5
 
6
6
  "use strict";
7
7
 
8
+ //------------------------------------------------------------------------------
9
+ // Requirements
10
+ //------------------------------------------------------------------------------
11
+
12
+ const astUtils = require("./utils/ast-utils");
13
+
8
14
  //------------------------------------------------------------------------------
9
15
  // Rule Definition
10
16
  //------------------------------------------------------------------------------
@@ -13,6 +19,7 @@
13
19
  module.exports = {
14
20
  meta: {
15
21
  type: "problem",
22
+ hasSuggestions: true,
16
23
 
17
24
  docs: {
18
25
  description: "Disallow unused private class members",
@@ -25,12 +32,168 @@ module.exports = {
25
32
  messages: {
26
33
  unusedPrivateClassMember:
27
34
  "'{{classMemberName}}' is defined but never used.",
35
+ removeUnusedPrivateClassMember:
36
+ "Remove unused private class member '{{classMemberName}}'.",
28
37
  },
29
38
  },
30
39
 
31
40
  create(context) {
41
+ const sourceCode = context.sourceCode;
32
42
  const trackedClasses = [];
33
43
 
44
+ /**
45
+ * Gets the start index of the line that contains a given token or node.
46
+ * @param {ASTNode|Token|Comment} nodeOrToken The token or node to check
47
+ * @returns {number} The line start index
48
+ */
49
+ function getLineStartIndex(nodeOrToken) {
50
+ return nodeOrToken.range[0] - nodeOrToken.loc.start.column;
51
+ }
52
+
53
+ /**
54
+ * Checks whether a token or node starts on its own line, preceded only by whitespace.
55
+ * @param {ASTNode|Token|Comment} nodeOrToken The token or node to check
56
+ * @returns {boolean} Whether the token or node starts on its own line
57
+ */
58
+ function startsOnOwnLine(nodeOrToken) {
59
+ return (
60
+ sourceCode.getTokenBefore(nodeOrToken, {
61
+ includeComments: true,
62
+ }).loc.end.line !== nodeOrToken.loc.start.line
63
+ );
64
+ }
65
+
66
+ /**
67
+ * Gets leading comments that are directly attached to a class member.
68
+ * @param {ASTNode} classMemberNode The class member node
69
+ * @returns {Comment[]} Leading comments to remove with the member
70
+ */
71
+ function getLeadingComments(classMemberNode) {
72
+ const commentsBefore =
73
+ sourceCode.getCommentsBefore(classMemberNode);
74
+ const lastNonLeadingCommentIndex = commentsBefore.findLastIndex(
75
+ (comment, index, self) => {
76
+ const next =
77
+ index < self.length - 1
78
+ ? self[index + 1]
79
+ : classMemberNode;
80
+
81
+ return (
82
+ !startsOnOwnLine(comment) ||
83
+ next.loc.start.line - comment.loc.end.line > 1
84
+ );
85
+ },
86
+ );
87
+
88
+ return commentsBefore.slice(lastNonLeadingCommentIndex + 1);
89
+ }
90
+
91
+ /**
92
+ * Checks whether a class member shares its line with another token.
93
+ * @param {ASTNode} classMemberNode The class member node
94
+ * @returns {boolean} Whether the member shares its line with another token
95
+ */
96
+ function sharesLineWithAnotherToken(classMemberNode) {
97
+ const previousToken = sourceCode.getTokenBefore(classMemberNode);
98
+ const nextToken = sourceCode.getTokenAfter(classMemberNode);
99
+
100
+ return (
101
+ previousToken.loc.end.line === classMemberNode.loc.start.line ||
102
+ nextToken.loc.start.line === classMemberNode.loc.end.line
103
+ );
104
+ }
105
+
106
+ /**
107
+ * Gets trailing comments that are directly attached to a class member.
108
+ * Same-line trailing comments are preserved when another token shares
109
+ * the line, because the comment might describe the remaining code rather
110
+ * than the unused member alone.
111
+ * @param {ASTNode} classMemberNode The class member node
112
+ * @returns {Comment[]} Trailing comments to remove with the member
113
+ */
114
+ function getTrailingComments(classMemberNode) {
115
+ if (sharesLineWithAnotherToken(classMemberNode)) {
116
+ return [];
117
+ }
118
+
119
+ return sourceCode
120
+ .getCommentsAfter(classMemberNode)
121
+ .filter(
122
+ comment =>
123
+ comment.loc.start.line === classMemberNode.loc.end.line,
124
+ );
125
+ }
126
+
127
+ /**
128
+ * Gets the token after which a semicolon should be inserted when removing a class member.
129
+ * @param {ASTNode} classMemberNode The member that would be removed
130
+ * @returns {Token|null} The token after which a semicolon should be inserted, or null if no semicolon is needed
131
+ */
132
+ function getSemicolonInsertionToken(classMemberNode) {
133
+ const nextToken = sourceCode.getTokenAfter(classMemberNode);
134
+
135
+ if (
136
+ astUtils.canContinueExpressionInClassBody(nextToken) &&
137
+ astUtils.needsPrecedingSemicolon(sourceCode, classMemberNode)
138
+ ) {
139
+ return sourceCode.getTokenBefore(classMemberNode);
140
+ }
141
+
142
+ return null;
143
+ }
144
+
145
+ /**
146
+ * Gets the replacement range for removing an unused class member.
147
+ * @param {ASTNode} classMemberNode The member that would be removed
148
+ * @returns {number[]} The text range to remove
149
+ */
150
+ function getMemberRemovalRange(classMemberNode) {
151
+ const leadingComments = getLeadingComments(classMemberNode);
152
+ const trailingComments = getTrailingComments(classMemberNode);
153
+ const shouldRemoveLeadingComments =
154
+ leadingComments.length > 0 &&
155
+ !sharesLineWithAnotherToken(classMemberNode);
156
+ const lastItemToRemove =
157
+ trailingComments.length > 0
158
+ ? trailingComments.at(-1)
159
+ : classMemberNode;
160
+
161
+ const previousToken = sourceCode.getTokenBefore(classMemberNode);
162
+ const nextToken = sourceCode.getTokenAfter(lastItemToRemove, {
163
+ includeComments: true,
164
+ });
165
+ const nextTokenStartsOnNewLine =
166
+ nextToken.loc.start.line > lastItemToRemove.loc.end.line;
167
+ const shouldRemoveOwnLine =
168
+ !shouldRemoveLeadingComments &&
169
+ startsOnOwnLine(classMemberNode) &&
170
+ nextTokenStartsOnNewLine;
171
+ let start = classMemberNode.range[0];
172
+ let end = lastItemToRemove.range[1];
173
+
174
+ if (shouldRemoveLeadingComments) {
175
+ start = nextTokenStartsOnNewLine
176
+ ? getLineStartIndex(leadingComments[0])
177
+ : leadingComments[0].range[0];
178
+ end = nextTokenStartsOnNewLine
179
+ ? getLineStartIndex(nextToken)
180
+ : nextToken.range[0];
181
+ } else if (shouldRemoveOwnLine) {
182
+ start = getLineStartIndex(classMemberNode);
183
+ end = getLineStartIndex(nextToken);
184
+ } else if (
185
+ previousToken.loc.end.line === classMemberNode.loc.start.line
186
+ ) {
187
+ start = previousToken.range[1];
188
+ } else if (
189
+ nextToken.loc.start.line === lastItemToRemove.loc.end.line
190
+ ) {
191
+ end = nextToken.range[0];
192
+ }
193
+
194
+ return [start, end];
195
+ }
196
+
34
197
  /**
35
198
  * Check whether the current node is in a write only assignment.
36
199
  * @param {ASTNode} privateIdentifierNode Node referring to a private identifier
@@ -86,6 +249,7 @@ module.exports = {
86
249
  if (bodyMember.key.type === "PrivateIdentifier") {
87
250
  privateMembers.set(bodyMember.key.name, {
88
251
  declaredNode: bodyMember,
252
+ hasReference: false,
89
253
  isAccessor:
90
254
  bodyMember.type === "MethodDefinition" &&
91
255
  (bodyMember.kind === "set" ||
@@ -128,6 +292,8 @@ module.exports = {
128
292
  return;
129
293
  }
130
294
 
295
+ memberDefinition.hasReference = true;
296
+
131
297
  /*
132
298
  * Any usage of an accessor is considered a read, as the getter/setter can have
133
299
  * side-effects in its definition.
@@ -199,11 +365,12 @@ module.exports = {
199
365
 
200
366
  for (const [
201
367
  classMemberName,
202
- { declaredNode, isUsed },
368
+ { declaredNode, hasReference, isUsed },
203
369
  ] of unusedPrivateMembers.entries()) {
204
370
  if (isUsed) {
205
371
  continue;
206
372
  }
373
+
207
374
  context.report({
208
375
  node: declaredNode,
209
376
  loc: declaredNode.key.loc,
@@ -211,6 +378,39 @@ module.exports = {
211
378
  data: {
212
379
  classMemberName: `#${classMemberName}`,
213
380
  },
381
+ suggest: [
382
+ {
383
+ messageId: "removeUnusedPrivateClassMember",
384
+ data: {
385
+ classMemberName: `#${classMemberName}`,
386
+ },
387
+ *fix(fixer) {
388
+ if (hasReference) {
389
+ return;
390
+ }
391
+
392
+ const removalRange =
393
+ getMemberRemovalRange(declaredNode);
394
+ const semicolonInsertionToken =
395
+ getSemicolonInsertionToken(
396
+ declaredNode,
397
+ );
398
+ const removalFix = fixer.replaceTextRange(
399
+ removalRange,
400
+ "",
401
+ );
402
+
403
+ yield removalFix;
404
+
405
+ if (semicolonInsertionToken) {
406
+ yield fixer.insertTextAfter(
407
+ semicolonInsertionToken,
408
+ ";",
409
+ );
410
+ }
411
+ },
412
+ },
413
+ ],
214
414
  });
215
415
  }
216
416
  },
@@ -33,7 +33,7 @@ function isConcatOperatorToken(token) {
33
33
  }
34
34
 
35
35
  /**
36
- * Get's the right most node on the left side of a BinaryExpression with + operator.
36
+ * Gets the right most node on the left side of a BinaryExpression with + operator.
37
37
  * @param {ASTNode} node A BinaryExpression node to check.
38
38
  * @returns {ASTNode} node
39
39
  */
@@ -47,7 +47,7 @@ function getLeft(node) {
47
47
  }
48
48
 
49
49
  /**
50
- * Get's the left most node on the right side of a BinaryExpression with + operator.
50
+ * Gets the left most node on the right side of a BinaryExpression with + operator.
51
51
  * @param {ASTNode} node A BinaryExpression node to check.
52
52
  * @returns {ASTNode} node
53
53
  */
@@ -237,8 +237,9 @@ module.exports = {
237
237
  const nextToken =
238
238
  sourceCode.getTokenAfter(node);
239
239
  const addSemiColon =
240
- nextToken.type === "Punctuator" &&
241
- nextToken.value === "[" &&
240
+ astUtils.canContinueExpressionInClassBody(
241
+ nextToken,
242
+ ) &&
242
243
  astUtils.needsPrecedingSemicolon(
243
244
  sourceCode,
244
245
  node,
@@ -110,12 +110,14 @@ module.exports = {
110
110
  */
111
111
  const nextToken = sourceCode.getTokenAfter(asyncToken);
112
112
  const addSemiColon =
113
- nextToken.type === "Punctuator" &&
114
- (nextToken.value === "[" || nextToken.value === "(") &&
115
- (nodeWithAsyncKeyword.type === "MethodDefinition" ||
113
+ ((astUtils.isOpeningParenToken(nextToken) &&
116
114
  astUtils.isStartOfExpressionStatement(
117
115
  nodeWithAsyncKeyword,
118
- )) &&
116
+ )) ||
117
+ (nodeWithAsyncKeyword.type === "MethodDefinition" &&
118
+ astUtils.canContinueExpressionInClassBody(
119
+ nextToken,
120
+ ))) &&
119
121
  astUtils.needsPrecedingSemicolon(
120
122
  sourceCode,
121
123
  nodeWithAsyncKeyword,
@@ -1258,12 +1258,31 @@ function isStartOfExpressionStatement(node) {
1258
1258
  }
1259
1259
 
1260
1260
  /**
1261
- * Determines whether an opening parenthesis `(`, bracket `[` or backtick ``` ` ``` needs to be preceded by a semicolon.
1262
- * This opening parenthesis or bracket should be at the start of an `ExpressionStatement`, a `MethodDefinition` or at
1263
- * the start of the body of an `ArrowFunctionExpression`.
1261
+ * Checks whether a token can cause continuation of a preceding expression
1262
+ * (for example, of a class field initializer expression) in a class body.
1263
+ * This function checks specifically for tokens that can appear at the start
1264
+ * of a class member: `[` (computed key), `*` (generator method), `in` or `instanceof` (valid keys)
1265
+ * Without a preceding semicolon, these tokens would be parsed as index access or operators.
1266
+ * @param {Token} token The token to check.
1267
+ * @returns {boolean} Whether the token can cause continuation of a preceding expression.
1268
+ */
1269
+ function canContinueExpressionInClassBody(token) {
1270
+ return (
1271
+ (token.type === "Punctuator" &&
1272
+ (token.value === "[" || token.value === "*")) ||
1273
+ // Different parsers may return these tokens as either "Identifier" or "Keyword"
1274
+ ((token.type === "Identifier" || token.type === "Keyword") &&
1275
+ (token.value === "in" || token.value === "instanceof"))
1276
+ );
1277
+ }
1278
+
1279
+ /**
1280
+ * Determines whether an opening parenthesis `(`, bracket `[`, asterisk `*`, or backtick ``` ` ``` needs to be preceded by a semicolon.
1281
+ * This opening parenthesis or bracket should be at the start of an `ExpressionStatement`, a `MethodDefinition`, a `PropertyDefinition`,
1282
+ * or at the start of the body of an `ArrowFunctionExpression`.
1264
1283
  * @type {(sourceCode: SourceCode, node: ASTNode) => boolean}
1265
1284
  * @param {SourceCode} sourceCode The source code object.
1266
- * @param {ASTNode} node A node at the position where an opening parenthesis or bracket will be inserted.
1285
+ * @param {ASTNode} node A node at the position where an opening parenthesis, bracket, or asterisk will be inserted.
1267
1286
  * @returns {boolean} Whether a semicolon is required before the opening parenthesis or bracket.
1268
1287
  */
1269
1288
  let needsPrecedingSemicolon;
@@ -1354,6 +1373,18 @@ let needsPrecedingSemicolon;
1354
1373
 
1355
1374
  const prevNode = sourceCode.getNodeByRangeIndex(prevToken.range[0]);
1356
1375
 
1376
+ // Uninitialized class fields don't need a semicolon
1377
+ if (
1378
+ // Key
1379
+ (prevNode.parent.type === "PropertyDefinition" &&
1380
+ prevNode.parent.key === prevNode) ||
1381
+ // Closing bracket of a computed key
1382
+ (prevNode.type === "PropertyDefinition" &&
1383
+ isClosingBracketToken(prevToken))
1384
+ ) {
1385
+ return false;
1386
+ }
1387
+
1357
1388
  if (
1358
1389
  prevNode.type === "TSDeclareFunction" ||
1359
1390
  prevNode.parent.type === "TSImportEqualsDeclaration" ||
@@ -2842,6 +2873,7 @@ module.exports = {
2842
2873
  isTopLevelExpressionStatement,
2843
2874
  isDirective,
2844
2875
  isStartOfExpressionStatement,
2876
+ canContinueExpressionInClassBody,
2845
2877
  needsPrecedingSemicolon,
2846
2878
  isImportAttributeKey,
2847
2879
  getOpeningParenOfParams,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint",
3
- "version": "10.2.0",
3
+ "version": "10.3.0",
4
4
  "author": "Nicholas C. Zakas <nicholas+npm@nczconsulting.com>",
5
5
  "description": "An AST-based pattern checker for JavaScript.",
6
6
  "type": "commonjs",
@@ -72,6 +72,8 @@
72
72
  "test": "node Makefile.js test",
73
73
  "test:browser": "node Makefile.js cypress",
74
74
  "test:cli": "mocha",
75
+ "test:ecosystem": "node tools/test-ecosystem/index.mjs",
76
+ "test:ecosystem:update": "node tools/test-ecosystem/update.mjs",
75
77
  "test:emfile": "node tools/check-emfile-handling.js",
76
78
  "test:fuzz": "node Makefile.js fuzz",
77
79
  "test:performance": "node Makefile.js perf",
@@ -119,10 +121,10 @@
119
121
  "dependencies": {
120
122
  "@eslint-community/eslint-utils": "^4.8.0",
121
123
  "@eslint-community/regexpp": "^4.12.2",
122
- "@eslint/config-array": "^0.23.4",
123
- "@eslint/config-helpers": "^0.5.4",
124
- "@eslint/core": "^1.2.0",
125
- "@eslint/plugin-kit": "^0.7.0",
124
+ "@eslint/config-array": "^0.23.5",
125
+ "@eslint/config-helpers": "^0.5.5",
126
+ "@eslint/core": "^1.2.1",
127
+ "@eslint/plugin-kit": "^0.7.1",
126
128
  "@humanfs/node": "^0.16.6",
127
129
  "@humanwhocodes/module-importer": "^1.0.1",
128
130
  "@humanwhocodes/retry": "^0.4.2",
@@ -157,7 +159,7 @@
157
159
  "@eslint/json": "^1.2.0",
158
160
  "@types/esquery": "^1.5.4",
159
161
  "@types/node": "^22.13.14",
160
- "@typescript-eslint/parser": "^8.56.0",
162
+ "@typescript-eslint/parser": "^8.58.2",
161
163
  "babel-loader": "^8.0.5",
162
164
  "c8": "^11.0.0",
163
165
  "chai": "^4.0.1",
@@ -196,7 +198,7 @@
196
198
  "mocha": "^11.7.1",
197
199
  "node-polyfill-webpack-plugin": "^1.0.3",
198
200
  "npm-license": "^0.3.3",
199
- "prettier": "3.8.1",
201
+ "prettier": "3.8.3",
200
202
  "progress": "^2.0.3",
201
203
  "proxyquire": "^2.0.1",
202
204
  "recast": "^0.23.0",
@@ -204,7 +206,7 @@
204
206
  "semver": "^7.5.3",
205
207
  "shelljs": "^0.10.0",
206
208
  "sinon": "^11.0.0",
207
- "typescript": "^5.9.3",
209
+ "typescript": "^6.0.3",
208
210
  "webpack": "^5.23.0",
209
211
  "webpack-cli": "^4.5.0",
210
212
  "yorkie": "^2.0.0"