diff --git a/src/js_parser/lower/lower_decorators.rs b/src/js_parser/lower/lower_decorators.rs index 77c2620dac5e..056c77a83e6e 100644 --- a/src/js_parser/lower/lower_decorators.rs +++ b/src/js_parser/lower/lower_decorators.rs @@ -7,7 +7,7 @@ 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, TempRef, is_eval_or_arguments}; use bun_ast::g::{DeclList, Property, PropertyKind}; use bun_ast::{self as js_ast, B, E, Expr, ExprNodeList, Flags, G, S, Stmt}; @@ -165,13 +165,106 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O self.call_runtime(l, name, list) } - /// newSymbol + scope.generated.append in one call. - fn new_sym(&mut self, kind: js_ast::symbol::Kind, name: &'a [u8]) -> Ref { + /// For the few generated symbols that are not declared in the scope being + /// lowered into (a synthesized setter's parameter, `arguments`). Anything + /// declared there is a temporary and goes through `new_temp`. + fn new_fixed_name_sym(&mut self, kind: js_ast::symbol::Kind, name: &'static [u8]) -> Ref { + debug_assert!( + !name.starts_with(b"_"), + "lowering temporaries go through new_temp" + ); let ref_ = self.new_symbol(kind, name); VecExt::append(&mut self.current_scope_mut().generated, ref_); ref_ } + /// A temporary (`_init`, `_dec`, a member's `_name` WeakMap, ...) that the + /// lowering declares in the statement list it is lowering into, i.e. a + /// binding of the enclosing var-hoisting scope. The bundler's renamers + /// rename it from there; without a renamer, symbols print under their + /// original names, so `base` is only a request and `name_decorator_temps` + /// picks the final name once the whole file has been visited. + fn new_temp(&mut self, base: &'a [u8]) -> Ref { + debug_assert!( + base.starts_with(b"_"), + "name_decorator_temps only reserves the file's `_`-prefixed identifiers" + ); + let ref_ = self.new_symbol(js_ast::symbol::Kind::Other, base); + let scope = self.var_hoisting_scope(); + self.declare_generated_binding(scope, ref_); + if !self.will_use_renamer() { + self.decorator_temp_refs.push(ref_); + } + ref_ + } + + /// Gives every `new_temp` temporary a name no other symbol in the file has: + /// the first request for a base keeps it, later ones get `base2`, `base3`, + /// ... (the renamer's convention). It runs after the visit pass because + /// references to undeclared globals only get a symbol when the visit + /// reaches them, and it reserves the symbols of every scope, not just the + /// one a temporary hoists to, because the temporary is read from code + /// nested in that scope, where a method parameter `_value` would shadow + /// the `_value` WeakMap of a `#value` field. + pub(crate) fn name_decorator_temps(&mut self) { + if self.decorator_temp_refs.is_empty() { + return; + } + + // Cleared first so that the reservation pass below doesn't see the bases. + let temps = core::mem::replace(&mut self.decorator_temp_refs, BumpVec::new_in(self.arena)); + let mut bases = BumpVec::<&'a [u8]>::with_capacity_in(temps.len(), self.arena); + for temp in temps.iter() { + let symbol = &mut self.symbols[temp.inner_index() as usize]; + bases.push(symbol.original_name.slice()); + symbol.original_name = js_ast::StoreStr::EMPTY; + } + + // name -> last suffix handed out for it (1 = the bare name). Every base + // starts with `_` (asserted by `new_temp`), so other names can't collide. + let mut taken: HashMap<&'a [u8], usize> = HashMap::default(); + for symbol in self.symbols.iter() { + let name: &'a [u8] = symbol.original_name.slice(); + if name.starts_with(b"_") { + taken.insert(name, 1); + } + } + + for (temp, base) in temps.iter().zip(bases.iter().copied()) { + let name = match taken.get(&base) { + None => { + taken.insert(base, 1); + base + } + Some(&last) => { + let mut n = last + 1; + let mut candidate = self.bump_name(base, Some(n)); + while taken.contains_key(&candidate) { + n += 1; + candidate = self.bump_name(base, Some(n)); + } + taken.insert(base, n); + taken.insert(candidate, 1); + candidate + } + }; + self.symbols[temp.inner_index() as usize].original_name = js_ast::StoreStr::new(name); + } + } + + /// Base name for the WeakMap backing an `accessor`: `_` when the key + /// is a plain name, since `accessor "x y"` must not declare `var _x y`. + fn accessor_storage_base(&self, key: Option) -> &'a [u8] { + if let Some(key) = key + && let js_ast::ExprData::EString(s) = &key.data + && s.is_utf8() + && js_lexer::is_identifier(&s.data) + { + return self.bump_name2(b"_", &s.data); + } + b"_accessor_storage" + } + /// Single var declaration statement. fn var_decl(&mut self, ref_: Ref, value: Option, l: bun_ast::Loc) -> Stmt { let binding = self.b(B::Identifier { r#ref: ref_ }, l); @@ -776,7 +869,12 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O (obj_expr, self.new_expr(E::This {}, obj_expr.loc)) } _ => { - let tmp_ref = self.generate_temp_ref(Some(b"_obj")); + // Declared by `drain_capture_temp_decls`. + let tmp_ref = self.new_temp(b"_obj"); + self.temp_refs_to_declare.push(TempRef { + r#ref: tmp_ref, + ..Default::default() + }); let write = self.assign_to(tmp_ref, obj_expr, expr_loc); let read = self.use_ref(tmp_ref, expr_loc); (write, read) @@ -1128,7 +1226,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let mut expr_var_decls = BumpVec::::new_in(bump); if is_expr { - let ecr = p.new_sym(js_ast::symbol::Kind::Other, b"_class"); + let ecr = p.new_temp(b"_class"); expr_class_ref = Some(ecr); let binding = p.b(B::Identifier { r#ref: ecr }, loc); expr_var_decls.push(G::Decl { @@ -1145,8 +1243,12 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O if let Some(name) = name_from_context && can_be_class_binding_name(name) { + // The class's own binding (it becomes `.name`), not a + // temporary; `generated` only gets it a minifier slot. + let name_ref = p.new_symbol(js_ast::symbol::Kind::Other, name); + VecExt::append(&mut p.current_scope_mut().generated, name_ref); class.class_name = Some(js_ast::LocRef { - ref_: p.new_sym(js_ast::symbol::Kind::Other, name), + ref_: name_ref, loc, }); } @@ -1163,7 +1265,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O .original_name .slice(); let name = p.bump_name2(b"_", cns); - inner_class_ref = p.new_sym(js_ast::symbol::Kind::Other, name); + inner_class_ref = p.new_temp(name); } // `ExprNodeList = Vec` owns its @@ -1175,7 +1277,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O bun_alloc::AstAlloc::take(&mut class.ts_decorators); let class_decorators_len = class_decorators.len_u32() as usize; - let init_ref = p.new_sym(js_ast::symbol::Kind::Other, b"_init"); + let init_ref = p.new_temp(b"_init"); if is_expr { let binding = p.b(B::Identifier { r#ref: init_ref }, loc); expr_var_decls.push(G::Decl { @@ -1186,7 +1288,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let mut base_ref: Option = None; if class.extends.is_some() { - let br = p.new_sym(js_ast::symbol::Kind::Other, b"_base"); + let br = p.new_temp(b"_base"); base_ref = Some(br); if is_expr { let binding = p.b(B::Identifier { r#ref: br }, loc); @@ -1198,13 +1300,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } // ── Phase 2: Pre-evaluate decorators/keys ──────── - let mut dec_counter: usize = 0; let mut class_dec_ref: Option = None; let mut class_dec_stmt: Stmt = Stmt::empty(); let mut class_dec_assign_expr: Option = None; if class_decorators_len > 0 { - dec_counter += 1; - let cdr = p.new_sym(js_ast::symbol::Kind::Other, b"_dec"); + let cdr = p.new_temp(b"_dec"); class_dec_ref = Some(cdr); // Move ownership into the AST node — `class_decorators` is not read // again on this branch (Phase-5's else-arm only runs when @@ -1231,8 +1331,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let mut prop_dec_refs: HashMap = HashMap::default(); let mut computed_key_refs: HashMap = HashMap::default(); + // Phase 8 adds the bindings these statements declare to `expr_var_decls`. let mut pre_eval_stmts = BumpVec::::new_in(bump); - let mut computed_key_counter: usize = 0; let props_slice: &mut [Property] = class.properties.slice_mut(); for (prop_idx, prop) in props_slice.iter_mut().enumerate() { @@ -1240,21 +1340,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O continue; } if prop.ts_decorators.len_u32() > 0 { - dec_counter += 1; - let dec_name: &'a [u8] = if dec_counter == 1 { - b"_dec" - } else { - p.bump_name(b"_dec", Some(dec_counter)) - }; - let dec_ref = p.new_sym(js_ast::symbol::Kind::Other, dec_name); + let dec_ref = p.new_temp(b"_dec"); prop_dec_refs.insert(prop_idx, dec_ref); - if is_expr { - let binding = p.b(B::Identifier { r#ref: dec_ref }, loc); - expr_var_decls.push(G::Decl { - binding, - value: None, - }); - } // SAFETY: shallow-reborrow arena Vec. let items: ExprNodeList = unsafe { core::ptr::read(&raw const prop.ts_decorators) }; let arr = p.new_expr( @@ -1270,21 +1357,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O && prop.key.is_some() && prop.ts_decorators.len_u32() > 0 { - computed_key_counter += 1; - let key_name: &'a [u8] = if computed_key_counter == 1 { - b"_computedKey" - } else { - p.bump_name(b"_computedKey", Some(computed_key_counter)) - }; - let key_ref = p.new_sym(js_ast::symbol::Kind::Other, key_name); + let key_ref = p.new_temp(b"_computedKey"); computed_key_refs.insert(prop_idx, key_ref); - if is_expr { - let binding = p.b(B::Identifier { r#ref: key_ref }, loc); - expr_var_decls.push(G::Decl { - binding, - value: None, - }); - } let key_loc = prop.key.expect("infallible: prop has key").loc; pre_eval_stmts.push(p.var_decl(key_ref, prop.key, loc)); prop.key = Some(p.use_ref(key_ref, key_loc)); @@ -1389,7 +1463,6 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O BumpVec::>::new_in(bump); let mut prefix_stmts = BumpVec::::new_in(bump); let mut private_lowered_map: PrivateLoweredMap = PrivateLoweredMap::default(); - let mut accessor_storage_counter: usize = 0; let mut emitted_private_adds: HashMap = HashMap::default(); let mut static_private_add_blocks = BumpVec::::new_in(bump); @@ -1456,7 +1529,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O ex.storage_ref } else { let nm = p.bump_name2(b"_", &npriv_orig[1..]); - p.new_sym(js_ast::symbol::Kind::Other, nm) + p.new_temp(nm) }; let fn_nm = { let mut v = BumpVec::::new_in(bump); @@ -1465,7 +1538,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O v.extend_from_slice(Self::fn_suffix(nk)); v.into_bump_slice() }; - let fn_ref = p.new_sym(js_ast::symbol::Kind::Other, fn_nm); + let fn_ref = p.new_temp(fn_nm); let mut new_info = existing.unwrap_or_else(|| PrivateLoweredInfo::new(ws_ref)); @@ -1514,7 +1587,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } else { // Non-decorated private field → WeakMap let wm_nm = p.bump_name2(b"_", &npriv_orig[1..]); - let wm_ref = p.new_sym(js_ast::symbol::Kind::Other, wm_nm); + let wm_ref = p.new_temp(wm_nm); private_lowered_map.insert(npriv_inner, PrivateLoweredInfo::new(wm_ref)); let wme = p.new_weak_map_expr(loc); prefix_stmts.push(p.var_decl(wm_ref, Some(wme), loc)); @@ -1541,20 +1614,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } // Undecorated auto-accessor → WeakMap + getter/setter if prop.kind == PropertyKind::AutoAccessor { - let accessor_name: &'a [u8] = 'brk: { - if let Some(k) = prop.key { - if let js_ast::ExprData::EString(s) = &k.data - && s.is_utf8() - { - break 'brk p.bump_name2(b"_", &s.data); - } - } - let name = - p.bump_name(b"_accessor_storage", Some(accessor_storage_counter)); - accessor_storage_counter += 1; - name - }; - let wm_ref = p.new_sym(js_ast::symbol::Kind::Other, accessor_name); + let accessor_name = p.accessor_storage_base(prop.key); + let wm_ref = p.new_temp(accessor_name); let wme = p.new_weak_map_expr(loc); prefix_stmts.push(p.var_decl(wm_ref, Some(wme), loc)); @@ -1577,7 +1638,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O }; // Setter: set foo(v) { __privateSet(this, _foo, v); } - let setter_param_ref = p.new_sym(js_ast::symbol::Kind::Other, b"v"); + let setter_param_ref = p.new_fixed_name_sym(js_ast::symbol::Kind::Other, b"v"); let this_e2 = p.new_expr(E::This {}, loc); let wm_e2 = p.use_ref(wm_ref, loc); let v_e = p.use_ref(setter_param_ref, loc); @@ -1724,7 +1785,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O ex.storage_ref } else { let nm = p.bump_name2(b"_", &private_orig[1..]); - p.new_sym(js_ast::symbol::Kind::Other, nm) + p.new_temp(nm) }; private_storage_ref = Some(ws_ref); let fn_nm = { @@ -1734,7 +1795,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O v.extend_from_slice(Self::fn_suffix(k)); v.into_bump_slice() }; - let fn_ref = p.new_sym(js_ast::symbol::Kind::Other, fn_nm); + let fn_ref = p.new_temp(fn_nm); private_method_fn_ref = Some(fn_ref); let mut new_info = existing.unwrap_or_else(|| PrivateLoweredInfo::new(ws_ref)); @@ -1756,7 +1817,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O dec_arg_count = 6; } else if k == 5 { let nm = p.bump_name2(b"_", &private_orig[1..]); - let wm_ref = p.new_sym(js_ast::symbol::Kind::Other, nm); + let wm_ref = p.new_temp(nm); private_storage_ref = Some(wm_ref); private_lowered_map.insert(priv_inner, PrivateLoweredInfo::new(wm_ref)); let wme = p.new_weak_map_expr(loc); @@ -1764,7 +1825,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O dec_arg_count = 5; } else if k == 4 { let nm = p.bump_name2(b"_", &private_orig[1..]); - let wm_ref = p.new_sym(js_ast::symbol::Kind::Other, nm); + let wm_ref = p.new_temp(nm); private_storage_ref = Some(wm_ref); let acc_nm = { let mut v = BumpVec::::new_in(bump); @@ -1773,7 +1834,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O v.extend_from_slice(b"_acc"); v.into_bump_slice() }; - let acc_ref = p.new_sym(js_ast::symbol::Kind::Other, acc_nm); + let acc_ref = p.new_temp(acc_nm); private_method_fn_ref = Some(acc_ref); private_lowered_map.insert( priv_inner, @@ -1791,17 +1852,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } } else if k == 4 { // Decorated public auto-accessor → WeakMap - let accessor_name: &'a [u8] = 'brk: { - if let js_ast::ExprData::EString(s) = &key_expr.data - && s.is_utf8() - { - break 'brk p.bump_name2(b"_", &s.data); - } - let name = p.bump_name(b"_accessor_storage", Some(accessor_storage_counter)); - accessor_storage_counter += 1; - name - }; - let wm_ref = p.new_sym(js_ast::symbol::Kind::Other, accessor_name); + let accessor_name = p.accessor_storage_base(Some(key_expr)); + let wm_ref = p.new_temp(accessor_name); private_extra_ref = Some(wm_ref); let wme = p.new_weak_map_expr(loc); prefix_stmts.push(p.var_decl(wm_ref, Some(wme), loc)); @@ -2348,7 +2400,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let mut ctor_stmts = BumpVec::::new_in(bump); if class.extends.is_some() { let target = p.new_expr(E::Super {}, loc); - let args_ref = p.new_sym(js_ast::symbol::Kind::Unbound, arguments_str); + let args_ref = + p.new_fixed_name_sym(js_ast::symbol::Kind::Unbound, arguments_str); let inner = p.new_expr( E::Identifier { ref_: args_ref, diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs index e14684e6a2f2..1cf66e561b30 100644 --- a/src/js_parser/p.rs +++ b/src/js_parser/p.rs @@ -536,6 +536,10 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> { pub(crate) temp_refs_to_declare: List<'a, TempRef>, pub(crate) temp_ref_count: i32, + /// Standard decorator lowering temporaries still to be named by + /// `name_decorator_temps` after the visit pass. + pub(crate) decorator_temp_refs: List<'a, Ref>, + // When bundling, hoisted top-level local variables declared with "var" in // nested scopes are moved up to be declared in the top-level scope instead. // The old "var" statements are turned into regular assignments instead. This @@ -2944,7 +2948,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O self.runtime_imports.put(b"__require", ref_); } - fn will_use_renamer(&self) -> bool { + pub(crate) fn will_use_renamer(&self) -> bool { self.options.bundle || self.options.features.minify_identifiers } @@ -7460,6 +7464,34 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O r#ref } + /// The scope a `var` emitted at the current position is a binding of. + pub(crate) fn var_hoisting_scope(&self) -> js_ast::StoreRef { + let mut scope = self.current_scope_ref(); + while !scope.kind_stops_hoisting() { + scope = scope.parent.expect("the module scope stops hoisting"); + } + scope + } + + /// Registers a generated symbol that a lowering declares in `scope` so the + /// renamers rename it like a user declaration. Nested scopes are renamed + /// from `scope.generated`; the top level of a file only from + /// `Part.declared_symbols`, so a symbol missing from the latter keeps its + /// original name. + pub(crate) fn declare_generated_binding( + &mut self, + mut scope: js_ast::StoreRef, + ref_: Ref, + ) { + VecExt::append(&mut scope.generated, ref_); + self.declared_symbols + .append(DeclaredSymbol { + ref_, + is_top_level: scope == self.module_scope, + }) + .expect("oom"); + } + pub(crate) fn should_lower_using_declarations(&self, stmts: &[Stmt]) -> bool { // TODO: We do not support lowering await, but when we do this needs to point to that var let lower_await = false; @@ -8830,6 +8862,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O await_target: None, temp_refs_to_declare: BumpVec::new_in(arena), temp_ref_count: 0, + decorator_temp_refs: BumpVec::new_in(arena), relocated_top_level_vars: BumpVec::new_in(arena), after_arrow_body_loc: bun_ast::Loc::EMPTY, const_values: Default::default(), @@ -9045,41 +9078,11 @@ impl LowerUsingDeclarationsContext { let err_ref = p.generate_temp_ref(Some(b"_err")); let has_err_ref = p.generate_temp_ref(Some(b"_hasErr")); - // `StoreRef` (Copy + safe `Deref`/`DerefMut`) lets the - // parent-chain walk and the `.generated` writes below run without - // raw-pointer `unsafe`, and does not borrow `p`. - let mut scope: js_ast::StoreRef = p.current_scope_ref(); - while !scope.kind_stops_hoisting() { - scope = scope.parent.unwrap(); + let scope = p.var_hoisting_scope(); + for ref_ in [self.stack_ref, caught_ref, err_ref, has_err_ref] { + p.declare_generated_binding(scope, ref_); } - let is_top_level = scope == p.module_scope; - scope - .generated - .append_slice(&[self.stack_ref, caught_ref, err_ref, has_err_ref]); - p.declared_symbols - .ensure_unused_capacity( - // 5 to include the _promise decl later on: - if self.has_await_using { 5 } else { 4 }, - ) - .expect("oom"); - p.declared_symbols.append_assume_capacity(DeclaredSymbol { - is_top_level, - ref_: self.stack_ref, - }); - p.declared_symbols.append_assume_capacity(DeclaredSymbol { - is_top_level, - ref_: caught_ref, - }); - p.declared_symbols.append_assume_capacity(DeclaredSymbol { - is_top_level, - ref_: err_ref, - }); - p.declared_symbols.append_assume_capacity(DeclaredSymbol { - is_top_level, - ref_: has_err_ref, - }); - let loc = self.first_using_loc; let call_dispose = { p.record_usage(self.stack_ref); @@ -9113,11 +9116,7 @@ impl LowerUsingDeclarationsContext { let finally_stmts: &'a mut [Stmt] = if self.has_await_using { let promise_ref = p.generate_temp_ref(Some(b"_promise")); - VecExt::append(&mut scope.generated, promise_ref); - p.declared_symbols.append_assume_capacity(DeclaredSymbol { - is_top_level, - ref_: promise_ref, - }); + p.declare_generated_binding(scope, promise_ref); let promise_ref_expr = p.new_expr( E::Identifier { diff --git a/src/js_parser/parse/parse_entry.rs b/src/js_parser/parse/parse_entry.rs index 22ae907b68d7..e4e83b1e8178 100644 --- a/src/js_parser/parse/parse_entry.rs +++ b/src/js_parser/parse/parse_entry.rs @@ -1001,6 +1001,8 @@ impl<'a> Parser<'a> { return Err(crate::Error::SyntaxError); } + p.name_decorator_temps(); + // `perf::Ctx` ends the span in its `Drop` impl — bind it for the rest of `_parse`. let _postvisit_tracer = bun_core::perf::trace("JSParser::postvisit"); diff --git a/test/bundler/bundler_edgecase.test.ts b/test/bundler/bundler_edgecase.test.ts index 7fe388d3a623..6df2d864865c 100644 --- a/test/bundler/bundler_edgecase.test.ts +++ b/test/bundler/bundler_edgecase.test.ts @@ -3170,6 +3170,176 @@ describe("bundler", () => { }, run: { stdout: "try:false" }, }); + + // Standard decorator / `accessor` lowering declares temporaries (_init, _dec, + // _, one _ WeakMap per lowered member, ...) at the top level of + // the module. They have to take part in renaming like any other top-level + // declaration, or they collide with the user's names and with each other. + itBundled("edgecase/DecoratorLoweringTempsVsUserNames", { + files: { + "/entry.js": /* js */ ` + const _init = "init"; + const _dec = "dec"; + let _x = "x"; + var _A = "A"; + function dec(value, ctx) {} + class A { + @dec m() {} + accessor x = 1; + } + console.log(_init, _dec, _x, _A, new A().x); + `, + }, + run: { stdout: "init dec x A 1" }, + }); + itBundled("edgecase/DecoratorLoweringTempsWithinOneClass", { + files: { + "/entry.js": /* js */ ` + function dec(value, ctx) { return value; } + class A { + @dec accessor x = 1; + #x = 2; + get hidden() { return this.#x; } + } + const a = new A(); + console.log(a.x, a.hidden); + `, + }, + run: { stdout: "1 2" }, + }); + // Constructors read `_init` at construction time, so a class must not pick up + // the initializers of a class evaluated after it. + // https://github.com/oven-sh/bun/issues/28316 + itBundled("edgecase/DecoratorLoweringTempsBetweenClassesInOneFile", { + files: { + "/entry.ts": /* ts */ ` + const inject = (target: string) => (value: undefined, context: ClassFieldDecoratorContext) => { + console.log("init", target, String(context.name)); + return function (initValue: unknown) { + console.log("get", target, String(context.name), initValue); + return initValue; + }; + }; + class Test1 { + @inject("test1") field1: string = "test1"; + } + class Test2 { + @inject("test2") field2: string = "test2"; + } + console.log(new Test1().field1); + `, + }, + run: { stdout: "init test1 field1\ninit test2 field2\nget test1 field1 test1\ntest1" }, + }); + // The same applies to a class from one file and a class from a later file. + itBundled("edgecase/DecoratorLoweringTempsAcrossFiles", { + files: { + "/entry.js": /* js */ ` + import { A } from "./a.js"; + import { B } from "./b.js"; + console.log(new A().a, new B().b); + `, + "/a.js": /* js */ ` + function double(value, ctx) { return x => x * 2; } + export const A = class { @double a = 1; }; + `, + "/b.js": /* js */ ` + function triple(value, ctx) { return x => x * 3; } + export const B = class { @triple b = 1; }; + `, + }, + run: { stdout: "2 3" }, + }); + // https://github.com/oven-sh/bun/issues/30568 + itBundled("edgecase/DecoratorLoweringAccessorStorageAcrossFiles", { + files: { + "/entry.js": /* js */ ` + import { ComponentA } from "./a.js"; + import { ComponentB } from "./b.js"; + console.log(new ComponentA().myData, new ComponentB().myData); + `, + "/a.js": /* js */ ` + import { state } from "./decorator.js"; + export class ComponentA { + @state() accessor myData = "A"; + } + `, + "/b.js": /* js */ ` + import { state } from "./decorator.js"; + export class ComponentB { + @state() accessor myData = "B"; + } + `, + "/decorator.js": /* js */ ` + export function state() { + return function (target, context) { + return { + get() { return target.get.call(this); }, + set(newValue) { target.set.call(this, newValue); }, + }; + }; + } + `, + }, + run: { stdout: "A B" }, + }); + // The temporary that captures the receiver of a private method call is + // declared inside the method (or next to the class, for relocated static + // code), and is read where the user's binding of the same name is visible. + itBundled("edgecase/DecoratorLoweringReceiverTempVsUserName", { + files: { + "/entry.js": /* js */ ` + const _obj = "outer"; + function dec(value, ctx) { return value; } + class A { + @dec m() {} + #secret() { return "secret"; } + static #staticSecret() { return "static secret"; } + self() { return this; } + static self() { return A; } + run() { return [this.self().#secret(), _obj]; } + static { console.log(A.self().#staticSecret(), _obj); } + } + console.log(...new A().run()); + `, + }, + run: { stdout: "static secret outer\nsecret outer" }, + }); + // A class expression inside a block still declares its temporaries with + // `var`, so the two blocks' temporaries are bindings of the same scope. + const decoratorTempsInSiblingBlocks = { + "/entry.js": /* js */ ` + function dec(value, ctx) { return value; } + let A, B; + { A = class { @dec m() {} accessor x = "a"; }; } + const a = new A(); + { B = class { @dec m() {} accessor x = "b"; }; } + console.log(a.x, new B().x); + `, + }; + itBundled("edgecase/DecoratorLoweringTempsInSiblingBlocks", { + files: decoratorTempsInSiblingBlocks, + run: { stdout: "a b" }, + }); + itBundled("edgecase/DecoratorLoweringTempsInSiblingBlocksMinified", { + files: decoratorTempsInSiblingBlocks, + minifyIdentifiers: true, + run: { stdout: "a b" }, + }); + itBundled("edgecase/DecoratorLoweringAccessorKeyNotAnIdentifier", { + files: { + "/entry.js": /* js */ ` + function dec(value, ctx) { return value; } + class A { + accessor "x y" = 1; + @dec accessor "x-y" = 2; + } + const a = new A(); + console.log(a["x y"], a["x-y"]); + `, + }, + run: { stdout: "1 2" }, + }); }); for (const backend of ["api", "cli"] as const) { diff --git a/test/bundler/transpiler/es-decorators.test.ts b/test/bundler/transpiler/es-decorators.test.ts index bdc61e68c48d..82a7fe5fd15e 100644 --- a/test/bundler/transpiler/es-decorators.test.ts +++ b/test/bundler/transpiler/es-decorators.test.ts @@ -1210,4 +1210,310 @@ describe("ES Decorators", () => { expect(exitCode).toBe(0); }); }); + + // The lowering declares temporaries (_init, _dec, _, one _ WeakMap + // per lowered member, ...) next to the class. The runtime transpiler prints + // symbols under their original names, so the names themselves must be unique: + // distinct from the identifiers the file uses and from each other. + describe.concurrent("lowering temporaries do not collide", () => { + test("with identifiers declared by the file (class statement)", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + const _init = "init"; + const _dec = "dec"; + let _x = "x"; + var _A = "A"; + function dec(value, ctx) {} + class A { + @dec m() {} + accessor x = 1; + } + console.log(_init, _dec, _x, _A, new A().x); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("init dec x A 1\n"); + expect(exitCode).toBe(0); + }); + + test("with identifiers declared by the file (class expression)", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + const _class = "class"; + const _init = "init"; + const _dec = "dec"; + const _base = "base"; + function dec(value, ctx) {} + class Base {} + const Foo = @dec class extends Base { + @dec m() {} + }; + console.log(_class, _init, _dec, _base, Foo.name, new Foo() instanceof Base); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("class init dec base Foo true\n"); + expect(exitCode).toBe(0); + }); + + test("with globals the file only references after the class", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + globalThis._init = "global init"; + globalThis._A = "global A"; + function dec(value, ctx) {} + class A { + @dec m() {} + } + console.log(_init, _A); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("global init global A\n"); + expect(exitCode).toBe(0); + }); + + test("with a parameter of a method that reads the lowered member", async () => { + // The `_value` WeakMap is read inside `set`, where a parameter of the + // same name would shadow it, so bindings of nested scopes count too. + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(value, ctx) { return value; } + class Store { + #value = 0; + @dec set(_value) { this.#value = _value; return this; } + get() { return this.#value; } + } + console.log(new Store().set(5).get()); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("5\n"); + expect(exitCode).toBe(0); + }); + + test("with a variable named like the temporary that captures a private call receiver", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + const _obj = "outer"; + function dec(value, ctx) { return value; } + class A { + @dec m() {} + #secret() { return "secret"; } + static #staticSecret() { return "static secret"; } + self() { return this; } + static self() { return A; } + run() { return [this.self().#secret(), _obj]; } + static { console.log(A.self().#staticSecret(), _obj); } + } + console.log(...new A().run()); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("static secret outer\nsecret outer\n"); + expect(exitCode).toBe(0); + }); + + test("between members of one class", async () => { + // `accessor x` and `#x` both want a WeakMap named `_x`. + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(value, ctx) { return value; } + class A { + @dec accessor x = 1; + #x = 2; + get hidden() { return this.#x; } + } + const a = new A(); + console.log(a.x, a.hidden); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("1 2\n"); + expect(exitCode).toBe(0); + }); + + test("between classes in one scope", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(value, ctx) { return value; } + function main() { + class A { + @dec accessor x = "a"; + #tag = "A"; + get tag() { return this.#tag; } + } + const a = new A(); + class B { + @dec accessor x = "b"; + #tag = "B"; + get tag() { return this.#tag; } + } + const b = new B(); + console.log(a.x, a.tag, b.x, b.tag); + } + main(); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("a A b B\n"); + expect(exitCode).toBe(0); + }); + + test("between classes whose constructors read a computed key", async () => { + // `this[_computedKey] = ...` runs at construction time, after the sibling + // class has been evaluated. + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(value, ctx) { return value; } + function main() { + const keyA = "a", keyB = "b"; + class A { @dec [keyA] = 1; } + class B { @dec [keyB] = 2; } + console.log(JSON.stringify(new A()), JSON.stringify(new B())); + } + main(); + `); + expect(stderr).toBe(""); + expect(stdout).toBe('{"a":1} {"b":2}\n'); + expect(exitCode).toBe(0); + }); + + test("between a base class and a subclass", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(value, ctx) { return value; } + class Base { + @dec get kind() { return "base"; } + #secret = "base secret"; + get baseSecret() { return this.#secret; } + } + class Derived extends Base { + @dec get kind() { return "derived"; } + #secret = "derived secret"; + get derivedSecret() { return this.#secret; } + } + const d = new Derived(); + console.log(d.kind, d.baseSecret, d.derivedSecret); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("derived base secret derived secret\n"); + expect(exitCode).toBe(0); + }); + + test("between the field initializers of a base class and a subclass", async () => { + // https://github.com/oven-sh/bun/issues/28010: the constructor reads its + // class's `_init` at construction time, after the subclass was evaluated. + const { stdout, stderr, exitCode } = await runDecorator(` + function tag(name) { + return function (value, context) { + return function (initialValue) { + console.log(name, String(context.name), initialValue); + return initialValue; + }; + }; + } + class Parent { + @tag("Parent.foo") foo = "parent_foo"; + @tag("Parent.shared") shared = "parent_shared"; + } + class Child extends Parent { + @tag("Child.foo") foo = "child_foo"; + @tag("Child.childOnly") childOnly = "child_childOnly"; + } + new Child(); + `); + expect(stderr).toBe(""); + expect(stdout).toBe( + "Parent.foo foo parent_foo\n" + + "Parent.shared shared parent_shared\n" + + "Child.foo foo child_foo\n" + + "Child.childOnly childOnly child_childOnly\n", + ); + expect(exitCode).toBe(0); + }); + + test("between the accessor storage of a base class and a subclass", async () => { + // https://github.com/oven-sh/bun/issues/29837 + const { stdout, stderr, exitCode } = await runDecorator(` + class A { + accessor name = "A"; + } + class B extends A { + accessor name = "B"; + logName() { + console.log(this.name); + console.log(super.name); + } + } + new B().logName(); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("B\nA\n"); + expect(exitCode).toBe(0); + }); + + test("between the decorated private methods of two classes", async () => { + // `this.#m()` reads the class's `_m` WeakSet and `_m_fn` (the decorated + // method) at call time. + const { stdout, stderr, exitCode } = await runDecorator(` + function first(value, ctx) { return function () { return "first"; }; } + function second(value, ctx) { return function () { return "second"; }; } + class A { @first #m() { return "a"; } call() { return this.#m(); } } + class B { @second #m() { return "b"; } call() { return this.#m(); } } + console.log(new A().call(), new B().call()); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("first second\n"); + expect(exitCode).toBe(0); + }); + + test("between a class named like a temporary and another class's temporary", async () => { + // The statement form keeps the class in a `_` binding, so + // `class init` asks for `_init`, the base name of every class's + // initializer array. + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(value, ctx) { return value; } + function answer(value, ctx) { return () => 42; } + @dec class init { @dec m() { return init; } } + const C = class { @answer x = 1; }; + console.log(new init().m() === init, new C().x); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("true 42\n"); + expect(exitCode).toBe(0); + }); + + test("between class expressions in sibling blocks", async () => { + // The `var` declarations for both expressions hoist to the same scope. + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(value, ctx) { return value; } + let A, B; + { A = class { @dec m() {} accessor x = "a"; }; } + const a = new A(); + { B = class { @dec m() {} accessor x = "b"; }; } + console.log(a.x, new B().x); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("a b\n"); + expect(exitCode).toBe(0); + }); + + test("between a class expression and one nested in its static initializer", async () => { + // https://github.com/oven-sh/bun/issues/31929: both expressions are + // lowered into the same scope, and the inner one is evaluated while the + // outer one is still using its temporaries. + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(value, ctx) { return value; } + const C = class Outer { + @dec static s = (class { @dec static x = 42; }).x; + }; + console.log(C.name, C.s); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("Outer 42\n"); + expect(exitCode).toBe(0); + }); + + test("accessor keys that are not identifiers", async () => { + const { stdout, stderr, exitCode } = await runDecorator(` + function dec(value, ctx) { return value; } + class A { + accessor "x y" = 1; + @dec accessor "x-y" = 2; + static accessor "x y" = 3; + } + const a = new A(); + a["x y"] += 10; + console.log(a["x y"], a["x-y"], A["x y"]); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("11 2 3\n"); + expect(exitCode).toBe(0); + }); + }); });