Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(transformer): class properties transform
Browse files Browse the repository at this point in the history
overlookmotel committed Nov 24, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 25f2a20 commit 1fdf61f
Showing 16 changed files with 5,044 additions and 124 deletions.
10 changes: 10 additions & 0 deletions crates/oxc_transformer/src/common/helper_loader.rs
Original file line number Diff line number Diff line change
@@ -147,6 +147,11 @@ pub enum Helper {
ObjectDestructuringEmpty,
ObjectWithoutProperties,
ToPropertyKey,
DefineProperty,
ClassPrivateFieldInitSpec,
ClassPrivateFieldGet2,
ClassPrivateFieldSet2,
AssertClassBrand,
}

impl Helper {
@@ -162,6 +167,11 @@ impl Helper {
Self::ObjectDestructuringEmpty => "objectDestructuringEmpty",
Self::ObjectWithoutProperties => "objectWithoutProperties",
Self::ToPropertyKey => "toPropertyKey",
Self::DefineProperty => "defineProperty",
Self::ClassPrivateFieldInitSpec => "classPrivateFieldInitSpec",
Self::ClassPrivateFieldGet2 => "classPrivateFieldGet2",
Self::ClassPrivateFieldSet2 => "classPrivateFieldSet2",
Self::AssertClassBrand => "assertClassBrand",
}
}
}
1 change: 0 additions & 1 deletion crates/oxc_transformer/src/common/var_declarations.rs
Original file line number Diff line number Diff line change
@@ -98,7 +98,6 @@ impl<'a> VarDeclarationsStore<'a> {

/// Add a `let` declaration to be inserted at top of current enclosing statement block,
/// given a `BoundIdentifier`.
#[expect(dead_code)]
pub fn insert_let(
&self,
binding: &BoundIdentifier<'a>,
1 change: 0 additions & 1 deletion crates/oxc_transformer/src/compiler_assumptions.rs
Original file line number Diff line number Diff line change
@@ -81,7 +81,6 @@ pub struct CompilerAssumptions {
pub set_computed_properties: bool,

#[serde(default)]
#[deprecated = "Not Implemented"]
pub set_public_class_fields: bool,

#[serde(default)]
94 changes: 0 additions & 94 deletions crates/oxc_transformer/src/es2022/class_properties.rs

This file was deleted.

765 changes: 765 additions & 0 deletions crates/oxc_transformer/src/es2022/class_properties/class.rs

Large diffs are not rendered by default.

656 changes: 656 additions & 0 deletions crates/oxc_transformer/src/es2022/class_properties/constructor.rs

Large diffs are not rendered by default.

296 changes: 296 additions & 0 deletions crates/oxc_transformer/src/es2022/class_properties/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
//! ES2022: Class Properties
//!
//! This plugin transforms class properties to initializers inside class constructor.
//!
//! > This plugin is included in `preset-env`, in ES2022
//!
//! ## Example
//!
//! Input:
//! ```js
//! class C {
//! foo = 123;
//! #bar = 456;
//! method() {
//! let bar = this.#bar;
//! this.#bar = bar + 1;
//! }
//! }
//!
//! let x = 123;
//! class D extends S {
//! foo = x;
//! constructor(x) {
//! if (x) {
//! let s = super(x);
//! } else {
//! super(x);
//! }
//! }
//! }
//! ```
//!
//! Output:
//! ```js
//! var _bar = /*#__PURE__*/ new WeakMap();
//! class C {
//! constructor() {
//! babelHelpers.defineProperty(this, "foo", 123);
//! babelHelpers.classPrivateFieldInitSpec(this, _bar, 456);
//! }
//! method() {
//! let bar = babelHelpers.classPrivateFieldGet2(_bar, this);
//! babelHelpers.classPrivateFieldSet2(_bar, this, bar + 1);
//! }
//! }
//!
//! let x = 123;
//! class D extends S {
//! constructor(_x) {
//! if (_x) {
//! let s = (super(_x), babelHelpers.defineProperty(this, "foo", x));
//! } else {
//! super(_x);
//! babelHelpers.defineProperty(this, "foo", x);
//! }
//! }
//! }
//! ```
//!
//! ## Implementation
//!
//! WORK IN PROGRESS. INCOMPLETE.
//!
//! ### Reference implementation
//!
//! Implementation based on [@babel/plugin-transform-class-properties](https://babel.dev/docs/babel-plugin-transform-class-properties).
//!
//! I (@overlookmotel) wrote this transform without reference to Babel's internal implementation,
//! but aiming to reproduce Babel's output, guided by Babel's test suite.
//!
//! ### Divergence from Babel
//!
//! In a few places, our implementation diverges from Babel, notably inserting property initializers
//! into constructor of a class with multiple `super()` calls (see comments in [`constructor`] module).
//!
//! ### High level overview
//!
//! Transform happens in 3 phases:
//!
//! 1. Check if class contains properties or static blocks, to determine if any transform is necessary
//! (in [`ClassProperties::transform_class`]).
//! 2. Extract class property declarations and static blocks from class and insert in class constructor
//! (instance properties) or before/after the class (static properties + static blocks)
//! (in [`ClassProperties::transform_class`]).
//! 3. Transform private property usages (`this.#prop`)
//! (in [`ClassProperties::transform_private_field_expression`] and other visitors).
//!
//! Implementation is split into several files:
//!
//! * `mod.rs`: Setup, visitor and ancillary types.
//! * `class.rs`: Transform of class body.
//! * `constructor.rs`: Insertion of property initializers into class constructor.
//! * `private.rs`: Transform of private property usages (`this.#prop`).
//! * `utils.rs`: Utility functions.
//!
//! ## References
//!
//! * Babel plugin implementation:
//! * <https://github.com/babel/babel/tree/v7.26.2/packages/babel-plugin-transform-class-properties>
//! * <https://github.com/babel/babel/blob/v7.26.2/packages/babel-helper-create-class-features-plugin/src/index.ts>
//! * <https://github.com/babel/babel/blob/v7.26.2/packages/babel-helper-create-class-features-plugin/src/fields.ts>
//! * Class properties TC39 proposal: <https://github.com/tc39/proposal-class-fields>
use std::hash::BuildHasherDefault;

use indexmap::IndexMap;
use rustc_hash::FxHasher;
use serde::Deserialize;

use oxc_allocator::{Address, GetAddress};
use oxc_ast::ast::*;
use oxc_data_structures::stack::{NonEmptyStack, SparseStack};
use oxc_traverse::{BoundIdentifier, Traverse, TraverseCtx};

use crate::TransformCtx;

mod class;
mod constructor;
mod private;
mod utils;

type FxIndexMap<K, V> = IndexMap<K, V, BuildHasherDefault<FxHasher>>;

#[derive(Debug, Default, Clone, Copy, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct ClassPropertiesOptions {
#[serde(alias = "loose")]
pub(crate) set_public_class_fields: bool,
}

/// Class properties transform.
///
/// See [module docs] for details.
///
/// [module docs]: self
pub struct ClassProperties<'a, 'ctx> {
// Options
set_public_class_fields: bool,
static_block: bool,

ctx: &'ctx TransformCtx<'a>,

// State during whole AST
/// Stack of private props.
/// Pushed to when entering a class (`None` if class has no private props, `Some` if it does).
/// Entries are a mapping from private prop name to binding for temp var.
/// This is then used as lookup when transforming e.g. `this.#x`.
// TODO: The way stack is used is not perfect, because pushing to/popping from it in
// `enter_expression` / `exit_expression`. If another transform replaces the class,
// then stack will get out of sync.
// TODO: Should push to the stack only when entering class body, because `#x` in class `extends`
// clause resolves to `#x` in *outer* class, not the current class.
// TODO(improve-on-babel): Order that temp vars are created in is not important. Use `FxHashMap` instead.
private_props_stack: SparseStack<PrivateProps<'a>>,
/// Addresses of class expressions being processed, to prevent same class being visited twice.
/// Have to use a stack because the revisit doesn't necessarily happen straight after the first visit.
/// e.g. `c = class C { [class D {}] = 1; }` -> `c = (_D = class D {}, class C { ... })`
class_expression_addresses_stack: NonEmptyStack<Address>,

// State during transform of class
/// `true` for class declaration, `false` for class expression
is_declaration: bool,
/// Var for class.
/// e.g. `X` in `class X {}`.
/// e.g. `_Class` in `_Class = class {}, _Class.x = 1, _Class`
class_name: ClassName<'a>,
/// Expressions to insert before class
insert_before: Vec<Expression<'a>>,
/// Expressions to insert after class expression
insert_after_exprs: Vec<Expression<'a>>,
/// Statements to insert after class declaration
insert_after_stmts: Vec<Statement<'a>>,
}

/// Representation of binding for class name.
enum ClassName<'a> {
/// Class has a name. This is the binding.
Binding(BoundIdentifier<'a>),
/// Class is anonymous.
/// This is the name it would have if we need to set class name, in order to reference it.
Name(&'a str),
}

/// Details of private properties for a class.
struct PrivateProps<'a> {
/// Private properties for class. Indexed by property name.
props: FxIndexMap<Atom<'a>, PrivateProp<'a>>,
/// Binding for class name
class_name_binding: Option<BoundIdentifier<'a>>,
/// `true` for class declaration, `false` for class expression
is_declaration: bool,
}

/// Details of a private property.
struct PrivateProp<'a> {
binding: BoundIdentifier<'a>,
is_static: bool,
}

impl<'a, 'ctx> ClassProperties<'a, 'ctx> {
pub fn new(
options: ClassPropertiesOptions,
static_block: bool,
ctx: &'ctx TransformCtx<'a>,
) -> Self {
// TODO: Raise error if these 2 options are inconsistent
let set_public_class_fields =
options.set_public_class_fields || ctx.assumptions.set_public_class_fields;

Self {
set_public_class_fields,
static_block,
ctx,
private_props_stack: SparseStack::new(),
class_expression_addresses_stack: NonEmptyStack::new(Address::DUMMY),
// Temporary values - overwritten when entering class
is_declaration: false,
class_name: ClassName::Name(""),
// `Vec`s and `FxHashMap`s which are reused for every class being transformed
insert_before: vec![],
insert_after_exprs: vec![],
insert_after_stmts: vec![],
}
}
}

impl<'a, 'ctx> Traverse<'a> for ClassProperties<'a, 'ctx> {
fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
// Note: `delete this.#prop` is an early syntax error, so no need to handle transforming it
match expr {
// `class {}`
Expression::ClassExpression(_) => {
self.transform_class_expression(expr, ctx);
}
// `object.#prop`
Expression::PrivateFieldExpression(_) => {
self.transform_private_field_expression(expr, ctx);
}
// `object.#prop()`
Expression::CallExpression(_) => {
self.transform_call_expression(expr, ctx);
}
// `object.#prop = value`, `object.#prop += value`, `object.#prop ??= value` etc
Expression::AssignmentExpression(_) => {
self.transform_assignment_expression(expr, ctx);
}
// `object.#prop++`, `--object.#prop`
Expression::UpdateExpression(_) => {
self.transform_update_expression(expr, ctx);
}
// `object?.#prop`
Expression::ChainExpression(_) => {
// TODO: `transform_chain_expression` is no-op at present
self.transform_chain_expression(expr, ctx);
}
// "object.#prop`xyz`"
Expression::TaggedTemplateExpression(_) => {
// TODO: `transform_tagged_template_expression` is no-op at present
self.transform_tagged_template_expression(expr, ctx);
}
// TODO: `[object.#prop] = value`
// TODO: `({x: object.#prop} = value)`
_ => {}
}
}

