mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
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 9769560da7)
This commit is contained in:
parent
ca328eab8f
commit
67e0ba7e03
8 changed files with 113 additions and 88 deletions
|
|
@ -89,7 +89,7 @@ export interface TcbDirectiveMetadata {
|
|||
typeParameters: TcbTypeParameter[] | null;
|
||||
inputs: ClassPropertyMapping<TcbInputMapping>;
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<ClassDeclaration<ts.ClassDeclaration>>,
|
||||
meta: TypeCheckBlockMetadata,
|
||||
env: Environment,
|
||||
genericContextBehavior: TcbGenericContextBehavior,
|
||||
): {tcbMeta: TcbTypeCheckBlockMetadata; component: TcbComponentMetadata} {
|
||||
const refCache = new Map<Reference<ClassDeclaration>, TcbReferenceMetadata>();
|
||||
const dirCache = new Map<TypeCheckableDirectiveMeta, TcbDirectiveMetadata>();
|
||||
|
||||
const extractRef = (ref: Reference<ClassDeclaration>) => {
|
||||
if (refCache.has(ref)) {
|
||||
return refCache.get(ref)!;
|
||||
|
|
@ -55,8 +60,6 @@ export function adaptTypeCheckBlockMetadata(
|
|||
return result;
|
||||
};
|
||||
|
||||
const dirCache = new Map<TypeCheckableDirectiveMeta, TcbDirectiveMetadata>();
|
||||
|
||||
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<ClassDeclaration>),
|
||||
isGeneric: dir.isGeneric,
|
||||
|
||||
typeParameters: (() => {
|
||||
const node = dir.ref.node as ClassDeclaration<ts.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<ts.ClassDeclaration>,
|
||||
env.reflector,
|
||||
env,
|
||||
),
|
||||
...adaptGenerics(
|
||||
dir.ref.node as ClassDeclaration<ts.ClassDeclaration>,
|
||||
env,
|
||||
TcbGenericContextBehavior.UseEmitter,
|
||||
),
|
||||
};
|
||||
|
||||
dirCache.set(dir, tcbDir);
|
||||
return tcbDir;
|
||||
};
|
||||
|
||||
const originalBoundTarget = meta.boundTarget.target;
|
||||
const adaptedBoundTarget: BoundTarget<TcbDirectiveMetadata> = {
|
||||
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<ClassDeclaration>),
|
||||
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<ClassDeclaration>),
|
||||
...adaptGenerics(
|
||||
ref.node,
|
||||
env,
|
||||
env.config.useContextGenericType
|
||||
? genericContextBehavior
|
||||
: TcbGenericContextBehavior.FallbackToAny,
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function adaptGenerics(
|
||||
node: ClassDeclaration<ts.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<string>(node.typeParameters.length).fill('any');
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
typeParameters = typeArguments = null;
|
||||
}
|
||||
|
||||
return {typeParameters, typeArguments};
|
||||
}
|
||||
|
||||
function extractReferenceMetadata(
|
||||
ref: Reference<ClassDeclaration>,
|
||||
env: Environment,
|
||||
|
|
|
|||
|
|
@ -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[] = [];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in a new issue