Skip to content

Commit 1b7c36e

Browse files
Florian Breischsindresorhus
authored andcommitted
Add type-error rule - fixes #27 (#73)
1 parent c280b12 commit 1b7c36e

File tree

5 files changed

+500
-2
lines changed

5 files changed

+500
-2
lines changed

docs/rules/prefer-type-error.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Enforce throwing `TypeError` in type checking conditions
2+
3+
This rule enforces you to throw a `TypeError` after a type checking if-statement, instead of a generic `Error`.
4+
5+
It's aware of the most commonly used type checking operators and identifiers like `typeof`, `instanceof`, `.isString()`, etc, borrowed from [ES2017](https://tc39.github.io/ecma262/), [Underscore](http://underscorejs.org), [Lodash](https://lodash.com), and [jQuery](https://jquery.com). For a complete list of the recognized identifiers, please take a look at the [identifier-definition](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/master/rules/prefer-type-error.js#L3).
6+
7+
The rule investigates every throw-statement which throws a generic `Error`. It will fail if the throw-statement is the only expression in the surrounding block and is preceeded by an if-statement whose condition consists of type-checks exclusively.
8+
9+
In order to fix the violation, you have to replace the `Error` with a `TypeError`. Fortunately, this rule is able to `--fix` it for you.
10+
11+
12+
## Fail
13+
14+
```js
15+
if (Array.isArray(foo) === false) {
16+
throw new Error('Array expected');
17+
}
18+
```
19+
20+
```js
21+
if (Number.isNaN(foo) === false && Number.isInteger(foo) === false) {
22+
throw new Error('Integer expected');
23+
}
24+
```
25+
26+
```js
27+
if (isNaN(foo) === false) {
28+
throw new Error('Number expected');
29+
}
30+
```
31+
32+
```js
33+
if (typeof foo !== 'function' &&
34+
foo instanceof CookieMonster === false &&
35+
foo instanceof Unicorn === false) {
36+
throw new Error('Magic expected');
37+
}
38+
```
39+
40+
41+
## Pass
42+
43+
```js
44+
if (Array.isArray(foo) === false) {
45+
throw new TypeError('Array expected');
46+
}
47+
```
48+
49+
```js
50+
if (Number.isNaN(foo) === false && Number.isInteger(foo) === false) {
51+
throw new TypeError('Integer expected');
52+
}
53+
```
54+
55+
```js
56+
if (isNaN(foo) === false) {
57+
throw new TypeError('Number expected');
58+
}
59+
```
60+
61+
```js
62+
if (typeof foo !== 'function' &&
63+
foo instanceof CookieMonster === false &&
64+
foo instanceof Unicorn === false) {
65+
throw new TypeError('Magic expected');
66+
}
67+
```

index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ module.exports = {
2525
'unicorn/no-new-buffer': 'error',
2626
'unicorn/no-hex-escape': 'error',
2727
'unicorn/custom-error-definition': 'error',
28-
'unicorn/prefer-starts-ends-with': 'error'
28+
'unicorn/prefer-starts-ends-with': 'error',
29+
'unicorn/prefer-type-error': 'error'
2930
}
3031
}
3132
}

readme.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ Configure it in `package.json`.
4545
"unicorn/no-new-buffer": "error",
4646
"unicorn/no-hex-escape": "error",
4747
"unicorn/custom-error-definition": "error",
48-
"unicorn/prefer-starts-ends-with": "error"
48+
"unicorn/prefer-starts-ends-with": "error",
49+
"unicorn/prefer-type-error": "error"
4950
}
5051
}
5152
}
@@ -67,6 +68,7 @@ Configure it in `package.json`.
6768
- [no-hex-escape](docs/rules/no-hex-escape.md) - Enforce the use of unicode escapes instead of hexadecimal escapes. *(fixable)*
6869
- [custom-error-definition](docs/rules/custom-error-definition.md) - Enforces the only valid way of `Error` subclassing. *(fixable)*
6970
- [prefer-starts-ends-with](docs/rules/prefer-starts-ends-with.md) - Prefer `String#startsWidth` & `String#endsWidth` over more complex alternatives.
71+
- [prefer-type-error](docs/rules/prefer-type-error.md) - Enforce throwing `TypeError` in type checking conditions. *(fixable)*
7072

