Skip to content
Open
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: 2 additions & 1 deletion scripts/build/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,8 @@ function emitRuntimeJs({ n, cfg, o, dirStamp }: Ctx): void {
n.build({
outputs: [out],
rule: "esbuild",
inputs: [src],
// runtime.bun.js re-exports runtime.js, which holds most of the helpers.
inputs: [src, resolve(cfg.cwd, "src", "runtime.js")],
implicitInputs: [o.rootInstall],
orderOnlyInputs: [dirStamp],
vars: {
Expand Down
74 changes: 46 additions & 28 deletions src/js_parser/lower/lower_decorators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -821,12 +821,6 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
} else {
Ref::NONE
};
let mut base_ref: Option<Ref> = None;
if has_decorators && let Some(extends) = class.extends {
let base = p.declared_temp(b"_base");
class.extends = Some(p.assign_to(base, extends, extends.loc));
base_ref = Some(base);
}

let mut members = BumpVec::<Property>::with_capacity_in(class.properties.len() + 4, bump);
let mut key_effects = BumpVec::<Expr>::new_in(bump);
Expand Down Expand Up @@ -977,8 +971,13 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
next_initializer[group] += 1;
}
private_lowered_map.insert(private_index, info);
let effects =
p.storage_init_effects(storage, prop.initializer, initializer_index, loc);
let effects = p.storage_init_effects(
storage,
prop.initializer,
FieldName::Key(name_expr),
initializer_index,
loc,
);
if is_static {
members.push(p.make_static_block(&effects, loc));
} else {
Expand Down Expand Up @@ -1039,8 +1038,18 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
initializer_index = Some((init_ref, next_initializer[group]));
next_initializer[group] += 1;
}
let effects =
p.storage_init_effects(storage, prop.initializer, initializer_index, loc);
let field_name = if is_constant_key(&name_expr) {
FieldName::Key(name_expr)
} else {
FieldName::Computed(name_expr)
};
let effects = p.storage_init_effects(
storage,
prop.initializer,
field_name,
initializer_index,
loc,
);
if is_static {
members.push(p.make_static_block(&effects, loc));
} else {
Expand All @@ -1058,10 +1067,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let name: &'a [u8] = p.symbols[private_index as usize].original_name.slice();
field_name = FieldName::Key(p.new_expr(E::EString::init(name), loc));
} else {
// Only a field that may carry effects has to name its function itself.
// Only a decorated field, or one that may carry effects, names its function itself.
let names_function = !is_method
&& !is_static
&& (!instance_effects.is_empty() || first_instance_host.is_none())
&& (dec_ref.is_some()
|| !is_static
&& (!instance_effects.is_empty() || first_instance_host.is_none()))
&& prop
.initializer
.is_some_and(|value| value.is_anonymous_named());
Expand Down Expand Up @@ -1089,6 +1099,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
init_ref,
next_initializer[group],
prop.initializer,
field_name,
loc,
);
next_initializer[group] += 1;
Expand Down Expand Up @@ -1166,11 +1177,14 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
leading.extend_from_slice(&static_brands);
let mut class_decorators_result: Option<ClassDecorators> = None;
if has_decorators {
let base = match base_ref {
Some(base) => p.use_ref(base, loc),
None => p.new_expr(E::Undefined {}, loc),
// `__decoratorStart` reads the class that `this` extends off its prototype.
let undefined = p.new_expr(E::Undefined {}, loc);
let start = if class.extends.is_some() {
let this = p.new_expr(E::This {}, loc);
p.call_rt(loc, b"__decoratorStart", &[undefined, this])
} else {
p.call_rt(loc, b"__decoratorStart", &[undefined])
};
let start = p.call_rt(loc, b"__decoratorStart", &[base]);
leading.push(p.assign_to(init_ref, start, loc));
let has_static_method_decorators = !decorate[0].is_empty();
for group in &decorate {
Expand Down Expand Up @@ -1394,17 +1408,21 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
prop.flags.insert(Flags::Property::IsComputed);
}

/// The initializer of a field that is about to go behind a comma. There an
/// anonymous function or class is not named by the field any more.
/// The initializer of a field that is about to go behind a comma.
fn hosted_initializer(
&mut self,
initializer: Option<Expr>,
field_name: FieldName,
loc: bun_ast::Loc,
) -> Expr {
let Some(value) = initializer else {
return self.new_expr(E::Undefined {}, loc);
};
match initializer {
Some(value) => self.named_by_field(value, field_name),
None => self.new_expr(E::Undefined {}, loc),
}
}

/// `value` with the name `field_name` gives it, for behind a comma or in a call argument.
fn named_by_field(&mut self, value: Expr, field_name: FieldName) -> Expr {
if !value.is_anonymous_named() {
return value;
}
Expand Down Expand Up @@ -1457,13 +1475,14 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
init_ref: Ref,
index: usize,
initializer: Option<Expr>,
field_name: FieldName,
loc: bun_ast::Loc,
) -> DecoratedInitializer {
let mut args = BumpVec::<Expr>::with_capacity_in(4, self.arena);
args.push(self.use_ref(init_ref, loc));
args.push(self.new_expr(E::Number::new(((4 + 2 * index) << 1) as f64), loc));
args.push(self.new_expr(E::This {}, loc));
args.extend(initializer);
args.extend(initializer.map(|value| self.named_by_field(value, field_name)));
let value = self.call_runtime(loc, b"__runInitializers", ExprNodeList::from_bump_vec(args));
let this = self.new_expr(E::This {}, loc);
let extra = self.run_initializers(init_ref, (((5 + 2 * index) << 1) | 1) as f64, this, loc);
Expand All @@ -1476,19 +1495,18 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
&mut self,
storage: Ref,
initializer: Option<Expr>,
field_name: FieldName,
decorated: Option<(Ref, usize)>,
loc: bun_ast::Loc,
) -> BumpVec<'a, Expr> {
let mut effects = BumpVec::<Expr>::with_capacity_in(2, self.arena);
let (value, extra) = match decorated {
Some((init_ref, index)) => {
let decorated = self.decorated_initializer(init_ref, index, initializer, loc);
let decorated =
self.decorated_initializer(init_ref, index, initializer, field_name, loc);
(decorated.value, Some(decorated.extra))
}
None => (
initializer.unwrap_or_else(|| self.new_expr(E::Undefined {}, loc)),
None,
),
None => (self.hosted_initializer(initializer, field_name, loc), None),
};
let this = self.new_expr(E::This {}, loc);
let storage = self.use_ref(storage, loc);
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: Standard decorator lowering names the function a decorated field or accessor is initialized with, and no longer names an anonymous base class.
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
11 changes: 9 additions & 2 deletions src/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,11 @@ export var __privateSet = (obj, member, value, setter) => (
);
export var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);

export var __decoratorStart = base => [, , , __create(base?.[__knownSymbol("metadata")] ?? null)];
// `target` is a derived class. Older output passes the class it extends as `base`.
export var __decoratorStart = (base, target) => (
target && (base = __getProtoOf(target)),
[, , , __create(base?.[__knownSymbol("metadata")] ?? null)]
);
var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
var __expectFn = fn => (fn !== void 0 && typeof fn !== "function" ? __typeError("Function expected") : fn);
var __decoratorContext = (kind, name, done, metadata, fns) => ({
Expand Down Expand Up @@ -286,7 +290,10 @@ export var __decorateElement = (array, flags, name, decorators, target, extra) =
},
name,
));
k ? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name) : __name(target, name);
// A static member called `name` has already replaced the string every class starts with.
k
? p && k < 4 && __name(extra, (k > 2 ? "set " : k > 1 ? "get " : "") + name)
: typeof __getOwnPropDesc(target, "name")?.value == "string" && __name(target, name);

for (var i = decorators.length - 1; i >= 0; i--) {
ctx = __decoratorContext(k, name, (done = {}), array[3], extraInitializers);
Expand Down
118 changes: 118 additions & 0 deletions test/bundler/transpiler/es-decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1584,6 +1584,94 @@ describe("ES Decorators", () => {
expect(exitCode).toBe(0);
});

test.concurrent("a decorated field or an accessor names the function it is initialized with", async () => {
const { stdout, stderr, exitCode } = await runDecorator(`
const dec = (v, ctx) => {};
const k = "dyn", sym = Symbol("desc");
class A {
@dec f = () => {}; @dec static g = function () {}; @dec h = class {};
@dec static [k] = () => {}; @dec [sym] = function () {}; @dec 123 = class {}; @dec "q r" = () => {}; @dec ["__proto__"] = function () {};
@dec own = class { static name() {} }; @dec named = function keep() {};
@dec #p = () => {}; @dec static #sp = class {};
accessor a1 = () => {}; @dec accessor a2 = function () {}; static accessor a3 = class {}; @dec static accessor [k + "2"] = () => {};
accessor #a4 = () => {}; @dec accessor #a5 = () => {}; @dec static accessor #a6 = class {};
privateNames() { return [this.#p.name, A.#sp.name, this.#a4.name, this.#a5.name, A.#a6.name] }
}
const a = new A();
console.log(JSON.stringify([a.f.name, A.g.name, a.h.name, A[k].name, a[sym].name, a[123].name, a["q r"].name, a["__proto__"].name, typeof a.own.name, a.named.name]));
console.log(JSON.stringify([a.privateNames(), a.a1.name, a.a2.name, A.a3.name, A[k + "2"].name, Object.getPrototypeOf(a) === A.prototype]));
// The \`init\` functions of the decorators get the function already named.
const seen = [];
const init = (v, ctx) => { const f = (x) => (seen.push(x.name), x); return ctx.kind === "accessor" ? { init: f } : f };
class B { @init f = () => {}; @init static g = class {}; @init accessor h = function () {}; @init #p = () => {}; }
new B();
console.log(JSON.stringify(seen));
`);
expect(stderr).toBe("");
expect(stdout).toBe(
'["f","g","h","dyn","[desc]","123","q r","__proto__","function","keep"]\n' +
'[["#p","#sp","#a4","#a5","#a6"],"a1","a2","a3","dyn2",true]\n' +
'["g","f","h","#p"]\n',
);
expect(exitCode).toBe(0);
});

test.concurrent("an anonymous class or function in the extends clause stays anonymous", async () => {
const { stdout, stderr, exitCode } = await runDecorator(`
const dec = (v, ctx) => { if (ctx.kind !== "class") ctx.metadata[ctx.name] = true };
const base = (C) => Object.getPrototypeOf(C);
const metadata = Symbol.metadata ?? Symbol.for("Symbol.metadata");
class A extends class {} { @dec m() {} }
class B extends (function () {}) { @dec m() {} }
class C extends class Named {} { @dec m() {} }
class D extends class { static name() {} } { @dec m() {} }
class E extends class { @dec x() {} } { @dec m() {} }
const F = class extends class {} { @dec m() {} };
@dec class G extends class {} {}
class H extends class { accessor y = 1 } { accessor x = 2 }
console.log(JSON.stringify([A, B, C, D, E, F, G, H].map(K => base(K).name)));
// The metadata of the class still inherits from the metadata of what it extends.
class Nul extends null { @dec static s() {} }
class Top { @dec t() {} }
console.log(JSON.stringify([
Object.getPrototypeOf(E[metadata]) === base(E)[metadata], Object.keys(E[metadata]), E[metadata].x,
Object.getPrototypeOf(Nul[metadata]), Object.getPrototypeOf(Top[metadata]),
]));
`);
expect(stderr).toBe("");
expect(stdout).toBe('["","","Named",null,"","","",""]\n[true,["m"],true,null,null]\n');
expect(exitCode).toBe(0);
});

test.concurrent("a class decorator leaves a static member called name alone", async () => {
const { stdout, stderr, exitCode } = await runDecorator(`
const seen = [];
const dec = (v, ctx) => {};
const see = (v, ctx) => { seen.push(typeof v.name + ":" + ctx.name) };
@see class A { static name() { return "method" } }
const B = @see class { static name() { return "method" } };
@see class C { static get name() { return "getter" } }
@dec class D { static accessor name = "accessor" }
@dec class E { @dec static accessor name = "decorated accessor" }
@dec class F { static ["na" + "me"]() { return "computed" } }
@dec class G { static name = 1 }
// Without such a member the class keeps its name, or gets the one its context gives it.
@see class H {}
const I = @see class {};
const J = @see class Inner {};
console.log(JSON.stringify([A.name(), B.name(), C.name, D.name, E.name, F.name(), G.name, H.name, I.name, J.name]));
console.log(JSON.stringify([A, C, H, I].map(K => Object.getOwnPropertyDescriptor(K, "name")).map(d => [typeof (d.value ?? d.get), d.writable, d.enumerable, d.configurable])));
console.log(seen.join(" "));
`);
expect(stderr).toBe("");
expect(stdout).toBe(
'["method","method","getter","accessor","decorated accessor","computed",1,"H","I","Inner"]\n' +
'[["function",true,false,true],["function",null,false,true],["string",false,false,true],["string",false,false,true]]\n' +
"function:A function:B string:C string:H string:I string:Inner\n",
);
expect(exitCode).toBe(0);
});

test.concurrent(
"a class with no key for its decorator lists: private names, extends and the inner name",
async () => {
Expand Down Expand Up @@ -2391,6 +2479,31 @@ const extraSections = `
const desc = Object.getOwnPropertyDescriptor(N.prototype, "k");
out.accessorStorage = [nn.a, N.a, nn.priv(), nn.k, mm.k, n, typeof desc.get, typeof desc.set, Outer.make().a];
}

// Names are the ones the class would have without decorators: of the function
// a decorated field or an accessor is initialized with, of an anonymous class
// in the extends clause, and of a class with a static member called \`name\`.
{
const none = (value, ctx) => {};
class N1 {
@none f = () => {};
@none static g = function () {};
@none accessor h = class {};
accessor i = () => {};
@none #p = () => {};
p() { return this.#p.name; }
}
class N2 extends class {} { @none m() {} }
class N3 extends (function () {}) { accessor a = 1; }
@none class N4 { static name() {} }
const N5 = @none class { static get name() { return "N5.name"; } };
const n1 = new N1();
out.names = [
[n1.f.name, N1.g.name, n1.h.name, n1.i.name, n1.p()],
[Object.getPrototypeOf(N2).name, Object.getPrototypeOf(N3).name],
[typeof N4.name, N5.name],
];
}
`;

const extraExpected = {
Expand Down Expand Up @@ -2434,6 +2547,11 @@ const extraExpected = {
accessorKeys: [11, 2, 3, 4],
issue31921: ["a", 1, "a", "b", [5, "a", "block", "b"]],
accessorStorage: [12, 13, 1, 14, 15, 2, "function", "function", 6],
names: [
["f", "g", "h", "i", "#p"],
["", ""],
["function", "N5.name"],
],
};

function buildFixture() {
Expand Down
Loading