Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 42 additions & 36 deletions crates/oxc_angular_compiler/src/component/decorator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -680,7 +680,7 @@ fn extract_constructor_deps<'a>(
let params = &ctor.value.params;

for param in &params.items {
let dep = extract_param_dependency(param, import_map, consts);
let dep = extract_param_dependency(allocator, param, import_map, consts);
deps.push(dep);
}

Expand All @@ -703,6 +703,7 @@ fn extract_constructor_deps<'a>(
/// Only Angular's parameter decorators count, imported from `@angular/core`
/// (see [`crate::directive::angular_param_decorator`]).
fn extract_param_dependency<'a>(
allocator: &'a Allocator,
param: &'a oxc_ast::ast::FormalParameter<'a>,
import_map: &ImportMap<'a>,
consts: &StringConsts<'a>,
Expand All @@ -712,7 +713,7 @@ fn extract_param_dependency<'a>(
let mut skip_self = false;
let mut self_ = false;
let mut host = false;
let mut inject_token: Option<Ident<'a>> = None;
let mut inject_token: Option<(Ident<'a>, &'a [Ident<'a>])> = None;
let mut inject_token_is_string = false;
let mut attribute_name: Option<Ident<'a>> = None;

Expand All @@ -728,7 +729,9 @@ fn extract_param_dependency<'a>(
let string_token =
arg.as_expression().and_then(extract_string_inject_token);
inject_token_is_string = string_token.is_some();
inject_token = string_token.or_else(|| extract_inject_token(arg));
inject_token = string_token
.map(|token| (token, &[][..]))
.or_else(|| extract_inject_token(allocator, arg));
}
}
}
Expand All @@ -754,7 +757,9 @@ fn extract_param_dependency<'a>(
// Determine the token:
// 1. If @Inject(TOKEN) is present, use TOKEN
// 2. Otherwise, use the type annotation
let token = inject_token.or_else(|| extract_param_token(param));
let (token, member_path) = inject_token
.or_else(|| extract_param_token(allocator, param))
.map_or((None, &[][..]), |(t, m)| (Some(t), m));

// Handle @Attribute decorator
if let Some(attr_name) = attribute_name {
Expand Down Expand Up @@ -785,6 +790,9 @@ fn extract_param_dependency<'a>(
} else {
let mut d = R3DependencyMetadata::new(token_name.clone());
d.token_source_module = Some(import_info.source_module.clone());
d.token_member_path = member_path;
d.token_is_namespace_import =
!member_path.is_empty() && !import_info.is_named_import;
// Carry the exported name so namespaced refs (`i1.X`) use the export
// name, not the local alias, for aliased imports.
d.token_imported_name = import_info.imported_name.clone();
Expand All @@ -795,7 +803,9 @@ fn extract_param_dependency<'a>(
d
}
} else {
R3DependencyMetadata::new(token_name.clone())
let mut d = R3DependencyMetadata::new(*token_name);
d.token_member_path = member_path;
d
}
}
None => R3DependencyMetadata::invalid(),
Expand All @@ -821,44 +831,40 @@ fn extract_string_inject_token<'a>(expr: &'a Expression<'a>) -> Option<Ident<'a>
}
}

