Skip to content

Commit 08652f2

Browse files
authored
Print Infinity and negative numeric property keys as computed properties (#31328)
### Problem Fuzzer-found invariant violation: `printed output does not reparse (loader=ts)`. ```js new Bun.Transpiler({ loader: "ts", minifyWhitespace: true }) .transformSync("(class {999…(325 digits)…999() {}})") // => (class{1/0(){}}); ← syntax error ``` A numeric property name that overflows to `Infinity` (a few hundred digits, or just `1e999`) is printed by `print_number` as `1/0` / `1 / 0`, which is not valid syntax in property-name position. The same happens for: - object literal properties, methods, getters/setters: `x={1/0:1}`, `x={get 1/0(){}}` - class methods and fields: `class{1/0(){}}`, `class{static 1/0=1}` - destructuring patterns: `const{1/0:x}=obj` - negative keys produced by cross-module `const enum` inlining with `--minify`: `{-1:1,1/0:2}` None of these reparse, so `bun build --minify` output can be unparseable. ### Fix In `src/js_printer/lib.rs`, when a non-computed property key is an `ENumber` whose printed form would not be a valid property name (sign bit set, or `+Infinity` in the configurations where `print_number` emits `1/0`), print it as a computed property instead: - `print_property` (object literals, classes) — mirrors esbuild's `printProperty` ("Automatically print numbers that would cause a syntax error as computed properties"). - `print_binding` (destructuring object patterns) — same check; esbuild's `printBinding` lacks it and has the same bug with `--minify`, so this goes one step further. Output now matches esbuild: `(class{[1/0](){}})`, `{[-1]:1,[1/0]:2}`, `const{[1/0]:x}=obj`. Semantics are unchanged since `[1/0]`/`[-1]` evaluate to the same property keys (`"Infinity"`, `"-1"`). ### Verification - `test/bundler/transpiler/transpiler.test.js` — new `describe("numeric property keys that overflow to Infinity")`: exact printed output with and without `minifyWhitespace` for object/class/destructuring positions, the original 325-digit fuzz input, and a runtime check that the minified output evaluates and the key is still `"Infinity"`. - `test/bundler/bundler_minify.test.ts` — new `itBundled("minify/NumericPropertyKeysPrintedAsComputed")`: bundles `1e999` keys plus an imported `const enum { Negative = -1 }` used as a computed key, and runs the output. Both tests fail on current canary (`(class{1/0(){}})`, bundle fails to parse) and pass with this change; the full files pass with the debug build (150 + 33 tests, 0 fail). ### Linked issue Fixes #14687 — the negative-key half of this change is exactly that report (imported `const enum` member `-1` used as an object key with `minify: true`). Verified with the issue's repro: ```js // before: var e={-1:"foo",1:"bar"};export{e as FooRecord}; ← syntax error // after: var e={[-1]:"foo",1:"bar"};export{e as FooRecord}; ```
1 parent dd0883d commit 08652f2

3 files changed

Lines changed: 98 additions & 1 deletion

File tree

src/js_printer/lib.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4664,6 +4664,15 @@ pub mod __gated_printer {
46644664
self.prev_reg_exp_end = self.writer.written();
46654665
}
46664666

4667+
/// Whether a number used as a non-computed property name must be printed as a
4668+
/// computed property instead, because `print_number` would render it as
4669+
/// something that is not a valid property name (e.g. "-1", "1/0", "1 / 0").
4670+
pub fn number_property_key_must_be_computed(&self, value: f64) -> bool {
4671+
value.is_sign_negative()
4672+
|| (value == f64::INFINITY
4673+
&& (self.options.minify_syntax || !self.options.has_run_symbol_renamer))
4674+
}
4675+
46674676
pub fn print_property(&mut self, item_in: &G::Property) {
46684677
// PORT NOTE: Zig took G.Property by value (Copy in Zig). Rust's
46694678
// G::Property isn't `Copy`, so take a borrow and shallow-copy the
@@ -4789,6 +4798,16 @@ pub mod __gated_printer {
47894798

47904799
let key = item.key.expect("infallible: prop has key");
47914800

4801+
// Automatically print numbers that would cause a syntax error as computed properties
4802+
if !IS_JSON
4803+
&& !item.flags.contains(js_ast::flags::Property::IsComputed)
4804+
&& matches!(&key.data, ExprData::ENumber(e) if self.number_property_key_must_be_computed(e.value))
4805+
{
4806+
// "{ -1: 0 }" must be printed as "{ [-1]: 0 }"
4807+
// "{ 1/0: 0 }" must be printed as "{ [1/0]: 0 }"
4808+
set_flag(&mut item.flags, js_ast::flags::Property::IsComputed, true);
4809+
}
4810+
47924811
if !IS_JSON && item.flags.contains(js_ast::flags::Property::IsComputed) {
47934812
self.print(b"[");
47944813
self.print_expr(key, Level::Comma, ExprFlag::none());
@@ -5061,7 +5080,15 @@ pub mod __gated_printer {
50615080
if property.flags.contains(js_ast::flags::Property::IsSpread) {
50625081
self.print(b"...");
50635082
} else {
5064-
if property.flags.contains(js_ast::flags::Property::IsComputed) {
5083+
// Automatically print numbers that would cause a syntax error as computed properties
5084+
let key_must_be_computed = matches!(
5085+
&property.key.data,
5086+
ExprData::ENumber(e) if self.number_property_key_must_be_computed(e.value)
5087+
);
5088+
5089+
if property.flags.contains(js_ast::flags::Property::IsComputed)
5090+
|| key_must_be_computed
5091+
{
50655092
self.print(b"[");
50665093
self.print_expr(property.key, Level::Comma, ExprFlag::none());
50675094
self.print(b"]:");

test/bundler/bundler_minify.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,31 @@ describe("bundler", () => {
272272
minifySyntax: true,
273273
minifyWhitespace: true,
274274
});
275+
// Numeric property names that would be printed as "1/0" (Infinity) or "-1" (inlined const
276+
// enums) are not valid syntax in property-name position and must become computed properties.
277+
// The negative const enum key case is https://github.com/oven-sh/bun/issues/14687
278+
itBundled("minify/NumericPropertyKeysPrintedAsComputed", {
279+
files: {
280+
"/entry.ts": /* ts */ `
281+
import { E } from "./enum";
282+
const obj = { 1e999: "inf", [E.Negative]: "neg" };
283+
const { 1e999: destructured } = obj;
284+
class C {
285+
1e999() { return "method"; }
286+
static 1e999 = "static";
287+
}
288+
console.log(JSON.stringify([obj[Infinity], obj[-1], destructured, new C()[Infinity](), C[Infinity]]));
289+
`,
290+
"enum.ts": /* ts */ `
291+
export const enum E { Negative = -1 }
292+
`,
293+
},
294+
minifySyntax: true,
295+
minifyWhitespace: true,
296+
run: {
297+
stdout: '["inf","neg","inf","method","static"]',
298+
},
299+
});
275300
itBundled("minify/InlineArraySpread", {
276301
files: {
277302
"/entry.js": /* js */ `

test/bundler/transpiler/transpiler.test.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4411,3 +4411,48 @@ describe("minifyWhitespace keeps the space before keyword operators", () => {
44114411
expect(minifier.transformSync("1 in y")).toBe("1 in y;");
44124412
});
44134413
});
4414+
4415+
// A numeric literal property name like `1e999` overflows to the number Infinity, which the
4416+
// printer emits as "1/0" / "1 / 0". That is not valid syntax in property-name position, so such
4417+
// keys must be printed as computed properties instead.
4418+
describe("numeric property keys that overflow to Infinity", () => {
4419+
const minifier = new Bun.Transpiler({ loader: "ts", minifyWhitespace: true });
4420+
const plain = new Bun.Transpiler({ loader: "ts" });
4421+
4422+
it("are printed as computed properties when minifying whitespace", () => {
4423+
expect(minifier.transformSync("x = { 1e999: 1 };")).toBe("x={[1/0]:1};");
4424+
expect(minifier.transformSync("x = { 1e999() {} };")).toBe("x={[1/0](){}};");
4425+
expect(minifier.transformSync("x = { get 1e999() {} };")).toBe("x={get[1/0](){}};");
4426+
expect(minifier.transformSync("x = { set 1e999(v) {} };")).toBe("x={set[1/0](v){}};");
4427+
expect(minifier.transformSync("x = class { 1e999() {} };")).toBe("x=class{[1/0](){}};");
4428+
expect(minifier.transformSync("x = class { static 1e999() {} };")).toBe("x=class{static[1/0](){}};");
4429+
expect(minifier.transformSync("x = class { 1e999 = 1 };")).toBe("x=class{[1/0]=1};");
4430+
expect(minifier.transformSync("x = class { static 1e999 = 1 };")).toBe("x=class{static[1/0]=1};");
4431+
expect(minifier.transformSync("const { 1e999: y } = x;")).toBe("const{[1/0]:y}=x;");
4432+
expect(minifier.transformSync("({ 1e999: x.y } = z);")).toBe("({[1/0]:x.y}=z);");
4433+
});
4434+
4435+
it("are printed as computed properties without minification", () => {
4436+
expect(plain.transformSync("x = { 1e999: 1 };")).toBe("x = { [1 / 0]: 1 };\n");
4437+
expect(plain.transformSync("x = class { 1e999() {} };")).toBe("x = class {\n [1 / 0]() {}\n};\n");
4438+
expect(plain.transformSync("const { 1e999: y } = x;")).toBe("const { [1 / 0]: y } = x;\n");
4439+
});
4440+
4441+
it("handles a method name with hundreds of digits", () => {
4442+
const digits = Buffer.alloc(325, "9").toString();
4443+
expect(minifier.transformSync(`(class { ${digits}() {} });`)).toBe("(class{[1/0](){}});");
4444+
});
4445+
4446+
it("still refers to the same property at runtime", () => {
4447+
const out = minifier.transformSync(`
4448+
const obj = { 1e999: "object" };
4449+
const { 1e999: destructured } = { 1e999: "destructured" };
4450+
class C {
4451+
1e999() { return "method"; }
4452+
static 1e999 = "static";
4453+
}
4454+
var result = [obj[Infinity], destructured, new C()[Infinity](), C[Infinity]];
4455+
`);
4456+
expect(new Function(`${out}; return result;`)()).toEqual(["object", "destructured", "method", "static"]);
4457+
});
4458+
});

0 commit comments

Comments
 (0)