fix(parser): report duplicate switch default clause in the parser (#23012)

## What

Moves the "A 'default' clause cannot appear more than once in a 'switch' statement" (TS1113) check out of the semantic checker (`check_switch_statement`) and into the parser's `parse_switch_statement`.

## Why

The check only reads the switch statement's own `cases` list, which is fully built at the parse site — it needs no scope, symbol, or strict-mode information, so it can move to the parser without introducing any new parser state. Reporting it during parsing surfaces the error in source order and removes a node-kind dispatch from the semantic builder.

## How

- The duplicate-`default` check is folded into the case-parsing pass (no second loop over `cases`). To do this without duplicating `parse_normal_list`'s open/loop/close logic, its callback bound is relaxed from `Fn` to `FnMut` (a safe generalization — all existing callers already satisfy it), so the case callback can track the first `default` span inline.
- Only the first duplicate per `switch` is reported, matching the previous semantic-checker behavior.
- A `unicorn/no-useless-switch-case` pass fixture that relied on a two-`default` switch being a *recoverable* (semantic-only) error was removed: the linter treats any parser error as fatal to the file, so a duplicate `default` now stops that file from being linted.

## Conformance

No positive/negative count changes in any suite. The only snapshot delta is a reordering in `parser_typescript.snap` (the error now fires during parsing, in source order, ahead of later errors in the same switch).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Boshen committed 2026-06-06 13:20:00 +00:00
1 parent 597ed85098
commit 4d722e09f8
8 files changed
+50 -59

No files matched your search

@@ -166,17 +166,6 @@ fn test() {
console.log('2')
}
",
"
switch (1) {
default:
handleDefaultCase1();
break;
case 1:
default:
handleDefaultCase2();
break;
}
",
];
let fail = vec![
+7 -2
View File
@@ -369,9 +369,14 @@ impl<'a, C: Config> ParserImpl<'a, C> {
result
}
pub(crate) fn parse_normal_list<F, T>(&mut self, open: Kind, close: Kind, f: F) -> Vec<'a, T>
pub(crate) fn parse_normal_list<F, T>(
&mut self,
open: Kind,
close: Kind,
mut f: F,
) -> Vec<'a, T>
where
F: Fn(&mut Self) -> T,
F: FnMut(&mut Self) -> T,
{
let opening_span = self.cur_token().span();
self.expect(open);
+9
View File
@@ -881,6 +881,15 @@ pub fn new_target_outside_function(span: Span) -> OxcDiagnostic {
.with_label(span)
}
#[cold]
pub fn switch_multiple_default_clause(first_default: Span, other_default: Span) -> OxcDiagnostic {
ts_error("1113", "A 'default' clause cannot appear more than once in a 'switch' statement.")
.with_labels(vec![
first_default.label("First 'default' clause is here."),
other_default.label("Another 'default' clause cannot appear here."),
])
}
#[cold]
pub fn import_meta(span: Span) -> OxcDiagnostic {
OxcDiagnostic::error("Unexpected import.meta expression")
+18 -1
View File
@@ -693,7 +693,24 @@ impl<'a, C: Config> ParserImpl<'a, C> {
let span = self.start_span();
self.bump_any(); // advance `switch`
let discriminant = self.parse_paren_expression();
let cases = self.parse_normal_list(Kind::LCurly, Kind::RCurly, Self::parse_switch_case);
// A `switch` may contain at most one `default` clause. Track the first one
// and report only the first duplicate, all in the single parsing pass.
let mut first_default: Option<Span> = None;
let mut reported_duplicate = false;
let cases = self.parse_normal_list(Kind::LCurly, Kind::RCurly, |p| {
let case = p.parse_switch_case();
if case.test.is_none() {
match first_default {
None => first_default = Some(case.span),
Some(first_span) if !reported_duplicate => {
p.error(diagnostics::switch_multiple_default_clause(first_span, case.span));
reported_duplicate = true;
}
Some(_) => {}
}
}
case
});
self.ast.statement_switch(self.end_span(span), discriminant, cases)
}
@@ -771,22 +771,6 @@ pub fn check_with_statement(stmt: &WithStatement, ctx: &SemanticBuilder<'_>) {
}
}
pub fn check_switch_statement<'a>(stmt: &SwitchStatement<'a>, ctx: &SemanticBuilder<'a>) {
let mut previous_default: Option<Span> = None;
for case in &stmt.cases {
if case.test.is_none() {
if let Some(previous_span) = previous_default {
ctx.error(diagnostics::switch_stmt_cannot_have_multiple_default_case(
previous_span,
case.span,
));
break;
}
previous_default.replace(case.span);
}
}
}
pub fn check_break_statement(stmt: &BreakStatement, ctx: &SemanticBuilder<'_>) {
// It is a Syntax Error if this BreakStatement is not nested, directly or indirectly (but not crossing function or static initialization block boundaries), within an IterationStatement or a SwitchStatement.
-1
View File
@@ -46,7 +46,6 @@ pub fn check<'a>(kind: AstKind<'a>, ctx: &SemanticBuilder<'a>) {
js::check_function_declaration(&stmt.body, false, ctx);
js::check_with_statement(stmt, ctx);
}
AstKind::SwitchStatement(stmt) => js::check_switch_statement(stmt, ctx),
AstKind::BreakStatement(stmt) => js::check_break_statement(stmt, ctx),
AstKind::ContinueStatement(stmt) => js::check_continue_statement(stmt, ctx),
AstKind::LabeledStatement(stmt) => {
-12
View File
@@ -456,15 +456,3 @@ pub fn ts_export_assignment_cannot_be_used_with_other_exports(span: Span) -> Oxc
.with_label(span)
.with_help("If you want to use `export =`, remove other `export`s and put all of them to the right hand value of `export =`. If you want to use `export`s, remove `export =` statement.")
}
#[cold]
pub fn switch_stmt_cannot_have_multiple_default_case(
first_default: Span,
other_default: Span,
) -> OxcDiagnostic {
ts_error("1113", "A 'default' clause cannot appear more than once in a 'switch' statement.")
.with_labels(vec![
first_default.label("First 'default' clause is here."),
other_default.label("Another 'default' clause cannot appear here."),
])
}
+16 -16
View File
@@ -12973,22 +12973,6 @@ Expect Syntax Error: tasks/coverage/typescript/tests/cases/conformance/types/wit
12 │ }
╰────
× Keywords cannot contain escape characters
╭─[typescript/tests/cases/compiler/switchStatementsWithMultipleDefaults.ts:25:13]
24 │ default: // Error, third 'default' clause
25 │ def\u0061ult: // Error, fourth 'default' clause.
· ────────────
26 │ // Errors on fifth-seventh
╰────
× TS(1108): A 'return' statement can only be used within a function body.
╭─[typescript/tests/cases/compiler/switchStatementsWithMultipleDefaults.ts:27:22]
26 │ // Errors on fifth-seventh
27 │ default: return;
· ──────
28 │ default: default:
╰────
× TS(1113): A 'default' clause cannot appear more than once in a 'switch' statement.
╭─[typescript/tests/cases/compiler/switchStatementsWithMultipleDefaults.ts:6:5]
5 │ case 2:
@@ -13013,6 +12997,22 @@ Expect Syntax Error: tasks/coverage/typescript/tests/cases/conformance/types/wit
22 │ case 10000:
╰────
× Keywords cannot contain escape characters
╭─[typescript/tests/cases/compiler/switchStatementsWithMultipleDefaults.ts:25:13]
24 │ default: // Error, third 'default' clause
25 │ def\u0061ult: // Error, fourth 'default' clause.
· ────────────
26 │ // Errors on fifth-seventh
╰────
× TS(1108): A 'return' statement can only be used within a function body.
╭─[typescript/tests/cases/compiler/switchStatementsWithMultipleDefaults.ts:27:22]
26 │ // Errors on fifth-seventh
27 │ default: return;
· ──────
28 │ default: default:
╰────
× TS(1113): A 'default' clause cannot appear more than once in a 'switch' statement.
╭─[typescript/tests/cases/compiler/switchStatementsWithMultipleDefaults1.ts:6:9]
5 │ case 2: