diff --git a/src/js_parser/parse/parse_stmt.rs b/src/js_parser/parse/parse_stmt.rs index f3059d8f612..44a5cd8b0d7 100644 --- a/src/js_parser/parse/parse_stmt.rs +++ b/src/js_parser/parse/parse_stmt.rs @@ -1052,7 +1052,26 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O }; } } - _ => {} + // "interface" turned out not to start an interface + // declaration: the nested statement came back as an + // expression statement ("export default interface = 2", + // "export default interface => 1") or a labeled statement + // ("export default interface: 0"). None of these can be a + // default export value, so report a syntax error instead of + // building an S.ExportDefault that the visit and print + // passes don't support. + _ => { + let r = js_lexer::range_of_identifier(p.source, stmt.loc); + p.log().add_range_error_fmt( + Some(p.source), + r, + format_args!( + "Unexpected \"{}\"", + bstr::BStr::new(p.source.text_for_range(r)) + ), + ); + return Err(err!("SyntaxError")); + } } p.create_default_name(default_loc).expect("unreachable") diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index 1fc1d57cdf5..fbc57ab7a28 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -209,6 +209,33 @@ describe("Bun.Transpiler", () => { err("module = (t) => 0 Foo { () => () => 0 }", 'Expected ";" but found "Foo"'); }); + it("export default interface that is not an interface declaration does not crash", () => { + const exp = ts.expectPrinted_; + const err = ts.expectParseError; + const unexpected = 'Unexpected "interface"'; + + // "interface" turns out to start an expression or a labeled statement, not an + // interface declaration. None of these can be a default export value. + err("export default interface=2", unexpected); + err("export default interface + 1", unexpected); + err("export default interface.foo()", unexpected); + err("export default interface => 1", unexpected); + err("export default interface: 2", unexpected); + + // The exact fuzz repro: tsx loader, no trailing newline. + expect(() => transpiler.transformSync("export default interface=2")).toThrow(unexpected); + + // Same shapes through the plain JavaScript loader must not crash either. + const js = new Bun.Transpiler({ loader: "js" }); + expect(() => js.transformSync("export default interface=2")).toThrow(unexpected); + expect(() => js.transformSync("export default interface => 1")).toThrow(unexpected); + expect(() => js.transformSync("export default interface: 2")).toThrow(unexpected); + + // Real interface declarations still parse and get erased. + exp("export default interface Foo {}", ""); + exp("export default interface Foo { bar(): void }\nexport const x = 1;", "export const x = 1;\n"); + }); + it("should parse empty type parameters", () => { const exp = ts.expectPrinted_; const err = ts.expectParseError;