From 82f56d02097710dcc9234f587a451586fe5a41a2 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 11 Mar 2026 14:40:32 +0100 Subject: [PATCH 1/4] refactor(compiler-cli): add generic for type nodes to AST factories Updates the type factories and various usage sites to add a generic for type nodes. --- .../linker/babel/src/ast/babel_ast_factory.ts | 6 ++- .../linker/babel/src/es2015_linker_plugin.ts | 6 ++- .../src/file_linker/emit_scopes/emit_scope.ts | 18 ++++++--- .../emit_scopes/local_emit_scope.ts | 6 ++- .../linker/src/file_linker/file_linker.ts | 12 +++--- .../src/file_linker/linker_environment.ts | 14 +++---- .../partial_linker_selector.ts | 4 +- .../linker/src/file_linker/translator.ts | 8 ++-- .../linker/src/linker_import_generator.ts | 4 +- .../emit_scopes/emit_scope_spec.ts | 40 ++++++++++++++----- .../emit_scopes/local_emit_scope_spec.ts | 16 ++++---- .../test/file_linker/file_linker_spec.ts | 15 +++---- .../test/file_linker/translator_spec.ts | 11 +++-- .../test/linker_import_generator_spec.ts | 8 ++-- .../ngtsc/translator/src/api/ast_factory.ts | 2 +- .../src/ngtsc/translator/src/translator.ts | 26 ++++++++++-- .../translator/src/typescript_ast_factory.ts | 2 +- .../translator/src/typescript_translator.ts | 4 +- 18 files changed, 131 insertions(+), 71 deletions(-) diff --git a/packages/compiler-cli/linker/babel/src/ast/babel_ast_factory.ts b/packages/compiler-cli/linker/babel/src/ast/babel_ast_factory.ts index c07a3f74eceb..fc868464eaeb 100644 --- a/packages/compiler-cli/linker/babel/src/ast/babel_ast_factory.ts +++ b/packages/compiler-cli/linker/babel/src/ast/babel_ast_factory.ts @@ -21,7 +21,11 @@ import { /** * A Babel flavored implementation of the AstFactory. */ -export class BabelAstFactory implements AstFactory { +export class BabelAstFactory implements AstFactory< + t.Statement, + t.Expression | t.SpreadElement, + t.TSType +> { constructor( /** The absolute path to the source file being compiled. */ private sourceUrl: string, diff --git a/packages/compiler-cli/linker/babel/src/es2015_linker_plugin.ts b/packages/compiler-cli/linker/babel/src/es2015_linker_plugin.ts index 8ef6056bb79b..0fb76585856f 100644 --- a/packages/compiler-cli/linker/babel/src/es2015_linker_plugin.ts +++ b/packages/compiler-cli/linker/babel/src/es2015_linker_plugin.ts @@ -29,7 +29,8 @@ export function createEs2015LinkerPlugin({ let fileLinker: FileLinker< ConstantScopePath, t.Statement, - t.Expression | t.SpreadElement + t.Expression | t.SpreadElement, + t.TSType > | null = null; return { @@ -53,7 +54,8 @@ export function createEs2015LinkerPlugin({ const linkerEnvironment = LinkerEnvironment.create< t.Statement, - t.Expression | t.SpreadElement + t.Expression | t.SpreadElement, + t.TSType >(fileSystem, logger, new BabelAstHost(), new BabelAstFactory(sourceUrl), options); fileLinker = new FileLinker(linkerEnvironment, sourceUrl, file.code); }, diff --git a/packages/compiler-cli/linker/src/file_linker/emit_scopes/emit_scope.ts b/packages/compiler-cli/linker/src/file_linker/emit_scopes/emit_scope.ts index 475977b9fcd8..9dba2811c862 100644 --- a/packages/compiler-cli/linker/src/file_linker/emit_scopes/emit_scope.ts +++ b/packages/compiler-cli/linker/src/file_linker/emit_scopes/emit_scope.ts @@ -21,13 +21,13 @@ import {Translator} from '../translator'; * * This implementation will emit the definition and the constant statements separately. */ -export class EmitScope { +export class EmitScope { readonly constantPool = new ConstantPool(); constructor( protected readonly ngImport: TExpression, - protected readonly translator: Translator, - private readonly factory: AstFactory, + protected readonly translator: Translator, + private readonly factory: AstFactory, ) {} /** @@ -38,7 +38,7 @@ export class EmitScope { translateDefinition(definition: LinkedDefinition): TExpression { const expression = this.translator.translateExpression( definition.expression, - new LinkerImportGenerator(this.factory, this.ngImport), + new LinkerImportGenerator(this.factory, this.ngImport), ); if (definition.statements.length > 0) { @@ -47,7 +47,10 @@ export class EmitScope { // insert statements after definitions. To work around this, the linker transforms the // definition into an IIFE which executes the definition statements before returning the // definition expression. - const importGenerator = new LinkerImportGenerator(this.factory, this.ngImport); + const importGenerator = new LinkerImportGenerator( + this.factory, + this.ngImport, + ); return this.wrapInIifeWithStatements( expression, definition.statements.map((statement) => @@ -64,7 +67,10 @@ export class EmitScope { * Return any constant statements that are shared between all uses of this `EmitScope`. */ getConstantStatements(): TStatement[] { - const importGenerator = new LinkerImportGenerator(this.factory, this.ngImport); + const importGenerator = new LinkerImportGenerator( + this.factory, + this.ngImport, + ); return this.constantPool.statements.map((statement) => this.translator.translateStatement(statement, importGenerator), ); diff --git a/packages/compiler-cli/linker/src/file_linker/emit_scopes/local_emit_scope.ts b/packages/compiler-cli/linker/src/file_linker/emit_scopes/local_emit_scope.ts index 052fcf8f08e6..daa00e8beddd 100644 --- a/packages/compiler-cli/linker/src/file_linker/emit_scopes/local_emit_scope.ts +++ b/packages/compiler-cli/linker/src/file_linker/emit_scopes/local_emit_scope.ts @@ -15,7 +15,11 @@ import {EmitScope} from './emit_scope'; * there is no clear shared scope for constant statements. In this case they are bundled with the * translated definition and will be emitted into an IIFE. */ -export class LocalEmitScope extends EmitScope { +export class LocalEmitScope extends EmitScope< + TStatement, + TExpression, + TType +> { /** * Translate the given Output AST definition expression into a generic `TExpression`. * diff --git a/packages/compiler-cli/linker/src/file_linker/file_linker.ts b/packages/compiler-cli/linker/src/file_linker/file_linker.ts index 403a35fbfad2..56a366c9b64f 100644 --- a/packages/compiler-cli/linker/src/file_linker/file_linker.ts +++ b/packages/compiler-cli/linker/src/file_linker/file_linker.ts @@ -21,17 +21,17 @@ export const NO_STATEMENTS: Readonly = [] as const; /** * This class is responsible for linking all the partial declarations found in a single file. */ -export class FileLinker { +export class FileLinker { private linkerSelector: PartialLinkerSelector; - private emitScopes = new Map>(); + private emitScopes = new Map>(); constructor( - private linkerEnvironment: LinkerEnvironment, + private linkerEnvironment: LinkerEnvironment, sourceUrl: AbsoluteFsPath, code: string, ) { this.linkerSelector = new PartialLinkerSelector( - createLinkerMap(this.linkerEnvironment, sourceUrl, code), + createLinkerMap(this.linkerEnvironment, sourceUrl, code), this.linkerEnvironment.logger, this.linkerEnvironment.options.unknownDeclarationVersionHandling, ); @@ -98,11 +98,11 @@ export class FileLinker { private getEmitScope( ngImport: TExpression, declarationScope: DeclarationScope, - ): EmitScope { + ): EmitScope { const constantScope = declarationScope.getConstantScopeRef(ngImport); if (constantScope === null) { // There is no constant scope so we will emit extra statements into the definition IIFE. - return new LocalEmitScope( + return new LocalEmitScope( ngImport, this.linkerEnvironment.translator, this.linkerEnvironment.factory, diff --git a/packages/compiler-cli/linker/src/file_linker/linker_environment.ts b/packages/compiler-cli/linker/src/file_linker/linker_environment.ts index 04f3dafa3d6f..6343df8bd0d6 100644 --- a/packages/compiler-cli/linker/src/file_linker/linker_environment.ts +++ b/packages/compiler-cli/linker/src/file_linker/linker_environment.ts @@ -14,30 +14,30 @@ import {AstHost} from '../ast/ast_host'; import {DEFAULT_LINKER_OPTIONS, LinkerOptions} from './linker_options'; import {Translator} from './translator'; -export class LinkerEnvironment { - readonly translator: Translator; +export class LinkerEnvironment { + readonly translator: Translator; readonly sourceFileLoader: SourceFileLoader | null; private constructor( readonly fileSystem: ReadonlyFileSystem, readonly logger: Logger, readonly host: AstHost, - readonly factory: AstFactory, + readonly factory: AstFactory, readonly options: LinkerOptions, ) { - this.translator = new Translator(this.factory); + this.translator = new Translator(this.factory); this.sourceFileLoader = this.options.sourceMapping ? new SourceFileLoader(this.fileSystem, this.logger, {}) : null; } - static create( + static create( fileSystem: ReadonlyFileSystem, logger: Logger, host: AstHost, - factory: AstFactory, + factory: AstFactory, options: Partial, - ): LinkerEnvironment { + ): LinkerEnvironment { return new LinkerEnvironment(fileSystem, logger, host, factory, { sourceMapping: options.sourceMapping ?? DEFAULT_LINKER_OPTIONS.sourceMapping, linkerJitMode: options.linkerJitMode ?? DEFAULT_LINKER_OPTIONS.linkerJitMode, diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker_selector.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker_selector.ts index e381fcc814e3..4da9a8e8c044 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker_selector.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker_selector.ts @@ -74,8 +74,8 @@ export interface LinkerRange { * `minVersion` of the partial-declaration should be updated, the new linker implementation should * be added to the end of the collection, and the version of the previous linker should be updated. */ -export function createLinkerMap( - environment: LinkerEnvironment, +export function createLinkerMap( + environment: LinkerEnvironment, sourceUrl: AbsoluteFsPath, code: string, ): Map[]> { diff --git a/packages/compiler-cli/linker/src/file_linker/translator.ts b/packages/compiler-cli/linker/src/file_linker/translator.ts index d529c663d8da..4e3410cb12c6 100644 --- a/packages/compiler-cli/linker/src/file_linker/translator.ts +++ b/packages/compiler-cli/linker/src/file_linker/translator.ts @@ -18,8 +18,8 @@ import {AstFactory} from '../../../src/ngtsc/translator/src/api/ast_factory'; * Generic translator helper class, which exposes methods for translating expressions and * statements. */ -export class Translator { - constructor(private factory: AstFactory) {} +export class Translator { + constructor(private factory: AstFactory) {} /** * Translate the given output AST in the context of an expression. @@ -30,7 +30,7 @@ export class Translator { options: TranslatorOptions = {}, ): TExpression { return expression.visitExpression( - new ExpressionTranslatorVisitor( + new ExpressionTranslatorVisitor( this.factory, imports, null, @@ -49,7 +49,7 @@ export class Translator { options: TranslatorOptions = {}, ): TStatement { return statement.visitStatement( - new ExpressionTranslatorVisitor( + new ExpressionTranslatorVisitor( this.factory, imports, null, diff --git a/packages/compiler-cli/linker/src/linker_import_generator.ts b/packages/compiler-cli/linker/src/linker_import_generator.ts index 59b95d5afc3f..0a102d84c2fb 100644 --- a/packages/compiler-cli/linker/src/linker_import_generator.ts +++ b/packages/compiler-cli/linker/src/linker_import_generator.ts @@ -17,11 +17,11 @@ import {FatalLinkerError} from './fatal_linker_error'; * must be achieved by property access on an `ng` namespace identifier, which is passed in via the * constructor. */ -export class LinkerImportGenerator +export class LinkerImportGenerator implements ImportGenerator { constructor( - private factory: AstFactory, + private factory: AstFactory, private ngImport: TExpression, ) {} diff --git a/packages/compiler-cli/linker/test/file_linker/emit_scopes/emit_scope_spec.ts b/packages/compiler-cli/linker/test/file_linker/emit_scopes/emit_scope_spec.ts index dd7e6589dfd1..f13f447c5cdf 100644 --- a/packages/compiler-cli/linker/test/file_linker/emit_scopes/emit_scope_spec.ts +++ b/packages/compiler-cli/linker/test/file_linker/emit_scopes/emit_scope_spec.ts @@ -17,9 +17,13 @@ describe('EmitScope', () => { describe('translateDefinition()', () => { it('should translate the given output AST into a TExpression', () => { const factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false); - const translator = new Translator(factory); + const translator = new Translator(factory); const ngImport = factory.createIdentifier('core'); - const emitScope = new EmitScope(ngImport, translator, factory); + const emitScope = new EmitScope( + ngImport, + translator, + factory, + ); const def = emitScope.translateDefinition({ expression: o.fn([], [], null, null, 'foo'), @@ -30,9 +34,13 @@ describe('EmitScope', () => { it('should use an IIFE if the definition being emitted includes associated statements', () => { const factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false); - const translator = new Translator(factory); + const translator = new Translator(factory); const ngImport = factory.createIdentifier('core'); - const emitScope = new EmitScope(ngImport, translator, factory); + const emitScope = new EmitScope( + ngImport, + translator, + factory, + ); const def = emitScope.translateDefinition({ expression: o.fn([], [], null, null, 'foo'), @@ -43,9 +51,13 @@ describe('EmitScope', () => { it('should use the `ngImport` identifier for imports when translating', () => { const factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false); - const translator = new Translator(factory); + const translator = new Translator(factory); const ngImport = factory.createIdentifier('core'); - const emitScope = new EmitScope(ngImport, translator, factory); + const emitScope = new EmitScope( + ngImport, + translator, + factory, + ); const coreImportRef = new o.ExternalReference('@angular/core', 'foo'); const def = emitScope.translateDefinition({ @@ -57,9 +69,13 @@ describe('EmitScope', () => { it('should not emit any shared constants in the replacement expression', () => { const factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false); - const translator = new Translator(factory); + const translator = new Translator(factory); const ngImport = factory.createIdentifier('core'); - const emitScope = new EmitScope(ngImport, translator, factory); + const emitScope = new EmitScope( + ngImport, + translator, + factory, + ); const constArray = o.literalArr([o.literal('CONST')]); // We have to add the constant twice or it will not create a shared statement @@ -77,9 +93,13 @@ describe('EmitScope', () => { describe('getConstantStatements()', () => { it('should return any constant statements that were added to the `constantPool`', () => { const factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false); - const translator = new Translator(factory); + const translator = new Translator(factory); const ngImport = factory.createIdentifier('core'); - const emitScope = new EmitScope(ngImport, translator, factory); + const emitScope = new EmitScope( + ngImport, + translator, + factory, + ); const constArray = o.literalArr([o.literal('CONST')]); // We have to add the constant twice or it will not create a shared statement diff --git a/packages/compiler-cli/linker/test/file_linker/emit_scopes/local_emit_scope_spec.ts b/packages/compiler-cli/linker/test/file_linker/emit_scopes/local_emit_scope_spec.ts index d38f242b7698..2f95d9e8928a 100644 --- a/packages/compiler-cli/linker/test/file_linker/emit_scopes/local_emit_scope_spec.ts +++ b/packages/compiler-cli/linker/test/file_linker/emit_scopes/local_emit_scope_spec.ts @@ -17,9 +17,9 @@ describe('LocalEmitScope', () => { describe('translateDefinition()', () => { it('should translate the given output AST into a TExpression, wrapped in an IIFE', () => { const factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false); - const translator = new Translator(factory); + const translator = new Translator(factory); const ngImport = factory.createIdentifier('core'); - const emitScope = new LocalEmitScope( + const emitScope = new LocalEmitScope( ngImport, translator, factory, @@ -37,9 +37,9 @@ describe('LocalEmitScope', () => { it('should use the `ngImport` identifier for imports when translating', () => { const factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false); - const translator = new Translator(factory); + const translator = new Translator(factory); const ngImport = factory.createIdentifier('core'); - const emitScope = new LocalEmitScope( + const emitScope = new LocalEmitScope( ngImport, translator, factory, @@ -58,9 +58,9 @@ describe('LocalEmitScope', () => { it('should not emit an IIFE if there are no shared constants', () => { const factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false); - const translator = new Translator(factory); + const translator = new Translator(factory); const ngImport = factory.createIdentifier('core'); - const emitScope = new LocalEmitScope( + const emitScope = new LocalEmitScope( ngImport, translator, factory, @@ -77,9 +77,9 @@ describe('LocalEmitScope', () => { describe('getConstantStatements()', () => { it('should throw an error', () => { const factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false); - const translator = new Translator(factory); + const translator = new Translator(factory); const ngImport = factory.createIdentifier('core'); - const emitScope = new LocalEmitScope( + const emitScope = new LocalEmitScope( ngImport, translator, factory, diff --git a/packages/compiler-cli/linker/test/file_linker/file_linker_spec.ts b/packages/compiler-cli/linker/test/file_linker/file_linker_spec.ts index 6b2a79c7ea55..b2b347c4b96b 100644 --- a/packages/compiler-cli/linker/test/file_linker/file_linker_spec.ts +++ b/packages/compiler-cli/linker/test/file_linker/file_linker_spec.ts @@ -264,22 +264,23 @@ describe('FileLinker', () => { function createFileLinker(code = '// test code'): { host: AstHost; - fileLinker: FileLinker; + fileLinker: FileLinker; } { const fs = new MockFileSystemNative(); const logger = new MockLogger(); - const linkerEnvironment = LinkerEnvironment.create( + const linkerEnvironment = LinkerEnvironment.create( fs, logger, new TypeScriptAstHost(), new TypeScriptAstFactory(/* annotateForClosureCompiler */ false), DEFAULT_LINKER_OPTIONS, ); - const fileLinker = new FileLinker( - linkerEnvironment, - fs.resolve('/test.js'), - code, - ); + const fileLinker = new FileLinker< + MockConstantScopeRef, + ts.Statement, + ts.Expression, + ts.TypeNode + >(linkerEnvironment, fs.resolve('/test.js'), code); return {host: linkerEnvironment.host, fileLinker}; } }); diff --git a/packages/compiler-cli/linker/test/file_linker/translator_spec.ts b/packages/compiler-cli/linker/test/file_linker/translator_spec.ts index 99714e4b4e41..71b09dffc88f 100644 --- a/packages/compiler-cli/linker/test/file_linker/translator_spec.ts +++ b/packages/compiler-cli/linker/test/file_linker/translator_spec.ts @@ -17,16 +17,19 @@ import {generate} from './helpers'; describe('Translator', () => { const ngImport = ts.factory.createIdentifier('ngImport'); let factory: TypeScriptAstFactory; - let importGenerator: LinkerImportGenerator; + let importGenerator: LinkerImportGenerator; beforeEach(() => { factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false); - importGenerator = new LinkerImportGenerator(factory, ngImport); + importGenerator = new LinkerImportGenerator( + factory, + ngImport, + ); }); describe('translateExpression()', () => { it('should generate expression specific output', () => { - const translator = new Translator(factory); + const translator = new Translator(factory); const outputAst = o.variable('foo').set(o.literal(42)); const translated = translator.translateExpression(outputAst, importGenerator); expect(generate(translated)).toEqual('foo = 42'); @@ -35,7 +38,7 @@ describe('Translator', () => { describe('translateStatement()', () => { it('should generate statement specific output', () => { - const translator = new Translator(factory); + const translator = new Translator(factory); const outputAst = new o.ExpressionStatement(o.variable('foo').set(o.literal(42))); const translated = translator.translateStatement(outputAst, importGenerator); expect(generate(translated)).toEqual('foo = 42;'); diff --git a/packages/compiler-cli/linker/test/linker_import_generator_spec.ts b/packages/compiler-cli/linker/test/linker_import_generator_spec.ts index 6267496f0e6c..fc40612a887e 100644 --- a/packages/compiler-cli/linker/test/linker_import_generator_spec.ts +++ b/packages/compiler-cli/linker/test/linker_import_generator_spec.ts @@ -15,7 +15,7 @@ const ngImport = ts.factory.createIdentifier('ngImport'); describe('LinkerImportGenerator', () => { describe('generateNamespaceImport()', () => { it('should error if the import is not `@angular/core`', () => { - const generator = new LinkerImportGenerator( + const generator = new LinkerImportGenerator( new TypeScriptAstFactory(false), ngImport, ); @@ -30,7 +30,7 @@ describe('LinkerImportGenerator', () => { }); it('should return the ngImport expression for `@angular/core`', () => { - const generator = new LinkerImportGenerator( + const generator = new LinkerImportGenerator( new TypeScriptAstFactory(false), ngImport, ); @@ -47,7 +47,7 @@ describe('LinkerImportGenerator', () => { describe('generateNamedImport()', () => { it('should error if the import is not `@angular/core`', () => { - const generator = new LinkerImportGenerator( + const generator = new LinkerImportGenerator( new TypeScriptAstFactory(false), ngImport, ); @@ -62,7 +62,7 @@ describe('LinkerImportGenerator', () => { }); it('should return a `NamedImport` object containing the ngImport expression', () => { - const generator = new LinkerImportGenerator( + const generator = new LinkerImportGenerator( new TypeScriptAstFactory(false), ngImport, ); diff --git a/packages/compiler-cli/src/ngtsc/translator/src/api/ast_factory.ts b/packages/compiler-cli/src/ngtsc/translator/src/api/ast_factory.ts index cb9dc984f55f..cd708924abaf 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/api/ast_factory.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/api/ast_factory.ts @@ -13,7 +13,7 @@ * It is up to the caller to do this - e.g. only call `createTaggedTemplate()` or pass `let`|`const` * to `createVariableDeclaration()` if the final JS will allow it. */ -export interface AstFactory { +export interface AstFactory { /** * Attach the `leadingComments` to the given `statement` node. * diff --git a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts index 517fd26790a2..9fbb46161fa3 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts @@ -69,15 +69,15 @@ export interface TranslatorOptions { annotateForClosureCompiler?: boolean; } -export class ExpressionTranslatorVisitor - implements o.ExpressionVisitor, o.StatementVisitor +export class ExpressionTranslatorVisitor + implements o.ExpressionVisitor, o.StatementVisitor, o.TypeVisitor { private downlevelTaggedTemplates: boolean; private downlevelVariableDeclarations: boolean; private recordWrappedNode: RecordWrappedNodeFn; constructor( - private factory: AstFactory, + private factory: AstFactory, private imports: ImportGenerator, private contextFile: TFile, options: TranslatorOptions, @@ -231,6 +231,26 @@ export class ExpressionTranslatorVisitor ); } + visitBuiltinType(type: o.BuiltinType, context: Context): TType { + throw new Error('Method not implemented'); + } + + visitExpressionType(type: o.ExpressionType, context: Context): TType { + throw new Error('Method not implemented'); + } + + visitArrayType(type: o.ArrayType, context: Context): TType { + throw new Error('Method not implemented'); + } + + visitMapType(type: o.MapType, context: Context): TType { + throw new Error('Method not implemented'); + } + + visitTransplantedType(type: o.TransplantedType, context: Context): TType { + throw new Error('Method not implemented'); + } + private createTaggedTemplateExpression( tag: TExpression, template: TemplateLiteral, diff --git a/packages/compiler-cli/src/ngtsc/translator/src/typescript_ast_factory.ts b/packages/compiler-cli/src/ngtsc/translator/src/typescript_ast_factory.ts index 50b69c0c26ea..ddd0e49f353c 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/typescript_ast_factory.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/typescript_ast_factory.ts @@ -37,7 +37,7 @@ enum PureAnnotation { /** * A TypeScript flavoured implementation of the AstFactory. */ -export class TypeScriptAstFactory implements AstFactory { +export class TypeScriptAstFactory implements AstFactory { private externalSourceFiles = new Map(); private readonly UNARY_OPERATORS: Record = diff --git a/packages/compiler-cli/src/ngtsc/translator/src/typescript_translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/typescript_translator.ts index a6aef153dee2..8d0051e5b025 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/typescript_translator.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/typescript_translator.ts @@ -21,7 +21,7 @@ export function translateExpression( options: TranslatorOptions = {}, ): ts.Expression { return expression.visitExpression( - new ExpressionTranslatorVisitor( + new ExpressionTranslatorVisitor( new TypeScriptAstFactory(options.annotateForClosureCompiler === true), imports, contextFile, @@ -38,7 +38,7 @@ export function translateStatement( options: TranslatorOptions = {}, ): ts.Statement { return statement.visitStatement( - new ExpressionTranslatorVisitor( + new ExpressionTranslatorVisitor( new TypeScriptAstFactory(options.annotateForClosureCompiler === true), imports, contextFile, From 76e75841866f1adaf9c0d692db5eb42416a49a12 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 11 Mar 2026 14:46:34 +0100 Subject: [PATCH 2/4] refactor(compiler-cli): add type nodes to translator Updates the translator and AST factories to account for type nodes. --- .../ngtsc/translator/src/api/ast_factory.ts | 60 ++++++++++++++-- .../src/ngtsc/translator/src/translator.ts | 71 +++++++++++++++---- 2 files changed, 113 insertions(+), 18 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/translator/src/api/ast_factory.ts b/packages/compiler-cli/src/ngtsc/translator/src/api/ast_factory.ts index cd708924abaf..9d2b1783daab 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/api/ast_factory.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/api/ast_factory.ts @@ -104,7 +104,7 @@ export interface AstFactory { */ createFunctionDeclaration( functionName: string, - parameters: string[], + parameters: Parameter[], body: TStatement, ): TStatement; @@ -118,7 +118,7 @@ export interface AstFactory { */ createFunctionExpression( functionName: string | null, - parameters: string[], + parameters: Parameter[], body: TStatement, ): TExpression; @@ -129,7 +129,10 @@ export interface AstFactory { * @param parameters the names of the function's parameters. * @param body an expression or block of statements that are the body of the function. */ - createArrowFunctionExpression(parameters: string[], body: TExpression | TStatement): TExpression; + createArrowFunctionExpression( + parameters: Parameter[], + body: TExpression | TStatement, + ): TExpression; /** * Creates an expression that represents a dynamic import @@ -264,12 +267,13 @@ export interface AstFactory { * * @param variableName the name of the variable. * @param initializer if not `null` then this expression is assigned to the declared variable. - * @param type whether this variable should be declared as `var`, `let` or `const`. + * @param variableType whether this variable should be declared as `var`, `let` or `const`. */ createVariableDeclaration( variableName: string, initializer: TExpression | null, - type: VariableDeclarationType, + variableType: VariableDeclarationType, + type: TType | null, ): TStatement; /** @@ -287,6 +291,37 @@ export interface AstFactory { */ createSpreadElement(expression: TExpression): TExpression; + /** + * Create a type node for a built-in type. + * @param type Type that should be created. + */ + createBuiltInType(type: BuiltInType): TType; + + /** + * Create an expression type. + * @param expression Expression to be turned into a type node. + * @param typeParams Type parameters for the expression. + */ + createExpressionType(expression: TExpression, typeParams: TType[] | null): TType; + + /** + * Create an array type. + * @param elementType Type of the array elements. + */ + createArrayType(elementType: TType): TType; + + /** + * Create a map type. + * @param valueType Type of the map values. + */ + createMapType(valueType: TType): TType; + + /** + * Forward a transplanted type. + * @param type Type to be transplanted, if supported. + */ + transplantType(type: TType): TType; + /** * Attach a source map range to the given node. * @@ -310,6 +345,21 @@ export type VariableDeclarationType = 'const' | 'let' | 'var'; */ export type UnaryOperator = '+' | '-' | '!'; +/** Supported built-in types. */ +export type BuiltInType = + | 'any' + | 'boolean' + | 'number' + | 'string' + | 'function' + | 'never' + | 'unknown'; + +export interface Parameter { + name: string; + type: TType | null; +} + /** * The binary operators supported by the `AstFactory`. */ diff --git a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts index 9fbb46161fa3..60a07cdb8cf1 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts @@ -10,9 +10,11 @@ import * as o from '@angular/compiler'; import { AstFactory, BinaryOperator, + BuiltInType, ObjectLiteralAssignment, ObjectLiteralProperty, ObjectLiteralSpread, + Parameter, SourceMapRange, TemplateElement, TemplateLiteral, @@ -98,6 +100,7 @@ export class ExpressionTranslatorVisitor stmt.name, stmt.value?.visitExpression(this, context.withExpressionMode), varType, + stmt.type?.visitType(this, context), ), stmt.leadingComments, ); @@ -107,7 +110,7 @@ export class ExpressionTranslatorVisitor return this.attachComments( this.factory.createFunctionDeclaration( stmt.name, - stmt.params.map((param) => param.name), + this.translateParams(stmt.params, context), this.factory.createBlock(this.visitStatements(stmt.statements, context.withStatementMode)), ), stmt.leadingComments, @@ -231,24 +234,59 @@ export class ExpressionTranslatorVisitor ); } - visitBuiltinType(type: o.BuiltinType, context: Context): TType { - throw new Error('Method not implemented'); + visitBuiltinType(ast: o.BuiltinType): TType | null { + let builtInType: BuiltInType; + + switch (ast.name) { + case o.BuiltinTypeName.Bool: + builtInType = 'boolean'; + break; + case o.BuiltinTypeName.String: + builtInType = 'string'; + break; + case o.BuiltinTypeName.Dynamic: + builtInType = 'any'; + break; + case o.BuiltinTypeName.Number: + case o.BuiltinTypeName.Int: + builtInType = 'number'; + break; + case o.BuiltinTypeName.Function: + builtInType = 'function'; + break; + case o.BuiltinTypeName.None: + builtInType = 'never'; + break; + case o.BuiltinTypeName.Inferred: + return null; + } + + return this.factory.createBuiltInType(builtInType); } - visitExpressionType(type: o.ExpressionType, context: Context): TType { - throw new Error('Method not implemented'); + visitExpressionType(ast: o.ExpressionType, context: Context): TType { + return this.factory.createExpressionType( + ast.value.visitExpression(this, context), + ast.typeParams === null || ast.typeParams.length === 0 + ? null + : ast.typeParams.map((param) => param.visitType(this, context)), + ); } - visitArrayType(type: o.ArrayType, context: Context): TType { - throw new Error('Method not implemented'); + visitArrayType(ast: o.ArrayType, context: Context): TType { + return this.factory.createArrayType(ast.of.visitType(this, context)); } - visitMapType(type: o.MapType, context: Context): TType { - throw new Error('Method not implemented'); + visitMapType(ast: o.MapType, context: Context): TType { + const valueType = + ast.valueType === null + ? this.factory.createBuiltInType('unknown') + : ast.valueType.visitType(this, context); + return this.factory.createMapType(valueType); } - visitTransplantedType(type: o.TransplantedType, context: Context): TType { - throw new Error('Method not implemented'); + visitTransplantedType(type: o.TransplantedType): TType { + return this.factory.transplantType(type.type); } private createTaggedTemplateExpression( @@ -356,14 +394,14 @@ export class ExpressionTranslatorVisitor visitFunctionExpr(ast: o.FunctionExpr, context: Context): TExpression { return this.factory.createFunctionExpression( ast.name ?? null, - ast.params.map((param) => param.name), + this.translateParams(ast.params, context), this.factory.createBlock(this.visitStatements(ast.statements, context)), ); } visitArrowFunctionExpr(ast: o.ArrowFunctionExpr, context: any) { return this.factory.createArrowFunctionExpression( - ast.params.map((param) => param.name), + this.translateParams(ast.params, context), Array.isArray(ast.body) ? this.factory.createBlock(this.visitStatements(ast.body, context)) : ast.body.visitExpression(this, context), @@ -507,6 +545,13 @@ export class ExpressionTranslatorVisitor expressions: ast.expressions.map((e) => e.visitExpression(this, context)), }; } + + private translateParams(params: o.outputAst.FnParam[], context: Context): Parameter[] { + return params.map((param) => ({ + name: param.name, + type: param.type?.visitType(this, context), + })); + } } /** From 8dcd6363cf6a154d3e1cdc05a56f7a7c86a8d99f Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 11 Mar 2026 14:48:06 +0100 Subject: [PATCH 3/4] refactor(compiler-cli): update ast factories to account for type nodes Updates the Babel and TypeScript AST factories to account to produce type nodes. --- .../linker/babel/src/ast/babel_ast_factory.ts | 94 +++++++++++-- .../babel/test/ast/babel_ast_factory_spec.ts | 121 ++++++++++++++-- .../translator/src/typescript_ast_factory.ts | 102 ++++++++++++-- .../test/typescript_ast_factory_spec.ts | 131 ++++++++++++++++-- 4 files changed, 402 insertions(+), 46 deletions(-) diff --git a/packages/compiler-cli/linker/babel/src/ast/babel_ast_factory.ts b/packages/compiler-cli/linker/babel/src/ast/babel_ast_factory.ts index fc868464eaeb..6e40fdd6c500 100644 --- a/packages/compiler-cli/linker/babel/src/ast/babel_ast_factory.ts +++ b/packages/compiler-cli/linker/babel/src/ast/babel_ast_factory.ts @@ -11,8 +11,10 @@ import {assert} from '../../../../linker'; import { AstFactory, BinaryOperator, + BuiltInType, LeadingComment, ObjectLiteralProperty, + Parameter, SourceMapRange, TemplateLiteral, VariableDeclarationType, @@ -104,40 +106,40 @@ export class BabelAstFactory implements AstFactory< createFunctionDeclaration( functionName: string, - parameters: string[], + parameters: Parameter[], body: t.Statement, ): t.Statement { assert(body, t.isBlockStatement, 'a block'); return t.functionDeclaration( t.identifier(functionName), - parameters.map((param) => t.identifier(param)), + parameters.map((param) => identifierWithType(param.name, param.type)), body, ); } createArrowFunctionExpression( - parameters: string[], + parameters: Parameter[], body: t.Statement | t.Expression, ): t.Expression { if (t.isStatement(body)) { assert(body, t.isBlockStatement, 'a block'); } return t.arrowFunctionExpression( - parameters.map((param) => t.identifier(param)), + parameters.map((param) => identifierWithType(param.name, param.type)), body, ); } createFunctionExpression( functionName: string | null, - parameters: string[], + parameters: Parameter[], body: t.Statement, ): t.Expression { assert(body, t.isBlockStatement, 'a block'); const name = functionName !== null ? t.identifier(functionName) : null; return t.functionExpression( name, - parameters.map((param) => t.identifier(param)), + parameters.map((param) => identifierWithType(param.name, param.type)), body, ); } @@ -228,10 +230,11 @@ export class BabelAstFactory implements AstFactory< createVariableDeclaration( variableName: string, initializer: t.Expression | null, - type: VariableDeclarationType, + variableType: VariableDeclarationType, + type: t.TSType | null, ): t.Statement { - return t.variableDeclaration(type, [ - t.variableDeclarator(t.identifier(variableName), initializer), + return t.variableDeclaration(variableType, [ + t.variableDeclarator(identifierWithType(variableName, type), initializer), ]); } @@ -265,6 +268,79 @@ export class BabelAstFactory implements AstFactory< return node; } + + createBuiltInType(type: BuiltInType): t.TSType { + switch (type) { + case 'any': + return t.tsAnyKeyword(); + case 'boolean': + return t.tsBooleanKeyword(); + case 'number': + return t.tsNumberKeyword(); + case 'string': + return t.tsStringKeyword(); + case 'function': + return t.tsTypeReference(t.identifier('Function')); + case 'never': + return t.tsNeverKeyword(); + case 'unknown': + return t.tsUnknownKeyword(); + } + } + + createExpressionType(expression: t.Expression, typeParams: t.TSType[] | null): t.TSType { + const typeName = getEntityTypeFromExpression(expression); + return t.tsTypeReference( + typeName, + typeParams ? t.tsTypeParameterInstantiation(typeParams) : null, + ); + } + + createArrayType(elementType: t.TSType): t.TSType { + return t.tsArrayType(elementType); + } + + createMapType(valueType: t.TSType): t.TSType { + const keySignature = identifierWithType('key', this.createBuiltInType('string')); + return t.tsTypeLiteral([t.tsIndexSignature([keySignature], t.tsTypeAnnotation(valueType))]); + } + + transplantType(type: t.TSType): t.TSType { + if (t.isNode(type) && t.isTSType(type)) { + return type; + } + throw new Error('Attempting to transplant a type node from a non-Babel AST: ' + type); + } +} + +function getEntityTypeFromExpression(expression: t.Expression): t.Identifier | t.TSQualifiedName { + if (t.isIdentifier(expression)) { + return expression; + } + + if (t.isMemberExpression(expression)) { + const left = getEntityTypeFromExpression(expression.object); + + if (!t.isIdentifier(expression.property)) { + throw new Error( + `Unsupported property access for type reference: ${expression.property.type}`, + ); + } + + return t.tsQualifiedName(left, expression.property); + } + + throw new Error(`Unsupported expression for type reference: ${expression.type}`); +} + +function identifierWithType(name: string, type: t.TSType | null): t.Identifier { + const node = t.identifier(name); + + if (type !== null) { + node.typeAnnotation = t.tsTypeAnnotation(type); + } + + return node; } function isLExpression(expr: t.Expression): expr is Extract { diff --git a/packages/compiler-cli/linker/babel/test/ast/babel_ast_factory_spec.ts b/packages/compiler-cli/linker/babel/test/ast/babel_ast_factory_spec.ts index de749ac9a2c9..d03642472723 100644 --- a/packages/compiler-cli/linker/babel/test/ast/babel_ast_factory_spec.ts +++ b/packages/compiler-cli/linker/babel/test/ast/babel_ast_factory_spec.ts @@ -149,9 +149,16 @@ describe('BabelAstFactory', () => { describe('createFunctionDeclaration()', () => { it('should create a function declaration node with the given name, parameters and body statements', () => { const stmts = statement.ast`{x = 10; y = 20;}`; - const fn = factory.createFunctionDeclaration('foo', ['arg1', 'arg2'], stmts); + const fn = factory.createFunctionDeclaration( + 'foo', + [ + {name: 'arg1', type: null}, + {name: 'arg2', type: factory.createBuiltInType('number')}, + ], + stmts, + ); expect(generate(fn).code).toEqual( - ['function foo(arg1, arg2) {', ' x = 10;', ' y = 20;', '}'].join('\n'), + ['function foo(arg1, arg2: number) {', ' x = 10;', ' y = 20;', '}'].join('\n'), ); }); }); @@ -159,18 +166,32 @@ describe('BabelAstFactory', () => { describe('createFunctionExpression()', () => { it('should create a function expression node with the given name, parameters and body statements', () => { const stmts = statement.ast`{x = 10; y = 20;}`; - const fn = factory.createFunctionExpression('foo', ['arg1', 'arg2'], stmts); + const fn = factory.createFunctionExpression( + 'foo', + [ + {name: 'arg1', type: null}, + {name: 'arg2', type: factory.createBuiltInType('number')}, + ], + stmts, + ); expect(t.isStatement(fn)).toBe(false); expect(generate(fn).code).toEqual( - ['function foo(arg1, arg2) {', ' x = 10;', ' y = 20;', '}'].join('\n'), + ['function foo(arg1, arg2: number) {', ' x = 10;', ' y = 20;', '}'].join('\n'), ); }); it('should create an anonymous function expression node if the name is null', () => { const stmts = statement.ast`{x = 10; y = 20;}`; - const fn = factory.createFunctionExpression(null, ['arg1', 'arg2'], stmts); + const fn = factory.createFunctionExpression( + null, + [ + {name: 'arg1', type: null}, + {name: 'arg2', type: factory.createBuiltInType('number')}, + ], + stmts, + ); expect(generate(fn).code).toEqual( - ['function (arg1, arg2) {', ' x = 10;', ' y = 20;', '}'].join('\n'), + ['function (arg1, arg2: number) {', ' x = 10;', ' y = 20;', '}'].join('\n'), ); }); }); @@ -178,8 +199,14 @@ describe('BabelAstFactory', () => { describe('createArrowFunctionExpression()', () => { it('should create an arrow function with an implicit return if a single statement is provided', () => { const expr = expression.ast`arg2 + arg1`; - const fn = factory.createArrowFunctionExpression(['arg1', 'arg2'], expr); - expect(generate(fn).code).toEqual('(arg1, arg2) => arg2 + arg1'); + const fn = factory.createArrowFunctionExpression( + [ + {name: 'arg1', type: null}, + {name: 'arg2', type: factory.createBuiltInType('number')}, + ], + expr, + ); + expect(generate(fn).code).toEqual('(arg1, arg2: number) => arg2 + arg1'); }); it('should create an arrow function with an implicit return object literal', () => { @@ -190,9 +217,15 @@ describe('BabelAstFactory', () => { it('should create an arrow function with a body when an array of statements is provided', () => { const stmts = statement.ast`{x = 10; y = 20; return x + y;}`; - const fn = factory.createArrowFunctionExpression(['arg1', 'arg2'], stmts); + const fn = factory.createArrowFunctionExpression( + [ + {name: 'arg1', type: null}, + {name: 'arg2', type: factory.createBuiltInType('number')}, + ], + stmts, + ); expect(generate(fn).code).toEqual( - ['(arg1, arg2) => {', ' x = 10;', ' y = 20;', ' return x + y;', '}'].join('\n'), + ['(arg1, arg2: number) => {', ' x = 10;', ' y = 20;', ' return x + y;', '}'].join('\n'), ); }); }); @@ -378,26 +411,37 @@ describe('BabelAstFactory', () => { describe('createVariableDeclaration()', () => { it('should create a variable declaration statement node for the given variable name and initializer', () => { const initializer = expression.ast`42`; - const varDecl = factory.createVariableDeclaration('foo', initializer, 'let'); + const varDecl = factory.createVariableDeclaration('foo', initializer, 'let', null); expect(generate(varDecl).code).toEqual('let foo = 42;'); }); it('should create a constant declaration statement node for the given variable name and initializer', () => { const initializer = expression.ast`42`; - const varDecl = factory.createVariableDeclaration('foo', initializer, 'const'); + const varDecl = factory.createVariableDeclaration('foo', initializer, 'const', null); expect(generate(varDecl).code).toEqual('const foo = 42;'); }); it('should create a downleveled variable declaration statement node for the given variable name and initializer', () => { const initializer = expression.ast`42`; - const varDecl = factory.createVariableDeclaration('foo', initializer, 'var'); + const varDecl = factory.createVariableDeclaration('foo', initializer, 'var', null); expect(generate(varDecl).code).toEqual('var foo = 42;'); }); it('should create an uninitialized variable declaration statement node for the given variable name and a null initializer', () => { - const varDecl = factory.createVariableDeclaration('foo', null, 'let'); + const varDecl = factory.createVariableDeclaration('foo', null, 'let', null); expect(generate(varDecl).code).toEqual('let foo;'); }); + + it('should create a variable declaration with a type', () => { + const initializer = expression.ast`42`; + const varDecl = factory.createVariableDeclaration( + 'foo', + initializer, + 'let', + factory.createBuiltInType('number'), + ); + expect(generate(varDecl).code).toEqual('let foo: number = 42;'); + }); }); describe('createRegularExpressionLiteral()', () => { @@ -429,6 +473,55 @@ describe('BabelAstFactory', () => { }); }); + describe('createBuiltInType()', () => { + it('should create type annotations for built in types', () => { + expect(generate(factory.createBuiltInType('any')).code).toEqual('any'); + expect(generate(factory.createBuiltInType('boolean')).code).toEqual('boolean'); + expect(generate(factory.createBuiltInType('number')).code).toEqual('number'); + expect(generate(factory.createBuiltInType('string')).code).toEqual('string'); + expect(generate(factory.createBuiltInType('never')).code).toEqual('never'); + expect(generate(factory.createBuiltInType('unknown')).code).toEqual('unknown'); + expect(generate(factory.createBuiltInType('function')).code).toEqual('Function'); + }); + }); + + describe('createExpressionType()', () => { + it('should create a generic type annotation for an identifier', () => { + const id = factory.createIdentifier('MyType'); + const type = factory.createExpressionType(id, null); + expect(generate(type).code).toEqual('MyType'); + }); + + it('should create a generic type annotation for property access', () => { + const expr = factory.createPropertyAccess(factory.createIdentifier('ns'), 'MyType'); + const type = factory.createExpressionType(expr, null); + expect(generate(type).code).toEqual('ns.MyType'); + }); + + it('should create a generic type annotation with type parameters', () => { + const id = factory.createIdentifier('MyType'); + const typeParam = factory.createBuiltInType('string'); + const type = factory.createExpressionType(id, [typeParam]); + expect(generate(type).code).toEqual('MyType'); + }); + }); + + describe('createArrayType()', () => { + it('should create an array type annotation', () => { + const elementType = factory.createBuiltInType('string'); + const type = factory.createArrayType(elementType); + expect(generate(type).code).toEqual('string[]'); + }); + }); + + describe('createMapType()', () => { + it('should create an object type with an indexer', () => { + const valueType = factory.createBuiltInType('number'); + const type = factory.createMapType(valueType); + expect(generate(type).code).toEqual('{\n [key: string]: number;\n}'); + }); + }); + describe('setSourceMapRange()', () => { it('should attach the `sourceMapRange` to the given `node`', () => { const expr = expression.ast`42`; diff --git a/packages/compiler-cli/src/ngtsc/translator/src/typescript_ast_factory.ts b/packages/compiler-cli/src/ngtsc/translator/src/typescript_ast_factory.ts index ddd0e49f353c..bf670f48995f 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/typescript_ast_factory.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/typescript_ast_factory.ts @@ -10,8 +10,10 @@ import ts from 'typescript'; import { AstFactory, BinaryOperator, + BuiltInType, LeadingComment, ObjectLiteralProperty, + Parameter, SourceMapRange, TemplateLiteral, UnaryOperator, @@ -160,7 +162,7 @@ export class TypeScriptAstFactory implements AstFactory[], body: ts.Statement, ): ts.Statement { if (!ts.isBlock(body)) { @@ -171,7 +173,7 @@ export class TypeScriptAstFactory implements AstFactory ts.factory.createParameterDeclaration(undefined, undefined, param)), + parameters.map((param) => this.createParameter(param)), undefined, body, ); @@ -179,7 +181,7 @@ export class TypeScriptAstFactory implements AstFactory[], body: ts.Statement, ): ts.Expression { if (!ts.isBlock(body)) { @@ -190,14 +192,14 @@ export class TypeScriptAstFactory implements AstFactory ts.factory.createParameterDeclaration(undefined, undefined, param)), + parameters.map((param) => this.createParameter(param)), undefined, body, ); } createArrowFunctionExpression( - parameters: string[], + parameters: Parameter[], body: ts.Statement | ts.Expression, ): ts.Expression { if (ts.isStatement(body) && !ts.isBlock(body)) { @@ -207,13 +209,23 @@ export class TypeScriptAstFactory implements AstFactory ts.factory.createParameterDeclaration(undefined, undefined, param)), + parameters.map((param) => this.createParameter(param)), undefined, undefined, body, ); } + private createParameter(param: Parameter): ts.ParameterDeclaration { + return ts.factory.createParameterDeclaration( + undefined, + undefined, + param.name, + undefined, + param.type ?? undefined, + ); + } + createIdentifier = ts.factory.createIdentifier; createIfStatement( @@ -330,7 +342,8 @@ export class TypeScriptAstFactory implements AstFactory { items: [body], generate, } = setupStatements('{x = 10; y = 20;}'); - const fn = factory.createFunctionDeclaration('foo', ['arg1', 'arg2'], body); - expect(generate(fn)).toEqual('function foo(arg1, arg2) { x = 10; y = 20; }'); + const fn = factory.createFunctionDeclaration( + 'foo', + [ + {name: 'arg1', type: null}, + {name: 'arg2', type: factory.createBuiltInType('number')}, + ], + body, + ); + expect(generate(fn)).toEqual('function foo(arg1, arg2: number) { x = 10; y = 20; }'); }); }); @@ -189,9 +196,16 @@ describe('TypeScriptAstFactory', () => { items: [body], generate, } = setupStatements('{x = 10; y = 20;}'); - const fn = factory.createFunctionExpression('foo', ['arg1', 'arg2'], body); + const fn = factory.createFunctionExpression( + 'foo', + [ + {name: 'arg1', type: null}, + {name: 'arg2', type: factory.createBuiltInType('number')}, + ], + body, + ); expect(ts.isExpressionStatement(fn)).toBe(false); - expect(generate(fn)).toEqual('function foo(arg1, arg2) { x = 10; y = 20; }'); + expect(generate(fn)).toEqual('function foo(arg1, arg2: number) { x = 10; y = 20; }'); }); it('should create an anonymous function expression node if the name is null', () => { @@ -199,8 +213,15 @@ describe('TypeScriptAstFactory', () => { items: [body], generate, } = setupStatements('{x = 10; y = 20;}'); - const fn = factory.createFunctionExpression(null, ['arg1', 'arg2'], body); - expect(generate(fn)).toEqual('function (arg1, arg2) { x = 10; y = 20; }'); + const fn = factory.createFunctionExpression( + null, + [ + {name: 'arg1', type: null}, + {name: 'arg2', type: factory.createBuiltInType('number')}, + ], + body, + ); + expect(generate(fn)).toEqual('function (arg1, arg2: number) { x = 10; y = 20; }'); }); }); @@ -242,8 +263,14 @@ describe('TypeScriptAstFactory', () => { items: [body], generate, } = setupExpressions('arg2 + arg1'); - const fn = factory.createArrowFunctionExpression(['arg1', 'arg2'], body); - expect(generate(fn)).toEqual('(arg1, arg2) => arg2 + arg1'); + const fn = factory.createArrowFunctionExpression( + [ + {name: 'arg1', type: null}, + {name: 'arg2', type: factory.createBuiltInType('number')}, + ], + body, + ); + expect(generate(fn)).toEqual('(arg1, arg2: number) => arg2 + arg1'); }); it('should create an arrow function with an implicit return object literal', () => { @@ -260,8 +287,14 @@ describe('TypeScriptAstFactory', () => { items: [body], generate, } = setupStatements('{x = 10; y = 20; return x + y;}'); - const fn = factory.createArrowFunctionExpression(['arg1', 'arg2'], body); - expect(generate(fn)).toEqual('(arg1, arg2) => { x = 10; y = 20; return x + y; }'); + const fn = factory.createArrowFunctionExpression( + [ + {name: 'arg1', type: null}, + {name: 'arg2', type: factory.createBuiltInType('number')}, + ], + body, + ); + expect(generate(fn)).toEqual('(arg1, arg2: number) => { x = 10; y = 20; return x + y; }'); }); }); @@ -447,7 +480,7 @@ describe('TypeScriptAstFactory', () => { items: [initializer], generate, } = setupExpressions(`42`); - const varDecl = factory.createVariableDeclaration('foo', initializer, 'let'); + const varDecl = factory.createVariableDeclaration('foo', initializer, 'let', null); expect(generate(varDecl)).toEqual('let foo = 42;'); }); @@ -456,7 +489,7 @@ describe('TypeScriptAstFactory', () => { items: [initializer], generate, } = setupExpressions(`42`); - const varDecl = factory.createVariableDeclaration('foo', initializer, 'const'); + const varDecl = factory.createVariableDeclaration('foo', initializer, 'const', null); expect(generate(varDecl)).toEqual('const foo = 42;'); }); @@ -465,15 +498,29 @@ describe('TypeScriptAstFactory', () => { items: [initializer], generate, } = setupExpressions(`42`); - const varDecl = factory.createVariableDeclaration('foo', initializer, 'var'); + const varDecl = factory.createVariableDeclaration('foo', initializer, 'var', null); expect(generate(varDecl)).toEqual('var foo = 42;'); }); it('should create an uninitialized variable declaration statement node for the given variable name and a null initializer', () => { const {generate} = setupStatements(); - const varDecl = factory.createVariableDeclaration('foo', null, 'let'); + const varDecl = factory.createVariableDeclaration('foo', null, 'let', null); expect(generate(varDecl)).toEqual('let foo;'); }); + + it('should create a variable declaration with a type', () => { + const { + items: [initializer], + generate, + } = setupExpressions(`42`); + const varDecl = factory.createVariableDeclaration( + 'foo', + initializer, + 'let', + factory.createBuiltInType('number'), + ); + expect(generate(varDecl)).toEqual('let foo: number = 42;'); + }); }); describe('createRegularExpressionLiteral()', () => { @@ -509,6 +556,62 @@ describe('TypeScriptAstFactory', () => { }); }); + describe('createBuiltInType()', () => { + it('should create keyword type nodes for simple types', () => { + const {generate} = setupStatements(); + expect(generate(factory.createBuiltInType('any'))).toEqual('any'); + expect(generate(factory.createBuiltInType('boolean'))).toEqual('boolean'); + expect(generate(factory.createBuiltInType('number'))).toEqual('number'); + expect(generate(factory.createBuiltInType('string'))).toEqual('string'); + expect(generate(factory.createBuiltInType('never'))).toEqual('never'); + expect(generate(factory.createBuiltInType('unknown'))).toEqual('unknown'); + }); + + it('should create a type reference for "function"', () => { + const {generate} = setupStatements(); + expect(generate(factory.createBuiltInType('function'))).toEqual('Function'); + }); + }); + + describe('createExpressionType()', () => { + it('should create a type reference for an identifier', () => { + const {items, generate} = setupExpressions('MyType'); + const type = factory.createExpressionType(items[0], null); + expect(generate(type)).toEqual('MyType'); + }); + + it('should create a type reference for a property access', () => { + const {items, generate} = setupExpressions('ns.MyType'); + const type = factory.createExpressionType(items[0], null); + expect(generate(type)).toEqual('ns.MyType'); + }); + + it('should create a type reference with type parameters', () => { + const {items, generate} = setupExpressions('MyType'); + const typeParam = factory.createBuiltInType('string'); + const type = factory.createExpressionType(items[0], [typeParam]); + expect(generate(type)).toEqual('MyType'); + }); + }); + + describe('createArrayType()', () => { + it('should create an array type node', () => { + const {generate} = setupStatements(); + const elementType = factory.createBuiltInType('string'); + const type = factory.createArrayType(elementType); + expect(generate(type)).toEqual('string[]'); + }); + }); + + describe('createMapType()', () => { + it('should create a type literal with an index signature', () => { + const {generate} = setupStatements(); + const valueType = factory.createBuiltInType('number'); + const type = factory.createMapType(valueType); + expect(generate(type)).toEqual('{\n [key: string]: number;\n}'); + }); + }); + describe('setSourceMapRange()', () => { it('should attach the `sourceMapRange` to the given `node`', () => { const { From 97e760d9faf98507cc70d5a88939ff04addfa575 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 11 Mar 2026 22:10:23 +0100 Subject: [PATCH 4/4] fix(compiler): ensure generated code compiles Initial pass to make sure some common cases produce code that compiles. --- .../linker/babel/src/ast/babel_ast_factory.ts | 40 ++++++----- .../linker/src/linker_import_generator.ts | 7 +- .../linker/test/ast/ast_value_spec.ts | 9 ++- .../isolated/isolated_compile_spec.ts | 12 ++-- .../test_cases/isolated/TEST_CASES.json | 68 +++++++++++++++++++ .../arrow_function/test.ngtypecheck.ts | 5 ++ .../isolated/arrow_function/test.ts | 9 +++ .../arrow_function/test_transformed.ts | 17 +++++ .../isolated/basic/test_transformed.ts | 24 +++++-- .../test_cases/isolated/embedded_view/test.ts | 9 +++ .../embedded_view/test_transformed.ts | 21 ++++++ .../event_listener/test.ngtypecheck.ts | 5 ++ .../isolated/event_listener/test.ts | 9 +++ .../event_listener/test_transformed.ts | 15 ++++ .../test_cases/isolated/queries/test.ts | 10 +++ .../isolated/queries/test_transformed.ts | 45 ++++++++++++ .../compiler/src/render3/r3_hmr_compiler.ts | 11 +-- .../src/render3/view/query_generation.ts | 6 +- .../src/template/pipeline/src/emit.ts | 4 +- .../src/template/pipeline/src/ingest.ts | 2 +- .../src/phases/pure_function_extraction.ts | 2 +- .../src/template/pipeline/src/phases/reify.ts | 12 +++- .../core/src/render3/instructions/queries.ts | 4 +- 23 files changed, 295 insertions(+), 51 deletions(-) create mode 100644 packages/compiler-cli/test/compliance/test_cases/isolated/arrow_function/test.ngtypecheck.ts create mode 100644 packages/compiler-cli/test/compliance/test_cases/isolated/arrow_function/test.ts create mode 100644 packages/compiler-cli/test/compliance/test_cases/isolated/arrow_function/test_transformed.ts create mode 100644 packages/compiler-cli/test/compliance/test_cases/isolated/embedded_view/test.ts create mode 100644 packages/compiler-cli/test/compliance/test_cases/isolated/embedded_view/test_transformed.ts create mode 100644 packages/compiler-cli/test/compliance/test_cases/isolated/event_listener/test.ngtypecheck.ts create mode 100644 packages/compiler-cli/test/compliance/test_cases/isolated/event_listener/test.ts create mode 100644 packages/compiler-cli/test/compliance/test_cases/isolated/event_listener/test_transformed.ts create mode 100644 packages/compiler-cli/test/compliance/test_cases/isolated/queries/test.ts create mode 100644 packages/compiler-cli/test/compliance/test_cases/isolated/queries/test_transformed.ts diff --git a/packages/compiler-cli/linker/babel/src/ast/babel_ast_factory.ts b/packages/compiler-cli/linker/babel/src/ast/babel_ast_factory.ts index 6e40fdd6c500..b8bf4a234431 100644 --- a/packages/compiler-cli/linker/babel/src/ast/babel_ast_factory.ts +++ b/packages/compiler-cli/linker/babel/src/ast/babel_ast_factory.ts @@ -28,10 +28,14 @@ export class BabelAstFactory implements AstFactory< t.Expression | t.SpreadElement, t.TSType > { + private readonly typesEnabled: boolean; + constructor( /** The absolute path to the source file being compiled. */ - private sourceUrl: string, - ) {} + private sourcePath: string, + ) { + this.typesEnabled = sourcePath.endsWith('.ts') || sourcePath.endsWith('.mts'); + } attachComments(statement: t.Statement | t.Expression, leadingComments: LeadingComment[]): void { // We must process the comments in reverse because `t.addComment()` will add new ones in front. @@ -112,7 +116,7 @@ export class BabelAstFactory implements AstFactory< assert(body, t.isBlockStatement, 'a block'); return t.functionDeclaration( t.identifier(functionName), - parameters.map((param) => identifierWithType(param.name, param.type)), + parameters.map((param) => this.identifierWithType(param.name, param.type)), body, ); } @@ -125,7 +129,7 @@ export class BabelAstFactory implements AstFactory< assert(body, t.isBlockStatement, 'a block'); } return t.arrowFunctionExpression( - parameters.map((param) => identifierWithType(param.name, param.type)), + parameters.map((param) => this.identifierWithType(param.name, param.type)), body, ); } @@ -139,7 +143,7 @@ export class BabelAstFactory implements AstFactory< const name = functionName !== null ? t.identifier(functionName) : null; return t.functionExpression( name, - parameters.map((param) => identifierWithType(param.name, param.type)), + parameters.map((param) => this.identifierWithType(param.name, param.type)), body, ); } @@ -234,7 +238,7 @@ export class BabelAstFactory implements AstFactory< type: t.TSType | null, ): t.Statement { return t.variableDeclaration(variableType, [ - t.variableDeclarator(identifierWithType(variableName, type), initializer), + t.variableDeclarator(this.identifierWithType(variableName, type), initializer), ]); } @@ -253,7 +257,7 @@ export class BabelAstFactory implements AstFactory< // Add in the filename so that we can map to external template files. // Note that Babel gets confused if you specify a filename when it is the original source // file. This happens when the template is inline, in which case just use `undefined`. - filename: sourceMapRange.url !== this.sourceUrl ? sourceMapRange.url : undefined, + filename: sourceMapRange.url !== this.sourcePath ? sourceMapRange.url : undefined, start: { line: sourceMapRange.start.line + 1, // lines are 1-based in Babel. column: sourceMapRange.start.column, @@ -301,7 +305,7 @@ export class BabelAstFactory implements AstFactory< } createMapType(valueType: t.TSType): t.TSType { - const keySignature = identifierWithType('key', this.createBuiltInType('string')); + const keySignature = this.identifierWithType('key', this.createBuiltInType('string')); return t.tsTypeLiteral([t.tsIndexSignature([keySignature], t.tsTypeAnnotation(valueType))]); } @@ -311,6 +315,16 @@ export class BabelAstFactory implements AstFactory< } throw new Error('Attempting to transplant a type node from a non-Babel AST: ' + type); } + + private identifierWithType(name: string, type: t.TSType | null): t.Identifier { + const node = t.identifier(name); + + if (this.typesEnabled && type != null) { + node.typeAnnotation = t.tsTypeAnnotation(type); + } + + return node; + } } function getEntityTypeFromExpression(expression: t.Expression): t.Identifier | t.TSQualifiedName { @@ -333,16 +347,6 @@ function getEntityTypeFromExpression(expression: t.Expression): t.Identifier | t throw new Error(`Unsupported expression for type reference: ${expression.type}`); } -function identifierWithType(name: string, type: t.TSType | null): t.Identifier { - const node = t.identifier(name); - - if (type !== null) { - node.typeAnnotation = t.tsTypeAnnotation(type); - } - - return node; -} - function isLExpression(expr: t.Expression): expr is Extract { // Some LVal types are not expressions, which prevents us from using `t.isLVal()` // directly with `assert()`. diff --git a/packages/compiler-cli/linker/src/linker_import_generator.ts b/packages/compiler-cli/linker/src/linker_import_generator.ts index 0a102d84c2fb..92925af575ae 100644 --- a/packages/compiler-cli/linker/src/linker_import_generator.ts +++ b/packages/compiler-cli/linker/src/linker_import_generator.ts @@ -17,9 +17,10 @@ import {FatalLinkerError} from './fatal_linker_error'; * must be achieved by property access on an `ng` namespace identifier, which is passed in via the * constructor. */ -export class LinkerImportGenerator - implements ImportGenerator -{ +export class LinkerImportGenerator implements ImportGenerator< + null, + TExpression +> { constructor( private factory: AstFactory, private ngImport: TExpression, diff --git a/packages/compiler-cli/linker/test/ast/ast_value_spec.ts b/packages/compiler-cli/linker/test/ast/ast_value_spec.ts index 31037989e4fb..723b041a959a 100644 --- a/packages/compiler-cli/linker/test/ast/ast_value_spec.ts +++ b/packages/compiler-cli/linker/test/ast/ast_value_spec.ts @@ -390,7 +390,14 @@ describe('AstValue', () => { describe('getFunctionParameters', () => { it('should return the parameters of a function expression', () => { - const funcExpr = factory.createFunctionExpression('foo', ['a', 'b'], factory.createBlock([])); + const funcExpr = factory.createFunctionExpression( + 'foo', + [ + {name: 'a', type: null}, + {name: 'b', type: null}, + ], + factory.createBlock([]), + ); expect(createAstValue(funcExpr).getFunctionParameters()).toEqual( ['a', 'b'].map((name) => createAstValue(factory.createIdentifier(name))), ); diff --git a/packages/compiler-cli/test/compliance/isolated/isolated_compile_spec.ts b/packages/compiler-cli/test/compliance/isolated/isolated_compile_spec.ts index 035ed894c1f8..4546188e57b5 100644 --- a/packages/compiler-cli/test/compliance/isolated/isolated_compile_spec.ts +++ b/packages/compiler-cli/test/compliance/isolated/isolated_compile_spec.ts @@ -9,7 +9,7 @@ import ts from 'typescript'; import {checkExpectations} from '../test_helpers/check_expectations'; import {checkNoUnexpectedErrors} from '../test_helpers/check_errors'; -import {FileSystem} from '../../../src/ngtsc/file_system'; +import {AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system'; import {NgtscTestCompilerHost} from '../../../src/ngtsc/testing'; import { getBuildOutputDirectory, @@ -25,8 +25,8 @@ describe('isolated compliance tests', () => { if (!test.relativePath.includes('isolated')) { continue; } - describe(`[${test.relativePath}]`, () => { - it(test.description, () => { + describe(`[${test.description}]`, () => { + (test.focusTest ? fit : it)(test.description, () => { const fs = initMockTestFileSystem(test.realTestPath); const {errors} = compileTests(fs, test); for (const expectation of test.expectations) { @@ -59,8 +59,8 @@ function compileTests(fs: FileSystem, test: ComplianceTest): {errors: string[]} const transformedFiles = preprocessor.transformAndPrint(); - const emittedFiles: string[] = []; - const validFiles = new Set(); + const emittedFiles: AbsoluteFsPath[] = []; + const validFiles = new Set(); for (const file of transformedFiles) { const relativePath = fs.relative(rootDir, fs.resolve(file.fileName)); @@ -87,8 +87,6 @@ function compileTests(fs: FileSystem, test: ComplianceTest): {errors: string[]} // than 'Bundler' or 'NodeNext' which expect specific package.json exports. moduleResolution: ts.ModuleResolutionKind.Node10, strict: true, - // TODO: enable once we fix the generated code - noImplicitAny: false, target: ts.ScriptTarget.ES2015, types: [], }, diff --git a/packages/compiler-cli/test/compliance/test_cases/isolated/TEST_CASES.json b/packages/compiler-cli/test/compliance/test_cases/isolated/TEST_CASES.json index b8b9e9ce73a5..94affde1f237 100644 --- a/packages/compiler-cli/test/compliance/test_cases/isolated/TEST_CASES.json +++ b/packages/compiler-cli/test/compliance/test_cases/isolated/TEST_CASES.json @@ -19,6 +19,74 @@ ] } ] + }, + { + "description": "component with an embedded view", + "inputFiles": ["embedded_view/test.ts"], + "compilationModeFilter": [], + "expectations": [ + { + "files": [ + { + "expected": "embedded_view/test_transformed.ts", + "generated": "embedded_view/test.ts" + } + ] + } + ] + }, + { + "description": "component with an event listener", + "inputFiles": ["event_listener/test.ts"], + "compilationModeFilter": [], + "expectations": [ + { + "files": [ + { + "expected": "event_listener/test_transformed.ts", + "generated": "event_listener/test.ts" + }, + { + "expected": "event_listener/test.ngtypecheck.ts", + "generated": "event_listener/test.ngtypecheck.ts" + } + ] + } + ] + }, + { + "description": "component with an arrow function", + "inputFiles": ["arrow_function/test.ts"], + "compilationModeFilter": [], + "expectations": [ + { + "files": [ + { + "expected": "arrow_function/test_transformed.ts", + "generated": "arrow_function/test.ts" + }, + { + "expected": "arrow_function/test.ngtypecheck.ts", + "generated": "arrow_function/test.ngtypecheck.ts" + } + ] + } + ] + }, + { + "description": "component using queries", + "inputFiles": ["queries/test.ts"], + "compilationModeFilter": [], + "expectations": [ + { + "files": [ + { + "expected": "queries/test_transformed.ts", + "generated": "queries/test.ts" + } + ] + } + ] } ] } diff --git a/packages/compiler-cli/test/compliance/test_cases/isolated/arrow_function/test.ngtypecheck.ts b/packages/compiler-cli/test/compliance/test_cases/isolated/arrow_function/test.ngtypecheck.ts new file mode 100644 index 000000000000..a7d18fb57dd5 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/isolated/arrow_function/test.ngtypecheck.ts @@ -0,0 +1,5 @@ +… +($event: any /*T:EP*/): any => { + (((this).value /*117,122*/) /*117,122*/).update /*123,129*/(prev /*D:ignore*/ => (prev /*138,142*/) + (1 /*145,146*/) /*138,146*/) /*117,147*/; +}; +… diff --git a/packages/compiler-cli/test/compliance/test_cases/isolated/arrow_function/test.ts b/packages/compiler-cli/test/compliance/test_cases/isolated/arrow_function/test.ts new file mode 100644 index 000000000000..8d065ce74b89 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/isolated/arrow_function/test.ts @@ -0,0 +1,9 @@ +import {Component, signal} from '@angular/core'; + +@Component({ + selector: 'test-cmp', + template: '', +}) +export class TestCmp { + value = signal(1); +} diff --git a/packages/compiler-cli/test/compliance/test_cases/isolated/arrow_function/test_transformed.ts b/packages/compiler-cli/test/compliance/test_cases/isolated/arrow_function/test_transformed.ts new file mode 100644 index 000000000000..db5125d1b1e2 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/isolated/arrow_function/test_transformed.ts @@ -0,0 +1,17 @@ +$r3$.ɵɵdefineComponent({ + type: TestCmp, + selectors: [["test-cmp"]], + decls: 1, + vars: 0, + consts: [[3, "click"]], + template: function TestCmp_Template(rf: number, ctx: any) { + if (rf & 1) { + $r3$.ɵɵdomElementStart(0, "button", 0); + $r3$.ɵɵdomListener("click", function TestCmp_Template_button_click_0_listener() { + return ctx.value.update((prev: any) => prev + 1); + }); + $r3$.ɵɵdomElementEnd(); + } + }, + encapsulation: 2 +}); diff --git a/packages/compiler-cli/test/compliance/test_cases/isolated/basic/test_transformed.ts b/packages/compiler-cli/test/compliance/test_cases/isolated/basic/test_transformed.ts index a9bb1d820385..adce51ea8d53 100644 --- a/packages/compiler-cli/test/compliance/test_cases/isolated/basic/test_transformed.ts +++ b/packages/compiler-cli/test/compliance/test_cases/isolated/basic/test_transformed.ts @@ -1,8 +1,20 @@ … - static ɵfac: i0.ɵɵFactoryDeclaration = function TestCmp_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || TestCmp)(); }; - static ɵcmp: i0.ɵɵComponentDeclaration = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: TestCmp, selectors: [["test-cmp"]], decls: 1, vars: 1, template: function TestCmp_Template(rf, ctx) { if (rf & 1) { - i0.ɵɵtext(0); - } if (rf & 2) { - i0.ɵɵtextInterpolate(ctx.x == null ? null : ctx.x.toString()); - } }, encapsulation: 2 }); +static ɵfac: $r3$.ɵɵFactoryDeclaration = function TestCmp_Factory(__ngFactoryType__: any) { + return new (__ngFactoryType__ || TestCmp)(); +}; + +static ɵcmp: $r3$.ɵɵComponentDeclaration = /*@__PURE__*/ + $r3$.ɵɵdefineComponent({ + type: TestCmp, + selectors: [["test-cmp"]], + decls: 1, + vars: 1, + template: function TestCmp_Template(rf: number, ctx: any) { + if (rf & 1) { + $r3$.ɵɵtext(0); + } + if (rf & 2) { + $r3$.ɵɵtextInterpolate(ctx.x == null ? null : ctx.x.toString()); + } + }, encapsulation: 2 }); … diff --git a/packages/compiler-cli/test/compliance/test_cases/isolated/embedded_view/test.ts b/packages/compiler-cli/test/compliance/test_cases/isolated/embedded_view/test.ts new file mode 100644 index 000000000000..743409ccde47 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/isolated/embedded_view/test.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'test-cmp', + template: '{{x}}', +}) +export class TestCmp { + x = 'hello'; +} diff --git a/packages/compiler-cli/test/compliance/test_cases/isolated/embedded_view/test_transformed.ts b/packages/compiler-cli/test/compliance/test_cases/isolated/embedded_view/test_transformed.ts new file mode 100644 index 000000000000..8676cc4b8c05 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/isolated/embedded_view/test_transformed.ts @@ -0,0 +1,21 @@ +function TestCmp_ng_template_0_Template(rf: number, ctx: any) { + if (rf & 1) { + $r3$.ɵɵtext(0); + } + if (rf & 2) { + const ctx_r0 = $r3$.ɵɵnextContext(); + $r3$.ɵɵtextInterpolate(ctx_r0.x); + } +} +… +$r3$.ɵɵdefineComponent({ + type: TestCmp, + selectors: [["test-cmp"]], + decls: 1, vars: 0, + template: function TestCmp_Template(rf: number, ctx: any) { + if (rf & 1) { + $r3$.ɵɵdomTemplate(0, TestCmp_ng_template_0_Template, 1, 1, "ng-template"); + } + }, + encapsulation: 2 +}) diff --git a/packages/compiler-cli/test/compliance/test_cases/isolated/event_listener/test.ngtypecheck.ts b/packages/compiler-cli/test/compliance/test_cases/isolated/event_listener/test.ngtypecheck.ts new file mode 100644 index 000000000000..f3e2ae0949b7 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/isolated/event_listener/test.ngtypecheck.ts @@ -0,0 +1,5 @@ +… +($event: any /*T:EP*/): any => { + (this).handleClick /*109,120*/($event /*121,127*/) /*109,128*/; +}; +… diff --git a/packages/compiler-cli/test/compliance/test_cases/isolated/event_listener/test.ts b/packages/compiler-cli/test/compliance/test_cases/isolated/event_listener/test.ts new file mode 100644 index 000000000000..597fe97f3729 --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/isolated/event_listener/test.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'test-cmp', + template: '', +}) +export class TestCmp { + handleClick(event: MouseEvent) {} +} diff --git a/packages/compiler-cli/test/compliance/test_cases/isolated/event_listener/test_transformed.ts b/packages/compiler-cli/test/compliance/test_cases/isolated/event_listener/test_transformed.ts new file mode 100644 index 000000000000..26c756f503fc --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/isolated/event_listener/test_transformed.ts @@ -0,0 +1,15 @@ +$r3$.ɵɵdefineComponent({ + type: TestCmp, + selectors: [["test-cmp"]], + decls: 1, + vars: 0, + consts: [[3, "click"]], + template: function TestCmp_Template(rf: number, ctx: any) { + if (rf & 1) { + $r3$.ɵɵdomElementStart(0, "button", 0); + $r3$.ɵɵdomListener("click", function TestCmp_Template_button_click_0_listener($event: any) { return ctx.handleClick($event); }); + $r3$.ɵɵdomElementEnd(); + } + }, + encapsulation: 2 +}); diff --git a/packages/compiler-cli/test/compliance/test_cases/isolated/queries/test.ts b/packages/compiler-cli/test/compliance/test_cases/isolated/queries/test.ts new file mode 100644 index 000000000000..dbe7cf46204d --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/isolated/queries/test.ts @@ -0,0 +1,10 @@ +import {Component, ContentChildren, ElementRef, QueryList, ViewChild} from '@angular/core'; + +@Component({ + selector: 'test-cmp', + template: '', +}) +export class TestCmp { + @ViewChild('span') span: ElementRef = null!; + @ContentChildren('projected') projected: QueryList = null!; +} diff --git a/packages/compiler-cli/test/compliance/test_cases/isolated/queries/test_transformed.ts b/packages/compiler-cli/test/compliance/test_cases/isolated/queries/test_transformed.ts new file mode 100644 index 000000000000..43b83064ce7f --- /dev/null +++ b/packages/compiler-cli/test/compliance/test_cases/isolated/queries/test_transformed.ts @@ -0,0 +1,45 @@ +const _c0 = ["projected"]; +const _c1 = ["span"]; +const _c2 = ["*"]; +… +static ɵfac: $r3$.ɵɵFactoryDeclaration = function TestCmp_Factory(__ngFactoryType__: any) { + return new (__ngFactoryType__ || TestCmp)(); +}; + +static ɵcmp: $r3$.ɵɵComponentDeclaration = /*@__PURE__*/ + $r3$.ɵɵdefineComponent({ + type: TestCmp, + selectors: [["test-cmp"]], + contentQueries: function TestCmp_ContentQueries(rf: number, ctx: any, dirIndex: number) { + if (rf & 1) { + $r3$.ɵɵcontentQuery(dirIndex, _c0, 4); + } + if (rf & 2) { + let _t: any; + $r3$.ɵɵqueryRefresh(_t = $r3$.ɵɵloadQuery()) && (ctx.projected = _t); + } + }, + viewQuery: function TestCmp_Query(rf: number, ctx: any) { + if (rf & 1) { + $r3$.ɵɵviewQuery(_c1, 5); + } + if (rf & 2) { + let _t: any; + $r3$.ɵɵqueryRefresh(_t = $r3$.ɵɵloadQuery()) && (ctx.span = _t.first); + } + }, + ngContentSelectors: _c2, + decls: 3, + vars: 0, + consts: [["span", ""]], + template: function TestCmp_Template(rf: number, ctx: any) { + if (rf & 1) { + $r3$.ɵɵprojectionDef(); + $r3$.ɵɵdomElementStart(0, "span", null, 0); + $r3$.ɵɵprojection(2); + $r3$.ɵɵdomElementEnd(); + } + }, + encapsulation: 2 + }); +… diff --git a/packages/compiler/src/render3/r3_hmr_compiler.ts b/packages/compiler/src/render3/r3_hmr_compiler.ts index 6250c838219c..82c830b1b79a 100644 --- a/packages/compiler/src/render3/r3_hmr_compiler.ts +++ b/packages/compiler/src/render3/r3_hmr_compiler.ts @@ -78,7 +78,10 @@ export function compileHmrInitializer(meta: R3HmrMetadata): o.Expression { ]); // (m) => m.default && ɵɵreplaceMetadata(...) - const replaceCallback = o.arrowFn([new o.FnParam(moduleName)], defaultRead.and(replaceCall)); + const replaceCallback = o.arrowFn( + [new o.FnParam(moduleName, o.DYNAMIC_TYPE)], + defaultRead.and(replaceCall), + ); // getReplaceMetadataURL(id, timestamp, import.meta.url) const url = o @@ -94,7 +97,7 @@ export function compileHmrInitializer(meta: R3HmrMetadata): o.Expression { // } const importCallback = new o.DeclareFunctionStmt( importCallbackName, - [new o.FnParam(timestampName)], + [new o.FnParam(timestampName, o.DYNAMIC_TYPE)], [ // The vite-ignore special comment is required to prevent Vite from generating a superfluous // warning for each usage within the development code. If Vite provides a method to @@ -110,7 +113,7 @@ export function compileHmrInitializer(meta: R3HmrMetadata): o.Expression { // (d) => d.id === id && Cmp_HmrLoad(d.timestamp) const updateCallback = o.arrowFn( - [new o.FnParam(dataName)], + [new o.FnParam(dataName, o.DYNAMIC_TYPE)], o .variable(dataName) .prop('id') @@ -173,7 +176,7 @@ export function compileHmrUpdateCallback( const body: o.Statement[] = []; for (const local of meta.localDependencies) { - params.push(new o.FnParam(local.name)); + params.push(new o.FnParam(local.name, o.DYNAMIC_TYPE)); } // Declare variables that read out the individual namespaces. diff --git a/packages/compiler/src/render3/view/query_generation.ts b/packages/compiler/src/render3/view/query_generation.ts index c32e94f5c876..6d803e0d0260 100644 --- a/packages/compiler/src/render3/view/query_generation.ts +++ b/packages/compiler/src/render3/view/query_generation.ts @@ -217,7 +217,7 @@ export function createViewQueriesFunction( const viewQueryFnName = name ? `${name}_Query` : null; return o.fn( - [new o.FnParam(RENDER_FLAGS, o.NUMBER_TYPE), new o.FnParam(CONTEXT_NAME, null)], + [new o.FnParam(RENDER_FLAGS, o.NUMBER_TYPE), new o.FnParam(CONTEXT_NAME, o.DYNAMIC_TYPE)], [ renderFlagCheckIfStmt(core.RenderFlags.Create, createStatements), renderFlagCheckIfStmt(core.RenderFlags.Update, collapseAdvanceStatements(updateStatements)), @@ -282,8 +282,8 @@ export function createContentQueriesFunction( return o.fn( [ new o.FnParam(RENDER_FLAGS, o.NUMBER_TYPE), - new o.FnParam(CONTEXT_NAME, null), - new o.FnParam('dirIndex', null), + new o.FnParam(CONTEXT_NAME, o.DYNAMIC_TYPE), + new o.FnParam('dirIndex', o.NUMBER_TYPE), ], [ renderFlagCheckIfStmt(core.RenderFlags.Create, createStatements), diff --git a/packages/compiler/src/template/pipeline/src/emit.ts b/packages/compiler/src/template/pipeline/src/emit.ts index 1437a13b5a6d..97d556e6f123 100644 --- a/packages/compiler/src/template/pipeline/src/emit.ts +++ b/packages/compiler/src/template/pipeline/src/emit.ts @@ -247,7 +247,7 @@ function emitView(view: ViewCompilationUnit): o.FunctionExpr { const createCond = maybeGenerateRfBlock(1, createStatements); const updateCond = maybeGenerateRfBlock(2, updateStatements); return o.fn( - [new o.FnParam(RENDER_FLAGS), new o.FnParam(CONTEXT_NAME)], + [new o.FnParam(RENDER_FLAGS, o.NUMBER_TYPE), new o.FnParam(CONTEXT_NAME, o.DYNAMIC_TYPE)], [...createCond, ...updateCond], /* type */ undefined, /* sourceSpan */ undefined, @@ -307,7 +307,7 @@ export function emitHostBindingFunction(job: HostBindingCompilationJob): o.Funct const createCond = maybeGenerateRfBlock(1, createStatements); const updateCond = maybeGenerateRfBlock(2, updateStatements); return o.fn( - [new o.FnParam(RENDER_FLAGS), new o.FnParam(CONTEXT_NAME)], + [new o.FnParam(RENDER_FLAGS, o.NUMBER_TYPE), new o.FnParam(CONTEXT_NAME, o.DYNAMIC_TYPE)], [...createCond, ...updateCond], /* type */ undefined, /* sourceSpan */ undefined, diff --git a/packages/compiler/src/template/pipeline/src/ingest.ts b/packages/compiler/src/template/pipeline/src/ingest.ts index 0b86eb0dd395..f3210eea050b 100644 --- a/packages/compiler/src/template/pipeline/src/ingest.ts +++ b/packages/compiler/src/template/pipeline/src/ingest.ts @@ -1203,7 +1203,7 @@ function convertAst( } else if (ast instanceof e.ArrowFunction) { return updateParameterReferences( o.arrowFn( - ast.parameters.map((arg) => new o.FnParam(arg.name)), + ast.parameters.map((arg) => new o.FnParam(arg.name, o.DYNAMIC_TYPE)), convertAst(ast.body, job, baseSourceSpan), ), ); diff --git a/packages/compiler/src/template/pipeline/src/phases/pure_function_extraction.ts b/packages/compiler/src/template/pipeline/src/phases/pure_function_extraction.ts index f9e766a5f458..e1d779d3bb15 100644 --- a/packages/compiler/src/template/pipeline/src/phases/pure_function_extraction.ts +++ b/packages/compiler/src/template/pipeline/src/phases/pure_function_extraction.ts @@ -45,7 +45,7 @@ class PureFunctionConstant extends GenericKeyFn implements SharedConstantDefinit toSharedConstantDeclaration(declName: string, keyExpr: o.Expression): o.Statement { const fnParams: o.FnParam[] = []; for (let idx = 0; idx < this.numArgs; idx++) { - fnParams.push(new o.FnParam('a' + idx)); + fnParams.push(new o.FnParam('a' + idx, o.DYNAMIC_TYPE)); } // We will never visit `ir.PureFunctionParameterExpr`s that don't belong to us, because this diff --git a/packages/compiler/src/template/pipeline/src/phases/reify.ts b/packages/compiler/src/template/pipeline/src/phases/reify.ts index ca89a7227274..90aa4d7e7841 100644 --- a/packages/compiler/src/template/pipeline/src/phases/reify.ts +++ b/packages/compiler/src/template/pipeline/src/phases/reify.ts @@ -859,7 +859,7 @@ function reifyListenerHandler( const params: o.FnParam[] = []; if (consumesDollarEvent) { // We need the `$event` parameter. - params.push(new o.FnParam('$event')); + params.push(new o.FnParam('$event', o.DYNAMIC_TYPE)); } return o.fn(params, handlerStmts, undefined, undefined, name); @@ -872,7 +872,10 @@ function reifyTrackBy(unit: CompilationUnit, op: ir.RepeaterCreateOp): o.Express return op.trackByFn; } - const params: o.FnParam[] = [new o.FnParam('$index'), new o.FnParam('$item')]; + const params: o.FnParam[] = [ + new o.FnParam('$index', o.NUMBER_TYPE), + new o.FnParam('$item', o.DYNAMIC_TYPE), + ]; let fn: o.FunctionExpr | o.ArrowFunctionExpr; if (op.trackByOps === null) { @@ -932,7 +935,10 @@ function getArrowFunctionFactory( : statements; return o.arrowFn( - [new o.FnParam(expr.contextName), new o.FnParam(expr.currentViewName)], + [ + new o.FnParam(expr.contextName, o.DYNAMIC_TYPE), + new o.FnParam(expr.currentViewName, o.DYNAMIC_TYPE), + ], o.arrowFn(expr.parameters, body), ); } diff --git a/packages/core/src/render3/instructions/queries.ts b/packages/core/src/render3/instructions/queries.ts index b52807e42410..bd510fad5886 100644 --- a/packages/core/src/render3/instructions/queries.ts +++ b/packages/core/src/render3/instructions/queries.ts @@ -35,7 +35,7 @@ import {isCreationMode} from '../util/view_utils'; export function ɵɵcontentQuery( directiveIndex: number, predicate: ProviderToken | string | string[], - flags: QueryFlags, + flags: number, read?: any, ): typeof ɵɵcontentQuery { createContentQuery(directiveIndex, predicate, flags, read); @@ -53,7 +53,7 @@ export function ɵɵcontentQuery( */ export function ɵɵviewQuery( predicate: ProviderToken | string | string[], - flags: QueryFlags, + flags: number, read?: any, ): typeof ɵɵviewQuery { createViewQuery(predicate, flags, read);