fn enter_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
match stmt {
// `class C {}`
Statement::ClassDeclaration(class) => {
let stmt_address = class.address();
self.transform_class_declaration(class, stmt_address, ctx);
}
// `export class C {}`
Statement::ExportNamedDeclaration(decl) => {
let stmt_address = decl.address();
if let Some(Declaration::ClassDeclaration(class)) = &mut decl.declaration {
self.transform_class_declaration(class, stmt_address, ctx);
}
}
// `export default class {}`
Statement::ExportDefaultDeclaration(decl) => {
let stmt_address = decl.address();
if let ExportDefaultDeclarationKind::ClassDeclaration(class) = &mut decl.declaration
{
self.transform_class_export_default(class, stmt_address, ctx);
}
}
_ => {}
}
}

fn exit_class(&mut self, class: &mut Class<'a>, _ctx: &mut TraverseCtx<'a>) {
self.transform_class_on_exit(class);
}
}
984 changes: 984 additions & 0 deletions crates/oxc_transformer/src/es2022/class_properties/private.rs

Large diffs are not rendered by default.

55 changes: 55 additions & 0 deletions crates/oxc_transformer/src/es2022/class_properties/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//! ES2022: Class Properties
//! Utility functions.
use oxc_ast::ast::*;
use oxc_span::SPAN;
use oxc_syntax::reference::ReferenceFlags;
use oxc_traverse::{BoundIdentifier, TraverseCtx};

