use oxc_allocator::{Box as ArenaBox, TakeIn};
use oxc_ast::ast::*;
use oxc_data_structures::stack::SparseStack;
use oxc_semantic::{Reference, ReferenceFlags, SymbolId};
use oxc_span::{ContentEq, SPAN};
use oxc_traverse::{MaybeBoundIdentifier, Traverse};
use rustc_hash::FxHashMap;
use crate::{
Helper,
common::{helper_loader::helper_call_expr, var_declarations::VarDeclarationsStore},
context::TraverseCtx,
state::TransformState,
utils::ast_builder::create_property_access,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum EnumType {
String,
Number,
Object,
}
pub(super) struct MethodMetadata<'a> {
pub r#type: Expression<'a>,
pub param_types: Expression<'a>,
pub return_type: Option<Expression<'a>>,
}
pub struct LegacyDecoratorMetadata<'a> {
method_metadata_stack: SparseStack<MethodMetadata<'a>>,
constructor_metadata_stack: SparseStack<Expression<'a>>,
enum_types: FxHashMap<SymbolId, EnumType>,
}
impl LegacyDecoratorMetadata<'_> {
pub fn new() -> Self {
LegacyDecoratorMetadata {
method_metadata_stack: SparseStack::new(),
constructor_metadata_stack: SparseStack::new(),
enum_types: FxHashMap::default(),
}
}
}
impl<'a> Traverse<'a, TransformState<'a>> for LegacyDecoratorMetadata<'a> {
#[inline]
fn exit_program(&mut self, _program: &mut Program<'a>, _ctx: &mut TraverseCtx<'a>) {
debug_assert!(
self.method_metadata_stack.is_exhausted(),
"All method metadata should have been popped."
);
debug_assert!(
self.constructor_metadata_stack.is_exhausted(),
"All constructor metadata should have been popped."
);
}
#[inline]
fn enter_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
if let Statement::TSEnumDeclaration(decl) = stmt {
self.collect_enum_type(decl, ctx);
}
}
fn enter_class(&mut self, class: &mut Class<'a>, ctx: &mut TraverseCtx<'a>) {
let should_transform = !(class.is_expression() || class.declare);
let constructor = class.body.body.iter_mut().find_map(|item| match item {
ClassElement::MethodDefinition(method)
if method.kind.is_constructor() && method.value.body.is_some() =>
{
Some(method)
}
_ => None,
});
let metadata = if should_transform
&& let Some(constructor) = constructor
&& !(class.decorators.is_empty()
&& constructor.value.params.items.iter().all(|param| param.decorators.is_empty()))
{
let serialized_type =
self.serialize_parameters_types_of_node(&constructor.value.params, ctx);
Some(self.create_metadata("design:paramtypes", serialized_type, ctx))
} else {
None
};
self.constructor_metadata_stack.push(metadata);
}
fn enter_method_definition(
&mut self,
method: &mut MethodDefinition<'a>,
ctx: &mut TraverseCtx<'a>,
) {
if method.kind.is_constructor() {
return;
}
let is_typescript_syntax = method.value.is_typescript_syntax();
let is_decorated = !is_typescript_syntax
&& (!method.decorators.is_empty()
|| method.value.params.items.iter().any(|param| !param.decorators.is_empty()));
let metadata = is_decorated.then(|| {
let (design_type, return_type) = if method.kind.is_get() {
(self.serialize_type_annotation(method.value.return_type.as_ref(), ctx), None)
} else if method.kind.is_set()
&& let Some(param) = method.value.params.items.first()
{
(self.serialize_parameter_types_of_node(param, ctx), None)
} else {
(
Self::global_function(ctx),
Some(self.serialize_return_type_of_node(&method.value, ctx)),
)
};
let param_types = self.serialize_parameters_types_of_node(&method.value.params, ctx);
MethodMetadata {
r#type: self.create_metadata("design:type", design_type, ctx),
param_types: self.create_metadata("design:paramtypes", param_types, ctx),
return_type: return_type.map(|t| self.create_metadata("design:returntype", t, ctx)),
}
});
self.method_metadata_stack.push(metadata);
}
#[inline]
fn enter_property_definition(
&mut self,
prop: &mut PropertyDefinition<'a>,
ctx: &mut TraverseCtx<'a>,
) {
if prop.decorators.is_empty() {
return;
}
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 collect_enum_type(&mut self, decl: &TSEnumDeclaration<'a>, ctx: &TraverseCtx<'a>) {
let symbol_id = decl.id.symbol_id();
let has_type_reference =
ctx.scoping().get_resolved_references(symbol_id).any(Reference::is_type);
if has_type_reference {
let enum_type = Self::infer_enum_type(&decl.body.members);
self.enum_types.insert(symbol_id, enum_type);
}
}
fn infer_enum_type(members: &[TSEnumMember<'a>]) -> EnumType {
let mut enum_type = EnumType::Object;
for member in members {
if let Some(init) = &member.initializer {
match init {
Expression::StringLiteral(_) | Expression::TemplateLiteral(_)
if enum_type != EnumType::Number =>
{
enum_type = EnumType::String;
}
Expression::NumericLiteral(_) | Expression::UnaryExpression(_)
if enum_type != EnumType::String =>
{
enum_type = EnumType::Number;
}
_ => return EnumType::Object,
}
} else {
if enum_type == EnumType::String {
return EnumType::Object;
}
enum_type = EnumType::Number;
}
}
enum_type
}
pub fn pop_method_metadata(&mut self) -> Option<MethodMetadata<'a>> {
self.method_metadata_stack.pop()
}
pub fn pop_constructor_metadata(&mut self) -> Option<Expression<'a>> {
self.constructor_metadata_stack.pop()
}
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)
}
}
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(), true, ctx)
}
TSType::TSUnionType(t) => {
self.serialize_union_or_intersection_constituents(t.types.iter(), 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(_)
| TSType::TSTypeQuery(_) | TSType::TSIndexedAccessType(_) | TSType::TSMappedType(_)
| TSType::TSTypeLiteral(_) | TSType::TSAnyKeyword(_) | TSType::TSUnknownKeyword(_)
| TSType::TSThisType(_) | TSType::TSImportType(_) | TSType::TSTypeOperatorType(_)
| TSType::TSInferType(_) | TSType::TSIntrinsicKeyword(_) | TSType::TSNamedTupleMember(_)
| TSType::JSDocUnknownType(_) => Self::global_object(ctx),
}
}
fn serialize_parameters_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| {
ArrayExpressionElement::from(self.serialize_parameter_types_of_node(param, ctx))
}));
if let Some(rest) = ¶ms.rest {
elements.push(ArrayExpressionElement::from(
self.serialize_type_annotation(rest.type_annotation.as_ref(), ctx),
));
}
ctx.ast.expression_array(SPAN, elements)
}
fn serialize_parameter_types_of_node(
&mut self,
param: &FormalParameter<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
let type_annotation = param.type_annotation.as_ref();
self.serialize_type_annotation(type_annotation, ctx)
}
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)
}
}
fn serialize_type_reference_node(
&mut self,
name: &TSTypeName<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
if let TSTypeName::IdentifierReference(ident) = name {
let symbol_id = ctx.scoping().get_reference(ident.reference_id()).symbol_id();
if let Some(symbol_id) = symbol_id
&& let Some(enum_type) = self.enum_types.get(&symbol_id)
{
return match enum_type {
EnumType::String => Self::global_string(ctx),
EnumType::Number => Self::global_number(ctx),
EnumType::Object => Self::global_object(ctx),
};
}
}
let Some(serialized_type) = self.serialize_entity_name_as_expression_fallback(name, ctx)
else {
return Self::global_object(ctx);
};
let binding = VarDeclarationsStore::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)
}
#[expect(clippy::self_only_used_in_recursion)]
fn serialize_entity_name_as_expression_fallback(
&mut self,
name: &TSTypeName<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Option<Expression<'a>> {
match name {
TSTypeName::IdentifierReference(ident) => {
let binding = MaybeBoundIdentifier::from_identifier_reference(ident, ctx);
if Self::is_type_symbol(binding.symbol_id, ctx) {
return None;
}
let flags = Self::get_reference_flags(&binding, ctx);
let ident1 = binding.create_expression(flags, ctx);
let ident2 = binding.create_expression(flags, ctx);
Some(Self::create_checked_value(ident1, ident2, ctx))
}
TSTypeName::QualifiedName(qualified) => {
if let TSTypeName::IdentifierReference(ident) = &qualified.left {
let binding = MaybeBoundIdentifier::from_identifier_reference(ident, ctx);
if Self::is_type_symbol(binding.symbol_id, ctx) {
return None;
}
let flags = Self::get_reference_flags(&binding, ctx);
let ident1 = binding.create_expression(flags, ctx);
let ident2 = binding.create_expression(flags, ctx);
let member = create_property_access(SPAN, ident1, &qualified.right.name, ctx);
Some(Self::create_checked_value(ident2, member, ctx))
} else {
let mut left =
self.serialize_entity_name_as_expression_fallback(&qualified.left, ctx)?;
let binding = VarDeclarationsStore::create_uid_var_based_on_node(&left, ctx);
let Expression::LogicalExpression(logical) = &mut left else { unreachable!() };
let right = logical.right.take_in(ctx.ast);
let right = ctx.ast.expression_assignment(
SPAN,
AssignmentOperator::Assign,
binding.create_write_target(ctx),
right,
);
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(SPAN, object, &qualified.right.name, ctx);
Some(ctx.ast.expression_logical(SPAN, left, LogicalOperator::And, member))
}
}
TSTypeName::ThisExpression(_) => None,
}
}
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),
_ => unreachable!(),
},
}
}
fn serialize_union_or_intersection_constituents<'t>(
&mut self,
types: impl Iterator<Item = &'t TSType<'a>>,
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 {
return ctx.ast.void_0(SPAN);
}
continue;
}
TSType::TSUnknownKeyword(_) => {
if !is_intersection {
return Self::global_object(ctx);
}
continue;
}
TSType::TSAnyKeyword(_) => {
return Self::global_object(ctx);
}
TSType::TSTypeReference(_) => 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")
{
return serialized_constituent;
}
if let Some(serialized_type) = &serialized_type {
if !Self::equate_serialized_type_nodes(serialized_type, &serialized_constituent) {
return Self::global_object(ctx);
}
} else {
serialized_type = Some(serialized_constituent);
}
}
serialized_type.unwrap_or_else(|| {
ctx.ast.void_0(SPAN)
})
}
#[inline]
fn equate_serialized_type_nodes(a: &Expression<'a>, b: &Expression<'a>) -> bool {
a.content_eq(b)
}
#[inline]
fn is_type_symbol(symbol_id: Option<oxc_semantic::SymbolId>, ctx: &TraverseCtx<'a>) -> bool {
symbol_id.is_some_and(|symbol_id| ctx.scoping().symbol_flags(symbol_id).is_type())
}
fn get_reference_flags(
binding: &MaybeBoundIdentifier<'a>,
ctx: &TraverseCtx<'a>,
) -> ReferenceFlags {
if let Some(symbol_id) = binding.symbol_id {
debug_assert!(ctx.scoping().symbol_flags(symbol_id).is_value());
ReferenceFlags::Read
} else {
ReferenceFlags::Type | ReferenceFlags::Read
}
}
#[inline]
fn create_global_identifier(ident: &'static str, ctx: &mut TraverseCtx<'a>) -> Expression<'a> {
ctx.create_unbound_ident_expr(SPAN, ctx.ast.ident(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)
}
fn create_checked_value(
left: Expression<'a>,
right: Expression<'a>,
ctx: &TraverseCtx<'a>,
) -> Expression<'a> {
let operator = BinaryOperator::StrictInequality;
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)
}
#[expect(clippy::unused_self)]
fn create_metadata(
&self,
key: &'a str,
value: Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
let arguments = ctx.ast.vec_from_array([
Argument::from(ctx.ast.expression_string_literal(SPAN, key, None)),
Argument::from(value),
]);
helper_call_expr(Helper::DecorateMetadata, arguments, ctx)
}
fn create_metadata_decorate(
&self,
key: &'a str,
value: Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Decorator<'a> {
ctx.ast.decorator(SPAN, self.create_metadata(key, value, ctx))
}
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)
}
}