@@ -9312,3 +9312,132 @@ export class MyComponent {}
93129312 result. code
93139313 ) ;
93149314}
9315+
9316+ // =============================================================================
9317+ // Regression: @Inject(TOKEN) on pipe constructor parameters
9318+ // =============================================================================
9319+ // The `extract_param_dependency` function in `pipe/decorator.rs` previously did
9320+ // not handle the `@Inject(TOKEN)` decorator, so the token was silently taken
9321+ // from the TypeScript type annotation instead. When the type was an interface
9322+ // (erased at runtime) this left the DI token undefined, which Angular 20's
9323+ // `assertDefined(token)` guard rejects immediately. See commit b2dd390.
9324+
9325+ /// `@Inject(TOKEN)` on a pipe constructor param must produce a factory that
9326+ /// injects the TOKEN identifier, not the erased type annotation.
9327+ #[ test]
9328+ fn test_pipe_factory_uses_inject_token_over_interface_type ( ) {
9329+ let allocator = Allocator :: default ( ) ;
9330+ let source = r"
9331+ import { Pipe, PipeTransform, Inject, InjectionToken } from '@angular/core';
9332+
9333+ export interface Config {
9334+ locale: string;
9335+ }
9336+
9337+ export const CONFIG = new InjectionToken<Config>('CONFIG');
9338+
9339+ @Pipe({ name: 'localized', standalone: true })
9340+ export class LocalizedPipe implements PipeTransform {
9341+ constructor(@Inject(CONFIG) private config: Config) {}
9342+ transform(value: string): string { return value; }
9343+ }
9344+ " ;
9345+
9346+ let result = transform_angular_file ( & allocator, "localized.pipe.ts" , source, None , None ) ;
9347+ assert ! ( !result. has_errors( ) , "Should not have errors: {:?}" , result. diagnostics) ;
9348+
9349+ let code = & result. code ;
9350+ let factory_section =
9351+ code. split ( "ɵfac" ) . nth ( 1 ) . expect ( "Should have a factory definition (ɵfac)" ) ;
9352+
9353+ // Factory must inject CONFIG (the @Inject token), not Config (the interface type).
9354+ assert ! (
9355+ factory_section. contains( "CONFIG" ) ,
9356+ "Pipe factory should inject the @Inject(CONFIG) token. Factory:\n {factory_section}"
9357+ ) ;
9358+ assert ! (
9359+ !factory_section. contains( "directiveInject(Config)" )
9360+ && !factory_section. contains( "ɵɵinject(Config)" ) ,
9361+ "Pipe factory must NOT inject the erased interface type 'Config'. Factory:\n {factory_section}"
9362+ ) ;
9363+ }
9364+
9365+ /// When `@Inject(TOKEN)` is used alongside modifier decorators (`@Optional`,
9366+ /// `@SkipSelf`), the factory must still pick up the TOKEN and forward the
9367+ /// correct DI flags.
9368+ #[ test]
9369+ fn test_pipe_factory_inject_token_with_optional_skip_self ( ) {
9370+ let allocator = Allocator :: default ( ) ;
9371+ let source = r"
9372+ import { Pipe, PipeTransform, Inject, Optional, SkipSelf, InjectionToken } from '@angular/core';
9373+
9374+ export const MY_TOKEN = new InjectionToken<string>('MY_TOKEN');
9375+
9376+ @Pipe({ name: 'tagged', standalone: true })
9377+ export class TaggedPipe implements PipeTransform {
9378+ constructor(
9379+ @Optional() @SkipSelf() @Inject(MY_TOKEN) private tag: string | null,
9380+ ) {}
9381+ transform(value: string): string { return value; }
9382+ }
9383+ " ;
9384+
9385+ let result = transform_angular_file ( & allocator, "tagged.pipe.ts" , source, None , None ) ;
9386+ assert ! ( !result. has_errors( ) , "Should not have errors: {:?}" , result. diagnostics) ;
9387+
9388+ let code = & result. code ;
9389+ let factory_section =
9390+ code. split ( "ɵfac" ) . nth ( 1 ) . expect ( "Should have a factory definition (ɵfac)" ) ;
9391+
9392+ // MY_TOKEN must be present in the factory as the DI token.
9393+ assert ! (
9394+ factory_section. contains( "directiveInject(MY_TOKEN" ) ,
9395+ "Pipe factory should inject MY_TOKEN. Factory:\n {factory_section}"
9396+ ) ;
9397+
9398+ // The DI flag bitmask must include Optional (8) | SkipSelf (4). Angular's
9399+ // pipe compilation also ORs in ForPipe (16), yielding 28. We only require
9400+ // that both Optional and SkipSelf bits are set.
9401+ let flags = factory_section
9402+ . split ( "directiveInject(MY_TOKEN," )
9403+ . nth ( 1 )
9404+ . and_then ( |s| s. split ( ')' ) . next ( ) )
9405+ . and_then ( |s| s. trim ( ) . parse :: < u32 > ( ) . ok ( ) )
9406+ . expect ( "Factory should encode numeric DI flags" ) ;
9407+ assert ! (
9408+ flags & 8 != 0 && flags & 4 != 0 ,
9409+ "Pipe factory flags should include Optional (8) and SkipSelf (4). Got: {flags}. Factory:\n {factory_section}"
9410+ ) ;
9411+ }
9412+
9413+ /// Without `@Inject`, the factory must still fall back to the type annotation
9414+ /// so that plain class-typed dependencies continue to resolve correctly.
9415+ #[ test]
9416+ fn test_pipe_factory_without_inject_still_uses_type_annotation ( ) {
9417+ let allocator = Allocator :: default ( ) ;
9418+ let source = r"
9419+ import { Pipe, PipeTransform } from '@angular/core';
9420+
9421+ export class Logger {
9422+ log(msg: string): void {}
9423+ }
9424+
9425+ @Pipe({ name: 'logged', standalone: true })
9426+ export class LoggedPipe implements PipeTransform {
9427+ constructor(private logger: Logger) {}
9428+ transform(value: string): string { return value; }
9429+ }
9430+ " ;
9431+
9432+ let result = transform_angular_file ( & allocator, "logged.pipe.ts" , source, None , None ) ;
9433+ assert ! ( !result. has_errors( ) , "Should not have errors: {:?}" , result. diagnostics) ;
9434+
9435+ let code = & result. code ;
9436+ let factory_section =
9437+ code. split ( "ɵfac" ) . nth ( 1 ) . expect ( "Should have a factory definition (ɵfac)" ) ;
9438+
9439+ assert ! (
9440+ factory_section. contains( "Logger" ) ,
9441+ "Pipe factory should fall back to the type annotation (Logger). Factory:\n {factory_section}"
9442+ ) ;
9443+ }
0 commit comments