/// Extract the injection token from an @Inject decorator argument.
fn extract_inject_token<'a>(arg: &'a Argument<'a>) -> Option<Ident<'a>> {
match arg {
Argument::Identifier(id) => Some(id.name.clone().into()),
_ => {
// For other expressions, try to get the expression form
let expr = arg.to_expression();
match expr {
Expression::Identifier(id) => Some(id.name.clone().into()),
_ => None,
}
}
}
/// Extract the injection token from an @Inject decorator argument: its leftmost
/// name and the members read off it (`TOKEN.a.b` → `TOKEN`, `[a, b]`).
fn extract_inject_token<'a>(
allocator: &'a Allocator,
arg: &'a Argument<'a>,
) -> Option<(Ident<'a>, &'a [Ident<'a>])> {
let mut members = std::vec::Vec::new();
let mut expr = arg.as_expression()?;
while let Expression::StaticMemberExpression(member) = expr {
members.push(member.property.name);
expr = &member.object;
}
let Expression::Identifier(id) = expr else { return None };
members.reverse();
Some((id.name, allocator.alloc_slice_copy(&members)))
}

/// Extract the injection token from a parameter's type annotation.
fn extract_param_token<'a>(param: &'a oxc_ast::ast::FormalParameter<'a>) -> Option<Ident<'a>> {
/// Extract the injection token from a parameter's type annotation: its
/// leftmost name and the members read off it (`ns.one.One` → `ns`, `[one, One]`).
fn extract_param_token<'a>(
allocator: &'a Allocator,
param: &'a oxc_ast::ast::FormalParameter<'a>,
) -> Option<(Ident<'a>, &'a [Ident<'a>])> {
// Get the type annotation (directly on FormalParameter)
let type_annotation = param.type_annotation.as_ref()?;
// Narrow `T | null` unions to `T` to match the reference compiler.
let ts_type = crate::util::resolve_di_token_type(&type_annotation.type_annotation)?;

// Handle TSTypeReference: SomeClass, SomeModule, etc.
if let oxc_ast::ast::TSType::TSTypeReference(type_ref) = ts_type {
// Get the type name
let type_name = match &type_ref.type_name {
oxc_ast::ast::TSTypeName::IdentifierReference(id) => Some(id.name.clone().into()),
oxc_ast::ast::TSTypeName::QualifiedName(_)
| oxc_ast::ast::TSTypeName::ThisExpression(_) => {
// Qualified names like Namespace.Type or 'this' type - not valid injection tokens
None
}
};
return type_name;
}

// For primitive types or other patterns, return None (invalid dependency)
None
// Handle TSTypeReference: `SomeClass`, or a qualified `ns.SomeClass`.
// For primitive types, `this` or other patterns, return None (invalid dependency).
let oxc_ast::ast::TSType::TSTypeReference(type_ref) = ts_type else { return None };
let path = crate::class_metadata::entity_path(&type_ref.type_name)?;
let (root, members) = path.split_first()?;
Some((*root, allocator.alloc_slice_copy(members)))
}

// =============================================================================
Expand Down
51 changes: 37 additions & 14 deletions crates/oxc_angular_compiler/src/component/dependency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ pub struct R3DependencyMetadata<'a> {
/// alias, or it resolves to `undefined` at runtime.
pub token_imported_name: Option<Ident<'a>>,

/// The members read off `token` for a qualified type (`ns.one.One` → token
/// `ns`, members `[one, One]`). Empty for a plain name.
pub token_member_path: &'a [Ident<'a>],

/// Whether `token` is a namespace import (`import * as ns`) whose members
/// are read: the emitted namespace import (`i1`) then stands for it.
pub token_is_namespace_import: bool,

/// For `@Attribute()` dependencies, the attribute name.
/// `None` for regular dependencies.
pub attribute_name: Option<Ident<'a>>,
Expand Down Expand Up @@ -88,6 +96,8 @@ impl<'a> R3DependencyMetadata<'a> {
token_source_module: None,
has_named_import: false,
token_imported_name: None,
token_member_path: &[],
token_is_namespace_import: false,
attribute_name: None,
host: false,
optional: false,
Expand All @@ -105,6 +115,8 @@ impl<'a> R3DependencyMetadata<'a> {
token_source_module: None,
has_named_import: false,
token_imported_name: None,
token_member_path: &[],
token_is_namespace_import: false,
attribute_name: None,
host: false,
optional: false,
Expand All @@ -125,6 +137,8 @@ impl<'a> R3DependencyMetadata<'a> {
token_source_module: None,
has_named_import: false,
token_imported_name: None,
token_member_path: &[],
token_is_namespace_import: false,
attribute_name: None,
host: false,
optional: false,
Expand Down Expand Up @@ -166,6 +180,8 @@ impl<'a> R3DependencyMetadata<'a> {
token_source_module: None,
has_named_import: false,
token_imported_name: None,
token_member_path: &[],
token_is_namespace_import: false,
attribute_name: Some(attribute_name),
host: false,
optional: false,
Expand Down Expand Up @@ -374,7 +390,7 @@ fn compile_inject_dependency<'a>(
///
/// This mimics Angular's import reuse behavior where existing named imports
/// are reused rather than generating new namespace imports.
fn create_token_expression<'a>(
pub fn create_token_expression<'a>(
allocator: &'a Allocator,
dep: &R3DependencyMetadata<'a>,
token_name: &Ident<'a>,
Expand All @@ -393,36 +409,43 @@ fn create_token_expression<'a>(
// Reuse existing named import - use bare token name
// This matches Angular's behavior: import { DIALOG_DATA } from "@angular/cdk/dialog"
// allows using `DIALOG_DATA` directly instead of `i1.DIALOG_DATA`
return OutputExpression::ReadVar(Box::new_in(
let token = OutputExpression::ReadVar(Box::new_in(
ReadVarExpr { name: token_name.clone(), source_span: None },
&allocator,
));
return crate::util::read_member_path(allocator, token, dep.token_member_path);
}

// Imported dependency without named import - use namespace.TokenName (e.g., i1.AuthService)
let namespace = namespace_registry.get_or_assign(source_module);
OutputExpression::ReadProp(Box::new_in(
let namespace = OutputExpression::ReadVar(Box::new_in(
ReadVarExpr {
name: namespace_registry.get_or_assign(source_module),
source_span: None,
},
&allocator,
));
if dep.token_is_namespace_import {
// `ns.one.One` for `import * as ns` → `i1.one.One`
return crate::util::read_member_path(allocator, namespace, dep.token_member_path);
}
let token = OutputExpression::ReadProp(Box::new_in(
ReadPropExpr {
receiver: Box::new_in(
OutputExpression::ReadVar(Box::new_in(
ReadVarExpr { name: namespace, source_span: None },
&allocator,
)),
&allocator,
),
receiver: Box::new_in(namespace, &allocator),
// Export name when aliased (`Foo as Bar` → `i1.Foo`).
name: dep.token_imported_name.clone().unwrap_or_else(|| token_name.clone()),
optional: false,
source_span: None,
},
&allocator,
))
));
crate::util::read_member_path(allocator, token, dep.token_member_path)
} else {
// Local dependency - use bare token name
OutputExpression::ReadVar(Box::new_in(
let token = OutputExpression::ReadVar(Box::new_in(
ReadVarExpr { name: token_name.clone(), source_span: None },
&allocator,
))
));
crate::util::read_member_path(allocator, token, dep.token_member_path)
}
}

Expand Down
30 changes: 25 additions & 5 deletions crates/oxc_angular_compiler/src/component/import_elision.rs
Original file line number Diff line number Diff line change
Expand Up @@ -555,9 +555,9 @@ impl<'a> ImportElisionAnalyzer<'a> {
/// class decorator, see [`crate::directive::angular_class_decorator`]):
///
/// - an Angular constructor parameter decorator of the first constructor (the one
/// `collect_constructor_decorator_spans` strips and the factory reads), with the argument of `@Inject(X)`
/// when it is a bare identifier: the factory names `X` through a namespace import (`i1.X`), while any other
/// argument (`TOKEN.sub`, `forwardRef(() => TOKEN)`) is copied as written
/// `collect_constructor_decorator_spans` strips and the factory reads), with the root of `@Inject(X)` /
/// `@Inject(X.sub)`: the factory names `X` through a namespace import (`i1.X.sub`), while any other
/// argument (`TOKEN['sub']`, `forwardRef(() => TOKEN)`) is copied as written
/// - the Angular member decorator itself on a `declare` property, not its arguments: a query predicate
/// (`viewQuery(Foo)`) or an input `transform` is compiled into the definition by its local name
///
Expand Down Expand Up @@ -586,6 +586,23 @@ impl<'a> ImportElisionAnalyzer<'a> {
}
}
}
/// The name a token resolves through: `TOKEN` in `TOKEN` or `TOKEN.a?.b`, like
/// `resolve_factory_dep_namespaces` does. `(TOKEN).a` or `TOKEN['a']` is copied as written.
fn member_chain_root<'b, 'a>(
expression: &'b Expression<'a>,
) -> Option<&'b oxc_ast::ast::IdentifierReference<'a>> {
match expression {
Expression::Identifier(id) => Some(id),
Expression::StaticMemberExpression(member) => member_chain_root(&member.object),
Expression::ChainExpression(chain) => match &chain.expression {
oxc_ast::ast::ChainElement::StaticMemberExpression(member) => {
member_chain_root(&member.object)
}
_ => None,
},
_ => None,
}
}
let mut references = References::default();
for stmt in &program.body {
let class = match stmt {
Expand Down Expand Up @@ -624,8 +641,11 @@ impl<'a> ImportElisionAnalyzer<'a> {
references.visit_callee(decorator);
if name == "Inject"
&& let Expression::CallExpression(call) = &decorator.expression
&& let Some(oxc_ast::ast::Argument::Identifier(token)) =
call.arguments.first()
&& let Some(token) = call
.arguments
.first()
.and_then(oxc_ast::ast::Argument::as_expression)
.and_then(member_chain_root)
{
references.visit_identifier_reference(token);
}
Expand Down
1 change: 1 addition & 0 deletions crates/oxc_angular_compiler/src/component/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub(crate) use decorator::find_component_decorator;
pub use definition::{
ComponentDefinitions, const_value_to_expression, generate_component_definitions,
};
pub(crate) use dependency::create_token_expression;
pub use dependency::{
FactoryTarget, InjectFlags, R3DependencyMetadata, compile_inject_dependencies,
generate_factory_di_args, get_inject_fn_name,
Expand Down
Loading
Loading