Skip to content

Commit 9357b57

Browse files
SamVerschuerenjfmengels
authored andcommitted
Add custom-error-definition rule - fixes sindresorhus#74 (sindresorhus#75)
* add custom-error rule - fixes sindresorhus#74 * add extra tests * tweaks * tweaks
1 parent 6215de4 commit 9357b57

File tree

5 files changed

+545
-2
lines changed

5 files changed

+545
-2
lines changed

docs/rules/custom-error-definition.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Enforce correct Error subclassing
2+
3+
Enforces the only valid way of `Error` subclassing. It works with any super class that ends in `Error`.
4+
5+
6+
## Fail
7+
8+
```js
9+
class CustomError extends Error {
10+
constructor(message) {
11+
super(message);
12+
this.message = message;
13+
this.name = 'CustomError';
14+
}
15+
}
16+
```
17+
18+
The `this.message` assignment is useless as it's already set via the `super()` call.
19+
20+
21+
```js
22+
class CustomError extends Error {
23+
constructor(message) {
24+
super();
25+
this.message = message;
26+
this.name = 'CustomError';
27+
}
28+
}
29+
```
30+
31+
Pass the error message to `super()` instead of setting `this.message`.
32+
33+
34+
```js
35+
class CustomError extends Error {
36+
constructor(message) {
37+
super(message);
38+
}
39+
}
40+
```
41+
42+
No `name` property set. The name property is needed so the error shows up as `[CustomError: foo]` and not `[Error: foo]`.
43+
44+
45+
```js
46+
class CustomError extends Error {
47+
constructor(message) {
48+
super(message);
49+
this.name = this.constructor.name;
50+
}
51+
}
52+
```
53+
54+
Use a string literal to set the `name` property as it will not change after minifying.
55+
56+
57+
```js
58+
class CustomError extends Error {
59+
constructor(message) {
60+
super(message);
61+
this.name = 'MyError';
62+
}
63+
}
64+
```
65+
66+
The `name` property should be set to the class name.
67+
68+
69+
```js
70+
class foo extends Error {
71+
constructor(message) {
72+
super(message);
73+
this.name = 'foo';
74+
}
75+
}
76+
```
77+
78+
The class name is invalid. It should be capitalized and end with `Error`. In this case it should be `FooError`.
79+
80+
81+
## Pass
82+
83+
```js
84+
class CustomError extends Error {
85+
constructor(message) {
86+
super(message);
87+
this.name = 'CustomError';
88+
}
89+
}
90+
```
91+
92+
```js
93+
class CustomError extends Error {
94+
constructor() {
95+
super('My custom error');
96+
this.name = 'CustomError';
97+
}
98+
}
99+
```
100+
101+
```js
102+
class CustomError extends TypeError {
103+
constructor() {
104+
super();
105+
this.name = 'CustomError';
106+
}
107+
}
108+
```

index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ module.exports = {
2323
'unicorn/escape-case': 'error',
2424
'unicorn/no-array-instanceof': 'error',
2525
'unicorn/no-new-buffer': 'error',
26-
'unicorn/no-hex-escape': 'error'
26+
'unicorn/no-hex-escape': 'error',
27+
'unicorn/custom-error-definition': 'error'
2728
}
2829
}
2930
}

readme.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ Configure it in `package.json`.
4343
"unicorn/escape-case": "error",
4444
"unicorn/no-array-instanceof": "error",
4545
"unicorn/no-new-buffer": "error",
46-
"unicorn/no-hex-escape": "error"
46+
"unicorn/no-hex-escape": "error",
47+
"unicorn/custom-error-definition": "error"
4748
}
4849
}
4950
}
@@ -63,6 +64,7 @@ Configure it in `package.json`.
6364
- [no-array-instanceof](docs/rules/no-array-instanceof.md) - Require `Array.isArray()` instead of `instanceof Array`. *(fixable)*
6465
- [no-new-buffer](docs/rules/no-new-buffer.md) - Enforce the use of `Buffer.from()` and `Buffer.alloc()` instead of the deprecated `new Buffer()`. *(fixable)*
6566
- [no-hex-escape](docs/rules/no-hex-escape.md) - Enforce the use of unicode escapes instead of hexadecimal escapes. *(fixable)*
67+
- [custom-error-definition](docs/rules/custom-error-definition.md) - Enforces the only valid way of `Error` subclassing. *(fixable)*
6668

