Skip to content

fix(eslint-plugin): [switch-exhaustive-check] handle infinite types #6922

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

Closed
wants to merge 14 commits into from
26 changes: 25 additions & 1 deletion packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
import { isTypeFlagSet, unionTypeParts } from 'tsutils';
import { isLiteralType, isTypeFlagSet, unionTypeParts } from 'tsutils';
import * as ts from 'typescript';

import {
Expand Down Expand Up @@ -113,13 +113,30 @@ export default createRule({
);
}

function isTypeFinite(type: ts.Type): boolean {
return (
// Check literal or unique symbol
isTypeFlagSet(
type,
ts.TypeFlags.Literal | ts.TypeFlags.UniqueESSymbol,
) &&
// Cannot be a general primitive
!isTypeFlagSet(
type,
ts.TypeFlags.Boolean | ts.TypeFlags.Number | ts.TypeFlags.String,
)
);
}

function checkSwitchExhaustive(node: TSESTree.SwitchStatement): void {
const discriminantType = getNodeType(node.discriminant);
const symbolName = discriminantType.getSymbol()?.escapedName;

if (discriminantType.isUnion()) {
const unionTypes = unionTypeParts(discriminantType);

const caseTypes: Set<ts.Type> = new Set();

for (const switchCase of node.cases) {
if (switchCase.test == null) {
// Switch has 'default' branch - do nothing.
Expand All @@ -129,6 +146,13 @@ export default createRule({
caseTypes.add(getNodeType(switchCase.test));
}

const isFinite = unionTypes.every(isTypeFinite);

if (!isFinite) {
// Union type is not finite - do nothing.
return;
}

const missingBranchTypes = unionTypes.filter(
unionType => !caseTypes.has(unionType),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,42 @@ function test(value: ObjectUnion): number {
case 1:
return 1;
}
}
`,
`
declare const a: 'a' | 'b' | number;
switch (a) {
case 'a':
break;
case 'b':
break;
}
`,
`
declare const a: number | string;
switch (a) {
case 2:
break;
}
`,
`
declare const a: 'a' | (string & {});
switch (a) {
case 'a':
break;
}
`,
`
type FooBar = (string & { foo: void }) | 'bar';

const foobar = 'bar' as FooBar;
let result = 0;

switch (foobar) {
case 'bar': {
result = 42;
break;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just posting here so it doesn't get lost - tests should end up including bigints, booleans, symbols, unique symbols, and other fun wacky combinations/things just to be safe.

}
`,
],
Expand Down Expand Up @@ -373,32 +409,6 @@ switch (day) {
},
],
},
{
// Still complains with union intersection part
code: `
type FooBar = (string & { foo: void }) | 'bar';

const foobar = 'bar' as FooBar;
let result = 0;

switch (foobar) {
case 'bar': {
result = 42;
break;
}
}
`,
errors: [
{
messageId: 'switchIsNotExhaustive',
line: 7,
column: 9,
data: {
missingBranches: 'string & { foo: void; }',
},
},
],
},
{
code: `
const a = Symbol('a');
Expand Down