/// Create assignment to a binding.
pub(super) fn create_assignment<'a>(
binding: &BoundIdentifier<'a>,
value: Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
ctx.ast.expression_assignment(
SPAN,
AssignmentOperator::Assign,
binding.create_target(ReferenceFlags::Write, ctx),
value,
)
}

/// Create `var` declaration.
pub(super) fn create_variable_declaration<'a>(
binding: &BoundIdentifier<'a>,
init: Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Statement<'a> {
let kind = VariableDeclarationKind::Var;
let declarator = ctx.ast.variable_declarator(
SPAN,
kind,
binding.create_binding_pattern(ctx),
Some(init),
false,
);
Statement::from(ctx.ast.declaration_variable(SPAN, kind, ctx.ast.vec1(declarator), false))
}

/// Convert an iterator of `Expression`s into an iterator of `Statement::ExpressionStatement`s.
pub(super) fn exprs_into_stmts<'a, 'c, E>(
exprs: E,
ctx: &'c TraverseCtx<'a>,
) -> impl Iterator<Item = Statement<'a>> + 'c
where
E: IntoIterator<Item = Expression<'a>>,
<E as IntoIterator>::IntoIter: 'c,
{
exprs.into_iter().map(|expr| ctx.ast.statement_expression(SPAN, expr))
}

