use oxc_allocator::Box as ArenaBox;
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, context::TraverseCtx,
decorator::DecoratorOptions, 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>,
strict_null_checks: bool,
}
impl LegacyDecoratorMetadata<'_> {
pub fn new(options: DecoratorOptions) -> Self {
LegacyDecoratorMetadata {
method_metadata_stack: SparseStack::new(),
constructor_metadata_stack: SparseStack::new(),
enum_types: FxHashMap::default(),
strict_null_checks: options.strict_null_checks,
}
}
}
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(
&self,
name: &TSTypeName<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
let Some((root_ident, properties)) = Self::decompose_entity_name(name) else {
return Self::global_object(ctx);
};
let symbol_id = ctx.scoping().get_reference(root_ident.reference_id()).symbol_id();
if properties.is_empty()
&& 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),
};
}
if properties.is_empty() && root_ident.name == "ReadonlyArray" && symbol_id.is_none() {
return Self::global_array(ctx);
}
if Self::is_type_symbol(symbol_id, ctx) {
return Self::global_object(ctx);
}
Self::build_undefined_guard(root_ident, &properties, ctx)
}
fn decompose_entity_name<'b>(
name: &'b TSTypeName<'a>,
) -> Option<(&'b IdentifierReference<'a>, Vec<&'b str>)> {
let mut properties: Vec<&str> = vec![];
let mut current = name;
loop {
match current {
TSTypeName::IdentifierReference(ident) => {
properties.reverse();
return Some((ident, properties));
}
TSTypeName::QualifiedName(q) => {
properties.push(q.right.name.as_str());
current = &q.left;
}
TSTypeName::ThisExpression(_) => return None,
}
}
}
fn build_undefined_guard(
root: &IdentifierReference<'a>,
properties: &[&str],
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
let binding = MaybeBoundIdentifier::from_identifier_reference(root, ctx);
let ref_flags = Self::get_reference_flags(&binding, ctx);
let root_expr = binding.create_expression(ref_flags, ctx);
let mut test = Self::typeof_undefined(root_expr, ctx);
for i in 1..=properties.len() {
let prefix = Self::build_path(&binding, ref_flags, &properties[..i], ctx);
let next = Self::typeof_undefined(prefix, ctx);
test = ctx.ast.expression_logical(SPAN, test, LogicalOperator::Or, next);
}
let alternate = Self::build_path(&binding, ref_flags, properties, ctx);
ctx.ast.expression_conditional(SPAN, test, Self::global_object(ctx), alternate)
}
fn build_path(
binding: &MaybeBoundIdentifier<'a>,
ref_flags: ReferenceFlags,
properties: &[&str],
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
let mut expr = binding.create_expression(ref_flags, ctx);
for prop in properties {
expr = create_property_access(SPAN, expr, prop, ctx);
}
expr
}
fn typeof_undefined(expr: Expression<'a>, ctx: &TraverseCtx<'a>) -> Expression<'a> {
let typeof_expr = ctx.ast.expression_unary(SPAN, UnaryOperator::Typeof, expr);
let undefined_str = ctx.ast.expression_string_literal(SPAN, "undefined", None);
ctx.ast.expression_binary(SPAN, typeof_expr, BinaryOperator::StrictEquality, undefined_str)
}
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::TSNullKeyword(_) | TSType::TSUndefinedKeyword(_)
if !is_intersection && !self.strict_null_checks =>
{
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)
}
#[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)
}
}