UNPKG

eslint-plugin-switch-statement

Version:

Rules for properly handling switch statements, including ensuring that appropriate exhaustive case handling.

238 lines (237 loc) 12.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const index_js_1 = require("../test-utils/index.js"); const require_appropriate_default_case_js_1 = require("./require-appropriate-default-case.js"); const require_appropriate_default_case_js_2 = __importDefault(require("./require-appropriate-default-case.js")); const ruleTester = (0, index_js_1.makeRuleTester)(); const assertNeverOptions = [ { unreachableDefaultCaseAssertionFunctionName: "assertNever" }, ]; const valid = [ { name: "not a switch statement", code: "if (foo) { bar(); }" }, { name: "literal-union switch with default case and default exhaustive function name", code: `declare const foo: 2 | 3; switch (foo) { default: { ${require_appropriate_default_case_js_1.DEFAULT_ASSERT_NEVER_FN_NAME}(foo); } }`, }, { name: "unit-type-union switch with default case and default exhaustive function name", code: `declare const foo: null | undefined; switch (foo) { default: { ${require_appropriate_default_case_js_1.DEFAULT_ASSERT_NEVER_FN_NAME}(foo); } }`, }, { name: "literal-union switch with default case and given function name", code: "declare const foo: 2 | 3; switch (foo) { default: { assertNever(foo); } }", options: assertNeverOptions, }, { name: "switch with default case on non-literal union", code: "declare const foo: number | string; switch (foo) { case 4: break; default: { throw new Error() } }", }, { name: "switch with only a default case on non-literal union", code: "declare const foo: number | string; switch (foo) { default: { throw new Error() } }", }, // TODO: while an empty default case is still a default case, maybe we should flag this? { name: "switch with an empty default case on non-literal union", code: "declare const foo: number | string; switch (foo) { case true: break; default: {} }", }, { name: "switch with only an empty default case on non-literal union", code: "declare const foo: number | string; switch (foo) { default: {} }", }, { name: "nested switches, all with default cases", code: "declare const foo: number | string; switch (foo) { default: { switch (foo) { default: 4 } } }", options: assertNeverOptions, } ]; // NB: in the tests below, the inclusion of output in some tests is doing a lot // of work by implicitly testing whether the code thought that the discriminant // of the union was eligible to be treated exhaustively (in which case it // applies the fix of adding a default assertNever case). const invalid = [ { name: "missing default in switch (on non-union type)", code: "switch (foo) { case 2: break }", errors: [{ messageId: "addDefaultCase" }], }, { name: "missing default in switch (on union literal type)", code: "declare const foo: 1 | 2; switch (foo) { case 2: break }", output: "declare const foo: 1 | 2; switch (foo) { case 2: break \ndefault: assertNever(foo); }", options: assertNeverOptions, errors: [{ messageId: "addDefaultCase" }], }, { name: "missing default in switch (on numeric enum type)", code: "enum Direction { Up = 1, Down } declare const foo: Direction; switch (foo) { case 2: break }", output: "enum Direction { Up = 1, Down } declare const foo: Direction; switch (foo) { case 2: break \ndefault: assertNever(foo); }", options: assertNeverOptions, errors: [{ messageId: "addDefaultCase" }], }, { name: "missing default in switch (on string enum type)", code: "enum Direction { Up = 'A', Down = 'B' } declare const foo: Direction; switch (foo) { case 2: break }", output: "enum Direction { Up = 'A', Down = 'B' } declare const foo: Direction; switch (foo) { case 2: break \ndefault: assertNever(foo); }", options: assertNeverOptions, errors: [{ messageId: "addDefaultCase" }], }, { name: "missing default in switch (on const enum type)", code: "const enum Direction { Up = 'A', Down = 'B' } declare const foo: Direction; switch (foo) { case 2: break }", output: "const enum Direction { Up = 'A', Down = 'B' } declare const foo: Direction; switch (foo) { case 2: break \ndefault: assertNever(foo); }", options: assertNeverOptions, errors: [{ messageId: "addDefaultCase" }], }, { name: "missing default in switch (on branded unit type)", code: "declare const foo: 13 & { _brand: 'Hi' }; switch (foo) { case 2: break }", output: "declare const foo: 13 & { _brand: 'Hi' }; switch (foo) { case 2: break \ndefault: assertNever(foo); }", options: assertNeverOptions, errors: [{ messageId: "addDefaultCase" }], }, { name: "missing default in switch (on intersection reducing to a unit type)", code: "declare const foo: (13 | null) & (13 | 14 | null); switch (foo) { case 2: break }", output: "declare const foo: (13 | null) & (13 | 14 | null); switch (foo) { case 2: break \ndefault: assertNever(foo); }", options: assertNeverOptions, errors: [{ messageId: "addDefaultCase" }], }, { name: "missing default in switch (on template literal types that reduce to a unit type)", code: "declare const foo: `${'hello' | 'goodbye'}-world`; switch (foo) { case 2: break }", output: "declare const foo: `${'hello' | 'goodbye'}-world`; switch (foo) { case 2: break \ndefault: assertNever(foo); }", options: assertNeverOptions, errors: [{ messageId: "addDefaultCase" }], }, { name: "missing default in switch (on conditional type that reduces to a unit type)", code: "type Foo<T> = T extends string ? 'hello' : object; declare const foo: Foo<'x'>; switch (foo) { case 2: break }", output: "type Foo<T> = T extends string ? 'hello' : object; declare const foo: Foo<'x'>; switch (foo) { case 2: break \ndefault: assertNever(foo); }", options: assertNeverOptions, errors: [{ messageId: "addDefaultCase" }], }, { name: "missing default in switch (on conditional type that reduces to a unit type, non-distributive but not yet evaluable)", code: ` type Foo<T> = T extends { x: true } ? 'hello' : 'world'; function test<T extends object>(foo: Foo<T>) { switch (foo) { case 2: break } }`, output: ` type Foo<T> = T extends { x: true } ? 'hello' : 'world'; function test<T extends object>(foo: Foo<T>) { switch (foo) { case 2: break \ndefault: assertNever(foo); } }`, options: assertNeverOptions, errors: [{ messageId: "addDefaultCase" }], }, { name: "missing default in switch (on conditional type that reduces to a unit type, distributive but not yet evaluable)", code: ` type Foo<T> = T extends string ? 'hello' : 'world'; function test<T extends string | number>(foo: Foo<T>) { switch (foo) { case 2: break } }`, output: ` type Foo<T> = T extends string ? 'hello' : 'world'; function test<T extends string | number>(foo: Foo<T>) { switch (foo) { case 2: break \ndefault: assertNever(foo); } }`, options: assertNeverOptions, errors: [{ messageId: "addDefaultCase" }], }, { name: "missing default in switch (on other types reducing to a unit type)", code: "declare const a: { x: 44 }; declare const foo: keyof typeof a; switch (foo) { case 2: break }", output: "declare const a: { x: 44 }; declare const foo: keyof typeof a; switch (foo) { case 2: break \ndefault: assertNever(foo); }", options: assertNeverOptions, errors: [{ messageId: "addDefaultCase" }], }, { name: "missing default in switch (on union of unit types)", code: "enum Direction { Up = 1, Down } declare const foo: Direction | null; switch (foo) { case 2: break }", output: "enum Direction { Up = 1, Down } declare const foo: Direction | null; switch (foo) { case 2: break \ndefault: assertNever(foo); }", options: assertNeverOptions, errors: [{ messageId: "addDefaultCase" }], }, { name: "missing default in switch (on intersection constrained to a unit type)", code: ` function x<T extends 'a' | 'b'>(foo: T) { switch (foo) { case 'a': break } }`, // prettier-ignore-next-line output: ` function x<T extends 'a' | 'b'>(foo: T) { switch (foo) { case 'a': break \ndefault: assertNever(foo); } }`, options: assertNeverOptions, errors: [{ messageId: "addDefaultCase" }], }, { name: "missing default in nested switch (on non-union type)", code: "switch (foo) { case 2: { switch (foo) {}; break; }; default: 3 }", options: assertNeverOptions, errors: [{ messageId: "addDefaultCase" }], }, { name: "no exhaustiveness check in default case of switch that could use one - with normal case", code: "declare const foo: 1 | 2; switch (foo) { case 1: break; default: { doSomethingElse(foo); } }", errors: [{ messageId: "considerExhaustiveSwitch" }], }, { name: "no exhaustiveness check in default case of switch that could use one - with only default case", code: "declare const foo: null | true; switch (foo) { default: { doSomethingElse(foo); } }", errors: [{ messageId: "considerExhaustiveSwitch" }], }, { name: "no exhaustiveness check in default case of switch that could use one - on enum with non-literal members", code: "enum Direction { X = 1 << 2, Y = 1 << 2 } declare const foo: Direction; switch (foo) { default: true }", errors: [{ messageId: "considerExhaustiveSwitch" }], }, { name: "no exhaustiveness check in default case of switch that could use one - on enum with computed members", code: 'enum Direction { X = "12".length, Y = "32".length } declare const foo: Direction; switch (foo) { default: true }', errors: [{ messageId: "considerExhaustiveSwitch" }], }, { name: "no exhaustiveness check in default case of switch that could use one - nested switch", code: "declare const foo: number | string; declare const x: 1 | 2; switch (foo) { default: { switch (x) { default: 4 } } }", errors: [{ messageId: "considerExhaustiveSwitch" }], }, { name: "no exhaustiveness check in default case of switch that could use one - outer switch missing check; nested switch has it", code: ` declare const foo: 1 | 2; switch (foo) { default: { switch (true) { default: assertUnreachable(foo); } } }`, errors: [{ messageId: "considerExhaustiveSwitch" }], }, { name: "switch without any cases", code: 'declare const foo: "a"; switch (foo) { } ', errors: [{ messageId: "addDefaultCase" }], output: 'declare const foo: "a"; switch (foo) { \ndefault: assertUnreachable(foo); } ', }, { name: "switch with unconventionally located case default case", code: "declare const foo: 'a'; switch (foo) { default: { doSomethingElse(foo); } case 'bar': doSomething(); break; }", errors: [{ messageId: "considerExhaustiveSwitch" }], }, { name: "switching on a member expression with an identifier on the LHS", code: "declare const foo: { bar: 'a' } | { bar: 'b' }; switch (foo.bar) {}", output: "declare const foo: { bar: 'a' } | { bar: 'b' }; switch (foo.bar) {\ndefault: assertUnreachable(foo); }", errors: [{ messageId: "addDefaultCase" }], }, ]; ruleTester.run(require_appropriate_default_case_js_1.RULE_NAME, require_appropriate_default_case_js_2.default, { valid, invalid });