Skip to content

Commit 10e7a0c

Browse files
fiskersindresorhus
andauthored
Add prefer-switch rule (#1181)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent 3521a0a commit 10e7a0c

File tree

10 files changed

+2180
-52
lines changed

10 files changed

+2180
-52
lines changed

docs/rules/prefer-switch.md

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# Prefer `switch` over multiple `else-if`
2+
3+
A switch statement is easier to read than multiple if statements with simple equality comparisons.
4+
5+
This rule is partly fixable.
6+
7+
## Fail
8+
9+
```js
10+
if (foo === 1) {
11+
// 1
12+
} else if (foo === 2) {
13+
// 2
14+
} else if (foo === 3) {
15+
// 3
16+
} else {
17+
// default
18+
}
19+
```
20+
21+
## Pass
22+
23+
```js
24+
if (foo === 1) {
25+
// 1
26+
} else if (foo === 2) {
27+
// 2
28+
}
29+
```
30+
31+
```js
32+
switch (foo) {
33+
case 1: {
34+
// 1
35+
break;
36+
}
37+
case 2: {
38+
// 2
39+
break;
40+
}
41+
case 3: {
42+
// 3
43+
break;
44+
}
45+
default: {
46+
// default
47+
}
48+
}
49+
```
50+
51+
### `options`
52+
53+
Type: `object`
54+
55+
#### `minimumCases`
56+
57+
Type: `integer`\
58+
Minimum: `2`\
59+
Default: `3`
60+
61+
The minimum number of cases before reporting.
62+
63+
The `default` branch doesn't count. Multiple comparisons on the same `if` block is considered one case.
64+
65+
Examples:
66+
67+
```js
68+
// eslint unicorn/prefer-switch: ["error", {"minimumCases": 4}]
69+
if (foo === 1) {}
70+
else (foo === 2) {}
71+
else (foo === 3) {}
72+
73+
// Passes, only 3 cases.
74+
```
75+
76+
```js
77+
// eslint unicorn/prefer-switch: ["error", {"minimumCases": 4}]
78+
if (foo === 1) {}
79+
else (foo === 2) {}
80+
else (foo === 3) {}
81+
else {}
82+
83+
// Passes, only 3 cases.
84+
```
85+
86+
```js
87+
// eslint unicorn/prefer-switch: ["error", {"minimumCases": 4}]
88+
if (foo === 1) {}
89+
else if (foo === 2 || foo === 3) {}
90+
else if (foo === 4) {}
91+
92+
// Passes, only 3 cases.
93+
```
94+
95+
```js
96+
// eslint unicorn/prefer-switch: ["error", {"minimumCases": 2}]
97+
if (foo === 1) {}
98+
else if (foo === 2) {}
99+
100+
// Fails
101+
```
102+
103+
#### `emptyDefaultCase`
104+
105+
Type: `string`\
106+
Default: `'no-default-comment'`
107+
108+
To avoid conflict with the [`default-case`](https://eslint.org/docs/rules/default-case) rule, choose the fix style you prefer:
109+
110+
- `'no-default-comment'` (default)
111+
Insert `// No default` comment after last case.
112+
- `'do-nothing-comment'`
113+
Insert `default` case and add `// Do nothing` comment.
114+
- `'no-default-case'`
115+
Don't insert default case or comment.
116+
117+
```js
118+
if (foo === 1) {}
119+
else (foo === 2) {}
120+
else (foo === 3) {}
121+
```
122+
123+
Fixed
124+
125+
```js
126+
/* eslint unicorn/prefer-switch: ["error", { "emptyDefaultCase": "no-default-comment" }] */
127+
switch (foo) {
128+
case 1: {
129+
break;
130+
}
131+
case 2: {
132+
break;
133+
}
134+
case 3: {
135+
break;
136+
}
137+
// No default
138+
}
139+
```
140+
141+
```js
142+
/* eslint unicorn/prefer-switch: ["error", { "emptyDefaultCase": "do-nothing-comment" }] */
143+
switch (foo) {
144+
case 1: {
145+
break;
146+
}
147+
case 2: {
148+
break;
149+
}
150+
case 3: {
151+
break;
152+
}
153+
default:
154+
// Do nothing
155+
}
156+
```
157+
158+
```js
159+
/* eslint unicorn/prefer-switch: ["error", { "emptyDefaultCase": "no-default-case" }] */
160+
switch (foo) {
161+
case 1: {
162+
break;
163+
}
164+
case 2: {
165+
break;
166+
}
167+
case 3: {
168+
break;
169+
}
170+
}
171+
```

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ module.exports = {
112112
'unicorn/prefer-string-slice': 'error',
113113
'unicorn/prefer-string-starts-ends-with': 'error',
114114
'unicorn/prefer-string-trim-start-end': 'error',
115+
'unicorn/prefer-switch': 'error',
115116
'unicorn/prefer-ternary': 'error',
116117
'unicorn/prefer-type-error': 'error',
117118
'unicorn/prevent-abbreviations': 'error',

readme.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ Configure it in `package.json`.
102102
"unicorn/prefer-string-slice": "error",
103103
"unicorn/prefer-string-starts-ends-with": "error",
104104
"unicorn/prefer-string-trim-start-end": "error",
105+
"unicorn/prefer-switch": "error",
105106
"unicorn/prefer-ternary": "off",
106107
"unicorn/prefer-type-error": "error",
107108
"unicorn/prevent-abbreviations": "error",
@@ -192,6 +193,7 @@ Each rule has emojis denoting:
192193
| [prefer-string-slice](docs/rules/prefer-string-slice.md) | Prefer `String#slice()` over `String#substr()` and `String#substring()`. || 🔧 |
193194
| [prefer-string-starts-ends-with](docs/rules/prefer-string-starts-ends-with.md) | Prefer `String#startsWith()` & `String#endsWith()` over `RegExp#test()`. || 🔧 |
194195
| [prefer-string-trim-start-end](docs/rules/prefer-string-trim-start-end.md) | Prefer `String#trimStart()` / `String#trimEnd()` over `String#trimLeft()` / `String#trimRight()`. || 🔧 |
196+
| [prefer-switch](docs/rules/prefer-switch.md) | Prefer `switch` over multiple `else-if`. || 🔧 |
195197
| [prefer-ternary](docs/rules/prefer-ternary.md) | Prefer ternary expressions over simple `if-else` statements. || 🔧 |
196198
| [prefer-type-error](docs/rules/prefer-type-error.md) | Enforce throwing `TypeError` in type checking conditions. || 🔧 |
197199
| [prevent-abbreviations](docs/rules/prevent-abbreviations.md) | Prevent abbreviations. || 🔧 |

rules/prefer-string-slice.js

Lines changed: 67 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -72,34 +72,45 @@ const create = context => {
7272

7373
let sliceArguments;
7474

75-
if (argumentNodes.length === 0) {
76-
sliceArguments = [];
77-
} else if (argumentNodes.length === 1) {
78-
sliceArguments = [firstArgument];
79-
} else if (argumentNodes.length === 2) {
80-
if (firstArgument === '0') {
75+
switch (argumentNodes.length) {
76+
case 0: {
77+
sliceArguments = [];
78+
break;
79+
}
80+
81+
case 1: {
8182
sliceArguments = [firstArgument];
82-
if (isLiteralNumber(secondArgument) || isLengthProperty(argumentNodes[1])) {
83-
sliceArguments.push(secondArgument);
84-
} else if (typeof getNumericValue(argumentNodes[1]) === 'number') {
85-
sliceArguments.push(Math.max(0, getNumericValue(argumentNodes[1])));
86-
} else {
87-
sliceArguments.push(`Math.max(0, ${secondArgument})`);
88-
}
89-
} else if (
90-
isLiteralNumber(argumentNodes[0]) &&
91-
isLiteralNumber(argumentNodes[1])
92-
) {
93-
sliceArguments = [
94-
firstArgument,
95-
argumentNodes[0].value + argumentNodes[1].value
96-
];
97-
} else if (
98-
isLikelyNumeric(argumentNodes[0]) &&
83+
break;
84+
}
85+
86+
case 2: {
87+
if (firstArgument === '0') {
88+
sliceArguments = [firstArgument];
89+
if (isLiteralNumber(secondArgument) || isLengthProperty(argumentNodes[1])) {
90+
sliceArguments.push(secondArgument);
91+
} else if (typeof getNumericValue(argumentNodes[1]) === 'number') {
92+
sliceArguments.push(Math.max(0, getNumericValue(argumentNodes[1])));
93+
} else {
94+
sliceArguments.push(`Math.max(0, ${secondArgument})`);
95+
}
96+
} else if (
97+
isLiteralNumber(argumentNodes[0]) &&
98+
isLiteralNumber(argumentNodes[1])
99+
) {
100+
sliceArguments = [
101+
firstArgument,
102+
argumentNodes[0].value + argumentNodes[1].value
103+
];
104+
} else if (
105+
isLikelyNumeric(argumentNodes[0]) &&
99106
isLikelyNumeric(argumentNodes[1])
100-
) {
101-
sliceArguments = [firstArgument, firstArgument + ' + ' + secondArgument];
107+
) {
108+
sliceArguments = [firstArgument, firstArgument + ' + ' + secondArgument];
109+
}
110+
111+
break;
102112
}
113+
// No default
103114
}
104115

105116
if (sliceArguments) {
@@ -129,33 +140,45 @@ const create = context => {
129140

130141
let sliceArguments;
131142

132-
if (argumentNodes.length === 0) {
133-
sliceArguments = [];
134-
} else if (argumentNodes.length === 1) {
135-
if (firstNumber !== undefined) {
136-
sliceArguments = [Math.max(0, firstNumber)];
137-
} else if (isLengthProperty(argumentNodes[0])) {
138-
sliceArguments = [firstArgument];
139-
} else {
140-
sliceArguments = [`Math.max(0, ${firstArgument})`];
143+
switch (argumentNodes.length) {
144+
case 0: {
145+
sliceArguments = [];
146+
break;
141147
}
142-
} else if (argumentNodes.length === 2) {
143-
const secondNumber = getNumericValue(argumentNodes[1]);
144-
145-
if (firstNumber !== undefined && secondNumber !== undefined) {
146-
sliceArguments = firstNumber > secondNumber ?
147-
[Math.max(0, secondNumber), Math.max(0, firstNumber)] :
148-
[Math.max(0, firstNumber), Math.max(0, secondNumber)];
149-
} else if (firstNumber === 0 || secondNumber === 0) {
150-
sliceArguments = [0, `Math.max(0, ${firstNumber === 0 ? secondArgument : firstArgument})`];
151-
} else {
148+
149+
case 1: {
150+
if (firstNumber !== undefined) {
151+
sliceArguments = [Math.max(0, firstNumber)];
152+
} else if (isLengthProperty(argumentNodes[0])) {
153+
sliceArguments = [firstArgument];
154+
} else {
155+
sliceArguments = [`Math.max(0, ${firstArgument})`];
156+
}
157+
158+
break;
159+
}
160+
161+
case 2: {
162+
const secondNumber = getNumericValue(argumentNodes[1]);
163+
164+
if (firstNumber !== undefined && secondNumber !== undefined) {
165+
sliceArguments = firstNumber > secondNumber ?
166+
[Math.max(0, secondNumber), Math.max(0, firstNumber)] :
167+
[Math.max(0, firstNumber), Math.max(0, secondNumber)];
168+
} else if (firstNumber === 0 || secondNumber === 0) {
169+
sliceArguments = [0, `Math.max(0, ${firstNumber === 0 ? secondArgument : firstArgument})`];
170+
} else {
152171
// As values aren't Literal, we can not know whether secondArgument will become smaller than the first or not, causing an issue:
153172
// .substring(0, 2) and .substring(2, 0) returns the same result
154173
// .slice(0, 2) and .slice(2, 0) doesn't return the same result
155174
// There's also an issue with us now knowing whether the value will be negative or not, due to:
156175
// .substring() treats a negative number the same as it treats a zero.
157176
// The latter issue could be solved by wrapping all dynamic numbers in Math.max(0, <value>), but the resulting code would not be nice
177+
}
178+
179+
break;
158180
}
181+
// No default
159182
}
160183

161184
if (sliceArguments) {

0 commit comments

Comments
 (0)