Skip to content

Commit fc900b6

Browse files
authored
Add no-useless-error-capture-stack-trace rule (#2676)
1 parent bc4dd3f commit fc900b6

8 files changed

+1168
-0
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Disallow unnecessary `Error.captureStackTrace(…)`
2+
3+
💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config).
4+
5+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6+
7+
<!-- end auto-generated rule header -->
8+
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` -->
9+
10+
Calling [`Error.captureStackTrace(…)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/captureStackTrace) inside the constructor of a built-in `Error` subclass is unnecessary, since the `Error` constructor calls it automatically.
11+
12+
## Examples
13+
14+
```js
15+
class MyError extends Error {
16+
constructor() {
17+
//
18+
Error.captureStackTrace(this, MyError);
19+
//
20+
Error.captureStackTrace?.(this, MyError);
21+
//
22+
Error.captureStackTrace(this, this.constructor);
23+
//
24+
Error.captureStackTrace?.(this, this.constructor);
25+
//
26+
Error.captureStackTrace(this, new.target);
27+
//
28+
Error.captureStackTrace?.(this, new.target);
29+
}
30+
}
31+
```

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
"eslint-remote-tester-repositories": "^2.0.1",
9696
"espree": "^10.4.0",
9797
"listr2": "^8.3.3",
98+
"lodash-es": "^4.17.21",
9899
"markdownlint-cli": "^0.45.0",
99100
"memoize": "^10.1.0",
100101
"nano-spawn": "^1.0.2",

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export default [
115115
| [no-unreadable-array-destructuring](docs/rules/no-unreadable-array-destructuring.md) | Disallow unreadable array destructuring. || 🔧 | |
116116
| [no-unreadable-iife](docs/rules/no-unreadable-iife.md) | Disallow unreadable IIFEs. || | |
117117
| [no-unused-properties](docs/rules/no-unused-properties.md) | Disallow unused object properties. | | | |
118+
| [no-useless-error-capture-stack-trace](docs/rules/no-useless-error-capture-stack-trace.md) | Disallow unnecessary `Error.captureStackTrace(…)`. || 🔧 | |
118119
| [no-useless-fallback-in-spread](docs/rules/no-useless-fallback-in-spread.md) | Disallow useless fallback when spreading in object literals. || 🔧 | |
119120
| [no-useless-length-check](docs/rules/no-useless-length-check.md) | Disallow useless array length check. || 🔧 | |
120121
| [no-useless-promise-resolve-reject](docs/rules/no-useless-promise-resolve-reject.md) | Disallow returning/yielding `Promise.resolve/reject()` in async functions or promise callbacks || 🔧 | |

rules/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export {default as 'no-unnecessary-slice-end'} from './no-unnecessary-slice-end.
5959
export {default as 'no-unreadable-array-destructuring'} from './no-unreadable-array-destructuring.js';
6060
export {default as 'no-unreadable-iife'} from './no-unreadable-iife.js';
6161
export {default as 'no-unused-properties'} from './no-unused-properties.js';
62+
export {default as 'no-useless-error-capture-stack-trace'} from './no-useless-error-capture-stack-trace.js';
6263
export {default as 'no-useless-fallback-in-spread'} from './no-useless-fallback-in-spread.js';
6364
export {default as 'no-useless-length-check'} from './no-useless-length-check.js';
6465
export {default as 'no-useless-promise-resolve-reject'} from './no-useless-promise-resolve-reject.js';
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import {findVariable} from '@eslint-community/eslint-utils';
2+
import {
3+
isMethodCall,
4+
isMemberExpression,
5+
} from './ast/index.js';
6+
import {} from './fix/index.js';
7+
import {} from './utils/index.js';
8+
import builtinErrors from './shared/builtin-errors.js';
9+
10+
const MESSAGE_ID_ERROR = 'no-useless-error-capture-stack-trace/error';
11+
const messages = {
12+
[MESSAGE_ID_ERROR]: 'Unnecessary `Error.captureStackTrace(…)` call.',
13+
};
14+
15+
// TODO: Make sure the super class is global
16+
// https://github.com/eslint/eslint/pull/19695
17+
const isSubclassOfBuiltinErrors = node =>
18+
node?.superClass
19+
&& node.superClass.type === 'Identifier'
20+
&& builtinErrors.includes(node.superClass.name);
21+
22+
const isClassReference = (node, classNode, context) => {
23+
// `new.target`
24+
if (
25+
node.type === 'MetaProperty'
26+
&& node.meta.type === 'Identifier'
27+
&& node.meta.name === 'new'
28+
&& node.property.type === 'Identifier'
29+
&& node.property.name === 'target'
30+
) {
31+
return true;
32+
}
33+
34+
// `this.constructor`
35+
if (
36+
isMemberExpression(node, {
37+
property: 'constructor',
38+
computed: false,
39+
optional: false,
40+
})
41+
&& node.object.type === 'ThisExpression'
42+
) {
43+
return true;
44+
}
45+
46+
if (node.type !== 'Identifier' || !classNode.id) {
47+
return false;
48+
}
49+
50+
const scope = context.sourceCode.getScope(node);
51+
const variable = findVariable(scope, node);
52+
53+
return variable
54+
&& variable.defs.length === 1
55+
&& variable.defs[0].type === 'ClassName'
56+
&& variable.defs[0].node === classNode;
57+
};
58+
59+
const isClassConstructor = (node, classNode) =>
60+
node
61+
&& node.parent.type === 'MethodDefinition'
62+
&& node.parent.kind === 'constructor'
63+
&& node.parent.value === node
64+
&& classNode.body.body.includes(node.parent);
65+
66+
/** @param {import('eslint').Rule.RuleContext} context */
67+
const create = context => {
68+
const classStack = [];
69+
const thisScopeStack = [];
70+
71+
context.on(['ClassDeclaration', 'ClassExpression'], classNode => {
72+
classStack.push(classNode);
73+
thisScopeStack.push(classNode);
74+
});
75+
76+
context.onExit(['ClassDeclaration', 'ClassExpression'], () => {
77+
classStack.pop();
78+
thisScopeStack.pop();
79+
});
80+
81+
context.on(['FunctionDeclaration', 'FunctionExpression'], functionNode => {
82+
thisScopeStack.push(functionNode);
83+
});
84+
85+
context.onExit(['FunctionDeclaration', 'FunctionExpression'], () => {
86+
thisScopeStack.pop();
87+
});
88+
89+
context.on('CallExpression', callExpression => {
90+
const errorClass = classStack.at(-1);
91+
92+
if (!(
93+
isSubclassOfBuiltinErrors(errorClass)
94+
&& isClassConstructor(thisScopeStack.at(-1), errorClass)
95+
&& isMethodCall(callExpression, {
96+
object: 'Error',
97+
method: 'captureStackTrace',
98+
argumentsLength: 2,
99+
optionalMember: false,
100+
})
101+
// TODO: Make sure the `object` is global
102+
)) {
103+
return;
104+
}
105+
106+
const [firstArgument, secondArgument] = callExpression.arguments;
107+
108+
if (
109+
firstArgument.type !== 'ThisExpression'
110+
|| !isClassReference(secondArgument, errorClass, context)
111+
) {
112+
return;
113+
}
114+
115+
const problem = {
116+
node: callExpression,
117+
messageId: MESSAGE_ID_ERROR,
118+
};
119+
120+
const maybeExpressionStatement = callExpression.parent === 'ChainExpression'
121+
? callExpression.parent.parent
122+
: callExpression.parent;
123+
124+
if (
125+
maybeExpressionStatement.type === 'ExpressionStatement'
126+
&& maybeExpressionStatement.parent.type === 'BlockStatement'
127+
) {
128+
problem.fix = fixer => fixer.remove(maybeExpressionStatement);
129+
}
130+
131+
return problem;
132+
});
133+
};
134+
135+
/** @type {import('eslint').Rule.RuleModule} */
136+
const config = {
137+
create,
138+
meta: {
139+
type: 'suggestion',
140+
docs: {
141+
description: 'Disallow unnecessary `Error.captureStackTrace(…)`.',
142+
recommended: true,
143+
},
144+
fixable: 'code',
145+
messages,
146+
},
147+
};
148+
149+
export default config;

0 commit comments

Comments
 (0)