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
25 changes: 24 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 types and enums.'
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 or the enum changes, it's easy to forget to modify th

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.

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

## Examples

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

<!--/tabs-->

## 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 only with `requireDefaultForNonUnion` enabled.

<!--/tabs-->

## When Not To Use It

If you don't frequently `switch` over union types or enums with many parts, or intentionally wish to leave out some parts.
66 changes: 57 additions & 9 deletions packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,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 types and enums',
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 = getSourceCode(context);
const services = getParserServices(context);
const checker = services.program.getTypeChecker();
Expand All @@ -39,9 +61,9 @@ 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 {
): TSESLint.RuleFix {
const lastCase =
node.cases.length > 0 ? node.cases[node.cases.length - 1] : null;
const caseIndent = lastCase
Expand All @@ -52,6 +74,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 @@ -159,7 +185,7 @@ export default createRule({
suggest: [
{
messageId: 'addMissingCases',
fix(fixer): TSESLint.RuleFix | null {
fix(fixer): TSESLint.RuleFix {
return fixSwitch(
fixer,
node,
Expand All @@ -170,6 +196,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 {
return fixSwitch(fixer, node, [null]);
},
},
],
});
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,21 @@ function test(value: ObjectUnion): number {
}
}
`,
// switch with default clause on non-union type
{
code: `
declare const value: number;
switch (value) {
case 0:
return 0;
case 1:
return 1;
default:
return -1;
}
`,
options: [{ requireDefaultForNonUnion: true }],
},
],
invalid: [
{
Expand Down Expand Up @@ -595,6 +610,38 @@ 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.