Skip to content
Open
4 changes: 2 additions & 2 deletions src/ast/e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,7 @@ impl Number {
/// by calling out to the APIs in WebKit which are responsible for this operation.
///
/// This can return `None` in wasm builds to avoid linking JSC
pub(crate) fn to_string(self, bump: &Bump) -> Option<Str> {
pub fn to_string(self, bump: &Bump) -> Option<Str> {
Self::to_string_from_f64(self.value(), bump)
}

Expand Down Expand Up @@ -819,7 +819,7 @@ impl BigInt {
/// a syntax error, so any literal that starts with `0` and has more than
/// one character is a radix literal.
#[inline]
pub(crate) fn has_radix(v: &[u8]) -> bool {
pub fn has_radix(v: &[u8]) -> bool {
v.len() >= 2 && v[0] == b'0'
}

Expand Down
56 changes: 33 additions & 23 deletions src/js_parser/lower/lower_decorators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ use bun_alloc::ArenaVecExt as _;

use bun_collections::{HashMap, VecExt};

use crate::lexer as js_lexer;
use crate::p::P;
use crate::parser::{ARGUMENTS_STR as arguments_str, Ref, is_eval_or_arguments};
use crate::parser::{ARGUMENTS_STR as arguments_str, Ref};
use bun_ast::g::{DeclList, Property, PropertyKind};
use bun_ast::{self as js_ast, B, E, Expr, ExprNodeList, Flags, G, S, Stmt};

Expand Down Expand Up @@ -124,19 +123,20 @@ fn class_copy(c: &G::Class) -> G::Class {
}
}

/// Whether a context-inferred name (`export default` → "default", object
/// property keys, assignment targets) can be attached to a lowered anonymous
/// class expression as its syntactic binding name. Class bodies are always
/// strict mode code and the output may be a module, so reserved words
/// ("default", "let", "await", …), `eval`/`arguments`, and non-identifier
/// strings would turn `_class = class <name> {}` into a syntax error.
#[inline]
fn can_be_class_binding_name(name: &[u8]) -> bool {
js_lexer::is_identifier(name)
&& js_lexer::keyword(name).is_none()
&& !js_lexer::is_strict_mode_reserved_word(name)
&& name != b"await"
&& !is_eval_or_arguments(name)
/// Installed before static blocks run; a `static name` field instead wins on its own.
fn defines_static_name_method(props: &[Property]) -> bool {
props.iter().any(|prop| {
prop.flags.contains(Flags::Property::IsStatic)
&& (prop.flags.contains(Flags::Property::IsMethod)
// A decorated accessor is installed from the suffix instead.
|| (prop.kind == PropertyKind::AutoAccessor && prop.ts_decorators.len_u32() == 0))
&& match &prop.key {
Some(key) => {
matches!(&key.data, js_ast::ExprData::EString(s) if s.eql_comptime(b"name"))
}
None => false,
}
})
}

// ── impl P ───────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -1142,19 +1142,14 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
class_name_ref = ecr;
class_name_loc = loc;
expr_class_is_anonymous = true;
if let Some(name) = name_from_context
&& can_be_class_binding_name(name)
{
class.class_name = Some(js_ast::LocRef {
ref_: p.new_sym(js_ast::symbol::Kind::Other, name),
loc,
});
}
}
} else {
class_name_ref = class.class_name.as_ref().unwrap().ref_;
class_name_loc = class.class_name.as_ref().unwrap().loc;
}
// Decided before Phase 2 replaces decorated computed keys with temporaries.
let restore_inferred_name =
expr_class_is_anonymous && !defines_static_name_method(class.properties.slice());

let mut inner_class_ref: Ref = class_name_ref;
if !is_expr {
Expand Down Expand Up @@ -2422,6 +2417,21 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
new_properties = merged;
}

// A string literal, unlike a class binding, survives the bundler's renaming.
if restore_inferred_name {
let this_e = p.new_expr(E::This {}, loc);
let name_e = p.new_expr(
E::EString {
data: name_from_context.unwrap_or(b"").into(),
..Default::default()
},
loc,
);
let set_name = p.call_rt(loc, b"__name", &[this_e, name_e]);
let block = p.make_static_block(set_name, loc);
new_properties.insert(0, block);
}

class.properties = bun_ast::StoreSlice::new_mut(new_properties.into_bump_slice_mut());
class.has_decorators = false;
class.should_lower_standard_decorators = false;
Expand Down
79 changes: 46 additions & 33 deletions src/js_parser/visit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,30 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
self.stmts_to_single_stmt(stmt.loc, stmts.into_bump_slice_mut())
}

/// The `.name` that `key` gives an anonymous class value; lowering the class would lose it.
pub(crate) fn decorator_class_name_from_key(
&self,
key: Option<Expr>,
value: &Expr,
) -> Option<&'a [u8]> {
let ExprData::EClass(class) = value.data else {
return None;
};
if class.class_name.is_some() || !class.should_lower_standard_decorators {
return None;
}
match key?.unwrap_inlined().data {
// `slice` flattens ropes and transcodes UTF-16; `data` alone does not.
ExprData::EString(mut str_) => Some(str_.slice(self.arena)),
ExprData::ENumber(num) => num.to_string(self.arena).map(|s| s.slice()),
ExprData::EBigInt(bigint) if !E::BigInt::has_radix(&bigint.value) => {
Some(bigint.value.slice())
}
ExprData::EPrivateIdentifier(private) => Some(self.load_name_from_ref(private.ref_)),
_ => None,
}
}

