diff --git a/baselines/packages/mimir/test/no-unreachable-code/typed/test.ts.lint b/baselines/packages/mimir/test/no-unreachable-code/typed/test.ts.lint index a180088ca..324f94a18 100644 --- a/baselines/packages/mimir/test/no-unreachable-code/typed/test.ts.lint +++ b/baselines/packages/mimir/test/no-unreachable-code/typed/test.ts.lint @@ -42,6 +42,7 @@ switch (Boolean()) { 'bar'; break; default: + ~~~~~~~ [error no-unreachable-code: 'default' clause is unreachable in exhaustive 'switch' statements.] 'baz'; break; 'bas'; diff --git a/packages/mimir/docs/no-unreachable-code.md b/packages/mimir/docs/no-unreachable-code.md index 74f6632f7..eb157d276 100644 --- a/packages/mimir/docs/no-unreachable-code.md +++ b/packages/mimir/docs/no-unreachable-code.md @@ -114,4 +114,5 @@ function foo() { ## Related Rules +* [`no-fallthrough`](no-fallthrough.md) * [`return-never-call`](return-never-call.md) diff --git a/packages/mimir/src/rules/no-unreachable-code.ts b/packages/mimir/src/rules/no-unreachable-code.ts index 54fa546f2..6d7fd44f1 100644 --- a/packages/mimir/src/rules/no-unreachable-code.ts +++ b/packages/mimir/src/rules/no-unreachable-code.ts @@ -7,6 +7,7 @@ import { getControlFlowEnd, endsControlFlow, isLabeledStatement, + hasExhaustiveCaseClauses, } from 'tsutils'; type ForDoWhileStatement = ts.ForStatement | ts.WhileStatement | ts.DoStatement; @@ -17,10 +18,13 @@ export class Rule extends AbstractRule { for (const node of this.context.getFlatAst()) { switch (node.kind) { case ts.SyntaxKind.Block: - case ts.SyntaxKind.CaseClause: - case ts.SyntaxKind.DefaultClause: + case ts.SyntaxKind.SourceFile: + case ts.SyntaxKind.ModuleBlock: this.checkBlock(node); break; + case ts.SyntaxKind.SwitchStatement: + this.checkSwitch(node); + break; case ts.SyntaxKind.IfStatement: this.checkIfStatement(node); break; @@ -31,9 +35,23 @@ export class Rule extends AbstractRule { } } + private checkSwitch(node: ts.SwitchStatement) { + for (const clause of node.caseBlock.clauses) { + this.checkBlock(clause); + if (clause.kind === ts.SyntaxKind.DefaultClause) { + const checker = this.program?.getTypeChecker(); + if (checker !== undefined && hasExhaustiveCaseClauses(node, checker)) + this.addFindingAtNode( + clause.getFirstToken(this.sourceFile)!, + "'default' clause is unreachable in exhaustive 'switch' statements.", + ); + } + } + } + private checkBlock(node: ts.BlockLike) { let i = node.statements.findIndex(this.nextStatementIsUnreachable, this); - if (i === -1 || i === node.statements.length - 1) + if (i === -1) return; for (i += 1; i < node.statements.length; ++i) if (isExecutableStatement(node.statements[i])) @@ -71,7 +89,9 @@ export class Rule extends AbstractRule { this.addFindingAtNode(node.getFirstToken(this.sourceFile)!, 'Unreachable code detected.'); } - private nextStatementIsUnreachable(statement: ts.Statement): boolean { + private nextStatementIsUnreachable(statement: ts.Statement, index: number, statements: readonly ts.Statement[]): boolean { + if (index === statements.length - 1) + return false; // no need to check the last statement in a block if (endsControlFlow(statement, this.program?.getTypeChecker())) return true; const labels: string[] = []; diff --git a/packages/wotan/src/utils.ts b/packages/wotan/src/utils.ts index f15445031..b286def10 100644 --- a/packages/wotan/src/utils.ts +++ b/packages/wotan/src/utils.ts @@ -47,8 +47,6 @@ export function format(value: T, fmt = Format.Yaml): string { schema: yaml.JSON_SCHEMA, sortKeys: true, }); - default: - return assertNever(fmt); } } @@ -81,10 +79,6 @@ function convertToPrintable(value: any): any { return added ? newValue : undefined; } -export function assertNever(v: never): never { - throw new Error(`unexpected value '${v}'`); -} - export function calculateChangeRange(original: string, changed: string): ts.TextChangeRange { const diff = changed.length - original.length; let start = 0; diff --git a/packages/wotan/test/utils.spec.ts b/packages/wotan/test/utils.spec.ts index 8dc050ce6..976e9475d 100644 --- a/packages/wotan/test/utils.spec.ts +++ b/packages/wotan/test/utils.spec.ts @@ -1,7 +1,6 @@ import test from 'ava'; import { calculateChangeRange, - assertNever, resolveCachedResult, } from '../src/utils'; @@ -46,10 +45,6 @@ test('calculateChangeRange', (t) => { } }); -test('assertNever', (t) => { - t.throws(() => assertNever('a')); -}); - test('resolveCachedResult', (t) => { const cache = new Map(); t.is(