From c3a7b3aa4119616d6ec33bbcf3cc8ac762535c5d Mon Sep 17 00:00:00 2001 From: Edouard Bozon Date: Sun, 25 Oct 2026 09:14:51 +0100 Subject: [PATCH] =?UTF-8?q?fix(compiler):=20inject=20`@Attribute()`=20para?= =?UTF-8?q?ms=20with=20`=C9=B5=C9=B5injectAttribute`=20outside=20component?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Directive, pipe, injectable and NgModule factories took `attributeNameType` from the parameter's type, which is unset for `string`, so the attribute name was injected as a DI token. Use the attribute name literal like ngtsc's di.ts. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/directive/decorator.rs | 19 ++-- .../src/injectable/decorator.rs | 19 ++-- .../src/ng_module/decorator.rs | 19 ++-- .../src/pipe/decorator.rs | 19 ++-- .../tests/attribute_inject_test.rs | 88 +++++++++++++++++++ 5 files changed, 132 insertions(+), 32 deletions(-) create mode 100644 crates/oxc_angular_compiler/tests/attribute_inject_test.rs diff --git a/crates/oxc_angular_compiler/src/directive/decorator.rs b/crates/oxc_angular_compiler/src/directive/decorator.rs index 72502a9a0..c1a861c2d 100644 --- a/crates/oxc_angular_compiler/src/directive/decorator.rs +++ b/crates/oxc_angular_compiler/src/directive/decorator.rs @@ -387,15 +387,18 @@ fn extract_param_dependency<'a>( // Handle @Attribute decorator if let Some(attr_name) = attribute_name { + let name = OutputExpression::Literal(Box::new_in( + crate::output::ast::LiteralExpr { + value: crate::output::ast::LiteralValue::String(attr_name), + source_span: None, + }, + &allocator, + )); + // ngtsc's `attributeNameType` is the attribute name itself (di.ts), so it is + // set whatever the parameter's type and compiles to `ɵɵinjectAttribute`. return R3DependencyMetadata { - token: Some(OutputExpression::Literal(Box::new_in( - crate::output::ast::LiteralExpr { - value: crate::output::ast::LiteralValue::String(attr_name), - source_span: None, - }, - &allocator, - ))), - attribute_name_type: token, // The type annotation + token: Some(name.clone_in(allocator)), + attribute_name_type: Some(name), host, optional, self_, diff --git a/crates/oxc_angular_compiler/src/injectable/decorator.rs b/crates/oxc_angular_compiler/src/injectable/decorator.rs index d4ee39076..2b552f937 100644 --- a/crates/oxc_angular_compiler/src/injectable/decorator.rs +++ b/crates/oxc_angular_compiler/src/injectable/decorator.rs @@ -651,15 +651,18 @@ fn extract_param_dependency<'a>( // Handle @Attribute decorator if let Some(attr_name) = attribute_name { + let name = OutputExpression::Literal(Box::new_in( + crate::output::ast::LiteralExpr { + value: crate::output::ast::LiteralValue::String(attr_name), + source_span: None, + }, + &allocator, + )); + // ngtsc's `attributeNameType` is the attribute name itself (di.ts), so it is + // set whatever the parameter's type and compiles to `ɵɵinjectAttribute`. return R3DependencyMetadata { - token: Some(OutputExpression::Literal(Box::new_in( - crate::output::ast::LiteralExpr { - value: crate::output::ast::LiteralValue::String(attr_name), - source_span: None, - }, - &allocator, - ))), - attribute_name_type: token, // The type annotation + token: Some(name.clone_in(allocator)), + attribute_name_type: Some(name), host, optional, self_, diff --git a/crates/oxc_angular_compiler/src/ng_module/decorator.rs b/crates/oxc_angular_compiler/src/ng_module/decorator.rs index 13e8da266..fe46fc0cc 100644 --- a/crates/oxc_angular_compiler/src/ng_module/decorator.rs +++ b/crates/oxc_angular_compiler/src/ng_module/decorator.rs @@ -584,15 +584,18 @@ fn extract_param_dependency<'a>( // Handle @Attribute decorator if let Some(attr_name) = attribute_name { + let name = OutputExpression::Literal(Box::new_in( + crate::output::ast::LiteralExpr { + value: crate::output::ast::LiteralValue::String(attr_name), + source_span: None, + }, + &allocator, + )); + // ngtsc's `attributeNameType` is the attribute name itself (di.ts), so it is + // set whatever the parameter's type and compiles to `ɵɵinjectAttribute`. return R3DependencyMetadata { - token: Some(OutputExpression::Literal(Box::new_in( - crate::output::ast::LiteralExpr { - value: crate::output::ast::LiteralValue::String(attr_name), - source_span: None, - }, - &allocator, - ))), - attribute_name_type: token, // The type annotation + token: Some(name.clone_in(allocator)), + attribute_name_type: Some(name), host, optional, self_, diff --git a/crates/oxc_angular_compiler/src/pipe/decorator.rs b/crates/oxc_angular_compiler/src/pipe/decorator.rs index 3933574d9..513f28846 100644 --- a/crates/oxc_angular_compiler/src/pipe/decorator.rs +++ b/crates/oxc_angular_compiler/src/pipe/decorator.rs @@ -349,15 +349,18 @@ fn extract_param_dependency<'a>( // Handle @Attribute decorator if let Some(attr_name) = attribute_name { + let name = OutputExpression::Literal(Box::new_in( + crate::output::ast::LiteralExpr { + value: crate::output::ast::LiteralValue::String(attr_name), + source_span: None, + }, + &allocator, + )); + // ngtsc's `attributeNameType` is the attribute name itself (di.ts), so it is + // set whatever the parameter's type and compiles to `ɵɵinjectAttribute`. return R3DependencyMetadata { - token: Some(OutputExpression::Literal(Box::new_in( - crate::output::ast::LiteralExpr { - value: crate::output::ast::LiteralValue::String(attr_name), - source_span: None, - }, - &allocator, - ))), - attribute_name_type: token, // The type annotation + token: Some(name.clone_in(allocator)), + attribute_name_type: Some(name), host, optional, self_, diff --git a/crates/oxc_angular_compiler/tests/attribute_inject_test.rs b/crates/oxc_angular_compiler/tests/attribute_inject_test.rs new file mode 100644 index 000000000..a0e1c1b44 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/attribute_inject_test.rs @@ -0,0 +1,88 @@ +//! `@Attribute('name')` on a constructor parameter compiles to +//! `ɵɵinjectAttribute('name')` whatever the class kind, as ngtsc does +//! (`compileInjectDependency` in `r3_factory.ts` checks `attributeNameType` +//! before the target). A plain token inject makes Angular look the attribute +//! name up as a DI token at runtime. + +use oxc_allocator::Allocator; +use oxc_angular_compiler::{TransformOptions, transform_angular_file}; + +/// The compiled code for `source`, without whitespace and with `'` as `"`. +fn compile(source: &str) -> String { + let allocator = Allocator::default(); + let result = transform_angular_file( + &allocator, + "test.ts", + source, + Some(&TransformOptions::default()), + None, + ); + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + result + .code + .chars() + .filter(|c| !c.is_whitespace()) + .map(|c| if c == '\'' { '"' } else { c }) + .collect() +} + +fn assert_injects_attribute(code: &str) { + assert!(code.contains(r#"(i0.ɵɵinjectAttribute("title"))"#), "Got:\n{code}"); +} + +#[test] +fn directive_factory_injects_the_attribute() { + let code = compile( + r" +import { Attribute, Directive } from '@angular/core'; +@Directive({ selector: '[d]' }) +export class D { + constructor(@Attribute('title') title: string) {} +} +", + ); + assert_injects_attribute(&code); +} + +#[test] +fn pipe_factory_injects_the_attribute() { + let code = compile( + r" +import { Attribute, Pipe } from '@angular/core'; +@Pipe({ name: 'p' }) +export class P { + constructor(@Attribute('title') title: string) {} + transform(v: unknown) { return v; } +} +", + ); + assert_injects_attribute(&code); +} + +#[test] +fn injectable_factory_injects_the_attribute() { + let code = compile( + r" +import { Attribute, Injectable } from '@angular/core'; +@Injectable() +export class S { + constructor(@Attribute('title') title: string) {} +} +", + ); + assert_injects_attribute(&code); +} + +#[test] +fn ng_module_factory_injects_the_attribute() { + let code = compile( + r" +import { Attribute, NgModule } from '@angular/core'; +@NgModule({}) +export class M { + constructor(@Attribute('title') title: string) {} +} +", + ); + assert_injects_attribute(&code); +}