Skip to content
6 changes: 5 additions & 1 deletion src/ast/g.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,11 @@ impl Class {
return false;
}

if property.kind == PropertyKind::Normal && f.contains(flags::Property::IsStatic) {
if matches!(
property.kind,
PropertyKind::Normal | PropertyKind::AutoAccessor
) && f.contains(flags::Property::IsStatic)
{
for val in [property.value, property.initializer].into_iter().flatten() {
match val.data {
ExprData::EArrow(..) | ExprData::EFunction(..) => {}
Expand Down
3 changes: 3 additions & 0 deletions src/ast/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2945,6 +2945,9 @@ pub mod flags {
IsStatic,
WasShorthand,
IsSpread,
/// Getter/setter pair generated for an `accessor` member; `ts_metadata`
/// on the one carrying the decorators is the member's declared type.
Comment thread
robobun marked this conversation as resolved.
IsLoweredAutoAccessor,
}
pub type PropertySet = EnumSet<Property>;
pub const PROPERTY_NONE: PropertySet = EnumSet::empty();
Expand Down
266 changes: 266 additions & 0 deletions src/js_parser/lower/lower_auto_accessors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
//! Lowers `accessor` members of classes that skip the standard-decorator
//! lowering (TypeScript with `experimentalDecorators`), in place:
//!
//! ```js
//! accessor x = 1; // -> #x = 1; get x() { return this.#x; } set x(v) { this.#x = v; }
//! ```
Comment thread
robobun marked this conversation as resolved.

use bun_collections::VecExt;

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

type BumpVec<'a, T> = bun_alloc::ArenaVec<'a, T>;

impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_ONLY> {
/// Call with the visited class body scope still current: the backing
/// fields are declared in it.
Comment thread
robobun marked this conversation as resolved.
pub(crate) fn lower_auto_accessors_in_place(&mut self, class: &mut G::Class) {
let accessor_count = class
.properties
.iter()
.filter(|prop| prop.kind == PropertyKind::AutoAccessor)
.count();
if accessor_count == 0 {
return;
}

let bump = self.arena;
let mut properties = BumpVec::<Property>::with_capacity_in(
class.properties.len() + 2 * accessor_count,
bump,
);
let mut generated_private_names =
BumpVec::<&'a [u8]>::with_capacity_in(accessor_count, bump);
let mut computed_key_decls = BumpVec::<G::Decl>::new_in(bump);

for slot in class.properties.slice_mut() {
let prop = core::mem::take(slot);
if prop.kind == PropertyKind::AutoAccessor {
self.lower_auto_accessor(
prop,
&mut properties,
&mut generated_private_names,
&mut computed_key_decls,
);
} else {
properties.push(prop);
}
}
class.properties = bun_ast::StoreSlice::from_bump(properties);

if !computed_key_decls.is_empty() {
let decl = self.s(
S::Local {
decls: DeclList::from_bump_vec(computed_key_decls),
..Default::default()
},
class.body_loc,
);
self.nearest_stmt_list_mut()
.expect("classes are only visited from within a statement list")
.push(decl);
}
}

fn lower_auto_accessor(
&mut self,
mut accessor: Property,
out: &mut BumpVec<'a, Property>,
generated_private_names: &mut BumpVec<'a, &'a [u8]>,
computed_key_decls: &mut BumpVec<'a, G::Decl>,
) {
let key = accessor
.key
.expect("infallible: auto-accessors always have a key");
let loc = key.loc;
let is_computed = accessor.flags.contains(Flags::Property::IsComputed);
let is_static = accessor.flags.contains(Flags::Property::IsStatic);

let preferred_name: &'a [u8] = match &key.data {
js_ast::ExprData::EString(name)
if !is_computed
&& name.is_utf8()
&& js_lexer::is_identifier(&name.data)
&& !name.eql_comptime(b"constructor") =>
{
self.bump_name2(b"#", &name.data)
}
// `accessor #p` keeps `#p` for its getter/setter pair.
js_ast::ExprData::EPrivateIdentifier(private) => {
let name = self.load_name_from_ref(private.ref_);
self.bump_name2(b"#_", &name[1..])
}
_ => b"#_accessor_storage",
};
let backing_name = self.unused_private_name(preferred_name, generated_private_names);
generated_private_names.push(backing_name);

let backing_kind = if is_static {
js_ast::symbol::Kind::PrivateStaticField
} else {
js_ast::symbol::Kind::PrivateField
};
let backing_ref = self.new_sym(backing_kind, backing_name);
self.record_declared_symbol(backing_ref);

// #x = <initializer>;
let mut backing_flags = accessor.flags;
backing_flags.remove(Flags::Property::IsComputed);
let backing_key = self.new_expr(E::PrivateIdentifier { ref_: backing_ref }, loc);
out.push(Property {
kind: PropertyKind::Normal,
flags: backing_flags,
key: Some(backing_key),
initializer: accessor.initializer.take(),
..Default::default()
});

// `get [_computedKey = expr]() {} set [_computedKey]() {}`
let needs_key_temp = is_computed
&& !matches!(
key.data,
js_ast::ExprData::EString(_) | js_ast::ExprData::ENumber(_)
);
let (getter_key, setter_key) = if needs_key_temp {
let temp_ref = self.declare_var_temp_ref(b"_computedKey");
let binding = self.b(B::Identifier { r#ref: temp_ref }, loc);
computed_key_decls.push(G::Decl {
binding,
value: None,
});
let temp_for_getter = self.use_ref(temp_ref, loc);
let temp_for_setter = self.use_ref(temp_ref, loc);
(Expr::assign(temp_for_getter, key), temp_for_setter)
} else {
(key, key)
};

let mut pair_flags = accessor.flags;
pair_flags.insert(Flags::Property::IsMethod);
pair_flags.insert(Flags::Property::IsLoweredAutoAccessor);

// get x() { return this.#x; }
let getter_value = self.backing_field_access(backing_ref, loc);
let getter_body = self.s(
S::Return {
value: Some(getter_value),
},
loc,
);
let getter = G::Fn {
body: G::FnBody {
stmts: bun_ast::StoreSlice::new_mut(self.arena.alloc_slice_copy(&[getter_body])),
loc,
},
..Default::default()
};
let getter_fn = self.new_expr(E::Function { func: getter }, loc);
out.push(Property {
kind: PropertyKind::Get,
flags: pair_flags,
key: Some(getter_key),
value: Some(getter_fn),
..Default::default()
});
Comment thread
claude[bot] marked this conversation as resolved.

// set x(v) { this.#x = v; }
// The decorators go on the setter because `lower_class` reuses the decorated
// member's key, and the setter's is the one without the assignment.
Comment thread
robobun marked this conversation as resolved.
let value_ref = self.new_sym(js_ast::symbol::Kind::Other, b"v");
let value_binding = self.b(B::Identifier { r#ref: value_ref }, loc);
let setter_arg = self.arena.alloc(G::Arg {
binding: value_binding,
..Default::default()
});
let assignment_target = self.backing_field_access(backing_ref, loc);
let assigned_value = self.use_ref(value_ref, loc);
let setter_body = self.s(
S::SExpr {
value: Expr::assign(assignment_target, assigned_value),
..Default::default()
},
loc,
);
let setter = G::Fn {
args: bun_ast::StoreSlice::new_mut(core::slice::from_mut(setter_arg)),
body: G::FnBody {
stmts: bun_ast::StoreSlice::new_mut(self.arena.alloc_slice_copy(&[setter_body])),
loc,
},
..Default::default()
};
let setter_fn = self.new_expr(E::Function { func: setter }, loc);
out.push(Property {
kind: PropertyKind::Set,
flags: pair_flags,
key: Some(setter_key),
value: Some(setter_fn),
ts_decorators: bun_alloc::AstAlloc::take(&mut accessor.ts_decorators),
ts_metadata: core::mem::take(&mut accessor.ts_metadata),
..Default::default()
});
}

/// Private names are printed as written (only the minifier renames them), so
/// the name must not be visible from this class body already.
Comment thread
robobun marked this conversation as resolved.
fn unused_private_name(&self, preferred: &'a [u8], generated: &[&'a [u8]]) -> &'a [u8] {
let mut candidate = preferred;
let mut suffix: usize = 2;
while generated.contains(&candidate) || self.is_declared_in_enclosing_scopes(candidate) {
candidate = self.bump_name(preferred, Some(suffix));
suffix += 1;
}
candidate
}

/// Symbol for a `var` emitted next to the class: it lives in the scope the
/// `var` hoists to, and the bundler's renamer only sees module-level
/// symbols through `declared_symbols`.
Comment thread
robobun marked this conversation as resolved.
fn declare_var_temp_ref(&mut self, name: &'a [u8]) -> Ref {
let mut scope = self.current_scope;
while !scope.kind_stops_hoisting() {
scope = scope
.parent
.expect("infallible: the module scope stops hoisting");
}
let ref_ = self.generate_temp_ref_with_scope(Some(name), scope);
self.declared_symbols
.append(DeclaredSymbol {
ref_,
is_top_level: scope == self.module_scope,
})
.expect("oom");
ref_
}

fn is_declared_in_enclosing_scopes(&self, name: &[u8]) -> bool {
let hash = js_ast::Scope::get_member_hash(name);
let mut scope = Some(self.current_scope);
while let Some(current) = scope {
if current.get_member_with_hash(name, hash).is_some() {
return true;
}
scope = current.parent;
}
false
}

/// `this.#x`
fn backing_field_access(&mut self, backing_ref: Ref, loc: bun_ast::Loc) -> Expr {
self.record_usage(backing_ref);
let target = self.new_expr(E::This {}, loc);
let index = self.new_expr(E::PrivateIdentifier { ref_: backing_ref }, loc);
self.new_expr(
E::Index {
target,
index,
optional_chain: None,
},
loc,
)
}
}
10 changes: 5 additions & 5 deletions src/js_parser/lower/lower_decorators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O

/// recordUsage + E.Identifier in one call.
#[inline]
fn use_ref(&mut self, ref_: Ref, l: bun_ast::Loc) -> Expr {
pub(crate) fn use_ref(&mut self, ref_: Ref, l: bun_ast::Loc) -> Expr {
self.record_usage(ref_);
self.new_expr(
E::Identifier {
Expand All @@ -166,7 +166,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}

/// newSymbol + scope.generated.append in one call.
fn new_sym(&mut self, kind: js_ast::symbol::Kind, name: &'a [u8]) -> Ref {
pub(crate) fn new_sym(&mut self, kind: js_ast::symbol::Kind, name: &'a [u8]) -> Ref {
let ref_ = self.new_symbol(kind, name);
VecExt::append(&mut self.current_scope_mut().generated, ref_);
ref_
Expand Down Expand Up @@ -394,8 +394,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
}

/// Bump-format `_{prefix}{n}` (or just `_{prefix}` when n is omitted).
fn bump_name(&self, prefix: &[u8], n: Option<usize>) -> &'a [u8] {
/// Bump-format `{prefix}{n}` (or just `{prefix}` when n is omitted).
pub(crate) fn bump_name(&self, prefix: &[u8], n: Option<usize>) -> &'a [u8] {
let mut v = BumpVec::<u8>::new_in(self.arena);
v.extend_from_slice(prefix);
if let Some(n) = n {
Expand All @@ -407,7 +407,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
v.into_bump_slice()
}

fn bump_name2(&self, a: &[u8], b: &[u8]) -> &'a [u8] {
pub(crate) fn bump_name2(&self, a: &[u8], b: &[u8]) -> &'a [u8] {
let mut v = BumpVec::<u8>::new_in(self.arena);
v.extend_from_slice(a);
v.extend_from_slice(b);
Expand Down
1 change: 1 addition & 0 deletions src/js_parser/lower/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod lower_auto_accessors;
pub mod lower_decorators;
pub mod lower_esm_exports_hmr;
14 changes: 11 additions & 3 deletions src/js_parser/p.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6435,8 +6435,6 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let descriptor_key = prop.key.expect("infallible: prop has key");
let loc = descriptor_key.loc;

// TODO: when we have the `accessor` modifier, add `and !prop.flags.contains(.has_accessor_modifier)` to
// the if statement.
let descriptor_kind: Expr =
if !prop.flags.contains(Flags::Property::IsMethod) {
self.new_expr(E::Undefined {}, loc)
Expand Down Expand Up @@ -6844,6 +6842,15 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
}
}
PropertyKind::Get | PropertyKind::Set
if prop.flags.contains(Flags::Property::IsLoweredAutoAccessor) =>
{
// typescript sets only design:type (the declared type) for an `accessor` member.
let v = self
.serialize_metadata(prop.ts_metadata.clone())
.expect("unreachable");
push_metadata!(b"design:type", v);
}
PropertyKind::Get => {
if prop.flags.contains(Flags::Property::IsMethod) {
// typescript sets design:type to the return value & design:paramtypes to [].
Expand Down Expand Up @@ -6911,7 +6918,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
}
}
PropertyKind::Spread | PropertyKind::Declare | PropertyKind::AutoAccessor => {} // not allowed in a class (auto_accessor is standard decorators only)
PropertyKind::Spread | PropertyKind::Declare => {}
PropertyKind::AutoAccessor => {} // lowered to a getter/setter pair before this runs
PropertyKind::ClassStaticBlock => {} // not allowed to decorate this
}
}
Expand Down
10 changes: 7 additions & 3 deletions src/js_parser/parse/parse_property.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
if let Some(prop) =
p.parse_property(kind, opts, None)?
{
if prop.kind == PropertyKind::Normal
// Legacy mode decorates an abstract accessor
// like an abstract field (tsc parity).
Comment thread
robobun marked this conversation as resolved.
if (prop.kind == PropertyKind::Normal
|| (prop.kind == PropertyKind::AutoAccessor
&& !p.options.features.standard_decorators))
&& prop.value.is_none()
&& opts.ts_decorators.len() > 0
{
Expand All @@ -463,10 +467,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
}
PropertyModifierKeyword::PAccessor => {
// "accessor" keyword for auto-accessor fields (TC39 standard decorators)
// "accessor" keyword for auto-accessor fields (valid in
// both decorator modes, like in TypeScript)
Comment thread
robobun marked this conversation as resolved.
if opts.is_class
&& !p.lexer.has_newline_before
&& p.options.features.standard_decorators
&& PropertyModifierKeyword::find(raw)
== Some(PropertyModifierKeyword::PAccessor)
{
Expand Down
Loading
Loading