Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): [switch-exhaustiveness-check] add requireDefaultForNonUnion option #7880

Merged
merged 9 commits into from
Nov 19, 2023
23 changes: 22 additions & 1 deletion packages/eslint-plugin/docs/rules/switch-exhaustiveness-check.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
description: 'Require switch-case statements to be exhaustive with union type.'
description: 'Require switch-case statements to be exhaustive.'
---

> 🛑 This file is source code, not the primary documentation location! 🛑
Expand All @@ -11,6 +11,8 @@ However, if the union type changes, it's easy to forget to modify the cases to a

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

There is also an option to check the exhaustiveness of switches on non-union types by requiring a default clause.

## Examples

<!--tabs-->
Expand Down Expand Up @@ -101,6 +103,25 @@ switch (day) {
}
```

## Options

### `requireDefaultForNonUnion`

Examples of additional **incorrect** code for this rule with `{ requireDefaultForNonUnion: true }`:

```ts option='{ "requireDefaultForNonUnion": true }' showPlaygroundButton
const value: number = Math.floor(Math.random() * 3);

switch (value) {
case 0:
return 0;
case 1:
return 1;
}
```

Since `value` is a non-union type it requires the switch case to have a default clause.
ST-DDT marked this conversation as resolved.
Show resolved Hide resolved

## When Not To Use It

If you don't frequently `switch` over union types with many parts, or intentionally wish to leave out some parts.
62 changes: 55 additions & 7 deletions packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,47 @@ import {
requiresQuoting,
} from '../util';

export default createRule({
type MessageIds = 'switchIsNotExhaustive' | 'addMissingCases';
type Options = [
{
/**
* If `true`, require a `default` clause for switches on non-union types.
*
* @default false
*/
requireDefaultForNonUnion?: boolean;
},
];

export default createRule<Options, MessageIds>({
name: 'switch-exhaustiveness-check',
meta: {
type: 'suggestion',
docs: {
description:
'Require switch-case statements to be exhaustive with union type',
description: 'Require switch-case statements to be exhaustive',
requiresTypeChecking: true,
},
hasSuggestions: true,
schema: [],
schema: [
{
type: 'object',
additionalProperties: false,
properties: {
requireDefaultForNonUnion: {
description: `If 'true', require a 'default' clause for switches on non-union types.`,
type: 'boolean',
},
},
},
],
messages: {
switchIsNotExhaustive:
'Switch is not exhaustive. Cases not matched: {{missingBranches}}',
addMissingCases: 'Add branches for missing cases.',
},
},
defaultOptions: [],
create(context) {
defaultOptions: [{ requireDefaultForNonUnion: false }],
create(context, [{ requireDefaultForNonUnion }]) {
const sourceCode = context.getSourceCode();
const services = getParserServices(context);
const checker = services.program.getTypeChecker();
Expand All @@ -38,7 +60,7 @@ export default createRule({
function fixSwitch(
fixer: TSESLint.RuleFixer,
node: TSESTree.SwitchStatement,
missingBranchTypes: ts.Type[],
missingBranchTypes: (ts.Type | null)[], // null means default branch
symbolName?: string,
): TSESLint.RuleFix | null {
const lastCase =
Expand All @@ -51,6 +73,10 @@ export default createRule({

const missingCases = [];
for (const missingBranchType of missingBranchTypes) {
if (missingBranchType == null) {
missingCases.push(`default: { throw new Error('default case') }`);
continue;
}
// While running this rule on checker.ts of TypeScript project
// the fix introduced a compiler error due to:
//
Expand Down Expand Up @@ -163,6 +189,28 @@ export default createRule({
},
],
});
} else if (requireDefaultForNonUnion) {
const hasDefault = node.cases.some(
switchCase => switchCase.test == null,
);

if (!hasDefault) {
context.report({
node: node.discriminant,
messageId: 'switchIsNotExhaustive',
data: {
missingBranches: 'default',
},
suggest: [
{
messageId: 'addMissingCases',
fix(fixer): TSESLint.RuleFix | null {
ST-DDT marked this conversation as resolved.
Show resolved Hide resolved
return fixSwitch(fixer, node, [null]);
},
},
],
});
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,22 @@ function test(value: ObjectUnion): number {
}
}
`,
// switch with default clause on non-union type
{
code: `
const value: number = Math.floor(Math.random() * 3);
ST-DDT marked this conversation as resolved.
Show resolved Hide resolved

switch (value) {
case 0:
return 0;
case 1:
return 1;
default:
return -1;
}
`,
options: [{ requireDefaultForNonUnion: true }],
},
],
invalid: [
{
Expand Down Expand Up @@ -595,6 +611,40 @@ function test(arg: Enum): string {
case Enum['9test']: { throw new Error('Not implemented yet: Enum['9test'] case') }
case Enum.test: { throw new Error('Not implemented yet: Enum.test case') }
}
}
`,
},
],
},
],
},
{
code: `
const value: number = Math.floor(Math.random() * 3);

switch (value) {
case 0:
return 0;
case 1:
return 1;
}
`,
options: [{ requireDefaultForNonUnion: true }],
errors: [
{
messageId: 'switchIsNotExhaustive',
suggestions: [
{
messageId: 'addMissingCases',
output: `
const value: number = Math.floor(Math.random() * 3);

switch (value) {
case 0:
return 0;
case 1:
return 1;
default: { throw new Error('default case') }
}
`,
},
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.