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 +1 -1
- package/lib/eslint/eslint.js +8 -4
- package/lib/linter/esquery.js +3 -0
- package/lib/rule-tester/rule-tester.js +3 -0
- package/lib/rules/array-callback-return.js +0 -1
- package/lib/rules/index.js +1 -0
- package/lib/rules/no-empty-function.js +19 -0
- package/lib/rules/no-empty-static-block.js +25 -1
- package/lib/rules/no-empty.js +37 -0
- package/lib/rules/no-eval.js +3 -1
- package/lib/rules/no-loss-of-precision.js +1 -1
- package/lib/rules/preserve-caught-error.js +509 -0
- package/lib/rules/strict.js +2 -1
- package/lib/services/suppressions-service.js +3 -0
- package/lib/types/index.d.ts +2 -0
- package/lib/types/rules.d.ts +21 -13
- package/package.json +3 -3
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://
|
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>
|
package/lib/eslint/eslint.js
CHANGED
@@ -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);
|
package/lib/linter/esquery.js
CHANGED
package/lib/rules/index.js
CHANGED
@@ -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
|
-
|
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
|
}
|
package/lib/rules/no-empty.js
CHANGED
@@ -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
|
},
|
package/lib/rules/no-eval.js
CHANGED
@@ -226,7 +226,9 @@ module.exports = {
|
|
226
226
|
|
227
227
|
Program(node) {
|
228
228
|
const scope = sourceCode.getScope(node),
|
229
|
-
features =
|
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
|
+
};
|
package/lib/rules/strict.js
CHANGED
@@ -101,7 +101,8 @@ module.exports = {
|
|
101
101
|
},
|
102
102
|
|
103
103
|
create(context) {
|
104
|
-
const ecmaFeatures =
|
104
|
+
const ecmaFeatures =
|
105
|
+
context.languageOptions.parserOptions.ecmaFeatures || {},
|
105
106
|
scopes = [],
|
106
107
|
classScopes = [];
|
107
108
|
let [mode] = context.options;
|
package/lib/types/index.d.ts
CHANGED
@@ -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 {
|
package/lib/types/rules.d.ts
CHANGED
@@ -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
|
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
|
69
|
+
allowImportNames?: string[];
|
76
70
|
importNames?: never;
|
77
71
|
importNamePattern?: never;
|
78
72
|
allowImportNamePattern?: never;
|
79
73
|
}
|
80
74
|
| {
|
81
|
-
allowImportNamePattern
|
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:
|
3445
|
-
|
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.
|
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.
|
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.
|
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",
|