6769

6870
## Recommended config

rules/custom-error-definition.js

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
'use strict';
2+
const upperfirst = require('lodash.upperfirst');
3+
4+
const nameRegexp = /^(?:[A-Z][a-z0-9]*)*Error$/;
5+
6+
const getClassName = name => upperfirst(name).replace(/(error|)$/i, 'Error');
7+
8+
const getConstructorMethod = className => `
9+
constructor() {
10+
super();
11+
this.name = '${className}';
12+
}
13+
`;
14+
15+
const hasValidSuperClass = node => {
16+
if (!node.superClass) {
17+
return false;
18+
}
19+
20+
let name = node.superClass.name;
21+
22+
if (node.superClass.type === 'MemberExpression') {
23+
name = node.superClass.property.name;
24+
}
25+
26+
return nameRegexp.test(name);
27+
};
28+
29+
const isSuperExpression = node => node.type === 'ExpressionStatement' && node.expression.type === 'CallExpression' && node.expression.callee.type === 'Super';
30+
31+
const isAssignmentExpression = (node, name) => {
32+
if (node.type !== 'ExpressionStatement' || node.expression.type !== 'AssignmentExpression') {
33+
return false;
34+
}
35+
36+
const lhs = node.expression.left;
37+
38+
if (!lhs.object || lhs.object.type !== 'ThisExpression') {
39+
return false;
40+
}
41+
42+
return lhs.property.name === name;
43+
};
44+
45+
const create = context => {
46+
return {
47+
ClassDeclaration: node => {
48+
if (!hasValidSuperClass(node)) {
49+
return;
50+
}
51+
52+
const name = node.id.name;
53+
const className = getClassName(name);
54+
55+
if (name !== className) {
56+
context.report({
57+
node: node.id,
58+
message: `Invalid class name, use \`${className}\`.`
59+
});
60+
}
61+
62+
const body = node.body.body;
63+
64+
const constructor = body.find(x => x.kind === 'constructor');
65+
66+
if (!constructor) {
67+
context.report({
68+
node,
69+
message: 'Add a constructor to your error.',
70+
fix: fixer => fixer.insertTextAfterRange([
71+
node.body.start,
72+
node.body.start + 1
73+
], getConstructorMethod(name))
74+
});
75+
return;
76+
}
77+
78+
const constructorBodyNode = constructor.value.body;
79+
const constructorBody = constructorBodyNode.body;
80+
81+
const superExpression = constructorBody.find(isSuperExpression);
82+
const messageExpressionIndex = constructorBody.findIndex(x => isAssignmentExpression(x, 'message'));
83+
84+
if (!superExpression) {
85+
context.report({
86+
node: constructorBodyNode,
87+
message: 'Missing call to `super()` in constructor.'
88+
});
89+
} else if (messageExpressionIndex !== -1 && superExpression.expression.arguments.length === 0) {
90+
const rhs = constructorBody[messageExpressionIndex].expression.right;
91+
92+
context.report({
93+
node: superExpression,
94+
message: 'Pass the error message to `super()`.',
95+
fix: fixer => fixer.insertTextAfterRange([
96+
superExpression.start,
97+
superExpression.start + 6
98+
], rhs.raw || rhs.name)
99+
});
100+
}
101+
102+
if (messageExpressionIndex !== -1) {
103+
const expression = constructorBody[messageExpressionIndex];
104+
105+
context.report({
106+
node: expression,
107+
message: 'Pass the error message to `super()` instead of setting `this.message`.',
108+
fix: fixer => fixer.removeRange([
109+
messageExpressionIndex === 0 ? constructorBodyNode.start : constructorBody[messageExpressionIndex - 1].end,
110+
expression.end
111+
])
112+
});
113+
}
114+
115+
const nameExpression = constructorBody.find(x => isAssignmentExpression(x, 'name'));
116+
117+
if (!nameExpression || nameExpression.expression.right.value !== name) {
118+
context.report({
119+
node: nameExpression ? nameExpression.expression.right : constructorBodyNode,
120+
message: `The \`name\` property should be set to \`${name}\`.`
121+
});
122+
}
123+
}
124+
};
125+
};
126+
127+
module.exports = {
128+
create,
129+
meta: {
130+
fixable: 'code'
131+
}
132+
};

0 commit comments

Comments
 (0)