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
3 changes: 3 additions & 0 deletions src/js_parser/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1312,6 +1312,9 @@ pub struct FnOnlyDataVisit {
/// or a class declaration). That means the top-level module scope "this" value
/// has been shadowed and is now inaccessible.
pub(crate) is_this_nested: bool,

/// True in a class field initializer or a class static block, which run as a method call.
pub(crate) is_new_target_undefined: bool,
}

/// Due to ES6 destructuring patterns, there are many cases where it's
Expand Down
6 changes: 5 additions & 1 deletion src/js_parser/visit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1062,7 +1062,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
self.fn_or_arrow_data_visit = FnOrArrowDataVisit::default();
self.fn_only_data_visit = FnOnlyDataVisit {
is_this_nested: true,
..Default::default()
is_new_target_undefined: true,
};
// PropertyKind::ClassStaticBlock guarantees `Some`; arena-owned for 'a.
let csb = property.class_static_block_mut().unwrap();
Expand Down Expand Up @@ -1193,6 +1193,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
if let Some(val) = property.initializer {
let class_body = self.current_scope;
self.field_init_class_bodies.push(class_body);
let old_is_new_target_undefined =
self.fn_only_data_visit.is_new_target_undefined;
self.fn_only_data_visit.is_new_target_undefined = true;
if let Some(name) = name_to_keep {
let was_anon = val.is_anonymous_named();
let prev_dcn2 = self.decorator_class_name;
Expand All @@ -1211,6 +1214,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
} else {
self.visit_expr(property.initializer.as_mut().unwrap());
}
self.fn_only_data_visit.is_new_target_undefined = old_is_new_target_undefined;
self.field_init_class_bodies.pop();
}

Expand Down
7 changes: 6 additions & 1 deletion src/js_parser/visit/visit_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,14 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
// Private associated fns on this impl so they can see the const-generic
// feature params.

fn e_new_target(_: &mut Self, _e: &mut Expr, _: ExprIn) {
fn e_new_target(p: &mut Self, e: &mut Expr, _: ExprIn) {
// The "Cannot use \"new.target\" here" range error is intentionally
// not emitted: it is not necessary and it was causing breakages.

// JavaScriptCore can throw for it here: https://github.com/oven-sh/WebKit/pull/647
if p.fn_only_data_visit.is_new_target_undefined {
*e = p.new_expr(E::Undefined {}, e.loc);
}
}

fn e_string(_: &mut Self, _e: &mut Expr, _: ExprIn) {
Expand Down
3 changes: 2 additions & 1 deletion src/jsc/RuntimeTranspilerCache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ bun_core::declare_scope!(cache, visible);
/// Version 30: String enum members are stored flat, so folds no longer append onto an inlined member.
/// Version 31: Standard decorator lowering temporaries have a per-file counter in their name (`_init$1`).
/// Version 32: Standard decorator lowering keeps class members in place.
const EXPECTED_VERSION: u32 = 32;
/// Version 33: `new.target` in a class field initializer or a class static block is printed as `undefined`.
const EXPECTED_VERSION: u32 = 33;

/// 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
26 changes: 26 additions & 0 deletions test/bundler/bundler_edgecase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2914,6 +2914,32 @@ describe("bundler", () => {
target: "bun",
run: { stdout: "before\nEFFECT1\n2\nEFFECT2\n3" },
});
// The `__esm` wrapper is an arrow function with no function around it. On
// entry to such an arrow function JavaScriptCore throws "ReferenceError:
// Can't find private variable: PrivateSymbol.newTargetLocal" when a class in
// it has `new.target` in a field initializer or a static block. The value
// there is always undefined, and the unbundled module runs.
itBundled("edgecase/EsmWrapNewTargetInClassFieldInitializer", {
files: {
"/lazy.js": `
export class A {
x = typeof new.target;
static y = typeof new.target;
static { A.z = typeof new.target; }
}
`,
"/entry.js": `
const { A } = await import("./lazy.js");
console.log(new A().x, A.y, A.z);
`,
},
entryPoints: ["/entry.js"],
target: "bun",
run: { stdout: "undefined undefined undefined" },
onAfterBundle(api) {
expect(api.readFile("/out.js")).toContain("__esm");
},
});
// https://github.com/oven-sh/bun/issues/30269
// Same bug for a nested `let` binding instead of a function parameter.
itBundled("identifiers/NestedLocalDoesNotShadowLaterHoistedFunction", {
Expand Down
80 changes: 80 additions & 0 deletions test/bundler/transpiler/runtime-transpiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,86 @@ test("math.pow", () => {
expect(20.4 ** -0.5 + "").toEqual("0.22140372138502384");
});

// A class field initializer and a class static block run as a method call, so
// `new.target` is undefined in them. JavaScriptCore throws
// "ReferenceError: Can't find private variable: PrivateSymbol.newTargetLocal"
// on entry to an arrow function that contains such a class and has no function
// around it.
test("new.target in a class field initializer or a class static block is undefined", async () => {
using dir = tempDir("transpiler-new-target-class-field", {
"index.mjs": `
const results = {};
async function t(name, f) {
try {
results[name] = await f();
} catch (e) {
results[name] = "throws " + e;
}
}

// The class is inside an arrow function with no function around it.
await t("instance field", () => { class A { x = typeof new.target; } return new A().x; });
await t("static field", () => { class A { static x = typeof new.target; } return A.x; });
await t("static block", () => { let r; class A { static { r = typeof new.target; } } return r; });
await t("private field", () => { class A { #x = typeof new.target; get x() { return this.#x; } } return new A().x; });
await t("class expression", () => new (class { x = typeof new.target; })().x);
await t("derived class", () => { class B {} class A extends B { x = typeof new.target; } return new A().x; });
await t("arrow in arrow", () => (() => { class A { x = typeof new.target; } return new A().x; })());
await t("arrow in field", () => { class A { x = (() => typeof new.target)(); } return new A().x; });
await t("key of a class in a field", () => { class A { x = class { static [typeof new.target] = 1; }; } return Object.keys(new A().x)[0]; });
await t("async arrow", async () => { class A { x = typeof new.target; } return new A().x; });

// A static block in an arrow function throws with a function around the arrow function too.
await t("static block, arrow in function", function () { return (() => { let r; class A { static { r = typeof new.target; } } return r; })(); });

// The transpiler moves the initializer of an auto-accessor into the constructor.
await t("auto-accessor", function () { class A { accessor x = typeof new.target; } return new A().x; });

// These see the new.target of a function, and keep it.
await t("function in field", () => { class A { f = function () { return new.target; }; } const f = new A().f; return new f() === f; });
await t("function in static block", () => { let f; class A { static { f = function () { return new.target; }; } } return new f() === f; });
await t("constructor", () => { class A { constructor() { this.x = new.target; } } return new A().x === A; });
await t("field key", () => { let k; function F() { class A { [new.target.name] = 1; } k = Object.keys(new A())[0]; } new F(); return k; });
await t("extends clause", () => { let ok; function F() { class A extends new.target.Base {} ok = new A() instanceof F.Base; } F.Base = class {}; new F(); return ok; });

console.log(JSON.stringify(results));
`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "index.mjs"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

// Debug builds print an ASAN warning here.
expect(stderr.split("\n").filter(line => line && !line.startsWith("WARNING: ASAN"))).toEqual([]);
expect(JSON.parse(stdout)).toEqual({
"instance field": "undefined",
"static field": "undefined",
"static block": "undefined",
"private field": "undefined",
"class expression": "undefined",
"derived class": "undefined",
"arrow in arrow": "undefined",
"arrow in field": "undefined",
"key of a class in a field": "undefined",
"async arrow": "undefined",
"static block, arrow in function": "undefined",
"auto-accessor": "undefined",
"function in field": true,
"function in static block": true,
"constructor": true,
"field key": "F",
"extends clause": true,
});
expect(exitCode).toBe(0);
});

describe("unterminated string literals in large files", () => {
test("reports an unterminated string literal at the end of a large JavaScript file", async () => {
using dir = tempDir("transpiler-long-unterminated-js", {
Expand Down
45 changes: 45 additions & 0 deletions test/bundler/transpiler/transpiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3817,6 +3817,51 @@ class Foo {
);
});

it("new.target in a class field initializer or a class static block is undefined", () => {
// Both run as a method call. An arrow function takes the value from them.
expectPrinted_("class Foo { x = new.target }", "class Foo {\n x = undefined;\n}");
expectPrinted_("class Foo { static x = new.target }", "class Foo {\n static x = undefined;\n}");
expectPrinted_("class Foo { #x = new.target }", "class Foo {\n #x = undefined;\n}");
expectPrinted_("class Foo { static { foo(new.target) } }", "class Foo {\n static {\n foo(undefined);\n }\n}");
expectPrinted_("class Foo { x = () => foo(new.target) }", "class Foo {\n x = () => foo(undefined);\n}");
expectPrinted_(
"class Foo { static { (() => foo(new.target))() } }",
"class Foo {\n static {\n (() => foo(undefined))();\n }\n}",
);
// The key of a class in a field initializer is part of the field initializer.
expectPrinted_(
"class Foo { x = class { [new.target] = new.target } }",
"class Foo {\n x = class {\n [undefined] = undefined;\n };\n}",
);

// A function has its own new.target.
expectPrinted_(
"class Foo { x = function() { return new.target } }",
"class Foo {\n x = function() {\n return new.target;\n };\n}",
);
expectPrinted_(
"class Foo { x = { m() { return new.target } } }",
"class Foo {\n x = { m() {\n return new.target;\n } };\n}",
);
expectPrinted_(
"class Foo { static { function f() { return new.target } } }",
"class Foo {\n static {\n function f() {\n return new.target;\n }\n }\n}",
);
expectPrinted_(
"class Foo { constructor() { foo(new.target) } m() { foo(new.target) } }",
"class Foo {\n constructor() {\n foo(new.target);\n }\n m() {\n foo(new.target);\n }\n}",
);
// A key and an extends clause belong to the code around the class.
expectPrinted_(
"function f() { class Foo extends new.target { [new.target] = 1; static [new.target] = 2; [new.target]() {} } }",
"function f() {\n class Foo extends new.target {\n [new.target] = 1;\n static [new.target] = 2;\n [new.target]() {}\n }\n}",
);
expectPrinted_(
"function f() { class Foo { x = function() { class Bar { [new.target] = new.target } } } }",
"function f() {\n class Foo {\n x = function() {\n class Bar {\n [new.target] = undefined;\n }\n };\n }\n}",
);
});

it("declarations named eval or arguments, and reserved words, in strict mode", () => {
expectParseError(
'"use strict"; var arguments = 1',
Expand Down
16 changes: 16 additions & 0 deletions test/bundler/transpiler/ts-use-define-for-class-fields.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,22 @@ describe("tsconfig compilerOptions.useDefineForClassFields", () => {
expect(exitCode).toBe(0);
});

// `new.target` is the class in the constructor and undefined in a field initializer.
test.concurrent("false: new.target in a field initializer stays undefined", async () => {
using dir = tempDir("udfcf-new-target", {
"tsconfig.json": JSON.stringify({ compilerOptions: { useDefineForClassFields: false } }),
"index.ts": `
class C { x = typeof new.target; y = () => typeof new.target; constructor(public z = typeof new.target) {} }
const c = new C();
process.stdout.write(JSON.stringify({ x: c.x, y: c.y(), z: c.z }));
`,
});
const { stdout, stderr, exitCode } = await run(String(dir));
expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual({ x: "undefined", y: "undefined", z: "function" });
expect(exitCode).toBe(0);
});

test.concurrent("false: computed literal keys use [[Set]] semantics", async () => {
using dir = tempDir("udfcf-computed", {
"tsconfig.json": JSON.stringify({ compilerOptions: { useDefineForClassFields: false } }),
Expand Down
Loading