@@ -3977,3 +3977,166 @@ fn test_for_index_xref_with_i18n_attribute_binding() {
39773977 // matching Angular TS which stores direct i18n.Message object references on BindingOp.
39783978 insta:: assert_snapshot!( "for_index_xref_with_i18n_attribute_binding" , js) ;
39793979}
3980+
3981+ /// Tests that setClassMetadata uses namespace-prefixed type references for imported
3982+ /// constructor parameter types.
3983+ ///
3984+ /// Angular's TypeScript compiler distinguishes between local and imported types in
3985+ /// the ɵsetClassMetadata constructor parameter metadata:
3986+ /// - Local types use bare names: `{ type: LocalService }`
3987+ /// - Imported types use namespace-prefixed names: `{ type: i1.ImportedService }`
3988+ ///
3989+ /// This is because TypeScript type annotations are erased at runtime, so imported
3990+ /// types need namespace imports (i0, i1, i2...) to be available as runtime values.
3991+ /// The factory function (ɵfac) already handles this correctly via R3DependencyMetadata
3992+ /// and create_token_expression, but setClassMetadata was using bare names for all types.
3993+ #[ test]
3994+ fn test_set_class_metadata_uses_namespace_for_imported_ctor_params ( ) {
3995+ let allocator = Allocator :: default ( ) ;
3996+ let source = r#"
3997+ import { Component } from '@angular/core';
3998+ import { SomeService } from './some.service';
3999+
4000+ @Component({
4001+ selector: 'test-comp',
4002+ template: '<div>hello</div>',
4003+ standalone: true,
4004+ })
4005+ export class TestComponent {
4006+ constructor(private svc: SomeService) {}
4007+ }
4008+ "# ;
4009+
4010+ let options = ComponentTransformOptions {
4011+ emit_class_metadata : true ,
4012+ ..ComponentTransformOptions :: default ( )
4013+ } ;
4014+
4015+ let result = transform_angular_file ( & allocator, "test.component.ts" , source, & options, None ) ;
4016+
4017+ assert ! ( !result. has_errors( ) , "Should not have errors: {:?}" , result. diagnostics) ;
4018+
4019+ // Extract the setClassMetadata section specifically (not the factory function)
4020+ let metadata_section = result
4021+ . code
4022+ . split ( "ɵsetClassMetadata" )
4023+ . nth ( 1 )
4024+ . expect ( "setClassMetadata should be present in output" ) ;
4025+
4026+ // The ctor_parameters callback should use namespace-prefixed type for
4027+ // the imported SomeService: `{type:i1.SomeService}` not `{type:SomeService}`
4028+ assert ! (
4029+ metadata_section. contains( "i1.SomeService" ) ,
4030+ "setClassMetadata ctor_parameters should use namespace-prefixed type (i1.SomeService) for imported constructor parameter. Metadata section:\n {}" ,
4031+ metadata_section
4032+ ) ;
4033+ assert ! (
4034+ !metadata_section. contains( "type:SomeService}" ) ,
4035+ "setClassMetadata should NOT use bare type name for imported types. Metadata section:\n {}" ,
4036+ metadata_section
4037+ ) ;
4038+ }
4039+
4040+ /// Tests that setClassMetadata uses namespace-prefixed type even when @Inject is present.
4041+ ///
4042+ /// When a constructor parameter has both a type annotation and @Inject decorator pointing
4043+ /// to the same imported class, the metadata `type` field should still use namespace prefix.
4044+ /// The factory correctly uses bare names for @Inject tokens with named imports, but the
4045+ /// metadata type always represents the TypeScript type annotation which is erased at runtime.
4046+ ///
4047+ /// Example:
4048+ /// - Factory: `ɵɵdirectiveInject(TagPickerComponent, 12)` (bare - ok, @Inject value import)
4049+ /// - Metadata: `{ type: i1.TagPickerComponent, decorators: [{type: Inject, ...}] }` (namespace)
4050+ #[ test]
4051+ fn test_set_class_metadata_namespace_with_inject_decorator ( ) {
4052+ let allocator = Allocator :: default ( ) ;
4053+ let source = r#"
4054+ import { Component, Inject, Optional, SkipSelf } from '@angular/core';
4055+ import { SomeService } from './some.service';
4056+
4057+ @Component({
4058+ selector: 'test-comp',
4059+ template: '<div>hello</div>',
4060+ standalone: true,
4061+ })
4062+ export class TestComponent {
4063+ constructor(
4064+ @Optional() @SkipSelf() @Inject(SomeService) private svc: SomeService
4065+ ) {}
4066+ }
4067+ "# ;
4068+
4069+ let options = ComponentTransformOptions {
4070+ emit_class_metadata : true ,
4071+ ..ComponentTransformOptions :: default ( )
4072+ } ;
4073+
4074+ let result = transform_angular_file ( & allocator, "test.component.ts" , source, & options, None ) ;
4075+
4076+ assert ! ( !result. has_errors( ) , "Should not have errors: {:?}" , result. diagnostics) ;
4077+
4078+ // Extract the setClassMetadata section
4079+ let metadata_section = result
4080+ . code
4081+ . split ( "ɵsetClassMetadata" )
4082+ . nth ( 1 )
4083+ . expect ( "setClassMetadata should be present in output" ) ;
4084+
4085+ // Even with @Inject(SomeService), the type field should use namespace prefix
4086+ // because the type annotation is erased by TypeScript
4087+ assert ! (
4088+ metadata_section. contains( "i1.SomeService" ) ,
4089+ "setClassMetadata should use namespace-prefixed type even with @Inject. Metadata section:\n {}" ,
4090+ metadata_section
4091+ ) ;
4092+ }
4093+
4094+ /// Tests that when @Inject token differs from the type annotation (e.g., @Inject(DOCUMENT)
4095+ /// on a parameter typed as Document), the metadata type uses bare name since the type
4096+ /// annotation may reference a global or different module than the injection token.
4097+ #[ test]
4098+ fn test_set_class_metadata_inject_differs_from_type ( ) {
4099+ let allocator = Allocator :: default ( ) ;
4100+ let source = r#"
4101+ import { Component, Inject } from '@angular/core';
4102+ import { DOCUMENT } from '@angular/common';
4103+
4104+ @Component({
4105+ selector: 'test-comp',
4106+ template: '<div>hello</div>',
4107+ standalone: true,
4108+ })
4109+ export class TestComponent {
4110+ constructor(@Inject(DOCUMENT) private doc: Document) {}
4111+ }
4112+ "# ;
4113+
4114+ let options = ComponentTransformOptions {
4115+ emit_class_metadata : true ,
4116+ ..ComponentTransformOptions :: default ( )
4117+ } ;
4118+
4119+ let result = transform_angular_file ( & allocator, "test.component.ts" , source, & options, None ) ;
4120+
4121+ assert ! ( !result. has_errors( ) , "Should not have errors: {:?}" , result. diagnostics) ;
4122+
4123+ let metadata_section = result
4124+ . code
4125+ . split ( "ɵsetClassMetadata" )
4126+ . nth ( 1 )
4127+ . expect ( "setClassMetadata should be present in output" ) ;
4128+
4129+ // The type should be bare "Document" (global type), not namespace-prefixed
4130+ // even though the @Inject token (DOCUMENT) is from @angular/common
4131+ assert ! (
4132+ metadata_section. contains( "type:Document" ) ,
4133+ "setClassMetadata should use bare type for globals when @Inject token differs. Metadata section:\n {}" ,
4134+ metadata_section
4135+ ) ;
4136+ // Should NOT add namespace prefix for Document
4137+ assert ! (
4138+ !metadata_section. contains( "i1.Document" ) ,
4139+ "setClassMetadata should NOT namespace-prefix global types. Metadata section:\n {}" ,
4140+ metadata_section
4141+ ) ;
4142+ }
0 commit comments