eslint 9.34.0 → 9.35.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
@@ -328,7 +328,7 @@ to get your logo on our READMEs and [website](https://eslint.org/sponsors).
328
328
  <p><a href="https://automattic.com"><img src="https://images.opencollective.com/automattic/d0ef3e1/logo.png" alt="Automattic" height="128"></a> <a href="https://www.airbnb.com/"><img src="https://images.opencollective.com/airbnb/d327d66/logo.png" alt="Airbnb" height="128"></a></p><h3>Gold Sponsors</h3>
329
329
  <p><a href="https://qlty.sh/"><img src="https://images.opencollective.com/qltysh/33d157d/logo.png" alt="Qlty Software" height="96"></a> <a href="https://trunk.io/"><img src="https://images.opencollective.com/trunkio/fb92d60/avatar.png" alt="trunk.io" height="96"></a> <a href="https://shopify.engineering/"><img src="https://avatars.githubusercontent.com/u/8085" alt="Shopify" height="96"></a></p><h3>Silver Sponsors</h3>
330
330
  <p><a href="https://vite.dev/"><img src="https://images.opencollective.com/vite/e6d15e1/logo.png" alt="Vite" height="64"></a> <a href="https://liftoff.io/"><img src="https://images.opencollective.com/liftoff/5c4fa84/logo.png" alt="Liftoff" height="64"></a> <a href="https://americanexpress.io"><img src="https://avatars.githubusercontent.com/u/3853301" alt="American Express" 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>
331
- <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://sentry.io"><img src="https://github.com/getsentry.png" alt="Sentry" height="32"></a> <a href="https://www.crosswordsolver.org/anagram-solver/"><img src="https://images.opencollective.com/anagram-solver/2666271/logo.png" alt="Anagram Solver" 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://nx.dev"><img src="https://avatars.githubusercontent.com/u/23692104" alt="Nx" height="32"></a> <a href="https://opensource.mercedes-benz.com/"><img src="https://avatars.githubusercontent.com/u/34240465" alt="Mercedes-Benz Group" 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="LambdaTest" height="32"></a></p>
331
+ <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://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://nx.dev"><img src="https://avatars.githubusercontent.com/u/23692104" alt="Nx" height="32"></a> <a href="https://opensource.mercedes-benz.com/"><img src="https://avatars.githubusercontent.com/u/34240465" alt="Mercedes-Benz Group" 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="LambdaTest" height="32"></a></p>
332
332
  <h3>Technology Sponsors</h3>
333
333
  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.
334
334
  <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>
@@ -263,11 +263,15 @@ async function locateConfigFileToUse({ configFile, cwd }) {
263
263
 
264
264
  /**
265
265
  * Creates an error to be thrown when an array of results passed to `getRulesMetaForResults` was not created by the current engine.
266
+ * @param {Error|undefined} cause The original error that led to this symptom error being thrown. Might not always be available.
266
267
  * @returns {TypeError} An error object.
267
268
  */
268
- function createExtraneousResultsError() {
269
+ function createExtraneousResultsError(cause) {
269
270
  return new TypeError(
270
271
  "Results object was not created from this ESLint instance.",
272
+ {
273
+ cause,
274
+ },
271
275
  );
272
276
  }
273
277
 
@@ -543,7 +547,7 @@ function validateOptionCloneability(options) {
543
547
  })
544
548
  .sort();
545
549
  const error = new TypeError(
546
- `The ${uncloneableOptionKeys.length === 1 ? "option" : "options"} ${new Intl.ListFormat("en-US").format(uncloneableOptionKeys.map(key => `"${key}"`))} cannot be cloned. When concurrency is enabled, all options must be cloneable. Remove uncloneable options or use an options module.`,
550
+ `The ${uncloneableOptionKeys.length === 1 ? "option" : "options"} ${new Intl.ListFormat("en-US").format(uncloneableOptionKeys.map(key => `"${key}"`))} cannot be cloned. When concurrency is enabled, all options must be cloneable values (JSON values). Remove uncloneable options or use an options module.`,
547
551
  );
548
552
  error.code = "ESLINT_UNCLONEABLE_OPTIONS";
549
553
  throw error;
@@ -773,8 +777,8 @@ class ESLint {
773
777
  try {
774
778
  configs =
775
779
  configLoader.getCachedConfigArrayForFile(filePath);
776
- } catch {
777
- throw createExtraneousResultsError();
780
+ } catch (err) {
781
+ throw createExtraneousResultsError(err);
778
782
  }
779
783
 
780
784
  const config = configs.getConfig(filePath);
@@ -271,6 +271,9 @@ function tryParseSelector(selector) {
271
271
  ) {
272
272
  throw new SyntaxError(
273
273
  `Syntax error in selector "${selector}" at position ${err.location.start.offset}: ${err.message}`,
274
+ {
275
+ cause: err,
276
+ },
274
277
  );
275
278
  }
276
279
  throw err;
@@ -851,6 +851,9 @@ class RuleTester {
851
851
  } catch (err) {
852
852
  throw new Error(
853
853
  `Schema for rule ${ruleName} is invalid: ${err.message}`,
854
+ {
855
+ cause: err,
856
+ },
854
857
  );
855
858
  }
856
859
  }
@@ -233,7 +233,6 @@ module.exports = {
233
233
  url: "https://eslint.org/docs/latest/rules/array-callback-return",
234
234
  },
235
235
 
236
- // eslint-disable-next-line eslint-plugin/require-meta-has-suggestions -- false positive
237
236
  hasSuggestions: true,
238
237
 
239
238
  schema: [
@@ -293,6 +293,7 @@ module.exports = new LazyLoadingRuleMap(
293
293
  "prefer-rest-params": () => require("./prefer-rest-params"),
294
294
  "prefer-spread": () => require("./prefer-spread"),
295
295
  "prefer-template": () => require("./prefer-template"),
296
+ "preserve-caught-error": () => require("./preserve-caught-error"),
296
297
  "quote-props": () => require("./quote-props"),
297
298
  quotes: () => require("./quotes"),
298
299
  radix: () => require("./radix"),
@@ -105,6 +105,7 @@ module.exports = {
105
105
  meta: {
106
106
  dialects: ["javascript", "typescript"],
107
107
  language: "javascript",
108
+ hasSuggestions: true,
108
109
  type: "suggestion",
109
110
 
110
111
  defaultOptions: [{ allow: [] }],
@@ -131,6 +132,7 @@ module.exports = {
131
132
 
132
133
  messages: {
133
134
  unexpected: "Unexpected empty {{name}}.",
135
+ suggestComment: "Add comment inside empty {{name}}.",
134
136
  },
135
137
  },
136
138
 
@@ -204,6 +206,23 @@ module.exports = {
204
206
  loc: node.body.loc,
205
207
  messageId: "unexpected",
206
208
  data: { name },
209
+ suggest: [
210
+ {
211
+ messageId: "suggestComment",
212
+ data: { name },
213
+ fix(fixer) {
214
+ const range = [
215
+ node.body.range[0] + 1,
216
+ node.body.range[1] - 1,
217
+ ];
218
+
219
+ return fixer.replaceTextRange(
220
+ range,
221
+ " /* empty */ ",
222
+ );
223
+ },
224
+ },
225
+ ],
207
226
  });
208
227
  }
209
228
  }
@@ -11,6 +11,7 @@
11
11
  /** @type {import('../types').Rule.RuleModule} */
12
12
  module.exports = {
13
13
  meta: {
14
+ hasSuggestions: true,
14
15
  type: "suggestion",
15
16
 
16
17
  docs: {
@@ -23,6 +24,7 @@ module.exports = {
23
24
 
24
25
  messages: {
25
26
  unexpected: "Unexpected empty static block.",
27
+ suggestComment: "Add comment inside empty static block.",
26
28
  },
27
29
  },
28
30
 
@@ -32,14 +34,36 @@ module.exports = {
32
34
  return {
33
35
  StaticBlock(node) {
34
36
  if (node.body.length === 0) {
37
+ const openingBrace = sourceCode.getFirstToken(node, {
38
+ skip: 1,
39
+ });
35
40
  const closingBrace = sourceCode.getLastToken(node);
36
41
 
37
42
  if (
38
43
  sourceCode.getCommentsBefore(closingBrace).length === 0
39
44
  ) {
40
45
  context.report({
41
- node,
46
+ loc: {
47
+ start: openingBrace.loc.start,
48
+ end: closingBrace.loc.end,
49
+ },
42
50
  messageId: "unexpected",
51
+ suggest: [
52
+ {
53
+ messageId: "suggestComment",
54
+ fix(fixer) {
55
+ const range = [
56
+ openingBrace.range[1],
57
+ closingBrace.range[0],
58
+ ];
59
+
60
+ return fixer.replaceTextRange(
61
+ range,
62
+ " /* empty */ ",
63
+ );
64
+ },
65
+ },
66
+ ],
43
67
  });
44
68
  }
45
69
  }
@@ -104,10 +104,47 @@ module.exports = {
104
104
  typeof node.cases === "undefined" ||
105
105
  node.cases.length === 0
106
106
  ) {
107
+ const openingBrace = sourceCode.getTokenAfter(
108
+ node.discriminant,
109
+ astUtils.isOpeningBraceToken,
110
+ );
111
+
112
+ const closingBrace = sourceCode.getLastToken(node);
113
+
114
+ if (
115
+ sourceCode.commentsExistBetween(
116
+ openingBrace,
117
+ closingBrace,
118
+ )
119
+ ) {
120
+ return;
121
+ }
122
+
107
123
  context.report({
108
124
  node,
125
+ loc: {
126
+ start: openingBrace.loc.start,
127
+ end: closingBrace.loc.end,
128
+ },
109
129
  messageId: "unexpected",
110
130
  data: { type: "switch" },
131
+ suggest: [
132
+ {
133
+ messageId: "suggestComment",
134
+ data: { type: "switch" },
135
+ fix(fixer) {
136
+ const range = [
137
+ openingBrace.range[1],
138
+ closingBrace.range[0],
139
+ ];
140
+
141
+ return fixer.replaceTextRange(
142
+ range,
143
+ " /* empty */ ",
144
+ );
145
+ },
146
+ },
147
+ ],
111
148
  });
112
149
  }
113
150
  },
@@ -226,7 +226,9 @@ module.exports = {
226
226
 
227
227
  Program(node) {
228
228
  const scope = sourceCode.getScope(node),
229
- features = context.parserOptions.ecmaFeatures || {},
229
+ features =
230
+ context.languageOptions.parserOptions.ecmaFeatures ||
231
+ {},
230
232
  strict =
231
233
  scope.isStrict ||
232
234
  node.sourceType === "module" ||
@@ -189,7 +189,7 @@ module.exports = {
189
189
  * @returns {boolean} true if they do not match
190
190
  */
191
191
  function baseTenLosesPrecision(node) {
192
- const rawNumber = getRaw(node);
192
+ const rawNumber = getRaw(node).toLowerCase();
193
193
 
194
194
  /*
195
195
  * If trailing zeros equal the exponent, this is a valid representation
@@ -0,0 +1,509 @@
1
+ /**
2
+ * @fileoverview Rule to preserve caught errors when re-throwing exceptions
3
+ * @author Amnish Singh Arora
4
+ */
5
+ "use strict";
6
+
7
+ //------------------------------------------------------------------------------
8
+ // Requirements
9
+ //------------------------------------------------------------------------------
10
+
11
+ const astUtils = require("./utils/ast-utils");
12
+
13
+ //----------------------------------------------------------------------
14
+ // Helpers
15
+ //----------------------------------------------------------------------
16
+
17
+ /*
18
+ * This is an indicator of an error cause node, that is too complicated to be detected and fixed.
19
+ * Eg, when error options is an `Identifier` or a `SpreadElement`.
20
+ */
21
+ const UNKNOWN_CAUSE = Symbol("unknown_cause");
22
+
23
+ const BUILT_IN_ERROR_TYPES = new Set([
24
+ "Error",
25
+ "EvalError",
26
+ "RangeError",
27
+ "ReferenceError",
28
+ "SyntaxError",
29
+ "TypeError",
30
+ "URIError",
31
+ "AggregateError",
32
+ ]);
33
+
34
+ /**
35
+ * Finds and returns the ASTNode that is used as the `cause` of the Error being thrown
36
+ * @param {ASTNode} throwStatement `ThrowStatement` to be checked.
37
+ * @returns {ASTNode | UNKNOWN_CAUSE | null} The `cause` of `Error` being thrown, `null` if not set.
38
+ */
39
+ function getErrorCause(throwStatement) {
40
+ const throwExpression = throwStatement.argument;
41
+ /*
42
+ * Determine which argument index holds the options object
43
+ * `AggregateError` is a special case as it accepts the `options` object as third argument.
44
+ */
45
+ const optionsIndex =
46
+ throwExpression.callee.name === "AggregateError" ? 2 : 1;
47
+
48
+ /*
49
+ * Make sure there is no `SpreadElement` at or before the `optionsIndex`
50
+ * as this messes up the effective order of arguments and makes it complicated
51
+ * to track where the actual error options need to be at
52
+ */
53
+ const spreadExpressionIndex = throwExpression.arguments.findIndex(
54
+ arg => arg.type === "SpreadElement",
55
+ );
56
+ if (spreadExpressionIndex >= 0 && spreadExpressionIndex <= optionsIndex) {
57
+ return UNKNOWN_CAUSE;
58
+ }
59
+
60
+ const errorOptions = throwExpression.arguments[optionsIndex];
61
+
62
+ if (errorOptions) {
63
+ if (errorOptions.type === "ObjectExpression") {
64
+ if (
65
+ errorOptions.properties.some(
66
+ prop => prop.type === "SpreadElement",
67
+ )
68
+ ) {
69
+ /*
70
+ * If there is a spread element as part of error options, it is too complicated
71
+ * to verify if the cause is used properly and auto-fix.
72
+ */
73
+ return UNKNOWN_CAUSE;
74
+ }
75
+
76
+ const causeProperty = errorOptions.properties.find(
77
+ prop =>
78
+ prop.type === "Property" &&
79
+ prop.key.type === "Identifier" &&
80
+ prop.key.name === "cause" &&
81
+ !prop.computed, // It is hard to accurately identify the value of computed props
82
+ );
83
+
84
+ return causeProperty ? causeProperty.value : null;
85
+ }
86
+
87
+ // Error options exist, but too complicated to be analyzed/fixed
88
+ return UNKNOWN_CAUSE;
89
+ }
90
+
91
+ return null;
92
+ }
93
+
94
+ /**
95
+ * Finds and returns the `CatchClause` node, that the `node` is part of.
96
+ * @param {ASTNode} node The AST node to be evaluated.
97
+ * @returns {ASTNode | null } The closest parent `CatchClause` node, `null` if the `node` is not in a catch block.
98
+ */
99
+ function findParentCatch(node) {
100
+ let currentNode = node;
101
+
102
+ while (currentNode && currentNode.type !== "CatchClause") {
103
+ if (
104
+ [
105
+ "FunctionDeclaration",
106
+ "FunctionExpression",
107
+ "ArrowFunctionExpression",
108
+ "StaticBlock",
109
+ ].includes(currentNode.type)
110
+ ) {
111
+ /*
112
+ * Make sure the ThrowStatement is not made inside a function definition or a static block inside a high level catch.
113
+ * In such cases, the caught error is not directly related to the Throw.
114
+ *
115
+ * For example,
116
+ * try {
117
+ * } catch (error) {
118
+ * foo = {
119
+ * bar() {
120
+ * throw new Error();
121
+ * }
122
+ * };
123
+ * }
124
+ */
125
+ return null;
126
+ }
127
+ currentNode = currentNode.parent;
128
+ }
129
+
130
+ return currentNode;
131
+ }
132
+
133
+ //------------------------------------------------------------------------------
134
+ // Rule Definition
135
+ //------------------------------------------------------------------------------
136
+
137
+ /** @type {import('../types').Rule.RuleModule} */
138
+ module.exports = {
139
+ meta: {
140
+ type: "suggestion",
141
+ docs: {
142
+ description:
143
+ "Disallow losing originally caught error when re-throwing custom errors",
144
+ recommended: false,
145
+ url: "https://eslint.org/docs/latest/rules/preserve-caught-error", // URL to the documentation page for this rule
146
+ },
147
+ /*
148
+ * TODO: We should allow passing `customErrorTypes` option once something like `typescript-eslint`'s
149
+ * `TypeOrValueSpecifier` is implemented in core Eslint.
150
+ * See:
151
+ * 1. https://typescript-eslint.io/packages/type-utils/type-or-value-specifier/
152
+ * 2. https://github.com/eslint/eslint/pull/19913#discussion_r2192608593
153
+ * 3. https://github.com/eslint/eslint/discussions/16540
154
+ */
155
+ schema: [
156
+ {
157
+ type: "object",
158
+ properties: {
159
+ requireCatchParameter: {
160
+ type: "boolean",
161
+ default: false,
162
+ description:
163
+ "Requires the catch blocks to always have the caught error parameter so it is not discarded.",
164
+ },
165
+ },
166
+ additionalProperties: false,
167
+ },
168
+ ],
169
+ messages: {
170
+ missingCause:
171
+ "There is no `cause` attached to the symptom error being thrown.",
172
+ incorrectCause:
173
+ "The symptom error is being thrown with an incorrect `cause`.",
174
+ includeCause:
175
+ "Include the original caught error as the `cause` of the symptom error.",
176
+ missingCatchErrorParam:
177
+ "The caught error is not accessible because the catch clause lacks the error parameter. Start referencing the caught error using the catch parameter.",
178
+ partiallyLostError:
179
+ "Re-throws cannot preserve the caught error as a part of it is being lost due to destructuring.",
180
+ caughtErrorShadowed:
181
+ "The caught error is being attached as `cause`, but is shadowed by a closer scoped redeclaration.",
182
+ },
183
+ hasSuggestions: true,
184
+ },
185
+
186
+ create(context) {
187
+ const sourceCode = context.sourceCode;
188
+ const options = context.options[0] || {};
189
+
190
+ //----------------------------------------------------------------------
191
+ // Helpers
192
+ //----------------------------------------------------------------------
193
+
194
+ /**
195
+ * Checks if a `ThrowStatement` is constructing and throwing a new `Error` object.
196
+ *
197
+ * Covers all the error types on `globalThis` that support `cause` property:
198
+ * https://github.com/microsoft/TypeScript/blob/main/src/lib/es2022.error.d.ts
199
+ * @param {ASTNode} throwStatement The `ThrowStatement` that needs to be checked.
200
+ * @returns {boolean} `true` if a new "Error" is being thrown, else `false`.
201
+ */
202
+ function isThrowingNewError(throwStatement) {
203
+ return (
204
+ (throwStatement.argument.type === "NewExpression" ||
205
+ throwStatement.argument.type === "CallExpression") &&
206
+ throwStatement.argument.callee.type === "Identifier" &&
207
+ BUILT_IN_ERROR_TYPES.has(throwStatement.argument.callee.name) &&
208
+ /*
209
+ * Make sure the thrown Error is instance is one of the built-in global error types.
210
+ * Custom imports could shadow this, which would lead to false positives.
211
+ * e.g. import { Error } from "./my-custom-error.js";
212
+ * throw Error("Failed to perform error prone operations");
213
+ */
214
+ sourceCode.isGlobalReference(throwStatement.argument.callee)
215
+ );
216
+ }
217
+
218
+ /**
219
+ * Inserts `cause: <caughtErrorName>` into an inline options object expression.
220
+ * @param {RuleFixer} fixer The fixer object.
221
+ * @param {ASTNode} optionsNode The options object node.
222
+ * @param {string} caughtErrorName The name of the caught error (e.g., "err").
223
+ * @returns {Fix} The fix object.
224
+ */
225
+ function insertCauseIntoOptions(fixer, optionsNode, caughtErrorName) {
226
+ const properties = optionsNode.properties;
227
+
228
+ if (properties.length === 0) {
229
+ // Insert inside empty braces: `{}` → `{ cause: err }`
230
+ return fixer.insertTextAfter(
231
+ sourceCode.getFirstToken(optionsNode),
232
+ `cause: ${caughtErrorName}`,
233
+ );
234
+ }
235
+
236
+ const lastProp = properties.at(-1);
237
+ return fixer.insertTextAfter(
238
+ lastProp,
239
+ `, cause: ${caughtErrorName}`,
240
+ );
241
+ }
242
+
243
+ //----------------------------------------------------------------------
244
+ // Public
245
+ //----------------------------------------------------------------------
246
+ return {
247
+ ThrowStatement(node) {
248
+ // Check if the throw is inside a catch block
249
+ const parentCatch = findParentCatch(node);
250
+ const throwStatement = node;
251
+
252
+ // Check if a new error is being thrown in a catch block
253
+ if (parentCatch && isThrowingNewError(throwStatement)) {
254
+ if (
255
+ parentCatch.param &&
256
+ parentCatch.param.type !== "Identifier"
257
+ ) {
258
+ /*
259
+ * When a part of the caught error is being lost at the parameter level, commonly due to destructuring.
260
+ * e.g. catch({ message, ...rest })
261
+ */
262
+ context.report({
263
+ messageId: "partiallyLostError",
264
+ node: parentCatch,
265
+ });
266
+ return;
267
+ }
268
+
269
+ const caughtError =
270
+ parentCatch.param?.type === "Identifier"
271
+ ? parentCatch.param
272
+ : null;
273
+
274
+ // Check if there are throw statements and caught error is being ignored
275
+ if (!caughtError) {
276
+ if (options.requireCatchParameter) {
277
+ context.report({
278
+ node: throwStatement,
279
+ messageId: "missingCatchErrorParam",
280
+ });
281
+ return;
282
+ }
283
+ return;
284
+ }
285
+
286
+ // Check if there is a cause attached to the new error
287
+ const thrownErrorCause = getErrorCause(throwStatement);
288
+
289
+ if (thrownErrorCause === UNKNOWN_CAUSE) {
290
+ // Error options exist, but too complicated to be analyzed/fixed
291
+ return;
292
+ }
293
+
294
+ if (thrownErrorCause === null) {
295
+ // If there is no `cause` attached to the error being thrown.
296
+ context.report({
297
+ messageId: "missingCause",
298
+ node: throwStatement,
299
+ suggest: [
300
+ {
301
+ messageId: "includeCause",
302
+ fix(fixer) {
303
+ const throwExpression =
304
+ throwStatement.argument;
305
+ const args = throwExpression.arguments;
306
+ const errorType =
307
+ throwExpression.callee.name;
308
+
309
+ // AggregateError: errors, message, options
310
+ if (errorType === "AggregateError") {
311
+ const errorsArg = args[0];
312
+ const messageArg = args[1];
313
+ const optionsArg = args[2];
314
+
315
+ if (!errorsArg) {
316
+ // Case: `throw new AggregateError()` → insert all arguments
317
+ const lastToken =
318
+ sourceCode.getLastToken(
319
+ throwExpression,
320
+ );
321
+ const lastCalleeToken =
322
+ sourceCode.getLastToken(
323
+ throwExpression.callee,
324
+ );
325
+ const parenToken =
326
+ sourceCode.getFirstTokenBetween(
327
+ lastCalleeToken,
328
+ lastToken,
329
+ astUtils.isOpeningParenToken,
330
+ );
331
+
332
+ if (parenToken) {
333
+ return fixer.insertTextAfter(
334
+ parenToken,
335
+ `[], "", { cause: ${caughtError.name} }`,
336
+ );
337
+ }
338
+ return fixer.insertTextAfter(
339
+ throwExpression.callee,
340
+ `([], "", { cause: ${caughtError.name} })`,
341
+ );
342
+ }
343
+
344
+ if (!messageArg) {
345
+ // Case: `throw new AggregateError([])` → insert message and options
346
+ return fixer.insertTextAfter(
347
+ errorsArg,
348
+ `, "", { cause: ${caughtError.name} }`,
349
+ );
350
+ }
351
+
352
+ if (!optionsArg) {
353
+ // Case: `throw new AggregateError([], "")` → insert error options only
354
+ return fixer.insertTextAfter(
355
+ messageArg,
356
+ `, { cause: ${caughtError.name} }`,
357
+ );
358
+ }
359
+
360
+ if (
361
+ optionsArg.type ===
362
+ "ObjectExpression"
363
+ ) {
364
+ return insertCauseIntoOptions(
365
+ fixer,
366
+ optionsArg,
367
+ caughtError.name,
368
+ );
369
+ }
370
+
371
+ // Complex dynamic options — skip
372
+ return null;
373
+ }
374
+
375
+ // Normal Error types
376
+ const messageArg = args[0];
377
+ const optionsArg = args[1];
378
+
379
+ if (!messageArg) {
380
+ // Case: `throw new Error()` → insert both message and options
381
+ const lastToken =
382
+ sourceCode.getLastToken(
383
+ throwExpression,
384
+ );
385
+ const lastCalleeToken =
386
+ sourceCode.getLastToken(
387
+ throwExpression.callee,
388
+ );
389
+ const parenToken =
390
+ sourceCode.getFirstTokenBetween(
391
+ lastCalleeToken,
392
+ lastToken,
393
+ astUtils.isOpeningParenToken,
394
+ );
395
+
396
+ if (parenToken) {
397
+ return fixer.insertTextAfter(
398
+ parenToken,
399
+ `"", { cause: ${caughtError.name} }`,
400
+ );
401
+ }
402
+ return fixer.insertTextAfter(
403
+ throwExpression.callee,
404
+ `("", { cause: ${caughtError.name} })`,
405
+ );
406
+ }
407
+ if (!optionsArg) {
408
+ // Case: `throw new Error("Some message")` → insert only options
409
+ return fixer.insertTextAfter(
410
+ messageArg,
411
+ `, { cause: ${caughtError.name} }`,
412
+ );
413
+ }
414
+
415
+ if (
416
+ optionsArg.type ===
417
+ "ObjectExpression"
418
+ ) {
419
+ return insertCauseIntoOptions(
420
+ fixer,
421
+ optionsArg,
422
+ caughtError.name,
423
+ );
424
+ }
425
+
426
+ return null; // Identifier or spread — do not fix
427
+ },
428
+ },
429
+ ],
430
+ });
431
+
432
+ // We don't need to check further
433
+ return;
434
+ }
435
+
436
+ // If there is an attached cause, verify that is matches the caught error
437
+ if (
438
+ !(
439
+ thrownErrorCause.type === "Identifier" &&
440
+ thrownErrorCause.name === caughtError.name
441
+ )
442
+ ) {
443
+ context.report({
444
+ messageId: "incorrectCause",
445
+ node: thrownErrorCause,
446
+ suggest: [
447
+ {
448
+ messageId: "includeCause",
449
+ fix(fixer) {
450
+ /*
451
+ * In case `cause` is attached using object property shorthand or as a method.
452
+ * e.g. throw Error("fail", { cause });
453
+ * throw Error("fail", { cause() { // do something } });
454
+ */
455
+ if (
456
+ thrownErrorCause.parent.method ||
457
+ thrownErrorCause.parent.shorthand
458
+ ) {
459
+ return fixer.replaceText(
460
+ thrownErrorCause.parent,
461
+ `cause: ${caughtError.name}`,
462
+ );
463
+ }
464
+
465
+ return fixer.replaceText(
466
+ thrownErrorCause,
467
+ caughtError.name,
468
+ );
469
+ },
470
+ },
471
+ ],
472
+ });
473
+ return;
474
+ }
475
+
476
+ /*
477
+ * If the attached cause matches the identifier name of the caught error,
478
+ * make sure it is not being shadowed by a closer scoped redeclaration.
479
+ *
480
+ * e.g. try {
481
+ * doSomething();
482
+ * } catch (error) {
483
+ * if (whatever) {
484
+ * const error = anotherError;
485
+ * throw new Error("Something went wrong");
486
+ * }
487
+ * }
488
+ */
489
+ let scope = sourceCode.getScope(throwStatement);
490
+ do {
491
+ const variable = scope.set.get(caughtError.name);
492
+ if (variable) {
493
+ break;
494
+ }
495
+ scope = scope.upper;
496
+ } while (scope);
497
+
498
+ if (scope?.block !== parentCatch) {
499
+ // Caught error is being shadowed
500
+ context.report({
501
+ messageId: "caughtErrorShadowed",
502
+ node: throwStatement,
503
+ });
504
+ }
505
+ }
506
+ },
507
+ };
508
+ },
509
+ };
@@ -101,7 +101,8 @@ module.exports = {
101
101
  },
102
102
 
103
103
  create(context) {
104
- const ecmaFeatures = context.parserOptions.ecmaFeatures || {},
104
+ const ecmaFeatures =
105
+ context.languageOptions.parserOptions.ecmaFeatures || {},
105
106
  scopes = [],
106
107
  classScopes = [];
107
108
  let [mode] = context.options;
@@ -221,6 +221,9 @@ class SuppressionsService {
221
221
  }
222
222
  throw new Error(
223
223
  `Failed to parse suppressions file at ${this.filePath}`,
224
+ {
225
+ cause: err,
226
+ },
224
227
  );
225
228
  }
226
229
  }
@@ -2259,6 +2259,8 @@ export namespace RuleTester {
2259
2259
  only?: boolean;
2260
2260
  languageOptions?: Linter.LanguageOptions | undefined;
2261
2261
  settings?: { [name: string]: any } | undefined;
2262
+ before?: () => void;
2263
+ after?: () => void;
2262
2264
  }
2263
2265
 
2264
2266
  interface SuggestionOutput {
@@ -60,25 +60,19 @@ type EitherGroupOrRegEx =
60
60
  // Base type for import name specifiers, ensuring mutual exclusivity
61
61
  type EitherNameSpecifiers =
62
62
  | {
63
- importNames: string[];
63
+ importNames?: string[];
64
+ importNamePattern?: string;
64
65
  allowImportNames?: never;
65
- importNamePattern?: never;
66
- allowImportNamePattern?: never;
67
- }
68
- | {
69
- importNamePattern: string;
70
- allowImportNames?: never;
71
- importNames?: never;
72
66
  allowImportNamePattern?: never;
73
67
  }
74
68
  | {
75
- allowImportNames: string[];
69
+ allowImportNames?: string[];
76
70
  importNames?: never;
77
71
  importNamePattern?: never;
78
72
  allowImportNamePattern?: never;
79
73
  }
80
74
  | {
81
- allowImportNamePattern: string;
75
+ allowImportNamePattern?: string;
82
76
  importNames?: never;
83
77
  allowImportNames?: never;
84
78
  importNamePattern?: never;
@@ -3441,9 +3435,9 @@ export interface ESLintRules extends Linter.RulesRecord {
3441
3435
  paths: Array<
3442
3436
  string | ValidNoRestrictedImportPathOptions
3443
3437
  >;
3444
- patterns: Array<
3445
- string | ValidNoRestrictedImportPatternOptions
3446
- >;
3438
+ patterns:
3439
+ | Array<string>
3440
+ | Array<ValidNoRestrictedImportPatternOptions>;
3447
3441
  }>
3448
3442
  >,
3449
3443
  ]
@@ -4809,6 +4803,20 @@ export interface ESLintRules extends Linter.RulesRecord {
4809
4803
  */
4810
4804
  "prefer-template": Linter.RuleEntry<[]>;
4811
4805
 
4806
+ /**
4807
+ * Rule to disallow losing originally caught error when re-throwing custom errors.
4808
+ *
4809
+ * @since 9.35.0
4810
+ * @see https://eslint.org/docs/latest/rules/preserve-caught-error
4811
+ */
4812
+ "preserve-caught-error": Linter.RuleEntry<
4813
+ [
4814
+ Partial<{
4815
+ requireCatchParameter: boolean;
4816
+ }>,
4817
+ ]
4818
+ >;
4819
+
4812
4820
  /**
4813
4821
  * Rule to require quotes around object literal property names.
4814
4822
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint",
3
- "version": "9.34.0",
3
+ "version": "9.35.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",
@@ -104,13 +104,13 @@
104
104
  "homepage": "https://eslint.org",
105
105
  "bugs": "https://github.com/eslint/eslint/issues/",
106
106
  "dependencies": {
107
- "@eslint-community/eslint-utils": "^4.2.0",
107
+ "@eslint-community/eslint-utils": "^4.8.0",
108
108
  "@eslint-community/regexpp": "^4.12.1",
109
109
  "@eslint/config-array": "^0.21.0",
110
110
  "@eslint/config-helpers": "^0.3.1",
111
111
  "@eslint/core": "^0.15.2",
112
112
  "@eslint/eslintrc": "^3.3.1",
113
- "@eslint/js": "9.34.0",
113
+ "@eslint/js": "9.35.0",
114
114
  "@eslint/plugin-kit": "^0.3.5",
115
115
  "@humanfs/node": "^0.16.6",
116
116
  "@humanwhocodes/module-importer": "^1.0.1",