diff --git a/crates/oxc_transformer/src/common/helper_loader.rs b/crates/oxc_transformer/src/common/helper_loader.rs index 93d310afa0e3f..b3f2c57159c8f 100644 --- a/crates/oxc_transformer/src/common/helper_loader.rs +++ b/crates/oxc_transformer/src/common/helper_loader.rs @@ -166,6 +166,7 @@ pub enum Helper { CheckInRHS, Decorate, DecorateParam, + DecorateMetadata, } impl Helper { @@ -197,6 +198,7 @@ impl Helper { Self::CheckInRHS => "checkInRHS", Self::Decorate => "decorate", Self::DecorateParam => "decorateParam", + Self::DecorateMetadata => "decorateMetadata", } } } diff --git a/crates/oxc_transformer/src/decorator/legacy/metadata.rs b/crates/oxc_transformer/src/decorator/legacy/metadata.rs new file mode 100644 index 0000000000000..4a261057778fc --- /dev/null +++ b/crates/oxc_transformer/src/decorator/legacy/metadata.rs @@ -0,0 +1,595 @@ +/// Emitting decorator metadata. +/// +/// This plugin is used to emit decorator metadata for legacy decorators by +/// the `__metadata` helper. +/// +/// ## Example +/// +/// Input: +/// ```ts +/// class Demo { +/// @LogMethod +/// public foo(bar: number) {} +/// +/// @Prop +/// prop: string = "hello"; +/// } +/// ``` +/// +/// Output: +/// ```js +/// class Demo { +/// foo(bar) {} +/// prop = "hello"; +/// } +/// babelHelpers.decorate([ +/// LogMethod, +/// babelHelpers.decorateParam(0, babelHelpers.decorateMetadata("design:type", Function)), +/// babelHelpers.decorateParam(0, babelHelpers.decorateMetadata("design:paramtypes", [Number])), +/// babelHelpers.decorateParam(0, babelHelpers.decorateMetadata("design:returntype", void 0)) +/// ], Demo.prototype, "foo", null); +/// babelHelpers.decorate([Prop, babelHelpers.decorateMetadata("design:type", String)], Demo.prototype, "prop", void 0); +/// ``` +/// +/// ## Implementation +/// +/// Implementation based on https://github.com/microsoft/TypeScript/blob/d85767abfd83880cea17cea70f9913e9c4496dcc/src/compiler/transformers/ts.ts#L1119-L1136 +/// +/// ## Limitations +/// +/// ### Compared to TypeScript +/// +/// We are lacking a kind of type inference ability that TypeScript has, so we are not able to determine +/// the exactly type of the type reference. See [`LegacyDecoratorMetadata::serialize_type_reference_node`] does. +/// +/// For example: +/// +/// Input: +/// ```ts +/// type Foo = string; +/// class Cls { +/// @dec +/// p: Foo = "" +/// } +/// ``` +/// +/// TypeScript Output: +/// ```js +/// class Cls { +/// constructor() { +/// this.p = ""; +/// } +/// } +/// __decorate([ +/// dec, +/// __metadata("design:type", String) // Infer the type of `Foo` is `String` +/// ], Cls.prototype, "p", void 0); +/// ``` +/// +/// OXC Output: +/// ```js +/// var _ref; +/// class Cls { +/// p = ""; +/// } +/// babelHelpers.decorate([ +/// dec, +/// babelHelpers.decorateMetadata("design:type", typeof (_ref = typeof Foo === "undefined" && Foo) === "function" ? _ref : Object) +/// ], +/// Cls.prototype, "p", void 0); +/// ``` +/// +/// ### Compared to SWC +/// +/// SWC also has the above limitation, considering that SWC has been adopted in [NestJS](https://docs.nestjs.com/recipes/swc#jest--swc), +/// so the limitation may not be a problem. In addition, SWC provides additional support for inferring enum members, which we currently +/// do not have. We haven't dived into how NestJS uses it, so we don't know if it matters, thus we may leave it until we receive feedback. +/// +/// ## References +/// * TypeScript's [emitDecoratorMetadata](https://www.typescriptlang.org/tsconfig#emitDecoratorMetadata) +use oxc_allocator::Box as ArenaBox; +use oxc_ast::ast::*; +use oxc_semantic::ReferenceFlags; +use oxc_span::{ContentEq, SPAN}; +use oxc_traverse::{MaybeBoundIdentifier, Traverse, TraverseCtx}; + +use crate::{context::TransformCtx, utils::ast_builder::create_property_access, Helper}; + +pub struct LegacyDecoratorMetadata<'a, 'ctx> { + ctx: &'ctx TransformCtx<'a>, +} + +impl<'a, 'ctx> LegacyDecoratorMetadata<'a, 'ctx> { + pub fn new(ctx: &'ctx TransformCtx<'a>) -> Self { + LegacyDecoratorMetadata { ctx } + } +} + +impl<'a> Traverse<'a> for LegacyDecoratorMetadata<'a, '_> { + fn enter_class(&mut self, class: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) { + if class.decorators.is_empty() { + return; + } + let constructor = class.body.body.iter_mut().find_map(|item| match item { + ClassElement::MethodDefinition(method) if method.kind.is_constructor() => Some(method), + _ => None, + }); + + if let Some(constructor) = constructor { + let serialized_type = + self.serialize_parameter_types_of_node(&constructor.value.params, ctx); + let metadata_decorator = + self.create_metadata_decorate("design:paramtypes", serialized_type, ctx); + + if let Some(param) = constructor.value.params.items.last_mut() { + // We need to make sure all metadata decorators are placed after parameter decorators + param.decorators.push(metadata_decorator); + } else { + class.decorators.push(metadata_decorator); + } + } + } + + fn enter_method_definition( + &mut self, + method: &mut MethodDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if method.kind.is_constructor() { + return; + } + + let is_decorated = !method.decorators.is_empty() + || method.value.params.items.iter().any(|param| !param.decorators.is_empty()); + if !is_decorated { + return; + } + + let metadata_decorators = ctx.ast.vec_from_array([ + self.create_metadata_decorate("design:type", Self::global_function(ctx), ctx), + { + let serialized_type = + self.serialize_parameter_types_of_node(&method.value.params, ctx); + self.create_metadata_decorate("design:paramtypes", serialized_type, ctx) + }, + { + let serialized_type = self.serialize_return_type_of_node(&method.value, ctx); + self.create_metadata_decorate("design:returntype", serialized_type, ctx) + }, + ]); + + if let Some(param) = method.value.params.items.last_mut() { + // We need to make sure all metadata decorators are placed after parameter decorators + param.decorators.extend(metadata_decorators); + } else { + method.decorators.extend(metadata_decorators); + } + } + + #[inline] + fn enter_property_definition( + &mut self, + prop: &mut PropertyDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if !prop.decorators.is_empty() { + prop.decorators + .push(self.create_design_type_metadata(prop.type_annotation.as_ref(), ctx)); + } + } + + #[inline] + fn enter_accessor_property( + &mut self, + prop: &mut AccessorProperty<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if !prop.decorators.is_empty() { + prop.decorators + .push(self.create_design_type_metadata(prop.type_annotation.as_ref(), ctx)); + } + } +} + +impl<'a> LegacyDecoratorMetadata<'a, '_> { + fn serialize_type_annotation( + &mut self, + type_annotation: Option<&ArenaBox<'a, TSTypeAnnotation<'a>>>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + if let Some(type_annotation) = type_annotation { + self.serialize_type_node(&type_annotation.type_annotation, ctx) + } else { + Self::global_object(ctx) + } + } + + /// Serializes a type node for use with decorator type metadata. + /// + /// Types are serialized in the following fashion: + /// - Void types point to "undefined" (e.g. "void 0") + /// - Function and Constructor types point to the global "Function" constructor. + /// - Interface types with a call or construct signature types point to the global + /// "Function" constructor. + /// - Array and Tuple types point to the global "Array" constructor. + /// - Type predicates and booleans point to the global "Boolean" constructor. + /// - String literal types and strings point to the global "String" constructor. + /// - Enum and number types point to the global "Number" constructor. + /// - Symbol types point to the global "Symbol" constructor. + /// - Type references to classes (or class-like variables) point to the constructor for the class. + /// - Anything else points to the global "Object" constructor. + fn serialize_type_node( + &mut self, + node: &TSType<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + match &node { + TSType::TSVoidKeyword(_) + | TSType::TSUndefinedKeyword(_) + | TSType::TSNullKeyword(_) + | TSType::TSNeverKeyword(_) => ctx.ast.void_0(SPAN), + TSType::TSFunctionType(_) | TSType::TSConstructorType(_) => Self::global_function(ctx), + TSType::TSArrayType(_) | TSType::TSTupleType(_) => Self::global_array(ctx), + TSType::TSTypePredicate(t) => { + if t.asserts { + ctx.ast.void_0(SPAN) + } else { + Self::global_boolean(ctx) + } + } + TSType::TSBooleanKeyword(_) => Self::global_boolean(ctx), + TSType::TSTemplateLiteralType(_) | TSType::TSStringKeyword(_) => { + Self::global_string(ctx) + } + TSType::TSLiteralType(literal) => { + Self::serialize_literal_of_literal_type_node(&literal.literal, ctx) + } + TSType::TSNumberKeyword(_) => Self::global_number(ctx), + TSType::TSBigIntKeyword(_) => Self::global_bigint(ctx), + TSType::TSSymbolKeyword(_) => Self::global_symbol(ctx), + TSType::TSTypeReference(t) => { + self.serialize_type_reference_node(&t.type_name, ctx) + } + TSType::TSIntersectionType(t) => { + self.serialize_union_or_intersection_constituents(t.types.iter(), /* is_intersection */ true, ctx) + } + TSType::TSUnionType(t) => { + self.serialize_union_or_intersection_constituents(t.types.iter(), /* is_intersection */ false, ctx) + } + TSType::TSConditionalType(t) => { + self.serialize_union_or_intersection_constituents( + [&t.true_type, &t.false_type].into_iter(), + false, + ctx + ) + } + TSType::TSTypeOperatorType(operator) + if operator.operator == TSTypeOperatorOperator::Readonly => + { + self.serialize_type_node(&operator.type_annotation, ctx) + } + TSType::JSDocNullableType(t) => { + self.serialize_type_node(&t.type_annotation, ctx) + } + TSType::JSDocNonNullableType(t) => { + self.serialize_type_node(&t.type_annotation, ctx) + } + TSType::TSParenthesizedType(t) => { + self.serialize_type_node(&t.type_annotation, ctx) + } + TSType::TSObjectKeyword(_) + // Fallback to `Object` + | TSType::TSTypeQuery(_) | TSType::TSIndexedAccessType(_) | TSType::TSMappedType(_) + | TSType::TSTypeLiteral(_) | TSType::TSAnyKeyword(_) | TSType::TSUnknownKeyword(_) + | TSType::TSThisType(_) | TSType::TSImportType(_) | TSType::TSTypeOperatorType(_) + // Not allowed to be used in the start of type annotations, fallback to `Object` + | TSType::TSInferType(_) | TSType::TSIntrinsicKeyword(_) | TSType::TSNamedTupleMember(_) + | TSType::JSDocUnknownType(_) => Self::global_object(ctx), + } + } + + /// Serializes the type of a node for use with decorator type metadata. + fn serialize_parameter_types_of_node( + &mut self, + params: &FormalParameters<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let mut elements = + ctx.ast.vec_with_capacity(params.items.len() + usize::from(params.rest.is_some())); + elements.extend(params.items.iter().map(|param| { + let type_annotation = match ¶m.pattern.kind { + BindingPatternKind::AssignmentPattern(pattern) => { + pattern.left.type_annotation.as_ref() + } + _ => param.pattern.type_annotation.as_ref(), + }; + ArrayExpressionElement::from(self.serialize_type_annotation(type_annotation, ctx)) + })); + + if let Some(rest) = ¶ms.rest { + elements.push(ArrayExpressionElement::from( + self.serialize_type_annotation(rest.argument.type_annotation.as_ref(), ctx), + )); + } + ctx.ast.expression_array(SPAN, elements, None) + } + + /// Serializes the return type of a node for use with decorator type metadata. + fn serialize_return_type_of_node( + &mut self, + func: &Function<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + if func.r#async { + Self::global_promise(ctx) + } else if let Some(return_type) = &func.return_type { + self.serialize_type_node(&return_type.type_annotation, ctx) + } else { + ctx.ast.void_0(SPAN) + } + } + + /// `A.B` -> `typeof (_a$b = typeof A !== "undefined" && A.B) == "function" ? _a$b : Object` + /// + /// NOTE: This function only ports `unknown` part from [TypeScript](https://github.com/microsoft/TypeScript/blob/d85767abfd83880cea17cea70f9913e9c4496dcc/src/compiler/transformers/typeSerializer.ts#L499-L506) + fn serialize_type_reference_node( + &mut self, + name: &TSTypeName<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let serialized_type = self.serialize_entity_name_as_expression_fallback(name, ctx); + let binding = self.ctx.var_declarations.create_uid_var_based_on_node(&serialized_type, ctx); + let target = binding.create_write_target(ctx); + let assignment = ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + target, + serialized_type, + ); + let type_of = ctx.ast.expression_unary(SPAN, UnaryOperator::Typeof, assignment); + let right = ctx.ast.expression_string_literal(SPAN, "function", None); + let operator = BinaryOperator::StrictEquality; + let test = ctx.ast.expression_binary(SPAN, type_of, operator, right); + let consequent = binding.create_read_expression(ctx); + let alternate = Self::global_object(ctx); + ctx.ast.expression_conditional(SPAN, test, consequent, alternate) + } + + /// Serializes an entity name which may not exist at runtime, but whose access shouldn't throw + fn serialize_entity_name_as_expression_fallback( + &mut self, + name: &TSTypeName<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + match name { + // `A` -> `typeof A !== "undefined" && A` + TSTypeName::IdentifierReference(ident) => { + let binding = MaybeBoundIdentifier::from_identifier_reference(ident, ctx); + let ident1 = binding.create_read_expression(ctx); + let ident2 = binding.create_read_expression(ctx); + Self::create_checked_value(ident1, ident2, ctx) + } + TSTypeName::QualifiedName(qualified) => { + if let TSTypeName::IdentifierReference(ident) = &qualified.left { + // `A.B` -> `typeof A !== "undefined" && A.B` + let binding = MaybeBoundIdentifier::from_identifier_reference(ident, ctx); + let ident1 = binding.create_read_expression(ctx); + let ident2 = binding.create_read_expression(ctx); + let member = create_property_access(ident1, &qualified.right.name, ctx); + Self::create_checked_value(ident2, member, ctx) + } else { + // `A.B.C` -> `typeof A !== "undefined" && (_a = A.B) !== void 0 && _a.C` + let mut left = + self.serialize_entity_name_as_expression_fallback(&qualified.left, ctx); + let binding = + self.ctx.var_declarations.create_uid_var_based_on_node(&left, ctx); + let Expression::LogicalExpression(logical) = &mut left else { unreachable!() }; + let right = ctx.ast.move_expression(&mut logical.right); + // `(_a = A.B)` + let right = ctx.ast.expression_assignment( + SPAN, + AssignmentOperator::Assign, + binding.create_write_target(ctx), + right, + ); + // `(_a = A.B) !== void 0` + logical.right = ctx.ast.expression_binary( + SPAN, + right, + BinaryOperator::StrictInequality, + ctx.ast.void_0(SPAN), + ); + + let object = binding.create_read_expression(ctx); + let member = create_property_access(object, &qualified.right.name, ctx); + ctx.ast.expression_logical(SPAN, left, LogicalOperator::And, member) + } + } + } + } + + fn serialize_literal_of_literal_type_node( + literal: &TSLiteral<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + match literal { + TSLiteral::BooleanLiteral(_) => Self::global_boolean(ctx), + TSLiteral::NumericLiteral(_) => Self::global_number(ctx), + TSLiteral::BigIntLiteral(_) => Self::global_bigint(ctx), + TSLiteral::StringLiteral(_) | TSLiteral::TemplateLiteral(_) => Self::global_string(ctx), + TSLiteral::UnaryExpression(expr) => match expr.argument { + Expression::NumericLiteral(_) => Self::global_number(ctx), + Expression::StringLiteral(_) => Self::global_string(ctx), + // Cannot be a type annotation + _ => unreachable!(), + }, + } + } + + fn serialize_union_or_intersection_constituents<'t>( + &mut self, + types: impl Iterator>, + is_intersection: bool, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> + where + 'a: 't, + { + let mut serialized_type = None; + + for t in types { + let t = t.without_parenthesized(); + match t { + TSType::TSNeverKeyword(_) => { + if is_intersection { + // Reduce to `never` in an intersection + return ctx.ast.void_0(SPAN); + } + // Elide `never` in a union + continue; + } + TSType::TSUnknownKeyword(_) => { + if !is_intersection { + // Reduce to `unknown` in a union + return Self::global_object(ctx); + } + // Elide `unknown` in an intersection + continue; + } + TSType::TSAnyKeyword(_) => { + return Self::global_object(ctx); + } + _ => {} + }; + + let serialized_constituent = self.serialize_type_node(t, ctx); + if matches!(&serialized_constituent, Expression::Identifier(ident) if ident.name == "Object") + { + // One of the individual is global object, return immediately + return serialized_constituent; + } + + // If there exists union that is not `void 0` expression, check if the the common type is identifier. + // anything more complex and we will just default to Object + if let Some(serialized_type) = &serialized_type { + // Different types + if !Self::equate_serialized_type_nodes(serialized_type, &serialized_constituent) { + return Self::global_object(ctx); + } + } else { + // Initialize the union type + serialized_type = Some(serialized_constituent); + } + } + + // If we were able to find common type, use it + serialized_type.unwrap_or_else(|| { + // Fallback is only hit if all union constituents are null/undefined/never + ctx.ast.void_0(SPAN) + }) + } + + /// Compares two serialized type nodes for equality. + /// + /// + #[inline] + fn equate_serialized_type_nodes(a: &Expression<'a>, b: &Expression<'a>) -> bool { + a.content_eq(b) + } + + #[inline] + fn create_global_identifier(ident: &'static str, ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + ctx.create_unbound_ident_expr(SPAN, Atom::new_const(ident), ReferenceFlags::Read) + } + + #[inline] + fn global_object(ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + Self::create_global_identifier("Object", ctx) + } + + #[inline] + fn global_function(ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + Self::create_global_identifier("Function", ctx) + } + + #[inline] + fn global_array(ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + Self::create_global_identifier("Array", ctx) + } + + #[inline] + fn global_boolean(ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + Self::create_global_identifier("Boolean", ctx) + } + + #[inline] + fn global_string(ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + Self::create_global_identifier("String", ctx) + } + + #[inline] + fn global_number(ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + Self::create_global_identifier("Number", ctx) + } + + #[inline] + fn global_bigint(ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + Self::create_global_identifier("BigInt", ctx) + } + + #[inline] + fn global_symbol(ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + Self::create_global_identifier("Symbol", ctx) + } + + #[inline] + fn global_promise(ctx: &mut TraverseCtx<'a>) -> Expression<'a> { + Self::create_global_identifier("Promise", ctx) + } + + /// Produces an expression that results in `right` if `left` is not undefined at runtime: + /// + /// ``` + /// typeof left !== "undefined" && right + /// ``` + /// + /// We use `typeof L !== "undefined"` (rather than `L !== undefined`) since `L` may not be declared. + /// It's acceptable for this expression to result in `false` at runtime, as the result is intended to be + /// further checked by any containing expression. + fn create_checked_value( + left: Expression<'a>, + right: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Expression<'a> { + let operator = BinaryOperator::StrictEquality; + let undefined = ctx.ast.expression_string_literal(SPAN, "undefined", None); + let typeof_left = ctx.ast.expression_unary(SPAN, UnaryOperator::Typeof, left); + let left_check = ctx.ast.expression_binary(SPAN, typeof_left, operator, undefined); + ctx.ast.expression_logical(SPAN, left_check, LogicalOperator::And, right) + } + + // `_metadata(key, value) + fn create_metadata_decorate( + &mut self, + key: &str, + value: Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Decorator<'a> { + let arguments = ctx.ast.vec_from_iter([ + Argument::from(ctx.ast.expression_string_literal(SPAN, key, None)), + Argument::from(value), + ]); + let expr = self.ctx.helper_call_expr(Helper::DecorateMetadata, SPAN, arguments, ctx); + ctx.ast.decorator(SPAN, expr) + } + + /// `_metadata("design:type", type)` + fn create_design_type_metadata( + &mut self, + type_annotation: Option<&ArenaBox<'a, TSTypeAnnotation<'a>>>, + ctx: &mut TraverseCtx<'a>, + ) -> Decorator<'a> { + let serialized_type = self.serialize_type_annotation(type_annotation, ctx); + self.create_metadata_decorate("design:type", serialized_type, ctx) + } +} diff --git a/crates/oxc_transformer/src/decorator/legacy_decorator.rs b/crates/oxc_transformer/src/decorator/legacy/mod.rs similarity index 96% rename from crates/oxc_transformer/src/decorator/legacy_decorator.rs rename to crates/oxc_transformer/src/decorator/legacy/mod.rs index 0e47a2333b118..0c5dfd37124b0 100644 --- a/crates/oxc_transformer/src/decorator/legacy_decorator.rs +++ b/crates/oxc_transformer/src/decorator/legacy/mod.rs @@ -43,8 +43,11 @@ //! ## References: //! * TypeScript Experimental Decorators documentation: +mod metadata; + use std::mem; +use metadata::LegacyDecoratorMetadata; use oxc_allocator::{Address, Vec as ArenaVec}; use oxc_ast::{ast::*, Visit, VisitMut, NONE}; use oxc_semantic::{ScopeFlags, SymbolFlags}; @@ -64,12 +67,14 @@ struct ClassDecoratorInfo { } pub struct LegacyDecorator<'a, 'ctx> { + emit_decorator_metadata: bool, + metadata: LegacyDecoratorMetadata<'a, 'ctx>, ctx: &'ctx TransformCtx<'a>, } impl<'a, 'ctx> LegacyDecorator<'a, 'ctx> { - pub fn new(ctx: &'ctx TransformCtx<'a>) -> Self { - Self { ctx } + pub fn new(emit_decorator_metadata: bool, ctx: &'ctx TransformCtx<'a>) -> Self { + Self { emit_decorator_metadata, metadata: LegacyDecoratorMetadata::new(ctx), ctx } } } @@ -88,6 +93,46 @@ impl<'a> Traverse<'a> for LegacyDecorator<'a, '_> { _ => {} }; } + + #[inline] + fn enter_class(&mut self, class: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) { + if self.emit_decorator_metadata { + self.metadata.enter_class(class, ctx); + } + } + + #[inline] + fn enter_method_definition( + &mut self, + node: &mut MethodDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.emit_decorator_metadata { + self.metadata.enter_method_definition(node, ctx); + } + } + + #[inline] + fn enter_accessor_property( + &mut self, + node: &mut AccessorProperty<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.emit_decorator_metadata { + self.metadata.enter_accessor_property(node, ctx); + } + } + + #[inline] + fn enter_property_definition( + &mut self, + node: &mut PropertyDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.emit_decorator_metadata { + self.metadata.enter_property_definition(node, ctx); + } + } } impl<'a> LegacyDecorator<'a, '_> { diff --git a/crates/oxc_transformer/src/decorator/mod.rs b/crates/oxc_transformer/src/decorator/mod.rs index 5b8b0fd7fe5e4..ebc9fe043df97 100644 --- a/crates/oxc_transformer/src/decorator/mod.rs +++ b/crates/oxc_transformer/src/decorator/mod.rs @@ -1,4 +1,4 @@ -mod legacy_decorator; +mod legacy; mod options; use oxc_ast::ast::*; @@ -6,7 +6,7 @@ use oxc_traverse::{Traverse, TraverseCtx}; use crate::TransformCtx; -use legacy_decorator::LegacyDecorator; +use legacy::LegacyDecorator; pub use options::DecoratorOptions; pub struct Decorator<'a, 'ctx> { @@ -18,7 +18,10 @@ pub struct Decorator<'a, 'ctx> { impl<'a, 'ctx> Decorator<'a, 'ctx> { pub fn new(options: DecoratorOptions, ctx: &'ctx TransformCtx<'a>) -> Self { - Self { legacy_decorator: LegacyDecorator::new(ctx), options } + Self { + legacy_decorator: LegacyDecorator::new(options.emit_decorator_metadata, ctx), + options, + } } } @@ -28,4 +31,44 @@ impl<'a> Traverse<'a> for Decorator<'a, '_> { self.legacy_decorator.enter_statement(stmt, ctx); } } + + #[inline] + fn enter_class(&mut self, node: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) { + if self.options.legacy { + self.legacy_decorator.enter_class(node, ctx); + } + } + + #[inline] + fn enter_method_definition( + &mut self, + node: &mut MethodDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.options.legacy { + self.legacy_decorator.enter_method_definition(node, ctx); + } + } + + #[inline] + fn enter_accessor_property( + &mut self, + node: &mut AccessorProperty<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.options.legacy { + self.legacy_decorator.enter_accessor_property(node, ctx); + } + } + + #[inline] + fn enter_property_definition( + &mut self, + node: &mut PropertyDefinition<'a>, + ctx: &mut TraverseCtx<'a>, + ) { + if self.options.legacy { + self.legacy_decorator.enter_property_definition(node, ctx); + } + } } diff --git a/crates/oxc_transformer/src/decorator/options.rs b/crates/oxc_transformer/src/decorator/options.rs index 4d3db1c37d46d..025d2b07cc9ff 100644 --- a/crates/oxc_transformer/src/decorator/options.rs +++ b/crates/oxc_transformer/src/decorator/options.rs @@ -11,4 +11,10 @@ pub struct DecoratorOptions { /// #[serde(skip)] pub legacy: bool, + + /// Enables emitting decorator metadata. + /// + /// This option the same as [emitDecoratorMetadata](https://www.typescriptlang.org/tsconfig/#emitDecoratorMetadata) + /// in TypeScript, and it only works when `legacy` is true. + pub emit_decorator_metadata: bool, } diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index 048a15724195e..f4fb1d86dde22 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -251,6 +251,7 @@ impl<'a> Traverse<'a> for TransformerImpl<'a, '_> { } fn enter_class(&mut self, class: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) { + self.decorator.enter_class(class, ctx); if let Some(typescript) = self.x0_typescript.as_mut() { typescript.enter_class(class, ctx); } @@ -419,6 +420,7 @@ impl<'a> Traverse<'a> for TransformerImpl<'a, '_> { def: &mut MethodDefinition<'a>, ctx: &mut TraverseCtx<'a>, ) { + self.decorator.enter_method_definition(def, ctx); if let Some(typescript) = self.x0_typescript.as_mut() { typescript.enter_method_definition(def, ctx); } @@ -445,6 +447,7 @@ impl<'a> Traverse<'a> for TransformerImpl<'a, '_> { def: &mut PropertyDefinition<'a>, ctx: &mut TraverseCtx<'a>, ) { + self.decorator.enter_property_definition(def, ctx); if let Some(typescript) = self.x0_typescript.as_mut() { typescript.enter_property_definition(def, ctx); } @@ -464,6 +467,7 @@ impl<'a> Traverse<'a> for TransformerImpl<'a, '_> { node: &mut AccessorProperty<'a>, ctx: &mut TraverseCtx<'a>, ) { + self.decorator.enter_accessor_property(node, ctx); if let Some(typescript) = self.x0_typescript.as_mut() { typescript.enter_accessor_property(node, ctx); } diff --git a/crates/oxc_transformer/src/options/babel/plugins.rs b/crates/oxc_transformer/src/options/babel/plugins.rs index d79d61c4152f4..3f42749e0522b 100644 --- a/crates/oxc_transformer/src/options/babel/plugins.rs +++ b/crates/oxc_transformer/src/options/babel/plugins.rs @@ -2,7 +2,7 @@ use serde::Deserialize; use crate::{ es2015::ArrowFunctionsOptions, es2018::ObjectRestSpreadOptions, es2022::ClassPropertiesOptions, - jsx::JsxOptions, TypeScriptOptions, + jsx::JsxOptions, DecoratorOptions, TypeScriptOptions, }; use super::PluginPresetEntries; @@ -71,7 +71,7 @@ pub struct BabelPlugins { pub class_properties: Option, // Decorator - pub legacy_decorator: bool, + pub legacy_decorator: Option, } impl TryFrom for BabelPlugins { @@ -142,7 +142,10 @@ impl TryFrom for BabelPlugins { .ok(); } // This is not a Babel plugin, we pretend it exists for running legacy decorator by Babel options - "transform-legacy-decorator" => p.legacy_decorator = true, + "transform-legacy-decorator" => { + p.legacy_decorator = + entry.value::().map_err(|err| p.errors.push(err)).ok(); + } s => p.unsupported.push(s.to_string()), } } diff --git a/crates/oxc_transformer/src/options/mod.rs b/crates/oxc_transformer/src/options/mod.rs index 5958ed723f24c..8579317704c08 100644 --- a/crates/oxc_transformer/src/options/mod.rs +++ b/crates/oxc_transformer/src/options/mod.rs @@ -150,7 +150,13 @@ impl TryFrom<&BabelOptions> for TransformOptions { .or_else(|| options.plugins.typescript.clone()) .unwrap_or_default(); - let decorator = DecoratorOptions { legacy: options.plugins.legacy_decorator }; + let decorator = DecoratorOptions { + legacy: options.plugins.legacy_decorator.is_some(), + emit_decorator_metadata: options + .plugins + .legacy_decorator + .is_some_and(|o| o.emit_decorator_metadata), + }; let jsx = if let Some(options) = &options.presets.jsx { options.clone() diff --git a/crates/oxc_transformer/src/utils/ast_builder.rs b/crates/oxc_transformer/src/utils/ast_builder.rs index 91c4fdd4efff2..d91ba62a31d00 100644 --- a/crates/oxc_transformer/src/utils/ast_builder.rs +++ b/crates/oxc_transformer/src/utils/ast_builder.rs @@ -81,3 +81,13 @@ pub(crate) fn create_prototype_member<'a>( let static_member = ctx.ast.member_expression_static(SPAN, object, property, false); Expression::from(static_member) } + +/// `object` -> `object.a`. +pub(crate) fn create_property_access<'a>( + object: Expression<'a>, + property: &str, + ctx: &mut TraverseCtx<'a>, +) -> Expression<'a> { + let property = ctx.ast.identifier_name(SPAN, ctx.ast.atom(property)); + Expression::from(ctx.ast.member_expression_static(SPAN, object, property, false)) +} diff --git a/napi/transform/src/transformer.rs b/napi/transform/src/transformer.rs index e5dfc4b9569c7..7cb29f88dbe46 100644 --- a/napi/transform/src/transformer.rs +++ b/napi/transform/src/transformer.rs @@ -282,7 +282,10 @@ pub struct DecoratorOptions { impl From for oxc::transformer::DecoratorOptions { fn from(options: DecoratorOptions) -> Self { - oxc::transformer::DecoratorOptions { legacy: options.legacy.unwrap_or_default() } + oxc::transformer::DecoratorOptions { + legacy: options.legacy.unwrap_or_default(), + emit_decorator_metadata: false, + } } } diff --git a/tasks/transform_conformance/snapshots/oxc.snap.md b/tasks/transform_conformance/snapshots/oxc.snap.md index 3e79b1643068f..b162be9bcd515 100644 --- a/tasks/transform_conformance/snapshots/oxc.snap.md +++ b/tasks/transform_conformance/snapshots/oxc.snap.md @@ -599,13 +599,37 @@ after transform: ["TypedPropertyDescriptor", "babelHelpers", "dec"] rebuilt : ["babelHelpers", "dec"] * typescript/method/decoratorOnClassMethod14/input.ts -x Output mismatch +Bindings mismatch: +after transform: ScopeId(0): ["Foo", "decorator"] +rebuilt : ScopeId(0): ["Foo"] +Reference symbol mismatch for "decorator": +after transform: SymbolId(0) "decorator" +rebuilt : +Unresolved references mismatch: +after transform: ["Function", "babelHelpers"] +rebuilt : ["Function", "babelHelpers", "decorator"] * typescript/method/decoratorOnClassMethod15/input.ts -x Output mismatch +Bindings mismatch: +after transform: ScopeId(0): ["Foo", "decorator"] +rebuilt : ScopeId(0): ["Foo"] +Reference symbol mismatch for "decorator": +after transform: SymbolId(0) "decorator" +rebuilt : +Unresolved references mismatch: +after transform: ["Function", "babelHelpers"] +rebuilt : ["Function", "babelHelpers", "decorator"] * typescript/method/decoratorOnClassMethod16/input.ts -x Output mismatch +Bindings mismatch: +after transform: ScopeId(0): ["Foo", "decorator"] +rebuilt : ScopeId(0): ["Foo"] +Reference symbol mismatch for "decorator": +after transform: SymbolId(0) "decorator" +rebuilt : +Unresolved references mismatch: +after transform: ["Function", "babelHelpers"] +rebuilt : ["Function", "babelHelpers", "decorator"] * typescript/method/decoratorOnClassMethod17/input.ts @@ -621,7 +645,15 @@ x Output mismatch * typescript/method/decoratorOnClassMethod18/input.ts -x Output mismatch +Bindings mismatch: +after transform: ScopeId(0): ["Foo", "decorator"] +rebuilt : ScopeId(0): ["Foo"] +Reference symbol mismatch for "decorator": +after transform: SymbolId(0) "decorator" +rebuilt : +Unresolved references mismatch: +after transform: ["Object", "babelHelpers"] +rebuilt : ["Object", "babelHelpers", "decorator"] * typescript/method/decoratorOnClassMethod19/input.ts x Output mismatch @@ -760,7 +792,9 @@ after transform: ScopeId(0): [ScopeId(1), ScopeId(2)] rebuilt : ScopeId(0): [ScopeId(1)] * typescript/property/decoratorOnClassProperty12/input.ts -x Output mismatch +Scope children mismatch: +after transform: ScopeId(0): [ScopeId(1), ScopeId(2)] +rebuilt : ScopeId(0): [ScopeId(1)] * typescript/property/decoratorOnClassProperty13/input.ts Scope children mismatch: