From 67e0ba7e03bb940639f0eafb3af45015e9727eac Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 16 Mar 2026 20:52:44 +0100 Subject: [PATCH] fix(compiler-cli): generic types not filled out correctly in type check block Fixes a regression caused by the recent TCB changes where we moved the type parameter processing earlier in the pipeline and stopped properly accounting for the `TcbGenericContextBehavior`. Fixes #67704. (cherry picked from commit 9769560da73efee4793dfdc1459c8b1ac10981de) --- .../src/ngtsc/typecheck/api/api.ts | 3 +- .../src/ngtsc/typecheck/src/context.ts | 8 +- .../src/ngtsc/typecheck/src/environment.ts | 2 +- .../src/ngtsc/typecheck/src/ops/scope.ts | 2 +- .../src/ngtsc/typecheck/src/tcb_adapter.ts | 117 +++++++++++------- .../ngtsc/typecheck/src/type_check_block.ts | 40 +----- .../ngtsc/typecheck/src/type_check_file.ts | 8 +- .../compiler-cli/test/ngtsc/ngtsc_spec.ts | 21 ++++ 8 files changed, 113 insertions(+), 88 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts index 4f66f1e7e69..e8be80bd7f1 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts @@ -89,7 +89,7 @@ export interface TcbDirectiveMetadata { typeParameters: TcbTypeParameter[] | null; inputs: ClassPropertyMapping; outputs: ClassPropertyMapping; - hasRequiresInlineTypeCtor: boolean; + requiresInlineTypeCtor: boolean; ngTemplateGuards: TemplateGuardMeta[]; hasNgTemplateContextGuard: boolean; hasNgFieldDirective: boolean; @@ -105,6 +105,7 @@ export interface TcbDirectiveMetadata { export interface TcbComponentMetadata { ref: TcbReferenceMetadata; typeParameters: TcbTypeParameter[] | null; + typeArguments: string[] | null; } export interface TcbTypeCheckBlockMetadata { diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts index 4cfb045d55c..abc7f65f477 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts @@ -671,7 +671,12 @@ class InlineTcbOp implements Op { const env = new Environment(this.config, im, refEmitter, this.reflector, sf); const fnName = `_tcb_${this.ref.node.pos}`; - const {tcbMeta, component} = adaptTypeCheckBlockMetadata(this.ref, this.meta, env); + const {tcbMeta, component} = adaptTypeCheckBlockMetadata( + this.ref, + this.meta, + env, + TcbGenericContextBehavior.CopyClassNodes, + ); // Inline TCBs should copy any generic type parameter nodes directly, as the TCB code is // inlined into the class in a context where that will always be legal. @@ -682,7 +687,6 @@ class InlineTcbOp implements Op { tcbMeta, this.domSchemaChecker, this.oobRecorder, - TcbGenericContextBehavior.CopyClassNodes, ); return fn; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts index 9666de22fbb..48da63a0c8a 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts @@ -69,7 +69,7 @@ export class Environment extends ReferenceEmitEnvironment { return new TcbExpr(this.typeCtors.get(key)!); } - if (dir.hasRequiresInlineTypeCtor) { + if (dir.requiresInlineTypeCtor) { // The constructor has already been created inline, we just need to construct a reference to // it. const typeCtorExpr = `${this.referenceTcbValue(dir.ref).print()}.ngTypeCtor`; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/scope.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/scope.ts index 41d8fe5be70..33b8f19d540 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ops/scope.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ops/scope.ts @@ -774,7 +774,7 @@ export class Scope { // The most common case is that when a directive is not generic, we use the normal // `TcbNonDirectiveTypeOp`. return new TcbNonGenericDirectiveTypeOp(this.tcb, this, node, dir); - } else if (!dir.hasRequiresInlineTypeCtor || this.tcb.env.config.useInlineTypeConstructors) { + } else if (!dir.requiresInlineTypeCtor || this.tcb.env.config.useInlineTypeConstructors) { // For generic directives, we use a type constructor to infer types. If a directive requires // an inline type constructor, then inlining must be available to use the // `TcbDirectiveCtorOp`. If not we, we fallback to using `any` – see below. diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/tcb_adapter.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/tcb_adapter.ts index ed6f56c43d9..3be223c35d6 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/tcb_adapter.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/tcb_adapter.ts @@ -15,6 +15,7 @@ import { TcbInputMapping, TcbPipeMetadata, TypeCheckableDirectiveMeta, + TcbTypeParameter, } from '../api'; import {Environment} from './environment'; import {ImportFlags, ReferenceEmitKind, Reference} from '../../imports'; @@ -35,6 +36,7 @@ import {generateTcbTypeParameters} from './tcb_util'; import {TypeParameterEmitter} from './type_parameter_emitter'; import {ClassDeclaration} from '../../reflection'; import ts from 'typescript'; +import {TcbGenericContextBehavior} from './ops/context'; /** * Adapts the compiler's `TypeCheckBlockMetadata` (which includes full TS AST nodes) @@ -44,8 +46,11 @@ export function adaptTypeCheckBlockMetadata( ref: Reference>, meta: TypeCheckBlockMetadata, env: Environment, + genericContextBehavior: TcbGenericContextBehavior, ): {tcbMeta: TcbTypeCheckBlockMetadata; component: TcbComponentMetadata} { const refCache = new Map, TcbReferenceMetadata>(); + const dirCache = new Map(); + const extractRef = (ref: Reference) => { if (refCache.has(ref)) { return refCache.get(ref)!; @@ -55,8 +60,6 @@ export function adaptTypeCheckBlockMetadata( return result; }; - const dirCache = new Map(); - const convertDir = (dir: TypeCheckableDirectiveMeta): TcbDirectiveMetadata => { if (dirCache.has(dir)) return dirCache.get(dir)!; @@ -110,45 +113,33 @@ export function adaptTypeCheckBlockMetadata( ref: extractRef(dir.ref as Reference), isGeneric: dir.isGeneric, - - typeParameters: (() => { - const node = dir.ref.node as ClassDeclaration; - if (!node.typeParameters) { - return null; - } - const emitter = new TypeParameterEmitter(node.typeParameters, env.reflector); - let emitted: ts.TypeParameterDeclaration[] | undefined; - if (!emitter.canEmit((ref) => env.canReferenceType(ref))) { - emitted = [...node.typeParameters] as ts.TypeParameterDeclaration[]; - } else { - emitted = emitter.emit((ref) => env.referenceType(ref)); - } - return generateTcbTypeParameters(emitted || [], env.contextFile); - })(), - hasRequiresInlineTypeCtor: requiresInlineTypeCtor( + requiresInlineTypeCtor: requiresInlineTypeCtor( dir.ref.node as ClassDeclaration, env.reflector, env, ), + ...adaptGenerics( + dir.ref.node as ClassDeclaration, + env, + TcbGenericContextBehavior.UseEmitter, + ), }; dirCache.set(dir, tcbDir); return tcbDir; }; + const originalBoundTarget = meta.boundTarget.target; const adaptedBoundTarget: BoundTarget = { - target: (() => { - const originalTarget = meta.boundTarget.target; - return { - template: originalTarget.template, - host: originalTarget.host - ? { - node: originalTarget.host.node, - directives: originalTarget.host.directives.map(convertDir), - } - : undefined, - }; - })(), + target: { + template: originalBoundTarget.template, + host: originalBoundTarget.host + ? { + node: originalBoundTarget.host.node, + directives: originalBoundTarget.host.directives.map(convertDir), + } + : undefined, + }, getUsedDirectives: () => meta.boundTarget.getUsedDirectives().map(convertDir), getEagerlyUsedDirectives: () => meta.boundTarget.getEagerlyUsedDirectives().map(convertDir), getUsedPipes: () => meta.boundTarget.getUsedPipes(), @@ -208,25 +199,59 @@ export function adaptTypeCheckBlockMetadata( isStandalone: meta.isStandalone, preserveWhitespaces: meta.preserveWhitespaces, }, - component: (() => { - return { - ref: extractRef(ref as Reference), - typeParameters: (() => { - if (!ref.node.typeParameters) return null; - const emitter = new TypeParameterEmitter(ref.node.typeParameters, env.reflector); - let emitted: ts.TypeParameterDeclaration[] | undefined; - if (!emitter.canEmit((r) => env.canReferenceType(r))) { - emitted = [...ref.node.typeParameters] as ts.TypeParameterDeclaration[]; - } else { - emitted = emitter.emit((r) => env.referenceType(r)); - } - return generateTcbTypeParameters(emitted || [], env.contextFile); - })(), - }; - })(), + component: { + ref: extractRef(ref as Reference), + ...adaptGenerics( + ref.node, + env, + env.config.useContextGenericType + ? genericContextBehavior + : TcbGenericContextBehavior.FallbackToAny, + ), + }, }; } +function adaptGenerics( + node: ClassDeclaration, + env: Environment, + genericContextBehavior: TcbGenericContextBehavior, +): { + typeParameters: TcbTypeParameter[] | null; + typeArguments: string[] | null; +} { + let typeParameters: TcbTypeParameter[] | null; + let typeArguments: string[] | null; + + if (node.typeParameters !== undefined && node.typeParameters.length > 0) { + switch (genericContextBehavior) { + case TcbGenericContextBehavior.UseEmitter: + const emitter = new TypeParameterEmitter(node.typeParameters, env.reflector); + const emittedParams = emitter.canEmit((r) => env.canReferenceType(r)) + ? emitter.emit((typeRef) => env.referenceType(typeRef)) + : undefined; + typeParameters = generateTcbTypeParameters( + emittedParams || node.typeParameters, + env.contextFile, + ); + typeArguments = typeParameters.map((param) => param.name); + break; + case TcbGenericContextBehavior.CopyClassNodes: + typeParameters = generateTcbTypeParameters(node.typeParameters, env.contextFile); + typeArguments = typeParameters.map((param) => param.name); + break; + case TcbGenericContextBehavior.FallbackToAny: + typeParameters = generateTcbTypeParameters(node.typeParameters, env.contextFile); + typeArguments = new Array(node.typeParameters.length).fill('any'); + break; + } + } else { + typeParameters = typeArguments = null; + } + + return {typeParameters, typeArguments}; +} + function extractReferenceMetadata( ref: Reference, env: Environment, diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts index 42e63be242e..b28a805f1df 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts @@ -6,13 +6,12 @@ * found in the LICENSE file at https://angular.dev/license */ -import ts from 'typescript'; -import {TcbComponentMetadata, TcbTypeCheckBlockMetadata, TcbTypeParameter} from '../api'; +import {TcbComponentMetadata, TcbTypeCheckBlockMetadata} from '../api'; import {DomSchemaChecker} from './dom'; import {Environment} from './environment'; import {OutOfBandDiagnosticRecorder} from './oob'; import {createHostBindingsBlockGuard} from './host_bindings'; -import {Context, TcbGenericContextBehavior} from './ops/context'; +import {Context} from './ops/context'; import {Scope} from './ops/scope'; import {getStatementsBlock} from './ops/codegen'; @@ -47,7 +46,6 @@ export function generateTypeCheckBlock( meta: TcbTypeCheckBlockMetadata, domSchemaChecker: DomSchemaChecker, oobRecorder: OutOfBandDiagnosticRecorder, - genericContextBehavior: TcbGenericContextBehavior, ): string { const tcb = new Context( env, @@ -61,42 +59,14 @@ export function generateTypeCheckBlock( meta.preserveWhitespaces, ); const ctxRawType = env.referenceTcbValue(component.ref); - let typeParameters: TcbTypeParameter[] | undefined = undefined; - let typeArguments: string[] | undefined = undefined; - - if (component.typeParameters !== undefined) { - if (!env.config.useContextGenericType) { - genericContextBehavior = TcbGenericContextBehavior.FallbackToAny; - } - - switch (genericContextBehavior) { - case TcbGenericContextBehavior.UseEmitter: - // Guaranteed to emit type parameters since we checked that the class has them above. - const emittedParams = component.typeParameters || []; - typeParameters = emittedParams; - typeArguments = typeParameters!.map((param) => param.name); - break; - case TcbGenericContextBehavior.CopyClassNodes: - const copiedParams = component.typeParameters ? [...component.typeParameters] : []; - typeParameters = copiedParams; - typeArguments = typeParameters!.map((param) => param.name); - break; - case TcbGenericContextBehavior.FallbackToAny: - typeArguments = Array.from({length: component.typeParameters?.length ?? 0}).map( - () => 'any', - ); - break; - } - } + const {typeParameters, typeArguments} = component; const typeParamsStr = - typeParameters === undefined || typeParameters.length === 0 + !env.config.useContextGenericType || typeParameters === null || typeParameters.length === 0 ? '' : `<${typeParameters.map((p) => p.representation).join(', ')}>`; const typeArgsStr = - typeArguments === undefined || typeArguments.length === 0 - ? '' - : `<${typeArguments.join(', ')}>`; + typeArguments === null || typeArguments.length === 0 ? '' : `<${typeArguments.join(', ')}>`; const thisParamStr = `this: ${ctxRawType.print()}${typeArgsStr}`; const statements: string[] = []; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_file.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_file.ts index 73e4c348285..f5cdce51af8 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_file.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_file.ts @@ -70,7 +70,12 @@ export class TypeCheckFile extends Environment { genericContextBehavior: TcbGenericContextBehavior, ): void { const fnId = `_tcb${this.nextTcbId++}`; - const {tcbMeta, component} = adaptTypeCheckBlockMetadata(ref, meta, this); + const {tcbMeta, component} = adaptTypeCheckBlockMetadata( + ref, + meta, + this, + genericContextBehavior, + ); const fn = generateTypeCheckBlock( this, component, @@ -78,7 +83,6 @@ export class TypeCheckFile extends Environment { tcbMeta, domSchemaChecker, oobRecorder, - genericContextBehavior, ); this.tcbStatements.push(fn); } diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 22d6b43d90d..19476a32ed5 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -10788,6 +10788,27 @@ runInEachFileSystem((os: string) => { expect(codes).toEqual([ngErrorCode(ErrorCode.NGMODULE_BOOTSTRAP_IS_STANDALONE)]); }); + it('should compile a component with a complex generic', () => { + env.write( + 'test.ts', + ` + import {Component} from '@angular/core'; + + @Component({ + selector: 'app-root', + template: '', + }) + export class App< + T extends object = object, + TOptions extends { [K in keyof T]?: T[K] } = object + > {} + `, + ); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(0); + }); + describe('InjectorDef emit optimizations for standalone', () => { it('should not filter components out of NgModule.imports', () => { env.write(