From 296574fb94339b330d8d8ca7688453a19c082872 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 01:23:37 +0000 Subject: [PATCH 1/3] js_parser: accept a bigint literal key after a class member modifier The lookahead that decides whether "static", "get", "set", "async", "accessor" and the TypeScript member modifiers are modifiers (rather than the member name itself) checks whether the next token can start a property name. It listed numeric and string literals but not bigint literals, so "static 2n = 1" was parsed as a field named "static" followed by a stray "2n" and rejected, and "static\n2n = 1" silently became two members. A bigint literal is a NumericLiteral and therefore a valid property name, so add it to the lookahead. The key itself was already parsed correctly when no modifier preceded it. --- src/js_parser/parse/parse_property.rs | 1 + test/bundler/transpiler/transpiler.test.js | 173 +++++++++++++++++++++ 2 files changed, 174 insertions(+) diff --git a/src/js_parser/parse/parse_property.rs b/src/js_parser/parse/parse_property.rs index 81fa6a8f0224..4b765979f4ae 100644 --- a/src/js_parser/parse/parse_property.rs +++ b/src/js_parser/parse/parse_property.rs @@ -350,6 +350,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O p.lexer.token, T::TOpenBracket | T::TNumericLiteral + | T::TBigIntegerLiteral | T::TStringLiteral | T::TPrivateIdentifier ) diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index a9a0d382aaed..cc43f9974091 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -311,6 +311,25 @@ describe("Bun.Transpiler", () => { exp("declare class Foo {}", ""); }); + it("class modifiers followed by a bigint literal key", () => { + const exp = ts.expectPrinted_; + + // Same output as tsc: the modifiers are erased and the bigint key is kept. + exp("class A { static 1n: bigint = 1n }", "class A {\n static 1n = 1n;\n}"); + exp("class A { static 1n!: number }", "class A {\n static 1n;\n}"); + exp("class A { public 1n = 1 }", "class A {\n 1n = 1;\n}"); + exp("class A { private static 1n = 1 }", "class A {\n static 1n = 1;\n}"); + exp("class A { protected static readonly 1n = 1 }", "class A {\n static 1n = 1;\n}"); + exp("class A { readonly 1n = 1 }", "class A {\n 1n = 1;\n}"); + exp("class A extends B { override 1n() {} }", "class A extends B {\n 1n() {}\n}"); + exp("class A extends B { public override async 1n() {} }", "class A extends B {\n async 1n() {}\n}"); + exp("class A { public get 1n(): number { return 1 } }", "class A {\n get 1n() {\n return 1;\n }\n}"); + exp("class A { private static set 1n(v: number) {} }", "class A {\n static set 1n(v) {}\n}"); + exp("class A { declare 1n: number; x = 1 }", "class A {\n x = 1;\n}"); + exp("class A { declare static 1n: number; x = 1 }", "class A {\n x = 1;\n}"); + exp("abstract class A { abstract 1n: number; abstract 2n(): void; x = 1 }", "class A {\n x = 1;\n}"); + }); + it("contextual keywords followed by a newline apply ASI instead of acting as modifiers", () => { const exp = ts.expectPrinted_; const err = ts.expectParseError; @@ -2869,6 +2888,160 @@ console.log(a) expectBunPrinted_("export const foo = 1 * 2", "export const foo = 2"); }); + it("bigint literal keys after modifier keywords", () => { + // A bigint literal is a valid property name, so like a numeric literal it + // marks the word before it as a modifier. + expectPrinted_("class A { static 1n = 1 }", "class A {\n static 1n = 1;\n}"); + expectPrinted_("class A { static 1n }", "class A {\n static 1n;\n}"); + expectPrinted_("class A { static 1n() {} }", "class A {\n static 1n() {}\n}"); + expectPrinted_("class A { get 1n() { return 1 } }", "class A {\n get 1n() {\n return 1;\n }\n}"); + expectPrinted_("class A { set 1n(v) {} }", "class A {\n set 1n(v) {}\n}"); + expectPrinted_("class A { async 1n() {} }", "class A {\n async 1n() {}\n}"); + expectPrinted_( + "class A { static get 1n() { return 1 } }", + "class A {\n static get 1n() {\n return 1;\n }\n}", + ); + expectPrinted_("class A { static set 1n(v) {} }", "class A {\n static set 1n(v) {}\n}"); + expectPrinted_("class A { static async 1n() {} }", "class A {\n static async 1n() {}\n}"); + expectPrinted_("class A { static async *1n() {} }", "class A {\n static async* 1n() {}\n}"); + expectPrinted_("class A { static *1n() {} }", "class A {\n static *1n() {}\n}"); + expectPrinted_("class A { async *1n() {} }", "class A {\n async* 1n() {}\n}"); + expectPrinted_("class A { 1n = 1; 2n() {} }", "class A {\n 1n = 1;\n 2n() {}\n}"); + + expectPrinted_("x = { get 1n() { return 1 } }", "x = { get 1n() {\n return 1;\n} };\n"); + expectPrinted_("x = { set 1n(v) {} }", "x = { set 1n(v) {} };\n"); + expectPrinted_("x = { async 1n() {} }", "x = { async 1n() {} };\n"); + expectPrinted_("x = { async *1n() {} }", "x = { async* 1n() {} };\n"); + + // "static", "get" and "set" bind to a key on the next line; no semicolon + // is inserted because the key can continue the member. + expectPrinted_("class A { static\n 1n = 1 }", "class A {\n static 1n = 1;\n}"); + expectPrinted_("class A { get\n 1n() { return 1 } }", "class A {\n get 1n() {\n return 1;\n }\n}"); + expectPrinted_("class A { set\n 1n(v) {} }", "class A {\n set 1n(v) {}\n}"); + // "async" must be on the same line as the key, so this is a field named + // "async" followed by a method. + expectPrinted_("class A { async\n 1n() {} }", "class A {\n async;\n 1n() {}\n}"); + + // The words are still plain keys when a bigint is the value rather than the next key. + expectPrinted_("class A { static = 1n }", "class A {\n static = 1n;\n}"); + expectPrinted_("class A { static; 1n = 1 }", "class A {\n static;\n 1n = 1;\n}"); + expectPrinted_("x = { get: 1n }", "x = { get: 1n };\n"); + expectPrinted_("x = { async: 1n, set: 2n }", "x = { async: 1n, set: 2n };\n"); + // "static" is only a modifier in a class body; like `{ static 1() {} }` this is rejected. + expectParseError("x = { static 1n() {} }", "Parse error"); + }); + + it("class members with bigint literal keys after modifiers behave like numeric keys at runtime", async () => { + // Uses a .js file rather than -e so that "accessor" is parsed (the repo + // tsconfig enables experimentalDecorators, which disables it). + using dir = tempDir("bigint-member-keys", { + "index.js": ` + class A { + static 1n = "static field"; + get 2n() { + return "getter"; + } + set 3n(v) { + this.setterValue = v; + } + async 4n() { + return "async method"; + } + static async 5n() { + return "static async method"; + } + static get 6n() { + return "static getter"; + } + static set 7n(v) { + this.staticSetterValue = v; + } + static async *8n() { + yield "static async generator"; + } + static + 9n = "static field on the next line"; + get + 10n() { + return "getter on the next line"; + } + async + 11n() { + return "method after a field named async"; + } + accessor 12n = "accessor"; + static accessor 13n = "static accessor"; + } + const o = { + get 1n() { + return "object getter"; + }, + set 2n(v) { + this.setterValue = v; + }, + async 3n() { + return "object async method"; + }, + }; + + const a = new A(); + a[3] = "setter"; + A[7] = "static setter"; + o[2] = "object setter"; + const isAccessor = (obj, key) => { + const { get, set } = Object.getOwnPropertyDescriptor(obj, key); + return typeof get === "function" && typeof set === "function"; + }; + + console.log( + JSON.stringify({ + 1: A[1], + 2: a[2], + 3: a.setterValue, + 4: await a[4](), + 5: await A[5](), + 6: A[6], + 7: A.staticSetterValue, + 8: (await A[8]().next()).value, + 9: A[9], + 10: a[10], + 11: [Object.hasOwn(a, "async"), await a[11]()], + 12: [a[12], isAccessor(A.prototype, "12")], + 13: [A[13], isAccessor(A, "13")], + o: [o[1], o.setterValue, await o[3]()], + prototypeKeys: Object.getOwnPropertyNames(A.prototype), + }), + ); + `, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "index.js"], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(JSON.parse(stdout)).toEqual({ + 1: "static field", + 2: "getter", + 3: "setter", + 4: "async method", + 5: "static async method", + 6: "static getter", + 7: "static setter", + 8: "static async generator", + 9: "static field on the next line", + 10: "getter on the next line", + 11: [true, "method after a field named async"], + 12: ["accessor", true], + 13: ["static accessor", true], + o: ["object getter", "object setter", "object async method"], + prototypeKeys: ["2", "3", "4", "10", "11", "12", "constructor"], + }); + expect(exitCode).toBe(0); + }); + it.todo("pass objects to macros", () => { var object = { helloooooooo: { From ed855a668fee4012a7391dbec771980dd31d9536 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 05:07:41 +0000 Subject: [PATCH 2/3] js_parser: accept bigint literal keys in skipped TypeScript types The member-name lists in skip_type_script_object_type (interface and type literal members) and skip_type_script_binding (destructured parameters in function types and method signatures) had the same omission as the modifier lookahead in parse_property: numeric and string literals but not bigint literals, so "interface I { 1n(): void }" and "({ 1n: a }: T) => void" were rejected while tsc parses both and bun already accepts the numeric forms. --- src/js_parser/parse/parse_skip_typescript.rs | 4 +++- test/bundler/transpiler/transpiler.test.js | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/js_parser/parse/parse_skip_typescript.rs b/src/js_parser/parse/parse_skip_typescript.rs index 1e40e3b5597a..9fe035ed19ad 100644 --- a/src/js_parser/parse/parse_skip_typescript.rs +++ b/src/js_parser/parse/parse_skip_typescript.rs @@ -115,8 +115,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } // "{1: y}" + // "{1n: y}" // "{'x': y}" - T::TStringLiteral | T::TNumericLiteral => { + T::TStringLiteral | T::TNumericLiteral | T::TBigIntegerLiteral => { self.lexer.next()?; } @@ -1036,6 +1037,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O while self.lexer.is_identifier_or_keyword() || self.lexer.token == T::TStringLiteral || self.lexer.token == T::TNumericLiteral + || self.lexer.token == T::TBigIntegerLiteral { self.lexer.next()?; found_key = true; diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index cc43f9974091..bb1bf06916e7 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -328,6 +328,10 @@ describe("Bun.Transpiler", () => { exp("class A { declare 1n: number; x = 1 }", "class A {\n x = 1;\n}"); exp("class A { declare static 1n: number; x = 1 }", "class A {\n x = 1;\n}"); exp("abstract class A { abstract 1n: number; abstract 2n(): void; x = 1 }", "class A {\n x = 1;\n}"); + exp( + "interface I { 1n(): void }\nclass A implements I { static 1n() {} 1n() {} }", + "class A {\n static 1n() {}\n 1n() {}\n}", + ); }); it("contextual keywords followed by a newline apply ASI instead of acting as modifiers", () => { @@ -1022,6 +1026,9 @@ function foo() {} exp("type x = [-1, 0, 1]\na([])", "a([]);\n"); exp("type x = [-1n, 0n, 1n]\na([])", "a([]);\n"); exp("type x = {0: number, readonly 1: boolean}\na([])", "a([]);\n"); + exp("type x = {0n: number, readonly 1n: boolean, 2n?: string}\na([])", "a([]);\n"); + exp("type x = {0n(): void, get 1n(): number, set 1n(v: number), 2n?(): void}\na([])", "a([]);\n"); + exp("interface x {0n: number, 1n(): void}\na([])", "a([]);\n"); exp("type x = {'a': number, readonly 'b': boolean}\na([])", "a([]);\n"); exp("type\nFoo = {}", "type;\nFoo = {};\n"); err("export type\nFoo = {}", 'Unexpected newline after "type"'); @@ -1052,6 +1059,10 @@ function foo() {} err("let x: {[typeof: string]: number}", 'Expected identifier but found ":"'); exp("let x: () => void = Foo", "let x = Foo;\n"); exp("let x: new () => void = Foo", "let x = Foo;\n"); + exp("let x: ({ 1: a, 'b': b }: T) => void = Foo", "let x = Foo;\n"); + exp("let x: ({ 1n: a }: T) => void = Foo", "let x = Foo;\n"); + exp("let x: new ({ 1n: a }: T) => void = Foo", "let x = Foo;\n"); + exp("type x = {m({ 1n: a }: T): void}\na([])", "a([]);\n"); exp("let x = 'x' as keyof T", 'let x = "x";\n'); exp("let x = [1] as readonly [number]", "let x = [1];\n"); exp("let x = 'x' as keyof typeof Foo", 'let x = "x";\n'); From f61f14c0de045f1ac090cb391836295ac229a9ed Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 05:12:25 +0000 Subject: [PATCH 3/3] ci: retrigger