diff --git a/crates/oxc_angular_compiler/src/component/decorator.rs b/crates/oxc_angular_compiler/src/component/decorator.rs index 2a617d1ea..525bb7eef 100644 --- a/crates/oxc_angular_compiler/src/component/decorator.rs +++ b/crates/oxc_angular_compiler/src/component/decorator.rs @@ -680,7 +680,7 @@ fn extract_constructor_deps<'a>( let params = &ctor.value.params; for param in ¶ms.items { - let dep = extract_param_dependency(param, import_map, consts); + let dep = extract_param_dependency(allocator, param, import_map, consts); deps.push(dep); } @@ -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>, @@ -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> = None; + let mut inject_token: Option<(Ident<'a>, &'a [Ident<'a>])> = None; let mut inject_token_is_string = false; let mut attribute_name: Option> = None; @@ -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)); } } } @@ -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 { @@ -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(); @@ -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(), @@ -821,44 +831,40 @@ fn extract_string_inject_token<'a>(expr: &'a Expression<'a>) -> Option } } -/// Extract the injection token from an @Inject decorator argument. -fn extract_inject_token<'a>(arg: &'a Argument<'a>) -> Option> { - 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> { +/// 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))) } // ============================================================================= diff --git a/crates/oxc_angular_compiler/src/component/dependency.rs b/crates/oxc_angular_compiler/src/component/dependency.rs index e247bcbae..5976390b8 100644 --- a/crates/oxc_angular_compiler/src/component/dependency.rs +++ b/crates/oxc_angular_compiler/src/component/dependency.rs @@ -51,6 +51,14 @@ pub struct R3DependencyMetadata<'a> { /// alias, or it resolves to `undefined` at runtime. pub token_imported_name: Option>, + /// 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>, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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>, @@ -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) } } diff --git a/crates/oxc_angular_compiler/src/component/import_elision.rs b/crates/oxc_angular_compiler/src/component/import_elision.rs index 07f7e7566..8d17e63c2 100644 --- a/crates/oxc_angular_compiler/src/component/import_elision.rs +++ b/crates/oxc_angular_compiler/src/component/import_elision.rs @@ -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 /// @@ -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 { @@ -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); } diff --git a/crates/oxc_angular_compiler/src/component/mod.rs b/crates/oxc_angular_compiler/src/component/mod.rs index 2ca029c04..5f2202b8d 100644 --- a/crates/oxc_angular_compiler/src/component/mod.rs +++ b/crates/oxc_angular_compiler/src/component/mod.rs @@ -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, diff --git a/crates/oxc_angular_compiler/src/component/transform.rs b/crates/oxc_angular_compiler/src/component/transform.rs index 9606e4c4b..178013e33 100644 --- a/crates/oxc_angular_compiler/src/component/transform.rs +++ b/crates/oxc_angular_compiler/src/component/transform.rs @@ -708,6 +708,9 @@ fn module_export_name_to_str<'a>(name: &ModuleExportName<'a>) -> Option<&'a str> /// /// This function replaces bare `ReadVar` tokens with namespace-prefixed `ReadProp` references /// (e.g., `Store` → `i1.Store`) for any token that has a corresponding import in the import map. +/// A member chain is resolved through its leftmost name, like ngtsc's `typeToValue`: a +/// qualified type `ns.one.One` reads off the namespace import (`i1.one.One`), and +/// `mod.Foo` off the named import (`i1.mod.Foo`). /// This ensures the factory works correctly even after import elision. fn resolve_factory_dep_namespaces<'a>( allocator: &'a Allocator, @@ -716,12 +719,14 @@ fn resolve_factory_dep_namespaces<'a>( namespace_registry: &mut NamespaceRegistry<'a>, ) { for dep in deps.iter_mut() { - let Some(ref token) = dep.token else { continue }; + let Some(token) = dep.token.as_mut() else { continue }; + let is_member_chain = matches!(token, OutputExpression::ReadProp(_)); + let root = member_chain_root(token); // Only process bare variable references (ReadVar) - let OutputExpression::ReadVar(var) = token else { continue }; - let name = &var.name; + let OutputExpression::ReadVar(var) = root else { continue }; + let name = var.name; // Look up this identifier in the import map - let Some(import_info) = import_map.get(name) else { continue }; + let Some(import_info) = import_map.get(&name) else { continue }; if import_info.is_type_only { // Type-only imports (`import type { X }` / `import { type X }`) are erased // at runtime, so they cannot be used as DI tokens. Flag the dep so the @@ -733,23 +738,36 @@ fn resolve_factory_dep_namespaces<'a>( continue; } // Replace with namespace-prefixed reference: i1.Store instead of Store - let namespace = namespace_registry.get_or_assign(&import_info.source_module); - dep.token = Some(OutputExpression::ReadProp(oxc_allocator::Box::new_in( - ReadPropExpr { - receiver: oxc_allocator::Box::new_in( - OutputExpression::ReadVar(oxc_allocator::Box::new_in( - ReadVarExpr { name: namespace, source_span: None }, - &allocator, - )), - &allocator, - ), - // Export name when aliased (`Foo as Bar` → `i1.Foo`). - name: import_info.imported_name.clone().unwrap_or_else(|| name.clone()), - optional: false, + let namespace = OutputExpression::ReadVar(oxc_allocator::Box::new_in( + ReadVarExpr { + name: namespace_registry.get_or_assign(&import_info.source_module), source_span: None, }, &allocator, - ))); + )); + *root = if is_member_chain && !import_info.is_named_import { + // `ns.X` for `import * as ns`: the emitted namespace import is `ns` itself. + namespace + } else { + OutputExpression::ReadProp(oxc_allocator::Box::new_in( + ReadPropExpr { + receiver: oxc_allocator::Box::new_in(namespace, &allocator), + // Export name when aliased (`Foo as Bar` → `i1.Foo`). + name: import_info.imported_name.unwrap_or(name), + optional: false, + source_span: None, + }, + &allocator, + )) + }; + } +} + +/// The leftmost expression of a property-read chain: `ns` in `ns.one.One`. +fn member_chain_root<'b, 'a>(expr: &'b mut OutputExpression<'a>) -> &'b mut OutputExpression<'a> { + match expr { + OutputExpression::ReadProp(prop) => member_chain_root(&mut prop.receiver), + other => other, } } @@ -5302,6 +5320,7 @@ fn compile_component_partial<'a>( view_queries: &[R3QueryMetadata<'a>], content_queries: &[R3QueryMetadata<'a>], pool_starting_index: u32, + namespace_registry: &mut NamespaceRegistry<'a>, ) -> FullCompilationResult { let inputs = crate::partial::PartialComponentInputs { template, @@ -5319,6 +5338,7 @@ fn compile_component_partial<'a>( allocator, core_namespace, metadata, + namespace_registry, ); // Detect `@defer` block presence by a cheap string scan — partial @@ -5523,6 +5543,7 @@ fn compile_component_full<'a>( &view_queries, &content_queries, pool_starting_index, + namespace_registry, )); } diff --git a/crates/oxc_angular_compiler/src/directive/decorator.rs b/crates/oxc_angular_compiler/src/directive/decorator.rs index c1a861c2d..490b7d69e 100644 --- a/crates/oxc_angular_compiler/src/directive/decorator.rs +++ b/crates/oxc_angular_compiler/src/directive/decorator.rs @@ -22,7 +22,7 @@ use super::metadata::{ }; use super::property_decorators::try_unwrap_forward_ref; use crate::factory::R3DependencyMetadata; -use crate::output::ast::{OutputAstBuilder, OutputExpression, ReadVarExpr}; +use crate::output::ast::{OutputAstBuilder, OutputExpression}; use crate::output::oxc_converter::convert_oxc_expression; use crate::r3::CORE; use crate::util::is_metadata_property; @@ -433,22 +433,9 @@ fn extract_param_token<'a>( // 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. + // Handle TSTypeReference: `SomeClass`, or a qualified `ns.SomeClass`. 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) => 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 - return None; - } - }; - - return Some(OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: type_name, source_span: None }, - &allocator, - ))); + return crate::util::type_name_to_value_expr(allocator, &type_ref.type_name); } // For primitive types or other patterns, return None (invalid dependency) diff --git a/crates/oxc_angular_compiler/src/injectable/decorator.rs b/crates/oxc_angular_compiler/src/injectable/decorator.rs index 2b552f937..78bc38309 100644 --- a/crates/oxc_angular_compiler/src/injectable/decorator.rs +++ b/crates/oxc_angular_compiler/src/injectable/decorator.rs @@ -692,22 +692,9 @@ fn extract_param_token<'a>( // 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. + // Handle TSTypeReference: `SomeClass`, or a qualified `ns.SomeClass`. if let oxc_ast::ast::TSType::TSTypeReference(type_ref) = ts_type { - // Get the type name - let type_name: Ident<'a> = match &type_ref.type_name { - oxc_ast::ast::TSTypeName::IdentifierReference(id) => 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 - return None; - } - }; - - return Some(OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: type_name, source_span: None }, - &allocator, - ))); + return crate::util::type_name_to_value_expr(allocator, &type_ref.type_name); } // For primitive types or other patterns, return None (invalid dependency) diff --git a/crates/oxc_angular_compiler/src/ng_module/decorator.rs b/crates/oxc_angular_compiler/src/ng_module/decorator.rs index fe46fc0cc..326d470b1 100644 --- a/crates/oxc_angular_compiler/src/ng_module/decorator.rs +++ b/crates/oxc_angular_compiler/src/ng_module/decorator.rs @@ -625,22 +625,9 @@ fn extract_param_token<'a>( // 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. + // Handle TSTypeReference: `SomeClass`, or a qualified `ns.SomeClass`. if let oxc_ast::ast::TSType::TSTypeReference(type_ref) = ts_type { - // Get the type name - let type_name: Ident<'a> = match &type_ref.type_name { - oxc_ast::ast::TSTypeName::IdentifierReference(id) => 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 - return None; - } - }; - - return Some(OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: type_name, source_span: None }, - &allocator, - ))); + return crate::util::type_name_to_value_expr(allocator, &type_ref.type_name); } // For primitive types or other patterns, return None (invalid dependency) diff --git a/crates/oxc_angular_compiler/src/partial/component.rs b/crates/oxc_angular_compiler/src/partial/component.rs index a703e069d..2ac6891bf 100644 --- a/crates/oxc_angular_compiler/src/partial/component.rs +++ b/crates/oxc_angular_compiler/src/partial/component.rs @@ -39,7 +39,7 @@ use super::{PLACEHOLDER_VERSION, wrap_forward_ref}; use crate::component::R3DependencyMetadata as ComponentDep; use crate::component::{ ChangeDetectionStrategy, ComponentMetadata, DeclarationListEmitMode, HostDirectiveMetadata, - HostMetadata, TemplateDependency, TemplateDependencyKind, ViewEncapsulation, + HostMetadata, NamespaceRegistry, TemplateDependency, TemplateDependencyKind, ViewEncapsulation, }; use crate::directive::{R3InputMetadata, R3QueryMetadata}; use crate::factory::{ @@ -363,10 +363,14 @@ pub fn compile_declare_component_from_metadata<'a>( } /// Builds the partial ɵfac factory paired with a Component. +/// +/// Tokens are built as in full mode (`i1.Zero`, `i1.one.One`): import elision drops +/// an import used only as a constructor parameter type. pub fn compile_declare_factory_for_component<'a>( allocator: &'a Allocator, core_namespace: &'a str, meta: &ComponentMetadata<'a>, + namespace_registry: &mut NamespaceRegistry<'a>, ) -> OutputExpression<'a> { let type_expr = OutputExpression::ReadVar(Box::new_in( ReadVarExpr { name: meta.class_name.clone(), source_span: None }, @@ -378,7 +382,12 @@ pub fn compile_declare_factory_for_component<'a>( type_expr: type_expr.clone_in(allocator), type_decl: type_expr, type_argument_count: 0, - deps: clone_factory_deps(allocator, &meta.constructor_deps, meta.uses_inheritance), + deps: clone_factory_deps( + allocator, + &meta.constructor_deps, + meta.uses_inheritance, + namespace_registry, + ), target: FactoryTarget::Component, }); compile_declare_factory_function(allocator, core_namespace, &factory_meta) @@ -827,27 +836,14 @@ fn clone_factory_deps<'a>( allocator: &'a Allocator, deps: &Option>>, uses_inheritance: bool, + namespace_registry: &mut NamespaceRegistry<'a>, ) -> R3FactoryDeps<'a> { match deps { Some(deps) => { let mut out = Vec::with_capacity_in(deps.len(), &allocator); for dep in deps { let token_expr = dep.token.as_ref().map(|t| { - if dep.token_is_string { - // `@Inject('TOKEN')` injects the string itself - OutputExpression::Literal(Box::new_in( - LiteralExpr { - value: LiteralValue::String(t.clone()), - source_span: None, - }, - &allocator, - )) - } else { - OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: t.clone(), source_span: None }, - &allocator, - )) - } + crate::component::create_token_expression(allocator, dep, t, namespace_registry) }); let attr_expr = dep.attribute_name.as_ref().map(|a| { OutputExpression::Literal(Box::new_in( diff --git a/crates/oxc_angular_compiler/src/pipe/decorator.rs b/crates/oxc_angular_compiler/src/pipe/decorator.rs index 513f28846..d75ed95cb 100644 --- a/crates/oxc_angular_compiler/src/pipe/decorator.rs +++ b/crates/oxc_angular_compiler/src/pipe/decorator.rs @@ -390,23 +390,9 @@ fn extract_param_token<'a>( // 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. + // Handle TSTypeReference: `SomeClass`, or a qualified `ns.SomeClass`. if let oxc_ast::ast::TSType::TSTypeReference(type_ref) = ts_type { - // Get the type name - let type_name: Ident<'a> = match &type_ref.type_name { - oxc_ast::ast::TSTypeName::IdentifierReference(id) => 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 - return None; - } - }; - - // Return a reference to the type - return Some(OutputExpression::ReadVar(Box::new_in( - ReadVarExpr { name: type_name, source_span: None }, - &allocator, - ))); + return crate::util::type_name_to_value_expr(allocator, &type_ref.type_name); } None diff --git a/crates/oxc_angular_compiler/src/util/type_extract.rs b/crates/oxc_angular_compiler/src/util/type_extract.rs index aa5feb681..f624b75f6 100644 --- a/crates/oxc_angular_compiler/src/util/type_extract.rs +++ b/crates/oxc_angular_compiler/src/util/type_extract.rs @@ -5,7 +5,11 @@ //! `null` literal type nodes out of a union before resolving the remaining type //! as an injection token. -use oxc_ast::ast::TSType; +use oxc_allocator::{Allocator, Box}; +use oxc_ast::ast::{TSType, TSTypeName}; +use oxc_str::Ident; + +use crate::output::ast::{OutputExpression, ReadPropExpr, ReadVarExpr}; /// Resolves a TypeScript type annotation to the concrete type usable as a DI /// injection token. @@ -53,3 +57,52 @@ fn unwrap_parens<'a, 'b>(ts_type: &'b TSType<'a>) -> &'b TSType<'a> { } current } + +/// The value a DI token's type name refers to, as written: `Foo` → `Foo`, +/// `ns.one.One` → `ns.one.One`. `this` has no value: `None`. +/// +/// Mirrors `entityNameToValue` in +/// `packages/compiler-cli/src/ngtsc/reflection/src/type_to_value.ts`. +pub fn type_name_to_value_expr<'a>( + allocator: &'a Allocator, + name: &TSTypeName<'a>, +) -> Option> { + match name { + TSTypeName::IdentifierReference(id) => Some(OutputExpression::ReadVar(Box::new_in( + ReadVarExpr { name: id.name, source_span: None }, + &allocator, + ))), + TSTypeName::QualifiedName(qualified) => { + let receiver = type_name_to_value_expr(allocator, &qualified.left)?; + Some(OutputExpression::ReadProp(Box::new_in( + ReadPropExpr { + receiver: Box::new_in(receiver, &allocator), + name: qualified.right.name, + optional: false, + source_span: None, + }, + &allocator, + ))) + } + TSTypeName::ThisExpression(_) => None, + } +} + +/// Fold `members` onto `base` as property reads: `base.a.b`. +pub fn read_member_path<'a>( + allocator: &'a Allocator, + base: OutputExpression<'a>, + members: &[Ident<'a>], +) -> OutputExpression<'a> { + members.iter().fold(base, |receiver, name| { + OutputExpression::ReadProp(Box::new_in( + ReadPropExpr { + receiver: Box::new_in(receiver, &allocator), + name: *name, + optional: false, + source_span: None, + }, + &allocator, + )) + }) +} diff --git a/crates/oxc_angular_compiler/tests/import_elision_output_references_test.rs b/crates/oxc_angular_compiler/tests/import_elision_output_references_test.rs index ac8ae8955..3bb420158 100644 --- a/crates/oxc_angular_compiler/tests/import_elision_output_references_test.rs +++ b/crates/oxc_angular_compiler/tests/import_elision_output_references_test.rs @@ -84,13 +84,29 @@ export class B { constructor(@Inject(TOKEN) v: unknown) {} } } #[test] -fn inject_member_expression_keeps_object() { - // The factory emits `ɵɵinject(TOKEN.sub)` with the local name. +fn inject_member_expression_reads_off_namespace_import() { + // The factory reads `TOKEN.sub` off the namespace import (`i1.TOKEN.sub`). + for (arg, member) in [("TOKEN.sub", "i1.TOKEN.sub"), ("TOKEN?.a.b", "i1.TOKEN?.a.b")] { + let code = compile(&format!( + r#" +import {{ Injectable, Inject }} from "@angular/core"; +import {{ TOKEN }} from "./tokens"; +@Injectable() export class S {{ constructor(@Inject({arg}) private v: unknown) {{}} }} +"# + )); + assert!(!imports(&code, "TOKEN"), "{code}"); + assert!(code.contains(member), "{code}"); + } +} + +#[test] +fn inject_computed_member_keeps_object() { + // The factory copies `TOKEN['sub']` as written, with the local name. assert_kept( r#" import { Injectable, Inject } from "@angular/core"; import { TOKEN } from "./tokens"; -@Injectable() export class S { constructor(@Inject(TOKEN.sub) private v: unknown) {} } +@Injectable() export class S { constructor(@Inject(TOKEN['sub']) private v: unknown) {} } "#, &["TOKEN"], ); diff --git a/crates/oxc_angular_compiler/tests/namespaced_ctor_token_test.rs b/crates/oxc_angular_compiler/tests/namespaced_ctor_token_test.rs new file mode 100644 index 000000000..1808ffd07 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/namespaced_ctor_token_test.rs @@ -0,0 +1,171 @@ +//! A constructor parameter typed with a qualified name (`ns.Foo`, `ns.one.One`) +//! is injected through the value that name refers to, as ngtsc's `typeToValue` +//! resolves it: a namespace import's members read off the emitted namespace +//! import (`i1.Zero`, `i1.one.One`), a named import's off its export +//! (`i1.ns.Zero`), and a local namespace's as written. The cases port +//! `ngtsc_spec.ts`: "should generate correct imports for type references to +//! namespaced symbols using a namespace import" / "... using named imports". + +use oxc_allocator::Allocator; +use oxc_angular_compiler::{CompilationMode, TransformOptions, transform_angular_file}; + +/// The compiled `source`, without whitespace. +fn compile_with(source: &str, options: &TransformOptions) -> String { + let allocator = Allocator::default(); + let result = transform_angular_file(&allocator, "test.ts", source, Some(options), None); + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + result.code.chars().filter(|c| !c.is_whitespace()).collect() +} + +fn compile(source: &str) -> String { + compile_with(source, &TransformOptions::default()) +} + +fn assert_contains(code: &str, expected: &str) { + assert!(code.contains(expected), "Expected `{expected}` in:\n{code}"); +} + +#[test] +fn namespace_import_members_are_injected_off_the_namespace() { + let code = compile( + r" +import { Injectable } from '@angular/core'; +import * as ns from 'ns'; +@Injectable() +export class MyService { + constructor(zero: ns.Zero, one: ns.one.One, two: ns.one.two.Two) {} +} +", + ); + assert_contains(&code, "import*asi1from'ns';"); + assert_contains( + &code, + "(i0.ɵɵinject(i1.Zero),i0.ɵɵinject(i1.one.One),i0.ɵɵinject(i1.one.two.Two))", + ); + assert!(!code.contains("ɵɵinvalidFactoryDep"), "Got:\n{code}"); +} + +#[test] +fn named_import_members_are_injected_off_the_export() { + let code = compile( + r" +import { Injectable } from '@angular/core'; +import { ns } from 'ns'; +import { ns as alias } from 'ns'; +@Injectable() +export class MyService { + constructor( + zero: ns.Zero, + one: ns.one.One, + two: ns.one.two.Two, + aliasedZero: alias.Zero, + aliasedOne: alias.one.One, + aliasedTwo: alias.one.two.Two, + ) {} +} +", + ); + assert_contains(&code, "import*asi1from'ns';"); + assert_contains( + &code, + "i0.ɵɵinject(i1.ns.Zero),i0.ɵɵinject(i1.ns.one.One),i0.ɵɵinject(i1.ns.one.two.Two),\ + i0.ɵɵinject(i1.ns.Zero),i0.ɵɵinject(i1.ns.one.One),i0.ɵɵinject(i1.ns.one.two.Two)", + ); +} + +#[test] +fn local_namespace_members_are_injected_as_written() { + let code = compile( + r" +import { Injectable } from '@angular/core'; +namespace local { export class L {} } +@Injectable() +export class MyService { + constructor(l: local.L) {} +} +", + ); + assert_contains(&code, "(i0.ɵɵinject(local.L))"); +} + +/// Every decorator compiles its factory deps the same way. +#[test] +fn every_decorator_injects_qualified_types() { + for (decorator, inject) in [ + ("Component({ selector: 'a-cmp', template: '' })", "ɵɵdirectiveInject"), + ("Directive({ selector: '[aDir]' })", "ɵɵdirectiveInject"), + ("NgModule({})", "ɵɵinject"), + ] { + let name = decorator.split('(').next().unwrap(); + let code = compile(&format!( + r" +import {{ {name} }} from '@angular/core'; +import * as ns from './ns'; +import {{ mod }} from './mod'; +@{decorator} +export class A {{ + constructor(zero: ns.Zero, one: ns.one.One, b: mod.B | null) {{}} +}} +" + )); + assert_contains( + &code, + &format!("(i0.{inject}(i1.Zero),i0.{inject}(i1.one.One),i0.{inject}(i2.mod.B))"), + ); + } + + let code = compile( + r" +import { Pipe } from '@angular/core'; +import * as ns from './ns'; +@Pipe({ name: 'a' }) +export class APipe { + constructor(one: ns.one.One) {} +} +", + ); + assert_contains(&code, "(i0.ɵɵdirectiveInject(i1.one.One,16))"); +} + +/// Partial compilation declares the token off the emitted namespace import, +/// like ngtsc: import elision drops the user's `import * as ns` / `{ ns }`. +#[test] +fn partial_component_declares_the_qualified_token() { + let options = + TransformOptions { compilation_mode: CompilationMode::Partial, ..Default::default() }; + for (import, token) in [("* as ns", "{token:i1.one.One}"), ("{ ns }", "{token:i1.ns.one.One}")] + { + let code = compile_with( + &format!( + r" +import {{ Component }} from '@angular/core'; +import {import} from './ns'; +@Component({{ selector: 'a-cmp', template: '' }}) +export class ACmp {{ + constructor(one: ns.one.One) {{}} +}} +" + ), + &options, + ); + assert_contains(&code, "import*asi1from'./ns';"); + assert_contains(&code, &format!("deps:[{token}]")); + } +} + +/// A component's `@Inject(TOKEN.sub)` is injected, not the parameter type. +#[test] +fn component_inject_member_chain_wins_over_the_type() { + let code = compile( + r" +import { Component, Inject } from '@angular/core'; +import * as ns from './ns'; +import { TOKEN } from './tokens'; +@Component({ selector: 'a-cmp', template: '' }) +export class ACmp { + constructor(@Inject(TOKEN.a.b) a: ns.Zero, @Inject(ns.TOKEN) b: unknown) {} +} +", + ); + assert_contains(&code, "(i0.ɵɵdirectiveInject(i1.TOKEN.a.b),i0.ɵɵdirectiveInject(i2.TOKEN))"); +}