Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/bundler/LinkerContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2390,7 +2390,7 @@ impl<'a> LinkerContext<'a> {
input_module_type: ast.module_type,
module_type: self.options.output_format,
print_dce_annotations: self.options.emit_dce_annotations,
has_run_symbol_renamer: true,
global_value_properties_unshadowed: true,

to_esm_ref,
to_commonjs_ref,
Expand Down
4 changes: 2 additions & 2 deletions src/bundler/linker_context/postProcessJSChunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ pub(crate) fn post_process_js_chunk(
let make_print_options = || js_printer::Options {
bundling: true,
indent: Default::default(),
has_run_symbol_renamer: true,
global_value_properties_unshadowed: true,

require_ref: runtime_require_ref,
minify_whitespace: c.options.minify_whitespace,
Expand Down Expand Up @@ -1271,7 +1271,7 @@ pub(crate) fn generate_entry_point_tail_js<'a>(
let print_options = js_printer::Options {
// TODO: IIFE indent
indent: Default::default(),
has_run_symbol_renamer: true,
global_value_properties_unshadowed: true,

to_esm_ref,
to_commonjs_ref: to_common_js_ref,
Expand Down
73 changes: 59 additions & 14 deletions src/js_printer/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1350,10 +1350,12 @@ pub struct Options<'a> {
pub print_dce_annotations: bool,

pub inline_require_and_import_errors: bool,
/// A bundler renamer named every symbol. Those renamers reserve `NaN`,
/// `Infinity` and `undefined`, so the printer may emit those globals as
/// bare identifiers without a user binding shadowing them.
pub has_run_symbol_renamer: bool,
/// No binding in the printed code is named `undefined`, `NaN` or
/// `Infinity` (the value properties of the global object): a bundler
/// renamer reserved those names, or `print_ast` found no such symbol in
/// the file. When false, the printer writes `void 0`, `0 / 0` and `1 / 0`
/// instead of the names.
pub global_value_properties_unshadowed: bool,

pub require_or_import_meta_for_source_callback: RequireOrImportMetaCallback,

Expand Down Expand Up @@ -1428,7 +1430,7 @@ impl<'a> Default for Options<'a> {
minify_syntax: false,
print_dce_annotations: true,
inline_require_and_import_errors: true,
has_run_symbol_renamer: false,
global_value_properties_unshadowed: false,
require_or_import_meta_for_source_callback: RequireOrImportMetaCallback::default(),
input_module_type: bundle_opts::ModuleType::Unknown,
module_type: bundle_opts::Format::Esm,
Expand Down Expand Up @@ -1551,6 +1553,12 @@ fn is_identifier_or_numeric_constant_or_property_access(expr: &js_ast::Expr) ->
}
}

/// The names `print_undefined` and `print_number` use for values that have no
/// literal (see `Options::global_value_properties_unshadowed`).
fn is_global_value_property(name: &[u8]) -> bool {
matches!(name, b"undefined" | b"NaN" | b"Infinity")
}

pub enum PrintResult {
Result(PrintResultSuccess),
Err(crate::Error),
Expand Down Expand Up @@ -1680,6 +1688,9 @@ pub(crate) mod __gated_printer {
pub(crate) stack_overflowed: bool,

pub(crate) was_lazy_export: bool,
/// Depth of `with` statement bodies around the current position; in
/// there any printed identifier may resolve to a property instead.
pub(crate) with_nesting: u32,
// Always carried; gated at call sites with MAY_HAVE_MODULE_INFO.
pub(crate) module_info: Option<&'a mut analyze_transpiled_module::ModuleInfo>,

Expand Down Expand Up @@ -2130,9 +2141,15 @@ pub(crate) mod __gated_printer {
Level::Comma
}

/// `undefined`, `NaN` and `Infinity` printed here resolve to the globals.
#[inline]
pub(crate) fn can_name_global_value_properties(&self) -> bool {
self.options.global_value_properties_unshadowed && self.with_nesting == 0
}

#[inline]
pub(crate) fn print_undefined(&mut self, loc: bun_ast::Loc, level: Level) {
if self.options.minify_syntax {
if self.options.minify_syntax || !self.can_name_global_value_properties() {
if level.gte(Level::Prefix) {
self.add_source_mapping(loc);
self.print(b"(void 0)");
Expand Down Expand Up @@ -4727,11 +4744,12 @@ pub(crate) mod __gated_printer {

/// Whether a number used as a non-computed property name must be printed as a
/// computed property instead, because `print_number` would render it as
/// something that is not a valid property name (e.g. "-1", "1/0", "1 / 0").
/// something that is not a valid property name (e.g. "-1", "0 / 0", "1 / 0").
pub(crate) fn number_property_key_must_be_computed(&self, value: f64) -> bool {
value.is_sign_negative()
|| (value.is_nan() && !self.can_name_global_value_properties())
|| (value == f64::INFINITY
&& (self.options.minify_syntax || !self.options.has_run_symbol_renamer))
&& (self.options.minify_syntax || !self.can_name_global_value_properties()))
}

/// `E::ObjectJSON` (JSON-only): always printed in JSON shape.
Expand Down Expand Up @@ -5936,7 +5954,9 @@ pub(crate) mod __gated_printer {
self.print(b"(");
self.print_expr(s.value, Level::Lowest, ExprFlag::none());
self.print(b")");
self.with_nesting += 1;
self.print_body(s.body);
self.with_nesting -= 1;
}
StmtData::SLabel(s) => {
if !self.options.minify_whitespace && self.options.indent.count > 0 {
Expand Down Expand Up @@ -6918,11 +6938,27 @@ pub(crate) mod __gated_printer {
let abs_value = value.abs();
if value.is_nan() {
self.print_space_before_identifier();
self.print(b"NaN");
if IS_JSON || self.can_name_global_value_properties() {
self.print(b"NaN");
} else {
let wrap = level.gte(Level::Multiply);
if wrap {
self.print(b"(");
}
if self.options.minify_whitespace {
self.print(b"0/0");
} else {
self.print(b"0 / 0");
}
if wrap {
self.print(b")");
}
}
} else if value.is_infinite() {
let is_neg_inf = value.is_sign_negative();
let wrap = ((!self.options.has_run_symbol_renamer || self.options.minify_syntax)
&& level.gte(Level::Multiply))
let by_name = IS_JSON
|| (!self.options.minify_syntax && self.can_name_global_value_properties());
let wrap = (!by_name && level.gte(Level::Multiply))
|| (is_neg_inf && level.gte(Level::Prefix));

if wrap {
Expand All @@ -6936,8 +6972,7 @@ pub(crate) mod __gated_printer {
self.print_space_before_identifier();
}

// If we are not running the symbol renamer, we must not print "Infinity".
if IS_JSON || (!self.options.minify_syntax && self.options.has_run_symbol_renamer) {
if by_name {
self.print(b"Infinity");
} else if self.options.minify_whitespace {
self.print(b"1/0");
Expand Down Expand Up @@ -7029,6 +7064,7 @@ pub(crate) mod __gated_printer {
stack_check: bun_core::StackCheck::init(),
stack_overflowed: false,
was_lazy_export: false,
with_nesting: 0,
module_info: None,
}
}
Expand Down Expand Up @@ -7803,6 +7839,16 @@ pub fn print_ast<'a, W: WriterTrait, const ASCII_ONLY: bool, const GENERATE_SOUR
let _restore =
bun_crash_handler::scoped_action(bun_crash_handler::Action::Print(source.path.text));

// Neither renamer below renames a user binding called `undefined`, `NaN`
// or `Infinity` out of the way (`MinifyRenamer` keeps exported names), so
// the printer may only use those names when no bound symbol has one.
let mut opts = opts;
opts.global_value_properties_unshadowed =
!symbols.symbols_for_source.iter().flatten().any(|symbol| {
symbol.kind != js_ast::symbol::Kind::Unbound
&& is_global_value_property(symbol.original_name.slice())
});

// `Renamer<'r,'src>` is invariant in `'src` (it holds `&'r mut`
// NoOpRenamer<'src>`), so the two arms must agree on `'src`; constructing the
// `MinifyRenamer` variant inline (rather than via `to_renamer() ->
Expand Down Expand Up @@ -7911,7 +7957,6 @@ pub fn print_ast<'a, W: WriterTrait, const ASCII_ONLY: bool, const GENERATE_SOUR
// backing `Vec`. Cheap no-op on a reused (already-grown) writer.
let _ = writer.reserve(source.contents().len() as u64);

let mut opts = opts;
let source_map_builder = get_source_map_builder::<ASCII_ONLY>(
GenerateSourceMap::lazy_if(GENERATE_SOURCE_MAP),
&mut opts,
Expand Down
3 changes: 2 additions & 1 deletion src/jsc/RuntimeTranspilerCache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ bun_core::declare_scope!(cache, visible);
/// Version 27: ModuleInfo string table holds Latin-1 / UTF-16 bodies, not WTF-8.
/// Version 28: the define table and `--drop` entries participate in the features hash.
/// Version 29: `new Array(x, ...spread)` is no longer folded into an array literal.
const EXPECTED_VERSION: u32 = 29;
/// Version 30: NaN prints as `0 / 0` in a file that binds `NaN`, `Infinity` or `undefined`, and inside `with`.
const EXPECTED_VERSION: u32 = 30;

/// Source files smaller than this are not written to / read from the on-disk
/// transpiler cache. Originally 50 KiB, which excluded almost every file in a
Expand Down
153 changes: 145 additions & 8 deletions test/bundler/transpiler/transpiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5606,13 +5606,14 @@ it("transform() result is unaffected by detaching the input ArrayBuffer while th
});

// A numeric literal property name like `1e999` overflows to the number Infinity, which the
// printer emits as "1/0" / "1 / 0". That is not valid syntax in property-name position, so such
// keys must be printed as computed properties instead.
// printer emits as "1/0" / "1 / 0" when minifying syntax (or when the file binds the name
// `Infinity`). That is not valid syntax in property-name position, so such keys must be printed
// as computed properties instead.
describe("numeric property keys that overflow to Infinity", () => {
const minifier = new Bun.Transpiler({ loader: "ts", minifyWhitespace: true });
const minifier = new Bun.Transpiler({ loader: "ts", minify: { whitespace: true, syntax: true } });
const plain = new Bun.Transpiler({ loader: "ts" });

it("are printed as computed properties when minifying whitespace", () => {
it("are printed as computed properties when minifying", () => {
expect(minifier.transformSync("x = { 1e999: 1 };")).toBe("x={[1/0]:1};");
expect(minifier.transformSync("x = { 1e999() {} };")).toBe("x={[1/0](){}};");
expect(minifier.transformSync("x = { get 1e999() {} };")).toBe("x={get[1/0](){}};");
Expand All @@ -5625,10 +5626,20 @@ describe("numeric property keys that overflow to Infinity", () => {
expect(minifier.transformSync("({ 1e999: x.y } = z);")).toBe("({[1/0]:x.y}=z);");
});

it("are printed as computed properties without minification", () => {
expect(plain.transformSync("x = { 1e999: 1 };")).toBe("x = { [1 / 0]: 1 };\n");
expect(plain.transformSync("x = class { 1e999() {} };")).toBe("x = class {\n [1 / 0]() {}\n};\n");
expect(plain.transformSync("const { 1e999: y } = x;")).toBe("const { [1 / 0]: y } = x;\n");
it("are printed by name without minification", () => {
expect(plain.transformSync("x = { 1e999: 1 };")).toBe("x = { Infinity: 1 };\n");
expect(plain.transformSync("x = class { 1e999() {} };")).toBe("x = class {\n Infinity() {}\n};\n");
expect(plain.transformSync("const { 1e999: y } = x;")).toBe("const { Infinity: y } = x;\n");
});

it("are printed as computed properties when the file binds the name Infinity", () => {
expect(plain.transformSync("let Infinity; x = { 1e999: 1 };")).toBe("let Infinity;\nx = { [1 / 0]: 1 };\n");
expect(plain.transformSync("let Infinity; x = class { 1e999() {} };")).toBe(
"let Infinity;\nx = class {\n [1 / 0]() {}\n};\n",
);
expect(plain.transformSync("let Infinity; const { 1e999: y } = x;")).toBe(
"let Infinity;\nconst { [1 / 0]: y } = x;\n",
);
});

it("handles a method name with hundreds of digits", () => {
Expand Down Expand Up @@ -6117,3 +6128,129 @@ describe("same-target destructuring with an unstable target", () => {
expect(exitCode).toBe(0);
});
});

// A folded constant (`+"abc"`, `0 / 0` with inlining, `void 0`, `1e999`) has
// no symbol to bind, so printing it as the name `NaN`, `undefined` or
// `Infinity` lets a binding with that name capture it. The bundler renames
// such bindings; `bun run`, `bun build --no-bundle` and `Bun.Transpiler` do
// not, so there the printer writes `0 / 0`, `void 0` and `1 / 0` in a file
// that declares one of those names, and inside `with`.
describe("NaN, undefined and Infinity next to a binding with that name", () => {
// Sloppy-mode script: evaluates to the `out` object. Every binding is kept
// alive (mutated or otherwise used) so dead code elimination cannot hide
// the capture.
const fixture = /* js */ `
var out = {};
for (let NaN = 0; NaN < 1; NaN++) out.forLet = [String(0 / 0), String(+"abc"), String(Math.min(0 / 0, 5))];
{ class NaN {} out.classDecl = String(0 / 0).slice(0, 5); }
switch (1) { case 1: let NaN = "case"; NaN += "!"; out.switchCase = [String(0 / 0), NaN]; }
label: { let NaN = "label"; NaN += "!"; out.label = [(0 / 0).toFixed(1), String(2 ** (0 / 0)), String((0 / 0) ** 0), NaN]; }
{ let undefined = "U"; undefined += "!"; out.undefinedLet = [String(void 0), typeof (void 0), undefined]; }
{ let Infinity = "I"; Infinity += "!"; out.infinityLet = [String(+"1e999"), String(-1e999), (1e999).toFixed(1), { 1e999: "key" }[1 / 0], Infinity]; }
function f(NaN, Infinity, undefined) {
undefined += "!";
return [String(+"abc"), String(+"1e999"), String(void 0), typeof +"abc", NaN, Infinity, undefined];
}
out.params = f("n", "i", "u");
with ({ NaN: "W", Infinity: "WI", undefined: "WU" }) out.with = [String(+"abc"), String(0 / 0), String(1e999), String(void 0)];
`;
// What the source means (and what node prints for it).
const expected = {
forLet: ["NaN", "NaN", "NaN"],
classDecl: "NaN",
switchCase: ["NaN", "case!"],
label: ["NaN", "NaN", "1", "label!"],
undefinedLet: ["undefined", "undefined", "U!"],
infinityLet: ["Infinity", "-Infinity", "Infinity", "key", "I!"],
params: ["NaN", "Infinity", "undefined", "number", "n", "i", "u!"],
with: ["NaN", "NaN", "Infinity", "undefined"],
};
// `new Function` hands the printed code straight to JavaScriptCore, without
// a second pass through Bun's transpiler.
const evaluate = code => new Function(code + "\nreturn out;")();

it.each([
["default", {}],
["inline + minify syntax (like bun run)", { inline: true, minify: { syntax: true } }],
["inline + minify whitespace", { inline: true, minifyWhitespace: true }],
])("Bun.Transpiler (%s)", (_, options) => {
const code = new Bun.Transpiler({ loader: "js", ...options }).transformSync(fixture);
expect(evaluate(code)).toEqual(expected);
});

it("prints the names only where nothing can shadow them", () => {
const plain = new Bun.Transpiler({ loader: "js" });
expect(plain.transformSync(`console.log(+"abc", void 0, 1e999, -1e999);`)).toBe(
"console.log(NaN, undefined, Infinity, -Infinity);\n",
);
expect(
plain.transformSync(`let NaN = 1;\nconsole.log(+"abc", void 0, 1e999, -1e999, (0 / 0).x, 2 ** +"abc");`),
).toBe("let NaN = 1;\nconsole.log(0 / 0, void 0, 1 / 0, -1 / 0, (0 / 0).x, 2 ** (0 / 0));\n");
expect(plain.transformSync(`with (x) y = [+"abc", void 0, 1e999];\nz = [+"abc", void 0, 1e999];`)).toBe(
"with (x)\n y = [0 / 0, void 0, 1 / 0];\nz = [NaN, undefined, Infinity];\n",
);
// The name of a class expression is a binding inside the class.
expect(plain.transformSync(`x = class NaN {\n static v = [+"abc", void 0, 1e999];\n};`)).toBe(
"x = class NaN {\n static v = [0 / 0, void 0, 1 / 0];\n};\n",
);
const minify = new Bun.Transpiler({ loader: "js", inline: true, minifyWhitespace: true });
expect(minify.transformSync(`let NaN = 1; console.log(+"abc", (0 / 0).x, x ** (0 / 0), -(0 / 0));`)).toBe(
"let NaN=1;console.log(0/0,(0/0).x,x**(0/0),0/0);",
);
// Data loaders turn top-level keys into bindings without going through the parser.
expect(plain.transformSync(`NaN = "n"\nInfinity = "i"\nx = nan\ny = -inf\n`, "toml")).toBe(
'var NaN = "n", Infinity = "i", x = 0 / 0, y = -1 / 0;\n\n' +
"export {\n NaN,\n Infinity,\n x,\n y\n};\n" +
"export default {\n NaN,\n Infinity,\n x,\n y\n};\n",
);
});

// The NaN twin of https://github.com/oven-sh/bun/issues/7263: the module's
// own export used to print as `export const NaN = NaN`, a TDZ error.
it.concurrent("bun run, module-level export", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`export const NaN = 0 / 0, Infinity = 1e999, undefined = void 0; console.log(String(NaN), String(Infinity), String(undefined));`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout).toBe("NaN Infinity undefined\n");
expect(exitCode).toBe(0);
});

it.concurrent("bun run", async () => {
using dir = tempDir("transpiler-nan-shadow", { "fixture.js": fixture + "\nconsole.log(JSON.stringify(out));" });
await using proc = Bun.spawn({
cmd: [bunExe(), "fixture.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
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(expected);
expect(exitCode).toBe(0);
});

it.concurrent("bun build --no-bundle", async () => {
using dir = tempDir("transpiler-nan-shadow", { "fixture.js": fixture });
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "--no-bundle", "fixture.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(evaluate(stdout)).toEqual(expected);
expect(exitCode).toBe(0);
});
});
Loading