8000 feat(eslint-plugin): [switch-exhaustiveness-check] add requireDefault… · c0sta/typescript-eslint@4cfcd45 · GitHub
[go: up one dir, main page]

Skip to content

Commit 4cfcd45

Browse files
feat(eslint-plugin): [switch-exhaustiveness-check] add requireDefaultForNonUnion option (typescript-eslint#7880)
* rule(switch-exhaustiveness-check): add requireDefaultForNonUnion option * Apply suggestions from code review Co-authored-by: Josh Goldberg ✨ <git@joshuakgoldberg.com> * chore: apply suggestions to more locations * fix lint * Fix linting and ordering of md --------- Co-authored-by: Josh Goldberg ✨ <git@joshuakgoldberg.com>
1 parent 9034d17 commit 4cfcd45

File tree

4 files changed

+147
-13
lines changed

4 files changed

+147
-13
lines changed

packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
description: 'Require switch-case statements to be exhaustive with union types and enums.'
2+
description: 'Require switch-case statements to be exhaustive.'
33
---
44

55
> 🛑 This file is source code, not the primary documentation location! 🛑
@@ -11,6 +11,8 @@ However, if the union type or the enum changes, it's easy to forget to modify th
1111

1212
This rule reports when a `switch` statement over a value typed as a union of literals or as an enum is missing a case for any of those literal types and does not have a `default` clause.
1313

14+
There is also an option to check the exhaustiveness of switches on non-union types by requiring a default clause.
15+
1416
## Examples
1517

1618
When the switch doesn't have exhaustive cases, either filling them all out or adding a default will correct the rule's complaint.
@@ -179,6 +181,27 @@ switch (fruit) {
179181

180182
<!--/tabs-->
181183

184+
## Options
185+
186+
### `requireDefaultForNonUnion`
187+
188+
Examples of additional **incorrect** code for this rule with `{ requireDefaultForNonUnion: true }`:
189+
190+
```ts option='{ "requireDefaultForNonUnion": true }' showPlaygroundButton
191+
const value: number = Math.floor(Math.random() * 3);
192+
193+
switch (value) {
194+
case 0:
195+
return 0;
196+
case 1:
197+
return 1;
198+
}
199+
```
200+
201+
Since `value` is a non-union type it requires the switch case to have a default clause only with `requireDefaultForNonUnion` enabled.
202+
203+
<!--/tabs-->
204+
182205
## When Not To Use It
183206

184207
If you don't frequently `switch` over union types or enums with many parts, or intentionally wish to leave out some parts.

packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,47 @@ import {
1212
requiresQuoting,
1313
} from '../util';
1414

15-
export default createRule({
15+
type MessageIds = 'switchIsNotExhaustive' | 'addMissingCases';
16+
type Options = [
17+
{
18+
/**
19+
* If `true`, require a `default` clause for switches on non-union types.
20+
*
21+
* @default false
22+
*/
23+
requireDefaultForNonUnion?: boolean;
24+
},
25+
];
26+
27+
export default createRule<Options, MessageIds>({
1628
name: 'switch-exhaustiveness-check',
1729
meta: {
1830
type: 'suggestion',
1931
docs: {
20-
description:
21-
'Require switch-case statements to be exhaustive with union types and enums',
32+
description: 'Require switch-case statements to be exhaustive',
2233
requiresTypeChecking: true,
2334
},
2435
hasSuggestions: true,
25-
schema: [],
36+
schema: [
37+
{
38+
type: 'object',
39+
additionalProperties: false,
40+
properties: {
41+
requireDefaultForNonUnion: {
42+
description: `If 'true', require a 'default' clause for switches on non-union types.`,
43+
type: 'boolean',
44+
},
45+
},
46+
},
47+
],
2648
messages: {
2749
switchIsNotExhaustive:
2850
'Switch is not exhaustive. Cases not matched: {{missingBranches}}',
2951
addMissingCases: 'Add branches for missing cases.',
3052
},
3153
},
32-
defaultOptions: [],
33-
create(context) {
54+
defaultOptions: [{ requireDefaultForNonUnion: false }],
55+
create(context, [{ requireDefaultForNonUnion }]) {
3456
const sourceCode = getSourceCode(context);
3557
const services = getParserServices(context);
3658
const checker = services.program.getTypeChecker();
@@ -39,9 +61,9 @@ export default createRule({
3961
function fixSwitch(
4062
fixer: TSESLint.RuleFixer,
4163
node: TSESTree.SwitchStatement,
42-
missingBranchTypes: ts.Type[],
64+
missingBranchTypes: (ts.Type | null)[], // null means default branch
4365
symbolName?: string,
44-
): TSESLint.RuleFix | null {
66+
): TSESLint.RuleFix {
4567
const lastCase =
4668
node.cases.length > 0 ? node.cases[node.cases.length - 1] : null;
4769
const caseIndent = lastCase
@@ -52,6 +74,10 @@ export default createRule({
5274

5375
const missingCases = [];
5476
for (const missingBranchType of missingBranchTypes) {
77+
if (missingBranchType == null) {
78+
missingCases.push(`default: { throw new Error('default case') }`);
79+
continue;
80+
}
5581
// While running this rule on checker.ts of TypeScript project
5682
// the fix introduced a compiler error due to:
5783
//
@@ -159,7 +185,7 @@ export default createRule({
159185
suggest: [
160186
{
161187
messageId: 'addMissingCases',
162-
fix(fixer): TSESLint.RuleFix | null {
188+
fix(fixer): TSESLint.RuleFix {
163189
return fixSwitch(
164190
fixer,
165191
node,
@@ -170,6 +196,28 @@ export default createRule({
170196
},
171197
],
172198
});
199+
} else if (requireDefaultForNonUnion) {
200+
const hasDefault = node.cases.some(
201+
switchCase => switchCase.test == null,
202+
);
203+
204+
if (!hasDefault) {
205+
context.report({
206+
node: node.discriminant,
207+
messageId: 'switchIsNotExhaustive',
208+
data: {
209+
missingBranches: 'default',
210+
},
211+
suggest: [
212+
{
213+
messageId: 'addMissingCases',
214+
fix(fixer): TSESLint.RuleFix {
215+
return fixSwitch(fixer, node, [null]);
216+
},
217+
},
218+
],
219+
});
220+
}
173221
}
174222
}
175223

packages/eslint-plugin/tests/rules/switch-exhaustiveness-check.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,21 @@ function test(value: ObjectUnion): number {
209209
}
210210
}
211211
`,
212+
// switch with default clause on non-union type
213+
{
214+
code: `
215+
declare const value: number;
216+
switch (value) {
217+
case 0:
218+
return 0;
219+
case 1:
220+
return 1;
221+
default:
222+
return -1;
223+
}
224+
`,
225+
options: [{ requireDefaultForNonUnion: true }],
226+
},
212227
],
213228
invalid: [
214229
{
@@ -595,6 +610,38 @@ function test(arg: Enum): string {
595610
case Enum['9test']: { throw new Error('Not implemented yet: Enum[\\'9test\\'] case') }
596611
case Enum.test: { throw new Error('Not implemented yet: Enum.test case') }
597612
}
613+
}
614+
`,
615+
},
616+
],
617+
},
618+
],
619+
},
620+
{
621+
code: `
622+
const value: number = Math.floor(Math.random() * 3);
623+
switch (value) {
624+
case 0:
625+
return 0;
626+
case 1:
627+
return 1;
628+
}
629+
`,
630+
options: [{ requireDefaultForNonUnion: true }],
631+
errors: [
632+
{
633+
messageId: 'switchIsNotExhaustive',
634+
suggestions: [
635+
{
636+
messageId: 'addMissingCases',
637+
output: `
638+
const value: number = Math.floor(Math.random() * 3);
639+
switch (value) {
640+
case 0:
641+
return 0;
642+
case 1:
643+
return 1;
644+
default: { throw new Error('default case') }
598645
}
599646
`,
600647
},

packages/eslint-plugin/tests/schema-snapshots/switch-exhaustiveness-check.shot

Lines changed: 19 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
0