/// Create `IdentifierName` for `_`.
pub(super) fn create_underscore_ident_name<'a>(ctx: &mut TraverseCtx<'a>) -> IdentifierName<'a> {
ctx.ast.identifier_name(SPAN, Atom::from("_"))
}
5 changes: 4 additions & 1 deletion crates/oxc_transformer/src/es2022/class_static_block.rs
Original file line number Diff line number Diff line change
@@ -130,7 +130,10 @@ impl ClassStaticBlock {
/// Convert static block to expression which will be value of private field.
/// `static { foo }` -> `foo`
/// `static { foo; bar; }` -> `(() => { foo; bar; })()`
fn convert_block_to_expression<'a>(
///
/// This function also used by `ClassProperties` transform.
/// TODO: Make this function non-pub if no longer use it for `ClassProperties`.
pub fn convert_block_to_expression<'a>(
block: &mut StaticBlock<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
44 changes: 32 additions & 12 deletions crates/oxc_transformer/src/es2022/mod.rs
Original file line number Diff line number Diff line change
@@ -14,31 +14,51 @@ use class_static_block::ClassStaticBlock;
pub use options::ES2022Options;

pub struct ES2022<'a, 'ctx> {
options: ES2022Options,
// Plugins
class_static_block: ClassStaticBlock,
class_static_block: Option<ClassStaticBlock>,
class_properties: Option<ClassProperties<'a, 'ctx>>,
}

impl<'a, 'ctx> ES2022<'a, 'ctx> {
pub fn new(options: ES2022Options, ctx: &'ctx TransformCtx<'a>) -> Self {
Self {
options,
class_static_block: ClassStaticBlock::new(),
class_properties: options
.class_properties
.map(|options| ClassProperties::new(options, ctx)),
}
// Class properties transform performs the static block transform differently.
// So only enable static block transform if class properties transform is disabled.
let (class_static_block, class_properties) =
if let Some(properties_options) = options.class_properties {
let class_properties =
ClassProperties::new(properties_options, options.class_static_block, ctx);
(None, Some(class_properties))
} else {
let class_static_block =
if options.class_static_block { Some(ClassStaticBlock::new()) } else { None };
(class_static_block, None)
};
Self { class_static_block, class_properties }
}
}

impl<'a, 'ctx> Traverse<'a> for ES2022<'a, 'ctx> {
fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
if let Some(class_properties) = &mut self.class_properties {
class_properties.enter_expression(expr, ctx);
}
}

