diff --git a/src/js_parser/parse/parse_typescript.rs b/src/js_parser/parse/parse_typescript.rs index a766177d7c44..8a5a53d756d8 100644 --- a/src/js_parser/parse/parse_typescript.rs +++ b/src/js_parser/parse/parse_typescript.rs @@ -587,7 +587,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O name: js_ast::StoreStr::new(b"" as &[u8]), value: None, }; - // Assigned in both live arms below; the third arm returns. + // Assigned in every live arm below; the error arm returns. let needs_symbol: bool; // Parse the name @@ -598,15 +598,33 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O debug_assert!(!estr.is_utf16); value.name = estr.data; needs_symbol = js_lexer::is_identifier(value.name.slice()); + p.lexer.next()?; + } else if p.lexer.token == T::TOpenBracket { + // TypeScript allows computed enum member names when the + // expression is a string literal or a substitution-free + // template literal: "enum E { ['a'] = 1, [`b`] = 2 }". + p.lexer.next()?; + if p.lexer.token != T::TStringLiteral + && p.lexer.token != T::TNoSubstitutionTemplateLiteral + { + p.lexer.expect(T::TStringLiteral)?; + return Err(err!("SyntaxError")); + } + let estr = p.lexer.to_utf8_e_string()?; + debug_assert!(!estr.is_utf16); + value.name = estr.data; + needs_symbol = js_lexer::is_identifier(value.name.slice()); + p.lexer.next()?; + p.lexer.expect(T::TCloseBracket)?; } else if p.lexer.is_identifier_or_keyword() { value.name = js_ast::StoreStr::new(p.lexer.identifier); needs_symbol = true; + p.lexer.next()?; } else { p.lexer.expect(T::TIdentifier)?; // error early, name is still `undefined` return Err(err!("SyntaxError")); } - p.lexer.next()?; // Identifiers can be referenced by other values if !opts.is_typescript_declare && needs_symbol { diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index ecb35f50c5b5..341de92e638c 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -512,8 +512,37 @@ describe("Bun.Transpiler", () => { it("malformed enums", () => { const err = ts.expectParseError; - err("enum Foo { [2]: 'hi' }", 'Expected identifier but found "["'); + err("enum Foo { [2]: 'hi' }", 'Expected string but found "2"'); err("enum [] { a }", 'Expected identifier but found "["'); + // Only string literals and substitution-free template literals are + // valid computed enum member names. + err("enum Foo { ['a' + 'b'] = 1 }", 'Expected "]" but found "+"'); + err("enum Foo { [`t${1}`] = 1 }", 'Expected string but found "`t${"'); + err("enum Foo { [Symbol.iterator] = 1 }", 'Expected string but found "Symbol"'); + }); + + it("enum members with computed string literal names", () => { + const exp = ts.expectPrinted_; + + exp( + "enum E { ['with space'] = 1 }", + 'var E;\n((E) => {\n E[E["with space"] = 1] = "with space";\n})(E ||= {});\n', + ); + exp("enum E { [`tmpl`] = 2 }", 'var E;\n((E) => {\n E[E["tmpl"] = 2] = "tmpl";\n})(E ||= {});\n'); + // A computed name that is a valid identifier can be referenced by + // later members, same as a string literal name. + exp( + "enum E { ['A'] = 5, B = A * 2 }", + 'var E;\n((E) => {\n E[E["A"] = 5] = "A";\n E[E["B"] = 10] = "B";\n})(E ||= {});\n', + ); + exp( + "enum E { ['x'], ['y z'] }", + 'var E;\n((E) => {\n E[E["x"] = 0] = "x";\n E[E["y z"] = 1] = "y z";\n})(E ||= {});\n', + ); + exp( + "const enum CE { ['c'] = 9 } console.log(CE.c)", + 'var CE;\n((CE) => {\n CE[CE["c"] = 9] = "c";\n})(CE ||= {});\nconsole.log(9 /* c */);\n', + ); }); it("doesn't crash with functions assigned to enum values", () => {