Skip to content

Commit

Permalink
fix: add containsNonLiteralType
Browse files Browse the repository at this point in the history
  • Loading branch information
Zamiell committed Jan 4, 2024
1 parent 8a48533 commit 0afc893
Show file tree
Hide file tree
Showing 2 changed files with 327 additions and 3 deletions.
64 changes: 62 additions & 2 deletions packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface SwitchMetadata {
readonly missingBranchTypes: ts.Type[];
readonly defaultCase: TSESTree.SwitchCase | undefined;
readonly isUnion: boolean;
readonly containsNonLiteralType: boolean;
}

type Options = [
Expand Down Expand Up @@ -104,12 +105,16 @@ export default createRule<Options, MessageIds>({
| string
| undefined;

const containsNonLiteralType =
doesTypeContainNonLiteralType(discriminantType);

if (!discriminantType.isUnion()) {
return {
symbolName,
missingBranchTypes: [],
defaultCase,
isUnion: false,
containsNonLiteralType,
};
}

Expand Down Expand Up @@ -138,9 +143,57 @@ export default createRule<Options, MessageIds>({
missingBranchTypes,
defaultCase,
isUnion: true,
containsNonLiteralType,
};
}

/**
* For example:
*
* - `"foo" | "bar"` is a type with all literal types.
* - `string` and `"foo" | number` are types that contain non-literal types.
*
* Default cases are never superfluous in switches with non-literal types.
*/
function doesTypeContainNonLiteralType(type: ts.Type): boolean {
const types = tsutils.unionTypeParts(type);
const typeNames = types.map(type => getTypeNameSpecific(type));

return typeNames.some(
typeName =>
typeName === 'string' ||
typeName === 'number' ||
typeName === 'bigint' ||
typeName === 'symbol',
);
}

/**
* Similar to the `getTypeName` function, but returns a more specific name.
* This is useful in differentiating between `string` and `"foo"`.
*/
function getTypeNameSpecific(type: ts.Type): string | undefined {
const escapedName = type.getSymbol()?.escapedName as string | undefined;
if (escapedName !== undefined && escapedName !== '__type') {
return escapedName;
}

const aliasSymbolName = type.aliasSymbol?.getName();
if (aliasSymbolName !== undefined) {
return aliasSymbolName;
}

// The above checks do not work with boolean values.
if ('intrinsicName' in type) {
const { intrinsicName } = type;
if (typeof intrinsicName === 'string' && intrinsicName !== '') {
return intrinsicName;
}
}

return undefined;
}

function checkSwitchExhaustive(
node: TSESTree.SwitchStatement,
switchMetadata: SwitchMetadata,
Expand Down Expand Up @@ -272,12 +325,19 @@ export default createRule<Options, MessageIds>({
return;
}

const { missingBranchTypes, defaultCase, isUnion } = switchMetadata;
const { missingBranchTypes, defaultCase, containsNonLiteralType } =
switchMetadata;

/*
console.log('1:', missingBranchTypes.length === 0);
console.log('2:', defaultCase !== undefined);
console.log('3:', !containsNonLiteralType);
*/

if (
missingBranchTypes.length === 0 &&
defaultCase !== undefined &&
!isUnion
!containsNonLiteralType
) {
context.report({
node: defaultCase,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,94 @@ switch (value) {
},
],
},
// switch with default clause on non-union type +
// switch with default clause on string type +
// "allowDefaultCaseForExhaustiveSwitch" option
{
code: `
declare const value: string;
switch (value) {
case 'foo':
return 0;
case 'bar':
return 1;
default:
return -1;
}
`,
options: [
{
allowDefaultCaseForExhaustiveSwitch: false,
requireDefaultForNonUnion: false,
},
],
},
// switch with default clause on number type +
// "allowDefaultCaseForExhaustiveSwitch" option
{
code: `
declare const value: number;
switch (value) {
case 0:
return 0;
case 1:
return 1;
default:
return -1;
}
`,
options: [
{
allowDefaultCaseForExhaustiveSwitch: false,
requireDefaultForNonUnion: false,
},
],
},
// switch with default clause on bigint type +
// "allowDefaultCaseForExhaustiveSwitch" option
{
code: `
declare const value: bigint;
switch (value) {
case 0:
return 0;
case 1:
return 1;
default:
return -1;
}
`,
options: [
{
allowDefaultCaseForExhaustiveSwitch: false,
requireDefaultForNonUnion: false,
},
],
},
// switch with default clause on symbol type +
// "allowDefaultCaseForExhaustiveSwitch" option
{
code: `
declare const value: symbol;
const foo = Symbol('foo');
switch (value) {
case foo:
return 0;
default:
return -1;
}
`,
options: [
{
allowDefaultCaseForExhaustiveSwitch: false,
requireDefaultForNonUnion: false,
},
],
},
// switch with default clause on union with number +
// "allowDefaultCaseForExhaustiveSwitch" option
{
code: `
declare const value: 0 | 1 | number;
switch (value) {
case 0:
return 0;
Expand Down Expand Up @@ -756,6 +839,7 @@ switch (value) {
],
},
{
// superfluous switch with a string-based union
code: `
type MyUnion = 'foo' | 'bar' | 'baz';
Expand All @@ -767,6 +851,186 @@ switch (myUnion) {
case 'baz': {
break;
}
default: {
break;
}
}
`,
options: [
{
allowDefaultCaseForExhaustiveSwitch: false,
requireDefaultForNonUnion: false,
},
],
errors: [
{
messageId: 'dangerousDefaultCase',
},
],
},
{
// superfluous switch with a string-based enum
code: `
enum MyEnum {
Foo = 'Foo',
Bar = 'Bar',
Baz = 'Baz',
}
declare const myEnum: MyEnum;
switch (myUnion) {
case MyEnum.Foo:
case MyEnum.Bar:
case MyEnum.Baz: {
break;
}
default: {
break;
}
}
`,
options: [
{
allowDefaultCaseForExhaustiveSwitch: false,
requireDefaultForNonUnion: false,
},
],
errors: [
{
messageId: 'dangerousDefaultCase',
},
],
},
{
// superfluous switch with a number-based enum
code: `
enum MyEnum {
Foo,
Bar,
Baz,
}
declare const myEnum: MyEnum;
switch (myUnion) {
case MyEnum.Foo:
case MyEnum.Bar:
case MyEnum.Baz: {
break;
}
default: {
break;
}
}
`,
options: [
{
allowDefaultCaseForExhaustiveSwitch: false,
requireDefaultForNonUnion: false,
},
],
errors: [
{
messageId: 'dangerousDefaultCase',
},
],
},
{
// superfluous switch with a boolean
code: `
declare const myBoolean: boolean;
switch (myBoolean) {
case true:
case false: {
break;
}
default: {
break;
}
}
`,
options: [
{
allowDefaultCaseForExhaustiveSwitch: false,
requireDefaultForNonUnion: false,
},
],
errors: [
{
messageId: 'dangerousDefaultCase',
},
],
},
{
// superfluous switch with undefined
code: `
declare const myValue: undefined;
switch (myValue) {
case undefined: {
break;
}
default: {
break;
}
}
`,
options: [
{
allowDefaultCaseForExhaustiveSwitch: false,
requireDefaultForNonUnion: false,
},
],
errors: [
{
messageId: 'dangerousDefaultCase',
},
],
},
{
// superfluous switch with null
code: `
declare const myValue: null;
switch (myValue) {
case null: {
break;
}
default: {
break;
}
}
`,
options: [
{
allowDefaultCaseForExhaustiveSwitch: false,
requireDefaultForNonUnion: false,
},
],
errors: [
{
messageId: 'dangerousDefaultCase',
},
],
},
{
// superfluous switch with union of various types
code: `
declare const myValue: 'foo' | boolean | undefined | null;
switch (myValue) {
case 'foo':
case true:
case false:
case undefined:
case null: {
break;
}
default: {
break;
}
Expand Down

0 comments on commit 0afc893

Please sign in to comment.