fn enter_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
if let Some(class_properties) = &mut self.class_properties {
class_properties.enter_statement(stmt, ctx);
}
}

fn enter_class_body(&mut self, body: &mut ClassBody<'a>, ctx: &mut TraverseCtx<'a>) {
if self.options.class_static_block {
self.class_static_block.enter_class_body(body, ctx);
if let Some(class_static_block) = &mut self.class_static_block {
class_static_block.enter_class_body(body, ctx);
}
}

fn exit_class(&mut self, class: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) {
if let Some(class_properties) = &mut self.class_properties {
class_properties.enter_class_body(body, ctx);
class_properties.exit_class(class, ctx);
}
}
}
6 changes: 6 additions & 0 deletions crates/oxc_transformer/src/lib.rs
Original file line number Diff line number Diff line change
@@ -241,6 +241,10 @@ impl<'a, 'ctx> Traverse<'a> for TransformerImpl<'a, 'ctx> {
}
}

fn exit_class(&mut self, class: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) {
self.x2_es2022.exit_class(class, ctx);
}

fn enter_class_body(&mut self, body: &mut ClassBody<'a>, ctx: &mut TraverseCtx<'a>) {
if let Some(typescript) = self.x0_typescript.as_mut() {
typescript.enter_class_body(body, ctx);
@@ -271,6 +275,7 @@ impl<'a, 'ctx> Traverse<'a> for TransformerImpl<'a, 'ctx> {
if let Some(typescript) = self.x0_typescript.as_mut() {
typescript.enter_expression(expr, ctx);
}
self.x2_es2022.enter_expression(expr, ctx);
self.x2_es2021.enter_expression(expr, ctx);
self.x2_es2020.enter_expression(expr, ctx);
self.x2_es2018.enter_expression(expr, ctx);
@@ -504,6 +509,7 @@ impl<'a, 'ctx> Traverse<'a> for TransformerImpl<'a, 'ctx> {
if let Some(typescript) = self.x0_typescript.as_mut() {
typescript.enter_statement(stmt, ctx);
}
self.x2_es2022.enter_statement(stmt, ctx);
self.x2_es2018.enter_statement(stmt, ctx);
}

Original file line number Diff line number Diff line change
@@ -74,6 +74,5 @@ a || (a = b);
########## 9 es2021
class foo { static {} }
----------
class foo {
static #_ = (() => {})();
}
class foo {}
(() => {})();
735 changes: 734 additions & 1 deletion tasks/transform_conformance/snapshots/babel.snap.md

Large diffs are not rendered by default.

1,508 changes: 1,499 additions & 9 deletions tasks/transform_conformance/snapshots/babel_exec.snap.md

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions tasks/transform_conformance/src/constants.rs
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ pub const PLUGINS: &[&str] = &[
// // ES2024
// "babel-plugin-transform-unicode-sets-regex",
// // ES2022
// "babel-plugin-transform-class-properties",
"babel-plugin-transform-class-properties",
"babel-plugin-transform-class-static-block",
// "babel-plugin-transform-private-methods",
// "babel-plugin-transform-private-property-in-object",
@@ -63,7 +63,6 @@ pub const PLUGINS: &[&str] = &[

pub const PLUGINS_NOT_SUPPORTED_YET: &[&str] = &[
"proposal-decorators",
"transform-class-properties",
"transform-classes",
"transform-destructuring",
"transform-modules-commonjs",

0 comments on commit 1fdf61f

Please sign in to comment.