pub(crate) fn visit_class(
&mut self,
name_scope_loc: bun_ast::Loc,
Expand Down Expand Up @@ -953,23 +977,18 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}

if let Some(val) = property.value {
let was_anon = val.is_anonymous_named();
let prev_dcn = self.decorator_class_name;
self.decorator_class_name =
self.decorator_class_name_from_key(property.key, &val);
self.visit_expr(property.value.as_mut().unwrap());
self.decorator_class_name = prev_dcn;
if let Some(name) = name_to_keep {
let was_anon = val.is_anonymous_named();
let prev_dcn = self.decorator_class_name;
if let ExprData::EClass(e_class) = &val.data {
if e_class.class_name.is_none()
&& e_class.should_lower_standard_decorators
{
self.decorator_class_name = Some(name);
}
}
let mut visited = val;
self.visit_expr(&mut visited);
property.value =
Some(self.maybe_keep_expr_symbol_name(visited, name, was_anon));
self.decorator_class_name = prev_dcn;
} else {
self.visit_expr(property.value.as_mut().unwrap());
property.value = Some(self.maybe_keep_expr_symbol_name(
property.value.expect("unreachable"),
name,
was_anon,
));
}

if Self::IS_TYPESCRIPT_ENABLED {
Expand All @@ -984,24 +1003,18 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}

if let Some(val) = property.initializer {
// if (property.flags.is_static and )
let was_anon = val.is_anonymous_named();
let prev_dcn = self.decorator_class_name;
self.decorator_class_name =
self.decorator_class_name_from_key(property.key, &val);
self.visit_expr(property.initializer.as_mut().unwrap());
self.decorator_class_name = prev_dcn;
if let Some(name) = name_to_keep {
let was_anon = val.is_anonymous_named();
let prev_dcn2 = self.decorator_class_name;
if let ExprData::EClass(e_class) = &val.data {
if e_class.class_name.is_none()
&& e_class.should_lower_standard_decorators
{
self.decorator_class_name = Some(name);
}
}
let mut visited = val;
self.visit_expr(&mut visited);
property.initializer =
Some(self.maybe_keep_expr_symbol_name(visited, name, was_anon));
self.decorator_class_name = prev_dcn2;
} else {
self.visit_expr(property.initializer.as_mut().unwrap());
property.initializer = Some(self.maybe_keep_expr_symbol_name(
property.initializer.expect("unreachable"),
name,
was_anon,
));
}
}

Expand Down
29 changes: 3 additions & 26 deletions src/js_parser/visit/visit_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1705,32 +1705,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}

if let Some(value) = &mut property.value {
// Propagate name from property key for decorated anonymous class expressions
// e.g., { Foo: @dec class {} } should give the class .name = "Foo"
if in_.assign_target == js_ast::AssignTarget::None
&& matches!(value.data, Data::EClass(..))
&& value
.data
.e_class()
.unwrap()
.should_lower_standard_decorators
&& value
.data
.e_class()
.expect("infallible: variant checked")
.class_name
.is_none()
&& let Some(key) = property.key
&& matches!(key.data, Data::EString(..))
{
let key_str = key.data.e_string().expect("infallible: variant checked");
// While E.rs has duplicate impls (E0034), reach the bytes directly
// — class-name keys are parser-produced (UTF-8, no rope).
p.decorator_class_name = if !key_str.is_utf16 {
Some(key_str.data.slice())
} else {
None
};
// { Foo: @dec class {} } gives the class .name = "Foo"
if in_.assign_target == js_ast::AssignTarget::None {
p.decorator_class_name = p.decorator_class_name_from_key(property.key, value);
}
p.visit_expr_in_out(
value,
Expand Down
45 changes: 45 additions & 0 deletions test/bundler/bundler_edgecase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3170,6 +3170,51 @@ describe("bundler", () => {
},
run: { stdout: "try:false" },
});
// Standard-decorator lowering rewrites `const Bar = class { ... }` into
// `_class = class { ... }`, so the class no longer infers its name from the
// binding. The name the lowering attaches instead has to survive the
// bundler's renaming of symbols that collide in the enclosing scope (the
// function-local `Bar` here, the CommonJS-wrapped module scope below) and
// identifier minification.
const decoratedAnonymousClassNames = /* js */ `
function dec() {}
function f() {
const Bar = class { @dec m() {} };
const Baz = class { accessor x; };
let Qux; Qux = class { @dec static s() {} };
const obj = { "not-an-identifier": class { @dec m() {} } };
return [Bar.name, Baz.name, Qux.name, obj["not-an-identifier"].name];
}
console.log(JSON.stringify(f()));
`;
itBundled("edgecase/DecoratedAnonymousClassExprKeepsInferredName", {
files: {
"/entry.js": decoratedAnonymousClassNames,
},
run: { stdout: '["Bar","Baz","Qux","not-an-identifier"]' },
});
itBundled("edgecase/DecoratedAnonymousClassExprKeepsInferredNameMinified", {
files: {
"/entry.js": decoratedAnonymousClassNames,
},
minifyIdentifiers: true,
minifySyntax: true,
minifyWhitespace: true,
run: { stdout: '["Bar","Baz","Qux","not-an-identifier"]' },
});
itBundled("edgecase/DecoratedAnonymousClassExprKeepsInferredNameInCJSWrapper", {
files: {
"/entry.js": /* js */ `
console.log(require("./mod.cjs").name);
`,
"/mod.cjs": /* js */ `
function dec() {}
const Bar = class { @dec m() {} };
module.exports = Bar;
`,
},
run: { stdout: "Bar" },
});
});

for (const backend of ["api", "cli"] as const) {
Expand Down
Loading
Loading