7173

7274
## Recommended config

rules/prefer-type-error.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
'use strict';
2+
3+
const tcIdentifiers = new Set([
4+
'isArguments',
5+
'isArray',
6+
'isArrayBuffer',
7+
'isArrayLike',
8+
'isArrayLikeObject',
9+
'isBoolean',
10+
'isBuffer',
11+
'isDate',
12+
'isElement',
13+
'isError',
14+
'isFinite',
15+
'isFunction',
16+
'isInteger',
17+
'isLength',
18+
'isMap',
19+
'isNaN',
20+
'isNative',
21+
'isNil',
22+
'isNull',
23+
'isNumber',
24+
'isObject',
25+
'isObjectLike',
26+
'isPlainObject',
27+
'isPrototypeOf',
28+
'isRegExp',
29+
'isSafeInteger',
30+
'isSet',
31+
'isString',
32+
'isSymbol',
33+
'isTypedArray',
34+
'isUndefined',
35+
'isView',
36+
'isWeakMap',
37+
'isWeakSet',
38+
'isWindow',
39+
'isXMLDoc'
40+
]);
41+
42+
const tcGlobalIdentifiers = new Set([
43+
'isNaN',
44+
'isFinite'
45+
]);
46+
47+
const isTypecheckingIdentifier = (node, callExpression, isMemberExpression) =>
48+
callExpression !== undefined &&
49+
callExpression.arguments.length > 0 &&
50+
node.type === 'Identifier' &&
51+
((isMemberExpression === true &&
52+
tcIdentifiers.has(node.name)) ||
53+
(isMemberExpression === false &&
54+
tcGlobalIdentifiers.has(node.name)));
55+
56+
const throwsErrorObject = node =>
57+
node.argument.type === 'NewExpression' &&
58+
node.argument.callee.type === 'Identifier' &&
59+
node.argument.callee.name === 'Error';
60+
61+
const isLone = node =>
62+
node.parent !== null &&
63+
node.parent.body !== null &&
64+
node.parent.body.length === 1;
65+
66+
const isTypecheckingMemberExpression = (node, callExpression) => {
67+
if (isTypecheckingIdentifier(node.property, callExpression, true)) {
68+
return true;
69+
}
70+
71+
if (node.object.type === 'MemberExpression') {
72+
return isTypecheckingMemberExpression(node.object, callExpression);
73+
}
74+
75+
return false;
76+
};
77+
78+
const isTypecheckingExpression = (node, callExpression) => {
79+
switch (node.type) {
80+
case 'Identifier':
81+
return isTypecheckingIdentifier(node, callExpression, false);
82+
case 'MemberExpression':
83+
return isTypecheckingMemberExpression(node, callExpression);
84+
case 'CallExpression':
85+
return isTypecheckingExpression(node.callee, node);
86+
case 'UnaryExpression':
87+
return node.operator === 'typeof' ||
88+
(node.operator === '!' &&
89+
isTypecheckingExpression(node.argument));
90+
case 'BinaryExpression':
91+
return node.operator === 'instanceof' ||
92+
isTypecheckingExpression(node.left, callExpression) ||
93+
isTypecheckingExpression(node.right, callExpression);
94+
case 'LogicalExpression':
95+
return isTypecheckingExpression(node.left, callExpression) &&
96+
isTypecheckingExpression(node.right, callExpression);
97+
default:
98+
return false;
99+
}
100+
};
101+
102+
const isTypechecking = node => node.type === 'IfStatement' && isTypecheckingExpression(node.test);
103+
104+
const create = context => {
105+
return {
106+
ThrowStatement: node => {
107+
if (throwsErrorObject(node) &&
108+
isLone(node) &&
109+
node.parent.parent !== null &&
110+
isTypechecking(node.parent.parent)) {
111+
context.report({
112+
node,
113+
message: '`new Error()` is too unspecific for a type check. Use `new TypeError()` instead.',
114+
fix: fixer => fixer.replaceText(node.argument.callee, 'TypeError')
115+
});
116+
}
117+
}
118+
};
119+
};
120+
121+
module.exports = {
122+
create,
123+
meta: {
124+
fixable: 'code'
125+
}
126+
};

0 commit comments

Comments
 (0)