diff --git a/.ng-dev/format.mts b/.ng-dev/format.mts index 8cd63a8b67659..61b26a7c1917b 100644 --- a/.ng-dev/format.mts +++ b/.ng-dev/format.mts @@ -18,6 +18,7 @@ export const format: FormatConfig = { 'packages/benchpress/**/*.{js,ts}', 'packages/common/**/*.{js,ts}', 'packages/compiler/**/*.{js,ts}', + 'packages/compiler-cli/**/*.{js,ts}', 'packages/core/**/*.{js,ts}', 'packages/docs/**/*.{js,ts}', 'packages/elements/**/*.{js,ts}', @@ -35,6 +36,8 @@ export const format: FormatConfig = { 'packages/upgrade/**/*.{js,ts}', 'packages/zone.js/**/*.{js,ts}', + // Test cases contain non valid code. + '!packages/compiler-cli/test/compliance/test_cases/**/*.{js,ts}', // Do not format d.ts files as they are generated '!**/*.d.ts', // Both third_party and .yarn are directories containing copied code which should @@ -83,6 +86,7 @@ export const format: FormatConfig = { '!packages/benchpress/**/*.{js,ts}', '!packages/common/**/*.{js,ts}', '!packages/compiler/**/*.{js,ts}', + '!packages/compiler-cli/**/*.{js,ts}', '!packages/core/**/*.{js,ts}', '!packages/docs/**/*.{js,ts}', '!packages/elements/**/*.{js,ts}', diff --git a/packages/compiler-cli/esbuild.config.js b/packages/compiler-cli/esbuild.config.js index d34723a0b4d9b..d5ed1080fd29e 100644 --- a/packages/compiler-cli/esbuild.config.js +++ b/packages/compiler-cli/esbuild.config.js @@ -7,12 +7,12 @@ */ module.exports = { - resolveExtensions : ['.mjs', '.js'], + resolveExtensions: ['.mjs', '.js'], // Note: `@bazel/esbuild` has a bug and does not pass-through the format from Starlark. - format : 'esm', - banner : { + format: 'esm', + banner: { // Workaround for: https://github.com/evanw/esbuild/issues/946 - js : ` + js: ` import {createRequire as __cjsCompatRequire} from 'module'; const require = __cjsCompatRequire(import.meta.url); `, diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/basic.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/basic.ts index e065134e45374..4ddce090fabbe 100644 --- a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/basic.ts +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/basic.ts @@ -15,17 +15,11 @@ import {Lib2Module} from 'lib2_built/module'; selector: 'id-app', template: '', }) -export class AppComponent { -} +export class AppComponent {} @NgModule({ - imports: [ - Lib2Module, - BrowserModule, - ServerModule, - ], + imports: [Lib2Module, BrowserModule, ServerModule], declarations: [AppComponent], bootstrap: [AppComponent], }) -export class BasicAppModule { -} +export class BasicAppModule {} diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/dep.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/dep.ts index 7f8e7dedb3c65..e479dc84ef9c7 100644 --- a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/dep.ts +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/dep.ts @@ -11,8 +11,7 @@ import {BrowserModule} from '@angular/platform-browser'; import {ServerModule} from '@angular/platform-server'; @Injectable() -export class NormalService { -} +export class NormalService {} @Component({ selector: 'dep-app', @@ -26,16 +25,12 @@ export class AppComponent { } @NgModule({ - imports: [ - BrowserModule, - ServerModule, - ], + imports: [BrowserModule, ServerModule], declarations: [AppComponent], bootstrap: [AppComponent], providers: [NormalService], }) -export class DepAppModule { -} +export class DepAppModule {} @Injectable({providedIn: DepAppModule}) export class ShakeableService { diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/hierarchy.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/hierarchy.ts index 2a4c73a53b376..a5b71e7b4b95a 100644 --- a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/hierarchy.ts +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/hierarchy.ts @@ -11,16 +11,14 @@ import {BrowserModule} from '@angular/platform-browser'; import {ServerModule} from '@angular/platform-server'; @Injectable() -export class Service { -} +export class Service {} @Component({ selector: 'hierarchy-app', template: '', providers: [Service], }) -export class AppComponent { -} +export class AppComponent {} @Component({ selector: 'child-cmp', @@ -29,18 +27,14 @@ export class AppComponent { export class ChildComponent { found: boolean; - constructor(@Optional() @Self() service: Service|null) { + constructor(@Optional() @Self() service: Service | null) { this.found = !!service; } } @NgModule({ - imports: [ - BrowserModule, - ServerModule, - ], + imports: [BrowserModule, ServerModule], declarations: [AppComponent, ChildComponent], bootstrap: [AppComponent], }) -export class HierarchyAppModule { -} +export class HierarchyAppModule {} diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root.ts index 8af32cb960d02..7b64c6ff4e6d8 100644 --- a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root.ts +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root.ts @@ -17,27 +17,22 @@ import {LazyModule} from './root_lazy'; selector: 'root-app', template: '', }) -export class AppComponent { -} +export class AppComponent {} export function children(): any { console.error('children', LazyModule); return LazyModule; } - @NgModule({ imports: [ BrowserModule, ServerModule, - RouterModule.forRoot( - [ - {path: '', pathMatch: 'prefix', loadChildren: children}, - ], - {initialNavigation: 'enabledBlocking'}), + RouterModule.forRoot([{path: '', pathMatch: 'prefix', loadChildren: children}], { + initialNavigation: 'enabledBlocking', + }), ], declarations: [AppComponent], bootstrap: [AppComponent], }) -export class RootAppModule { -} +export class RootAppModule {} diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root_lazy.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root_lazy.ts index d62b6b0e40ee5..40aa38ab26eca 100644 --- a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root_lazy.ts +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root_lazy.ts @@ -25,11 +25,6 @@ export class RouteComponent { @NgModule({ declarations: [RouteComponent], - imports: [ - RouterModule.forChild([ - {path: '', pathMatch: 'prefix', component: RouteComponent}, - ]), - ], + imports: [RouterModule.forChild([{path: '', pathMatch: 'prefix', component: RouteComponent}])], }) -export class LazyModule { -} +export class LazyModule {} diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root_service.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root_service.ts index 8cd0dd1beb4b0..582ca36929f81 100644 --- a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root_service.ts +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/root_service.ts @@ -11,5 +11,4 @@ import {Injectable} from '@angular/core'; @Injectable({ providedIn: 'root', }) -export class Service { -} +export class Service {} diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/self.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/self.ts index f606f6cf28542..f17e22d0693dc 100644 --- a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/self.ts +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/self.ts @@ -12,7 +12,7 @@ import {ServerModule} from '@angular/platform-server'; @Injectable() export class NormalService { - constructor(@Optional() @Self() readonly shakeable: ShakeableService|null) {} + constructor(@Optional() @Self() readonly shakeable: ShakeableService | null) {} } @Component({ @@ -27,17 +27,12 @@ export class AppComponent { } @NgModule({ - imports: [ - BrowserModule, - ServerModule, - ], + imports: [BrowserModule, ServerModule], declarations: [AppComponent], bootstrap: [AppComponent], providers: [NormalService], }) -export class SelfAppModule { -} +export class SelfAppModule {} @Injectable({providedIn: SelfAppModule}) -export class ShakeableService { -} +export class ShakeableService {} diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/string.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/string.ts index 2ffdf243aee00..e474377e8bb96 100644 --- a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/string.ts +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/string.ts @@ -10,7 +10,6 @@ import {Component, Inject, Injectable, NgModule} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; import {ServerModule} from '@angular/platform-server'; - @Component({ selector: 'string-app', template: '{{data}}', @@ -23,16 +22,12 @@ export class AppComponent { } @NgModule({ - imports: [ - BrowserModule, - ServerModule, - ], + imports: [BrowserModule, ServerModule], declarations: [AppComponent], bootstrap: [AppComponent], providers: [{provide: 'someStringToken', useValue: 'works'}], }) -export class StringAppModule { -} +export class StringAppModule {} @Injectable({providedIn: StringAppModule}) export class Service { diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/token.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/token.ts index e61b70b819197..a8d83f63fc86e 100644 --- a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/token.ts +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/src/token.ts @@ -6,24 +6,30 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, forwardRef, Inject, inject, Injectable, InjectionToken, NgModule} from '@angular/core'; +import { + Component, + forwardRef, + Inject, + inject, + Injectable, + InjectionToken, + NgModule, +} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; import {ServerModule} from '@angular/platform-server'; export interface IService { - readonly dep: {readonly data: string;}; + readonly dep: {readonly data: string}; } @NgModule({}) -export class TokenModule { -} +export class TokenModule {} export const TOKEN = new InjectionToken('test', { providedIn: TokenModule, factory: () => new Service(inject(Dep)), }); - @Component({ selector: 'token-app', template: '{{data}}', @@ -36,17 +42,12 @@ export class AppComponent { } @NgModule({ - imports: [ - BrowserModule, - ServerModule, - TokenModule, - ], + imports: [BrowserModule, ServerModule, TokenModule], providers: [forwardRef(() => Dep)], declarations: [AppComponent], bootstrap: [AppComponent], }) -export class TokenAppModule { -} +export class TokenAppModule {} @Injectable() export class Dep { diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/app_spec.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/app_spec.ts index 213cd829e42e0..155176deb3d34 100644 --- a/packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/app_spec.ts +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/app/test/app_spec.ts @@ -18,71 +18,71 @@ import {StringAppModule} from 'app_built/src/string'; import {TokenAppModule} from 'app_built/src/token'; describe('ngInjectableDef Bazel Integration', () => { - it('works in AOT', done => { + it('works in AOT', (done) => { renderModule(BasicAppModule, { document: '', url: '/', - }).then(html => { + }).then((html) => { expect(html).toMatch(/>0:0<\//); done(); }); }); - it('@Self() works in component hierarchies', done => { + it('@Self() works in component hierarchies', (done) => { renderModule(HierarchyAppModule, { document: '', url: '/', - }).then(html => { + }).then((html) => { expect(html).toMatch(/>false<\//); done(); }); }); - it('@Optional() Self() resolves to @Injectable() scoped service', done => { + it('@Optional() Self() resolves to @Injectable() scoped service', (done) => { renderModule(SelfAppModule, { document: '', url: '/', - }).then(html => { + }).then((html) => { expect(html).toMatch(/>true<\//); done(); }); }); - it('InjectionToken ngInjectableDef works', done => { + it('InjectionToken ngInjectableDef works', (done) => { renderModule(TokenAppModule, { document: '', url: '/', - }).then(html => { + }).then((html) => { expect(html).toMatch(/>fromToken<\//); done(); }); }); - it('APP_ROOT_SCOPE works', done => { + it('APP_ROOT_SCOPE works', (done) => { renderModule(RootAppModule, { document: '', url: '/', - }).then(html => { + }).then((html) => { expect(html).toMatch(/>true:false<\//); done(); }); }); - it('can inject dependencies', done => { + it('can inject dependencies', (done) => { renderModule(DepAppModule, { document: '', url: '/', - }).then(html => { + }).then((html) => { expect(html).toMatch(/>true<\//); done(); }); }); - it('string tokens work', done => { + it('string tokens work', (done) => { renderModule(StringAppModule, { document: '', url: '/', - }).then(html => { + }).then((html) => { expect(html).toMatch(/>works<\//); done(); }); @@ -105,8 +105,7 @@ describe('ngInjectableDef Bazel Integration', () => { it('allows provider override in JIT for module-scoped @Injectables', () => { @NgModule() - class Module { - } + class Module {} @Injectable({ providedIn: Module, @@ -159,33 +158,32 @@ describe('ngInjectableDef Bazel Integration', () => { expect(() => TestBed.inject(ChildService).value).toThrowError(/ChildService/); }); - it('uses legacy `ngInjectable` property even if it inherits from a class that has `ɵprov` property', - () => { - @Injectable({ - providedIn: 'root', - useValue: new ParentService('parent'), - }) - class ParentService { - constructor(public value: string) {} - } - - // ChildServices extends ParentService but does not have @Injectable - class ChildService extends ParentService { - constructor(value: string) { - super(value); - } - static ngInjectableDef = { - providedIn: 'root', - factory: () => new ChildService('child'), - token: ChildService, - }; - } - - TestBed.configureTestingModule({}); - // We are asserting that system throws an error, rather than taking the inherited - // annotation. - expect(TestBed.inject(ChildService).value).toEqual('child'); - }); + it('uses legacy `ngInjectable` property even if it inherits from a class that has `ɵprov` property', () => { + @Injectable({ + providedIn: 'root', + useValue: new ParentService('parent'), + }) + class ParentService { + constructor(public value: string) {} + } + + // ChildServices extends ParentService but does not have @Injectable + class ChildService extends ParentService { + constructor(value: string) { + super(value); + } + static ngInjectableDef = { + providedIn: 'root', + factory: () => new ChildService('child'), + token: ChildService, + }; + } + + TestBed.configureTestingModule({}); + // We are asserting that system throws an error, rather than taking the inherited + // annotation. + expect(TestBed.inject(ChildService).value).toEqual('child'); + }); it('NgModule injector understands requests for INJECTABLE', () => { TestBed.configureTestingModule({ @@ -200,8 +198,7 @@ describe('ngInjectableDef Bazel Integration', () => { template: 'test', providers: [{provide: 'foo', useValue: 'bar'}], }) - class TestCmp { - } + class TestCmp {} TestBed.configureTestingModule({ declarations: [TestCmp], diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/lib1/module.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/lib1/module.ts index b0db0bd4ded03..029b47da70dda 100644 --- a/packages/compiler-cli/integrationtest/bazel/injectable_def/lib1/module.ts +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/lib1/module.ts @@ -9,8 +9,7 @@ import {Injectable, NgModule} from '@angular/core'; @NgModule({}) -export class Lib1Module { -} +export class Lib1Module {} @Injectable({ providedIn: Lib1Module, diff --git a/packages/compiler-cli/integrationtest/bazel/injectable_def/lib2/module.ts b/packages/compiler-cli/integrationtest/bazel/injectable_def/lib2/module.ts index 625363165b6ae..e0393c99aacc4 100644 --- a/packages/compiler-cli/integrationtest/bazel/injectable_def/lib2/module.ts +++ b/packages/compiler-cli/integrationtest/bazel/injectable_def/lib2/module.ts @@ -28,5 +28,4 @@ export class Lib2Cmp { exports: [Lib2Cmp], imports: [Lib1Module], }) -export class Lib2Module { -} +export class Lib2Module {} diff --git a/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/src/module.ts b/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/src/module.ts index a649a7462886a..3dbcc575c53d5 100644 --- a/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/src/module.ts +++ b/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/src/module.ts @@ -11,30 +11,25 @@ import {Injectable, InjectionToken, NgModule} from '@angular/core'; export const AOT_TOKEN = new InjectionToken('TOKEN'); @Injectable() -export class AotService { -} +export class AotService {} @NgModule({ providers: [AotService], }) -export class AotServiceModule { -} +export class AotServiceModule {} @NgModule({ providers: [{provide: AOT_TOKEN, useValue: 'imports'}], }) -export class AotImportedModule { -} +export class AotImportedModule {} @NgModule({ providers: [{provide: AOT_TOKEN, useValue: 'exports'}], }) -export class AotExportedModule { -} +export class AotExportedModule {} @NgModule({ imports: [AotServiceModule, AotImportedModule], exports: [AotExportedModule], }) -export class AotModule { -} +export class AotModule {} diff --git a/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/test/module_spec.ts b/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/test/module_spec.ts index c05950ee1b25c..f769e97dd68c0 100644 --- a/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/test/module_spec.ts +++ b/packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app/test/module_spec.ts @@ -6,7 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import {forwardRef, Injectable, InjectionToken, Injector, NgModule, ɵcreateInjector as createInjector} from '@angular/core'; +import { + forwardRef, + Injectable, + InjectionToken, + Injector, + NgModule, + ɵcreateInjector as createInjector, +} from '@angular/core'; import {AOT_TOKEN, AotModule, AotService} from 'app_built/src/module'; describe('NgModule', () => { @@ -25,24 +32,19 @@ describe('NgModule', () => { }); }); - - describe('JIT', () => { @Injectable({providedIn: null}) - class Service { - } + class Service {} @NgModule({ providers: [Service], }) - class JitModule { - } + class JitModule {} @NgModule({ imports: [JitModule], }) - class JitAppModule { - } + class JitAppModule {} it('works', () => { createInjector(JitAppModule); @@ -52,20 +54,18 @@ describe('NgModule', () => { @NgModule({ imports: [forwardRef(() => BModule)], }) - class AModule { - } + class AModule {} @NgModule({ imports: [AModule], }) - class BModule { - } + class BModule {} - expect(() => createInjector(AModule)) - .toThrowError( - 'NG0200: Circular dependency in DI detected for AModule. ' + - 'Dependency path: AModule > BModule > AModule. ' + - 'Find more at https://angular.io/errors/NG0200'); + expect(() => createInjector(AModule)).toThrowError( + 'NG0200: Circular dependency in DI detected for AModule. ' + + 'Dependency path: AModule > BModule > AModule. ' + + 'Find more at https://angular.io/errors/NG0200', + ); }); it('merges imports and exports', () => { @@ -73,20 +73,17 @@ describe('NgModule', () => { @NgModule({ providers: [{provide: TOKEN, useValue: 'provided from A'}], }) - class AModule { - } + class AModule {} @NgModule({ providers: [{provide: TOKEN, useValue: 'provided from B'}], }) - class BModule { - } + class BModule {} @NgModule({ imports: [AModule], exports: [BModule], }) - class CModule { - } + class CModule {} const injector = createInjector(CModule); expect(injector.get(TOKEN)).toEqual('provided from B'); 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 108c44486c27b..00047cff09321 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 @@ -8,15 +8,24 @@ import {types as t} from '@babel/core'; import {assert} from '../../../../linker'; -import {AstFactory, BinaryOperator, LeadingComment, ObjectLiteralProperty, SourceMapRange, TemplateLiteral, VariableDeclarationType} from '../../../../src/ngtsc/translator'; +import { + AstFactory, + BinaryOperator, + LeadingComment, + ObjectLiteralProperty, + SourceMapRange, + TemplateLiteral, + VariableDeclarationType, +} from '../../../../src/ngtsc/translator'; /** * A Babel flavored implementation of the AstFactory. */ export class BabelAstFactory implements AstFactory { constructor( - /** The absolute path to the source file being compiled. */ - private sourceUrl: string) {} + /** The absolute path to the source file being compiled. */ + private sourceUrl: string, + ) {} attachComments(statement: t.Statement, leadingComments: LeadingComment[]): void { // We must process the comments in reverse because `t.addComment()` will add new ones in front. @@ -34,8 +43,10 @@ export class BabelAstFactory implements AstFactory { } createBinaryExpression( - leftOperand: t.Expression, operator: BinaryOperator, - rightOperand: t.Expression): t.Expression { + leftOperand: t.Expression, + operator: BinaryOperator, + rightOperand: t.Expression, + ): t.Expression { switch (operator) { case '&&': case '||': @@ -64,26 +75,44 @@ export class BabelAstFactory implements AstFactory { createExpressionStatement = t.expressionStatement; - createFunctionDeclaration(functionName: string, parameters: string[], body: t.Statement): - t.Statement { + createFunctionDeclaration( + functionName: string, + parameters: string[], + body: t.Statement, + ): t.Statement { assert(body, t.isBlockStatement, 'a block'); return t.functionDeclaration( - t.identifier(functionName), parameters.map(param => t.identifier(param)), body); + t.identifier(functionName), + parameters.map((param) => t.identifier(param)), + body, + ); } - createArrowFunctionExpression(parameters: string[], body: t.Statement|t.Expression): - t.Expression { + createArrowFunctionExpression( + parameters: string[], + 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)), body); + return t.arrowFunctionExpression( + parameters.map((param) => t.identifier(param)), + body, + ); } - createFunctionExpression(functionName: string|null, parameters: string[], body: t.Statement): - t.Expression { + createFunctionExpression( + functionName: string | null, + parameters: string[], + 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)), body); + return t.functionExpression( + name, + parameters.map((param) => t.identifier(param)), + body, + ); } createIdentifier = t.identifier; @@ -94,7 +123,7 @@ export class BabelAstFactory implements AstFactory { return this.createCallExpression(t.import(), [t.stringLiteral(url)], false /* pure */); } - createLiteral(value: string|number|boolean|null|undefined): t.Expression { + createLiteral(value: string | number | boolean | null | undefined): t.Expression { if (typeof value === 'string') { return t.stringLiteral(value); } else if (typeof value === 'number') { @@ -113,11 +142,14 @@ export class BabelAstFactory implements AstFactory { createNewExpression = t.newExpression; createObjectLiteral(properties: ObjectLiteralProperty[]): t.Expression { - return t.objectExpression(properties.map(prop => { - const key = - prop.quoted ? t.stringLiteral(prop.propertyName) : t.identifier(prop.propertyName); - return t.objectProperty(key, prop.value); - })); + return t.objectExpression( + properties.map((prop) => { + const key = prop.quoted + ? t.stringLiteral(prop.propertyName) + : t.identifier(prop.propertyName); + return t.objectProperty(key, prop.value); + }), + ); } createParenthesizedExpression = t.parenthesizedExpression; @@ -129,9 +161,12 @@ export class BabelAstFactory implements AstFactory { createReturnStatement = t.returnStatement; createTaggedTemplate(tag: t.Expression, template: TemplateLiteral): t.Expression { - const elements = template.elements.map( - (element, i) => this.setSourceMapRange( - t.templateElement(element, i === template.elements.length - 1), element.range)); + const elements = template.elements.map((element, i) => + this.setSourceMapRange( + t.templateElement(element, i === template.elements.length - 1), + element.range, + ), + ); return t.taggedTemplateExpression(tag, t.templateLiteral(elements, template.expressions)); } @@ -144,14 +179,19 @@ export class BabelAstFactory implements AstFactory { createUnaryExpression = t.unaryExpression; createVariableDeclaration( - variableName: string, initializer: t.Expression|null, - type: VariableDeclarationType): t.Statement { - return t.variableDeclaration( - type, [t.variableDeclarator(t.identifier(variableName), initializer)]); - } - - setSourceMapRange( - node: T, sourceMapRange: SourceMapRange|null): T { + variableName: string, + initializer: t.Expression | null, + type: VariableDeclarationType, + ): t.Statement { + return t.variableDeclaration(type, [ + t.variableDeclarator(t.identifier(variableName), initializer), + ]); + } + + setSourceMapRange( + node: T, + sourceMapRange: SourceMapRange | null, + ): T { if (sourceMapRange === null) { return node; } @@ -161,14 +201,14 @@ export class BabelAstFactory implements AstFactory { // file. This happens when the template is inline, in which case just use `undefined`. filename: sourceMapRange.url !== this.sourceUrl ? sourceMapRange.url : undefined, start: { - line: sourceMapRange.start.line + 1, // lines are 1-based in Babel. + line: sourceMapRange.start.line + 1, // lines are 1-based in Babel. column: sourceMapRange.start.column, }, end: { - line: sourceMapRange.end.line + 1, // lines are 1-based in Babel. + line: sourceMapRange.end.line + 1, // lines are 1-based in Babel. column: sourceMapRange.end.column, }, - } as any; // Needed because the Babel typings for `loc` don't include `filename`. + } as any; // Needed because the Babel typings for `loc` don't include `filename`. node.start = sourceMapRange.start.offset; node.end = sourceMapRange.end.offset; diff --git a/packages/compiler-cli/linker/babel/src/ast/babel_ast_host.ts b/packages/compiler-cli/linker/babel/src/ast/babel_ast_host.ts index 918dce7412c57..e40919b2d4715 100644 --- a/packages/compiler-cli/linker/babel/src/ast/babel_ast_host.ts +++ b/packages/compiler-cli/linker/babel/src/ast/babel_ast_host.ts @@ -14,7 +14,7 @@ import {assert, AstHost, FatalLinkerError, Range} from '../../../../linker'; * This implementation of `AstHost` is able to get information from Babel AST nodes. */ export class BabelAstHost implements AstHost { - getSymbolName(node: t.Expression): string|null { + getSymbolName(node: t.Expression): string | null { if (t.isIdentifier(node)) { return node.name; } else if (t.isMemberExpression(node) && t.isIdentifier(node.property)) { @@ -60,7 +60,7 @@ export class BabelAstHost implements AstHost { parseArrayLiteral(array: t.Expression): t.Expression[] { assert(array, t.isArrayExpression, 'an array literal'); - return array.elements.map(element => { + return array.elements.map((element) => { assert(element, isNotEmptyElement, 'element in array not to be empty'); assert(element, isNotSpreadElement, 'element in array not to use spread syntax'); return element; @@ -84,8 +84,9 @@ export class BabelAstHost implements AstHost { return result; } - isFunctionExpression(node: t.Expression): - node is Extract { + isFunctionExpression( + node: t.Expression, + ): node is Extract { return t.isFunction(node) || t.isArrowFunctionExpression(node); } @@ -102,7 +103,9 @@ export class BabelAstHost implements AstHost { if (fn.body.body.length !== 1) { throw new FatalLinkerError( - fn.body, 'Unsupported syntax, expected a function body with a single return statement.'); + fn.body, + 'Unsupported syntax, expected a function body with a single return statement.', + ); } const stmt = fn.body.body[0]; assert(stmt, t.isReturnStatement, 'a function body with a single return statement'); @@ -117,7 +120,7 @@ export class BabelAstHost implements AstHost { parseParameters(fn: t.Expression): t.Expression[] { assert(fn, this.isFunctionExpression, 'a function'); - return fn.params.map(param => { + return fn.params.map((param) => { assert(param, t.isIdentifier, 'an identifier'); return param; }); @@ -131,7 +134,7 @@ export class BabelAstHost implements AstHost { } parseArguments(call: t.Expression): t.Expression[] { assert(call, t.isCallExpression, 'a call expression'); - return call.arguments.map(arg => { + return call.arguments.map((arg) => { assert(arg, isNotSpreadArgument, 'argument not to use spread syntax'); assert(arg, t.isExpression, 'argument to be an expression'); return arg; @@ -141,10 +144,12 @@ export class BabelAstHost implements AstHost { getRange(node: t.Expression): Range { if (node.loc == null || node.start == null || node.end == null) { throw new FatalLinkerError( - node, 'Unable to read range for node - it is missing location information.'); + node, + 'Unable to read range for node - it is missing location information.', + ); } return { - startLine: node.loc.start.line - 1, // Babel lines are 1-based + startLine: node.loc.start.line - 1, // Babel lines are 1-based startCol: node.loc.start.column, startPos: node.start, endPos: node.end, @@ -156,8 +161,9 @@ export class BabelAstHost implements AstHost { * Return true if the expression does not represent an empty element in an array literal. * For example in `[,foo]` the first element is "empty". */ -function isNotEmptyElement(e: t.Expression|t.SpreadElement|null): e is t.Expression| - t.SpreadElement { +function isNotEmptyElement( + e: t.Expression | t.SpreadElement | null, +): e is t.Expression | t.SpreadElement { return e !== null; } @@ -165,11 +171,10 @@ function isNotEmptyElement(e: t.Expression|t.SpreadElement|null): e is t.Express * Return true if the expression is not a spread element of an array literal. * For example in `[x, ...rest]` the `...rest` expression is a spread element. */ -function isNotSpreadElement(e: t.Expression|t.SpreadElement): e is t.Expression { +function isNotSpreadElement(e: t.Expression | t.SpreadElement): e is t.Expression { return !t.isSpreadElement(e); } - /** * Return true if the node can be considered a text based property name for an * object expression. @@ -177,8 +182,9 @@ function isNotSpreadElement(e: t.Expression|t.SpreadElement): e is t.Expression * Notably in the Babel AST, object patterns (for destructuring) could be of type * `t.PrivateName` so we need a distinction between object expressions and patterns. */ -function isObjectExpressionPropertyName(n: t.Node): n is t.Identifier|t.StringLiteral| - t.NumericLiteral { +function isObjectExpressionPropertyName( + n: t.Node, +): n is t.Identifier | t.StringLiteral | t.NumericLiteral { return t.isIdentifier(n) || t.isStringLiteral(n) || t.isNumericLiteral(n); } @@ -194,12 +200,17 @@ function isNotSpreadArgument(arg: ArgumentType): arg is Exclude|NodePath|NodePath; + | NodePath + | NodePath + | NodePath; /** * This class represents the lexical scope of a partial declaration in Babel source code. @@ -36,7 +38,7 @@ export class BabelDeclarationScope implements DeclarationScope|null = null; +export function createEs2015LinkerPlugin({ + fileSystem, + logger, + ...options +}: LinkerPluginOptions): PluginObj { + let fileLinker: FileLinker | null = null; return { visitor: { Program: { - /** * Create a new `FileLinker` as we enter each file (`t.Program` in Babel). */ @@ -40,12 +42,18 @@ export function createEs2015LinkerPlugin({fileSystem, logger, ...options}: Linke const filename = file.opts.filename ?? file.opts.filenameRelative; if (!filename) { throw new Error( - 'No filename (nor filenameRelative) provided by Babel. This is required for the linking of partially compiled directives and components.'); + 'No filename (nor filenameRelative) provided by Babel. This is required for the linking of partially compiled directives and components.', + ); } const sourceUrl = fileSystem.resolve(file.opts.cwd ?? '.', filename); const linkerEnvironment = LinkerEnvironment.create( - fileSystem, logger, new BabelAstHost(), new BabelAstFactory(sourceUrl), options); + fileSystem, + logger, + new BabelAstHost(), + new BabelAstFactory(sourceUrl), + options, + ); fileLinker = new FileLinker(linkerEnvironment, sourceUrl, file.code); }, @@ -59,7 +67,7 @@ export function createEs2015LinkerPlugin({fileSystem, logger, ...options}: Linke insertStatements(constantScope, statements); } fileLinker = null; - } + }, }, /** @@ -89,11 +97,11 @@ export function createEs2015LinkerPlugin({fileSystem, logger, ...options}: Linke call.replaceWith(replacement); } catch (e) { - const node = isFatalLinkerError(e) ? e.node as t.Node : call.node; + const node = isFatalLinkerError(e) ? (e.node as t.Node) : call.node; throw buildCodeFrameError(state.file, (e as Error).message, node); } - } - } + }, + }, }; } @@ -114,7 +122,9 @@ function insertStatements(path: ConstantScopePath, statements: t.Statement[]): v * Insert the `statements` at the top of the body of the `fn` function. */ function insertIntoFunction( - fn: NodePath, statements: t.Statement[]): void { + fn: NodePath, + statements: t.Statement[], +): void { const body = fn.get('body'); body.unshiftContainer('body', statements); } @@ -124,7 +134,7 @@ function insertIntoFunction( */ function insertIntoProgram(program: NodePath, statements: t.Statement[]): void { const body = program.get('body'); - const importStatements = body.filter(statement => statement.isImportDeclaration()); + const importStatements = body.filter((statement) => statement.isImportDeclaration()); if (importStatements.length === 0) { program.unshiftContainer('body', statements); } else { @@ -132,7 +142,7 @@ function insertIntoProgram(program: NodePath, statements: t.Statement } } -function getCalleeName(call: NodePath): string|null { +function getCalleeName(call: NodePath): string | null { const callee = call.node.callee; if (t.isIdentifier(callee)) { return callee.name; @@ -149,13 +159,13 @@ function getCalleeName(call: NodePath): string|null { * Return true if all the `nodes` are Babel expressions. */ function isExpressionArray(nodes: t.Node[]): nodes is t.Expression[] { - return nodes.every(node => t.isExpression(node)); + return nodes.every((node) => t.isExpression(node)); } /** * Assert that the given `obj` is `null`. */ -function assertNull(obj: T|null): asserts obj is null { +function assertNull(obj: T | null): asserts obj is null { if (obj !== null) { throw new Error('BUG - expected `obj` to be null'); } @@ -164,7 +174,7 @@ function assertNull(obj: T|null): asserts obj is null { /** * Assert that the given `obj` is not `null`. */ -function assertNotNull(obj: T|null): asserts obj is T { +function assertNotNull(obj: T | null): asserts obj is T { if (obj === null) { throw new Error('BUG - expected `obj` not to be null'); } 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 ecf317aae9127..43d6785a330a7 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 @@ -22,19 +22,17 @@ const statement = template.statement; describe('BabelAstFactory', () => { let factory: BabelAstFactory; - beforeEach(() => factory = new BabelAstFactory('/original.ts')); + beforeEach(() => (factory = new BabelAstFactory('/original.ts'))); describe('attachComments()', () => { it('should add the comments to the given statement', () => { const stmt = statement.ast`x = 10;`; - factory.attachComments( - stmt, [leadingComment('comment 1', true), leadingComment('comment 2', false)]); + factory.attachComments(stmt, [ + leadingComment('comment 1', true), + leadingComment('comment 2', false), + ]); - expect(generate(stmt).code).toEqual([ - '/* comment 1 */', - '//comment 2', - 'x = 10;', - ].join('\n')); + expect(generate(stmt).code).toEqual(['/* comment 1 */', '//comment 2', 'x = 10;'].join('\n')); }); }); @@ -79,12 +77,7 @@ describe('BabelAstFactory', () => { const stmt1 = statement.ast`x = 10`; const stmt2 = statement.ast`y = 20`; const block = factory.createBlock([stmt1, stmt2]); - expect(generate(block).code).toEqual([ - '{', - ' x = 10;', - ' y = 20;', - '}', - ].join('\n')); + expect(generate(block).code).toEqual(['{', ' x = 10;', ' y = 20;', '}'].join('\n')); }); }); @@ -135,76 +128,54 @@ 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); - expect(generate(fn).code).toEqual([ - 'function foo(arg1, arg2) {', - ' x = 10;', - ' y = 20;', - '}', - ].join('\n')); - }); + 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); + expect(generate(fn).code).toEqual( + ['function foo(arg1, arg2) {', ' x = 10;', ' y = 20;', '}'].join('\n'), + ); + }); }); 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); - expect(t.isStatement(fn)).toBe(false); - expect(generate(fn).code).toEqual([ - 'function foo(arg1, arg2) {', - ' x = 10;', - ' y = 20;', - '}', - ].join('\n')); - }); + 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); + expect(t.isStatement(fn)).toBe(false); + expect(generate(fn).code).toEqual( + ['function foo(arg1, arg2) {', ' 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); - expect(generate(fn).code).toEqual([ - 'function (arg1, arg2) {', - ' x = 10;', - ' y = 20;', - '}', - ].join('\n')); + expect(generate(fn).code).toEqual( + ['function (arg1, arg2) {', ' x = 10;', ' y = 20;', '}'].join('\n'), + ); }); }); 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'); - }); + 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'); + }); it('should create an arrow function with an implicit return object literal', () => { const expr = expression.ast`{a: 1, b: 2}`; const fn = factory.createArrowFunctionExpression([], expr); - expect(generate(fn).code).toEqual([ - '() => ({', - ' a: 1,', - ' b: 2', - '})', - ].join('\n')); - }); - - 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); - expect(generate(fn).code).toEqual([ - '(arg1, arg2) => {', - ' x = 10;', - ' y = 20;', - ' return x + y;', - '}', - ].join('\n')); - }); + expect(generate(fn).code).toEqual(['() => ({', ' a: 1,', ' b: 2', '})'].join('\n')); + }); + + 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); + expect(generate(fn).code).toEqual( + ['(arg1, arg2) => {', ' x = 10;', ' y = 20;', ' return x + y;', '}'].join('\n'), + ); + }); }); describe('createIdentifier()', () => { @@ -279,14 +250,13 @@ describe('BabelAstFactory', () => { }); describe('createNewExpression()', () => { - it('should create a `new` operation on the constructor `expression` with the given `args`', - () => { - const expr = expression.ast`Foo`; - const arg1 = expression.ast`42`; - const arg2 = expression.ast`"moo"`; - const call = factory.createNewExpression(expr, [arg1, arg2]); - expect(generate(call).code).toEqual('new Foo(42, "moo")'); - }); + it('should create a `new` operation on the constructor `expression` with the given `args`', () => { + const expr = expression.ast`Foo`; + const arg1 = expression.ast`42`; + const arg2 = expression.ast`"moo"`; + const call = factory.createNewExpression(expr, [arg1, arg2]); + expect(generate(call).code).toEqual('new Foo(42, "moo")'); + }); }); describe('createObjectLiteral()', () => { @@ -297,12 +267,7 @@ describe('BabelAstFactory', () => { {propertyName: 'prop1', value: prop1, quoted: false}, {propertyName: 'prop2', value: prop2, quoted: true}, ]); - expect(generate(obj).code).toEqual([ - '{', - ' prop1: 42,', - ' "prop2": "moo"', - '}', - ].join('\n')); + expect(generate(obj).code).toEqual(['{', ' prop1: 42,', ' "prop2": "moo"', '}'].join('\n')); }); }); @@ -342,10 +307,7 @@ describe('BabelAstFactory', () => { {raw: 'raw2', cooked: 'cooked2', range: null}, {raw: 'raw3', cooked: 'cooked3', range: null}, ]; - const expressions = [ - expression.ast`42`, - expression.ast`"moo"`, - ]; + const expressions = [expression.ast`42`, expression.ast`"moo"`]; const tag = expression.ast`tagFn`; const template = factory.createTaggedTemplate(tag, {elements, expressions}); expect(generate(template).code).toEqual('tagFn`raw1${42}raw2${"moo"}raw3`'); @@ -377,32 +339,28 @@ 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'); - 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'); - 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'); - 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'); - expect(generate(varDecl).code).toEqual('let foo;'); - }); + 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'); + 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'); + 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'); + 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'); + expect(generate(varDecl).code).toEqual('let foo;'); + }); }); describe('setSourceMapRange()', () => { @@ -416,7 +374,7 @@ describe('BabelAstFactory', () => { start: {line: 0, column: 1, offset: 1}, end: {line: 2, column: 3, offset: 15}, content: '-****\n*****\n****', - url: 'other.ts' + url: 'other.ts', }); // Lines are 1-based in Babel. @@ -424,7 +382,7 @@ describe('BabelAstFactory', () => { filename: 'other.ts', start: {line: 1, column: 1}, end: {line: 3, column: 3}, - } as any); // The typings for `loc` do not include `filename`. + } as any); // The typings for `loc` do not include `filename`. expect(expr.start).toEqual(1); expect(expr.end).toEqual(15); }); @@ -435,7 +393,7 @@ describe('BabelAstFactory', () => { start: {line: 0, column: 1, offset: 1}, end: {line: 2, column: 3, offset: 15}, content: '-****\n*****\n****', - url: '/original.ts' + url: '/original.ts', }); // Lines are 1-based in Babel. @@ -443,7 +401,7 @@ describe('BabelAstFactory', () => { filename: undefined, start: {line: 1, column: 1}, end: {line: 3, column: 3}, - } as any); // The typings for `loc` do not include `filename`. + } as any); // The typings for `loc` do not include `filename`. }); }); }); diff --git a/packages/compiler-cli/linker/babel/test/ast/babel_ast_host_spec.ts b/packages/compiler-cli/linker/babel/test/ast/babel_ast_host_spec.ts index 81e84f81e3ad5..eac2587dea95e 100644 --- a/packages/compiler-cli/linker/babel/test/ast/babel_ast_host_spec.ts +++ b/packages/compiler-cli/linker/babel/test/ast/babel_ast_host_spec.ts @@ -11,7 +11,7 @@ import {BabelAstHost} from '../../src/ast/babel_ast_host'; describe('BabelAstHost', () => { let host: BabelAstHost; - beforeEach(() => host = new BabelAstHost()); + beforeEach(() => (host = new BabelAstHost())); describe('getSymbolName()', () => { it('should return the name of an identifier', () => { @@ -30,7 +30,7 @@ describe('BabelAstHost', () => { describe('isStringLiteral()', () => { it('should return true if the expression is a string literal', () => { expect(host.isStringLiteral(expr('"moo"'))).toBe(true); - expect(host.isStringLiteral(expr('\'moo\''))).toBe(true); + expect(host.isStringLiteral(expr("'moo'"))).toBe(true); }); it('should return false if the expression is not a string literal', () => { @@ -40,23 +40,24 @@ describe('BabelAstHost', () => { expect(host.isStringLiteral(expr('{}'))).toBe(false); expect(host.isStringLiteral(expr('[]'))).toBe(false); expect(host.isStringLiteral(expr('null'))).toBe(false); - expect(host.isStringLiteral(expr('\'a\' + \'b\''))).toBe(false); + expect(host.isStringLiteral(expr("'a' + 'b'"))).toBe(false); }); it('should return false if the expression is a template string', () => { - expect(host.isStringLiteral(expr('\`moo\`'))).toBe(false); + expect(host.isStringLiteral(expr('`moo`'))).toBe(false); }); }); describe('parseStringLiteral()', () => { it('should extract the string value', () => { expect(host.parseStringLiteral(expr('"moo"'))).toEqual('moo'); - expect(host.parseStringLiteral(expr('\'moo\''))).toEqual('moo'); + expect(host.parseStringLiteral(expr("'moo'"))).toEqual('moo'); }); it('should error if the value is not a string literal', () => { - expect(() => host.parseStringLiteral(expr('42'))) - .toThrowError('Unsupported syntax, expected a string literal.'); + expect(() => host.parseStringLiteral(expr('42'))).toThrowError( + 'Unsupported syntax, expected a string literal.', + ); }); }); @@ -68,13 +69,13 @@ describe('BabelAstHost', () => { it('should return false if the expression is not a number literal', () => { expect(host.isStringLiteral(expr('true'))).toBe(false); expect(host.isNumericLiteral(expr('"moo"'))).toBe(false); - expect(host.isNumericLiteral(expr('\'moo\''))).toBe(false); + expect(host.isNumericLiteral(expr("'moo'"))).toBe(false); expect(host.isNumericLiteral(expr('someIdentifier'))).toBe(false); expect(host.isNumericLiteral(expr('{}'))).toBe(false); expect(host.isNumericLiteral(expr('[]'))).toBe(false); expect(host.isNumericLiteral(expr('null'))).toBe(false); - expect(host.isNumericLiteral(expr('\'a\' + \'b\''))).toBe(false); - expect(host.isNumericLiteral(expr('\`moo\`'))).toBe(false); + expect(host.isNumericLiteral(expr("'a' + 'b'"))).toBe(false); + expect(host.isNumericLiteral(expr('`moo`'))).toBe(false); }); }); @@ -84,8 +85,9 @@ describe('BabelAstHost', () => { }); it('should error if the value is not a numeric literal', () => { - expect(() => host.parseNumericLiteral(expr('"moo"'))) - .toThrowError('Unsupported syntax, expected a numeric literal.'); + expect(() => host.parseNumericLiteral(expr('"moo"'))).toThrowError( + 'Unsupported syntax, expected a numeric literal.', + ); }); }); @@ -102,14 +104,14 @@ describe('BabelAstHost', () => { it('should return false if the expression is not a boolean literal', () => { expect(host.isBooleanLiteral(expr('"moo"'))).toBe(false); - expect(host.isBooleanLiteral(expr('\'moo\''))).toBe(false); + expect(host.isBooleanLiteral(expr("'moo'"))).toBe(false); expect(host.isBooleanLiteral(expr('someIdentifier'))).toBe(false); expect(host.isBooleanLiteral(expr('42'))).toBe(false); expect(host.isBooleanLiteral(expr('{}'))).toBe(false); expect(host.isBooleanLiteral(expr('[]'))).toBe(false); expect(host.isBooleanLiteral(expr('null'))).toBe(false); - expect(host.isBooleanLiteral(expr('\'a\' + \'b\''))).toBe(false); - expect(host.isBooleanLiteral(expr('\`moo\`'))).toBe(false); + expect(host.isBooleanLiteral(expr("'a' + 'b'"))).toBe(false); + expect(host.isBooleanLiteral(expr('`moo`'))).toBe(false); expect(host.isBooleanLiteral(expr('!2'))).toBe(false); expect(host.isBooleanLiteral(expr('~1'))).toBe(false); }); @@ -127,8 +129,9 @@ describe('BabelAstHost', () => { }); it('should error if the value is not a boolean literal', () => { - expect(() => host.parseBooleanLiteral(expr('"moo"'))) - .toThrowError('Unsupported syntax, expected a boolean literal.'); + expect(() => host.parseBooleanLiteral(expr('"moo"'))).toThrowError( + 'Unsupported syntax, expected a boolean literal.', + ); }); }); @@ -141,67 +144,71 @@ describe('BabelAstHost', () => { it('should return false if the expression is not an array literal', () => { expect(host.isArrayLiteral(expr('"moo"'))).toBe(false); - expect(host.isArrayLiteral(expr('\'moo\''))).toBe(false); + expect(host.isArrayLiteral(expr("'moo'"))).toBe(false); expect(host.isArrayLiteral(expr('someIdentifier'))).toBe(false); expect(host.isArrayLiteral(expr('42'))).toBe(false); expect(host.isArrayLiteral(expr('{}'))).toBe(false); expect(host.isArrayLiteral(expr('null'))).toBe(false); - expect(host.isArrayLiteral(expr('\'a\' + \'b\''))).toBe(false); - expect(host.isArrayLiteral(expr('\`moo\`'))).toBe(false); + expect(host.isArrayLiteral(expr("'a' + 'b'"))).toBe(false); + expect(host.isArrayLiteral(expr('`moo`'))).toBe(false); }); }); describe('parseArrayLiteral()', () => { it('should extract the expressions in the array', () => { - const moo = expr('\'moo\''); + const moo = expr("'moo'"); expect(host.parseArrayLiteral(expr('[]'))).toEqual([]); - expect(host.parseArrayLiteral(expr('[\'moo\']'))).toEqual([moo]); + expect(host.parseArrayLiteral(expr("['moo']"))).toEqual([moo]); }); it('should error if there is an empty item', () => { - expect(() => host.parseArrayLiteral(expr('[,]'))) - .toThrowError('Unsupported syntax, expected element in array not to be empty.'); + expect(() => host.parseArrayLiteral(expr('[,]'))).toThrowError( + 'Unsupported syntax, expected element in array not to be empty.', + ); }); it('should error if there is a spread element', () => { - expect(() => host.parseArrayLiteral(expr('[...[0,1]]'))) - .toThrowError('Unsupported syntax, expected element in array not to use spread syntax.'); + expect(() => host.parseArrayLiteral(expr('[...[0,1]]'))).toThrowError( + 'Unsupported syntax, expected element in array not to use spread syntax.', + ); }); }); describe('isObjectLiteral()', () => { it('should return true if the expression is an object literal', () => { expect(host.isObjectLiteral(rhs('x = {}'))).toBe(true); - expect(host.isObjectLiteral(rhs('x = { foo: \'bar\' }'))).toBe(true); + expect(host.isObjectLiteral(rhs("x = { foo: 'bar' }"))).toBe(true); }); it('should return false if the expression is not an object literal', () => { expect(host.isObjectLiteral(rhs('x = "moo"'))).toBe(false); - expect(host.isObjectLiteral(rhs('x = \'moo\''))).toBe(false); + expect(host.isObjectLiteral(rhs("x = 'moo'"))).toBe(false); expect(host.isObjectLiteral(rhs('x = someIdentifier'))).toBe(false); expect(host.isObjectLiteral(rhs('x = 42'))).toBe(false); expect(host.isObjectLiteral(rhs('x = []'))).toBe(false); expect(host.isObjectLiteral(rhs('x = null'))).toBe(false); - expect(host.isObjectLiteral(rhs('x = \'a\' + \'b\''))).toBe(false); - expect(host.isObjectLiteral(rhs('x = \`moo\`'))).toBe(false); + expect(host.isObjectLiteral(rhs("x = 'a' + 'b'"))).toBe(false); + expect(host.isObjectLiteral(rhs('x = `moo`'))).toBe(false); }); }); describe('parseObjectLiteral()', () => { it('should extract the properties from the object', () => { - const moo = expr('\'moo\''); + const moo = expr("'moo'"); expect(host.parseObjectLiteral(rhs('x = {}'))).toEqual(new Map()); - expect(host.parseObjectLiteral(rhs('x = {a: \'moo\'}'))).toEqual(new Map([['a', moo]])); + expect(host.parseObjectLiteral(rhs("x = {a: 'moo'}"))).toEqual(new Map([['a', moo]])); }); it('should error if there is a method', () => { - expect(() => host.parseObjectLiteral(rhs('x = { foo() {} }'))) - .toThrowError('Unsupported syntax, expected a property assignment.'); + expect(() => host.parseObjectLiteral(rhs('x = { foo() {} }'))).toThrowError( + 'Unsupported syntax, expected a property assignment.', + ); }); it('should error if there is a spread element', () => { - expect(() => host.parseObjectLiteral(rhs('x = {...{a:\'moo\'}}'))) - .toThrowError('Unsupported syntax, expected a property assignment.'); + expect(() => host.parseObjectLiteral(rhs("x = {...{a:'moo'}}"))).toThrowError( + 'Unsupported syntax, expected a property assignment.', + ); }); }); @@ -220,57 +227,61 @@ describe('BabelAstHost', () => { it('should return false if the expression is not a function expression', () => { expect(host.isFunctionExpression(expr('[]'))).toBe(false); expect(host.isFunctionExpression(expr('"moo"'))).toBe(false); - expect(host.isFunctionExpression(expr('\'moo\''))).toBe(false); + expect(host.isFunctionExpression(expr("'moo'"))).toBe(false); expect(host.isFunctionExpression(expr('someIdentifier'))).toBe(false); expect(host.isFunctionExpression(expr('42'))).toBe(false); expect(host.isFunctionExpression(expr('{}'))).toBe(false); expect(host.isFunctionExpression(expr('null'))).toBe(false); - expect(host.isFunctionExpression(expr('\'a\' + \'b\''))).toBe(false); - expect(host.isFunctionExpression(expr('\`moo\`'))).toBe(false); + expect(host.isFunctionExpression(expr("'a' + 'b'"))).toBe(false); + expect(host.isFunctionExpression(expr('`moo`'))).toBe(false); }); }); describe('parseReturnValue()', () => { it('should extract the return value of a function', () => { - const moo = expr('\'moo\''); - expect(host.parseReturnValue(rhs('x = function() { return \'moo\'; }'))).toEqual(moo); + const moo = expr("'moo'"); + expect(host.parseReturnValue(rhs("x = function() { return 'moo'; }"))).toEqual(moo); }); it('should extract the value of a simple arrow function', () => { - const moo = expr('\'moo\''); - expect(host.parseReturnValue(rhs('x = () => \'moo\''))).toEqual(moo); + const moo = expr("'moo'"); + expect(host.parseReturnValue(rhs("x = () => 'moo'"))).toEqual(moo); }); it('should extract the return value of an arrow function', () => { - const moo = expr('\'moo\''); - expect(host.parseReturnValue(rhs('x = () => { return \'moo\' }'))).toEqual(moo); + const moo = expr("'moo'"); + expect(host.parseReturnValue(rhs("x = () => { return 'moo' }"))).toEqual(moo); }); it('should error if the body has 0 statements', () => { - expect(() => host.parseReturnValue(rhs('x = function () { }'))) - .toThrowError( - 'Unsupported syntax, expected a function body with a single return statement.'); - expect(() => host.parseReturnValue(rhs('x = () => { }'))) - .toThrowError( - 'Unsupported syntax, expected a function body with a single return statement.'); + expect(() => host.parseReturnValue(rhs('x = function () { }'))).toThrowError( + 'Unsupported syntax, expected a function body with a single return statement.', + ); + expect(() => host.parseReturnValue(rhs('x = () => { }'))).toThrowError( + 'Unsupported syntax, expected a function body with a single return statement.', + ); }); it('should error if the body has more than 1 statement', () => { - expect(() => host.parseReturnValue(rhs('x = function () { const x = 10; return x; }'))) - .toThrowError( - 'Unsupported syntax, expected a function body with a single return statement.'); - expect(() => host.parseReturnValue(rhs('x = () => { const x = 10; return x; }'))) - .toThrowError( - 'Unsupported syntax, expected a function body with a single return statement.'); + expect(() => + host.parseReturnValue(rhs('x = function () { const x = 10; return x; }')), + ).toThrowError( + 'Unsupported syntax, expected a function body with a single return statement.', + ); + expect(() => + host.parseReturnValue(rhs('x = () => { const x = 10; return x; }')), + ).toThrowError( + 'Unsupported syntax, expected a function body with a single return statement.', + ); }); it('should error if the single statement is not a return statement', () => { - expect(() => host.parseReturnValue(rhs('x = function () { const x = 10; }'))) - .toThrowError( - 'Unsupported syntax, expected a function body with a single return statement.'); - expect(() => host.parseReturnValue(rhs('x = () => { const x = 10; }'))) - .toThrowError( - 'Unsupported syntax, expected a function body with a single return statement.'); + expect(() => host.parseReturnValue(rhs('x = function () { const x = 10; }'))).toThrowError( + 'Unsupported syntax, expected a function body with a single return statement.', + ); + expect(() => host.parseReturnValue(rhs('x = () => { const x = 10; }'))).toThrowError( + 'Unsupported syntax, expected a function body with a single return statement.', + ); }); }); @@ -281,13 +292,15 @@ describe('BabelAstHost', () => { }); it('should error if the node is not a function declaration or arrow function', () => { - expect(() => host.parseParameters(expr('[]'))) - .toThrowError('Unsupported syntax, expected a function.'); + expect(() => host.parseParameters(expr('[]'))).toThrowError( + 'Unsupported syntax, expected a function.', + ); }); it('should error if a parameter uses spread syntax', () => { - expect(() => host.parseParameters(rhs('x = function(a, ...other) {}'))) - .toThrowError('Unsupported syntax, expected an identifier.'); + expect(() => host.parseParameters(rhs('x = function(a, ...other) {}'))).toThrowError( + 'Unsupported syntax, expected an identifier.', + ); }); }); @@ -301,13 +314,13 @@ describe('BabelAstHost', () => { it('should return false if the expression is not a call expression', () => { expect(host.isCallExpression(expr('[]'))).toBe(false); expect(host.isCallExpression(expr('"moo"'))).toBe(false); - expect(host.isCallExpression(expr('\'moo\''))).toBe(false); + expect(host.isCallExpression(expr("'moo'"))).toBe(false); expect(host.isCallExpression(expr('someIdentifier'))).toBe(false); expect(host.isCallExpression(expr('42'))).toBe(false); expect(host.isCallExpression(expr('{}'))).toBe(false); expect(host.isCallExpression(expr('null'))).toBe(false); - expect(host.isCallExpression(expr('\'a\' + \'b\''))).toBe(false); - expect(host.isCallExpression(expr('\`moo\`'))).toBe(false); + expect(host.isCallExpression(expr("'a' + 'b'"))).toBe(false); + expect(host.isCallExpression(expr('`moo`'))).toBe(false); }); }); @@ -318,8 +331,9 @@ describe('BabelAstHost', () => { }); it('should error if the node is not a call expression', () => { - expect(() => host.parseCallee(expr('[]'))) - .toThrowError('Unsupported syntax, expected a call expression.'); + expect(() => host.parseCallee(expr('[]'))).toThrowError( + 'Unsupported syntax, expected a call expression.', + ); }); }); @@ -330,30 +344,37 @@ describe('BabelAstHost', () => { }); it('should error if the node is not a call expression', () => { - expect(() => host.parseArguments(expr('[]'))) - .toThrowError('Unsupported syntax, expected a call expression.'); + expect(() => host.parseArguments(expr('[]'))).toThrowError( + 'Unsupported syntax, expected a call expression.', + ); }); it('should error if an argument uses spread syntax', () => { - expect(() => host.parseArguments(expr('foo(1, ...[])'))) - .toThrowError('Unsupported syntax, expected argument not to use spread syntax.'); + expect(() => host.parseArguments(expr('foo(1, ...[])'))).toThrowError( + 'Unsupported syntax, expected argument not to use spread syntax.', + ); }); }); describe('getRange()', () => { it('should extract the range from the expression', () => { - const file = parse('// preamble\nx = \'moo\';'); + const file = parse("// preamble\nx = 'moo';"); const stmt = file!.program.body[0] as t.Statement; assertExpressionStatement(stmt); assertAssignmentExpression(stmt.expression); - expect(host.getRange(stmt.expression.right)) - .toEqual({startLine: 1, startCol: 4, startPos: 16, endPos: 21}); + expect(host.getRange(stmt.expression.right)).toEqual({ + startLine: 1, + startCol: 4, + startPos: 16, + endPos: 21, + }); }); it('should error if there is no range information', () => { - const moo = rhs('// preamble\nx = \'moo\';'); - expect(() => host.getRange(moo)) - .toThrowError('Unable to read range for node - it is missing location information.'); + const moo = rhs("// preamble\nx = 'moo';"); + expect(() => host.getRange(moo)).toThrowError( + 'Unable to read range for node - it is missing location information.', + ); }); }); }); diff --git a/packages/compiler-cli/linker/babel/test/babel_declaration_scope_spec.ts b/packages/compiler-cli/linker/babel/test/babel_declaration_scope_spec.ts index 2b30d53d874ac..535ba50213354 100644 --- a/packages/compiler-cli/linker/babel/test/babel_declaration_scope_spec.ts +++ b/packages/compiler-cli/linker/babel/test/babel_declaration_scope_spec.ts @@ -14,13 +14,14 @@ describe('BabelDeclarationScope', () => { describe('getConstantScopeRef()', () => { it('should return a path to the ES module where the expression was imported', () => { const ast = parse( - [ - 'import * as core from \'@angular/core\';', - 'function foo() {', - ' var TEST = core;', - '}', - ].join('\n'), - {sourceType: 'module'}) as t.File; + [ + "import * as core from '@angular/core';", + 'function foo() {', + ' var TEST = core;', + '}', + ].join('\n'), + {sourceType: 'module'}, + ) as t.File; const nodePath = findVarDeclaration(ast, 'TEST'); const scope = new BabelDeclarationScope(nodePath.scope); const constantScope = scope.getConstantScopeRef(nodePath.get('init').node); @@ -30,13 +31,9 @@ describe('BabelDeclarationScope', () => { it('should return a path to the ES Module where the expression is declared', () => { const ast = parse( - [ - 'var core;', - 'export function foo() {', - ' var TEST = core;', - '}', - ].join('\n'), - {sourceType: 'module'}) as t.File; + ['var core;', 'export function foo() {', ' var TEST = core;', '}'].join('\n'), + {sourceType: 'module'}, + ) as t.File; const nodePath = findVarDeclaration(ast, 'TEST'); const scope = new BabelDeclarationScope(nodePath.scope); const constantScope = scope.getConstantScopeRef(nodePath.get('init').node); @@ -45,14 +42,9 @@ describe('BabelDeclarationScope', () => { }); it('should return null if the file is not an ES module', () => { - const ast = parse( - [ - 'var core;', - 'function foo() {', - ' var TEST = core;', - '}', - ].join('\n'), - {sourceType: 'script'}) as t.File; + const ast = parse(['var core;', 'function foo() {', ' var TEST = core;', '}'].join('\n'), { + sourceType: 'script', + }) as t.File; const nodePath = findVarDeclaration(ast, 'TEST'); const scope = new BabelDeclarationScope(nodePath.scope); const constantScope = scope.getConstantScopeRef(nodePath.get('init').node); @@ -61,16 +53,17 @@ describe('BabelDeclarationScope', () => { it('should return the IIFE factory function where the expression is a parameter', () => { const ast = parse( - [ - 'var core;', - '(function(core) {', - ' var BLOCK = \'block\';', - ' function foo() {', - ' var TEST = core;', - ' }', - '})(core);', - ].join('\n'), - {sourceType: 'script'}) as t.File; + [ + 'var core;', + '(function(core) {', + " var BLOCK = 'block';", + ' function foo() {', + ' var TEST = core;', + ' }', + '})(core);', + ].join('\n'), + {sourceType: 'script'}, + ) as t.File; const nodePath = findVarDeclaration(ast, 'TEST'); const fnPath = findFirstFunction(ast); const scope = new BabelDeclarationScope(nodePath.scope); @@ -88,11 +81,13 @@ describe('BabelDeclarationScope', () => { * Note: the `init` property is explicitly omitted to workaround a performance cliff in the * TypeScript type checker. */ -type InitializedVariableDeclarator = Omit&{init: t.Expression}; +type InitializedVariableDeclarator = Omit & {init: t.Expression}; function findVarDeclaration( - file: t.File, varName: string): NodePath { - let varDecl: NodePath|undefined = undefined; + file: t.File, + varName: string, +): NodePath { + let varDecl: NodePath | undefined = undefined; traverse(file, { VariableDeclarator: (path: NodePath) => { const id = path.get('id'); @@ -100,7 +95,7 @@ function findVarDeclaration( varDecl = path; path.stop(); } - } + }, }); if (varDecl === undefined) { throw new Error(`TEST BUG: expected to find variable declaration for ${varName}.`); @@ -109,12 +104,12 @@ function findVarDeclaration( } function findFirstFunction(file: t.File): NodePath { - let fn: NodePath|undefined = undefined; + let fn: NodePath | undefined = undefined; traverse(file, { Function: (path) => { fn = path; path.stop(); - } + }, }); if (fn === undefined) { throw new Error(`TEST BUG: expected to find a function.`); diff --git a/packages/compiler-cli/linker/babel/test/babel_plugin_spec.ts b/packages/compiler-cli/linker/babel/test/babel_plugin_spec.ts index 1c74ad1e518c4..9711e53c030cd 100644 --- a/packages/compiler-cli/linker/babel/test/babel_plugin_spec.ts +++ b/packages/compiler-cli/linker/babel/test/babel_plugin_spec.ts @@ -10,7 +10,7 @@ import babel from '@babel/core'; describe('default babel plugin entry-point', () => { it('should work as a Babel plugin using the module specifier', async () => { const result = (await babel.transformAsync( - ` + ` import * as i0 from "@angular/core"; export class MyMod {} @@ -18,12 +18,11 @@ describe('default babel plugin entry-point', () => { MyMod.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyMod, declarations: [MyComponent] }); `, - { - plugins: [ - '@angular/compiler-cli/linker/babel/index.mjs', - ], - filename: 'test.js', - }))!; + { + plugins: ['@angular/compiler-cli/linker/babel/index.mjs'], + filename: 'test.js', + }, + ))!; expect(result).not.toBeNull(); expect(result.code).not.toContain('ɵɵngDeclareNgModule'); @@ -33,7 +32,7 @@ describe('default babel plugin entry-point', () => { it('should be configurable', async () => { const result = (await babel.transformAsync( - ` + ` import * as i0 from "@angular/core"; export class MyMod {} @@ -41,12 +40,11 @@ describe('default babel plugin entry-point', () => { MyMod.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: MyMod, declarations: [MyComponent] }); `, - { - plugins: [ - ['@angular/compiler-cli/linker/babel/index.mjs', {linkerJitMode: true}], - ], - filename: 'test.js', - }))!; + { + plugins: [['@angular/compiler-cli/linker/babel/index.mjs', {linkerJitMode: true}]], + filename: 'test.js', + }, + ))!; expect(result).not.toBeNull(); expect(result.code).not.toContain('ɵɵngDeclareNgModule'); diff --git a/packages/compiler-cli/linker/babel/test/es2015_linker_plugin_spec.ts b/packages/compiler-cli/linker/babel/test/es2015_linker_plugin_spec.ts index a2d803b852b96..e1734320ac1da 100644 --- a/packages/compiler-cli/linker/babel/test/es2015_linker_plugin_spec.ts +++ b/packages/compiler-cli/linker/babel/test/es2015_linker_plugin_spec.ts @@ -20,115 +20,119 @@ import {PartialDirectiveLinkerVersion1} from '../../src/file_linker/partial_link import {createEs2015LinkerPlugin} from '../src/es2015_linker_plugin'; describe('createEs2015LinkerPlugin()', () => { - it('should return a Babel plugin visitor that handles Program (enter/exit) and CallExpression nodes', - () => { - const fileSystem = new MockFileSystemNative(); - const logger = new MockLogger(); - const plugin = createEs2015LinkerPlugin({fileSystem, logger}); - expect(plugin.visitor).toEqual({ - Program: { - enter: jasmine.any(Function), - exit: jasmine.any(Function), - }, - CallExpression: jasmine.any(Function), - }); - }); + it('should return a Babel plugin visitor that handles Program (enter/exit) and CallExpression nodes', () => { + const fileSystem = new MockFileSystemNative(); + const logger = new MockLogger(); + const plugin = createEs2015LinkerPlugin({fileSystem, logger}); + expect(plugin.visitor).toEqual({ + Program: { + enter: jasmine.any(Function), + exit: jasmine.any(Function), + }, + CallExpression: jasmine.any(Function), + }); + }); - it('should return a Babel plugin that calls FileLinker.isPartialDeclaration() on each call expression', - () => { - const isPartialDeclarationSpy = spyOn(FileLinker.prototype, 'isPartialDeclaration'); + it('should return a Babel plugin that calls FileLinker.isPartialDeclaration() on each call expression', () => { + const isPartialDeclarationSpy = spyOn(FileLinker.prototype, 'isPartialDeclaration'); - const fileSystem = new MockFileSystemNative(); - const logger = new MockLogger(); - const plugin = createEs2015LinkerPlugin({fileSystem, logger}); - babel.transformSync( - [ - 'var core;', `fn1()`, 'fn2({prop: () => fn3({})});', `x.method(() => fn4());`, - 'spread(...x);' - ].join('\n'), - { - plugins: [plugin], - filename: '/test.js', - parserOpts: {sourceType: 'unambiguous'}, - }); - expect(isPartialDeclarationSpy.calls.allArgs()).toEqual([ - ['fn1'], - ['fn2'], - ['fn3'], - ['method'], - ['fn4'], - ['spread'], - ]); - }); + const fileSystem = new MockFileSystemNative(); + const logger = new MockLogger(); + const plugin = createEs2015LinkerPlugin({fileSystem, logger}); + babel.transformSync( + [ + 'var core;', + `fn1()`, + 'fn2({prop: () => fn3({})});', + `x.method(() => fn4());`, + 'spread(...x);', + ].join('\n'), + { + plugins: [plugin], + filename: '/test.js', + parserOpts: {sourceType: 'unambiguous'}, + }, + ); + expect(isPartialDeclarationSpy.calls.allArgs()).toEqual([ + ['fn1'], + ['fn2'], + ['fn3'], + ['method'], + ['fn4'], + ['spread'], + ]); + }); - it('should return a Babel plugin that calls FileLinker.linkPartialDeclaration() on each matching declaration', - () => { - const linkSpy = spyOn(FileLinker.prototype, 'linkPartialDeclaration') - .and.returnValue(t.identifier('REPLACEMENT')); - const fileSystem = new MockFileSystemNative(); - const logger = new MockLogger(); - const plugin = createEs2015LinkerPlugin({fileSystem, logger}); + it('should return a Babel plugin that calls FileLinker.linkPartialDeclaration() on each matching declaration', () => { + const linkSpy = spyOn(FileLinker.prototype, 'linkPartialDeclaration').and.returnValue( + t.identifier('REPLACEMENT'), + ); + const fileSystem = new MockFileSystemNative(); + const logger = new MockLogger(); + const plugin = createEs2015LinkerPlugin({fileSystem, logger}); - babel.transformSync( - [ - 'var core;', - `ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core, x: 1});`, - `i0.ɵɵngDeclareComponent({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core, foo: () => ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core, x: 2})});`, - `x.qux(() => ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core, x: 3}));`, - 'spread(...x);', - `i0['ɵɵngDeclareDirective']({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core, x: 4});`, - ].join('\n'), - { - plugins: [createEs2015LinkerPlugin({fileSystem, logger})], - filename: '/test.js', - parserOpts: {sourceType: 'unambiguous'}, - }); + babel.transformSync( + [ + 'var core;', + `ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core, x: 1});`, + `i0.ɵɵngDeclareComponent({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core, foo: () => ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core, x: 2})});`, + `x.qux(() => ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core, x: 3}));`, + 'spread(...x);', + `i0['ɵɵngDeclareDirective']({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core, x: 4});`, + ].join('\n'), + { + plugins: [createEs2015LinkerPlugin({fileSystem, logger})], + filename: '/test.js', + parserOpts: {sourceType: 'unambiguous'}, + }, + ); - expect(humanizeLinkerCalls(linkSpy.calls)).toEqual([ - [ - 'ɵɵngDeclareDirective', - '{minVersion:\'0.0.0-PLACEHOLDER\',version:\'0.0.0-PLACEHOLDER\',ngImport:core,x:1}' - ], - [ - 'ɵɵngDeclareComponent', - '{minVersion:\'0.0.0-PLACEHOLDER\',version:\'0.0.0-PLACEHOLDER\',ngImport:core,foo:()=>ɵɵngDeclareDirective({minVersion:\'0.0.0-PLACEHOLDER\',version:\'0.0.0-PLACEHOLDER\',ngImport:core,x:2})}' - ], - // Note we do not process `x:2` declaration since it is nested within another declaration - [ - 'ɵɵngDeclareDirective', - '{minVersion:\'0.0.0-PLACEHOLDER\',version:\'0.0.0-PLACEHOLDER\',ngImport:core,x:3}' - ], - [ - 'ɵɵngDeclareDirective', - '{minVersion:\'0.0.0-PLACEHOLDER\',version:\'0.0.0-PLACEHOLDER\',ngImport:core,x:4}' - ], - ]); - }); + expect(humanizeLinkerCalls(linkSpy.calls)).toEqual([ + [ + 'ɵɵngDeclareDirective', + "{minVersion:'0.0.0-PLACEHOLDER',version:'0.0.0-PLACEHOLDER',ngImport:core,x:1}", + ], + [ + 'ɵɵngDeclareComponent', + "{minVersion:'0.0.0-PLACEHOLDER',version:'0.0.0-PLACEHOLDER',ngImport:core,foo:()=>ɵɵngDeclareDirective({minVersion:'0.0.0-PLACEHOLDER',version:'0.0.0-PLACEHOLDER',ngImport:core,x:2})}", + ], + // Note we do not process `x:2` declaration since it is nested within another declaration + [ + 'ɵɵngDeclareDirective', + "{minVersion:'0.0.0-PLACEHOLDER',version:'0.0.0-PLACEHOLDER',ngImport:core,x:3}", + ], + [ + 'ɵɵngDeclareDirective', + "{minVersion:'0.0.0-PLACEHOLDER',version:'0.0.0-PLACEHOLDER',ngImport:core,x:4}", + ], + ]); + }); - it('should return a Babel plugin that replaces call expressions with the return value from FileLinker.linkPartialDeclaration()', - () => { - let replaceCount = 0; - spyOn(FileLinker.prototype, 'linkPartialDeclaration') - .and.callFake(() => t.identifier('REPLACEMENT_' + ++replaceCount)); - const fileSystem = new MockFileSystemNative(); - const logger = new MockLogger(); - const plugin = createEs2015LinkerPlugin({fileSystem, logger}); - const result = babel.transformSync( - [ - 'var core;', - 'ɵɵngDeclareDirective({version: \'0.0.0-PLACEHOLDER\', ngImport: core});', - 'ɵɵngDeclareDirective({version: \'0.0.0-PLACEHOLDER\', ngImport: core, foo: () => bar({})});', - 'x.qux();', - 'spread(...x);', - ].join('\n'), - { - plugins: [createEs2015LinkerPlugin({fileSystem, logger})], - filename: '/test.js', - parserOpts: {sourceType: 'unambiguous'}, - generatorOpts: {compact: true}, - }); - expect(result!.code).toEqual('var core;REPLACEMENT_1;REPLACEMENT_2;x.qux();spread(...x);'); - }); + it('should return a Babel plugin that replaces call expressions with the return value from FileLinker.linkPartialDeclaration()', () => { + let replaceCount = 0; + spyOn(FileLinker.prototype, 'linkPartialDeclaration').and.callFake(() => + t.identifier('REPLACEMENT_' + ++replaceCount), + ); + const fileSystem = new MockFileSystemNative(); + const logger = new MockLogger(); + const plugin = createEs2015LinkerPlugin({fileSystem, logger}); + const result = babel.transformSync( + [ + 'var core;', + "ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core});", + "ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core, foo: () => bar({})});", + 'x.qux();', + 'spread(...x);', + ].join('\n'), + { + plugins: [createEs2015LinkerPlugin({fileSystem, logger})], + filename: '/test.js', + parserOpts: {sourceType: 'unambiguous'}, + generatorOpts: {compact: true}, + }, + ); + expect(result!.code).toEqual('var core;REPLACEMENT_1;REPLACEMENT_2;x.qux();spread(...x);'); + }); it('should return a Babel plugin that adds shared statements after any imports', () => { spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT')); @@ -136,165 +140,180 @@ describe('createEs2015LinkerPlugin()', () => { const logger = new MockLogger(); const plugin = createEs2015LinkerPlugin({fileSystem, logger}); const result = babel.transformSync( - [ - 'import * as core from \'some-module\';', - 'import {id} from \'other-module\';', - `ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, - `ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, - `ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, - ].join('\n'), - { - plugins: [createEs2015LinkerPlugin({fileSystem, logger})], - filename: '/test.js', - parserOpts: {sourceType: 'unambiguous'}, - generatorOpts: {compact: true}, - }); - expect(result!.code) - .toEqual( - 'import*as core from\'some-module\';import{id}from\'other-module\';const _c0=[1];const _c1=[2];const _c2=[3];"REPLACEMENT";"REPLACEMENT";"REPLACEMENT";'); + [ + "import * as core from 'some-module';", + "import {id} from 'other-module';", + `ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, + `ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, + `ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, + ].join('\n'), + { + plugins: [createEs2015LinkerPlugin({fileSystem, logger})], + filename: '/test.js', + parserOpts: {sourceType: 'unambiguous'}, + generatorOpts: {compact: true}, + }, + ); + expect(result!.code).toEqual( + 'import*as core from\'some-module\';import{id}from\'other-module\';const _c0=[1];const _c1=[2];const _c2=[3];"REPLACEMENT";"REPLACEMENT";"REPLACEMENT";', + ); }); - it('should return a Babel plugin that adds shared statements at the start of the program if it is an ECMAScript Module and there are no imports', - () => { - spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT')); - const fileSystem = new MockFileSystemNative(); - const logger = new MockLogger(); - const plugin = createEs2015LinkerPlugin({fileSystem, logger}); - const result = babel.transformSync( - [ - 'var core;', - `ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, - `ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, - `ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, - ].join('\n'), - { - plugins: [createEs2015LinkerPlugin({fileSystem, logger})], - filename: '/test.js', - // We declare the file as a module because this cannot be inferred from the source - parserOpts: {sourceType: 'module'}, - generatorOpts: {compact: true}, - }); - expect(result!.code) - .toEqual( - 'const _c0=[1];const _c1=[2];const _c2=[3];var core;"REPLACEMENT";"REPLACEMENT";"REPLACEMENT";'); - }); + it('should return a Babel plugin that adds shared statements at the start of the program if it is an ECMAScript Module and there are no imports', () => { + spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT')); + const fileSystem = new MockFileSystemNative(); + const logger = new MockLogger(); + const plugin = createEs2015LinkerPlugin({fileSystem, logger}); + const result = babel.transformSync( + [ + 'var core;', + `ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, + `ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, + `ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, + ].join('\n'), + { + plugins: [createEs2015LinkerPlugin({fileSystem, logger})], + filename: '/test.js', + // We declare the file as a module because this cannot be inferred from the source + parserOpts: {sourceType: 'module'}, + generatorOpts: {compact: true}, + }, + ); + expect(result!.code).toEqual( + 'const _c0=[1];const _c1=[2];const _c2=[3];var core;"REPLACEMENT";"REPLACEMENT";"REPLACEMENT";', + ); + }); - it('should return a Babel plugin that adds shared statements at the start of the function body if the ngImport is from a function parameter', - () => { - spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT')); - const fileSystem = new MockFileSystemNative(); - const logger = new MockLogger(); - const plugin = createEs2015LinkerPlugin({fileSystem, logger}); - const result = babel.transformSync( - [ - 'function run(core) {', - ` ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, - ` ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, - ` ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, - '}' - ].join('\n'), - { - plugins: [createEs2015LinkerPlugin({fileSystem, logger})], - filename: '/test.js', - parserOpts: {sourceType: 'unambiguous'}, - generatorOpts: {compact: true}, - }); - expect(result!.code) - .toEqual( - 'function run(core){const _c0=[1];const _c1=[2];const _c2=[3];"REPLACEMENT";"REPLACEMENT";"REPLACEMENT";}'); - }); + it('should return a Babel plugin that adds shared statements at the start of the function body if the ngImport is from a function parameter', () => { + spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT')); + const fileSystem = new MockFileSystemNative(); + const logger = new MockLogger(); + const plugin = createEs2015LinkerPlugin({fileSystem, logger}); + const result = babel.transformSync( + [ + 'function run(core) {', + ` ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, + ` ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, + ` ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, + '}', + ].join('\n'), + { + plugins: [createEs2015LinkerPlugin({fileSystem, logger})], + filename: '/test.js', + parserOpts: {sourceType: 'unambiguous'}, + generatorOpts: {compact: true}, + }, + ); + expect(result!.code).toEqual( + 'function run(core){const _c0=[1];const _c1=[2];const _c2=[3];"REPLACEMENT";"REPLACEMENT";"REPLACEMENT";}', + ); + }); - it('should return a Babel plugin that adds shared statements into an IIFE if no scope could not be derived for the ngImport', - () => { - spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT')); - const fileSystem = new MockFileSystemNative(); - const logger = new MockLogger(); - const plugin = createEs2015LinkerPlugin({fileSystem, logger}); - const result = babel.transformSync( - [ - 'function run() {', - ` ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, - ` ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, - ` ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, - '}', - ].join('\n'), - { - plugins: [createEs2015LinkerPlugin({fileSystem, logger})], - filename: '/test.js', - parserOpts: {sourceType: 'unambiguous'}, - generatorOpts: {compact: true}, - }); - expect(result!.code).toEqual([ - `function run(){`, - `(function(){const _c0=[1];return"REPLACEMENT";})();`, - `(function(){const _c0=[2];return"REPLACEMENT";})();`, - `(function(){const _c0=[3];return"REPLACEMENT";})();`, - `}`, - ].join('')); - }); + it('should return a Babel plugin that adds shared statements into an IIFE if no scope could not be derived for the ngImport', () => { + spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT')); + const fileSystem = new MockFileSystemNative(); + const logger = new MockLogger(); + const plugin = createEs2015LinkerPlugin({fileSystem, logger}); + const result = babel.transformSync( + [ + 'function run() {', + ` ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, + ` ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, + ` ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, + '}', + ].join('\n'), + { + plugins: [createEs2015LinkerPlugin({fileSystem, logger})], + filename: '/test.js', + parserOpts: {sourceType: 'unambiguous'}, + generatorOpts: {compact: true}, + }, + ); + expect(result!.code).toEqual( + [ + `function run(){`, + `(function(){const _c0=[1];return"REPLACEMENT";})();`, + `(function(){const _c0=[2];return"REPLACEMENT";})();`, + `(function(){const _c0=[3];return"REPLACEMENT";})();`, + `}`, + ].join(''), + ); + }); - it('should still execute other plugins that match AST nodes inside the result of the replacement', - () => { - spyOnLinkPartialDeclarationWithConstants(o.fn([], [], null, null, 'FOO')); - const fileSystem = new MockFileSystemNative(); - const logger = new MockLogger(); - const plugin = createEs2015LinkerPlugin({fileSystem, logger}); - const result = babel.transformSync( - [ - `ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core}); FOO;`, - ].join('\n'), - { - plugins: [ - createEs2015LinkerPlugin({fileSystem, logger}), - createIdentifierMapperPlugin('FOO', 'BAR'), - createIdentifierMapperPlugin('_c0', 'x1'), - ], - filename: '/test.js', - parserOpts: {sourceType: 'module'}, - generatorOpts: {compact: true}, - }); - expect(result!.code).toEqual([ - `(function(){const x1=[1];return function BAR(){};})();BAR;`, - ].join('')); - }); + it('should still execute other plugins that match AST nodes inside the result of the replacement', () => { + spyOnLinkPartialDeclarationWithConstants(o.fn([], [], null, null, 'FOO')); + const fileSystem = new MockFileSystemNative(); + const logger = new MockLogger(); + const plugin = createEs2015LinkerPlugin({fileSystem, logger}); + const result = babel.transformSync( + [ + `ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core}); FOO;`, + ].join('\n'), + { + plugins: [ + createEs2015LinkerPlugin({fileSystem, logger}), + createIdentifierMapperPlugin('FOO', 'BAR'), + createIdentifierMapperPlugin('_c0', 'x1'), + ], + filename: '/test.js', + parserOpts: {sourceType: 'module'}, + generatorOpts: {compact: true}, + }, + ); + expect(result!.code).toEqual( + [`(function(){const x1=[1];return function BAR(){};})();BAR;`].join(''), + ); + }); it('should not process call expressions within inserted functions', () => { - spyOn(PartialDirectiveLinkerVersion1.prototype, 'linkPartialDeclaration') - .and.callFake((constantPool => { - // Insert a call expression into the constant pool. This is inserted into - // Babel's AST upon program exit, and will therefore be visited by Babel - // outside of an active linker context. - constantPool.statements.push( - o.fn(/* params */[], /* body */[], /* type */ undefined, - /* sourceSpan */ undefined, /* name */ 'inserted') - .callFn([]) - .toStmt()); + spyOn(PartialDirectiveLinkerVersion1.prototype, 'linkPartialDeclaration').and.callFake((( + constantPool, + ) => { + // Insert a call expression into the constant pool. This is inserted into + // Babel's AST upon program exit, and will therefore be visited by Babel + // outside of an active linker context. + constantPool.statements.push( + o + .fn( + /* params */ [], + /* body */ [], + /* type */ undefined, + /* sourceSpan */ undefined, + /* name */ 'inserted', + ) + .callFn([]) + .toStmt(), + ); - return { - expression: o.literal('REPLACEMENT'), - statements: [], - }; - }) as typeof PartialDirectiveLinkerVersion1.prototype.linkPartialDeclaration); + return { + expression: o.literal('REPLACEMENT'), + statements: [], + }; + }) as typeof PartialDirectiveLinkerVersion1.prototype.linkPartialDeclaration); - const isPartialDeclarationSpy = - spyOn(FileLinker.prototype, 'isPartialDeclaration').and.callThrough(); + const isPartialDeclarationSpy = spyOn( + FileLinker.prototype, + 'isPartialDeclaration', + ).and.callThrough(); const fileSystem = new MockFileSystemNative(); const logger = new MockLogger(); const plugin = createEs2015LinkerPlugin({fileSystem, logger}); const result = babel.transformSync( - [ - 'import * as core from \'some-module\';', - `ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, - ].join('\n'), - { - plugins: [createEs2015LinkerPlugin({fileSystem, logger})], - filename: '/test.js', - parserOpts: {sourceType: 'unambiguous'}, - generatorOpts: {compact: true}, - }); - expect(result!.code) - .toEqual('import*as core from\'some-module\';(function inserted(){})();"REPLACEMENT";'); + [ + "import * as core from 'some-module';", + `ɵɵngDeclareDirective({minVersion: '0.0.0-PLACEHOLDER', version: '0.0.0-PLACEHOLDER', ngImport: core})`, + ].join('\n'), + { + plugins: [createEs2015LinkerPlugin({fileSystem, logger})], + filename: '/test.js', + parserOpts: {sourceType: 'unambiguous'}, + generatorOpts: {compact: true}, + }, + ); + expect(result!.code).toEqual( + 'import*as core from\'some-module\';(function inserted(){})();"REPLACEMENT";', + ); expect(isPartialDeclarationSpy.calls.allArgs()).toEqual([['ɵɵngDeclareDirective']]); }); @@ -304,7 +323,8 @@ describe('createEs2015LinkerPlugin()', () => { * Convert the arguments of the spied-on `calls` into a human readable array. */ function humanizeLinkerCalls( - calls: jasmine.Calls) { + calls: jasmine.Calls, +) { return calls.all().map(({args: [fn, args]}) => [fn, generate(args[0], {compact: true}).code]); } @@ -314,17 +334,18 @@ function humanizeLinkerCalls( */ function spyOnLinkPartialDeclarationWithConstants(replacement: o.Expression) { let callCount = 0; - spyOn(PartialDirectiveLinkerVersion1.prototype, 'linkPartialDeclaration') - .and.callFake((constantPool => { - const constArray = o.literalArr([o.literal(++callCount)]); - // We have to add the constant twice or it will not create a shared statement - constantPool.getConstLiteral(constArray); - constantPool.getConstLiteral(constArray); - return { - expression: replacement, - statements: [], - }; - }) as typeof PartialDirectiveLinkerVersion1.prototype.linkPartialDeclaration); + spyOn(PartialDirectiveLinkerVersion1.prototype, 'linkPartialDeclaration').and.callFake((( + constantPool, + ) => { + const constArray = o.literalArr([o.literal(++callCount)]); + // We have to add the constant twice or it will not create a shared statement + constantPool.getConstLiteral(constArray); + constantPool.getConstLiteral(constArray); + return { + expression: replacement, + statements: [], + }; + }) as typeof PartialDirectiveLinkerVersion1.prototype.linkPartialDeclaration); } /** @@ -338,7 +359,7 @@ function createIdentifierMapperPlugin(src: string, dest: string): PluginObj { if (path.node.name === src) { path.replaceWith(t.identifier(dest)); } - } + }, }, }; } diff --git a/packages/compiler-cli/linker/src/ast/ast_host.ts b/packages/compiler-cli/linker/src/ast/ast_host.ts index 1a7cdde4f4402..31fc7f3316317 100644 --- a/packages/compiler-cli/linker/src/ast/ast_host.ts +++ b/packages/compiler-cli/linker/src/ast/ast_host.ts @@ -15,7 +15,7 @@ export interface AstHost { * Get the name of the symbol represented by the given expression node, or `null` if it is not a * symbol. */ - getSymbolName(node: TExpression): string|null; + getSymbolName(node: TExpression): string | null; /** * Return `true` if the given expression is a string literal, or false otherwise. diff --git a/packages/compiler-cli/linker/src/ast/ast_value.ts b/packages/compiler-cli/linker/src/ast/ast_value.ts index 81cd052af8e47..77adf75d53444 100644 --- a/packages/compiler-cli/linker/src/ast/ast_value.ts +++ b/packages/compiler-cli/linker/src/ast/ast_value.ts @@ -17,17 +17,17 @@ import {AstHost, Range} from './ast_host'; * Note: Excluding `Array` types as we consider object literals are "objects" * in the AST. */ -type ObjectType = T extends Array? never : T extends Record? T : never; +type ObjectType = T extends Array ? never : T extends Record ? T : never; /** * Represents the value type of an object literal. */ -type ObjectValueType = T extends Record? R : never; +type ObjectValueType = T extends Record ? R : never; /** * Represents the value type of an array literal. */ -type ArrayValueType = T extends Array? R : never; +type ArrayValueType = T extends Array ? R : never; /** * Ensures that `This` has its generic type `Actual` conform to the expected generic type in @@ -38,7 +38,7 @@ type ConformsTo = Actual extends Expected ? This : never /** * Represents only the string keys of type `T`. */ -type PropertyKey = keyof T&string; +type PropertyKey = keyof T & string; /** * This helper class wraps an object expression along with an `AstHost` object, exposing helper @@ -57,15 +57,19 @@ export class AstObject { /** * Create a new `AstObject` from the given `expression` and `host`. */ - static parse(expression: TExpression, host: AstHost): - AstObject { + static parse( + expression: TExpression, + host: AstHost, + ): AstObject { const obj = host.parseObjectLiteral(expression); return new AstObject(expression, obj, host); } private constructor( - readonly expression: TExpression, private obj: Map, - private host: AstHost) {} + readonly expression: TExpression, + private obj: Map, + private host: AstHost, + ) {} /** * Returns true if the object has a property called `propertyName`. @@ -79,8 +83,10 @@ export class AstObject { * * Throws an error if there is no such property or the property is not a number. */ - getNumber>(this: ConformsTo, propertyName: K): - number { + getNumber>( + this: ConformsTo, + propertyName: K, + ): number { return this.host.parseNumericLiteral(this.getRequiredProperty(propertyName)); } @@ -89,8 +95,10 @@ export class AstObject { * * Throws an error if there is no such property or the property is not a string. */ - getString>(this: ConformsTo, propertyName: K): - string { + getString>( + this: ConformsTo, + propertyName: K, + ): string { return this.host.parseStringLiteral(this.getRequiredProperty(propertyName)); } @@ -99,8 +107,10 @@ export class AstObject { * * Throws an error if there is no such property or the property is not a boolean. */ - getBoolean>(this: ConformsTo, propertyName: K): - boolean { + getBoolean>( + this: ConformsTo, + propertyName: K, + ): boolean { return this.host.parseBooleanLiteral(this.getRequiredProperty(propertyName)) as any; } @@ -109,8 +119,10 @@ export class AstObject { * * Throws an error if there is no such property or the property is not an object. */ - getObject>(this: ConformsTo, propertyName: K): - AstObject, TExpression> { + getObject>( + this: ConformsTo, + propertyName: K, + ): AstObject, TExpression> { const expr = this.getRequiredProperty(propertyName); const obj = this.host.parseObjectLiteral(expr); return new AstObject, TExpression>(expr, obj, this.host); @@ -121,10 +133,12 @@ export class AstObject { * * Throws an error if there is no such property or the property is not an array. */ - getArray>(this: ConformsTo, propertyName: K): - AstValue, TExpression>[] { + getArray>( + this: ConformsTo, + propertyName: K, + ): AstValue, TExpression>[] { const arr = this.host.parseArrayLiteral(this.getRequiredProperty(propertyName)); - return arr.map(entry => new AstValue, TExpression>(entry, this.host)); + return arr.map((entry) => new AstValue, TExpression>(entry, this.host)); } /** @@ -159,12 +173,15 @@ export class AstObject { * Converts the AstObject to a raw JavaScript object, mapping each property value (as an * `AstValue`) to the generic type (`T`) via the `mapper` function. */ - toLiteral(mapper: (value: AstValue, TExpression>, key: string) => V): - Record { + toLiteral( + mapper: (value: AstValue, TExpression>, key: string) => V, + ): Record { const result: Record = {}; for (const [key, expression] of this.obj) { - result[key] = - mapper(new AstValue, TExpression>(expression, this.host), key); + result[key] = mapper( + new AstValue, TExpression>(expression, this.host), + key, + ); } return result; } @@ -184,7 +201,9 @@ export class AstObject { private getRequiredProperty(propertyName: PropertyKey): TExpression { if (!this.obj.has(propertyName)) { throw new FatalLinkerError( - this.expression, `Expected property '${propertyName}' to be present.`); + this.expression, + `Expected property '${propertyName}' to be present.`, + ); } return this.obj.get(propertyName)!; } @@ -202,13 +221,16 @@ export class AstValue { /** Type brand that ensures that the `T` type is respected for assignability. */ ɵtypeBrand: T = null!; - constructor(readonly expression: TExpression, private host: AstHost) {} + constructor( + readonly expression: TExpression, + private host: AstHost, + ) {} /** * Get the name of the symbol represented by the given expression node, or `null` if it is not a * symbol. */ - getSymbolName(): string|null { + getSymbolName(): string | null { return this.host.getSymbolName(this.expression); } @@ -285,7 +307,7 @@ export class AstValue { */ getArray(this: ConformsTo): AstValue, TExpression>[] { const arr = this.host.parseArrayLiteral(this.expression); - return arr.map(entry => new AstValue, TExpression>(entry, this.host)); + return arr.map((entry) => new AstValue, TExpression>(entry, this.host)); } /** @@ -308,7 +330,9 @@ export class AstValue { * function expression. */ getFunctionParameters(this: ConformsTo): AstValue[] { - return this.host.parseParameters(this.expression).map(param => new AstValue(param, this.host)); + return this.host + .parseParameters(this.expression) + .map((param) => new AstValue(param, this.host)); } isCallExpression(): boolean { @@ -321,7 +345,7 @@ export class AstValue { getArguments(): AstValue[] { const args = this.host.parseArguments(this.expression); - return args.map(arg => new AstValue(arg, this.host)); + return args.map((arg) => new AstValue(arg, this.host)); } /** diff --git a/packages/compiler-cli/linker/src/ast/typescript/typescript_ast_host.ts b/packages/compiler-cli/linker/src/ast/typescript/typescript_ast_host.ts index f08fcb1549726..8a118a4238c5c 100644 --- a/packages/compiler-cli/linker/src/ast/typescript/typescript_ast_host.ts +++ b/packages/compiler-cli/linker/src/ast/typescript/typescript_ast_host.ts @@ -12,7 +12,6 @@ import {FatalLinkerError} from '../../fatal_linker_error'; import {AstHost, Range} from '../ast_host'; import {assert} from '../utils'; - /** * This implementation of `AstHost` is able to get information from TypeScript AST nodes. * @@ -23,7 +22,7 @@ import {assert} from '../utils'; * do linking in the future. */ export class TypeScriptAstHost implements AstHost { - getSymbolName(node: ts.Expression): string|null { + getSymbolName(node: ts.Expression): string | null { if (ts.isIdentifier(node)) { return node.text; } else if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name)) { @@ -59,7 +58,7 @@ export class TypeScriptAstHost implements AstHost { if (isBooleanLiteral(bool)) { return bool.kind === ts.SyntaxKind.TrueKeyword; } else if (isMinifiedBooleanLiteral(bool)) { - return !(+bool.operand.text); + return !+bool.operand.text; } else { throw new FatalLinkerError(bool, 'Unsupported syntax, expected a boolean literal.'); } @@ -69,7 +68,7 @@ export class TypeScriptAstHost implements AstHost { parseArrayLiteral(array: ts.Expression): ts.Expression[] { assert(array, this.isArrayLiteral, 'an array literal'); - return array.elements.map(element => { + return array.elements.map((element) => { assert(element, isNotEmptyElement, 'element in array not to be empty'); assert(element, isNotSpreadElement, 'element in array not to use spread syntax'); return element; @@ -90,7 +89,7 @@ export class TypeScriptAstHost implements AstHost { return result; } - isFunctionExpression(node: ts.Expression): node is ts.FunctionExpression|ts.ArrowFunction { + isFunctionExpression(node: ts.Expression): node is ts.FunctionExpression | ts.ArrowFunction { return ts.isFunctionExpression(node) || ts.isArrowFunction(node); } @@ -107,7 +106,9 @@ export class TypeScriptAstHost implements AstHost { if (fn.body.statements.length !== 1) { throw new FatalLinkerError( - fn.body, 'Unsupported syntax, expected a function body with a single return statement.'); + fn.body, + 'Unsupported syntax, expected a function body with a single return statement.', + ); } const stmt = fn.body.statements[0]; assert(stmt, ts.isReturnStatement, 'a function body with a single return statement'); @@ -120,7 +121,7 @@ export class TypeScriptAstHost implements AstHost { parseParameters(fn: ts.Expression): ts.Expression[] { assert(fn, this.isFunctionExpression, 'a function'); - return fn.parameters.map(param => { + return fn.parameters.map((param) => { assert(param.name, ts.isIdentifier, 'an identifier'); if (param.dotDotDotToken) { throw new FatalLinkerError(fn.body, 'Unsupported syntax, expected an identifier.'); @@ -138,7 +139,7 @@ export class TypeScriptAstHost implements AstHost { parseArguments(call: ts.Expression): ts.Expression[] { assert(call, ts.isCallExpression, 'a call expression'); - return call.arguments.map(arg => { + return call.arguments.map((arg) => { assert(arg, isNotSpreadElement, 'argument not to use spread syntax'); return arg; }); @@ -148,7 +149,9 @@ export class TypeScriptAstHost implements AstHost { const file = node.getSourceFile(); if (file === undefined) { throw new FatalLinkerError( - node, 'Unable to read range for node - it is missing parent information.'); + node, + 'Unable to read range for node - it is missing parent information.', + ); } const startPos = node.getStart(); const endPos = node.getEnd(); @@ -161,8 +164,9 @@ export class TypeScriptAstHost implements AstHost { * Return true if the expression does not represent an empty element in an array literal. * For example in `[,foo]` the first element is "empty". */ -function isNotEmptyElement(e: ts.Expression|ts.SpreadElement| - ts.OmittedExpression): e is ts.Expression|ts.SpreadElement { +function isNotEmptyElement( + e: ts.Expression | ts.SpreadElement | ts.OmittedExpression, +): e is ts.Expression | ts.SpreadElement { return !ts.isOmittedExpression(e); } @@ -170,30 +174,36 @@ function isNotEmptyElement(e: ts.Expression|ts.SpreadElement| * Return true if the expression is not a spread element of an array literal. * For example in `[x, ...rest]` the `...rest` expression is a spread element. */ -function isNotSpreadElement(e: ts.Expression|ts.SpreadElement): e is ts.Expression { +function isNotSpreadElement(e: ts.Expression | ts.SpreadElement): e is ts.Expression { return !ts.isSpreadElement(e); } /** * Return true if the expression can be considered a text based property name. */ -function isPropertyName(e: ts.PropertyName): e is ts.Identifier|ts.StringLiteral|ts.NumericLiteral { +function isPropertyName( + e: ts.PropertyName, +): e is ts.Identifier | ts.StringLiteral | ts.NumericLiteral { return ts.isIdentifier(e) || ts.isStringLiteral(e) || ts.isNumericLiteral(e); } /** * Return true if the node is either `true` or `false` literals. */ -function isBooleanLiteral(node: ts.Expression): node is ts.TrueLiteral|ts.FalseLiteral { +function isBooleanLiteral(node: ts.Expression): node is ts.TrueLiteral | ts.FalseLiteral { return node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword; } -type MinifiedBooleanLiteral = ts.PrefixUnaryExpression&{operand: ts.NumericLiteral}; +type MinifiedBooleanLiteral = ts.PrefixUnaryExpression & {operand: ts.NumericLiteral}; /** * Return true if the node is either `!0` or `!1`. */ function isMinifiedBooleanLiteral(node: ts.Expression): node is MinifiedBooleanLiteral { - return ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.ExclamationToken && - ts.isNumericLiteral(node.operand) && (node.operand.text === '0' || node.operand.text === '1'); + return ( + ts.isPrefixUnaryExpression(node) && + node.operator === ts.SyntaxKind.ExclamationToken && + ts.isNumericLiteral(node.operand) && + (node.operand.text === '0' || node.operand.text === '1') + ); } diff --git a/packages/compiler-cli/linker/src/ast/utils.ts b/packages/compiler-cli/linker/src/ast/utils.ts index 2aa9437361f5c..2803ebfd44bf0 100644 --- a/packages/compiler-cli/linker/src/ast/utils.ts +++ b/packages/compiler-cli/linker/src/ast/utils.ts @@ -11,7 +11,10 @@ import {FatalLinkerError} from '../fatal_linker_error'; * Assert that the given `node` is of the type guarded by the `predicate` function. */ export function assert( - node: T, predicate: (node: T) => node is K, expected: string): asserts node is K { + node: T, + predicate: (node: T) => node is K, + expected: string, +): asserts node is K { if (!predicate(node)) { throw new FatalLinkerError(node, `Unsupported syntax, expected ${expected}.`); } diff --git a/packages/compiler-cli/linker/src/fatal_linker_error.ts b/packages/compiler-cli/linker/src/fatal_linker_error.ts index 49bf52f3f3576..aa23ead610a07 100644 --- a/packages/compiler-cli/linker/src/fatal_linker_error.ts +++ b/packages/compiler-cli/linker/src/fatal_linker_error.ts @@ -18,7 +18,10 @@ export class FatalLinkerError extends Error { * @param node The AST node where the error occurred. * @param message A description of the error. */ - constructor(public node: unknown, message: string) { + constructor( + public node: unknown, + message: string, + ) { super(message); } } diff --git a/packages/compiler-cli/linker/src/file_linker/declaration_scope.ts b/packages/compiler-cli/linker/src/file_linker/declaration_scope.ts index 320a269a26c23..269f44df33218 100644 --- a/packages/compiler-cli/linker/src/file_linker/declaration_scope.ts +++ b/packages/compiler-cli/linker/src/file_linker/declaration_scope.ts @@ -41,5 +41,5 @@ export interface DeclarationScope { * @returns a reference to a reference object for where the shared constant statements will be * inserted, or `null` if it is not possible to have a shared scope. */ - getConstantScopeRef(expression: TExpression): TSharedConstantScope|null; + getConstantScopeRef(expression: TExpression): TSharedConstantScope | null; } 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 dd22af73defed..073e4b308ac6c 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 @@ -25,9 +25,10 @@ export class EmitScope { readonly constantPool = new ConstantPool(); constructor( - protected readonly ngImport: TExpression, - protected readonly translator: Translator, - private readonly factory: AstFactory) {} + protected readonly ngImport: TExpression, + protected readonly translator: Translator, + private readonly factory: AstFactory, + ) {} /** * Translate the given Output AST definition expression into a generic `TExpression`. @@ -36,7 +37,9 @@ export class EmitScope { */ translateDefinition(definition: LinkedDefinition): TExpression { const expression = this.translator.translateExpression( - definition.expression, new LinkerImportGenerator(this.factory, this.ngImport)); + definition.expression, + new LinkerImportGenerator(this.factory, this.ngImport), + ); if (definition.statements.length > 0) { // Definition statements must be emitted "after" the declaration for which the definition is @@ -46,9 +49,11 @@ export class EmitScope { // definition expression. const importGenerator = new LinkerImportGenerator(this.factory, this.ngImport); return this.wrapInIifeWithStatements( - expression, - definition.statements.map( - statement => this.translator.translateStatement(statement, importGenerator))); + expression, + definition.statements.map((statement) => + this.translator.translateStatement(statement, importGenerator), + ), + ); } else { // Since there are no definition statements, just return the definition expression directly. return expression; @@ -60,14 +65,15 @@ export class EmitScope { */ getConstantStatements(): TStatement[] { const importGenerator = new LinkerImportGenerator(this.factory, this.ngImport); - return this.constantPool.statements.map( - statement => this.translator.translateStatement(statement, importGenerator)); + return this.constantPool.statements.map((statement) => + this.translator.translateStatement(statement, importGenerator), + ); } private wrapInIifeWithStatements(expression: TExpression, statements: TStatement[]): TExpression { const returnStatement = this.factory.createReturnStatement(expression); const body = this.factory.createBlock([...statements, returnStatement]); - const fn = this.factory.createFunctionExpression(/* name */ null, /* args */[], body); - return this.factory.createCallExpression(fn, /* args */[], /* pure */ false); + const fn = this.factory.createFunctionExpression(/* name */ null, /* args */ [], body); + return this.factory.createCallExpression(fn, /* args */ [], /* pure */ false); } } 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 14c55d41e9371..0240c16bd7bc2 100644 --- a/packages/compiler-cli/linker/src/file_linker/file_linker.ts +++ b/packages/compiler-cli/linker/src/file_linker/file_linker.ts @@ -26,11 +26,15 @@ export class FileLinker { private emitScopes = new Map>(); constructor( - private linkerEnvironment: LinkerEnvironment, - sourceUrl: AbsoluteFsPath, code: string) { + private linkerEnvironment: LinkerEnvironment, + sourceUrl: AbsoluteFsPath, + code: string, + ) { this.linkerSelector = new PartialLinkerSelector( - createLinkerMap(this.linkerEnvironment, sourceUrl, code), this.linkerEnvironment.logger, - this.linkerEnvironment.options.unknownDeclarationVersionHandling); + createLinkerMap(this.linkerEnvironment, sourceUrl, code), + this.linkerEnvironment.logger, + this.linkerEnvironment.options.unknownDeclarationVersionHandling, + ); } /** @@ -53,16 +57,20 @@ export class FileLinker { * @param declarationScope the scope that contains this call to the declaration function. */ linkPartialDeclaration( - declarationFn: string, args: TExpression[], - declarationScope: DeclarationScope): TExpression { + declarationFn: string, + args: TExpression[], + declarationScope: DeclarationScope, + ): TExpression { if (args.length !== 1) { throw new Error( - `Invalid function call: It should have only a single object literal argument, but contained ${ - args.length}.`); + `Invalid function call: It should have only a single object literal argument, but contained ${args.length}.`, + ); } - const metaObj = - AstObject.parse(args[0], this.linkerEnvironment.host); + const metaObj = AstObject.parse( + args[0], + this.linkerEnvironment.host, + ); const ngImport = metaObj.getNode('ngImport'); const emitScope = this.getEmitScope(ngImport, declarationScope); @@ -78,8 +86,8 @@ export class FileLinker { * Return all the shared constant statements and their associated constant scope references, so * that they can be inserted into the source code. */ - getConstantStatements(): {constantScope: TConstantScope, statements: TStatement[]}[] { - const results: {constantScope: TConstantScope, statements: TStatement[]}[] = []; + getConstantStatements(): {constantScope: TConstantScope; statements: TStatement[]}[] { + const results: {constantScope: TConstantScope; statements: TStatement[]}[] = []; for (const [constantScope, emitScope] of this.emitScopes.entries()) { const statements = emitScope.getConstantStatements(); results.push({constantScope, statements}); @@ -88,20 +96,24 @@ export class FileLinker { } private getEmitScope( - ngImport: TExpression, declarationScope: DeclarationScope): - EmitScope { + ngImport: TExpression, + declarationScope: DeclarationScope, + ): 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( - ngImport, this.linkerEnvironment.translator, this.linkerEnvironment.factory); + ngImport, + this.linkerEnvironment.translator, + this.linkerEnvironment.factory, + ); } if (!this.emitScopes.has(constantScope)) { this.emitScopes.set( - constantScope, - new EmitScope( - ngImport, this.linkerEnvironment.translator, this.linkerEnvironment.factory)); + constantScope, + new EmitScope(ngImport, this.linkerEnvironment.translator, this.linkerEnvironment.factory), + ); } return this.emitScopes.get(constantScope)!; } diff --git a/packages/compiler-cli/linker/src/file_linker/get_source_file.ts b/packages/compiler-cli/linker/src/file_linker/get_source_file.ts index f7764e2c86e40..928f99d477c86 100644 --- a/packages/compiler-cli/linker/src/file_linker/get_source_file.ts +++ b/packages/compiler-cli/linker/src/file_linker/get_source_file.ts @@ -12,20 +12,23 @@ import {SourceFile, SourceFileLoader} from '../../../src/ngtsc/sourcemaps'; /** * A function that will return a `SourceFile` object (or null) for the current file being linked. */ -export type GetSourceFileFn = () => SourceFile|null; +export type GetSourceFileFn = () => SourceFile | null; /** * Create a `GetSourceFileFn` that will return the `SourceFile` being linked or `null`, if not * available. */ export function createGetSourceFile( - sourceUrl: AbsoluteFsPath, code: string, loader: SourceFileLoader|null): GetSourceFileFn { + sourceUrl: AbsoluteFsPath, + code: string, + loader: SourceFileLoader | null, +): GetSourceFileFn { if (loader === null) { // No source-mapping so just return a function that always returns `null`. return () => null; } else { // Source-mapping is available so return a function that will load (and cache) the `SourceFile`. - let sourceFile: SourceFile|null|undefined = undefined; + let sourceFile: SourceFile | null | undefined = undefined; return () => { if (sourceFile === undefined) { sourceFile = loader.loadSourceFile(sourceUrl, code); 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 c3477f3b7a8a9..f0c6e8f83d52f 100644 --- a/packages/compiler-cli/linker/src/file_linker/linker_environment.ts +++ b/packages/compiler-cli/linker/src/file_linker/linker_environment.ts @@ -16,23 +16,31 @@ import {Translator} from './translator'; export class LinkerEnvironment { readonly translator = new Translator(this.factory); - readonly sourceFileLoader = - this.options.sourceMapping ? new SourceFileLoader(this.fileSystem, this.logger, {}) : null; + readonly sourceFileLoader = this.options.sourceMapping + ? new SourceFileLoader(this.fileSystem, this.logger, {}) + : null; private constructor( - readonly fileSystem: ReadonlyFileSystem, readonly logger: Logger, - readonly host: AstHost, readonly factory: AstFactory, - readonly options: LinkerOptions) {} + readonly fileSystem: ReadonlyFileSystem, + readonly logger: Logger, + readonly host: AstHost, + readonly factory: AstFactory, + readonly options: LinkerOptions, + ) {} static create( - fileSystem: ReadonlyFileSystem, logger: Logger, host: AstHost, - factory: AstFactory, - options: Partial): LinkerEnvironment { + fileSystem: ReadonlyFileSystem, + logger: Logger, + host: AstHost, + factory: AstFactory, + options: Partial, + ): LinkerEnvironment { return new LinkerEnvironment(fileSystem, logger, host, factory, { sourceMapping: options.sourceMapping ?? DEFAULT_LINKER_OPTIONS.sourceMapping, linkerJitMode: options.linkerJitMode ?? DEFAULT_LINKER_OPTIONS.linkerJitMode, - unknownDeclarationVersionHandling: options.unknownDeclarationVersionHandling ?? - DEFAULT_LINKER_OPTIONS.unknownDeclarationVersionHandling, + unknownDeclarationVersionHandling: + options.unknownDeclarationVersionHandling ?? + DEFAULT_LINKER_OPTIONS.unknownDeclarationVersionHandling, }); } } diff --git a/packages/compiler-cli/linker/src/file_linker/linker_options.ts b/packages/compiler-cli/linker/src/file_linker/linker_options.ts index 85945e00e5145..3442d298d3fae 100644 --- a/packages/compiler-cli/linker/src/file_linker/linker_options.ts +++ b/packages/compiler-cli/linker/src/file_linker/linker_options.ts @@ -36,7 +36,7 @@ export interface LinkerOptions { * * The default is `error`. */ - unknownDeclarationVersionHandling: 'ignore'|'warn'|'error'; + unknownDeclarationVersionHandling: 'ignore' | 'warn' | 'error'; } /** diff --git a/packages/compiler-cli/linker/src/file_linker/needs_linking.ts b/packages/compiler-cli/linker/src/file_linker/needs_linking.ts index 1a0901e519461..3f0972cbea2bc 100644 --- a/packages/compiler-cli/linker/src/file_linker/needs_linking.ts +++ b/packages/compiler-cli/linker/src/file_linker/needs_linking.ts @@ -24,5 +24,5 @@ import {declarationFunctions} from './partial_linkers/partial_linker_selector'; * @returns whether the source file may contain declarations that need to be linked. */ export function needsLinking(path: string, source: string): boolean { - return declarationFunctions.some(fn => source.includes(fn)); + return declarationFunctions.some((fn) => source.includes(fn)); } diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_class_metadata_async_linker_1.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_class_metadata_async_linker_1.ts index 0effd02c9ba20..442bba029f84a 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_class_metadata_async_linker_1.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_class_metadata_async_linker_1.ts @@ -5,7 +5,12 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {compileOpaqueAsyncClassMetadata, ConstantPool, R3ClassMetadata, R3DeclareClassMetadataAsync} from '@angular/compiler'; +import { + compileOpaqueAsyncClassMetadata, + ConstantPool, + R3ClassMetadata, + R3DeclareClassMetadataAsync, +} from '@angular/compiler'; import {AstObject, AstValue} from '../../ast/ast_value'; import {FatalLinkerError} from '../../fatal_linker_error'; @@ -15,23 +20,30 @@ import {LinkedDefinition, PartialLinker} from './partial_linker'; /** * A `PartialLinker` that is designed to process `ɵɵngDeclareClassMetadataAsync()` call expressions. */ -export class PartialClassMetadataAsyncLinkerVersion1 implements - PartialLinker { +export class PartialClassMetadataAsyncLinkerVersion1 + implements PartialLinker +{ linkPartialDeclaration( - constantPool: ConstantPool, - metaObj: AstObject): LinkedDefinition { + constantPool: ConstantPool, + metaObj: AstObject, + ): LinkedDefinition { const resolveMetadataKey = 'resolveMetadata'; - const resolveMetadata = - metaObj.getValue(resolveMetadataKey) as unknown as AstValue; + const resolveMetadata = metaObj.getValue(resolveMetadataKey) as unknown as AstValue< + Function, + TExpression + >; if (!resolveMetadata.isFunction()) { throw new FatalLinkerError( - resolveMetadata, `Unsupported \`${resolveMetadataKey}\` value. Expected a function.`); + resolveMetadata, + `Unsupported \`${resolveMetadataKey}\` value. Expected a function.`, + ); } const dependencyResolverFunction = metaObj.getOpaque('resolveDeferredDeps'); - const deferredSymbolNames = - resolveMetadata.getFunctionParameters().map(p => p.getSymbolName()!); + const deferredSymbolNames = resolveMetadata + .getFunctionParameters() + .map((p) => p.getSymbolName()!); const returnValue = resolveMetadata.getFunctionReturnValue().getObject(); const metadata: R3ClassMetadata = { type: metaObj.getOpaque('type'), @@ -42,7 +54,10 @@ export class PartialClassMetadataAsyncLinkerVersion1 implements return { expression: compileOpaqueAsyncClassMetadata( - metadata, dependencyResolverFunction, deferredSymbolNames), + metadata, + dependencyResolverFunction, + deferredSymbolNames, + ), statements: [], }; } diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_class_metadata_linker_1.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_class_metadata_linker_1.ts index 75d64eed72966..10c4adcf04781 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_class_metadata_linker_1.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_class_metadata_linker_1.ts @@ -5,7 +5,14 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {compileClassMetadata, ConstantPool, outputAst as o, R3ClassMetadata, R3DeclareClassMetadata, R3PartialDeclaration} from '@angular/compiler'; +import { + compileClassMetadata, + ConstantPool, + outputAst as o, + R3ClassMetadata, + R3DeclareClassMetadata, + R3PartialDeclaration, +} from '@angular/compiler'; import {AstObject} from '../../ast/ast_value'; @@ -16,8 +23,9 @@ import {LinkedDefinition, PartialLinker} from './partial_linker'; */ export class PartialClassMetadataLinkerVersion1 implements PartialLinker { linkPartialDeclaration( - constantPool: ConstantPool, - metaObj: AstObject): LinkedDefinition { + constantPool: ConstantPool, + metaObj: AstObject, + ): LinkedDefinition { const meta = toR3ClassMetadata(metaObj); return { expression: compileClassMetadata(meta), @@ -30,7 +38,8 @@ export class PartialClassMetadataLinkerVersion1 implements PartialL * Derives the `R3ClassMetadata` structure from the AST object. */ export function toR3ClassMetadata( - metaObj: AstObject): R3ClassMetadata { + metaObj: AstObject, +): R3ClassMetadata { return { type: metaObj.getOpaque('type'), decorators: metaObj.getOpaque('decorators'), diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts index 85e154be47bac..0e50779a3b81d 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts @@ -5,7 +5,33 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {BoundTarget, ChangeDetectionStrategy, compileComponentFromMetadata, ConstantPool, DeclarationListEmitMode, DEFAULT_INTERPOLATION_CONFIG, DeferBlockDepsEmitMode, ForwardRefHandling, InterpolationConfig, makeBindingParser, outputAst as o, parseTemplate, R3ComponentDeferMetadata, R3ComponentMetadata, R3DeclareComponentMetadata, R3DeclareDirectiveDependencyMetadata, R3DeclarePipeDependencyMetadata, R3DirectiveDependencyMetadata, R3PartialDeclaration, R3TargetBinder, R3TemplateDependencyKind, R3TemplateDependencyMetadata, SelectorMatcher, TmplAstDeferredBlock, ViewEncapsulation} from '@angular/compiler'; +import { + BoundTarget, + ChangeDetectionStrategy, + compileComponentFromMetadata, + ConstantPool, + DeclarationListEmitMode, + DEFAULT_INTERPOLATION_CONFIG, + DeferBlockDepsEmitMode, + ForwardRefHandling, + InterpolationConfig, + makeBindingParser, + outputAst as o, + parseTemplate, + R3ComponentDeferMetadata, + R3ComponentMetadata, + R3DeclareComponentMetadata, + R3DeclareDirectiveDependencyMetadata, + R3DeclarePipeDependencyMetadata, + R3DirectiveDependencyMetadata, + R3PartialDeclaration, + R3TargetBinder, + R3TemplateDependencyKind, + R3TemplateDependencyMetadata, + SelectorMatcher, + TmplAstDeferredBlock, + ViewEncapsulation, +} from '@angular/compiler'; import semver from 'semver'; import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system'; @@ -19,39 +45,46 @@ import {LinkedDefinition, PartialLinker} from './partial_linker'; import {extractForwardRef, PLACEHOLDER_VERSION} from './util'; function makeDirectiveMetadata( - directiveExpr: AstObject, - typeExpr: o.WrappedNodeExpr, - isComponentByDefault: true|null = null): R3DirectiveDependencyMetadata { + directiveExpr: AstObject, + typeExpr: o.WrappedNodeExpr, + isComponentByDefault: true | null = null, +): R3DirectiveDependencyMetadata { return { kind: R3TemplateDependencyKind.Directive, - isComponent: isComponentByDefault || - (directiveExpr.has('kind') && directiveExpr.getString('kind') === 'component'), + isComponent: + isComponentByDefault || + (directiveExpr.has('kind') && directiveExpr.getString('kind') === 'component'), type: typeExpr, selector: directiveExpr.getString('selector'), - inputs: directiveExpr.has('inputs') ? - directiveExpr.getArray('inputs').map(input => input.getString()) : - [], - outputs: directiveExpr.has('outputs') ? - directiveExpr.getArray('outputs').map(input => input.getString()) : - [], - exportAs: directiveExpr.has('exportAs') ? - directiveExpr.getArray('exportAs').map(exportAs => exportAs.getString()) : - null, + inputs: directiveExpr.has('inputs') + ? directiveExpr.getArray('inputs').map((input) => input.getString()) + : [], + outputs: directiveExpr.has('outputs') + ? directiveExpr.getArray('outputs').map((input) => input.getString()) + : [], + exportAs: directiveExpr.has('exportAs') + ? directiveExpr.getArray('exportAs').map((exportAs) => exportAs.getString()) + : null, }; } /** * A `PartialLinker` that is designed to process `ɵɵngDeclareComponent()` call expressions. */ -export class PartialComponentLinkerVersion1 implements - PartialLinker { +export class PartialComponentLinkerVersion1 + implements PartialLinker +{ constructor( - private readonly getSourceFile: GetSourceFileFn, private sourceUrl: AbsoluteFsPath, - private code: string) {} + private readonly getSourceFile: GetSourceFileFn, + private sourceUrl: AbsoluteFsPath, + private code: string, + ) {} linkPartialDeclaration( - constantPool: ConstantPool, metaObj: AstObject, - version: string): LinkedDefinition { + constantPool: ConstantPool, + metaObj: AstObject, + version: string, + ): LinkedDefinition { const meta = this.toR3ComponentMeta(metaObj, version); return compileComponentFromMetadata(meta, constantPool, makeBindingParser()); } @@ -60,8 +93,9 @@ export class PartialComponentLinkerVersion1 implements * This function derives the `R3ComponentMetadata` from the provided AST object. */ private toR3ComponentMeta( - metaObj: AstObject, - version: string): R3ComponentMetadata { + metaObj: AstObject, + version: string, + ): R3ComponentMetadata { const interpolation = parseInterpolationConfig(metaObj); const templateSource = metaObj.getValue('template'); const isInline = metaObj.has('isInline') ? metaObj.getBoolean('isInline') : false; @@ -76,30 +110,34 @@ export class PartialComponentLinkerVersion1 implements interpolationConfig: interpolation, range: templateInfo.range, enableI18nLegacyMessageIdFormat: false, - preserveWhitespaces: - metaObj.has('preserveWhitespaces') ? metaObj.getBoolean('preserveWhitespaces') : false, + preserveWhitespaces: metaObj.has('preserveWhitespaces') + ? metaObj.getBoolean('preserveWhitespaces') + : false, // We normalize line endings if the template is was inline. i18nNormalizeLineEndingsInICUs: isInline, enableBlockSyntax, }); if (template.errors !== null) { - const errors = template.errors.map(err => err.toString()).join('\n'); + const errors = template.errors.map((err) => err.toString()).join('\n'); throw new FatalLinkerError( - templateSource.expression, `Errors found in the template:\n${errors}`); + templateSource.expression, + `Errors found in the template:\n${errors}`, + ); } const binder = new R3TargetBinder(new SelectorMatcher()); const boundTarget = binder.bind({template: template.nodes}); let declarationListEmitMode = DeclarationListEmitMode.Direct; - const extractDeclarationTypeExpr = - (type: AstValue o.Expression), TExpression>) => { - const {expression, forwardRef} = extractForwardRef(type); - if (forwardRef === ForwardRefHandling.Unwrapped) { - declarationListEmitMode = DeclarationListEmitMode.Closure; - } - return expression; - }; + const extractDeclarationTypeExpr = ( + type: AstValue o.Expression), TExpression>, + ) => { + const {expression, forwardRef} = extractForwardRef(type); + if (forwardRef === ForwardRefHandling.Unwrapped) { + declarationListEmitMode = DeclarationListEmitMode.Closure; + } + return expression; + }; let declarations: R3TemplateDependencyMetadata[] = []; @@ -110,21 +148,25 @@ export class PartialComponentLinkerVersion1 implements // Process the old style fields: if (metaObj.has('components')) { - declarations.push(...metaObj.getArray('components').map(dir => { - const dirExpr = dir.getObject(); - const typeExpr = extractDeclarationTypeExpr(dirExpr.getValue('type')); - return makeDirectiveMetadata(dirExpr, typeExpr, /* isComponentByDefault */ true); - })); + declarations.push( + ...metaObj.getArray('components').map((dir) => { + const dirExpr = dir.getObject(); + const typeExpr = extractDeclarationTypeExpr(dirExpr.getValue('type')); + return makeDirectiveMetadata(dirExpr, typeExpr, /* isComponentByDefault */ true); + }), + ); } if (metaObj.has('directives')) { - declarations.push(...metaObj.getArray('directives').map(dir => { - const dirExpr = dir.getObject(); - const typeExpr = extractDeclarationTypeExpr(dirExpr.getValue('type')); - return makeDirectiveMetadata(dirExpr, typeExpr); - })); + declarations.push( + ...metaObj.getArray('directives').map((dir) => { + const dirExpr = dir.getObject(); + const typeExpr = extractDeclarationTypeExpr(dirExpr.getValue('type')); + return makeDirectiveMetadata(dirExpr, typeExpr); + }), + ); } if (metaObj.has('pipes')) { - const pipes = metaObj.getObject('pipes').toMap(pipe => pipe); + const pipes = metaObj.getObject('pipes').toMap((pipe) => pipe); for (const [name, type] of pipes) { const typeExpr = extractDeclarationTypeExpr(type); declarations.push({ @@ -147,8 +189,10 @@ export class PartialComponentLinkerVersion1 implements declarations.push(makeDirectiveMetadata(depObj, typeExpr)); break; case 'pipe': - const pipeObj = - depObj as AstObject; + const pipeObj = depObj as AstObject< + R3DeclarePipeDependencyMetadata & {kind: 'pipe'}, + TExpression + >; declarations.push({ kind: R3TemplateDependencyKind.Pipe, name: pipeObj.getString('name'), @@ -176,16 +220,17 @@ export class PartialComponentLinkerVersion1 implements ngContentSelectors: template.ngContentSelectors, }, declarationListEmitMode, - styles: metaObj.has('styles') ? metaObj.getArray('styles').map(entry => entry.getString()) : - [], + styles: metaObj.has('styles') + ? metaObj.getArray('styles').map((entry) => entry.getString()) + : [], defer: this.createR3ComponentDeferMetadata(metaObj, boundTarget), - encapsulation: metaObj.has('encapsulation') ? - parseEncapsulation(metaObj.getValue('encapsulation')) : - ViewEncapsulation.Emulated, + encapsulation: metaObj.has('encapsulation') + ? parseEncapsulation(metaObj.getValue('encapsulation')) + : ViewEncapsulation.Emulated, interpolation, - changeDetection: metaObj.has('changeDetection') ? - parseChangeDetectionStrategy(metaObj.getValue('changeDetection')) : - ChangeDetectionStrategy.Default, + changeDetection: metaObj.has('changeDetection') + ? parseChangeDetectionStrategy(metaObj.getValue('changeDetection')) + : ChangeDetectionStrategy.Default, animations: metaObj.has('animations') ? metaObj.getOpaque('animations') : null, relativeContextFilePath: this.sourceUrl, i18nUseExternalIds: false, @@ -196,8 +241,10 @@ export class PartialComponentLinkerVersion1 implements /** * Update the range to remove the start and end chars, which should be quotes around the template. */ - private getTemplateInfo(templateNode: AstValue, isInline: boolean): - TemplateInfo { + private getTemplateInfo( + templateNode: AstValue, + isInline: boolean, + ): TemplateInfo { const range = templateNode.getRange(); if (!isInline) { @@ -214,7 +261,7 @@ export class PartialComponentLinkerVersion1 implements return this.templateFromPartialCode(templateNode, range); } - private tryExternalTemplate(range: Range): TemplateInfo|null { + private tryExternalTemplate(range: Range): TemplateInfo | null { const sourceFile = this.getSourceFile(); if (sourceFile === null) { return null; @@ -225,12 +272,19 @@ export class PartialComponentLinkerVersion1 implements // * the file is different to the current file // * the file does not end in `.js` or `.ts` (we expect it to be something like `.html`). // * the range starts at the beginning of the file - if (pos === null || pos.file === this.sourceUrl || /\.[jt]s$/.test(pos.file) || - pos.line !== 0 || pos.column !== 0) { + if ( + pos === null || + pos.file === this.sourceUrl || + /\.[jt]s$/.test(pos.file) || + pos.line !== 0 || + pos.column !== 0 + ) { return null; } - const templateContents = sourceFile.sources.find(src => src?.sourcePath === pos.file)!.contents; + const templateContents = sourceFile.sources.find( + (src) => src?.sourcePath === pos.file, + )!.contents; return { code: templateContents, @@ -241,13 +295,17 @@ export class PartialComponentLinkerVersion1 implements } private templateFromPartialCode( - templateNode: AstValue, - {startPos, endPos, startLine, startCol}: Range): TemplateInfo { + templateNode: AstValue, + {startPos, endPos, startLine, startCol}: Range, + ): TemplateInfo { if (!/["'`]/.test(this.code[startPos]) || this.code[startPos] !== this.code[endPos - 1]) { throw new FatalLinkerError( - templateNode.expression, - `Expected the template string to be wrapped in quotes but got: ${ - this.code.substring(startPos, endPos)}`); + templateNode.expression, + `Expected the template string to be wrapped in quotes but got: ${this.code.substring( + startPos, + endPos, + )}`, + ); } return { code: this.code, @@ -258,12 +316,14 @@ export class PartialComponentLinkerVersion1 implements } private createR3ComponentDeferMetadata( - metaObj: AstObject, - boundTarget: BoundTarget): R3ComponentDeferMetadata { + metaObj: AstObject, + boundTarget: BoundTarget, + ): R3ComponentDeferMetadata { const deferredBlocks = boundTarget.getDeferBlocks(); - const blocks = new Map(); - const dependencies = - metaObj.has('deferBlockDependencies') ? metaObj.getArray('deferBlockDependencies') : null; + const blocks = new Map(); + const dependencies = metaObj.has('deferBlockDependencies') + ? metaObj.getArray('deferBlockDependencies') + : null; for (let i = 0; i < deferredBlocks.length; i++) { const matchingDependencyFn = dependencies?.[i]; @@ -272,8 +332,9 @@ export class PartialComponentLinkerVersion1 implements blocks.set(deferredBlocks[i], null); } else { blocks.set( - deferredBlocks[i], - matchingDependencyFn.isNull() ? null : matchingDependencyFn.getOpaque()); + deferredBlocks[i], + matchingDependencyFn.isNull() ? null : matchingDependencyFn.getOpaque(), + ); } } @@ -292,17 +353,19 @@ interface TemplateInfo { * Extract an `InterpolationConfig` from the component declaration. */ function parseInterpolationConfig( - metaObj: AstObject): InterpolationConfig { + metaObj: AstObject, +): InterpolationConfig { if (!metaObj.has('interpolation')) { return DEFAULT_INTERPOLATION_CONFIG; } const interpolationExpr = metaObj.getValue('interpolation'); - const values = interpolationExpr.getArray().map(entry => entry.getString()); + const values = interpolationExpr.getArray().map((entry) => entry.getString()); if (values.length !== 2) { throw new FatalLinkerError( - interpolationExpr.expression, - 'Unsupported interpolation config, expected an array containing exactly two strings'); + interpolationExpr.expression, + 'Unsupported interpolation config, expected an array containing exactly two strings', + ); } return InterpolationConfig.fromArray(values as [string, string]); } @@ -311,11 +374,14 @@ function parseInterpolationConfig( * Determines the `ViewEncapsulation` mode from the AST value's symbol name. */ function parseEncapsulation( - encapsulation: AstValue): ViewEncapsulation { + encapsulation: AstValue, +): ViewEncapsulation { const symbolName = encapsulation.getSymbolName(); if (symbolName === null) { throw new FatalLinkerError( - encapsulation.expression, 'Expected encapsulation to have a symbol name'); + encapsulation.expression, + 'Expected encapsulation to have a symbol name', + ); } const enumValue = ViewEncapsulation[symbolName as keyof typeof ViewEncapsulation]; if (enumValue === undefined) { @@ -328,18 +394,21 @@ function parseEncapsulation( * Determines the `ChangeDetectionStrategy` from the AST value's symbol name. */ function parseChangeDetectionStrategy( - changeDetectionStrategy: AstValue): - ChangeDetectionStrategy { + changeDetectionStrategy: AstValue, +): ChangeDetectionStrategy { const symbolName = changeDetectionStrategy.getSymbolName(); if (symbolName === null) { throw new FatalLinkerError( - changeDetectionStrategy.expression, - 'Expected change detection strategy to have a symbol name'); + changeDetectionStrategy.expression, + 'Expected change detection strategy to have a symbol name', + ); } const enumValue = ChangeDetectionStrategy[symbolName as keyof typeof ChangeDetectionStrategy]; if (enumValue === undefined) { throw new FatalLinkerError( - changeDetectionStrategy.expression, 'Unsupported change detection strategy'); + changeDetectionStrategy.expression, + 'Unsupported change detection strategy', + ); } return enumValue; } diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_directive_linker_1.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_directive_linker_1.ts index 334ec65c527bf..69fcdc11d8026 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_directive_linker_1.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_directive_linker_1.ts @@ -5,7 +5,26 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {compileDirectiveFromMetadata, ConstantPool, ForwardRefHandling, LegacyInputPartialMapping, makeBindingParser, outputAst as o, ParseLocation, ParseSourceFile, ParseSourceSpan, R3DeclareDirectiveMetadata, R3DeclareHostDirectiveMetadata, R3DeclareQueryMetadata, R3DirectiveMetadata, R3HostDirectiveMetadata, R3HostMetadata, R3InputMetadata, R3PartialDeclaration, R3QueryMetadata} from '@angular/compiler'; +import { + compileDirectiveFromMetadata, + ConstantPool, + ForwardRefHandling, + LegacyInputPartialMapping, + makeBindingParser, + outputAst as o, + ParseLocation, + ParseSourceFile, + ParseSourceSpan, + R3DeclareDirectiveMetadata, + R3DeclareHostDirectiveMetadata, + R3DeclareQueryMetadata, + R3DirectiveMetadata, + R3HostDirectiveMetadata, + R3HostMetadata, + R3InputMetadata, + R3PartialDeclaration, + R3QueryMetadata, +} from '@angular/compiler'; import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system'; import {Range} from '../../ast/ast_host'; @@ -19,11 +38,15 @@ import {extractForwardRef, wrapReference} from './util'; * A `PartialLinker` that is designed to process `ɵɵngDeclareDirective()` call expressions. */ export class PartialDirectiveLinkerVersion1 implements PartialLinker { - constructor(private sourceUrl: AbsoluteFsPath, private code: string) {} + constructor( + private sourceUrl: AbsoluteFsPath, + private code: string, + ) {} linkPartialDeclaration( - constantPool: ConstantPool, - metaObj: AstObject): LinkedDefinition { + constantPool: ConstantPool, + metaObj: AstObject, + ): LinkedDefinition { const meta = toR3DirectiveMeta(metaObj, this.code, this.sourceUrl); return compileDirectiveFromMetadata(meta, constantPool, makeBindingParser()); } @@ -33,13 +56,17 @@ export class PartialDirectiveLinkerVersion1 implements PartialLinke * Derives the `R3DirectiveMetadata` structure from the AST object. */ export function toR3DirectiveMeta( - metaObj: AstObject, code: string, - sourceUrl: AbsoluteFsPath): R3DirectiveMetadata { + metaObj: AstObject, + code: string, + sourceUrl: AbsoluteFsPath, +): R3DirectiveMetadata { const typeExpr = metaObj.getValue('type'); const typeName = typeExpr.getSymbolName(); if (typeName === null) { throw new FatalLinkerError( - typeExpr.expression, 'Unsupported type, its name could not be determined'); + typeExpr.expression, + 'Unsupported type, its name could not be determined', + ); } return { @@ -49,21 +76,21 @@ export function toR3DirectiveMeta( deps: null, host: toHostMetadata(metaObj), inputs: metaObj.has('inputs') ? metaObj.getObject('inputs').toLiteral(toInputMapping) : {}, - outputs: metaObj.has('outputs') ? - metaObj.getObject('outputs').toLiteral(value => value.getString()) : - {}, - queries: metaObj.has('queries') ? - metaObj.getArray('queries').map(entry => toQueryMetadata(entry.getObject())) : - [], - viewQueries: metaObj.has('viewQueries') ? - metaObj.getArray('viewQueries').map(entry => toQueryMetadata(entry.getObject())) : - [], + outputs: metaObj.has('outputs') + ? metaObj.getObject('outputs').toLiteral((value) => value.getString()) + : {}, + queries: metaObj.has('queries') + ? metaObj.getArray('queries').map((entry) => toQueryMetadata(entry.getObject())) + : [], + viewQueries: metaObj.has('viewQueries') + ? metaObj.getArray('viewQueries').map((entry) => toQueryMetadata(entry.getObject())) + : [], providers: metaObj.has('providers') ? metaObj.getOpaque('providers') : null, fullInheritance: false, selector: metaObj.has('selector') ? metaObj.getString('selector') : null, - exportAs: metaObj.has('exportAs') ? - metaObj.getArray('exportAs').map(entry => entry.getString()) : - null, + exportAs: metaObj.has('exportAs') + ? metaObj.getArray('exportAs').map((entry) => entry.getString()) + : null, lifecycle: { usesOnChanges: metaObj.has('usesOnChanges') ? metaObj.getBoolean('usesOnChanges') : false, }, @@ -71,9 +98,9 @@ export function toR3DirectiveMeta( usesInheritance: metaObj.has('usesInheritance') ? metaObj.getBoolean('usesInheritance') : false, isStandalone: metaObj.has('isStandalone') ? metaObj.getBoolean('isStandalone') : false, isSignal: metaObj.has('isSignal') ? metaObj.getBoolean('isSignal') : false, - hostDirectives: metaObj.has('hostDirectives') ? - toHostDirectivesMetadata(metaObj.getValue('hostDirectives')) : - null, + hostDirectives: metaObj.has('hostDirectives') + ? toHostDirectivesMetadata(metaObj.getValue('hostDirectives')) + : null, }; } @@ -81,8 +108,9 @@ export function toR3DirectiveMeta( * Decodes the AST value for a single input to its representation as used in the metadata. */ function toInputMapping( - value: AstValue[string], TExpression>, - key: string): R3InputMetadata { + value: AstValue[string], TExpression>, + key: string, +): R3InputMetadata { if (value.isObject()) { const obj = value.getObject(); const transformValue = obj.getValue('transformFunction'); @@ -97,7 +125,9 @@ function toInputMapping( } return parseLegacyInputPartialOutput( - key, value as AstValue); + key, + value as AstValue, + ); } /** @@ -107,7 +137,9 @@ function toInputMapping( * TODO(legacy-partial-output-inputs): Remove function in v18. */ function parseLegacyInputPartialOutput( - key: string, value: AstValue): R3InputMetadata { + key: string, + value: AstValue, +): R3InputMetadata { if (value.isString()) { return { bindingPropertyName: value.getString(), @@ -121,8 +153,9 @@ function parseLegacyInputPartialOutput( const values = value.getArray(); if (values.length !== 2 && values.length !== 3) { throw new FatalLinkerError( - value.expression, - 'Unsupported input, expected a string or an array containing two strings and an optional function'); + value.expression, + 'Unsupported input, expected a string or an array containing two strings and an optional function', + ); } return { @@ -137,8 +170,9 @@ function parseLegacyInputPartialOutput( /** * Extracts the host metadata configuration from the AST metadata object. */ -function toHostMetadata(metaObj: AstObject): - R3HostMetadata { +function toHostMetadata( + metaObj: AstObject, +): R3HostMetadata { if (!metaObj.has('host')) { return { attributes: {}, @@ -159,15 +193,15 @@ function toHostMetadata(metaObj: AstObject value.getOpaque()) : - {}, - listeners: host.has('listeners') ? - host.getObject('listeners').toLiteral(value => value.getString()) : - {}, - properties: host.has('properties') ? - host.getObject('properties').toLiteral(value => value.getString()) : - {}, + attributes: host.has('attributes') + ? host.getObject('attributes').toLiteral((value) => value.getOpaque()) + : {}, + listeners: host.has('listeners') + ? host.getObject('listeners').toLiteral((value) => value.getString()) + : {}, + properties: host.has('properties') + ? host.getObject('properties').toLiteral((value) => value.getString()) + : {}, specialAttributes, }; } @@ -175,12 +209,13 @@ function toHostMetadata(metaObj: AstObject(obj: AstObject): - R3QueryMetadata { +function toQueryMetadata( + obj: AstObject, +): R3QueryMetadata { let predicate: R3QueryMetadata['predicate']; const predicateExpr = obj.getValue('predicate'); if (predicateExpr.isArray()) { - predicate = predicateExpr.getArray().map(entry => entry.getString()); + predicate = predicateExpr.getArray().map((entry) => entry.getString()); } else { predicate = extractForwardRef(predicateExpr); } @@ -189,8 +224,9 @@ function toQueryMetadata(obj: AstObject(obj: AstObject( - hostDirectives: AstValue): - R3HostDirectiveMetadata[] { - return hostDirectives.getArray().map(hostDirective => { + hostDirectives: AstValue, +): R3HostDirectiveMetadata[] { + return hostDirectives.getArray().map((hostDirective) => { const hostObject = hostDirective.getObject(); const type = extractForwardRef(hostObject.getValue('directive')); const meta: R3HostDirectiveMetadata = { directive: wrapReference(type.expression), isForwardReference: type.forwardRef !== ForwardRefHandling.None, - inputs: hostObject.has('inputs') ? - getHostDirectiveBindingMapping(hostObject.getArray('inputs')) : - null, - outputs: hostObject.has('outputs') ? - getHostDirectiveBindingMapping(hostObject.getArray('outputs')) : - null, + inputs: hostObject.has('inputs') + ? getHostDirectiveBindingMapping(hostObject.getArray('inputs')) + : null, + outputs: hostObject.has('outputs') + ? getHostDirectiveBindingMapping(hostObject.getArray('outputs')) + : null, }; return meta; @@ -222,7 +258,7 @@ function toHostDirectivesMetadata( } function getHostDirectiveBindingMapping(array: AstValue[]) { - let result: {[publicName: string]: string}|null = null; + let result: {[publicName: string]: string} | null = null; for (let i = 1; i < array.length; i += 2) { result = result || {}; @@ -234,7 +270,11 @@ function getHostDirectiveBindingMapping(array: AstValue implements PartialLinker { linkPartialDeclaration( - constantPool: ConstantPool, - metaObj: AstObject): LinkedDefinition { + constantPool: ConstantPool, + metaObj: AstObject, + ): LinkedDefinition { const meta = toR3FactoryMeta(metaObj); return compileFactoryFunction(meta); } @@ -29,12 +39,15 @@ export class PartialFactoryLinkerVersion1 implements PartialLinker< * Derives the `R3FactoryMetadata` structure from the AST object. */ export function toR3FactoryMeta( - metaObj: AstObject): R3FactoryMetadata { + metaObj: AstObject, +): R3FactoryMetadata { const typeExpr = metaObj.getValue('type'); const typeName = typeExpr.getSymbolName(); if (typeName === null) { throw new FatalLinkerError( - typeExpr.expression, 'Unsupported type, its name could not be determined'); + typeExpr.expression, + 'Unsupported type, its name could not be determined', + ); } return { @@ -47,14 +60,15 @@ export function toR3FactoryMeta( } function getDependencies( - metaObj: AstObject, - propName: keyof R3DeclareFactoryMetadata): R3DependencyMetadata[]|null|'invalid' { + metaObj: AstObject, + propName: keyof R3DeclareFactoryMetadata, +): R3DependencyMetadata[] | null | 'invalid' { if (!metaObj.has(propName)) { return null; } const deps = metaObj.getValue(propName); if (deps.isArray()) { - return deps.getArray().map(dep => getDependency(dep.getObject())); + return deps.getArray().map((dep) => getDependency(dep.getObject())); } if (deps.isString()) { return 'invalid'; diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_injectable_linker_1.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_injectable_linker_1.ts index 10def995b76fb..3ea057e6b5645 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_injectable_linker_1.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_injectable_linker_1.ts @@ -5,7 +5,16 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {compileInjectable, ConstantPool, createMayBeForwardRefExpression, ForwardRefHandling, outputAst as o, R3DeclareInjectableMetadata, R3InjectableMetadata, R3PartialDeclaration} from '@angular/compiler'; +import { + compileInjectable, + ConstantPool, + createMayBeForwardRefExpression, + ForwardRefHandling, + outputAst as o, + R3DeclareInjectableMetadata, + R3InjectableMetadata, + R3PartialDeclaration, +} from '@angular/compiler'; import {AstObject} from '../../ast/ast_value'; import {FatalLinkerError} from '../../fatal_linker_error'; @@ -18,8 +27,9 @@ import {extractForwardRef, getDependency, wrapReference} from './util'; */ export class PartialInjectableLinkerVersion1 implements PartialLinker { linkPartialDeclaration( - constantPool: ConstantPool, - metaObj: AstObject): LinkedDefinition { + constantPool: ConstantPool, + metaObj: AstObject, + ): LinkedDefinition { const meta = toR3InjectableMeta(metaObj); return compileInjectable(meta, /* resolveForwardRefs */ false); } @@ -29,21 +39,24 @@ export class PartialInjectableLinkerVersion1 implements PartialLink * Derives the `R3InjectableMetadata` structure from the AST object. */ export function toR3InjectableMeta( - metaObj: AstObject): R3InjectableMetadata { + metaObj: AstObject, +): R3InjectableMetadata { const typeExpr = metaObj.getValue('type'); const typeName = typeExpr.getSymbolName(); if (typeName === null) { throw new FatalLinkerError( - typeExpr.expression, 'Unsupported type, its name could not be determined'); + typeExpr.expression, + 'Unsupported type, its name could not be determined', + ); } const meta: R3InjectableMetadata = { name: typeName, type: wrapReference(typeExpr.getOpaque()), typeArgumentCount: 0, - providedIn: metaObj.has('providedIn') ? - extractForwardRef(metaObj.getValue('providedIn')) : - createMayBeForwardRefExpression(o.literal(null), ForwardRefHandling.None), + providedIn: metaObj.has('providedIn') + ? extractForwardRef(metaObj.getValue('providedIn')) + : createMayBeForwardRefExpression(o.literal(null), ForwardRefHandling.None), }; if (metaObj.has('useClass')) { @@ -60,7 +73,7 @@ export function toR3InjectableMeta( } if (metaObj.has('deps')) { - meta.deps = metaObj.getArray('deps').map(dep => getDependency(dep.getObject())); + meta.deps = metaObj.getArray('deps').map((dep) => getDependency(dep.getObject())); } return meta; diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_injector_linker_1.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_injector_linker_1.ts index 0575dcc52c475..92ebe0efc105a 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_injector_linker_1.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_injector_linker_1.ts @@ -5,7 +5,14 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {compileInjector, ConstantPool, outputAst as o, R3DeclareInjectorMetadata, R3InjectorMetadata, R3PartialDeclaration} from '@angular/compiler'; +import { + compileInjector, + ConstantPool, + outputAst as o, + R3DeclareInjectorMetadata, + R3InjectorMetadata, + R3PartialDeclaration, +} from '@angular/compiler'; import {AstObject} from '../../ast/ast_value'; import {FatalLinkerError} from '../../fatal_linker_error'; @@ -18,8 +25,9 @@ import {wrapReference} from './util'; */ export class PartialInjectorLinkerVersion1 implements PartialLinker { linkPartialDeclaration( - constantPool: ConstantPool, - metaObj: AstObject): LinkedDefinition { + constantPool: ConstantPool, + metaObj: AstObject, + ): LinkedDefinition { const meta = toR3InjectorMeta(metaObj); return compileInjector(meta); } @@ -29,18 +37,21 @@ export class PartialInjectorLinkerVersion1 implements PartialLinker * Derives the `R3InjectorMetadata` structure from the AST object. */ export function toR3InjectorMeta( - metaObj: AstObject): R3InjectorMetadata { + metaObj: AstObject, +): R3InjectorMetadata { const typeExpr = metaObj.getValue('type'); const typeName = typeExpr.getSymbolName(); if (typeName === null) { throw new FatalLinkerError( - typeExpr.expression, 'Unsupported type, its name could not be determined'); + typeExpr.expression, + 'Unsupported type, its name could not be determined', + ); } return { name: typeName, type: wrapReference(typeExpr.getOpaque()), providers: metaObj.has('providers') ? metaObj.getOpaque('providers') : null, - imports: metaObj.has('imports') ? metaObj.getArray('imports').map(i => i.getOpaque()) : [], + imports: metaObj.has('imports') ? metaObj.getArray('imports').map((i) => i.getOpaque()) : [], }; } diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker.ts index 680db8a33ecac..894320d06290e 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker.ts @@ -28,6 +28,8 @@ export interface PartialLinker { * `R3DeclareComponentMetadata` interfaces. */ linkPartialDeclaration( - constantPool: ConstantPool, metaObj: AstObject, - version: string): LinkedDefinition; + constantPool: ConstantPool, + metaObj: AstObject, + version: string, + ): LinkedDefinition; } 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 22f11f04da7da..58a1fb30cca01 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 @@ -34,9 +34,15 @@ export const ɵɵngDeclareNgModule = 'ɵɵngDeclareNgModule'; export const ɵɵngDeclarePipe = 'ɵɵngDeclarePipe'; export const ɵɵngDeclareClassMetadataAsync = 'ɵɵngDeclareClassMetadataAsync'; export const declarationFunctions = [ - ɵɵngDeclareDirective, ɵɵngDeclareClassMetadata, ɵɵngDeclareComponent, ɵɵngDeclareFactory, - ɵɵngDeclareInjectable, ɵɵngDeclareInjector, ɵɵngDeclareNgModule, ɵɵngDeclarePipe, - ɵɵngDeclareClassMetadataAsync + ɵɵngDeclareDirective, + ɵɵngDeclareClassMetadata, + ɵɵngDeclareComponent, + ɵɵngDeclareFactory, + ɵɵngDeclareInjectable, + ɵɵngDeclareInjector, + ɵɵngDeclareNgModule, + ɵɵngDeclarePipe, + ɵɵngDeclareClassMetadataAsync, ]; export interface LinkerRange { @@ -69,8 +75,10 @@ export interface LinkerRange { * be added to the end of the collection, and the version of the previous linker should be updated. */ export function createLinkerMap( - environment: LinkerEnvironment, sourceUrl: AbsoluteFsPath, - code: string): Map[]> { + environment: LinkerEnvironment, + sourceUrl: AbsoluteFsPath, + code: string, +): Map[]> { const linkers = new Map[]>(); const LATEST_VERSION_RANGE = getRange('<=', PLACEHOLDER_VERSION); @@ -87,7 +95,10 @@ export function createLinkerMap( { range: LATEST_VERSION_RANGE, linker: new PartialComponentLinkerVersion1( - createGetSourceFile(sourceUrl, code, environment.sourceFileLoader), sourceUrl, code) + createGetSourceFile(sourceUrl, code, environment.sourceFileLoader), + sourceUrl, + code, + ), }, ]); linkers.set(ɵɵngDeclareFactory, [ @@ -102,7 +113,7 @@ export function createLinkerMap( linkers.set(ɵɵngDeclareNgModule, [ { range: LATEST_VERSION_RANGE, - linker: new PartialNgModuleLinkerVersion1(environment.options.linkerJitMode) + linker: new PartialNgModuleLinkerVersion1(environment.options.linkerJitMode), }, ]); linkers.set(ɵɵngDeclarePipe, [ @@ -129,9 +140,10 @@ export function createLinkerMap( */ export class PartialLinkerSelector { constructor( - private readonly linkers: Map[]>, - private readonly logger: Logger, - private readonly unknownDeclarationVersionHandling: 'ignore'|'warn'|'error') {} + private readonly linkers: Map[]>, + private readonly logger: Logger, + private readonly unknownDeclarationVersionHandling: 'ignore' | 'warn' | 'error', + ) {} /** * Returns true if there are `PartialLinker` classes that can handle functions with this name. @@ -164,9 +176,9 @@ export class PartialLinkerSelector { } const message = - `This application depends upon a library published using Angular version ${version}, ` + - `which requires Angular version ${minVersion} or newer to work correctly.\n` + - `Consider upgrading your application to use a more recent version of Angular.`; + `This application depends upon a library published using Angular version ${version}, ` + + `which requires Angular version ${minVersion} or newer to work correctly.\n` + + `Consider upgrading your application to use a more recent version of Angular.`; if (this.unknownDeclarationVersionHandling === 'error') { throw new Error(message); @@ -190,7 +202,7 @@ export class PartialLinkerSelector { * @param versionStr the version given in the partial declaration * @returns A semver range for the provided `version` and comparator. */ -function getRange(comparator: '<='|'>=', versionStr: string): semver.Range { +function getRange(comparator: '<=' | '>=', versionStr: string): semver.Range { // If the provided version is exactly `0.0.0` then we are known to be running with an unpublished // version of angular and assume that all ranges are compatible. if (versionStr === '0.0.0' && (PLACEHOLDER_VERSION as string) === '0.0.0') { diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_ng_module_linker_1.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_ng_module_linker_1.ts index 2e3a2bfabd304..08024e1803c62 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_ng_module_linker_1.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_ng_module_linker_1.ts @@ -5,7 +5,17 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {compileNgModule, ConstantPool, outputAst as o, R3DeclareNgModuleMetadata, R3NgModuleMetadata, R3NgModuleMetadataKind, R3PartialDeclaration, R3Reference, R3SelectorScopeMode} from '@angular/compiler'; +import { + compileNgModule, + ConstantPool, + outputAst as o, + R3DeclareNgModuleMetadata, + R3NgModuleMetadata, + R3NgModuleMetadataKind, + R3PartialDeclaration, + R3Reference, + R3SelectorScopeMode, +} from '@angular/compiler'; import {AstObject, AstValue} from '../../ast/ast_value'; @@ -17,15 +27,17 @@ import {wrapReference} from './util'; */ export class PartialNgModuleLinkerVersion1 implements PartialLinker { constructor( - /** - * If true then emit the additional declarations, imports, exports, etc in the NgModule - * definition. These are only used by JIT compilation. - */ - private emitInline: boolean) {} + /** + * If true then emit the additional declarations, imports, exports, etc in the NgModule + * definition. These are only used by JIT compilation. + */ + private emitInline: boolean, + ) {} linkPartialDeclaration( - constantPool: ConstantPool, - metaObj: AstObject): LinkedDefinition { + constantPool: ConstantPool, + metaObj: AstObject, + ): LinkedDefinition { const meta = toR3NgModuleMeta(metaObj, this.emitInline); return compileNgModule(meta); } @@ -35,8 +47,9 @@ export class PartialNgModuleLinkerVersion1 implements PartialLinker * Derives the `R3NgModuleMetadata` structure from the AST object. */ export function toR3NgModuleMeta( - metaObj: AstObject, - supportJit: boolean): R3NgModuleMetadata { + metaObj: AstObject, + supportJit: boolean, +): R3NgModuleMetadata { const wrappedType = metaObj.getOpaque('type'); const meta: R3NgModuleMetadata = { @@ -69,8 +82,7 @@ export function toR3NgModuleMeta( if (bootstrap.isFunction()) { meta.containsForwardDecls = true; meta.bootstrap = wrapReferences(unwrapForwardRefs(bootstrap)); - } else - meta.bootstrap = wrapReferences(bootstrap as AstValue); + } else meta.bootstrap = wrapReferences(bootstrap as AstValue); } if (metaObj.has('declarations')) { @@ -78,8 +90,7 @@ export function toR3NgModuleMeta( if (declarations.isFunction()) { meta.containsForwardDecls = true; meta.declarations = wrapReferences(unwrapForwardRefs(declarations)); - } else - meta.declarations = wrapReferences(declarations as AstValue); + } else meta.declarations = wrapReferences(declarations as AstValue); } if (metaObj.has('imports')) { @@ -87,8 +98,7 @@ export function toR3NgModuleMeta( if (imports.isFunction()) { meta.containsForwardDecls = true; meta.imports = wrapReferences(unwrapForwardRefs(imports)); - } else - meta.imports = wrapReferences(imports as AstValue); + } else meta.imports = wrapReferences(imports as AstValue); } if (metaObj.has('exports')) { @@ -96,8 +106,7 @@ export function toR3NgModuleMeta( if (exports.isFunction()) { meta.containsForwardDecls = true; meta.exports = wrapReferences(unwrapForwardRefs(exports)); - } else - meta.exports = wrapReferences(exports as AstValue); + } else meta.exports = wrapReferences(exports as AstValue); } if (metaObj.has('schemas')) { @@ -114,8 +123,9 @@ export function toR3NgModuleMeta( * If `field` is `function() { return [exp1, exp2, exp3]; }` then we return `[exp1, exp2, exp3]`. * */ -function unwrapForwardRefs(field: AstValue): - AstValue { +function unwrapForwardRefs( + field: AstValue, +): AstValue { return (field as AstValue).getFunctionReturnValue(); } @@ -123,5 +133,5 @@ function unwrapForwardRefs(field: AstValue): * Wrap the array of expressions into an array of R3 references. */ function wrapReferences(values: AstValue): R3Reference[] { - return values.getArray().map(i => wrapReference(i.getOpaque())); + return values.getArray().map((i) => wrapReference(i.getOpaque())); } diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_pipe_linker_1.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_pipe_linker_1.ts index c26cde9b0dcc0..f74aee1926f3e 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_pipe_linker_1.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_pipe_linker_1.ts @@ -5,7 +5,14 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {compilePipeFromMetadata, ConstantPool, outputAst as o, R3DeclarePipeMetadata, R3PartialDeclaration, R3PipeMetadata} from '@angular/compiler'; +import { + compilePipeFromMetadata, + ConstantPool, + outputAst as o, + R3DeclarePipeMetadata, + R3PartialDeclaration, + R3PipeMetadata, +} from '@angular/compiler'; import {AstObject} from '../../ast/ast_value'; import {FatalLinkerError} from '../../fatal_linker_error'; @@ -20,8 +27,9 @@ export class PartialPipeLinkerVersion1 implements PartialLinker): LinkedDefinition { + constantPool: ConstantPool, + metaObj: AstObject, + ): LinkedDefinition { const meta = toR3PipeMeta(metaObj); return compilePipeFromMetadata(meta); } @@ -30,13 +38,16 @@ export class PartialPipeLinkerVersion1 implements PartialLinker(metaObj: AstObject): - R3PipeMetadata { +export function toR3PipeMeta( + metaObj: AstObject, +): R3PipeMetadata { const typeExpr = metaObj.getValue('type'); const typeName = typeExpr.getSymbolName(); if (typeName === null) { throw new FatalLinkerError( - typeExpr.expression, 'Unsupported type, its name could not be determined'); + typeExpr.expression, + 'Unsupported type, its name could not be determined', + ); } const pure = metaObj.has('pure') ? metaObj.getBoolean('pure') : true; diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/util.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/util.ts index c7ae16836ad49..5767048d46c5e 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/util.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/util.ts @@ -5,7 +5,15 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {createMayBeForwardRefExpression, ForwardRefHandling, MaybeForwardRefExpression, outputAst as o, R3DeclareDependencyMetadata, R3DependencyMetadata, R3Reference} from '@angular/compiler'; +import { + createMayBeForwardRefExpression, + ForwardRefHandling, + MaybeForwardRefExpression, + outputAst as o, + R3DeclareDependencyMetadata, + R3DependencyMetadata, + R3Reference, +} from '@angular/compiler'; import {AstObject, AstValue} from '../../ast/ast_value'; import {FatalLinkerError} from '../../fatal_linker_error'; @@ -20,7 +28,9 @@ export function wrapReference(wrapped: o.WrappedNodeExpr( - value: AstValue, Enum: TEnum): TEnum[keyof TEnum] { + value: AstValue, + Enum: TEnum, +): TEnum[keyof TEnum] { const symbolName = value.getSymbolName(); if (symbolName === null) { throw new FatalLinkerError(value.expression, 'Expected value to have a symbol name'); @@ -36,7 +46,8 @@ export function parseEnum( * Parse a dependency structure from an AST object. */ export function getDependency( - depObj: AstObject): R3DependencyMetadata { + depObj: AstObject, +): R3DependencyMetadata { const isAttribute = depObj.has('attribute') && depObj.getBoolean('attribute'); const token = depObj.getOpaque('token'); // Normally `attribute` is a string literal and so its `attributeNameType` is the same string @@ -56,7 +67,6 @@ export function getDependency( }; } - /** * Return an `R3ProviderExpression` that represents either the extracted type reference expression * from a `forwardRef` function call, or the type itself. @@ -67,8 +77,9 @@ export function getDependency( * * If there is no forwardRef call expression then we just return the opaque type. */ -export function extractForwardRef(expr: AstValue): - MaybeForwardRefExpression> { +export function extractForwardRef( + expr: AstValue, +): MaybeForwardRefExpression> { if (!expr.isCallExpression()) { return createMayBeForwardRefExpression(expr.getOpaque(), ForwardRefHandling.None); } @@ -76,22 +87,29 @@ export function extractForwardRef(expr: AstValue; if (!wrapperFn.isFunction()) { throw new FatalLinkerError( - wrapperFn, 'Unsupported `forwardRef(fn)` call, expected its argument to be a function'); + wrapperFn, + 'Unsupported `forwardRef(fn)` call, expected its argument to be a function', + ); } return createMayBeForwardRefExpression( - wrapperFn.getFunctionReturnValue().getOpaque(), ForwardRefHandling.Unwrapped); + wrapperFn.getFunctionReturnValue().getOpaque(), + ForwardRefHandling.Unwrapped, + ); } diff --git a/packages/compiler-cli/linker/src/file_linker/translator.ts b/packages/compiler-cli/linker/src/file_linker/translator.ts index 2ddc078a05bf5..ad803dcbad538 100644 --- a/packages/compiler-cli/linker/src/file_linker/translator.ts +++ b/packages/compiler-cli/linker/src/file_linker/translator.ts @@ -7,7 +7,13 @@ */ import * as o from '@angular/compiler'; -import {AstFactory, Context, ExpressionTranslatorVisitor, ImportGenerator, TranslatorOptions} from '../../../src/ngtsc/translator'; +import { + AstFactory, + Context, + ExpressionTranslatorVisitor, + ImportGenerator, + TranslatorOptions, +} from '../../../src/ngtsc/translator'; /** * Generic translator helper class, which exposes methods for translating expressions and @@ -20,23 +26,37 @@ export class Translator { * Translate the given output AST in the context of an expression. */ translateExpression( - expression: o.Expression, imports: ImportGenerator, - options: TranslatorOptions = {}): TExpression { + expression: o.Expression, + imports: ImportGenerator, + options: TranslatorOptions = {}, + ): TExpression { return expression.visitExpression( - new ExpressionTranslatorVisitor( - this.factory, imports, null, options), - new Context(false)); + new ExpressionTranslatorVisitor( + this.factory, + imports, + null, + options, + ), + new Context(false), + ); } /** * Translate the given output AST in the context of a statement. */ translateStatement( - statement: o.Statement, imports: ImportGenerator, - options: TranslatorOptions = {}): TStatement { + statement: o.Statement, + imports: ImportGenerator, + options: TranslatorOptions = {}, + ): TStatement { return statement.visitStatement( - new ExpressionTranslatorVisitor( - this.factory, imports, null, options), - new Context(true)); + new ExpressionTranslatorVisitor( + this.factory, + imports, + null, + options, + ), + new Context(true), + ); } } diff --git a/packages/compiler-cli/linker/src/linker_import_generator.ts b/packages/compiler-cli/linker/src/linker_import_generator.ts index 32ef19f546b55..5550f18627753 100644 --- a/packages/compiler-cli/linker/src/linker_import_generator.ts +++ b/packages/compiler-cli/linker/src/linker_import_generator.ts @@ -17,10 +17,13 @@ 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 { - constructor(private factory: AstFactory, private ngImport: TExpression) { - } +export class LinkerImportGenerator + implements ImportGenerator +{ + constructor( + private factory: AstFactory, + private ngImport: TExpression, + ) {} addImport(request: ImportRequest): TExpression { this.assertModuleName(request.exportModuleSpecifier); @@ -35,7 +38,9 @@ export class LinkerImportGenerator implements private assertModuleName(moduleName: string): void { if (moduleName !== '@angular/core') { throw new FatalLinkerError( - this.ngImport, `Unable to import from anything other than '@angular/core'`); + this.ngImport, + `Unable to import from anything other than '@angular/core'`, + ); } } } 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 91aabcfcb6b34..c12d5743a3182 100644 --- a/packages/compiler-cli/linker/test/ast/ast_value_spec.ts +++ b/packages/compiler-cli/linker/test/ast/ast_value_spec.ts @@ -28,17 +28,20 @@ const nestedObj = factory.createObjectLiteral([ {propertyName: 'x', quoted: false, value: factory.createLiteral(42)}, {propertyName: 'y', quoted: false, value: factory.createLiteral('X')}, ]); -const nestedArray = - factory.createArrayLiteral([factory.createLiteral(1), factory.createLiteral(2)]); +const nestedArray = factory.createArrayLiteral([ + factory.createLiteral(1), + factory.createLiteral(2), +]); const obj = AstObject.parse( - factory.createObjectLiteral([ - {propertyName: 'a', quoted: false, value: factory.createLiteral(42)}, - {propertyName: 'b', quoted: false, value: factory.createLiteral('X')}, - {propertyName: 'c', quoted: false, value: factory.createLiteral(true)}, - {propertyName: 'd', quoted: false, value: nestedObj}, - {propertyName: 'e', quoted: false, value: nestedArray}, - ]), - host); + factory.createObjectLiteral([ + {propertyName: 'a', quoted: false, value: factory.createLiteral(42)}, + {propertyName: 'b', quoted: false, value: factory.createLiteral('X')}, + {propertyName: 'c', quoted: false, value: factory.createLiteral(true)}, + {propertyName: 'd', quoted: false, value: nestedObj}, + {propertyName: 'e', quoted: false, value: nestedArray}, + ]), + host, +); describe('AstObject', () => { describe('has()', () => { @@ -59,8 +62,9 @@ describe('AstObject', () => { it('should throw an error if the property is not a number', () => { // @ts-expect-error - expect(() => obj.getNumber('b')) - .toThrowError('Unsupported syntax, expected a numeric literal.'); + expect(() => obj.getNumber('b')).toThrowError( + 'Unsupported syntax, expected a numeric literal.', + ); }); }); @@ -71,8 +75,9 @@ describe('AstObject', () => { it('should throw an error if the property is not a string', () => { // @ts-expect-error - expect(() => obj.getString('a')) - .toThrowError('Unsupported syntax, expected a string literal.'); + expect(() => obj.getString('a')).toThrowError( + 'Unsupported syntax, expected a string literal.', + ); }); }); @@ -83,8 +88,9 @@ describe('AstObject', () => { it('should throw an error if the property is not a boolean', () => { // @ts-expect-error - expect(() => obj.getBoolean('b')) - .toThrowError('Unsupported syntax, expected a boolean literal.'); + expect(() => obj.getBoolean('b')).toThrowError( + 'Unsupported syntax, expected a boolean literal.', + ); }); }); @@ -95,24 +101,25 @@ describe('AstObject', () => { it('should throw an error if the property is not an object expression', () => { // @ts-expect-error - expect(() => obj.getObject('b')) - .toThrowError('Unsupported syntax, expected an object literal.'); + expect(() => obj.getObject('b')).toThrowError( + 'Unsupported syntax, expected an object literal.', + ); }); }); describe('getArray()', () => { - it('should return an array of AstValue instances of parsed from the value of the property', - () => { - expect(obj.getArray('e')).toEqual([ - new AstValue(factory.createLiteral(1), host), - new AstValue(factory.createLiteral(2), host) - ]); - }); + it('should return an array of AstValue instances of parsed from the value of the property', () => { + expect(obj.getArray('e')).toEqual([ + new AstValue(factory.createLiteral(1), host), + new AstValue(factory.createLiteral(2), host), + ]); + }); it('should throw an error if the property is not an array of expressions', () => { // @ts-expect-error - expect(() => obj.getArray('b')) - .toThrowError('Unsupported syntax, expected an array literal.'); + expect(() => obj.getArray('b')).toThrowError( + 'Unsupported syntax, expected an array literal.', + ); }); }); @@ -123,8 +130,9 @@ describe('AstObject', () => { }); it('should throw an error if the property does not exist', () => { - expect(() => obj.getOpaque('missing')) - .toThrowError(`Expected property 'missing' to be present.`); + expect(() => obj.getOpaque('missing')).toThrowError( + `Expected property 'missing' to be present.`, + ); // @ts-expect-error expect(() => obj.getOpaque('x')).toThrowError(`Expected property 'x' to be present.`); @@ -137,8 +145,9 @@ describe('AstObject', () => { }); it('should throw an error if the property does not exist', () => { - expect(() => obj.getNode('missing')) - .toThrowError(`Expected property 'missing' to be present.`); + expect(() => obj.getNode('missing')).toThrowError( + `Expected property 'missing' to be present.`, + ); // @ts-expect-error expect(() => obj.getNode('x')).toThrowError(`Expected property 'x' to be present.`); @@ -152,8 +161,9 @@ describe('AstObject', () => { }); it('should throw an error if the property does not exist', () => { - expect(() => obj.getValue('missing')) - .toThrowError(`Expected property 'missing' to be present.`); + expect(() => obj.getValue('missing')).toThrowError( + `Expected property 'missing' to be present.`, + ); // @ts-expect-error expect(() => obj.getValue('x')).toThrowError(`Expected property 'x' to be present.`); @@ -162,7 +172,7 @@ describe('AstObject', () => { describe('toLiteral()', () => { it('should convert the AstObject to a raw object with each property mapped', () => { - expect(obj.toLiteral(value => value.getOpaque())).toEqual({ + expect(obj.toLiteral((value) => value.getOpaque())).toEqual({ a: obj.getOpaque('a'), b: obj.getOpaque('b'), c: obj.getOpaque('c'), @@ -174,13 +184,15 @@ describe('AstObject', () => { describe('toMap()', () => { it('should convert the AstObject to a Map with each property mapped', () => { - expect(obj.toMap(value => value.getOpaque())).toEqual(new Map([ - ['a', obj.getOpaque('a')], - ['b', obj.getOpaque('b')], - ['c', obj.getOpaque('c')], - ['d', obj.getOpaque('d')], - ['e', obj.getOpaque('e')], - ])); + expect(obj.toMap((value) => value.getOpaque())).toEqual( + new Map([ + ['a', obj.getOpaque('a')], + ['b', obj.getOpaque('b')], + ['c', obj.getOpaque('c')], + ['d', obj.getOpaque('d')], + ['e', obj.getOpaque('e')], + ]), + ); }); }); }); @@ -197,7 +209,9 @@ describe('AstValue', () => { it('should return the name of a property access', () => { const propertyAccess = factory.createPropertyAccess( - factory.createIdentifier('Foo'), factory.createIdentifier('Bar')); + factory.createIdentifier('Foo'), + factory.createIdentifier('Bar'), + ); expect(createAstValue(propertyAccess).getSymbolName()).toEqual('Bar'); }); @@ -223,8 +237,9 @@ describe('AstValue', () => { it('should throw an error if the property is not a number', () => { // @ts-expect-error - expect(() => createAstValue(factory.createLiteral('a')).getNumber()) - .toThrowError('Unsupported syntax, expected a numeric literal.'); + expect(() => createAstValue(factory.createLiteral('a')).getNumber()).toThrowError( + 'Unsupported syntax, expected a numeric literal.', + ); }); }); @@ -245,8 +260,9 @@ describe('AstValue', () => { it('should throw an error if the property is not a string', () => { // @ts-expect-error - expect(() => createAstValue(factory.createLiteral(42)).getString()) - .toThrowError('Unsupported syntax, expected a string literal.'); + expect(() => createAstValue(factory.createLiteral(42)).getString()).toThrowError( + 'Unsupported syntax, expected a string literal.', + ); }); }); @@ -267,8 +283,9 @@ describe('AstValue', () => { it('should throw an error if the property is not a boolean', () => { // @ts-expect-error - expect(() => createAstValue(factory.createLiteral(42)).getBoolean()) - .toThrowError('Unsupported syntax, expected a boolean literal.'); + expect(() => createAstValue(factory.createLiteral(42)).getBoolean()).toThrowError( + 'Unsupported syntax, expected a boolean literal.', + ); }); }); @@ -284,14 +301,16 @@ describe('AstValue', () => { describe('getObject', () => { it('should return the AstObject value of the AstValue', () => { - expect(createAstValue(nestedObj).getObject()) - .toEqual(AstObject.parse(nestedObj, host)); + expect(createAstValue(nestedObj).getObject()).toEqual( + AstObject.parse(nestedObj, host), + ); }); it('should throw an error if the property is not an object literal', () => { // @ts-expect-error - expect(() => createAstValue(factory.createLiteral(42)).getObject()) - .toThrowError('Unsupported syntax, expected an object literal.'); + expect(() => createAstValue(factory.createLiteral(42)).getObject()).toThrowError( + 'Unsupported syntax, expected an object literal.', + ); }); }); @@ -315,16 +334,19 @@ describe('AstValue', () => { it('should throw an error if the property is not an array', () => { // @ts-expect-error - expect(() => createAstValue(factory.createLiteral(42)).getArray()) - .toThrowError('Unsupported syntax, expected an array literal.'); + expect(() => createAstValue(factory.createLiteral(42)).getArray()).toThrowError( + 'Unsupported syntax, expected an array literal.', + ); }); }); describe('isFunction', () => { it('should return true if the value is a function expression', () => { const funcExpr = factory.createFunctionExpression( - 'foo', [], - factory.createBlock([factory.createReturnStatement(factory.createLiteral(42))])); + 'foo', + [], + factory.createBlock([factory.createReturnStatement(factory.createLiteral(42))]), + ); expect(createAstValue(funcExpr).isFunction()).toEqual(true); }); @@ -336,41 +358,49 @@ describe('AstValue', () => { describe('getFunctionReturnValue', () => { it('should return the "return value" of the function expression', () => { const funcExpr = factory.createFunctionExpression( - 'foo', [], - factory.createBlock([factory.createReturnStatement(factory.createLiteral(42))])); - expect(createAstValue(funcExpr).getFunctionReturnValue()) - .toEqual(createAstValue(factory.createLiteral(42))); + 'foo', + [], + factory.createBlock([factory.createReturnStatement(factory.createLiteral(42))]), + ); + expect(createAstValue(funcExpr).getFunctionReturnValue()).toEqual( + createAstValue(factory.createLiteral(42)), + ); }); it('should throw an error if the property is not a function expression', () => { - // @ts-expect-error - expect(() => createAstValue(factory.createLiteral(42)).getFunctionReturnValue()) - .toThrowError('Unsupported syntax, expected a function.'); - }); - - it('should throw an error if the property is a function expression with no return value', - () => { - const funcExpr = factory.createFunctionExpression( - 'foo', [], factory.createBlock([factory.createExpressionStatement( - factory.createLiteral('do nothing'))])); - expect(() => createAstValue(funcExpr).getFunctionReturnValue()) - .toThrowError( - 'Unsupported syntax, expected a function body with a single return statement.'); - }); + expect(() => + // @ts-expect-error + createAstValue(factory.createLiteral(42)).getFunctionReturnValue(), + ).toThrowError('Unsupported syntax, expected a function.'); + }); + + it('should throw an error if the property is a function expression with no return value', () => { + const funcExpr = factory.createFunctionExpression( + 'foo', + [], + factory.createBlock([ + factory.createExpressionStatement(factory.createLiteral('do nothing')), + ]), + ); + expect(() => createAstValue(funcExpr).getFunctionReturnValue()).toThrowError( + 'Unsupported syntax, expected a function body with a single return statement.', + ); + }); }); describe('getFunctionParameters', () => { it('should return the parameters of a function expression', () => { const funcExpr = factory.createFunctionExpression('foo', ['a', 'b'], factory.createBlock([])); - expect(createAstValue(funcExpr).getFunctionParameters()).toEqual([ - 'a', 'b' - ].map(name => createAstValue(factory.createIdentifier(name)))); + expect(createAstValue(funcExpr).getFunctionParameters()).toEqual( + ['a', 'b'].map((name) => createAstValue(factory.createIdentifier(name))), + ); }); it('should throw an error if the property is not a function declaration', () => { - // @ts-expect-error - expect(() => createAstValue(factory.createLiteral(42)).getFunctionParameters()) - .toThrowError('Unsupported syntax, expected a function.'); + expect(() => + // @ts-expect-error + createAstValue(factory.createLiteral(42)).getFunctionParameters(), + ).toThrowError('Unsupported syntax, expected a function.'); }); }); @@ -389,25 +419,25 @@ describe('AstValue', () => { describe('getCallee', () => { it('should return the callee expression as a value', () => { const callExpr = factory.createCallExpression(factory.createIdentifier('foo'), [], false); - expect(createAstValue(callExpr).getCallee()) - .toEqual(createAstValue(factory.createIdentifier('foo'))); + expect(createAstValue(callExpr).getCallee()).toEqual( + createAstValue(factory.createIdentifier('foo')), + ); }); it('should throw an error if the value is not a call expression', () => { - expect(() => createAstValue(factory.createLiteral(42)).getCallee()) - .toThrowError('Unsupported syntax, expected a call expression.'); + expect(() => createAstValue(factory.createLiteral(42)).getCallee()).toThrowError( + 'Unsupported syntax, expected a call expression.', + ); }); }); describe('getArguments', () => { it('should return the arguments as an array of values', () => { const callExpr = factory.createCallExpression( - factory.createIdentifier('foo'), - [ - factory.createLiteral(1), - factory.createLiteral(2), - ], - false); + factory.createIdentifier('foo'), + [factory.createLiteral(1), factory.createLiteral(2)], + false, + ); expect(createAstValue(callExpr).getArguments()).toEqual([ createAstValue(factory.createLiteral(1)), createAstValue(factory.createLiteral(2)), @@ -415,34 +445,45 @@ describe('AstValue', () => { }); it('should throw an error if the value is not a call expression', () => { - expect(() => createAstValue(factory.createLiteral(42)).getArguments()) - .toThrowError('Unsupported syntax, expected a call expression.'); + expect(() => createAstValue(factory.createLiteral(42)).getArguments()).toThrowError( + 'Unsupported syntax, expected a call expression.', + ); }); }); describe('getOpaque()', () => { it('should return the value wrapped in a `WrappedNodeExpr`', () => { - expect(createAstValue(factory.createLiteral(42)).getOpaque()) - .toEqual(jasmine.any(WrappedNodeExpr)); - expect(createAstValue(factory.createLiteral(42)).getOpaque().node) - .toEqual(factory.createLiteral(42)); + expect(createAstValue(factory.createLiteral(42)).getOpaque()).toEqual( + jasmine.any(WrappedNodeExpr), + ); + expect(createAstValue(factory.createLiteral(42)).getOpaque().node).toEqual( + factory.createLiteral(42), + ); }); }); describe('getRange()', () => { it('should return the source range of the AST node', () => { const file = ts.createSourceFile( - 'test.ts', '// preamble\nx = \'moo\';', ts.ScriptTarget.ES2015, - /* setParentNodes */ true); + 'test.ts', + "// preamble\nx = 'moo';", + ts.ScriptTarget.ES2015, + /* setParentNodes */ true, + ); // Grab the `'moo'` string literal from the generated AST const stmt = file.statements[0] as ts.ExpressionStatement; - const mooString = - (stmt.expression as ts.AssignmentExpression>).right; + const mooString = ( + stmt.expression as ts.AssignmentExpression> + ).right; // Check that this string literal has the expected range. - expect(createAstValue(mooString).getRange()) - .toEqual({startLine: 1, startCol: 4, startPos: 16, endPos: 21}); + expect(createAstValue(mooString).getRange()).toEqual({ + startLine: 1, + startCol: 4, + startPos: 16, + endPos: 21, + }); }); }); }); diff --git a/packages/compiler-cli/linker/test/ast/typescript/typescript_ast_host_spec.ts b/packages/compiler-cli/linker/test/ast/typescript/typescript_ast_host_spec.ts index 81dd714dc90cc..0634f7cb1aa3f 100644 --- a/packages/compiler-cli/linker/test/ast/typescript/typescript_ast_host_spec.ts +++ b/packages/compiler-cli/linker/test/ast/typescript/typescript_ast_host_spec.ts @@ -11,7 +11,7 @@ import {TypeScriptAstHost} from '../../../src/ast/typescript/typescript_ast_host describe('TypeScriptAstHost', () => { let host: TypeScriptAstHost; - beforeEach(() => host = new TypeScriptAstHost()); + beforeEach(() => (host = new TypeScriptAstHost())); describe('getSymbolName()', () => { it('should return the name of an identifier', () => { @@ -30,7 +30,7 @@ describe('TypeScriptAstHost', () => { describe('isStringLiteral()', () => { it('should return true if the expression is a string literal', () => { expect(host.isStringLiteral(expr('"moo"'))).toBe(true); - expect(host.isStringLiteral(expr('\'moo\''))).toBe(true); + expect(host.isStringLiteral(expr("'moo'"))).toBe(true); }); it('should return false if the expression is not a string literal', () => { @@ -40,23 +40,24 @@ describe('TypeScriptAstHost', () => { expect(host.isStringLiteral(rhs('x = {}'))).toBe(false); expect(host.isStringLiteral(expr('[]'))).toBe(false); expect(host.isStringLiteral(expr('null'))).toBe(false); - expect(host.isStringLiteral(expr('\'a\' + \'b\''))).toBe(false); + expect(host.isStringLiteral(expr("'a' + 'b'"))).toBe(false); }); it('should return false if the expression is a template string', () => { - expect(host.isStringLiteral(expr('\`moo\`'))).toBe(false); + expect(host.isStringLiteral(expr('`moo`'))).toBe(false); }); }); describe('parseStringLiteral()', () => { it('should extract the string value', () => { expect(host.parseStringLiteral(expr('"moo"'))).toEqual('moo'); - expect(host.parseStringLiteral(expr('\'moo\''))).toEqual('moo'); + expect(host.parseStringLiteral(expr("'moo'"))).toEqual('moo'); }); it('should error if the value is not a string literal', () => { - expect(() => host.parseStringLiteral(expr('42'))) - .toThrowError('Unsupported syntax, expected a string literal.'); + expect(() => host.parseStringLiteral(expr('42'))).toThrowError( + 'Unsupported syntax, expected a string literal.', + ); }); }); @@ -68,13 +69,13 @@ describe('TypeScriptAstHost', () => { it('should return false if the expression is not a number literal', () => { expect(host.isStringLiteral(expr('true'))).toBe(false); expect(host.isNumericLiteral(expr('"moo"'))).toBe(false); - expect(host.isNumericLiteral(expr('\'moo\''))).toBe(false); + expect(host.isNumericLiteral(expr("'moo'"))).toBe(false); expect(host.isNumericLiteral(expr('someIdentifier'))).toBe(false); expect(host.isNumericLiteral(rhs('x = {}'))).toBe(false); expect(host.isNumericLiteral(expr('[]'))).toBe(false); expect(host.isNumericLiteral(expr('null'))).toBe(false); - expect(host.isNumericLiteral(expr('\'a\' + \'b\''))).toBe(false); - expect(host.isNumericLiteral(expr('\`moo\`'))).toBe(false); + expect(host.isNumericLiteral(expr("'a' + 'b'"))).toBe(false); + expect(host.isNumericLiteral(expr('`moo`'))).toBe(false); }); }); @@ -84,8 +85,9 @@ describe('TypeScriptAstHost', () => { }); it('should error if the value is not a numeric literal', () => { - expect(() => host.parseNumericLiteral(expr('"moo"'))) - .toThrowError('Unsupported syntax, expected a numeric literal.'); + expect(() => host.parseNumericLiteral(expr('"moo"'))).toThrowError( + 'Unsupported syntax, expected a numeric literal.', + ); }); }); @@ -102,14 +104,14 @@ describe('TypeScriptAstHost', () => { it('should return false if the expression is not a boolean literal', () => { expect(host.isBooleanLiteral(expr('"moo"'))).toBe(false); - expect(host.isBooleanLiteral(expr('\'moo\''))).toBe(false); + expect(host.isBooleanLiteral(expr("'moo'"))).toBe(false); expect(host.isBooleanLiteral(expr('someIdentifier'))).toBe(false); expect(host.isBooleanLiteral(expr('42'))).toBe(false); expect(host.isBooleanLiteral(rhs('x = {}'))).toBe(false); expect(host.isBooleanLiteral(expr('[]'))).toBe(false); expect(host.isBooleanLiteral(expr('null'))).toBe(false); - expect(host.isBooleanLiteral(expr('\'a\' + \'b\''))).toBe(false); - expect(host.isBooleanLiteral(expr('\`moo\`'))).toBe(false); + expect(host.isBooleanLiteral(expr("'a' + 'b'"))).toBe(false); + expect(host.isBooleanLiteral(expr('`moo`'))).toBe(false); expect(host.isBooleanLiteral(expr('!2'))).toBe(false); expect(host.isBooleanLiteral(expr('~1'))).toBe(false); }); @@ -127,8 +129,9 @@ describe('TypeScriptAstHost', () => { }); it('should error if the value is not a boolean literal', () => { - expect(() => host.parseBooleanLiteral(expr('"moo"'))) - .toThrowError('Unsupported syntax, expected a boolean literal.'); + expect(() => host.parseBooleanLiteral(expr('"moo"'))).toThrowError( + 'Unsupported syntax, expected a boolean literal.', + ); }); }); @@ -141,13 +144,13 @@ describe('TypeScriptAstHost', () => { it('should return false if the expression is not an array literal', () => { expect(host.isArrayLiteral(expr('"moo"'))).toBe(false); - expect(host.isArrayLiteral(expr('\'moo\''))).toBe(false); + expect(host.isArrayLiteral(expr("'moo'"))).toBe(false); expect(host.isArrayLiteral(expr('someIdentifier'))).toBe(false); expect(host.isArrayLiteral(expr('42'))).toBe(false); expect(host.isArrayLiteral(rhs('x = {}'))).toBe(false); expect(host.isArrayLiteral(expr('null'))).toBe(false); - expect(host.isArrayLiteral(expr('\'a\' + \'b\''))).toBe(false); - expect(host.isArrayLiteral(expr('\`moo\`'))).toBe(false); + expect(host.isArrayLiteral(expr("'a' + 'b'"))).toBe(false); + expect(host.isArrayLiteral(expr('`moo`'))).toBe(false); }); }); @@ -155,35 +158,37 @@ describe('TypeScriptAstHost', () => { it('should extract the expressions in the array', () => { const moo = jasmine.objectContaining({text: 'moo', kind: ts.SyntaxKind.StringLiteral}); expect(host.parseArrayLiteral(expr('[]'))).toEqual([]); - expect(host.parseArrayLiteral(expr('[\'moo\']'))).toEqual([moo]); + expect(host.parseArrayLiteral(expr("['moo']"))).toEqual([moo]); }); it('should error if there is an empty item', () => { - expect(() => host.parseArrayLiteral(expr('[,]'))) - .toThrowError('Unsupported syntax, expected element in array not to be empty.'); + expect(() => host.parseArrayLiteral(expr('[,]'))).toThrowError( + 'Unsupported syntax, expected element in array not to be empty.', + ); }); it('should error if there is a spread element', () => { - expect(() => host.parseArrayLiteral(expr('[...[0,1]]'))) - .toThrowError('Unsupported syntax, expected element in array not to use spread syntax.'); + expect(() => host.parseArrayLiteral(expr('[...[0,1]]'))).toThrowError( + 'Unsupported syntax, expected element in array not to use spread syntax.', + ); }); }); describe('isObjectLiteral()', () => { it('should return true if the expression is an object literal', () => { expect(host.isObjectLiteral(rhs('x = {}'))).toBe(true); - expect(host.isObjectLiteral(rhs('x = { foo: \'bar\' }'))).toBe(true); + expect(host.isObjectLiteral(rhs("x = { foo: 'bar' }"))).toBe(true); }); it('should return false if the expression is not an object literal', () => { expect(host.isObjectLiteral(rhs('x = "moo"'))).toBe(false); - expect(host.isObjectLiteral(rhs('x = \'moo\''))).toBe(false); + expect(host.isObjectLiteral(rhs("x = 'moo'"))).toBe(false); expect(host.isObjectLiteral(rhs('x = someIdentifier'))).toBe(false); expect(host.isObjectLiteral(rhs('x = 42'))).toBe(false); expect(host.isObjectLiteral(rhs('x = []'))).toBe(false); expect(host.isObjectLiteral(rhs('x = null'))).toBe(false); - expect(host.isObjectLiteral(rhs('x = \'a\' + \'b\''))).toBe(false); - expect(host.isObjectLiteral(rhs('x = \`moo\`'))).toBe(false); + expect(host.isObjectLiteral(rhs("x = 'a' + 'b'"))).toBe(false); + expect(host.isObjectLiteral(rhs('x = `moo`'))).toBe(false); }); }); @@ -191,17 +196,19 @@ describe('TypeScriptAstHost', () => { it('should extract the properties from the object', () => { const moo = jasmine.objectContaining({text: 'moo', kind: ts.SyntaxKind.StringLiteral}); expect(host.parseObjectLiteral(rhs('x = {}'))).toEqual(new Map()); - expect(host.parseObjectLiteral(rhs('x = {a: \'moo\'}'))).toEqual(new Map([['a', moo]])); + expect(host.parseObjectLiteral(rhs("x = {a: 'moo'}"))).toEqual(new Map([['a', moo]])); }); it('should error if there is a method', () => { - expect(() => host.parseObjectLiteral(rhs('x = { foo() {} }'))) - .toThrowError('Unsupported syntax, expected a property assignment.'); + expect(() => host.parseObjectLiteral(rhs('x = { foo() {} }'))).toThrowError( + 'Unsupported syntax, expected a property assignment.', + ); }); it('should error if there is a spread element', () => { - expect(() => host.parseObjectLiteral(rhs('x = {...{ a: \'moo\' }}'))) - .toThrowError('Unsupported syntax, expected a property assignment.'); + expect(() => host.parseObjectLiteral(rhs("x = {...{ a: 'moo' }}"))).toThrowError( + 'Unsupported syntax, expected a property assignment.', + ); }); }); @@ -216,57 +223,61 @@ describe('TypeScriptAstHost', () => { it('should return false if the expression is not a function', () => { expect(host.isFunctionExpression(expr('[]'))).toBe(false); expect(host.isFunctionExpression(expr('"moo"'))).toBe(false); - expect(host.isFunctionExpression(expr('\'moo\''))).toBe(false); + expect(host.isFunctionExpression(expr("'moo'"))).toBe(false); expect(host.isFunctionExpression(expr('someIdentifier'))).toBe(false); expect(host.isFunctionExpression(expr('42'))).toBe(false); expect(host.isFunctionExpression(rhs('x = {}'))).toBe(false); expect(host.isFunctionExpression(expr('null'))).toBe(false); - expect(host.isFunctionExpression(expr('\'a\' + \'b\''))).toBe(false); - expect(host.isFunctionExpression(expr('\`moo\`'))).toBe(false); + expect(host.isFunctionExpression(expr("'a' + 'b'"))).toBe(false); + expect(host.isFunctionExpression(expr('`moo`'))).toBe(false); }); }); describe('parseReturnValue()', () => { it('should extract the return value of a function', () => { const moo = jasmine.objectContaining({text: 'moo', kind: ts.SyntaxKind.StringLiteral}); - expect(host.parseReturnValue(rhs('x = function() { return \'moo\'; }'))).toEqual(moo); + expect(host.parseReturnValue(rhs("x = function() { return 'moo'; }"))).toEqual(moo); }); it('should extract the value of a simple arrow function', () => { const moo = jasmine.objectContaining({text: 'moo', kind: ts.SyntaxKind.StringLiteral}); - expect(host.parseReturnValue(rhs('x = () => \'moo\''))).toEqual(moo); + expect(host.parseReturnValue(rhs("x = () => 'moo'"))).toEqual(moo); }); it('should extract the return value of an arrow function', () => { const moo = jasmine.objectContaining({text: 'moo', kind: ts.SyntaxKind.StringLiteral}); - expect(host.parseReturnValue(rhs('x = () => { return \'moo\' }'))).toEqual(moo); + expect(host.parseReturnValue(rhs("x = () => { return 'moo' }"))).toEqual(moo); }); it('should error if the body has 0 statements', () => { - expect(() => host.parseReturnValue(rhs('x = function () { }'))) - .toThrowError( - 'Unsupported syntax, expected a function body with a single return statement.'); - expect(() => host.parseReturnValue(rhs('x = () => { }'))) - .toThrowError( - 'Unsupported syntax, expected a function body with a single return statement.'); + expect(() => host.parseReturnValue(rhs('x = function () { }'))).toThrowError( + 'Unsupported syntax, expected a function body with a single return statement.', + ); + expect(() => host.parseReturnValue(rhs('x = () => { }'))).toThrowError( + 'Unsupported syntax, expected a function body with a single return statement.', + ); }); it('should error if the body has more than 1 statement', () => { - expect(() => host.parseReturnValue(rhs('x = function () { const x = 10; return x; }'))) - .toThrowError( - 'Unsupported syntax, expected a function body with a single return statement.'); - expect(() => host.parseReturnValue(rhs('x = () => { const x = 10; return x; }'))) - .toThrowError( - 'Unsupported syntax, expected a function body with a single return statement.'); + expect(() => + host.parseReturnValue(rhs('x = function () { const x = 10; return x; }')), + ).toThrowError( + 'Unsupported syntax, expected a function body with a single return statement.', + ); + expect(() => + host.parseReturnValue(rhs('x = () => { const x = 10; return x; }')), + ).toThrowError( + 'Unsupported syntax, expected a function body with a single return statement.', + ); }); it('should error if the single statement is not a return statement', () => { - expect(() => host.parseReturnValue(rhs('x = function () { const x = 10; }'))) - .toThrowError( - 'Unsupported syntax, expected a function body with a single return statement.'); - expect(() => host.parseReturnValue(rhs('x = () => { const x = 10; }'))) - .toThrowError( - 'Unsupported syntax, expected a function body with a single return statement.'); + expect(() => host.parseReturnValue(rhs('x = function () { const x = 10; }'))).toThrowError( + 'Unsupported syntax, expected a function body with a single return statement.', + ); + expect(() => host.parseReturnValue(rhs('x = () => { const x = 10; }'))).toThrowError( + 'Unsupported syntax, expected a function body with a single return statement.', + ); }); }); @@ -279,13 +290,15 @@ describe('TypeScriptAstHost', () => { }); it('should error if the node is not a function declaration or arrow function', () => { - expect(() => host.parseParameters(expr('[]'))) - .toThrowError('Unsupported syntax, expected a function.'); + expect(() => host.parseParameters(expr('[]'))).toThrowError( + 'Unsupported syntax, expected a function.', + ); }); it('should error if a parameter uses spread syntax', () => { - expect(() => host.parseParameters(rhs('x = function(a, ...other) {}'))) - .toThrowError('Unsupported syntax, expected an identifier.'); + expect(() => host.parseParameters(rhs('x = function(a, ...other) {}'))).toThrowError( + 'Unsupported syntax, expected an identifier.', + ); }); }); @@ -299,13 +312,13 @@ describe('TypeScriptAstHost', () => { it('should return false if the expression is not a call expression', () => { expect(host.isCallExpression(expr('[]'))).toBe(false); expect(host.isCallExpression(expr('"moo"'))).toBe(false); - expect(host.isCallExpression(expr('\'moo\''))).toBe(false); + expect(host.isCallExpression(expr("'moo'"))).toBe(false); expect(host.isCallExpression(expr('someIdentifier'))).toBe(false); expect(host.isCallExpression(expr('42'))).toBe(false); expect(host.isCallExpression(rhs('x = {}'))).toBe(false); expect(host.isCallExpression(expr('null'))).toBe(false); - expect(host.isCallExpression(expr('\'a\' + \'b\''))).toBe(false); - expect(host.isCallExpression(expr('\`moo\`'))).toBe(false); + expect(host.isCallExpression(expr("'a' + 'b'"))).toBe(false); + expect(host.isCallExpression(expr('`moo`'))).toBe(false); }); }); @@ -318,8 +331,9 @@ describe('TypeScriptAstHost', () => { }); it('should error if the node is not a call expression', () => { - expect(() => host.parseCallee(expr('[]'))) - .toThrowError('Unsupported syntax, expected a call expression.'); + expect(() => host.parseCallee(expr('[]'))).toThrowError( + 'Unsupported syntax, expected a call expression.', + ); }); }); @@ -332,26 +346,29 @@ describe('TypeScriptAstHost', () => { }); it('should error if the node is not a call expression', () => { - expect(() => host.parseArguments(expr('[]'))) - .toThrowError('Unsupported syntax, expected a call expression.'); + expect(() => host.parseArguments(expr('[]'))).toThrowError( + 'Unsupported syntax, expected a call expression.', + ); }); it('should error if an argument uses spread syntax', () => { - expect(() => host.parseArguments(expr('foo(1, ...[])'))) - .toThrowError('Unsupported syntax, expected argument not to use spread syntax.'); + expect(() => host.parseArguments(expr('foo(1, ...[])'))).toThrowError( + 'Unsupported syntax, expected argument not to use spread syntax.', + ); }); }); describe('getRange()', () => { it('should extract the range from the expression', () => { - const moo = rhs('// preamble\nx = \'moo\';'); + const moo = rhs("// preamble\nx = 'moo';"); expect(host.getRange(moo)).toEqual({startLine: 1, startCol: 4, startPos: 16, endPos: 21}); }); it('should error if the nodes do not have attached parents', () => { - const moo = rhs('// preamble\nx = \'moo\';', false); - expect(() => host.getRange(moo)) - .toThrowError('Unable to read range for node - it is missing parent information.'); + const moo = rhs("// preamble\nx = 'moo';", false); + expect(() => host.getRange(moo)).toThrowError( + 'Unable to read range for node - it is missing parent information.', + ); }); }); }); diff --git a/packages/compiler-cli/linker/test/fatal_linker_error_spec.ts b/packages/compiler-cli/linker/test/fatal_linker_error_spec.ts index 1b4572fff86fd..d2c44130dd338 100644 --- a/packages/compiler-cli/linker/test/fatal_linker_error_spec.ts +++ b/packages/compiler-cli/linker/test/fatal_linker_error_spec.ts @@ -11,8 +11,9 @@ import {FatalLinkerError, isFatalLinkerError} from '../src/fatal_linker_error'; describe('FatalLinkerError', () => { it('should expose the `node` and `message`', () => { const node = {}; - expect(new FatalLinkerError(node, 'Some message')) - .toEqual(jasmine.objectContaining({node, message: 'Some message'})); + expect(new FatalLinkerError(node, 'Some message')).toEqual( + jasmine.objectContaining({node, message: 'Some message'}), + ); }); }); 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 52812f7a3630f..c07582cc471c9 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 @@ -36,9 +36,7 @@ describe('EmitScope', () => { const def = emitScope.translateDefinition({ expression: o.fn([], [], null, null, 'foo'), - statements: [ - o.variable('testFn').callFn([]).toStmt(), - ], + statements: [o.variable('testFn').callFn([]).toStmt()], }); expect(generate(def)).toEqual('function () { testFn(); return function foo() { }; }()'); }); 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 8f314391642d9..a0757f0e1136e 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 @@ -20,24 +20,31 @@ describe('LocalEmitScope', () => { const factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false); const translator = new Translator(factory); const ngImport = factory.createIdentifier('core'); - const emitScope = - new LocalEmitScope(ngImport, translator, factory); + const emitScope = new LocalEmitScope( + ngImport, + translator, + factory, + ); addSharedStatement(emitScope.constantPool); const def = emitScope.translateDefinition({ expression: o.fn([], [], null, null, 'foo'), statements: [], }); - expect(generate(def)) - .toEqual('function () { const _c0 = ["CONST"]; return function foo() { }; }()'); + expect(generate(def)).toEqual( + 'function () { const _c0 = ["CONST"]; return function foo() { }; }()', + ); }); it('should use the `ngImport` identifier for imports when translating', () => { const factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false); const translator = new Translator(factory); const ngImport = factory.createIdentifier('core'); - const emitScope = - new LocalEmitScope(ngImport, translator, factory); + const emitScope = new LocalEmitScope( + ngImport, + translator, + factory, + ); addSharedStatement(emitScope.constantPool); const coreImportRef = new o.ExternalReference('@angular/core', 'foo'); @@ -45,16 +52,20 @@ describe('LocalEmitScope', () => { expression: o.importExpr(coreImportRef).prop('bar').callFn([]), statements: [], }); - expect(generate(def)) - .toEqual('function () { const _c0 = ["CONST"]; return core.foo.bar(); }()'); + expect(generate(def)).toEqual( + 'function () { const _c0 = ["CONST"]; return core.foo.bar(); }()', + ); }); it('should not emit an IIFE if there are no shared constants', () => { const factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false); const translator = new Translator(factory); const ngImport = factory.createIdentifier('core'); - const emitScope = - new LocalEmitScope(ngImport, translator, factory); + const emitScope = new LocalEmitScope( + ngImport, + translator, + factory, + ); const def = emitScope.translateDefinition({ expression: o.fn([], [], null, null, 'foo'), @@ -69,8 +80,11 @@ describe('LocalEmitScope', () => { const factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false); const translator = new Translator(factory); const ngImport = factory.createIdentifier('core'); - const emitScope = - new LocalEmitScope(ngImport, translator, factory); + const emitScope = new LocalEmitScope( + ngImport, + translator, + factory, + ); expect(() => emitScope.getConstantStatements()).toThrowError(); }); }); 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 97c349482b7c0..812ba1adead14 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 @@ -23,7 +23,7 @@ import {generate} from './helpers'; describe('FileLinker', () => { let factory: TypeScriptAstFactory; - beforeEach(() => factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false)); + beforeEach(() => (factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false))); describe('isPartialDeclaration()', () => { it('should return true if the callee is recognized', () => { @@ -48,10 +48,9 @@ describe('FileLinker', () => { {propertyName: 'version', quoted: false, value: version}, {propertyName: 'ngImport', quoted: false, value: ngImport}, ]); - expect( - () => fileLinker.linkPartialDeclaration( - 'foo', [declarationArg], new MockDeclarationScope())) - .toThrowError('Unknown partial declaration function foo.'); + expect(() => + fileLinker.linkPartialDeclaration('foo', [declarationArg], new MockDeclarationScope()), + ).toThrowError('Unknown partial declaration function foo.'); }); it('should throw an error if the metadata object does not have a `minVersion` property', () => { @@ -62,10 +61,13 @@ describe('FileLinker', () => { {propertyName: 'version', quoted: false, value: version}, {propertyName: 'ngImport', quoted: false, value: ngImport}, ]); - expect( - () => fileLinker.linkPartialDeclaration( - 'ɵɵngDeclareDirective', [declarationArg], new MockDeclarationScope())) - .toThrowError(`Expected property 'minVersion' to be present.`); + expect(() => + fileLinker.linkPartialDeclaration( + 'ɵɵngDeclareDirective', + [declarationArg], + new MockDeclarationScope(), + ), + ).toThrowError(`Expected property 'minVersion' to be present.`); }); it('should throw an error if the metadata object does not have a `version` property', () => { @@ -76,10 +78,13 @@ describe('FileLinker', () => { {propertyName: 'minVersion', quoted: false, value: version}, {propertyName: 'ngImport', quoted: false, value: ngImport}, ]); - expect( - () => fileLinker.linkPartialDeclaration( - 'ɵɵngDeclareDirective', [declarationArg], new MockDeclarationScope())) - .toThrowError(`Expected property 'version' to be present.`); + expect(() => + fileLinker.linkPartialDeclaration( + 'ɵɵngDeclareDirective', + [declarationArg], + new MockDeclarationScope(), + ), + ).toThrowError(`Expected property 'version' to be present.`); }); it('should throw an error if the metadata object does not have a `ngImport` property', () => { @@ -89,19 +94,24 @@ describe('FileLinker', () => { {propertyName: 'minVersion', quoted: false, value: version}, {propertyName: 'version', quoted: false, value: version}, ]); - expect( - () => fileLinker.linkPartialDeclaration( - 'ɵɵngDeclareDirective', [declarationArg], new MockDeclarationScope())) - .toThrowError(`Expected property 'ngImport' to be present.`); + expect(() => + fileLinker.linkPartialDeclaration( + 'ɵɵngDeclareDirective', + [declarationArg], + new MockDeclarationScope(), + ), + ).toThrowError(`Expected property 'ngImport' to be present.`); }); it('should call `linkPartialDeclaration()` on the appropriate partial compiler', () => { const {fileLinker} = createFileLinker(); - const compileSpy = spyOn(PartialDirectiveLinkerVersion1.prototype, 'linkPartialDeclaration') - .and.returnValue({ - expression: o.literal('compilation result'), - statements: [], - }); + const compileSpy = spyOn( + PartialDirectiveLinkerVersion1.prototype, + 'linkPartialDeclaration', + ).and.returnValue({ + expression: o.literal('compilation result'), + statements: [], + }); const ngImport = factory.createIdentifier('core'); const version = factory.createLiteral('0.0.0-PLACEHOLDER'); @@ -112,7 +122,10 @@ describe('FileLinker', () => { ]); const compilationResult = fileLinker.linkPartialDeclaration( - 'ɵɵngDeclareDirective', [declarationArg], new MockDeclarationScope()); + 'ɵɵngDeclareDirective', + [declarationArg], + new MockDeclarationScope(), + ); expect(compilationResult).toEqual(factory.createLiteral('compilation result')); expect(compileSpy).toHaveBeenCalled(); @@ -139,10 +152,13 @@ describe('FileLinker', () => { // the template string to have offsets which synthetic nodes do not. const {fileLinker} = createFileLinker(source); const sourceFile = ts.createSourceFile('', source, ts.ScriptTarget.Latest, true); - const call = - (sourceFile.statements[0] as ts.ExpressionStatement).expression as ts.CallExpression; + const call = (sourceFile.statements[0] as ts.ExpressionStatement) + .expression as ts.CallExpression; const result = fileLinker.linkPartialDeclaration( - 'ɵɵngDeclareComponent', [call.arguments[0]], new MockDeclarationScope()); + 'ɵɵngDeclareComponent', + [call.arguments[0]], + new MockDeclarationScope(), + ); return ts.createPrinter().printNode(ts.EmitHint.Unspecified, result, sourceFile); } @@ -157,8 +173,9 @@ describe('FileLinker', () => { }); it('should not enable block syntax if compiled with a version older than 17', () => { - expect(linkComponentWithTemplate('16.2.0', '@Input() is a decorator. This is a brace }')) - .toContain('@Input() is a decorator. This is a brace }'); + expect( + linkComponentWithTemplate('16.2.0', '@Input() is a decorator. This is a brace }'), + ).toContain('@Input() is a decorator. This is a brace }'); }); }); @@ -174,13 +191,16 @@ describe('FileLinker', () => { { propertyName: 'minVersion', quoted: false, - value: factory.createLiteral('0.0.0-PLACEHOLDER') + value: factory.createLiteral('0.0.0-PLACEHOLDER'), }, {propertyName: 'version', quoted: false, value: factory.createLiteral('0.0.0-PLACEHOLDER')}, ]); const replacement = fileLinker.linkPartialDeclaration( - 'ɵɵngDeclareDirective', [declarationArg], new MockDeclarationScope()); + 'ɵɵngDeclareDirective', + [declarationArg], + new MockDeclarationScope(), + ); expect(generate(replacement)).toEqual('"REPLACEMENT"'); const results = fileLinker.getConstantStatements(); @@ -190,53 +210,62 @@ describe('FileLinker', () => { expect(statements.map(generate)).toEqual(['const _c0 = [1];']); }); - it('should be no shared constant statements to capture when they are emitted into the replacement IIFE', - () => { - const {fileLinker} = createFileLinker(); - spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT')); + it('should be no shared constant statements to capture when they are emitted into the replacement IIFE', () => { + const {fileLinker} = createFileLinker(); + spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT')); - // Here we use a string literal `"not-a-module"` for `ngImport` to cause constant - // statements to be emitted in an IIFE rather than added to the shared constant scope. - const declarationArg = factory.createObjectLiteral([ - {propertyName: 'ngImport', quoted: false, value: factory.createLiteral('not-a-module')}, - { - propertyName: 'minVersion', - quoted: false, - value: factory.createLiteral('0.0.0-PLACEHOLDER') - }, - { - propertyName: 'version', - quoted: false, - value: factory.createLiteral('0.0.0-PLACEHOLDER') - }, - ]); + // Here we use a string literal `"not-a-module"` for `ngImport` to cause constant + // statements to be emitted in an IIFE rather than added to the shared constant scope. + const declarationArg = factory.createObjectLiteral([ + {propertyName: 'ngImport', quoted: false, value: factory.createLiteral('not-a-module')}, + { + propertyName: 'minVersion', + quoted: false, + value: factory.createLiteral('0.0.0-PLACEHOLDER'), + }, + { + propertyName: 'version', + quoted: false, + value: factory.createLiteral('0.0.0-PLACEHOLDER'), + }, + ]); - const replacement = fileLinker.linkPartialDeclaration( - 'ɵɵngDeclareDirective', [declarationArg], new MockDeclarationScope()); - expect(generate(replacement)) - .toEqual('function () { const _c0 = [1]; return "REPLACEMENT"; }()'); + const replacement = fileLinker.linkPartialDeclaration( + 'ɵɵngDeclareDirective', + [declarationArg], + new MockDeclarationScope(), + ); + expect(generate(replacement)).toEqual( + 'function () { const _c0 = [1]; return "REPLACEMENT"; }()', + ); - const results = fileLinker.getConstantStatements(); - expect(results.length).toEqual(0); - }); + const results = fileLinker.getConstantStatements(); + expect(results.length).toEqual(0); + }); }); function createFileLinker(code = '// test code'): { - host: AstHost, - fileLinker: FileLinker, + host: AstHost; + fileLinker: FileLinker; } { const fs = new MockFileSystemNative(); const logger = new MockLogger(); const linkerEnvironment = LinkerEnvironment.create( - fs, logger, new TypeScriptAstHost(), - new TypeScriptAstFactory(/* annotateForClosureCompiler */ false), DEFAULT_LINKER_OPTIONS); + fs, + logger, + new TypeScriptAstHost(), + new TypeScriptAstFactory(/* annotateForClosureCompiler */ false), + DEFAULT_LINKER_OPTIONS, + ); const fileLinker = new FileLinker( - linkerEnvironment, fs.resolve('/test.js'), code); + linkerEnvironment, + fs.resolve('/test.js'), + code, + ); return {host: linkerEnvironment.host, fileLinker}; } }); - /** * This mock implementation of `DeclarationScope` will return a singleton instance of * `MockConstantScopeRef` if the expression is an identifier, or `null` otherwise. @@ -244,7 +273,7 @@ describe('FileLinker', () => { * This way we can simulate whether the constants will be shared or inlined into an IIFE. */ class MockDeclarationScope implements DeclarationScope { - getConstantScopeRef(expression: ts.Expression): MockConstantScopeRef|null { + getConstantScopeRef(expression: ts.Expression): MockConstantScopeRef | null { if (ts.isIdentifier(expression)) { return MockConstantScopeRef.singleton; } else { @@ -264,15 +293,16 @@ class MockConstantScopeRef { */ function spyOnLinkPartialDeclarationWithConstants(replacement: o.Expression) { let callCount = 0; - spyOn(PartialDirectiveLinkerVersion1.prototype, 'linkPartialDeclaration') - .and.callFake((constantPool => { - const constArray = o.literalArr([o.literal(++callCount)]); - // We have to add the constant twice or it will not create a shared statement - constantPool.getConstLiteral(constArray); - constantPool.getConstLiteral(constArray); - return { - expression: replacement, - statements: [], - }; - }) as typeof PartialDirectiveLinkerVersion1.prototype.linkPartialDeclaration); + spyOn(PartialDirectiveLinkerVersion1.prototype, 'linkPartialDeclaration').and.callFake((( + constantPool, + ) => { + const constArray = o.literalArr([o.literal(++callCount)]); + // We have to add the constant twice or it will not create a shared statement + constantPool.getConstLiteral(constArray); + constantPool.getConstLiteral(constArray); + return { + expression: replacement, + statements: [], + }; + }) as typeof PartialDirectiveLinkerVersion1.prototype.linkPartialDeclaration); } diff --git a/packages/compiler-cli/linker/test/file_linker/needs_linking_spec.ts b/packages/compiler-cli/linker/test/file_linker/needs_linking_spec.ts index b83842939e7f0..b34e4b874ea21 100644 --- a/packages/compiler-cli/linker/test/file_linker/needs_linking_spec.ts +++ b/packages/compiler-cli/linker/test/file_linker/needs_linking_spec.ts @@ -10,58 +10,98 @@ import {needsLinking} from '../../src/file_linker/needs_linking'; describe('needsLinking', () => { it('should return true for directive declarations', () => { - expect(needsLinking('file.js', ` + expect( + needsLinking( + 'file.js', + ` export class Dir { ɵdir = ɵɵngDeclareDirective({type: Dir}); } - `)).toBeTrue(); + `, + ), + ).toBeTrue(); }); it('should return true for namespaced directive declarations', () => { - expect(needsLinking('file.js', ` + expect( + needsLinking( + 'file.js', + ` export class Dir { ɵdir = ng.ɵɵngDeclareDirective({type: Dir}); } - `)).toBeTrue(); + `, + ), + ).toBeTrue(); }); it('should return true for unrelated usages of ɵɵngDeclareDirective', () => { - expect(needsLinking('file.js', ` + expect( + needsLinking( + 'file.js', + ` const fnName = 'ɵɵngDeclareDirective'; - `)).toBeTrue(); + `, + ), + ).toBeTrue(); }); it('should return false when the file does not contain ɵɵngDeclareDirective', () => { - expect(needsLinking('file.js', ` + expect( + needsLinking( + 'file.js', + ` const foo = ngDeclareDirective; - `)).toBeFalse(); + `, + ), + ).toBeFalse(); }); it('should return true for component declarations', () => { - expect(needsLinking('file.js', ` + expect( + needsLinking( + 'file.js', + ` export class Cmp { ɵdir = ɵɵngDeclareComponent({type: Cmp}); } - `)).toBeTrue(); + `, + ), + ).toBeTrue(); }); it('should return true for namespaced component declarations', () => { - expect(needsLinking('file.js', ` + expect( + needsLinking( + 'file.js', + ` export class Cmp { ɵdir = ng.ɵɵngDeclareComponent({type: Cmp}); } - `)).toBeTrue(); + `, + ), + ).toBeTrue(); }); it('should return true for unrelated usages of ɵɵngDeclareComponent', () => { - expect(needsLinking('file.js', ` + expect( + needsLinking( + 'file.js', + ` const fnName = 'ɵɵngDeclareComponent'; - `)).toBeTrue(); + `, + ), + ).toBeTrue(); }); it('should return false when the file does not contain ɵɵngDeclareComponent', () => { - expect(needsLinking('file.js', ` + expect( + needsLinking( + 'file.js', + ` const foo = ngDeclareComponent; - `)).toBeFalse(); + `, + ), + ).toBeFalse(); }); }); diff --git a/packages/compiler-cli/linker/test/file_linker/partial_linkers/partial_linker_selector_spec.ts b/packages/compiler-cli/linker/test/file_linker/partial_linkers/partial_linker_selector_spec.ts index 189715c0c99e2..5c95cc220c71f 100644 --- a/packages/compiler-cli/linker/test/file_linker/partial_linkers/partial_linker_selector_spec.ts +++ b/packages/compiler-cli/linker/test/file_linker/partial_linkers/partial_linker_selector_spec.ts @@ -9,7 +9,10 @@ import semver from 'semver'; import {MockLogger} from '../../../../src/ngtsc/logging/testing'; import {PartialLinker} from '../../../src/file_linker/partial_linkers/partial_linker'; -import {LinkerRange, PartialLinkerSelector} from '../../../src/file_linker/partial_linkers/partial_linker_selector'; +import { + LinkerRange, + PartialLinkerSelector, +} from '../../../src/file_linker/partial_linkers/partial_linker_selector'; describe('PartialLinkerSelector', () => { let logger: MockLogger; @@ -23,12 +26,11 @@ describe('PartialLinkerSelector', () => { }); describe('supportsDeclaration()', () => { - it('should return true if there is at least one linker that matches the given function name', - () => { - const selector = createSelector('error'); - expect(selector.supportsDeclaration('declareA')).toBe(true); - expect(selector.supportsDeclaration('invalid')).toBe(false); - }); + it('should return true if there is at least one linker that matches the given function name', () => { + const selector = createSelector('error'); + expect(selector.supportsDeclaration('declareA')).toBe(true); + expect(selector.supportsDeclaration('invalid')).toBe(false); + }); it('should return false for methods on `Object`', () => { const selector = createSelector('error'); @@ -54,20 +56,21 @@ describe('PartialLinkerSelector', () => { expect(selector.getLinker('declareA', '12.0.1-next.7', '12.0.1-next.7')).toBe(linkerA2); }); - it('should return the most recent linker if `version` is `0.0.0-PLACEHOLDER`, regardless of `minVersion`', - () => { - const selector = createSelector('error'); - expect(selector.getLinker('declareA', '11.1.2', '0.0.0-PLACEHOLDER')).toBe(linkerA2); - expect(selector.getLinker('declareA', '0.0.0-PLACEHOLDER', '11.1.2')).toBe(linkerA); - expect(selector.getLinker('declareA', '0.0.0-PLACEHOLDER', '0.0.0-PLACEHOLDER')) - .toBe(linkerA2); - }); + it('should return the most recent linker if `version` is `0.0.0-PLACEHOLDER`, regardless of `minVersion`', () => { + const selector = createSelector('error'); + expect(selector.getLinker('declareA', '11.1.2', '0.0.0-PLACEHOLDER')).toBe(linkerA2); + expect(selector.getLinker('declareA', '0.0.0-PLACEHOLDER', '11.1.2')).toBe(linkerA); + expect(selector.getLinker('declareA', '0.0.0-PLACEHOLDER', '0.0.0-PLACEHOLDER')).toBe( + linkerA2, + ); + }); it('should throw an error if there is no linker that matches the given name', () => { const selector = createSelector('error'); // `$foo` is not a valid name, even though `11.1.2` is a valid version for other declarations - expect(() => selector.getLinker('$foo', '11.1.2', '11.2.0')) - .toThrowError('Unknown partial declaration function $foo.'); + expect(() => selector.getLinker('$foo', '11.1.2', '11.2.0')).toThrowError( + 'Unknown partial declaration function $foo.', + ); }); describe('[unknown declaration version]', () => { @@ -84,10 +87,12 @@ describe('PartialLinkerSelector', () => { const selector = createSelector('warn'); expect(selector.getLinker('declareA', '13.1.0', '14.0.5')).toBe(linkerA2); expect(logger.logs.warn).toEqual([ - [`This application depends upon a library published using Angular version 14.0.5, ` + - `which requires Angular version 13.1.0 or newer to work correctly.\n` + - `Consider upgrading your application to use a more recent version of Angular.\n` + - 'Attempting to continue using this version of Angular.'] + [ + `This application depends upon a library published using Angular version 14.0.5, ` + + `which requires Angular version 13.1.0 or newer to work correctly.\n` + + `Consider upgrading your application to use a more recent version of Angular.\n` + + 'Attempting to continue using this version of Angular.', + ], ]); }); }); @@ -95,11 +100,11 @@ describe('PartialLinkerSelector', () => { describe('[unknownDeclarationVersionHandling is "error"]', () => { it('should throw an error', () => { const selector = createSelector('error'); - expect(() => selector.getLinker('declareA', '13.1.0', '14.0.5')) - .toThrowError( - `This application depends upon a library published using Angular version 14.0.5, ` + - `which requires Angular version 13.1.0 or newer to work correctly.\n` + - `Consider upgrading your application to use a more recent version of Angular.`); + expect(() => selector.getLinker('declareA', '13.1.0', '14.0.5')).toThrowError( + `This application depends upon a library published using Angular version 14.0.5, ` + + `which requires Angular version 13.1.0 or newer to work correctly.\n` + + `Consider upgrading your application to use a more recent version of Angular.`, + ); }); }); }); @@ -108,11 +113,11 @@ describe('PartialLinkerSelector', () => { /** * Create a selector for testing */ - function createSelector(unknownDeclarationVersionHandling: 'error'|'warn'|'ignore') { + function createSelector(unknownDeclarationVersionHandling: 'error' | 'warn' | 'ignore') { const linkerMap = new Map[]>(); linkerMap.set('declareA', [ {range: new semver.Range('<=12.0.0'), linker: linkerA}, - {range: new semver.Range('<=13.0.0'), linker: linkerA2} + {range: new semver.Range('<=13.0.0'), linker: linkerA2}, ]); linkerMap.set('declareB', [ {range: new semver.Range('<=12.0.0'), linker: linkerB}, 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 ac81dd16fee5e..7c56c0f52e552 100644 --- a/packages/compiler-cli/linker/test/linker_import_generator_spec.ts +++ b/packages/compiler-cli/linker/test/linker_import_generator_spec.ts @@ -16,41 +16,56 @@ describe('LinkerImportGenerator', () => { describe('generateNamespaceImport()', () => { it('should error if the import is not `@angular/core`', () => { const generator = new LinkerImportGenerator( - new TypeScriptAstFactory(false), ngImport); + new TypeScriptAstFactory(false), + ngImport, + ); - expect( - () => generator.addImport( - {exportModuleSpecifier: 'other/import', exportSymbolName: null, requestedFile: null})) - .toThrowError(`Unable to import from anything other than '@angular/core'`); + expect(() => + generator.addImport({ + exportModuleSpecifier: 'other/import', + exportSymbolName: null, + requestedFile: null, + }), + ).toThrowError(`Unable to import from anything other than '@angular/core'`); }); it('should return the ngImport expression for `@angular/core`', () => { const generator = new LinkerImportGenerator( - new TypeScriptAstFactory(false), ngImport); + new TypeScriptAstFactory(false), + ngImport, + ); - expect(generator.addImport({ - exportModuleSpecifier: '@angular/core', - exportSymbolName: null, - requestedFile: null - })).toBe(ngImport); + expect( + generator.addImport({ + exportModuleSpecifier: '@angular/core', + exportSymbolName: null, + requestedFile: null, + }), + ).toBe(ngImport); }); }); describe('generateNamedImport()', () => { it('should error if the import is not `@angular/core`', () => { const generator = new LinkerImportGenerator( - new TypeScriptAstFactory(false), ngImport); + new TypeScriptAstFactory(false), + ngImport, + ); - expect(() => generator.addImport({ - exportModuleSpecifier: 'other/import', - exportSymbolName: 'someSymbol', - requestedFile: null, - })).toThrowError(`Unable to import from anything other than '@angular/core'`); + expect(() => + generator.addImport({ + exportModuleSpecifier: 'other/import', + exportSymbolName: 'someSymbol', + requestedFile: null, + }), + ).toThrowError(`Unable to import from anything other than '@angular/core'`); }); it('should return a `NamedImport` object containing the ngImport expression', () => { const generator = new LinkerImportGenerator( - new TypeScriptAstFactory(false), ngImport); + new TypeScriptAstFactory(false), + ngImport, + ); const result = generator.addImport({ exportModuleSpecifier: '@angular/core', diff --git a/packages/compiler-cli/ngcc/index.ts b/packages/compiler-cli/ngcc/index.ts index a0f58c8e6122d..6e02d1d178e08 100644 --- a/packages/compiler-cli/ngcc/index.ts +++ b/packages/compiler-cli/ngcc/index.ts @@ -9,14 +9,21 @@ // https://github.com/chalk/chalk/blob/a370f468a43999e4397094ff5c3d17aadcc4860e/source/utilities.js#L21 function stringEncaseCRLFWithFirstIndex( - value: string, prefix: string, postfix: string, index: number): string { + value: string, + prefix: string, + postfix: string, + index: number, +): string { let endIndex = 0; let returnValue = ''; do { const gotCR = value[index - 1] === '\r'; - returnValue += value.substring(endIndex, gotCR ? index - 1 : index) + prefix + - (gotCR ? '\r\n' : '\n') + postfix; + returnValue += + value.substring(endIndex, gotCR ? index - 1 : index) + + prefix + + (gotCR ? '\r\n' : '\n') + + postfix; endIndex = index + 1; index = value.indexOf('\n', endIndex); } while (index !== -1); diff --git a/packages/compiler-cli/private/migrations.ts b/packages/compiler-cli/private/migrations.ts index 02147dc40d468..5a11053812e5e 100644 --- a/packages/compiler-cli/private/migrations.ts +++ b/packages/compiler-cli/private/migrations.ts @@ -13,6 +13,17 @@ export {forwardRefResolver} from '../src/ngtsc/annotations'; export {Reference} from '../src/ngtsc/imports'; -export {DynamicValue, PartialEvaluator, ResolvedValue, ResolvedValueMap, StaticInterpreter} from '../src/ngtsc/partial_evaluator'; +export { + DynamicValue, + PartialEvaluator, + ResolvedValue, + ResolvedValueMap, + StaticInterpreter, +} from '../src/ngtsc/partial_evaluator'; export {reflectObjectLiteral, TypeScriptReflectionHost} from '../src/ngtsc/reflection'; -export {PotentialImport, PotentialImportKind, PotentialImportMode, TemplateTypeChecker} from '../src/ngtsc/typecheck/api'; +export { + PotentialImport, + PotentialImportKind, + PotentialImportMode, + TemplateTypeChecker, +} from '../src/ngtsc/typecheck/api'; diff --git a/packages/compiler-cli/private/tooling.ts b/packages/compiler-cli/private/tooling.ts index 86000a9400dc5..cf689b57500cc 100644 --- a/packages/compiler-cli/private/tooling.ts +++ b/packages/compiler-cli/private/tooling.ts @@ -37,7 +37,9 @@ export const GLOBAL_DEFS_FOR_TERSER_WITH_AOT = { * NOTE: Signature is explicitly captured here to highlight the * contract various Angular CLI versions are relying on. */ -export const constructorParametersDownlevelTransform = - (program: ts.Program, isCore = false): ts.TransformerFactory => { - return angularJitApplicationTransform(program, isCore); - }; +export const constructorParametersDownlevelTransform = ( + program: ts.Program, + isCore = false, +): ts.TransformerFactory => { + return angularJitApplicationTransform(program, isCore); +}; diff --git a/packages/compiler-cli/src/bin/ngc.ts b/packages/compiler-cli/src/bin/ngc.ts index ba2e29c1af79d..258b6815df20f 100644 --- a/packages/compiler-cli/src/bin/ngc.ts +++ b/packages/compiler-cli/src/bin/ngc.ts @@ -22,7 +22,7 @@ async function runNgcComamnd() { process.exitCode = main(args, undefined, undefined, undefined, undefined, undefined); } -runNgcComamnd().catch(e => { +runNgcComamnd().catch((e) => { console.error(e); process.exitCode = 1; }); diff --git a/packages/compiler-cli/src/extract_i18n.ts b/packages/compiler-cli/src/extract_i18n.ts index 302a8131bf32e..47799ddb14b0e 100644 --- a/packages/compiler-cli/src/extract_i18n.ts +++ b/packages/compiler-cli/src/extract_i18n.ts @@ -17,7 +17,9 @@ import {ParsedConfiguration} from './perform_compile'; import * as api from './transformers/api'; export function mainXi18n( - args: string[], consoleError: (msg: string) => void = console.error): number { + args: string[], + consoleError: (msg: string) => void = console.error, +): number { const config = readXi18nCommandLineAndConfiguration(args); return main(args, consoleError, config, undefined, undefined, undefined); } @@ -25,10 +27,10 @@ export function mainXi18n( function readXi18nCommandLineAndConfiguration(args: string[]): ParsedConfiguration { const options: api.CompilerOptions = {}; const parsedArgs = yargs(args) - .option('i18nFormat', {type: 'string'}) - .option('locale', {type: 'string'}) - .option('outFile', {type: 'string'}) - .parseSync(); + .option('i18nFormat', {type: 'string'}) + .option('locale', {type: 'string'}) + .option('outFile', {type: 'string'}) + .parseSync(); if (parsedArgs.outFile) options.i18nOutFile = parsedArgs.outFile; if (parsedArgs.i18nFormat) options.i18nOutFormat = parsedArgs.i18nFormat; diff --git a/packages/compiler-cli/src/main.ts b/packages/compiler-cli/src/main.ts index 83294119ae284..17971977d4092 100644 --- a/packages/compiler-cli/src/main.ts +++ b/packages/compiler-cli/src/main.ts @@ -9,18 +9,34 @@ import ts from 'typescript'; import yargs from 'yargs'; -import {exitCodeFromResult, formatDiagnostics, ParsedConfiguration, performCompilation, readConfiguration} from './perform_compile'; +import { + exitCodeFromResult, + formatDiagnostics, + ParsedConfiguration, + performCompilation, + readConfiguration, +} from './perform_compile'; import {createPerformWatchHost, performWatchCompilation} from './perform_watch'; import * as api from './transformers/api'; export function main( - args: string[], consoleError: (s: string) => void = console.error, - config?: NgcParsedConfiguration, customTransformers?: api.CustomTransformers, programReuse?: { - program: api.Program|undefined, - }, - modifiedResourceFiles?: Set|null): number { - let {project, rootNames, options, errors: configErrors, watch, emitFlags} = - config || readNgcCommandLineAndConfiguration(args); + args: string[], + consoleError: (s: string) => void = console.error, + config?: NgcParsedConfiguration, + customTransformers?: api.CustomTransformers, + programReuse?: { + program: api.Program | undefined; + }, + modifiedResourceFiles?: Set | null, +): number { + let { + project, + rootNames, + options, + errors: configErrors, + watch, + emitFlags, + } = config || readNgcCommandLineAndConfiguration(args); if (configErrors.length) { return reportErrorsAndExit(configErrors, /*options*/ undefined, consoleError); } @@ -29,13 +45,19 @@ export function main( return reportErrorsAndExit(result.firstCompileResult, options, consoleError); } - let oldProgram: api.Program|undefined; + let oldProgram: api.Program | undefined; if (programReuse !== undefined) { oldProgram = programReuse.program; } - const {diagnostics: compileDiags, program} = performCompilation( - {rootNames, options, emitFlags, oldProgram, customTransformers, modifiedResourceFiles}); + const {diagnostics: compileDiags, program} = performCompilation({ + rootNames, + options, + emitFlags, + oldProgram, + customTransformers, + modifiedResourceFiles, + }); if (programReuse !== undefined) { programReuse.program = program; } @@ -43,13 +65,20 @@ export function main( } export function mainDiagnosticsForTest( - args: string[], config?: NgcParsedConfiguration, - programReuse?: {program: api.Program|undefined}, modifiedResourceFiles?: Set|null): { - exitCode: number, - diagnostics: ReadonlyArray, + args: string[], + config?: NgcParsedConfiguration, + programReuse?: {program: api.Program | undefined}, + modifiedResourceFiles?: Set | null, +): { + exitCode: number; + diagnostics: ReadonlyArray; } { - let {rootNames, options, errors: configErrors, emitFlags} = - config || readNgcCommandLineAndConfiguration(args); + let { + rootNames, + options, + errors: configErrors, + emitFlags, + } = config || readNgcCommandLineAndConfiguration(args); if (configErrors.length) { return { exitCode: exitCodeFromResult(configErrors), @@ -57,7 +86,7 @@ export function mainDiagnosticsForTest( }; } - let oldProgram: api.Program|undefined; + let oldProgram: api.Program | undefined; if (programReuse !== undefined) { oldProgram = programReuse.program; } @@ -86,38 +115,44 @@ export interface NgcParsedConfiguration extends ParsedConfiguration { export function readNgcCommandLineAndConfiguration(args: string[]): NgcParsedConfiguration { const options: api.CompilerOptions = {}; - const parsedArgs = - yargs(args) - .parserConfiguration({'strip-aliased': true}) - .option('i18nFile', {type: 'string'}) - .option('i18nFormat', {type: 'string'}) - .option('locale', {type: 'string'}) - .option('missingTranslation', {type: 'string', choices: ['error', 'warning', 'ignore']}) - .option('outFile', {type: 'string'}) - .option('watch', {type: 'boolean', alias: ['w']}) - .parseSync(); + const parsedArgs = yargs(args) + .parserConfiguration({'strip-aliased': true}) + .option('i18nFile', {type: 'string'}) + .option('i18nFormat', {type: 'string'}) + .option('locale', {type: 'string'}) + .option('missingTranslation', {type: 'string', choices: ['error', 'warning', 'ignore']}) + .option('outFile', {type: 'string'}) + .option('watch', {type: 'boolean', alias: ['w']}) + .parseSync(); if (parsedArgs.i18nFile) options.i18nInFile = parsedArgs.i18nFile; if (parsedArgs.i18nFormat) options.i18nInFormat = parsedArgs.i18nFormat; if (parsedArgs.locale) options.i18nInLocale = parsedArgs.locale; if (parsedArgs.missingTranslation) options.i18nInMissingTranslations = - parsedArgs.missingTranslation as api.CompilerOptions['i18nInMissingTranslations']; - - const config = readCommandLineAndConfiguration( - args, options, ['i18nFile', 'i18nFormat', 'locale', 'missingTranslation', 'watch']); + parsedArgs.missingTranslation as api.CompilerOptions['i18nInMissingTranslations']; + + const config = readCommandLineAndConfiguration(args, options, [ + 'i18nFile', + 'i18nFormat', + 'locale', + 'missingTranslation', + 'watch', + ]); return {...config, watch: parsedArgs.watch}; } export function readCommandLineAndConfiguration( - args: string[], existingOptions: api.CompilerOptions = {}, - ngCmdLineOptions: string[] = []): ParsedConfiguration { + args: string[], + existingOptions: api.CompilerOptions = {}, + ngCmdLineOptions: string[] = [], +): ParsedConfiguration { let cmdConfig = ts.parseCommandLine(args); const project = cmdConfig.options.project || '.'; - const cmdErrors = cmdConfig.errors.filter(e => { + const cmdErrors = cmdConfig.errors.filter((e) => { if (typeof e.messageText === 'string') { const msg = e.messageText; - return !ngCmdLineOptions.some(o => msg.indexOf(o) >= 0); + return !ngCmdLineOptions.some((o) => msg.indexOf(o) >= 0); } return true; }); @@ -127,7 +162,7 @@ export function readCommandLineAndConfiguration( rootNames: [], options: cmdConfig.options, errors: cmdErrors, - emitFlags: api.EmitFlags.Default + emitFlags: api.EmitFlags.Default, }; } const config = readConfiguration(project, cmdConfig.options); @@ -140,7 +175,7 @@ export function readCommandLineAndConfiguration( rootNames: config.rootNames, options, errors: config.errors, - emitFlags: config.emitFlags + emitFlags: config.emitFlags, }; } @@ -151,7 +186,7 @@ function getFormatDiagnosticsHost(options?: api.CompilerOptions): ts.FormatDiagn // We need to normalize the path separators here because by default, TypeScript // compiler hosts use posix canonical paths. In order to print consistent diagnostics, // we also normalize the paths. - getCanonicalFileName: fileName => fileName.replace(/\\/g, '/'), + getCanonicalFileName: (fileName) => fileName.replace(/\\/g, '/'), getNewLine: () => { // Manually determine the proper new line string based on the passed compiler // options. There is no public TypeScript function that returns the corresponding @@ -165,24 +200,39 @@ function getFormatDiagnosticsHost(options?: api.CompilerOptions): ts.FormatDiagn } function reportErrorsAndExit( - allDiagnostics: ReadonlyArray, options?: api.CompilerOptions, - consoleError: (s: string) => void = console.error): number { - const errorsAndWarnings = - allDiagnostics.filter(d => d.category !== ts.DiagnosticCategory.Message); + allDiagnostics: ReadonlyArray, + options?: api.CompilerOptions, + consoleError: (s: string) => void = console.error, +): number { + const errorsAndWarnings = allDiagnostics.filter( + (d) => d.category !== ts.DiagnosticCategory.Message, + ); printDiagnostics(errorsAndWarnings, options, consoleError); return exitCodeFromResult(allDiagnostics); } export function watchMode( - project: string, options: api.CompilerOptions, consoleError: (s: string) => void) { - return performWatchCompilation(createPerformWatchHost(project, diagnostics => { - printDiagnostics(diagnostics, options, consoleError); - }, options, undefined)); + project: string, + options: api.CompilerOptions, + consoleError: (s: string) => void, +) { + return performWatchCompilation( + createPerformWatchHost( + project, + (diagnostics) => { + printDiagnostics(diagnostics, options, consoleError); + }, + options, + undefined, + ), + ); } function printDiagnostics( - diagnostics: ReadonlyArray, options: api.CompilerOptions|undefined, - consoleError: (s: string) => void): void { + diagnostics: ReadonlyArray, + options: api.CompilerOptions | undefined, + consoleError: (s: string) => void, +): void { if (diagnostics.length === 0) { return; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/common/src/api.ts b/packages/compiler-cli/src/ngtsc/annotations/common/src/api.ts index 2ff84162eab64..5895c9f061f63 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/common/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/common/src/api.ts @@ -47,7 +47,7 @@ export interface ResourceLoader { * if the file has already been loaded. * @throws An Error if pre-loading is not available. */ - preload(resolvedUrl: string, context: ResourceLoaderContext): Promise|undefined; + preload(resolvedUrl: string, context: ResourceLoaderContext): Promise | undefined; /** * Preprocess the content data of an inline resource, asynchronously. @@ -81,7 +81,7 @@ export interface ResourceLoaderContext { * * Resources referenced via a component's `template` or `templateUrl` properties are of type * `template`. */ - type: 'style'|'template'; + type: 'style' | 'template'; /** * The absolute path to the file that contains the resource or reference to the resource. diff --git a/packages/compiler-cli/src/ngtsc/annotations/common/src/debug_info.ts b/packages/compiler-cli/src/ngtsc/annotations/common/src/debug_info.ts index 7c1bd916514e2..1f4bbc91d7840 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/common/src/debug_info.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/common/src/debug_info.ts @@ -12,8 +12,11 @@ import * as path from 'path'; import {DeclarationNode, ReflectionHost} from '../../../reflection'; export function extractClassDebugInfo( - clazz: DeclarationNode, reflection: ReflectionHost, rootDirs: ReadonlyArray, - forbidOrphanRendering: boolean): R3ClassDebugInfo|null { + clazz: DeclarationNode, + reflection: ReflectionHost, + rootDirs: ReadonlyArray, + forbidOrphanRendering: boolean, +): R3ClassDebugInfo | null { if (!reflection.isClass(clazz)) { return null; } @@ -34,8 +37,10 @@ export function extractClassDebugInfo( * Computes a source file path relative to the project root folder if possible, otherwise returns * null. */ -function computeRelativePathIfPossible(filePath: string, rootDirs: ReadonlyArray): string| - null { +function computeRelativePathIfPossible( + filePath: string, + rootDirs: ReadonlyArray, +): string | null { for (const rootDir of rootDirs) { const rel = path.relative(rootDir, filePath); if (!rel.startsWith('..')) { diff --git a/packages/compiler-cli/src/ngtsc/annotations/common/src/di.ts b/packages/compiler-cli/src/ngtsc/annotations/common/src/di.ts index a4f7f1bb3686c..85e19e4b2ca94 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/common/src/di.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/common/src/di.ts @@ -10,17 +10,25 @@ import {Expression, LiteralExpr, R3DependencyMetadata, WrappedNodeExpr} from '@a import ts from 'typescript'; import {ErrorCode, FatalDiagnosticError, makeRelatedInformation} from '../../../diagnostics'; -import {ClassDeclaration, CtorParameter, ReflectionHost, TypeValueReferenceKind, UnavailableValue, ValueUnavailableKind,} from '../../../reflection'; +import { + ClassDeclaration, + CtorParameter, + ReflectionHost, + TypeValueReferenceKind, + UnavailableValue, + ValueUnavailableKind, +} from '../../../reflection'; import {isAngularCore, valueReferenceToExpression} from './util'; - -export type ConstructorDeps = { - deps: R3DependencyMetadata[]; -}|{ - deps: null; - errors: ConstructorDepError[]; -}; +export type ConstructorDeps = + | { + deps: R3DependencyMetadata[]; + } + | { + deps: null; + errors: ConstructorDepError[]; + }; export interface ConstructorDepError { index: number; @@ -29,7 +37,10 @@ export interface ConstructorDepError { } export function getConstructorDependencies( - clazz: ClassDeclaration, reflector: ReflectionHost, isCore: boolean): ConstructorDeps|null { + clazz: ClassDeclaration, + reflector: ReflectionHost, + isCore: boolean, +): ConstructorDeps | null { const deps: R3DependencyMetadata[] = []; const errors: ConstructorDepError[] = []; let ctorParams = reflector.getConstructorParameters(clazz); @@ -43,50 +54,64 @@ export function getConstructorDependencies( ctorParams.forEach((param, idx) => { let token = valueReferenceToExpression(param.typeValueReference); - let attributeNameType: Expression|null = null; - let optional = false, self = false, skipSelf = false, host = false; + let attributeNameType: Expression | null = null; + let optional = false, + self = false, + skipSelf = false, + host = false; - (param.decorators || []).filter(dec => isCore || isAngularCore(dec)).forEach(dec => { - const name = isCore || dec.import === null ? dec.name : dec.import!.name; - if (name === 'Inject') { - if (dec.args === null || dec.args.length !== 1) { - throw new FatalDiagnosticError( - ErrorCode.DECORATOR_ARITY_WRONG, dec.node, - `Unexpected number of arguments to @Inject().`); - } - token = new WrappedNodeExpr(dec.args[0]); - } else if (name === 'Optional') { - optional = true; - } else if (name === 'SkipSelf') { - skipSelf = true; - } else if (name === 'Self') { - self = true; - } else if (name === 'Host') { - host = true; - } else if (name === 'Attribute') { - if (dec.args === null || dec.args.length !== 1) { - throw new FatalDiagnosticError( - ErrorCode.DECORATOR_ARITY_WRONG, dec.node, - `Unexpected number of arguments to @Attribute().`); - } - const attributeName = dec.args[0]; - token = new WrappedNodeExpr(attributeName); - if (ts.isStringLiteralLike(attributeName)) { - attributeNameType = new LiteralExpr(attributeName.text); + (param.decorators || []) + .filter((dec) => isCore || isAngularCore(dec)) + .forEach((dec) => { + const name = isCore || dec.import === null ? dec.name : dec.import!.name; + if (name === 'Inject') { + if (dec.args === null || dec.args.length !== 1) { + throw new FatalDiagnosticError( + ErrorCode.DECORATOR_ARITY_WRONG, + dec.node, + `Unexpected number of arguments to @Inject().`, + ); + } + token = new WrappedNodeExpr(dec.args[0]); + } else if (name === 'Optional') { + optional = true; + } else if (name === 'SkipSelf') { + skipSelf = true; + } else if (name === 'Self') { + self = true; + } else if (name === 'Host') { + host = true; + } else if (name === 'Attribute') { + if (dec.args === null || dec.args.length !== 1) { + throw new FatalDiagnosticError( + ErrorCode.DECORATOR_ARITY_WRONG, + dec.node, + `Unexpected number of arguments to @Attribute().`, + ); + } + const attributeName = dec.args[0]; + token = new WrappedNodeExpr(attributeName); + if (ts.isStringLiteralLike(attributeName)) { + attributeNameType = new LiteralExpr(attributeName.text); + } else { + attributeNameType = new WrappedNodeExpr( + ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), + ); + } } else { - attributeNameType = - new WrappedNodeExpr(ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)); + throw new FatalDiagnosticError( + ErrorCode.DECORATOR_UNEXPECTED, + dec.node, + `Unexpected decorator ${name} on parameter.`, + ); } - } else { - throw new FatalDiagnosticError( - ErrorCode.DECORATOR_UNEXPECTED, dec.node, `Unexpected decorator ${name} on parameter.`); - } - }); + }); if (token === null) { if (param.typeValueReference.kind !== TypeValueReferenceKind.UNAVAILABLE) { throw new Error( - 'Illegal state: expected value reference to be unavailable if no token is present'); + 'Illegal state: expected value reference to be unavailable if no token is present', + ); } errors.push({ index: idx, @@ -111,8 +136,9 @@ export function getConstructorDependencies( * * This is a companion function to `validateConstructorDependencies` which accepts invalid deps. */ -export function unwrapConstructorDependencies(deps: ConstructorDeps|null): R3DependencyMetadata[]| - 'invalid'|null { +export function unwrapConstructorDependencies( + deps: ConstructorDeps | null, +): R3DependencyMetadata[] | 'invalid' | null { if (deps === null) { return null; } else if (deps.deps !== null) { @@ -125,10 +151,14 @@ export function unwrapConstructorDependencies(deps: ConstructorDeps|null): R3Dep } export function getValidConstructorDependencies( - clazz: ClassDeclaration, reflector: ReflectionHost, isCore: boolean): R3DependencyMetadata[]| - null { + clazz: ClassDeclaration, + reflector: ReflectionHost, + isCore: boolean, +): R3DependencyMetadata[] | null { return validateConstructorDependencies( - clazz, getConstructorDependencies(clazz, reflector, isCore)); + clazz, + getConstructorDependencies(clazz, reflector, isCore), + ); } /** @@ -139,7 +169,9 @@ export function getValidConstructorDependencies( * deps. */ export function validateConstructorDependencies( - clazz: ClassDeclaration, deps: ConstructorDeps|null): R3DependencyMetadata[]|null { + clazz: ClassDeclaration, + deps: ConstructorDeps | null, +): R3DependencyMetadata[] | null { if (deps === null) { return null; } else if (deps.deps !== null) { @@ -157,10 +189,12 @@ export function validateConstructorDependencies( * @param error The reason why no valid injection token is available. */ function createUnsuitableInjectionTokenError( - clazz: ClassDeclaration, error: ConstructorDepError): FatalDiagnosticError { + clazz: ClassDeclaration, + error: ConstructorDepError, +): FatalDiagnosticError { const {param, index, reason} = error; - let chainMessage: string|undefined = undefined; - let hints: ts.DiagnosticRelatedInformation[]|undefined = undefined; + let chainMessage: string | undefined = undefined; + let hints: ts.DiagnosticRelatedInformation[] | undefined = undefined; switch (reason.kind) { case ValueUnavailableKind.UNSUPPORTED: chainMessage = 'Consider using the @Inject decorator to specify an injection token.'; @@ -172,8 +206,9 @@ function createUnsuitableInjectionTokenError( chainMessage = 'Consider using the @Inject decorator to specify an injection token.'; hints = [ makeRelatedInformation( - reason.typeNode, - 'This type does not have a value, so it cannot be used as injection token.'), + reason.typeNode, + 'This type does not have a value, so it cannot be used as injection token.', + ), ]; if (reason.decl !== null) { hints.push(makeRelatedInformation(reason.decl, 'The type is declared here.')); @@ -181,11 +216,12 @@ function createUnsuitableInjectionTokenError( break; case ValueUnavailableKind.TYPE_ONLY_IMPORT: chainMessage = - 'Consider changing the type-only import to a regular import, or use the @Inject decorator to specify an injection token.'; + 'Consider changing the type-only import to a regular import, or use the @Inject decorator to specify an injection token.'; hints = [ makeRelatedInformation( - reason.typeNode, - 'This type is imported using a type-only import, which prevents it from being usable as an injection token.'), + reason.typeNode, + 'This type is imported using a type-only import, which prevents it from being usable as an injection token.', + ), makeRelatedInformation(reason.node, 'The type-only import occurs here.'), ]; break; @@ -193,8 +229,9 @@ function createUnsuitableInjectionTokenError( chainMessage = 'Consider using the @Inject decorator to specify an injection token.'; hints = [ makeRelatedInformation( - reason.typeNode, - 'This type corresponds with a namespace, which cannot be used as injection token.'), + reason.typeNode, + 'This type corresponds with a namespace, which cannot be used as injection token.', + ), makeRelatedInformation(reason.importClause, 'The namespace import occurs here.'), ]; break; @@ -204,20 +241,23 @@ function createUnsuitableInjectionTokenError( break; case ValueUnavailableKind.MISSING_TYPE: chainMessage = - 'Consider adding a type to the parameter or use the @Inject decorator to specify an injection token.'; + 'Consider adding a type to the parameter or use the @Inject decorator to specify an injection token.'; break; } const chain: ts.DiagnosticMessageChain = { messageText: `No suitable injection token for parameter '${param.name || index}' of class '${ - clazz.name.text}'.`, + clazz.name.text + }'.`, category: ts.DiagnosticCategory.Error, code: 0, - next: [{ - messageText: chainMessage, - category: ts.DiagnosticCategory.Message, - code: 0, - }], + next: [ + { + messageText: chainMessage, + category: ts.DiagnosticCategory.Message, + code: 0, + }, + ], }; return new FatalDiagnosticError(ErrorCode.PARAM_MISSING_TOKEN, param.nameNode, chain, hints); diff --git a/packages/compiler-cli/src/ngtsc/annotations/common/src/diagnostics.ts b/packages/compiler-cli/src/ngtsc/annotations/common/src/diagnostics.ts index 0b64d9ddd099b..afe9db530b24f 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/common/src/diagnostics.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/common/src/diagnostics.ts @@ -8,18 +8,35 @@ import ts from 'typescript'; -import {ErrorCode, FatalDiagnosticError, makeDiagnostic, makeRelatedInformation} from '../../../diagnostics'; +import { + ErrorCode, + FatalDiagnosticError, + makeDiagnostic, + makeRelatedInformation, +} from '../../../diagnostics'; import {Reference} from '../../../imports'; -import {ClassPropertyName, DirectiveMeta, flattenInheritedDirectiveMetadata, HostDirectiveMeta, isHostDirectiveMetaForGlobalMode, MetadataReader} from '../../../metadata'; -import {describeResolvedType, DynamicValue, PartialEvaluator, ResolvedValue, traceDynamicValue} from '../../../partial_evaluator'; +import { + ClassPropertyName, + DirectiveMeta, + flattenInheritedDirectiveMetadata, + HostDirectiveMeta, + isHostDirectiveMetaForGlobalMode, + MetadataReader, +} from '../../../metadata'; +import { + describeResolvedType, + DynamicValue, + PartialEvaluator, + ResolvedValue, + traceDynamicValue, +} from '../../../partial_evaluator'; import {ClassDeclaration, ReflectionHost} from '../../../reflection'; import {DeclarationData, LocalModuleScopeRegistry} from '../../../scope'; import {identifierOfNode, isFromDtsFile} from '../../../util/src/typescript'; import {InjectableClassRegistry} from './injectable_registry'; import {isAbstractClassDeclaration, readBaseClass} from './util'; -import { CompilationMode } from '../../../transform'; - +import {CompilationMode} from '../../../transform'; /** * Create a `ts.Diagnostic` which indicates the given class is part of the declarations of two or @@ -29,7 +46,10 @@ import { CompilationMode } from '../../../transform'; * the directive/pipe exists in its `declarations` (if possible). */ export function makeDuplicateDeclarationError( - node: ClassDeclaration, data: DeclarationData[], kind: string): ts.Diagnostic { + node: ClassDeclaration, + data: DeclarationData[], + kind: string, +): ts.Diagnostic { const context: ts.DiagnosticRelatedInformation[] = []; for (const decl of data) { if (decl.rawDeclarations === null) { @@ -38,19 +58,23 @@ export function makeDuplicateDeclarationError( // Try to find the reference to the declaration within the declarations array, to hang the // error there. If it can't be found, fall back on using the NgModule's name. const contextNode = decl.ref.getOriginForDiagnostics(decl.rawDeclarations, decl.ngModule.name); - context.push(makeRelatedInformation( + context.push( + makeRelatedInformation( contextNode, - `'${node.name.text}' is listed in the declarations of the NgModule '${ - decl.ngModule.name.text}'.`)); + `'${node.name.text}' is listed in the declarations of the NgModule '${decl.ngModule.name.text}'.`, + ), + ); } // Finally, produce the diagnostic. return makeDiagnostic( - ErrorCode.NGMODULE_DECLARATION_NOT_UNIQUE, node.name, - `The ${kind} '${node.name.text}' is declared by more than one NgModule.`, context); + ErrorCode.NGMODULE_DECLARATION_NOT_UNIQUE, + node.name, + `The ${kind} '${node.name.text}' is declared by more than one NgModule.`, + context, + ); } - /** * Creates a `FatalDiagnosticError` for a node that did not evaluate to the expected type. The * diagnostic that is created will include details on why the value is incorrect, i.e. it includes @@ -62,9 +86,12 @@ export function makeDuplicateDeclarationError( * @param messageText The message text of the error. */ export function createValueHasWrongTypeError( - node: ts.Node, value: ResolvedValue, messageText: string): FatalDiagnosticError { + node: ts.Node, + value: ResolvedValue, + messageText: string, +): FatalDiagnosticError { let chainedMessage: string; - let relatedInformation: ts.DiagnosticRelatedInformation[]|undefined; + let relatedInformation: ts.DiagnosticRelatedInformation[] | undefined; if (value instanceof DynamicValue) { chainedMessage = 'Value could not be determined statically.'; relatedInformation = traceDynamicValue(node, value); @@ -82,11 +109,13 @@ export function createValueHasWrongTypeError( messageText, category: ts.DiagnosticCategory.Error, code: 0, - next: [{ - messageText: chainedMessage, - category: ts.DiagnosticCategory.Message, - code: 0, - }] + next: [ + { + messageText: chainedMessage, + category: ts.DiagnosticCategory.Message, + code: 0, + }, + ], }; return new FatalDiagnosticError(ErrorCode.VALUE_HAS_WRONG_TYPE, node, chain, relatedInformation); @@ -99,8 +128,10 @@ export function createValueHasWrongTypeError( * @param registry Registry that keeps track of the registered injectable classes. */ export function getProviderDiagnostics( - providerClasses: Set>, providersDeclaration: ts.Expression, - registry: InjectableClassRegistry): ts.Diagnostic[] { + providerClasses: Set>, + providersDeclaration: ts.Expression, + registry: InjectableClassRegistry, +): ts.Diagnostic[] { const diagnostics: ts.Diagnostic[] = []; for (const provider of providerClasses) { @@ -112,29 +143,34 @@ export function getProviderDiagnostics( } const contextNode = provider.getOriginForDiagnostics(providersDeclaration); - diagnostics.push(makeDiagnostic( - ErrorCode.UNDECORATED_PROVIDER, contextNode, - `The class '${ - provider.node.name - .text}' cannot be created via dependency injection, as it does not have an Angular decorator. This will result in an error at runtime. - -Either add the @Injectable() decorator to '${ - provider.node.name - .text}', or configure a different provider (such as a provider with 'useFactory'). + diagnostics.push( + makeDiagnostic( + ErrorCode.UNDECORATED_PROVIDER, + contextNode, + `The class '${provider.node.name.text}' cannot be created via dependency injection, as it does not have an Angular decorator. This will result in an error at runtime. + +Either add the @Injectable() decorator to '${provider.node.name.text}', or configure a different provider (such as a provider with 'useFactory'). `, - [makeRelatedInformation(provider.node, `'${provider.node.name.text}' is declared here.`)])); + [makeRelatedInformation(provider.node, `'${provider.node.name.text}' is declared here.`)], + ), + ); } return diagnostics; } export function getDirectiveDiagnostics( - node: ClassDeclaration, injectableRegistry: InjectableClassRegistry, - evaluator: PartialEvaluator, reflector: ReflectionHost, scopeRegistry: LocalModuleScopeRegistry, - strictInjectionParameters: boolean, kind: 'Directive'|'Component'): ts.Diagnostic[]|null { - let diagnostics: ts.Diagnostic[]|null = []; - - const addDiagnostics = (more: ts.Diagnostic|ts.Diagnostic[]|null) => { + node: ClassDeclaration, + injectableRegistry: InjectableClassRegistry, + evaluator: PartialEvaluator, + reflector: ReflectionHost, + scopeRegistry: LocalModuleScopeRegistry, + strictInjectionParameters: boolean, + kind: 'Directive' | 'Component', +): ts.Diagnostic[] | null { + let diagnostics: ts.Diagnostic[] | null = []; + + const addDiagnostics = (more: ts.Diagnostic | ts.Diagnostic[] | null) => { if (more === null) { return; } else if (diagnostics === null) { @@ -152,13 +188,24 @@ export function getDirectiveDiagnostics( addDiagnostics(makeDuplicateDeclarationError(node, duplicateDeclarations, kind)); } - addDiagnostics(checkInheritanceOfInjectable( - node, injectableRegistry, reflector, evaluator, strictInjectionParameters, kind)); + addDiagnostics( + checkInheritanceOfInjectable( + node, + injectableRegistry, + reflector, + evaluator, + strictInjectionParameters, + kind, + ), + ); return diagnostics; } export function validateHostDirectives( - origin: ts.Expression, hostDirectives: HostDirectiveMeta[], metaReader: MetadataReader) { + origin: ts.Expression, + hostDirectives: HostDirectiveMeta[], + metaReader: MetadataReader, +) { const diagnostics: ts.DiagnosticWithLocation[] = []; for (const current of hostDirectives) { @@ -169,34 +216,48 @@ export function validateHostDirectives( const hostMeta = flattenInheritedDirectiveMetadata(metaReader, current.directive); if (hostMeta === null) { - diagnostics.push(makeDiagnostic( - ErrorCode.HOST_DIRECTIVE_INVALID, current.directive.getOriginForDiagnostics(origin), - `${ - current.directive - .debugName} must be a standalone directive to be used as a host directive`)); + diagnostics.push( + makeDiagnostic( + ErrorCode.HOST_DIRECTIVE_INVALID, + current.directive.getOriginForDiagnostics(origin), + `${current.directive.debugName} must be a standalone directive to be used as a host directive`, + ), + ); continue; } if (!hostMeta.isStandalone) { - diagnostics.push(makeDiagnostic( + diagnostics.push( + makeDiagnostic( ErrorCode.HOST_DIRECTIVE_NOT_STANDALONE, current.directive.getOriginForDiagnostics(origin), - `Host directive ${hostMeta.name} must be standalone`)); + `Host directive ${hostMeta.name} must be standalone`, + ), + ); } if (hostMeta.isComponent) { - diagnostics.push(makeDiagnostic( - ErrorCode.HOST_DIRECTIVE_COMPONENT, current.directive.getOriginForDiagnostics(origin), - `Host directive ${hostMeta.name} cannot be a component`)); + diagnostics.push( + makeDiagnostic( + ErrorCode.HOST_DIRECTIVE_COMPONENT, + current.directive.getOriginForDiagnostics(origin), + `Host directive ${hostMeta.name} cannot be a component`, + ), + ); } const requiredInputNames = Array.from(hostMeta.inputs) - .filter(input => input.required) - .map(input => input.classPropertyName); + .filter((input) => input.required) + .map((input) => input.classPropertyName); validateHostDirectiveMappings( - 'input', current, hostMeta, origin, diagnostics, - requiredInputNames.length > 0 ? new Set(requiredInputNames) : null); + 'input', + current, + hostMeta, + origin, + diagnostics, + requiredInputNames.length > 0 ? new Set(requiredInputNames) : null, + ); validateHostDirectiveMappings('output', current, hostMeta, origin, diagnostics, null); } @@ -204,16 +265,20 @@ export function validateHostDirectives( } function validateHostDirectiveMappings( - bindingType: 'input'|'output', hostDirectiveMeta: HostDirectiveMeta, meta: DirectiveMeta, - origin: ts.Expression, diagnostics: ts.DiagnosticWithLocation[], - requiredBindings: Set|null) { + bindingType: 'input' | 'output', + hostDirectiveMeta: HostDirectiveMeta, + meta: DirectiveMeta, + origin: ts.Expression, + diagnostics: ts.DiagnosticWithLocation[], + requiredBindings: Set | null, +) { if (!isHostDirectiveMetaForGlobalMode(hostDirectiveMeta)) { throw new Error('Impossible state: diagnostics code path for local compilation'); } const className = meta.name; const hostDirectiveMappings = - bindingType === 'input' ? hostDirectiveMeta.inputs : hostDirectiveMeta.outputs; + bindingType === 'input' ? hostDirectiveMeta.inputs : hostDirectiveMeta.outputs; const existingBindings = bindingType === 'input' ? meta.inputs : meta.outputs; const exposedRequiredBindings = new Set(); @@ -222,11 +287,13 @@ function validateHostDirectiveMappings( const bindings = existingBindings.getByBindingPropertyName(publicName); if (bindings === null) { - diagnostics.push(makeDiagnostic( + diagnostics.push( + makeDiagnostic( ErrorCode.HOST_DIRECTIVE_UNDEFINED_BINDING, hostDirectiveMeta.directive.getOriginForDiagnostics(origin), - `Directive ${className} does not have an ${bindingType} with a public name of ${ - publicName}.`)); + `Directive ${className} does not have an ${bindingType} with a public name of ${publicName}.`, + ), + ); } else if (requiredBindings !== null) { for (const field of bindings) { if (requiredBindings.has(field.classPropertyName)) { @@ -241,12 +308,13 @@ function validateHostDirectiveMappings( if (bindingsForPublicName !== null) { for (const binding of bindingsForPublicName) { if (binding.bindingPropertyName !== publicName) { - diagnostics.push(makeDiagnostic( + diagnostics.push( + makeDiagnostic( ErrorCode.HOST_DIRECTIVE_CONFLICTING_ALIAS, hostDirectiveMeta.directive.getOriginForDiagnostics(origin), - `Cannot alias ${bindingType} ${publicName} of host directive ${className} to ${ - remappedPublicName}, because it already has a different ${ - bindingType} with the same public name.`)); + `Cannot alias ${bindingType} ${publicName} of host directive ${className} to ${remappedPublicName}, because it already has a different ${bindingType} with the same public name.`, + ), + ); } } } @@ -266,27 +334,37 @@ function validateHostDirectiveMappings( } } - diagnostics.push(makeDiagnostic( + diagnostics.push( + makeDiagnostic( ErrorCode.HOST_DIRECTIVE_MISSING_REQUIRED_BINDING, hostDirectiveMeta.directive.getOriginForDiagnostics(origin), - `Required ${bindingType}${missingBindings.length === 1 ? '' : 's'} ${ - missingBindings.join(', ')} from host directive ${className} must be exposed.`)); + `Required ${bindingType}${missingBindings.length === 1 ? '' : 's'} ${missingBindings.join( + ', ', + )} from host directive ${className} must be exposed.`, + ), + ); } } - -export function getUndecoratedClassWithAngularFeaturesDiagnostic(node: ClassDeclaration): - ts.Diagnostic { +export function getUndecoratedClassWithAngularFeaturesDiagnostic( + node: ClassDeclaration, +): ts.Diagnostic { return makeDiagnostic( - ErrorCode.UNDECORATED_CLASS_USING_ANGULAR_FEATURES, node.name, - `Class is using Angular features but is not decorated. Please add an explicit ` + - `Angular decorator.`); + ErrorCode.UNDECORATED_CLASS_USING_ANGULAR_FEATURES, + node.name, + `Class is using Angular features but is not decorated. Please add an explicit ` + + `Angular decorator.`, + ); } export function checkInheritanceOfInjectable( - node: ClassDeclaration, injectableRegistry: InjectableClassRegistry, reflector: ReflectionHost, - evaluator: PartialEvaluator, strictInjectionParameters: boolean, - kind: 'Directive'|'Component'|'Pipe'|'Injectable'): ts.Diagnostic|null { + node: ClassDeclaration, + injectableRegistry: InjectableClassRegistry, + reflector: ReflectionHost, + evaluator: PartialEvaluator, + strictInjectionParameters: boolean, + kind: 'Directive' | 'Component' | 'Pipe' | 'Injectable', +): ts.Diagnostic | null { const classWithCtor = findInheritedCtor(node, injectableRegistry, reflector, evaluator); if (classWithCtor === null || classWithCtor.isCtorValid) { // The class does not inherit a constructor, or the inherited constructor is compatible @@ -325,8 +403,11 @@ interface ClassWithCtor { } export function findInheritedCtor( - node: ClassDeclaration, injectableRegistry: InjectableClassRegistry, reflector: ReflectionHost, - evaluator: PartialEvaluator): ClassWithCtor|null { + node: ClassDeclaration, + injectableRegistry: InjectableClassRegistry, + reflector: ReflectionHost, + evaluator: PartialEvaluator, +): ClassWithCtor | null { if (!reflector.isClass(node) || reflector.getConstructorParameters(node) !== null) { // We should skip nodes that aren't classes. If a constructor exists, then no base class // definition is required on the runtime side - it's legal to inherit from any class. @@ -374,52 +455,65 @@ export function findInheritedCtor( } function getInheritedInvalidCtorDiagnostic( - node: ClassDeclaration, baseClass: Reference, - kind: 'Directive'|'Component'|'Pipe'|'Injectable') { + node: ClassDeclaration, + baseClass: Reference, + kind: 'Directive' | 'Component' | 'Pipe' | 'Injectable', +) { const baseClassName = baseClass.debugName; return makeDiagnostic( - ErrorCode.INJECTABLE_INHERITS_INVALID_CONSTRUCTOR, node.name, - `The ${kind.toLowerCase()} ${node.name.text} inherits its constructor from ${ - baseClassName}, ` + - `but the latter has a constructor parameter that is not compatible with dependency injection. ` + - `Either add an explicit constructor to ${node.name.text} or change ${ - baseClassName}'s constructor to ` + - `use parameters that are valid for DI.`); + ErrorCode.INJECTABLE_INHERITS_INVALID_CONSTRUCTOR, + node.name, + `The ${kind.toLowerCase()} ${node.name.text} inherits its constructor from ${baseClassName}, ` + + `but the latter has a constructor parameter that is not compatible with dependency injection. ` + + `Either add an explicit constructor to ${node.name.text} or change ${baseClassName}'s constructor to ` + + `use parameters that are valid for DI.`, + ); } function getInheritedUndecoratedCtorDiagnostic( - node: ClassDeclaration, baseClass: Reference, - kind: 'Directive'|'Component'|'Pipe'|'Injectable') { + node: ClassDeclaration, + baseClass: Reference, + kind: 'Directive' | 'Component' | 'Pipe' | 'Injectable', +) { const baseClassName = baseClass.debugName; const baseNeedsDecorator = - kind === 'Component' || kind === 'Directive' ? 'Directive' : 'Injectable'; + kind === 'Component' || kind === 'Directive' ? 'Directive' : 'Injectable'; return makeDiagnostic( - ErrorCode.DIRECTIVE_INHERITS_UNDECORATED_CTOR, node.name, - `The ${kind.toLowerCase()} ${node.name.text} inherits its constructor from ${ - baseClassName}, ` + - `but the latter does not have an Angular decorator of its own. Dependency injection will not be able to ` + - `resolve the parameters of ${baseClassName}'s constructor. Either add a @${ - baseNeedsDecorator} decorator ` + - `to ${baseClassName}, or add an explicit constructor to ${node.name.text}.`); + ErrorCode.DIRECTIVE_INHERITS_UNDECORATED_CTOR, + node.name, + `The ${kind.toLowerCase()} ${node.name.text} inherits its constructor from ${baseClassName}, ` + + `but the latter does not have an Angular decorator of its own. Dependency injection will not be able to ` + + `resolve the parameters of ${baseClassName}'s constructor. Either add a @${baseNeedsDecorator} decorator ` + + `to ${baseClassName}, or add an explicit constructor to ${node.name.text}.`, + ); } /** - * Throws `FatalDiagnosticError` with error code `LOCAL_COMPILATION_UNRESOLVED_CONST` + * Throws `FatalDiagnosticError` with error code `LOCAL_COMPILATION_UNRESOLVED_CONST` * if the compilation mode is local and the value is not resolved due to being imported * from external files. This is a common scenario for errors in local compilation mode, * and so this helper can be used to quickly generate the relevant errors. - * - * @param nodeToHighlight Node to be highlighted in teh error message. - * Will default to value.node if not provided. + * + * @param nodeToHighlight Node to be highlighted in teh error message. + * Will default to value.node if not provided. */ -export function assertLocalCompilationUnresolvedConst(compilationMode: CompilationMode, value: ResolvedValue, nodeToHighlight: ts.Node|null, errorMessage: string): void { - if (compilationMode === CompilationMode.LOCAL && value instanceof DynamicValue && - value.isFromUnknownIdentifier()) { +export function assertLocalCompilationUnresolvedConst( + compilationMode: CompilationMode, + value: ResolvedValue, + nodeToHighlight: ts.Node | null, + errorMessage: string, +): void { + if ( + compilationMode === CompilationMode.LOCAL && + value instanceof DynamicValue && + value.isFromUnknownIdentifier() + ) { throw new FatalDiagnosticError( - ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST, - nodeToHighlight ?? value.node, - errorMessage); + ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST, + nodeToHighlight ?? value.node, + errorMessage, + ); } } diff --git a/packages/compiler-cli/src/ngtsc/annotations/common/src/evaluation.ts b/packages/compiler-cli/src/ngtsc/annotations/common/src/evaluation.ts index 81978b08a84a9..2188f83f7ed03 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/common/src/evaluation.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/common/src/evaluation.ts @@ -17,11 +17,13 @@ import {ClassDeclaration, Decorator} from '../../../reflection'; import {createValueHasWrongTypeError} from './diagnostics'; import {isAngularCoreReference, unwrapExpression} from './util'; - export function resolveEnumValue( - evaluator: PartialEvaluator, metadata: Map, field: string, - enumSymbolName: string): number|null { - let resolved: number|null = null; + evaluator: PartialEvaluator, + metadata: Map, + field: string, + enumSymbolName: string, +): number | null { + let resolved: number | null = null; if (metadata.has(field)) { const expr = metadata.get(field)!; const value = evaluator.evaluate(expr) as any; @@ -29,7 +31,10 @@ export function resolveEnumValue( resolved = value.resolved as number; } else { throw createValueHasWrongTypeError( - expr, value, `${field} must be a member of ${enumSymbolName} enum from @angular/core`); + expr, + value, + `${field} must be a member of ${enumSymbolName} enum from @angular/core`, + ); } } return resolved; @@ -42,7 +47,7 @@ export function resolveEnumValue( * The static analysis is still needed in local compilation mode since the value of this enum will * be used later to decide the generated code for styles. */ -export function resolveEncapsulationEnumValueLocally(expr?: ts.Expression): number|null { +export function resolveEncapsulationEnumValueLocally(expr?: ts.Expression): number | null { if (!expr) { return null; } @@ -69,13 +74,16 @@ export function resolveEncapsulationEnumValueLocally(expr?: ts.Expression): numb /** Determines if the result of an evaluation is a string array. */ export function isStringArray(resolvedValue: ResolvedValue): resolvedValue is string[] { - return Array.isArray(resolvedValue) && resolvedValue.every(elem => typeof elem === 'string'); + return Array.isArray(resolvedValue) && resolvedValue.every((elem) => typeof elem === 'string'); } -export function isClassReferenceArray(resolvedValue: ResolvedValue): - resolvedValue is Reference[] { - return Array.isArray(resolvedValue) && - resolvedValue.every(elem => elem instanceof Reference && ts.isClassDeclaration(elem.node)); +export function isClassReferenceArray( + resolvedValue: ResolvedValue, +): resolvedValue is Reference[] { + return ( + Array.isArray(resolvedValue) && + resolvedValue.every((elem) => elem instanceof Reference && ts.isClassDeclaration(elem.node)) + ); } export function isArray(value: ResolvedValue): value is Array { @@ -83,21 +91,27 @@ export function isArray(value: ResolvedValue): value is Array { } export function resolveLiteral( - decorator: Decorator, - literalCache: Map): ts.ObjectLiteralExpression { + decorator: Decorator, + literalCache: Map, +): ts.ObjectLiteralExpression { if (literalCache.has(decorator)) { return literalCache.get(decorator)!; } if (decorator.args === null || decorator.args.length !== 1) { throw new FatalDiagnosticError( - ErrorCode.DECORATOR_ARITY_WRONG, decorator.node, - `Incorrect number of arguments to @${decorator.name} decorator`); + ErrorCode.DECORATOR_ARITY_WRONG, + decorator.node, + `Incorrect number of arguments to @${decorator.name} decorator`, + ); } const meta = unwrapExpression(decorator.args[0]); if (!ts.isObjectLiteralExpression(meta)) { throw new FatalDiagnosticError( - ErrorCode.DECORATOR_ARG_NOT_LITERAL, meta, `Decorator argument must be literal.`); + ErrorCode.DECORATOR_ARG_NOT_LITERAL, + meta, + `Decorator argument must be literal.`, + ); } literalCache.set(decorator, meta); diff --git a/packages/compiler-cli/src/ngtsc/annotations/common/src/factory.ts b/packages/compiler-cli/src/ngtsc/annotations/common/src/factory.ts index 8ecd6d320c590..05fa2d9d952fc 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/common/src/factory.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/common/src/factory.ts @@ -6,7 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import {compileDeclareFactoryFunction, compileFactoryFunction, R3FactoryMetadata} from '@angular/compiler'; +import { + compileDeclareFactoryFunction, + compileFactoryFunction, + R3FactoryMetadata, +} from '@angular/compiler'; import {CompileResult} from '../../../transform'; @@ -19,7 +23,7 @@ export function compileNgFactoryDefField(metadata: R3FactoryMetadata): CompileRe initializer: res.expression, statements: res.statements, type: res.type, - deferrableImports: null + deferrableImports: null, }; } @@ -30,6 +34,6 @@ export function compileDeclareFactory(metadata: R3FactoryMetadata): CompileResul initializer: res.expression, statements: res.statements, type: res.type, - deferrableImports: null + deferrableImports: null, }; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/common/src/injectable_registry.ts b/packages/compiler-cli/src/ngtsc/annotations/common/src/injectable_registry.ts index 0d1a94ab2aabb..392cb6f296e84 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/common/src/injectable_registry.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/common/src/injectable_registry.ts @@ -12,9 +12,8 @@ import {ClassDeclaration, ReflectionHost} from '../../../reflection'; import {getConstructorDependencies, unwrapConstructorDependencies} from './di'; - export interface InjectableMeta { - ctorDeps: R3DependencyMetadata[]|'invalid'|null; + ctorDeps: R3DependencyMetadata[] | 'invalid' | null; } /** @@ -24,13 +23,16 @@ export interface InjectableMeta { export class InjectableClassRegistry { private classes = new Map(); - constructor(private host: ReflectionHost, private isCore: boolean) {} + constructor( + private host: ReflectionHost, + private isCore: boolean, + ) {} registerInjectable(declaration: ClassDeclaration, meta: InjectableMeta): void { this.classes.set(declaration, meta); } - getInjectableMeta(declaration: ClassDeclaration): InjectableMeta|null { + getInjectableMeta(declaration: ClassDeclaration): InjectableMeta | null { // Figure out whether the class is injectable based on the registered classes, otherwise // fall back to looking at its members since we might not have been able to register the class // if it was compiled in another compilation unit. diff --git a/packages/compiler-cli/src/ngtsc/annotations/common/src/input_transforms.ts b/packages/compiler-cli/src/ngtsc/annotations/common/src/input_transforms.ts index 37c70d2846d62..9a92f24cace7b 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/common/src/input_transforms.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/common/src/input_transforms.ts @@ -12,8 +12,9 @@ import {ClassPropertyMapping, InputMapping} from '../../../metadata'; import {CompileResult} from '../../../transform'; /** Generates additional fields to be added to a class that has inputs with transform functions. */ -export function compileInputTransformFields(inputs: ClassPropertyMapping): - CompileResult[] { +export function compileInputTransformFields( + inputs: ClassPropertyMapping, +): CompileResult[] { const extraFields: CompileResult[] = []; for (const input of inputs) { @@ -26,7 +27,7 @@ export function compileInputTransformFields(inputs: ClassPropertyMapping Decorator = dec => dec): R3ClassMetadata|null { + clazz: DeclarationNode, + reflection: ReflectionHost, + isCore: boolean, + annotateForClosureCompiler?: boolean, + angularDecoratorTransform: (dec: Decorator) => Decorator = (dec) => dec, +): R3ClassMetadata | null { if (!reflection.isClass(clazz)) { return null; } @@ -36,50 +53,60 @@ export function extractClassMetadata( if (classDecorators === null) { return null; } - const ngClassDecorators = - classDecorators.filter(dec => isAngularDecorator(dec, isCore)) - .map( - decorator => decoratorToMetadata( - angularDecoratorTransform(decorator), annotateForClosureCompiler)) - // Since the `setClassMetadata` call is intended to be emitted after the class - // declaration, we have to strip references to the existing identifiers or - // TypeScript might generate invalid code when it emits to JS. In particular - // this can break when emitting a class to ES5 which has a custom decorator - // and is referenced inside of its own metadata (see #39509 for more information). - .map(decorator => removeIdentifierReferences(decorator, id.text)); + const ngClassDecorators = classDecorators + .filter((dec) => isAngularDecorator(dec, isCore)) + .map((decorator) => + decoratorToMetadata(angularDecoratorTransform(decorator), annotateForClosureCompiler), + ) + // Since the `setClassMetadata` call is intended to be emitted after the class + // declaration, we have to strip references to the existing identifiers or + // TypeScript might generate invalid code when it emits to JS. In particular + // this can break when emitting a class to ES5 which has a custom decorator + // and is referenced inside of its own metadata (see #39509 for more information). + .map((decorator) => removeIdentifierReferences(decorator, id.text)); if (ngClassDecorators.length === 0) { return null; } - const metaDecorators = - new WrappedNodeExpr(ts.factory.createArrayLiteralExpression(ngClassDecorators)); + const metaDecorators = new WrappedNodeExpr( + ts.factory.createArrayLiteralExpression(ngClassDecorators), + ); // Convert the constructor parameters to metadata, passing null if none are present. - let metaCtorParameters: Expression|null = null; + let metaCtorParameters: Expression | null = null; const classCtorParameters = reflection.getConstructorParameters(clazz); if (classCtorParameters !== null) { - const ctorParameters = classCtorParameters.map(param => ctorParameterToMetadata(param, isCore)); + const ctorParameters = classCtorParameters.map((param) => + ctorParameterToMetadata(param, isCore), + ); metaCtorParameters = new ArrowFunctionExpr([], new LiteralArrayExpr(ctorParameters)); } // Do the same for property decorators. - let metaPropDecorators: Expression|null = null; - const classMembers = reflection.getMembersOfClass(clazz).filter( - member => !member.isStatic && member.decorators !== null && member.decorators.length > 0); - const duplicateDecoratedMemberNames = - classMembers.map(member => member.name).filter((name, i, arr) => arr.indexOf(name) < i); + let metaPropDecorators: Expression | null = null; + const classMembers = reflection + .getMembersOfClass(clazz) + .filter( + (member) => !member.isStatic && member.decorators !== null && member.decorators.length > 0, + ); + const duplicateDecoratedMemberNames = classMembers + .map((member) => member.name) + .filter((name, i, arr) => arr.indexOf(name) < i); if (duplicateDecoratedMemberNames.length > 0) { // This should theoretically never happen, because the only way to have duplicate instance // member names is getter/setter pairs and decorators cannot appear in both a getter and the // corresponding setter. throw new Error( - `Duplicate decorated properties found on class '${clazz.name.text}': ` + - duplicateDecoratedMemberNames.join(', ')); + `Duplicate decorated properties found on class '${clazz.name.text}': ` + + duplicateDecoratedMemberNames.join(', '), + ); } - const decoratedMembers = classMembers.map( - member => classMemberToMetadata(member.nameNode ?? member.name, member.decorators!, isCore)); + const decoratedMembers = classMembers.map((member) => + classMemberToMetadata(member.nameNode ?? member.name, member.decorators!, isCore), + ); if (decoratedMembers.length > 0) { - metaPropDecorators = - new WrappedNodeExpr(ts.factory.createObjectLiteralExpression(decoratedMembers)); + metaPropDecorators = new WrappedNodeExpr( + ts.factory.createObjectLiteralExpression(decoratedMembers), + ); } return { @@ -96,18 +123,20 @@ export function extractClassMetadata( function ctorParameterToMetadata(param: CtorParameter, isCore: boolean): Expression { // Parameters sometimes have a type that can be referenced. If so, then use it, otherwise // its type is undefined. - const type = param.typeValueReference.kind !== TypeValueReferenceKind.UNAVAILABLE ? - valueReferenceToExpression(param.typeValueReference) : - new LiteralExpr(undefined); + const type = + param.typeValueReference.kind !== TypeValueReferenceKind.UNAVAILABLE + ? valueReferenceToExpression(param.typeValueReference) + : new LiteralExpr(undefined); - const mapEntries: {key: string, value: Expression, quoted: false}[] = [ + const mapEntries: {key: string; value: Expression; quoted: false}[] = [ {key: 'type', value: type, quoted: false}, ]; // If the parameter has decorators, include the ones from Angular. if (param.decorators !== null) { - const ngDecorators = param.decorators.filter(dec => isAngularDecorator(dec, isCore)) - .map((decorator: Decorator) => decoratorToMetadata(decorator)); + const ngDecorators = param.decorators + .filter((dec) => isAngularDecorator(dec, isCore)) + .map((decorator: Decorator) => decoratorToMetadata(decorator)); const value = new WrappedNodeExpr(ts.factory.createArrayLiteralExpression(ngDecorators)); mapEntries.push({key: 'decorators', value, quoted: false}); } @@ -118,9 +147,13 @@ function ctorParameterToMetadata(param: CtorParameter, isCore: boolean): Express * Convert a reflected class member to metadata. */ function classMemberToMetadata( - name: ts.PropertyName|string, decorators: Decorator[], isCore: boolean): ts.PropertyAssignment { - const ngDecorators = decorators.filter(dec => isAngularDecorator(dec, isCore)) - .map((decorator: Decorator) => decoratorToMetadata(decorator)); + name: ts.PropertyName | string, + decorators: Decorator[], + isCore: boolean, +): ts.PropertyAssignment { + const ngDecorators = decorators + .filter((dec) => isAngularDecorator(dec, isCore)) + .map((decorator: Decorator) => decoratorToMetadata(decorator)); const decoratorMeta = ts.factory.createArrayLiteralExpression(ngDecorators); return ts.factory.createPropertyAssignment(name, decoratorMeta); } @@ -129,7 +162,9 @@ function classMemberToMetadata( * Convert a reflected decorator to metadata. */ function decoratorToMetadata( - decorator: Decorator, wrapFunctionsInParens?: boolean): ts.ObjectLiteralExpression { + decorator: Decorator, + wrapFunctionsInParens?: boolean, +): ts.ObjectLiteralExpression { if (decorator.identifier === null) { throw new Error('Illegal state: synthesized decorator cannot be emitted in class metadata.'); } @@ -139,11 +174,12 @@ function decoratorToMetadata( ]; // Sometimes they have arguments. if (decorator.args !== null && decorator.args.length > 0) { - const args = decorator.args.map(arg => { + const args = decorator.args.map((arg) => { return wrapFunctionsInParens ? wrapFunctionExpressionsInParens(arg) : arg; }); properties.push( - ts.factory.createPropertyAssignment('args', ts.factory.createArrayLiteralExpression(args))); + ts.factory.createPropertyAssignment('args', ts.factory.createArrayLiteralExpression(args)), + ); } return ts.factory.createObjectLiteralExpression(properties, true); } @@ -163,15 +199,20 @@ function isAngularDecorator(decorator: Decorator, isCore: boolean): boolean { * taken from one place any emitted to another one exactly as it has been written. */ export function removeIdentifierReferences( - node: T, names: string|Set): T { - const result = - ts.transform(node, [context => root => ts.visitNode(root, function walk(current: ts.Node): T { - return (ts.isIdentifier(current) && - (typeof names === 'string' ? current.text === names : - names.has(current.text)) ? - ts.factory.createIdentifier(current.text) : - ts.visitEachChild(current, walk, context)) as T; - }) as T]); + node: T, + names: string | Set, +): T { + const result = ts.transform(node, [ + (context) => (root) => + ts.visitNode(root, function walk(current: ts.Node): T { + return ( + ts.isIdentifier(current) && + (typeof names === 'string' ? current.text === names : names.has(current.text)) + ? ts.factory.createIdentifier(current.text) + : ts.visitEachChild(current, walk, context) + ) as T; + }) as T, + ]); return result.transformed[0]; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/common/src/schema.ts b/packages/compiler-cli/src/ngtsc/annotations/common/src/schema.ts index 2466ef26bc8db..d31ed22ffd5c5 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/common/src/schema.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/common/src/schema.ts @@ -15,7 +15,10 @@ import {PartialEvaluator} from '../../../partial_evaluator'; import {createValueHasWrongTypeError} from './diagnostics'; export function extractSchemas( - rawExpr: ts.Expression, evaluator: PartialEvaluator, context: string): SchemaMetadata[] { + rawExpr: ts.Expression, + evaluator: PartialEvaluator, + context: string, +): SchemaMetadata[] { const schemas: SchemaMetadata[] = []; const result = evaluator.evaluate(rawExpr); if (!Array.isArray(result)) { @@ -25,12 +28,18 @@ export function extractSchemas( for (const schemaRef of result) { if (!(schemaRef instanceof Reference)) { throw createValueHasWrongTypeError( - rawExpr, result, `${context}.schemas must be an array of schemas`); + rawExpr, + result, + `${context}.schemas must be an array of schemas`, + ); } const id = schemaRef.getIdentityIn(schemaRef.node.getSourceFile()); if (id === null || schemaRef.ownedByModuleGuess !== '@angular/core') { throw createValueHasWrongTypeError( - rawExpr, result, `${context}.schemas must be an array of schemas`); + rawExpr, + result, + `${context}.schemas must be an array of schemas`, + ); } // Since `id` is the `ts.Identifier` within the schema ref's declaration file, it's safe to // use `id.text` here to figure out which schema is in use. Even if the actual reference was @@ -44,7 +53,10 @@ export function extractSchemas( break; default: throw createValueHasWrongTypeError( - rawExpr, schemaRef, `'${schemaRef.debugName}' is not a valid ${context} schema`); + rawExpr, + schemaRef, + `'${schemaRef.debugName}' is not a valid ${context} schema`, + ); } } return schemas; diff --git a/packages/compiler-cli/src/ngtsc/annotations/common/src/util.ts b/packages/compiler-cli/src/ngtsc/annotations/common/src/util.ts index 415cd901538bf..569a1d8f7f631 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/common/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/common/src/util.ts @@ -6,13 +6,42 @@ * found in the LICENSE file at https://angular.io/license */ -import {Expression, ExternalExpr, FactoryTarget, ParseLocation, ParseSourceFile, ParseSourceSpan, R3CompiledExpression, R3FactoryMetadata, R3Reference, ReadPropExpr, Statement, WrappedNodeExpr} from '@angular/compiler'; +import { + Expression, + ExternalExpr, + FactoryTarget, + ParseLocation, + ParseSourceFile, + ParseSourceSpan, + R3CompiledExpression, + R3FactoryMetadata, + R3Reference, + ReadPropExpr, + Statement, + WrappedNodeExpr, +} from '@angular/compiler'; import ts from 'typescript'; -import {assertSuccessfulReferenceEmit, ImportedFile, ImportFlags, ModuleResolver, Reference, ReferenceEmitter} from '../../../imports'; +import { + assertSuccessfulReferenceEmit, + ImportedFile, + ImportFlags, + ModuleResolver, + Reference, + ReferenceEmitter, +} from '../../../imports'; import {attachDefaultImportDeclaration} from '../../../imports/src/default'; import {ForeignFunctionResolver, PartialEvaluator} from '../../../partial_evaluator'; -import {ClassDeclaration, Decorator, Import, ImportedTypeValueReference, LocalTypeValueReference, ReflectionHost, TypeValueReference, TypeValueReferenceKind} from '../../../reflection'; +import { + ClassDeclaration, + Decorator, + Import, + ImportedTypeValueReference, + LocalTypeValueReference, + ReflectionHost, + TypeValueReference, + TypeValueReferenceKind, +} from '../../../reflection'; import {CompileResult} from '../../../transform'; /** Module name of the framework core. */ @@ -25,10 +54,11 @@ export const CORE_MODULE = '@angular/core'; * references are converted to an `ExternalExpr`. Note that this is only valid in the context of the * file in which the `TypeValueReference` originated. */ -export function valueReferenceToExpression(valueRef: LocalTypeValueReference| - ImportedTypeValueReference): Expression; -export function valueReferenceToExpression(valueRef: TypeValueReference): Expression|null; -export function valueReferenceToExpression(valueRef: TypeValueReference): Expression|null { +export function valueReferenceToExpression( + valueRef: LocalTypeValueReference | ImportedTypeValueReference, +): Expression; +export function valueReferenceToExpression(valueRef: TypeValueReference): Expression | null; +export function valueReferenceToExpression(valueRef: TypeValueReference): Expression | null { if (valueRef.kind === TypeValueReferenceKind.UNAVAILABLE) { return null; } else if (valueRef.kind === TypeValueReferenceKind.LOCAL) { @@ -38,8 +68,10 @@ export function valueReferenceToExpression(valueRef: TypeValueReference): Expres } return expr; } else { - let importExpr: Expression = - new ExternalExpr({moduleName: valueRef.moduleName, name: valueRef.importedName}); + let importExpr: Expression = new ExternalExpr({ + moduleName: valueRef.moduleName, + name: valueRef.importedName, + }); if (valueRef.nestedPath !== null) { for (const property of valueRef.nestedPath) { importExpr = new ReadPropExpr(importExpr, property); @@ -50,13 +82,19 @@ export function valueReferenceToExpression(valueRef: TypeValueReference): Expres } export function toR3Reference( - origin: ts.Node, ref: Reference, context: ts.SourceFile, - refEmitter: ReferenceEmitter): R3Reference { + origin: ts.Node, + ref: Reference, + context: ts.SourceFile, + refEmitter: ReferenceEmitter, +): R3Reference { const emittedValueRef = refEmitter.emit(ref, context); assertSuccessfulReferenceEmit(emittedValueRef, origin, 'class'); - const emittedTypeRef = - refEmitter.emit(ref, context, ImportFlags.ForceNewImport | ImportFlags.AllowTypeImports); + const emittedTypeRef = refEmitter.emit( + ref, + context, + ImportFlags.ForceNewImport | ImportFlags.AllowTypeImports, + ); assertSuccessfulReferenceEmit(emittedTypeRef, origin, 'class'); return { @@ -65,7 +103,7 @@ export function toR3Reference( }; } -export function isAngularCore(decorator: Decorator): decorator is(Decorator & {import: Import}) { +export function isAngularCore(decorator: Decorator): decorator is Decorator & {import: Import} { return decorator.import !== null && decorator.import.from === CORE_MODULE; } @@ -74,8 +112,11 @@ export function isAngularCoreReference(reference: Reference, symbolName: string) } export function findAngularDecorator( - decorators: Decorator[], name: string, isCore: boolean): Decorator|undefined { - return decorators.find(decorator => isAngularDecorator(decorator, name, isCore)); + decorators: Decorator[], + name: string, + isCore: boolean, +): Decorator | undefined { + return decorators.find((decorator) => isAngularDecorator(decorator, name, isCore)); } export function isAngularDecorator(decorator: Decorator, name: string, isCore: boolean): boolean { @@ -88,8 +129,11 @@ export function isAngularDecorator(decorator: Decorator, name: string, isCore: b } export function getAngularDecorators( - decorators: Decorator[], names: readonly string[], isCore: boolean) { - return decorators.filter(decorator => { + decorators: Decorator[], + names: readonly string[], + isCore: boolean, +) { + return decorators.filter((decorator) => { const name = isCore ? decorator.name : decorator.import?.name; if (name === undefined || !names.includes(name)) { return false; @@ -111,7 +155,7 @@ export function unwrapExpression(node: ts.Expression): ts.Expression { return node; } -function expandForwardRef(arg: ts.Expression): ts.Expression|null { +function expandForwardRef(arg: ts.Expression): ts.Expression | null { arg = unwrapExpression(arg); if (!ts.isArrowFunction(arg) && !ts.isFunctionExpression(arg)) { return null; @@ -135,7 +179,6 @@ function expandForwardRef(arg: ts.Expression): ts.Expression|null { } } - /** * If the given `node` is a forwardRef() expression then resolve its inner value, otherwise return * `null`. @@ -145,15 +188,18 @@ function expandForwardRef(arg: ts.Expression): ts.Expression|null { * @returns the resolved expression, if the original expression was a forwardRef(), or `null` * otherwise. */ -export function tryUnwrapForwardRef(node: ts.Expression, reflector: ReflectionHost): ts.Expression| - null { +export function tryUnwrapForwardRef( + node: ts.Expression, + reflector: ReflectionHost, +): ts.Expression | null { node = unwrapExpression(node); if (!ts.isCallExpression(node) || node.arguments.length !== 1) { return null; } - const fn = - ts.isPropertyAccessExpression(node.expression) ? node.expression.name : node.expression; + const fn = ts.isPropertyAccessExpression(node.expression) + ? node.expression.name + : node.expression; if (!ts.isIdentifier(fn)) { return null; } @@ -179,18 +225,22 @@ export function tryUnwrapForwardRef(node: ts.Expression, reflector: ReflectionHo * @param args the arguments to the invocation of the forwardRef expression * @returns an unwrapped argument if `ref` pointed to forwardRef, or null otherwise */ -export const forwardRefResolver: ForeignFunctionResolver = - (fn, callExpr, resolve, unresolvable) => { - if (!isAngularCoreReference(fn, 'forwardRef') || callExpr.arguments.length !== 1) { - return unresolvable; - } - const expanded = expandForwardRef(callExpr.arguments[0]); - if (expanded !== null) { - return resolve(expanded); - } else { - return unresolvable; - } - }; +export const forwardRefResolver: ForeignFunctionResolver = ( + fn, + callExpr, + resolve, + unresolvable, +) => { + if (!isAngularCoreReference(fn, 'forwardRef') || callExpr.arguments.length !== 1) { + return unresolvable; + } + const expanded = expandForwardRef(callExpr.arguments[0]); + if (expanded !== null) { + return resolve(expanded); + } else { + return unresolvable; + } +}; /** * Combines an array of resolver functions into a one. @@ -209,7 +259,10 @@ export function combineResolvers(resolvers: ForeignFunctionResolver[]): ForeignF } export function isExpressionForwardReference( - expr: Expression, context: ts.Node, contextSource: ts.SourceFile): boolean { + expr: Expression, + context: ts.Node, + contextSource: ts.SourceFile, +): boolean { if (isWrappedTsNodeExpr(expr)) { const node = ts.getOriginalNode(expr.node); return node.getSourceFile() === contextSource && context.pos < node.pos; @@ -223,8 +276,10 @@ export function isWrappedTsNodeExpr(expr: Expression): expr is WrappedNodeExpr|'dynamic'|null { + node: ClassDeclaration, + reflector: ReflectionHost, + evaluator: PartialEvaluator, +): Reference | 'dynamic' | null { const baseExpression = reflector.getBaseClassExpression(node); if (baseExpression !== null) { const baseClass = evaluator.evaluate(baseExpression); @@ -238,17 +293,18 @@ export function readBaseClass( return null; } -const parensWrapperTransformerFactory: ts.TransformerFactory = - (context: ts.TransformationContext) => { - const visitor: ts.Visitor = (node: ts.Node): ts.Node => { - const visited = ts.visitEachChild(node, visitor, context); - if (ts.isArrowFunction(visited) || ts.isFunctionExpression(visited)) { - return ts.factory.createParenthesizedExpression(visited); - } - return visited; - }; - return (node: ts.Expression) => ts.visitEachChild(node, visitor, context); - }; +const parensWrapperTransformerFactory: ts.TransformerFactory = ( + context: ts.TransformationContext, +) => { + const visitor: ts.Visitor = (node: ts.Node): ts.Node => { + const visited = ts.visitEachChild(node, visitor, context); + if (ts.isArrowFunction(visited) || ts.isFunctionExpression(visited)) { + return ts.factory.createParenthesizedExpression(visited); + } + return visited; + }; + return (node: ts.Expression) => ts.visitEachChild(node, visitor, context); +}; /** * Wraps all functions in a given expression in parentheses. This is needed to avoid problems @@ -270,8 +326,10 @@ export function wrapFunctionExpressionsInParens(expression: ts.Expression): ts.E * @param rawProviders Expression that declared the providers array in the source. */ export function resolveProvidersRequiringFactory( - rawProviders: ts.Expression, reflector: ReflectionHost, - evaluator: PartialEvaluator): Set> { + rawProviders: ts.Expression, + reflector: ReflectionHost, + evaluator: PartialEvaluator, +): Set> { const providers = new Set>(); const resolvedProviders = evaluator.evaluate(rawProviders); @@ -280,7 +338,7 @@ export function resolveProvidersRequiringFactory( } resolvedProviders.forEach(function processProviders(provider) { - let tokenClass: Reference|null = null; + let tokenClass: Reference | null = null; if (Array.isArray(provider)) { // If we ran into an array, recurse into it until we've resolve all the classes. @@ -299,8 +357,11 @@ export function resolveProvidersRequiringFactory( // `getConstructorParameters`, but that fix causes more classes to be recognized here as needing // provider checks, which is a breaking change in g3. Avoid this breakage for now by skipping // classes from .d.ts files here directly, until g3 can be cleaned up. - if (tokenClass !== null && !tokenClass.node.getSourceFile().isDeclarationFile && - reflector.isClass(tokenClass.node)) { + if ( + tokenClass !== null && + !tokenClass.node.getSourceFile().isDeclarationFile && + reflector.isClass(tokenClass.node) + ) { const constructorParameters = reflector.getConstructorParameters(tokenClass.node); // Note that we only want to capture providers with a non-trivial constructor, @@ -336,17 +397,23 @@ export function createSourceSpan(node: ts.Node): ParseSourceSpan { // +1 because values are zero-indexed. return new ParseSourceSpan( - new ParseLocation(parseSf, startOffset, startLine + 1, startCol + 1), - new ParseLocation(parseSf, endOffset, endLine + 1, endCol + 1)); + new ParseLocation(parseSf, startOffset, startLine + 1, startCol + 1), + new ParseLocation(parseSf, endOffset, endLine + 1, endCol + 1), + ); } /** * Collate the factory and definition compiled results into an array of CompileResult objects. */ export function compileResults( - fac: CompileResult, def: R3CompiledExpression, metadataStmt: Statement|null, propName: string, - additionalFields: CompileResult[]|null, deferrableImports: Set|null, - debugInfo: Statement|null = null): CompileResult[] { + fac: CompileResult, + def: R3CompiledExpression, + metadataStmt: Statement | null, + propName: string, + additionalFields: CompileResult[] | null, + deferrableImports: Set | null, + debugInfo: Statement | null = null, +): CompileResult[] { const statements = def.statements; if (metadataStmt !== null) { @@ -376,19 +443,24 @@ export function compileResults( } export function toFactoryMetadata( - meta: Omit, target: FactoryTarget): R3FactoryMetadata { + meta: Omit, + target: FactoryTarget, +): R3FactoryMetadata { return { name: meta.name, type: meta.type, typeArgumentCount: meta.typeArgumentCount, deps: meta.deps, - target + target, }; } export function resolveImportedFile( - moduleResolver: ModuleResolver, importedFile: ImportedFile, expr: Expression, - origin: ts.SourceFile): ts.SourceFile|null { + moduleResolver: ModuleResolver, + importedFile: ImportedFile, + expr: Expression, + origin: ts.SourceFile, +): ts.SourceFile | null { // If `importedFile` is not 'unknown' then it accurately reflects the source file that is // being imported. if (importedFile !== 'unknown') { @@ -406,14 +478,15 @@ export function resolveImportedFile( return moduleResolver.resolveModule(expr.value.moduleName!, origin.fileName); } - /** * Determines the most appropriate expression for diagnostic reporting purposes. If `expr` is * contained within `container` then `expr` is used as origin node, otherwise `container` itself is * used. */ export function getOriginNodeForDiagnostics( - expr: ts.Expression, container: ts.Expression): ts.Expression { + expr: ts.Expression, + container: ts.Expression, +): ts.Expression { const nodeSf = expr.getSourceFile(); const exprSf = container.getSourceFile(); @@ -427,7 +500,7 @@ export function getOriginNodeForDiagnostics( } export function isAbstractClassDeclaration(clazz: ClassDeclaration): boolean { - return ts.canHaveModifiers(clazz) && clazz.modifiers !== undefined ? - clazz.modifiers.some(mod => mod.kind === ts.SyntaxKind.AbstractKeyword) : - false; + return ts.canHaveModifiers(clazz) && clazz.modifiers !== undefined + ? clazz.modifiers.some((mod) => mod.kind === ts.SyntaxKind.AbstractKeyword) + : false; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/common/test/diagnostics_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/common/test/diagnostics_spec.ts index fc192a865ca2a..3a856f80ac43e 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/common/test/diagnostics_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/common/test/diagnostics_spec.ts @@ -27,8 +27,9 @@ runInEachFileSystem(() => { } expect(error.diagnosticMessage.messageText).toBe('Error message'); expect(error.diagnosticMessage.next!.length).toBe(1); - expect(error.diagnosticMessage.next![0].messageText) - .toBe(`Value could not be determined statically.`); + expect(error.diagnosticMessage.next![0].messageText).toBe( + `Value could not be determined statically.`, + ); expect(error.relatedInformation).toBeDefined(); expect(error.relatedInformation!.length).toBe(1); @@ -39,9 +40,9 @@ runInEachFileSystem(() => { }); it('should include a pointer for a reference to a named declaration', () => { - const {error, program} = createError( - `import {Foo} from './foo';`, 'Foo', 'Error message', - [{name: _('/foo.ts'), contents: 'export class Foo {}'}]); + const {error, program} = createError(`import {Foo} from './foo';`, 'Foo', 'Error message', [ + {name: _('/foo.ts'), contents: 'export class Foo {}'}, + ]); const fooSf = getSourceFileOrError(program, _('/foo.ts')); if (typeof error.diagnosticMessage === 'string') { @@ -59,9 +60,9 @@ runInEachFileSystem(() => { }); it('should include a pointer for a reference to an anonymous declaration', () => { - const {error, program} = createError( - `import Foo from './foo';`, 'Foo', 'Error message', - [{name: _('/foo.ts'), contents: 'export default class {}'}]); + const {error, program} = createError(`import Foo from './foo';`, 'Foo', 'Error message', [ + {name: _('/foo.ts'), contents: 'export default class {}'}, + ]); const fooSf = getSourceFileOrError(program, _('/foo.ts')); if (typeof error.diagnosticMessage === 'string') { @@ -69,8 +70,9 @@ runInEachFileSystem(() => { } expect(error.diagnosticMessage.messageText).toBe('Error message'); expect(error.diagnosticMessage.next!.length).toBe(1); - expect(error.diagnosticMessage.next![0].messageText) - .toBe(`Value is a reference to an anonymous declaration.`); + expect(error.diagnosticMessage.next![0].messageText).toBe( + `Value is a reference to an anonymous declaration.`, + ); expect(error.relatedInformation).toBeDefined(); expect(error.relatedInformation!.length).toBe(1); @@ -79,7 +81,7 @@ runInEachFileSystem(() => { expect(getSourceCode(error.relatedInformation![0])).toBe('export default class {}'); }); - it('should include a representation of the value\'s type', () => { + it("should include a representation of the value's type", () => { const {error} = createError('', '{a: 2}', 'Error message'); if (typeof error.diagnosticMessage === 'string') { @@ -87,8 +89,9 @@ runInEachFileSystem(() => { } expect(error.diagnosticMessage.messageText).toBe('Error message'); expect(error.diagnosticMessage.next!.length).toBe(1); - expect(error.diagnosticMessage.next![0].messageText) - .toBe(`Value is of type '{ a: number }'.`); + expect(error.diagnosticMessage.next![0].messageText).toBe( + `Value is of type '{ a: number }'.`, + ); expect(error.relatedInformation).not.toBeDefined(); }); @@ -102,10 +105,17 @@ function getSourceCode(diag: ts.DiagnosticRelatedInformation): string { } function createError( - code: string, expr: string, messageText: string, supportingFiles: TestFile[] = []) { + code: string, + expr: string, + messageText: string, + supportingFiles: TestFile[] = [], +) { const {program} = makeProgram( - [{name: _('/entry.ts'), contents: `${code}; const target$ = ${expr}`}, ...supportingFiles], - /* options */ undefined, /* host */ undefined, /* checkForErrors */ false); + [{name: _('/entry.ts'), contents: `${code}; const target$ = ${expr}`}, ...supportingFiles], + /* options */ undefined, + /* host */ undefined, + /* checkForErrors */ false, + ); const checker = program.getTypeChecker(); const decl = getDeclaration(program, _('/entry.ts'), 'target$', ts.isVariableDeclaration); const valueExpr = decl.initializer!; diff --git a/packages/compiler-cli/src/ngtsc/annotations/common/test/metadata_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/common/test/metadata_spec.ts index 17cdbdf6ab38b..8507536c93d94 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/common/test/metadata_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/common/test/metadata_spec.ts @@ -12,7 +12,11 @@ import {absoluteFrom, getSourceFileOrError} from '../../../file_system'; import {runInEachFileSystem, TestFile} from '../../../file_system/testing'; import {TypeScriptReflectionHost} from '../../../reflection'; import {getDeclaration, makeProgram} from '../../../testing'; -import {ImportManager, presetImportManagerForceNamespaceImports, translateStatement} from '../../../translator'; +import { + ImportManager, + presetImportManagerForceNamespaceImports, + translateStatement, +} from '../../../translator'; import {extractClassMetadata} from '../src/metadata'; runInEachFileSystem(() => { @@ -24,7 +28,8 @@ runInEachFileSystem(() => { @Component('metadata') class Target {} `); expect(res).toEqual( - `(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(Target, [{ type: Component, args: ['metadata'] }], null, null); })();`); + `(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(Target, [{ type: Component, args: ['metadata'] }], null, null); })();`, + ); }); it('should convert namespaced decorated class metadata', () => { @@ -34,7 +39,8 @@ runInEachFileSystem(() => { @core.Component('metadata') class Target {} `); expect(res).toEqual( - `(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(Target, [{ type: core.Component, args: ['metadata'] }], null, null); })();`); + `(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(Target, [{ type: core.Component, args: ['metadata'] }], null, null); })();`, + ); }); it('should convert decorated class constructor parameter metadata', () => { @@ -47,7 +53,8 @@ runInEachFileSystem(() => { } `); expect(res).toContain( - `() => [{ type: undefined, decorators: [{ type: Inject, args: [FOO] }] }, { type: i0.Injector }], null);`); + `() => [{ type: undefined, decorators: [{ type: Inject, args: [FOO] }] }, { type: i0.Injector }], null);`, + ); }); it('should convert decorated field metadata', () => { @@ -101,7 +108,8 @@ runInEachFileSystem(() => { } `); expect(res).toContain( - `{ 'has-dashes-in-name': [{ type: Input }], noDashesInName: [{ type: Input }] })`); + `{ 'has-dashes-in-name': [{ type: Input }], noDashesInName: [{ type: Input }] })`, + ); }); }); @@ -114,17 +122,19 @@ runInEachFileSystem(() => { export declare function Inject(...args: any[]): any; export declare function Component(...args: any[]): any; export declare class Injector {} - ` + `, }; const {program} = makeProgram( - [ - CORE, { - name: _('/index.ts'), - contents, - } - ], - {target: ts.ScriptTarget.ES2015}); + [ + CORE, + { + name: _('/index.ts'), + contents, + }, + ], + {target: ts.ScriptTarget.ES2015}, + ); const host = new TypeScriptReflectionHost(program.getTypeChecker()); const target = getDeclaration(program, _('/index.ts'), 'Target', ts.isClassDeclaration); const call = extractClassMetadata(target, host, false); diff --git a/packages/compiler-cli/src/ngtsc/annotations/common/test/util_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/common/test/util_spec.ts index 652ffa9881b45..2e6d950515f39 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/common/test/util_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/common/test/util_spec.ts @@ -24,13 +24,17 @@ describe('ngtsc annotation utilities', () => { it('should unwrap an ObjectLiteralExpression with a type cast', () => { const cast = ts.factory.createAsExpression( - obj, ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); + obj, + ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), + ); expect(unwrapExpression(cast)).toBe(obj); }); it('should unwrap an ObjectLiteralExpression with a type cast in parentheses', () => { const cast = ts.factory.createAsExpression( - obj, ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); + obj, + ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), + ); const wrapped = ts.factory.createParenthesizedExpression(cast); expect(unwrapExpression(wrapped)).toBe(obj); }); diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/diagnostics.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/diagnostics.ts index 67774bc520f1b..957f74da33757 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/diagnostics.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/diagnostics.ts @@ -12,25 +12,28 @@ import {Cycle} from '../../../cycles'; import {makeRelatedInformation} from '../../../diagnostics'; import {Reference} from '../../../imports'; - /** * Generate a diagnostic related information object that describes a potential cyclic import path. */ export function makeCyclicImportInfo( - ref: Reference, type: string, cycle: Cycle): ts.DiagnosticRelatedInformation { + ref: Reference, + type: string, + cycle: Cycle, +): ts.DiagnosticRelatedInformation { const name = ref.debugName || '(unknown)'; - const path = cycle.getPath().map(sf => sf.fileName).join(' -> '); - const message = - `The ${type} '${name}' is used in the template but importing it would create a cycle: `; + const path = cycle + .getPath() + .map((sf) => sf.fileName) + .join(' -> '); + const message = `The ${type} '${name}' is used in the template but importing it would create a cycle: `; return makeRelatedInformation(ref.node, message + path); } - /** * Checks whether a selector is a valid custom element tag name. * Based loosely on https://github.com/sindresorhus/validate-element-name. */ -export function checkCustomElementSelectorForErrors(selector: string): string|null { +export function checkCustomElementSelectorForErrors(selector: string): string | null { // Avoid flagging components with an attribute or class selector. This isn't bulletproof since it // won't catch cases like `foo[]bar`, but we don't need it to be. This is mainly to avoid flagging // something like `foo-bar[baz]` incorrectly. @@ -38,7 +41,7 @@ export function checkCustomElementSelectorForErrors(selector: string): string|nu return null; } - if (!(/^[a-z]/.test(selector))) { + if (!/^[a-z]/.test(selector)) { return 'Selector of a ShadowDom-encapsulated component must start with a lower case letter.'; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts index 6179ef9b6cd6e..ae47eb7918963 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts @@ -6,91 +6,248 @@ * found in the LICENSE file at https://angular.io/license */ -import {AnimationTriggerNames, BoundTarget, compileClassDebugInfo, compileComponentClassMetadata, compileComponentDeclareClassMetadata, compileComponentFromMetadata, compileDeclareComponentFromMetadata, compileDeferResolverFunction, ConstantPool, CssSelector, DeclarationListEmitMode, DeclareComponentTemplateInfo, DEFAULT_INTERPOLATION_CONFIG, DeferBlockDepsEmitMode, DomElementSchemaRegistry, ExternalExpr, FactoryTarget, makeBindingParser, outputAst as o, R3ComponentDeferMetadata, R3ComponentMetadata, R3DeferPerComponentDependency, R3DirectiveDependencyMetadata, R3NgModuleDependencyMetadata, R3PipeDependencyMetadata, R3TargetBinder, R3TemplateDependency, R3TemplateDependencyKind, R3TemplateDependencyMetadata, SchemaMetadata, SelectorMatcher, TmplAstDeferredBlock, ViewEncapsulation} from '@angular/compiler'; +import { + AnimationTriggerNames, + BoundTarget, + compileClassDebugInfo, + compileComponentClassMetadata, + compileComponentDeclareClassMetadata, + compileComponentFromMetadata, + compileDeclareComponentFromMetadata, + compileDeferResolverFunction, + ConstantPool, + CssSelector, + DeclarationListEmitMode, + DeclareComponentTemplateInfo, + DEFAULT_INTERPOLATION_CONFIG, + DeferBlockDepsEmitMode, + DomElementSchemaRegistry, + ExternalExpr, + FactoryTarget, + makeBindingParser, + outputAst as o, + R3ComponentDeferMetadata, + R3ComponentMetadata, + R3DeferPerComponentDependency, + R3DirectiveDependencyMetadata, + R3NgModuleDependencyMetadata, + R3PipeDependencyMetadata, + R3TargetBinder, + R3TemplateDependency, + R3TemplateDependencyKind, + R3TemplateDependencyMetadata, + SchemaMetadata, + SelectorMatcher, + TmplAstDeferredBlock, + ViewEncapsulation, +} from '@angular/compiler'; import ts from 'typescript'; import {Cycle, CycleAnalyzer, CycleHandlingStrategy} from '../../../cycles'; -import {ErrorCode, FatalDiagnosticError, makeDiagnostic, makeRelatedInformation} from '../../../diagnostics'; +import { + ErrorCode, + FatalDiagnosticError, + makeDiagnostic, + makeRelatedInformation, +} from '../../../diagnostics'; import {absoluteFrom, relative} from '../../../file_system'; -import {assertSuccessfulReferenceEmit, DeferredSymbolTracker, ImportedFile, ImportedSymbolsTracker, LocalCompilationExtraImportsTracker, ModuleResolver, Reference, ReferenceEmitter} from '../../../imports'; +import { + assertSuccessfulReferenceEmit, + DeferredSymbolTracker, + ImportedFile, + ImportedSymbolsTracker, + LocalCompilationExtraImportsTracker, + ModuleResolver, + Reference, + ReferenceEmitter, +} from '../../../imports'; import {DependencyTracker} from '../../../incremental/api'; -import {extractSemanticTypeParameters, SemanticDepGraphUpdater} from '../../../incremental/semantic_graph'; +import { + extractSemanticTypeParameters, + SemanticDepGraphUpdater, +} from '../../../incremental/semantic_graph'; import {IndexingContext} from '../../../indexer'; -import {DirectiveMeta, extractDirectiveTypeCheckMeta, HostDirectivesResolver, MatchSource, MetadataReader, MetadataRegistry, MetaKind, NgModuleMeta, PipeMeta, ResourceRegistry} from '../../../metadata'; +import { + DirectiveMeta, + extractDirectiveTypeCheckMeta, + HostDirectivesResolver, + MatchSource, + MetadataReader, + MetadataRegistry, + MetaKind, + NgModuleMeta, + PipeMeta, + ResourceRegistry, +} from '../../../metadata'; import {PartialEvaluator} from '../../../partial_evaluator'; import {PerfEvent, PerfRecorder} from '../../../perf'; -import {ClassDeclaration, DeclarationNode, Decorator, Import, isNamedClassDeclaration, ReflectionHost, reflectObjectLiteral} from '../../../reflection'; -import {ComponentScopeKind, ComponentScopeReader, DtsModuleScopeResolver, LocalModuleScope, LocalModuleScopeRegistry, makeNotStandaloneDiagnostic, makeUnknownComponentImportDiagnostic, StandaloneScope, TypeCheckScopeRegistry} from '../../../scope'; -import {getDiagnosticNode, makeUnknownComponentDeferredImportDiagnostic} from '../../../scope/src/util'; -import {AnalysisOutput, CompilationMode, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../../transform'; +import { + ClassDeclaration, + DeclarationNode, + Decorator, + Import, + isNamedClassDeclaration, + ReflectionHost, + reflectObjectLiteral, +} from '../../../reflection'; +import { + ComponentScopeKind, + ComponentScopeReader, + DtsModuleScopeResolver, + LocalModuleScope, + LocalModuleScopeRegistry, + makeNotStandaloneDiagnostic, + makeUnknownComponentImportDiagnostic, + StandaloneScope, + TypeCheckScopeRegistry, +} from '../../../scope'; +import { + getDiagnosticNode, + makeUnknownComponentDeferredImportDiagnostic, +} from '../../../scope/src/util'; +import { + AnalysisOutput, + CompilationMode, + CompileResult, + DecoratorHandler, + DetectResult, + HandlerPrecedence, + ResolveResult, +} from '../../../transform'; import {TypeCheckableDirectiveMeta, TypeCheckContext} from '../../../typecheck/api'; import {ExtendedTemplateChecker} from '../../../typecheck/extended/api'; import {TemplateSemanticsChecker} from '../../../typecheck/template_semantics/api/api'; import {getSourceFile} from '../../../util/src/typescript'; import {Xi18nContext} from '../../../xi18n'; -import {combineResolvers, compileDeclareFactory, compileInputTransformFields, compileNgFactoryDefField, compileResults, extractClassDebugInfo, extractClassMetadata, extractSchemas, findAngularDecorator, forwardRefResolver, getDirectiveDiagnostics, getProviderDiagnostics, InjectableClassRegistry, isExpressionForwardReference, readBaseClass, ReferencesRegistry, removeIdentifierReferences, resolveEncapsulationEnumValueLocally, resolveEnumValue, resolveImportedFile, resolveLiteral, resolveProvidersRequiringFactory, ResourceLoader, toFactoryMetadata, tryUnwrapForwardRef, validateHostDirectives, wrapFunctionExpressionsInParens} from '../../common'; +import { + combineResolvers, + compileDeclareFactory, + compileInputTransformFields, + compileNgFactoryDefField, + compileResults, + extractClassDebugInfo, + extractClassMetadata, + extractSchemas, + findAngularDecorator, + forwardRefResolver, + getDirectiveDiagnostics, + getProviderDiagnostics, + InjectableClassRegistry, + isExpressionForwardReference, + readBaseClass, + ReferencesRegistry, + removeIdentifierReferences, + resolveEncapsulationEnumValueLocally, + resolveEnumValue, + resolveImportedFile, + resolveLiteral, + resolveProvidersRequiringFactory, + ResourceLoader, + toFactoryMetadata, + tryUnwrapForwardRef, + validateHostDirectives, + wrapFunctionExpressionsInParens, +} from '../../common'; import {extractDirectiveMetadata, parseDirectiveStyles} from '../../directive'; import {createModuleWithProvidersResolver, NgModuleSymbol} from '../../ng_module'; import {checkCustomElementSelectorForErrors, makeCyclicImportInfo} from './diagnostics'; -import {ComponentAnalysisData, ComponentResolutionData, DeferredComponentDependency} from './metadata'; -import {_extractTemplateStyleUrls, extractComponentStyleUrls, extractStyleResources, extractTemplate, makeResourceNotFoundError, ParsedTemplateWithSource, parseTemplateDeclaration, preloadAndParseTemplate, ResourceTypeForDiagnostics, StyleUrlMeta, transformDecoratorResources} from './resources'; +import { + ComponentAnalysisData, + ComponentResolutionData, + DeferredComponentDependency, +} from './metadata'; +import { + _extractTemplateStyleUrls, + extractComponentStyleUrls, + extractStyleResources, + extractTemplate, + makeResourceNotFoundError, + ParsedTemplateWithSource, + parseTemplateDeclaration, + preloadAndParseTemplate, + ResourceTypeForDiagnostics, + StyleUrlMeta, + transformDecoratorResources, +} from './resources'; import {ComponentSymbol} from './symbol'; -import {animationTriggerResolver, collectAnimationNames, validateAndFlattenComponentImports} from './util'; +import { + animationTriggerResolver, + collectAnimationNames, + validateAndFlattenComponentImports, +} from './util'; const EMPTY_ARRAY: any[] = []; -type UsedDirective = - R3DirectiveDependencyMetadata&{ref: Reference, importedFile: ImportedFile}; +type UsedDirective = R3DirectiveDependencyMetadata & { + ref: Reference; + importedFile: ImportedFile; +}; -type UsedPipe = R3PipeDependencyMetadata&{ - ref: Reference, - importedFile: ImportedFile, +type UsedPipe = R3PipeDependencyMetadata & { + ref: Reference; + importedFile: ImportedFile; }; -type UsedNgModule = R3NgModuleDependencyMetadata&{ - importedFile: ImportedFile, +type UsedNgModule = R3NgModuleDependencyMetadata & { + importedFile: ImportedFile; }; -type AnyUsedType = UsedPipe|UsedDirective|UsedNgModule; +type AnyUsedType = UsedPipe | UsedDirective | UsedNgModule; type ComponentTemplate = R3ComponentMetadata['template']; const isUsedDirective = (decl: AnyUsedType): decl is UsedDirective => - decl.kind === R3TemplateDependencyKind.Directive; + decl.kind === R3TemplateDependencyKind.Directive; const isUsedPipe = (decl: AnyUsedType): decl is UsedPipe => - decl.kind === R3TemplateDependencyKind.Pipe; + decl.kind === R3TemplateDependencyKind.Pipe; /** * `DecoratorHandler` which handles the `@Component` annotation. */ -export class ComponentDecoratorHandler implements - DecoratorHandler { +export class ComponentDecoratorHandler + implements + DecoratorHandler +{ constructor( - private reflector: ReflectionHost, private evaluator: PartialEvaluator, - private metaRegistry: MetadataRegistry, private metaReader: MetadataReader, - private scopeReader: ComponentScopeReader, private dtsScopeReader: DtsModuleScopeResolver, - private scopeRegistry: LocalModuleScopeRegistry, - private typeCheckScopeRegistry: TypeCheckScopeRegistry, - private resourceRegistry: ResourceRegistry, private isCore: boolean, - private strictCtorDeps: boolean, private resourceLoader: ResourceLoader, - private rootDirs: ReadonlyArray, private defaultPreserveWhitespaces: boolean, - private i18nUseExternalIds: boolean, private enableI18nLegacyMessageIdFormat: boolean, - private usePoisonedData: boolean, private i18nNormalizeLineEndingsInICUs: boolean, - private moduleResolver: ModuleResolver, private cycleAnalyzer: CycleAnalyzer, - private cycleHandlingStrategy: CycleHandlingStrategy, private refEmitter: ReferenceEmitter, - private referencesRegistry: ReferencesRegistry, private depTracker: DependencyTracker|null, - private injectableRegistry: InjectableClassRegistry, - private semanticDepGraphUpdater: SemanticDepGraphUpdater|null, - private annotateForClosureCompiler: boolean, private perf: PerfRecorder, - private hostDirectivesResolver: HostDirectivesResolver, - private importTracker: ImportedSymbolsTracker, private includeClassMetadata: boolean, - private readonly compilationMode: CompilationMode, - private readonly deferredSymbolTracker: DeferredSymbolTracker, - private readonly forbidOrphanRendering: boolean, private readonly enableBlockSyntax: boolean, - private readonly localCompilationExtraImportsTracker: LocalCompilationExtraImportsTracker| - null) { + private reflector: ReflectionHost, + private evaluator: PartialEvaluator, + private metaRegistry: MetadataRegistry, + private metaReader: MetadataReader, + private scopeReader: ComponentScopeReader, + private dtsScopeReader: DtsModuleScopeResolver, + private scopeRegistry: LocalModuleScopeRegistry, + private typeCheckScopeRegistry: TypeCheckScopeRegistry, + private resourceRegistry: ResourceRegistry, + private isCore: boolean, + private strictCtorDeps: boolean, + private resourceLoader: ResourceLoader, + private rootDirs: ReadonlyArray, + private defaultPreserveWhitespaces: boolean, + private i18nUseExternalIds: boolean, + private enableI18nLegacyMessageIdFormat: boolean, + private usePoisonedData: boolean, + private i18nNormalizeLineEndingsInICUs: boolean, + private moduleResolver: ModuleResolver, + private cycleAnalyzer: CycleAnalyzer, + private cycleHandlingStrategy: CycleHandlingStrategy, + private refEmitter: ReferenceEmitter, + private referencesRegistry: ReferencesRegistry, + private depTracker: DependencyTracker | null, + private injectableRegistry: InjectableClassRegistry, + private semanticDepGraphUpdater: SemanticDepGraphUpdater | null, + private annotateForClosureCompiler: boolean, + private perf: PerfRecorder, + private hostDirectivesResolver: HostDirectivesResolver, + private importTracker: ImportedSymbolsTracker, + private includeClassMetadata: boolean, + private readonly compilationMode: CompilationMode, + private readonly deferredSymbolTracker: DeferredSymbolTracker, + private readonly forbidOrphanRendering: boolean, + private readonly enableBlockSyntax: boolean, + private readonly localCompilationExtraImportsTracker: LocalCompilationExtraImportsTracker | null, + ) { this.extractTemplateOptions = { enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat, i18nNormalizeLineEndingsInICUs: this.i18nNormalizeLineEndingsInICUs, @@ -108,20 +265,22 @@ export class ComponentDecoratorHandler implements * thrown away, and the parsed template is reused during the analyze phase. */ private preanalyzeTemplateCache = new Map(); - private preanalyzeStylesCache = new Map(); + private preanalyzeStylesCache = new Map(); private extractTemplateOptions: { - enableI18nLegacyMessageIdFormat: boolean, - i18nNormalizeLineEndingsInICUs: boolean, - usePoisonedData: boolean, - enableBlockSyntax: boolean, + enableI18nLegacyMessageIdFormat: boolean; + i18nNormalizeLineEndingsInICUs: boolean; + usePoisonedData: boolean; + enableBlockSyntax: boolean; }; readonly precedence = HandlerPrecedence.PRIMARY; readonly name = 'ComponentDecoratorHandler'; - - detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult|undefined { + detect( + node: ClassDeclaration, + decorators: Decorator[] | null, + ): DetectResult | undefined { if (!decorators) { return undefined; } @@ -137,7 +296,7 @@ export class ComponentDecoratorHandler implements } } - preanalyze(node: ClassDeclaration, decorator: Readonly): Promise|undefined { + preanalyze(node: ClassDeclaration, decorator: Readonly): Promise | undefined { // In preanalyze, resource URLs associated with the component are asynchronously preloaded via // the resourceLoader. This is the only time async operations are allowed for a component. // These resources are: @@ -158,7 +317,7 @@ export class ComponentDecoratorHandler implements const component = reflectObjectLiteral(meta); const containingFile = node.getSourceFile().fileName; - const resolveStyleUrl = (styleUrl: string): Promise|undefined => { + const resolveStyleUrl = (styleUrl: string): Promise | undefined => { try { const resourceUrl = this.resourceLoader.resolve(styleUrl, containingFile); return this.resourceLoader.preload(resourceUrl, {type: 'style', containingFile}); @@ -170,19 +329,27 @@ export class ComponentDecoratorHandler implements }; // A Promise that waits for the template and all ed styles within it to be preloaded. - const templateAndTemplateStyleResources = - preloadAndParseTemplate( - this.evaluator, this.resourceLoader, this.depTracker, this.preanalyzeTemplateCache, - node, decorator, component, containingFile, this.defaultPreserveWhitespaces, - this.extractTemplateOptions, this.compilationMode) - .then((template: ParsedTemplateWithSource|null): Promise|undefined => { - if (template === null) { - return undefined; - } - - return Promise.all(template.styleUrls.map(styleUrl => resolveStyleUrl(styleUrl))) - .then(() => undefined); - }); + const templateAndTemplateStyleResources = preloadAndParseTemplate( + this.evaluator, + this.resourceLoader, + this.depTracker, + this.preanalyzeTemplateCache, + node, + decorator, + component, + containingFile, + this.defaultPreserveWhitespaces, + this.extractTemplateOptions, + this.compilationMode, + ).then((template: ParsedTemplateWithSource | null): Promise | undefined => { + if (template === null) { + return undefined; + } + + return Promise.all(template.styleUrls.map((styleUrl) => resolveStyleUrl(styleUrl))).then( + () => undefined, + ); + }); // Extract all the styleUrls in the decorator. const componentStyleUrls = extractComponentStyleUrls(this.evaluator, component); @@ -194,41 +361,51 @@ export class ComponentDecoratorHandler implements if (litStyles === null) { this.preanalyzeStylesCache.set(node, null); } else { - inlineStyles = Promise - .all(litStyles.map( - style => this.resourceLoader.preprocessInline( - style, {type: 'style', containingFile}))) - .then(styles => { - this.preanalyzeStylesCache.set(node, styles); - }); + inlineStyles = Promise.all( + litStyles.map((style) => + this.resourceLoader.preprocessInline(style, {type: 'style', containingFile}), + ), + ).then((styles) => { + this.preanalyzeStylesCache.set(node, styles); + }); } } else { this.preanalyzeStylesCache.set(node, null); } // Wait for both the template and all styleUrl resources to resolve. - return Promise - .all([ - templateAndTemplateStyleResources, inlineStyles, - ...componentStyleUrls.map(styleUrl => resolveStyleUrl(styleUrl.url)) - ]) - .then(() => undefined); + return Promise.all([ + templateAndTemplateStyleResources, + inlineStyles, + ...componentStyleUrls.map((styleUrl) => resolveStyleUrl(styleUrl.url)), + ]).then(() => undefined); } - analyze(node: ClassDeclaration, decorator: Readonly): - AnalysisOutput { + analyze( + node: ClassDeclaration, + decorator: Readonly, + ): AnalysisOutput { this.perf.eventCount(PerfEvent.AnalyzeComponent); const containingFile = node.getSourceFile().fileName; this.literalCache.delete(decorator); - let diagnostics: ts.Diagnostic[]|undefined; + let diagnostics: ts.Diagnostic[] | undefined; let isPoisoned = false; // @Component inherits @Directive, so begin by extracting the @Directive metadata and building // on it. const directiveResult = extractDirectiveMetadata( - node, decorator, this.reflector, this.importTracker, this.evaluator, this.refEmitter, - this.referencesRegistry, this.isCore, this.annotateForClosureCompiler, this.compilationMode, - this.elementSchemaRegistry.getDefaultComponentElementName()); + node, + decorator, + this.reflector, + this.importTracker, + this.evaluator, + this.refEmitter, + this.referencesRegistry, + this.isCore, + this.annotateForClosureCompiler, + this.compilationMode, + this.elementSchemaRegistry.getDefaultComponentElementName(), + ); if (directiveResult === undefined) { // `extractDirectiveMetadata` returns undefined when the @Directive has `jit: true`. In this // case, compilation of the decorator is skipped. Returning an empty object signifies @@ -237,87 +414,119 @@ export class ComponentDecoratorHandler implements } // Next, read the `@Component`-specific fields. - const {decorator: component, metadata, inputs, outputs, hostDirectives, rawHostDirectives} = - directiveResult; + const { + decorator: component, + metadata, + inputs, + outputs, + hostDirectives, + rawHostDirectives, + } = directiveResult; const encapsulation: number = - (this.compilationMode !== CompilationMode.LOCAL ? - resolveEnumValue(this.evaluator, component, 'encapsulation', 'ViewEncapsulation') : - resolveEncapsulationEnumValueLocally(component.get('encapsulation'))) ?? - ViewEncapsulation.Emulated; + (this.compilationMode !== CompilationMode.LOCAL + ? resolveEnumValue(this.evaluator, component, 'encapsulation', 'ViewEncapsulation') + : resolveEncapsulationEnumValueLocally(component.get('encapsulation'))) ?? + ViewEncapsulation.Emulated; - let changeDetection: number|o.Expression|null = null; + let changeDetection: number | o.Expression | null = null; if (this.compilationMode !== CompilationMode.LOCAL) { - changeDetection = - resolveEnumValue(this.evaluator, component, 'changeDetection', 'ChangeDetectionStrategy'); + changeDetection = resolveEnumValue( + this.evaluator, + component, + 'changeDetection', + 'ChangeDetectionStrategy', + ); } else if (component.has('changeDetection')) { changeDetection = new o.WrappedNodeExpr(component.get('changeDetection')!); } - let animations: o.Expression|null = null; - let animationTriggerNames: AnimationTriggerNames|null = null; + let animations: o.Expression | null = null; + let animationTriggerNames: AnimationTriggerNames | null = null; if (component.has('animations')) { const animationExpression = component.get('animations')!; animations = new o.WrappedNodeExpr(animationExpression); - const animationsValue = - this.evaluator.evaluate(animationExpression, animationTriggerResolver); + const animationsValue = this.evaluator.evaluate( + animationExpression, + animationTriggerResolver, + ); animationTriggerNames = {includesDynamicAnimations: false, staticTriggerNames: []}; collectAnimationNames(animationsValue, animationTriggerNames); } // Go through the root directories for this project, and select the one with the smallest // relative path representation. - const relativeContextFilePath = this.rootDirs.reduce((previous, rootDir) => { - const candidate = relative(absoluteFrom(rootDir), absoluteFrom(containingFile)); - if (previous === undefined || candidate.length < previous.length) { - return candidate; - } else { - return previous; - } - }, undefined)!; - + const relativeContextFilePath = this.rootDirs.reduce( + (previous, rootDir) => { + const candidate = relative(absoluteFrom(rootDir), absoluteFrom(containingFile)); + if (previous === undefined || candidate.length < previous.length) { + return candidate; + } else { + return previous; + } + }, + undefined, + )!; // Note that we could technically combine the `viewProvidersRequiringFactory` and // `providersRequiringFactory` into a single set, but we keep the separate so that // we can distinguish where an error is coming from when logging the diagnostics in `resolve`. - let viewProvidersRequiringFactory: Set>|null = null; - let providersRequiringFactory: Set>|null = null; - let wrappedViewProviders: o.Expression|null = null; + let viewProvidersRequiringFactory: Set> | null = null; + let providersRequiringFactory: Set> | null = null; + let wrappedViewProviders: o.Expression | null = null; if (component.has('viewProviders')) { const viewProviders = component.get('viewProviders')!; - viewProvidersRequiringFactory = - resolveProvidersRequiringFactory(viewProviders, this.reflector, this.evaluator); + viewProvidersRequiringFactory = resolveProvidersRequiringFactory( + viewProviders, + this.reflector, + this.evaluator, + ); wrappedViewProviders = new o.WrappedNodeExpr( - this.annotateForClosureCompiler ? wrapFunctionExpressionsInParens(viewProviders) : - viewProviders); + this.annotateForClosureCompiler + ? wrapFunctionExpressionsInParens(viewProviders) + : viewProviders, + ); } if (component.has('providers')) { providersRequiringFactory = resolveProvidersRequiringFactory( - component.get('providers')!, this.reflector, this.evaluator); + component.get('providers')!, + this.reflector, + this.evaluator, + ); } - let resolvedImports: Reference[]|null = null; - let resolvedDeferredImports: Reference[]|null = null; + let resolvedImports: Reference[] | null = null; + let resolvedDeferredImports: Reference[] | null = null; - let rawImports: ts.Expression|null = component.get('imports') ?? null; - let rawDeferredImports: ts.Expression|null = component.get('deferredImports') ?? null; + let rawImports: ts.Expression | null = component.get('imports') ?? null; + let rawDeferredImports: ts.Expression | null = component.get('deferredImports') ?? null; if ((rawImports || rawDeferredImports) && !metadata.isStandalone) { if (diagnostics === undefined) { diagnostics = []; } const importsField = rawImports ? 'imports' : 'deferredImports'; - diagnostics.push(makeDiagnostic( - ErrorCode.COMPONENT_NOT_STANDALONE, component.get(importsField)!, + diagnostics.push( + makeDiagnostic( + ErrorCode.COMPONENT_NOT_STANDALONE, + component.get(importsField)!, `'${importsField}' is only valid on a component that is standalone.`, - [makeRelatedInformation( - node.name, `Did you forget to add 'standalone: true' to this @Component?`)])); + [ + makeRelatedInformation( + node.name, + `Did you forget to add 'standalone: true' to this @Component?`, + ), + ], + ), + ); // Poison the component so that we don't spam further template type-checking errors that // result from misconfigured imports. isPoisoned = true; } else if ( - this.compilationMode !== CompilationMode.LOCAL && (rawImports || rawDeferredImports)) { + this.compilationMode !== CompilationMode.LOCAL && + (rawImports || rawDeferredImports) + ) { const importResolvers = combineResolvers([ createModuleWithProvidersResolver(this.reflector, this.isCore), forwardRefResolver, @@ -328,8 +537,11 @@ export class ComponentDecoratorHandler implements if (rawImports) { const expr = rawImports; const imported = this.evaluator.evaluate(expr, importResolvers); - const {imports: flattened, diagnostics} = - validateAndFlattenComponentImports(imported, expr, false /* isDeferred */); + const {imports: flattened, diagnostics} = validateAndFlattenComponentImports( + imported, + expr, + false /* isDeferred */, + ); importDiagnostics.push(...diagnostics); resolvedImports = flattened; rawImports = expr; @@ -338,8 +550,11 @@ export class ComponentDecoratorHandler implements if (rawDeferredImports) { const expr = rawDeferredImports; const imported = this.evaluator.evaluate(expr, importResolvers); - const {imports: flattened, diagnostics} = - validateAndFlattenComponentImports(imported, expr, true /* isDeferred */); + const {imports: flattened, diagnostics} = validateAndFlattenComponentImports( + imported, + expr, + true /* isDeferred */, + ); importDiagnostics.push(...diagnostics); resolvedDeferredImports = flattened; rawDeferredImports = expr; @@ -354,14 +569,18 @@ export class ComponentDecoratorHandler implements } } - let schemas: SchemaMetadata[]|null = null; + let schemas: SchemaMetadata[] | null = null; if (component.has('schemas') && !metadata.isStandalone) { if (diagnostics === undefined) { diagnostics = []; } - diagnostics.push(makeDiagnostic( - ErrorCode.COMPONENT_NOT_STANDALONE, component.get('schemas')!, - `'schemas' is only valid on a component that is standalone.`)); + diagnostics.push( + makeDiagnostic( + ErrorCode.COMPONENT_NOT_STANDALONE, + component.get('schemas')!, + `'schemas' is only valid on a component that is standalone.`, + ), + ); } else if (this.compilationMode !== CompilationMode.LOCAL && component.has('schemas')) { schemas = extractSchemas(component.get('schemas')!, this.evaluator, 'Component'); } else if (metadata.isStandalone) { @@ -382,21 +601,35 @@ export class ComponentDecoratorHandler implements template = preanalyzed; } else { const templateDecl = parseTemplateDeclaration( - node, decorator, component, containingFile, this.evaluator, this.depTracker, - this.resourceLoader, this.defaultPreserveWhitespaces); + node, + decorator, + component, + containingFile, + this.evaluator, + this.depTracker, + this.resourceLoader, + this.defaultPreserveWhitespaces, + ); template = extractTemplate( - node, templateDecl, this.evaluator, this.depTracker, this.resourceLoader, { - enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat, - i18nNormalizeLineEndingsInICUs: this.i18nNormalizeLineEndingsInICUs, - usePoisonedData: this.usePoisonedData, - enableBlockSyntax: this.enableBlockSyntax, - }, - this.compilationMode); + node, + templateDecl, + this.evaluator, + this.depTracker, + this.resourceLoader, + { + enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat, + i18nNormalizeLineEndingsInICUs: this.i18nNormalizeLineEndingsInICUs, + usePoisonedData: this.usePoisonedData, + enableBlockSyntax: this.enableBlockSyntax, + }, + this.compilationMode, + ); } - const templateResource = - template.declaration.isInline ? {path: null, expression: component.get('template')!} : { + const templateResource = template.declaration.isInline + ? {path: null, expression: component.get('template')!} + : { path: absoluteFrom(template.declaration.resolvedTemplateUrl), - expression: template.sourceMapping.node + expression: template.sourceMapping.node, }; // Figure out the set of styles. The ordering here is important: external resources (styleUrls) @@ -407,7 +640,7 @@ export class ComponentDecoratorHandler implements const styleResources = extractStyleResources(this.resourceLoader, component, containingFile); const styleUrls: StyleUrlMeta[] = [ ...extractComponentStyleUrls(this.evaluator, component), - ..._extractTemplateStyleUrls(template) + ..._extractTemplateStyleUrls(template), ]; for (const styleUrl of styleUrls) { @@ -430,12 +663,16 @@ export class ComponentDecoratorHandler implements diagnostics = []; } const resourceType = - styleUrl.source === ResourceTypeForDiagnostics.StylesheetFromDecorator ? - ResourceTypeForDiagnostics.StylesheetFromDecorator : - ResourceTypeForDiagnostics.StylesheetFromTemplate; + styleUrl.source === ResourceTypeForDiagnostics.StylesheetFromDecorator + ? ResourceTypeForDiagnostics.StylesheetFromDecorator + : ResourceTypeForDiagnostics.StylesheetFromTemplate; diagnostics.push( - makeResourceNotFoundError(styleUrl.url, styleUrl.nodeForError, resourceType) - .toDiagnostic()); + makeResourceNotFoundError( + styleUrl.url, + styleUrl.nodeForError, + resourceType, + ).toDiagnostic(), + ); } } @@ -445,14 +682,18 @@ export class ComponentDecoratorHandler implements if (diagnostics === undefined) { diagnostics = []; } - diagnostics.push(makeDiagnostic( - ErrorCode.COMPONENT_INVALID_SHADOW_DOM_SELECTOR, component.get('selector')!, - selectorError)); + diagnostics.push( + makeDiagnostic( + ErrorCode.COMPONENT_INVALID_SHADOW_DOM_SELECTOR, + component.get('selector')!, + selectorError, + ), + ); } } // If inline styles were preprocessed use those - let inlineStyles: string[]|null = null; + let inlineStyles: string[] | null = null; if (this.preanalyzeStylesCache.has(node)) { inlineStyles = this.preanalyzeStylesCache.get(node)!; this.preanalyzeStylesCache.delete(node); @@ -484,7 +725,7 @@ export class ComponentDecoratorHandler implements // (if it exists) and populate the `DeferredSymbolTracker` state. These operations are safe // for the local compilation mode, since they don't require accessing/resolving symbols // outside of the current source file. - let explicitlyDeferredTypes: R3DeferPerComponentDependency[]|null = null; + let explicitlyDeferredTypes: R3DeferPerComponentDependency[] | null = null; if (metadata.isStandalone && rawDeferredImports !== null) { const deferredTypes = this.collectExplicitlyDeferredSymbols(rawDeferredImports); for (const [deferredType, importDetails] of deferredTypes) { @@ -495,7 +736,11 @@ export class ComponentDecoratorHandler implements isDefaultImport: isDefaultImport(importDetails.node), }); this.deferredSymbolTracker.markAsDeferrableCandidate( - deferredType, importDetails.node, node, true /* isExplicitlyDeferred */); + deferredType, + importDetails.node, + node, + true /* isExplicitlyDeferred */, + ); } } @@ -523,14 +768,21 @@ export class ComponentDecoratorHandler implements rawImports: rawImports !== null ? new o.WrappedNodeExpr(rawImports) : undefined, }, typeCheckMeta: extractDirectiveTypeCheckMeta(node, inputs, this.reflector), - classMetadata: this.includeClassMetadata ? - extractClassMetadata( - node, this.reflector, this.isCore, this.annotateForClosureCompiler, - dec => transformDecoratorResources(dec, component, styles, template)) : - null, + classMetadata: this.includeClassMetadata + ? extractClassMetadata( + node, + this.reflector, + this.isCore, + this.annotateForClosureCompiler, + (dec) => transformDecoratorResources(dec, component, styles, template), + ) + : null, classDebugInfo: extractClassDebugInfo( - node, this.reflector, this.rootDirs, - /* forbidOrphanRenderering */ this.forbidOrphanRendering), + node, + this.reflector, + this.rootDirs, + /* forbidOrphanRenderering */ this.forbidOrphanRendering, + ), template, providersRequiringFactory, viewProvidersRequiringFactory, @@ -548,7 +800,7 @@ export class ComponentDecoratorHandler implements resolvedDeferredImports, explicitlyDeferredTypes, schemas, - decorator: decorator?.node as ts.Decorator | null ?? null, + decorator: (decorator?.node as ts.Decorator | null) ?? null, }, diagnostics, }; @@ -560,8 +812,14 @@ export class ComponentDecoratorHandler implements const typeParameters = extractSemanticTypeParameters(node); return new ComponentSymbol( - node, analysis.meta.selector, analysis.inputs, analysis.outputs, analysis.meta.exportAs, - analysis.typeCheckMeta, typeParameters); + node, + analysis.meta.selector, + analysis.inputs, + analysis.outputs, + analysis.meta.exportAs, + analysis.typeCheckMeta, + typeParameters, + ); } register(node: ClassDeclaration, analysis: ComponentAnalysisData): void { @@ -577,7 +835,7 @@ export class ComponentDecoratorHandler implements exportAs: analysis.meta.exportAs, inputs: analysis.inputs, outputs: analysis.outputs, - queries: analysis.meta.queries.map(query => query.propertyName), + queries: analysis.meta.queries.map((query) => query.propertyName), isComponent: true, baseClass: analysis.baseClass, hostDirectives: analysis.hostDirectives, @@ -604,7 +862,10 @@ export class ComponentDecoratorHandler implements } index( - context: IndexingContext, node: ClassDeclaration, analysis: Readonly) { + context: IndexingContext, + node: ClassDeclaration, + analysis: Readonly, + ) { if (analysis.isPoisoned && !this.usePoisonedData) { return null; } @@ -613,10 +874,11 @@ export class ComponentDecoratorHandler implements const matcher = new SelectorMatcher(); if (scope !== null) { let {dependencies, isPoisoned} = - scope.kind === ComponentScopeKind.NgModule ? scope.compilation : scope; - if ((isPoisoned || - (scope.kind === ComponentScopeKind.NgModule && scope.exported.isPoisoned)) && - !this.usePoisonedData) { + scope.kind === ComponentScopeKind.NgModule ? scope.compilation : scope; + if ( + (isPoisoned || (scope.kind === ComponentScopeKind.NgModule && scope.exported.isPoisoned)) && + !this.usePoisonedData + ) { // Don't bother indexing components which had erroneous scopes, unless specifically // requested. return null; @@ -624,8 +886,10 @@ export class ComponentDecoratorHandler implements for (const dep of dependencies) { if (dep.kind === MetaKind.Directive && dep.selector !== null) { - matcher.addSelectables( - CssSelector.parse(dep.selector), [...this.hostDirectivesResolver.resolve(dep), dep]); + matcher.addSelectables(CssSelector.parse(dep.selector), [ + ...this.hostDirectivesResolver.resolve(dep), + dep, + ]); } } } @@ -644,8 +908,11 @@ export class ComponentDecoratorHandler implements return null; } - typeCheck(ctx: TypeCheckContext, node: ClassDeclaration, meta: Readonly): - void { + typeCheck( + ctx: TypeCheckContext, + node: ClassDeclaration, + meta: Readonly, + ): void { if (this.typeCheckScopeRegistry === null || !ts.isClassDeclaration(node)) { return; } @@ -661,26 +928,38 @@ export class ComponentDecoratorHandler implements const binder = new R3TargetBinder(scope.matcher); ctx.addTemplate( - new Reference(node), binder, meta.template.diagNodes, scope.pipes, scope.schemas, - meta.template.sourceMapping, meta.template.file, meta.template.errors, - meta.meta.isStandalone, meta.meta.template.preserveWhitespaces ?? false); + new Reference(node), + binder, + meta.template.diagNodes, + scope.pipes, + scope.schemas, + meta.template.sourceMapping, + meta.template.file, + meta.template.errors, + meta.meta.isStandalone, + meta.meta.template.preserveWhitespaces ?? false, + ); } extendedTemplateCheck( - component: ts.ClassDeclaration, - extendedTemplateChecker: ExtendedTemplateChecker): ts.Diagnostic[] { + component: ts.ClassDeclaration, + extendedTemplateChecker: ExtendedTemplateChecker, + ): ts.Diagnostic[] { return extendedTemplateChecker.getDiagnosticsForComponent(component); } templateSemanticsCheck( - component: ts.ClassDeclaration, - templateSemanticsChecker: TemplateSemanticsChecker): ts.Diagnostic[] { + component: ts.ClassDeclaration, + templateSemanticsChecker: TemplateSemanticsChecker, + ): ts.Diagnostic[] { return templateSemanticsChecker.getDiagnosticsForComponent(component); } resolve( - node: ClassDeclaration, analysis: Readonly, - symbol: ComponentSymbol): ResolveResult { + node: ClassDeclaration, + analysis: Readonly, + symbol: ComponentSymbol, + ): ResolveResult { const metadata = analysis.meta as Readonly>; const diagnostics: ts.Diagnostic[] = []; const context = getSourceFile(node); @@ -689,18 +968,22 @@ export class ComponentDecoratorHandler implements // the `@Component.deferredImports` field, but those imports contain other symbols // and thus the declaration can not be removed. This diagnostics is shared between local and // global compilation modes. - const nonRemovableImports = - this.deferredSymbolTracker.getNonRemovableDeferredImports(context, node); + const nonRemovableImports = this.deferredSymbolTracker.getNonRemovableDeferredImports( + context, + node, + ); if (nonRemovableImports.length > 0) { for (const importDecl of nonRemovableImports) { const diagnostic = makeDiagnostic( - ErrorCode.DEFERRED_DEPENDENCY_IMPORTED_EAGERLY, importDecl, - `This import contains symbols that are used both inside and outside of the ` + - `\`@Component.deferredImports\` fields in the file. This renders all these ` + - `defer imports useless as this import remains and its module is eagerly loaded. ` + - `To fix this, make sure that all symbols from the import are *only* used within ` + - `\`@Component.deferredImports\` arrays and there are no other references to those ` + - `symbols present in this file.`); + ErrorCode.DEFERRED_DEPENDENCY_IMPORTED_EAGERLY, + importDecl, + `This import contains symbols that are used both inside and outside of the ` + + `\`@Component.deferredImports\` fields in the file. This renders all these ` + + `defer imports useless as this import remains and its module is eagerly loaded. ` + + `To fix this, make sure that all symbols from the import are *only* used within ` + + `\`@Component.deferredImports\` arrays and there are no other references to those ` + + `symbols present in this file.`, + ); diagnostics.push(diagnostic); } return {diagnostics}; @@ -712,9 +995,10 @@ export class ComponentDecoratorHandler implements // Initial value in local compilation mode. data = { declarations: EMPTY_ARRAY, - declarationListEmitMode: (!analysis.meta.isStandalone || analysis.rawImports !== null) ? - DeclarationListEmitMode.RuntimeResolved : - DeclarationListEmitMode.Direct, + declarationListEmitMode: + !analysis.meta.isStandalone || analysis.rawImports !== null + ? DeclarationListEmitMode.RuntimeResolved + : DeclarationListEmitMode.Direct, deferPerBlockDependencies: this.locateDeferBlocksWithoutScope(analysis.template), deferBlockDepsEmitMode: DeferBlockDepsEmitMode.PerComponent, deferrableDeclToImportDecl: new Map(), @@ -776,10 +1060,16 @@ export class ComponentDecoratorHandler implements // Make sure that `@Component.imports` and `@Component.deferredImports` do not have // the same dependencies. - if (metadata.isStandalone && analysis.rawDeferredImports !== null && - explicitlyDeferredDependencies.length > 0) { + if ( + metadata.isStandalone && + analysis.rawDeferredImports !== null && + explicitlyDeferredDependencies.length > 0 + ) { const diagnostic = validateNoImportOverlap( - dependencies, explicitlyDeferredDependencies, analysis.rawDeferredImports); + dependencies, + explicitlyDeferredDependencies, + analysis.rawDeferredImports, + ); if (diagnostic !== null) { diagnostics.push(diagnostic); } @@ -839,7 +1129,7 @@ export class ComponentDecoratorHandler implements } } - const declarations = new Map(); + const declarations = new Map(); // Transform the dependencies list, filtering out unused dependencies. for (const dep of allDependencies) { @@ -855,7 +1145,10 @@ export class ComponentDecoratorHandler implements } const dirType = this.refEmitter.emit(dep.ref, context); assertSuccessfulReferenceEmit( - dirType, node.name, dep.isComponent ? 'component' : 'directive'); + dirType, + node.name, + dep.isComponent ? 'component' : 'directive', + ); declarations.set(dep.ref.node, { kind: R3TemplateDependencyKind.Directive, @@ -898,20 +1191,21 @@ export class ComponentDecoratorHandler implements } } - const getSemanticReference = (decl: UsedDirective|UsedPipe) => - this.semanticDepGraphUpdater!.getSemanticReference(decl.ref.node, decl.type); + const getSemanticReference = (decl: UsedDirective | UsedPipe) => + this.semanticDepGraphUpdater!.getSemanticReference(decl.ref.node, decl.type); if (this.semanticDepGraphUpdater !== null) { - symbol.usedDirectives = - Array.from(declarations.values()).filter(isUsedDirective).map(getSemanticReference); - symbol.usedPipes = - Array.from(declarations.values()).filter(isUsedPipe).map(getSemanticReference); + symbol.usedDirectives = Array.from(declarations.values()) + .filter(isUsedDirective) + .map(getSemanticReference); + symbol.usedPipes = Array.from(declarations.values()) + .filter(isUsedPipe) + .map(getSemanticReference); } - const eagerDeclarations = Array.from(declarations.values()) - .filter( - decl => decl.kind === R3TemplateDependencyKind.NgModule || - eagerlyUsed.has(decl.ref.node)); + const eagerDeclarations = Array.from(declarations.values()).filter( + (decl) => decl.kind === R3TemplateDependencyKind.NgModule || eagerlyUsed.has(decl.ref.node), + ); // Process information related to defer blocks if (this.compilationMode !== CompilationMode.LOCAL) { @@ -947,7 +1241,7 @@ export class ComponentDecoratorHandler implements // the reference is `synthetic`, implying it came from _any_ foreign function resolver, // including the `forwardRef` resolver. const standaloneImportMayBeForwardDeclared = - analysis.resolvedImports !== null && analysis.resolvedImports.some(ref => ref.synthetic); + analysis.resolvedImports !== null && analysis.resolvedImports.some((ref) => ref.synthetic); const cycleDetected = cyclesFromDirectives.size !== 0 || cyclesFromPipes.size !== 0; if (!cycleDetected) { @@ -960,19 +1254,24 @@ export class ComponentDecoratorHandler implements // Check whether the dependencies arrays in ɵcmp need to be wrapped in a closure. // This is required if any dependency reference is to a declaration in the same file // but declared after this component. - const declarationIsForwardDeclared = eagerDeclarations.some( - decl => isExpressionForwardReference(decl.type, node.name, context)); - - if (this.compilationMode !== CompilationMode.LOCAL && - (declarationIsForwardDeclared || standaloneImportMayBeForwardDeclared)) { + const declarationIsForwardDeclared = eagerDeclarations.some((decl) => + isExpressionForwardReference(decl.type, node.name, context), + ); + + if ( + this.compilationMode !== CompilationMode.LOCAL && + (declarationIsForwardDeclared || standaloneImportMayBeForwardDeclared) + ) { data.declarationListEmitMode = DeclarationListEmitMode.Closure; } data.declarations = eagerDeclarations; // Register extra local imports. - if (this.compilationMode === CompilationMode.LOCAL && - this.localCompilationExtraImportsTracker !== null) { + if ( + this.compilationMode === CompilationMode.LOCAL && + this.localCompilationExtraImportsTracker !== null + ) { // In global compilation mode `eagerDeclarations` contains "all" the component // dependencies, whose import statements will be added to the file. In local compilation // mode `eagerDeclarations` only includes the "local" dependencies, meaning those that are @@ -984,7 +1283,9 @@ export class ComponentDecoratorHandler implements for (const {type} of eagerDeclarations) { if (type instanceof ExternalExpr && type.value.moduleName) { this.localCompilationExtraImportsTracker.addImportForFile( - context, type.value.moduleName); + context, + type.value.moduleName, + ); } } } @@ -994,39 +1295,51 @@ export class ComponentDecoratorHandler implements // create a cycle. Instead, mark this component as requiring remote scoping, so that the // NgModule file will take care of setting the directives for the component. this.scopeRegistry.setComponentRemoteScope( - node, eagerDeclarations.filter(isUsedDirective).map(dir => dir.ref), - eagerDeclarations.filter(isUsedPipe).map(pipe => pipe.ref)); + node, + eagerDeclarations.filter(isUsedDirective).map((dir) => dir.ref), + eagerDeclarations.filter(isUsedPipe).map((pipe) => pipe.ref), + ); symbol.isRemotelyScoped = true; // If a semantic graph is being tracked, record the fact that this component is remotely // scoped with the declaring NgModule symbol as the NgModule's emit becomes dependent on // the directive/pipe usages of this component. - if (this.semanticDepGraphUpdater !== null && scope.kind === ComponentScopeKind.NgModule && - scope.ngModule !== null) { + if ( + this.semanticDepGraphUpdater !== null && + scope.kind === ComponentScopeKind.NgModule && + scope.ngModule !== null + ) { const moduleSymbol = this.semanticDepGraphUpdater.getSymbol(scope.ngModule); if (!(moduleSymbol instanceof NgModuleSymbol)) { throw new Error( - `AssertionError: Expected ${scope.ngModule.name} to be an NgModuleSymbol.`); + `AssertionError: Expected ${scope.ngModule.name} to be an NgModuleSymbol.`, + ); } moduleSymbol.addRemotelyScopedComponent( - symbol, symbol.usedDirectives, symbol.usedPipes); + symbol, + symbol.usedDirectives, + symbol.usedPipes, + ); } } else { // We are not able to handle this cycle so throw an error. const relatedMessages: ts.DiagnosticRelatedInformation[] = []; for (const [dir, cycle] of cyclesFromDirectives) { relatedMessages.push( - makeCyclicImportInfo(dir.ref, dir.isComponent ? 'component' : 'directive', cycle)); + makeCyclicImportInfo(dir.ref, dir.isComponent ? 'component' : 'directive', cycle), + ); } for (const [pipe, cycle] of cyclesFromPipes) { relatedMessages.push(makeCyclicImportInfo(pipe.ref, 'pipe', cycle)); } throw new FatalDiagnosticError( - ErrorCode.IMPORT_CYCLE_DETECTED, node, - 'One or more import cycles would need to be created to compile this component, ' + - 'which is not supported by the current compiler configuration.', - relatedMessages); + ErrorCode.IMPORT_CYCLE_DETECTED, + node, + 'One or more import cycles would need to be created to compile this component, ' + + 'which is not supported by the current compiler configuration.', + relatedMessages, + ); } } } else { @@ -1040,44 +1353,70 @@ export class ComponentDecoratorHandler implements // Validate `@Component.imports` and `@Component.deferredImports` fields. if (analysis.resolvedImports !== null && analysis.rawImports !== null) { const importDiagnostics = validateStandaloneImports( - analysis.resolvedImports, analysis.rawImports, this.metaReader, this.scopeReader, - false /* isDeferredImport */); + analysis.resolvedImports, + analysis.rawImports, + this.metaReader, + this.scopeReader, + false /* isDeferredImport */, + ); diagnostics.push(...importDiagnostics); } if (analysis.resolvedDeferredImports !== null && analysis.rawDeferredImports !== null) { const importDiagnostics = validateStandaloneImports( - analysis.resolvedDeferredImports, analysis.rawDeferredImports, this.metaReader, - this.scopeReader, true /* isDeferredImport */); + analysis.resolvedDeferredImports, + analysis.rawDeferredImports, + this.metaReader, + this.scopeReader, + true /* isDeferredImport */, + ); diagnostics.push(...importDiagnostics); } - if (analysis.providersRequiringFactory !== null && - analysis.meta.providers instanceof o.WrappedNodeExpr) { + if ( + analysis.providersRequiringFactory !== null && + analysis.meta.providers instanceof o.WrappedNodeExpr + ) { const providerDiagnostics = getProviderDiagnostics( - analysis.providersRequiringFactory, analysis.meta.providers!.node, - this.injectableRegistry); + analysis.providersRequiringFactory, + analysis.meta.providers!.node, + this.injectableRegistry, + ); diagnostics.push(...providerDiagnostics); } - if (analysis.viewProvidersRequiringFactory !== null && - analysis.meta.viewProviders instanceof o.WrappedNodeExpr) { + if ( + analysis.viewProvidersRequiringFactory !== null && + analysis.meta.viewProviders instanceof o.WrappedNodeExpr + ) { const viewProviderDiagnostics = getProviderDiagnostics( - analysis.viewProvidersRequiringFactory, analysis.meta.viewProviders!.node, - this.injectableRegistry); + analysis.viewProvidersRequiringFactory, + analysis.meta.viewProviders!.node, + this.injectableRegistry, + ); diagnostics.push(...viewProviderDiagnostics); } const directiveDiagnostics = getDirectiveDiagnostics( - node, this.injectableRegistry, this.evaluator, this.reflector, this.scopeRegistry, - this.strictCtorDeps, 'Component'); + node, + this.injectableRegistry, + this.evaluator, + this.reflector, + this.scopeRegistry, + this.strictCtorDeps, + 'Component', + ); if (directiveDiagnostics !== null) { diagnostics.push(...directiveDiagnostics); } - const hostDirectivesDiagnostics = analysis.hostDirectives && analysis.rawHostDirectives ? - validateHostDirectives( - analysis.rawHostDirectives, analysis.hostDirectives, this.metaReader) : - null; + const hostDirectivesDiagnostics = + analysis.hostDirectives && analysis.rawHostDirectives + ? validateHostDirectives( + analysis.rawHostDirectives, + analysis.hostDirectives, + this.metaReader, + ) + : null; if (hostDirectivesDiagnostics !== null) { diagnostics.push(...hostDirectivesDiagnostics); } @@ -1090,11 +1429,16 @@ export class ComponentDecoratorHandler implements return {data}; } - xi18n(ctx: Xi18nContext, node: ClassDeclaration, analysis: Readonly): - void { + xi18n( + ctx: Xi18nContext, + node: ClassDeclaration, + analysis: Readonly, + ): void { ctx.updateFromTemplate( - analysis.template.content, analysis.template.declaration.resolvedTemplateUrl, - analysis.template.interpolationConfig ?? DEFAULT_INTERPOLATION_CONFIG); + analysis.template.content, + analysis.template.declaration.resolvedTemplateUrl, + analysis.template.interpolationConfig ?? DEFAULT_INTERPOLATION_CONFIG, + ); } updateResources(node: ClassDeclaration, analysis: ComponentAnalysisData): void { @@ -1104,8 +1448,14 @@ export class ComponentDecoratorHandler implements const templateDecl = analysis.template.declaration; if (!templateDecl.isInline) { analysis.template = extractTemplate( - node, templateDecl, this.evaluator, this.depTracker, this.resourceLoader, - this.extractTemplateOptions, this.compilationMode); + node, + templateDecl, + this.evaluator, + this.depTracker, + this.resourceLoader, + this.extractTemplateOptions, + this.compilationMode, + ); } // Update any external stylesheets and rebuild the combined 'styles' list. @@ -1133,12 +1483,15 @@ export class ComponentDecoratorHandler implements styles.push(styleText); } - analysis.meta.styles = styles.filter(s => s.trim().length > 0); + analysis.meta.styles = styles.filter((s) => s.trim().length > 0); } compileFull( - node: ClassDeclaration, analysis: Readonly, - resolution: Readonly, pool: ConstantPool): CompileResult[] { + node: ClassDeclaration, + analysis: Readonly, + resolution: Readonly, + pool: ConstantPool, + ): CompileResult[] { if (analysis.template.errors !== null && analysis.template.errors.length > 0) { return []; } @@ -1155,20 +1508,31 @@ export class ComponentDecoratorHandler implements const def = compileComponentFromMetadata(meta, pool, makeBindingParser()); const inputTransformFields = compileInputTransformFields(analysis.inputs); - const classMetadata = analysis.classMetadata !== null ? - compileComponentClassMetadata(analysis.classMetadata, perComponentDeferredDeps).toStmt() : - null; - const debugInfo = analysis.classDebugInfo !== null ? - compileClassDebugInfo(analysis.classDebugInfo).toStmt() : - null; + const classMetadata = + analysis.classMetadata !== null + ? compileComponentClassMetadata(analysis.classMetadata, perComponentDeferredDeps).toStmt() + : null; + const debugInfo = + analysis.classDebugInfo !== null + ? compileClassDebugInfo(analysis.classDebugInfo).toStmt() + : null; const deferrableImports = this.deferredSymbolTracker.getDeferrableImportDecls(); return compileResults( - fac, def, classMetadata, 'ɵcmp', inputTransformFields, deferrableImports, debugInfo); + fac, + def, + classMetadata, + 'ɵcmp', + inputTransformFields, + deferrableImports, + debugInfo, + ); } compilePartial( - node: ClassDeclaration, analysis: Readonly, - resolution: Readonly): CompileResult[] { + node: ClassDeclaration, + analysis: Readonly, + resolution: Readonly, + ): CompileResult[] { if (analysis.template.errors !== null && analysis.template.errors.length > 0) { return []; } @@ -1176,9 +1540,10 @@ export class ComponentDecoratorHandler implements content: analysis.template.content, sourceUrl: analysis.template.declaration.resolvedTemplateUrl, isInline: analysis.template.declaration.isInline, - inlineTemplateLiteralExpression: analysis.template.sourceMapping.type === 'direct' ? - new o.WrappedNodeExpr(analysis.template.sourceMapping.node) : - null, + inlineTemplateLiteralExpression: + analysis.template.sourceMapping.type === 'direct' + ? new o.WrappedNodeExpr(analysis.template.sourceMapping.node) + : null, }; const perComponentDeferredDeps = this.resolveAllDeferredDependencies(resolution); @@ -1190,17 +1555,23 @@ export class ComponentDecoratorHandler implements const fac = compileDeclareFactory(toFactoryMetadata(meta, FactoryTarget.Component)); const inputTransformFields = compileInputTransformFields(analysis.inputs); const def = compileDeclareComponentFromMetadata(meta, analysis.template, templateInfo); - const classMetadata = analysis.classMetadata !== null ? - compileComponentDeclareClassMetadata(analysis.classMetadata, perComponentDeferredDeps) - .toStmt() : - null; + const classMetadata = + analysis.classMetadata !== null + ? compileComponentDeclareClassMetadata( + analysis.classMetadata, + perComponentDeferredDeps, + ).toStmt() + : null; const deferrableImports = this.deferredSymbolTracker.getDeferrableImportDecls(); return compileResults(fac, def, classMetadata, 'ɵcmp', inputTransformFields, deferrableImports); } compileLocal( - node: ClassDeclaration, analysis: Readonly, - resolution: Readonly>, pool: ConstantPool): CompileResult[] { + node: ClassDeclaration, + analysis: Readonly, + resolution: Readonly>, + pool: ConstantPool, + ): CompileResult[] { if (analysis.template.errors !== null && analysis.template.errors.length > 0) { return []; } @@ -1223,23 +1594,33 @@ export class ComponentDecoratorHandler implements const fac = compileNgFactoryDefField(toFactoryMetadata(meta, FactoryTarget.Component)); const def = compileComponentFromMetadata(meta, pool, makeBindingParser()); const inputTransformFields = compileInputTransformFields(analysis.inputs); - const classMetadata = analysis.classMetadata !== null ? - compileComponentClassMetadata(analysis.classMetadata, deferrableTypes).toStmt() : - null; - const debugInfo = analysis.classDebugInfo !== null ? - compileClassDebugInfo(analysis.classDebugInfo).toStmt() : - null; + const classMetadata = + analysis.classMetadata !== null + ? compileComponentClassMetadata(analysis.classMetadata, deferrableTypes).toStmt() + : null; + const debugInfo = + analysis.classDebugInfo !== null + ? compileClassDebugInfo(analysis.classDebugInfo).toStmt() + : null; const deferrableImports = this.deferredSymbolTracker.getDeferrableImportDecls(); return compileResults( - fac, def, classMetadata, 'ɵcmp', inputTransformFields, deferrableImports, debugInfo); + fac, + def, + classMetadata, + 'ɵcmp', + inputTransformFields, + deferrableImports, + debugInfo, + ); } /** * Locates defer blocks in case scope information is not available. * For example, this happens in the local compilation mode. */ - private locateDeferBlocksWithoutScope(template: ComponentTemplate): - Map { + private locateDeferBlocksWithoutScope( + template: ComponentTemplate, + ): Map { const deferBlocks = new Map(); const directivelessBinder = new R3TargetBinder(new SelectorMatcher()); const bound = directivelessBinder.bind({template: template.nodes}); @@ -1256,8 +1637,9 @@ export class ComponentDecoratorHandler implements * Computes a list of deferrable symbols based on dependencies from * the `@Component.imports` field and their usage in `@defer` blocks. */ - private resolveAllDeferredDependencies(resolution: Readonly): - R3DeferPerComponentDependency[] { + private resolveAllDeferredDependencies( + resolution: Readonly, + ): R3DeferPerComponentDependency[] { const deferrableTypes: R3DeferPerComponentDependency[] = []; // Go over all dependencies of all defer blocks and update the value of // the `isDeferrable` flag and the `importPath` to reflect the current @@ -1265,7 +1647,7 @@ export class ComponentDecoratorHandler implements for (const [_, deps] of resolution.deferPerBlockDependencies) { for (const deferBlockDep of deps) { const importDecl = - resolution.deferrableDeclToImportDecl.get(deferBlockDep.declaration.node) ?? null; + resolution.deferrableDeclToImportDecl.get(deferBlockDep.declaration.node) ?? null; if (importDecl !== null && this.deferredSymbolTracker.canDefer(importDecl)) { deferBlockDep.isDeferrable = true; deferBlockDep.importPath = (importDecl.moduleSpecifier as ts.StringLiteral).text; @@ -1280,8 +1662,9 @@ export class ComponentDecoratorHandler implements /** * Collects deferrable symbols from the `@Component.deferredImports` field. */ - private collectExplicitlyDeferredSymbols(rawDeferredImports: ts.Expression): - Map { + private collectExplicitlyDeferredSymbols( + rawDeferredImports: ts.Expression, + ): Map { const deferredTypes = new Map(); if (!ts.isArrayLiteralExpression(rawDeferredImports)) { return deferredTypes; @@ -1310,7 +1693,10 @@ export class ComponentDecoratorHandler implements * @returns a `Cycle` object if a cycle would be created, otherwise `null`. */ private _checkForCyclicImport( - importedFile: ImportedFile, expr: o.Expression, origin: ts.SourceFile): Cycle|null { + importedFile: ImportedFile, + expr: o.Expression, + origin: ts.SourceFile, + ): Cycle | null { const imported = resolveImportedFile(this.moduleResolver, importedFile, expr, origin); if (imported === null) { return null; @@ -1320,7 +1706,10 @@ export class ComponentDecoratorHandler implements } private maybeRecordSyntheticImport( - importedFile: ImportedFile, expr: o.Expression, origin: ts.SourceFile): void { + importedFile: ImportedFile, + expr: o.Expression, + origin: ts.SourceFile, + ): void { const imported = resolveImportedFile(this.moduleResolver, importedFile, expr, origin); if (imported === null) { return; @@ -1334,12 +1723,12 @@ export class ComponentDecoratorHandler implements * available for the final `compile` step. */ private resolveDeferBlocks( - componentClassDecl: ClassDeclaration, - deferBlocks: Map>, - deferrableDecls: Map, - resolutionData: ComponentResolutionData, - analysisData: Readonly, - eagerlyUsedDecls: Set, + componentClassDecl: ClassDeclaration, + deferBlocks: Map>, + deferrableDecls: Map, + resolutionData: ComponentResolutionData, + analysisData: Readonly, + eagerlyUsedDecls: Set, ) { // Collect all deferred decls from all defer blocks from the entire template // to intersect with the information from the `imports` field of a particular @@ -1347,7 +1736,7 @@ export class ComponentDecoratorHandler implements const allDeferredDecls = new Set(); for (const [deferBlock, bound] of deferBlocks) { - const usedDirectives = new Set(bound.getEagerlyUsedDirectives().map(d => d.ref.node)); + const usedDirectives = new Set(bound.getEagerlyUsedDirectives().map((d) => d.ref.node)); const usedPipes = new Set(bound.getEagerlyUsedPipes()); let deps: DeferredComponentDependency[]; @@ -1362,8 +1751,10 @@ export class ComponentDecoratorHandler implements if (decl.kind === R3TemplateDependencyKind.NgModule) { continue; } - if (decl.kind === R3TemplateDependencyKind.Directive && - !usedDirectives.has(decl.ref.node)) { + if ( + decl.kind === R3TemplateDependencyKind.Directive && + !usedDirectives.has(decl.ref.node) + ) { continue; } if (decl.kind === R3TemplateDependencyKind.Pipe && !usedPipes.has(decl.name)) { @@ -1390,13 +1781,23 @@ export class ComponentDecoratorHandler implements if (analysisData.meta.isStandalone) { if (analysisData.rawImports !== null) { this.registerDeferrableCandidates( - componentClassDecl, analysisData.rawImports, false /* isDeferredImport */, - allDeferredDecls, eagerlyUsedDecls, resolutionData); + componentClassDecl, + analysisData.rawImports, + false /* isDeferredImport */, + allDeferredDecls, + eagerlyUsedDecls, + resolutionData, + ); } if (analysisData.rawDeferredImports !== null) { this.registerDeferrableCandidates( - componentClassDecl, analysisData.rawDeferredImports, true /* isDeferredImport */, - allDeferredDecls, eagerlyUsedDecls, resolutionData); + componentClassDecl, + analysisData.rawDeferredImports, + true /* isDeferredImport */, + allDeferredDecls, + eagerlyUsedDecls, + resolutionData, + ); } } } @@ -1407,9 +1808,13 @@ export class ComponentDecoratorHandler implements * candidates. */ private registerDeferrableCandidates( - componentClassDecl: ClassDeclaration, importsExpr: ts.Expression, isDeferredImport: boolean, - allDeferredDecls: Set, eagerlyUsedDecls: Set, - resolutionData: ComponentResolutionData) { + componentClassDecl: ClassDeclaration, + importsExpr: ts.Expression, + isDeferredImport: boolean, + allDeferredDecls: Set, + eagerlyUsedDecls: Set, + resolutionData: ComponentResolutionData, + ) { if (!ts.isArrayLiteralExpression(importsExpr)) { return; } @@ -1472,29 +1877,36 @@ export class ComponentDecoratorHandler implements resolutionData.deferrableDeclToImportDecl.set(decl.node, imp.node); this.deferredSymbolTracker.markAsDeferrableCandidate( - node, imp.node, componentClassDecl, isDeferredImport); + node, + imp.node, + componentClassDecl, + isDeferredImport, + ); } } - private compileDeferBlocks(resolution: Readonly>): - R3ComponentDeferMetadata { + private compileDeferBlocks( + resolution: Readonly>, + ): R3ComponentDeferMetadata { const { deferBlockDepsEmitMode: mode, deferPerBlockDependencies: perBlockDeps, - deferPerComponentDependencies: perComponentDeps + deferPerComponentDependencies: perComponentDeps, } = resolution; if (mode === DeferBlockDepsEmitMode.PerBlock) { if (!perBlockDeps) { throw new Error( - 'Internal error: deferPerBlockDependencies must be present when compiling in PerBlock mode'); + 'Internal error: deferPerBlockDependencies must be present when compiling in PerBlock mode', + ); } - const blocks = new Map(); + const blocks = new Map(); for (const [block, dependencies] of perBlockDeps) { blocks.set( - block, - dependencies.length === 0 ? null : compileDeferResolverFunction({mode, dependencies})); + block, + dependencies.length === 0 ? null : compileDeferResolverFunction({mode, dependencies}), + ); } return {mode, blocks}; @@ -1503,13 +1915,15 @@ export class ComponentDecoratorHandler implements if (mode === DeferBlockDepsEmitMode.PerComponent) { if (!perComponentDeps) { throw new Error( - 'Internal error: deferPerComponentDependencies must be present in PerComponent mode'); + 'Internal error: deferPerComponentDependencies must be present in PerComponent mode', + ); } return { mode, - dependenciesFn: perComponentDeps.length === 0 ? - null : - compileDeferResolverFunction({mode, dependencies: perComponentDeps}) + dependenciesFn: + perComponentDeps.length === 0 + ? null + : compileDeferResolverFunction({mode, dependencies: perComponentDeps}), }; } @@ -1520,7 +1934,7 @@ export class ComponentDecoratorHandler implements /** * Creates an instance of a target binder based on provided dependencies. */ -function createTargetBinder(dependencies: Array) { +function createTargetBinder(dependencies: Array) { const matcher = new SelectorMatcher(); for (const dep of dependencies) { if (dep.kind === MetaKind.Directive && dep.selector !== null) { @@ -1533,14 +1947,15 @@ function createTargetBinder(dependencies: Array): - Map { +function extractPipes( + dependencies: Array, +): Map { const pipes = new Map(); for (const dep of dependencies) { if (dep.kind === MetaKind.Pipe) { @@ -1555,11 +1970,15 @@ function extractPipes(dependencies: Array): * in the `setClassMetadataAsync` call. Otherwise, an import declaration gets retained. */ function removeDeferrableTypesFromComponentDecorator( - analysis: Readonly, deferrableTypes: R3DeferPerComponentDependency[]) { + analysis: Readonly, + deferrableTypes: R3DeferPerComponentDependency[], +) { if (analysis.classMetadata) { - const deferrableSymbols = new Set(deferrableTypes.map(t => t.symbolName)); + const deferrableSymbols = new Set(deferrableTypes.map((t) => t.symbolName)); const rewrittenDecoratorsNode = removeIdentifierReferences( - (analysis.classMetadata.decorators as o.WrappedNodeExpr).node, deferrableSymbols); + (analysis.classMetadata.decorators as o.WrappedNodeExpr).node, + deferrableSymbols, + ); analysis.classMetadata.decorators = new o.WrappedNodeExpr(rewrittenDecoratorsNode); } } @@ -1569,23 +1988,27 @@ function removeDeferrableTypesFromComponentDecorator( * overlapping dependencies. */ function validateNoImportOverlap( - eagerDeps: Array, - deferredDeps: Array, rawDeferredImports: ts.Expression) { - let diagnostic: ts.Diagnostic|null = null; + eagerDeps: Array, + deferredDeps: Array, + rawDeferredImports: ts.Expression, +) { + let diagnostic: ts.Diagnostic | null = null; const eagerDepsSet = new Set(); for (const eagerDep of eagerDeps) { eagerDepsSet.add(eagerDep.ref.node); } for (const deferredDep of deferredDeps) { if (eagerDepsSet.has(deferredDep.ref.node)) { - const classInfo = deferredDep.ref.debugName ? `The \`${deferredDep.ref.debugName}\`` : - 'One of the dependencies'; + const classInfo = deferredDep.ref.debugName + ? `The \`${deferredDep.ref.debugName}\`` + : 'One of the dependencies'; diagnostic = makeDiagnostic( - ErrorCode.DEFERRED_DEPENDENCY_IMPORTED_EAGERLY, - getDiagnosticNode(deferredDep.ref, rawDeferredImports), - `\`${classInfo}\` is imported via both \`@Component.imports\` and ` + - `\`@Component.deferredImports\`. To fix this, make sure that ` + - `dependencies are imported only once.`); + ErrorCode.DEFERRED_DEPENDENCY_IMPORTED_EAGERLY, + getDiagnosticNode(deferredDep.ref, rawDeferredImports), + `\`${classInfo}\` is imported via both \`@Component.imports\` and ` + + `\`@Component.deferredImports\`. To fix this, make sure that ` + + `dependencies are imported only once.`, + ); break; } } @@ -1593,17 +2016,26 @@ function validateNoImportOverlap( } function validateStandaloneImports( - importRefs: Reference[], importExpr: ts.Expression, - metaReader: MetadataReader, scopeReader: ComponentScopeReader, - isDeferredImport: boolean): ts.Diagnostic[] { + importRefs: Reference[], + importExpr: ts.Expression, + metaReader: MetadataReader, + scopeReader: ComponentScopeReader, + isDeferredImport: boolean, +): ts.Diagnostic[] { const diagnostics: ts.Diagnostic[] = []; for (const ref of importRefs) { const dirMeta = metaReader.getDirectiveMetadata(ref); if (dirMeta !== null) { if (!dirMeta.isStandalone) { // Directly importing a directive that's not standalone is an error. - diagnostics.push(makeNotStandaloneDiagnostic( - scopeReader, ref, importExpr, dirMeta.isComponent ? 'component' : 'directive')); + diagnostics.push( + makeNotStandaloneDiagnostic( + scopeReader, + ref, + importExpr, + dirMeta.isComponent ? 'component' : 'directive', + ), + ); } continue; } @@ -1624,8 +2056,9 @@ function validateStandaloneImports( } // Make an error? - const error = isDeferredImport ? makeUnknownComponentDeferredImportDiagnostic(ref, importExpr) : - makeUnknownComponentImportDiagnostic(ref, importExpr); + const error = isDeferredImport + ? makeUnknownComponentDeferredImportDiagnostic(ref, importExpr) + : makeUnknownComponentImportDiagnostic(ref, importExpr); diagnostics.push(error); } diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/metadata.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/metadata.ts index 97ee588fe2ffe..b446517800715 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/metadata.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/metadata.ts @@ -6,11 +6,29 @@ * found in the LICENSE file at https://angular.io/license */ -import {AnimationTriggerNames, DeclarationListEmitMode, DeferBlockDepsEmitMode, R3ClassDebugInfo, R3ClassMetadata, R3ComponentMetadata, R3DeferPerBlockDependency, R3DeferPerComponentDependency, R3TemplateDependencyMetadata, SchemaMetadata, TmplAstDeferredBlock} from '@angular/compiler'; +import { + AnimationTriggerNames, + DeclarationListEmitMode, + DeferBlockDepsEmitMode, + R3ClassDebugInfo, + R3ClassMetadata, + R3ComponentMetadata, + R3DeferPerBlockDependency, + R3DeferPerComponentDependency, + R3TemplateDependencyMetadata, + SchemaMetadata, + TmplAstDeferredBlock, +} from '@angular/compiler'; import ts from 'typescript'; import {Reference} from '../../../imports'; -import {ClassPropertyMapping, ComponentResources, DirectiveTypeCheckMeta, HostDirectiveMeta, InputMapping} from '../../../metadata'; +import { + ClassPropertyMapping, + ComponentResources, + DirectiveTypeCheckMeta, + HostDirectiveMeta, + InputMapping, +} from '../../../metadata'; import {ClassDeclaration} from '../../../reflection'; import {SubsetOfKeys} from '../../../util/src/typescript'; @@ -23,8 +41,9 @@ import {ParsedTemplateWithSource, StyleUrlMeta} from './resources'; * be included here. */ export type ComponentMetadataResolvedFields = SubsetOfKeys< - R3ComponentMetadata, - 'declarations'|'declarationListEmitMode'|'defer'>; + R3ComponentMetadata, + 'declarations' | 'declarationListEmitMode' | 'defer' +>; export interface ComponentAnalysisData { /** @@ -32,11 +51,11 @@ export interface ComponentAnalysisData { * (not during resolve). */ meta: Omit, ComponentMetadataResolvedFields>; - baseClass: Reference|'dynamic'|null; + baseClass: Reference | 'dynamic' | null; typeCheckMeta: DirectiveTypeCheckMeta; template: ParsedTemplateWithSource; - classMetadata: R3ClassMetadata|null; - classDebugInfo: R3ClassDebugInfo|null; + classMetadata: R3ClassMetadata | null; + classDebugInfo: R3ClassDebugInfo | null; inputs: ClassPropertyMapping; outputs: ClassPropertyMapping; @@ -45,48 +64,48 @@ export interface ComponentAnalysisData { * Providers extracted from the `providers` field of the component annotation which will require * an Angular factory definition at runtime. */ - providersRequiringFactory: Set>|null; + providersRequiringFactory: Set> | null; /** * Providers extracted from the `viewProviders` field of the component annotation which will * require an Angular factory definition at runtime. */ - viewProvidersRequiringFactory: Set>|null; + viewProvidersRequiringFactory: Set> | null; resources: ComponentResources; /** * `styleUrls` extracted from the decorator, if present. */ - styleUrls: StyleUrlMeta[]|null; + styleUrls: StyleUrlMeta[] | null; /** * Inline stylesheets extracted from the decorator, if present. */ - inlineStyles: string[]|null; + inlineStyles: string[] | null; isPoisoned: boolean; - animationTriggerNames: AnimationTriggerNames|null; + animationTriggerNames: AnimationTriggerNames | null; - rawImports: ts.Expression|null; - resolvedImports: Reference[]|null; - rawDeferredImports: ts.Expression|null; - resolvedDeferredImports: Reference[]|null; + rawImports: ts.Expression | null; + resolvedImports: Reference[] | null; + rawDeferredImports: ts.Expression | null; + resolvedDeferredImports: Reference[] | null; /** * Map of symbol name -> import path for types from `@Component.deferredImports` field. */ - explicitlyDeferredTypes: R3DeferPerComponentDependency[]|null; + explicitlyDeferredTypes: R3DeferPerComponentDependency[] | null; - schemas: SchemaMetadata[]|null; + schemas: SchemaMetadata[] | null; - decorator: ts.Decorator|null; + decorator: ts.Decorator | null; /** Additional directives applied to the component host. */ - hostDirectives: HostDirectiveMeta[]|null; + hostDirectives: HostDirectiveMeta[] | null; /** Raw expression that defined the host directives array. Used for diagnostics. */ - rawHostDirectives: ts.Expression|null; + rawHostDirectives: ts.Expression | null; } export interface ComponentResolutionData { @@ -123,7 +142,7 @@ export interface ComponentResolutionData { /** * Describes a dependency used within a `@defer` block. */ -export type DeferredComponentDependency = R3DeferPerBlockDependency&{ +export type DeferredComponentDependency = R3DeferPerBlockDependency & { /** Reference to the declaration that defines the dependency. */ declaration: Reference; }; diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/resources.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/resources.ts index 5cacc21645d82..65b670d4ac490 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/resources.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/resources.ts @@ -6,7 +6,15 @@ * found in the LICENSE file at https://angular.io/license */ -import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig, LexerRange, ParsedTemplate, ParseSourceFile, parseTemplate, TmplAstNode,} from '@angular/compiler'; +import { + DEFAULT_INTERPOLATION_CONFIG, + InterpolationConfig, + LexerRange, + ParsedTemplate, + ParseSourceFile, + parseTemplate, + TmplAstNode, +} from '@angular/compiler'; import ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../../diagnostics'; @@ -17,7 +25,12 @@ import {DynamicValue, PartialEvaluator, traceDynamicValue} from '../../../partia import {ClassDeclaration, DeclarationNode, Decorator} from '../../../reflection'; import {CompilationMode} from '../../../transform'; import {TemplateSourceMapping} from '../../../typecheck/api'; -import {createValueHasWrongTypeError, isStringArray, ResourceLoader, assertLocalCompilationUnresolvedConst} from '../../common'; +import { + createValueHasWrongTypeError, + isStringArray, + ResourceLoader, + assertLocalCompilationUnresolvedConst, +} from '../../common'; /** * The literal style url extracted from the decorator, along with metadata for diagnostics. @@ -25,8 +38,9 @@ import {createValueHasWrongTypeError, isStringArray, ResourceLoader, assertLocal export interface StyleUrlMeta { url: string; nodeForError: ts.Node; - source: ResourceTypeForDiagnostics.StylesheetFromTemplate| - ResourceTypeForDiagnostics.StylesheetFromDecorator; + source: + | ResourceTypeForDiagnostics.StylesheetFromTemplate + | ResourceTypeForDiagnostics.StylesheetFromDecorator; } /** @@ -44,7 +58,6 @@ export const enum ResourceTypeForDiagnostics { StylesheetFromDecorator, } - /** * Information about the template which was extracted during parsing. * @@ -72,8 +85,6 @@ export interface ParsedTemplateWithSource extends ParsedComponentTemplate { declaration: TemplateDeclaration; } - - /** * Common fields extracted from the declaration of a template. */ @@ -108,7 +119,7 @@ export interface ExternalTemplateDeclaration extends CommonTemplateDeclaration { * information, `ComponentDecoratorHandler` is able to re-read the template and update the component * record without needing to parse the original decorator again. */ -export type TemplateDeclaration = InlineTemplateDeclaration|ExternalTemplateDeclaration; +export type TemplateDeclaration = InlineTemplateDeclaration | ExternalTemplateDeclaration; /** Determines the node to use for debugging purposes for the given TemplateDeclaration. */ export function getTemplateDeclarationNodeForError(declaration: TemplateDeclaration): ts.Node { @@ -123,19 +134,26 @@ export interface ExtractTemplateOptions { } export function extractTemplate( - node: ClassDeclaration, template: TemplateDeclaration, evaluator: PartialEvaluator, - depTracker: DependencyTracker|null, resourceLoader: ResourceLoader, - options: ExtractTemplateOptions, compilationMode: CompilationMode): ParsedTemplateWithSource { + node: ClassDeclaration, + template: TemplateDeclaration, + evaluator: PartialEvaluator, + depTracker: DependencyTracker | null, + resourceLoader: ResourceLoader, + options: ExtractTemplateOptions, + compilationMode: CompilationMode, +): ParsedTemplateWithSource { if (template.isInline) { let sourceStr: string; - let sourceParseRange: LexerRange|null = null; + let sourceParseRange: LexerRange | null = null; let templateContent: string; let sourceMapping: TemplateSourceMapping; let escapedString = false; - let sourceMapUrl: string|null; + let sourceMapUrl: string | null; // We only support SourceMaps for inline templates that are simple string literals. - if (ts.isStringLiteral(template.expression) || - ts.isNoSubstitutionTemplateLiteral(template.expression)) { + if ( + ts.isStringLiteral(template.expression) || + ts.isNoSubstitutionTemplateLiteral(template.expression) + ) { // the start and end of the `templateExpr` node includes the quotation marks, which we must // strip sourceParseRange = getTemplateRange(template.expression); @@ -152,16 +170,23 @@ export function extractTemplate( // The identifier used for @Component.template cannot be resolved in local compilation mode. An error specific to this situation is generated. assertLocalCompilationUnresolvedConst( - compilationMode, resolvedTemplate, template.expression, - 'Unresolved identifier found for @Component.template field! ' + - 'Did you import this identifier from a file outside of the compilation unit? ' + 'This is not allowed when Angular compiler runs in local mode. ' + - 'Possible solutions: 1) Move the declaration into a file within the ' + - 'compilation unit, 2) Inline the template, 3) Move the template into ' + - 'a separate .html file and include it using @Component.templateUrl'); + compilationMode, + resolvedTemplate, + template.expression, + 'Unresolved identifier found for @Component.template field! ' + + 'Did you import this identifier from a file outside of the compilation unit? ' + + 'This is not allowed when Angular compiler runs in local mode. ' + + 'Possible solutions: 1) Move the declaration into a file within the ' + + 'compilation unit, 2) Inline the template, 3) Move the template into ' + + 'a separate .html file and include it using @Component.templateUrl', + ); if (typeof resolvedTemplate !== 'string') { throw createValueHasWrongTypeError( - template.expression, resolvedTemplate, 'template must be a string'); + template.expression, + resolvedTemplate, + 'template must be a string', + ); } // We do not parse the template directly from the source file using a lexer range, so // the template source and content are set to the statically resolved template. @@ -182,7 +207,13 @@ export function extractTemplate( return { ...parseExtractedTemplate( - template, sourceStr, sourceParseRange, escapedString, sourceMapUrl, options), + template, + sourceStr, + sourceParseRange, + escapedString, + sourceMapUrl, + options, + ), content: templateContent, sourceMapping, declaration: template, @@ -191,14 +222,20 @@ export function extractTemplate( const templateContent = resourceLoader.load(template.resolvedTemplateUrl); if (depTracker !== null) { depTracker.addResourceDependency( - node.getSourceFile(), absoluteFrom(template.resolvedTemplateUrl)); + node.getSourceFile(), + absoluteFrom(template.resolvedTemplateUrl), + ); } return { ...parseExtractedTemplate( - template, /* sourceStr */ templateContent, /* sourceParseRange */ null, - /* escapedString */ false, - /* sourceMapUrl */ template.resolvedTemplateUrl, options), + template, + /* sourceStr */ templateContent, + /* sourceParseRange */ null, + /* escapedString */ false, + /* sourceMapUrl */ template.resolvedTemplateUrl, + options, + ), content: templateContent, sourceMapping: { type: 'external', @@ -213,9 +250,13 @@ export function extractTemplate( } function parseExtractedTemplate( - template: TemplateDeclaration, sourceStr: string, sourceParseRange: LexerRange|null, - escapedString: boolean, sourceMapUrl: string|null, - options: ExtractTemplateOptions): ParsedComponentTemplate { + template: TemplateDeclaration, + sourceStr: string, + sourceParseRange: LexerRange | null, + escapedString: boolean, + sourceMapUrl: string | null, + options: ExtractTemplateOptions, +): ParsedComponentTemplate { // We always normalize line endings if the template has been escaped (i.e. is inline). const i18nNormalizeLineEndingsInICUs = escapedString || options.i18nNormalizeLineEndingsInICUs; @@ -266,9 +307,15 @@ function parseExtractedTemplate( } export function parseTemplateDeclaration( - node: ClassDeclaration, decorator: Decorator, component: Map, - containingFile: string, evaluator: PartialEvaluator, depTracker: DependencyTracker|null, - resourceLoader: ResourceLoader, defaultPreserveWhitespaces: boolean): TemplateDeclaration { + node: ClassDeclaration, + decorator: Decorator, + component: Map, + containingFile: string, + evaluator: PartialEvaluator, + depTracker: DependencyTracker | null, + resourceLoader: ResourceLoader, + defaultPreserveWhitespaces: boolean, +): TemplateDeclaration { let preserveWhitespaces: boolean = defaultPreserveWhitespaces; if (component.has('preserveWhitespaces')) { const expr = component.get('preserveWhitespaces')!; @@ -283,10 +330,16 @@ export function parseTemplateDeclaration( if (component.has('interpolation')) { const expr = component.get('interpolation')!; const value = evaluator.evaluate(expr); - if (!Array.isArray(value) || value.length !== 2 || - !value.every(element => typeof element === 'string')) { + if ( + !Array.isArray(value) || + value.length !== 2 || + !value.every((element) => typeof element === 'string') + ) { throw createValueHasWrongTypeError( - expr, value, 'interpolation must be an array with 2 elements of string type'); + expr, + value, + 'interpolation must be an array with 2 elements of string type', + ); } interpolationConfig = InterpolationConfig.fromArray(value as [string, string]); } @@ -296,7 +349,10 @@ export function parseTemplateDeclaration( const templateUrl = evaluator.evaluate(templateUrlExpr); if (typeof templateUrl !== 'string') { throw createValueHasWrongTypeError( - templateUrlExpr, templateUrl, 'templateUrl must be a string'); + templateUrlExpr, + templateUrl, + 'templateUrl must be a string', + ); } try { const resourceUrl = resourceLoader.resolve(templateUrl, containingFile); @@ -316,7 +372,10 @@ export function parseTemplateDeclaration( } throw makeResourceNotFoundError( - templateUrl, templateUrlExpr, ResourceTypeForDiagnostics.Template); + templateUrl, + templateUrlExpr, + ResourceTypeForDiagnostics.Template, + ); } } else if (component.has('template')) { return { @@ -329,38 +388,67 @@ export function parseTemplateDeclaration( }; } else { throw new FatalDiagnosticError( - ErrorCode.COMPONENT_MISSING_TEMPLATE, decorator.node, 'component is missing a template'); + ErrorCode.COMPONENT_MISSING_TEMPLATE, + decorator.node, + 'component is missing a template', + ); } } export function preloadAndParseTemplate( - evaluator: PartialEvaluator, resourceLoader: ResourceLoader, depTracker: DependencyTracker|null, - preanalyzeTemplateCache: Map, node: ClassDeclaration, - decorator: Decorator, component: Map, containingFile: string, - defaultPreserveWhitespaces: boolean, options: ExtractTemplateOptions, - compilationMode: CompilationMode): Promise { + evaluator: PartialEvaluator, + resourceLoader: ResourceLoader, + depTracker: DependencyTracker | null, + preanalyzeTemplateCache: Map, + node: ClassDeclaration, + decorator: Decorator, + component: Map, + containingFile: string, + defaultPreserveWhitespaces: boolean, + options: ExtractTemplateOptions, + compilationMode: CompilationMode, +): Promise { if (component.has('templateUrl')) { // Extract the templateUrl and preload it. const templateUrlExpr = component.get('templateUrl')!; const templateUrl = evaluator.evaluate(templateUrlExpr); if (typeof templateUrl !== 'string') { throw createValueHasWrongTypeError( - templateUrlExpr, templateUrl, 'templateUrl must be a string'); + templateUrlExpr, + templateUrl, + 'templateUrl must be a string', + ); } try { const resourceUrl = resourceLoader.resolve(templateUrl, containingFile); - const templatePromise = - resourceLoader.preload(resourceUrl, {type: 'template', containingFile}); + const templatePromise = resourceLoader.preload(resourceUrl, { + type: 'template', + containingFile, + }); // If the preload worked, then actually load and parse the template, and wait for any // style URLs to resolve. if (templatePromise !== undefined) { return templatePromise.then(() => { const templateDecl = parseTemplateDeclaration( - node, decorator, component, containingFile, evaluator, depTracker, resourceLoader, - defaultPreserveWhitespaces); + node, + decorator, + component, + containingFile, + evaluator, + depTracker, + resourceLoader, + defaultPreserveWhitespaces, + ); const template = extractTemplate( - node, templateDecl, evaluator, depTracker, resourceLoader, options, compilationMode); + node, + templateDecl, + evaluator, + depTracker, + resourceLoader, + options, + compilationMode, + ); preanalyzeTemplateCache.set(node, template); return template; }); @@ -375,14 +463,31 @@ export function preloadAndParseTemplate( } throw makeResourceNotFoundError( - templateUrl, templateUrlExpr, ResourceTypeForDiagnostics.Template); + templateUrl, + templateUrlExpr, + ResourceTypeForDiagnostics.Template, + ); } } else { const templateDecl = parseTemplateDeclaration( - node, decorator, component, containingFile, evaluator, depTracker, resourceLoader, - defaultPreserveWhitespaces); + node, + decorator, + component, + containingFile, + evaluator, + depTracker, + resourceLoader, + defaultPreserveWhitespaces, + ); const template = extractTemplate( - node, templateDecl, evaluator, depTracker, resourceLoader, options, compilationMode); + node, + templateDecl, + evaluator, + depTracker, + resourceLoader, + options, + compilationMode, + ); preanalyzeTemplateCache.set(node, template); return Promise.resolve(template); } @@ -390,8 +495,10 @@ export function preloadAndParseTemplate( function getTemplateRange(templateExpr: ts.Expression) { const startPos = templateExpr.getStart() + 1; - const {line, character} = - ts.getLineAndCharacterOfPosition(templateExpr.getSourceFile(), startPos); + const {line, character} = ts.getLineAndCharacterOfPosition( + templateExpr.getSourceFile(), + startPos, + ); return { startPos, startLine: line, @@ -401,8 +508,10 @@ function getTemplateRange(templateExpr: ts.Expression) { } export function makeResourceNotFoundError( - file: string, nodeForError: ts.Node, - resourceType: ResourceTypeForDiagnostics): FatalDiagnosticError { + file: string, + nodeForError: ts.Node, + resourceType: ResourceTypeForDiagnostics, +): FatalDiagnosticError { let errorText: string; switch (resourceType) { case ResourceTypeForDiagnostics.Template: @@ -419,7 +528,6 @@ export function makeResourceNotFoundError( return new FatalDiagnosticError(ErrorCode.COMPONENT_RESOURCE_NOT_FOUND, nodeForError, errorText); } - /** * Transforms the given decorator to inline external resources. i.e. if the decorator * resolves to `@Component`, the `templateUrl` and `styleUrls` metadata fields will be @@ -433,16 +541,23 @@ export function makeResourceNotFoundError( * external resources exclusively for the class metadata. */ export function transformDecoratorResources( - dec: Decorator, component: Map, styles: string[], - template: ParsedTemplateWithSource): Decorator { + dec: Decorator, + component: Map, + styles: string[], + template: ParsedTemplateWithSource, +): Decorator { if (dec.name !== 'Component') { return dec; } // If no external resources are referenced, preserve the original decorator // for the best source map experience when the decorator is emitted in TS. - if (!component.has('templateUrl') && !component.has('styleUrls') && !component.has('styleUrl') && - !component.has('styles')) { + if ( + !component.has('templateUrl') && + !component.has('styleUrls') && + !component.has('styleUrl') && + !component.has('styles') + ) { return dec; } @@ -484,17 +599,19 @@ export function transformDecoratorResources( } export function extractComponentStyleUrls( - evaluator: PartialEvaluator, - component: Map, - ): StyleUrlMeta[] { + evaluator: PartialEvaluator, + component: Map, +): StyleUrlMeta[] { const styleUrlsExpr = component.get('styleUrls'); const styleUrlExpr = component.get('styleUrl'); if (styleUrlsExpr !== undefined && styleUrlExpr !== undefined) { throw new FatalDiagnosticError( - ErrorCode.COMPONENT_INVALID_STYLE_URLS, styleUrlExpr, - '@Component cannot define both `styleUrl` and `styleUrls`. ' + - 'Use `styleUrl` if the component has one stylesheet, or `styleUrls` if it has multiple'); + ErrorCode.COMPONENT_INVALID_STYLE_URLS, + styleUrlExpr, + '@Component cannot define both `styleUrl` and `styleUrls`. ' + + 'Use `styleUrl` if the component has one stylesheet, or `styleUrls` if it has multiple', + ); } if (styleUrlsExpr !== undefined) { @@ -508,18 +625,22 @@ export function extractComponentStyleUrls( throw createValueHasWrongTypeError(styleUrlExpr, styleUrl, 'styleUrl must be a string'); } - return [{ - url: styleUrl, - source: ResourceTypeForDiagnostics.StylesheetFromDecorator, - nodeForError: styleUrlExpr, - }]; + return [ + { + url: styleUrl, + source: ResourceTypeForDiagnostics.StylesheetFromDecorator, + nodeForError: styleUrlExpr, + }, + ]; } return []; } function extractStyleUrlsFromExpression( - evaluator: PartialEvaluator, styleUrlsExpr: ts.Expression): StyleUrlMeta[] { + evaluator: PartialEvaluator, + styleUrlsExpr: ts.Expression, +): StyleUrlMeta[] { const styleUrls: StyleUrlMeta[] = []; if (ts.isArrayLiteralExpression(styleUrlsExpr)) { @@ -544,7 +665,10 @@ function extractStyleUrlsFromExpression( const evaluatedStyleUrls = evaluator.evaluate(styleUrlsExpr); if (!isStringArray(evaluatedStyleUrls)) { throw createValueHasWrongTypeError( - styleUrlsExpr, evaluatedStyleUrls, 'styleUrls must be an array of strings'); + styleUrlsExpr, + evaluatedStyleUrls, + 'styleUrls must be an array of strings', + ); } for (const styleUrl of evaluatedStyleUrls) { @@ -560,8 +684,10 @@ function extractStyleUrlsFromExpression( } export function extractStyleResources( - resourceLoader: ResourceLoader, component: Map, - containingFile: string): ReadonlySet { + resourceLoader: ResourceLoader, + component: Map, + containingFile: string, +): ReadonlySet { const styles = new Set(); function stringLiteralElements(array: ts.ArrayLiteralExpression): ts.StringLiteralLike[] { return array.elements.filter((e): e is ts.StringLiteralLike => ts.isStringLiteralLike(e)); @@ -602,8 +728,10 @@ export function extractStyleResources( } function stringLiteralUrlToResource( - resourceLoader: ResourceLoader, expression: ts.StringLiteralLike, - containingFile: string): Resource|null { + resourceLoader: ResourceLoader, + expression: ts.StringLiteralLike, + containingFile: string, +): Resource | null { try { const resourceUrl = resourceLoader.resolve(expression.text, containingFile); return {path: absoluteFrom(resourceUrl), expression}; @@ -621,6 +749,9 @@ export function _extractTemplateStyleUrls(template: ParsedTemplateWithSource): S } const nodeForError = getTemplateDeclarationNodeForError(template.declaration); - return template.styleUrls.map( - url => ({url, source: ResourceTypeForDiagnostics.StylesheetFromTemplate, nodeForError})); + return template.styleUrls.map((url) => ({ + url, + source: ResourceTypeForDiagnostics.StylesheetFromTemplate, + nodeForError, + })); } diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/symbol.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/symbol.ts index 701ecea1f9bdb..17b9f364b4334 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/symbol.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/symbol.ts @@ -6,7 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {isArrayEqual, isReferenceEqual, SemanticReference, SemanticSymbol} from '../../../incremental/semantic_graph'; +import { + isArrayEqual, + isReferenceEqual, + SemanticReference, + SemanticSymbol, +} from '../../../incremental/semantic_graph'; import {DirectiveSymbol} from '../../directive'; /** @@ -17,8 +22,10 @@ export class ComponentSymbol extends DirectiveSymbol { usedPipes: SemanticReference[] = []; isRemotelyScoped = false; - override isEmitAffected(previousSymbol: SemanticSymbol, publicApiAffected: Set): - boolean { + override isEmitAffected( + previousSymbol: SemanticSymbol, + publicApiAffected: Set, + ): boolean { if (!(previousSymbol instanceof ComponentSymbol)) { return true; } @@ -27,7 +34,7 @@ export class ComponentSymbol extends DirectiveSymbol { // declaration, but only if the symbol in the current compilation does not have its public API // affected. const isSymbolUnaffected = (current: SemanticReference, previous: SemanticReference) => - isReferenceEqual(current, previous) && !publicApiAffected.has(current.symbol); + isReferenceEqual(current, previous) && !publicApiAffected.has(current.symbol); // The emit of a component is affected if either of the following is true: // 1. The component used to be remotely scoped but no longer is, or vice versa. @@ -36,13 +43,17 @@ export class ComponentSymbol extends DirectiveSymbol { // the component must still be re-emitted, as this may affect directive instantiation order. // 3. The list of used pipes has changed, or any of those pipes have had their public API // changed. - return this.isRemotelyScoped !== previousSymbol.isRemotelyScoped || - !isArrayEqual(this.usedDirectives, previousSymbol.usedDirectives, isSymbolUnaffected) || - !isArrayEqual(this.usedPipes, previousSymbol.usedPipes, isSymbolUnaffected); + return ( + this.isRemotelyScoped !== previousSymbol.isRemotelyScoped || + !isArrayEqual(this.usedDirectives, previousSymbol.usedDirectives, isSymbolUnaffected) || + !isArrayEqual(this.usedPipes, previousSymbol.usedPipes, isSymbolUnaffected) + ); } override isTypeCheckBlockAffected( - previousSymbol: SemanticSymbol, typeCheckApiAffected: Set): boolean { + previousSymbol: SemanticSymbol, + typeCheckApiAffected: Set, + ): boolean { if (!(previousSymbol instanceof ComponentSymbol)) { return true; } @@ -50,7 +61,7 @@ export class ComponentSymbol extends DirectiveSymbol { // To verify that a used directive is not affected we need to verify that its full inheritance // chain is not present in `typeCheckApiAffected`. const isInheritanceChainAffected = (symbol: SemanticSymbol): boolean => { - let currentSymbol: SemanticSymbol|null = symbol; + let currentSymbol: SemanticSymbol | null = symbol; while (currentSymbol instanceof DirectiveSymbol) { if (typeCheckApiAffected.has(currentSymbol)) { return true; @@ -65,21 +76,22 @@ export class ComponentSymbol extends DirectiveSymbol { // declaration and if the symbol and all symbols it inherits from in the current compilation // do not have their type-check API affected. const isDirectiveUnaffected = (current: SemanticReference, previous: SemanticReference) => - isReferenceEqual(current, previous) && !isInheritanceChainAffected(current.symbol); + isReferenceEqual(current, previous) && !isInheritanceChainAffected(current.symbol); // Create an equality function that considers pipes equal if they represent the same // declaration and if the symbol in the current compilation does not have its type-check // API affected. const isPipeUnaffected = (current: SemanticReference, previous: SemanticReference) => - isReferenceEqual(current, previous) && !typeCheckApiAffected.has(current.symbol); + isReferenceEqual(current, previous) && !typeCheckApiAffected.has(current.symbol); // The emit of a type-check block of a component is affected if either of the following is true: // 1. The list of used directives has changed or any of those directives have had their // type-check API changed. // 2. The list of used pipes has changed, or any of those pipes have had their type-check API // changed. - return !isArrayEqual( - this.usedDirectives, previousSymbol.usedDirectives, isDirectiveUnaffected) || - !isArrayEqual(this.usedPipes, previousSymbol.usedPipes, isPipeUnaffected); + return ( + !isArrayEqual(this.usedDirectives, previousSymbol.usedDirectives, isDirectiveUnaffected) || + !isArrayEqual(this.usedPipes, previousSymbol.usedPipes, isPipeUnaffected) + ); } } diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/util.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/util.ts index 881d98b69d3f9..48a864af429e1 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/util.ts @@ -7,12 +7,20 @@ */ import {AnimationTriggerNames} from '@angular/compiler'; -import {isResolvedModuleWithProviders, ResolvedModuleWithProviders,} from '@angular/compiler-cli/src/ngtsc/annotations/ng_module'; +import { + isResolvedModuleWithProviders, + ResolvedModuleWithProviders, +} from '@angular/compiler-cli/src/ngtsc/annotations/ng_module'; import {ErrorCode, makeDiagnostic} from '@angular/compiler-cli/src/ngtsc/diagnostics'; import ts from 'typescript'; import {Reference} from '../../../imports'; -import {ForeignFunctionResolver, ResolvedValue, ResolvedValueMap, SyntheticValue} from '../../../partial_evaluator'; +import { + ForeignFunctionResolver, + ResolvedValue, + ResolvedValueMap, + SyntheticValue, +} from '../../../partial_evaluator'; import {ClassDeclaration, isNamedClassDeclaration} from '../../../reflection'; import {createValueHasWrongTypeError, getOriginNodeForDiagnostics} from '../../common'; @@ -23,7 +31,9 @@ import {createValueHasWrongTypeError, getOriginNodeForDiagnostics} from '../../c * statically evaluated. */ export function collectAnimationNames( - value: ResolvedValue, animationTriggerNames: AnimationTriggerNames) { + value: ResolvedValue, + animationTriggerNames: AnimationTriggerNames, +) { if (value instanceof Map) { const name = value.get('name'); if (typeof name === 'string') { @@ -41,34 +51,42 @@ export function collectAnimationNames( } export function isAngularAnimationsReference(reference: Reference, symbolName: string): boolean { - return reference.ownedByModuleGuess === '@angular/animations' && - reference.debugName === symbolName; + return ( + reference.ownedByModuleGuess === '@angular/animations' && reference.debugName === symbolName + ); } -export const animationTriggerResolver: ForeignFunctionResolver = - (fn, node, resolve, unresolvable) => { - const animationTriggerMethodName = 'trigger'; - if (!isAngularAnimationsReference(fn, animationTriggerMethodName)) { - return unresolvable; - } - const triggerNameExpression = node.arguments[0]; - if (!triggerNameExpression) { - return unresolvable; - } - const res = new Map(); - res.set('name', resolve(triggerNameExpression)); - return res; - }; +export const animationTriggerResolver: ForeignFunctionResolver = ( + fn, + node, + resolve, + unresolvable, +) => { + const animationTriggerMethodName = 'trigger'; + if (!isAngularAnimationsReference(fn, animationTriggerMethodName)) { + return unresolvable; + } + const triggerNameExpression = node.arguments[0]; + if (!triggerNameExpression) { + return unresolvable; + } + const res = new Map(); + res.set('name', resolve(triggerNameExpression)); + return res; +}; export function validateAndFlattenComponentImports( - imports: ResolvedValue, expr: ts.Expression, isDeferred: boolean): { - imports: Reference[], - diagnostics: ts.Diagnostic[], + imports: ResolvedValue, + expr: ts.Expression, + isDeferred: boolean, +): { + imports: Reference[]; + diagnostics: ts.Diagnostic[]; } { const flattened: Reference[] = []; - const errorMessage = isDeferred ? - `'deferredImports' must be an array of components, directives, or pipes.` : - `'imports' must be an array of components, directives, pipes, or NgModules.`; + const errorMessage = isDeferred + ? `'deferredImports' must be an array of components, directives, or pipes.` + : `'imports' must be an array of components, directives, pipes, or NgModules.`; if (!Array.isArray(imports)) { const error = createValueHasWrongTypeError(expr, imports, errorMessage).toDiagnostic(); return { @@ -81,7 +99,7 @@ export function validateAndFlattenComponentImports( for (const ref of imports) { if (Array.isArray(ref)) { const {imports: childImports, diagnostics: childDiagnostics} = - validateAndFlattenComponentImports(ref, expr, isDeferred); + validateAndFlattenComponentImports(ref, expr, isDeferred); flattened.push(...childImports); diagnostics.push(...childDiagnostics); } else if (ref instanceof Reference) { @@ -89,8 +107,12 @@ export function validateAndFlattenComponentImports( flattened.push(ref as Reference); } else { diagnostics.push( - createValueHasWrongTypeError(ref.getOriginForDiagnostics(expr), ref, errorMessage) - .toDiagnostic()); + createValueHasWrongTypeError( + ref.getOriginForDiagnostics(expr), + ref, + errorMessage, + ).toDiagnostic(), + ); } } else if (isLikelyModuleWithProviders(ref)) { let origin = expr; @@ -100,11 +122,15 @@ export function validateAndFlattenComponentImports( // node that points at the specific call expression. origin = getOriginNodeForDiagnostics(ref.value.mwpCall, expr); } - diagnostics.push(makeDiagnostic( - ErrorCode.COMPONENT_UNKNOWN_IMPORT, origin, + diagnostics.push( + makeDiagnostic( + ErrorCode.COMPONENT_UNKNOWN_IMPORT, + origin, `Component imports contains a ModuleWithProviders value, likely the result of a 'Module.forRoot()'-style call. ` + - `These calls are not used to configure components and are not valid in standalone component imports - ` + - `consider importing them in the application bootstrap instead.`)); + `These calls are not used to configure components and are not valid in standalone component imports - ` + + `consider importing them in the application bootstrap instead.`, + ), + ); } else { diagnostics.push(createValueHasWrongTypeError(expr, imports, errorMessage).toDiagnostic()); } @@ -118,8 +144,9 @@ export function validateAndFlattenComponentImports( * approximation only suitable for error reporting as any resolved object with an `ngModule` * key is considered a `ModuleWithProviders`. */ -function isLikelyModuleWithProviders(value: ResolvedValue): - value is SyntheticValue|ResolvedValueMap { +function isLikelyModuleWithProviders( + value: ResolvedValue, +): value is SyntheticValue | ResolvedValueMap { if (value instanceof SyntheticValue && isResolvedModuleWithProviders(value)) { // This is a `ModuleWithProviders` as extracted from a foreign function call. return true; diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/test/component_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/component/test/component_spec.ts index 41fe15e790292..ddeee72a4aa46 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/test/component_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/test/component_spec.ts @@ -13,15 +13,36 @@ import {CycleAnalyzer, CycleHandlingStrategy, ImportGraph} from '../../../cycles import {ErrorCode, FatalDiagnosticError, ngErrorCode} from '../../../diagnostics'; import {absoluteFrom} from '../../../file_system'; import {runInEachFileSystem} from '../../../file_system/testing'; -import {DeferredSymbolTracker, ImportedSymbolsTracker, ModuleResolver, Reference, ReferenceEmitter} from '../../../imports'; -import {CompoundMetadataReader, DtsMetadataReader, HostDirectivesResolver, LocalMetadataRegistry, ResourceRegistry} from '../../../metadata'; +import { + DeferredSymbolTracker, + ImportedSymbolsTracker, + ModuleResolver, + Reference, + ReferenceEmitter, +} from '../../../imports'; +import { + CompoundMetadataReader, + DtsMetadataReader, + HostDirectivesResolver, + LocalMetadataRegistry, + ResourceRegistry, +} from '../../../metadata'; import {PartialEvaluator} from '../../../partial_evaluator'; import {NOOP_PERF_RECORDER} from '../../../perf'; import {isNamedClassDeclaration, TypeScriptReflectionHost} from '../../../reflection'; -import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver, TypeCheckScopeRegistry} from '../../../scope'; +import { + LocalModuleScopeRegistry, + MetadataDtsModuleScopeResolver, + TypeCheckScopeRegistry, +} from '../../../scope'; import {getDeclaration, makeProgram} from '../../../testing'; import {CompilationMode} from '../../../transform'; -import {InjectableClassRegistry, NoopReferencesRegistry, ResourceLoader, ResourceLoaderContext} from '../../common'; +import { + InjectableClassRegistry, + NoopReferencesRegistry, + ResourceLoader, + ResourceLoaderContext, +} from '../../common'; import {ComponentDecoratorHandler} from '../src/handler'; export class StubResourceLoader implements ResourceLoader { @@ -33,7 +54,7 @@ export class StubResourceLoader implements ResourceLoader { load(v: string): string { return ''; } - preload(): Promise|undefined { + preload(): Promise | undefined { throw new Error('Not implemented'); } preprocessInline(_data: string, _context: ResourceLoaderContext): Promise { @@ -42,16 +63,23 @@ export class StubResourceLoader implements ResourceLoader { } function setup( - program: ts.Program, options: ts.CompilerOptions, host: ts.CompilerHost, - opts: {compilationMode: CompilationMode, usePoisonedData?: boolean} = { - compilationMode: CompilationMode.FULL - }) { + program: ts.Program, + options: ts.CompilerOptions, + host: ts.CompilerHost, + opts: {compilationMode: CompilationMode; usePoisonedData?: boolean} = { + compilationMode: CompilationMode.FULL, + }, +) { const {compilationMode, usePoisonedData} = opts; const checker = program.getTypeChecker(); const reflectionHost = new TypeScriptReflectionHost(checker); const evaluator = new PartialEvaluator(reflectionHost, checker, /* dependencyTracker */ null); - const moduleResolver = - new ModuleResolver(program, options, host, /* moduleResolutionCache */ null); + const moduleResolver = new ModuleResolver( + program, + options, + host, + /* moduleResolutionCache */ null, + ); const importGraph = new ImportGraph(checker, NOOP_PERF_RECORDER); const cycleAnalyzer = new CycleAnalyzer(importGraph); const metaRegistry = new LocalMetadataRegistry(); @@ -59,54 +87,62 @@ function setup( const dtsResolver = new MetadataDtsModuleScopeResolver(dtsReader, null); const metaReader = new CompoundMetadataReader([metaRegistry, dtsReader]); const scopeRegistry = new LocalModuleScopeRegistry( - metaRegistry, metaReader, dtsResolver, new ReferenceEmitter([]), null); + metaRegistry, + metaReader, + dtsResolver, + new ReferenceEmitter([]), + null, + ); const refEmitter = new ReferenceEmitter([]); const referencesRegistry = new NoopReferencesRegistry(); const injectableRegistry = new InjectableClassRegistry(reflectionHost, /* isCore */ false); const resourceRegistry = new ResourceRegistry(); const hostDirectivesResolver = new HostDirectivesResolver(metaReader); - const typeCheckScopeRegistry = - new TypeCheckScopeRegistry(scopeRegistry, metaReader, hostDirectivesResolver); + const typeCheckScopeRegistry = new TypeCheckScopeRegistry( + scopeRegistry, + metaReader, + hostDirectivesResolver, + ); const resourceLoader = new StubResourceLoader(); const importTracker = new ImportedSymbolsTracker(); const handler = new ComponentDecoratorHandler( - reflectionHost, - evaluator, - metaRegistry, - metaReader, - scopeRegistry, - dtsResolver, - scopeRegistry, - typeCheckScopeRegistry, - resourceRegistry, - /* isCore */ false, - /* strictCtorDeps */ false, - resourceLoader, - /* rootDirs */['/'], - /* defaultPreserveWhitespaces */ false, - /* i18nUseExternalIds */ true, - /* enableI18nLegacyMessageIdFormat */ false, - !!usePoisonedData, - /* i18nNormalizeLineEndingsInICUs */ false, - moduleResolver, - cycleAnalyzer, - CycleHandlingStrategy.UseRemoteScoping, - refEmitter, - referencesRegistry, - /* depTracker */ null, - injectableRegistry, - /* semanticDepGraphUpdater */ null, - /* annotateForClosureCompiler */ false, - NOOP_PERF_RECORDER, - hostDirectivesResolver, - importTracker, - true, - compilationMode, - new DeferredSymbolTracker(checker, /* onlyExplicitDeferDependencyImports */ false), - /* forbidOrphanRenderering */ false, - /* enableBlockSyntax */ true, - /* localCompilationExtraImportsTracker */ null, + reflectionHost, + evaluator, + metaRegistry, + metaReader, + scopeRegistry, + dtsResolver, + scopeRegistry, + typeCheckScopeRegistry, + resourceRegistry, + /* isCore */ false, + /* strictCtorDeps */ false, + resourceLoader, + /* rootDirs */ ['/'], + /* defaultPreserveWhitespaces */ false, + /* i18nUseExternalIds */ true, + /* enableI18nLegacyMessageIdFormat */ false, + !!usePoisonedData, + /* i18nNormalizeLineEndingsInICUs */ false, + moduleResolver, + cycleAnalyzer, + CycleHandlingStrategy.UseRemoteScoping, + refEmitter, + referencesRegistry, + /* depTracker */ null, + injectableRegistry, + /* semanticDepGraphUpdater */ null, + /* annotateForClosureCompiler */ false, + NOOP_PERF_RECORDER, + hostDirectivesResolver, + importTracker, + true, + compilationMode, + new DeferredSymbolTracker(checker, /* onlyExplicitDeferDependencyImports */ false), + /* forbidOrphanRenderering */ false, + /* enableBlockSyntax */ true, + /* localCompilationExtraImportsTracker */ null, ); return {reflectionHost, handler, resourceLoader, metaRegistry}; } @@ -114,7 +150,7 @@ function setup( runInEachFileSystem(() => { describe('ComponentDecoratorHandler', () => { let _: typeof absoluteFrom; - beforeEach(() => _ = absoluteFrom); + beforeEach(() => (_ = absoluteFrom)); it('should produce a diagnostic when @Component has non-literal argument', () => { const {program, options, host} = makeProgram([ @@ -129,7 +165,7 @@ runInEachFileSystem(() => { const TEST = ''; @Component(TEST) class TestCmp {} - ` + `, }, ]); const {reflectionHost, handler} = setup(program, options, host); @@ -167,7 +203,7 @@ runInEachFileSystem(() => { @Component({ template: '${template}', }) class TestCmp {} - ` + `, }, ]); const {reflectionHost, handler} = setup(program, options, host); @@ -200,7 +236,7 @@ runInEachFileSystem(() => { @Component({ templateUrl: '${templateUrl}', }) class TestCmp {} - ` + `, }, ]); const {reflectionHost, handler} = setup(program, options, host); @@ -237,7 +273,7 @@ runInEachFileSystem(() => { styleUrls: ['/myStyle.css', ignoredStyleUrl], styles: ['a { color: red; }', 'b { color: blue; }', ignoredStyle, ...[ignoredStyle]], }) class TestCmp {} - ` + `, }, ]); const {reflectionHost, handler} = setup(program, options, host); @@ -267,7 +303,7 @@ runInEachFileSystem(() => { @Component({ template: TEMPLATE, }) class TestCmp {} - ` + `, }, ]); const {reflectionHost, handler} = setup(program, options, host); @@ -294,7 +330,7 @@ runInEachFileSystem(() => { @Component({ template: '${template}', }) class TestCmp {} - ` + `, }, ]); @@ -308,8 +344,12 @@ runInEachFileSystem(() => { const symbol = handler.symbol(TestCmp, analysis!); const resolution = handler.resolve(TestCmp, analysis!, symbol); - const compileResult = - handler.compileFull(TestCmp, analysis!, resolution.data!, new ConstantPool()); + const compileResult = handler.compileFull( + TestCmp, + analysis!, + resolution.data!, + new ConstantPool(), + ); expect(compileResult).toEqual([]); }); @@ -328,13 +368,13 @@ runInEachFileSystem(() => { template: '', styles: ['.abc {}'] }) class TestCmp {} - ` + `, }, ]); const {reflectionHost, handler, resourceLoader} = setup(program, options, host); resourceLoader.canPreload = true; resourceLoader.canPreprocess = true; - resourceLoader.preprocessInline = async function(data, context) { + resourceLoader.preprocessInline = async function (data, context) { expect(data).toBe('.abc {}'); expect(context.containingFile).toBe(_('/entry.ts').toLowerCase()); expect(context.type).toBe('style'); @@ -369,7 +409,7 @@ runInEachFileSystem(() => { template: '', styles: ['.abc {}'] }) class TestCmp {} - ` + `, }, ]); const {reflectionHost, handler, resourceLoader} = setup(program, options, host); @@ -382,8 +422,9 @@ runInEachFileSystem(() => { return fail('Failed to recognize @Component'); } - expect(() => handler.analyze(TestCmp, detected.metadata)) - .toThrowError('Inline resource processing requires asynchronous preanalyze.'); + expect(() => handler.analyze(TestCmp, detected.metadata)).toThrowError( + 'Inline resource processing requires asynchronous preanalyze.', + ); }); it('should not error if component has no inline styles and canPreprocess is true', async () => { @@ -400,13 +441,13 @@ runInEachFileSystem(() => { @Component({ template: '', }) class TestCmp {} - ` + `, }, ]); const {reflectionHost, handler, resourceLoader} = setup(program, options, host); resourceLoader.canPreload = true; resourceLoader.canPreprocess = true; - resourceLoader.preprocessInline = async function(data, context) { + resourceLoader.preprocessInline = async function (data, context) { fail('preprocessInline should not have been called.'); return data; }; @@ -446,7 +487,7 @@ runInEachFileSystem(() => { ], }) class TestCmp {} - ` + `, }, ]); const {reflectionHost, handler, metaRegistry} = setup(program, options, host); @@ -459,7 +500,8 @@ runInEachFileSystem(() => { handler.register(TestCmp, analysis!); const meta = metaRegistry.getDirectiveMetadata(new Reference(TestCmp)); expect(meta?.animationTriggerNames?.staticTriggerNames).toEqual([ - 'animationName', 'nestedAnimationName' + 'animationName', + 'nestedAnimationName', ]); expect(meta?.animationTriggerNames?.includesDynamicAnimations).toBeFalse(); }); @@ -492,7 +534,7 @@ runInEachFileSystem(() => { ], }) class TestCmp {} - ` + `, }, ]); const {reflectionHost, handler, metaRegistry} = setup(program, options, host); @@ -533,7 +575,7 @@ runInEachFileSystem(() => { animations: buildComplexAnimations(), }) class TestCmp {} - ` + `, }, ]); const {reflectionHost, handler, metaRegistry} = setup(program, options, host); @@ -552,14 +594,14 @@ runInEachFileSystem(() => { describe('localCompilation', () => { it('should not produce diagnostic for cross-file imports in standalone component', () => { const {program, options, host} = makeProgram( - [ - { - name: _('/node_modules/@angular/core/index.d.ts'), - contents: 'export const Component: any;', - }, - { - name: _('/entry.ts'), - contents: ` + [ + { + name: _('/node_modules/@angular/core/index.d.ts'), + contents: 'export const Component: any;', + }, + { + name: _('/entry.ts'), + contents: ` import {Component} from '@angular/core'; import {SomeModule} from './some_where'; @@ -569,16 +611,22 @@ runInEachFileSystem(() => { template: 'Hi!', imports: [SomeModule], }) class TestCmp {} - ` - }, - ], - undefined, undefined, false); - const {reflectionHost, handler} = - setup(program, options, host, {compilationMode: CompilationMode.LOCAL}); + `, + }, + ], + undefined, + undefined, + false, + ); + const {reflectionHost, handler} = setup(program, options, host, { + compilationMode: CompilationMode.LOCAL, + }); const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration); - const detected = - handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp)); + const detected = handler.detect( + TestCmp, + reflectionHost.getDecoratorsOfDeclaration(TestCmp), + ); if (detected === undefined) { return fail('Failed to recognize @Component'); } @@ -589,14 +637,14 @@ runInEachFileSystem(() => { it('should produce diagnostic for imports in non-standalone component', () => { const {program, options, host} = makeProgram( - [ - { - name: _('/node_modules/@angular/core/index.d.ts'), - contents: 'export const Component: any;', - }, - { - name: _('/entry.ts'), - contents: ` + [ + { + name: _('/node_modules/@angular/core/index.d.ts'), + contents: 'export const Component: any;', + }, + { + name: _('/entry.ts'), + contents: ` import {Component} from '@angular/core'; import {SomeModule} from './some_where'; @@ -605,37 +653,45 @@ runInEachFileSystem(() => { template: 'Hi!', imports: [SomeModule], }) class TestCmp {} - ` - }, - ], - undefined, undefined, false); - const {reflectionHost, handler} = - setup(program, options, host, {compilationMode: CompilationMode.LOCAL}); + `, + }, + ], + undefined, + undefined, + false, + ); + const {reflectionHost, handler} = setup(program, options, host, { + compilationMode: CompilationMode.LOCAL, + }); const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration); - const detected = - handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp)); + const detected = handler.detect( + TestCmp, + reflectionHost.getDecoratorsOfDeclaration(TestCmp), + ); if (detected === undefined) { return fail('Failed to recognize @Component'); } const {diagnostics} = handler.analyze(TestCmp, detected.metadata); - expect(diagnostics).toContain(jasmine.objectContaining({ - code: ngErrorCode(ErrorCode.COMPONENT_NOT_STANDALONE), - messageText: jasmine.stringContaining(`'imports' is only valid`), - })); + expect(diagnostics).toContain( + jasmine.objectContaining({ + code: ngErrorCode(ErrorCode.COMPONENT_NOT_STANDALONE), + messageText: jasmine.stringContaining(`'imports' is only valid`), + }), + ); }); it('should not produce diagnostic for cross-file schemas in standalone component', () => { const {program, options, host} = makeProgram( - [ - { - name: _('/node_modules/@angular/core/index.d.ts'), - contents: 'export const Component: any; export const CUSTOM_ELEMENTS_SCHEMA: any;', - }, - { - name: _('/entry.ts'), - contents: ` + [ + { + name: _('/node_modules/@angular/core/index.d.ts'), + contents: 'export const Component: any; export const CUSTOM_ELEMENTS_SCHEMA: any;', + }, + { + name: _('/entry.ts'), + contents: ` import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; import {SomeModule} from './some_where'; @@ -645,16 +701,22 @@ runInEachFileSystem(() => { template: 'Hi!', schemas: [CUSTOM_ELEMENTS_SCHEMA], }) class TestCmp {} - ` - }, - ], - undefined, undefined, false); - const {reflectionHost, handler} = - setup(program, options, host, {compilationMode: CompilationMode.LOCAL}); + `, + }, + ], + undefined, + undefined, + false, + ); + const {reflectionHost, handler} = setup(program, options, host, { + compilationMode: CompilationMode.LOCAL, + }); const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration); - const detected = - handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp)); + const detected = handler.detect( + TestCmp, + reflectionHost.getDecoratorsOfDeclaration(TestCmp), + ); if (detected === undefined) { return fail('Failed to recognize @Component'); } @@ -666,14 +728,14 @@ runInEachFileSystem(() => { it('should produce diagnostic for schemas in non-standalone component', () => { const {program, options, host} = makeProgram( - [ - { - name: _('/node_modules/@angular/core/index.d.ts'), - contents: 'export const Component: any; export const CUSTOM_ELEMENTS_SCHEMA: any;', - }, - { - name: _('/entry.ts'), - contents: ` + [ + { + name: _('/node_modules/@angular/core/index.d.ts'), + contents: 'export const Component: any; export const CUSTOM_ELEMENTS_SCHEMA: any;', + }, + { + name: _('/entry.ts'), + contents: ` import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; import {SomeModule} from './some_where'; @@ -682,26 +744,34 @@ runInEachFileSystem(() => { template: 'Hi!', schemas: [CUSTOM_ELEMENTS_SCHEMA], }) class TestCmp {} - ` - }, - ], - undefined, undefined, false); - const {reflectionHost, handler} = - setup(program, options, host, {compilationMode: CompilationMode.LOCAL}); + `, + }, + ], + undefined, + undefined, + false, + ); + const {reflectionHost, handler} = setup(program, options, host, { + compilationMode: CompilationMode.LOCAL, + }); const TestCmp = getDeclaration(program, _('/entry.ts'), 'TestCmp', isNamedClassDeclaration); - const detected = - handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp)); + const detected = handler.detect( + TestCmp, + reflectionHost.getDecoratorsOfDeclaration(TestCmp), + ); if (detected === undefined) { return fail('Failed to recognize @Component'); } const {diagnostics} = handler.analyze(TestCmp, detected.metadata); - expect(diagnostics).toContain(jasmine.objectContaining({ - code: ngErrorCode(ErrorCode.COMPONENT_NOT_STANDALONE), - messageText: jasmine.stringContaining(`'schemas' is only valid`), - })); + expect(diagnostics).toContain( + jasmine.objectContaining({ + code: ngErrorCode(ErrorCode.COMPONENT_NOT_STANDALONE), + messageText: jasmine.stringContaining(`'schemas' is only valid`), + }), + ); }); }); }); diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts index d2d239c49a510..ca0ce43e16e88 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts @@ -6,88 +6,161 @@ * found in the LICENSE file at https://angular.io/license */ -import {compileClassMetadata, compileDeclareClassMetadata, compileDeclareDirectiveFromMetadata, compileDirectiveFromMetadata, ConstantPool, FactoryTarget, makeBindingParser, R3ClassMetadata, R3DirectiveMetadata, WrappedNodeExpr} from '@angular/compiler'; +import { + compileClassMetadata, + compileDeclareClassMetadata, + compileDeclareDirectiveFromMetadata, + compileDirectiveFromMetadata, + ConstantPool, + FactoryTarget, + makeBindingParser, + R3ClassMetadata, + R3DirectiveMetadata, + WrappedNodeExpr, +} from '@angular/compiler'; import ts from 'typescript'; import {ImportedSymbolsTracker, Reference, ReferenceEmitter} from '../../../imports'; -import {extractSemanticTypeParameters, SemanticDepGraphUpdater} from '../../../incremental/semantic_graph'; -import {ClassPropertyMapping, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, HostDirectiveMeta, InputMapping, MatchSource, MetadataReader, MetadataRegistry, MetaKind} from '../../../metadata'; +import { + extractSemanticTypeParameters, + SemanticDepGraphUpdater, +} from '../../../incremental/semantic_graph'; +import { + ClassPropertyMapping, + DirectiveTypeCheckMeta, + extractDirectiveTypeCheckMeta, + HostDirectiveMeta, + InputMapping, + MatchSource, + MetadataReader, + MetadataRegistry, + MetaKind, +} from '../../../metadata'; import {PartialEvaluator} from '../../../partial_evaluator'; import {PerfEvent, PerfRecorder} from '../../../perf'; -import {ClassDeclaration, ClassMember, ClassMemberKind, Decorator, ReflectionHost} from '../../../reflection'; +import { + ClassDeclaration, + ClassMember, + ClassMemberKind, + Decorator, + ReflectionHost, +} from '../../../reflection'; import {LocalModuleScopeRegistry} from '../../../scope'; -import {AnalysisOutput, CompilationMode, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../../transform'; -import {compileDeclareFactory, compileInputTransformFields, compileNgFactoryDefField, compileResults, extractClassMetadata, findAngularDecorator, getDirectiveDiagnostics, getProviderDiagnostics, getUndecoratedClassWithAngularFeaturesDiagnostic, InjectableClassRegistry, isAngularDecorator, readBaseClass, ReferencesRegistry, resolveProvidersRequiringFactory, toFactoryMetadata, validateHostDirectives} from '../../common'; +import { + AnalysisOutput, + CompilationMode, + CompileResult, + DecoratorHandler, + DetectResult, + HandlerPrecedence, + ResolveResult, +} from '../../../transform'; +import { + compileDeclareFactory, + compileInputTransformFields, + compileNgFactoryDefField, + compileResults, + extractClassMetadata, + findAngularDecorator, + getDirectiveDiagnostics, + getProviderDiagnostics, + getUndecoratedClassWithAngularFeaturesDiagnostic, + InjectableClassRegistry, + isAngularDecorator, + readBaseClass, + ReferencesRegistry, + resolveProvidersRequiringFactory, + toFactoryMetadata, + validateHostDirectives, +} from '../../common'; import {extractDirectiveMetadata} from './shared'; import {DirectiveSymbol} from './symbol'; const FIELD_DECORATORS = [ - 'Input', 'Output', 'ViewChild', 'ViewChildren', 'ContentChild', 'ContentChildren', 'HostBinding', - 'HostListener' + 'Input', + 'Output', + 'ViewChild', + 'ViewChildren', + 'ContentChild', + 'ContentChildren', + 'HostBinding', + 'HostListener', ]; const LIFECYCLE_HOOKS = new Set([ - 'ngOnChanges', 'ngOnInit', 'ngOnDestroy', 'ngDoCheck', 'ngAfterViewInit', 'ngAfterViewChecked', - 'ngAfterContentInit', 'ngAfterContentChecked' + 'ngOnChanges', + 'ngOnInit', + 'ngOnDestroy', + 'ngDoCheck', + 'ngAfterViewInit', + 'ngAfterViewChecked', + 'ngAfterContentInit', + 'ngAfterContentChecked', ]); export interface DirectiveHandlerData { - baseClass: Reference|'dynamic'|null; + baseClass: Reference | 'dynamic' | null; typeCheckMeta: DirectiveTypeCheckMeta; meta: R3DirectiveMetadata; - classMetadata: R3ClassMetadata|null; - providersRequiringFactory: Set>|null; + classMetadata: R3ClassMetadata | null; + providersRequiringFactory: Set> | null; inputs: ClassPropertyMapping; outputs: ClassPropertyMapping; isPoisoned: boolean; isStructural: boolean; - decorator: ts.Decorator|null; - hostDirectives: HostDirectiveMeta[]|null; - rawHostDirectives: ts.Expression|null; + decorator: ts.Decorator | null; + hostDirectives: HostDirectiveMeta[] | null; + rawHostDirectives: ts.Expression | null; } -export class DirectiveDecoratorHandler implements - DecoratorHandler { +export class DirectiveDecoratorHandler + implements DecoratorHandler +{ constructor( - private reflector: ReflectionHost, - private evaluator: PartialEvaluator, - private metaRegistry: MetadataRegistry, - private scopeRegistry: LocalModuleScopeRegistry, - private metaReader: MetadataReader, - private injectableRegistry: InjectableClassRegistry, - private refEmitter: ReferenceEmitter, - private referencesRegistry: ReferencesRegistry, - private isCore: boolean, - private strictCtorDeps: boolean, - private semanticDepGraphUpdater: SemanticDepGraphUpdater|null, - private annotateForClosureCompiler: boolean, - private perf: PerfRecorder, - private importTracker: ImportedSymbolsTracker, - private includeClassMetadata: boolean, - private readonly compilationMode: CompilationMode, - private readonly generateExtraImportsInLocalMode: boolean, + private reflector: ReflectionHost, + private evaluator: PartialEvaluator, + private metaRegistry: MetadataRegistry, + private scopeRegistry: LocalModuleScopeRegistry, + private metaReader: MetadataReader, + private injectableRegistry: InjectableClassRegistry, + private refEmitter: ReferenceEmitter, + private referencesRegistry: ReferencesRegistry, + private isCore: boolean, + private strictCtorDeps: boolean, + private semanticDepGraphUpdater: SemanticDepGraphUpdater | null, + private annotateForClosureCompiler: boolean, + private perf: PerfRecorder, + private importTracker: ImportedSymbolsTracker, + private includeClassMetadata: boolean, + private readonly compilationMode: CompilationMode, + private readonly generateExtraImportsInLocalMode: boolean, ) {} readonly precedence = HandlerPrecedence.PRIMARY; readonly name = 'DirectiveDecoratorHandler'; - detect(node: ClassDeclaration, decorators: Decorator[]|null): - DetectResult|undefined { + detect( + node: ClassDeclaration, + decorators: Decorator[] | null, + ): DetectResult | undefined { // If a class is undecorated but uses Angular features, we detect it as an // abstract directive. This is an unsupported pattern as of v10, but we want // to still detect these patterns so that we can report diagnostics. if (!decorators) { const angularField = this.findClassFieldWithAngularFeatures(node); - return angularField ? {trigger: angularField.node, decorator: null, metadata: null} : - undefined; + return angularField + ? {trigger: angularField.node, decorator: null, metadata: null} + : undefined; } else { const decorator = findAngularDecorator(decorators, 'Directive', this.isCore); return decorator ? {trigger: decorator.node, decorator, metadata: decorator} : undefined; } } - analyze(node: ClassDeclaration, decorator: Readonly): - AnalysisOutput { + analyze( + node: ClassDeclaration, + decorator: Readonly, + ): AnalysisOutput { // Skip processing of the class declaration if compilation of undecorated classes // with Angular features is disabled. Previously in ngtsc, such classes have always // been processed, but we want to enforce a consistent decorator mental model. @@ -104,18 +177,30 @@ export class DirectiveDecoratorHandler implements this.perf.eventCount(PerfEvent.AnalyzeDirective); const directiveResult = extractDirectiveMetadata( - node, decorator, this.reflector, this.importTracker, this.evaluator, this.refEmitter, - this.referencesRegistry, this.isCore, this.annotateForClosureCompiler, this.compilationMode, - /* defaultSelector */ null); + node, + decorator, + this.reflector, + this.importTracker, + this.evaluator, + this.refEmitter, + this.referencesRegistry, + this.isCore, + this.annotateForClosureCompiler, + this.compilationMode, + /* defaultSelector */ null, + ); if (directiveResult === undefined) { return {}; } const analysis = directiveResult.metadata; - let providersRequiringFactory: Set>|null = null; + let providersRequiringFactory: Set> | null = null; if (directiveResult !== undefined && directiveResult.decorator.has('providers')) { providersRequiringFactory = resolveProvidersRequiringFactory( - directiveResult.decorator.get('providers')!, this.reflector, this.evaluator); + directiveResult.decorator.get('providers')!, + this.reflector, + this.evaluator, + ); } return { @@ -125,17 +210,16 @@ export class DirectiveDecoratorHandler implements meta: analysis, hostDirectives: directiveResult.hostDirectives, rawHostDirectives: directiveResult.rawHostDirectives, - classMetadata: this.includeClassMetadata ? - extractClassMetadata( - node, this.reflector, this.isCore, this.annotateForClosureCompiler) : - null, + classMetadata: this.includeClassMetadata + ? extractClassMetadata(node, this.reflector, this.isCore, this.annotateForClosureCompiler) + : null, baseClass: readBaseClass(node, this.reflector, this.evaluator), typeCheckMeta: extractDirectiveTypeCheckMeta(node, directiveResult.inputs, this.reflector), providersRequiringFactory, isPoisoned: false, isStructural: directiveResult.isStructural, - decorator: decorator?.node as ts.Decorator | null ?? null, - } + decorator: (decorator?.node as ts.Decorator | null) ?? null, + }, }; } @@ -143,8 +227,14 @@ export class DirectiveDecoratorHandler implements const typeParameters = extractSemanticTypeParameters(node); return new DirectiveSymbol( - node, analysis.meta.selector, analysis.inputs, analysis.outputs, analysis.meta.exportAs, - analysis.typeCheckMeta, typeParameters); + node, + analysis.meta.selector, + analysis.inputs, + analysis.outputs, + analysis.meta.exportAs, + analysis.typeCheckMeta, + typeParameters, + ); } register(node: ClassDeclaration, analysis: Readonly): void { @@ -160,7 +250,7 @@ export class DirectiveDecoratorHandler implements exportAs: analysis.meta.exportAs, inputs: analysis.inputs, outputs: analysis.outputs, - queries: analysis.meta.queries.map(query => query.propertyName), + queries: analysis.meta.queries.map((query) => query.propertyName), isComponent: false, baseClass: analysis.baseClass, hostDirectives: analysis.hostDirectives, @@ -187,8 +277,11 @@ export class DirectiveDecoratorHandler implements }); } - resolve(node: ClassDeclaration, analysis: DirectiveHandlerData, symbol: DirectiveSymbol): - ResolveResult { + resolve( + node: ClassDeclaration, + analysis: DirectiveHandlerData, + symbol: DirectiveSymbol, + ): ResolveResult { if (this.compilationMode === CompilationMode.LOCAL) { return {}; } @@ -198,25 +291,39 @@ export class DirectiveDecoratorHandler implements } const diagnostics: ts.Diagnostic[] = []; - if (analysis.providersRequiringFactory !== null && - analysis.meta.providers instanceof WrappedNodeExpr) { + if ( + analysis.providersRequiringFactory !== null && + analysis.meta.providers instanceof WrappedNodeExpr + ) { const providerDiagnostics = getProviderDiagnostics( - analysis.providersRequiringFactory, analysis.meta.providers!.node, - this.injectableRegistry); + analysis.providersRequiringFactory, + analysis.meta.providers!.node, + this.injectableRegistry, + ); diagnostics.push(...providerDiagnostics); } const directiveDiagnostics = getDirectiveDiagnostics( - node, this.injectableRegistry, this.evaluator, this.reflector, this.scopeRegistry, - this.strictCtorDeps, 'Directive'); + node, + this.injectableRegistry, + this.evaluator, + this.reflector, + this.scopeRegistry, + this.strictCtorDeps, + 'Directive', + ); if (directiveDiagnostics !== null) { diagnostics.push(...directiveDiagnostics); } - const hostDirectivesDiagnotics = analysis.hostDirectives && analysis.rawHostDirectives ? - validateHostDirectives( - analysis.rawHostDirectives, analysis.hostDirectives, this.metaReader) : - null; + const hostDirectivesDiagnotics = + analysis.hostDirectives && analysis.rawHostDirectives + ? validateHostDirectives( + analysis.rawHostDirectives, + analysis.hostDirectives, + this.metaReader, + ) + : null; if (hostDirectivesDiagnotics !== null) { diagnostics.push(...hostDirectivesDiagnotics); } @@ -225,43 +332,72 @@ export class DirectiveDecoratorHandler implements } compileFull( - node: ClassDeclaration, analysis: Readonly, - resolution: Readonly, pool: ConstantPool): CompileResult[] { + node: ClassDeclaration, + analysis: Readonly, + resolution: Readonly, + pool: ConstantPool, + ): CompileResult[] { const fac = compileNgFactoryDefField(toFactoryMetadata(analysis.meta, FactoryTarget.Directive)); const def = compileDirectiveFromMetadata(analysis.meta, pool, makeBindingParser()); const inputTransformFields = compileInputTransformFields(analysis.inputs); - const classMetadata = analysis.classMetadata !== null ? - compileClassMetadata(analysis.classMetadata).toStmt() : - null; + const classMetadata = + analysis.classMetadata !== null + ? compileClassMetadata(analysis.classMetadata).toStmt() + : null; return compileResults( - fac, def, classMetadata, 'ɵdir', inputTransformFields, null /* deferrableImports */); + fac, + def, + classMetadata, + 'ɵdir', + inputTransformFields, + null /* deferrableImports */, + ); } compilePartial( - node: ClassDeclaration, analysis: Readonly, - resolution: Readonly): CompileResult[] { + node: ClassDeclaration, + analysis: Readonly, + resolution: Readonly, + ): CompileResult[] { const fac = compileDeclareFactory(toFactoryMetadata(analysis.meta, FactoryTarget.Directive)); const def = compileDeclareDirectiveFromMetadata(analysis.meta); const inputTransformFields = compileInputTransformFields(analysis.inputs); - const classMetadata = analysis.classMetadata !== null ? - compileDeclareClassMetadata(analysis.classMetadata).toStmt() : - null; + const classMetadata = + analysis.classMetadata !== null + ? compileDeclareClassMetadata(analysis.classMetadata).toStmt() + : null; return compileResults( - fac, def, classMetadata, 'ɵdir', inputTransformFields, null /* deferrableImports */); + fac, + def, + classMetadata, + 'ɵdir', + inputTransformFields, + null /* deferrableImports */, + ); } compileLocal( - node: ClassDeclaration, analysis: Readonly, - resolution: Readonly, pool: ConstantPool): CompileResult[] { + node: ClassDeclaration, + analysis: Readonly, + resolution: Readonly, + pool: ConstantPool, + ): CompileResult[] { const fac = compileNgFactoryDefField(toFactoryMetadata(analysis.meta, FactoryTarget.Directive)); const def = compileDirectiveFromMetadata(analysis.meta, pool, makeBindingParser()); const inputTransformFields = compileInputTransformFields(analysis.inputs); - const classMetadata = analysis.classMetadata !== null ? - compileClassMetadata(analysis.classMetadata).toStmt() : - null; + const classMetadata = + analysis.classMetadata !== null + ? compileClassMetadata(analysis.classMetadata).toStmt() + : null; return compileResults( - fac, def, classMetadata, 'ɵdir', inputTransformFields, null /* deferrableImports */); + fac, + def, + classMetadata, + 'ɵdir', + inputTransformFields, + null /* deferrableImports */, + ); } /** @@ -270,16 +406,21 @@ export class DirectiveDecoratorHandler implements * contain class members that are either decorated with a known Angular decorator, * or if they correspond to a known Angular lifecycle hook. */ - private findClassFieldWithAngularFeatures(node: ClassDeclaration): ClassMember|undefined { - return this.reflector.getMembersOfClass(node).find(member => { - if (!member.isStatic && member.kind === ClassMemberKind.Method && - LIFECYCLE_HOOKS.has(member.name)) { + private findClassFieldWithAngularFeatures(node: ClassDeclaration): ClassMember | undefined { + return this.reflector.getMembersOfClass(node).find((member) => { + if ( + !member.isStatic && + member.kind === ClassMemberKind.Method && + LIFECYCLE_HOOKS.has(member.name) + ) { return true; } if (member.decorators) { - return member.decorators.some( - decorator => FIELD_DECORATORS.some( - decoratorName => isAngularDecorator(decorator, decoratorName, this.isCore))); + return member.decorators.some((decorator) => + FIELD_DECORATORS.some((decoratorName) => + isAngularDecorator(decorator, decoratorName, this.isCore), + ), + ); } return false; }); diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/initializer_function_access.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/initializer_function_access.ts index 947ff47ffc018..a5bbe01966536 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/initializer_function_access.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/initializer_function_access.ts @@ -20,15 +20,24 @@ import {InitializerFunctionMetadata} from './initializer_functions'; * incompatible. */ export function validateAccessOfInitializerApiMember( - {api, call}: InitializerFunctionMetadata, member: Pick): void { + {api, call}: InitializerFunctionMetadata, + member: Pick, +): void { if (!api.allowedAccessLevels.includes(member.accessLevel)) { throw new FatalDiagnosticError( - ErrorCode.INITIALIZER_API_DISALLOWED_MEMBER_VISIBILITY, call, - makeDiagnosticChain( - `Cannot use "${api.functionName}" on a class member that is declared as ${ - classMemberAccessLevelToString(member.accessLevel)}.`, - [makeDiagnosticChain( - `Update the class field to be either: ` + - api.allowedAccessLevels.map(l => classMemberAccessLevelToString(l)).join(', '))])); + ErrorCode.INITIALIZER_API_DISALLOWED_MEMBER_VISIBILITY, + call, + makeDiagnosticChain( + `Cannot use "${api.functionName}" on a class member that is declared as ${classMemberAccessLevelToString( + member.accessLevel, + )}.`, + [ + makeDiagnosticChain( + `Update the class field to be either: ` + + api.allowedAccessLevels.map((l) => classMemberAccessLevelToString(l)).join(', '), + ), + ], + ), + ); } } diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/initializer_functions.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/initializer_functions.ts index 68b0b104a22da..37d3fee671b3c 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/initializer_functions.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/initializer_functions.ts @@ -25,10 +25,17 @@ import {ClassMemberAccessLevel, ReflectionHost} from '../../../reflection'; export interface InitializerApiFunction { /** Module name where the initializer function is imported from. */ - owningModule: '@angular/core'|'@angular/core/rxjs-interop'; + owningModule: '@angular/core' | '@angular/core/rxjs-interop'; /** Export name of the initializer function. */ - functionName: ('input'|'model'|'output'|'outputFromObservable'|'viewChild'|'viewChildren'| - 'contentChild'|'contentChildren'); + functionName: + | 'input' + | 'model' + | 'output' + | 'outputFromObservable' + | 'viewChild' + | 'viewChildren' + | 'contentChild' + | 'contentChildren'; /** Class member access levels compatible with the API. */ allowedAccessLevels: ClassMemberAccessLevel[]; } @@ -70,16 +77,19 @@ interface StaticInitializerData { * @returns The parsed initializer API, or null if none was found. */ export function tryParseInitializerApi( - functions: Functions, expression: ts.Expression, reflector: ReflectionHost, - importTracker: ImportedSymbolsTracker): (InitializerFunctionMetadata&{api: Functions[number]}| - null) { + functions: Functions, + expression: ts.Expression, + reflector: ReflectionHost, + importTracker: ImportedSymbolsTracker, +): (InitializerFunctionMetadata & {api: Functions[number]}) | null { if (!ts.isCallExpression(expression)) { return null; } - const staticResult = parseTopLevelCall(expression, functions, importTracker) || - parseTopLevelRequiredCall(expression, functions, importTracker) || - parseTopLevelCallFromNamespace(expression, functions, importTracker); + const staticResult = + parseTopLevelCall(expression, functions, importTracker) || + parseTopLevelRequiredCall(expression, functions, importTracker) || + parseTopLevelCallFromNamespace(expression, functions, importTracker); if (staticResult === null) { return null; @@ -91,8 +101,11 @@ export function tryParseInitializerApi - importTracker.isPotentialReferenceToNamedImport(node, fn.functionName, fn.owningModule)); + const matchingApi = functions.find((fn) => + importTracker.isPotentialReferenceToNamedImport(node, fn.functionName, fn.owningModule), + ); if (matchingApi === undefined) { return null; } @@ -131,19 +146,24 @@ function parseTopLevelCall( * e.g. `prop = input.required()`. Returns null if it can't be parsed. */ function parseTopLevelRequiredCall( - call: ts.CallExpression, functions: InitializerApiFunction[], - importTracker: ImportedSymbolsTracker): StaticInitializerData|null { + call: ts.CallExpression, + functions: InitializerApiFunction[], + importTracker: ImportedSymbolsTracker, +): StaticInitializerData | null { const node = call.expression; - if (!ts.isPropertyAccessExpression(node) || !ts.isIdentifier(node.expression) || - node.name.text !== 'required') { + if ( + !ts.isPropertyAccessExpression(node) || + !ts.isIdentifier(node.expression) || + node.name.text !== 'required' + ) { return null; } const expression = node.expression; - const matchingApi = functions.find( - fn => importTracker.isPotentialReferenceToNamedImport( - expression, fn.functionName, fn.owningModule)); + const matchingApi = functions.find((fn) => + importTracker.isPotentialReferenceToNamedImport(expression, fn.functionName, fn.owningModule), + ); if (matchingApi === undefined) { return null; } @@ -151,22 +171,23 @@ function parseTopLevelRequiredCall( return {api: matchingApi, apiReference: expression, isRequired: true}; } - /** * Attempts to parse a top-level call to a function referenced via a namespace import, * e.g. `prop = core.input.required()`. Returns null if it can't be parsed. */ function parseTopLevelCallFromNamespace( - call: ts.CallExpression, functions: InitializerApiFunction[], - importTracker: ImportedSymbolsTracker): StaticInitializerData|null { + call: ts.CallExpression, + functions: InitializerApiFunction[], + importTracker: ImportedSymbolsTracker, +): StaticInitializerData | null { const node = call.expression; if (!ts.isPropertyAccessExpression(node)) { return null; } - let apiReference: ts.Identifier|null = null; - let matchingApi: InitializerApiFunction|undefined = undefined; + let apiReference: ts.Identifier | null = null; + let matchingApi: InitializerApiFunction | undefined = undefined; let isRequired = false; // `prop = core.input()` @@ -175,20 +196,26 @@ function parseTopLevelCallFromNamespace( apiReference = node.name; matchingApi = functions.find( - fn => node.name.text === fn.functionName && - importTracker.isPotentialReferenceToNamespaceImport(namespaceRef, fn.owningModule)); + (fn) => + node.name.text === fn.functionName && + importTracker.isPotentialReferenceToNamespaceImport(namespaceRef, fn.owningModule), + ); } else if ( - // `prop = core.input.required()` - ts.isPropertyAccessExpression(node.expression) && - ts.isIdentifier(node.expression.expression) && ts.isIdentifier(node.expression.name) && - node.name.text === 'required') { + // `prop = core.input.required()` + ts.isPropertyAccessExpression(node.expression) && + ts.isIdentifier(node.expression.expression) && + ts.isIdentifier(node.expression.name) && + node.name.text === 'required' + ) { const potentialName = node.expression.name.text; const namespaceRef = node.expression.expression; apiReference = node.expression.name; matchingApi = functions.find( - fn => fn.functionName === potentialName && - importTracker.isPotentialReferenceToNamespaceImport(namespaceRef!, fn.owningModule)); + (fn) => + fn.functionName === potentialName && + importTracker.isPotentialReferenceToNamespaceImport(namespaceRef!, fn.owningModule), + ); isRequired = true; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/input_function.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/input_function.ts index ed98ff3c67e75..e141ff8961446 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/input_function.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/input_function.ts @@ -37,25 +37,31 @@ export const INPUT_INITIALIZER_FN: InitializerApiFunction = { * input mapping if possible. */ export function tryParseSignalInputMapping( - member: Pick, reflector: ReflectionHost, - importTracker: ImportedSymbolsTracker): InputMapping|null { + member: Pick, + reflector: ReflectionHost, + importTracker: ImportedSymbolsTracker, +): InputMapping | null { if (member.value === null) { return null; } - const signalInput = - tryParseInitializerApi([INPUT_INITIALIZER_FN], member.value, reflector, importTracker); + const signalInput = tryParseInitializerApi( + [INPUT_INITIALIZER_FN], + member.value, + reflector, + importTracker, + ); if (signalInput === null) { return null; } validateAccessOfInitializerApiMember(signalInput, member); - const optionsNode = (signalInput.isRequired ? signalInput.call.arguments[0] : - signalInput.call.arguments[1]) as ts.Expression | - undefined; + const optionsNode = ( + signalInput.isRequired ? signalInput.call.arguments[0] : signalInput.call.arguments[1] + ) as ts.Expression | undefined; const options = - optionsNode !== undefined ? parseAndValidateInputAndOutputOptions(optionsNode) : null; + optionsNode !== undefined ? parseAndValidateInputAndOutputOptions(optionsNode) : null; const classPropertyName = member.name; return { diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/input_output_parse_options.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/input_output_parse_options.ts index 9ee615bef7135..87866be37eb4e 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/input_output_parse_options.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/input_output_parse_options.ts @@ -18,23 +18,28 @@ import {reflectObjectLiteral} from '../../../reflection'; * options for signal inputs are runtime constructs that aren't relevant at * compile time. */ -export function parseAndValidateInputAndOutputOptions(optionsNode: ts.Expression): - {alias: string|undefined} { +export function parseAndValidateInputAndOutputOptions(optionsNode: ts.Expression): { + alias: string | undefined; +} { if (!ts.isObjectLiteralExpression(optionsNode)) { throw new FatalDiagnosticError( - ErrorCode.VALUE_HAS_WRONG_TYPE, optionsNode, - 'Argument needs to be an object literal that is statically analyzable.'); + ErrorCode.VALUE_HAS_WRONG_TYPE, + optionsNode, + 'Argument needs to be an object literal that is statically analyzable.', + ); } const options = reflectObjectLiteral(optionsNode); - let alias: string|undefined = undefined; + let alias: string | undefined = undefined; if (options.has('alias')) { const aliasExpr = options.get('alias')!; if (!ts.isStringLiteralLike(aliasExpr)) { throw new FatalDiagnosticError( - ErrorCode.VALUE_HAS_WRONG_TYPE, aliasExpr, - 'Alias needs to be a string that is statically analyzable.'); + ErrorCode.VALUE_HAS_WRONG_TYPE, + aliasExpr, + 'Alias needs to be a string that is statically analyzable.', + ); } alias = aliasExpr.text; diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/model_function.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/model_function.ts index 6e459b87af035..70876b63b418b 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/model_function.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/model_function.ts @@ -35,25 +35,31 @@ export const MODEL_INITIALIZER_FN: InitializerApiFunction = { * Attempts to parse a model class member. Returns the parsed model mapping if possible. */ export function tryParseSignalModelMapping( - member: Pick, reflector: ReflectionHost, - importTracker: ImportedSymbolsTracker): ModelMapping|null { + member: Pick, + reflector: ReflectionHost, + importTracker: ImportedSymbolsTracker, +): ModelMapping | null { if (member.value === null) { return null; } - const model = - tryParseInitializerApi([MODEL_INITIALIZER_FN], member.value, reflector, importTracker); + const model = tryParseInitializerApi( + [MODEL_INITIALIZER_FN], + member.value, + reflector, + importTracker, + ); if (model === null) { return null; } validateAccessOfInitializerApiMember(model, member); - const optionsNode = - (model.isRequired ? model.call.arguments[0] : model.call.arguments[1]) as ts.Expression | - undefined; + const optionsNode = (model.isRequired ? model.call.arguments[0] : model.call.arguments[1]) as + | ts.Expression + | undefined; const options = - optionsNode !== undefined ? parseAndValidateInputAndOutputOptions(optionsNode) : null; + optionsNode !== undefined ? parseAndValidateInputAndOutputOptions(optionsNode) : null; const classPropertyName = member.name; const bindingPropertyName = options?.alias ?? classPropertyName; @@ -70,6 +76,6 @@ export function tryParseSignalModelMapping( isSignal: false, classPropertyName, bindingPropertyName: bindingPropertyName + 'Change', - } + }, }; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/output_function.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/output_function.ts index 2e00101372228..9d4a23b95b069 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/output_function.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/output_function.ts @@ -39,7 +39,7 @@ export const OUTPUT_INITIALIZER_FNS: InitializerApiFunction[] = [ { functionName: 'outputFromObservable', owningModule: '@angular/core/rxjs-interop', - allowedAccessLevels + allowedAccessLevels, }, ]; @@ -48,33 +48,40 @@ export const OUTPUT_INITIALIZER_FNS: InitializerApiFunction[] = [ * input mapping if possible. */ export function tryParseInitializerBasedOutput( - member: Pick, reflector: ReflectionHost, - importTracker: ImportedSymbolsTracker): {call: ts.CallExpression, metadata: InputOrOutput}| - null { + member: Pick, + reflector: ReflectionHost, + importTracker: ImportedSymbolsTracker, +): {call: ts.CallExpression; metadata: InputOrOutput} | null { if (member.value === null) { return null; } - const output = - tryParseInitializerApi(OUTPUT_INITIALIZER_FNS, member.value, reflector, importTracker); + const output = tryParseInitializerApi( + OUTPUT_INITIALIZER_FNS, + member.value, + reflector, + importTracker, + ); if (output === null) { return null; } if (output.isRequired) { throw new FatalDiagnosticError( - ErrorCode.INITIALIZER_API_NO_REQUIRED_FUNCTION, output.call, - `Output does not support ".required()".`); + ErrorCode.INITIALIZER_API_NO_REQUIRED_FUNCTION, + output.call, + `Output does not support ".required()".`, + ); } validateAccessOfInitializerApiMember(output, member); // Options are the first parameter for `output()`, while for // the interop `outputFromObservable()` they are the second argument. - const optionsNode = (output.api.functionName === 'output' ? - output.call.arguments[0] : - output.call.arguments[1]) as (ts.Expression | undefined); + const optionsNode = ( + output.api.functionName === 'output' ? output.call.arguments[0] : output.call.arguments[1] + ) as ts.Expression | undefined; const options = - optionsNode !== undefined ? parseAndValidateInputAndOutputOptions(optionsNode) : null; + optionsNode !== undefined ? parseAndValidateInputAndOutputOptions(optionsNode) : null; const classPropertyName = member.name; return { @@ -84,6 +91,6 @@ export function tryParseInitializerBasedOutput( isSignal: false, classPropertyName, bindingPropertyName: options?.alias ?? classPropertyName, - } + }, }; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/query_functions.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/query_functions.ts index 193f66ee1f68a..0c004fa77c38b 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/query_functions.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/query_functions.ts @@ -6,41 +6,54 @@ * found in the LICENSE file at https://angular.io/license */ - -import {createMayBeForwardRefExpression, ForwardRefHandling, MaybeForwardRefExpression, outputAst as o, R3QueryMetadata} from '@angular/compiler'; +import { + createMayBeForwardRefExpression, + ForwardRefHandling, + MaybeForwardRefExpression, + outputAst as o, + R3QueryMetadata, +} from '@angular/compiler'; import ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../../diagnostics'; import {ImportedSymbolsTracker} from '../../../imports'; -import {ClassMember, ClassMemberAccessLevel, ReflectionHost, reflectObjectLiteral} from '../../../reflection'; +import { + ClassMember, + ClassMemberAccessLevel, + ReflectionHost, + reflectObjectLiteral, +} from '../../../reflection'; import {tryUnwrapForwardRef} from '../../common'; import {validateAccessOfInitializerApiMember} from './initializer_function_access'; import {tryParseInitializerApi} from './initializer_functions'; /** Possible query initializer API functions. */ -export type QueryFunctionName = 'viewChild'|'contentChild'|'viewChildren'|'contentChildren'; +export type QueryFunctionName = 'viewChild' | 'contentChild' | 'viewChildren' | 'contentChildren'; /** Possible names of query initializer APIs. */ -const queryFunctionNames: QueryFunctionName[] = - ['viewChild', 'viewChildren', 'contentChild', 'contentChildren']; +const queryFunctionNames: QueryFunctionName[] = [ + 'viewChild', + 'viewChildren', + 'contentChild', + 'contentChildren', +]; /** Possible query initializer API functions. */ -export const QUERY_INITIALIZER_FNS = queryFunctionNames.map( - fnName => ({ - functionName: fnName, - owningModule: '@angular/core' as const, - // Queries are accessed from within static blocks, via the query definition functions. - // Conceptually, the fields could access private members— even ES private fields. - // Support for ES private fields requires special caution and complexity when partial - // output is linked— hence not supported. TS private members are allowed in static blocks. - allowedAccessLevels: [ - ClassMemberAccessLevel.PublicWritable, - ClassMemberAccessLevel.PublicReadonly, - ClassMemberAccessLevel.Protected, - ClassMemberAccessLevel.Private, - ], - })); +export const QUERY_INITIALIZER_FNS = queryFunctionNames.map((fnName) => ({ + functionName: fnName, + owningModule: '@angular/core' as const, + // Queries are accessed from within static blocks, via the query definition functions. + // Conceptually, the fields could access private members— even ES private fields. + // Support for ES private fields requires special caution and complexity when partial + // output is linked— hence not supported. TS private members are allowed in static blocks. + allowedAccessLevels: [ + ClassMemberAccessLevel.PublicWritable, + ClassMemberAccessLevel.PublicReadonly, + ClassMemberAccessLevel.Protected, + ClassMemberAccessLevel.Private, + ], +})); // The `descendants` option is enabled by default, except for content children. const defaultDescendantsValue = (type: QueryFunctionName) => type !== 'contentChildren'; @@ -54,15 +67,20 @@ const defaultDescendantsValue = (type: QueryFunctionName) => type !== 'contentCh * @returns Resolved query metadata, or null if no query is declared. */ export function tryParseSignalQueryFromInitializer( - member: Pick, reflector: ReflectionHost, - importTracker: ImportedSymbolsTracker): - {name: QueryFunctionName, metadata: R3QueryMetadata, call: ts.CallExpression}|null { + member: Pick, + reflector: ReflectionHost, + importTracker: ImportedSymbolsTracker, +): {name: QueryFunctionName; metadata: R3QueryMetadata; call: ts.CallExpression} | null { if (member.value === null) { return null; } - const query = - tryParseInitializerApi(QUERY_INITIALIZER_FNS, member.value, reflector, importTracker); + const query = tryParseInitializerApi( + QUERY_INITIALIZER_FNS, + member.value, + reflector, + importTracker, + ); if (query === null) { return null; } @@ -74,19 +92,25 @@ export function tryParseSignalQueryFromInitializer( const predicateNode = query.call.arguments[0] as ts.Expression | undefined; if (predicateNode === undefined) { throw new FatalDiagnosticError( - ErrorCode.VALUE_HAS_WRONG_TYPE, query.call, 'No locator specified.'); + ErrorCode.VALUE_HAS_WRONG_TYPE, + query.call, + 'No locator specified.', + ); } const optionsNode = query.call.arguments[1] as ts.Expression | undefined; if (optionsNode !== undefined && !ts.isObjectLiteralExpression(optionsNode)) { throw new FatalDiagnosticError( - ErrorCode.VALUE_HAS_WRONG_TYPE, optionsNode, 'Argument needs to be an object literal.'); + ErrorCode.VALUE_HAS_WRONG_TYPE, + optionsNode, + 'Argument needs to be an object literal.', + ); } const options = optionsNode && reflectObjectLiteral(optionsNode); const read = options?.has('read') ? parseReadOption(options.get('read')!) : null; - const descendants = options?.has('descendants') ? - parseDescendantsOption(options.get('descendants')!) : - defaultDescendantsValue(functionName); + const descendants = options?.has('descendants') + ? parseDescendantsOption(options.get('descendants')!) + : defaultDescendantsValue(functionName); return { name: functionName, @@ -105,8 +129,10 @@ export function tryParseSignalQueryFromInitializer( } /** Parses the locator/predicate of the query. */ -function parseLocator(expression: ts.Expression, reflector: ReflectionHost): string[]| - MaybeForwardRefExpression { +function parseLocator( + expression: ts.Expression, + reflector: ReflectionHost, +): string[] | MaybeForwardRefExpression { // Attempt to unwrap `forwardRef` calls. const unwrappedExpression = tryUnwrapForwardRef(expression, reflector); if (unwrappedExpression !== null) { @@ -118,8 +144,9 @@ function parseLocator(expression: ts.Expression, reflector: ReflectionHost): str } return createMayBeForwardRefExpression( - new o.WrappedNodeExpr(expression), - unwrappedExpression !== null ? ForwardRefHandling.Unwrapped : ForwardRefHandling.None); + new o.WrappedNodeExpr(expression), + unwrappedExpression !== null ? ForwardRefHandling.Unwrapped : ForwardRefHandling.None, + ); } /** @@ -134,19 +161,26 @@ function parseLocator(expression: ts.Expression, reflector: ReflectionHost): str * live outside of the class in the static class definition. */ function parseReadOption(value: ts.Expression): o.Expression { - if (ts.isExpressionWithTypeArguments(value) || ts.isParenthesizedExpression(value) || - ts.isAsExpression(value)) { + if ( + ts.isExpressionWithTypeArguments(value) || + ts.isParenthesizedExpression(value) || + ts.isAsExpression(value) + ) { return parseReadOption(value.expression); } - if (ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression) || - ts.isIdentifier(value)) { + if ( + (ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression)) || + ts.isIdentifier(value) + ) { return new o.WrappedNodeExpr(value); } throw new FatalDiagnosticError( - ErrorCode.VALUE_NOT_LITERAL, value, - `Query "read" option expected a literal class reference.`); + ErrorCode.VALUE_NOT_LITERAL, + value, + `Query "read" option expected a literal class reference.`, + ); } /** Parses the `descendants` option of a query. */ @@ -157,6 +191,8 @@ function parseDescendantsOption(value: ts.Expression): boolean { return false; } throw new FatalDiagnosticError( - ErrorCode.VALUE_HAS_WRONG_TYPE, value, - `Expected "descendants" option to be a boolean literal.`); + ErrorCode.VALUE_HAS_WRONG_TYPE, + value, + `Expected "descendants" option to be a boolean literal.`, + ); } diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts index b367ba21124e1..dee6fcf80d6f1 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts @@ -6,16 +6,79 @@ * found in the LICENSE file at https://angular.io/license */ -import {createMayBeForwardRefExpression, emitDistinctChangesOnlyDefaultValue, Expression, ExternalExpr, ForwardRefHandling, getSafePropertyAccessString, MaybeForwardRefExpression, ParsedHostBindings, ParseError, parseHostBindings, R3DirectiveMetadata, R3HostDirectiveMetadata, R3InputMetadata, R3QueryMetadata, R3Reference, verifyHostBindings, WrappedNodeExpr} from '@angular/compiler'; +import { + createMayBeForwardRefExpression, + emitDistinctChangesOnlyDefaultValue, + Expression, + ExternalExpr, + ForwardRefHandling, + getSafePropertyAccessString, + MaybeForwardRefExpression, + ParsedHostBindings, + ParseError, + parseHostBindings, + R3DirectiveMetadata, + R3HostDirectiveMetadata, + R3InputMetadata, + R3QueryMetadata, + R3Reference, + verifyHostBindings, + WrappedNodeExpr, +} from '@angular/compiler'; import ts from 'typescript'; import {ErrorCode, FatalDiagnosticError, makeRelatedInformation} from '../../../diagnostics'; -import {assertSuccessfulReferenceEmit, ImportedSymbolsTracker, ImportFlags, Reference, ReferenceEmitter} from '../../../imports'; -import {ClassPropertyMapping, DecoratorInputTransform, HostDirectiveMeta, InputMapping, InputOrOutput, isHostDirectiveMetaForGlobalMode} from '../../../metadata'; -import {DynamicValue, EnumValue, PartialEvaluator, ResolvedValue, traceDynamicValue} from '../../../partial_evaluator'; -import {AmbientImport, ClassDeclaration, ClassMember, ClassMemberKind, Decorator, filterToMembersWithDecorator, isNamedClassDeclaration, ReflectionHost, reflectObjectLiteral} from '../../../reflection'; +import { + assertSuccessfulReferenceEmit, + ImportedSymbolsTracker, + ImportFlags, + Reference, + ReferenceEmitter, +} from '../../../imports'; +import { + ClassPropertyMapping, + DecoratorInputTransform, + HostDirectiveMeta, + InputMapping, + InputOrOutput, + isHostDirectiveMetaForGlobalMode, +} from '../../../metadata'; +import { + DynamicValue, + EnumValue, + PartialEvaluator, + ResolvedValue, + traceDynamicValue, +} from '../../../partial_evaluator'; +import { + AmbientImport, + ClassDeclaration, + ClassMember, + ClassMemberKind, + Decorator, + filterToMembersWithDecorator, + isNamedClassDeclaration, + ReflectionHost, + reflectObjectLiteral, +} from '../../../reflection'; import {CompilationMode} from '../../../transform'; -import {assertLocalCompilationUnresolvedConst, createSourceSpan, createValueHasWrongTypeError, forwardRefResolver, getAngularDecorators, getConstructorDependencies, isAngularDecorator, ReferencesRegistry, toR3Reference, tryUnwrapForwardRef, unwrapConstructorDependencies, unwrapExpression, validateConstructorDependencies, wrapFunctionExpressionsInParens, wrapTypeReference} from '../../common'; +import { + assertLocalCompilationUnresolvedConst, + createSourceSpan, + createValueHasWrongTypeError, + forwardRefResolver, + getAngularDecorators, + getConstructorDependencies, + isAngularDecorator, + ReferencesRegistry, + toR3Reference, + tryUnwrapForwardRef, + unwrapConstructorDependencies, + unwrapExpression, + validateConstructorDependencies, + wrapFunctionExpressionsInParens, + wrapTypeReference, +} from '../../common'; import {tryParseSignalInputMapping} from './input_function'; import {tryParseSignalModelMapping} from './model_function'; @@ -24,9 +87,13 @@ import {tryParseSignalQueryFromInitializer} from './query_functions'; const EMPTY_OBJECT: {[key: string]: string} = {}; -type QueryDecoratorName = 'ViewChild'|'ViewChildren'|'ContentChild'|'ContentChildren'; -export const queryDecoratorNames: QueryDecoratorName[] = - ['ViewChild', 'ViewChildren', 'ContentChild', 'ContentChildren']; +type QueryDecoratorName = 'ViewChild' | 'ViewChildren' | 'ContentChild' | 'ContentChildren'; +export const queryDecoratorNames: QueryDecoratorName[] = [ + 'ViewChild', + 'ViewChildren', + 'ContentChild', + 'ContentChildren', +]; const QUERY_TYPES = new Set(queryDecoratorNames); @@ -37,31 +104,45 @@ const QUERY_TYPES = new Set(queryDecoratorNames); * the module. */ export function extractDirectiveMetadata( - clazz: ClassDeclaration, decorator: Readonly, reflector: ReflectionHost, - importTracker: ImportedSymbolsTracker, evaluator: PartialEvaluator, - refEmitter: ReferenceEmitter, referencesRegistry: ReferencesRegistry, isCore: boolean, - annotateForClosureCompiler: boolean, compilationMode: CompilationMode, - defaultSelector: string|null): { - decorator: Map, - metadata: R3DirectiveMetadata, - inputs: ClassPropertyMapping, - outputs: ClassPropertyMapping, - isStructural: boolean; - hostDirectives: HostDirectiveMeta[] | null, rawHostDirectives: ts.Expression | null, -}|undefined { + clazz: ClassDeclaration, + decorator: Readonly, + reflector: ReflectionHost, + importTracker: ImportedSymbolsTracker, + evaluator: PartialEvaluator, + refEmitter: ReferenceEmitter, + referencesRegistry: ReferencesRegistry, + isCore: boolean, + annotateForClosureCompiler: boolean, + compilationMode: CompilationMode, + defaultSelector: string | null, +): + | { + decorator: Map; + metadata: R3DirectiveMetadata; + inputs: ClassPropertyMapping; + outputs: ClassPropertyMapping; + isStructural: boolean; + hostDirectives: HostDirectiveMeta[] | null; + rawHostDirectives: ts.Expression | null; + } + | undefined { let directive: Map; if (decorator.args === null || decorator.args.length === 0) { directive = new Map(); } else if (decorator.args.length !== 1) { throw new FatalDiagnosticError( - ErrorCode.DECORATOR_ARITY_WRONG, decorator.node, - `Incorrect number of arguments to @${decorator.name} decorator`); + ErrorCode.DECORATOR_ARITY_WRONG, + decorator.node, + `Incorrect number of arguments to @${decorator.name} decorator`, + ); } else { const meta = unwrapExpression(decorator.args[0]); if (!ts.isObjectLiteralExpression(meta)) { throw new FatalDiagnosticError( - ErrorCode.DECORATOR_ARG_NOT_LITERAL, meta, - `@${decorator.name} argument must be an object literal`); + ErrorCode.DECORATOR_ARG_NOT_LITERAL, + meta, + `@${decorator.name} argument must be an object literal`, + ); } directive = reflectObjectLiteral(meta); } @@ -75,50 +156,85 @@ export function extractDirectiveMetadata( // Precompute a list of ts.ClassElements that have decorators. This includes things like @Input, // @Output, @HostBinding, etc. - const decoratedElements = - members.filter(member => !member.isStatic && member.decorators !== null); + const decoratedElements = members.filter( + (member) => !member.isStatic && member.decorators !== null, + ); const coreModule = isCore ? undefined : '@angular/core'; // Construct the map of inputs both from the @Directive/@Component // decorator, and the decorated fields. - const inputsFromMeta = - parseInputsArray(clazz, directive, evaluator, reflector, refEmitter, compilationMode); + const inputsFromMeta = parseInputsArray( + clazz, + directive, + evaluator, + reflector, + refEmitter, + compilationMode, + ); const inputsFromFields = parseInputFields( - clazz, members, evaluator, reflector, importTracker, refEmitter, isCore, compilationMode, - inputsFromMeta, decorator); + clazz, + members, + evaluator, + reflector, + importTracker, + refEmitter, + isCore, + compilationMode, + inputsFromMeta, + decorator, + ); const inputs = ClassPropertyMapping.fromMappedObject({...inputsFromMeta, ...inputsFromFields}); // And outputs. const outputsFromMeta = parseOutputsArray(directive, evaluator); const outputsFromFields = parseOutputFields( - clazz, decorator, members, isCore, reflector, importTracker, evaluator, outputsFromMeta); + clazz, + decorator, + members, + isCore, + reflector, + importTracker, + evaluator, + outputsFromMeta, + ); const outputs = ClassPropertyMapping.fromMappedObject({...outputsFromMeta, ...outputsFromFields}); // Parse queries of fields. - const {viewQueries, contentQueries} = - parseQueriesOfClassFields(members, reflector, importTracker, evaluator, isCore); + const {viewQueries, contentQueries} = parseQueriesOfClassFields( + members, + reflector, + importTracker, + evaluator, + isCore, + ); if (directive.has('queries')) { const signalQueryFields = new Set( - [...viewQueries, ...contentQueries].filter(q => q.isSignal).map(q => q.propertyName)); - const queriesFromDecorator = - extractQueriesFromDecorator(directive.get('queries')!, reflector, evaluator, isCore); + [...viewQueries, ...contentQueries].filter((q) => q.isSignal).map((q) => q.propertyName), + ); + const queriesFromDecorator = extractQueriesFromDecorator( + directive.get('queries')!, + reflector, + evaluator, + isCore, + ); // Checks if the query is already declared/reserved via class members declaration. // If so, we throw a fatal diagnostic error to prevent this unintentional pattern. - const checkAndUnwrapQuery = (q: {expr: ts.Expression, metadata: R3QueryMetadata}) => { + const checkAndUnwrapQuery = (q: {expr: ts.Expression; metadata: R3QueryMetadata}) => { if (signalQueryFields.has(q.metadata.propertyName)) { throw new FatalDiagnosticError( - ErrorCode.INITIALIZER_API_DECORATOR_METADATA_COLLISION, q.expr, - `Query is declared multiple times. "@${ - decorator.name}" declares a query for the same property.`); + ErrorCode.INITIALIZER_API_DECORATOR_METADATA_COLLISION, + q.expr, + `Query is declared multiple times. "@${decorator.name}" declares a query for the same property.`, + ); } return q.metadata; }; - contentQueries.push(...queriesFromDecorator.content.map(q => checkAndUnwrapQuery(q))); - viewQueries.push(...queriesFromDecorator.view.map(q => checkAndUnwrapQuery(q))); + contentQueries.push(...queriesFromDecorator.content.map((q) => checkAndUnwrapQuery(q))); + viewQueries.push(...queriesFromDecorator.view.map((q) => checkAndUnwrapQuery(q))); } // Parse the selector. @@ -127,12 +243,15 @@ export function extractDirectiveMetadata( const expr = directive.get('selector')!; const resolved = evaluator.evaluate(expr); assertLocalCompilationUnresolvedConst( - compilationMode, resolved, null, - 'Unresolved identifier found for @Component.selector field! Did you ' + - 'import this identifier from a file outside of the compilation unit? ' + - 'This is not allowed when Angular compiler runs in local mode. Possible ' + - 'solutions: 1) Move the declarations into a file within the compilation ' + - 'unit, 2) Inline the selector'); + compilationMode, + resolved, + null, + 'Unresolved identifier found for @Component.selector field! Did you ' + + 'import this identifier from a file outside of the compilation unit? ' + + 'This is not allowed when Angular compiler runs in local mode. Possible ' + + 'solutions: 1) Move the declarations into a file within the compilation ' + + 'unit, 2) Inline the selector', + ); if (typeof resolved !== 'string') { throw createValueHasWrongTypeError(expr, resolved, `selector must be a string`); } @@ -140,44 +259,56 @@ export function extractDirectiveMetadata( selector = resolved === '' ? defaultSelector : resolved; if (!selector) { throw new FatalDiagnosticError( - ErrorCode.DIRECTIVE_MISSING_SELECTOR, expr, - `Directive ${clazz.name.text} has no selector, please add it!`); + ErrorCode.DIRECTIVE_MISSING_SELECTOR, + expr, + `Directive ${clazz.name.text} has no selector, please add it!`, + ); } } - const host = - extractHostBindings(decoratedElements, evaluator, coreModule, compilationMode, directive); + const host = extractHostBindings( + decoratedElements, + evaluator, + coreModule, + compilationMode, + directive, + ); - const providers: Expression|null = directive.has('providers') ? - new WrappedNodeExpr( - annotateForClosureCompiler ? - wrapFunctionExpressionsInParens(directive.get('providers')!) : - directive.get('providers')!) : - null; + const providers: Expression | null = directive.has('providers') + ? new WrappedNodeExpr( + annotateForClosureCompiler + ? wrapFunctionExpressionsInParens(directive.get('providers')!) + : directive.get('providers')!, + ) + : null; // Determine if `ngOnChanges` is a lifecycle hook defined on the component. const usesOnChanges = members.some( - member => !member.isStatic && member.kind === ClassMemberKind.Method && - member.name === 'ngOnChanges'); + (member) => + !member.isStatic && member.kind === ClassMemberKind.Method && member.name === 'ngOnChanges', + ); // Parse exportAs. - let exportAs: string[]|null = null; + let exportAs: string[] | null = null; if (directive.has('exportAs')) { const expr = directive.get('exportAs')!; const resolved = evaluator.evaluate(expr); assertLocalCompilationUnresolvedConst( - compilationMode, resolved, null, - 'Unresolved identifier found for exportAs field! Did you import this ' + - 'identifier from a file outside of the compilation unit? This is not ' + - 'allowed when Angular compiler runs in local mode. Possible solutions: ' + - '1) Move the declarations into a file within the compilation unit, ' + - '2) Inline the selector'); + compilationMode, + resolved, + null, + 'Unresolved identifier found for exportAs field! Did you import this ' + + 'identifier from a file outside of the compilation unit? This is not ' + + 'allowed when Angular compiler runs in local mode. Possible solutions: ' + + '1) Move the declarations into a file within the compilation unit, ' + + '2) Inline the selector', + ); if (typeof resolved !== 'string') { throw createValueHasWrongTypeError(expr, resolved, `exportAs must be a string`); } - exportAs = resolved.split(',').map(part => part.trim()); + exportAs = resolved.split(',').map((part) => part.trim()); } const rawCtorDeps = getConstructorDependencies(clazz, reflector, isCore); @@ -185,15 +316,21 @@ export function extractDirectiveMetadata( // Non-abstract directives (those with a selector) require valid constructor dependencies, whereas // abstract directives are allowed to have invalid dependencies, given that a subclass may call // the constructor explicitly. - const ctorDeps = selector !== null ? validateConstructorDependencies(clazz, rawCtorDeps) : - unwrapConstructorDependencies(rawCtorDeps); + const ctorDeps = + selector !== null + ? validateConstructorDependencies(clazz, rawCtorDeps) + : unwrapConstructorDependencies(rawCtorDeps); // Structural directives must have a `TemplateRef` dependency. - const isStructural = ctorDeps !== null && ctorDeps !== 'invalid' && - ctorDeps.some( - dep => (dep.token instanceof ExternalExpr) && - dep.token.value.moduleName === '@angular/core' && - dep.token.value.name === 'TemplateRef'); + const isStructural = + ctorDeps !== null && + ctorDeps !== 'invalid' && + ctorDeps.some( + (dep) => + dep.token instanceof ExternalExpr && + dep.token.value.moduleName === '@angular/core' && + dep.token.value.name === 'TemplateRef', + ); let isStandalone = false; if (directive.has('standalone')) { @@ -220,22 +357,26 @@ export function extractDirectiveMetadata( const type = wrapTypeReference(reflector, clazz); const rawHostDirectives = directive.get('hostDirectives') || null; - const hostDirectives = rawHostDirectives === null ? - null : - extractHostDirectives(rawHostDirectives, evaluator, compilationMode); + const hostDirectives = + rawHostDirectives === null + ? null + : extractHostDirectives(rawHostDirectives, evaluator, compilationMode); if (compilationMode !== CompilationMode.LOCAL && hostDirectives !== null) { // In global compilation mode where we do type checking, the template type-checker will need to // import host directive types, so add them as referenced by `clazz`. This will ensure that // libraries are required to export host directives which are visible from publicly exported // components. - referencesRegistry.add(clazz, ...hostDirectives.map(hostDir => { - if (!isHostDirectiveMetaForGlobalMode(hostDir)) { - throw new Error('Impossible state'); - } + referencesRegistry.add( + clazz, + ...hostDirectives.map((hostDir) => { + if (!isHostDirectiveMetaForGlobalMode(hostDir)) { + throw new Error('Impossible state'); + } - return hostDir.directive; - })); + return hostDir.directive; + }), + ); } const metadata: R3DirectiveMetadata = { @@ -262,8 +403,8 @@ export function extractDirectiveMetadata( isStandalone, isSignal, hostDirectives: - hostDirectives?.map(hostDir => toHostDirectiveMetadata(hostDir, sourceFile, refEmitter)) || - null, + hostDirectives?.map((hostDir) => toHostDirectiveMetadata(hostDir, sourceFile, refEmitter)) || + null, }; return { decorator: directive, @@ -277,11 +418,19 @@ export function extractDirectiveMetadata( } export function extractDecoratorQueryMetadata( - exprNode: ts.Node, name: string, args: ReadonlyArray, propertyName: string, - reflector: ReflectionHost, evaluator: PartialEvaluator): R3QueryMetadata { + exprNode: ts.Node, + name: string, + args: ReadonlyArray, + propertyName: string, + reflector: ReflectionHost, + evaluator: PartialEvaluator, +): R3QueryMetadata { if (args.length === 0) { throw new FatalDiagnosticError( - ErrorCode.DECORATOR_ARITY_WRONG, exprNode, `@${name} must have arguments`); + ErrorCode.DECORATOR_ARITY_WRONG, + exprNode, + `@${name} must have arguments`, + ); } const first = name === 'ViewChild' || name === 'ContentChild'; const forwardReferenceTarget = tryUnwrapForwardRef(args[0], reflector); @@ -293,12 +442,13 @@ export function extractDecoratorQueryMetadata( let isStatic: boolean = false; // Extract the predicate - let predicate: MaybeForwardRefExpression|string[]|null = null; + let predicate: MaybeForwardRefExpression | string[] | null = null; if (arg instanceof Reference || arg instanceof DynamicValue) { // References and predicates that could not be evaluated statically are emitted as is. predicate = createMayBeForwardRefExpression( - new WrappedNodeExpr(node), - forwardReferenceTarget !== null ? ForwardRefHandling.Unwrapped : ForwardRefHandling.None); + new WrappedNodeExpr(node), + forwardReferenceTarget !== null ? ForwardRefHandling.Unwrapped : ForwardRefHandling.None, + ); } else if (typeof arg === 'string') { predicate = [arg]; } else if (isStringArrayOrDie(arg, `@${name} predicate`, node)) { @@ -308,7 +458,7 @@ export function extractDecoratorQueryMetadata( } // Extract the read and descendants options. - let read: Expression|null = null; + let read: Expression | null = null; // The default value for descendants is true for every decorator except @ContentChildren. let descendants: boolean = name !== 'ContentChildren'; let emitDistinctChangesOnly: boolean = emitDistinctChangesOnlyDefaultValue; @@ -316,8 +466,10 @@ export function extractDecoratorQueryMetadata( const optionsExpr = unwrapExpression(args[1]); if (!ts.isObjectLiteralExpression(optionsExpr)) { throw new FatalDiagnosticError( - ErrorCode.DECORATOR_ARG_NOT_LITERAL, optionsExpr, - `@${name} options must be an object literal`); + ErrorCode.DECORATOR_ARG_NOT_LITERAL, + optionsExpr, + `@${name} options must be an object literal`, + ); } const options = reflectObjectLiteral(optionsExpr); if (options.has('read')) { @@ -329,7 +481,10 @@ export function extractDecoratorQueryMetadata( const descendantsValue = evaluator.evaluate(descendantsExpr); if (typeof descendantsValue !== 'boolean') { throw createValueHasWrongTypeError( - descendantsExpr, descendantsValue, `@${name} options.descendants must be a boolean`); + descendantsExpr, + descendantsValue, + `@${name} options.descendants must be a boolean`, + ); } descendants = descendantsValue; } @@ -339,8 +494,10 @@ export function extractDecoratorQueryMetadata( const emitDistinctChangesOnlyValue = evaluator.evaluate(emitDistinctChangesOnlyExpr); if (typeof emitDistinctChangesOnlyValue !== 'boolean') { throw createValueHasWrongTypeError( - emitDistinctChangesOnlyExpr, emitDistinctChangesOnlyValue, - `@${name} options.emitDistinctChangesOnly must be a boolean`); + emitDistinctChangesOnlyExpr, + emitDistinctChangesOnlyValue, + `@${name} options.emitDistinctChangesOnly must be a boolean`, + ); } emitDistinctChangesOnly = emitDistinctChangesOnlyValue; } @@ -349,15 +506,20 @@ export function extractDecoratorQueryMetadata( const staticValue = evaluator.evaluate(options.get('static')!); if (typeof staticValue !== 'boolean') { throw createValueHasWrongTypeError( - node, staticValue, `@${name} options.static must be a boolean`); + node, + staticValue, + `@${name} options.static must be a boolean`, + ); } isStatic = staticValue; } - } else if (args.length > 2) { // Too many arguments. throw new FatalDiagnosticError( - ErrorCode.DECORATOR_ARITY_WRONG, node, `@${name} has too many arguments`); + ErrorCode.DECORATOR_ARITY_WRONG, + node, + `@${name} has too many arguments`, + ); } return { @@ -372,10 +534,13 @@ export function extractDecoratorQueryMetadata( }; } - export function extractHostBindings( - members: ClassMember[], evaluator: PartialEvaluator, coreModule: string|undefined, - compilationMode: CompilationMode, metadata?: Map): ParsedHostBindings { + members: ClassMember[], + evaluator: PartialEvaluator, + coreModule: string | undefined, + compilationMode: CompilationMode, + metadata?: Map, +): ParsedHostBindings { let bindings: ParsedHostBindings; if (metadata && metadata.has('host')) { bindings = evaluateHostExpressionBindings(metadata.get('host')!, evaluator); @@ -383,133 +548,171 @@ export function extractHostBindings( bindings = parseHostBindings({}); } - filterToMembersWithDecorator(members, 'HostBinding', coreModule) - .forEach(({member, decorators}) => { - decorators.forEach(decorator => { - let hostPropertyName: string = member.name; - if (decorator.args !== null && decorator.args.length > 0) { - if (decorator.args.length !== 1) { - throw new FatalDiagnosticError( - ErrorCode.DECORATOR_ARITY_WRONG, decorator.node, - `@HostBinding can have at most one argument, got ${ - decorator.args.length} argument(s)`); - } - - const resolved = evaluator.evaluate(decorator.args[0]); - - // Specific error for local compilation mode if the argument cannot be resolved - assertLocalCompilationUnresolvedConst( - compilationMode, resolved, null, - 'Unresolved identifier found for @HostBinding\'s argument! Did ' + - 'you import this identifier from a file outside of the compilation ' + - 'unit? This is not allowed when Angular compiler runs in local mode. ' + - 'Possible solutions: 1) Move the declaration into a file within ' + - 'the compilation unit, 2) Inline the argument'); - - if (typeof resolved !== 'string') { - throw createValueHasWrongTypeError( - decorator.node, resolved, `@HostBinding's argument must be a string`); - } + filterToMembersWithDecorator(members, 'HostBinding', coreModule).forEach( + ({member, decorators}) => { + decorators.forEach((decorator) => { + let hostPropertyName: string = member.name; + if (decorator.args !== null && decorator.args.length > 0) { + if (decorator.args.length !== 1) { + throw new FatalDiagnosticError( + ErrorCode.DECORATOR_ARITY_WRONG, + decorator.node, + `@HostBinding can have at most one argument, got ${decorator.args.length} argument(s)`, + ); + } - hostPropertyName = resolved; + const resolved = evaluator.evaluate(decorator.args[0]); + + // Specific error for local compilation mode if the argument cannot be resolved + assertLocalCompilationUnresolvedConst( + compilationMode, + resolved, + null, + "Unresolved identifier found for @HostBinding's argument! Did " + + 'you import this identifier from a file outside of the compilation ' + + 'unit? This is not allowed when Angular compiler runs in local mode. ' + + 'Possible solutions: 1) Move the declaration into a file within ' + + 'the compilation unit, 2) Inline the argument', + ); + + if (typeof resolved !== 'string') { + throw createValueHasWrongTypeError( + decorator.node, + resolved, + `@HostBinding's argument must be a string`, + ); } - // Since this is a decorator, we know that the value is a class member. Always access it - // through `this` so that further down the line it can't be confused for a literal value - // (e.g. if there's a property called `true`). There is no size penalty, because all - // values (except literals) are converted to `ctx.propName` eventually. - bindings.properties[hostPropertyName] = getSafePropertyAccessString('this', member.name); - }); - }); + hostPropertyName = resolved; + } - filterToMembersWithDecorator(members, 'HostListener', coreModule) - .forEach(({member, decorators}) => { - decorators.forEach(decorator => { - let eventName: string = member.name; - let args: string[] = []; - if (decorator.args !== null && decorator.args.length > 0) { - if (decorator.args.length > 2) { - throw new FatalDiagnosticError( - ErrorCode.DECORATOR_ARITY_WRONG, decorator.args[2], - `@HostListener can have at most two arguments`); - } + // Since this is a decorator, we know that the value is a class member. Always access it + // through `this` so that further down the line it can't be confused for a literal value + // (e.g. if there's a property called `true`). There is no size penalty, because all + // values (except literals) are converted to `ctx.propName` eventually. + bindings.properties[hostPropertyName] = getSafePropertyAccessString('this', member.name); + }); + }, + ); + + filterToMembersWithDecorator(members, 'HostListener', coreModule).forEach( + ({member, decorators}) => { + decorators.forEach((decorator) => { + let eventName: string = member.name; + let args: string[] = []; + if (decorator.args !== null && decorator.args.length > 0) { + if (decorator.args.length > 2) { + throw new FatalDiagnosticError( + ErrorCode.DECORATOR_ARITY_WRONG, + decorator.args[2], + `@HostListener can have at most two arguments`, + ); + } - const resolved = evaluator.evaluate(decorator.args[0]); + const resolved = evaluator.evaluate(decorator.args[0]); + + // Specific error for local compilation mode if the event name cannot be resolved + assertLocalCompilationUnresolvedConst( + compilationMode, + resolved, + null, + "Unresolved identifier found for @HostListener's event name " + + 'argument! Did you import this identifier from a file outside of ' + + 'the compilation unit? This is not allowed when Angular compiler ' + + 'runs in local mode. Possible solutions: 1) Move the declaration ' + + 'into a file within the compilation unit, 2) Inline the argument', + ); + + if (typeof resolved !== 'string') { + throw createValueHasWrongTypeError( + decorator.args[0], + resolved, + `@HostListener's event name argument must be a string`, + ); + } - // Specific error for local compilation mode if the event name cannot be resolved - assertLocalCompilationUnresolvedConst( - compilationMode, resolved, null, - 'Unresolved identifier found for @HostListener\'s event name ' + - 'argument! Did you import this identifier from a file outside of ' + - 'the compilation unit? This is not allowed when Angular compiler ' + - 'runs in local mode. Possible solutions: 1) Move the declaration ' + - 'into a file within the compilation unit, 2) Inline the argument'); + eventName = resolved; - if (typeof resolved !== 'string') { + if (decorator.args.length === 2) { + const expression = decorator.args[1]; + const resolvedArgs = evaluator.evaluate(decorator.args[1]); + if (!isStringArrayOrDie(resolvedArgs, '@HostListener.args', expression)) { throw createValueHasWrongTypeError( - decorator.args[0], resolved, - `@HostListener's event name argument must be a string`); - } - - eventName = resolved; - - if (decorator.args.length === 2) { - const expression = decorator.args[1]; - const resolvedArgs = evaluator.evaluate(decorator.args[1]); - if (!isStringArrayOrDie(resolvedArgs, '@HostListener.args', expression)) { - throw createValueHasWrongTypeError( - decorator.args[1], resolvedArgs, - `@HostListener's second argument must be a string array`); - } - args = resolvedArgs; + decorator.args[1], + resolvedArgs, + `@HostListener's second argument must be a string array`, + ); } + args = resolvedArgs; } + } - bindings.listeners[eventName] = `${member.name}(${args.join(',')})`; - }); + bindings.listeners[eventName] = `${member.name}(${args.join(',')})`; }); + }, + ); return bindings; } function extractQueriesFromDecorator( - queryData: ts.Expression, reflector: ReflectionHost, evaluator: PartialEvaluator, - isCore: boolean): { - content: {expr: ts.Expression, metadata: R3QueryMetadata}[], - view: {expr: ts.Expression, metadata: R3QueryMetadata}[], + queryData: ts.Expression, + reflector: ReflectionHost, + evaluator: PartialEvaluator, + isCore: boolean, +): { + content: {expr: ts.Expression; metadata: R3QueryMetadata}[]; + view: {expr: ts.Expression; metadata: R3QueryMetadata}[]; } { - const content: {expr: ts.Expression, metadata: R3QueryMetadata}[] = []; - const view: {expr: ts.Expression, metadata: R3QueryMetadata}[] = []; + const content: {expr: ts.Expression; metadata: R3QueryMetadata}[] = []; + const view: {expr: ts.Expression; metadata: R3QueryMetadata}[] = []; if (!ts.isObjectLiteralExpression(queryData)) { throw new FatalDiagnosticError( - ErrorCode.VALUE_HAS_WRONG_TYPE, queryData, - 'Decorator queries metadata must be an object literal'); + ErrorCode.VALUE_HAS_WRONG_TYPE, + queryData, + 'Decorator queries metadata must be an object literal', + ); } reflectObjectLiteral(queryData).forEach((queryExpr, propertyName) => { queryExpr = unwrapExpression(queryExpr); if (!ts.isNewExpression(queryExpr)) { throw new FatalDiagnosticError( - ErrorCode.VALUE_HAS_WRONG_TYPE, queryData, - 'Decorator query metadata must be an instance of a query type'); + ErrorCode.VALUE_HAS_WRONG_TYPE, + queryData, + 'Decorator query metadata must be an instance of a query type', + ); } - const queryType = ts.isPropertyAccessExpression(queryExpr.expression) ? - queryExpr.expression.name : - queryExpr.expression; + const queryType = ts.isPropertyAccessExpression(queryExpr.expression) + ? queryExpr.expression.name + : queryExpr.expression; if (!ts.isIdentifier(queryType)) { throw new FatalDiagnosticError( - ErrorCode.VALUE_HAS_WRONG_TYPE, queryData, - 'Decorator query metadata must be an instance of a query type'); + ErrorCode.VALUE_HAS_WRONG_TYPE, + queryData, + 'Decorator query metadata must be an instance of a query type', + ); } const type = reflector.getImportOfIdentifier(queryType); - if (type === null || (!isCore && type.from !== '@angular/core') || - !QUERY_TYPES.has(type.name)) { + if ( + type === null || + (!isCore && type.from !== '@angular/core') || + !QUERY_TYPES.has(type.name) + ) { throw new FatalDiagnosticError( - ErrorCode.VALUE_HAS_WRONG_TYPE, queryData, - 'Decorator query metadata must be an instance of a query type'); + ErrorCode.VALUE_HAS_WRONG_TYPE, + queryData, + 'Decorator query metadata must be an instance of a query type', + ); } const query = extractDecoratorQueryMetadata( - queryExpr, type.name, queryExpr.arguments || [], propertyName, reflector, evaluator); + queryExpr, + type.name, + queryExpr.arguments || [], + propertyName, + reflector, + evaluator, + ); if (type.name.startsWith('Content')) { content.push({expr: queryExpr, metadata: query}); } else { @@ -520,8 +723,10 @@ function extractQueriesFromDecorator( } export function parseDirectiveStyles( - directive: Map, evaluator: PartialEvaluator, - compilationMode: CompilationMode): null|string[] { + directive: Map, + evaluator: PartialEvaluator, + compilationMode: CompilationMode, +): null | string[] { const expression = directive.get('styles'); if (!expression) { @@ -534,11 +739,11 @@ export function parseDirectiveStyles( // Check if the identifier used for @Component.styles cannot be resolved in local compilation // mode. if the case, an error specific to this situation is generated. if (compilationMode === CompilationMode.LOCAL) { - let unresolvedNode: ts.Node|null = null; + let unresolvedNode: ts.Node | null = null; if (Array.isArray(value)) { - const entry = value.find(e => e instanceof DynamicValue && e.isFromUnknownIdentifier()) as - DynamicValue | - undefined; + const entry = value.find((e) => e instanceof DynamicValue && e.isFromUnknownIdentifier()) as + | DynamicValue + | undefined; unresolvedNode = entry?.node ?? null; } else if (value instanceof DynamicValue && value.isFromUnknownIdentifier()) { unresolvedNode = value.node; @@ -546,28 +751,34 @@ export function parseDirectiveStyles( if (unresolvedNode !== null) { throw new FatalDiagnosticError( - ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST, unresolvedNode, - 'Unresolved identifier found for @Component.styles field! Did you import ' + - 'this identifier from a file outside of the compilation unit? This is ' + - 'not allowed when Angular compiler runs in local mode. Possible ' + - 'solutions: 1) Move the declarations into a file within the compilation ' + - 'unit, 2) Inline the styles, 3) Move the styles into separate files and ' + - 'include it using @Component.styleUrls'); + ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST, + unresolvedNode, + 'Unresolved identifier found for @Component.styles field! Did you import ' + + 'this identifier from a file outside of the compilation unit? This is ' + + 'not allowed when Angular compiler runs in local mode. Possible ' + + 'solutions: 1) Move the declarations into a file within the compilation ' + + 'unit, 2) Inline the styles, 3) Move the styles into separate files and ' + + 'include it using @Component.styleUrls', + ); } } if (!isStringArrayOrDie(value, 'styles', expression)) { throw createValueHasWrongTypeError( - expression, value, - `Failed to resolve @Component.styles to a string or an array of strings`); + expression, + value, + `Failed to resolve @Component.styles to a string or an array of strings`, + ); } return value; } export function parseFieldStringArrayValue( - directive: Map, field: string, evaluator: PartialEvaluator): null| - string[] { + directive: Map, + field: string, + evaluator: PartialEvaluator, +): null | string[] { if (!directive.has(field)) { return null; } @@ -577,7 +788,10 @@ export function parseFieldStringArrayValue( const value = evaluator.evaluate(expression); if (!isStringArrayOrDie(value, field, expression)) { throw createValueHasWrongTypeError( - expression, value, `Failed to resolve @Directive.${field} to a string array`); + expression, + value, + `Failed to resolve @Directive.${field} to a string array`, + ); } return value; @@ -591,15 +805,21 @@ function isStringArrayOrDie(value: any, name: string, node: ts.Expression): valu for (let i = 0; i < value.length; i++) { if (typeof value[i] !== 'string') { throw createValueHasWrongTypeError( - node, value[i], `Failed to resolve ${name} at position ${i} to a string`); + node, + value[i], + `Failed to resolve ${name} at position ${i} to a string`, + ); } } return true; } function tryGetQueryFromFieldDecorator( - member: ClassMember, reflector: ReflectionHost, evaluator: PartialEvaluator, isCore: boolean): - {name: QueryDecoratorName, decorator: Decorator, metadata: R3QueryMetadata}|null { + member: ClassMember, + reflector: ReflectionHost, + evaluator: PartialEvaluator, + isCore: boolean, +): {name: QueryDecoratorName; decorator: Decorator; metadata: R3QueryMetadata} | null { const decorators = member.decorators; if (decorators === null) { return null; @@ -611,22 +831,29 @@ function tryGetQueryFromFieldDecorator( } if (queryDecorators.length !== 1) { throw new FatalDiagnosticError( - ErrorCode.DECORATOR_COLLISION, member.node ?? queryDecorators[0].node, - 'Cannot combine multiple query decorators.'); + ErrorCode.DECORATOR_COLLISION, + member.node ?? queryDecorators[0].node, + 'Cannot combine multiple query decorators.', + ); } const decorator = queryDecorators[0]; const node = member.node || decorator.node; // Throw in case of `@Input() @ContentChild('foo') foo: any`, which is not supported in Ivy - if (decorators.some(v => v.name === 'Input')) { + if (decorators.some((v) => v.name === 'Input')) { throw new FatalDiagnosticError( - ErrorCode.DECORATOR_COLLISION, node, - 'Cannot combine @Input decorators with query decorators'); + ErrorCode.DECORATOR_COLLISION, + node, + 'Cannot combine @Input decorators with query decorators', + ); } if (!isPropertyTypeMember(member)) { throw new FatalDiagnosticError( - ErrorCode.DECORATOR_UNEXPECTED, node, 'Query decorator must go on a property-type member'); + ErrorCode.DECORATOR_UNEXPECTED, + node, + 'Query decorator must go on a property-type member', + ); } // Either the decorator was aliased, or is referenced directly with @@ -637,39 +864,55 @@ function tryGetQueryFromFieldDecorator( name, decorator, metadata: extractDecoratorQueryMetadata( - node, name, decorator.args || [], member.name, reflector, evaluator), + node, + name, + decorator.args || [], + member.name, + reflector, + evaluator, + ), }; } function isPropertyTypeMember(member: ClassMember): boolean { - return member.kind === ClassMemberKind.Getter || member.kind === ClassMemberKind.Setter || - member.kind === ClassMemberKind.Property; + return ( + member.kind === ClassMemberKind.Getter || + member.kind === ClassMemberKind.Setter || + member.kind === ClassMemberKind.Property + ); } function parseMappingStringArray(values: string[]) { - return values.reduce((results, value) => { - if (typeof value !== 'string') { - throw new Error('Mapping value must be a string'); - } + return values.reduce( + (results, value) => { + if (typeof value !== 'string') { + throw new Error('Mapping value must be a string'); + } - const [bindingPropertyName, fieldName] = parseMappingString(value); - results[fieldName] = bindingPropertyName; - return results; - }, {} as {[field: string]: string}); + const [bindingPropertyName, fieldName] = parseMappingString(value); + results[fieldName] = bindingPropertyName; + return results; + }, + {} as {[field: string]: string}, + ); } function parseMappingString(value: string): [bindingPropertyName: string, fieldName: string] { // Either the value is 'field' or 'field: property'. In the first case, `property` will // be undefined, in which case the field name should also be used as the property name. - const [fieldName, bindingPropertyName] = value.split(':', 2).map(str => str.trim()); + const [fieldName, bindingPropertyName] = value.split(':', 2).map((str) => str.trim()); return [bindingPropertyName ?? fieldName, fieldName]; } /** Parses the `inputs` array of a directive/component decorator. */ function parseInputsArray( - clazz: ClassDeclaration, decoratorMetadata: Map, - evaluator: PartialEvaluator, reflector: ReflectionHost, refEmitter: ReferenceEmitter, - compilationMode: CompilationMode): Record { + clazz: ClassDeclaration, + decoratorMetadata: Map, + evaluator: PartialEvaluator, + reflector: ReflectionHost, + refEmitter: ReferenceEmitter, + compilationMode: CompilationMode, +): Record { const inputsField = decoratorMetadata.get('inputs'); if (inputsField === undefined) { @@ -681,7 +924,10 @@ function parseInputsArray( if (!Array.isArray(inputsArray)) { throw createValueHasWrongTypeError( - inputsField, inputsArray, `Failed to resolve @Directive.inputs to an array`); + inputsField, + inputsArray, + `Failed to resolve @Directive.inputs to an array`, + ); } for (let i = 0; i < inputsArray.length; i++) { @@ -703,12 +949,14 @@ function parseInputsArray( const name = value.get('name'); const alias = value.get('alias'); const required = value.get('required'); - let transform: DecoratorInputTransform|null = null; + let transform: DecoratorInputTransform | null = null; if (typeof name !== 'string') { throw createValueHasWrongTypeError( - inputsField, name, - `Value at position ${i} of @Directive.inputs array must have a "name" property`); + inputsField, + name, + `Value at position ${i} of @Directive.inputs array must have a "name" property`, + ); } if (value.has('transform')) { @@ -716,12 +964,20 @@ function parseInputsArray( if (!(transformValue instanceof DynamicValue) && !(transformValue instanceof Reference)) { throw createValueHasWrongTypeError( - inputsField, transformValue, - `Transform of value at position ${i} of @Directive.inputs array must be a function`); + inputsField, + transformValue, + `Transform of value at position ${i} of @Directive.inputs array must be a function`, + ); } transform = parseDecoratorInputTransformFunction( - clazz, name, transformValue, reflector, refEmitter, compilationMode); + clazz, + name, + transformValue, + reflector, + refEmitter, + compilationMode, + ); } inputs[name] = { @@ -734,8 +990,10 @@ function parseInputsArray( }; } else { throw createValueHasWrongTypeError( - inputsField, value, - `@Directive.inputs array can only contain strings or object literals`); + inputsField, + value, + `@Directive.inputs array can only contain strings or object literals`, + ); } } @@ -744,7 +1002,10 @@ function parseInputsArray( /** Attempts to find a given Angular decorator on the class member. */ function tryGetDecoratorOnMember( - member: ClassMember, decoratorName: string, isCore: boolean): Decorator|null { + member: ClassMember, + decoratorName: string, + isCore: boolean, +): Decorator | null { if (member.decorators === null) { return null; } @@ -758,9 +1019,15 @@ function tryGetDecoratorOnMember( } function tryParseInputFieldMapping( - clazz: ClassDeclaration, member: ClassMember, evaluator: PartialEvaluator, - reflector: ReflectionHost, importTracker: ImportedSymbolsTracker, isCore: boolean, - refEmitter: ReferenceEmitter, compilationMode: CompilationMode): InputMapping|null { + clazz: ClassDeclaration, + member: ClassMember, + evaluator: PartialEvaluator, + reflector: ReflectionHost, + importTracker: ImportedSymbolsTracker, + isCore: boolean, + refEmitter: ReferenceEmitter, + compilationMode: CompilationMode, +): InputMapping | null { const classPropertyName = member.name; const decorator = tryGetDecoratorOnMember(member, 'Input', isCore); @@ -769,27 +1036,32 @@ function tryParseInputFieldMapping( if (decorator !== null && signalInputMapping !== null) { throw new FatalDiagnosticError( - ErrorCode.INITIALIZER_API_WITH_DISALLOWED_DECORATOR, decorator.node, - `Using @Input with a signal input is not allowed.`); + ErrorCode.INITIALIZER_API_WITH_DISALLOWED_DECORATOR, + decorator.node, + `Using @Input with a signal input is not allowed.`, + ); } if (decorator !== null && modelInputMapping !== null) { throw new FatalDiagnosticError( - ErrorCode.INITIALIZER_API_WITH_DISALLOWED_DECORATOR, decorator.node, - `Using @Input with a model input is not allowed.`); + ErrorCode.INITIALIZER_API_WITH_DISALLOWED_DECORATOR, + decorator.node, + `Using @Input with a model input is not allowed.`, + ); } // Check `@Input` case. if (decorator !== null) { if (decorator.args !== null && decorator.args.length > 1) { throw new FatalDiagnosticError( - ErrorCode.DECORATOR_ARITY_WRONG, decorator.node, - `@${decorator.name} can have at most one argument, got ${ - decorator.args.length} argument(s)`); + ErrorCode.DECORATOR_ARITY_WRONG, + decorator.node, + `@${decorator.name} can have at most one argument, got ${decorator.args.length} argument(s)`, + ); } const optionsNode = - decorator.args !== null && decorator.args.length === 1 ? decorator.args[0] : undefined; + decorator.args !== null && decorator.args.length === 1 ? decorator.args[0] : undefined; const options = optionsNode !== undefined ? evaluator.evaluate(optionsNode) : null; const required = options instanceof Map ? options.get('required') === true : false; @@ -797,11 +1069,13 @@ function tryParseInputFieldMapping( // passed, we sanity check for unsupported values here again. if (options !== null && typeof options !== 'string' && !(options instanceof Map)) { throw createValueHasWrongTypeError( - decorator.node, options, - `@${decorator.name} decorator argument must resolve to a string or an object literal`); + decorator.node, + options, + `@${decorator.name} decorator argument must resolve to a string or an object literal`, + ); } - let alias: string|null = null; + let alias: string | null = null; if (typeof options === 'string') { alias = options; } else if (options instanceof Map && typeof options.get('alias') === 'string') { @@ -810,17 +1084,26 @@ function tryParseInputFieldMapping( const publicInputName = alias ?? classPropertyName; - let transform: DecoratorInputTransform|null = null; + let transform: DecoratorInputTransform | null = null; if (options instanceof Map && options.has('transform')) { const transformValue = options.get('transform'); if (!(transformValue instanceof DynamicValue) && !(transformValue instanceof Reference)) { throw createValueHasWrongTypeError( - optionsNode!, transformValue, `Input transform must be a function`); + optionsNode!, + transformValue, + `Input transform must be a function`, + ); } transform = parseDecoratorInputTransformFunction( - clazz, classPropertyName, transformValue, reflector, refEmitter, compilationMode); + clazz, + classPropertyName, + transformValue, + reflector, + refEmitter, + compilationMode, + ); } return { @@ -846,24 +1129,30 @@ function tryParseInputFieldMapping( /** Parses the class members that declare inputs (via decorator or initializer). */ function parseInputFields( - clazz: ClassDeclaration, members: ClassMember[], evaluator: PartialEvaluator, - reflector: ReflectionHost, importTracker: ImportedSymbolsTracker, refEmitter: ReferenceEmitter, - isCore: boolean, compilationMode: CompilationMode, - inputsFromClassDecorator: Record, - classDecorator: Decorator): Record { + clazz: ClassDeclaration, + members: ClassMember[], + evaluator: PartialEvaluator, + reflector: ReflectionHost, + importTracker: ImportedSymbolsTracker, + refEmitter: ReferenceEmitter, + isCore: boolean, + compilationMode: CompilationMode, + inputsFromClassDecorator: Record, + classDecorator: Decorator, +): Record { const inputs = {} as Record; for (const member of members) { const classPropertyName = member.name; const inputMapping = tryParseInputFieldMapping( - clazz, - member, - evaluator, - reflector, - importTracker, - isCore, - refEmitter, - compilationMode, + clazz, + member, + evaluator, + reflector, + importTracker, + isCore, + refEmitter, + compilationMode, ); if (inputMapping === null) { continue; @@ -871,16 +1160,19 @@ function parseInputFields( if (member.isStatic) { throw new FatalDiagnosticError( - ErrorCode.INCORRECTLY_DECLARED_ON_STATIC_MEMBER, member.node ?? clazz, - `Input "${member.name}" is incorrectly declared as static member of "${ - clazz.name.text}".`); + ErrorCode.INCORRECTLY_DECLARED_ON_STATIC_MEMBER, + member.node ?? clazz, + `Input "${member.name}" is incorrectly declared as static member of "${clazz.name.text}".`, + ); } // Validate that signal inputs are not accidentally declared in the `inputs` metadata. if (inputMapping.isSignal && inputsFromClassDecorator.hasOwnProperty(classPropertyName)) { throw new FatalDiagnosticError( - ErrorCode.INITIALIZER_API_DECORATOR_METADATA_COLLISION, member.node ?? clazz, - `Input "${member.name}" is also declared as non-signal in @${classDecorator.name}.`); + ErrorCode.INITIALIZER_API_DECORATOR_METADATA_COLLISION, + member.node ?? clazz, + `Input "${member.name}" is also declared as non-signal in @${classDecorator.name}.`, + ); } inputs[classPropertyName] = inputMapping; @@ -901,26 +1193,33 @@ function parseInputFields( * */ export function parseDecoratorInputTransformFunction( - clazz: ClassDeclaration, classPropertyName: string, value: DynamicValue|Reference, - reflector: ReflectionHost, refEmitter: ReferenceEmitter, - compilationMode: CompilationMode): DecoratorInputTransform { + clazz: ClassDeclaration, + classPropertyName: string, + value: DynamicValue | Reference, + reflector: ReflectionHost, + refEmitter: ReferenceEmitter, + compilationMode: CompilationMode, +): DecoratorInputTransform { // In local compilation mode we can skip type checking the function args. This is because usually // the type check is done in a separate build which runs in full compilation mode. So here we skip // all the diagnostics. if (compilationMode === CompilationMode.LOCAL) { const node = - value instanceof Reference ? value.getIdentityIn(clazz.getSourceFile()) : value.node; + value instanceof Reference ? value.getIdentityIn(clazz.getSourceFile()) : value.node; // This should never be null since we know the reference originates // from the same file, but we null check it just in case. if (node === null) { throw createValueHasWrongTypeError( - value.node, value, 'Input transform function could not be referenced'); + value.node, + value, + 'Input transform function could not be referenced', + ); } return { node, - type: new Reference(ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)) + type: new Reference(ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)), }; } @@ -932,12 +1231,18 @@ export function parseDecoratorInputTransformFunction( if (definition.typeParameters !== null && definition.typeParameters.length > 0) { throw createValueHasWrongTypeError( - value.node, value, 'Input transform function cannot be generic'); + value.node, + value, + 'Input transform function cannot be generic', + ); } if (definition.signatureCount > 1) { throw createValueHasWrongTypeError( - value.node, value, 'Input transform function cannot have multiple signatures'); + value.node, + value, + 'Input transform function cannot have multiple signatures', + ); } const members = reflector.getMembersOfClass(clazz); @@ -947,9 +1252,10 @@ export function parseDecoratorInputTransformFunction( if (member.name === conflictingName && member.isStatic) { throw new FatalDiagnosticError( - ErrorCode.CONFLICTING_INPUT_TRANSFORM, value.node, - `Class cannot have both a transform function on Input ${ - classPropertyName} and a static member called ${conflictingName}`); + ErrorCode.CONFLICTING_INPUT_TRANSFORM, + value.node, + `Class cannot have both a transform function on Input ${classPropertyName} and a static member called ${conflictingName}`, + ); } } @@ -959,33 +1265,42 @@ export function parseDecoratorInputTransformFunction( // from the same file, but we null check it just in case. if (node === null) { throw createValueHasWrongTypeError( - value.node, value, 'Input transform function could not be referenced'); + value.node, + value, + 'Input transform function could not be referenced', + ); } // Skip over `this` parameters since they're typing the context, not the actual parameter. // `this` parameters are guaranteed to be first if they exist, and the only to distinguish them // is using the name, TS doesn't have a special AST for them. - const firstParam = definition.parameters[0]?.name === 'this' ? definition.parameters[1] : - definition.parameters[0]; + const firstParam = + definition.parameters[0]?.name === 'this' ? definition.parameters[1] : definition.parameters[0]; // Treat functions with no arguments as `unknown` since returning // the same value from the transform function is valid. if (!firstParam) { return { node, - type: new Reference(ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)) + type: new Reference(ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)), }; } // This should be caught by `noImplicitAny` already, but null check it just in case. if (!firstParam.type) { throw createValueHasWrongTypeError( - value.node, value, 'Input transform function first parameter must have a type'); + value.node, + value, + 'Input transform function first parameter must have a type', + ); } if (firstParam.node.dotDotDotToken) { throw createValueHasWrongTypeError( - value.node, value, 'Input transform function first parameter cannot be a spread parameter'); + value.node, + value, + 'Input transform function first parameter cannot be a spread parameter', + ); } assertEmittableInputType(firstParam.type, clazz.getSourceFile(), reflector, refEmitter); @@ -999,8 +1314,11 @@ export function parseDecoratorInputTransformFunction( * it can be referenced in a specific context file. */ function assertEmittableInputType( - type: ts.TypeNode, contextFile: ts.SourceFile, reflector: ReflectionHost, - refEmitter: ReferenceEmitter): void { + type: ts.TypeNode, + contextFile: ts.SourceFile, + reflector: ReflectionHost, + refEmitter: ReferenceEmitter, +): void { (function walk(node: ts.Node) { if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) { const declaration = reflector.getDeclarationOfIdentifier(node.typeName); @@ -1011,18 +1329,25 @@ function assertEmittableInputType( // exported, otherwise TS won't emit it to the .d.ts. if (declaration.node.getSourceFile() !== contextFile) { const emittedType = refEmitter.emit( - new Reference( - declaration.node, declaration.viaModule === AmbientImport ? AmbientImport : null), - contextFile, - ImportFlags.NoAliasing | ImportFlags.AllowTypeImports | - ImportFlags.AllowRelativeDtsImports | ImportFlags.AllowAmbientReferences); + new Reference( + declaration.node, + declaration.viaModule === AmbientImport ? AmbientImport : null, + ), + contextFile, + ImportFlags.NoAliasing | + ImportFlags.AllowTypeImports | + ImportFlags.AllowRelativeDtsImports | + ImportFlags.AllowAmbientReferences, + ); assertSuccessfulReferenceEmit(emittedType, node, 'type'); } else if (!reflector.isStaticallyExported(declaration.node)) { throw new FatalDiagnosticError( - ErrorCode.SYMBOL_NOT_EXPORTED, type, - `Symbol must be exported in order to be used as the type of an Input transform function`, - [makeRelatedInformation(declaration.node, `The symbol is declared here.`)]); + ErrorCode.SYMBOL_NOT_EXPORTED, + type, + `Symbol must be exported in order to be used as the type of an Input transform function`, + [makeRelatedInformation(declaration.node, `The symbol is declared here.`)], + ); } } } @@ -1039,10 +1364,14 @@ function assertEmittableInputType( * initializers for signal-based queries. */ function parseQueriesOfClassFields( - members: ClassMember[], reflector: ReflectionHost, importTracker: ImportedSymbolsTracker, - evaluator: PartialEvaluator, isCore: boolean): { - viewQueries: R3QueryMetadata[], - contentQueries: R3QueryMetadata[], + members: ClassMember[], + reflector: ReflectionHost, + importTracker: ImportedSymbolsTracker, + evaluator: PartialEvaluator, + isCore: boolean, +): { + viewQueries: R3QueryMetadata[]; + contentQueries: R3QueryMetadata[]; } { const viewQueries: R3QueryMetadata[] = []; const contentQueries: R3QueryMetadata[] = []; @@ -1061,15 +1390,19 @@ function parseQueriesOfClassFields( if (decoratorQuery !== null && signalQuery !== null) { throw new FatalDiagnosticError( - ErrorCode.INITIALIZER_API_WITH_DISALLOWED_DECORATOR, decoratorQuery.decorator.node, - `Using @${decoratorQuery.name} with a signal-based query is not allowed.`); + ErrorCode.INITIALIZER_API_WITH_DISALLOWED_DECORATOR, + decoratorQuery.decorator.node, + `Using @${decoratorQuery.name} with a signal-based query is not allowed.`, + ); } const queryNode = decoratorQuery?.decorator.node ?? signalQuery?.call; if (queryNode !== undefined && member.isStatic) { throw new FatalDiagnosticError( - ErrorCode.INCORRECTLY_DECLARED_ON_STATIC_MEMBER, queryNode, - `Query is incorrectly declared on a static class member.`); + ErrorCode.INCORRECTLY_DECLARED_ON_STATIC_MEMBER, + queryNode, + `Query is incorrectly declared on a static class member.`, + ); } if (decoratorQuery !== null) { @@ -1109,16 +1442,24 @@ function parseQueriesOfClassFields( /** Parses the `outputs` array of a directive/component. */ function parseOutputsArray( - directive: Map, evaluator: PartialEvaluator): Record { + directive: Map, + evaluator: PartialEvaluator, +): Record { const metaValues = parseFieldStringArrayValue(directive, 'outputs', evaluator); return metaValues ? parseMappingStringArray(metaValues) : EMPTY_OBJECT; } /** Parses the class members that are outputs. */ function parseOutputFields( - clazz: ClassDeclaration, classDecorator: Decorator, members: ClassMember[], isCore: boolean, - reflector: ReflectionHost, importTracker: ImportedSymbolsTracker, evaluator: PartialEvaluator, - outputsFromMeta: Record): Record { + clazz: ClassDeclaration, + classDecorator: Decorator, + members: ClassMember[], + isCore: boolean, + reflector: ReflectionHost, + importTracker: ImportedSymbolsTracker, + evaluator: PartialEvaluator, + outputsFromMeta: Record, +): Record { const outputs = {} as Record; for (const member of members) { @@ -1128,22 +1469,28 @@ function parseOutputFields( if (decoratorOutput !== null && initializerOutput !== null) { throw new FatalDiagnosticError( - ErrorCode.INITIALIZER_API_WITH_DISALLOWED_DECORATOR, decoratorOutput.decorator.node, - `Using "@Output" with "output()" is not allowed.`); + ErrorCode.INITIALIZER_API_WITH_DISALLOWED_DECORATOR, + decoratorOutput.decorator.node, + `Using "@Output" with "output()" is not allowed.`, + ); } if (decoratorOutput !== null && modelMapping !== null) { throw new FatalDiagnosticError( - ErrorCode.INITIALIZER_API_WITH_DISALLOWED_DECORATOR, decoratorOutput.decorator.node, - `Using @Output with a model input is not allowed.`); + ErrorCode.INITIALIZER_API_WITH_DISALLOWED_DECORATOR, + decoratorOutput.decorator.node, + `Using @Output with a model input is not allowed.`, + ); } const queryNode = - decoratorOutput?.decorator.node ?? initializerOutput?.call ?? modelMapping?.call; + decoratorOutput?.decorator.node ?? initializerOutput?.call ?? modelMapping?.call; if (queryNode !== undefined && member.isStatic) { throw new FatalDiagnosticError( - ErrorCode.INCORRECTLY_DECLARED_ON_STATIC_MEMBER, queryNode, - `Output is incorrectly declared on a static class member.`); + ErrorCode.INCORRECTLY_DECLARED_ON_STATIC_MEMBER, + queryNode, + `Output is incorrectly declared on a static class member.`, + ); } let bindingPropertyName: string; @@ -1160,11 +1507,15 @@ function parseOutputFields( // Validate that initializer-based outputs are not accidentally declared // in the `outputs` class metadata. - if (((initializerOutput !== null || modelMapping !== null) && - outputsFromMeta.hasOwnProperty(member.name))) { + if ( + (initializerOutput !== null || modelMapping !== null) && + outputsFromMeta.hasOwnProperty(member.name) + ) { throw new FatalDiagnosticError( - ErrorCode.INITIALIZER_API_DECORATOR_METADATA_COLLISION, member.node ?? clazz, - `Output "${member.name}" is unexpectedly declared in @${classDecorator.name} as well.`); + ErrorCode.INITIALIZER_API_DECORATOR_METADATA_COLLISION, + member.node ?? clazz, + `Output "${member.name}" is unexpectedly declared in @${classDecorator.name} as well.`, + ); } outputs[member.name] = bindingPropertyName; @@ -1174,26 +1525,34 @@ function parseOutputFields( } /** Attempts to parse a decorator-based @Output. */ -function tryParseDecoratorOutput(member: ClassMember, evaluator: PartialEvaluator, isCore: boolean): - {decorator: Decorator, metadata: InputOrOutput}|null { +function tryParseDecoratorOutput( + member: ClassMember, + evaluator: PartialEvaluator, + isCore: boolean, +): {decorator: Decorator; metadata: InputOrOutput} | null { const decorator = tryGetDecoratorOnMember(member, 'Output', isCore); if (decorator === null) { return null; } if (decorator.args !== null && decorator.args.length > 1) { throw new FatalDiagnosticError( - ErrorCode.DECORATOR_ARITY_WRONG, decorator.node, - `@Output can have at most one argument, got ${decorator.args.length} argument(s)`); + ErrorCode.DECORATOR_ARITY_WRONG, + decorator.node, + `@Output can have at most one argument, got ${decorator.args.length} argument(s)`, + ); } const classPropertyName = member.name; - let alias: string|null = null; + let alias: string | null = null; if (decorator.args?.length === 1) { const resolvedAlias = evaluator.evaluate(decorator.args[0]); if (typeof resolvedAlias !== 'string') { throw createValueHasWrongTypeError( - decorator.node, resolvedAlias, `@Output decorator argument must resolve to a string`); + decorator.node, + resolvedAlias, + `@Output decorator argument must resolve to a string`, + ); } alias = resolvedAlias; } @@ -1204,18 +1563,23 @@ function tryParseDecoratorOutput(member: ClassMember, evaluator: PartialEvaluato isSignal: false, classPropertyName, bindingPropertyName: alias ?? classPropertyName, - } + }, }; } function evaluateHostExpressionBindings( - hostExpr: ts.Expression, evaluator: PartialEvaluator): ParsedHostBindings { + hostExpr: ts.Expression, + evaluator: PartialEvaluator, +): ParsedHostBindings { const hostMetaMap = evaluator.evaluate(hostExpr); if (!(hostMetaMap instanceof Map)) { throw createValueHasWrongTypeError( - hostExpr, hostMetaMap, `Decorator host metadata must be an object`); + hostExpr, + hostMetaMap, + `Decorator host metadata must be an object`, + ); } - const hostMetadata: Record = {}; + const hostMetadata: Record = {}; hostMetaMap.forEach((value, key) => { // Resolve Enum references to their declared value. if (value instanceof EnumValue) { @@ -1224,8 +1588,10 @@ function evaluateHostExpressionBindings( if (typeof key !== 'string') { throw createValueHasWrongTypeError( - hostExpr, key, - `Decorator host metadata must be a string -> string object, but found unparseable key`); + hostExpr, + key, + `Decorator host metadata must be a string -> string object, but found unparseable key`, + ); } if (typeof value == 'string') { @@ -1234,8 +1600,10 @@ function evaluateHostExpressionBindings( hostMetadata[key] = new WrappedNodeExpr(value.node as ts.Expression); } else { throw createValueHasWrongTypeError( - hostExpr, value, - `Decorator host metadata must be a string -> string object, but found unparseable value`); + hostExpr, + value, + `Decorator host metadata must be a string -> string object, but found unparseable value`, + ); } }); @@ -1244,10 +1612,12 @@ function evaluateHostExpressionBindings( const errors = verifyHostBindings(bindings, createSourceSpan(hostExpr)); if (errors.length > 0) { throw new FatalDiagnosticError( - // TODO: provide more granular diagnostic and output specific host expression that - // triggered an error instead of the whole host object. - ErrorCode.HOST_BINDING_PARSE_ERROR, hostExpr, - errors.map((error: ParseError) => error.msg).join('\n')); + // TODO: provide more granular diagnostic and output specific host expression that + // triggered an error instead of the whole host object. + ErrorCode.HOST_BINDING_PARSE_ERROR, + hostExpr, + errors.map((error: ParseError) => error.msg).join('\n'), + ); } return bindings; @@ -1258,31 +1628,42 @@ function evaluateHostExpressionBindings( * @param rawHostDirectives Expression that defined the `hostDirectives`. */ function extractHostDirectives( - rawHostDirectives: ts.Expression, evaluator: PartialEvaluator, - compilationMode: CompilationMode): HostDirectiveMeta[] { + rawHostDirectives: ts.Expression, + evaluator: PartialEvaluator, + compilationMode: CompilationMode, +): HostDirectiveMeta[] { const resolved = evaluator.evaluate(rawHostDirectives, forwardRefResolver); if (!Array.isArray(resolved)) { throw createValueHasWrongTypeError( - rawHostDirectives, resolved, 'hostDirectives must be an array'); + rawHostDirectives, + resolved, + 'hostDirectives must be an array', + ); } - return resolved.map(value => { + return resolved.map((value) => { const hostReference = value instanceof Map ? value.get('directive') : value; // Diagnostics if (compilationMode !== CompilationMode.LOCAL) { if (!(hostReference instanceof Reference)) { throw createValueHasWrongTypeError( - rawHostDirectives, hostReference, 'Host directive must be a reference'); + rawHostDirectives, + hostReference, + 'Host directive must be a reference', + ); } if (!isNamedClassDeclaration(hostReference.node)) { throw createValueHasWrongTypeError( - rawHostDirectives, hostReference, 'Host directive reference must be a class'); + rawHostDirectives, + hostReference, + 'Host directive reference must be a class', + ); } } - let directive: Reference|Expression; + let directive: Reference | Expression; let nameForErrors = (fieldName: string) => '@Directive.hostDirectives'; if (compilationMode === CompilationMode.LOCAL && hostReference instanceof DynamicValue) { // At the moment in local compilation we only support simple array for host directives, i.e., @@ -1290,18 +1671,24 @@ function extractHostDirectives( // expressions applied on externally imported directives. The main reason is simplicity, and // that almost nobody wants to use host directives this way (e.g., what would be the point of // forward ref for imported symbols?!) - if (!ts.isIdentifier(hostReference.node) && - !ts.isPropertyAccessExpression(hostReference.node)) { + if ( + !ts.isIdentifier(hostReference.node) && + !ts.isPropertyAccessExpression(hostReference.node) + ) { throw new FatalDiagnosticError( - ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION, hostReference.node, - `In local compilation mode, host directive cannot be an expression. Use an identifier instead`); + ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION, + hostReference.node, + `In local compilation mode, host directive cannot be an expression. Use an identifier instead`, + ); } directive = new WrappedNodeExpr(hostReference.node); } else if (hostReference instanceof Reference) { directive = hostReference as Reference; - nameForErrors = (fieldName: string) => `@Directive.hostDirectives.${ - (directive as Reference).node.name.text}.${fieldName}`; + nameForErrors = (fieldName: string) => + `@Directive.hostDirectives.${ + (directive as Reference).node.name.text + }.${fieldName}`; } else { throw new Error('Impossible state'); } @@ -1309,10 +1696,18 @@ function extractHostDirectives( const meta: HostDirectiveMeta = { directive, isForwardReference: hostReference instanceof Reference && hostReference.synthetic, - inputs: - parseHostDirectivesMapping('inputs', value, nameForErrors('input'), rawHostDirectives), - outputs: - parseHostDirectivesMapping('outputs', value, nameForErrors('output'), rawHostDirectives), + inputs: parseHostDirectivesMapping( + 'inputs', + value, + nameForErrors('input'), + rawHostDirectives, + ), + outputs: parseHostDirectivesMapping( + 'outputs', + value, + nameForErrors('output'), + rawHostDirectives, + ), }; return meta; @@ -1327,8 +1722,11 @@ function extractHostDirectives( * @param sourceExpression Expression that the host directive is referenced in. */ function parseHostDirectivesMapping( - field: 'inputs'|'outputs', resolvedValue: ResolvedValue, nameForErrors: string, - sourceExpression: ts.Expression): {[bindingPropertyName: string]: string}|null { + field: 'inputs' | 'outputs', + resolvedValue: ResolvedValue, + nameForErrors: string, + sourceExpression: ts.Expression, +): {[bindingPropertyName: string]: string} | null { if (resolvedValue instanceof Map && resolvedValue.has(field)) { const rawInputs = resolvedValue.get(field); @@ -1342,12 +1740,18 @@ function parseHostDirectivesMapping( /** Converts the parsed host directive information into metadata. */ function toHostDirectiveMetadata( - hostDirective: HostDirectiveMeta, context: ts.SourceFile, - refEmitter: ReferenceEmitter): R3HostDirectiveMetadata { + hostDirective: HostDirectiveMeta, + context: ts.SourceFile, + refEmitter: ReferenceEmitter, +): R3HostDirectiveMetadata { let directive: R3Reference; if (hostDirective.directive instanceof Reference) { - directive = - toR3Reference(hostDirective.directive.node, hostDirective.directive, context, refEmitter); + directive = toR3Reference( + hostDirective.directive.node, + hostDirective.directive, + context, + refEmitter, + ); } else { directive = { value: hostDirective.directive, @@ -1359,7 +1763,7 @@ function toHostDirectiveMetadata( directive, isForwardReference: hostDirective.isForwardReference, inputs: hostDirective.inputs || null, - outputs: hostDirective.outputs || null + outputs: hostDirective.outputs || null, }; } @@ -1369,8 +1773,8 @@ function toR3InputMetadata(mapping: InputMapping): R3InputMetadata { classPropertyName: mapping.classPropertyName, bindingPropertyName: mapping.bindingPropertyName, required: mapping.required, - transformFunction: mapping.transform !== null ? new WrappedNodeExpr(mapping.transform.node) : - null, + transformFunction: + mapping.transform !== null ? new WrappedNodeExpr(mapping.transform.node) : null, isSignal: mapping.isSignal, }; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/symbol.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/symbol.ts index 04a5e5c4a79cb..778d7795f383f 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/symbol.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/symbol.ts @@ -6,8 +6,21 @@ * found in the LICENSE file at https://angular.io/license */ -import {areTypeParametersEqual, isArrayEqual, isSetEqual, isSymbolEqual, SemanticSymbol, SemanticTypeParameter} from '../../../incremental/semantic_graph'; -import {ClassPropertyMapping, DirectiveTypeCheckMeta, InputMapping, InputOrOutput, TemplateGuardMeta} from '../../../metadata'; +import { + areTypeParametersEqual, + isArrayEqual, + isSetEqual, + isSymbolEqual, + SemanticSymbol, + SemanticTypeParameter, +} from '../../../incremental/semantic_graph'; +import { + ClassPropertyMapping, + DirectiveTypeCheckMeta, + InputMapping, + InputOrOutput, + TemplateGuardMeta, +} from '../../../metadata'; import {ClassDeclaration} from '../../../reflection'; /** @@ -15,14 +28,17 @@ import {ClassDeclaration} from '../../../reflection'; * from this symbol. */ export class DirectiveSymbol extends SemanticSymbol { - baseClass: SemanticSymbol|null = null; + baseClass: SemanticSymbol | null = null; constructor( - decl: ClassDeclaration, public readonly selector: string|null, - public readonly inputs: ClassPropertyMapping, - public readonly outputs: ClassPropertyMapping, public readonly exportAs: string[]|null, - public readonly typeCheckMeta: DirectiveTypeCheckMeta, - public readonly typeParameters: SemanticTypeParameter[]|null) { + decl: ClassDeclaration, + public readonly selector: string | null, + public readonly inputs: ClassPropertyMapping, + public readonly outputs: ClassPropertyMapping, + public readonly exportAs: string[] | null, + public readonly typeCheckMeta: DirectiveTypeCheckMeta, + public readonly typeParameters: SemanticTypeParameter[] | null, + ) { super(decl); } @@ -39,10 +55,12 @@ export class DirectiveSymbol extends SemanticSymbol { // 2. The binding names of their inputs and outputs; a change in ordering is also considered // to be a change in public API. // 3. The list of exportAs names and its ordering. - return this.selector !== previousSymbol.selector || - !isArrayEqual(this.inputs.propertyNames, previousSymbol.inputs.propertyNames) || - !isArrayEqual(this.outputs.propertyNames, previousSymbol.outputs.propertyNames) || - !isArrayEqual(this.exportAs, previousSymbol.exportAs); + return ( + this.selector !== previousSymbol.selector || + !isArrayEqual(this.inputs.propertyNames, previousSymbol.inputs.propertyNames) || + !isArrayEqual(this.outputs.propertyNames, previousSymbol.outputs.propertyNames) || + !isArrayEqual(this.exportAs, previousSymbol.exportAs) + ); } override isTypeCheckApiAffected(previousSymbol: SemanticSymbol): boolean { @@ -57,10 +75,18 @@ export class DirectiveSymbol extends SemanticSymbol { // The type-check block also depends on the class property names, as writes property bindings // directly into the backing fields. - if (!isArrayEqual( - Array.from(this.inputs), Array.from(previousSymbol.inputs), isInputMappingEqual) || - !isArrayEqual( - Array.from(this.outputs), Array.from(previousSymbol.outputs), isInputOrOutputEqual)) { + if ( + !isArrayEqual( + Array.from(this.inputs), + Array.from(previousSymbol.inputs), + isInputMappingEqual, + ) || + !isArrayEqual( + Array.from(this.outputs), + Array.from(previousSymbol.outputs), + isInputOrOutputEqual, + ) + ) { return true; } @@ -92,13 +118,17 @@ function isInputMappingEqual(current: InputMapping, previous: InputMapping): boo } function isInputOrOutputEqual(current: InputOrOutput, previous: InputOrOutput): boolean { - return current.classPropertyName === previous.classPropertyName && - current.bindingPropertyName === previous.bindingPropertyName && - current.isSignal === previous.isSignal; + return ( + current.classPropertyName === previous.classPropertyName && + current.bindingPropertyName === previous.bindingPropertyName && + current.isSignal === previous.isSignal + ); } function isTypeCheckMetaEqual( - current: DirectiveTypeCheckMeta, previous: DirectiveTypeCheckMeta): boolean { + current: DirectiveTypeCheckMeta, + previous: DirectiveTypeCheckMeta, +): boolean { if (current.hasNgTemplateContextGuard !== previous.hasNgTemplateContextGuard) { return false; } @@ -131,7 +161,10 @@ function isTemplateGuardEqual(current: TemplateGuardMeta, previous: TemplateGuar return current.inputName === previous.inputName && current.type === previous.type; } -function isBaseClassEqual(current: SemanticSymbol|null, previous: SemanticSymbol|null): boolean { +function isBaseClassEqual( + current: SemanticSymbol | null, + previous: SemanticSymbol | null, +): boolean { if (current === null || previous === null) { return current === previous; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/test/directive_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/test/directive_spec.ts index 5eb5007f777da..93d029619ce97 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/test/directive_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/test/directive_spec.ts @@ -5,7 +5,14 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {CssSelector, DirectiveMeta as T2DirectiveMeta, parseTemplate, R3TargetBinder, SelectorMatcher, TmplAstElement} from '@angular/compiler'; +import { + CssSelector, + DirectiveMeta as T2DirectiveMeta, + parseTemplate, + R3TargetBinder, + SelectorMatcher, + TmplAstElement, +} from '@angular/compiler'; import ts from 'typescript'; import {absoluteFrom} from '../../../file_system'; @@ -14,7 +21,11 @@ import {ImportedSymbolsTracker, ReferenceEmitter} from '../../../imports'; import {CompoundMetadataReader, DtsMetadataReader, LocalMetadataRegistry} from '../../../metadata'; import {PartialEvaluator} from '../../../partial_evaluator'; import {NOOP_PERF_RECORDER} from '../../../perf'; -import {ClassDeclaration, isNamedClassDeclaration, TypeScriptReflectionHost} from '../../../reflection'; +import { + ClassDeclaration, + isNamedClassDeclaration, + TypeScriptReflectionHost, +} from '../../../reflection'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../../scope'; import {getDeclaration, makeProgram} from '../../../testing'; import {CompilationMode} from '../../../transform'; @@ -23,7 +34,7 @@ import {DirectiveDecoratorHandler} from '../index'; runInEachFileSystem(() => { let _: typeof absoluteFrom; - beforeEach(() => _ = absoluteFrom); + beforeEach(() => (_ = absoluteFrom)); describe('DirectiveDecoratorHandler', () => { it('should use the `ReflectionHost` to detect class inheritance', () => { @@ -171,20 +182,33 @@ runInEachFileSystem(() => { const refEmitter = new ReferenceEmitter([]); const referenceRegistry = new NoopReferencesRegistry(); const scopeRegistry = new LocalModuleScopeRegistry( - metaReader, new CompoundMetadataReader([metaReader, dtsReader]), - new MetadataDtsModuleScopeResolver(dtsReader, null), refEmitter, null); + metaReader, + new CompoundMetadataReader([metaReader, dtsReader]), + new MetadataDtsModuleScopeResolver(dtsReader, null), + refEmitter, + null, + ); const injectableRegistry = new InjectableClassRegistry(reflectionHost, /* isCore */ false); const importTracker = new ImportedSymbolsTracker(); const handler = new DirectiveDecoratorHandler( - reflectionHost, evaluator, scopeRegistry, scopeRegistry, metaReader, injectableRegistry, - refEmitter, referenceRegistry, - /*isCore*/ false, - /*strictCtorDeps*/ false, - /*semanticDepGraphUpdater*/ null, - /*annotateForClosureCompiler*/ false, NOOP_PERF_RECORDER, importTracker, - /*includeClassMetadata*/ true, - /*compilationMode */ CompilationMode.FULL, - /*generateExtraImportsInLocalMode*/ false); + reflectionHost, + evaluator, + scopeRegistry, + scopeRegistry, + metaReader, + injectableRegistry, + refEmitter, + referenceRegistry, + /*isCore*/ false, + /*strictCtorDeps*/ false, + /*semanticDepGraphUpdater*/ null, + /*annotateForClosureCompiler*/ false, + NOOP_PERF_RECORDER, + importTracker, + /*includeClassMetadata*/ true, + /*compilationMode */ CompilationMode.FULL, + /*generateExtraImportsInLocalMode*/ false, + ); const DirNode = getDeclaration(program, _('/entry.ts'), dirName, isNamedClassDeclaration); diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/test/initializer_functions_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/test/initializer_functions_spec.ts index 74517814911bb..e779a2b6537e3 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/test/initializer_functions_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/test/initializer_functions_spec.ts @@ -18,7 +18,6 @@ import {makeProgram} from '../../../testing'; import {validateAccessOfInitializerApiMember} from '../src/initializer_function_access'; import {InitializerApiFunction, tryParseInitializerApi} from '../src/initializer_functions'; - runInEachFileSystem(() => { const modelApi: InitializerApiFunction = { functionName: 'model', @@ -57,15 +56,18 @@ runInEachFileSystem(() => { `); const result = tryParseInitializerApi( - [ - { - functionName: 'input', - owningModule: '@angular/core', - allowedAccessLevels: [ClassMemberAccessLevel.PublicWritable], - }, - modelApi, - ], - member.value!, reflector, importTracker); + [ + { + functionName: 'input', + owningModule: '@angular/core', + allowedAccessLevels: [ClassMemberAccessLevel.PublicWritable], + }, + modelApi, + ], + member.value!, + reflector, + importTracker, + ); expect(result).toEqual({ api: modelApi, @@ -86,15 +88,18 @@ runInEachFileSystem(() => { `); const result = tryParseInitializerApi( - [ - modelApi, - { - functionName: 'outputFromObservable', - owningModule: '@angular/core/rxjs-interop', - allowedAccessLevels: [ClassMemberAccessLevel.PublicWritable], - }, - ], - member.value!, reflector, importTracker); + [ + modelApi, + { + functionName: 'outputFromObservable', + owningModule: '@angular/core/rxjs-interop', + allowedAccessLevels: [ClassMemberAccessLevel.PublicWritable], + }, + ], + member.value!, + reflector, + importTracker, + ); expect(result).toEqual({ api: { @@ -279,9 +284,8 @@ runInEachFileSystem(() => { }); }); - it('should identify an initializer function in a file containing an import whose name overlaps with an object prototype member', - () => { - const {member, reflector, importTracker} = setup(` + it('should identify an initializer function in a file containing an import whose name overlaps with an object prototype member', () => { + const {member, reflector, importTracker} = setup(` import {Directive, model} from '@angular/core'; import {toString} from '@unknown/utils'; @@ -291,14 +295,14 @@ runInEachFileSystem(() => { } `); - const result = tryParseInitializerApi([modelApi], member.value!, reflector, importTracker); + const result = tryParseInitializerApi([modelApi], member.value!, reflector, importTracker); - expect(result).toEqual({ - api: modelApi, - isRequired: false, - call: jasmine.objectContaining({kind: ts.SyntaxKind.CallExpression}), - }); - }); + expect(result).toEqual({ + api: modelApi, + isRequired: false, + call: jasmine.objectContaining({kind: ts.SyntaxKind.CallExpression}), + }); + }); describe('`validateAccessOfInitializerApiMember`', () => { it('should report errors if a private field is used, but not allowed', () => { @@ -314,10 +318,11 @@ runInEachFileSystem(() => { const result = tryParseInitializerApi([modelApi], member.value!, reflector, importTracker); expect(result).not.toBeNull(); - expect(() => validateAccessOfInitializerApiMember(result!, member)) - .toThrowMatching( - err => err instanceof FatalDiagnosticError && - err.code === ErrorCode.INITIALIZER_API_DISALLOWED_MEMBER_VISIBILITY); + expect(() => validateAccessOfInitializerApiMember(result!, member)).toThrowMatching( + (err) => + err instanceof FatalDiagnosticError && + err.code === ErrorCode.INITIALIZER_API_DISALLOWED_MEMBER_VISIBILITY, + ); }); it('should report errors if a protected field is used, but not allowed', () => { @@ -333,10 +338,11 @@ runInEachFileSystem(() => { const result = tryParseInitializerApi([modelApi], member.value!, reflector, importTracker); expect(result).not.toBeNull(); - expect(() => validateAccessOfInitializerApiMember(result!, member)) - .toThrowMatching( - err => err instanceof FatalDiagnosticError && - err.code === ErrorCode.INITIALIZER_API_DISALLOWED_MEMBER_VISIBILITY); + expect(() => validateAccessOfInitializerApiMember(result!, member)).toThrowMatching( + (err) => + err instanceof FatalDiagnosticError && + err.code === ErrorCode.INITIALIZER_API_DISALLOWED_MEMBER_VISIBILITY, + ); }); it('should report errors if an ECMAScript private field is used, but not allowed', () => { @@ -352,10 +358,11 @@ runInEachFileSystem(() => { const result = tryParseInitializerApi([modelApi], member.value!, reflector, importTracker); expect(result).not.toBeNull(); - expect(() => validateAccessOfInitializerApiMember(result!, member)) - .toThrowMatching( - err => err instanceof FatalDiagnosticError && - err.code === ErrorCode.INITIALIZER_API_DISALLOWED_MEMBER_VISIBILITY); + expect(() => validateAccessOfInitializerApiMember(result!, member)).toThrowMatching( + (err) => + err instanceof FatalDiagnosticError && + err.code === ErrorCode.INITIALIZER_API_DISALLOWED_MEMBER_VISIBILITY, + ); }); it('should report errors if a readonly public field is used, but not allowed', () => { @@ -372,10 +379,11 @@ runInEachFileSystem(() => { const result = tryParseInitializerApi([modelApi], member.value!, reflector, importTracker); expect(result).not.toBeNull(); - expect(() => validateAccessOfInitializerApiMember(result!, member)) - .toThrowMatching( - err => err instanceof FatalDiagnosticError && - err.code === ErrorCode.INITIALIZER_API_DISALLOWED_MEMBER_VISIBILITY); + expect(() => validateAccessOfInitializerApiMember(result!, member)).toThrowMatching( + (err) => + err instanceof FatalDiagnosticError && + err.code === ErrorCode.INITIALIZER_API_DISALLOWED_MEMBER_VISIBILITY, + ); }); it('should allow private field if API explicitly allows it', () => { @@ -390,51 +398,56 @@ runInEachFileSystem(() => { `); const result = tryParseInitializerApi( - [{...modelApi, allowedAccessLevels: [ClassMemberAccessLevel.Private]}], member.value!, - reflector, importTracker); - - expect(result?.api).toEqual(jasmine.objectContaining({ - functionName: 'model' - })); + [{...modelApi, allowedAccessLevels: [ClassMemberAccessLevel.Private]}], + member.value!, + reflector, + importTracker, + ); + + expect(result?.api).toEqual( + jasmine.objectContaining({ + functionName: 'model', + }), + ); expect(() => validateAccessOfInitializerApiMember(result!, member)).not.toThrow(); }); }); }); - function setup(contents: string) { const fileName = absoluteFrom('/test.ts'); const {program} = makeProgram( - [ - { - name: absoluteFrom('/node_modules/@angular/core/index.d.ts'), - contents: ` + [ + { + name: absoluteFrom('/node_modules/@angular/core/index.d.ts'), + contents: ` export const Directive: any; export const input: any; export const model: any; `, - }, - { - name: absoluteFrom('/node_modules/@angular/core/rxjs-interop/index.d.ts'), - contents: ` + }, + { + name: absoluteFrom('/node_modules/@angular/core/rxjs-interop/index.d.ts'), + contents: ` export const outputFromObservable: any; `, - }, - { - name: absoluteFrom('/node_modules/@unknown/utils/index.d.ts'), - contents: ` + }, + { + name: absoluteFrom('/node_modules/@unknown/utils/index.d.ts'), + contents: ` export declare function toString(value: any): string; `, - }, - { - name: absoluteFrom('/node_modules/@not-angular/core/index.d.ts'), - contents: ` + }, + { + name: absoluteFrom('/node_modules/@not-angular/core/index.d.ts'), + contents: ` export const model: any; `, - }, - {name: fileName, contents} - ], - {target: ts.ScriptTarget.ESNext}); + }, + {name: fileName, contents}, + ], + {target: ts.ScriptTarget.ESNext}, + ); const sourceFile = program.getSourceFile(fileName); const importTracker = new ImportedSymbolsTracker(); const reflector = new TypeScriptReflectionHost(program.getTypeChecker()); @@ -443,12 +456,14 @@ function setup(contents: string) { throw new Error(`Cannot resolve test file ${fileName}`); } - let member: Pick|null = null; + let member: Pick | null = null; (function walk(node: ts.Node) { - if (ts.isPropertyDeclaration(node) && - (ts.isIdentifier(node.name) && node.name.text === 'test' || - ts.isPrivateIdentifier(node.name) && node.name.text === '#test')) { + if ( + ts.isPropertyDeclaration(node) && + ((ts.isIdentifier(node.name) && node.name.text === 'test') || + (ts.isPrivateIdentifier(node.name) && node.name.text === '#test')) + ) { member = reflectClassMember(node); } else { ts.forEachChild(node, walk); diff --git a/packages/compiler-cli/src/ngtsc/annotations/index.ts b/packages/compiler-cli/src/ngtsc/annotations/index.ts index 33ca4d16f32a5..d325b0d480115 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/index.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/index.ts @@ -8,9 +8,31 @@ /// -export {forwardRefResolver, getAngularDecorators, isAngularDecorator, NoopReferencesRegistry, ReferencesRegistry, ResourceLoader, ResourceLoaderContext} from './common'; +export { + forwardRefResolver, + getAngularDecorators, + isAngularDecorator, + NoopReferencesRegistry, + ReferencesRegistry, + ResourceLoader, + ResourceLoaderContext, +} from './common'; export {ComponentDecoratorHandler} from './component'; -export {DirectiveDecoratorHandler, InitializerApiFunction, INPUT_INITIALIZER_FN, MODEL_INITIALIZER_FN, OUTPUT_INITIALIZER_FNS, QUERY_INITIALIZER_FNS, queryDecoratorNames, QueryFunctionName, tryParseInitializerApi, tryParseInitializerBasedOutput, tryParseSignalInputMapping, tryParseSignalModelMapping, tryParseSignalQueryFromInitializer} from './directive'; +export { + DirectiveDecoratorHandler, + InitializerApiFunction, + INPUT_INITIALIZER_FN, + MODEL_INITIALIZER_FN, + OUTPUT_INITIALIZER_FNS, + QUERY_INITIALIZER_FNS, + queryDecoratorNames, + QueryFunctionName, + tryParseInitializerApi, + tryParseInitializerBasedOutput, + tryParseSignalInputMapping, + tryParseSignalModelMapping, + tryParseSignalQueryFromInitializer, +} from './directive'; export {NgModuleDecoratorHandler} from './ng_module'; export {InjectableDecoratorHandler} from './src/injectable'; export {PipeDecoratorHandler} from './src/pipe'; diff --git a/packages/compiler-cli/src/ngtsc/annotations/ng_module/index.ts b/packages/compiler-cli/src/ngtsc/annotations/ng_module/index.ts index 1b93ded83e405..13af80f2bdee0 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/ng_module/index.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/ng_module/index.ts @@ -7,4 +7,8 @@ */ export {NgModuleDecoratorHandler, NgModuleSymbol} from './src/handler'; -export {createModuleWithProvidersResolver, isResolvedModuleWithProviders, ResolvedModuleWithProviders} from './src/module_with_providers'; +export { + createModuleWithProvidersResolver, + isResolvedModuleWithProviders, + ResolvedModuleWithProviders, +} from './src/module_with_providers'; diff --git a/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/handler.ts index 6e7a462b2fd88..0370df99a5e20 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/handler.ts @@ -6,43 +6,132 @@ * found in the LICENSE file at https://angular.io/license */ -import {compileClassMetadata, compileDeclareClassMetadata, compileDeclareInjectorFromMetadata, compileDeclareNgModuleFromMetadata, compileInjector, compileNgModule, Expression, ExternalExpr, FactoryTarget, FunctionExpr, InvokeFunctionExpr, LiteralArrayExpr, R3ClassMetadata, R3CompiledExpression, R3FactoryMetadata, R3Identifiers, R3InjectorMetadata, R3NgModuleMetadata, R3NgModuleMetadataKind, R3Reference, R3SelectorScopeMode, ReturnStatement, SchemaMetadata, Statement, WrappedNodeExpr} from '@angular/compiler'; +import { + compileClassMetadata, + compileDeclareClassMetadata, + compileDeclareInjectorFromMetadata, + compileDeclareNgModuleFromMetadata, + compileInjector, + compileNgModule, + Expression, + ExternalExpr, + FactoryTarget, + FunctionExpr, + InvokeFunctionExpr, + LiteralArrayExpr, + R3ClassMetadata, + R3CompiledExpression, + R3FactoryMetadata, + R3Identifiers, + R3InjectorMetadata, + R3NgModuleMetadata, + R3NgModuleMetadataKind, + R3Reference, + R3SelectorScopeMode, + ReturnStatement, + SchemaMetadata, + Statement, + WrappedNodeExpr, +} from '@angular/compiler'; import ts from 'typescript'; -import {ErrorCode, FatalDiagnosticError, makeDiagnostic, makeRelatedInformation} from '../../../diagnostics'; -import {assertSuccessfulReferenceEmit, LocalCompilationExtraImportsTracker, Reference, ReferenceEmitter} from '../../../imports'; -import {isArrayEqual, isReferenceEqual, isSymbolEqual, SemanticDepGraphUpdater, SemanticReference, SemanticSymbol,} from '../../../incremental/semantic_graph'; -import {ExportedProviderStatusResolver, MetadataReader, MetadataRegistry, MetaKind} from '../../../metadata'; -import {DynamicValue, PartialEvaluator, ResolvedValue, SyntheticValue} from '../../../partial_evaluator'; +import { + ErrorCode, + FatalDiagnosticError, + makeDiagnostic, + makeRelatedInformation, +} from '../../../diagnostics'; +import { + assertSuccessfulReferenceEmit, + LocalCompilationExtraImportsTracker, + Reference, + ReferenceEmitter, +} from '../../../imports'; +import { + isArrayEqual, + isReferenceEqual, + isSymbolEqual, + SemanticDepGraphUpdater, + SemanticReference, + SemanticSymbol, +} from '../../../incremental/semantic_graph'; +import { + ExportedProviderStatusResolver, + MetadataReader, + MetadataRegistry, + MetaKind, +} from '../../../metadata'; +import { + DynamicValue, + PartialEvaluator, + ResolvedValue, + SyntheticValue, +} from '../../../partial_evaluator'; import {PerfEvent, PerfRecorder} from '../../../perf'; -import {ClassDeclaration, DeclarationNode, Decorator, ReflectionHost, reflectObjectLiteral,} from '../../../reflection'; +import { + ClassDeclaration, + DeclarationNode, + Decorator, + ReflectionHost, + reflectObjectLiteral, +} from '../../../reflection'; import {LocalModuleScopeRegistry, ScopeData} from '../../../scope'; import {getDiagnosticNode} from '../../../scope/src/util'; -import {AnalysisOutput, CompilationMode, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../../transform'; +import { + AnalysisOutput, + CompilationMode, + CompileResult, + DecoratorHandler, + DetectResult, + HandlerPrecedence, + ResolveResult, +} from '../../../transform'; import {getSourceFile} from '../../../util/src/typescript'; -import {combineResolvers, compileDeclareFactory, compileNgFactoryDefField, createValueHasWrongTypeError, extractClassMetadata, extractSchemas, findAngularDecorator, forwardRefResolver, getProviderDiagnostics, getValidConstructorDependencies, InjectableClassRegistry, isExpressionForwardReference, ReferencesRegistry, resolveProvidersRequiringFactory, toR3Reference, unwrapExpression, wrapFunctionExpressionsInParens, wrapTypeReference,} from '../../common'; - -import {createModuleWithProvidersResolver, isResolvedModuleWithProviders} from './module_with_providers'; +import { + combineResolvers, + compileDeclareFactory, + compileNgFactoryDefField, + createValueHasWrongTypeError, + extractClassMetadata, + extractSchemas, + findAngularDecorator, + forwardRefResolver, + getProviderDiagnostics, + getValidConstructorDependencies, + InjectableClassRegistry, + isExpressionForwardReference, + ReferencesRegistry, + resolveProvidersRequiringFactory, + toR3Reference, + unwrapExpression, + wrapFunctionExpressionsInParens, + wrapTypeReference, +} from '../../common'; + +import { + createModuleWithProvidersResolver, + isResolvedModuleWithProviders, +} from './module_with_providers'; export interface NgModuleAnalysis { mod: R3NgModuleMetadata; inj: R3InjectorMetadata; fac: R3FactoryMetadata; - classMetadata: R3ClassMetadata|null; + classMetadata: R3ClassMetadata | null; declarations: Reference[]; - rawDeclarations: ts.Expression|null; + rawDeclarations: ts.Expression | null; schemas: SchemaMetadata[]; imports: TopLevelImportedExpression[]; importRefs: Reference[]; - rawImports: ts.Expression|null; + rawImports: ts.Expression | null; exports: Reference[]; - rawExports: ts.Expression|null; - id: Expression|null; + rawExports: ts.Expression | null; + id: Expression | null; factorySymbolName: string; - providersRequiringFactory: Set>|null; - providers: ts.Expression|null; + providersRequiringFactory: Set> | null; + providers: ts.Expression | null; remoteScopesMayRequireCycleProtection: boolean; - decorator: ts.Decorator|null; + decorator: ts.Decorator | null; } export interface NgModuleResolution { @@ -54,9 +143,9 @@ export interface NgModuleResolution { */ export class NgModuleSymbol extends SemanticSymbol { private remotelyScopedComponents: { - component: SemanticSymbol, - usedDirectives: SemanticReference[], - usedPipes: SemanticReference[] + component: SemanticSymbol; + usedDirectives: SemanticReference[]; + usedPipes: SemanticReference[]; }[] = []; /** @@ -70,7 +159,10 @@ export class NgModuleSymbol extends SemanticSymbol { */ private transitiveImportsFromStandaloneComponents = new Set(); - constructor(decl: ClassDeclaration, public readonly hasProviders: boolean) { + constructor( + decl: ClassDeclaration, + public readonly hasProviders: boolean, + ) { super(decl); } @@ -99,7 +191,7 @@ export class NgModuleSymbol extends SemanticSymbol { } for (const currEntry of this.remotelyScopedComponents) { - const prevEntry = previousSymbol.remotelyScopedComponents.find(prevEntry => { + const prevEntry = previousSymbol.remotelyScopedComponents.find((prevEntry) => { return isSymbolEqual(prevEntry.component, currEntry.component); }); @@ -123,15 +215,18 @@ export class NgModuleSymbol extends SemanticSymbol { } } - if (previousSymbol.transitiveImportsFromStandaloneComponents.size !== - this.transitiveImportsFromStandaloneComponents.size) { + if ( + previousSymbol.transitiveImportsFromStandaloneComponents.size !== + this.transitiveImportsFromStandaloneComponents.size + ) { return true; } const previousImports = Array.from(previousSymbol.transitiveImportsFromStandaloneComponents); for (const transitiveImport of this.transitiveImportsFromStandaloneComponents) { - const prevEntry = - previousImports.find(prevEntry => isSymbolEqual(prevEntry, transitiveImport)); + const prevEntry = previousImports.find((prevEntry) => + isSymbolEqual(prevEntry, transitiveImport), + ); if (prevEntry === undefined) { return true; } @@ -153,8 +248,10 @@ export class NgModuleSymbol extends SemanticSymbol { } addRemotelyScopedComponent( - component: SemanticSymbol, usedDirectives: SemanticReference[], - usedPipes: SemanticReference[]): void { + component: SemanticSymbol, + usedDirectives: SemanticReference[], + usedPipes: SemanticReference[], + ): void { this.remotelyScopedComponents.push({component, usedDirectives, usedPipes}); } @@ -166,27 +263,37 @@ export class NgModuleSymbol extends SemanticSymbol { /** * Compiles @NgModule annotations to ngModuleDef fields. */ -export class NgModuleDecoratorHandler implements - DecoratorHandler { +export class NgModuleDecoratorHandler + implements DecoratorHandler +{ constructor( - private reflector: ReflectionHost, private evaluator: PartialEvaluator, - private metaReader: MetadataReader, private metaRegistry: MetadataRegistry, - private scopeRegistry: LocalModuleScopeRegistry, - private referencesRegistry: ReferencesRegistry, - private exportedProviderStatusResolver: ExportedProviderStatusResolver, - private semanticDepGraphUpdater: SemanticDepGraphUpdater|null, private isCore: boolean, - private refEmitter: ReferenceEmitter, private annotateForClosureCompiler: boolean, - private onlyPublishPublicTypings: boolean, - private injectableRegistry: InjectableClassRegistry, private perf: PerfRecorder, - private includeClassMetadata: boolean, private includeSelectorScope: boolean, - private readonly compilationMode: CompilationMode, - private readonly localCompilationExtraImportsTracker: LocalCompilationExtraImportsTracker| - null) {} + private reflector: ReflectionHost, + private evaluator: PartialEvaluator, + private metaReader: MetadataReader, + private metaRegistry: MetadataRegistry, + private scopeRegistry: LocalModuleScopeRegistry, + private referencesRegistry: ReferencesRegistry, + private exportedProviderStatusResolver: ExportedProviderStatusResolver, + private semanticDepGraphUpdater: SemanticDepGraphUpdater | null, + private isCore: boolean, + private refEmitter: ReferenceEmitter, + private annotateForClosureCompiler: boolean, + private onlyPublishPublicTypings: boolean, + private injectableRegistry: InjectableClassRegistry, + private perf: PerfRecorder, + private includeClassMetadata: boolean, + private includeSelectorScope: boolean, + private readonly compilationMode: CompilationMode, + private readonly localCompilationExtraImportsTracker: LocalCompilationExtraImportsTracker | null, + ) {} readonly precedence = HandlerPrecedence.PRIMARY; readonly name = 'NgModuleDecoratorHandler'; - detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult|undefined { + detect( + node: ClassDeclaration, + decorators: Decorator[] | null, + ): DetectResult | undefined { if (!decorators) { return undefined; } @@ -202,26 +309,34 @@ export class NgModuleDecoratorHandler implements } } - analyze(node: ClassDeclaration, decorator: Readonly): - AnalysisOutput { + analyze( + node: ClassDeclaration, + decorator: Readonly, + ): AnalysisOutput { this.perf.eventCount(PerfEvent.AnalyzeNgModule); const name = node.name.text; if (decorator.args === null || decorator.args.length > 1) { throw new FatalDiagnosticError( - ErrorCode.DECORATOR_ARITY_WRONG, decorator.node, - `Incorrect number of arguments to @NgModule decorator`); + ErrorCode.DECORATOR_ARITY_WRONG, + decorator.node, + `Incorrect number of arguments to @NgModule decorator`, + ); } // @NgModule can be invoked without arguments. In case it is, pretend as if a blank object // literal was specified. This simplifies the code below. - const meta = decorator.args.length === 1 ? unwrapExpression(decorator.args[0]) : - ts.factory.createObjectLiteralExpression([]); + const meta = + decorator.args.length === 1 + ? unwrapExpression(decorator.args[0]) + : ts.factory.createObjectLiteralExpression([]); if (!ts.isObjectLiteralExpression(meta)) { throw new FatalDiagnosticError( - ErrorCode.DECORATOR_ARG_NOT_LITERAL, meta, - '@NgModule argument must be an object literal'); + ErrorCode.DECORATOR_ARG_NOT_LITERAL, + meta, + '@NgModule argument must be an object literal', + ); } const ngModule = reflectObjectLiteral(meta); @@ -239,26 +354,31 @@ export class NgModuleDecoratorHandler implements // Resolving declarations let declarationRefs: Reference[] = []; - const rawDeclarations: ts.Expression|null = ngModule.get('declarations') ?? null; + const rawDeclarations: ts.Expression | null = ngModule.get('declarations') ?? null; if (rawDeclarations !== null) { const declarationMeta = this.evaluator.evaluate(rawDeclarations, forwardRefResolver); declarationRefs = this.resolveTypeList( - rawDeclarations, declarationMeta, name, 'declarations', 0, - this.compilationMode === CompilationMode.LOCAL) - .references; + rawDeclarations, + declarationMeta, + name, + 'declarations', + 0, + this.compilationMode === CompilationMode.LOCAL, + ).references; // Look through the declarations to make sure they're all a part of the current compilation. for (const ref of declarationRefs) { if (ref.node.getSourceFile().isDeclarationFile) { const errorNode = ref.getOriginForDiagnostics(rawDeclarations); - diagnostics.push(makeDiagnostic( - ErrorCode.NGMODULE_INVALID_DECLARATION, errorNode, - `Cannot declare '${ - ref.node.name - .text}' in an NgModule as it's not a part of the current compilation.`, - [makeRelatedInformation( - ref.node.name, `'${ref.node.name.text}' is declared here.`)])); + diagnostics.push( + makeDiagnostic( + ErrorCode.NGMODULE_INVALID_DECLARATION, + errorNode, + `Cannot declare '${ref.node.name.text}' in an NgModule as it's not a part of the current compilation.`, + [makeRelatedInformation(ref.node.name, `'${ref.node.name.text}' is declared here.`)], + ), + ); } } } @@ -269,16 +389,23 @@ export class NgModuleDecoratorHandler implements // Resolving imports let importRefs: Reference[] = []; - let rawImports: ts.Expression|null = ngModule.get('imports') ?? null; + let rawImports: ts.Expression | null = ngModule.get('imports') ?? null; if (rawImports !== null) { const importsMeta = this.evaluator.evaluate(rawImports, moduleResolvers); const result = this.resolveTypeList( - rawImports, importsMeta, name, 'imports', 0, - this.compilationMode === CompilationMode.LOCAL); - - if (this.compilationMode === CompilationMode.LOCAL && - this.localCompilationExtraImportsTracker !== null) { + rawImports, + importsMeta, + name, + 'imports', + 0, + this.compilationMode === CompilationMode.LOCAL, + ); + + if ( + this.compilationMode === CompilationMode.LOCAL && + this.localCompilationExtraImportsTracker !== null + ) { // For generating extra imports in local mode, the NgModule imports that are from external // files (i.e., outside of the compilation unit) are to be added to all the files in the // compilation unit. This is because any external component that is a dependency of some @@ -298,25 +425,33 @@ export class NgModuleDecoratorHandler implements // Resolving exports let exportRefs: Reference[] = []; - const rawExports: ts.Expression|null = ngModule.get('exports') ?? null; + const rawExports: ts.Expression | null = ngModule.get('exports') ?? null; if (rawExports !== null) { const exportsMeta = this.evaluator.evaluate(rawExports, moduleResolvers); exportRefs = this.resolveTypeList( - rawExports, exportsMeta, name, 'exports', 0, - this.compilationMode === CompilationMode.LOCAL) - .references; + rawExports, + exportsMeta, + name, + 'exports', + 0, + this.compilationMode === CompilationMode.LOCAL, + ).references; this.referencesRegistry.add(node, ...exportRefs); } // Resolving bootstrap let bootstrapRefs: Reference[] = []; - const rawBootstrap: ts.Expression|null = ngModule.get('bootstrap') ?? null; + const rawBootstrap: ts.Expression | null = ngModule.get('bootstrap') ?? null; if (this.compilationMode !== CompilationMode.LOCAL && rawBootstrap !== null) { const bootstrapMeta = this.evaluator.evaluate(rawBootstrap, forwardRefResolver); bootstrapRefs = this.resolveTypeList( - rawBootstrap, bootstrapMeta, name, 'bootstrap', 0, - /* allowUnresolvedReferences */ false) - .references; + rawBootstrap, + bootstrapMeta, + name, + 'bootstrap', + 0, + /* allowUnresolvedReferences */ false, + ).references; // Verify that the `@NgModule.bootstrap` list doesn't have Standalone Components. for (const ref of bootstrapRefs) { @@ -327,19 +462,22 @@ export class NgModuleDecoratorHandler implements } } - const schemas = this.compilationMode !== CompilationMode.LOCAL && ngModule.has('schemas') ? - extractSchemas(ngModule.get('schemas')!, this.evaluator, 'NgModule') : - []; + const schemas = + this.compilationMode !== CompilationMode.LOCAL && ngModule.has('schemas') + ? extractSchemas(ngModule.get('schemas')!, this.evaluator, 'NgModule') + : []; - let id: Expression|null = null; + let id: Expression | null = null; if (ngModule.has('id')) { const idExpr = ngModule.get('id')!; if (!isModuleIdExpression(idExpr)) { id = new WrappedNodeExpr(idExpr); } else { const diag = makeDiagnostic( - ErrorCode.WARN_NGMODULE_ID_UNNECESSARY, idExpr, - `Using 'module.id' for NgModule.id is a common anti-pattern that is ignored by the Angular compiler.`); + ErrorCode.WARN_NGMODULE_ID_UNNECESSARY, + idExpr, + `Using 'module.id' for NgModule.id is a common anti-pattern that is ignored by the Angular compiler.`, + ); diag.category = ts.DiagnosticCategory.Warning; diagnostics.push(diag); } @@ -347,35 +485,43 @@ export class NgModuleDecoratorHandler implements const valueContext = node.getSourceFile(); - const exportedNodes = new Set(exportRefs.map(ref => ref.node)); + const exportedNodes = new Set(exportRefs.map((ref) => ref.node)); const declarations: R3Reference[] = []; const exportedDeclarations: Expression[] = []; - const bootstrap = bootstrapRefs.map( - bootstrap => this._toR3Reference( - bootstrap.getOriginForDiagnostics(meta, node.name), bootstrap, valueContext)); + const bootstrap = bootstrapRefs.map((bootstrap) => + this._toR3Reference( + bootstrap.getOriginForDiagnostics(meta, node.name), + bootstrap, + valueContext, + ), + ); for (const ref of declarationRefs) { - const decl = - this._toR3Reference(ref.getOriginForDiagnostics(meta, node.name), ref, valueContext); + const decl = this._toR3Reference( + ref.getOriginForDiagnostics(meta, node.name), + ref, + valueContext, + ); declarations.push(decl); if (exportedNodes.has(ref.node)) { exportedDeclarations.push(decl.type); } } - const imports = importRefs.map( - imp => - this._toR3Reference(imp.getOriginForDiagnostics(meta, node.name), imp, valueContext)); - const exports = exportRefs.map( - exp => - this._toR3Reference(exp.getOriginForDiagnostics(meta, node.name), exp, valueContext)); - + const imports = importRefs.map((imp) => + this._toR3Reference(imp.getOriginForDiagnostics(meta, node.name), imp, valueContext), + ); + const exports = exportRefs.map((exp) => + this._toR3Reference(exp.getOriginForDiagnostics(meta, node.name), exp, valueContext), + ); const isForwardReference = (ref: R3Reference) => - isExpressionForwardReference(ref.value, node.name!, valueContext); - const containsForwardDecls = bootstrap.some(isForwardReference) || - declarations.some(isForwardReference) || imports.some(isForwardReference) || - exports.some(isForwardReference); + isExpressionForwardReference(ref.value, node.name!, valueContext); + const containsForwardDecls = + bootstrap.some(isForwardReference) || + declarations.some(isForwardReference) || + imports.some(isForwardReference) || + exports.some(isForwardReference); const type = wrapTypeReference(this.reflector, node); @@ -411,23 +557,28 @@ export class NgModuleDecoratorHandler implements id, // Use `ɵɵsetNgModuleScope` to patch selector scopes onto the generated definition in a // tree-shakeable way. - selectorScopeMode: this.includeSelectorScope ? R3SelectorScopeMode.SideEffect : - R3SelectorScopeMode.Omit, + selectorScopeMode: this.includeSelectorScope + ? R3SelectorScopeMode.SideEffect + : R3SelectorScopeMode.Omit, // TODO: to be implemented as a part of FW-1004. schemas: [], }; } const rawProviders = ngModule.has('providers') ? ngModule.get('providers')! : null; - let wrappedProviders: WrappedNodeExpr|null = null; + let wrappedProviders: WrappedNodeExpr | null = null; // In most cases the providers will be an array literal. Check if it has any elements // and don't include the providers if it doesn't which saves us a few bytes. - if (rawProviders !== null && - (!ts.isArrayLiteralExpression(rawProviders) || rawProviders.elements.length > 0)) { + if ( + rawProviders !== null && + (!ts.isArrayLiteralExpression(rawProviders) || rawProviders.elements.length > 0) + ) { wrappedProviders = new WrappedNodeExpr( - this.annotateForClosureCompiler ? wrapFunctionExpressionsInParens(rawProviders) : - rawProviders); + this.annotateForClosureCompiler + ? wrapFunctionExpressionsInParens(rawProviders) + : rawProviders, + ); } const topLevelImports: TopLevelImportedExpression[] = []; @@ -455,8 +606,13 @@ export class NgModuleDecoratorHandler implements const resolved = this.evaluator.evaluate(importExpr, moduleResolvers); const {references, hasModuleWithProviders} = this.resolveTypeList( - importExpr, [resolved], node.name.text, 'imports', absoluteIndex, - /* allowUnresolvedReferences */ false); + importExpr, + [resolved], + node.name.text, + 'imports', + absoluteIndex, + /* allowUnresolvedReferences */ false, + ); absoluteIndex += references.length; topLevelImports.push({ @@ -485,7 +641,7 @@ export class NgModuleDecoratorHandler implements if (ts.isArrayLiteralExpression(exp)) { // If array expression then add it entry-by-entry to the injector imports if (exp.elements) { - injectorMetadata.imports.push(...exp.elements.map(n => new WrappedNodeExpr(n))); + injectorMetadata.imports.push(...exp.elements.map((n) => new WrappedNodeExpr(n))); } } else { // if not array expression then add it as is to the injector's imports field. @@ -524,7 +680,7 @@ export class NgModuleDecoratorHandler implements // a `ForeignFunctionResolver` _like_ the `forwardRef` resolver. So we know when it's safe to // not use a closure, and will use one just in case otherwise. const remoteScopesMayRequireCycleProtection = - declarationRefs.some(isSyntheticReference) || importRefs.some(isSyntheticReference); + declarationRefs.some(isSyntheticReference) || importRefs.some(isSyntheticReference); return { diagnostics: diagnostics.length > 0 ? diagnostics : undefined, @@ -542,16 +698,15 @@ export class NgModuleDecoratorHandler implements exports: exportRefs, rawExports, providers: rawProviders, - providersRequiringFactory: rawProviders ? - resolveProvidersRequiringFactory(rawProviders, this.reflector, this.evaluator) : - null, - classMetadata: this.includeClassMetadata ? - extractClassMetadata( - node, this.reflector, this.isCore, this.annotateForClosureCompiler) : - null, + providersRequiringFactory: rawProviders + ? resolveProvidersRequiringFactory(rawProviders, this.reflector, this.evaluator) + : null, + classMetadata: this.includeClassMetadata + ? extractClassMetadata(node, this.reflector, this.isCore, this.annotateForClosureCompiler) + : null, factorySymbolName: node.name.text, remoteScopesMayRequireCycleProtection, - decorator: decorator?.node as ts.Decorator | null ?? null, + decorator: (decorator?.node as ts.Decorator | null) ?? null, }, }; } @@ -583,8 +738,10 @@ export class NgModuleDecoratorHandler implements }); } - resolve(node: ClassDeclaration, analysis: Readonly): - ResolveResult { + resolve( + node: ClassDeclaration, + analysis: Readonly, + ): ResolveResult { if (this.compilationMode === CompilationMode.LOCAL) { return {}; } @@ -599,7 +756,10 @@ export class NgModuleDecoratorHandler implements if (analysis.providersRequiringFactory !== null) { const providerDiagnostics = getProviderDiagnostics( - analysis.providersRequiringFactory, analysis.providers!, this.injectableRegistry); + analysis.providersRequiringFactory, + analysis.providers!, + this.injectableRegistry, + ); diagnostics.push(...providerDiagnostics); } @@ -617,7 +777,7 @@ export class NgModuleDecoratorHandler implements } const refsToEmit: Reference[] = []; - let symbol: NgModuleSymbol|null = null; + let symbol: NgModuleSymbol | null = null; if (this.semanticDepGraphUpdater !== null) { const sym = this.semanticDepGraphUpdater.getSymbol(node) as NgModuleSymbol; if (sym instanceof NgModuleSymbol) { @@ -634,16 +794,18 @@ export class NgModuleDecoratorHandler implements } // Check whether this component has providers. - const mayExportProviders = - this.exportedProviderStatusResolver.mayExportProviders(dirMeta.ref, (importRef) => { - // We need to keep track of which transitive imports were used to decide - // `mayExportProviders`, since if those change in a future compilation this - // NgModule will need to be re-emitted. - if (symbol !== null && this.semanticDepGraphUpdater !== null) { - const importSymbol = this.semanticDepGraphUpdater.getSymbol(importRef.node); - symbol.addTransitiveImportFromStandaloneComponent(importSymbol); - } - }); + const mayExportProviders = this.exportedProviderStatusResolver.mayExportProviders( + dirMeta.ref, + (importRef) => { + // We need to keep track of which transitive imports were used to decide + // `mayExportProviders`, since if those change in a future compilation this + // NgModule will need to be re-emitted. + if (symbol !== null && this.semanticDepGraphUpdater !== null) { + const importSymbol = this.semanticDepGraphUpdater.getSymbol(importRef.node); + symbol.addTransitiveImportFromStandaloneComponent(importSymbol); + } + }, + ); if (!mayExportProviders) { // Skip emit of components that don't carry providers. @@ -694,8 +856,10 @@ export class NgModuleDecoratorHandler implements if (dirMeta.selector === null) { throw new FatalDiagnosticError( - ErrorCode.DIRECTIVE_MISSING_SELECTOR, decl.node, - `${refType} ${decl.node.name.text} has no selector, please add it!`); + ErrorCode.DIRECTIVE_MISSING_SELECTOR, + decl.node, + `${refType} ${decl.node.name.text} has no selector, please add it!`, + ); } continue; @@ -707,8 +871,12 @@ export class NgModuleDecoratorHandler implements return {diagnostics}; } - if (scope === null || scope.compilation.isPoisoned || scope.exported.isPoisoned || - scope.reexports === null) { + if ( + scope === null || + scope.compilation.isPoisoned || + scope.exported.isPoisoned || + scope.reexports === null + ) { return {data}; } else { return { @@ -719,10 +887,17 @@ export class NgModuleDecoratorHandler implements } compileFull( - node: ClassDeclaration, - {inj, mod, fac, classMetadata, declarations, remoteScopesMayRequireCycleProtection}: - Readonly, - {injectorImports}: Readonly): CompileResult[] { + node: ClassDeclaration, + { + inj, + mod, + fac, + classMetadata, + declarations, + remoteScopesMayRequireCycleProtection, + }: Readonly, + {injectorImports}: Readonly, + ): CompileResult[] { const factoryFn = compileNgFactoryDefField(fac); const ngInjectorDef = compileInjector({ ...inj, @@ -733,14 +908,20 @@ export class NgModuleDecoratorHandler implements const metadata = classMetadata !== null ? compileClassMetadata(classMetadata) : null; this.insertMetadataStatement(statements, metadata); this.appendRemoteScopingStatements( - statements, node, declarations, remoteScopesMayRequireCycleProtection); + statements, + node, + declarations, + remoteScopesMayRequireCycleProtection, + ); return this.compileNgModule(factoryFn, ngInjectorDef, ngModuleDef); } compilePartial( - node: ClassDeclaration, {inj, fac, mod, classMetadata}: Readonly, - {injectorImports}: Readonly): CompileResult[] { + node: ClassDeclaration, + {inj, fac, mod, classMetadata}: Readonly, + {injectorImports}: Readonly, + ): CompileResult[] { const factoryFn = compileDeclareFactory(fac); const injectorDef = compileDeclareInjectorFromMetadata({ ...inj, @@ -754,9 +935,16 @@ export class NgModuleDecoratorHandler implements } compileLocal( - node: ClassDeclaration, - {inj, mod, fac, classMetadata, declarations, remoteScopesMayRequireCycleProtection}: - Readonly): CompileResult[] { + node: ClassDeclaration, + { + inj, + mod, + fac, + classMetadata, + declarations, + remoteScopesMayRequireCycleProtection, + }: Readonly, + ): CompileResult[] { const factoryFn = compileNgFactoryDefField(fac); const ngInjectorDef = compileInjector({ ...inj, @@ -766,7 +954,11 @@ export class NgModuleDecoratorHandler implements const metadata = classMetadata !== null ? compileClassMetadata(classMetadata) : null; this.insertMetadataStatement(statements, metadata); this.appendRemoteScopingStatements( - statements, node, declarations, remoteScopesMayRequireCycleProtection); + statements, + node, + declarations, + remoteScopesMayRequireCycleProtection, + ); return this.compileNgModule(factoryFn, ngInjectorDef, ngModuleDef); } @@ -774,8 +966,10 @@ export class NgModuleDecoratorHandler implements /** * Add class metadata statements, if provided, to the `ngModuleStatements`. */ - private insertMetadataStatement(ngModuleStatements: Statement[], metadata: Expression|null): - void { + private insertMetadataStatement( + ngModuleStatements: Statement[], + metadata: Expression | null, + ): void { if (metadata !== null) { ngModuleStatements.unshift(metadata.toStmt()); } @@ -785,9 +979,11 @@ export class NgModuleDecoratorHandler implements * Add remote scoping statements, as needed, to the `ngModuleStatements`. */ private appendRemoteScopingStatements( - ngModuleStatements: Statement[], node: ClassDeclaration, - declarations: Reference[], - remoteScopesMayRequireCycleProtection: boolean): void { + ngModuleStatements: Statement[], + node: ClassDeclaration, + declarations: Reference[], + remoteScopesMayRequireCycleProtection: boolean, + ): void { // Local compilation mode generates its own runtimes to compute the dependencies. So there no // need to add remote scope statements (which also conflicts with local compilation runtimes) if (this.compilationMode === CompilationMode.LOCAL) { @@ -797,12 +993,12 @@ export class NgModuleDecoratorHandler implements for (const decl of declarations) { const remoteScope = this.scopeRegistry.getRemoteScope(decl.node); if (remoteScope !== null) { - const directives = remoteScope.directives.map(directive => { + const directives = remoteScope.directives.map((directive) => { const type = this.refEmitter.emit(directive, context); assertSuccessfulReferenceEmit(type, node, 'directive'); return type.expression; }); - const pipes = remoteScope.pipes.map(pipe => { + const pipes = remoteScope.pipes.map((pipe) => { const type = this.refEmitter.emit(pipe, context); assertSuccessfulReferenceEmit(type, node, 'pipe'); return type.expression; @@ -810,18 +1006,23 @@ export class NgModuleDecoratorHandler implements const directiveArray = new LiteralArrayExpr(directives); const pipesArray = new LiteralArrayExpr(pipes); - const directiveExpr = remoteScopesMayRequireCycleProtection && directives.length > 0 ? - new FunctionExpr([], [new ReturnStatement(directiveArray)]) : - directiveArray; - const pipesExpr = remoteScopesMayRequireCycleProtection && pipes.length > 0 ? - new FunctionExpr([], [new ReturnStatement(pipesArray)]) : - pipesArray; + const directiveExpr = + remoteScopesMayRequireCycleProtection && directives.length > 0 + ? new FunctionExpr([], [new ReturnStatement(directiveArray)]) + : directiveArray; + const pipesExpr = + remoteScopesMayRequireCycleProtection && pipes.length > 0 + ? new FunctionExpr([], [new ReturnStatement(pipesArray)]) + : pipesArray; const componentType = this.refEmitter.emit(decl, context); assertSuccessfulReferenceEmit(componentType, node, 'component'); const declExpr = componentType.expression; const setComponentScope = new ExternalExpr(R3Identifiers.setComponentScope); - const callExpr = - new InvokeFunctionExpr(setComponentScope, [declExpr, directiveExpr, pipesExpr]); + const callExpr = new InvokeFunctionExpr(setComponentScope, [ + declExpr, + directiveExpr, + pipesExpr, + ]); ngModuleStatements.push(callExpr.toStmt()); } @@ -829,8 +1030,10 @@ export class NgModuleDecoratorHandler implements } private compileNgModule( - factoryFn: CompileResult, injectorDef: R3CompiledExpression, - ngModuleDef: R3CompiledExpression): CompileResult[] { + factoryFn: CompileResult, + injectorDef: R3CompiledExpression, + ngModuleDef: R3CompiledExpression, + ): CompileResult[] { const res: CompileResult[] = [ factoryFn, { @@ -852,8 +1055,10 @@ export class NgModuleDecoratorHandler implements } private _toR3Reference( - origin: ts.Node, valueRef: Reference, - valueContext: ts.SourceFile): R3Reference { + origin: ts.Node, + valueRef: Reference, + valueContext: ts.SourceFile, + ): R3Reference { if (valueRef.hasOwningModuleGuess) { return toR3Reference(origin, valueRef, valueContext, this.refEmitter); } else { @@ -870,11 +1075,16 @@ export class NgModuleDecoratorHandler implements * Compute a list of `Reference`s from a resolved metadata value. */ private resolveTypeList( - expr: ts.Node, resolvedList: ResolvedValue, className: string, arrayName: string, - absoluteIndex: number, allowUnresolvedReferences: boolean): { - references: Reference[], - hasModuleWithProviders: boolean, - dynamicValues: DynamicValue[] + expr: ts.Node, + resolvedList: ResolvedValue, + className: string, + arrayName: string, + absoluteIndex: number, + allowUnresolvedReferences: boolean, + ): { + references: Reference[]; + hasModuleWithProviders: boolean; + dynamicValues: DynamicValue[]; } { let hasModuleWithProviders = false; const refList: Reference[] = []; @@ -890,8 +1100,10 @@ export class NgModuleDecoratorHandler implements } throw createValueHasWrongTypeError( - expr, resolvedList, - `Expected array when reading the NgModule.${arrayName} of ${className}`); + expr, + resolvedList, + `Expected array when reading the NgModule.${arrayName} of ${className}`, + ); } for (let idx = 0; idx < resolvedList.length; idx++) { @@ -909,7 +1121,13 @@ export class NgModuleDecoratorHandler implements if (Array.isArray(entry)) { // Recurse into nested arrays. const recursiveResult = this.resolveTypeList( - expr, entry, className, arrayName, absoluteIndex, allowUnresolvedReferences); + expr, + entry, + className, + arrayName, + absoluteIndex, + allowUnresolvedReferences, + ); refList.push(...recursiveResult.references); for (const d of recursiveResult.dynamicValues) { @@ -921,9 +1139,10 @@ export class NgModuleDecoratorHandler implements } else if (entry instanceof Reference) { if (!this.isClassDeclarationReference(entry)) { throw createValueHasWrongTypeError( - entry.node, entry, - `Value at position ${absoluteIndex} in the NgModule.${arrayName} of ${ - className} is not a class`); + entry.node, + entry, + `Value at position ${absoluteIndex} in the NgModule.${arrayName} of ${className} is not a class`, + ); } refList.push(entry); absoluteIndex += 1; @@ -933,9 +1152,10 @@ export class NgModuleDecoratorHandler implements } else { // TODO(alxhub): Produce a better diagnostic here - the array index may be an inner array. throw createValueHasWrongTypeError( - expr, entry, - `Value at position ${absoluteIndex} in the NgModule.${arrayName} of ${ - className} is not a reference`); + expr, + entry, + `Value at position ${absoluteIndex} in the NgModule.${arrayName} of ${className} is not a reference`, + ); } } @@ -948,15 +1168,19 @@ export class NgModuleDecoratorHandler implements } function isNgModule(node: ClassDeclaration, compilation: ScopeData): boolean { - return !compilation.dependencies.some(dep => dep.ref.node === node); + return !compilation.dependencies.some((dep) => dep.ref.node === node); } /** * Checks whether the given `ts.Expression` is the expression `module.id`. */ function isModuleIdExpression(expr: ts.Expression): boolean { - return ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression) && - expr.expression.text === 'module' && expr.name.text === 'id'; + return ( + ts.isPropertyAccessExpression(expr) && + ts.isIdentifier(expr.expression) && + expr.expression.text === 'module' && + expr.name.text === 'id' + ); } export interface TopLevelImportedExpression { @@ -970,20 +1194,26 @@ export interface TopLevelImportedExpression { * is referenced in the `@NgModule.bootstrap` array. */ function makeStandaloneBootstrapDiagnostic( - ngModuleClass: ClassDeclaration, bootstrappedClassRef: Reference, - rawBootstrapExpr: ts.Expression|null): ts.Diagnostic { + ngModuleClass: ClassDeclaration, + bootstrappedClassRef: Reference, + rawBootstrapExpr: ts.Expression | null, +): ts.Diagnostic { const componentClassName = bootstrappedClassRef.node.name.text; // Note: this error message should be aligned with the one produced by JIT. - const message = // - `The \`${componentClassName}\` class is a standalone component, which can ` + - `not be used in the \`@NgModule.bootstrap\` array. Use the \`bootstrapApplication\` ` + - `function for bootstrap instead.`; - const relatedInformation: ts.DiagnosticRelatedInformation[]|undefined = - [makeRelatedInformation(ngModuleClass, `The 'bootstrap' array is present on this NgModule.`)]; + const message = // + `The \`${componentClassName}\` class is a standalone component, which can ` + + `not be used in the \`@NgModule.bootstrap\` array. Use the \`bootstrapApplication\` ` + + `function for bootstrap instead.`; + const relatedInformation: ts.DiagnosticRelatedInformation[] | undefined = [ + makeRelatedInformation(ngModuleClass, `The 'bootstrap' array is present on this NgModule.`), + ]; return makeDiagnostic( - ErrorCode.NGMODULE_BOOTSTRAP_IS_STANDALONE, - getDiagnosticNode(bootstrappedClassRef, rawBootstrapExpr), message, relatedInformation); + ErrorCode.NGMODULE_BOOTSTRAP_IS_STANDALONE, + getDiagnosticNode(bootstrappedClassRef, rawBootstrapExpr), + message, + relatedInformation, + ); } function isSyntheticReference(ref: Reference): boolean { diff --git a/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/module_with_providers.ts b/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/module_with_providers.ts index 5ee9fecebf636..6e320f1fd5798 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/module_with_providers.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/ng_module/src/module_with_providers.ts @@ -11,7 +11,13 @@ import ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../../diagnostics'; import {Reference} from '../../../imports'; import {ForeignFunctionResolver, SyntheticValue} from '../../../partial_evaluator'; -import {ClassDeclaration, entityNameToValue, isNamedClassDeclaration, ReflectionHost, typeNodeToValueExpr} from '../../../reflection'; +import { + ClassDeclaration, + entityNameToValue, + isNamedClassDeclaration, + ReflectionHost, + typeNodeToValueExpr, +} from '../../../reflection'; /** * Creates a foreign function resolver to detect a `ModuleWithProviders` type in a return type @@ -22,7 +28,9 @@ import {ClassDeclaration, entityNameToValue, isNamedClassDeclaration, Reflection * @param isCore Whether the @angular/core package is being compiled. */ export function createModuleWithProvidersResolver( - reflector: ReflectionHost, isCore: boolean): ForeignFunctionResolver { + reflector: ReflectionHost, + isCore: boolean, +): ForeignFunctionResolver { /** * Retrieve an `NgModule` identifier (T) from the specified `type`, if it is of the form: * `ModuleWithProviders` @@ -30,17 +38,19 @@ export function createModuleWithProvidersResolver( * @returns the identifier of the NgModule type if found, or null otherwise. */ function _reflectModuleFromTypeParam( - type: ts.TypeNode, - node: ts.FunctionDeclaration|ts.MethodDeclaration|ts.FunctionExpression): ts.Expression|null { + type: ts.TypeNode, + node: ts.FunctionDeclaration | ts.MethodDeclaration | ts.FunctionExpression, + ): ts.Expression | null { // Examine the type of the function to see if it's a ModuleWithProviders reference. if (!ts.isTypeReferenceNode(type)) { return null; } - const typeName = type && - (ts.isIdentifier(type.typeName) && type.typeName || - ts.isQualifiedName(type.typeName) && type.typeName.right) || - null; + const typeName = + (type && + ((ts.isIdentifier(type.typeName) && type.typeName) || + (ts.isQualifiedName(type.typeName) && type.typeName.right))) || + null; if (typeName === null) { return null; } @@ -61,14 +71,17 @@ export function createModuleWithProvidersResolver( // If there's no type parameter specified, bail. if (type.typeArguments === undefined || type.typeArguments.length !== 1) { const parent = - ts.isMethodDeclaration(node) && ts.isClassDeclaration(node.parent) ? node.parent : null; - const symbolName = (parent && parent.name ? parent.name.getText() + '.' : '') + - (node.name ? node.name.getText() : 'anonymous'); + ts.isMethodDeclaration(node) && ts.isClassDeclaration(node.parent) ? node.parent : null; + const symbolName = + (parent && parent.name ? parent.name.getText() + '.' : '') + + (node.name ? node.name.getText() : 'anonymous'); throw new FatalDiagnosticError( - ErrorCode.NGMODULE_MODULE_WITH_PROVIDERS_MISSING_GENERIC, type, - `${symbolName} returns a ModuleWithProviders type without a generic type argument. ` + - `Please add a generic type argument to the ModuleWithProviders type. If this ` + - `occurrence is in library code you don't control, please contact the library authors.`); + ErrorCode.NGMODULE_MODULE_WITH_PROVIDERS_MISSING_GENERIC, + type, + `${symbolName} returns a ModuleWithProviders type without a generic type argument. ` + + `Please add a generic type argument to the ModuleWithProviders type. If this ` + + `occurrence is in library code you don't control, please contact the library authors.`, + ); } const arg = type.typeArguments[0]; @@ -82,18 +95,21 @@ export function createModuleWithProvidersResolver( * @param type The type to reflect on. * @returns the identifier of the NgModule type if found, or null otherwise. */ - function _reflectModuleFromLiteralType(type: ts.TypeNode): ts.Expression|null { + function _reflectModuleFromLiteralType(type: ts.TypeNode): ts.Expression | null { if (!ts.isIntersectionTypeNode(type)) { return null; } for (const t of type.types) { if (ts.isTypeLiteralNode(t)) { for (const m of t.members) { - const ngModuleType = ts.isPropertySignature(m) && ts.isIdentifier(m.name) && - m.name.text === 'ngModule' && m.type || - null; + const ngModuleType = + (ts.isPropertySignature(m) && + ts.isIdentifier(m.name) && + m.name.text === 'ngModule' && + m.type) || + null; - let ngModuleExpression: ts.Expression|null = null; + let ngModuleExpression: ts.Expression | null = null; // Handle `: typeof X` or `: X` cases. if (ngModuleType !== null && ts.isTypeQueryNode(ngModuleType)) { @@ -118,7 +134,7 @@ export function createModuleWithProvidersResolver( } const type = - _reflectModuleFromTypeParam(rawType, fn.node) ?? _reflectModuleFromLiteralType(rawType); + _reflectModuleFromTypeParam(rawType, fn.node) ?? _reflectModuleFromLiteralType(rawType); if (type === null) { return unresolvable; } @@ -139,9 +155,13 @@ export interface ResolvedModuleWithProviders { mwpCall: ts.CallExpression; } -export function isResolvedModuleWithProviders(sv: SyntheticValue): - sv is SyntheticValue { - return typeof sv.value === 'object' && sv.value != null && - sv.value.hasOwnProperty('ngModule' as keyof ResolvedModuleWithProviders) && - sv.value.hasOwnProperty('mwpCall' as keyof ResolvedModuleWithProviders); +export function isResolvedModuleWithProviders( + sv: SyntheticValue, +): sv is SyntheticValue { + return ( + typeof sv.value === 'object' && + sv.value != null && + sv.value.hasOwnProperty('ngModule' as keyof ResolvedModuleWithProviders) && + sv.value.hasOwnProperty('mwpCall' as keyof ResolvedModuleWithProviders) + ); } diff --git a/packages/compiler-cli/src/ngtsc/annotations/ng_module/test/ng_module_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/ng_module/test/ng_module_spec.ts index fe806c0cee4cb..52246eeb971eb 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/ng_module/test/ng_module_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/ng_module/test/ng_module_spec.ts @@ -12,10 +12,19 @@ import ts from 'typescript'; import {absoluteFrom} from '../../../file_system'; import {runInEachFileSystem} from '../../../file_system/testing'; import {LocalIdentifierStrategy, ReferenceEmitter} from '../../../imports'; -import {CompoundMetadataReader, DtsMetadataReader, ExportedProviderStatusResolver, LocalMetadataRegistry} from '../../../metadata'; +import { + CompoundMetadataReader, + DtsMetadataReader, + ExportedProviderStatusResolver, + LocalMetadataRegistry, +} from '../../../metadata'; import {PartialEvaluator} from '../../../partial_evaluator'; import {NOOP_PERF_RECORDER} from '../../../perf'; -import {ClassDeclaration, isNamedClassDeclaration, TypeScriptReflectionHost} from '../../../reflection'; +import { + ClassDeclaration, + isNamedClassDeclaration, + TypeScriptReflectionHost, +} from '../../../reflection'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../../scope'; import {getDeclaration, makeProgram} from '../../../testing'; import {CompilationMode} from '../../../transform'; @@ -24,34 +33,55 @@ import {NgModuleDecoratorHandler} from '../src/handler'; function setup(program: ts.Program, compilationMode = CompilationMode.FULL) { const checker = program.getTypeChecker(); - const reflectionHost = - new TypeScriptReflectionHost(checker, compilationMode === CompilationMode.LOCAL); + const reflectionHost = new TypeScriptReflectionHost( + checker, + compilationMode === CompilationMode.LOCAL, + ); const evaluator = new PartialEvaluator(reflectionHost, checker, /* dependencyTracker */ null); const referencesRegistry = new NoopReferencesRegistry(); const metaRegistry = new LocalMetadataRegistry(); const dtsReader = new DtsMetadataReader(checker, reflectionHost); const metaReader = new CompoundMetadataReader([metaRegistry, dtsReader]); const scopeRegistry = new LocalModuleScopeRegistry( - metaRegistry, metaReader, new MetadataDtsModuleScopeResolver(dtsReader, null), - new ReferenceEmitter([]), null); + metaRegistry, + metaReader, + new MetadataDtsModuleScopeResolver(dtsReader, null), + new ReferenceEmitter([]), + null, + ); const refEmitter = new ReferenceEmitter([new LocalIdentifierStrategy()]); const injectableRegistry = new InjectableClassRegistry(reflectionHost, /* isCore */ false); const exportedProviderStatusResolver = new ExportedProviderStatusResolver(metaReader); const handler = new NgModuleDecoratorHandler( - reflectionHost, evaluator, metaReader, metaRegistry, scopeRegistry, referencesRegistry, - exportedProviderStatusResolver, /* semanticDepGraphUpdater */ null, - /* isCore */ false, refEmitter, - /* annotateForClosureCompiler */ false, - /* onlyPublishPublicTypings */ false, injectableRegistry, NOOP_PERF_RECORDER, true, true, - compilationMode, /* localCompilationExtraImportsTracker */ null); + reflectionHost, + evaluator, + metaReader, + metaRegistry, + scopeRegistry, + referencesRegistry, + exportedProviderStatusResolver, + /* semanticDepGraphUpdater */ null, + /* isCore */ false, + refEmitter, + /* annotateForClosureCompiler */ false, + /* onlyPublishPublicTypings */ false, + injectableRegistry, + NOOP_PERF_RECORDER, + true, + true, + compilationMode, + /* localCompilationExtraImportsTracker */ null, + ); return {handler, reflectionHost}; } function detectNgModule( - module: ClassDeclaration, handler: NgModuleDecoratorHandler, - reflectionHost: TypeScriptReflectionHost) { + module: ClassDeclaration, + handler: NgModuleDecoratorHandler, + reflectionHost: TypeScriptReflectionHost, +) { const detected = handler.detect(module, reflectionHost.getDecoratorsOfDeclaration(module)); if (detected === undefined) { @@ -94,51 +124,62 @@ runInEachFileSystem(() => { imports: [forwardRef(() => TestModuleDependency)] }) export class TestModule {} - ` + `, }, ]); const {handler, reflectionHost} = setup(program); - const TestModule = - getDeclaration(program, _('/entry.ts'), 'TestModule', isNamedClassDeclaration); + const TestModule = getDeclaration( + program, + _('/entry.ts'), + 'TestModule', + isNamedClassDeclaration, + ); const detected = detectNgModule(TestModule, handler, reflectionHost); - const moduleDef = - handler.analyze(TestModule, detected.metadata).analysis!.mod as R3NgModuleMetadataGlobal; + const moduleDef = handler.analyze(TestModule, detected.metadata).analysis! + .mod as R3NgModuleMetadataGlobal; expect(getReferenceIdentifierTexts(moduleDef.declarations)).toEqual(['TestComp']); expect(getReferenceIdentifierTexts(moduleDef.exports)).toEqual(['TestComp']); expect(getReferenceIdentifierTexts(moduleDef.imports)).toEqual(['TestModuleDependency']); function getReferenceIdentifierTexts(references: R3Reference[]) { - return references.map(ref => (ref.value as WrappedNodeExpr).node.text); + return references.map((ref) => (ref.value as WrappedNodeExpr).node.text); } }); describe('local compilation mode', () => { it('should not produce diagnostic for cross-file imports', () => { const {program} = makeProgram( - [ - { - name: _('/node_modules/@angular/core/index.d.ts'), - contents: 'export const NgModule: any;', - }, - { - name: _('/entry.ts'), - contents: ` + [ + { + name: _('/node_modules/@angular/core/index.d.ts'), + contents: 'export const NgModule: any;', + }, + { + name: _('/entry.ts'), + contents: ` import {NgModule} from '@angular/core'; import {SomeModule} from './some_where'; @NgModule({ imports: [SomeModule], }) class TestModule {} - ` - } - ], - undefined, undefined, false); + `, + }, + ], + undefined, + undefined, + false, + ); const {reflectionHost, handler} = setup(program, CompilationMode.LOCAL); - const TestModule = - getDeclaration(program, _('/entry.ts'), 'TestModule', isNamedClassDeclaration); + const TestModule = getDeclaration( + program, + _('/entry.ts'), + 'TestModule', + isNamedClassDeclaration, + ); const detected = detectNgModule(TestModule, handler, reflectionHost); const {diagnostics} = handler.analyze(TestModule, detected.metadata); @@ -148,27 +189,34 @@ runInEachFileSystem(() => { it('should not produce diagnostic for cross-file exports', () => { const {program} = makeProgram( - [ - { - name: _('/node_modules/@angular/core/index.d.ts'), - contents: 'export const NgModule: any;', - }, - { - name: _('/entry.ts'), - contents: ` + [ + { + name: _('/node_modules/@angular/core/index.d.ts'), + contents: 'export const NgModule: any;', + }, + { + name: _('/entry.ts'), + contents: ` import {NgModule} from '@angular/core'; import {SomeModule} from './some_where'; @NgModule({ exports: [SomeModule], }) class TestModule {} - ` - } - ], - undefined, undefined, false); + `, + }, + ], + undefined, + undefined, + false, + ); const {reflectionHost, handler} = setup(program, CompilationMode.LOCAL); - const TestModule = - getDeclaration(program, _('/entry.ts'), 'TestModule', isNamedClassDeclaration); + const TestModule = getDeclaration( + program, + _('/entry.ts'), + 'TestModule', + isNamedClassDeclaration, + ); const detected = detectNgModule(TestModule, handler, reflectionHost); const {diagnostics} = handler.analyze(TestModule, detected.metadata); @@ -178,27 +226,34 @@ runInEachFileSystem(() => { it('should not produce diagnostic for cross-file declarations', () => { const {program} = makeProgram( - [ - { - name: _('/node_modules/@angular/core/index.d.ts'), - contents: 'export const NgModule: any;', - }, - { - name: _('/entry.ts'), - contents: ` + [ + { + name: _('/node_modules/@angular/core/index.d.ts'), + contents: 'export const NgModule: any;', + }, + { + name: _('/entry.ts'), + contents: ` import {NgModule} from '@angular/core'; import {SomeComponent} from './some_where'; @NgModule({ declarations: [SomeComponent], }) class TestModule {} - ` - } - ], - undefined, undefined, false); + `, + }, + ], + undefined, + undefined, + false, + ); const {reflectionHost, handler} = setup(program, CompilationMode.LOCAL); - const TestModule = - getDeclaration(program, _('/entry.ts'), 'TestModule', isNamedClassDeclaration); + const TestModule = getDeclaration( + program, + _('/entry.ts'), + 'TestModule', + isNamedClassDeclaration, + ); const detected = detectNgModule(TestModule, handler, reflectionHost); const {diagnostics} = handler.analyze(TestModule, detected.metadata); @@ -208,27 +263,34 @@ runInEachFileSystem(() => { it('should not produce diagnostic for cross-file bootstrap', () => { const {program} = makeProgram( - [ - { - name: _('/node_modules/@angular/core/index.d.ts'), - contents: 'export const NgModule: any;', - }, - { - name: _('/entry.ts'), - contents: ` + [ + { + name: _('/node_modules/@angular/core/index.d.ts'), + contents: 'export const NgModule: any;', + }, + { + name: _('/entry.ts'), + contents: ` import {NgModule} from '@angular/core'; import {SomeComponent} from './some_where'; @NgModule({ bootstrap: [SomeComponent], }) class TestModule {} - ` - } - ], - undefined, undefined, false); + `, + }, + ], + undefined, + undefined, + false, + ); const {reflectionHost, handler} = setup(program, CompilationMode.LOCAL); - const TestModule = - getDeclaration(program, _('/entry.ts'), 'TestModule', isNamedClassDeclaration); + const TestModule = getDeclaration( + program, + _('/entry.ts'), + 'TestModule', + isNamedClassDeclaration, + ); const detected = detectNgModule(TestModule, handler, reflectionHost); const {diagnostics} = handler.analyze(TestModule, detected.metadata); @@ -238,27 +300,34 @@ runInEachFileSystem(() => { it('should not produce diagnostic for schemas', () => { const {program} = makeProgram( - [ - { - name: _('/node_modules/@angular/core/index.d.ts'), - contents: 'export const NgModule: any; export const CUSTOM_ELEMENTS_SCHEMA: any;', - }, - { - name: _('/entry.ts'), - contents: ` + [ + { + name: _('/node_modules/@angular/core/index.d.ts'), + contents: 'export const NgModule: any; export const CUSTOM_ELEMENTS_SCHEMA: any;', + }, + { + name: _('/entry.ts'), + contents: ` import {NgModule, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; import {SomeComponent} from './some_where'; @NgModule({ schemas: [CUSTOM_ELEMENTS_SCHEMA], }) class TestModule {} - ` - } - ], - undefined, undefined, false); + `, + }, + ], + undefined, + undefined, + false, + ); const {reflectionHost, handler} = setup(program, CompilationMode.LOCAL); - const TestModule = - getDeclaration(program, _('/entry.ts'), 'TestModule', isNamedClassDeclaration); + const TestModule = getDeclaration( + program, + _('/entry.ts'), + 'TestModule', + isNamedClassDeclaration, + ); const detected = detectNgModule(TestModule, handler, reflectionHost); const {diagnostics} = handler.analyze(TestModule, detected.metadata); diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts index 174416fc8c8d6..7b79e66178474 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts @@ -6,7 +6,23 @@ * found in the LICENSE file at https://angular.io/license */ -import {compileClassMetadata, CompileClassMetadataFn, compileDeclareClassMetadata, compileDeclareInjectableFromMetadata, compileInjectable, createMayBeForwardRefExpression, FactoryTarget, ForwardRefHandling, LiteralExpr, MaybeForwardRefExpression, R3ClassMetadata, R3CompiledExpression, R3DependencyMetadata, R3InjectableMetadata, WrappedNodeExpr,} from '@angular/compiler'; +import { + compileClassMetadata, + CompileClassMetadataFn, + compileDeclareClassMetadata, + compileDeclareInjectableFromMetadata, + compileInjectable, + createMayBeForwardRefExpression, + FactoryTarget, + ForwardRefHandling, + LiteralExpr, + MaybeForwardRefExpression, + R3ClassMetadata, + R3CompiledExpression, + R3DependencyMetadata, + R3InjectableMetadata, + WrappedNodeExpr, +} from '@angular/compiler'; import ts from 'typescript'; import {InjectableClassRegistry, isAbstractClassDeclaration} from '../../annotations/common'; @@ -14,38 +30,70 @@ import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {PartialEvaluator} from '../../partial_evaluator'; import {PerfEvent, PerfRecorder} from '../../perf'; import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection'; -import {AnalysisOutput, CompilationMode, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult,} from '../../transform'; -import {checkInheritanceOfInjectable, compileDeclareFactory, CompileFactoryFn, compileNgFactoryDefField, extractClassMetadata, findAngularDecorator, getConstructorDependencies, getValidConstructorDependencies, isAngularCore, toFactoryMetadata, tryUnwrapForwardRef, unwrapConstructorDependencies, validateConstructorDependencies, wrapTypeReference,} from '../common'; +import { + AnalysisOutput, + CompilationMode, + CompileResult, + DecoratorHandler, + DetectResult, + HandlerPrecedence, + ResolveResult, +} from '../../transform'; +import { + checkInheritanceOfInjectable, + compileDeclareFactory, + CompileFactoryFn, + compileNgFactoryDefField, + extractClassMetadata, + findAngularDecorator, + getConstructorDependencies, + getValidConstructorDependencies, + isAngularCore, + toFactoryMetadata, + tryUnwrapForwardRef, + unwrapConstructorDependencies, + validateConstructorDependencies, + wrapTypeReference, +} from '../common'; export interface InjectableHandlerData { meta: R3InjectableMetadata; - classMetadata: R3ClassMetadata|null; - ctorDeps: R3DependencyMetadata[]|'invalid'|null; + classMetadata: R3ClassMetadata | null; + ctorDeps: R3DependencyMetadata[] | 'invalid' | null; needsFactory: boolean; } /** * Adapts the `compileInjectable` compiler for `@Injectable` decorators to the Ivy compiler. */ -export class InjectableDecoratorHandler implements - DecoratorHandler { +export class InjectableDecoratorHandler + implements DecoratorHandler +{ constructor( - private reflector: ReflectionHost, private evaluator: PartialEvaluator, - private isCore: boolean, private strictCtorDeps: boolean, - private injectableRegistry: InjectableClassRegistry, private perf: PerfRecorder, - private includeClassMetadata: boolean, private readonly compilationMode: CompilationMode, - /** - * What to do if the injectable already contains a ɵprov property. - * - * If true then an error diagnostic is reported. - * If false then there is no error and a new ɵprov property is not added. - */ - private errorOnDuplicateProv = true) {} + private reflector: ReflectionHost, + private evaluator: PartialEvaluator, + private isCore: boolean, + private strictCtorDeps: boolean, + private injectableRegistry: InjectableClassRegistry, + private perf: PerfRecorder, + private includeClassMetadata: boolean, + private readonly compilationMode: CompilationMode, + /** + * What to do if the injectable already contains a ɵprov property. + * + * If true then an error diagnostic is reported. + * If false then there is no error and a new ɵprov property is not added. + */ + private errorOnDuplicateProv = true, + ) {} readonly precedence = HandlerPrecedence.SHARED; readonly name = 'InjectableDecoratorHandler'; - detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult|undefined { + detect( + node: ClassDeclaration, + decorators: Decorator[] | null, + ): DetectResult | undefined { if (!decorators) { return undefined; } @@ -61,8 +109,10 @@ export class InjectableDecoratorHandler implements } } - analyze(node: ClassDeclaration, decorator: Readonly): - AnalysisOutput { + analyze( + node: ClassDeclaration, + decorator: Readonly, + ): AnalysisOutput { this.perf.eventCount(PerfEvent.AnalyzeInjectable); const meta = extractInjectableMetadata(node, decorator, this.reflector); @@ -72,14 +122,21 @@ export class InjectableDecoratorHandler implements analysis: { meta, ctorDeps: extractInjectableCtorDeps( - node, meta, decorator, this.reflector, this.isCore, this.strictCtorDeps), - classMetadata: this.includeClassMetadata ? - extractClassMetadata(node, this.reflector, this.isCore) : - null, + node, + meta, + decorator, + this.reflector, + this.isCore, + this.strictCtorDeps, + ), + classMetadata: this.includeClassMetadata + ? extractClassMetadata(node, this.reflector, this.isCore) + : null, // Avoid generating multiple factories if a class has // more Angular decorators, apart from Injectable. - needsFactory: !decorators || - decorators.every(current => !isAngularCore(current) || current.name === 'Injectable') + needsFactory: + !decorators || + decorators.every((current) => !isAngularCore(current) || current.name === 'Injectable'), }, }; } @@ -98,16 +155,23 @@ export class InjectableDecoratorHandler implements }); } - resolve(node: ClassDeclaration, analysis: Readonly): - ResolveResult { + resolve( + node: ClassDeclaration, + analysis: Readonly, + ): ResolveResult { if (this.compilationMode === CompilationMode.LOCAL) { return {}; } if (requiresValidCtor(analysis.meta)) { const diagnostic = checkInheritanceOfInjectable( - node, this.injectableRegistry, this.reflector, this.evaluator, this.strictCtorDeps, - 'Injectable'); + node, + this.injectableRegistry, + this.reflector, + this.evaluator, + this.strictCtorDeps, + 'Injectable', + ); if (diagnostic !== null) { return { diagnostics: [diagnostic], @@ -120,45 +184,64 @@ export class InjectableDecoratorHandler implements compileFull(node: ClassDeclaration, analysis: Readonly): CompileResult[] { return this.compile( - compileNgFactoryDefField, meta => compileInjectable(meta, false), compileClassMetadata, - node, analysis); + compileNgFactoryDefField, + (meta) => compileInjectable(meta, false), + compileClassMetadata, + node, + analysis, + ); } - compilePartial(node: ClassDeclaration, analysis: Readonly): - CompileResult[] { + compilePartial( + node: ClassDeclaration, + analysis: Readonly, + ): CompileResult[] { return this.compile( - compileDeclareFactory, compileDeclareInjectableFromMetadata, compileDeclareClassMetadata, - node, analysis); + compileDeclareFactory, + compileDeclareInjectableFromMetadata, + compileDeclareClassMetadata, + node, + analysis, + ); } compileLocal(node: ClassDeclaration, analysis: Readonly): CompileResult[] { return this.compile( - compileNgFactoryDefField, meta => compileInjectable(meta, false), compileClassMetadata, - node, analysis); + compileNgFactoryDefField, + (meta) => compileInjectable(meta, false), + compileClassMetadata, + node, + analysis, + ); } private compile( - compileFactoryFn: CompileFactoryFn, - compileInjectableFn: (meta: R3InjectableMetadata) => R3CompiledExpression, - compileClassMetadataFn: CompileClassMetadataFn, node: ClassDeclaration, - analysis: Readonly): CompileResult[] { + compileFactoryFn: CompileFactoryFn, + compileInjectableFn: (meta: R3InjectableMetadata) => R3CompiledExpression, + compileClassMetadataFn: CompileClassMetadataFn, + node: ClassDeclaration, + analysis: Readonly, + ): CompileResult[] { const results: CompileResult[] = []; if (analysis.needsFactory) { const meta = analysis.meta; const factoryRes = compileFactoryFn( - toFactoryMetadata({...meta, deps: analysis.ctorDeps}, FactoryTarget.Injectable)); + toFactoryMetadata({...meta, deps: analysis.ctorDeps}, FactoryTarget.Injectable), + ); if (analysis.classMetadata !== null) { factoryRes.statements.push(compileClassMetadataFn(analysis.classMetadata).toStmt()); } results.push(factoryRes); } - const ɵprov = this.reflector.getMembersOfClass(node).find(member => member.name === 'ɵprov'); + const ɵprov = this.reflector.getMembersOfClass(node).find((member) => member.name === 'ɵprov'); if (ɵprov !== undefined && this.errorOnDuplicateProv) { throw new FatalDiagnosticError( - ErrorCode.INJECTABLE_DUPLICATE_PROV, ɵprov.nameNode || ɵprov.node || node, - 'Injectables cannot contain a static ɵprov property, because the compiler is going to generate one.'); + ErrorCode.INJECTABLE_DUPLICATE_PROV, + ɵprov.nameNode || ɵprov.node || node, + 'Injectables cannot contain a static ɵprov property, because the compiler is going to generate one.', + ); } if (ɵprov === undefined) { @@ -169,7 +252,7 @@ export class InjectableDecoratorHandler implements initializer: res.expression, statements: res.statements, type: res.type, - deferrableImports: null + deferrableImports: null, }); } @@ -184,14 +267,19 @@ export class InjectableDecoratorHandler implements * A `null` return value indicates this is @Injectable has invalid data. */ function extractInjectableMetadata( - clazz: ClassDeclaration, decorator: Decorator, - reflector: ReflectionHost): R3InjectableMetadata { + clazz: ClassDeclaration, + decorator: Decorator, + reflector: ReflectionHost, +): R3InjectableMetadata { const name = clazz.name.text; const type = wrapTypeReference(reflector, clazz); const typeArgumentCount = reflector.getGenericArityOfClass(clazz) || 0; if (decorator.args === null) { throw new FatalDiagnosticError( - ErrorCode.DECORATOR_NOT_CALLED, decorator.node, '@Injectable must be called'); + ErrorCode.DECORATOR_NOT_CALLED, + decorator.node, + '@Injectable must be called', + ); } if (decorator.args.length === 0) { return { @@ -207,26 +295,30 @@ function extractInjectableMetadata( // used to solve - if this restriction proves too undesirable we can re-implement lowering. if (!ts.isObjectLiteralExpression(metaNode)) { throw new FatalDiagnosticError( - ErrorCode.DECORATOR_ARG_NOT_LITERAL, metaNode, - `@Injectable argument must be an object literal`); + ErrorCode.DECORATOR_ARG_NOT_LITERAL, + metaNode, + `@Injectable argument must be an object literal`, + ); } // Resolve the fields of the literal into a map of field name to expression. const meta = reflectObjectLiteral(metaNode); - const providedIn = meta.has('providedIn') ? - getProviderExpression(meta.get('providedIn')!, reflector) : - createMayBeForwardRefExpression(new LiteralExpr(null), ForwardRefHandling.None); + const providedIn = meta.has('providedIn') + ? getProviderExpression(meta.get('providedIn')!, reflector) + : createMayBeForwardRefExpression(new LiteralExpr(null), ForwardRefHandling.None); - let deps: R3DependencyMetadata[]|undefined = undefined; + let deps: R3DependencyMetadata[] | undefined = undefined; if ((meta.has('useClass') || meta.has('useFactory')) && meta.has('deps')) { const depsExpr = meta.get('deps')!; if (!ts.isArrayLiteralExpression(depsExpr)) { throw new FatalDiagnosticError( - ErrorCode.VALUE_NOT_LITERAL, depsExpr, - `@Injectable deps metadata must be an inline array`); + ErrorCode.VALUE_NOT_LITERAL, + depsExpr, + `@Injectable deps metadata must be an inline array`, + ); } - deps = depsExpr.elements.map(dep => getDep(dep, reflector)); + deps = depsExpr.elements.map((dep) => getDep(dep, reflector)); } const result: R3InjectableMetadata = {name, type, typeArgumentCount, providedIn}; @@ -244,7 +336,10 @@ function extractInjectableMetadata( return result; } else { throw new FatalDiagnosticError( - ErrorCode.DECORATOR_ARITY_WRONG, decorator.args[2], 'Too many arguments to @Injectable'); + ErrorCode.DECORATOR_ARITY_WRONG, + decorator.args[2], + 'Too many arguments to @Injectable', + ); } } @@ -256,22 +351,33 @@ function extractInjectableMetadata( * object to indicate whether the value needed unwrapping. */ function getProviderExpression( - expression: ts.Expression, reflector: ReflectionHost): MaybeForwardRefExpression { + expression: ts.Expression, + reflector: ReflectionHost, +): MaybeForwardRefExpression { const forwardRefValue = tryUnwrapForwardRef(expression, reflector); return createMayBeForwardRefExpression( - new WrappedNodeExpr(forwardRefValue ?? expression), - forwardRefValue !== null ? ForwardRefHandling.Unwrapped : ForwardRefHandling.None); + new WrappedNodeExpr(forwardRefValue ?? expression), + forwardRefValue !== null ? ForwardRefHandling.Unwrapped : ForwardRefHandling.None, + ); } function extractInjectableCtorDeps( - clazz: ClassDeclaration, meta: R3InjectableMetadata, decorator: Decorator, - reflector: ReflectionHost, isCore: boolean, strictCtorDeps: boolean) { + clazz: ClassDeclaration, + meta: R3InjectableMetadata, + decorator: Decorator, + reflector: ReflectionHost, + isCore: boolean, + strictCtorDeps: boolean, +) { if (decorator.args === null) { throw new FatalDiagnosticError( - ErrorCode.DECORATOR_NOT_CALLED, decorator.node, '@Injectable must be called'); + ErrorCode.DECORATOR_NOT_CALLED, + decorator.node, + '@Injectable must be called', + ); } - let ctorDeps: R3DependencyMetadata[]|'invalid'|null = null; + let ctorDeps: R3DependencyMetadata[] | 'invalid' | null = null; if (decorator.args.length === 0) { // Ideally, using @Injectable() would have the same effect as using @Injectable({...}), and be @@ -285,8 +391,9 @@ function extractInjectableCtorDeps( if (strictCtorDeps && !isAbstractClassDeclaration(clazz)) { ctorDeps = getValidConstructorDependencies(clazz, reflector, isCore); } else { - ctorDeps = - unwrapConstructorDependencies(getConstructorDependencies(clazz, reflector, isCore)); + ctorDeps = unwrapConstructorDependencies( + getConstructorDependencies(clazz, reflector, isCore), + ); } return ctorDeps; @@ -306,8 +413,12 @@ function extractInjectableCtorDeps( } function requiresValidCtor(meta: R3InjectableMetadata): boolean { - return meta.useValue === undefined && meta.useExisting === undefined && - meta.useClass === undefined && meta.useFactory === undefined; + return ( + meta.useValue === undefined && + meta.useExisting === undefined && + meta.useClass === undefined && + meta.useFactory === undefined + ); } function getDep(dep: ts.Expression, reflector: ReflectionHost): R3DependencyMetadata { @@ -321,7 +432,10 @@ function getDep(dep: ts.Expression, reflector: ReflectionHost): R3DependencyMeta }; function maybeUpdateDecorator( - dec: ts.Identifier, reflector: ReflectionHost, token?: ts.Expression): boolean { + dec: ts.Identifier, + reflector: ReflectionHost, + token?: ts.Expression, + ): boolean { const source = reflector.getImportOfIdentifier(dec); if (source === null || source.from !== '@angular/core') { return false; @@ -348,12 +462,12 @@ function getDep(dep: ts.Expression, reflector: ReflectionHost): R3DependencyMeta } if (ts.isArrayLiteralExpression(dep)) { - dep.elements.forEach(el => { + dep.elements.forEach((el) => { let isDecorator = false; if (ts.isIdentifier(el)) { isDecorator = maybeUpdateDecorator(el, reflector); } else if (ts.isNewExpression(el) && ts.isIdentifier(el.expression)) { - const token = el.arguments && el.arguments.length > 0 && el.arguments[0] || undefined; + const token = (el.arguments && el.arguments.length > 0 && el.arguments[0]) || undefined; isDecorator = maybeUpdateDecorator(el.expression, reflector, token); } if (!isDecorator) { diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts b/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts index f52ff6152cebc..c1abd5752bb81 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts @@ -6,7 +6,16 @@ * found in the LICENSE file at https://angular.io/license */ -import {compileClassMetadata, compileDeclareClassMetadata, compileDeclarePipeFromMetadata, compilePipeFromMetadata, FactoryTarget, R3ClassMetadata, R3PipeMetadata, WrappedNodeExpr,} from '@angular/compiler'; +import { + compileClassMetadata, + compileDeclareClassMetadata, + compileDeclarePipeFromMetadata, + compilePipeFromMetadata, + FactoryTarget, + R3ClassMetadata, + R3PipeMetadata, + WrappedNodeExpr, +} from '@angular/compiler'; import ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; @@ -17,21 +26,45 @@ import {PartialEvaluator} from '../../partial_evaluator'; import {PerfEvent, PerfRecorder} from '../../perf'; import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection'; import {LocalModuleScopeRegistry} from '../../scope'; -import {AnalysisOutput, CompilationMode, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult,} from '../../transform'; -import {compileDeclareFactory, compileNgFactoryDefField, compileResults, createValueHasWrongTypeError, extractClassMetadata, findAngularDecorator, getValidConstructorDependencies, InjectableClassRegistry, makeDuplicateDeclarationError, toFactoryMetadata, unwrapExpression, wrapTypeReference,} from '../common'; +import { + AnalysisOutput, + CompilationMode, + CompileResult, + DecoratorHandler, + DetectResult, + HandlerPrecedence, + ResolveResult, +} from '../../transform'; +import { + compileDeclareFactory, + compileNgFactoryDefField, + compileResults, + createValueHasWrongTypeError, + extractClassMetadata, + findAngularDecorator, + getValidConstructorDependencies, + InjectableClassRegistry, + makeDuplicateDeclarationError, + toFactoryMetadata, + unwrapExpression, + wrapTypeReference, +} from '../common'; export interface PipeHandlerData { meta: R3PipeMetadata; - classMetadata: R3ClassMetadata|null; + classMetadata: R3ClassMetadata | null; pipeNameExpr: ts.Expression; - decorator: ts.Decorator|null; + decorator: ts.Decorator | null; } /** * Represents an Angular pipe. */ export class PipeSymbol extends SemanticSymbol { - constructor(decl: ClassDeclaration, public readonly name: string) { + constructor( + decl: ClassDeclaration, + public readonly name: string, + ) { super(decl); } @@ -48,20 +81,29 @@ export class PipeSymbol extends SemanticSymbol { } } -export class PipeDecoratorHandler implements - DecoratorHandler { +export class PipeDecoratorHandler + implements DecoratorHandler +{ constructor( - private reflector: ReflectionHost, private evaluator: PartialEvaluator, - private metaRegistry: MetadataRegistry, private scopeRegistry: LocalModuleScopeRegistry, - private injectableRegistry: InjectableClassRegistry, private isCore: boolean, - private perf: PerfRecorder, private includeClassMetadata: boolean, - private readonly compilationMode: CompilationMode, - private readonly generateExtraImportsInLocalMode: boolean) {} + private reflector: ReflectionHost, + private evaluator: PartialEvaluator, + private metaRegistry: MetadataRegistry, + private scopeRegistry: LocalModuleScopeRegistry, + private injectableRegistry: InjectableClassRegistry, + private isCore: boolean, + private perf: PerfRecorder, + private includeClassMetadata: boolean, + private readonly compilationMode: CompilationMode, + private readonly generateExtraImportsInLocalMode: boolean, + ) {} readonly precedence = HandlerPrecedence.PRIMARY; readonly name = 'PipeDecoratorHandler'; - detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult|undefined { + detect( + node: ClassDeclaration, + decorators: Decorator[] | null, + ): DetectResult | undefined { if (!decorators) { return undefined; } @@ -77,8 +119,10 @@ export class PipeDecoratorHandler implements } } - analyze(clazz: ClassDeclaration, decorator: Readonly): - AnalysisOutput { + analyze( + clazz: ClassDeclaration, + decorator: Readonly, + ): AnalysisOutput { this.perf.eventCount(PerfEvent.AnalyzePipe); const name = clazz.name.text; @@ -86,22 +130,34 @@ export class PipeDecoratorHandler implements if (decorator.args === null) { throw new FatalDiagnosticError( - ErrorCode.DECORATOR_NOT_CALLED, decorator.node, `@Pipe must be called`); + ErrorCode.DECORATOR_NOT_CALLED, + decorator.node, + `@Pipe must be called`, + ); } if (decorator.args.length !== 1) { throw new FatalDiagnosticError( - ErrorCode.DECORATOR_ARITY_WRONG, decorator.node, '@Pipe must have exactly one argument'); + ErrorCode.DECORATOR_ARITY_WRONG, + decorator.node, + '@Pipe must have exactly one argument', + ); } const meta = unwrapExpression(decorator.args[0]); if (!ts.isObjectLiteralExpression(meta)) { throw new FatalDiagnosticError( - ErrorCode.DECORATOR_ARG_NOT_LITERAL, meta, '@Pipe must have a literal argument'); + ErrorCode.DECORATOR_ARG_NOT_LITERAL, + meta, + '@Pipe must have a literal argument', + ); } const pipe = reflectObjectLiteral(meta); if (!pipe.has('name')) { throw new FatalDiagnosticError( - ErrorCode.PIPE_MISSING_NAME, meta, `@Pipe decorator is missing name field`); + ErrorCode.PIPE_MISSING_NAME, + meta, + `@Pipe decorator is missing name field`, + ); } const pipeNameExpr = pipe.get('name')!; const pipeName = this.evaluator.evaluate(pipeNameExpr); @@ -140,11 +196,11 @@ export class PipeDecoratorHandler implements pure, isStandalone, }, - classMetadata: this.includeClassMetadata ? - extractClassMetadata(clazz, this.reflector, this.isCore) : - null, + classMetadata: this.includeClassMetadata + ? extractClassMetadata(clazz, this.reflector, this.isCore) + : null, pipeNameExpr, - decorator: decorator?.node as ts.Decorator | null ?? null, + decorator: (decorator?.node as ts.Decorator | null) ?? null, }, }; } @@ -189,27 +245,30 @@ export class PipeDecoratorHandler implements compileFull(node: ClassDeclaration, analysis: Readonly): CompileResult[] { const fac = compileNgFactoryDefField(toFactoryMetadata(analysis.meta, FactoryTarget.Pipe)); const def = compilePipeFromMetadata(analysis.meta); - const classMetadata = analysis.classMetadata !== null ? - compileClassMetadata(analysis.classMetadata).toStmt() : - null; + const classMetadata = + analysis.classMetadata !== null + ? compileClassMetadata(analysis.classMetadata).toStmt() + : null; return compileResults(fac, def, classMetadata, 'ɵpipe', null, null /* deferrableImports */); } compilePartial(node: ClassDeclaration, analysis: Readonly): CompileResult[] { const fac = compileDeclareFactory(toFactoryMetadata(analysis.meta, FactoryTarget.Pipe)); const def = compileDeclarePipeFromMetadata(analysis.meta); - const classMetadata = analysis.classMetadata !== null ? - compileDeclareClassMetadata(analysis.classMetadata).toStmt() : - null; + const classMetadata = + analysis.classMetadata !== null + ? compileDeclareClassMetadata(analysis.classMetadata).toStmt() + : null; return compileResults(fac, def, classMetadata, 'ɵpipe', null, null /* deferrableImports */); } compileLocal(node: ClassDeclaration, analysis: Readonly): CompileResult[] { const fac = compileNgFactoryDefField(toFactoryMetadata(analysis.meta, FactoryTarget.Pipe)); const def = compilePipeFromMetadata(analysis.meta); - const classMetadata = analysis.classMetadata !== null ? - compileClassMetadata(analysis.classMetadata).toStmt() : - null; + const classMetadata = + analysis.classMetadata !== null + ? compileClassMetadata(analysis.classMetadata).toStmt() + : null; return compileResults(fac, def, classMetadata, 'ɵpipe', null, null /* deferrableImports */); } } diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/injectable_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/injectable_spec.ts index 276bdd3745dd2..8ba7e852eb4dd 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/injectable_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/injectable_spec.ts @@ -19,31 +19,29 @@ import {InjectableDecoratorHandler} from '../src/injectable'; runInEachFileSystem(() => { describe('InjectableDecoratorHandler', () => { describe('compile()', () => { - it('should produce a diagnostic when injectable already has a static ɵprov property (with errorOnDuplicateProv true)', - () => { - const {handler, TestClass, ɵprov, analysis} = - setupHandler(/* errorOnDuplicateProv */ true); - try { - handler.compileFull(TestClass, analysis); - return fail('Compilation should have failed'); - } catch (err) { - if (!(err instanceof FatalDiagnosticError)) { - return fail('Error should be a FatalDiagnosticError'); - } - const diag = err.toDiagnostic(); - expect(diag.code).toEqual(ngErrorCode(ErrorCode.INJECTABLE_DUPLICATE_PROV)); - expect(diag.file.fileName.endsWith('entry.ts')).toBe(true); - expect(diag.start).toBe(ɵprov.nameNode!.getStart()); - } - }); + it('should produce a diagnostic when injectable already has a static ɵprov property (with errorOnDuplicateProv true)', () => { + const {handler, TestClass, ɵprov, analysis} = setupHandler(/* errorOnDuplicateProv */ true); + try { + handler.compileFull(TestClass, analysis); + return fail('Compilation should have failed'); + } catch (err) { + if (!(err instanceof FatalDiagnosticError)) { + return fail('Error should be a FatalDiagnosticError'); + } + const diag = err.toDiagnostic(); + expect(diag.code).toEqual(ngErrorCode(ErrorCode.INJECTABLE_DUPLICATE_PROV)); + expect(diag.file.fileName.endsWith('entry.ts')).toBe(true); + expect(diag.start).toBe(ɵprov.nameNode!.getStart()); + } + }); - it('should not add new ɵprov property when injectable already has one (with errorOnDuplicateProv false)', - () => { - const {handler, TestClass, ɵprov, analysis} = - setupHandler(/* errorOnDuplicateProv */ false); - const res = handler.compileFull(TestClass, analysis); - expect(res).not.toContain(jasmine.objectContaining({name: 'ɵprov'})); - }); + it('should not add new ɵprov property when injectable already has one (with errorOnDuplicateProv false)', () => { + const {handler, TestClass, ɵprov, analysis} = setupHandler( + /* errorOnDuplicateProv */ false, + ); + const res = handler.compileFull(TestClass, analysis); + expect(res).not.toContain(jasmine.objectContaining({name: 'ɵprov'})); + }); }); }); }); @@ -64,7 +62,7 @@ function setupHandler(errorOnDuplicateProv: boolean) { @Injectable({providedIn: 'module'}) export class TestClass { static ɵprov = ɵɵdefineInjectable({ factory: () => {}, token: TestClassToken, providedIn: "module" }); - }` + }`, }, ]); const checker = program.getTypeChecker(); @@ -72,11 +70,20 @@ function setupHandler(errorOnDuplicateProv: boolean) { const injectableRegistry = new InjectableClassRegistry(reflectionHost, /* isCore */ false); const evaluator = new PartialEvaluator(reflectionHost, checker, null); const handler = new InjectableDecoratorHandler( - reflectionHost, evaluator, /* isCore */ false, - /* strictCtorDeps */ false, injectableRegistry, NOOP_PERF_RECORDER, true, - /*compilationMode */ CompilationMode.FULL, errorOnDuplicateProv); + reflectionHost, + evaluator, + /* isCore */ false, + /* strictCtorDeps */ false, + injectableRegistry, + NOOP_PERF_RECORDER, + true, + /*compilationMode */ CompilationMode.FULL, + errorOnDuplicateProv, + ); const TestClass = getDeclaration(program, ENTRY_FILE, 'TestClass', isNamedClassDeclaration); - const ɵprov = reflectionHost.getMembersOfClass(TestClass).find(member => member.name === 'ɵprov'); + const ɵprov = reflectionHost + .getMembersOfClass(TestClass) + .find((member) => member.name === 'ɵprov'); if (ɵprov === undefined) { throw new Error('TestClass did not contain a `ɵprov` member'); } diff --git a/packages/compiler-cli/src/ngtsc/core/api/src/adapter.ts b/packages/compiler-cli/src/ngtsc/core/api/src/adapter.ts index 214318c2457ce..c93a61e4168df 100644 --- a/packages/compiler-cli/src/ngtsc/core/api/src/adapter.ts +++ b/packages/compiler-cli/src/ngtsc/core/api/src/adapter.ts @@ -17,17 +17,20 @@ import {ExtendedTsCompilerHost, UnifiedModulesHost} from './interfaces'; * `NgCompilerAdapter`. */ export type ExtendedCompilerHostMethods = - // Used to normalize filenames for the host system. Important for proper case-sensitive file - // handling. - 'getCanonicalFileName'| - // An optional method of `ts.CompilerHost` where an implementer can override module resolution. - 'resolveModuleNames'| - // Retrieve the current working directory. Unlike in `ts.ModuleResolutionHost`, this is a - // required method. - 'getCurrentDirectory'| - // Additional methods of `ExtendedTsCompilerHost` related to resource files (e.g. HTML - // templates). These are optional. - 'getModifiedResourceFiles'|'readResource'|'resourceNameToFileName'|'transformResource'; + // Used to normalize filenames for the host system. Important for proper case-sensitive file + // handling. + | 'getCanonicalFileName' + // An optional method of `ts.CompilerHost` where an implementer can override module resolution. + | 'resolveModuleNames' + // Retrieve the current working directory. Unlike in `ts.ModuleResolutionHost`, this is a + // required method. + | 'getCurrentDirectory' + // Additional methods of `ExtendedTsCompilerHost` related to resource files (e.g. HTML + // templates). These are optional. + | 'getModifiedResourceFiles' + | 'readResource' + | 'resourceNameToFileName' + | 'transformResource'; /** * Adapter for `NgCompiler` that allows it to be used in various circumstances, such as @@ -37,12 +40,12 @@ export type ExtendedCompilerHostMethods = * which is relied upon by `NgCompiler`. A consumer of `NgCompiler` can therefore use the * `NgCompilerHost` or implement `NgCompilerAdapter` itself. */ -export interface NgCompilerAdapter extends - // getCurrentDirectory is removed from `ts.ModuleResolutionHost` because it's optional, and - // incompatible with the `ts.CompilerHost` version which isn't. The combination of these two - // still satisfies `ts.ModuleResolutionHost`. - Omit, - Pick, +export interface NgCompilerAdapter + // getCurrentDirectory is removed from `ts.ModuleResolutionHost` because it's optional, and + // incompatible with the `ts.CompilerHost` version which isn't. The combination of these two + // still satisfies `ts.ModuleResolutionHost`. + extends Omit, + Pick, SourceFileTypeIdentifier { /** * A path to a single file which represents the entrypoint of an Angular Package Format library, @@ -51,7 +54,7 @@ export interface NgCompilerAdapter extends * This is used to emit a flat module index if requested, and can be left `null` if that is not * required. */ - readonly entryPoint: AbsoluteFsPath|null; + readonly entryPoint: AbsoluteFsPath | null; /** * An array of `ts.Diagnostic`s that occurred during construction of the `ts.Program`. @@ -73,7 +76,7 @@ export interface NgCompilerAdapter extends * * If not required, this can be `null`. */ - readonly unifiedModulesHost: UnifiedModulesHost|null; + readonly unifiedModulesHost: UnifiedModulesHost | null; /** * Resolved list of root directories explicitly set in, or inferred from, the tsconfig. diff --git a/packages/compiler-cli/src/ngtsc/core/api/src/interfaces.ts b/packages/compiler-cli/src/ngtsc/core/api/src/interfaces.ts index 072f7a77bd66d..2a31e301eba4a 100644 --- a/packages/compiler-cli/src/ngtsc/core/api/src/interfaces.ts +++ b/packages/compiler-cli/src/ngtsc/core/api/src/interfaces.ts @@ -38,8 +38,10 @@ export interface ResourceHost { * the implementation's `resourceNameToFileName` resolution fails. */ resourceNameToFileName( - resourceName: string, containingFilePath: string, - fallbackResolve?: (url: string, fromFile: string) => string | null): string|null; + resourceName: string, + containingFilePath: string, + fallbackResolve?: (url: string, fromFile: string) => string | null, + ): string | null; /** * Load a referenced resource either statically or asynchronously. If the host returns a @@ -47,13 +49,13 @@ export interface ResourceHost { * `loadNgStructureAsync()`. Returning `Promise` outside `loadNgStructureAsync()` will * cause a diagnostics error or an exception to be thrown. */ - readResource(fileName: string): Promise|string; + readResource(fileName: string): Promise | string; /** * Get the absolute paths to the changed files that triggered the current compilation * or `undefined` if this is not an incremental build. */ - getModifiedResourceFiles?(): Set|undefined; + getModifiedResourceFiles?(): Set | undefined; /** * Transform an inline or external resource asynchronously. @@ -66,8 +68,10 @@ export interface ResourceHost { * @param context Information regarding the resource such as the type and containing file. * @returns A promise of either the transformed resource data or null if no transformation occurs. */ - transformResource? - (data: string, context: ResourceHostContext): Promise; + transformResource?( + data: string, + context: ResourceHostContext, + ): Promise; } /** @@ -83,7 +87,7 @@ export interface ResourceHostContext { /** * The absolute path to the resource file. If the resource is inline, the value will be null. */ - readonly resourceFile: string|null; + readonly resourceFile: string | null; /** * The absolute path to the file that contains the resource or reference to the resource. */ @@ -106,5 +110,7 @@ export interface TransformResourceResult { * A `ts.CompilerHost` interface which supports some number of optional methods in addition to the * core interface. */ -export interface ExtendedTsCompilerHost extends ts.CompilerHost, Partial, - Partial {} +export interface ExtendedTsCompilerHost + extends ts.CompilerHost, + Partial, + Partial {} diff --git a/packages/compiler-cli/src/ngtsc/core/api/src/options.ts b/packages/compiler-cli/src/ngtsc/core/api/src/options.ts index 2b4eb4c902fe0..c858135153c38 100644 --- a/packages/compiler-cli/src/ngtsc/core/api/src/options.ts +++ b/packages/compiler-cli/src/ngtsc/core/api/src/options.ts @@ -8,8 +8,15 @@ import ts from 'typescript'; -import {BazelAndG3Options, DiagnosticOptions, I18nOptions, LegacyNgcOptions, MiscOptions, StrictTemplateOptions, TargetOptions} from './public_options'; - +import { + BazelAndG3Options, + DiagnosticOptions, + I18nOptions, + LegacyNgcOptions, + MiscOptions, + StrictTemplateOptions, + TargetOptions, +} from './public_options'; /** * Non-public options which are useful during testing of the compiler. @@ -90,10 +97,17 @@ export interface InternalOptions { * * Also includes a few miscellaneous options. */ -export interface NgCompilerOptions extends ts.CompilerOptions, LegacyNgcOptions, BazelAndG3Options, - DiagnosticOptions, StrictTemplateOptions, - TestOnlyOptions, I18nOptions, TargetOptions, - InternalOptions, MiscOptions { +export interface NgCompilerOptions + extends ts.CompilerOptions, + LegacyNgcOptions, + BazelAndG3Options, + DiagnosticOptions, + StrictTemplateOptions, + TestOnlyOptions, + I18nOptions, + TargetOptions, + InternalOptions, + MiscOptions { // Replace the index signature type from `ts.CompilerOptions` as it is more strict than it needs // to be and would conflict with some types from the other interfaces. This is ok because Angular // compiler options are actually separate from TS compiler options in the `tsconfig.json` and we diff --git a/packages/compiler-cli/src/ngtsc/core/api/src/public_options.ts b/packages/compiler-cli/src/ngtsc/core/api/src/public_options.ts index 0fb266ced9935..acd94044a60f5 100644 --- a/packages/compiler-cli/src/ngtsc/core/api/src/public_options.ts +++ b/packages/compiler-cli/src/ngtsc/core/api/src/public_options.ts @@ -103,7 +103,6 @@ export interface StrictTemplateOptions { */ strictTemplates?: boolean; - /** * Whether to check the type of a binding to a directive/component input against the type of the * field on the directive/component. @@ -358,7 +357,6 @@ export interface I18nOptions { */ i18nOutFile?: string; - /** * Locale of the application (used when xi18n is requested). */ @@ -410,7 +408,7 @@ export interface TargetOptions { * * The default value is 'full'. */ - compilationMode?: 'full'|'partial'|'experimental-local'; + compilationMode?: 'full' | 'partial' | 'experimental-local'; } /** diff --git a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts index 0ba383a4c1488..46cfd7f3599db 100644 --- a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts +++ b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts @@ -9,30 +9,111 @@ import {R3Identifiers} from '@angular/compiler'; import ts from 'typescript'; -import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, NoopReferencesRegistry, PipeDecoratorHandler, ReferencesRegistry} from '../../annotations'; +import { + ComponentDecoratorHandler, + DirectiveDecoratorHandler, + InjectableDecoratorHandler, + NgModuleDecoratorHandler, + NoopReferencesRegistry, + PipeDecoratorHandler, + ReferencesRegistry, +} from '../../annotations'; import {InjectableClassRegistry} from '../../annotations/common'; import {CycleAnalyzer, CycleHandlingStrategy, ImportGraph} from '../../cycles'; -import {COMPILER_ERRORS_WITH_GUIDES, ERROR_DETAILS_PAGE_BASE_URL, ErrorCode, isFatalDiagnosticError, ngErrorCode} from '../../diagnostics'; +import { + COMPILER_ERRORS_WITH_GUIDES, + ERROR_DETAILS_PAGE_BASE_URL, + ErrorCode, + isFatalDiagnosticError, + ngErrorCode, +} from '../../diagnostics'; import {DocEntry, DocsExtractor} from '../../docs'; import {checkForPrivateExports, ReferenceGraph} from '../../entry_point'; -import {absoluteFromSourceFile, AbsoluteFsPath, LogicalFileSystem, resolve} from '../../file_system'; -import {AbsoluteModuleStrategy, AliasingHost, AliasStrategy, DefaultImportTracker, DeferredSymbolTracker, ImportedSymbolsTracker, ImportRewriter, LocalCompilationExtraImportsTracker, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, PrivateExportAliasingHost, R3SymbolsImportRewriter, Reference, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy, UnifiedModulesAliasingHost, UnifiedModulesStrategy} from '../../imports'; -import {IncrementalBuildStrategy, IncrementalCompilation, IncrementalState} from '../../incremental'; +import { + absoluteFromSourceFile, + AbsoluteFsPath, + LogicalFileSystem, + resolve, +} from '../../file_system'; +import { + AbsoluteModuleStrategy, + AliasingHost, + AliasStrategy, + DefaultImportTracker, + DeferredSymbolTracker, + ImportedSymbolsTracker, + ImportRewriter, + LocalCompilationExtraImportsTracker, + LocalIdentifierStrategy, + LogicalProjectStrategy, + ModuleResolver, + NoopImportRewriter, + PrivateExportAliasingHost, + R3SymbolsImportRewriter, + Reference, + ReferenceEmitStrategy, + ReferenceEmitter, + RelativePathStrategy, + UnifiedModulesAliasingHost, + UnifiedModulesStrategy, +} from '../../imports'; +import { + IncrementalBuildStrategy, + IncrementalCompilation, + IncrementalState, +} from '../../incremental'; import {SemanticSymbol} from '../../incremental/semantic_graph'; import {generateAnalysis, IndexedComponent, IndexingContext} from '../../indexer'; -import {ComponentResources, CompoundMetadataReader, CompoundMetadataRegistry, DirectiveMeta, DtsMetadataReader, ExportedProviderStatusResolver, HostDirectivesResolver, LocalMetadataRegistry, MetadataReader, MetadataReaderWithIndex, PipeMeta, ResourceRegistry} from '../../metadata'; +import { + ComponentResources, + CompoundMetadataReader, + CompoundMetadataRegistry, + DirectiveMeta, + DtsMetadataReader, + ExportedProviderStatusResolver, + HostDirectivesResolver, + LocalMetadataRegistry, + MetadataReader, + MetadataReaderWithIndex, + PipeMeta, + ResourceRegistry, +} from '../../metadata'; import {NgModuleIndexImpl} from '../../metadata/src/ng_module_index'; import {PartialEvaluator} from '../../partial_evaluator'; -import {ActivePerfRecorder, DelegatingPerfRecorder, PerfCheckpoint, PerfEvent, PerfPhase} from '../../perf'; +import { + ActivePerfRecorder, + DelegatingPerfRecorder, + PerfCheckpoint, + PerfEvent, + PerfPhase, +} from '../../perf'; import {FileUpdate, ProgramDriver, UpdateMode} from '../../program_driver'; import {DeclarationNode, isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection'; import {AdapterResourceLoader} from '../../resource'; -import {ComponentScopeReader, CompoundComponentScopeReader, LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver, TypeCheckScopeRegistry} from '../../scope'; +import { + ComponentScopeReader, + CompoundComponentScopeReader, + LocalModuleScopeRegistry, + MetadataDtsModuleScopeResolver, + TypeCheckScopeRegistry, +} from '../../scope'; import {StandaloneComponentScopeReader} from '../../scope/src/standalone'; -import {aliasTransformFactory, CompilationMode, declarationTransformFactory, DecoratorHandler, DtsTransformRegistry, ivyTransformFactory, TraitCompiler} from '../../transform'; +import { + aliasTransformFactory, + CompilationMode, + declarationTransformFactory, + DecoratorHandler, + DtsTransformRegistry, + ivyTransformFactory, + TraitCompiler, +} from '../../transform'; import {TemplateTypeCheckerImpl} from '../../typecheck'; import {OptimizeFor, TemplateTypeChecker, TypeCheckingConfig} from '../../typecheck/api'; -import {ALL_DIAGNOSTIC_FACTORIES, ExtendedTemplateCheckerImpl, SUPPORTED_DIAGNOSTIC_NAMES} from '../../typecheck/extended'; +import { + ALL_DIAGNOSTIC_FACTORIES, + ExtendedTemplateCheckerImpl, + SUPPORTED_DIAGNOSTIC_NAMES, +} from '../../typecheck/extended'; import {ExtendedTemplateChecker} from '../../typecheck/extended/api'; import {TemplateSemanticsChecker} from '../../typecheck/template_semantics/api/api'; import {TemplateSemanticsCheckerImpl} from '../../typecheck/template_semantics/src/template_semantics_checker'; @@ -55,24 +136,22 @@ interface LazyCompilationState { metaReader: MetadataReader; scopeRegistry: LocalModuleScopeRegistry; typeCheckScopeRegistry: TypeCheckScopeRegistry; - exportReferenceGraph: ReferenceGraph|null; + exportReferenceGraph: ReferenceGraph | null; dtsTransforms: DtsTransformRegistry; - aliasingHost: AliasingHost|null; + aliasingHost: AliasingHost | null; refEmitter: ReferenceEmitter; templateTypeChecker: TemplateTypeChecker; resourceRegistry: ResourceRegistry; - extendedTemplateChecker: ExtendedTemplateChecker|null; - templateSemanticsChecker: TemplateSemanticsChecker|null; - sourceFileValidator: SourceFileValidator|null; + extendedTemplateChecker: ExtendedTemplateChecker | null; + templateSemanticsChecker: TemplateSemanticsChecker | null; + sourceFileValidator: SourceFileValidator | null; /** * Only available in local compilation mode when option `generateExtraImportsInLocalMode` is set. */ - localCompilationExtraImportsTracker: LocalCompilationExtraImportsTracker|null; + localCompilationExtraImportsTracker: LocalCompilationExtraImportsTracker | null; } - - /** * Discriminant type for a `CompilationTicket`. */ @@ -125,17 +204,23 @@ export interface IncrementalResourceCompilationTicket { * Angular compiler. They abstract the starting state of compilation and allow `NgCompiler` to be * managed independently of any incremental compilation lifecycle. */ -export type CompilationTicket = FreshCompilationTicket|IncrementalTypeScriptCompilationTicket| - IncrementalResourceCompilationTicket; +export type CompilationTicket = + | FreshCompilationTicket + | IncrementalTypeScriptCompilationTicket + | IncrementalResourceCompilationTicket; /** * Create a `CompilationTicket` for a brand new compilation, using no prior state. */ export function freshCompilationTicket( - tsProgram: ts.Program, options: NgCompilerOptions, - incrementalBuildStrategy: IncrementalBuildStrategy, programDriver: ProgramDriver, - perfRecorder: ActivePerfRecorder|null, enableTemplateTypeChecker: boolean, - usePoisonedData: boolean): CompilationTicket { + tsProgram: ts.Program, + options: NgCompilerOptions, + incrementalBuildStrategy: IncrementalBuildStrategy, + programDriver: ProgramDriver, + perfRecorder: ActivePerfRecorder | null, + enableTemplateTypeChecker: boolean, + usePoisonedData: boolean, +): CompilationTicket { return { kind: CompilationTicketKind.Fresh, tsProgram, @@ -153,18 +238,27 @@ export function freshCompilationTicket( * instance and a new `ts.Program`. */ export function incrementalFromCompilerTicket( - oldCompiler: NgCompiler, newProgram: ts.Program, - incrementalBuildStrategy: IncrementalBuildStrategy, programDriver: ProgramDriver, - modifiedResourceFiles: Set, - perfRecorder: ActivePerfRecorder|null): CompilationTicket { + oldCompiler: NgCompiler, + newProgram: ts.Program, + incrementalBuildStrategy: IncrementalBuildStrategy, + programDriver: ProgramDriver, + modifiedResourceFiles: Set, + perfRecorder: ActivePerfRecorder | null, +): CompilationTicket { const oldProgram = oldCompiler.getCurrentProgram(); const oldState = oldCompiler.incrementalStrategy.getIncrementalState(oldProgram); if (oldState === null) { // No incremental step is possible here, since no IncrementalState was found for the old // program. return freshCompilationTicket( - newProgram, oldCompiler.options, incrementalBuildStrategy, programDriver, perfRecorder, - oldCompiler.enableTemplateTypeChecker, oldCompiler.usePoisonedData); + newProgram, + oldCompiler.options, + incrementalBuildStrategy, + programDriver, + perfRecorder, + oldCompiler.enableTemplateTypeChecker, + oldCompiler.usePoisonedData, + ); } if (perfRecorder === null) { @@ -172,8 +266,13 @@ export function incrementalFromCompilerTicket( } const incrementalCompilation = IncrementalCompilation.incremental( - newProgram, versionMapFromProgram(newProgram, programDriver), oldProgram, oldState, - modifiedResourceFiles, perfRecorder); + newProgram, + versionMapFromProgram(newProgram, programDriver), + oldProgram, + oldState, + modifiedResourceFiles, + perfRecorder, + ); return { kind: CompilationTicketKind.IncrementalTypeScript, @@ -193,17 +292,28 @@ export function incrementalFromCompilerTicket( * state, along with a new `ts.Program`. */ export function incrementalFromStateTicket( - oldProgram: ts.Program, oldState: IncrementalState, newProgram: ts.Program, - options: NgCompilerOptions, incrementalBuildStrategy: IncrementalBuildStrategy, - programDriver: ProgramDriver, modifiedResourceFiles: Set, - perfRecorder: ActivePerfRecorder|null, enableTemplateTypeChecker: boolean, - usePoisonedData: boolean): CompilationTicket { + oldProgram: ts.Program, + oldState: IncrementalState, + newProgram: ts.Program, + options: NgCompilerOptions, + incrementalBuildStrategy: IncrementalBuildStrategy, + programDriver: ProgramDriver, + modifiedResourceFiles: Set, + perfRecorder: ActivePerfRecorder | null, + enableTemplateTypeChecker: boolean, + usePoisonedData: boolean, +): CompilationTicket { if (perfRecorder === null) { perfRecorder = ActivePerfRecorder.zeroedToNow(); } const incrementalCompilation = IncrementalCompilation.incremental( - newProgram, versionMapFromProgram(newProgram, programDriver), oldProgram, oldState, - modifiedResourceFiles, perfRecorder); + newProgram, + versionMapFromProgram(newProgram, programDriver), + oldProgram, + oldState, + modifiedResourceFiles, + perfRecorder, + ); return { kind: CompilationTicketKind.IncrementalTypeScript, newProgram, @@ -217,8 +327,10 @@ export function incrementalFromStateTicket( }; } -export function resourceChangeTicket(compiler: NgCompiler, modifiedResourceFiles: Set): - IncrementalResourceCompilationTicket { +export function resourceChangeTicket( + compiler: NgCompiler, + modifiedResourceFiles: Set, +): IncrementalResourceCompilationTicket { return { kind: CompilationTicketKind.IncrementalResource, compiler, @@ -227,7 +339,6 @@ export function resourceChangeTicket(compiler: NgCompiler, modifiedResourceFiles }; } - /** * The heart of the Angular Ivy compiler. * @@ -246,7 +357,7 @@ export class NgCompiler { * * This is created on demand by calling `ensureAnalyzed`. */ - private compilation: LazyCompilationState|null = null; + private compilation: LazyCompilationState | null = null; /** * Any diagnostics related to the construction of the compilation. @@ -261,11 +372,11 @@ export class NgCompiler { * * This is set by (and memoizes) `getNonTemplateDiagnostics`. */ - private nonTemplateDiagnostics: ts.Diagnostic[]|null = null; + private nonTemplateDiagnostics: ts.Diagnostic[] | null = null; private closureCompilerEnabled: boolean; private currentProgram: ts.Program; - private entryPoint: ts.SourceFile|null; + private entryPoint: ts.SourceFile | null; private moduleResolver: ModuleResolver; private resourceManager: AdapterResourceLoader; private cycleAnalyzer: CycleAnalyzer; @@ -273,7 +384,7 @@ export class NgCompiler { readonly ignoreForEmit: Set; readonly enableTemplateTypeChecker: boolean; private readonly enableBlockSyntax: boolean; - private readonly angularCoreVersion: string|null; + private readonly angularCoreVersion: string | null; /** * `NgCompiler` can be reused for multiple compilations (for resource-only changes), and each @@ -295,28 +406,30 @@ export class NgCompiler { switch (ticket.kind) { case CompilationTicketKind.Fresh: return new NgCompiler( - adapter, - ticket.options, + adapter, + ticket.options, + ticket.tsProgram, + ticket.programDriver, + ticket.incrementalBuildStrategy, + IncrementalCompilation.fresh( ticket.tsProgram, - ticket.programDriver, - ticket.incrementalBuildStrategy, - IncrementalCompilation.fresh( - ticket.tsProgram, versionMapFromProgram(ticket.tsProgram, ticket.programDriver)), - ticket.enableTemplateTypeChecker, - ticket.usePoisonedData, - ticket.perfRecorder, + versionMapFromProgram(ticket.tsProgram, ticket.programDriver), + ), + ticket.enableTemplateTypeChecker, + ticket.usePoisonedData, + ticket.perfRecorder, ); case CompilationTicketKind.IncrementalTypeScript: return new NgCompiler( - adapter, - ticket.options, - ticket.newProgram, - ticket.programDriver, - ticket.incrementalBuildStrategy, - ticket.incrementalCompilation, - ticket.enableTemplateTypeChecker, - ticket.usePoisonedData, - ticket.perfRecorder, + adapter, + ticket.options, + ticket.newProgram, + ticket.programDriver, + ticket.incrementalBuildStrategy, + ticket.incrementalCompilation, + ticket.enableTemplateTypeChecker, + ticket.usePoisonedData, + ticket.perfRecorder, ); case CompilationTicketKind.IncrementalResource: const compiler = ticket.compiler; @@ -326,47 +439,56 @@ export class NgCompiler { } private constructor( - private adapter: NgCompilerAdapter, - readonly options: NgCompilerOptions, - private inputProgram: ts.Program, - readonly programDriver: ProgramDriver, - readonly incrementalStrategy: IncrementalBuildStrategy, - readonly incrementalCompilation: IncrementalCompilation, - enableTemplateTypeChecker: boolean, - readonly usePoisonedData: boolean, - private livePerfRecorder: ActivePerfRecorder, + private adapter: NgCompilerAdapter, + readonly options: NgCompilerOptions, + private inputProgram: ts.Program, + readonly programDriver: ProgramDriver, + readonly incrementalStrategy: IncrementalBuildStrategy, + readonly incrementalCompilation: IncrementalCompilation, + enableTemplateTypeChecker: boolean, + readonly usePoisonedData: boolean, + private livePerfRecorder: ActivePerfRecorder, ) { this.delegatingPerfRecorder = new DelegatingPerfRecorder(this.perfRecorder); this.enableTemplateTypeChecker = - enableTemplateTypeChecker || (options['_enableTemplateTypeChecker'] ?? false); + enableTemplateTypeChecker || (options['_enableTemplateTypeChecker'] ?? false); // TODO(crisbeto): remove this flag and base `enableBlockSyntax` on the `angularCoreVersion`. this.enableBlockSyntax = options['_enableBlockSyntax'] ?? true; this.angularCoreVersion = options['_angularCoreVersion'] ?? null; this.constructionDiagnostics.push( - ...this.adapter.constructionDiagnostics, ...verifyCompatibleTypeCheckOptions(this.options)); + ...this.adapter.constructionDiagnostics, + ...verifyCompatibleTypeCheckOptions(this.options), + ); this.currentProgram = inputProgram; this.closureCompilerEnabled = !!this.options.annotateForClosureCompiler; this.entryPoint = - adapter.entryPoint !== null ? getSourceFileOrNull(inputProgram, adapter.entryPoint) : null; + adapter.entryPoint !== null ? getSourceFileOrNull(inputProgram, adapter.entryPoint) : null; const moduleResolutionCache = ts.createModuleResolutionCache( - this.adapter.getCurrentDirectory(), - // doen't retain a reference to `this`, if other closures in the constructor here reference - // `this` internally then a closure created here would retain them. This can cause major - // memory leak issues since the `moduleResolutionCache` is a long-lived object and finds its - // way into all kinds of places inside TS internal objects. - this.adapter.getCanonicalFileName.bind(this.adapter)); - this.moduleResolver = - new ModuleResolver(inputProgram, this.options, this.adapter, moduleResolutionCache); + this.adapter.getCurrentDirectory(), + // doen't retain a reference to `this`, if other closures in the constructor here reference + // `this` internally then a closure created here would retain them. This can cause major + // memory leak issues since the `moduleResolutionCache` is a long-lived object and finds its + // way into all kinds of places inside TS internal objects. + this.adapter.getCanonicalFileName.bind(this.adapter), + ); + this.moduleResolver = new ModuleResolver( + inputProgram, + this.options, + this.adapter, + moduleResolutionCache, + ); this.resourceManager = new AdapterResourceLoader(adapter, this.options); this.cycleAnalyzer = new CycleAnalyzer( - new ImportGraph(inputProgram.getTypeChecker(), this.delegatingPerfRecorder)); + new ImportGraph(inputProgram.getTypeChecker(), this.delegatingPerfRecorder), + ); this.incrementalStrategy.setIncrementalState(this.incrementalCompilation.state, inputProgram); - this.ignoreForDiagnostics = - new Set(inputProgram.getSourceFiles().filter(sf => this.adapter.isShim(sf))); + this.ignoreForDiagnostics = new Set( + inputProgram.getSourceFiles().filter((sf) => this.adapter.isShim(sf)), + ); this.ignoreForEmit = this.adapter.ignoreForEmit; let dtsFileCount = 0; @@ -388,7 +510,9 @@ export class NgCompiler { } private updateWithChangedResources( - changedResources: Set, perfRecorder: ActivePerfRecorder): void { + changedResources: Set, + perfRecorder: ActivePerfRecorder, + ): void { this.livePerfRecorder = perfRecorder; this.delegatingPerfRecorder.target = perfRecorder; @@ -438,9 +562,7 @@ export class NgCompiler { * Get all Angular-related diagnostics for this compilation. */ getDiagnostics(): ts.Diagnostic[] { - const diagnostics: ts.Diagnostic[] = [ - ...this.getNonTemplateDiagnostics(), - ]; + const diagnostics: ts.Diagnostic[] = [...this.getNonTemplateDiagnostics()]; // Type check code may throw fatal diagnostic errors if e.g. the type check // block cannot be generated. Gracefully return the associated diagnostic. @@ -465,8 +587,9 @@ export class NgCompiler { * If a `ts.SourceFile` is passed, only diagnostics related to that file are returned. */ getDiagnosticsForFile(file: ts.SourceFile, optimizeFor: OptimizeFor): ts.Diagnostic[] { - const diagnostics: ts.Diagnostic[] = - [...this.getNonTemplateDiagnostics().filter(diag => diag.file === file)]; + const diagnostics: ts.Diagnostic[] = [ + ...this.getNonTemplateDiagnostics().filter((diag) => diag.file === file), + ]; // Type check code may throw fatal diagnostic errors if e.g. the type check // block cannot be generated. Gracefully return the associated diagnostic. @@ -475,8 +598,9 @@ export class NgCompiler { // generate the same TCB. try { diagnostics.push( - ...this.getTemplateDiagnosticsForFile(file, optimizeFor), - ...this.runAdditionalChecks(file)); + ...this.getTemplateDiagnosticsForFile(file, optimizeFor), + ...this.runAdditionalChecks(file), + ); } catch (err: unknown) { if (!isFatalDiagnosticError(err)) { throw err; @@ -524,12 +648,13 @@ export class NgCompiler { * Add Angular.io error guide links to diagnostics for this compilation. */ private addMessageTextDetails(diagnostics: ts.Diagnostic[]): ts.Diagnostic[] { - return diagnostics.map(diag => { + return diagnostics.map((diag) => { if (diag.code && COMPILER_ERRORS_WITH_GUIDES.has(ngErrorCode(diag.code))) { return { ...diag, - messageText: diag.messageText + - `. Find more at ${ERROR_DETAILS_PAGE_BASE_URL}/NG${ngErrorCode(diag.code)}` + messageText: + diag.messageText + + `. Find more at ${ERROR_DETAILS_PAGE_BASE_URL}/NG${ngErrorCode(diag.code)}`, }; } return diag; @@ -565,7 +690,8 @@ export class NgCompiler { getTemplateTypeChecker(): TemplateTypeChecker { if (!this.enableTemplateTypeChecker) { throw new Error( - 'The `TemplateTypeChecker` does not work without `enableTemplateTypeChecker`.'); + 'The `TemplateTypeChecker` does not work without `enableTemplateTypeChecker`.', + ); } return this.ensureAnalyzed().templateTypeChecker; } @@ -589,7 +715,7 @@ export class NgCompiler { /** * Retrieves external resources for the given component. */ - getComponentResources(classDecl: DeclarationNode): ComponentResources|null { + getComponentResources(classDecl: DeclarationNode): ComponentResources | null { if (!isNamedClassDeclaration(classDecl)) { return null; } @@ -603,7 +729,7 @@ export class NgCompiler { return {styles, template}; } - getMeta(classDecl: DeclarationNode): PipeMeta|DirectiveMeta|null { + getMeta(classDecl: DeclarationNode): PipeMeta | DirectiveMeta | null { if (!isNamedClassDeclaration(classDecl)) { return null; } @@ -657,7 +783,7 @@ export class NgCompiler { * program with Angular-added definitions. */ prepareEmit(): { - transformers: ts.CustomTransformers, + transformers: ts.CustomTransformers; } { const compilation = this.ensureAnalyzed(); @@ -673,9 +799,15 @@ export class NgCompiler { const before = [ ivyTransformFactory( - compilation.traitCompiler, compilation.reflector, importRewriter, defaultImportTracker, - compilation.localCompilationExtraImportsTracker, this.delegatingPerfRecorder, - compilation.isCore, this.closureCompilerEnabled), + compilation.traitCompiler, + compilation.reflector, + importRewriter, + defaultImportTracker, + compilation.localCompilationExtraImportsTracker, + this.delegatingPerfRecorder, + compilation.isCore, + this.closureCompilerEnabled, + ), aliasTransformFactory(compilation.traitCompiler.exportStatements), defaultImportTracker.importPreservingTransformer(), ]; @@ -684,11 +816,18 @@ export class NgCompiler { // In local compilation mode we don't make use of .d.ts files for Angular compilation, so their // transformation can be ditched. - if (this.options.compilationMode !== 'experimental-local' && - compilation.dtsTransforms !== null) { - afterDeclarations.push(declarationTransformFactory( - compilation.dtsTransforms, compilation.reflector, compilation.refEmitter, - importRewriter)); + if ( + this.options.compilationMode !== 'experimental-local' && + compilation.dtsTransforms !== null + ) { + afterDeclarations.push( + declarationTransformFactory( + compilation.dtsTransforms, + compilation.reflector, + compilation.refEmitter, + importRewriter, + ), + ); } // Only add aliasing re-exports to the .d.ts output if the `AliasingHost` requests it. @@ -724,7 +863,7 @@ export class NgCompiler { const checker = this.inputProgram.getTypeChecker(); const docsExtractor = new DocsExtractor(checker, compilation.metaReader); - const entryPointSourceFile = this.inputProgram.getSourceFiles().find(sourceFile => { + const entryPointSourceFile = this.inputProgram.getSourceFiles().find((sourceFile) => { // TODO: this will need to be more specific than `.includes`, but the exact path comparison // will be easier to figure out when the pipeline is running end-to-end. return sourceFile.fileName.includes(entryPoint); @@ -809,9 +948,9 @@ export class NgCompiler { // to type check signals in two-way bindings. We also allow version 0.0.0 in case somebody is // using Angular at head. let allowSignalsInTwoWayBindings = - coreHasSymbol(this.inputProgram, R3Identifiers.unwrapWritableSignal) ?? - (this.angularCoreVersion === null || - coreVersionSupportsFeature(this.angularCoreVersion, '>= 17.2.0-0')); + coreHasSymbol(this.inputProgram, R3Identifiers.unwrapWritableSignal) ?? + (this.angularCoreVersion === null || + coreVersionSupportsFeature(this.angularCoreVersion, '>= 17.2.0-0')); // First select a type-checking configuration, based on whether full template type-checking is // requested. @@ -851,7 +990,7 @@ export class NgCompiler { // mode, the user is in full control of type inference. suggestionsForSuboptimalTypeInference: this.enableTemplateTypeChecker && !strictTemplates, controlFlowPreventingContentProjection: - this.options.extendedDiagnostics?.defaultCategory || DiagnosticCategoryLabel.Warning, + this.options.extendedDiagnostics?.defaultCategory || DiagnosticCategoryLabel.Warning, allowSignalsInTwoWayBindings, }; } else { @@ -883,7 +1022,7 @@ export class NgCompiler { // not checked anyways. suggestionsForSuboptimalTypeInference: false, controlFlowPreventingContentProjection: - this.options.extendedDiagnostics?.defaultCategory || DiagnosticCategoryLabel.Warning, + this.options.extendedDiagnostics?.defaultCategory || DiagnosticCategoryLabel.Warning, allowSignalsInTwoWayBindings, }; } @@ -896,7 +1035,7 @@ export class NgCompiler { } if (this.options.strictInputAccessModifiers !== undefined) { typeCheckingConfig.honorAccessModifiersForInputBindings = - this.options.strictInputAccessModifiers; + this.options.strictInputAccessModifiers; } if (this.options.strictNullInputTypes !== undefined) { typeCheckingConfig.strictNullInputBindings = this.options.strictNullInputTypes; @@ -923,10 +1062,11 @@ export class NgCompiler { if (this.options.strictLiteralTypes !== undefined) { typeCheckingConfig.strictLiteralTypes = this.options.strictLiteralTypes; } - if (this.options.extendedDiagnostics?.checks?.controlFlowPreventingContentProjection !== - undefined) { + if ( + this.options.extendedDiagnostics?.checks?.controlFlowPreventingContentProjection !== undefined + ) { typeCheckingConfig.controlFlowPreventingContentProjection = - this.options.extendedDiagnostics.checks.controlFlowPreventingContentProjection; + this.options.extendedDiagnostics.checks.controlFlowPreventingContentProjection; } return typeCheckingConfig; @@ -943,7 +1083,8 @@ export class NgCompiler { } diagnostics.push( - ...compilation.templateTypeChecker.getDiagnosticsForFile(sf, OptimizeFor.WholeProgram)); + ...compilation.templateTypeChecker.getDiagnosticsForFile(sf, OptimizeFor.WholeProgram), + ); } const program = this.programDriver.getProgram(); @@ -953,8 +1094,10 @@ export class NgCompiler { return diagnostics; } - private getTemplateDiagnosticsForFile(sf: ts.SourceFile, optimizeFor: OptimizeFor): - ReadonlyArray { + private getTemplateDiagnosticsForFile( + sf: ts.SourceFile, + optimizeFor: OptimizeFor, + ): ReadonlyArray { const compilation = this.ensureAnalyzed(); // Get the diagnostics. @@ -975,8 +1118,13 @@ export class NgCompiler { const compilation = this.ensureAnalyzed(); this.nonTemplateDiagnostics = [...compilation.traitCompiler.diagnostics]; if (this.entryPoint !== null && compilation.exportReferenceGraph !== null) { - this.nonTemplateDiagnostics.push(...checkForPrivateExports( - this.entryPoint, this.inputProgram.getTypeChecker(), compilation.exportReferenceGraph)); + this.nonTemplateDiagnostics.push( + ...checkForPrivateExports( + this.entryPoint, + this.inputProgram.getTypeChecker(), + compilation.exportReferenceGraph, + ), + ); } } return this.nonTemplateDiagnostics; @@ -997,14 +1145,18 @@ export class NgCompiler { } if (templateSemanticsChecker !== null) { - diagnostics.push(...compilation.traitCompiler.runAdditionalChecks(sf, (clazz, handler) => { - return handler.templateSemanticsCheck?.(clazz, templateSemanticsChecker) || null; - })); + diagnostics.push( + ...compilation.traitCompiler.runAdditionalChecks(sf, (clazz, handler) => { + return handler.templateSemanticsCheck?.(clazz, templateSemanticsChecker) || null; + }), + ); } if (this.options.strictTemplates && extendedTemplateChecker !== null) { - diagnostics.push(...compilation.traitCompiler.runAdditionalChecks(sf, (clazz, handler) => { - return handler.extendedTemplateCheck?.(clazz, extendedTemplateChecker) || null; - })); + diagnostics.push( + ...compilation.traitCompiler.runAdditionalChecks(sf, (clazz, handler) => { + return handler.extendedTemplateCheck?.(clazz, extendedTemplateChecker) || null; + }), + ); } } @@ -1034,15 +1186,19 @@ export class NgCompiler { const checker = this.inputProgram.getTypeChecker(); - const reflector = - new TypeScriptReflectionHost(checker, compilationMode === CompilationMode.LOCAL); + const reflector = new TypeScriptReflectionHost( + checker, + compilationMode === CompilationMode.LOCAL, + ); // Construct the ReferenceEmitter. let refEmitter: ReferenceEmitter; - let aliasingHost: AliasingHost|null = null; - if (this.adapter.unifiedModulesHost === null || - (!this.options['_useHostForImportGeneration'] && - !this.options['_useHostForImportAndAliasGeneration'])) { + let aliasingHost: AliasingHost | null = null; + if ( + this.adapter.unifiedModulesHost === null || + (!this.options['_useHostForImportGeneration'] && + !this.options['_useHostForImportAndAliasGeneration']) + ) { let localImportStrategy: ReferenceEmitStrategy; // The strategy used for local, in-project imports depends on whether TS has been configured @@ -1050,12 +1206,16 @@ export class NgCompiler { // namespace" and the logic of `LogicalProjectStrategy` is required to generate correct // imports which may cross these multiple directories. Otherwise, plain relative imports are // sufficient. - if (this.options.rootDir !== undefined || - (this.options.rootDirs !== undefined && this.options.rootDirs.length > 0)) { + if ( + this.options.rootDir !== undefined || + (this.options.rootDirs !== undefined && this.options.rootDirs.length > 0) + ) { // rootDirs logic is in effect - use the `LogicalProjectStrategy` for in-project relative // imports. localImportStrategy = new LogicalProjectStrategy( - reflector, new LogicalFileSystem([...this.adapter.rootDirs], this.adapter)); + reflector, + new LogicalFileSystem([...this.adapter.rootDirs], this.adapter), + ); } else { // Plain relative imports are all that's needed. localImportStrategy = new RelativePathStrategy(reflector); @@ -1098,8 +1258,11 @@ export class NgCompiler { } } - const evaluator = - new PartialEvaluator(reflector, checker, this.incrementalCompilation.depGraph); + const evaluator = new PartialEvaluator( + reflector, + checker, + this.incrementalCompilation.depGraph, + ); const dtsReader = new DtsMetadataReader(checker, reflector); const localMetaRegistry = new LocalMetadataRegistry(); const localMetaReader: MetadataReaderWithIndex = localMetaRegistry; @@ -1107,11 +1270,21 @@ export class NgCompiler { const metaReader = new CompoundMetadataReader([localMetaReader, dtsReader]); const ngModuleIndex = new NgModuleIndexImpl(metaReader, localMetaReader); const ngModuleScopeRegistry = new LocalModuleScopeRegistry( - localMetaReader, metaReader, depScopeReader, refEmitter, aliasingHost); - const standaloneScopeReader = - new StandaloneComponentScopeReader(metaReader, ngModuleScopeRegistry, depScopeReader); - const scopeReader: ComponentScopeReader = - new CompoundComponentScopeReader([ngModuleScopeRegistry, standaloneScopeReader]); + localMetaReader, + metaReader, + depScopeReader, + refEmitter, + aliasingHost, + ); + const standaloneScopeReader = new StandaloneComponentScopeReader( + metaReader, + ngModuleScopeRegistry, + depScopeReader, + ); + const scopeReader: ComponentScopeReader = new CompoundComponentScopeReader([ + ngModuleScopeRegistry, + standaloneScopeReader, + ]); const semanticDepGraphUpdater = this.incrementalCompilation.semanticDepGraphUpdater; const metaRegistry = new CompoundMetadataRegistry([localMetaRegistry, ngModuleScopeRegistry]); const injectableRegistry = new InjectableClassRegistry(reflector, isCore); @@ -1119,15 +1292,17 @@ export class NgCompiler { const exportedProviderStatusResolver = new ExportedProviderStatusResolver(metaReader); const importTracker = new ImportedSymbolsTracker(); - const typeCheckScopeRegistry = - new TypeCheckScopeRegistry(scopeReader, metaReader, hostDirectivesResolver); - + const typeCheckScopeRegistry = new TypeCheckScopeRegistry( + scopeReader, + metaReader, + hostDirectivesResolver, + ); // If a flat module entrypoint was specified, then track references via a `ReferenceGraph` in // order to produce proper diagnostics for incorrectly exported directives/pipes/etc. If there // is no flat module entrypoint then don't pay the cost of tracking references. let referencesRegistry: ReferencesRegistry; - let exportReferenceGraph: ReferenceGraph|null = null; + let exportReferenceGraph: ReferenceGraph | null = null; if (this.entryPoint !== null) { exportReferenceGraph = new ReferenceGraph(); referencesRegistry = new ReferenceGraphAdapter(exportReferenceGraph); @@ -1140,10 +1315,11 @@ export class NgCompiler { const resourceRegistry = new ResourceRegistry(); const deferredSymbolsTracker = new DeferredSymbolTracker( - this.inputProgram.getTypeChecker(), - this.options.onlyExplicitDeferDependencyImports ?? false); + this.inputProgram.getTypeChecker(), + this.options.onlyExplicitDeferDependencyImports ?? false, + ); - let localCompilationExtraImportsTracker: LocalCompilationExtraImportsTracker|null = null; + let localCompilationExtraImportsTracker: LocalCompilationExtraImportsTracker | null = null; if (compilationMode === CompilationMode.LOCAL && this.options.generateExtraImportsInLocalMode) { localCompilationExtraImportsTracker = new LocalCompilationExtraImportsTracker(checker); } @@ -1151,9 +1327,10 @@ export class NgCompiler { // Cycles are handled in full and local compilation modes by "remote scoping". // "Remote scoping" does not work well with tree shaking for libraries. // So in partial compilation mode, when building a library, a cycle will cause an error. - const cycleHandlingStrategy = compilationMode === CompilationMode.PARTIAL ? - CycleHandlingStrategy.Error : - CycleHandlingStrategy.UseRemoteScoping; + const cycleHandlingStrategy = + compilationMode === CompilationMode.PARTIAL + ? CycleHandlingStrategy.Error + : CycleHandlingStrategy.UseRemoteScoping; const strictCtorDeps = this.options.strictInjectionParameters || false; const supportJitMode = this.options['supportJitMode'] ?? true; @@ -1164,11 +1341,13 @@ export class NgCompiler { // prevent potential downstream application testing breakage. if (supportTestBed === false && compilationMode === CompilationMode.PARTIAL) { throw new Error( - 'TestBed support ("supportTestBed" option) cannot be disabled in partial compilation mode.'); + 'TestBed support ("supportTestBed" option) cannot be disabled in partial compilation mode.', + ); } if (supportJitMode === false && compilationMode === CompilationMode.PARTIAL) { throw new Error( - 'JIT mode support ("supportJitMode" option) cannot be disabled in partial compilation mode.'); + 'JIT mode support ("supportJitMode" option) cannot be disabled in partial compilation mode.', + ); } // Currently forbidOrphanComponents depends on the code generated behind ngJitMode flag. Until @@ -1176,86 +1355,179 @@ export class NgCompiler { // order for forbidOrphanComponents to be able to work properly. if (supportJitMode === false && this.options.forbidOrphanComponents) { throw new Error( - 'JIT mode support ("supportJitMode" option) cannot be disabled when forbidOrphanComponents is set to true'); + 'JIT mode support ("supportJitMode" option) cannot be disabled when forbidOrphanComponents is set to true', + ); } // Set up the IvyCompilation, which manages state for the Ivy transformer. - const handlers: DecoratorHandler[] = [ + const handlers: DecoratorHandler[] = [ new ComponentDecoratorHandler( - reflector, evaluator, metaRegistry, metaReader, scopeReader, depScopeReader, - ngModuleScopeRegistry, typeCheckScopeRegistry, resourceRegistry, isCore, strictCtorDeps, - this.resourceManager, this.adapter.rootDirs, this.options.preserveWhitespaces || false, - this.options.i18nUseExternalIds !== false, - this.options.enableI18nLegacyMessageIdFormat !== false, this.usePoisonedData, - this.options.i18nNormalizeLineEndingsInICUs === true, this.moduleResolver, - this.cycleAnalyzer, cycleHandlingStrategy, refEmitter, referencesRegistry, - this.incrementalCompilation.depGraph, injectableRegistry, semanticDepGraphUpdater, - this.closureCompilerEnabled, this.delegatingPerfRecorder, hostDirectivesResolver, - importTracker, supportTestBed, compilationMode, deferredSymbolsTracker, - !!this.options.forbidOrphanComponents, this.enableBlockSyntax, - localCompilationExtraImportsTracker), + reflector, + evaluator, + metaRegistry, + metaReader, + scopeReader, + depScopeReader, + ngModuleScopeRegistry, + typeCheckScopeRegistry, + resourceRegistry, + isCore, + strictCtorDeps, + this.resourceManager, + this.adapter.rootDirs, + this.options.preserveWhitespaces || false, + this.options.i18nUseExternalIds !== false, + this.options.enableI18nLegacyMessageIdFormat !== false, + this.usePoisonedData, + this.options.i18nNormalizeLineEndingsInICUs === true, + this.moduleResolver, + this.cycleAnalyzer, + cycleHandlingStrategy, + refEmitter, + referencesRegistry, + this.incrementalCompilation.depGraph, + injectableRegistry, + semanticDepGraphUpdater, + this.closureCompilerEnabled, + this.delegatingPerfRecorder, + hostDirectivesResolver, + importTracker, + supportTestBed, + compilationMode, + deferredSymbolsTracker, + !!this.options.forbidOrphanComponents, + this.enableBlockSyntax, + localCompilationExtraImportsTracker, + ), // TODO(alxhub): understand why the cast here is necessary (something to do with `null` // not being assignable to `unknown` when wrapped in `Readonly`). // clang-format off - new DirectiveDecoratorHandler( - reflector, evaluator, metaRegistry, ngModuleScopeRegistry, metaReader, - injectableRegistry, refEmitter, referencesRegistry, isCore, strictCtorDeps, semanticDepGraphUpdater, - this.closureCompilerEnabled, - this.delegatingPerfRecorder, - importTracker, - supportTestBed, compilationMode, - !!this.options.generateExtraImportsInLocalMode, - ) as Readonly>, + new DirectiveDecoratorHandler( + reflector, + evaluator, + metaRegistry, + ngModuleScopeRegistry, + metaReader, + injectableRegistry, + refEmitter, + referencesRegistry, + isCore, + strictCtorDeps, + semanticDepGraphUpdater, + this.closureCompilerEnabled, + this.delegatingPerfRecorder, + importTracker, + supportTestBed, + compilationMode, + !!this.options.generateExtraImportsInLocalMode, + ) as Readonly>, // clang-format on // Pipe handler must be before injectable handler in list so pipe factories are printed // before injectable factories (so injectable factories can delegate to them) new PipeDecoratorHandler( - reflector, evaluator, metaRegistry, ngModuleScopeRegistry, injectableRegistry, isCore, - this.delegatingPerfRecorder, supportTestBed, compilationMode, - !!this.options.generateExtraImportsInLocalMode), + reflector, + evaluator, + metaRegistry, + ngModuleScopeRegistry, + injectableRegistry, + isCore, + this.delegatingPerfRecorder, + supportTestBed, + compilationMode, + !!this.options.generateExtraImportsInLocalMode, + ), new InjectableDecoratorHandler( - reflector, evaluator, isCore, strictCtorDeps, injectableRegistry, - this.delegatingPerfRecorder, supportTestBed, compilationMode), + reflector, + evaluator, + isCore, + strictCtorDeps, + injectableRegistry, + this.delegatingPerfRecorder, + supportTestBed, + compilationMode, + ), new NgModuleDecoratorHandler( - reflector, evaluator, metaReader, metaRegistry, ngModuleScopeRegistry, referencesRegistry, - exportedProviderStatusResolver, semanticDepGraphUpdater, isCore, refEmitter, - this.closureCompilerEnabled, this.options.onlyPublishPublicTypingsForNgModules ?? false, - injectableRegistry, this.delegatingPerfRecorder, supportTestBed, supportJitMode, - compilationMode, localCompilationExtraImportsTracker), + reflector, + evaluator, + metaReader, + metaRegistry, + ngModuleScopeRegistry, + referencesRegistry, + exportedProviderStatusResolver, + semanticDepGraphUpdater, + isCore, + refEmitter, + this.closureCompilerEnabled, + this.options.onlyPublishPublicTypingsForNgModules ?? false, + injectableRegistry, + this.delegatingPerfRecorder, + supportTestBed, + supportJitMode, + compilationMode, + localCompilationExtraImportsTracker, + ), ]; const traitCompiler = new TraitCompiler( - handlers, reflector, this.delegatingPerfRecorder, this.incrementalCompilation, - this.options.compileNonExportedClasses !== false, compilationMode, dtsTransforms, - semanticDepGraphUpdater, this.adapter); + handlers, + reflector, + this.delegatingPerfRecorder, + this.incrementalCompilation, + this.options.compileNonExportedClasses !== false, + compilationMode, + dtsTransforms, + semanticDepGraphUpdater, + this.adapter, + ); // Template type-checking may use the `ProgramDriver` to produce new `ts.Program`(s). If this // happens, they need to be tracked by the `NgCompiler`. - const notifyingDriver = - new NotifyingProgramDriverWrapper(this.programDriver, (program: ts.Program) => { - this.incrementalStrategy.setIncrementalState(this.incrementalCompilation.state, program); - this.currentProgram = program; - }); + const notifyingDriver = new NotifyingProgramDriverWrapper( + this.programDriver, + (program: ts.Program) => { + this.incrementalStrategy.setIncrementalState(this.incrementalCompilation.state, program); + this.currentProgram = program; + }, + ); const templateTypeChecker = new TemplateTypeCheckerImpl( - this.inputProgram, notifyingDriver, traitCompiler, this.getTypeCheckingConfig(), refEmitter, - reflector, this.adapter, this.incrementalCompilation, metaReader, localMetaReader, - ngModuleIndex, scopeReader, typeCheckScopeRegistry, this.delegatingPerfRecorder); + this.inputProgram, + notifyingDriver, + traitCompiler, + this.getTypeCheckingConfig(), + refEmitter, + reflector, + this.adapter, + this.incrementalCompilation, + metaReader, + localMetaReader, + ngModuleIndex, + scopeReader, + typeCheckScopeRegistry, + this.delegatingPerfRecorder, + ); // Only construct the extended template checker if the configuration is valid and usable. - const extendedTemplateChecker = this.constructionDiagnostics.length === 0 ? - new ExtendedTemplateCheckerImpl( - templateTypeChecker, checker, ALL_DIAGNOSTIC_FACTORIES, this.options) : - null; - - const templateSemanticsChecker = this.constructionDiagnostics.length === 0 ? - new TemplateSemanticsCheckerImpl(templateTypeChecker) : - null; - - const sourceFileValidator = this.constructionDiagnostics.length === 0 ? - new SourceFileValidator(reflector, importTracker) : - null; + const extendedTemplateChecker = + this.constructionDiagnostics.length === 0 + ? new ExtendedTemplateCheckerImpl( + templateTypeChecker, + checker, + ALL_DIAGNOSTIC_FACTORIES, + this.options, + ) + : null; + + const templateSemanticsChecker = + this.constructionDiagnostics.length === 0 + ? new TemplateSemanticsCheckerImpl(templateTypeChecker) + : null; + + const sourceFileValidator = + this.constructionDiagnostics.length === 0 + ? new SourceFileValidator(reflector, importTracker) + : null; return { isCore, @@ -1289,19 +1561,21 @@ export function isAngularCorePackage(program: ts.Program): boolean { } // Look for the constant ITS_JUST_ANGULAR in that file. - return r3Symbols.statements.some(stmt => { + return r3Symbols.statements.some((stmt) => { // The statement must be a variable declaration statement. if (!ts.isVariableStatement(stmt)) { return false; } // It must be exported. const modifiers = ts.getModifiers(stmt); - if (modifiers === undefined || - !modifiers.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword)) { + if ( + modifiers === undefined || + !modifiers.some((mod) => mod.kind === ts.SyntaxKind.ExportKeyword) + ) { return false; } // It must declare ITS_JUST_ANGULAR. - return stmt.declarationList.declarations.some(decl => { + return stmt.declarationList.declarations.some((decl) => { // The declaration must match the name. if (!ts.isIdentifier(decl.name) || decl.name.text !== 'ITS_JUST_ANGULAR') { return false; @@ -1319,8 +1593,10 @@ export function isAngularCorePackage(program: ts.Program): boolean { /** * Find the 'r3_symbols.ts' file in the given `Program`, or return `null` if it wasn't there. */ -function getR3SymbolsFile(program: ts.Program): ts.SourceFile|null { - return program.getSourceFiles().find(file => file.fileName.indexOf('r3_symbols.ts') >= 0) || null; +function getR3SymbolsFile(program: ts.Program): ts.SourceFile | null { + return ( + program.getSourceFiles().find((file) => file.fileName.indexOf('r3_symbols.ts') >= 0) || null + ); } /** @@ -1328,9 +1604,9 @@ function getR3SymbolsFile(program: ts.Program): ts.SourceFile|null { * "fullTemplateTypeCheck", it is required that the latter is not explicitly disabled if the * former is enabled. */ -function* - verifyCompatibleTypeCheckOptions(options: NgCompilerOptions): - Generator { +function* verifyCompatibleTypeCheckOptions( + options: NgCompilerOptions, +): Generator { if (options.fullTemplateTypeCheck === false && options.strictTemplates === true) { yield makeConfigDiagnostic({ category: ts.DiagnosticCategory.Error, @@ -1374,8 +1650,7 @@ One of the following actions is required: category: ts.DiagnosticCategory.Error, code: ErrorCode.CONFIG_EXTENDED_DIAGNOSTICS_UNKNOWN_CATEGORY_LABEL, messageText: ` -Angular compiler option "extendedDiagnostics.defaultCategory" has an unknown diagnostic category: "${ - defaultCategory}". +Angular compiler option "extendedDiagnostics.defaultCategory" has an unknown diagnostic category: "${defaultCategory}". Allowed diagnostic categories are: ${allowedCategoryLabels.join('\n')} @@ -1402,8 +1677,7 @@ ${Array.from(SUPPORTED_DIAGNOSTIC_NAMES).join('\n')} category: ts.DiagnosticCategory.Error, code: ErrorCode.CONFIG_EXTENDED_DIAGNOSTICS_UNKNOWN_CATEGORY_LABEL, messageText: ` -Angular compiler option "extendedDiagnostics.checks['${ - checkName}']" has an unknown diagnostic category: "${category}". +Angular compiler option "extendedDiagnostics.checks['${checkName}']" has an unknown diagnostic category: "${category}". Allowed diagnostic categories are: ${allowedCategoryLabels.join('\n')} @@ -1413,10 +1687,14 @@ ${allowedCategoryLabels.join('\n')} } } -function makeConfigDiagnostic({category, code, messageText}: { - category: ts.DiagnosticCategory, - code: ErrorCode, - messageText: string, +function makeConfigDiagnostic({ + category, + code, + messageText, +}: { + category: ts.DiagnosticCategory; + code: ErrorCode; + messageText: string; }): ts.Diagnostic { return { category, @@ -1450,7 +1728,9 @@ class NotifyingProgramDriverWrapper implements ProgramDriver { getSourceFileVersion: ProgramDriver['getSourceFileVersion']; constructor( - private delegate: ProgramDriver, private notifyNewProgram: (program: ts.Program) => void) { + private delegate: ProgramDriver, + private notifyNewProgram: (program: ts.Program) => void, + ) { this.getSourceFileVersion = this.delegate.getSourceFileVersion?.bind(this); } @@ -1469,7 +1749,9 @@ class NotifyingProgramDriverWrapper implements ProgramDriver { } function versionMapFromProgram( - program: ts.Program, driver: ProgramDriver): Map|null { + program: ts.Program, + driver: ProgramDriver, +): Map | null { if (driver.getSourceFileVersion === undefined) { return null; } diff --git a/packages/compiler-cli/src/ngtsc/core/src/core_version.ts b/packages/compiler-cli/src/ngtsc/core/src/core_version.ts index 6fa532ea2b75c..469b7b784d37e 100644 --- a/packages/compiler-cli/src/ngtsc/core/src/core_version.ts +++ b/packages/compiler-cli/src/ngtsc/core/src/core_version.ts @@ -9,7 +9,7 @@ import {ExternalReference} from '@angular/compiler'; import ts from 'typescript'; -export function coreHasSymbol(program: ts.Program, symbol: ExternalReference): boolean|null { +export function coreHasSymbol(program: ts.Program, symbol: ExternalReference): boolean | null { const checker = program.getTypeChecker(); for (const sf of program.getSourceFiles().filter(isMaybeCore)) { const sym = checker.getSymbolAtLocation(sf); @@ -27,6 +27,9 @@ export function coreHasSymbol(program: ts.Program, symbol: ExternalReference): b } export function isMaybeCore(sf: ts.SourceFile): boolean { - return sf.isDeclarationFile && sf.fileName.includes('@angular/core') && - sf.fileName.endsWith('index.d.ts'); + return ( + sf.isDeclarationFile && + sf.fileName.includes('@angular/core') && + sf.fileName.endsWith('index.d.ts') + ); } diff --git a/packages/compiler-cli/src/ngtsc/core/src/host.ts b/packages/compiler-cli/src/ngtsc/core/src/host.ts index 2c5a72f3dedaa..a31e0864a8ec4 100644 --- a/packages/compiler-cli/src/ngtsc/core/src/host.ts +++ b/packages/compiler-cli/src/ngtsc/core/src/host.ts @@ -16,7 +16,12 @@ import {PerFileShimGenerator, TopLevelShimGenerator} from '../../shims/api'; import {TypeCheckShimGenerator} from '../../typecheck'; import {normalizeSeparators} from '../../util/src/path'; import {getRootDirs, isNonDeclarationTsPath, RequiredDelegations} from '../../util/src/typescript'; -import {ExtendedTsCompilerHost, NgCompilerAdapter, NgCompilerOptions, UnifiedModulesHost} from '../api'; +import { + ExtendedTsCompilerHost, + NgCompilerAdapter, + NgCompilerOptions, + UnifiedModulesHost, +} from '../api'; // A persistent source of bugs in CompilerHost delegation has been the addition by TS of new, // optional methods on ts.CompilerHost. Since these methods are optional, it's not a type error that @@ -30,8 +35,9 @@ import {ExtendedTsCompilerHost, NgCompilerAdapter, NgCompilerOptions, UnifiedMod * If a new method is added to `ts.CompilerHost` which is not delegated, a type error will be * generated for this class. */ -export class DelegatingCompilerHost implements - Omit, 'getSourceFile'|'fileExists'> { +export class DelegatingCompilerHost + implements Omit, 'getSourceFile' | 'fileExists'> +{ createHash; directoryExists; fileNameToModuleName; @@ -105,14 +111,17 @@ export class DelegatingCompilerHost implements this.getModuleResolutionCache = this.delegateMethod('getModuleResolutionCache'); this.hasInvalidatedResolutions = this.delegateMethod('hasInvalidatedResolutions'); this.resolveModuleNameLiterals = this.delegateMethod('resolveModuleNameLiterals'); - this.resolveTypeReferenceDirectiveReferences = - this.delegateMethod('resolveTypeReferenceDirectiveReferences'); + this.resolveTypeReferenceDirectiveReferences = this.delegateMethod( + 'resolveTypeReferenceDirectiveReferences', + ); } - private delegateMethod(name: M): - ExtendedTsCompilerHost[M] { - return this.delegate[name] !== undefined ? (this.delegate[name] as any).bind(this.delegate) : - undefined; + private delegateMethod( + name: M, + ): ExtendedTsCompilerHost[M] { + return this.delegate[name] !== undefined + ? (this.delegate[name] as any).bind(this.delegate) + : undefined; } } @@ -127,20 +136,25 @@ export class DelegatingCompilerHost implements * The interface implementations here ensure that `NgCompilerHost` fully delegates to * `ExtendedTsCompilerHost` methods whenever present. */ -export class NgCompilerHost extends DelegatingCompilerHost implements - RequiredDelegations, ExtendedTsCompilerHost, NgCompilerAdapter { - readonly entryPoint: AbsoluteFsPath|null = null; +export class NgCompilerHost + extends DelegatingCompilerHost + implements RequiredDelegations, ExtendedTsCompilerHost, NgCompilerAdapter +{ + readonly entryPoint: AbsoluteFsPath | null = null; readonly constructionDiagnostics: ts.Diagnostic[]; readonly inputFiles: ReadonlyArray; readonly rootDirs: ReadonlyArray; - constructor( - delegate: ExtendedTsCompilerHost, inputFiles: ReadonlyArray, - rootDirs: ReadonlyArray, private shimAdapter: ShimAdapter, - private shimTagger: ShimReferenceTagger, entryPoint: AbsoluteFsPath|null, - diagnostics: ts.Diagnostic[]) { + delegate: ExtendedTsCompilerHost, + inputFiles: ReadonlyArray, + rootDirs: ReadonlyArray, + private shimAdapter: ShimAdapter, + private shimTagger: ShimReferenceTagger, + entryPoint: AbsoluteFsPath | null, + diagnostics: ts.Diagnostic[], + ) { super(delegate); this.entryPoint = entryPoint; @@ -185,8 +199,11 @@ export class NgCompilerHost extends DelegatingCompilerHost implements * of TypeScript and Angular compiler options. */ static wrap( - delegate: ts.CompilerHost, inputFiles: ReadonlyArray, options: NgCompilerOptions, - oldProgram: ts.Program|null): NgCompilerHost { + delegate: ts.CompilerHost, + inputFiles: ReadonlyArray, + options: NgCompilerOptions, + oldProgram: ts.Program | null, + ): NgCompilerHost { const topLevelShimGenerators: TopLevelShimGenerator[] = []; const perFileShimGenerators: PerFileShimGenerator[] = []; @@ -204,7 +221,7 @@ export class NgCompilerHost extends DelegatingCompilerHost implements normalizedTsInputFiles.push(resolve(inputFile)); } - let entryPoint: AbsoluteFsPath|null = null; + let entryPoint: AbsoluteFsPath | null = null; if (options.flatModuleOutFile != null && options.flatModuleOutFile !== '') { entryPoint = findFlatIndexEntryPoint(normalizedTsInputFiles); if (entryPoint === null) { @@ -223,24 +240,39 @@ export class NgCompilerHost extends DelegatingCompilerHost implements start: undefined, length: undefined, messageText: - 'Angular compiler option "flatModuleOutFile" requires one and only one .ts file in the "files" field.', + 'Angular compiler option "flatModuleOutFile" requires one and only one .ts file in the "files" field.', }); } else { const flatModuleId = options.flatModuleId || null; const flatModuleOutFile = normalizeSeparators(options.flatModuleOutFile); - const flatIndexGenerator = - new FlatIndexGenerator(entryPoint, flatModuleOutFile, flatModuleId); + const flatIndexGenerator = new FlatIndexGenerator( + entryPoint, + flatModuleOutFile, + flatModuleId, + ); topLevelShimGenerators.push(flatIndexGenerator); } } const shimAdapter = new ShimAdapter( - delegate, normalizedTsInputFiles, topLevelShimGenerators, perFileShimGenerators, - oldProgram); - const shimTagger = - new ShimReferenceTagger(perFileShimGenerators.map(gen => gen.extensionPrefix)); + delegate, + normalizedTsInputFiles, + topLevelShimGenerators, + perFileShimGenerators, + oldProgram, + ); + const shimTagger = new ShimReferenceTagger( + perFileShimGenerators.map((gen) => gen.extensionPrefix), + ); return new NgCompilerHost( - delegate, inputFiles, rootDirs, shimAdapter, shimTagger, entryPoint, diagnostics); + delegate, + inputFiles, + rootDirs, + shimAdapter, + shimTagger, + entryPoint, + diagnostics, + ); } /** @@ -263,9 +295,11 @@ export class NgCompilerHost extends DelegatingCompilerHost implements } getSourceFile( - fileName: string, languageVersionOrOptions: ts.ScriptTarget|ts.CreateSourceFileOptions, - onError?: ((message: string) => void)|undefined, - shouldCreateNewSourceFile?: boolean|undefined): ts.SourceFile|undefined { + fileName: string, + languageVersionOrOptions: ts.ScriptTarget | ts.CreateSourceFileOptions, + onError?: ((message: string) => void) | undefined, + shouldCreateNewSourceFile?: boolean | undefined, + ): ts.SourceFile | undefined { // Is this a previously known shim? const shimSf = this.shimAdapter.maybeGenerate(resolve(fileName)); if (shimSf !== null) { @@ -275,7 +309,11 @@ export class NgCompilerHost extends DelegatingCompilerHost implements // No, so it's a file which might need shims (or a file which doesn't exist). const sf = this.delegate.getSourceFile( - fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile); + fileName, + languageVersionOrOptions, + onError, + shouldCreateNewSourceFile, + ); if (sf === undefined) { return undefined; } @@ -292,22 +330,32 @@ export class NgCompilerHost extends DelegatingCompilerHost implements // internally only passes POSIX-like paths. // // Also note that the `maybeGenerate` check below checks for both `null` and `undefined`. - return this.delegate.fileExists(fileName) || - this.shimAdapter.maybeGenerate(resolve(fileName)) != null; + return ( + this.delegate.fileExists(fileName) || + this.shimAdapter.maybeGenerate(resolve(fileName)) != null + ); } - get unifiedModulesHost(): UnifiedModulesHost|null { - return this.fileNameToModuleName !== undefined ? this as UnifiedModulesHost : null; + get unifiedModulesHost(): UnifiedModulesHost | null { + return this.fileNameToModuleName !== undefined ? (this as UnifiedModulesHost) : null; } private createCachedResolveModuleNamesFunction(): ts.CompilerHost['resolveModuleNames'] { const moduleResolutionCache = ts.createModuleResolutionCache( - this.getCurrentDirectory(), this.getCanonicalFileName.bind(this)); + this.getCurrentDirectory(), + this.getCanonicalFileName.bind(this), + ); return (moduleNames, containingFile, reusedNames, redirectedReference, options) => { - return moduleNames.map(moduleName => { + return moduleNames.map((moduleName) => { const module = ts.resolveModuleName( - moduleName, containingFile, options, this, moduleResolutionCache, redirectedReference); + moduleName, + containingFile, + options, + this, + moduleResolutionCache, + redirectedReference, + ); return module.resolvedModule; }); }; diff --git a/packages/compiler-cli/src/ngtsc/core/test/compiler_test.ts b/packages/compiler-cli/src/ngtsc/core/test/compiler_test.ts index 78e7c544b9ebd..8ff603e3ff082 100644 --- a/packages/compiler-cli/src/ngtsc/core/test/compiler_test.ts +++ b/packages/compiler-cli/src/ngtsc/core/test/compiler_test.ts @@ -8,7 +8,14 @@ import ts from 'typescript'; -import {absoluteFrom as _, FileSystem, getFileSystem, getSourceFileOrError, NgtscCompilerHost, setFileSystem} from '../../file_system'; +import { + absoluteFrom as _, + FileSystem, + getFileSystem, + getSourceFileOrError, + NgtscCompilerHost, + setFileSystem, +} from '../../file_system'; import {runInEachFileSystem} from '../../file_system/testing'; import {IncrementalBuildStrategy, NoopIncrementalBuildStrategy} from '../../incremental'; import {ProgramDriver, TsCreateProgramDriver} from '../../program_driver'; @@ -21,12 +28,23 @@ import {freshCompilationTicket, NgCompiler, resourceChangeTicket} from '../src/c import {NgCompilerHost} from '../src/host'; function makeFreshCompiler( - host: NgCompilerHost, options: NgCompilerOptions, program: ts.Program, - programStrategy: ProgramDriver, incrementalStrategy: IncrementalBuildStrategy, - enableTemplateTypeChecker: boolean, usePoisonedData: boolean): NgCompiler { + host: NgCompilerHost, + options: NgCompilerOptions, + program: ts.Program, + programStrategy: ProgramDriver, + incrementalStrategy: IncrementalBuildStrategy, + enableTemplateTypeChecker: boolean, + usePoisonedData: boolean, +): NgCompiler { const ticket = freshCompilationTicket( - program, options, incrementalStrategy, programStrategy, /* perfRecorder */ null, - enableTemplateTypeChecker, usePoisonedData); + program, + options, + incrementalStrategy, + programStrategy, + /* perfRecorder */ null, + enableTemplateTypeChecker, + usePoisonedData, + ); return NgCompiler.fromTicket(ticket, host); } @@ -37,21 +55,27 @@ runInEachFileSystem(() => { beforeEach(() => { fs = getFileSystem(); fs.ensureDir(_('/node_modules/@angular/core')); - fs.writeFile(_('/node_modules/@angular/core/index.d.ts'), ` + fs.writeFile( + _('/node_modules/@angular/core/index.d.ts'), + ` export declare const Component: any; - `); + `, + ); }); it('should also return template diagnostics when asked for component diagnostics', () => { const COMPONENT = _('/cmp.ts'); - fs.writeFile(COMPONENT, ` + fs.writeFile( + COMPONENT, + ` import {Component} from '@angular/core'; @Component({ selector: 'test-cmp', templateUrl: './template.html', }) export class Cmp {} - `); + `, + ); fs.writeFile(_('/template.html'), `{{does_not_exist.foo}}`); const options: NgCompilerOptions = { @@ -61,12 +85,19 @@ runInEachFileSystem(() => { const host = NgCompilerHost.wrap(baseHost, [COMPONENT], options, /* oldProgram */ null); const program = ts.createProgram({host, options, rootNames: host.inputFiles}); const compiler = makeFreshCompiler( - host, options, program, new TsCreateProgramDriver(program, host, options, []), - new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false, - /* usePoisonedData */ false); + host, + options, + program, + new TsCreateProgramDriver(program, host, options, []), + new NoopIncrementalBuildStrategy(), + /** enableTemplateTypeChecker */ false, + /* usePoisonedData */ false, + ); const diags = compiler.getDiagnosticsForFile( - getSourceFileOrError(program, COMPONENT), OptimizeFor.SingleFile); + getSourceFileOrError(program, COMPONENT), + OptimizeFor.SingleFile, + ); expect(diags.length).toBe(1); expect(diags[0].messageText).toContain('does_not_exist'); }); @@ -76,46 +107,64 @@ runInEachFileSystem(() => { const templateFile = _('/template.html'); fs.writeFile(templateFile, `This is the template, used by components CmpA and CmpC`); const cmpAFile = _('/cmp-a.ts'); - fs.writeFile(cmpAFile, ` + fs.writeFile( + cmpAFile, + ` import {Component} from '@angular/core'; @Component({ selector: 'cmp-a', templateUrl: './template.html', }) export class CmpA {} - `); + `, + ); const cmpBFile = _('/cmp-b.ts'); - fs.writeFile(cmpBFile, ` + fs.writeFile( + cmpBFile, + ` import {Component} from '@angular/core'; @Component({ selector: 'cmp-b', template: 'CmpB does not use an external template', }) export class CmpB {} - `); + `, + ); const cmpCFile = _('/cmp-c.ts'); - fs.writeFile(cmpCFile, ` + fs.writeFile( + cmpCFile, + ` import {Component} from '@angular/core'; @Component({ selector: 'cmp-c', templateUrl: './template.html', }) export class CmpC {} - `); + `, + ); const options: NgCompilerOptions = {}; const baseHost = new NgtscCompilerHost(getFileSystem(), options); const host = NgCompilerHost.wrap( - baseHost, [cmpAFile, cmpBFile, cmpCFile], options, /* oldProgram */ null); + baseHost, + [cmpAFile, cmpBFile, cmpCFile], + options, + /* oldProgram */ null, + ); const program = ts.createProgram({host, options, rootNames: host.inputFiles}); const CmpA = getClass(getSourceFileOrError(program, cmpAFile), 'CmpA'); const CmpC = getClass(getSourceFileOrError(program, cmpCFile), 'CmpC'); const compiler = makeFreshCompiler( - host, options, program, new TsCreateProgramDriver(program, host, options, []), - new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false, - /* usePoisonedData */ false); + host, + options, + program, + new TsCreateProgramDriver(program, host, options, []), + new NoopIncrementalBuildStrategy(), + /** enableTemplateTypeChecker */ false, + /* usePoisonedData */ false, + ); const components = compiler.getComponentsWithTemplateFile(templateFile); expect(components).toEqual(new Set([CmpA, CmpC])); }); @@ -126,7 +175,9 @@ runInEachFileSystem(() => { const styleFile = _('/style.css'); fs.writeFile(styleFile, `/* This is the style, used by components CmpA and CmpC */`); const cmpAFile = _('/cmp-a.ts'); - fs.writeFile(cmpAFile, ` + fs.writeFile( + cmpAFile, + ` import {Component} from '@angular/core'; @Component({ selector: 'cmp-a', @@ -134,9 +185,12 @@ runInEachFileSystem(() => { styleUrls: ['./style.css'], }) export class CmpA {} - `); + `, + ); const cmpBFile = _('/cmp-b.ts'); - fs.writeFile(cmpBFile, ` + fs.writeFile( + cmpBFile, + ` import {Component} from '@angular/core'; @Component({ selector: 'cmp-b', @@ -144,9 +198,12 @@ runInEachFileSystem(() => { styles: ['/* CmpB does not use external style */'], }) export class CmpB {} - `); + `, + ); const cmpCFile = _('/cmp-c.ts'); - fs.writeFile(cmpCFile, ` + fs.writeFile( + cmpCFile, + ` import {Component} from '@angular/core'; @Component({ selector: 'cmp-c', @@ -154,20 +211,30 @@ runInEachFileSystem(() => { styleUrls: ['./style.css'] }) export class CmpC {} - `); + `, + ); const options: NgCompilerOptions = {}; const baseHost = new NgtscCompilerHost(getFileSystem(), options); const host = NgCompilerHost.wrap( - baseHost, [cmpAFile, cmpBFile, cmpCFile], options, /* oldProgram */ null); + baseHost, + [cmpAFile, cmpBFile, cmpCFile], + options, + /* oldProgram */ null, + ); const program = ts.createProgram({host, options, rootNames: host.inputFiles}); const CmpA = getClass(getSourceFileOrError(program, cmpAFile), 'CmpA'); const CmpC = getClass(getSourceFileOrError(program, cmpCFile), 'CmpC'); const compiler = makeFreshCompiler( - host, options, program, new TsCreateProgramDriver(program, host, options, []), - new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false, - /* usePoisonedData */ false); + host, + options, + program, + new TsCreateProgramDriver(program, host, options, []), + new NoopIncrementalBuildStrategy(), + /** enableTemplateTypeChecker */ false, + /* usePoisonedData */ false, + ); const components = compiler.getComponentsWithStyleFile(styleFile); expect(components).toEqual(new Set([CmpA, CmpC])); }); @@ -182,7 +249,9 @@ runInEachFileSystem(() => { const templateFile = _('/template.ng.html'); fs.writeFile(templateFile, `This is the template, used by components CmpA`); const cmpAFile = _('/cmp-a.ts'); - fs.writeFile(cmpAFile, ` + fs.writeFile( + cmpAFile, + ` import {Component} from '@angular/core'; @Component({ selector: 'cmp-a', @@ -190,7 +259,8 @@ runInEachFileSystem(() => { styleUrls: ['./style.css', '../../style2.css'], }) export class CmpA {} - `); + `, + ); const options: NgCompilerOptions = {}; @@ -199,15 +269,20 @@ runInEachFileSystem(() => { const program = ts.createProgram({host, options, rootNames: host.inputFiles}); const CmpA = getClass(getSourceFileOrError(program, cmpAFile), 'CmpA'); const compiler = makeFreshCompiler( - host, options, program, new TsCreateProgramDriver(program, host, options, []), - new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false, - /* usePoisonedData */ false); + host, + options, + program, + new TsCreateProgramDriver(program, host, options, []), + new NoopIncrementalBuildStrategy(), + /** enableTemplateTypeChecker */ false, + /* usePoisonedData */ false, + ); const resources = compiler.getComponentResources(CmpA); expect(resources).not.toBeNull(); const {template, styles} = resources!; - expect(template !.path).toEqual(templateFile); + expect(template!.path).toEqual(templateFile); expect(styles.size).toEqual(2); - const actualPaths = new Set(Array.from(styles).map(r => r.path)); + const actualPaths = new Set(Array.from(styles).map((r) => r.path)); expect(actualPaths).toEqual(new Set([styleFile, styleFile2])); }); @@ -217,7 +292,9 @@ runInEachFileSystem(() => { const styleFile2 = _('/style2.css'); fs.writeFile(styleFile2, `/* This is the template, used by components CmpA */`); const cmpAFile = _('/cmp-a.ts'); - fs.writeFile(cmpAFile, ` + fs.writeFile( + cmpAFile, + ` import {Component} from '@angular/core'; const STYLES = ['./style.css', '../../style2.css']; @Component({ @@ -226,7 +303,8 @@ runInEachFileSystem(() => { styleUrls: STYLES, }) export class CmpA {} - `); + `, + ); const options: NgCompilerOptions = {}; @@ -235,9 +313,14 @@ runInEachFileSystem(() => { const program = ts.createProgram({host, options, rootNames: host.inputFiles}); const CmpA = getClass(getSourceFileOrError(program, cmpAFile), 'CmpA'); const compiler = makeFreshCompiler( - host, options, program, new TsCreateProgramDriver(program, host, options, []), - new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false, - /* usePoisonedData */ false); + host, + options, + program, + new TsCreateProgramDriver(program, host, options, []), + new NoopIncrementalBuildStrategy(), + /** enableTemplateTypeChecker */ false, + /* usePoisonedData */ false, + ); const resources = compiler.getComponentResources(CmpA); expect(resources).not.toBeNull(); const {styles} = resources!; @@ -248,7 +331,9 @@ runInEachFileSystem(() => { describe('getResourceDependencies', () => { it('should return resource dependencies of a component source file', () => { const COMPONENT = _('/cmp.ts'); - fs.writeFile(COMPONENT, ` + fs.writeFile( + COMPONENT, + ` import {Component} from '@angular/core'; @Component({ selector: 'test-cmp', @@ -256,7 +341,8 @@ runInEachFileSystem(() => { styleUrls: ['./style.css'], }) export class Cmp {} - `); + `, + ); fs.writeFile(_('/template.html'), `

Resource

`); fs.writeFile(_('/style.css'), `h1 { }`); @@ -267,16 +353,23 @@ runInEachFileSystem(() => { const host = NgCompilerHost.wrap(baseHost, [COMPONENT], options, /* oldProgram */ null); const program = ts.createProgram({host, options, rootNames: host.inputFiles}); const compiler = makeFreshCompiler( - host, options, program, new TsCreateProgramDriver(program, host, options, []), - new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false, - /* usePoisonedData */ false); + host, + options, + program, + new TsCreateProgramDriver(program, host, options, []), + new NoopIncrementalBuildStrategy(), + /** enableTemplateTypeChecker */ false, + /* usePoisonedData */ false, + ); const deps = compiler.getResourceDependencies(getSourceFileOrError(program, COMPONENT)); expect(deps.length).toBe(2); - expect(deps).toEqual(jasmine.arrayContaining([ - jasmine.stringMatching(/\/template.html$/), - jasmine.stringMatching(/\/style.css$/), - ])); + expect(deps).toEqual( + jasmine.arrayContaining([ + jasmine.stringMatching(/\/template.html$/), + jasmine.stringMatching(/\/style.css$/), + ]), + ); }); }); @@ -284,14 +377,17 @@ runInEachFileSystem(() => { it('should reuse the full compilation state for a resource-only change', () => { const COMPONENT = _('/cmp.ts'); const TEMPLATE = _('/template.html'); - fs.writeFile(COMPONENT, ` + fs.writeFile( + COMPONENT, + ` import {Component} from '@angular/core'; @Component({ selector: 'test-cmp', templateUrl: './template.html', }) export class Cmp {} - `); + `, + ); fs.writeFile(TEMPLATE, `

Resource

`); const options: NgCompilerOptions = { @@ -301,15 +397,21 @@ runInEachFileSystem(() => { const host = NgCompilerHost.wrap(baseHost, [COMPONENT], options, /* oldProgram */ null); const program = ts.createProgram({host, options, rootNames: host.inputFiles}); const compilerA = makeFreshCompiler( - host, options, program, new TsCreateProgramDriver(program, host, options, []), - new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false, - /* usePoisonedData */ false); + host, + options, + program, + new TsCreateProgramDriver(program, host, options, []), + new NoopIncrementalBuildStrategy(), + /** enableTemplateTypeChecker */ false, + /* usePoisonedData */ false, + ); const componentSf = getSourceFileOrError(program, COMPONENT); // There should be no diagnostics for the component. - expect(compilerA.getDiagnosticsForFile(componentSf, OptimizeFor.WholeProgram).length) - .toBe(0); + expect(compilerA.getDiagnosticsForFile(componentSf, OptimizeFor.WholeProgram).length).toBe( + 0, + ); // Change the resource file and introduce an error. fs.writeFile(TEMPLATE, `

Resource

`); @@ -322,14 +424,14 @@ runInEachFileSystem(() => { expect(compilerB).toBe(compilerA); // The new template error should be reported in component diagnostics. - expect(compilerB.getDiagnosticsForFile(componentSf, OptimizeFor.WholeProgram).length) - .toBe(1); + expect(compilerB.getDiagnosticsForFile(componentSf, OptimizeFor.WholeProgram).length).toBe( + 1, + ); }); }); }); }); - function getClass(sf: ts.SourceFile, name: string): ClassDeclaration { for (const stmt of sf.statements) { if (isNamedClassDeclaration(stmt) && stmt.name.text === name) { diff --git a/packages/compiler-cli/src/ngtsc/cycles/src/analyzer.ts b/packages/compiler-cli/src/ngtsc/cycles/src/analyzer.ts index 03e0fd71465ed..aff41c95c3a86 100644 --- a/packages/compiler-cli/src/ngtsc/cycles/src/analyzer.ts +++ b/packages/compiler-cli/src/ngtsc/cycles/src/analyzer.ts @@ -20,7 +20,7 @@ export class CycleAnalyzer { * file has not changed. This avoids visiting the import graph that is reachable from multiple * directives/pipes more than once. */ - private cachedResults: CycleResults|null = null; + private cachedResults: CycleResults | null = null; constructor(private importGraph: ImportGraph) {} @@ -31,7 +31,7 @@ export class CycleAnalyzer { * @returns a `Cycle` object if an import between `from` and `to` would create a cycle; `null` * otherwise. */ - wouldCreateCycle(from: ts.SourceFile, to: ts.SourceFile): Cycle|null { + wouldCreateCycle(from: ts.SourceFile, to: ts.SourceFile): Cycle | null { // Try to reuse the cached results as long as the `from` source file is the same. if (this.cachedResults === null || this.cachedResults.from !== from) { this.cachedResults = new CycleResults(from, this.importGraph); @@ -57,7 +57,7 @@ const NgCyclicResult = Symbol('NgCyclicResult'); type CyclicResultMarker = { __brand: 'CyclicResultMarker'; }; -type CyclicSourceFile = ts.SourceFile&{[NgCyclicResult]?: CyclicResultMarker}; +type CyclicSourceFile = ts.SourceFile & {[NgCyclicResult]?: CyclicResultMarker}; /** * Stores the results of cycle detection in a memory efficient manner. A symbol is attached to @@ -70,7 +70,10 @@ class CycleResults { private readonly cyclic = {} as CyclicResultMarker; private readonly acyclic = {} as CyclicResultMarker; - constructor(readonly from: ts.SourceFile, private importGraph: ImportGraph) {} + constructor( + readonly from: ts.SourceFile, + private importGraph: ImportGraph, + ) {} wouldBeCyclic(sf: ts.SourceFile): boolean { const cached = this.getCachedResult(sf); @@ -103,7 +106,7 @@ class CycleResults { * Returns whether the source file is already known to be cyclic, or `null` if the result is not * yet known. */ - private getCachedResult(sf: CyclicSourceFile): boolean|null { + private getCachedResult(sf: CyclicSourceFile): boolean | null { const result = sf[NgCyclicResult]; if (result === this.cyclic) { return true; @@ -133,7 +136,10 @@ class CycleResults { */ export class Cycle { constructor( - private importGraph: ImportGraph, readonly from: ts.SourceFile, readonly to: ts.SourceFile) {} + private importGraph: ImportGraph, + readonly from: ts.SourceFile, + readonly to: ts.SourceFile, + ) {} /** * Compute an array of source-files that illustrates the cyclic path between `from` and `to`. @@ -146,7 +152,6 @@ export class Cycle { } } - /** * What to do if a cycle is detected. */ diff --git a/packages/compiler-cli/src/ngtsc/cycles/src/imports.ts b/packages/compiler-cli/src/ngtsc/cycles/src/imports.ts index 67d79c4a276c9..3ec135538466f 100644 --- a/packages/compiler-cli/src/ngtsc/cycles/src/imports.ts +++ b/packages/compiler-cli/src/ngtsc/cycles/src/imports.ts @@ -19,7 +19,10 @@ import {PerfPhase, PerfRecorder} from '../../perf'; export class ImportGraph { private imports = new Map>(); - constructor(private checker: ts.TypeChecker, private perf: PerfRecorder) {} + constructor( + private checker: ts.TypeChecker, + private perf: PerfRecorder, + ) {} /** * List the direct (not transitive) imports of a given `ts.SourceFile`. @@ -44,7 +47,7 @@ export class ImportGraph { * @returns an array of source files that connect the `start` and `end` source files, or `null` if * no path could be found. */ - findPath(start: ts.SourceFile, end: ts.SourceFile): ts.SourceFile[]|null { + findPath(start: ts.SourceFile, end: ts.SourceFile): ts.SourceFile[] | null { if (start === end) { // Escape early for the case where `start` and `end` are the same. return [start]; @@ -86,13 +89,18 @@ export class ImportGraph { const imports = new Set(); // Look through the source file for import and export statements. for (const stmt of sf.statements) { - if ((!ts.isImportDeclaration(stmt) && !ts.isExportDeclaration(stmt)) || - stmt.moduleSpecifier === undefined) { + if ( + (!ts.isImportDeclaration(stmt) && !ts.isExportDeclaration(stmt)) || + stmt.moduleSpecifier === undefined + ) { continue; } - if (ts.isImportDeclaration(stmt) && stmt.importClause !== undefined && - isTypeOnlyImportClause(stmt.importClause)) { + if ( + ts.isImportDeclaration(stmt) && + stmt.importClause !== undefined && + isTypeOnlyImportClause(stmt.importClause) + ) { // Exclude type-only imports as they are always elided, so they don't contribute to // cycles. continue; @@ -125,8 +133,11 @@ function isTypeOnlyImportClause(node: ts.ImportClause): boolean { } // All the specifiers in the cause are type-only (e.g. `import {type a, type b} from '...'`). - if (node.namedBindings !== undefined && ts.isNamedImports(node.namedBindings) && - node.namedBindings.elements.every(specifier => specifier.isTypeOnly)) { + if ( + node.namedBindings !== undefined && + ts.isNamedImports(node.namedBindings) && + node.namedBindings.elements.every((specifier) => specifier.isTypeOnly) + ) { return true; } @@ -138,7 +149,10 @@ function isTypeOnlyImportClause(node: ts.ImportClause): boolean { * `getPath()` above. */ class Found { - constructor(readonly sourceFile: ts.SourceFile, readonly parent: Found|null) {} + constructor( + readonly sourceFile: ts.SourceFile, + readonly parent: Found | null, + ) {} /** * Back track through this found SourceFile and its ancestors to generate an array of @@ -146,7 +160,7 @@ class Found { */ toPath(): ts.SourceFile[] { const array: ts.SourceFile[] = []; - let current: Found|null = this; + let current: Found | null = this; while (current !== null) { array.push(current.sourceFile); current = current.parent; diff --git a/packages/compiler-cli/src/ngtsc/cycles/test/analyzer_spec.ts b/packages/compiler-cli/src/ngtsc/cycles/test/analyzer_spec.ts index 30a6abf6acbb6..be9346b5598a2 100644 --- a/packages/compiler-cli/src/ngtsc/cycles/test/analyzer_spec.ts +++ b/packages/compiler-cli/src/ngtsc/cycles/test/analyzer_spec.ts @@ -16,20 +16,20 @@ import {importPath, makeProgramFromGraph} from './util'; runInEachFileSystem(() => { describe('cycle analyzer', () => { let _: typeof absoluteFrom; - beforeEach(() => _ = absoluteFrom); + beforeEach(() => (_ = absoluteFrom)); - it('should not detect a cycle when there isn\'t one', () => { + it("should not detect a cycle when there isn't one", () => { const {program, analyzer} = makeAnalyzer('a:b,c;b;c'); - const b = getSourceFileOrError(program, (_('/b.ts'))); - const c = getSourceFileOrError(program, (_('/c.ts'))); + const b = getSourceFileOrError(program, _('/b.ts')); + const c = getSourceFileOrError(program, _('/c.ts')); expect(analyzer.wouldCreateCycle(b, c)).toBe(null); expect(analyzer.wouldCreateCycle(c, b)).toBe(null); }); it('should detect a simple cycle between two files', () => { const {program, analyzer} = makeAnalyzer('a:b;b'); - const a = getSourceFileOrError(program, (_('/a.ts'))); - const b = getSourceFileOrError(program, (_('/b.ts'))); + const a = getSourceFileOrError(program, _('/a.ts')); + const b = getSourceFileOrError(program, _('/b.ts')); expect(analyzer.wouldCreateCycle(a, b)).toBe(null); const cycle = analyzer.wouldCreateCycle(b, a); expect(cycle).toBeInstanceOf(Cycle); @@ -40,10 +40,10 @@ runInEachFileSystem(() => { // a -> b -> c -> d // ^---------/ const {program, analyzer} = makeAnalyzer('a:b;b:c;c:d;d:b'); - const a = getSourceFileOrError(program, (_('/a.ts'))); - const b = getSourceFileOrError(program, (_('/b.ts'))); - const c = getSourceFileOrError(program, (_('/c.ts'))); - const d = getSourceFileOrError(program, (_('/d.ts'))); + const a = getSourceFileOrError(program, _('/a.ts')); + const b = getSourceFileOrError(program, _('/b.ts')); + const c = getSourceFileOrError(program, _('/c.ts')); + const d = getSourceFileOrError(program, _('/d.ts')); expect(analyzer.wouldCreateCycle(a, b)).toBe(null); expect(analyzer.wouldCreateCycle(a, c)).toBe(null); expect(analyzer.wouldCreateCycle(a, d)).toBe(null); @@ -54,9 +54,9 @@ runInEachFileSystem(() => { it('should detect a cycle with a re-export in the chain', () => { const {program, analyzer} = makeAnalyzer('a:*b;b:c;c'); - const a = getSourceFileOrError(program, (_('/a.ts'))); - const b = getSourceFileOrError(program, (_('/b.ts'))); - const c = getSourceFileOrError(program, (_('/c.ts'))); + const a = getSourceFileOrError(program, _('/a.ts')); + const b = getSourceFileOrError(program, _('/b.ts')); + const c = getSourceFileOrError(program, _('/c.ts')); expect(analyzer.wouldCreateCycle(a, c)).toBe(null); const cycle = analyzer.wouldCreateCycle(c, a); expect(cycle).toBeInstanceOf(Cycle); @@ -65,11 +65,11 @@ runInEachFileSystem(() => { it('should detect a cycle in a more complex program', () => { const {program, analyzer} = makeAnalyzer('a:*b,*c;b:*e,*f;c:*g,*h;e:f;f:c;g;h:g'); - const b = getSourceFileOrError(program, (_('/b.ts'))); - const c = getSourceFileOrError(program, (_('/c.ts'))); - const e = getSourceFileOrError(program, (_('/e.ts'))); - const f = getSourceFileOrError(program, (_('/f.ts'))); - const g = getSourceFileOrError(program, (_('/g.ts'))); + const b = getSourceFileOrError(program, _('/b.ts')); + const c = getSourceFileOrError(program, _('/c.ts')); + const e = getSourceFileOrError(program, _('/e.ts')); + const f = getSourceFileOrError(program, _('/f.ts')); + const g = getSourceFileOrError(program, _('/g.ts')); expect(analyzer.wouldCreateCycle(b, g)).toBe(null); const cycle = analyzer.wouldCreateCycle(g, b); expect(cycle).toBeInstanceOf(Cycle); @@ -78,8 +78,8 @@ runInEachFileSystem(() => { it('should detect a cycle caused by a synthetic edge', () => { const {program, analyzer} = makeAnalyzer('a:b,c;b;c'); - const b = getSourceFileOrError(program, (_('/b.ts'))); - const c = getSourceFileOrError(program, (_('/c.ts'))); + const b = getSourceFileOrError(program, _('/b.ts')); + const c = getSourceFileOrError(program, _('/c.ts')); expect(analyzer.wouldCreateCycle(b, c)).toBe(null); analyzer.recordSyntheticImport(c, b); const cycle = analyzer.wouldCreateCycle(b, c); @@ -89,9 +89,9 @@ runInEachFileSystem(() => { it('should not consider type-only imports', () => { const {program, analyzer} = makeAnalyzer('a:b,c!;b;c'); - const a = getSourceFileOrError(program, (_('/a.ts'))); - const b = getSourceFileOrError(program, (_('/b.ts'))); - const c = getSourceFileOrError(program, (_('/c.ts'))); + const a = getSourceFileOrError(program, _('/a.ts')); + const b = getSourceFileOrError(program, _('/b.ts')); + const c = getSourceFileOrError(program, _('/c.ts')); expect(analyzer.wouldCreateCycle(c, a)).toBe(null); const cycle = analyzer.wouldCreateCycle(b, a); expect(cycle).toBeInstanceOf(Cycle); @@ -99,7 +99,7 @@ runInEachFileSystem(() => { }); }); - function makeAnalyzer(graph: string): {program: ts.Program, analyzer: CycleAnalyzer} { + function makeAnalyzer(graph: string): {program: ts.Program; analyzer: CycleAnalyzer} { const {program} = makeProgramFromGraph(getFileSystem(), graph); return { program, diff --git a/packages/compiler-cli/src/ngtsc/cycles/test/imports_spec.ts b/packages/compiler-cli/src/ngtsc/cycles/test/imports_spec.ts index 0902e13dc8cbd..4e541cecb83cd 100644 --- a/packages/compiler-cli/src/ngtsc/cycles/test/imports_spec.ts +++ b/packages/compiler-cli/src/ngtsc/cycles/test/imports_spec.ts @@ -15,14 +15,14 @@ import {importPath, makeProgramFromGraph} from './util'; runInEachFileSystem(() => { describe('ImportGraph', () => { let _: typeof absoluteFrom; - beforeEach(() => _ = absoluteFrom); + beforeEach(() => (_ = absoluteFrom)); describe('importsOf()', () => { it('should record imports of a simple program', () => { const {program, graph} = makeImportGraph('a:b;b:c;c'); - const a = getSourceFileOrError(program, (_('/a.ts'))); - const b = getSourceFileOrError(program, (_('/b.ts'))); - const c = getSourceFileOrError(program, (_('/c.ts'))); + const a = getSourceFileOrError(program, _('/a.ts')); + const b = getSourceFileOrError(program, _('/b.ts')); + const c = getSourceFileOrError(program, _('/c.ts')); expect(importsToString(graph.importsOf(a))).toBe('b'); expect(importsToString(graph.importsOf(b))).toBe('c'); }); @@ -31,10 +31,10 @@ runInEachFileSystem(() => { describe('findPath()', () => { it('should be able to compute the path between two source files if there is a cycle', () => { const {program, graph} = makeImportGraph('a:*b,*c;b:*e,*f;c:*g,*h;e:f;f;g:e;h:g'); - const a = getSourceFileOrError(program, (_('/a.ts'))); - const b = getSourceFileOrError(program, (_('/b.ts'))); - const c = getSourceFileOrError(program, (_('/c.ts'))); - const e = getSourceFileOrError(program, (_('/e.ts'))); + const a = getSourceFileOrError(program, _('/a.ts')); + const b = getSourceFileOrError(program, _('/b.ts')); + const c = getSourceFileOrError(program, _('/c.ts')); + const e = getSourceFileOrError(program, _('/e.ts')); expect(importPath(graph.findPath(a, a)!)).toBe('a'); expect(importPath(graph.findPath(a, b)!)).toBe('a,b'); expect(importPath(graph.findPath(c, e)!)).toBe('c,g,e'); @@ -47,15 +47,15 @@ runInEachFileSystem(() => { // ^----/ | // ^---------/ const {program, graph} = makeImportGraph('a:b;b:a,c;c:a,d;d'); - const a = getSourceFileOrError(program, (_('/a.ts'))); - const c = getSourceFileOrError(program, (_('/c.ts'))); - const d = getSourceFileOrError(program, (_('/d.ts'))); + const a = getSourceFileOrError(program, _('/a.ts')); + const c = getSourceFileOrError(program, _('/c.ts')); + const d = getSourceFileOrError(program, _('/d.ts')); expect(importPath(graph.findPath(a, d)!)).toBe('a,b,c,d'); }); }); }); - function makeImportGraph(graph: string): {program: ts.Program, graph: ImportGraph} { + function makeImportGraph(graph: string): {program: ts.Program; graph: ImportGraph} { const {program} = makeProgramFromGraph(getFileSystem(), graph); return { program, @@ -66,8 +66,8 @@ runInEachFileSystem(() => { function importsToString(imports: Set): string { const fs = getFileSystem(); return Array.from(imports) - .map(sf => fs.basename(sf.fileName).replace('.ts', '')) - .sort() - .join(','); + .map((sf) => fs.basename(sf.fileName).replace('.ts', '')) + .sort() + .join(','); } }); diff --git a/packages/compiler-cli/src/ngtsc/cycles/test/util.ts b/packages/compiler-cli/src/ngtsc/cycles/test/util.ts index 88ba9388bb4c8..f2cfddf5d23a1 100644 --- a/packages/compiler-cli/src/ngtsc/cycles/test/util.ts +++ b/packages/compiler-cli/src/ngtsc/cycles/test/util.ts @@ -34,27 +34,30 @@ import {makeProgram} from '../../testing'; * * An import can be suffixed with ! to make it a type-only import. */ -export function makeProgramFromGraph(fs: PathManipulation, graph: string): { - program: ts.Program, - host: ts.CompilerHost, - options: ts.CompilerOptions, +export function makeProgramFromGraph( + fs: PathManipulation, + graph: string, +): { + program: ts.Program; + host: ts.CompilerHost; + options: ts.CompilerOptions; } { - const files: TestFile[] = graph.split(';').map(fileSegment => { + const files: TestFile[] = graph.split(';').map((fileSegment) => { const [name, importList] = fileSegment.split(':'); - const contents = (importList ? importList.split(',') : []) - .map(i => { - if (i.startsWith('*')) { - const sym = i.slice(1); - return `export {${sym}} from './${sym}';`; - } else if (i.endsWith('!')) { - const sym = i.slice(0, -1); - return `import type {${sym}} from './${sym}';`; - } else { - return `import {${i}} from './${i}';`; - } - }) - .join('\n') + - `export const ${name} = '${name}';\n`; + const contents = + (importList ? importList.split(',') : []) + .map((i) => { + if (i.startsWith('*')) { + const sym = i.slice(1); + return `export {${sym}} from './${sym}';`; + } else if (i.endsWith('!')) { + const sym = i.slice(0, -1); + return `import type {${sym}} from './${sym}';`; + } else { + return `import {${i}} from './${i}';`; + } + }) + .join('\n') + `export const ${name} = '${name}';\n`; return { name: fs.resolve(`/${name}.ts`), contents, @@ -65,5 +68,5 @@ export function makeProgramFromGraph(fs: PathManipulation, graph: string): { export function importPath(files: ts.SourceFile[]): string { const fs = getFileSystem(); - return files.map(sf => fs.basename(sf.fileName).replace('.ts', '')).join(','); + return files.map((sf) => fs.basename(sf.fileName).replace('.ts', '')).join(','); } diff --git a/packages/compiler-cli/src/ngtsc/diagnostics/index.ts b/packages/compiler-cli/src/ngtsc/diagnostics/index.ts index 0160ed139ee1a..8ffb82afce2b6 100644 --- a/packages/compiler-cli/src/ngtsc/diagnostics/index.ts +++ b/packages/compiler-cli/src/ngtsc/diagnostics/index.ts @@ -7,7 +7,15 @@ */ export {COMPILER_ERRORS_WITH_GUIDES} from './src/docs'; -export {addDiagnosticChain, FatalDiagnosticError, isFatalDiagnosticError, isLocalCompilationDiagnostics, makeDiagnostic, makeDiagnosticChain, makeRelatedInformation} from './src/error'; +export { + addDiagnosticChain, + FatalDiagnosticError, + isFatalDiagnosticError, + isLocalCompilationDiagnostics, + makeDiagnostic, + makeDiagnosticChain, + makeRelatedInformation, +} from './src/error'; export {ErrorCode} from './src/error_code'; export {ERROR_DETAILS_PAGE_BASE_URL} from './src/error_details_base_url'; export {ExtendedTemplateDiagnosticName} from './src/extended_template_diagnostic_name'; diff --git a/packages/compiler-cli/src/ngtsc/diagnostics/src/error.ts b/packages/compiler-cli/src/ngtsc/diagnostics/src/error.ts index 21ac9e0c72aa4..5902c44e8abaf 100644 --- a/packages/compiler-cli/src/ngtsc/diagnostics/src/error.ts +++ b/packages/compiler-cli/src/ngtsc/diagnostics/src/error.ts @@ -13,11 +13,17 @@ import {ngErrorCode} from './util'; export class FatalDiagnosticError extends Error { constructor( - readonly code: ErrorCode, readonly node: ts.Node, - readonly diagnosticMessage: string|ts.DiagnosticMessageChain, - readonly relatedInformation?: ts.DiagnosticRelatedInformation[]) { - super(`FatalDiagnosticError: Code: ${code}, Message: ${ - ts.flattenDiagnosticMessageText(diagnosticMessage, '\n')}`); + readonly code: ErrorCode, + readonly node: ts.Node, + readonly diagnosticMessage: string | ts.DiagnosticMessageChain, + readonly relatedInformation?: ts.DiagnosticRelatedInformation[], + ) { + super( + `FatalDiagnosticError: Code: ${code}, Message: ${ts.flattenDiagnosticMessageText( + diagnosticMessage, + '\n', + )}`, + ); // Extending `Error` ends up breaking some internal tests. This appears to be a known issue // when extending errors in TS and the workaround is to explicitly set the prototype. @@ -40,8 +46,11 @@ export class FatalDiagnosticError extends Error { } export function makeDiagnostic( - code: ErrorCode, node: ts.Node, messageText: string|ts.DiagnosticMessageChain, - relatedInformation?: ts.DiagnosticRelatedInformation[]): ts.DiagnosticWithLocation { + code: ErrorCode, + node: ts.Node, + messageText: string | ts.DiagnosticMessageChain, + relatedInformation?: ts.DiagnosticRelatedInformation[], +): ts.DiagnosticWithLocation { node = ts.getOriginalNode(node); return { category: ts.DiagnosticCategory.Error, @@ -55,7 +64,9 @@ export function makeDiagnostic( } export function makeDiagnosticChain( - messageText: string, next?: ts.DiagnosticMessageChain[]): ts.DiagnosticMessageChain { + messageText: string, + next?: ts.DiagnosticMessageChain[], +): ts.DiagnosticMessageChain { return { category: ts.DiagnosticCategory.Message, code: 0, @@ -65,7 +76,9 @@ export function makeDiagnosticChain( } export function makeRelatedInformation( - node: ts.Node, messageText: string): ts.DiagnosticRelatedInformation { + node: ts.Node, + messageText: string, +): ts.DiagnosticRelatedInformation { node = ts.getOriginalNode(node); return { category: ts.DiagnosticCategory.Message, @@ -78,8 +91,9 @@ export function makeRelatedInformation( } export function addDiagnosticChain( - messageText: string|ts.DiagnosticMessageChain, - add: ts.DiagnosticMessageChain[]): ts.DiagnosticMessageChain { + messageText: string | ts.DiagnosticMessageChain, + add: ts.DiagnosticMessageChain[], +): ts.DiagnosticMessageChain { if (typeof messageText === 'string') { return makeDiagnosticChain(messageText, add); } @@ -104,6 +118,8 @@ export function isFatalDiagnosticError(err: any): err is FatalDiagnosticError { * compilation in order to add some g3 specific info to it. */ export function isLocalCompilationDiagnostics(diagnostic: ts.Diagnostic): boolean { - return diagnostic.code === ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST) || - diagnostic.code === ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION); + return ( + diagnostic.code === ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST) || + diagnostic.code === ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION) + ); } diff --git a/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts b/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts index 599bb4eabef4c..ebaf3180758bc 100644 --- a/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts +++ b/packages/compiler-cli/src/ngtsc/diagnostics/src/error_code.ts @@ -435,7 +435,6 @@ export enum ErrorCode { */ OPTIONAL_CHAIN_NOT_NULLABLE = 8107, - /** * `ngSkipHydration` should not be a binding (it should be a static attribute). * diff --git a/packages/compiler-cli/src/ngtsc/docs/src/class_extractor.ts b/packages/compiler-cli/src/ngtsc/docs/src/class_extractor.ts index 7c3c424861e94..970105117bf6f 100644 --- a/packages/compiler-cli/src/ngtsc/docs/src/class_extractor.ts +++ b/packages/compiler-cli/src/ngtsc/docs/src/class_extractor.ts @@ -9,10 +9,28 @@ import ts from 'typescript'; import {Reference} from '../../imports'; -import {DirectiveMeta, InputMapping, InputOrOutput, MetadataReader, NgModuleMeta, PipeMeta,} from '../../metadata'; +import { + DirectiveMeta, + InputMapping, + InputOrOutput, + MetadataReader, + NgModuleMeta, + PipeMeta, +} from '../../metadata'; import {ClassDeclaration} from '../../reflection'; -import {ClassEntry, DirectiveEntry, EntryType, InterfaceEntry, MemberEntry, MemberTags, MemberType, MethodEntry, PipeEntry, PropertyEntry,} from './entities'; +import { + ClassEntry, + DirectiveEntry, + EntryType, + InterfaceEntry, + MemberEntry, + MemberTags, + MemberType, + MethodEntry, + PipeEntry, + PropertyEntry, +} from './entities'; import {isAngularPrivateName} from './filters'; import {FunctionExtractor} from './function_extractor'; import {extractGenerics} from './generics_extractor'; @@ -23,35 +41,35 @@ import {extractResolvedTypeString} from './type_extractor'; // For the purpose of extraction, we can largely treat properties and accessors the same. /** A class member declaration that is *like* a property (including accessors) */ -type PropertyDeclarationLike = ts.PropertyDeclaration|ts.AccessorDeclaration; +type PropertyDeclarationLike = ts.PropertyDeclaration | ts.AccessorDeclaration; // For the purposes of extraction, we can treat interfaces as identical to classes // with a couple of shorthand types to normalize over the differences between them. /** Type representing either a class declaration ro an interface declaration. */ -type ClassDeclarationLike = ts.ClassDeclaration|ts.InterfaceDeclaration; +type ClassDeclarationLike = ts.ClassDeclaration | ts.InterfaceDeclaration; /** Type representing either a class or interface member. */ -type MemberElement = ts.ClassElement|ts.TypeElement; +type MemberElement = ts.ClassElement | ts.TypeElement; /** Type representing a signature element of an interface. */ -type SignatureElement = ts.CallSignatureDeclaration|ts.ConstructSignatureDeclaration; +type SignatureElement = ts.CallSignatureDeclaration | ts.ConstructSignatureDeclaration; /** * Type representing either: */ -type MethodLike = ts.MethodDeclaration|ts.MethodSignature; +type MethodLike = ts.MethodDeclaration | ts.MethodSignature; /** * Type representing either a class property declaration or an interface property signature. */ -type PropertyLike = PropertyDeclarationLike|ts.PropertySignature; +type PropertyLike = PropertyDeclarationLike | ts.PropertySignature; /** Extractor to pull info for API reference documentation for a TypeScript class or interface. */ class ClassExtractor { constructor( - protected declaration: ClassDeclaration&ClassDeclarationLike, - protected typeChecker: ts.TypeChecker, + protected declaration: ClassDeclaration & ClassDeclarationLike, + protected typeChecker: ts.TypeChecker, ) {} /** Extract docs info specific to classes. */ @@ -59,8 +77,9 @@ class ClassExtractor { return { name: this.declaration.name.text, isAbstract: this.isAbstract(), - entryType: ts.isInterfaceDeclaration(this.declaration) ? EntryType.Interface : - EntryType.UndecoratedClass, + entryType: ts.isInterfaceDeclaration(this.declaration) + ? EntryType.Interface + : EntryType.UndecoratedClass, members: this.extractSignatures().concat(this.extractAllClassMembers()), generics: extractGenerics(this.declaration), description: extractJsDocDescription(this.declaration), @@ -86,7 +105,7 @@ class ClassExtractor { } /** Extract docs for a class's members (methods and properties). */ - protected extractClassMember(memberDeclaration: MemberElement): MemberEntry|undefined { + protected extractClassMember(memberDeclaration: MemberElement): MemberEntry | undefined { if (this.isMethod(memberDeclaration) && !this.isImplementationForOverload(memberDeclaration)) { return this.extractMethod(memberDeclaration); } else if (this.isProperty(memberDeclaration)) { @@ -108,9 +127,9 @@ class ClassExtractor { /** Extracts docs for a class method. */ protected extractMethod(methodDeclaration: MethodLike): MethodEntry { const functionExtractor = new FunctionExtractor( - methodDeclaration.name.getText(), - methodDeclaration, - this.typeChecker, + methodDeclaration.name.getText(), + methodDeclaration, + this.typeChecker, ); return { ...functionExtractor.extract(), @@ -125,9 +144,9 @@ class ClassExtractor { // For construct signatures we are using `new` as the name of the function for now. // TODO: Consider exposing a new entry type for signature types. const functionExtractor = new FunctionExtractor( - ts.isConstructSignatureDeclaration(signature) ? 'new' : '', - signature, - this.typeChecker, + ts.isConstructSignatureDeclaration(signature) ? 'new' : '', + signature, + this.typeChecker, ); return { ...functionExtractor.extract(), @@ -157,7 +176,7 @@ class ClassExtractor { } /** Gets the tags for a member (protected, readonly, static, etc.) */ - protected getMemberTags(member: MethodLike|PropertyLike): MemberTags[] { + protected getMemberTags(member: MethodLike | PropertyLike): MemberTags[] { const tags: MemberTags[] = this.getMemberTagsFromModifiers(member.modifiers ?? []); if (member.questionToken) { @@ -225,7 +244,7 @@ class ClassExtractor { } /** Gets the doc tag corresponding to a class member modifier (readonly, protected, etc.). */ - private getTagForMemberModifier(mod: ts.ModifierLike): MemberTags|undefined { + private getTagForMemberModifier(mod: ts.ModifierLike): MemberTags | undefined { switch (mod.kind) { case ts.SyntaxKind.StaticKeyword: return MemberTags.Static; @@ -251,14 +270,17 @@ class ClassExtractor { */ private isMemberExcluded(member: MemberElement): boolean { return ( - !member.name || !this.isDocumentableMember(member) || - !!member.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.PrivateKeyword) || - member.name.getText() === 'prototype' || isAngularPrivateName(member.name.getText()) || - isInternal(member)); + !member.name || + !this.isDocumentableMember(member) || + !!member.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.PrivateKeyword) || + member.name.getText() === 'prototype' || + isAngularPrivateName(member.name.getText()) || + isInternal(member) + ); } /** Gets whether a class member is a method, property, or accessor. */ - private isDocumentableMember(member: ts.Node): member is MethodLike|PropertyLike { + private isDocumentableMember(member: ts.Node): member is MethodLike | PropertyLike { return this.isMethod(member) || this.isProperty(member) || ts.isAccessor(member); } @@ -275,10 +297,12 @@ class ClassExtractor { } /** Gets whether the given signature declaration is documentable. */ - private isDocumentableSignature(signature: ts.SignatureDeclaration): - signature is SignatureElement { + private isDocumentableSignature( + signature: ts.SignatureDeclaration, + ): signature is SignatureElement { return ( - ts.isConstructSignatureDeclaration(signature) || ts.isCallSignatureDeclaration(signature)); + ts.isConstructSignatureDeclaration(signature) || ts.isCallSignatureDeclaration(signature) + ); } /** Gets whether the declaration for this extractor is abstract. */ @@ -288,25 +312,25 @@ class ClassExtractor { } /** Gets whether a method is the concrete implementation for an overloaded function. */ - private isImplementationForOverload(method: MethodLike): boolean|undefined { + private isImplementationForOverload(method: MethodLike): boolean | undefined { // Method signatures (in an interface) are never implementations. if (method.kind === ts.SyntaxKind.MethodSignature) return false; const signature = this.typeChecker.getSignatureFromDeclaration(method); return ( - signature && - this.typeChecker.isImplementationOfOverload( - signature.declaration as ts.SignatureDeclaration)); + signature && + this.typeChecker.isImplementationOfOverload(signature.declaration as ts.SignatureDeclaration) + ); } } /** Extractor to pull info for API reference documentation for an Angular directive. */ class DirectiveExtractor extends ClassExtractor { constructor( - declaration: ClassDeclaration&ts.ClassDeclaration, - protected reference: Reference, - protected metadata: DirectiveMeta, - checker: ts.TypeChecker, + declaration: ClassDeclaration & ts.ClassDeclaration, + protected reference: Reference, + protected metadata: DirectiveMeta, + checker: ts.TypeChecker, ) { super(declaration, checker); } @@ -343,13 +367,13 @@ class DirectiveExtractor extends ClassExtractor { } /** Gets the input metadata for a directive property. */ - private getInputMetadata(prop: ts.PropertyDeclaration): InputMapping|undefined { + private getInputMetadata(prop: ts.PropertyDeclaration): InputMapping | undefined { const propName = prop.name.getText(); return this.metadata.inputs?.getByClassPropertyName(propName) ?? undefined; } /** Gets the output metadata for a directive property. */ - private getOutputMetadata(prop: ts.PropertyDeclaration): InputOrOutput|undefined { + private getOutputMetadata(prop: ts.PropertyDeclaration): InputOrOutput | undefined { const propName = prop.name.getText(); return this.metadata?.outputs?.getByClassPropertyName(propName) ?? undefined; } @@ -358,10 +382,10 @@ class DirectiveExtractor extends ClassExtractor { /** Extractor to pull info for API reference documentation for an Angular pipe. */ class PipeExtractor extends ClassExtractor { constructor( - declaration: ClassDeclaration&ts.ClassDeclaration, - protected reference: Reference, - private metadata: PipeMeta, - typeChecker: ts.TypeChecker, + declaration: ClassDeclaration & ts.ClassDeclaration, + protected reference: Reference, + private metadata: PipeMeta, + typeChecker: ts.TypeChecker, ) { super(declaration, typeChecker); } @@ -379,10 +403,10 @@ class PipeExtractor extends ClassExtractor { /** Extractor to pull info for API reference documentation for an Angular pipe. */ class NgModuleExtractor extends ClassExtractor { constructor( - declaration: ClassDeclaration&ts.ClassDeclaration, - protected reference: Reference, - private metadata: NgModuleMeta, - typeChecker: ts.TypeChecker, + declaration: ClassDeclaration & ts.ClassDeclaration, + protected reference: Reference, + private metadata: NgModuleMeta, + typeChecker: ts.TypeChecker, ) { super(declaration, typeChecker); } @@ -397,10 +421,10 @@ class NgModuleExtractor extends ClassExtractor { /** Extracts documentation info for a class, potentially including Angular-specific info. */ export function extractClass( - classDeclaration: ClassDeclaration&ts.ClassDeclaration, - metadataReader: MetadataReader, - typeChecker: ts.TypeChecker, - ): ClassEntry { + classDeclaration: ClassDeclaration & ts.ClassDeclaration, + metadataReader: MetadataReader, + typeChecker: ts.TypeChecker, +): ClassEntry { const ref = new Reference(classDeclaration); let extractor: ClassExtractor; @@ -424,9 +448,9 @@ export function extractClass( /** Extracts documentation info for an interface. */ export function extractInterface( - declaration: ts.InterfaceDeclaration, - typeChecker: ts.TypeChecker, - ): InterfaceEntry { + declaration: ts.InterfaceDeclaration, + typeChecker: ts.TypeChecker, +): InterfaceEntry { const extractor = new ClassExtractor(declaration, typeChecker); return extractor.extract(); } diff --git a/packages/compiler-cli/src/ngtsc/docs/src/constant_extractor.ts b/packages/compiler-cli/src/ngtsc/docs/src/constant_extractor.ts index d35fe8a362835..9dbc7805edc29 100644 --- a/packages/compiler-cli/src/ngtsc/docs/src/constant_extractor.ts +++ b/packages/compiler-cli/src/ngtsc/docs/src/constant_extractor.ts @@ -16,15 +16,18 @@ const LITERAL_AS_ENUM_TAG = 'object-literal-as-enum'; /** Extracts documentation entry for a constant. */ export function extractConstant( - declaration: ts.VariableDeclaration, typeChecker: ts.TypeChecker): ConstantEntry|EnumEntry { + declaration: ts.VariableDeclaration, + typeChecker: ts.TypeChecker, +): ConstantEntry | EnumEntry { // For constants specifically, we want to get the base type for any literal types. // For example, TypeScript by default extracts `const PI = 3.14` as PI having a type of the // literal `3.14`. We don't want this behavior for constants, since generally one wants the // _value_ of the constant to be able to change between releases without changing the type. // `VERSION` is a good example here; the version is always a `string`, but the actual value of // the version string shouldn't matter to the type system. - const resolvedType = - typeChecker.getBaseTypeOfLiteralType(typeChecker.getTypeAtLocation(declaration)); + const resolvedType = typeChecker.getBaseTypeOfLiteralType( + typeChecker.getTypeAtLocation(declaration), + ); // In the TS AST, the leading comment for a variable declaration is actually // on the ancestor `ts.VariableStatement` (since a single variable statement may @@ -35,14 +38,14 @@ export function extractConstant( const name = declaration.name.getText(); // Some constants have to be treated as enums for documentation purposes. - if (jsdocTags.some(tag => tag.name === LITERAL_AS_ENUM_TAG)) { + if (jsdocTags.some((tag) => tag.name === LITERAL_AS_ENUM_TAG)) { return { name, entryType: EntryType.Enum, members: extractLiteralPropertiesAsEnumMembers(declaration), rawComment, description, - jsdocTags: jsdocTags.filter(tag => tag.name !== LITERAL_AS_ENUM_TAG), + jsdocTags: jsdocTags.filter((tag) => tag.name !== LITERAL_AS_ENUM_TAG), }; } @@ -61,39 +64,45 @@ export function isSyntheticAngularConstant(declaration: ts.VariableDeclaration) return declaration.name.getText() === 'USED_FOR_NG_TYPE_CHECKING'; } - /** * Extracts the properties of a variable initialized as an object literal as if they were enum * members. Will throw for any variables that can't be statically analyzed easily. */ -function extractLiteralPropertiesAsEnumMembers(declaration: ts.VariableDeclaration): - EnumMemberEntry[] { +function extractLiteralPropertiesAsEnumMembers( + declaration: ts.VariableDeclaration, +): EnumMemberEntry[] { let initializer = declaration.initializer; // Unwrap `as` and parenthesized expressions. - while (initializer && - (ts.isAsExpression(initializer) || ts.isParenthesizedExpression(initializer))) { + while ( + initializer && + (ts.isAsExpression(initializer) || ts.isParenthesizedExpression(initializer)) + ) { initializer = initializer.expression; } if (initializer === undefined || !ts.isObjectLiteralExpression(initializer)) { - throw new Error(`Declaration tagged with "${ - LITERAL_AS_ENUM_TAG}" must be initialized to an object literal, but received ${ - initializer ? ts.SyntaxKind[initializer.kind] : 'undefined'}`); + throw new Error( + `Declaration tagged with "${LITERAL_AS_ENUM_TAG}" must be initialized to an object literal, but received ${ + initializer ? ts.SyntaxKind[initializer.kind] : 'undefined' + }`, + ); } - return initializer.properties.map(prop => { + return initializer.properties.map((prop) => { if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) { - throw new Error(`Property in declaration tagged with "${ - LITERAL_AS_ENUM_TAG}" must be a property assignment with a static name`); + throw new Error( + `Property in declaration tagged with "${LITERAL_AS_ENUM_TAG}" must be a property assignment with a static name`, + ); } if (!ts.isNumericLiteral(prop.initializer) && !ts.isStringLiteralLike(prop.initializer)) { - throw new Error(`Property in declaration tagged with "${ - LITERAL_AS_ENUM_TAG}" must be initialized to a number or string literal`); + throw new Error( + `Property in declaration tagged with "${LITERAL_AS_ENUM_TAG}" must be initialized to a number or string literal`, + ); } - return ({ + return { name: prop.name.text, type: `${declaration.name.getText()}.${prop.name.text}`, value: prop.initializer.getText(), @@ -101,6 +110,6 @@ function extractLiteralPropertiesAsEnumMembers(declaration: ts.VariableDeclarati jsdocTags: extractJsDocTags(prop), description: extractJsDocDescription(prop), memberTags: [], - }); + }; }); } diff --git a/packages/compiler-cli/src/ngtsc/docs/src/decorator_extractor.ts b/packages/compiler-cli/src/ngtsc/docs/src/decorator_extractor.ts index 6cd5e3e5caf67..42de88abc14a8 100644 --- a/packages/compiler-cli/src/ngtsc/docs/src/decorator_extractor.ts +++ b/packages/compiler-cli/src/ngtsc/docs/src/decorator_extractor.ts @@ -14,7 +14,9 @@ import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from './jsdo /** Extracts an API documentation entry for an Angular decorator. */ export function extractorDecorator( - declaration: ts.VariableDeclaration, typeChecker: ts.TypeChecker): DecoratorEntry { + declaration: ts.VariableDeclaration, + typeChecker: ts.TypeChecker, +): DecoratorEntry { const documentedNode = getDecoratorJsDocNode(declaration); const decoratorType = getDecoratorType(declaration); @@ -40,14 +42,19 @@ export function isDecoratorDeclaration(declaration: ts.VariableDeclaration): boo /** Gets whether an interface is the options interface for a decorator in the same file. */ export function isDecoratorOptionsInterface(declaration: ts.InterfaceDeclaration): boolean { - return declaration.getSourceFile().statements.some( - s => ts.isVariableStatement(s) && - s.declarationList.declarations.some( - d => isDecoratorDeclaration(d) && d.name.getText() === declaration.name.getText())); + return declaration + .getSourceFile() + .statements.some( + (s) => + ts.isVariableStatement(s) && + s.declarationList.declarations.some( + (d) => isDecoratorDeclaration(d) && d.name.getText() === declaration.name.getText(), + ), + ); } /** Gets the type of decorator, or undefined if the declaration is not a decorator. */ -function getDecoratorType(declaration: ts.VariableDeclaration): DecoratorType|undefined { +function getDecoratorType(declaration: ts.VariableDeclaration): DecoratorType | undefined { // All Angular decorators are initialized with one of `makeDecorator`, `makePropDecorator`, // or `makeParamDecorator`. const initializer = declaration.initializer?.getFullText() ?? ''; @@ -60,14 +67,18 @@ function getDecoratorType(declaration: ts.VariableDeclaration): DecoratorType|un /** Gets the doc entry for the options object for an Angular decorator */ function getDecoratorOptions( - declaration: ts.VariableDeclaration, typeChecker: ts.TypeChecker): PropertyEntry[] { + declaration: ts.VariableDeclaration, + typeChecker: ts.TypeChecker, +): PropertyEntry[] { const name = declaration.name.getText(); // Every decorator has an interface with its options in the same SourceFile. // Queries, however, are defined as a type alias pointing to an interface. - const optionsDeclaration = declaration.getSourceFile().statements.find(node => { - return (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) && - node.name.getText() === name; + const optionsDeclaration = declaration.getSourceFile().statements.find((node) => { + return ( + (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) && + node.name.getText() === name + ); }); if (!optionsDeclaration) { @@ -79,9 +90,10 @@ function getDecoratorOptions( // We hard-code the assumption that if the decorator's option type is a type alias, // it resolves to a single interface (this is true for all query decorators at time of // this writing). - const aliasedType = typeChecker.getTypeAtLocation((optionsDeclaration.type)); - optionsInterface = (aliasedType.getSymbol()?.getDeclarations() ?? - []).find(d => ts.isInterfaceDeclaration(d)) as ts.InterfaceDeclaration; + const aliasedType = typeChecker.getTypeAtLocation(optionsDeclaration.type); + optionsInterface = (aliasedType.getSymbol()?.getDeclarations() ?? []).find((d) => + ts.isInterfaceDeclaration(d), + ) as ts.InterfaceDeclaration; } else { optionsInterface = optionsDeclaration as ts.InterfaceDeclaration; } @@ -110,7 +122,7 @@ function getDecoratorJsDocNode(declaration: ts.VariableDeclaration): ts.HasJSDoc // Assume the existence of an interface in the same file with the same name // suffixed with "Decorator". - const decoratorInterface = declaration.getSourceFile().statements.find(s => { + const decoratorInterface = declaration.getSourceFile().statements.find((s) => { return ts.isInterfaceDeclaration(s) && s.name.getText() === `${name}Decorator`; }); @@ -119,7 +131,7 @@ function getDecoratorJsDocNode(declaration: ts.VariableDeclaration): ts.HasJSDoc } // The public-facing JsDoc for each decorator is on one of its interface's call signatures. - const callSignature = decoratorInterface.members.find(node => { + const callSignature = decoratorInterface.members.find((node) => { // The description block lives on one of the call signatures for this interface. return ts.isCallSignatureDeclaration(node) && extractRawJsDoc(node); }); diff --git a/packages/compiler-cli/src/ngtsc/docs/src/entities.ts b/packages/compiler-cli/src/ngtsc/docs/src/entities.ts index e4c80139f5c30..2bfac609b11e8 100644 --- a/packages/compiler-cli/src/ngtsc/docs/src/entities.ts +++ b/packages/compiler-cli/src/ngtsc/docs/src/entities.ts @@ -60,8 +60,8 @@ export interface JsDocTagEntry { /** Documentation entity for single generic parameter. */ export interface GenericEntry { name: string; - constraint: string|undefined; - default: string|undefined; + constraint: string | undefined; + default: string | undefined; } export interface SourceEntry { @@ -158,7 +158,7 @@ export interface PropertyEntry extends MemberEntry { } /** Sub-entry for a class method. */ -export type MethodEntry = MemberEntry&FunctionEntry; +export type MethodEntry = MemberEntry & FunctionEntry; /** Sub-entry for a single function parameter. */ export interface ParameterEntry { @@ -173,7 +173,7 @@ export interface ParameterEntry { export interface FunctionWithOverloads { name: string; signatures: FunctionEntry[]; - implementation: FunctionEntry|null; + implementation: FunctionEntry | null; } /** diff --git a/packages/compiler-cli/src/ngtsc/docs/src/enum_extractor.ts b/packages/compiler-cli/src/ngtsc/docs/src/enum_extractor.ts index 426d73baf30af..7fa9aec761c1d 100644 --- a/packages/compiler-cli/src/ngtsc/docs/src/enum_extractor.ts +++ b/packages/compiler-cli/src/ngtsc/docs/src/enum_extractor.ts @@ -6,15 +6,25 @@ * found in the LICENSE file at https://angular.io/license */ -import {EntryType, EnumEntry, EnumMemberEntry, MemberType} from '@angular/compiler-cli/src/ngtsc/docs/src/entities'; -import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from '@angular/compiler-cli/src/ngtsc/docs/src/jsdoc_extractor'; +import { + EntryType, + EnumEntry, + EnumMemberEntry, + MemberType, +} from '@angular/compiler-cli/src/ngtsc/docs/src/entities'; +import { + extractJsDocDescription, + extractJsDocTags, + extractRawJsDoc, +} from '@angular/compiler-cli/src/ngtsc/docs/src/jsdoc_extractor'; import {extractResolvedTypeString} from '@angular/compiler-cli/src/ngtsc/docs/src/type_extractor'; import ts from 'typescript'; - /** Extracts documentation entry for an enum. */ export function extractEnum( - declaration: ts.EnumDeclaration, typeChecker: ts.TypeChecker): EnumEntry { + declaration: ts.EnumDeclaration, + typeChecker: ts.TypeChecker, +): EnumEntry { return { name: declaration.name.getText(), entryType: EntryType.Enum, @@ -27,26 +37,32 @@ export function extractEnum( /** Extracts doc info for an enum's members. */ function extractEnumMembers( - declaration: ts.EnumDeclaration, checker: ts.TypeChecker): EnumMemberEntry[] { - return declaration.members.map(member => ({ - name: member.name.getText(), - type: extractResolvedTypeString(member, checker), - value: getEnumMemberValue(member), - memberType: MemberType.EnumItem, - jsdocTags: extractJsDocTags(member), - description: extractJsDocDescription(member), - memberTags: [], - })); + declaration: ts.EnumDeclaration, + checker: ts.TypeChecker, +): EnumMemberEntry[] { + return declaration.members.map((member) => ({ + name: member.name.getText(), + type: extractResolvedTypeString(member, checker), + value: getEnumMemberValue(member), + memberType: MemberType.EnumItem, + jsdocTags: extractJsDocTags(member), + description: extractJsDocDescription(member), + memberTags: [], + })); } /** Gets the explicitly assigned value for an enum member, or an empty string if there is none. */ function getEnumMemberValue(memberNode: ts.EnumMember): string { // If the enum member has a child number literal or string literal, // we use that literal as the "value" of the member. - const literal = memberNode.getChildren().find(n => { - return ts.isNumericLiteral(n) || ts.isStringLiteral(n) || - (ts.isPrefixUnaryExpression(n) && n.operator === ts.SyntaxKind.MinusToken && - ts.isNumericLiteral(n.operand)); + const literal = memberNode.getChildren().find((n) => { + return ( + ts.isNumericLiteral(n) || + ts.isStringLiteral(n) || + (ts.isPrefixUnaryExpression(n) && + n.operator === ts.SyntaxKind.MinusToken && + ts.isNumericLiteral(n.operand)) + ); }); return literal?.getText() ?? ''; } diff --git a/packages/compiler-cli/src/ngtsc/docs/src/extractor.ts b/packages/compiler-cli/src/ngtsc/docs/src/extractor.ts index 9be92d993d8a7..9503953b77b78 100644 --- a/packages/compiler-cli/src/ngtsc/docs/src/extractor.ts +++ b/packages/compiler-cli/src/ngtsc/docs/src/extractor.ts @@ -13,22 +13,32 @@ import {isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflectio import {extractClass, extractInterface} from './class_extractor'; import {extractConstant, isSyntheticAngularConstant} from './constant_extractor'; -import {extractorDecorator, isDecoratorDeclaration, isDecoratorOptionsInterface} from './decorator_extractor'; +import { + extractorDecorator, + isDecoratorDeclaration, + isDecoratorOptionsInterface, +} from './decorator_extractor'; import {DocEntry, DocEntryWithSourceInfo} from './entities'; import {extractEnum} from './enum_extractor'; import {isAngularPrivateName} from './filters'; import {FunctionExtractor} from './function_extractor'; -import {extractInitializerApiFunction, isInitializerApiFunction} from './initializer_api_function_extractor'; +import { + extractInitializerApiFunction, + isInitializerApiFunction, +} from './initializer_api_function_extractor'; import {extractTypeAlias} from './type_alias_extractor'; -type DeclarationWithExportName = readonly[string, ts.Declaration]; +type DeclarationWithExportName = readonly [string, ts.Declaration]; /** * Extracts all information from a source file that may be relevant for generating * public API documentation. */ export class DocsExtractor { - constructor(private typeChecker: ts.TypeChecker, private metadataReader: MetadataReader) {} + constructor( + private typeChecker: ts.TypeChecker, + private metadataReader: MetadataReader, + ) {} /** * Gets the set of all documentable entries from a source file, including @@ -71,7 +81,7 @@ export class DocsExtractor { } /** Extract the doc entry for a single declaration. */ - private extractDeclaration(node: ts.Declaration): DocEntry|null { + private extractDeclaration(node: ts.Declaration): DocEntry | null { // Ignore anonymous classes. if (isNamedClassDeclaration(node)) { return extractClass(node, this.metadataReader, this.typeChecker); @@ -92,8 +102,9 @@ export class DocsExtractor { } if (ts.isVariableDeclaration(node) && !isSyntheticAngularConstant(node)) { - return isDecoratorDeclaration(node) ? extractorDecorator(node, this.typeChecker) : - extractConstant(node, this.typeChecker); + return isDecoratorDeclaration(node) + ? extractorDecorator(node, this.typeChecker) + : extractConstant(node, this.typeChecker); } if (ts.isTypeAliasDeclaration(node)) { @@ -115,9 +126,9 @@ export class DocsExtractor { const exportedDeclarationMap = reflector.getExportsOfModule(sourceFile); // Augment each declaration with the exported name in the public API. - let exportedDeclarations = - Array.from(exportedDeclarationMap?.entries() ?? []) - .map(([exportName, declaration]) => [exportName, declaration.node] as const); + let exportedDeclarations = Array.from(exportedDeclarationMap?.entries() ?? []).map( + ([exportName, declaration]) => [exportName, declaration.node] as const, + ); // Cache the declaration count since we're going to be appending more declarations as // we iterate. @@ -129,7 +140,9 @@ export class DocsExtractor { const [exportName, declaration] = exportedDeclarations[i]; if (ts.isFunctionDeclaration(declaration)) { const extractor = new FunctionExtractor(exportName, declaration, this.typeChecker); - const overloads = extractor.getOverloads().map(overload => [exportName, overload] as const); + const overloads = extractor + .getOverloads() + .map((overload) => [exportName, overload] as const); exportedDeclarations.push(...overloads); } @@ -138,7 +151,8 @@ export class DocsExtractor { // Sort the declaration nodes into declaration position because their order is lost in // reading from the export map. This is primarily useful for testing and debugging. return exportedDeclarations.sort( - ([a, declarationA], [b, declarationB]) => declarationA.pos - declarationB.pos); + ([a, declarationA], [b, declarationB]) => declarationA.pos - declarationB.pos, + ); } } @@ -161,17 +175,17 @@ function isIgnoredInterface(node: ts.InterfaceDeclaration) { * never has JSDoc tags attached, but rather the parent variable statement. */ function isIgnoredDocEntry(entry: DocEntry): boolean { - const isDocsPrivate = entry.jsdocTags.find(e => e.name === 'docsPrivate'); + const isDocsPrivate = entry.jsdocTags.find((e) => e.name === 'docsPrivate'); if (isDocsPrivate !== undefined && isDocsPrivate.comment === '') { throw new Error( - `Docs extraction: Entry "${entry.name}" is marked as ` + - `"@docsPrivate" but without reasoning.`); + `Docs extraction: Entry "${entry.name}" is marked as ` + + `"@docsPrivate" but without reasoning.`, + ); } return isDocsPrivate !== undefined; } - function getRelativeFilePath(sourceFile: ts.SourceFile, rootDir: string): string { const fullPath = sourceFile.fileName; const relativePath = fullPath.replace(rootDir, ''); diff --git a/packages/compiler-cli/src/ngtsc/docs/src/function_extractor.ts b/packages/compiler-cli/src/ngtsc/docs/src/function_extractor.ts index 1b45d3e2a9515..90bee13e07da7 100644 --- a/packages/compiler-cli/src/ngtsc/docs/src/function_extractor.ts +++ b/packages/compiler-cli/src/ngtsc/docs/src/function_extractor.ts @@ -13,21 +13,27 @@ import {extractGenerics} from './generics_extractor'; import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from './jsdoc_extractor'; import {extractResolvedTypeString} from './type_extractor'; -export type FunctionLike = ts.FunctionDeclaration|ts.MethodDeclaration|ts.MethodSignature| - ts.CallSignatureDeclaration|ts.ConstructSignatureDeclaration; +export type FunctionLike = + | ts.FunctionDeclaration + | ts.MethodDeclaration + | ts.MethodSignature + | ts.CallSignatureDeclaration + | ts.ConstructSignatureDeclaration; export class FunctionExtractor { constructor( - private name: string, private declaration: FunctionLike, - private typeChecker: ts.TypeChecker) {} + private name: string, + private declaration: FunctionLike, + private typeChecker: ts.TypeChecker, + ) {} extract(): FunctionEntry { // TODO: is there any real situation in which the signature would not be available here? // Is void a better type? const signature = this.typeChecker.getSignatureFromDeclaration(this.declaration); - const returnType = signature ? - this.typeChecker.typeToString(this.typeChecker.getReturnTypeOfSignature(signature)) : - 'unknown'; + const returnType = signature + ? this.typeChecker.typeToString(this.typeChecker.getReturnTypeOfSignature(signature)) + : 'unknown'; return { params: extractAllParams(this.declaration.parameters, this.typeChecker), @@ -59,8 +65,11 @@ export class FunctionExtractor { // Skip the declaration we started with. if (overloadDeclaration?.pos === this.declaration.pos) continue; - if (overloadDeclaration && ts.isFunctionDeclaration(overloadDeclaration) && - overloadDeclaration.modifiers?.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword)) { + if ( + overloadDeclaration && + ts.isFunctionDeclaration(overloadDeclaration) && + overloadDeclaration.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.ExportKeyword) + ) { overloads.push(overloadDeclaration); } } @@ -69,20 +78,23 @@ export class FunctionExtractor { return overloads; } - private getSymbol(): ts.Symbol|undefined { - return this.typeChecker.getSymbolsInScope(this.declaration, ts.SymbolFlags.Function) - .find(s => s.name === this.declaration.name?.getText()); + private getSymbol(): ts.Symbol | undefined { + return this.typeChecker + .getSymbolsInScope(this.declaration, ts.SymbolFlags.Function) + .find((s) => s.name === this.declaration.name?.getText()); } } /** Extracts parameters of the given parameter declaration AST nodes. */ export function extractAllParams( - params: ts.NodeArray, typeChecker: ts.TypeChecker): ParameterEntry[] { - return params.map(param => ({ - name: param.name.getText(), - description: extractJsDocDescription(param), - type: extractResolvedTypeString(param, typeChecker), - isOptional: !!(param.questionToken || param.initializer), - isRestParam: !!param.dotDotDotToken, - })); + params: ts.NodeArray, + typeChecker: ts.TypeChecker, +): ParameterEntry[] { + return params.map((param) => ({ + name: param.name.getText(), + description: extractJsDocDescription(param), + type: extractResolvedTypeString(param, typeChecker), + isOptional: !!(param.questionToken || param.initializer), + isRestParam: !!param.dotDotDotToken, + })); } diff --git a/packages/compiler-cli/src/ngtsc/docs/src/generics_extractor.ts b/packages/compiler-cli/src/ngtsc/docs/src/generics_extractor.ts index cda905b382663..40e195fe46296 100644 --- a/packages/compiler-cli/src/ngtsc/docs/src/generics_extractor.ts +++ b/packages/compiler-cli/src/ngtsc/docs/src/generics_extractor.ts @@ -11,15 +11,16 @@ import ts from 'typescript'; import {GenericEntry} from './entities'; type DeclarationWithTypeParams = { - typeParameters?: ts.NodeArray|undefined + typeParameters?: ts.NodeArray | undefined; }; /** Gets a list of all the generic type parameters for a declaration. */ export function extractGenerics(declaration: DeclarationWithTypeParams): GenericEntry[] { - return declaration.typeParameters?.map(typeParam => ({ - name: typeParam.name.getText(), - constraint: typeParam.constraint?.getText(), - default: typeParam.default?.getText(), - })) ?? - []; + return ( + declaration.typeParameters?.map((typeParam) => ({ + name: typeParam.name.getText(), + constraint: typeParam.constraint?.getText(), + default: typeParam.default?.getText(), + })) ?? [] + ); } diff --git a/packages/compiler-cli/src/ngtsc/docs/src/initializer_api_function_extractor.ts b/packages/compiler-cli/src/ngtsc/docs/src/initializer_api_function_extractor.ts index 703937e658ac5..da4d6a2db0a86 100644 --- a/packages/compiler-cli/src/ngtsc/docs/src/initializer_api_function_extractor.ts +++ b/packages/compiler-cli/src/ngtsc/docs/src/initializer_api_function_extractor.ts @@ -8,7 +8,12 @@ import ts from 'typescript'; -import {EntryType, FunctionWithOverloads, InitializerApiFunctionEntry, JsDocTagEntry} from './entities'; +import { + EntryType, + FunctionWithOverloads, + InitializerApiFunctionEntry, + JsDocTagEntry, +} from './entities'; import {extractAllParams} from './function_extractor'; import {extractGenerics} from './generics_extractor'; import {extractJsDocDescription, extractJsDocTags, extractRawJsDoc} from './jsdoc_extractor'; @@ -25,8 +30,10 @@ const initializerApiTag = 'initializerApiFunction'; * Note: The node may be a function overload signature that is automatically * resolved to its implementation to detect the JSDoc tag. */ -export function isInitializerApiFunction(node: ts.Node, typeChecker: ts.TypeChecker): - node is ts.VariableDeclaration|ts.FunctionDeclaration { +export function isInitializerApiFunction( + node: ts.Node, + typeChecker: ts.TypeChecker, +): node is ts.VariableDeclaration | ts.FunctionDeclaration { // If this is matching an overload signature, resolve to the implementation // as it would hold the `@initializerApiFunction` tag. if (ts.isFunctionDeclaration(node) && node.name !== undefined && node.body === undefined) { @@ -45,7 +52,7 @@ export function isInitializerApiFunction(node: ts.Node, typeChecker: ts.TypeChec return false; } const tags = ts.getJSDocTags(tagContainer); - return tags.some(t => t.tagName.text === initializerApiTag); + return tags.some((t) => t.tagName.text === initializerApiTag); } /** @@ -53,8 +60,9 @@ export function isInitializerApiFunction(node: ts.Node, typeChecker: ts.TypeChec * a docs entry that can be rendered to represent the API function. */ export function extractInitializerApiFunction( - node: ts.VariableDeclaration|ts.FunctionDeclaration, - typeChecker: ts.TypeChecker): InitializerApiFunctionEntry { + node: ts.VariableDeclaration | ts.FunctionDeclaration, + typeChecker: ts.TypeChecker, +): InitializerApiFunctionEntry { if (node.name === undefined || !ts.isIdentifier(node.name)) { throw new Error(`Initializer API: Expected literal variable name.`); } @@ -68,8 +76,11 @@ export function extractInitializerApiFunction( const type = typeChecker.getTypeAtLocation(node); // Top-level call signatures. E.g. `input()`, `input(initialValue: ReadT)`. etc. - const callFunction: FunctionWithOverloads = - extractFunctionWithOverloads(name, type.getCallSignatures(), typeChecker); + const callFunction: FunctionWithOverloads = extractFunctionWithOverloads( + name, + type.getCallSignatures(), + typeChecker, + ); // Sub-functions like `input.required()`. const subFunctions: FunctionWithOverloads[] = []; @@ -78,12 +89,14 @@ export function extractInitializerApiFunction( const subDecl = property.getDeclarations()?.[0]; if (subDecl === undefined || !ts.isPropertySignature(subDecl)) { throw new Error( - `Initializer API: Could not resolve declaration of sub-property: ${name}.${subName}`); + `Initializer API: Could not resolve declaration of sub-property: ${name}.${subName}`, + ); } const subType = typeChecker.getTypeAtLocation(subDecl); subFunctions.push( - extractFunctionWithOverloads(subName, subType.getCallSignatures(), typeChecker)); + extractFunctionWithOverloads(subName, subType.getCallSignatures(), typeChecker), + ); } let jsdocTags: JsDocTagEntry[]; @@ -109,8 +122,11 @@ export function extractInitializerApiFunction( jsdocTags: extractJsDocTags(implementation), params: extractAllParams(implementation.parameters, typeChecker), rawComment: extractRawJsDoc(implementation), - returnType: typeChecker.typeToString(typeChecker.getReturnTypeOfSignature( - typeChecker.getSignatureFromDeclaration(implementation)!)), + returnType: typeChecker.typeToString( + typeChecker.getReturnTypeOfSignature( + typeChecker.getSignatureFromDeclaration(implementation)!, + ), + ), }; jsdocTags = callFunction.implementation.jsdocTags; @@ -123,11 +139,12 @@ export function extractInitializerApiFunction( } // Extract additional docs metadata from the initializer API JSDoc tag. - const metadataTag = jsdocTags.find(t => t.name === initializerApiTag); + const metadataTag = jsdocTags.find((t) => t.name === initializerApiTag); if (metadataTag === undefined) { throw new Error( - 'Initializer API: Detected initializer API function does ' + - `not have "@initializerApiFunction" tag: ${name}`); + 'Initializer API: Detected initializer API function does ' + + `not have "@initializerApiFunction" tag: ${name}`, + ); } let parsedMetadata: InitializerApiFunctionEntry['__docsMetadata__'] = undefined; @@ -158,7 +175,7 @@ export function extractInitializerApiFunction( * but the JSDoc tag is not attached to the node, but to the containing variable * statement. */ -function getContainerVariableStatement(node: ts.VariableDeclaration): ts.VariableStatement|null { +function getContainerVariableStatement(node: ts.VariableDeclaration): ts.VariableStatement | null { if (!ts.isVariableDeclarationList(node.parent)) { return null; } @@ -170,7 +187,7 @@ function getContainerVariableStatement(node: ts.VariableDeclaration): ts.Variabl /** Filters the list signatures to valid initializer API signatures. */ function filterSignatureDeclarations(signatures: readonly ts.Signature[]) { - const result: Array = []; + const result: Array = []; for (const signature of signatures) { const decl = signature.getDeclaration(); if (ts.isFunctionDeclaration(decl) || ts.isCallSignatureDeclaration(decl)) { @@ -190,24 +207,25 @@ function filterSignatureDeclarations(signatures: readonly ts.Signature[]) { * that is statically retrievable. The constant holds the overall API description. */ function extractFunctionWithOverloads( - name: string, signatures: readonly ts.Signature[], - typeChecker: ts.TypeChecker): FunctionWithOverloads { + name: string, + signatures: readonly ts.Signature[], + typeChecker: ts.TypeChecker, +): FunctionWithOverloads { return { name, - signatures: - filterSignatureDeclarations(signatures) - .map(s => ({ - name, - entryType: EntryType.Function, - description: extractJsDocDescription(s), - generics: extractGenerics(s), - isNewType: false, - jsdocTags: extractJsDocTags(s), - params: extractAllParams(s.parameters, typeChecker), - rawComment: extractRawJsDoc(s), - returnType: typeChecker.typeToString(typeChecker.getReturnTypeOfSignature( - typeChecker.getSignatureFromDeclaration(s)!)), - })), + signatures: filterSignatureDeclarations(signatures).map((s) => ({ + name, + entryType: EntryType.Function, + description: extractJsDocDescription(s), + generics: extractGenerics(s), + isNewType: false, + jsdocTags: extractJsDocTags(s), + params: extractAllParams(s.parameters, typeChecker), + rawComment: extractRawJsDoc(s), + returnType: typeChecker.typeToString( + typeChecker.getReturnTypeOfSignature(typeChecker.getSignatureFromDeclaration(s)!), + ), + })), // Implementation may be populated later. implementation: null, }; @@ -215,12 +233,15 @@ function extractFunctionWithOverloads( /** Finds the implementation of the given function declaration overload signature. */ function findImplementationOfFunction( - node: ts.FunctionDeclaration, typeChecker: ts.TypeChecker): ts.FunctionDeclaration|undefined { + node: ts.FunctionDeclaration, + typeChecker: ts.TypeChecker, +): ts.FunctionDeclaration | undefined { if (node.body !== undefined || node.name === undefined) { return node; } const symbol = typeChecker.getSymbolAtLocation(node.name); return symbol?.declarations?.find( - (s): s is ts.FunctionDeclaration => ts.isFunctionDeclaration(s) && s.body !== undefined); + (s): s is ts.FunctionDeclaration => ts.isFunctionDeclaration(s) && s.body !== undefined, + ); } diff --git a/packages/compiler-cli/src/ngtsc/docs/src/internal.ts b/packages/compiler-cli/src/ngtsc/docs/src/internal.ts index 197ab8848fd59..fd00f0c6e500b 100644 --- a/packages/compiler-cli/src/ngtsc/docs/src/internal.ts +++ b/packages/compiler-cli/src/ngtsc/docs/src/internal.ts @@ -15,8 +15,9 @@ import {extractJsDocTags} from './jsdoc_extractor'; */ export function isInternal(member: ts.HasJSDoc): boolean { return ( - extractJsDocTags(member).some((tag) => tag.name === 'internal') || - hasLeadingInternalComment(member)); + extractJsDocTags(member).some((tag) => tag.name === 'internal') || + hasLeadingInternalComment(member) + ); } /* @@ -25,14 +26,14 @@ export function isInternal(member: ts.HasJSDoc): boolean { function hasLeadingInternalComment(member: ts.Node): boolean { const memberText = member.getSourceFile().text; return ( - ts.reduceEachLeadingCommentRange( - memberText, - member.getFullStart(), - (pos, end, kind, hasTrailingNewLine, containsInternal) => { - return containsInternal || memberText.slice(pos, end).includes('@internal'); - }, - /* state */ false, - /* initial */ false, - ) ?? - false); + ts.reduceEachLeadingCommentRange( + memberText, + member.getFullStart(), + (pos, end, kind, hasTrailingNewLine, containsInternal) => { + return containsInternal || memberText.slice(pos, end).includes('@internal'); + }, + /* state */ false, + /* initial */ false, + ) ?? false + ); } diff --git a/packages/compiler-cli/src/ngtsc/docs/src/jsdoc_extractor.ts b/packages/compiler-cli/src/ngtsc/docs/src/jsdoc_extractor.ts index cf8cebf1cdf9f..141cb763081d1 100644 --- a/packages/compiler-cli/src/ngtsc/docs/src/jsdoc_extractor.ts +++ b/packages/compiler-cli/src/ngtsc/docs/src/jsdoc_extractor.ts @@ -15,13 +15,13 @@ import {JsDocTagEntry} from './entities'; * decorators in JsDoc blocks so that they're not parsed as JsDoc tags. */ const decoratorExpression = - /@(?=(Injectable|Component|Directive|Pipe|NgModule|Input|Output|HostBinding|HostListener|Inject|Optional|Self|Host|SkipSelf))/g; + /@(?=(Injectable|Component|Directive|Pipe|NgModule|Input|Output|HostBinding|HostListener|Inject|Optional|Self|Host|SkipSelf))/g; /** Gets the set of JsDoc tags applied to a node. */ export function extractJsDocTags(node: ts.HasJSDoc): JsDocTagEntry[] { const escapedNode = getEscapedNode(node); - return ts.getJSDocTags(escapedNode).map(t => { + return ts.getJSDocTags(escapedNode).map((t) => { return { name: t.tagName.getText(), comment: unescapeAngularDecorators(ts.getTextOfJSDocComment(t.comment) ?? ''), @@ -39,13 +39,13 @@ export function extractJsDocDescription(node: ts.HasJSDoc): string { // If the node is a top-level statement (const, class, function, etc.), we will get // a `ts.JSDoc` here. If the node is a `ts.ParameterDeclaration`, we will get // a `ts.JSDocParameterTag`. - const commentOrTag = ts.getJSDocCommentsAndTags(escapedNode).find(d => { + const commentOrTag = ts.getJSDocCommentsAndTags(escapedNode).find((d) => { return ts.isJSDoc(d) || ts.isJSDocParameterTag(d); }); const comment = commentOrTag?.comment ?? ''; const description = - typeof comment === 'string' ? comment : ts.getTextOfJSDocComment(comment) ?? ''; + typeof comment === 'string' ? comment : ts.getTextOfJSDocComment(comment) ?? ''; return unescapeAngularDecorators(description); } @@ -76,7 +76,7 @@ function getEscapedNode(node: ts.HasJSDoc): ts.HasJSDoc { const rawComment = extractRawJsDoc(node); const escaped = escapeAngularDecorators(rawComment); const file = ts.createSourceFile('x.ts', `${escaped}class X {}`, ts.ScriptTarget.ES2020, true); - return file.statements.find(s => ts.isClassDeclaration(s)) as ts.ClassDeclaration; + return file.statements.find((s) => ts.isClassDeclaration(s)) as ts.ClassDeclaration; } /** Escape the `@` character for Angular decorators. */ diff --git a/packages/compiler-cli/src/ngtsc/entry_point/src/generator.ts b/packages/compiler-cli/src/ngtsc/entry_point/src/generator.ts index bd3538d4a59d9..abbcd133f4372 100644 --- a/packages/compiler-cli/src/ngtsc/entry_point/src/generator.ts +++ b/packages/compiler-cli/src/ngtsc/entry_point/src/generator.ts @@ -19,10 +19,12 @@ export class FlatIndexGenerator implements TopLevelShimGenerator { readonly shouldEmit = true; constructor( - readonly entryPoint: AbsoluteFsPath, relativeFlatIndexPath: string, - readonly moduleName: string|null) { + readonly entryPoint: AbsoluteFsPath, + relativeFlatIndexPath: string, + readonly moduleName: string | null, + ) { this.flatIndexPath = - join(dirname(entryPoint), relativeFlatIndexPath).replace(/\.js$/, '') + '.ts'; + join(dirname(entryPoint), relativeFlatIndexPath).replace(/\.js$/, '') + '.ts'; } makeTopLevelShim(): ts.SourceFile { @@ -34,7 +36,12 @@ export class FlatIndexGenerator implements TopLevelShimGenerator { export * from '${relativeEntryPoint}'; `; const genFile = ts.createSourceFile( - this.flatIndexPath, contents, ts.ScriptTarget.ES2015, true, ts.ScriptKind.TS); + this.flatIndexPath, + contents, + ts.ScriptTarget.ES2015, + true, + ts.ScriptKind.TS, + ); if (this.moduleName !== null) { genFile.moduleName = this.moduleName; } diff --git a/packages/compiler-cli/src/ngtsc/entry_point/src/logic.ts b/packages/compiler-cli/src/ngtsc/entry_point/src/logic.ts index f929b80bfb72e..a2d65d4ba5ee8 100644 --- a/packages/compiler-cli/src/ngtsc/entry_point/src/logic.ts +++ b/packages/compiler-cli/src/ngtsc/entry_point/src/logic.ts @@ -9,13 +9,14 @@ import {AbsoluteFsPath, getFileSystem} from '../../file_system'; import {isNonDeclarationTsPath} from '../../util/src/typescript'; -export function findFlatIndexEntryPoint(rootFiles: ReadonlyArray): AbsoluteFsPath| - null { +export function findFlatIndexEntryPoint( + rootFiles: ReadonlyArray, +): AbsoluteFsPath | null { // There are two ways for a file to be recognized as the flat module index: // 1) if it's the only file!!!!!! // 2) (deprecated) if it's named 'index.ts' and has the shortest path of all such files. - const tsFiles = rootFiles.filter(file => isNonDeclarationTsPath(file)); - let resolvedEntryPoint: AbsoluteFsPath|null = null; + const tsFiles = rootFiles.filter((file) => isNonDeclarationTsPath(file)); + let resolvedEntryPoint: AbsoluteFsPath | null = null; if (tsFiles.length === 1) { // There's only one file - this is the flat module index. @@ -27,8 +28,10 @@ export function findFlatIndexEntryPoint(rootFiles: ReadonlyArray // // This behavior is DEPRECATED and only exists to support existing usages. for (const tsFile of tsFiles) { - if (getFileSystem().basename(tsFile) === 'index.ts' && - (resolvedEntryPoint === null || tsFile.length <= resolvedEntryPoint.length)) { + if ( + getFileSystem().basename(tsFile) === 'index.ts' && + (resolvedEntryPoint === null || tsFile.length <= resolvedEntryPoint.length) + ) { resolvedEntryPoint = tsFile; } } diff --git a/packages/compiler-cli/src/ngtsc/entry_point/src/private_export_checker.ts b/packages/compiler-cli/src/ngtsc/entry_point/src/private_export_checker.ts index fbe94d8bba735..d6e4867e00ccd 100644 --- a/packages/compiler-cli/src/ngtsc/entry_point/src/private_export_checker.ts +++ b/packages/compiler-cli/src/ngtsc/entry_point/src/private_export_checker.ts @@ -36,7 +36,10 @@ import {ReferenceGraph} from './reference_graph'; * properly. */ export function checkForPrivateExports( - entryPoint: ts.SourceFile, checker: ts.TypeChecker, refGraph: ReferenceGraph): ts.Diagnostic[] { + entryPoint: ts.SourceFile, + checker: ts.TypeChecker, + refGraph: ReferenceGraph, +): ts.Diagnostic[] { const diagnostics: ts.Diagnostic[] = []; // Firstly, compute the exports of the entry point. These are all the Exported classes. @@ -51,7 +54,7 @@ export function checkForPrivateExports( // Loop through the exported symbols, de-alias if needed, and add them to `topLevelExports`. // TODO(alxhub): use proper iteration when build.sh is removed. (#27762) - exportedSymbols.forEach(symbol => { + exportedSymbols.forEach((symbol) => { if (symbol.flags & ts.SymbolFlags.Alias) { symbol = checker.getAliasedSymbol(symbol); } @@ -68,9 +71,9 @@ export function checkForPrivateExports( // Loop through each Exported class. // TODO(alxhub): use proper iteration when the legacy build is removed. (#27762) - topLevelExports.forEach(mainExport => { + topLevelExports.forEach((mainExport) => { // Loop through each class made Visible by the Exported class. - refGraph.transitiveReferencesOf(mainExport).forEach(transitiveReference => { + refGraph.transitiveReferencesOf(mainExport).forEach((transitiveReference) => { // Skip classes which have already been checked. if (checkedSet.has(transitiveReference)) { return; @@ -90,7 +93,7 @@ export function checkForPrivateExports( let visibleVia = 'NgModule exports'; const transitivePath = refGraph.pathFrom(mainExport, transitiveReference); if (transitivePath !== null) { - visibleVia = transitivePath.map(seg => getNameOfDeclaration(seg)).join(' -> '); + visibleVia = transitivePath.map((seg) => getNameOfDeclaration(seg)).join(' -> '); } const diagnostic: ts.Diagnostic = { @@ -98,9 +101,7 @@ export function checkForPrivateExports( code: ngErrorCode(ErrorCode.SYMBOL_NOT_EXPORTED), file: transitiveReference.getSourceFile(), ...getPosOfDeclaration(transitiveReference), - messageText: `Unsupported private ${descriptor} ${name}. This ${ - descriptor} is visible to consumers via ${ - visibleVia}, but is not exported from the top-level library entrypoint.`, + messageText: `Unsupported private ${descriptor} ${name}. This ${descriptor} is visible to consumers via ${visibleVia}, but is not exported from the top-level library entrypoint.`, }; diagnostics.push(diagnostic); @@ -111,7 +112,7 @@ export function checkForPrivateExports( return diagnostics; } -function getPosOfDeclaration(decl: DeclarationNode): {start: number, length: number} { +function getPosOfDeclaration(decl: DeclarationNode): {start: number; length: number} { const node: ts.Node = getIdentifierOfDeclaration(decl) || decl; return { start: node.getStart(), @@ -119,10 +120,14 @@ function getPosOfDeclaration(decl: DeclarationNode): {start: number, length: num }; } -function getIdentifierOfDeclaration(decl: DeclarationNode): ts.Identifier|null { - if ((ts.isClassDeclaration(decl) || ts.isVariableDeclaration(decl) || - ts.isFunctionDeclaration(decl)) && - decl.name !== undefined && ts.isIdentifier(decl.name)) { +function getIdentifierOfDeclaration(decl: DeclarationNode): ts.Identifier | null { + if ( + (ts.isClassDeclaration(decl) || + ts.isVariableDeclaration(decl) || + ts.isFunctionDeclaration(decl)) && + decl.name !== undefined && + ts.isIdentifier(decl.name) + ) { return decl.name; } else { return null; diff --git a/packages/compiler-cli/src/ngtsc/entry_point/src/reference_graph.ts b/packages/compiler-cli/src/ngtsc/entry_point/src/reference_graph.ts index 168943cf6a98b..e4a516ae8fd57 100644 --- a/packages/compiler-cli/src/ngtsc/entry_point/src/reference_graph.ts +++ b/packages/compiler-cli/src/ngtsc/entry_point/src/reference_graph.ts @@ -24,11 +24,11 @@ export class ReferenceGraph { return set; } - pathFrom(source: T, target: T): T[]|null { + pathFrom(source: T, target: T): T[] | null { return this.collectPathFrom(source, target, new Set()); } - private collectPathFrom(source: T, target: T, seen: Set): T[]|null { + private collectPathFrom(source: T, target: T, seen: Set): T[] | null { if (source === target) { // Looking for a path from the target to itself - that path is just the target. This is the // "base case" of the search. @@ -46,8 +46,8 @@ export class ReferenceGraph { } else { // Look through the outgoing edges of `source`. // TODO(alxhub): use proper iteration when the legacy build is removed. (#27762) - let candidatePath: T[]|null = null; - this.references.get(source)!.forEach(edge => { + let candidatePath: T[] | null = null; + this.references.get(source)!.forEach((edge) => { // Early exit if a path has already been found. if (candidatePath !== null) { return; @@ -67,7 +67,7 @@ export class ReferenceGraph { private collectTransitiveReferences(set: Set, decl: T): void { if (this.references.has(decl)) { // TODO(alxhub): use proper iteration when the legacy build is removed. (#27762) - this.references.get(decl)!.forEach(ref => { + this.references.get(decl)!.forEach((ref) => { if (!set.has(ref)) { set.add(ref); this.collectTransitiveReferences(set, ref); diff --git a/packages/compiler-cli/src/ngtsc/entry_point/test/entry_point_spec.ts b/packages/compiler-cli/src/ngtsc/entry_point/test/entry_point_spec.ts index 6c9a61fd2a438..15ffabbbb77ca 100644 --- a/packages/compiler-cli/src/ngtsc/entry_point/test/entry_point_spec.ts +++ b/packages/compiler-cli/src/ngtsc/entry_point/test/entry_point_spec.ts @@ -1,4 +1,3 @@ - /** * @license * Copyright Google LLC All Rights Reserved. @@ -13,7 +12,7 @@ import {findFlatIndexEntryPoint} from '../src/logic'; runInEachFileSystem(() => { describe('entry_point logic', () => { let _: typeof absoluteFrom; - beforeEach(() => _ = absoluteFrom); + beforeEach(() => (_ = absoluteFrom)); describe('findFlatIndexEntryPoint', () => { it('should use the only source file if only a single one is specified', () => { @@ -21,9 +20,9 @@ runInEachFileSystem(() => { }); it('should use the shortest source file ending with "index.ts" for multiple files', () => { - expect(findFlatIndexEntryPoint([ - _('/src/deep/index.ts'), _('/src/index.ts'), _('/index.ts') - ])).toBe(_('/index.ts')); + expect( + findFlatIndexEntryPoint([_('/src/deep/index.ts'), _('/src/index.ts'), _('/index.ts')]), + ).toBe(_('/index.ts')); }); }); }); diff --git a/packages/compiler-cli/src/ngtsc/entry_point/test/reference_graph_spec.ts b/packages/compiler-cli/src/ngtsc/entry_point/test/reference_graph_spec.ts index 287065813888e..77a732948cdbe 100644 --- a/packages/compiler-cli/src/ngtsc/entry_point/test/reference_graph_spec.ts +++ b/packages/compiler-cli/src/ngtsc/entry_point/test/reference_graph_spec.ts @@ -1,4 +1,3 @@ - /** * @license * Copyright Google LLC All Rights Reserved. @@ -46,7 +45,7 @@ describe('entry_point reference graph', () => { expect(graph.pathFrom('beta', 'alpha')).toEqual(['beta', 'delta', 'alpha']); }); - it('should not report a path that doesn\'t exist', () => { + it("should not report a path that doesn't exist", () => { expect(graph.pathFrom('gamma', 'beta')).toBeNull(); }); }); diff --git a/packages/compiler-cli/src/ngtsc/file_system/index.ts b/packages/compiler-cli/src/ngtsc/file_system/index.ts index 7b25db8e8e0d0..24e90f1f1c72a 100644 --- a/packages/compiler-cli/src/ngtsc/file_system/index.ts +++ b/packages/compiler-cli/src/ngtsc/file_system/index.ts @@ -6,8 +6,31 @@ * found in the LICENSE file at https://angular.io/license */ export {NgtscCompilerHost} from './src/compiler_host'; -export {absoluteFrom, absoluteFromSourceFile, basename, dirname, getFileSystem, isLocalRelativePath, isRoot, isRooted, join, relative, relativeFrom, resolve, setFileSystem, toRelativeImport} from './src/helpers'; +export { + absoluteFrom, + absoluteFromSourceFile, + basename, + dirname, + getFileSystem, + isLocalRelativePath, + isRoot, + isRooted, + join, + relative, + relativeFrom, + resolve, + setFileSystem, + toRelativeImport, +} from './src/helpers'; export {LogicalFileSystem, LogicalProjectPath} from './src/logical'; export {NodeJSFileSystem} from './src/node_js_file_system'; -export {AbsoluteFsPath, FileStats, FileSystem, PathManipulation, PathSegment, PathString, ReadonlyFileSystem} from './src/types'; +export { + AbsoluteFsPath, + FileStats, + FileSystem, + PathManipulation, + PathSegment, + PathString, + ReadonlyFileSystem, +} from './src/types'; export {getSourceFileOrError} from './src/util'; diff --git a/packages/compiler-cli/src/ngtsc/file_system/src/compiler_host.ts b/packages/compiler-cli/src/ngtsc/file_system/src/compiler_host.ts index 63cfba1b0970a..b01886195cd68 100644 --- a/packages/compiler-cli/src/ngtsc/file_system/src/compiler_host.ts +++ b/packages/compiler-cli/src/ngtsc/file_system/src/compiler_host.ts @@ -14,12 +14,16 @@ import {absoluteFrom} from './helpers'; import {FileSystem} from './types'; export class NgtscCompilerHost implements ts.CompilerHost { - constructor(protected fs: FileSystem, protected options: ts.CompilerOptions = {}) {} + constructor( + protected fs: FileSystem, + protected options: ts.CompilerOptions = {}, + ) {} - getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined { + getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile | undefined { const text = this.readFile(fileName); - return text !== undefined ? ts.createSourceFile(fileName, text, languageVersion, true) : - undefined; + return text !== undefined + ? ts.createSourceFile(fileName, text, languageVersion, true) + : undefined; } getDefaultLibFileName(options: ts.CompilerOptions): string { @@ -31,9 +35,12 @@ export class NgtscCompilerHost implements ts.CompilerHost { } writeFile( - fileName: string, data: string, writeByteOrderMark: boolean, - onError: ((message: string) => void)|undefined, - sourceFiles?: ReadonlyArray): void { + fileName: string, + data: string, + writeByteOrderMark: boolean, + onError: ((message: string) => void) | undefined, + sourceFiles?: ReadonlyArray, + ): void { const path = absoluteFrom(fileName); this.fs.ensureDir(this.fs.dirname(path)); this.fs.writeFile(path, data); @@ -67,7 +74,7 @@ export class NgtscCompilerHost implements ts.CompilerHost { return this.fs.exists(absPath) && this.fs.stat(absPath).isFile(); } - readFile(fileName: string): string|undefined { + readFile(fileName: string): string | undefined { const absPath = this.fs.resolve(fileName); if (!this.fileExists(absPath)) { return undefined; diff --git a/packages/compiler-cli/src/ngtsc/file_system/src/helpers.ts b/packages/compiler-cli/src/ngtsc/file_system/src/helpers.ts index b584794af0c08..e64b62eee54a2 100644 --- a/packages/compiler-cli/src/ngtsc/file_system/src/helpers.ts +++ b/packages/compiler-cli/src/ngtsc/file_system/src/helpers.ts @@ -35,7 +35,7 @@ const ABSOLUTE_PATH = Symbol('AbsolutePath'); * Extract an `AbsoluteFsPath` from a `ts.SourceFile`-like object. */ export function absoluteFromSourceFile(sf: {fileName: string}): AbsoluteFsPath { - const sfWithPatch = sf as {fileName: string, [ABSOLUTE_PATH]?: AbsoluteFsPath}; + const sfWithPatch = sf as {fileName: string; [ABSOLUTE_PATH]?: AbsoluteFsPath}; if (sfWithPatch[ABSOLUTE_PATH] === undefined) { sfWithPatch[ABSOLUTE_PATH] = fs.resolve(sfWithPatch.fileName); @@ -93,7 +93,7 @@ export function isRooted(path: string): boolean { /** * Static access to `relative`. */ -export function relative(from: T, to: T): PathSegment|AbsoluteFsPath { +export function relative(from: T, to: T): PathSegment | AbsoluteFsPath { return fs.relative(from, to); } @@ -119,7 +119,8 @@ export function isLocalRelativePath(relativePath: string): boolean { * * In other words it adds the `./` to the path if it is locally relative. */ -export function toRelativeImport(relativePath: PathSegment|AbsoluteFsPath): PathSegment| - AbsoluteFsPath { - return isLocalRelativePath(relativePath) ? `./${relativePath}` as PathSegment : relativePath; +export function toRelativeImport( + relativePath: PathSegment | AbsoluteFsPath, +): PathSegment | AbsoluteFsPath { + return isLocalRelativePath(relativePath) ? (`./${relativePath}` as PathSegment) : relativePath; } diff --git a/packages/compiler-cli/src/ngtsc/file_system/src/invalid_file_system.ts b/packages/compiler-cli/src/ngtsc/file_system/src/invalid_file_system.ts index 5dadaa1fbdb4b..9770daf31ba9a 100644 --- a/packages/compiler-cli/src/ngtsc/file_system/src/invalid_file_system.ts +++ b/packages/compiler-cli/src/ngtsc/file_system/src/invalid_file_system.ts @@ -25,7 +25,7 @@ export class InvalidFileSystem implements FileSystem { readFileBuffer(path: AbsoluteFsPath): Uint8Array { throw makeError(); } - writeFile(path: AbsoluteFsPath, data: string|Uint8Array, exclusive?: boolean): void { + writeFile(path: AbsoluteFsPath, data: string | Uint8Array, exclusive?: boolean): void { throw makeError(); } removeFile(path: AbsoluteFsPath): void { @@ -49,7 +49,7 @@ export class InvalidFileSystem implements FileSystem { chdir(path: AbsoluteFsPath): void { throw makeError(); } - extname(path: AbsoluteFsPath|PathSegment): string { + extname(path: AbsoluteFsPath | PathSegment): string { throw makeError(); } copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void { @@ -82,7 +82,7 @@ export class InvalidFileSystem implements FileSystem { isRooted(path: string): boolean { throw makeError(); } - relative(from: T, to: T): PathSegment|AbsoluteFsPath { + relative(from: T, to: T): PathSegment | AbsoluteFsPath { throw makeError(); } basename(filePath: string, extension?: string): PathSegment { @@ -101,5 +101,6 @@ export class InvalidFileSystem implements FileSystem { function makeError() { return new Error( - 'FileSystem has not been configured. Please call `setFileSystem()` before calling this method.'); + 'FileSystem has not been configured. Please call `setFileSystem()` before calling this method.', + ); } diff --git a/packages/compiler-cli/src/ngtsc/file_system/src/logical.ts b/packages/compiler-cli/src/ngtsc/file_system/src/logical.ts index 767e25cbeb422..7ef2cba2880b2 100644 --- a/packages/compiler-cli/src/ngtsc/file_system/src/logical.ts +++ b/packages/compiler-cli/src/ngtsc/file_system/src/logical.ts @@ -7,12 +7,17 @@ */ import ts from 'typescript'; -import {absoluteFromSourceFile, dirname, isLocalRelativePath, relative, resolve, toRelativeImport} from './helpers'; +import { + absoluteFromSourceFile, + dirname, + isLocalRelativePath, + relative, + resolve, + toRelativeImport, +} from './helpers'; import {AbsoluteFsPath, BrandedPath, PathSegment} from './types'; import {stripExtension} from './util'; - - /** * A path that's relative to the logical root of a TypeScript project (one of the project's * rootDirs). @@ -28,7 +33,7 @@ export const LogicalProjectPath = { * This will return a `PathSegment` which would be a valid module specifier to use in `from` when * importing from `to`. */ - relativePathBetween: function(from: LogicalProjectPath, to: LogicalProjectPath): PathSegment { + relativePathBetween: function (from: LogicalProjectPath, to: LogicalProjectPath): PathSegment { const relativePath = relative(dirname(resolve(from)), resolve(to)); return toRelativeImport(relativePath) as PathSegment; }, @@ -54,16 +59,18 @@ export class LogicalFileSystem { * A cache of file paths to project paths, because computation of these paths is slightly * expensive. */ - private cache: Map = new Map(); + private cache: Map = new Map(); constructor( - rootDirs: AbsoluteFsPath[], - private compilerHost: Pick) { + rootDirs: AbsoluteFsPath[], + private compilerHost: Pick, + ) { // Make a copy and sort it by length in reverse order (longest first). This speeds up lookups, // since there's no need to keep going through the array once a match is found. this.rootDirs = rootDirs.concat([]).sort((a, b) => b.length - a.length); - this.canonicalRootDirs = - this.rootDirs.map(dir => this.compilerHost.getCanonicalFileName(dir) as AbsoluteFsPath); + this.canonicalRootDirs = this.rootDirs.map( + (dir) => this.compilerHost.getCanonicalFileName(dir) as AbsoluteFsPath, + ); } /** @@ -72,7 +79,7 @@ export class LogicalFileSystem { * This method is provided as a convenient alternative to calling * `logicalPathOfFile(absoluteFromSourceFile(sf))`. */ - logicalPathOfSf(sf: ts.SourceFile): LogicalProjectPath|null { + logicalPathOfSf(sf: ts.SourceFile): LogicalProjectPath | null { return this.logicalPathOfFile(absoluteFromSourceFile(sf)); } @@ -82,11 +89,12 @@ export class LogicalFileSystem { * @returns A `LogicalProjectPath` to the source file, or `null` if the source file is not in any * of the TS project's root directories. */ - logicalPathOfFile(physicalFile: AbsoluteFsPath): LogicalProjectPath|null { + logicalPathOfFile(physicalFile: AbsoluteFsPath): LogicalProjectPath | null { if (!this.cache.has(physicalFile)) { - const canonicalFilePath = - this.compilerHost.getCanonicalFileName(physicalFile) as AbsoluteFsPath; - let logicalFile: LogicalProjectPath|null = null; + const canonicalFilePath = this.compilerHost.getCanonicalFileName( + physicalFile, + ) as AbsoluteFsPath; + let logicalFile: LogicalProjectPath | null = null; for (let i = 0; i < this.rootDirs.length; i++) { const rootDir = this.rootDirs[i]; const canonicalRootDir = this.canonicalRootDirs[i]; @@ -107,8 +115,10 @@ export class LogicalFileSystem { return this.cache.get(physicalFile)!; } - private createLogicalProjectPath(file: AbsoluteFsPath, rootDir: AbsoluteFsPath): - LogicalProjectPath { + private createLogicalProjectPath( + file: AbsoluteFsPath, + rootDir: AbsoluteFsPath, + ): LogicalProjectPath { const logicalPath = stripExtension(file.slice(rootDir.length)); return (logicalPath.startsWith('/') ? logicalPath : '/' + logicalPath) as LogicalProjectPath; } diff --git a/packages/compiler-cli/src/ngtsc/file_system/src/node_js_file_system.ts b/packages/compiler-cli/src/ngtsc/file_system/src/node_js_file_system.ts index f577c8c0c23b4..f7b9ea5dba4a5 100644 --- a/packages/compiler-cli/src/ngtsc/file_system/src/node_js_file_system.ts +++ b/packages/compiler-cli/src/ngtsc/file_system/src/node_js_file_system.ts @@ -11,7 +11,15 @@ import {createRequire} from 'module'; import * as p from 'path'; import {fileURLToPath} from 'url'; -import {AbsoluteFsPath, FileStats, FileSystem, PathManipulation, PathSegment, PathString, ReadonlyFileSystem} from './types'; +import { + AbsoluteFsPath, + FileStats, + FileSystem, + PathManipulation, + PathSegment, + PathString, + ReadonlyFileSystem, +} from './types'; /** * A wrapper around the Node.js file-system that supports path manipulation. @@ -39,13 +47,13 @@ export class NodeJSPathManipulation implements PathManipulation { isRooted(path: string): boolean { return p.isAbsolute(path); } - relative(from: T, to: T): PathSegment|AbsoluteFsPath { + relative(from: T, to: T): PathSegment | AbsoluteFsPath { return this.normalize(p.relative(from, to)) as PathSegment | AbsoluteFsPath; } basename(filePath: string, extension?: string): PathSegment { return p.basename(filePath, extension) as PathSegment; } - extname(path: AbsoluteFsPath|PathSegment): string { + extname(path: AbsoluteFsPath | PathSegment): string { return p.extname(path); } normalize(path: T): T { @@ -64,7 +72,7 @@ const currentFileName = isCommonJS ? __filename : fileURLToPath(currentFileUrl!) * A wrapper around the Node.js file-system that supports readonly operations and path manipulation. */ export class NodeJSReadonlyFileSystem extends NodeJSPathManipulation implements ReadonlyFileSystem { - private _caseSensitive: boolean|undefined = undefined; + private _caseSensitive: boolean | undefined = undefined; isCaseSensitive(): boolean { if (this._caseSensitive === undefined) { // Note the use of the real file-system is intentional: @@ -105,7 +113,7 @@ export class NodeJSReadonlyFileSystem extends NodeJSPathManipulation implements * A wrapper around the Node.js file-system (i.e. the `fs` package). */ export class NodeJSFileSystem extends NodeJSReadonlyFileSystem implements FileSystem { - writeFile(path: AbsoluteFsPath, data: string|Uint8Array, exclusive: boolean = false): void { + writeFile(path: AbsoluteFsPath, data: string | Uint8Array, exclusive: boolean = false): void { fs.writeFileSync(path, data, exclusive ? {flag: 'wx'} : undefined); } removeFile(path: AbsoluteFsPath): void { @@ -132,5 +140,7 @@ export class NodeJSFileSystem extends NodeJSReadonlyFileSystem implements FileSy * Toggle the case of each character in a string. */ function toggleCase(str: string): string { - return str.replace(/\w/g, ch => ch.toUpperCase() === ch ? ch.toLowerCase() : ch.toUpperCase()); + return str.replace(/\w/g, (ch) => + ch.toUpperCase() === ch ? ch.toLowerCase() : ch.toUpperCase(), + ); } diff --git a/packages/compiler-cli/src/ngtsc/file_system/src/types.ts b/packages/compiler-cli/src/ngtsc/file_system/src/types.ts index 6720de0b45701..f48498be0d450 100644 --- a/packages/compiler-cli/src/ngtsc/file_system/src/types.ts +++ b/packages/compiler-cli/src/ngtsc/file_system/src/types.ts @@ -12,7 +12,7 @@ * A `string` is not assignable to a `BrandedPath`, but a `BrandedPath` is assignable to a `string`. * Two `BrandedPath`s with different brands are not mutually assignable. */ -export type BrandedPath = string&{ +export type BrandedPath = string & { _brand: B; }; @@ -32,11 +32,11 @@ export type PathSegment = BrandedPath<'PathSegment'>; * An abstraction over the path manipulation aspects of a file-system. */ export interface PathManipulation { - extname(path: AbsoluteFsPath|PathSegment): string; + extname(path: AbsoluteFsPath | PathSegment): string; isRoot(path: AbsoluteFsPath): boolean; isRooted(path: string): boolean; dirname(file: T): T; - extname(path: AbsoluteFsPath|PathSegment): string; + extname(path: AbsoluteFsPath | PathSegment): string; join(basePath: T, ...paths: string[]): T; /** * Compute the relative path between `from` and `to`. @@ -45,7 +45,7 @@ export interface PathManipulation { * "relative" (i.e. `PathSegment`). For example, Windows can have multiple drives : * `relative('c:/a/b', 'd:/a/c')` would be `d:/a/c'. */ - relative(from: T, to: T): PathSegment|AbsoluteFsPath; + relative(from: T, to: T): PathSegment | AbsoluteFsPath; basename(filePath: string, extension?: string): PathSegment; normalize(path: T): T; resolve(...paths: string[]): AbsoluteFsPath; @@ -75,7 +75,7 @@ export interface ReadonlyFileSystem extends PathManipulation { * but also to create clever file-systems that have features such as caching. */ export interface FileSystem extends ReadonlyFileSystem { - writeFile(path: AbsoluteFsPath, data: string|Uint8Array, exclusive?: boolean): void; + writeFile(path: AbsoluteFsPath, data: string | Uint8Array, exclusive?: boolean): void; removeFile(path: AbsoluteFsPath): void; symlink(target: AbsoluteFsPath, path: AbsoluteFsPath): void; copyFile(from: AbsoluteFsPath, to: AbsoluteFsPath): void; @@ -84,7 +84,7 @@ export interface FileSystem extends ReadonlyFileSystem { removeDeep(path: AbsoluteFsPath): void; } -export type PathString = string|AbsoluteFsPath|PathSegment; +export type PathString = string | AbsoluteFsPath | PathSegment; /** * Information about an object in the FileSystem. diff --git a/packages/compiler-cli/src/ngtsc/file_system/src/util.ts b/packages/compiler-cli/src/ngtsc/file_system/src/util.ts index 90ae17cde46d0..3dc2804b163be 100644 --- a/packages/compiler-cli/src/ngtsc/file_system/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/file_system/src/util.ts @@ -28,8 +28,12 @@ export function stripExtension(path: T): T { export function getSourceFileOrError(program: ts.Program, fileName: AbsoluteFsPath): ts.SourceFile { const sf = program.getSourceFile(fileName); if (sf === undefined) { - throw new Error(`Program does not contain "${fileName}" - available files are ${ - program.getSourceFiles().map(sf => sf.fileName).join(', ')}`); + throw new Error( + `Program does not contain "${fileName}" - available files are ${program + .getSourceFiles() + .map((sf) => sf.fileName) + .join(', ')}`, + ); } return sf; } diff --git a/packages/compiler-cli/src/ngtsc/file_system/test/compiler_host_spec.ts b/packages/compiler-cli/src/ngtsc/file_system/test/compiler_host_spec.ts index 9fe7ee1c6da83..dbaaf5dbfe036 100644 --- a/packages/compiler-cli/src/ngtsc/file_system/test/compiler_host_spec.ts +++ b/packages/compiler-cli/src/ngtsc/file_system/test/compiler_host_spec.ts @@ -53,15 +53,15 @@ runInEachFileSystem(() => { }); describe('getCanonicalFileName()', () => { - it('should return the original filename if FS is case-sensitive or lower case otherwise', - () => { - const directory = absoluteFrom('/a/b/c'); - const fs = getFileSystem(); - fs.ensureDir(directory); - const host = new NgtscCompilerHost(fs); - expect(host.getCanonicalFileName(('AbCd.ts'))) - .toEqual(fs.isCaseSensitive() ? 'AbCd.ts' : 'abcd.ts'); - }); + it('should return the original filename if FS is case-sensitive or lower case otherwise', () => { + const directory = absoluteFrom('/a/b/c'); + const fs = getFileSystem(); + fs.ensureDir(directory); + const host = new NgtscCompilerHost(fs); + expect(host.getCanonicalFileName('AbCd.ts')).toEqual( + fs.isCaseSensitive() ? 'AbCd.ts' : 'abcd.ts', + ); + }); }); }); }); diff --git a/packages/compiler-cli/src/ngtsc/file_system/test/helpers_spec.ts b/packages/compiler-cli/src/ngtsc/file_system/test/helpers_spec.ts index 278dfcb5c016c..57cc895043b1e 100644 --- a/packages/compiler-cli/src/ngtsc/file_system/test/helpers_spec.ts +++ b/packages/compiler-cli/src/ngtsc/file_system/test/helpers_spec.ts @@ -24,10 +24,9 @@ describe('path types', () => { it('should not throw when creating one from a windows absolute path', () => { expect(absoluteFrom('C:\\test.txt')).toEqual('C:/test.txt'); }); - it('should not throw when creating one from a windows absolute path with POSIX separators', - () => { - expect(absoluteFrom('C:/test.txt')).toEqual('C:/test.txt'); - }); + it('should not throw when creating one from a windows absolute path with POSIX separators', () => { + expect(absoluteFrom('C:/test.txt')).toEqual('C:/test.txt'); + }); it('should support windows drive letters', () => { expect(absoluteFrom('D:\\foo\\test.txt')).toEqual('D:/foo/test.txt'); }); diff --git a/packages/compiler-cli/src/ngtsc/file_system/test/logical_spec.ts b/packages/compiler-cli/src/ngtsc/file_system/test/logical_spec.ts index 354fd3bd36950..044fcdd381761 100644 --- a/packages/compiler-cli/src/ngtsc/file_system/test/logical_spec.ts +++ b/packages/compiler-cli/src/ngtsc/file_system/test/logical_spec.ts @@ -24,10 +24,12 @@ runInEachFileSystem(() => { describe('LogicalFileSystem', () => { it('should determine logical paths in a single root file system', () => { const fs = new LogicalFileSystem([_('/test')], host); - expect(fs.logicalPathOfFile(_('/test/foo/foo.ts'))) - .toEqual('/foo/foo' as LogicalProjectPath); - expect(fs.logicalPathOfFile(_('/test/bar/bar.ts'))) - .toEqual('/bar/bar' as LogicalProjectPath); + expect(fs.logicalPathOfFile(_('/test/foo/foo.ts'))).toEqual( + '/foo/foo' as LogicalProjectPath, + ); + expect(fs.logicalPathOfFile(_('/test/bar/bar.ts'))).toEqual( + '/bar/bar' as LogicalProjectPath, + ); expect(fs.logicalPathOfFile(_('/not-test/bar.ts'))).toBeNull(); }); @@ -45,22 +47,27 @@ runInEachFileSystem(() => { it('should always return `/` prefixed logical paths', () => { const rootFs = new LogicalFileSystem([_('/')], host); - expect(rootFs.logicalPathOfFile(_('/foo/foo.ts'))) - .toEqual('/foo/foo' as LogicalProjectPath); + expect(rootFs.logicalPathOfFile(_('/foo/foo.ts'))).toEqual( + '/foo/foo' as LogicalProjectPath, + ); const nonRootFs = new LogicalFileSystem([_('/test/')], host); - expect(nonRootFs.logicalPathOfFile(_('/test/foo/foo.ts'))) - .toEqual('/foo/foo' as LogicalProjectPath); + expect(nonRootFs.logicalPathOfFile(_('/test/foo/foo.ts'))).toEqual( + '/foo/foo' as LogicalProjectPath, + ); }); it('should maintain casing of logical paths', () => { const fs = new LogicalFileSystem([_('/Test')], host); - expect(fs.logicalPathOfFile(_('/Test/foo/Foo.ts'))) - .toEqual('/foo/Foo' as LogicalProjectPath); - expect(fs.logicalPathOfFile(_('/Test/foo/foo.ts'))) - .toEqual('/foo/foo' as LogicalProjectPath); - expect(fs.logicalPathOfFile(_('/Test/bar/bAR.ts'))) - .toEqual('/bar/bAR' as LogicalProjectPath); + expect(fs.logicalPathOfFile(_('/Test/foo/Foo.ts'))).toEqual( + '/foo/Foo' as LogicalProjectPath, + ); + expect(fs.logicalPathOfFile(_('/Test/foo/foo.ts'))).toEqual( + '/foo/foo' as LogicalProjectPath, + ); + expect(fs.logicalPathOfFile(_('/Test/bar/bAR.ts'))).toEqual( + '/bar/bAR' as LogicalProjectPath, + ); }); it('should use case-sensitivity when matching rootDirs', () => { @@ -68,8 +75,9 @@ runInEachFileSystem(() => { if (host.useCaseSensitiveFileNames()) { expect(fs.logicalPathOfFile(_('/test/car/CAR.ts'))).toBe(null); } else { - expect(fs.logicalPathOfFile(_('/test/car/CAR.ts'))) - .toEqual('/car/CAR' as LogicalProjectPath); + expect(fs.logicalPathOfFile(_('/test/car/CAR.ts'))).toEqual( + '/car/CAR' as LogicalProjectPath, + ); } }); }); @@ -77,19 +85,25 @@ runInEachFileSystem(() => { describe('utilities', () => { it('should give a relative path between two adjacent logical files', () => { const res = LogicalProjectPath.relativePathBetween( - '/foo' as LogicalProjectPath, '/bar' as LogicalProjectPath); + '/foo' as LogicalProjectPath, + '/bar' as LogicalProjectPath, + ); expect(res).toEqual('./bar'); }); it('should give a relative path between two non-adjacent logical files', () => { const res = LogicalProjectPath.relativePathBetween( - '/foo/index' as LogicalProjectPath, '/bar/index' as LogicalProjectPath); + '/foo/index' as LogicalProjectPath, + '/bar/index' as LogicalProjectPath, + ); expect(res).toEqual('../bar/index'); }); it('should maintain casing in relative path between logical files', () => { const res = LogicalProjectPath.relativePathBetween( - '/fOO' as LogicalProjectPath, '/bAR' as LogicalProjectPath); + '/fOO' as LogicalProjectPath, + '/bAR' as LogicalProjectPath, + ); expect(res).toEqual('./bAR'); }); }); diff --git a/packages/compiler-cli/src/ngtsc/file_system/test/node_js_file_system_spec.ts b/packages/compiler-cli/src/ngtsc/file_system/test/node_js_file_system_spec.ts index 3881abc8175ac..cabb4c43dee30 100644 --- a/packages/compiler-cli/src/ngtsc/file_system/test/node_js_file_system_spec.ts +++ b/packages/compiler-cli/src/ngtsc/file_system/test/node_js_file_system_spec.ts @@ -12,7 +12,11 @@ import realFs from 'fs'; import os from 'os'; import url from 'url'; -import {NodeJSFileSystem, NodeJSPathManipulation, NodeJSReadonlyFileSystem} from '../src/node_js_file_system'; +import { + NodeJSFileSystem, + NodeJSPathManipulation, + NodeJSReadonlyFileSystem, +} from '../src/node_js_file_system'; import {AbsoluteFsPath, PathSegment} from '../src/types'; describe('NodeJSPathManipulation', () => { diff --git a/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system.ts b/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system.ts index 3193d4aad3cfb..86d47f2a55e45 100644 --- a/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system.ts +++ b/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system.ts @@ -16,8 +16,10 @@ export abstract class MockFileSystem implements FileSystem { private _fileTree: Folder = {}; private _cwd: AbsoluteFsPath; - - constructor(private _isCaseSensitive = false, cwd: AbsoluteFsPath = '/' as AbsoluteFsPath) { + constructor( + private _isCaseSensitive = false, + cwd: AbsoluteFsPath = '/' as AbsoluteFsPath, + ) { this._cwd = this.normalize(cwd); } @@ -47,16 +49,22 @@ export abstract class MockFileSystem implements FileSystem { } } - writeFile(path: AbsoluteFsPath, data: string|Uint8Array, exclusive: boolean = false): void { + writeFile(path: AbsoluteFsPath, data: string | Uint8Array, exclusive: boolean = false): void { const [folderPath, basename] = this.splitIntoFolderAndFile(path); const {entity} = this.findFromPath(folderPath); if (entity === null || !isFolder(entity)) { throw new MockFileSystemError( - 'ENOENT', path, `Unable to write file "${path}". The containing folder does not exist.`); + 'ENOENT', + path, + `Unable to write file "${path}". The containing folder does not exist.`, + ); } if (exclusive && entity[basename] !== undefined) { throw new MockFileSystemError( - 'EEXIST', path, `Unable to exclusively write file "${path}". The file already exists.`); + 'EEXIST', + path, + `Unable to exclusively write file "${path}". The file already exists.`, + ); } entity[basename] = data; } @@ -66,11 +74,17 @@ export abstract class MockFileSystem implements FileSystem { const {entity} = this.findFromPath(folderPath); if (entity === null || !isFolder(entity)) { throw new MockFileSystemError( - 'ENOENT', path, `Unable to remove file "${path}". The containing folder does not exist.`); + 'ENOENT', + path, + `Unable to remove file "${path}". The containing folder does not exist.`, + ); } if (isFolder(entity[basename])) { throw new MockFileSystemError( - 'EISDIR', path, `Unable to remove file "${path}". The path to remove is a folder.`); + 'EISDIR', + path, + `Unable to remove file "${path}". The path to remove is a folder.`, + ); } delete entity[basename]; } @@ -80,8 +94,10 @@ export abstract class MockFileSystem implements FileSystem { const {entity} = this.findFromPath(folderPath); if (entity === null || !isFolder(entity)) { throw new MockFileSystemError( - 'ENOENT', path, - `Unable to create symlink at "${path}". The containing folder does not exist.`); + 'ENOENT', + path, + `Unable to create symlink at "${path}". The containing folder does not exist.`, + ); } entity[basename] = new SymLink(target); } @@ -90,11 +106,17 @@ export abstract class MockFileSystem implements FileSystem { const {entity} = this.findFromPath(path); if (entity === null) { throw new MockFileSystemError( - 'ENOENT', path, `Unable to read directory "${path}". It does not exist.`); + 'ENOENT', + path, + `Unable to read directory "${path}". It does not exist.`, + ); } if (isFile(entity)) { throw new MockFileSystemError( - 'ENOTDIR', path, `Unable to read directory "${path}". It is a file.`); + 'ENOTDIR', + path, + `Unable to read directory "${path}". It is a file.`, + ); } return Object.keys(entity) as PathSegment[]; } @@ -128,7 +150,7 @@ export abstract class MockFileSystem implements FileSystem { } ensureDir(path: AbsoluteFsPath): Folder { - const segments = this.splitPath(path).map(segment => this.getCanonicalPath(segment)); + const segments = this.splitPath(path).map((segment) => this.getCanonicalPath(segment)); // Convert the root folder to a canonical empty string `''` (on Windows it would be `'C:'`). segments[0] = ''; @@ -155,8 +177,10 @@ export abstract class MockFileSystem implements FileSystem { const {entity} = this.findFromPath(folderPath); if (entity === null || !isFolder(entity)) { throw new MockFileSystemError( - 'ENOENT', path, - `Unable to remove folder "${path}". The containing folder does not exist.`); + 'ENOENT', + path, + `Unable to remove folder "${path}". The containing folder does not exist.`, + ); } delete entity[basename]; } @@ -165,7 +189,7 @@ export abstract class MockFileSystem implements FileSystem { return this.dirname(path) === path; } - extname(path: AbsoluteFsPath|PathSegment): string { + extname(path: AbsoluteFsPath | PathSegment): string { const match = /.+(\.[^.]*)$/.exec(path); return match !== null ? match[1] : ''; } @@ -174,7 +198,10 @@ export abstract class MockFileSystem implements FileSystem { const result = this.findFromPath(filePath, {followSymLinks: true}); if (result.entity === null) { throw new MockFileSystemError( - 'ENOENT', filePath, `Unable to find the real path of "${filePath}". It does not exist.`); + 'ENOENT', + filePath, + `Unable to find the real path of "${filePath}". It does not exist.`, + ); } else { return result.path; } @@ -220,7 +247,7 @@ export abstract class MockFileSystem implements FileSystem { abstract resolve(...paths: string[]): AbsoluteFsPath; abstract dirname(file: T): T; abstract join(basePath: T, ...paths: string[]): T; - abstract relative(from: T, to: T): PathSegment|AbsoluteFsPath; + abstract relative(from: T, to: T): PathSegment | AbsoluteFsPath; abstract basename(filePath: string, extension?: string): PathSegment; abstract isRooted(path: string): boolean; abstract normalize(path: T): T; @@ -268,7 +295,6 @@ export abstract class MockFileSystem implements FileSystem { } } - protected findFromPath(path: AbsoluteFsPath, options?: {followSymLinks: boolean}): FindResult { const followSymLinks = !!options && options.followSymLinks; const segments = this.splitPath(path); @@ -278,7 +304,7 @@ export abstract class MockFileSystem implements FileSystem { } // Convert the root folder to a canonical empty string `""` (on Windows it would be `C:`). segments[0] = ''; - let current: Entity|null = this._fileTree; + let current: Entity | null = this._fileTree; while (segments.length) { current = current[this.getCanonicalPath(segments.shift()!)]; if (current === undefined) { @@ -318,18 +344,18 @@ export abstract class MockFileSystem implements FileSystem { } protected getCanonicalPath(p: T): T { - return this.isCaseSensitive() ? p : p.toLowerCase() as T; + return this.isCaseSensitive() ? p : (p.toLowerCase() as T); } } export interface FindResult { path: AbsoluteFsPath; - entity: Entity|null; + entity: Entity | null; } -export type Entity = Folder|File|SymLink; +export type Entity = Folder | File | SymLink; export interface Folder { [pathSegments: string]: Entity; } -export type File = string|Uint8Array; +export type File = string | Uint8Array; export class SymLink { constructor(public path: AbsoluteFsPath) {} } @@ -348,19 +374,23 @@ class MockFileStats implements FileStats { } class MockFileSystemError extends Error { - constructor(public code: string, public path: string, message: string) { + constructor( + public code: string, + public path: string, + message: string, + ) { super(message); } } -export function isFile(item: Entity|null): item is File { +export function isFile(item: Entity | null): item is File { return Buffer.isBuffer(item) || typeof item === 'string'; } -export function isSymLink(item: Entity|null): item is SymLink { +export function isSymLink(item: Entity | null): item is SymLink { return item instanceof SymLink; } -export function isFolder(item: Entity|null): item is Folder { +export function isFolder(item: Entity | null): item is Folder { return item !== null && !isFile(item) && !isSymLink(item); } diff --git a/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system_native.ts b/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system_native.ts index a27c8f4648b22..abd10223f7093 100644 --- a/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system_native.ts +++ b/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system_native.ts @@ -30,7 +30,7 @@ export class MockFileSystemNative extends MockFileSystem { override join(basePath: T, ...paths: string[]): T { return NodeJSFileSystem.prototype.join.call(this, basePath, ...paths) as T; } - override relative(from: T, to: T): PathSegment|AbsoluteFsPath { + override relative(from: T, to: T): PathSegment | AbsoluteFsPath { return NodeJSFileSystem.prototype.relative.call(this, from, to); } diff --git a/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system_posix.ts b/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system_posix.ts index b526fc8484bf0..ef521237e8f79 100644 --- a/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system_posix.ts +++ b/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system_posix.ts @@ -25,7 +25,7 @@ export class MockFileSystemPosix extends MockFileSystem { return this.normalize(p.posix.join(basePath, ...paths)) as T; } - override relative(from: T, to: T): PathSegment|AbsoluteFsPath { + override relative(from: T, to: T): PathSegment | AbsoluteFsPath { return this.normalize(p.posix.relative(from, to)) as PathSegment | AbsoluteFsPath; } diff --git a/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system_windows.ts b/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system_windows.ts index def5ede041ecc..c554b5771c18f 100644 --- a/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system_windows.ts +++ b/packages/compiler-cli/src/ngtsc/file_system/testing/src/mock_file_system_windows.ts @@ -25,7 +25,7 @@ export class MockFileSystemWindows extends MockFileSystem { return this.normalize(p.win32.join(basePath, ...paths)) as T; } - override relative(from: T, to: T): PathSegment|AbsoluteFsPath { + override relative(from: T, to: T): PathSegment | AbsoluteFsPath { return this.normalize(p.win32.relative(from, to)) as PathSegment | AbsoluteFsPath; } diff --git a/packages/compiler-cli/src/ngtsc/file_system/testing/src/test_helper.ts b/packages/compiler-cli/src/ngtsc/file_system/testing/src/test_helper.ts index 273732cfa50fc..7242cbee978e9 100644 --- a/packages/compiler-cli/src/ngtsc/file_system/testing/src/test_helper.ts +++ b/packages/compiler-cli/src/ngtsc/file_system/testing/src/test_helper.ts @@ -20,7 +20,7 @@ import {MockFileSystemWindows} from './mock_file_system_windows'; export interface TestFile { name: AbsoluteFsPath; contents: string; - isRoot?: boolean|undefined; + isRoot?: boolean | undefined; } export interface RunInEachFileSystemFn { @@ -38,7 +38,7 @@ const FS_WINDOWS = 'Windows'; const FS_ALL = [FS_OS_X, FS_WINDOWS, FS_UNIX, FS_NATIVE]; function runInEachFileSystemFn(callback: (os: string) => void) { - FS_ALL.forEach(os => runInFileSystem(os, callback, false)); + FS_ALL.forEach((os) => runInFileSystem(os, callback, false)); } function runInFileSystem(os: string, callback: (os: string) => void, error: boolean) { @@ -55,16 +55,16 @@ function runInFileSystem(os: string, callback: (os: string) => void, error: bool } export const runInEachFileSystem: RunInEachFileSystemFn = - runInEachFileSystemFn as RunInEachFileSystemFn; + runInEachFileSystemFn as RunInEachFileSystemFn; runInEachFileSystem.native = (callback: (os: string) => void) => - runInFileSystem(FS_NATIVE, callback, true); + runInFileSystem(FS_NATIVE, callback, true); runInEachFileSystem.osX = (callback: (os: string) => void) => - runInFileSystem(FS_OS_X, callback, true); + runInFileSystem(FS_OS_X, callback, true); runInEachFileSystem.unix = (callback: (os: string) => void) => - runInFileSystem(FS_UNIX, callback, true); + runInFileSystem(FS_UNIX, callback, true); runInEachFileSystem.windows = (callback: (os: string) => void) => - runInFileSystem(FS_WINDOWS, callback, true); + runInFileSystem(FS_WINDOWS, callback, true); export function initMockFileSystem(os: string, cwd?: AbsoluteFsPath): MockFileSystem { const fs = createMockFileSystem(os, cwd); @@ -89,7 +89,7 @@ function createMockFileSystem(os: string, cwd?: AbsoluteFsPath): MockFileSystem } function monkeyPatchTypeScript(fs: MockFileSystem) { - ts.sys.fileExists = path => { + ts.sys.fileExists = (path) => { const absPath = fs.resolve(path); return fs.exists(absPath) && fs.stat(absPath).isFile(); }; @@ -102,7 +102,7 @@ function monkeyPatchTypeScript(fs: MockFileSystem) { ts.sys.readDirectory = readDirectory; function getDirectories(path: string): string[] { - return fs.readdir(absoluteFrom(path)).filter(p => fs.stat(fs.resolve(path, p)).isDirectory()); + return fs.readdir(absoluteFrom(path)).filter((p) => fs.stat(fs.resolve(path, p)).isDirectory()); } function getFileSystemEntries(path: string): FileSystemEntries { @@ -137,18 +137,37 @@ function monkeyPatchTypeScript(fs: MockFileSystem) { // Rather than completely re-implementing we are using the `ts.matchFiles` function, // which is internal to the `ts` namespace. const tsMatchFiles: ( - path: string, extensions: ReadonlyArray|undefined, - excludes: ReadonlyArray|undefined, includes: ReadonlyArray|undefined, - useCaseSensitiveFileNames: boolean, currentDirectory: string, depth: number|undefined, - getFileSystemEntries: (path: string) => FileSystemEntries, realpath: (path: string) => string, - directoryExists: (path: string) => boolean) => string[] = (ts as any).matchFiles; + path: string, + extensions: ReadonlyArray | undefined, + excludes: ReadonlyArray | undefined, + includes: ReadonlyArray | undefined, + useCaseSensitiveFileNames: boolean, + currentDirectory: string, + depth: number | undefined, + getFileSystemEntries: (path: string) => FileSystemEntries, + realpath: (path: string) => string, + directoryExists: (path: string) => boolean, + ) => string[] = (ts as any).matchFiles; function readDirectory( - path: string, extensions?: ReadonlyArray, excludes?: ReadonlyArray, - includes?: ReadonlyArray, depth?: number): string[] { + path: string, + extensions?: ReadonlyArray, + excludes?: ReadonlyArray, + includes?: ReadonlyArray, + depth?: number, + ): string[] { return tsMatchFiles( - path, extensions, excludes, includes, fs.isCaseSensitive(), fs.pwd(), depth, - getFileSystemEntries, realPath, directoryExists); + path, + extensions, + excludes, + includes, + fs.isCaseSensitive(), + fs.pwd(), + depth, + getFileSystemEntries, + realPath, + directoryExists, + ); } } diff --git a/packages/compiler-cli/src/ngtsc/imports/index.ts b/packages/compiler-cli/src/ngtsc/imports/index.ts index 1f6ea639b9f6b..45e2fca522583 100644 --- a/packages/compiler-cli/src/ngtsc/imports/index.ts +++ b/packages/compiler-cli/src/ngtsc/imports/index.ts @@ -6,14 +6,43 @@ * found in the LICENSE file at https://angular.io/license */ -export {AliasingHost, AliasStrategy, PrivateExportAliasingHost, UnifiedModulesAliasingHost} from './src/alias'; -export {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter, validateAndRewriteCoreSymbol} from './src/core'; +export { + AliasingHost, + AliasStrategy, + PrivateExportAliasingHost, + UnifiedModulesAliasingHost, +} from './src/alias'; +export { + ImportRewriter, + NoopImportRewriter, + R3SymbolsImportRewriter, + validateAndRewriteCoreSymbol, +} from './src/core'; export {DefaultImportTracker} from './src/default'; export {DeferredSymbolTracker} from './src/deferred_symbol_tracker'; -export {AbsoluteModuleStrategy, assertSuccessfulReferenceEmit, EmittedReference, FailedEmitResult, ImportedFile, ImportFlags, LocalIdentifierStrategy, LogicalProjectStrategy, ReferenceEmitKind, ReferenceEmitResult, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy, UnifiedModulesStrategy} from './src/emitter'; +export { + AbsoluteModuleStrategy, + assertSuccessfulReferenceEmit, + EmittedReference, + FailedEmitResult, + ImportedFile, + ImportFlags, + LocalIdentifierStrategy, + LogicalProjectStrategy, + ReferenceEmitKind, + ReferenceEmitResult, + ReferenceEmitStrategy, + ReferenceEmitter, + RelativePathStrategy, + UnifiedModulesStrategy, +} from './src/emitter'; export {ImportedSymbolsTracker} from './src/imported_symbols_tracker'; export {LocalCompilationExtraImportsTracker} from './src/local_compilation_extra_imports_tracker'; -export {AliasImportDeclaration, isAliasImportDeclaration, loadIsReferencedAliasDeclarationPatch} from './src/patch_alias_reference_resolution'; +export { + AliasImportDeclaration, + isAliasImportDeclaration, + loadIsReferencedAliasDeclarationPatch, +} from './src/patch_alias_reference_resolution'; export {Reexport} from './src/reexport'; export {OwningModule, Reference} from './src/references'; export {ModuleResolver} from './src/resolver'; diff --git a/packages/compiler-cli/src/ngtsc/imports/src/alias.ts b/packages/compiler-cli/src/ngtsc/imports/src/alias.ts index 4c41e3cc4a345..a864421f27748 100644 --- a/packages/compiler-cli/src/ngtsc/imports/src/alias.ts +++ b/packages/compiler-cli/src/ngtsc/imports/src/alias.ts @@ -15,7 +15,6 @@ import {ClassDeclaration, ReflectionHost} from '../../reflection'; import {EmittedReference, ImportFlags, ReferenceEmitKind, ReferenceEmitStrategy} from './emitter'; import {Reference} from './references'; - // Escape anything that isn't alphanumeric, '/' or '_'. const CHARS_TO_ESCAPE = /[^a-zA-Z0-9/_]/g; @@ -67,8 +66,11 @@ export interface AliasingHost { * NgModule (as opposed to being declared by it directly). */ maybeAliasSymbolAs( - ref: Reference, context: ts.SourceFile, ngModuleName: string, - isReExport: boolean): string|null; + ref: Reference, + context: ts.SourceFile, + ngModuleName: string, + isReExport: boolean, + ): string | null; /** * Determine an `Expression` by which `decl` should be imported from `via` using an alias export @@ -81,7 +83,7 @@ export interface AliasingHost { * aliased. * @param via the `ts.SourceFile` which might contain an alias to the */ - getAliasIn(decl: ClassDeclaration, via: ts.SourceFile, isReExport: boolean): Expression|null; + getAliasIn(decl: ClassDeclaration, via: ts.SourceFile, isReExport: boolean): Expression | null; } /** @@ -101,8 +103,11 @@ export class UnifiedModulesAliasingHost implements AliasingHost { readonly aliasExportsInDts = false; maybeAliasSymbolAs( - ref: Reference, context: ts.SourceFile, ngModuleName: string, - isReExport: boolean): string|null { + ref: Reference, + context: ts.SourceFile, + ngModuleName: string, + isReExport: boolean, + ): string | null { if (!isReExport) { // Aliasing is used with a UnifiedModulesHost to prevent transitive dependencies. Thus, // aliases @@ -117,7 +122,7 @@ export class UnifiedModulesAliasingHost implements AliasingHost { * Generates an `Expression` to import `decl` from `via`, assuming an export was added when `via` * was compiled per `maybeAliasSymbolAs` above. */ - getAliasIn(decl: ClassDeclaration, via: ts.SourceFile, isReExport: boolean): Expression|null { + getAliasIn(decl: ClassDeclaration, via: ts.SourceFile, isReExport: boolean): Expression | null { if (!isReExport) { // Directly exported directives/pipes don't require an alias, per the logic in // `maybeAliasSymbolAs`. @@ -135,7 +140,9 @@ export class UnifiedModulesAliasingHost implements AliasingHost { private aliasName(decl: ClassDeclaration, context: ts.SourceFile): string { // The declared module is used to get the name of the alias. const declModule = this.unifiedModulesHost.fileNameToModuleName( - decl.getSourceFile().fileName, context.fileName); + decl.getSourceFile().fileName, + context.fileName, + ); const replaced = declModule.replace(CHARS_TO_ESCAPE, '_').replace(/\//g, '$'); return 'ɵng$' + replaced + '$$' + decl.name.text; @@ -163,7 +170,10 @@ export class PrivateExportAliasingHost implements AliasingHost { readonly aliasExportsInDts = true; maybeAliasSymbolAs( - ref: Reference, context: ts.SourceFile, ngModuleName: string): string|null { + ref: Reference, + context: ts.SourceFile, + ngModuleName: string, + ): string | null { if (ref.hasOwningModuleGuess) { // Skip nodes that already have an associated absolute module specifier, since they can be // safely imported from that specifier. @@ -180,7 +190,7 @@ export class PrivateExportAliasingHost implements AliasingHost { throw new Error(`Could not determine the exports of: ${context.fileName}`); } let found: boolean = false; - exports.forEach(value => { + exports.forEach((value) => { if (value.node === ref.node) { found = true; } @@ -212,7 +222,7 @@ export class PrivateExportAliasingHost implements AliasingHost { * directive or pipe, if it exists. */ export class AliasStrategy implements ReferenceEmitStrategy { - emit(ref: Reference, context: ts.SourceFile, importMode: ImportFlags): EmittedReference|null { + emit(ref: Reference, context: ts.SourceFile, importMode: ImportFlags): EmittedReference | null { if (importMode & ImportFlags.NoAliasing || ref.alias === null) { return null; } diff --git a/packages/compiler-cli/src/ngtsc/imports/src/core.ts b/packages/compiler-cli/src/ngtsc/imports/src/core.ts index 1b3996394b044..8116a4f21c699 100644 --- a/packages/compiler-cli/src/ngtsc/imports/src/core.ts +++ b/packages/compiler-cli/src/ngtsc/imports/src/core.ts @@ -83,8 +83,9 @@ export class R3SymbolsImportRewriter implements ImportRewriter { const relativePathToR3Symbols = relativePathBetween(inContextOfFile, this.r3SymbolsPath); if (relativePathToR3Symbols === null) { - throw new Error(`Failed to rewrite import inside ${CORE_MODULE}: ${inContextOfFile} -> ${ - this.r3SymbolsPath}`); + throw new Error( + `Failed to rewrite import inside ${CORE_MODULE}: ${inContextOfFile} -> ${this.r3SymbolsPath}`, + ); } return relativePathToR3Symbols; diff --git a/packages/compiler-cli/src/ngtsc/imports/src/default.ts b/packages/compiler-cli/src/ngtsc/imports/src/default.ts index bf275816eebf3..89a873f277663 100644 --- a/packages/compiler-cli/src/ngtsc/imports/src/default.ts +++ b/packages/compiler-cli/src/ngtsc/imports/src/default.ts @@ -24,7 +24,9 @@ interface WithDefaultImportDeclaration { * default import. */ export function attachDefaultImportDeclaration( - expr: WrappedNodeExpr, importDecl: ts.ImportDeclaration): void { + expr: WrappedNodeExpr, + importDecl: ts.ImportDeclaration, +): void { (expr as WithDefaultImportDeclaration)[DefaultImportDeclaration] = importDecl; } @@ -32,8 +34,9 @@ export function attachDefaultImportDeclaration( * Obtains the default import declaration that `expr` depends on, or `null` if there is no such * dependency. */ -export function getDefaultImportDeclaration(expr: WrappedNodeExpr): ts.ImportDeclaration| - null { +export function getDefaultImportDeclaration( + expr: WrappedNodeExpr, +): ts.ImportDeclaration | null { return (expr as WithDefaultImportDeclaration)[DefaultImportDeclaration] ?? null; } @@ -93,10 +96,10 @@ export class DefaultImportTracker { * This transformer must run after any other transformers which call `recordUsedImport`. */ importPreservingTransformer(): ts.TransformerFactory { - return context => { - let clausesToPreserve: Set|null = null; + return (context) => { + let clausesToPreserve: Set | null = null; - return sourceFile => { + return (sourceFile) => { const clausesForFile = this.sourceFileToUsedImports.get(sourceFile.fileName); if (clausesForFile !== undefined) { diff --git a/packages/compiler-cli/src/ngtsc/imports/src/deferred_symbol_tracker.ts b/packages/compiler-cli/src/ngtsc/imports/src/deferred_symbol_tracker.ts index fb104e78f13b9..e029f69b3e497 100644 --- a/packages/compiler-cli/src/ngtsc/imports/src/deferred_symbol_tracker.ts +++ b/packages/compiler-cli/src/ngtsc/imports/src/deferred_symbol_tracker.ts @@ -18,7 +18,7 @@ type AssumeEager = typeof AssumeEager; * Maps imported symbol name to a set of locations where the symbols is used * in a source file. */ -type SymbolMap = Map|AssumeEager>; +type SymbolMap = Map | AssumeEager>; /** * Allows to register a symbol as deferrable and keep track of its usage. @@ -37,8 +37,9 @@ export class DeferredSymbolTracker { private readonly explicitlyDeferredImports = new Map(); constructor( - private readonly typeChecker: ts.TypeChecker, - private onlyExplicitDeferDependencyImports: boolean) {} + private readonly typeChecker: ts.TypeChecker, + private onlyExplicitDeferDependencyImports: boolean, + ) {} /** * Given an import declaration node, extract the names of all imported symbols @@ -91,8 +92,10 @@ export class DeferredSymbolTracker { * can not be removed, since there are other symbols imported alongside deferred * components. */ - getNonRemovableDeferredImports(sourceFile: ts.SourceFile, classDecl: ClassDeclaration): - ts.ImportDeclaration[] { + getNonRemovableDeferredImports( + sourceFile: ts.SourceFile, + classDecl: ClassDeclaration, + ): ts.ImportDeclaration[] { const affectedImports: ts.ImportDeclaration[] = []; const importDecls = this.explicitlyDeferredImports.get(classDecl) ?? []; for (const importDecl of importDecls) { @@ -108,8 +111,11 @@ export class DeferredSymbolTracker { * for defer loading. */ markAsDeferrableCandidate( - identifier: ts.Identifier, importDecl: ts.ImportDeclaration, - componentClassDecl: ClassDeclaration, isExplicitlyDeferred: boolean): void { + identifier: ts.Identifier, + importDecl: ts.ImportDeclaration, + componentClassDecl: ClassDeclaration, + isExplicitlyDeferred: boolean, + ): void { if (this.onlyExplicitDeferDependencyImports && !isExplicitlyDeferred) { // Ignore deferrable candidates when only explicit deferred imports mode is enabled. // In that mode only dependencies from the `@Component.deferredImports` field are @@ -135,14 +141,17 @@ export class DeferredSymbolTracker { if (!symbolMap.has(identifier.text)) { throw new Error( - `The '${identifier.text}' identifier doesn't belong ` + - `to the provided import declaration.`); + `The '${identifier.text}' identifier doesn't belong ` + + `to the provided import declaration.`, + ); } if (symbolMap.get(identifier.text) === AssumeEager) { // We process this symbol for the first time, populate references. symbolMap.set( - identifier.text, this.lookupIdentifiersInSourceFile(identifier.text, importDecl)); + identifier.text, + this.lookupIdentifiersInSourceFile(identifier.text, importDecl), + ); } const identifiers = symbolMap.get(identifier.text) as Set; @@ -186,8 +195,10 @@ export class DeferredSymbolTracker { return deferrableDecls; } - private lookupIdentifiersInSourceFile(name: string, importDecl: ts.ImportDeclaration): - Set { + private lookupIdentifiersInSourceFile( + name: string, + importDecl: ts.ImportDeclaration, + ): Set { const results = new Set(); const visit = (node: ts.Node): void => { if (node === importDecl) { diff --git a/packages/compiler-cli/src/ngtsc/imports/src/emitter.ts b/packages/compiler-cli/src/ngtsc/imports/src/emitter.ts index 2baa9b14cc8da..0342ed3e81e86 100644 --- a/packages/compiler-cli/src/ngtsc/imports/src/emitter.ts +++ b/packages/compiler-cli/src/ngtsc/imports/src/emitter.ts @@ -9,17 +9,35 @@ import {Expression, ExternalExpr, ExternalReference, WrappedNodeExpr} from '@ang import ts from 'typescript'; import {UnifiedModulesHost} from '../../core/api'; -import {ErrorCode, FatalDiagnosticError, makeDiagnosticChain, makeRelatedInformation} from '../../diagnostics'; -import {absoluteFromSourceFile, dirname, LogicalFileSystem, LogicalProjectPath, relative, toRelativeImport} from '../../file_system'; +import { + ErrorCode, + FatalDiagnosticError, + makeDiagnosticChain, + makeRelatedInformation, +} from '../../diagnostics'; +import { + absoluteFromSourceFile, + dirname, + LogicalFileSystem, + LogicalProjectPath, + relative, + toRelativeImport, +} from '../../file_system'; import {stripExtension} from '../../file_system/src/util'; import {DeclarationNode, ReflectionHost} from '../../reflection'; -import {getSourceFile, identifierOfNode, isDeclaration, isNamedDeclaration, isTypeDeclaration, nodeNameForError} from '../../util/src/typescript'; +import { + getSourceFile, + identifierOfNode, + isDeclaration, + isNamedDeclaration, + isTypeDeclaration, + nodeNameForError, +} from '../../util/src/typescript'; import {findExportedNameOfNode} from './find_export'; import {Reference} from './references'; import {ModuleResolver} from './resolver'; - /** * Flags which alter the imports generated by the `ReferenceEmitter`. */ @@ -80,7 +98,7 @@ export enum ImportFlags { * source file, then `'unknown'` should be returned. If the generated expression does not represent * an import then `null` should be used. */ -export type ImportedFile = ts.SourceFile|'unknown'|null; +export type ImportedFile = ts.SourceFile | 'unknown' | null; export const enum ReferenceEmitKind { Success, @@ -130,7 +148,7 @@ export interface FailedEmitResult { reason: string; } -export type ReferenceEmitResult = EmittedReference|FailedEmitResult; +export type ReferenceEmitResult = EmittedReference | FailedEmitResult; /** * Verifies that a reference was emitted successfully, or raises a `FatalDiagnosticError` otherwise. @@ -140,18 +158,21 @@ export type ReferenceEmitResult = EmittedReference|FailedEmitResult; * 'class'. */ export function assertSuccessfulReferenceEmit( - result: ReferenceEmitResult, origin: ts.Node, - typeKind: string): asserts result is EmittedReference { + result: ReferenceEmitResult, + origin: ts.Node, + typeKind: string, +): asserts result is EmittedReference { if (result.kind === ReferenceEmitKind.Success) { return; } const message = makeDiagnosticChain( - `Unable to import ${typeKind} ${nodeNameForError(result.ref.node)}.`, - [makeDiagnosticChain(result.reason)]); - throw new FatalDiagnosticError( - ErrorCode.IMPORT_GENERATION_FAILURE, origin, message, - [makeRelatedInformation(result.ref.node, `The ${typeKind} is declared here.`)]); + `Unable to import ${typeKind} ${nodeNameForError(result.ref.node)}.`, + [makeDiagnosticChain(result.reason)], + ); + throw new FatalDiagnosticError(ErrorCode.IMPORT_GENERATION_FAILURE, origin, message, [ + makeRelatedInformation(result.ref.node, `The ${typeKind} is declared here.`), + ]); } /** @@ -177,7 +198,11 @@ export interface ReferenceEmitStrategy { * @returns an `EmittedReference` which refers to the `Reference`, or `null` if none can be * generated */ - emit(ref: Reference, context: ts.SourceFile, importFlags: ImportFlags): ReferenceEmitResult|null; + emit( + ref: Reference, + context: ts.SourceFile, + importFlags: ImportFlags, + ): ReferenceEmitResult | null; } /** @@ -189,8 +214,11 @@ export interface ReferenceEmitStrategy { export class ReferenceEmitter { constructor(private strategies: ReferenceEmitStrategy[]) {} - emit(ref: Reference, context: ts.SourceFile, importFlags: ImportFlags = ImportFlags.None): - ReferenceEmitResult { + emit( + ref: Reference, + context: ts.SourceFile, + importFlags: ImportFlags = ImportFlags.None, + ): ReferenceEmitResult { for (const strategy of this.strategies) { const emitted = strategy.emit(ref, context, importFlags); if (emitted !== null) { @@ -212,7 +240,7 @@ export class ReferenceEmitter { * such identifiers are available. */ export class LocalIdentifierStrategy implements ReferenceEmitStrategy { - emit(ref: Reference, context: ts.SourceFile, importFlags: ImportFlags): EmittedReference|null { + emit(ref: Reference, context: ts.SourceFile, importFlags: ImportFlags): EmittedReference | null { const refSf = getSourceFile(ref.node); // If the emitter has specified ForceNewImport, then LocalIdentifierStrategy should not use a @@ -270,12 +298,12 @@ interface ModuleExports { /** * The source file of the module. */ - module: ts.SourceFile|null; + module: ts.SourceFile | null; /** * The map of declarations to their exported name. */ - exportMap: Map|null; + exportMap: Map | null; } /** @@ -295,21 +323,34 @@ export class AbsoluteModuleStrategy implements ReferenceEmitStrategy { private moduleExportsCache = new Map(); constructor( - protected program: ts.Program, protected checker: ts.TypeChecker, - protected moduleResolver: ModuleResolver, private reflectionHost: ReflectionHost) {} - - emit(ref: Reference, context: ts.SourceFile, importFlags: ImportFlags): ReferenceEmitResult|null { + protected program: ts.Program, + protected checker: ts.TypeChecker, + protected moduleResolver: ModuleResolver, + private reflectionHost: ReflectionHost, + ) {} + + emit( + ref: Reference, + context: ts.SourceFile, + importFlags: ImportFlags, + ): ReferenceEmitResult | null { if (ref.bestGuessOwningModule === null) { // There is no module name available for this Reference, meaning it was arrived at via a // relative path. return null; } else if (!isDeclaration(ref.node)) { // It's not possible to import something which isn't a declaration. - throw new Error(`Debug assert: unable to import a Reference to non-declaration of type ${ - ts.SyntaxKind[ref.node.kind]}.`); + throw new Error( + `Debug assert: unable to import a Reference to non-declaration of type ${ + ts.SyntaxKind[ref.node.kind] + }.`, + ); } else if ((importFlags & ImportFlags.AllowTypeImports) === 0 && isTypeDeclaration(ref.node)) { - throw new Error(`Importing a type-only declaration of type ${ - ts.SyntaxKind[ref.node.kind]} in a value position is not allowed.`); + throw new Error( + `Importing a type-only declaration of type ${ + ts.SyntaxKind[ref.node.kind] + } in a value position is not allowed.`, + ); } // Try to find the exported name of the declaration, if one is available. @@ -327,8 +368,7 @@ export class AbsoluteModuleStrategy implements ReferenceEmitStrategy { kind: ReferenceEmitKind.Failed, ref, context, - reason: - `The symbol is not exported from ${exports.module.fileName} (module '${specifier}').`, + reason: `The symbol is not exported from ${exports.module.fileName} (module '${specifier}').`, }; } const symbolName = exports.exportMap.get(ref.node)!; @@ -388,11 +428,18 @@ export class AbsoluteModuleStrategy implements ReferenceEmitStrategy { export class LogicalProjectStrategy implements ReferenceEmitStrategy { private relativePathStrategy: RelativePathStrategy; - constructor(private reflector: ReflectionHost, private logicalFs: LogicalFileSystem) { + constructor( + private reflector: ReflectionHost, + private logicalFs: LogicalFileSystem, + ) { this.relativePathStrategy = new RelativePathStrategy(this.reflector); } - emit(ref: Reference, context: ts.SourceFile, importFlags: ImportFlags): ReferenceEmitResult|null { + emit( + ref: Reference, + context: ts.SourceFile, + importFlags: ImportFlags, + ): ReferenceEmitResult | null { const destSf = getSourceFile(ref.node); // Compute the relative path from the importing file to the file being imported. This is done @@ -419,7 +466,8 @@ export class LogicalProjectStrategy implements ReferenceEmitStrategy { const originPath = this.logicalFs.logicalPathOfSf(context); if (originPath === null) { throw new Error( - `Debug assert: attempt to import from ${context.fileName} but it's outside the program?`); + `Debug assert: attempt to import from ${context.fileName} but it's outside the program?`, + ); } // There's no way to emit a relative reference from a file to itself. @@ -458,10 +506,12 @@ export class LogicalProjectStrategy implements ReferenceEmitStrategy { export class RelativePathStrategy implements ReferenceEmitStrategy { constructor(private reflector: ReflectionHost) {} - emit(ref: Reference, context: ts.SourceFile): ReferenceEmitResult|null { + emit(ref: Reference, context: ts.SourceFile): ReferenceEmitResult | null { const destSf = getSourceFile(ref.node); - const relativePath = - relative(dirname(absoluteFromSourceFile(context)), absoluteFromSourceFile(destSf)); + const relativePath = relative( + dirname(absoluteFromSourceFile(context)), + absoluteFromSourceFile(destSf), + ); const moduleName = toRelativeImport(stripExtension(relativePath)); const name = findExportedNameOfNode(ref.node, destSf, this.reflector); @@ -486,17 +536,22 @@ export class RelativePathStrategy implements ReferenceEmitStrategy { * references. */ export class UnifiedModulesStrategy implements ReferenceEmitStrategy { - constructor(private reflector: ReflectionHost, private unifiedModulesHost: UnifiedModulesHost) {} + constructor( + private reflector: ReflectionHost, + private unifiedModulesHost: UnifiedModulesHost, + ) {} - emit(ref: Reference, context: ts.SourceFile): EmittedReference|null { + emit(ref: Reference, context: ts.SourceFile): EmittedReference | null { const destSf = getSourceFile(ref.node); const name = findExportedNameOfNode(ref.node, destSf, this.reflector); if (name === null) { return null; } - const moduleName = - this.unifiedModulesHost.fileNameToModuleName(destSf.fileName, context.fileName); + const moduleName = this.unifiedModulesHost.fileNameToModuleName( + destSf.fileName, + context.fileName, + ); return { kind: ReferenceEmitKind.Success, diff --git a/packages/compiler-cli/src/ngtsc/imports/src/find_export.ts b/packages/compiler-cli/src/ngtsc/imports/src/find_export.ts index 61214cee43e2d..7b5c074b81b9a 100644 --- a/packages/compiler-cli/src/ngtsc/imports/src/find_export.ts +++ b/packages/compiler-cli/src/ngtsc/imports/src/find_export.ts @@ -15,7 +15,10 @@ import {isNamedDeclaration} from '../../util/src/typescript'; * Find the name, if any, by which a node is exported from a given file. */ export function findExportedNameOfNode( - target: ts.Node, file: ts.SourceFile, reflector: ReflectionHost): string|null { + target: ts.Node, + file: ts.SourceFile, + reflector: ReflectionHost, +): string | null { const exports = reflector.getExportsOfModule(file); if (exports === null) { return null; @@ -24,7 +27,7 @@ export function findExportedNameOfNode( const declaredName = isNamedDeclaration(target) ? target.name.text : null; // Look for the export which declares the node. - let foundExportName: string|null = null; + let foundExportName: string | null = null; for (const [exportName, declaration] of exports) { if (declaration.node !== target) { continue; diff --git a/packages/compiler-cli/src/ngtsc/imports/src/imported_symbols_tracker.ts b/packages/compiler-cli/src/ngtsc/imports/src/imported_symbols_tracker.ts index 6c28b2c752854..6f8aef3c8153c 100644 --- a/packages/compiler-cli/src/ngtsc/imports/src/imported_symbols_tracker.ts +++ b/packages/compiler-cli/src/ngtsc/imports/src/imported_symbols_tracker.ts @@ -35,8 +35,11 @@ export class ImportedSymbolsTracker { * @param exportedName Name of the exported symbol that is being searched for. * @param moduleName Module from which the symbol should be imported. */ - isPotentialReferenceToNamedImport(node: ts.Identifier, exportedName: string, moduleName: string): - boolean { + isPotentialReferenceToNamedImport( + node: ts.Identifier, + exportedName: string, + moduleName: string, + ): boolean { const sourceFile = node.getSourceFile(); this.scanImports(sourceFile); const fileImports = this.fileToNamedImports.get(sourceFile)!; @@ -95,8 +98,11 @@ export class ImportedSymbolsTracker { // Only check top-level imports. for (const stmt of sourceFile.statements) { - if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteralLike(stmt.moduleSpecifier) || - stmt.importClause?.namedBindings === undefined) { + if ( + !ts.isImportDeclaration(stmt) || + !ts.isStringLiteralLike(stmt.moduleSpecifier) || + stmt.importClause?.namedBindings === undefined + ) { continue; } @@ -113,7 +119,7 @@ export class ImportedSymbolsTracker { for (const element of stmt.importClause.namedBindings.elements) { const localName = element.name.text; const exportedName = - element.propertyName === undefined ? localName : element.propertyName.text; + element.propertyName === undefined ? localName : element.propertyName.text; if (!namedImports.has(moduleName)) { namedImports.set(moduleName, new Map()); diff --git a/packages/compiler-cli/src/ngtsc/imports/src/local_compilation_extra_imports_tracker.ts b/packages/compiler-cli/src/ngtsc/imports/src/local_compilation_extra_imports_tracker.ts index a895313f80737..0557599d2d9d6 100644 --- a/packages/compiler-cli/src/ngtsc/imports/src/local_compilation_extra_imports_tracker.ts +++ b/packages/compiler-cli/src/ngtsc/imports/src/local_compilation_extra_imports_tracker.ts @@ -10,7 +10,6 @@ import ts from 'typescript'; import {getContainingImportDeclaration} from '../../reflection/src/typescript'; - /** * A tool to track extra imports to be added to the generated files in the local compilation mode. * @@ -62,7 +61,7 @@ export class LocalCompilationExtraImportsTracker { * to smallest possible candidate files instead of all files. */ addGlobalImportFromIdentifier(node: ts.Node): void { - let identifier: ts.Identifier|null = null; + let identifier: ts.Identifier | null = null; if (ts.isIdentifier(node)) { identifier = node; } else if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression)) { @@ -78,7 +77,6 @@ export class LocalCompilationExtraImportsTracker { return; } - const importClause = sym.declarations[0]; const decl = getContainingImportDeclaration(importClause); @@ -91,10 +89,7 @@ export class LocalCompilationExtraImportsTracker { * Returns the list of all module names that the given file should include as its extra imports. */ getImportsForFile(sf: ts.SourceFile): string[] { - return [ - ...this.globalImportsSet, - ...(this.localImportsMap.get(sf.fileName) ?? []), - ]; + return [...this.globalImportsSet, ...(this.localImportsMap.get(sf.fileName) ?? [])]; } } diff --git a/packages/compiler-cli/src/ngtsc/imports/src/patch_alias_reference_resolution.ts b/packages/compiler-cli/src/ngtsc/imports/src/patch_alias_reference_resolution.ts index 182fdc81e88dd..06bf8bdde6237 100644 --- a/packages/compiler-cli/src/ngtsc/imports/src/patch_alias_reference_resolution.ts +++ b/packages/compiler-cli/src/ngtsc/imports/src/patch_alias_reference_resolution.ts @@ -9,7 +9,7 @@ import ts from 'typescript'; /** Possible alias import declarations */ -export type AliasImportDeclaration = ts.ImportSpecifier|ts.NamespaceImport|ts.ImportClause; +export type AliasImportDeclaration = ts.ImportSpecifier | ts.NamespaceImport | ts.ImportClause; /** * Describes a TypeScript transformation context with the internal emit @@ -74,8 +74,9 @@ interface EmitResolver { * Github. * https://sourcegraph.com/github.com/microsoft/TypeScript@3eaa7c65f6f076a08a5f7f1946fd0df7c7430259/-/blob/src/compiler/checker.ts#L31219-31257 */ -export function loadIsReferencedAliasDeclarationPatch(context: ts.TransformationContext): - Set { +export function loadIsReferencedAliasDeclarationPatch( + context: ts.TransformationContext, +): Set { // If the `getEmitResolver` method is not available, TS most likely changed the // internal structure of the transformation context. We will abort gracefully. if (!isTransformationContextWithEmitResolver(context)) { @@ -99,13 +100,13 @@ export function loadIsReferencedAliasDeclarationPatch(context: ts.Transformation } const referencedAliases = new Set(); - emitResolver.isReferencedAliasDeclaration = function(node, ...args) { + emitResolver.isReferencedAliasDeclaration = function (node, ...args) { if (isAliasImportDeclaration(node) && (referencedAliases as Set).has(node)) { return true; } return originalIsReferencedAliasDeclaration.call(emitResolver, node, ...args); }; - return emitResolver[patchedReferencedAliasesSymbol] = referencedAliases; + return (emitResolver[patchedReferencedAliasesSymbol] = referencedAliases); } /** @@ -118,12 +119,12 @@ export function isAliasImportDeclaration(node: ts.Node): node is AliasImportDecl } /** Whether the transformation context exposes its emit resolver. */ -function isTransformationContextWithEmitResolver(context: ts.TransformationContext): - context is TransformationContextWithResolver { +function isTransformationContextWithEmitResolver( + context: ts.TransformationContext, +): context is TransformationContextWithResolver { return (context as Partial).getEmitResolver !== undefined; } - /** * Throws an error about an incompatible TypeScript version for which the alias * declaration reference resolution could not be monkey-patched. The error will @@ -131,8 +132,9 @@ function isTransformationContextWithEmitResolver(context: ts.TransformationConte */ function throwIncompatibleTransformationContextError(): never { throw Error( - 'Angular compiler is incompatible with this version of the TypeScript compiler.\n\n' + + 'Angular compiler is incompatible with this version of the TypeScript compiler.\n\n' + 'If you recently updated TypeScript and this issue surfaces now, consider downgrading.\n\n' + 'Please report an issue on the Angular repositories when this issue ' + - 'surfaces and you are using a supposedly compatible TypeScript version.'); + 'surfaces and you are using a supposedly compatible TypeScript version.', + ); } diff --git a/packages/compiler-cli/src/ngtsc/imports/src/references.ts b/packages/compiler-cli/src/ngtsc/imports/src/references.ts index adfadfee8161b..80f628dff261c 100644 --- a/packages/compiler-cli/src/ngtsc/imports/src/references.ts +++ b/packages/compiler-cli/src/ngtsc/imports/src/references.ts @@ -41,7 +41,7 @@ export class Reference { * * If `bestGuessOwningModule` is `null`, then it's likely the node came from the current program. */ - readonly bestGuessOwningModule: OwningModule|null; + readonly bestGuessOwningModule: OwningModule | null; private identifiers: ts.Identifier[] = []; @@ -53,11 +53,14 @@ export class Reference { */ synthetic = false; - private _alias: Expression|null = null; + private _alias: Expression | null = null; readonly isAmbient: boolean; - constructor(readonly node: T, bestGuessOwningModule: OwningModule|AmbientImport|null = null) { + constructor( + readonly node: T, + bestGuessOwningModule: OwningModule | AmbientImport | null = null, + ) { if (bestGuessOwningModule === AmbientImport) { this.isAmbient = true; this.bestGuessOwningModule = null; @@ -76,7 +79,7 @@ export class Reference { * The best guess at which module specifier owns this particular reference, or `null` if there * isn't one. */ - get ownedByModuleGuess(): string|null { + get ownedByModuleGuess(): string | null { if (this.bestGuessOwningModule !== null) { return this.bestGuessOwningModule.specifier; } else { @@ -99,16 +102,15 @@ export class Reference { * This is only suited for debugging. Any actual references to this node should be made with * `ts.Identifier`s (see `getIdentityIn`). */ - get debugName(): string|null { + get debugName(): string | null { const id = identifierOfNode(this.node); return id !== null ? id.text : null; } - get alias(): Expression|null { + get alias(): Expression | null { return this._alias; } - /** * Record a `ts.Identifier` by which it's valid to refer to this node, within the context of this * `Reference`. @@ -121,8 +123,8 @@ export class Reference { * Get a `ts.Identifier` within this `Reference` that can be used to refer within the context of a * given `ts.SourceFile`, if any. */ - getIdentityIn(context: ts.SourceFile): ts.Identifier|null { - return this.identifiers.find(id => id.getSourceFile() === context) || null; + getIdentityIn(context: ts.SourceFile): ts.Identifier | null { + return this.identifiers.find((id) => id.getSourceFile() === context) || null; } /** @@ -132,17 +134,18 @@ export class Reference { * extracted from some larger expression, as it can be used to pinpoint the `ts.Identifier` within * the expression from which the `Reference` originated. */ - getIdentityInExpression(expr: ts.Expression): ts.Identifier|null { + getIdentityInExpression(expr: ts.Expression): ts.Identifier | null { const sf = expr.getSourceFile(); - return this.identifiers.find(id => { - if (id.getSourceFile() !== sf) { - return false; - } - - // This identifier is a match if its position lies within the given expression. - return id.pos >= expr.pos && id.end <= expr.end; - }) || - null; + return ( + this.identifiers.find((id) => { + if (id.getSourceFile() !== sf) { + return false; + } + + // This identifier is a match if its position lies within the given expression. + return id.pos >= expr.pos && id.end <= expr.end; + }) || null + ); } /** @@ -162,23 +165,29 @@ export class Reference { * If no specific node can be found, then the `fallback` expression is used, which defaults to the * entire containing expression. */ - getOriginForDiagnostics(container: ts.Expression, fallback: ts.Expression = container): - ts.Expression { + getOriginForDiagnostics( + container: ts.Expression, + fallback: ts.Expression = container, + ): ts.Expression { const id = this.getIdentityInExpression(container); return id !== null ? id : fallback; } cloneWithAlias(alias: Expression): Reference { - const ref = - new Reference(this.node, this.isAmbient ? AmbientImport : this.bestGuessOwningModule); + const ref = new Reference( + this.node, + this.isAmbient ? AmbientImport : this.bestGuessOwningModule, + ); ref.identifiers = [...this.identifiers]; ref._alias = alias; return ref; } cloneWithNoIdentifiers(): Reference { - const ref = - new Reference(this.node, this.isAmbient ? AmbientImport : this.bestGuessOwningModule); + const ref = new Reference( + this.node, + this.isAmbient ? AmbientImport : this.bestGuessOwningModule, + ); ref._alias = this._alias; ref.identifiers = []; return ref; diff --git a/packages/compiler-cli/src/ngtsc/imports/src/resolver.ts b/packages/compiler-cli/src/ngtsc/imports/src/resolver.ts index 893c4530b4aa0..a7d73b7665756 100644 --- a/packages/compiler-cli/src/ngtsc/imports/src/resolver.ts +++ b/packages/compiler-cli/src/ngtsc/imports/src/resolver.ts @@ -18,13 +18,20 @@ import {getSourceFileOrNull, resolveModuleName} from '../../util/src/typescript' */ export class ModuleResolver { constructor( - private program: ts.Program, private compilerOptions: ts.CompilerOptions, - private host: ts.ModuleResolutionHost&Pick, - private moduleResolutionCache: ts.ModuleResolutionCache|null) {} + private program: ts.Program, + private compilerOptions: ts.CompilerOptions, + private host: ts.ModuleResolutionHost & Pick, + private moduleResolutionCache: ts.ModuleResolutionCache | null, + ) {} - resolveModule(moduleName: string, containingFile: string): ts.SourceFile|null { + resolveModule(moduleName: string, containingFile: string): ts.SourceFile | null { const resolved = resolveModuleName( - moduleName, containingFile, this.compilerOptions, this.host, this.moduleResolutionCache); + moduleName, + containingFile, + this.compilerOptions, + this.host, + this.moduleResolutionCache, + ); if (resolved === undefined) { return null; } diff --git a/packages/compiler-cli/src/ngtsc/imports/test/default_spec.ts b/packages/compiler-cli/src/ngtsc/imports/test/default_spec.ts index aacfda6e2fd5e..940a02bc94342 100644 --- a/packages/compiler-cli/src/ngtsc/imports/test/default_spec.ts +++ b/packages/compiler-cli/src/ngtsc/imports/test/default_spec.ts @@ -15,30 +15,31 @@ import {DefaultImportTracker} from '../src/default'; runInEachFileSystem(() => { describe('DefaultImportTracker', () => { let _: typeof absoluteFrom; - beforeEach(() => _ = absoluteFrom); + beforeEach(() => (_ = absoluteFrom)); it('should prevent a default import from being elided if used', () => { const {program, host} = makeProgram( - [ - {name: _('/dep.ts'), contents: `export default class Foo {}`}, - { - name: _('/test.ts'), - contents: `import Foo from './dep'; export function test(f: Foo) {}` - }, + [ + {name: _('/dep.ts'), contents: `export default class Foo {}`}, + { + name: _('/test.ts'), + contents: `import Foo from './dep'; export function test(f: Foo) {}`, + }, - // This control file is identical to the test file, but will not have its import marked - // for preservation. It exists to verify that it is in fact the action of - // DefaultImportTracker and not some other artifact of the test setup which causes the - // import to be preserved. It will also verify that DefaultImportTracker does not - // preserve imports which are not marked for preservation. - { - name: _('/ctrl.ts'), - contents: `import Foo from './dep'; export function test(f: Foo) {}` - }, - ], + // This control file is identical to the test file, but will not have its import marked + // for preservation. It exists to verify that it is in fact the action of + // DefaultImportTracker and not some other artifact of the test setup which causes the + // import to be preserved. It will also verify that DefaultImportTracker does not + // preserve imports which are not marked for preservation. { - module: ts.ModuleKind.ES2015, - }); + name: _('/ctrl.ts'), + contents: `import Foo from './dep'; export function test(f: Foo) {}`, + }, + ], + { + module: ts.ModuleKind.ES2015, + }, + ); const fooClause = getDeclaration(program, _('/test.ts'), 'Foo', ts.isImportClause); const fooDecl = fooClause.parent; @@ -57,16 +58,17 @@ runInEachFileSystem(() => { it('should transpile imports correctly into commonjs', () => { const {program, host} = makeProgram( - [ - {name: _('/dep.ts'), contents: `export default class Foo {}`}, - { - name: _('/test.ts'), - contents: `import Foo from './dep'; export function test(f: Foo) {}` - }, - ], + [ + {name: _('/dep.ts'), contents: `export default class Foo {}`}, { - module: ts.ModuleKind.CommonJS, - }); + name: _('/test.ts'), + contents: `import Foo from './dep'; export function test(f: Foo) {}`, + }, + ], + { + module: ts.ModuleKind.CommonJS, + }, + ); const fooClause = getDeclaration(program, _('/test.ts'), 'Foo', ts.isImportClause); const fooId = fooClause.name!; const fooDecl = fooClause.parent; @@ -74,10 +76,7 @@ runInEachFileSystem(() => { const tracker = new DefaultImportTracker(); tracker.recordUsedImport(fooDecl); program.emit(undefined, undefined, undefined, undefined, { - before: [ - addReferenceTransformer(fooId), - tracker.importPreservingTransformer(), - ], + before: [addReferenceTransformer(fooId), tracker.importPreservingTransformer()], }); const testContents = host.readFile('/test.js')!; expect(testContents).toContain(`var dep_1 = require("./dep");`); @@ -91,9 +90,12 @@ runInEachFileSystem(() => { if (id.getSourceFile().fileName === sf.fileName) { return ts.factory.updateSourceFile(sf, [ ...sf.statements, - ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList([ - ts.factory.createVariableDeclaration('ref', undefined, undefined, id), - ])) + ts.factory.createVariableStatement( + undefined, + ts.factory.createVariableDeclarationList([ + ts.factory.createVariableDeclaration('ref', undefined, undefined, id), + ]), + ), ]); } return sf; diff --git a/packages/compiler-cli/src/ngtsc/imports/test/emitter_spec.ts b/packages/compiler-cli/src/ngtsc/imports/test/emitter_spec.ts index be224a01f8fc5..af26f5fc44939 100644 --- a/packages/compiler-cli/src/ngtsc/imports/test/emitter_spec.ts +++ b/packages/compiler-cli/src/ngtsc/imports/test/emitter_spec.ts @@ -13,7 +13,14 @@ import {absoluteFrom as _, basename, LogicalFileSystem} from '../../file_system' import {runInEachFileSystem, TestFile} from '../../file_system/testing'; import {Declaration, TypeScriptReflectionHost} from '../../reflection'; import {getDeclaration, makeProgram} from '../../testing'; -import {AbsoluteModuleStrategy, ImportFlags, LogicalProjectStrategy, ReferenceEmitKind, RelativePathStrategy, UnifiedModulesStrategy} from '../src/emitter'; +import { + AbsoluteModuleStrategy, + ImportFlags, + LogicalProjectStrategy, + ReferenceEmitKind, + RelativePathStrategy, + UnifiedModulesStrategy, +} from '../src/emitter'; import {Reference} from '../src/references'; import {ModuleResolver} from '../src/resolver'; @@ -23,9 +30,17 @@ runInEachFileSystem(() => { const {program, host} = makeProgram(files); const checker = program.getTypeChecker(); const moduleResolver = new ModuleResolver( - program, program.getCompilerOptions(), host, /* moduleResolutionCache */ null); + program, + program.getCompilerOptions(), + host, + /* moduleResolutionCache */ null, + ); const strategy = new AbsoluteModuleStrategy( - program, checker, moduleResolver, new TypeScriptReflectionHost(checker)); + program, + checker, + moduleResolver, + new TypeScriptReflectionHost(checker), + ); return {strategy, program}; } @@ -41,8 +56,12 @@ runInEachFileSystem(() => { contents: 'export class Context {}', }, ]); - const decl = - getDeclaration(program, _('/node_modules/external.d.ts'), 'Foo', ts.isClassDeclaration); + const decl = getDeclaration( + program, + _('/node_modules/external.d.ts'), + 'Foo', + ts.isClassDeclaration, + ); const context = program.getSourceFile(_('/context.ts'))!; const reference = new Reference(decl); @@ -66,8 +85,12 @@ runInEachFileSystem(() => { contents: 'export class Context {}', }, ]); - const decl = - getDeclaration(program, _('/node_modules/external.d.ts'), 'Foo', ts.isClassDeclaration); + const decl = getDeclaration( + program, + _('/node_modules/external.d.ts'), + 'Foo', + ts.isClassDeclaration, + ); const context = program.getSourceFile(_('/context.ts'))!; const reference = new Reference(decl, { @@ -99,8 +122,12 @@ runInEachFileSystem(() => { contents: 'export class Context {}', }, ]); - const decl = - getDeclaration(program, _('/node_modules/external.d.ts'), 'Foo', ts.isClassDeclaration); + const decl = getDeclaration( + program, + _('/node_modules/external.d.ts'), + 'Foo', + ts.isClassDeclaration, + ); const context = program.getSourceFile(_('/context.ts'))!; const reference = new Reference(decl, { @@ -130,16 +157,20 @@ runInEachFileSystem(() => { }, ]); const decl = getDeclaration( - program, _('/node_modules/external.d.ts'), 'Foo', ts.isInterfaceDeclaration); + program, + _('/node_modules/external.d.ts'), + 'Foo', + ts.isInterfaceDeclaration, + ); const context = program.getSourceFile(_('/context.ts'))!; const reference = new Reference(decl, { specifier: 'external', resolutionContext: context.fileName, }); - expect(() => strategy.emit(reference, context, ImportFlags.None)) - .toThrowError( - 'Importing a type-only declaration of type InterfaceDeclaration in a value position is not allowed.'); + expect(() => strategy.emit(reference, context, ImportFlags.None)).toThrowError( + 'Importing a type-only declaration of type InterfaceDeclaration in a value position is not allowed.', + ); }); it('should generate an import to a type-only declaration when allowed', () => { @@ -154,11 +185,17 @@ runInEachFileSystem(() => { }, ]); const decl = getDeclaration( - program, _('/node_modules/external.d.ts'), 'Foo', ts.isInterfaceDeclaration); + program, + _('/node_modules/external.d.ts'), + 'Foo', + ts.isInterfaceDeclaration, + ); const context = program.getSourceFile(_('/context.ts'))!; - const reference = - new Reference(decl, {specifier: 'external', resolutionContext: context.fileName}); + const reference = new Reference(decl, { + specifier: 'external', + resolutionContext: context.fileName, + }); const emitted = strategy.emit(reference, context, ImportFlags.AllowTypeImports); if (emitted === null || emitted.kind !== ReferenceEmitKind.Success) { return fail('Reference should be emitted'); @@ -175,7 +212,7 @@ runInEachFileSystem(() => { it('should enumerate exports with the ReflectionHost', () => { // Use a modified ReflectionHost that prefixes all export names that it enumerates. class TestHost extends TypeScriptReflectionHost { - override getExportsOfModule(node: ts.Node): Map|null { + override getExportsOfModule(node: ts.Node): Map | null { const realExports = super.getExportsOfModule(node); if (realExports === null) { return null; @@ -196,7 +233,7 @@ runInEachFileSystem(() => { { name: _('/context.ts'), contents: 'export class Context {}', - } + }, ]); const checker = program.getTypeChecker(); const logicalFs = new LogicalFileSystem([_('/')], host); @@ -226,7 +263,7 @@ runInEachFileSystem(() => { { name: _('/context.ts'), contents: 'export class Context {}', - } + }, ]); const checker = program.getTypeChecker(); const logicalFs = new LogicalFileSystem([_('/')], host); @@ -244,96 +281,96 @@ runInEachFileSystem(() => { expect(emitted.expression.value.moduleName).toEqual('./index'); }); - it('should never use relative imports outside of the logical filesystem for source files', - () => { - const {program, host} = makeProgram([ - { - name: _('/app/context.ts'), - contents: ` + it('should never use relative imports outside of the logical filesystem for source files', () => { + const {program, host} = makeProgram([ + { + name: _('/app/context.ts'), + contents: ` export {}; `, - }, - { - name: _('/foo.ts'), - contents: 'export declare class Foo {}', - } - ]); - const checker = program.getTypeChecker(); - const logicalFs = new LogicalFileSystem([_('/app')], host); - const strategy = - new LogicalProjectStrategy(new TypeScriptReflectionHost(checker), logicalFs); - const decl = getDeclaration(program, _('/foo.ts'), 'Foo', ts.isClassDeclaration); - const context = program.getSourceFile(_('/app/context.ts'))!; - const emitted = - strategy.emit(new Reference(decl), context, ImportFlags.AllowRelativeDtsImports); - if (emitted === null || emitted.kind !== ReferenceEmitKind.Failed) { - return fail('Reference emit should have failed'); - } - expect(emitted.reason) - .toEqual(`The file ${ - decl.getSourceFile().fileName} is outside of the configured 'rootDir'.`); - }); + }, + { + name: _('/foo.ts'), + contents: 'export declare class Foo {}', + }, + ]); + const checker = program.getTypeChecker(); + const logicalFs = new LogicalFileSystem([_('/app')], host); + const strategy = new LogicalProjectStrategy(new TypeScriptReflectionHost(checker), logicalFs); + const decl = getDeclaration(program, _('/foo.ts'), 'Foo', ts.isClassDeclaration); + const context = program.getSourceFile(_('/app/context.ts'))!; + const emitted = strategy.emit( + new Reference(decl), + context, + ImportFlags.AllowRelativeDtsImports, + ); + if (emitted === null || emitted.kind !== ReferenceEmitKind.Failed) { + return fail('Reference emit should have failed'); + } + expect(emitted.reason).toEqual( + `The file ${decl.getSourceFile().fileName} is outside of the configured 'rootDir'.`, + ); + }); - it('should use relative imports outside of the logical filesystem for declaration files if allowed', - () => { - const {program, host} = makeProgram([ - { - name: _('/app/context.ts'), - contents: ` + it('should use relative imports outside of the logical filesystem for declaration files if allowed', () => { + const {program, host} = makeProgram([ + { + name: _('/app/context.ts'), + contents: ` export {}; `, - }, - { - name: _('/foo.d.ts'), - contents: 'export declare class Foo {}', - } - ]); - const checker = program.getTypeChecker(); - const logicalFs = new LogicalFileSystem([_('/app')], host); - const strategy = - new LogicalProjectStrategy(new TypeScriptReflectionHost(checker), logicalFs); - const decl = getDeclaration(program, _('/foo.d.ts'), 'Foo', ts.isClassDeclaration); - const context = program.getSourceFile(_('/app/context.ts'))!; - const emitted = - strategy.emit(new Reference(decl), context, ImportFlags.AllowRelativeDtsImports); - if (emitted === null || emitted.kind !== ReferenceEmitKind.Success) { - return fail('Reference should be emitted'); - } - if (!(emitted.expression instanceof ExternalExpr)) { - return fail('Reference should be emitted as ExternalExpr'); - } - expect(emitted.expression.value.name).toEqual('Foo'); - expect(emitted.expression.value.moduleName).toEqual('../foo'); - }); + }, + { + name: _('/foo.d.ts'), + contents: 'export declare class Foo {}', + }, + ]); + const checker = program.getTypeChecker(); + const logicalFs = new LogicalFileSystem([_('/app')], host); + const strategy = new LogicalProjectStrategy(new TypeScriptReflectionHost(checker), logicalFs); + const decl = getDeclaration(program, _('/foo.d.ts'), 'Foo', ts.isClassDeclaration); + const context = program.getSourceFile(_('/app/context.ts'))!; + const emitted = strategy.emit( + new Reference(decl), + context, + ImportFlags.AllowRelativeDtsImports, + ); + if (emitted === null || emitted.kind !== ReferenceEmitKind.Success) { + return fail('Reference should be emitted'); + } + if (!(emitted.expression instanceof ExternalExpr)) { + return fail('Reference should be emitted as ExternalExpr'); + } + expect(emitted.expression.value.name).toEqual('Foo'); + expect(emitted.expression.value.moduleName).toEqual('../foo'); + }); - it('should not use relative imports outside of the logical filesystem for declaration files if not allowed', - () => { - const {program, host} = makeProgram([ - { - name: _('/app/context.ts'), - contents: ` + it('should not use relative imports outside of the logical filesystem for declaration files if not allowed', () => { + const {program, host} = makeProgram([ + { + name: _('/app/context.ts'), + contents: ` export {}; `, - }, - { - name: _('/foo.d.ts'), - contents: 'export declare class Foo {}', - } - ]); - const checker = program.getTypeChecker(); - const logicalFs = new LogicalFileSystem([_('/app')], host); - const strategy = - new LogicalProjectStrategy(new TypeScriptReflectionHost(checker), logicalFs); - const decl = getDeclaration(program, _('/foo.d.ts'), 'Foo', ts.isClassDeclaration); - const context = program.getSourceFile(_('/app/context.ts'))!; - const emitted = strategy.emit(new Reference(decl), context, ImportFlags.None); - if (emitted === null || emitted.kind !== ReferenceEmitKind.Failed) { - return fail('Reference emit should have failed'); - } - expect(emitted.reason) - .toEqual(`The file ${ - decl.getSourceFile().fileName} is outside of the configured 'rootDir'.`); - }); + }, + { + name: _('/foo.d.ts'), + contents: 'export declare class Foo {}', + }, + ]); + const checker = program.getTypeChecker(); + const logicalFs = new LogicalFileSystem([_('/app')], host); + const strategy = new LogicalProjectStrategy(new TypeScriptReflectionHost(checker), logicalFs); + const decl = getDeclaration(program, _('/foo.d.ts'), 'Foo', ts.isClassDeclaration); + const context = program.getSourceFile(_('/app/context.ts'))!; + const emitted = strategy.emit(new Reference(decl), context, ImportFlags.None); + if (emitted === null || emitted.kind !== ReferenceEmitKind.Failed) { + return fail('Reference emit should have failed'); + } + expect(emitted.reason).toEqual( + `The file ${decl.getSourceFile().fileName} is outside of the configured 'rootDir'.`, + ); + }); }); describe('RelativePathStrategy', () => { @@ -351,7 +388,7 @@ runInEachFileSystem(() => { { name: _('/context.ts'), contents: 'export class Context {}', - } + }, ]); const checker = program.getTypeChecker(); const strategy = new RelativePathStrategy(new TypeScriptReflectionHost(checker)); @@ -384,13 +421,13 @@ runInEachFileSystem(() => { { name: _('/context.ts'), contents: 'export class Context {}', - } + }, ]); const checker = program.getTypeChecker(); const host: UnifiedModulesHost = { fileNameToModuleName(importedFilePath): string { return basename(importedFilePath, '.ts'); - } + }, }; const strategy = new UnifiedModulesStrategy(new TypeScriptReflectionHost(checker), host); const decl = getDeclaration(program, _('/index.ts'), 'Foo', ts.isClassDeclaration); diff --git a/packages/compiler-cli/src/ngtsc/incremental/api.ts b/packages/compiler-cli/src/ngtsc/incremental/api.ts index a86eda3e41075..a5d0fd3cb4ce9 100644 --- a/packages/compiler-cli/src/ngtsc/incremental/api.ts +++ b/packages/compiler-cli/src/ngtsc/incremental/api.ts @@ -21,12 +21,12 @@ export interface IncrementalBuild { /** * Retrieve the prior analysis work, if any, done for the given source file. */ - priorAnalysisFor(sf: ts.SourceFile): AnalysisT[]|null; + priorAnalysisFor(sf: ts.SourceFile): AnalysisT[] | null; /** * Retrieve the prior type-checking work, if any, that's been done for the given source file. */ - priorTypeCheckingResultsFor(fileSf: ts.SourceFile): FileTypeCheckDataT|null; + priorTypeCheckingResultsFor(fileSf: ts.SourceFile): FileTypeCheckDataT | null; /** * Reports that template type-checking has completed successfully, with a map of type-checking diff --git a/packages/compiler-cli/src/ngtsc/incremental/index.ts b/packages/compiler-cli/src/ngtsc/incremental/index.ts index 8bcda25025786..a5aa562bad959 100644 --- a/packages/compiler-cli/src/ngtsc/incremental/index.ts +++ b/packages/compiler-cli/src/ngtsc/incremental/index.ts @@ -8,6 +8,12 @@ export {IncrementalCompilation} from './src/incremental'; export {NOOP_INCREMENTAL_BUILD} from './src/noop'; -export {AnalyzedIncrementalState, DeltaIncrementalState, FreshIncrementalState, IncrementalState, IncrementalStateKind} from './src/state'; +export { + AnalyzedIncrementalState, + DeltaIncrementalState, + FreshIncrementalState, + IncrementalState, + IncrementalStateKind, +} from './src/state'; export * from './src/strategy'; diff --git a/packages/compiler-cli/src/ngtsc/incremental/semantic_graph/index.ts b/packages/compiler-cli/src/ngtsc/incremental/semantic_graph/index.ts index 5bf44115596a2..db755073ea6ab 100644 --- a/packages/compiler-cli/src/ngtsc/incremental/semantic_graph/index.ts +++ b/packages/compiler-cli/src/ngtsc/incremental/semantic_graph/index.ts @@ -8,5 +8,9 @@ export {SemanticReference, SemanticSymbol} from './src/api'; export {SemanticDepGraph, SemanticDepGraphUpdater} from './src/graph'; -export {areTypeParametersEqual, extractSemanticTypeParameters, SemanticTypeParameter} from './src/type_parameters'; +export { + areTypeParametersEqual, + extractSemanticTypeParameters, + SemanticTypeParameter, +} from './src/type_parameters'; export {isArrayEqual, isReferenceEqual, isSetEqual, isSymbolEqual} from './src/util'; diff --git a/packages/compiler-cli/src/ngtsc/incremental/semantic_graph/src/api.ts b/packages/compiler-cli/src/ngtsc/incremental/semantic_graph/src/api.ts index 8453d2a40f2e8..d104345ef2dcf 100644 --- a/packages/compiler-cli/src/ngtsc/incremental/semantic_graph/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/incremental/semantic_graph/src/api.ts @@ -31,13 +31,13 @@ export abstract class SemanticSymbol { * case, the symbol is always assumed to have semantically changed to guarantee a proper * rebuild. */ - public readonly identifier: string|null; + public readonly identifier: string | null; constructor( - /** - * The declaration for this symbol. - */ - public readonly decl: ClassDeclaration, + /** + * The declaration for this symbol. + */ + public readonly decl: ClassDeclaration, ) { this.path = absoluteFromSourceFile(decl.getSourceFile()); this.identifier = getSymbolIdentifier(decl); @@ -94,8 +94,10 @@ export abstract class SemanticSymbol { * different type of symbol, if e.g. a Component was changed into a Directive with the same name. * @param typeCheckApiAffected The set of symbols of which the type-check API has changed. */ - isTypeCheckBlockAffected? - (previousSymbol: SemanticSymbol, typeCheckApiAffected: Set): boolean; + isTypeCheckBlockAffected?( + previousSymbol: SemanticSymbol, + typeCheckApiAffected: Set, + ): boolean; } /** @@ -112,10 +114,10 @@ export interface SemanticReference { /** * The path by which the symbol has been referenced. */ - importPath: string|null; + importPath: string | null; } -function getSymbolIdentifier(decl: ClassDeclaration): string|null { +function getSymbolIdentifier(decl: ClassDeclaration): string | null { if (!ts.isSourceFile(decl.parent)) { return null; } diff --git a/packages/compiler-cli/src/ngtsc/incremental/semantic_graph/src/graph.ts b/packages/compiler-cli/src/ngtsc/incremental/semantic_graph/src/graph.ts index 28964d6c0d343..628a54fe53a4c 100644 --- a/packages/compiler-cli/src/ngtsc/incremental/semantic_graph/src/graph.ts +++ b/packages/compiler-cli/src/ngtsc/incremental/semantic_graph/src/graph.ts @@ -56,8 +56,10 @@ export class SemanticDepGraph { // error TS2742: The inferred type of 'symbolByDecl' cannot be named without a reference to // '../../../../../../../external/npm/node_modules/typescript/lib/typescript'. This is likely // not portable. A type annotation is necessary. - readonly symbolByDecl: Map = - new Map(); + readonly symbolByDecl: Map = new Map< + ClassDeclaration, + SemanticSymbol + >(); /** * Registers a symbol in the graph. The symbol is given a unique identifier if possible, such that @@ -85,7 +87,7 @@ export class SemanticDepGraph { * @param symbol The symbol from another graph for which its equivalent in this graph should be * found. */ - getEquivalentSymbol(symbol: SemanticSymbol): SemanticSymbol|null { + getEquivalentSymbol(symbol: SemanticSymbol): SemanticSymbol | null { // First lookup the symbol by its declaration. It is typical for the declaration to not have // changed across rebuilds, so this is likely to find the symbol. Using the declaration also // allows to diff symbols for which no unique identifier could be determined. @@ -104,7 +106,7 @@ export class SemanticDepGraph { /** * Attempts to find the symbol by its identifier. */ - private getSymbolByName(path: AbsoluteFsPath, identifier: string): SemanticSymbol|null { + private getSymbolByName(path: AbsoluteFsPath, identifier: string): SemanticSymbol | null { if (!this.files.has(path)) { return null; } @@ -118,7 +120,7 @@ export class SemanticDepGraph { /** * Attempts to resolve the declaration to its semantic symbol. */ - getSymbolByDecl(decl: ClassDeclaration): SemanticSymbol|null { + getSymbolByDecl(decl: ClassDeclaration): SemanticSymbol | null { if (!this.symbolByDecl.has(decl)) { return null; } @@ -140,11 +142,12 @@ export class SemanticDepGraphUpdater { private readonly opaqueSymbols = new Map(); constructor( - /** - * The semantic dependency graph of the most recently succeeded compilation, or null if this - * is the initial build. - */ - private priorGraph: SemanticDepGraph|null) {} + /** + * The semantic dependency graph of the most recently succeeded compilation, or null if this + * is the initial build. + */ + private priorGraph: SemanticDepGraph | null, + ) {} /** * Registers the symbol in the new graph that is being created. @@ -231,8 +234,10 @@ export class SemanticDepGraphUpdater { } const previousSymbol = priorGraph.getEquivalentSymbol(symbol); - if (previousSymbol === null || - symbol.isTypeCheckBlockAffected(previousSymbol, isTypeCheckApiAffected)) { + if ( + previousSymbol === null || + symbol.isTypeCheckBlockAffected(previousSymbol, isTypeCheckApiAffected) + ) { needsTypeCheckEmit.add(symbol.path); } } @@ -280,7 +285,7 @@ export class SemanticDepGraphUpdater { } } -function getImportPath(expr: Expression): string|null { +function getImportPath(expr: Expression): string | null { if (expr instanceof ExternalExpr) { return `${expr.value.moduleName}\$${expr.value.name}`; } else { diff --git a/packages/compiler-cli/src/ngtsc/incremental/semantic_graph/src/type_parameters.ts b/packages/compiler-cli/src/ngtsc/incremental/semantic_graph/src/type_parameters.ts index 25c1f0ea292d4..a533004da5a61 100644 --- a/packages/compiler-cli/src/ngtsc/incremental/semantic_graph/src/type_parameters.ts +++ b/packages/compiler-cli/src/ngtsc/incremental/semantic_graph/src/type_parameters.ts @@ -34,21 +34,25 @@ export interface SemanticTypeParameter { * Converts the type parameters of the given class into their semantic representation. If the class * does not have any type parameters, then `null` is returned. */ -export function extractSemanticTypeParameters(node: ClassDeclaration): SemanticTypeParameter[]| - null { +export function extractSemanticTypeParameters( + node: ClassDeclaration, +): SemanticTypeParameter[] | null { if (!ts.isClassDeclaration(node) || node.typeParameters === undefined) { return null; } - return node.typeParameters.map( - typeParam => ({hasGenericTypeBound: typeParam.constraint !== undefined})); + return node.typeParameters.map((typeParam) => ({ + hasGenericTypeBound: typeParam.constraint !== undefined, + })); } /** * Compares the list of type parameters to determine if they can be considered equal. */ export function areTypeParametersEqual( - current: SemanticTypeParameter[]|null, previous: SemanticTypeParameter[]|null): boolean { + current: SemanticTypeParameter[] | null, + previous: SemanticTypeParameter[] | null, +): boolean { // First compare all type parameters one-to-one; any differences mean that the list of type // parameters has changed. if (!isArrayEqual(current, previous, isTypeParameterEqual)) { @@ -58,7 +62,7 @@ export function areTypeParametersEqual( // If there is a current list of type parameters and if any of them has a generic type constraint, // then the meaning of that type parameter may have changed without us being aware; as such we // have to assume that the type parameters have in fact changed. - if (current !== null && current.some(typeParam => typeParam.hasGenericTypeBound)) { + if (current !== null && current.some((typeParam) => typeParam.hasGenericTypeBound)) { return false; } diff --git a/packages/compiler-cli/src/ngtsc/incremental/semantic_graph/src/util.ts b/packages/compiler-cli/src/ngtsc/incremental/semantic_graph/src/util.ts index 2f26510054591..2b9ab0b0132d4 100644 --- a/packages/compiler-cli/src/ngtsc/incremental/semantic_graph/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/incremental/semantic_graph/src/util.ts @@ -48,8 +48,10 @@ export function referenceEquality(a: T, b: T): boolean { * that is called for all entries in the array. */ export function isArrayEqual( - a: readonly T[]|null, b: readonly T[]|null, - equalityTester: (a: T, b: T) => boolean = referenceEquality): boolean { + a: readonly T[] | null, + b: readonly T[] | null, + equalityTester: (a: T, b: T) => boolean = referenceEquality, +): boolean { if (a === null || b === null) { return a === b; } @@ -66,8 +68,10 @@ export function isArrayEqual( * Sets that only differ in ordering are considered equal. */ export function isSetEqual( - a: ReadonlySet|null, b: ReadonlySet|null, - equalityTester: (a: T, b: T) => boolean = referenceEquality): boolean { + a: ReadonlySet | null, + b: ReadonlySet | null, + equalityTester: (a: T, b: T) => boolean = referenceEquality, +): boolean { if (a === null || b === null) { return a === b; } diff --git a/packages/compiler-cli/src/ngtsc/incremental/src/dependency_tracking.ts b/packages/compiler-cli/src/ngtsc/incremental/src/dependency_tracking.ts index 48b07cdb0a7cf..62da8723c2c43 100644 --- a/packages/compiler-cli/src/ngtsc/incremental/src/dependency_tracking.ts +++ b/packages/compiler-cli/src/ngtsc/incremental/src/dependency_tracking.ts @@ -23,8 +23,9 @@ import {DependencyTracker} from '../api'; * 2. One of its dependencies has physically changed. * 3. One of its resource dependencies has physically changed. */ -export class FileDependencyGraph implements - DependencyTracker { +export class FileDependencyGraph + implements DependencyTracker +{ private nodes = new Map(); addDependency(from: T, on: T): void { @@ -67,9 +68,11 @@ export class FileDependencyGraph i * P(n) = the physically changed files from build n - 1 to build n. */ updateWithPhysicalChanges( - previous: FileDependencyGraph, changedTsPaths: Set, - deletedTsPaths: Set, - changedResources: Set): Set { + previous: FileDependencyGraph, + changedTsPaths: Set, + deletedTsPaths: Set, + changedResources: Set, + ): Set { const logicallyChanged = new Set(); for (const sf of previous.nodes.keys()) { @@ -106,9 +109,12 @@ export class FileDependencyGraph i * changed files and resources. */ function isLogicallyChanged( - sf: T, node: FileNode, changedTsPaths: ReadonlySet, - deletedTsPaths: ReadonlySet, - changedResources: ReadonlySet): boolean { + sf: T, + node: FileNode, + changedTsPaths: ReadonlySet, + deletedTsPaths: ReadonlySet, + changedResources: ReadonlySet, +): boolean { // A file is assumed to have logically changed if its dependencies could not be determined // accurately. if (node.failedAnalysis) { diff --git a/packages/compiler-cli/src/ngtsc/incremental/src/incremental.ts b/packages/compiler-cli/src/ngtsc/incremental/src/incremental.ts index 939ae312fe859..274ad2698a98e 100644 --- a/packages/compiler-cli/src/ngtsc/incremental/src/incremental.ts +++ b/packages/compiler-cli/src/ngtsc/incremental/src/incremental.ts @@ -18,7 +18,12 @@ import {IncrementalBuild} from '../api'; import {SemanticDepGraphUpdater} from '../semantic_graph'; import {FileDependencyGraph} from './dependency_tracking'; -import {AnalyzedIncrementalState, DeltaIncrementalState, IncrementalState, IncrementalStateKind} from './state'; +import { + AnalyzedIncrementalState, + DeltaIncrementalState, + IncrementalState, + IncrementalStateKind, +} from './state'; /** * Information about the previous compilation being used as a starting point for the current one, @@ -58,7 +63,7 @@ interface TypeCheckAndEmitPhase { /** * Represents the current phase of a compilation. */ -type Phase = AnalysisPhase|TypeCheckAndEmitPhase; +type Phase = AnalysisPhase | TypeCheckAndEmitPhase; /** * Manages the incremental portion of an Angular compilation, allowing for reuse of a prior @@ -77,23 +82,29 @@ export class IncrementalCompilation implements IncrementalBuild|null, private step: IncrementalStep|null) { + state: IncrementalState, + readonly depGraph: FileDependencyGraph, + private versions: Map | null, + private step: IncrementalStep | null, + ) { this._state = state; // The compilation begins in analysis phase. this.phase = { kind: PhaseKind.Analysis, - semanticDepGraphUpdater: - new SemanticDepGraphUpdater(step !== null ? step.priorState.semanticDepGraph : null), + semanticDepGraphUpdater: new SemanticDepGraphUpdater( + step !== null ? step.priorState.semanticDepGraph : null, + ), }; } /** * Begin a fresh `IncrementalCompilation`. */ - static fresh(program: ts.Program, versions: Map|null): - IncrementalCompilation { + static fresh( + program: ts.Program, + versions: Map | null, + ): IncrementalCompilation { const state: IncrementalState = { kind: IncrementalStateKind.Fresh, }; @@ -101,14 +112,17 @@ export class IncrementalCompilation implements IncrementalBuild|null, oldProgram: ts.Program, - oldState: IncrementalState, modifiedResourceFiles: Set|null, - perf: PerfRecorder): IncrementalCompilation { + program: ts.Program, + newVersions: Map | null, + oldProgram: ts.Program, + oldState: IncrementalState, + modifiedResourceFiles: Set | null, + perf: PerfRecorder, + ): IncrementalCompilation { return perf.inPhase(PerfPhase.Reconciliation, () => { const physicallyChangedTsFiles = new Set(); const changedResourceFiles = new Set(modifiedResourceFiles ?? []); - let priorAnalysis: AnalyzedIncrementalState; switch (oldState.kind) { case IncrementalStateKind.Fresh: @@ -138,7 +152,7 @@ export class IncrementalCompilation implements IncrementalBuild absoluteFromSourceFile(sf))); + const deletedTsFiles = new Set(oldFilesArray.map((sf) => absoluteFromSourceFile(sf))); for (const possiblyRedirectedNewFile of program.getSourceFiles()) { const sf = toOriginalSourceFile(possiblyRedirectedNewFile); @@ -160,8 +174,11 @@ export class IncrementalCompilation implements IncrementalBuild|null; + typeCheckResults: Map | null; /** * Cumulative set of source file paths which were definitively emitted by this compilation or @@ -73,7 +73,7 @@ export interface AnalyzedIncrementalState { /** * Map of source file paths to the version of this file as seen in the compilation. */ - versions: Map|null; + versions: Map | null; } /** @@ -111,4 +111,7 @@ export interface DeltaIncrementalState { * * Discriminated by the `IncrementalStateKind` enum. */ -export type IncrementalState = AnalyzedIncrementalState|DeltaIncrementalState|FreshIncrementalState; +export type IncrementalState = + | AnalyzedIncrementalState + | DeltaIncrementalState + | FreshIncrementalState; diff --git a/packages/compiler-cli/src/ngtsc/incremental/src/strategy.ts b/packages/compiler-cli/src/ngtsc/incremental/src/strategy.ts index f13cb54e82375..07c0e8bf59abf 100644 --- a/packages/compiler-cli/src/ngtsc/incremental/src/strategy.ts +++ b/packages/compiler-cli/src/ngtsc/incremental/src/strategy.ts @@ -18,7 +18,7 @@ export interface IncrementalBuildStrategy { /** * Determine the Angular `IncrementalState` for the given `ts.Program`, if one is available. */ - getIncrementalState(program: ts.Program): IncrementalState|null; + getIncrementalState(program: ts.Program): IncrementalState | null; /** * Associate the given `IncrementalState` with the given `ts.Program` and make it available to @@ -53,10 +53,10 @@ export class NoopIncrementalBuildStrategy implements IncrementalBuildStrategy { * Tracks an `IncrementalState` within the strategy itself. */ export class TrackedIncrementalBuildStrategy implements IncrementalBuildStrategy { - private state: IncrementalState|null = null; + private state: IncrementalState | null = null; private isSet: boolean = false; - getIncrementalState(): IncrementalState|null { + getIncrementalState(): IncrementalState | null { return this.state; } @@ -78,7 +78,7 @@ export class TrackedIncrementalBuildStrategy implements IncrementalBuildStrategy * program under `SYM_INCREMENTAL_STATE`. */ export class PatchedProgramIncrementalBuildStrategy implements IncrementalBuildStrategy { - getIncrementalState(program: ts.Program): IncrementalState|null { + getIncrementalState(program: ts.Program): IncrementalState | null { const state = (program as MayHaveIncrementalState)[SYM_INCREMENTAL_STATE]; if (state === undefined) { return null; @@ -95,7 +95,6 @@ export class PatchedProgramIncrementalBuildStrategy implements IncrementalBuildS } } - /** * Symbol under which the `IncrementalState` is stored on a `ts.Program`. * diff --git a/packages/compiler-cli/src/ngtsc/incremental/test/incremental_spec.ts b/packages/compiler-cli/src/ngtsc/incremental/test/incremental_spec.ts index 1dd3edca02318..62b13a32622f7 100644 --- a/packages/compiler-cli/src/ngtsc/incremental/test/incremental_spec.ts +++ b/packages/compiler-cli/src/ngtsc/incremental/test/incremental_spec.ts @@ -17,24 +17,24 @@ runInEachFileSystem(() => { describe('incremental reconciliation', () => { it('should treat source files with changed versions as changed', () => { const FOO_PATH = absoluteFrom('/foo.ts'); - const {program} = makeProgram([ - {name: FOO_PATH, contents: `export const FOO = true;`}, - ]); + const {program} = makeProgram([{name: FOO_PATH, contents: `export const FOO = true;`}]); const fooSf = getSourceFileOrError(program, FOO_PATH); const traitCompiler = {getAnalyzedRecords: () => new Map()} as TraitCompiler; const versionMapFirst = new Map([[FOO_PATH, 'version.1']]); - const firstCompilation = IncrementalCompilation.fresh( - program, - versionMapFirst, - ); + const firstCompilation = IncrementalCompilation.fresh(program, versionMapFirst); firstCompilation.recordSuccessfulAnalysis(traitCompiler); firstCompilation.recordSuccessfulEmit(fooSf); const versionMapSecond = new Map([[FOO_PATH, 'version.2']]); const secondCompilation = IncrementalCompilation.incremental( - program, versionMapSecond, program, firstCompilation.state, new Set(), - NOOP_PERF_RECORDER); + program, + versionMapSecond, + program, + firstCompilation.state, + new Set(), + NOOP_PERF_RECORDER, + ); secondCompilation.recordSuccessfulAnalysis(traitCompiler); expect(secondCompilation.safeToSkipEmit(fooSf)).toBeFalse(); diff --git a/packages/compiler-cli/src/ngtsc/indexer/src/api.ts b/packages/compiler-cli/src/ngtsc/indexer/src/api.ts index fc166ac33a2c3..4f3bcd47e626a 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/src/api.ts @@ -15,7 +15,7 @@ import {ClassDeclaration, DeclarationNode} from '../../reflection'; */ export enum IdentifierKind { Property, - Method, // TODO: No longer being used. To be removed together with `MethodIdentifier`. + Method, // TODO: No longer being used. To be removed together with `MethodIdentifier`. Element, Template, Attribute, @@ -39,7 +39,7 @@ interface ExpressionIdentifier extends TemplateIdentifier { * ReferenceIdentifier or VariableIdentifier in the template that this identifier targets, if * any. If the target is `null`, it points to a declaration on the component class. * */ - target: ReferenceIdentifier|VariableIdentifier|null; + target: ReferenceIdentifier | VariableIdentifier | null; } /** Describes a property accessed in a template. */ @@ -94,14 +94,14 @@ export interface ReferenceIdentifier extends TemplateIdentifier { /** The target of this reference. If the target is not known, this is `null`. */ target: { /** The template AST node that the reference targets. */ - node: ElementIdentifier|TemplateIdentifier; + node: ElementIdentifier | TemplateIdentifier; /** * The directive on `node` that the reference targets. If no directive is targeted, this is * `null`. */ directive: ClassDeclaration | null; - }|null; + } | null; } /** Describes a template variable like "foo" in `
`. */ @@ -113,14 +113,22 @@ export interface VariableIdentifier extends TemplateIdentifier { * Identifiers recorded at the top level of the template, without any context about the HTML nodes * they were discovered in. */ -export type TopLevelIdentifier = PropertyIdentifier|ElementIdentifier|TemplateNodeIdentifier| - ReferenceIdentifier|VariableIdentifier|MethodIdentifier; +export type TopLevelIdentifier = + | PropertyIdentifier + | ElementIdentifier + | TemplateNodeIdentifier + | ReferenceIdentifier + | VariableIdentifier + | MethodIdentifier; /** * Describes the absolute byte offsets of a text anchor in a source code. */ export class AbsoluteSourceSpan { - constructor(public start: number, public end: number) {} + constructor( + public start: number, + public end: number, + ) {} } /** @@ -128,12 +136,12 @@ export class AbsoluteSourceSpan { */ export interface IndexedComponent { name: string; - selector: string|null; + selector: string | null; file: ParseSourceFile; template: { - identifiers: Set, - usedComponents: Set, - isInline: boolean, + identifiers: Set; + usedComponents: Set; + isInline: boolean; file: ParseSourceFile; }; errors: Error[]; diff --git a/packages/compiler-cli/src/ngtsc/indexer/src/context.ts b/packages/compiler-cli/src/ngtsc/indexer/src/context.ts index 028d140386246..3da2cad42516c 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/src/context.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/src/context.ts @@ -16,7 +16,7 @@ export interface ComponentMeta extends DirectiveMeta { /** * Unparsed selector of the directive, or null if the directive does not have a selector. */ - selector: string|null; + selector: string | null; } /** @@ -27,7 +27,7 @@ export interface ComponentInfo { declaration: ClassDeclaration; /** Component template selector if it exists, otherwise null. */ - selector: string|null; + selector: string | null; /** * BoundTarget containing the parsed template. Can also be used to query for directives used in diff --git a/packages/compiler-cli/src/ngtsc/indexer/src/template.ts b/packages/compiler-cli/src/ngtsc/indexer/src/template.ts index 7e224cc3353d9..2906b32b3559d 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/src/template.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/src/template.ts @@ -5,11 +5,52 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {AST, ASTWithSource, BoundTarget, ImplicitReceiver, ParseSourceSpan, PropertyRead, PropertyWrite, RecursiveAstVisitor, TmplAstBoundAttribute, TmplAstBoundDeferredTrigger, TmplAstBoundEvent, TmplAstBoundText, TmplAstDeferredBlock, TmplAstDeferredBlockError, TmplAstDeferredBlockLoading, TmplAstDeferredBlockPlaceholder, TmplAstDeferredTrigger, TmplAstElement, TmplAstForLoopBlock, TmplAstForLoopBlockEmpty, TmplAstIfBlock, TmplAstIfBlockBranch, TmplAstNode, TmplAstRecursiveVisitor, TmplAstReference, TmplAstSwitchBlock, TmplAstSwitchBlockCase, TmplAstTemplate, TmplAstVariable} from '@angular/compiler'; +import { + AST, + ASTWithSource, + BoundTarget, + ImplicitReceiver, + ParseSourceSpan, + PropertyRead, + PropertyWrite, + RecursiveAstVisitor, + TmplAstBoundAttribute, + TmplAstBoundDeferredTrigger, + TmplAstBoundEvent, + TmplAstBoundText, + TmplAstDeferredBlock, + TmplAstDeferredBlockError, + TmplAstDeferredBlockLoading, + TmplAstDeferredBlockPlaceholder, + TmplAstDeferredTrigger, + TmplAstElement, + TmplAstForLoopBlock, + TmplAstForLoopBlockEmpty, + TmplAstIfBlock, + TmplAstIfBlockBranch, + TmplAstNode, + TmplAstRecursiveVisitor, + TmplAstReference, + TmplAstSwitchBlock, + TmplAstSwitchBlockCase, + TmplAstTemplate, + TmplAstVariable, +} from '@angular/compiler'; import {ClassDeclaration, DeclarationNode} from '../../reflection'; -import {AbsoluteSourceSpan, AttributeIdentifier, ElementIdentifier, IdentifierKind, MethodIdentifier, PropertyIdentifier, ReferenceIdentifier, TemplateNodeIdentifier, TopLevelIdentifier, VariableIdentifier} from './api'; +import { + AbsoluteSourceSpan, + AttributeIdentifier, + ElementIdentifier, + IdentifierKind, + MethodIdentifier, + PropertyIdentifier, + ReferenceIdentifier, + TemplateNodeIdentifier, + TopLevelIdentifier, + VariableIdentifier, +} from './api'; import {ComponentMeta} from './context'; /** @@ -21,9 +62,9 @@ interface HTMLNode extends TmplAstNode { name?: string; } -type ExpressionIdentifier = PropertyIdentifier|MethodIdentifier; -type TmplTarget = TmplAstReference|TmplAstVariable; -type TargetIdentifier = ReferenceIdentifier|VariableIdentifier; +type ExpressionIdentifier = PropertyIdentifier | MethodIdentifier; +type TmplTarget = TmplAstReference | TmplAstVariable; +type TargetIdentifier = ReferenceIdentifier | VariableIdentifier; type TargetIdentifierMap = Map; /** @@ -40,9 +81,11 @@ class ExpressionVisitor extends RecursiveAstVisitor { readonly errors: Error[] = []; private constructor( - private readonly expressionStr: string, private readonly absoluteOffset: number, - private readonly boundTemplate: BoundTarget, - private readonly targetToIdentifier: (target: TmplTarget) => TargetIdentifier | null) { + private readonly expressionStr: string, + private readonly absoluteOffset: number, + private readonly boundTemplate: BoundTarget, + private readonly targetToIdentifier: (target: TmplTarget) => TargetIdentifier | null, + ) { super(); } @@ -58,11 +101,18 @@ class ExpressionVisitor extends RecursiveAstVisitor { * @param targetToIdentifier closure converting a template target node to its identifier. */ static getIdentifiers( - ast: AST, source: string, absoluteOffset: number, boundTemplate: BoundTarget, - targetToIdentifier: (target: TmplTarget) => TargetIdentifier | - null): {identifiers: TopLevelIdentifier[], errors: Error[]} { - const visitor = - new ExpressionVisitor(source, absoluteOffset, boundTemplate, targetToIdentifier); + ast: AST, + source: string, + absoluteOffset: number, + boundTemplate: BoundTarget, + targetToIdentifier: (target: TmplTarget) => TargetIdentifier | null, + ): {identifiers: TopLevelIdentifier[]; errors: Error[]} { + const visitor = new ExpressionVisitor( + source, + absoluteOffset, + boundTemplate, + targetToIdentifier, + ); visitor.visit(ast); return {identifiers: visitor.identifiers, errors: visitor.errors}; } @@ -88,7 +138,9 @@ class ExpressionVisitor extends RecursiveAstVisitor { * @param kind identifier kind */ private visitIdentifier( - ast: AST&{name: string, receiver: AST}, kind: ExpressionIdentifier['kind']) { + ast: AST & {name: string; receiver: AST}, + kind: ExpressionIdentifier['kind'], + ) { // The definition of a non-top-level property such as `bar` in `{{foo.bar}}` is currently // impossible to determine by an indexer and unsupported by the indexing module. // The indexing module also does not currently support references to identifiers declared in the @@ -107,8 +159,11 @@ class ExpressionVisitor extends RecursiveAstVisitor { } if (!this.expressionStr.substring(identifierStart).startsWith(ast.name)) { - this.errors.push(new Error(`Impossible state: "${ast.name}" not found in "${ - this.expressionStr}" at location ${identifierStart}`)); + this.errors.push( + new Error( + `Impossible state: "${ast.name}" not found in "${this.expressionStr}" at location ${identifierStart}`, + ), + ); return; } @@ -143,8 +198,10 @@ class TemplateVisitor extends TmplAstRecursiveVisitor { private readonly targetIdentifierCache: TargetIdentifierMap = new Map(); // Map of elements and templates to their identifiers. - private readonly elementAndTemplateIdentifierCache = - new Map(); + private readonly elementAndTemplateIdentifierCache = new Map< + TmplAstElement | TmplAstTemplate, + ElementIdentifier | TemplateNodeIdentifier + >(); /** * Creates a template visitor for a bound template target. The bound target can be used when @@ -166,7 +223,7 @@ class TemplateVisitor extends TmplAstRecursiveVisitor { } visitAll(nodes: TmplAstNode[]) { - nodes.forEach(node => this.visit(node)); + nodes.forEach((node) => this.visit(node)); } /** @@ -180,7 +237,6 @@ class TemplateVisitor extends TmplAstRecursiveVisitor { this.identifiers.add(elementIdentifier); } - this.visitAll(element.references); this.visitAll(element.inputs); this.visitAll(element.attributes); @@ -209,9 +265,13 @@ class TemplateVisitor extends TmplAstRecursiveVisitor { } const {identifiers, errors} = ExpressionVisitor.getIdentifiers( - attribute.value, attribute.valueSpan.toString(), attribute.valueSpan.start.offset, - this.boundTemplate, this.targetToIdentifier.bind(this)); - identifiers.forEach(id => this.identifiers.add(id)); + attribute.value, + attribute.valueSpan.toString(), + attribute.valueSpan.start.offset, + this.boundTemplate, + this.targetToIdentifier.bind(this), + ); + identifiers.forEach((id) => this.identifiers.add(id)); this.errors.push(...errors); } override visitBoundEvent(attribute: TmplAstBoundEvent) { @@ -292,15 +352,16 @@ class TemplateVisitor extends TmplAstRecursiveVisitor { } /** Creates an identifier for a template element or template node. */ - private elementOrTemplateToIdentifier(node: TmplAstElement|TmplAstTemplate): ElementIdentifier - |TemplateNodeIdentifier|null { + private elementOrTemplateToIdentifier( + node: TmplAstElement | TmplAstTemplate, + ): ElementIdentifier | TemplateNodeIdentifier | null { // If this node has already been seen, return the cached result. if (this.elementAndTemplateIdentifierCache.has(node)) { return this.elementAndTemplateIdentifierCache.get(node)!; } let name: string; - let kind: IdentifierKind.Element|IdentifierKind.Template; + let kind: IdentifierKind.Element | IdentifierKind.Template; if (node instanceof TmplAstTemplate) { name = node.tagName ?? 'ng-template'; kind = IdentifierKind.Template; @@ -341,22 +402,23 @@ class TemplateVisitor extends TmplAstRecursiveVisitor { span: absoluteSpan, kind, attributes: new Set(attributes), - usedDirectives: new Set(usedDirectives.map(dir => { - return { - node: dir.ref.node, - selector: dir.selector, - }; - })), + usedDirectives: new Set( + usedDirectives.map((dir) => { + return { + node: dir.ref.node, + selector: dir.selector, + }; + }), + ), // cast b/c pre-TypeScript 3.5 unions aren't well discriminated - } as ElementIdentifier | - TemplateNodeIdentifier; + } as ElementIdentifier | TemplateNodeIdentifier; this.elementAndTemplateIdentifierCache.set(node, identifier); return identifier; } /** Creates an identifier for a template reference or template variable target. */ - private targetToIdentifier(node: TmplAstReference|TmplAstVariable): TargetIdentifier|null { + private targetToIdentifier(node: TmplAstReference | TmplAstVariable): TargetIdentifier | null { // If this node has already been seen, return the cached result. if (this.targetIdentifierCache.has(node)) { return this.targetIdentifierCache.get(node)!; @@ -369,7 +431,7 @@ class TemplateVisitor extends TmplAstRecursiveVisitor { } const span = new AbsoluteSourceSpan(start, start + name.length); - let identifier: ReferenceIdentifier|VariableIdentifier; + let identifier: ReferenceIdentifier | VariableIdentifier; if (node instanceof TmplAstReference) { // If the node is a reference, we care about its target. The target can be an element, a // template, a directive applied on a template or element (in which case the directive field @@ -377,8 +439,8 @@ class TemplateVisitor extends TmplAstRecursiveVisitor { const refTarget = this.boundTemplate.getReferenceTarget(node); let target = null; if (refTarget) { - let node: ElementIdentifier|TemplateNodeIdentifier|null = null; - let directive: ClassDeclaration|null = null; + let node: ElementIdentifier | TemplateNodeIdentifier | null = null; + let directive: ClassDeclaration | null = null; if (refTarget instanceof TmplAstElement || refTarget instanceof TmplAstTemplate) { node = this.elementOrTemplateToIdentifier(refTarget); } else { @@ -414,7 +476,7 @@ class TemplateVisitor extends TmplAstRecursiveVisitor { } /** Gets the start location of a string in a SourceSpan */ - private getStartLocation(name: string, context: ParseSourceSpan): number|null { + private getStartLocation(name: string, context: ParseSourceSpan): number | null { const localStr = context.toString(); if (!localStr.includes(name)) { this.errors.push(new Error(`Impossible state: "${name}" not found in "${localStr}"`)); @@ -436,8 +498,13 @@ class TemplateVisitor extends TmplAstRecursiveVisitor { const targetToIdentifier = this.targetToIdentifier.bind(this); const absoluteOffset = ast.sourceSpan.start; const {identifiers, errors} = ExpressionVisitor.getIdentifiers( - ast, ast.source, absoluteOffset, this.boundTemplate, targetToIdentifier); - identifiers.forEach(id => this.identifiers.add(id)); + ast, + ast.source, + absoluteOffset, + this.boundTemplate, + targetToIdentifier, + ); + identifiers.forEach((id) => this.identifiers.add(id)); this.errors.push(...errors); } } @@ -449,8 +516,10 @@ class TemplateVisitor extends TmplAstRecursiveVisitor { * @param boundTemplate bound template target, which can be used for querying expression targets. * @return identifiers in template */ -export function getTemplateIdentifiers(boundTemplate: BoundTarget): - {identifiers: Set, errors: Error[]} { +export function getTemplateIdentifiers(boundTemplate: BoundTarget): { + identifiers: Set; + errors: Error[]; +} { const visitor = new TemplateVisitor(boundTemplate); if (boundTemplate.target.template !== undefined) { visitor.visitAll(boundTemplate.target.template); diff --git a/packages/compiler-cli/src/ngtsc/indexer/src/transform.ts b/packages/compiler-cli/src/ngtsc/indexer/src/transform.ts index 28b9cbc3fd1a3..6ed61d5d056a0 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/src/transform.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/src/transform.ts @@ -28,7 +28,7 @@ export function generateAnalysis(context: IndexingContext): Map(); const usedDirs = boundTemplate.getUsedDirectives(); - usedDirs.forEach(dir => { + usedDirs.forEach((dir) => { if (dir.isComponent) { usedComponents.add(dir.ref.node); } @@ -37,7 +37,9 @@ export function generateAnalysis(context: IndexingContext): Map { }, }); - expect(context.components).toEqual(new Set([ - { - declaration, - selector: 'c-selector', - boundTemplate, - templateMeta: { - isInline: false, - file: new ParseSourceFile('
', declaration.getSourceFile().fileName), + expect(context.components).toEqual( + new Set([ + { + declaration, + selector: 'c-selector', + boundTemplate, + templateMeta: { + isInline: false, + file: new ParseSourceFile('
', declaration.getSourceFile().fileName), + }, }, - }, - ])); + ]), + ); }); }); }); diff --git a/packages/compiler-cli/src/ngtsc/indexer/test/template_spec.ts b/packages/compiler-cli/src/ngtsc/indexer/test/template_spec.ts index da38b965aaf98..e67575f14a5a6 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/test/template_spec.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/test/template_spec.ts @@ -8,7 +8,16 @@ import {BoundTarget} from '@angular/compiler'; -import {AbsoluteSourceSpan, AttributeIdentifier, ElementIdentifier, IdentifierKind, ReferenceIdentifier, TemplateNodeIdentifier, TopLevelIdentifier, VariableIdentifier} from '..'; +import { + AbsoluteSourceSpan, + AttributeIdentifier, + ElementIdentifier, + IdentifierKind, + ReferenceIdentifier, + TemplateNodeIdentifier, + TopLevelIdentifier, + VariableIdentifier, +} from '..'; import {runInEachFileSystem} from '../../file_system/testing'; import {ComponentMeta} from '../src/context'; import {getTemplateIdentifiers as getTemplateIdentifiersAndErrors} from '../src/template'; @@ -128,26 +137,28 @@ runInEachFileSystem(() => { const refs = getTemplateIdentifiers(bind(template)); const refArr = Array.from(refs); - expect(refArr).toEqual(jasmine.arrayContaining([ - { - name: 'bar', - kind: IdentifierKind.Property, - span: new AbsoluteSourceSpan(12, 15), - target: null, - }, - { - name: 'bar', - kind: IdentifierKind.Property, - span: new AbsoluteSourceSpan(18, 21), - target: null, - }, - { - name: 'bar', - kind: IdentifierKind.Property, - span: new AbsoluteSourceSpan(24, 27), - target: null, - }, - ] as TopLevelIdentifier[])); + expect(refArr).toEqual( + jasmine.arrayContaining([ + { + name: 'bar', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(12, 15), + target: null, + }, + { + name: 'bar', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(18, 21), + target: null, + }, + { + name: 'bar', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(24, 27), + target: null, + }, + ] as TopLevelIdentifier[]), + ); }); describe('generates identifiers for PropertyReads', () => { @@ -219,13 +230,15 @@ runInEachFileSystem(() => { const refs = getTemplateIdentifiers(bind(template)); const refArr = Array.from(refs); - expect(refArr).toEqual([{ - name: 'div', - kind: IdentifierKind.Element, - span: new AbsoluteSourceSpan(1, 4), - attributes: new Set(), - usedDirectives: new Set(), - }]); + expect(refArr).toEqual([ + { + name: 'div', + kind: IdentifierKind.Element, + span: new AbsoluteSourceSpan(1, 4), + attributes: new Set(), + usedDirectives: new Set(), + }, + ]); }); it('should discover variables in bound attributes', () => { @@ -259,26 +272,28 @@ runInEachFileSystem(() => { const refs = getTemplateIdentifiers(bind(template)); const refArr = Array.from(refs); - expect(refArr).toEqual(jasmine.arrayContaining([ - { - name: 'bar', - kind: IdentifierKind.Property, - span: new AbsoluteSourceSpan(12, 15), - target: null, - }, - { - name: 'bar1', - kind: IdentifierKind.Property, - span: new AbsoluteSourceSpan(18, 22), - target: null, - }, - { - name: 'bar2', - kind: IdentifierKind.Property, - span: new AbsoluteSourceSpan(25, 29), - target: null, - }, - ] as TopLevelIdentifier[])); + expect(refArr).toEqual( + jasmine.arrayContaining([ + { + name: 'bar', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(12, 15), + target: null, + }, + { + name: 'bar1', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(18, 22), + target: null, + }, + { + name: 'bar2', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(25, 29), + target: null, + }, + ] as TopLevelIdentifier[]), + ); }); it('should discover properties in template expressions', () => { @@ -299,26 +314,28 @@ runInEachFileSystem(() => { const refs = getTemplateIdentifiers(bind(template)); const refArr = Array.from(refs); - expect(refArr).toEqual(jasmine.arrayContaining([ - { - name: 'foos', - kind: IdentifierKind.Property, - span: new AbsoluteSourceSpan(25, 29), - target: null, - }, - { - name: 'foos', - kind: IdentifierKind.Property, - span: new AbsoluteSourceSpan(32, 36), - target: null, - }, - { - name: 'foos', - kind: IdentifierKind.Property, - span: new AbsoluteSourceSpan(39, 43), - target: null, - }, - ])); + expect(refArr).toEqual( + jasmine.arrayContaining([ + { + name: 'foos', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(25, 29), + target: null, + }, + { + name: 'foos', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(32, 36), + target: null, + }, + { + name: 'foos', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(39, 43), + target: null, + }, + ]), + ); }); }); @@ -328,20 +345,22 @@ runInEachFileSystem(() => { const refs = getTemplateIdentifiers(bind(template)); const refArr = Array.from(refs); - expect(refArr).toEqual(jasmine.arrayContaining([ - { - name: 'foo', - kind: IdentifierKind.Property, - span: new AbsoluteSourceSpan(14, 17), - target: null, - }, - { - name: 'bar', - kind: IdentifierKind.Property, - span: new AbsoluteSourceSpan(18, 21), - target: null, - } - ] as TopLevelIdentifier[])); + expect(refArr).toEqual( + jasmine.arrayContaining([ + { + name: 'foo', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(14, 17), + target: null, + }, + { + name: 'bar', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(18, 21), + target: null, + }, + ] as TopLevelIdentifier[]), + ); }); it('should discover nested property writes', () => { @@ -349,12 +368,16 @@ runInEachFileSystem(() => { const refs = getTemplateIdentifiers(bind(template)); const refArr = Array.from(refs); - expect(refArr).toEqual(jasmine.arrayContaining([{ - name: 'foo', - kind: IdentifierKind.Property, - span: new AbsoluteSourceSpan(20, 23), - target: null, - }] as TopLevelIdentifier[])); + expect(refArr).toEqual( + jasmine.arrayContaining([ + { + name: 'foo', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(20, 23), + target: null, + }, + ] as TopLevelIdentifier[]), + ); }); it('should ignore property writes that are not implicitly received by the template', () => { @@ -362,7 +385,7 @@ runInEachFileSystem(() => { const refs = getTemplateIdentifiers(bind(template)); const refArr = Array.from(refs); - const bar = refArr.find(ref => ref.name.includes('bar')); + const bar = refArr.find((ref) => ref.name.includes('bar')); expect(bar).toBeUndefined(); }); }); @@ -446,12 +469,15 @@ runInEachFileSystem(() => { const refArray = Array.from(refs); expect(refArray).toEqual( - jasmine.arrayContaining([{ - name: 'foo', - kind: IdentifierKind.Reference, - span: new AbsoluteSourceSpan(6, 9), - target: {node: elementReference, directive: null}, - }] as TopLevelIdentifier[])); + jasmine.arrayContaining([ + { + name: 'foo', + kind: IdentifierKind.Reference, + span: new AbsoluteSourceSpan(6, 9), + target: {node: elementReference, directive: null}, + }, + ] as TopLevelIdentifier[]), + ); }); it('should discover nested references', () => { @@ -467,12 +493,15 @@ runInEachFileSystem(() => { const refArray = Array.from(refs); expect(refArray).toEqual( - jasmine.arrayContaining([{ - name: 'foo', - kind: IdentifierKind.Reference, - span: new AbsoluteSourceSpan(12, 15), - target: {node: elementReference, directive: null}, - }] as TopLevelIdentifier[])); + jasmine.arrayContaining([ + { + name: 'foo', + kind: IdentifierKind.Reference, + span: new AbsoluteSourceSpan(12, 15), + target: {node: elementReference, directive: null}, + }, + ] as TopLevelIdentifier[]), + ); }); it('should discover references to references', () => { @@ -493,14 +522,18 @@ runInEachFileSystem(() => { }; const refArr = Array.from(refs); - expect(refArr).toEqual(jasmine.arrayContaining([ - elementIdentifier, referenceIdentifier, { - name: 'foo', - kind: IdentifierKind.Property, - span: new AbsoluteSourceSpan(12, 15), - target: referenceIdentifier, - } - ] as TopLevelIdentifier[])); + expect(refArr).toEqual( + jasmine.arrayContaining([ + elementIdentifier, + referenceIdentifier, + { + name: 'foo', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(12, 15), + target: referenceIdentifier, + }, + ] as TopLevelIdentifier[]), + ); }); it('should discover forward references', () => { @@ -521,14 +554,18 @@ runInEachFileSystem(() => { }; const refArr = Array.from(refs); - expect(refArr).toEqual(jasmine.arrayContaining([ - elementIdentifier, referenceIdentifier, { - name: 'foo', - kind: IdentifierKind.Property, - span: new AbsoluteSourceSpan(2, 5), - target: referenceIdentifier, - } - ] as TopLevelIdentifier[])); + expect(refArr).toEqual( + jasmine.arrayContaining([ + elementIdentifier, + referenceIdentifier, + { + name: 'foo', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(2, 5), + target: referenceIdentifier, + }, + ] as TopLevelIdentifier[]), + ); }); it('should generate information directive targets', () => { @@ -540,7 +577,7 @@ runInEachFileSystem(() => { const refs = getTemplateIdentifiers(boundTemplate); const refArr = Array.from(refs); - let fooRef = refArr.find(id => id.name === 'foo'); + let fooRef = refArr.find((id) => id.name === 'foo'); expect(fooRef).toBeDefined(); expect(fooRef!.kind).toBe(IdentifierKind.Reference); @@ -570,14 +607,18 @@ runInEachFileSystem(() => { }; const refArr = Array.from(refs); - expect(refArr).toEqual(jasmine.arrayContaining([ - elementIdentifier, referenceIdentifier, { - name: 'foo', - kind: IdentifierKind.Property, - span: new AbsoluteSourceSpan(25, 28), - target: referenceIdentifier, - } - ] as TopLevelIdentifier[])); + expect(refArr).toEqual( + jasmine.arrayContaining([ + elementIdentifier, + referenceIdentifier, + { + name: 'foo', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(25, 28), + target: referenceIdentifier, + }, + ] as TopLevelIdentifier[]), + ); }); }); @@ -587,11 +628,15 @@ runInEachFileSystem(() => { const refs = getTemplateIdentifiers(bind(template)); const refArray = Array.from(refs); - expect(refArray).toEqual(jasmine.arrayContaining([{ - name: 'foo', - kind: IdentifierKind.Variable, - span: new AbsoluteSourceSpan(17, 20), - }] as TopLevelIdentifier[])); + expect(refArray).toEqual( + jasmine.arrayContaining([ + { + name: 'foo', + kind: IdentifierKind.Variable, + span: new AbsoluteSourceSpan(17, 20), + }, + ] as TopLevelIdentifier[]), + ); }); it('should discover variables with let- syntax', () => { @@ -599,11 +644,15 @@ runInEachFileSystem(() => { const refs = getTemplateIdentifiers(bind(template)); const refArray = Array.from(refs); - expect(refArray).toEqual(jasmine.arrayContaining([{ - name: 'var', - kind: IdentifierKind.Variable, - span: new AbsoluteSourceSpan(17, 20), - }] as TopLevelIdentifier[])); + expect(refArray).toEqual( + jasmine.arrayContaining([ + { + name: 'var', + kind: IdentifierKind.Variable, + span: new AbsoluteSourceSpan(17, 20), + }, + ] as TopLevelIdentifier[]), + ); }); it('should discover nested variables', () => { @@ -611,11 +660,15 @@ runInEachFileSystem(() => { const refs = getTemplateIdentifiers(bind(template)); const refArray = Array.from(refs); - expect(refArray).toEqual(jasmine.arrayContaining([{ - name: 'foo', - kind: IdentifierKind.Variable, - span: new AbsoluteSourceSpan(23, 26), - }] as TopLevelIdentifier[])); + expect(refArray).toEqual( + jasmine.arrayContaining([ + { + name: 'foo', + kind: IdentifierKind.Variable, + span: new AbsoluteSourceSpan(23, 26), + }, + ] as TopLevelIdentifier[]), + ); }); it('should discover references to variables', () => { @@ -633,22 +686,24 @@ runInEachFileSystem(() => { }; const refArr = Array.from(refs); - expect(refArr).toEqual(jasmine.arrayContaining([ - fooIdentifier, - { - name: 'foo', - kind: IdentifierKind.Property, - span: new AbsoluteSourceSpan(47, 50), - target: fooIdentifier, - }, - iIdentifier, - { - name: 'i', - kind: IdentifierKind.Property, - span: new AbsoluteSourceSpan(53, 54), - target: iIdentifier, - }, - ] as TopLevelIdentifier[])); + expect(refArr).toEqual( + jasmine.arrayContaining([ + fooIdentifier, + { + name: 'foo', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(47, 50), + target: fooIdentifier, + }, + iIdentifier, + { + name: 'i', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(53, 54), + target: iIdentifier, + }, + ] as TopLevelIdentifier[]), + ); }); it('should discover references to variables', () => { @@ -661,14 +716,17 @@ runInEachFileSystem(() => { }; const refArr = Array.from(refs); - expect(refArr).toEqual(jasmine.arrayContaining([ - variableIdentifier, { - name: 'foo', - kind: IdentifierKind.Property, - span: new AbsoluteSourceSpan(42, 45), - target: variableIdentifier, - } - ] as TopLevelIdentifier[])); + expect(refArr).toEqual( + jasmine.arrayContaining([ + variableIdentifier, + { + name: 'foo', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(42, 45), + target: variableIdentifier, + }, + ] as TopLevelIdentifier[]), + ); }); }); @@ -762,18 +820,20 @@ runInEachFileSystem(() => { const [ref] = Array.from(refs); const attrs = (ref as ElementIdentifier).attributes; - expect(attrs).toEqual(new Set([ - { - name: 'attrA', - kind: IdentifierKind.Attribute, - span: new AbsoluteSourceSpan(5, 10), - }, - { - name: 'attrB', - kind: IdentifierKind.Attribute, - span: new AbsoluteSourceSpan(11, 22), - } - ])); + expect(attrs).toEqual( + new Set([ + { + name: 'attrA', + kind: IdentifierKind.Attribute, + span: new AbsoluteSourceSpan(5, 10), + }, + { + name: 'attrB', + kind: IdentifierKind.Attribute, + span: new AbsoluteSourceSpan(11, 22), + }, + ]), + ); }); it('should generate information about used directives', () => { @@ -790,20 +850,22 @@ runInEachFileSystem(() => { const refs = getTemplateIdentifiers(boundTemplate); const [ref] = Array.from(refs); const usedDirectives = (ref as ElementIdentifier).usedDirectives; - expect(usedDirectives).toEqual(new Set([ - { - node: declA, - selector: 'a-selector', - }, - { - node: declB, - selector: '[b-selector]', - }, - { - node: declC, - selector: ':not(never-selector)', - } - ])); + expect(usedDirectives).toEqual( + new Set([ + { + node: declA, + selector: 'a-selector', + }, + { + node: declB, + selector: '[b-selector]', + }, + { + node: declC, + selector: ':not(never-selector)', + }, + ]), + ); }); }); @@ -852,18 +914,20 @@ runInEachFileSystem(() => { const [ref] = Array.from(refs); const attrs = (ref as TemplateNodeIdentifier).attributes; - expect(attrs).toEqual(new Set([ - { - name: 'attrA', - kind: IdentifierKind.Attribute, - span: new AbsoluteSourceSpan(13, 18), - }, - { - name: 'attrB', - kind: IdentifierKind.Attribute, - span: new AbsoluteSourceSpan(19, 30), - } - ])); + expect(attrs).toEqual( + new Set([ + { + name: 'attrA', + kind: IdentifierKind.Attribute, + span: new AbsoluteSourceSpan(13, 18), + }, + { + name: 'attrB', + kind: IdentifierKind.Attribute, + span: new AbsoluteSourceSpan(19, 30), + }, + ]), + ); }); it('should generate information about used directives', () => { @@ -878,16 +942,18 @@ runInEachFileSystem(() => { const refs = getTemplateIdentifiers(boundTemplate); const [ref] = Array.from(refs); const usedDirectives = (ref as ElementIdentifier).usedDirectives; - expect(usedDirectives).toEqual(new Set([ - { - node: declB, - selector: '[b-selector]', - }, - { - node: declC, - selector: ':not(never-selector)', - } - ])); + expect(usedDirectives).toEqual( + new Set([ + { + node: declB, + selector: '[b-selector]', + }, + { + node: declC, + selector: ':not(never-selector)', + }, + ]), + ); }); it('should handle interpolations in attributes, preceded by HTML entity', () => { @@ -907,7 +973,7 @@ runInEachFileSystem(() => { name: 'foo', span: new AbsoluteSourceSpan(18, 21), target: null, - } + }, ]); }); }); diff --git a/packages/compiler-cli/src/ngtsc/indexer/test/transform_spec.ts b/packages/compiler-cli/src/ngtsc/indexer/test/transform_spec.ts index 6e2148b6efb21..453038bff3b02 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/test/transform_spec.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/test/transform_spec.ts @@ -19,8 +19,13 @@ import * as util from './util'; * Adds information about a component to a context. */ function populateContext( - context: IndexingContext, component: ClassDeclaration, selector: string, template: string, - boundTemplate: BoundTarget, isInline: boolean = false) { + context: IndexingContext, + component: ClassDeclaration, + selector: string, + template: string, + boundTemplate: BoundTarget, + isInline: boolean = false, +) { context.addComponent({ declaration: component, selector, @@ -49,8 +54,8 @@ runInEachFileSystem(() => { selector: 'c-selector', file: new ParseSourceFile('class C {}', decl.getSourceFile().fileName), template: { - identifiers: - getTemplateIdentifiers(util.getBoundTemplate('
{{foo}}
')).identifiers, + identifiers: getTemplateIdentifiers(util.getBoundTemplate('
{{foo}}
')) + .identifiers, usedComponents: new Set(), isInline: false, file: new ParseSourceFile('
{{foo}}
', decl.getSourceFile().fileName), @@ -64,16 +69,22 @@ runInEachFileSystem(() => { const decl = util.getComponentDeclaration('class C {}', 'C'); const template = '
{{foo}}
'; populateContext( - context, decl, 'c-selector', '
{{foo}}
', util.getBoundTemplate(template), - /* inline template */ true); + context, + decl, + 'c-selector', + '
{{foo}}
', + util.getBoundTemplate(template), + /* inline template */ true, + ); const analysis = generateAnalysis(context); expect(analysis.size).toBe(1); const info = analysis.get(decl); expect(info).toBeDefined(); - expect(info!.template.file) - .toEqual(new ParseSourceFile('class C {}', decl.getSourceFile().fileName)); + expect(info!.template.file).toEqual( + new ParseSourceFile('class C {}', decl.getSourceFile().fileName), + ); }); it('should give external templates their own source file', () => { @@ -87,8 +98,9 @@ runInEachFileSystem(() => { const info = analysis.get(decl); expect(info).toBeDefined(); - expect(info!.template.file) - .toEqual(new ParseSourceFile('
{{foo}}
', decl.getSourceFile().fileName)); + expect(info!.template.file).toEqual( + new ParseSourceFile('
{{foo}}
', decl.getSourceFile().fileName), + ); }); it('should emit used components', () => { @@ -100,10 +112,12 @@ runInEachFileSystem(() => { const templateB = ''; const declB = util.getComponentDeclaration('class B {}', 'B'); - const boundA = - util.getBoundTemplate(templateA, {}, [{selector: 'b-selector', declaration: declB}]); - const boundB = - util.getBoundTemplate(templateB, {}, [{selector: 'a-selector', declaration: declA}]); + const boundA = util.getBoundTemplate(templateA, {}, [ + {selector: 'b-selector', declaration: declB}, + ]); + const boundB = util.getBoundTemplate(templateB, {}, [ + {selector: 'a-selector', declaration: declA}, + ]); populateContext(context, declA, 'a-selector', templateA, boundA); populateContext(context, declB, 'b-selector', templateB, boundB); diff --git a/packages/compiler-cli/src/ngtsc/indexer/test/util.ts b/packages/compiler-cli/src/ngtsc/indexer/test/util.ts index 2907c6291e711..bb5fce0a5f00f 100644 --- a/packages/compiler-cli/src/ngtsc/indexer/test/util.ts +++ b/packages/compiler-cli/src/ngtsc/indexer/test/util.ts @@ -6,7 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import {BoundTarget, CssSelector, parseTemplate, ParseTemplateOptions, R3TargetBinder, SelectorMatcher} from '@angular/compiler'; +import { + BoundTarget, + CssSelector, + parseTemplate, + ParseTemplateOptions, + R3TargetBinder, + SelectorMatcher, +} from '@angular/compiler'; import ts from 'typescript'; import {absoluteFrom, AbsoluteFsPath} from '../../file_system'; @@ -28,8 +35,11 @@ export function getComponentDeclaration(componentStr: string, className: string) const program = makeProgram([{name: getTestFilePath(), contents: componentStr}]); return getDeclaration( - program.program, getTestFilePath(), className, - (value: ts.Node): value is ClassDeclaration => ts.isClassDeclaration(value)); + program.program, + getTestFilePath(), + className, + (value: ts.Node): value is ClassDeclaration => ts.isClassDeclaration(value), + ); } /** @@ -41,24 +51,27 @@ export function getComponentDeclaration(componentStr: string, className: string) * @param components components to bind to the template target */ export function getBoundTemplate( - template: string, options: ParseTemplateOptions = {}, - components: Array<{selector: string, declaration: ClassDeclaration}> = - []): BoundTarget { + template: string, + options: ParseTemplateOptions = {}, + components: Array<{selector: string; declaration: ClassDeclaration}> = [], +): BoundTarget { const matcher = new SelectorMatcher(); components.forEach(({selector, declaration}) => { - matcher.addSelectables(CssSelector.parse(selector), [{ - ref: new Reference(declaration), - selector, - name: declaration.name.getText(), - isComponent: true, - inputs: ClassPropertyMapping.fromMappedObject({}), - outputs: ClassPropertyMapping.fromMappedObject({}), - exportAs: null, - isStructural: false, - animationTriggerNames: null, - ngContentSelectors: null, - preserveWhitespaces: false, - }]); + matcher.addSelectables(CssSelector.parse(selector), [ + { + ref: new Reference(declaration), + selector, + name: declaration.name.getText(), + isComponent: true, + inputs: ClassPropertyMapping.fromMappedObject({}), + outputs: ClassPropertyMapping.fromMappedObject({}), + exportAs: null, + isStructural: false, + animationTriggerNames: null, + ngContentSelectors: null, + preserveWhitespaces: false, + }, + ]); }); const binder = new R3TargetBinder(matcher); diff --git a/packages/compiler-cli/src/ngtsc/metadata/index.ts b/packages/compiler-cli/src/ngtsc/metadata/index.ts index efbf40fcf203c..c47f4dba243f1 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/index.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/index.ts @@ -10,8 +10,24 @@ export * from './src/api'; export {DtsMetadataReader} from './src/dts'; export {flattenInheritedDirectiveMetadata} from './src/inheritance'; export {CompoundMetadataRegistry, LocalMetadataRegistry} from './src/registry'; -export {ResourceRegistry, Resource, ComponentResources, isExternalResource, ExternalResource} from './src/resource_registry'; -export {extractDirectiveTypeCheckMeta, hasInjectableFields, CompoundMetadataReader, isHostDirectiveMetaForGlobalMode} from './src/util'; -export {BindingPropertyName, ClassPropertyMapping, ClassPropertyName, InputOrOutput} from './src/property_mapping'; +export { + ResourceRegistry, + Resource, + ComponentResources, + isExternalResource, + ExternalResource, +} from './src/resource_registry'; +export { + extractDirectiveTypeCheckMeta, + hasInjectableFields, + CompoundMetadataReader, + isHostDirectiveMetaForGlobalMode, +} from './src/util'; +export { + BindingPropertyName, + ClassPropertyMapping, + ClassPropertyName, + InputOrOutput, +} from './src/property_mapping'; export {ExportedProviderStatusResolver} from './src/providers'; export {HostDirectivesResolver} from './src/host_directives_resolver'; diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/api.ts b/packages/compiler-cli/src/ngtsc/metadata/src/api.ts index de29832f2efc5..aa6cdb32b1cac 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/api.ts @@ -31,7 +31,7 @@ export interface NgModuleMeta { * If this is `null`, then either no declarations exist, or no expression was available (likely * because the module came from a .d.ts file). */ - rawDeclarations: ts.Expression|null; + rawDeclarations: ts.Expression | null; /** * The raw `ts.Expression` which gave rise to `imports`, if one exists. @@ -39,7 +39,7 @@ export interface NgModuleMeta { * If this is `null`, then either no imports exist, or no expression was available (likely * because the module came from a .d.ts file). */ - rawImports: ts.Expression|null; + rawImports: ts.Expression | null; /** * The raw `ts.Expression` which gave rise to `exports`, if one exists. @@ -47,14 +47,14 @@ export interface NgModuleMeta { * If this is `null`, then either no exports exist, or no expression was available (likely * because the module came from a .d.ts file). */ - rawExports: ts.Expression|null; + rawExports: ts.Expression | null; /** * The primary decorator associated with this `ngModule`. * * If this is `null`, no decorator exists, meaning it's probably from a .d.ts file. */ - decorator: ts.Decorator|null; + decorator: ts.Decorator | null; /** * Whether this NgModule may declare providers. @@ -132,7 +132,7 @@ export enum MatchSource { } /** Metadata for a single input mapping. */ -export type InputMapping = InputOrOutput&{ +export type InputMapping = InputOrOutput & { required: boolean; /** @@ -147,7 +147,7 @@ export type InputMapping = InputOrOutput&{ * write type needs to be captured in a coercion member as the decorator information * is lost in the `.d.ts` for type-checking. */ - transform: DecoratorInputTransform|null + transform: DecoratorInputTransform | null; }; /** Metadata for a model mapping. */ @@ -191,7 +191,7 @@ export interface DirectiveMeta extends T2DirectiveMeta, DirectiveTypeCheckMeta { /** * Unparsed selector of the directive, or null if the directive does not have a selector. */ - selector: string|null; + selector: string | null; queries: string[]; /** @@ -210,7 +210,7 @@ export interface DirectiveMeta extends T2DirectiveMeta, DirectiveTypeCheckMeta { * A value of `'dynamic'` indicates that while the analyzer detected that this directive extends * another type, it could not statically determine the base class. */ - baseClass: Reference|'dynamic'|null; + baseClass: Reference | 'dynamic' | null; /** * Whether the directive had some issue with its declaration that means it might not have complete @@ -236,28 +236,28 @@ export interface DirectiveMeta extends T2DirectiveMeta, DirectiveTypeCheckMeta { /** * For standalone components, the list of imported types. */ - imports: Reference[]|null; + imports: Reference[] | null; /** * For standalone components, the list of imported types that can be used * in `@defer` blocks (when only explicit dependencies are allowed). */ - deferredImports: Reference[]|null; + deferredImports: Reference[] | null; /** * For standalone components, the list of schemas declared. */ - schemas: SchemaMetadata[]|null; + schemas: SchemaMetadata[] | null; /** * The primary decorator associated with this directive. * * If this is `null`, no decorator exists, meaning it's probably from a .d.ts file. */ - decorator: ts.Decorator|null; + decorator: ts.Decorator | null; /** Additional directives applied to the directive host. */ - hostDirectives: HostDirectiveMeta[]|null; + hostDirectives: HostDirectiveMeta[] | null; /** * Whether the directive should be assumed to export providers if imported as a standalone type. @@ -280,16 +280,16 @@ export interface HostDirectiveMeta { * which indicates the expression could not be resolved due to being imported from some external * file. In this case, the expression is the raw expression as appears in the decorator. */ - directive: Reference|Expression; + directive: Reference | Expression; /** Whether the reference to the host directive is a forward reference. */ isForwardReference: boolean; /** Inputs from the host directive that have been exposed. */ - inputs: {[publicName: string]: string}|null; + inputs: {[publicName: string]: string} | null; /** Outputs from the host directive that have been exposed. */ - outputs: {[publicName: string]: string}|null; + outputs: {[publicName: string]: string} | null; } /** @@ -324,7 +324,7 @@ export interface TemplateGuardMeta { * type can result in narrowing of the input type. * - 'binding' means that the input binding expression itself is used as template guard. */ - type: 'invocation'|'binding'; + type: 'invocation' | 'binding'; } /** @@ -334,9 +334,9 @@ export interface PipeMeta { kind: MetaKind.Pipe; ref: Reference; name: string; - nameExpr: ts.Expression|null; + nameExpr: ts.Expression | null; isStandalone: boolean; - decorator: ts.Decorator|null; + decorator: ts.Decorator | null; isExplicitlyDeferred: boolean; } @@ -345,9 +345,9 @@ export interface PipeMeta { * or a registry. */ export interface MetadataReader { - getDirectiveMetadata(node: Reference): DirectiveMeta|null; - getNgModuleMetadata(node: Reference): NgModuleMeta|null; - getPipeMetadata(node: Reference): PipeMeta|null; + getDirectiveMetadata(node: Reference): DirectiveMeta | null; + getNgModuleMetadata(node: Reference): NgModuleMeta | null; + getPipeMetadata(node: Reference): PipeMeta | null; } /** diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts b/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts index d468ebf8c77aa..ac1ba16d91f62 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts @@ -9,19 +9,44 @@ import ts from 'typescript'; import {OwningModule, Reference} from '../../imports'; -import {ClassDeclaration, isNamedClassDeclaration, ReflectionHost, TypeValueReferenceKind} from '../../reflection'; +import { + ClassDeclaration, + isNamedClassDeclaration, + ReflectionHost, + TypeValueReferenceKind, +} from '../../reflection'; import {nodeDebugInfo} from '../../util/src/typescript'; -import {DirectiveMeta, HostDirectiveMeta, InputMapping, MatchSource, MetadataReader, MetaKind, NgModuleMeta, PipeMeta} from './api'; +import { + DirectiveMeta, + HostDirectiveMeta, + InputMapping, + MatchSource, + MetadataReader, + MetaKind, + NgModuleMeta, + PipeMeta, +} from './api'; import {ClassPropertyMapping} from './property_mapping'; -import {extractDirectiveTypeCheckMeta, extractReferencesFromType, extraReferenceFromTypeQuery, readBooleanType, readMapType, readStringArrayType, readStringType} from './util'; +import { + extractDirectiveTypeCheckMeta, + extractReferencesFromType, + extraReferenceFromTypeQuery, + readBooleanType, + readMapType, + readStringArrayType, + readStringType, +} from './util'; /** * A `MetadataReader` that can read metadata from `.d.ts` files, which have static Ivy properties * from an upstream compilation already. */ export class DtsMetadataReader implements MetadataReader { - constructor(private checker: ts.TypeChecker, private reflector: ReflectionHost) {} + constructor( + private checker: ts.TypeChecker, + private reflector: ReflectionHost, + ) {} /** * Read the metadata from a class that has already been compiled somehow (either it's in a .d.ts @@ -29,20 +54,23 @@ export class DtsMetadataReader implements MetadataReader { * * @param ref `Reference` to the class of interest, with the context of how it was obtained. */ - getNgModuleMetadata(ref: Reference): NgModuleMeta|null { + getNgModuleMetadata(ref: Reference): NgModuleMeta | null { const clazz = ref.node; // This operation is explicitly not memoized, as it depends on `ref.ownedByModuleGuess`. // TODO(alxhub): investigate caching of .d.ts module metadata. - const ngModuleDef = this.reflector.getMembersOfClass(clazz).find( - member => member.name === 'ɵmod' && member.isStatic); + const ngModuleDef = this.reflector + .getMembersOfClass(clazz) + .find((member) => member.name === 'ɵmod' && member.isStatic); if (ngModuleDef === undefined) { return null; } else if ( - // Validate that the shape of the ngModuleDef type is correct. - ngModuleDef.type === null || !ts.isTypeReferenceNode(ngModuleDef.type) || - ngModuleDef.type.typeArguments === undefined || - ngModuleDef.type.typeArguments.length !== 4) { + // Validate that the shape of the ngModuleDef type is correct. + ngModuleDef.type === null || + !ts.isTypeReferenceNode(ngModuleDef.type) || + ngModuleDef.type.typeArguments === undefined || + ngModuleDef.type.typeArguments.length !== 4 + ) { return null; } @@ -51,8 +79,11 @@ export class DtsMetadataReader implements MetadataReader { return { kind: MetaKind.NgModule, ref, - declarations: - extractReferencesFromType(this.checker, declarationMetadata, ref.bestGuessOwningModule), + declarations: extractReferencesFromType( + this.checker, + declarationMetadata, + ref.bestGuessOwningModule, + ), exports: extractReferencesFromType(this.checker, exportMetadata, ref.bestGuessOwningModule), imports: extractReferencesFromType(this.checker, importMetadata, ref.bestGuessOwningModule), schemas: [], @@ -69,16 +100,20 @@ export class DtsMetadataReader implements MetadataReader { /** * Read directive (or component) metadata from a referenced class in a .d.ts file. */ - getDirectiveMetadata(ref: Reference): DirectiveMeta|null { + getDirectiveMetadata(ref: Reference): DirectiveMeta | null { const clazz = ref.node; - const def = this.reflector.getMembersOfClass(clazz).find( - field => field.isStatic && (field.name === 'ɵcmp' || field.name === 'ɵdir')); + const def = this.reflector + .getMembersOfClass(clazz) + .find((field) => field.isStatic && (field.name === 'ɵcmp' || field.name === 'ɵdir')); if (def === undefined) { // No definition could be found. return null; } else if ( - def.type === null || !ts.isTypeReferenceNode(def.type) || - def.type.typeArguments === undefined || def.type.typeArguments.length < 2) { + def.type === null || + !ts.isTypeReferenceNode(def.type) || + def.type.typeArguments === undefined || + def.type.typeArguments.length < 2 + ) { // The type metadata was the wrong shape. return null; } @@ -90,27 +125,34 @@ export class DtsMetadataReader implements MetadataReader { // A directive is considered to be structural if: // 1) it's a directive, not a component, and // 2) it injects `TemplateRef` - const isStructural = !isComponent && ctorParams !== null && ctorParams.some(param => { - return param.typeValueReference.kind === TypeValueReferenceKind.IMPORTED && + const isStructural = + !isComponent && + ctorParams !== null && + ctorParams.some((param) => { + return ( + param.typeValueReference.kind === TypeValueReferenceKind.IMPORTED && param.typeValueReference.moduleName === '@angular/core' && - param.typeValueReference.importedName === 'TemplateRef'; - }); + param.typeValueReference.importedName === 'TemplateRef' + ); + }); const ngContentSelectors = - def.type.typeArguments.length > 6 ? readStringArrayType(def.type.typeArguments[6]) : null; + def.type.typeArguments.length > 6 ? readStringArrayType(def.type.typeArguments[6]) : null; const isStandalone = - def.type.typeArguments.length > 7 && (readBooleanType(def.type.typeArguments[7]) ?? false); + def.type.typeArguments.length > 7 && (readBooleanType(def.type.typeArguments[7]) ?? false); const inputs = ClassPropertyMapping.fromMappedObject(readInputsType(def.type.typeArguments[3])); const outputs = ClassPropertyMapping.fromMappedObject( - readMapType(def.type.typeArguments[4], readStringType)); + readMapType(def.type.typeArguments[4], readStringType), + ); - const hostDirectives = def.type.typeArguments.length > 8 ? - readHostDirectivesType(this.checker, def.type.typeArguments[8], ref.bestGuessOwningModule) : - null; + const hostDirectives = + def.type.typeArguments.length > 8 + ? readHostDirectivesType(this.checker, def.type.typeArguments[8], ref.bestGuessOwningModule) + : null; const isSignal = - def.type.typeArguments.length > 9 && (readBooleanType(def.type.typeArguments[9]) ?? false); + def.type.typeArguments.length > 9 && (readBooleanType(def.type.typeArguments[9]) ?? false); return { kind: MetaKind.Directive, @@ -151,15 +193,19 @@ export class DtsMetadataReader implements MetadataReader { /** * Read pipe metadata from a referenced class in a .d.ts file. */ - getPipeMetadata(ref: Reference): PipeMeta|null { - const def = this.reflector.getMembersOfClass(ref.node).find( - field => field.isStatic && field.name === 'ɵpipe'); + getPipeMetadata(ref: Reference): PipeMeta | null { + const def = this.reflector + .getMembersOfClass(ref.node) + .find((field) => field.isStatic && field.name === 'ɵpipe'); if (def === undefined) { // No definition could be found. return null; } else if ( - def.type === null || !ts.isTypeReferenceNode(def.type) || - def.type.typeArguments === undefined || def.type.typeArguments.length < 2) { + def.type === null || + !ts.isTypeReferenceNode(def.type) || + def.type.typeArguments === undefined || + def.type.typeArguments.length < 2 + ) { // The type metadata was the wrong shape. return null; } @@ -171,7 +217,7 @@ export class DtsMetadataReader implements MetadataReader { const name = type.literal.text; const isStandalone = - def.type.typeArguments.length > 2 && (readBooleanType(def.type.typeArguments[2]) ?? false); + def.type.typeArguments.length > 2 && (readBooleanType(def.type.typeArguments[2]) ?? false); return { kind: MetaKind.Pipe, @@ -190,9 +236,12 @@ function readInputsType(type: ts.TypeNode): Record { if (ts.isTypeLiteralNode(type)) { for (const member of type.members) { - if (!ts.isPropertySignature(member) || member.type === undefined || - member.name === undefined || - (!ts.isStringLiteral(member.name) && !ts.isIdentifier(member.name))) { + if ( + !ts.isPropertySignature(member) || + member.type === undefined || + member.name === undefined || + (!ts.isStringLiteral(member.name) && !ts.isIdentifier(member.name)) + ) { continue; } @@ -214,9 +263,9 @@ function readInputsType(type: ts.TypeNode): Record { transform: null, }; } else { - const config = readMapType(member.type, innerValue => { - return readStringType(innerValue) ?? readBooleanType(innerValue); - }) as {alias: string, required: boolean, isSignal?: boolean}; + const config = readMapType(member.type, (innerValue) => { + return readStringType(innerValue) ?? readBooleanType(innerValue); + }) as {alias: string; required: boolean; isSignal?: boolean}; inputsMap[classPropertyName] = { classPropertyName, @@ -235,8 +284,11 @@ function readInputsType(type: ts.TypeNode): Record { return inputsMap; } -function readBaseClass(clazz: ClassDeclaration, checker: ts.TypeChecker, reflector: ReflectionHost): - Reference|'dynamic'|null { +function readBaseClass( + clazz: ClassDeclaration, + checker: ts.TypeChecker, + reflector: ReflectionHost, +): Reference | 'dynamic' | null { if (!isNamedClassDeclaration(clazz)) { // Technically this is an error in a .d.ts file, but for the purposes of finding the base class // it's ignored. @@ -253,8 +305,10 @@ function readBaseClass(clazz: ClassDeclaration, checker: ts.TypeChecker, reflect } else if (symbol.flags & ts.SymbolFlags.Alias) { symbol = checker.getAliasedSymbol(symbol); } - if (symbol.valueDeclaration !== undefined && - isNamedClassDeclaration(symbol.valueDeclaration)) { + if ( + symbol.valueDeclaration !== undefined && + isNamedClassDeclaration(symbol.valueDeclaration) + ) { return new Reference(symbol.valueDeclaration); } else { return 'dynamic'; @@ -265,10 +319,11 @@ function readBaseClass(clazz: ClassDeclaration, checker: ts.TypeChecker, reflect return null; } - function readHostDirectivesType( - checker: ts.TypeChecker, type: ts.TypeNode, - bestGuessOwningModule: OwningModule|null): HostDirectiveMeta[]|null { + checker: ts.TypeChecker, + type: ts.TypeNode, + bestGuessOwningModule: OwningModule | null, +): HostDirectiveMeta[] | null { if (!ts.isTupleTypeNode(type) || type.elements.length === 0) { return null; } @@ -276,7 +331,7 @@ function readHostDirectivesType( const result: HostDirectiveMeta[] = []; for (const hostDirectiveType of type.elements) { - const {directive, inputs, outputs} = readMapType(hostDirectiveType, type => type); + const {directive, inputs, outputs} = readMapType(hostDirectiveType, (type) => type); if (directive) { if (!ts.isTypeQueryNode(directive)) { @@ -287,7 +342,7 @@ function readHostDirectivesType( directive: extraReferenceFromTypeQuery(checker, directive, type, bestGuessOwningModule), isForwardReference: false, inputs: readMapType(inputs, readStringType), - outputs: readMapType(outputs, readStringType) + outputs: readMapType(outputs, readStringType), }); } } diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/host_directives_resolver.ts b/packages/compiler-cli/src/ngtsc/metadata/src/host_directives_resolver.ts index 1820fa3cdf288..ab135ffa0d426 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/host_directives_resolver.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/host_directives_resolver.ts @@ -27,9 +27,10 @@ export class HostDirectivesResolver { return this.cache.get(metadata.ref.node)!; } - const results = metadata.hostDirectives && metadata.hostDirectives.length > 0 ? - this.walkHostDirectives(metadata.hostDirectives, []) : - EMPTY_ARRAY; + const results = + metadata.hostDirectives && metadata.hostDirectives.length > 0 + ? this.walkHostDirectives(metadata.hostDirectives, []) + : EMPTY_ARRAY; this.cache.set(metadata.ref.node, results); return results; } @@ -39,8 +40,9 @@ export class HostDirectivesResolver { * directive metadata representing the host directives that apply to the host. */ private walkHostDirectives( - directives: NonNullable, - results: DirectiveMeta[]): ReadonlyArray { + directives: NonNullable, + results: DirectiveMeta[], + ): ReadonlyArray { for (const current of directives) { if (!isHostDirectiveMetaForGlobalMode(current)) { throw new Error('Impossible state: resolving code path in local compilation mode'); @@ -61,9 +63,11 @@ export class HostDirectivesResolver { ...hostMeta, matchSource: MatchSource.HostDirective, inputs: ClassPropertyMapping.fromMappedObject( - this.filterMappings(hostMeta.inputs, current.inputs, resolveInput)), + this.filterMappings(hostMeta.inputs, current.inputs, resolveInput), + ), outputs: ClassPropertyMapping.fromMappedObject( - this.filterMappings(hostMeta.outputs, current.outputs, resolveOutput)), + this.filterMappings(hostMeta.outputs, current.outputs, resolveOutput), + ), }); } @@ -77,8 +81,10 @@ export class HostDirectivesResolver { * @param valueResolver Function used to resolve the value that is assigned to the final mapping. */ private filterMappings( - source: ClassPropertyMapping, allowedProperties: Record|null, - valueResolver: (bindingName: string, binding: M) => T): Record { + source: ClassPropertyMapping, + allowedProperties: Record | null, + valueResolver: (bindingName: string, binding: M) => T, + ): Record { const result: Record = {}; if (allowedProperties !== null) { @@ -88,8 +94,10 @@ export class HostDirectivesResolver { if (bindings !== null) { for (const binding of bindings) { - result[binding.classPropertyName] = - valueResolver(allowedProperties[publicName], binding); + result[binding.classPropertyName] = valueResolver( + allowedProperties[publicName], + binding, + ); } } } diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/inheritance.ts b/packages/compiler-cli/src/ngtsc/metadata/src/inheritance.ts index 6d3c845f71b19..100d732410081 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/inheritance.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/inheritance.ts @@ -21,7 +21,9 @@ import {ClassPropertyMapping, ClassPropertyName} from './property_mapping'; * followed. */ export function flattenInheritedDirectiveMetadata( - reader: MetadataReader, dir: Reference): DirectiveMeta|null { + reader: MetadataReader, + dir: Reference, +): DirectiveMeta | null { const topMeta = reader.getDirectiveMetadata(dir); if (topMeta === null) { return null; @@ -34,7 +36,7 @@ export function flattenInheritedDirectiveMetadata( const undeclaredInputFields = new Set(); const restrictedInputFields = new Set(); const stringLiteralInputFields = new Set(); - let hostDirectives: HostDirectiveMeta[]|null = null; + let hostDirectives: HostDirectiveMeta[] | null = null; let isDynamic = false; let inputs = ClassPropertyMapping.empty(); let outputs = ClassPropertyMapping.empty(); diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/ng_module_index.ts b/packages/compiler-cli/src/ngtsc/metadata/src/ng_module_index.ts index b85bdedb7e0c3..7a108f522d707 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/ng_module_index.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/ng_module_index.ts @@ -15,7 +15,10 @@ import {MetadataReader, MetadataReaderWithIndex, MetaKind, NgModuleIndex} from ' * An index of all NgModules that export or re-export a given trait. */ export class NgModuleIndexImpl implements NgModuleIndex { - constructor(private metaReader: MetadataReader, private localReader: MetadataReaderWithIndex) {} + constructor( + private metaReader: MetadataReader, + private localReader: MetadataReaderWithIndex, + ) {} // A map from an NgModule's Class Declaration to the "main" reference to that module, aka the one // present in the reader metadata object @@ -50,8 +53,9 @@ export class NgModuleIndexImpl implements NgModuleIndex { } private indexTrait( - ref: Reference, - seenTypesWithReexports: Map>): void { + ref: Reference, + seenTypesWithReexports: Map>, + ): void { if (seenTypesWithReexports.has(ref.node)) { // We've processed this type before. return; @@ -59,7 +63,7 @@ export class NgModuleIndexImpl implements NgModuleIndex { seenTypesWithReexports.set(ref.node, new Set()); const meta = - this.metaReader.getDirectiveMetadata(ref) ?? this.metaReader.getNgModuleMetadata(ref); + this.metaReader.getDirectiveMetadata(ref) ?? this.metaReader.getNgModuleMetadata(ref); if (meta === null) { return; } @@ -79,9 +83,10 @@ export class NgModuleIndexImpl implements NgModuleIndex { for (const childRef of meta.exports) { this.indexTrait(childRef, seenTypesWithReexports); - const childMeta = this.metaReader.getDirectiveMetadata(childRef) ?? - this.metaReader.getPipeMetadata(childRef) ?? - this.metaReader.getNgModuleMetadata(childRef); + const childMeta = + this.metaReader.getDirectiveMetadata(childRef) ?? + this.metaReader.getPipeMetadata(childRef) ?? + this.metaReader.getNgModuleMetadata(childRef); if (childMeta === null) { continue; } diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/property_mapping.ts b/packages/compiler-cli/src/ngtsc/metadata/src/property_mapping.ts index 2fe1c1c5fef5f..e25361ea559a1 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/property_mapping.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/property_mapping.ts @@ -53,8 +53,9 @@ export interface InputOrOutput { * Allows bidirectional querying of the mapping - looking up all inputs/outputs with a given * property name, or mapping from a specific class property to its binding property name. */ -export class ClassPropertyMapping implements - InputOutputPropertySet { +export class ClassPropertyMapping + implements InputOutputPropertySet +{ /** * Mapping from class property names to the single `InputOrOutput` for that class property. */ @@ -82,8 +83,9 @@ export class ClassPropertyMapping imple * to either binding property names or an array that contains both names, which is used in on-disk * metadata formats (e.g. in .d.ts files). */ - static fromMappedObject( - obj: {[classPropertyName: string]: BindingPropertyName|T}): ClassPropertyMapping { + static fromMappedObject(obj: { + [classPropertyName: string]: BindingPropertyName | T; + }): ClassPropertyMapping { const forwardMap = new Map(); for (const classPropertyName of Object.keys(obj)) { @@ -112,8 +114,10 @@ export class ClassPropertyMapping imple * Merge two mappings into one, with class properties from `b` taking precedence over class * properties from `a`. */ - static merge(a: ClassPropertyMapping, b: ClassPropertyMapping): - ClassPropertyMapping { + static merge( + a: ClassPropertyMapping, + b: ClassPropertyMapping, + ): ClassPropertyMapping { const forwardMap = new Map(a.forwardMap.entries()); for (const [classPropertyName, inputOrOutput] of b.forwardMap) { forwardMap.set(classPropertyName, inputOrOutput); @@ -146,14 +150,14 @@ export class ClassPropertyMapping imple /** * Lookup all `InputOrOutput`s that use this `propertyName`. */ - getByBindingPropertyName(propertyName: string): ReadonlyArray|null { + getByBindingPropertyName(propertyName: string): ReadonlyArray | null { return this.reverseMap.has(propertyName) ? this.reverseMap.get(propertyName)! : null; } /** * Lookup the `InputOrOutput` associated with a `classPropertyName`. */ - getByClassPropertyName(classPropertyName: string): T|null { + getByClassPropertyName(classPropertyName: string): T | null { return this.forwardMap.has(classPropertyName) ? this.forwardMap.get(classPropertyName)! : null; } @@ -189,15 +193,16 @@ export class ClassPropertyMapping imple * Implement the iterator protocol and return entry objects which contain the class and binding * property names (and are useful for destructuring). */ - * [Symbol.iterator](): IterableIterator { + *[Symbol.iterator](): IterableIterator { for (const inputOrOutput of this.forwardMap.values()) { yield inputOrOutput; } } } -function reverseMapFromForwardMap(forwardMap: Map): - Map { +function reverseMapFromForwardMap( + forwardMap: Map, +): Map { const reverseMap = new Map(); for (const [_, inputOrOutput] of forwardMap) { if (!reverseMap.has(inputOrOutput.bindingPropertyName)) { diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/providers.ts b/packages/compiler-cli/src/ngtsc/metadata/src/providers.ts index 236eac0bb1036..187980b0ab857 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/providers.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/providers.ts @@ -44,8 +44,9 @@ export class ExportedProviderStatusResolver { * prove that it does not */ mayExportProviders( - ref: Reference, - dependencyCallback?: (importRef: Reference) => void): boolean { + ref: Reference, + dependencyCallback?: (importRef: Reference) => void, + ): boolean { if (this.calculating.has(ref.node)) { // For cycles, we treat the cyclic edge as not having providers. return false; @@ -68,8 +69,9 @@ export class ExportedProviderStatusResolver { } // If one of the imports contains providers, then so does this component. - return (dirMeta.imports ?? []) - .some(importRef => this.mayExportProviders(importRef, dependencyCallback)); + return (dirMeta.imports ?? []).some((importRef) => + this.mayExportProviders(importRef, dependencyCallback), + ); } const pipeMeta = this.metaReader.getPipeMetadata(ref); @@ -84,8 +86,9 @@ export class ExportedProviderStatusResolver { } // If one of the NgModule's imports may contain providers, then so does this NgModule. - return ngModuleMeta.imports.some( - importRef => this.mayExportProviders(importRef, dependencyCallback)); + return ngModuleMeta.imports.some((importRef) => + this.mayExportProviders(importRef, dependencyCallback), + ); } return false; diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/registry.ts b/packages/compiler-cli/src/ngtsc/metadata/src/registry.ts index aa23f172e2afc..64188e6db1c0a 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/registry.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/registry.ts @@ -9,7 +9,14 @@ import {Reference} from '../../imports'; import {ClassDeclaration} from '../../reflection'; -import {DirectiveMeta, MetadataReaderWithIndex, MetadataRegistry, MetaKind, NgModuleMeta, PipeMeta} from './api'; +import { + DirectiveMeta, + MetadataReaderWithIndex, + MetadataRegistry, + MetaKind, + NgModuleMeta, + PipeMeta, +} from './api'; /** * A registry of directive, pipe, and module metadata for types defined in the current compilation @@ -20,13 +27,13 @@ export class LocalMetadataRegistry implements MetadataRegistry, MetadataReaderWi private ngModules = new Map(); private pipes = new Map(); - getDirectiveMetadata(ref: Reference): DirectiveMeta|null { + getDirectiveMetadata(ref: Reference): DirectiveMeta | null { return this.directives.has(ref.node) ? this.directives.get(ref.node)! : null; } - getNgModuleMetadata(ref: Reference): NgModuleMeta|null { + getNgModuleMetadata(ref: Reference): NgModuleMeta | null { return this.ngModules.has(ref.node) ? this.ngModules.get(ref.node)! : null; } - getPipeMetadata(ref: Reference): PipeMeta|null { + getPipeMetadata(ref: Reference): PipeMeta | null { return this.pipes.has(ref.node) ? this.pipes.get(ref.node)! : null; } @@ -43,11 +50,11 @@ export class LocalMetadataRegistry implements MetadataRegistry, MetadataReaderWi getKnown(kind: MetaKind): Array { switch (kind) { case MetaKind.Directive: - return Array.from(this.directives.values()).map(v => v.ref.node); + return Array.from(this.directives.values()).map((v) => v.ref.node); case MetaKind.Pipe: - return Array.from(this.pipes.values()).map(v => v.ref.node); + return Array.from(this.pipes.values()).map((v) => v.ref.node); case MetaKind.NgModule: - return Array.from(this.ngModules.values()).map(v => v.ref.node); + return Array.from(this.ngModules.values()).map((v) => v.ref.node); } } } diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/resource_registry.ts b/packages/compiler-cli/src/ngtsc/metadata/src/resource_registry.ts index 2a229a7ac972e..786edd81ccbfe 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/resource_registry.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/resource_registry.ts @@ -19,7 +19,7 @@ import {ClassDeclaration} from '../../reflection'; * If the resource is inline, the `path` will be `null`. */ export interface Resource { - path: AbsoluteFsPath|null; + path: AbsoluteFsPath | null; expression: ts.Expression; } @@ -82,7 +82,7 @@ export class ResourceRegistry { this.componentToTemplateMap.set(component, templateResource); } - getTemplate(component: ClassDeclaration): Resource|null { + getTemplate(component: ClassDeclaration): Resource | null { if (!this.componentToTemplateMap.has(component)) { return null; } diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/util.ts b/packages/compiler-cli/src/ngtsc/metadata/src/util.ts index 7f16331a18de8..1e0429fbbdfe9 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/util.ts @@ -9,20 +9,39 @@ import ts from 'typescript'; import {OwningModule, Reference} from '../../imports'; -import {ClassDeclaration, ClassMember, ClassMemberKind, isNamedClassDeclaration, ReflectionHost, reflectTypeEntityToDeclaration} from '../../reflection'; +import { + ClassDeclaration, + ClassMember, + ClassMemberKind, + isNamedClassDeclaration, + ReflectionHost, + reflectTypeEntityToDeclaration, +} from '../../reflection'; import {nodeDebugInfo} from '../../util/src/typescript'; -import {DirectiveMeta, DirectiveTypeCheckMeta, HostDirectiveMeta, HostDirectiveMetaForGlobalMode, InputMapping, MetadataReader, NgModuleMeta, PipeMeta, TemplateGuardMeta} from './api'; +import { + DirectiveMeta, + DirectiveTypeCheckMeta, + HostDirectiveMeta, + HostDirectiveMetaForGlobalMode, + InputMapping, + MetadataReader, + NgModuleMeta, + PipeMeta, + TemplateGuardMeta, +} from './api'; import {ClassPropertyMapping, ClassPropertyName} from './property_mapping'; export function extractReferencesFromType( - checker: ts.TypeChecker, def: ts.TypeNode, - bestGuessOwningModule: OwningModule|null): Reference[] { + checker: ts.TypeChecker, + def: ts.TypeNode, + bestGuessOwningModule: OwningModule | null, +): Reference[] { if (!ts.isTupleTypeNode(def)) { return []; } - return def.elements.map(element => { + return def.elements.map((element) => { if (!ts.isTypeQueryNode(element)) { throw new Error(`Expected TypeQueryNode: ${nodeDebugInfo(element)}`); } @@ -32,8 +51,11 @@ export function extractReferencesFromType( } export function extraReferenceFromTypeQuery( - checker: ts.TypeChecker, typeNode: ts.TypeQueryNode, origin: ts.TypeNode, - bestGuessOwningModule: OwningModule|null) { + checker: ts.TypeChecker, + typeNode: ts.TypeQueryNode, + origin: ts.TypeNode, + bestGuessOwningModule: OwningModule | null, +) { const type = typeNode.exprName; const {node, from} = reflectTypeEntityToDeclaration(type, checker); if (!isNamedClassDeclaration(node)) { @@ -42,15 +64,17 @@ export function extraReferenceFromTypeQuery( if (from !== null && !from.startsWith('.')) { // The symbol was imported using an absolute module specifier so return a reference that // uses that absolute module specifier as its best guess owning module. - return new Reference( - node, {specifier: from, resolutionContext: origin.getSourceFile().fileName}); + return new Reference(node, { + specifier: from, + resolutionContext: origin.getSourceFile().fileName, + }); } // For local symbols or symbols that were imported using a relative module import it is // assumed that the symbol is exported from the provided best guess owning module. return new Reference(node, bestGuessOwningModule); } -export function readBooleanType(type: ts.TypeNode): boolean|null { +export function readBooleanType(type: ts.TypeNode): boolean | null { if (!ts.isLiteralTypeNode(type)) { return null; } @@ -65,7 +89,7 @@ export function readBooleanType(type: ts.TypeNode): boolean|null { } } -export function readStringType(type: ts.TypeNode): string|null { +export function readStringType(type: ts.TypeNode): string | null { if (!ts.isLiteralTypeNode(type) || !ts.isStringLiteral(type.literal)) { return null; } @@ -73,14 +97,20 @@ export function readStringType(type: ts.TypeNode): string|null { } export function readMapType( - type: ts.TypeNode, valueTransform: (type: ts.TypeNode) => T | null): {[key: string]: T} { + type: ts.TypeNode, + valueTransform: (type: ts.TypeNode) => T | null, +): {[key: string]: T} { if (!ts.isTypeLiteralNode(type)) { return {}; } const obj: {[key: string]: T} = {}; - type.members.forEach(member => { - if (!ts.isPropertySignature(member) || member.type === undefined || member.name === undefined || - (!ts.isStringLiteral(member.name) && !ts.isIdentifier(member.name))) { + type.members.forEach((member) => { + if ( + !ts.isPropertySignature(member) || + member.type === undefined || + member.name === undefined || + (!ts.isStringLiteral(member.name) && !ts.isIdentifier(member.name)) + ) { return; } const value = valueTransform(member.type); @@ -96,7 +126,7 @@ export function readStringArrayType(type: ts.TypeNode): string[] { return []; } const res: string[] = []; - type.elements.forEach(el => { + type.elements.forEach((el) => { if (!ts.isLiteralTypeNode(el) || !ts.isStringLiteral(el.literal)) { return; } @@ -111,31 +141,36 @@ export function readStringArrayType(type: ts.TypeNode): string[] { * making this metadata invariant to changes of inherited classes. */ export function extractDirectiveTypeCheckMeta( - node: ClassDeclaration, inputs: ClassPropertyMapping, - reflector: ReflectionHost): DirectiveTypeCheckMeta { + node: ClassDeclaration, + inputs: ClassPropertyMapping, + reflector: ReflectionHost, +): DirectiveTypeCheckMeta { const members = reflector.getMembersOfClass(node); - const staticMembers = members.filter(member => member.isStatic); - const ngTemplateGuards = staticMembers.map(extractTemplateGuard) - .filter((guard): guard is TemplateGuardMeta => guard !== null); + const staticMembers = members.filter((member) => member.isStatic); + const ngTemplateGuards = staticMembers + .map(extractTemplateGuard) + .filter((guard): guard is TemplateGuardMeta => guard !== null); const hasNgTemplateContextGuard = staticMembers.some( - member => member.kind === ClassMemberKind.Method && member.name === 'ngTemplateContextGuard'); + (member) => member.kind === ClassMemberKind.Method && member.name === 'ngTemplateContextGuard', + ); const coercedInputFields = new Set( - staticMembers.map(extractCoercedInput).filter((inputName): inputName is ClassPropertyName => { - // If the input refers to a signal input, we will not respect coercion members. - // A transform function should be used instead. - if (inputName === null || inputs.getByClassPropertyName(inputName)?.isSignal) { - return false; - } - return true; - })); + staticMembers.map(extractCoercedInput).filter((inputName): inputName is ClassPropertyName => { + // If the input refers to a signal input, we will not respect coercion members. + // A transform function should be used instead. + if (inputName === null || inputs.getByClassPropertyName(inputName)?.isSignal) { + return false; + } + return true; + }), + ); const restrictedInputFields = new Set(); const stringLiteralInputFields = new Set(); const undeclaredInputFields = new Set(); for (const {classPropertyName, transform} of inputs) { - const field = members.find(member => member.name === classPropertyName); + const field = members.find((member) => member.name === classPropertyName); if (field === undefined || field.node === null) { undeclaredInputFields.add(classPropertyName); continue; @@ -167,21 +202,30 @@ export function extractDirectiveTypeCheckMeta( function isRestricted(node: ts.Node): boolean { const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined; - return modifiers !== undefined && modifiers.some(({kind}) => { - return kind === ts.SyntaxKind.PrivateKeyword || kind === ts.SyntaxKind.ProtectedKeyword || - kind === ts.SyntaxKind.ReadonlyKeyword; - }); + return ( + modifiers !== undefined && + modifiers.some(({kind}) => { + return ( + kind === ts.SyntaxKind.PrivateKeyword || + kind === ts.SyntaxKind.ProtectedKeyword || + kind === ts.SyntaxKind.ReadonlyKeyword + ); + }) + ); } -function extractTemplateGuard(member: ClassMember): TemplateGuardMeta|null { +function extractTemplateGuard(member: ClassMember): TemplateGuardMeta | null { if (!member.name.startsWith('ngTemplateGuard_')) { return null; } const inputName = afterUnderscore(member.name); if (member.kind === ClassMemberKind.Property) { - let type: string|null = null; - if (member.type !== null && ts.isLiteralTypeNode(member.type) && - ts.isStringLiteral(member.type.literal)) { + let type: string | null = null; + if ( + member.type !== null && + ts.isLiteralTypeNode(member.type) && + ts.isStringLiteral(member.type.literal) + ) { type = member.type.literal.text; } @@ -197,7 +241,7 @@ function extractTemplateGuard(member: ClassMember): TemplateGuardMeta|null { } } -function extractCoercedInput(member: ClassMember): string|null { +function extractCoercedInput(member: ClassMember): string | null { if (member.kind !== ClassMemberKind.Property || !member.name.startsWith('ngAcceptInputType_')) { return null!; } @@ -214,7 +258,7 @@ function extractCoercedInput(member: ClassMember): string|null { export class CompoundMetadataReader implements MetadataReader { constructor(private readers: MetadataReader[]) {} - getDirectiveMetadata(node: Reference>): DirectiveMeta|null { + getDirectiveMetadata(node: Reference>): DirectiveMeta | null { for (const reader of this.readers) { const meta = reader.getDirectiveMetadata(node); if (meta !== null) { @@ -224,7 +268,7 @@ export class CompoundMetadataReader implements MetadataReader { return null; } - getNgModuleMetadata(node: Reference>): NgModuleMeta|null { + getNgModuleMetadata(node: Reference>): NgModuleMeta | null { for (const reader of this.readers) { const meta = reader.getNgModuleMetadata(node); if (meta !== null) { @@ -233,7 +277,7 @@ export class CompoundMetadataReader implements MetadataReader { } return null; } - getPipeMetadata(node: Reference>): PipeMeta|null { + getPipeMetadata(node: Reference>): PipeMeta | null { for (const reader of this.readers) { const meta = reader.getPipeMetadata(node); if (meta !== null) { @@ -258,7 +302,8 @@ export function hasInjectableFields(clazz: ClassDeclaration, host: ReflectionHos return members.some(({isStatic, name}) => isStatic && (name === 'ɵprov' || name === 'ɵfac')); } -export function isHostDirectiveMetaForGlobalMode(hostDirectiveMeta: HostDirectiveMeta): - hostDirectiveMeta is HostDirectiveMetaForGlobalMode { +export function isHostDirectiveMetaForGlobalMode( + hostDirectiveMeta: HostDirectiveMeta, +): hostDirectiveMeta is HostDirectiveMetaForGlobalMode { return hostDirectiveMeta.directive instanceof Reference; } diff --git a/packages/compiler-cli/src/ngtsc/metadata/test/dts_spec.ts b/packages/compiler-cli/src/ngtsc/metadata/test/dts_spec.ts index 9958264e0bf0e..3c4d0b04a6e76 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/test/dts_spec.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/test/dts_spec.ts @@ -23,7 +23,8 @@ runInEachFileSystem(() => { it('should not assume directives are structural', () => { const mainPath = absoluteFrom('/main.d.ts'); const {program} = makeProgram( - [{ + [ + { name: mainPath, contents: ` import {ViewContainerRef} from '@angular/core'; @@ -33,12 +34,14 @@ runInEachFileSystem(() => { constructor(p0: ViewContainerRef); static ɵdir: i0.ɵɵDirectiveDeclaration } - ` - }], - { - skipLibCheck: true, - lib: ['es6', 'dom'], - }); + `, + }, + ], + { + skipLibCheck: true, + lib: ['es6', 'dom'], + }, + ); const sf = getSourceFileOrError(program, mainPath); const clazz = sf.statements[2]; @@ -47,8 +50,10 @@ runInEachFileSystem(() => { } const typeChecker = program.getTypeChecker(); - const dtsReader = - new DtsMetadataReader(typeChecker, new TypeScriptReflectionHost(typeChecker)); + const dtsReader = new DtsMetadataReader( + typeChecker, + new TypeScriptReflectionHost(typeChecker), + ); const meta = dtsReader.getDirectiveMetadata(new Reference(clazz))!; expect(meta.isStructural).toBeFalse(); @@ -57,7 +62,8 @@ runInEachFileSystem(() => { it('should identify a structural directive by its constructor', () => { const mainPath = absoluteFrom('/main.d.ts'); const {program} = makeProgram( - [{ + [ + { name: mainPath, contents: ` import {TemplateRef, ViewContainerRef} from '@angular/core'; @@ -67,12 +73,14 @@ runInEachFileSystem(() => { constructor(p0: ViewContainerRef, p1: TemplateRef); static ɵdir: i0.ɵɵDirectiveDeclaration } - ` - }], - { - skipLibCheck: true, - lib: ['es6', 'dom'], - }); + `, + }, + ], + { + skipLibCheck: true, + lib: ['es6', 'dom'], + }, + ); const sf = getSourceFileOrError(program, mainPath); const clazz = sf.statements[2]; @@ -81,8 +89,10 @@ runInEachFileSystem(() => { } const typeChecker = program.getTypeChecker(); - const dtsReader = - new DtsMetadataReader(typeChecker, new TypeScriptReflectionHost(typeChecker)); + const dtsReader = new DtsMetadataReader( + typeChecker, + new TypeScriptReflectionHost(typeChecker), + ); const meta = dtsReader.getDirectiveMetadata(new Reference(clazz))!; expect(meta.isStructural).toBeTrue(); @@ -91,10 +101,10 @@ runInEachFileSystem(() => { it('should retain an absolute owning module for relative imports', () => { const externalPath = absoluteFrom('/external.d.ts'); const {program} = makeProgram( - [ - { - name: externalPath, - contents: ` + [ + { + name: externalPath, + contents: ` import * as i0 from '@angular/core'; import * as i1 from 'absolute'; import * as i2 from './relative'; @@ -102,33 +112,34 @@ runInEachFileSystem(() => { export declare class ExternalModule { static ɵmod: i0.ɵɵNgModuleDeclaration; } - ` - }, - { - name: absoluteFrom('/relative.d.ts'), - contents: ` + `, + }, + { + name: absoluteFrom('/relative.d.ts'), + contents: ` import * as i0 from '@angular/core'; export declare class RelativeDir { static ɵdir: i0.ɵɵDirectiveDeclaration; } - ` - }, - { - name: absoluteFrom('/node_modules/absolute.d.ts'), - contents: ` + `, + }, + { + name: absoluteFrom('/node_modules/absolute.d.ts'), + contents: ` import * as i0 from '@angular/core'; export declare class AbsoluteDir { static ɵdir: i0.ɵɵDirectiveDeclaration; } - ` - } - ], - { - skipLibCheck: true, - lib: ['es6', 'dom'], - }); + `, + }, + ], + { + skipLibCheck: true, + lib: ['es6', 'dom'], + }, + ); const externalSf = getSourceFileOrError(program, externalPath); const clazz = externalSf.statements[3]; @@ -137,8 +148,10 @@ runInEachFileSystem(() => { } const typeChecker = program.getTypeChecker(); - const dtsReader = - new DtsMetadataReader(typeChecker, new TypeScriptReflectionHost(typeChecker)); + const dtsReader = new DtsMetadataReader( + typeChecker, + new TypeScriptReflectionHost(typeChecker), + ); const withoutOwningModule = dtsReader.getNgModuleMetadata(new Reference(clazz))!; expect(withoutOwningModule.exports.length).toBe(2); @@ -180,7 +193,8 @@ runInEachFileSystem(() => { it('should identify host directives', () => { const mainPath = absoluteFrom('/main.d.ts'); const {program} = makeProgram( - [{ + [ + { name: mainPath, contents: ` import * as i0 from '@angular/core'; @@ -199,12 +213,14 @@ runInEachFileSystem(() => { {directive: typeof AdvancedHostDir; inputs: { "inputAlias": "customInputAlias"; }; outputs: { "outputAlias": "customOutputAlias"; };} ]> } - ` - }], - { - skipLibCheck: true, - lib: ['es6', 'dom'], - }); + `, + }, + ], + { + skipLibCheck: true, + lib: ['es6', 'dom'], + }, + ); const sf = getSourceFileOrError(program, mainPath); const clazz = sf.statements[3]; @@ -217,14 +233,14 @@ runInEachFileSystem(() => { const dtsReader = new DtsMetadataReader(typeChecker, new TypeScriptReflectionHost(typeChecker)); const meta = dtsReader.getDirectiveMetadata(new Reference(clazz))!; - const hostDirectives = meta.hostDirectives?.map( - hostDir => ({ - name: isHostDirectiveMetaForGlobalMode(hostDir) ? hostDir.directive.debugName : - 'Unresolved host dir', - directive: hostDir.directive, - inputs: hostDir.inputs, - outputs: hostDir.outputs - })); + const hostDirectives = meta.hostDirectives?.map((hostDir) => ({ + name: isHostDirectiveMetaForGlobalMode(hostDir) + ? hostDir.directive.debugName + : 'Unresolved host dir', + directive: hostDir.directive, + inputs: hostDir.inputs, + outputs: hostDir.outputs, + })); expect(hostDirectives).toEqual([ { @@ -237,15 +253,16 @@ runInEachFileSystem(() => { name: 'AdvancedHostDir', directive: jasmine.any(Reference), inputs: {inputAlias: 'customInputAlias'}, - outputs: {outputAlias: 'customOutputAlias'} - } + outputs: {outputAlias: 'customOutputAlias'}, + }, ]); }); it('should read the post-v16 inputs map syntax', () => { const mainPath = absoluteFrom('/main.d.ts'); const {program} = makeProgram( - [{ + [ + { name: mainPath, contents: ` import * as i0 from '@angular/core'; @@ -256,12 +273,14 @@ runInEachFileSystem(() => { "otherInput": {"alias": "alias", "required": true} }, {}, never> } - ` - }], - { - skipLibCheck: true, - lib: ['es6', 'dom'], - }); + `, + }, + ], + { + skipLibCheck: true, + lib: ['es6', 'dom'], + }, + ); const sf = getSourceFileOrError(program, mainPath); const clazz = sf.statements[1]; @@ -274,15 +293,18 @@ runInEachFileSystem(() => { const meta = dtsReader.getDirectiveMetadata(new Reference(clazz))!; expect(meta.inputs.toDirectMappedObject()).toEqual({input: 'input', otherInput: 'alias'}); - expect(Array.from(meta.inputs).filter(i => i.required).map(i => i.classPropertyName)).toEqual([ - 'otherInput' - ]); + expect( + Array.from(meta.inputs) + .filter((i) => i.required) + .map((i) => i.classPropertyName), + ).toEqual(['otherInput']); }); it('should read the pre-v16 inputs map syntax', () => { const mainPath = absoluteFrom('/main.d.ts'); const {program} = makeProgram( - [{ + [ + { name: mainPath, contents: ` import * as i0 from '@angular/core'; @@ -293,12 +315,14 @@ runInEachFileSystem(() => { "otherInput": "alias" }, {}, never> } - ` - }], - { - skipLibCheck: true, - lib: ['es6', 'dom'], - }); + `, + }, + ], + { + skipLibCheck: true, + lib: ['es6', 'dom'], + }, + ); const sf = getSourceFileOrError(program, mainPath); const clazz = sf.statements[1]; @@ -311,6 +335,6 @@ runInEachFileSystem(() => { const meta = dtsReader.getDirectiveMetadata(new Reference(clazz))!; expect(meta.inputs.toDirectMappedObject()).toEqual({input: 'input', otherInput: 'alias'}); - expect(Array.from(meta.inputs).filter(i => i.required)).toEqual([]); + expect(Array.from(meta.inputs).filter((i) => i.required)).toEqual([]); }); }); diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/index.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/index.ts index 2a887d3fcaecf..0908d9c494896 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/index.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/index.ts @@ -10,5 +10,11 @@ export {describeResolvedType, traceDynamicValue} from './src/diagnostics'; export {DynamicValue} from './src/dynamic'; export {ForeignFunctionResolver, PartialEvaluator} from './src/interface'; export {StaticInterpreter} from './src/interpreter'; -export {EnumValue, KnownFn, ResolvedValue, ResolvedValueArray, ResolvedValueMap} from './src/result'; +export { + EnumValue, + KnownFn, + ResolvedValue, + ResolvedValueArray, + ResolvedValueMap, +} from './src/result'; export {SyntheticValue} from './src/synthetic'; diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/builtin.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/builtin.ts index bafd4b360653d..7e9a3d4fce508 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/builtin.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/builtin.ts @@ -55,8 +55,12 @@ export class StringConcatBuiltinFn extends KnownFn { for (const arg of args) { const resolved = arg instanceof EnumValue ? arg.resolved : arg; - if (typeof resolved === 'string' || typeof resolved === 'number' || - typeof resolved === 'boolean' || resolved == null) { + if ( + typeof resolved === 'string' || + typeof resolved === 'number' || + typeof resolved === 'boolean' || + resolved == null + ) { // Cast to `any`, because `concat` will convert // anything to a string, but TS only allows strings. result = result.concat(resolved as any); diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/diagnostics.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/diagnostics.ts index 556bd7c410515..ccf38f9c2727a 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/diagnostics.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/diagnostics.ts @@ -47,7 +47,7 @@ export function describeResolvedType(value: ResolvedValue, maxDepth: number = 1) if (maxDepth === 0) { return 'Array'; } - return `[${value.map(v => describeResolvedType(v, maxDepth - 1)).join(', ')}]`; + return `[${value.map((v) => describeResolvedType(v, maxDepth - 1)).join(', ')}]`; } else if (value instanceof DynamicValue) { return '(not statically analyzable)'; } else if (value instanceof KnownFn) { @@ -61,7 +61,7 @@ function quoteKey(key: string): string { if (/^[a-z0-9_]+$/i.test(key)) { return key; } else { - return `'${key.replace(/'/g, '\\\'')}'`; + return `'${key.replace(/'/g, "\\'")}'`; } } @@ -73,52 +73,63 @@ function quoteKey(key: string): string { * @param value The dynamic value for which a trace should be created. */ export function traceDynamicValue( - node: ts.Node, value: DynamicValue): ts.DiagnosticRelatedInformation[] { + node: ts.Node, + value: DynamicValue, +): ts.DiagnosticRelatedInformation[] { return value.accept(new TraceDynamicValueVisitor(node)); } class TraceDynamicValueVisitor implements DynamicValueVisitor { - private currentContainerNode: ts.Node|null = null; + private currentContainerNode: ts.Node | null = null; constructor(private node: ts.Node) {} visitDynamicInput(value: DynamicValue): ts.DiagnosticRelatedInformation[] { const trace = value.reason.accept(this); if (this.shouldTrace(value.node)) { - const info = - makeRelatedInformation(value.node, 'Unable to evaluate this expression statically.'); + const info = makeRelatedInformation( + value.node, + 'Unable to evaluate this expression statically.', + ); trace.unshift(info); } return trace; } - visitSyntheticInput(value: DynamicValue>): - ts.DiagnosticRelatedInformation[] { + visitSyntheticInput( + value: DynamicValue>, + ): ts.DiagnosticRelatedInformation[] { return [makeRelatedInformation(value.node, 'Unable to evaluate this expression further.')]; } visitDynamicString(value: DynamicValue): ts.DiagnosticRelatedInformation[] { - return [makeRelatedInformation( - value.node, 'A string value could not be determined statically.')]; + return [ + makeRelatedInformation(value.node, 'A string value could not be determined statically.'), + ]; } - visitExternalReference(value: DynamicValue>): - ts.DiagnosticRelatedInformation[] { + visitExternalReference( + value: DynamicValue>, + ): ts.DiagnosticRelatedInformation[] { const name = value.reason.debugName; const description = name !== null ? `'${name}'` : 'an anonymous declaration'; - return [makeRelatedInformation( + return [ + makeRelatedInformation( value.node, - `A value for ${ - description} cannot be determined statically, as it is an external declaration.`)]; + `A value for ${description} cannot be determined statically, as it is an external declaration.`, + ), + ]; } - visitComplexFunctionCall(value: DynamicValue): - ts.DiagnosticRelatedInformation[] { + visitComplexFunctionCall( + value: DynamicValue, + ): ts.DiagnosticRelatedInformation[] { return [ makeRelatedInformation( - value.node, - 'Unable to evaluate function call of complex function. A function must have exactly one return statement.'), - makeRelatedInformation(value.reason.node, 'Function is declared here.') + value.node, + 'Unable to evaluate function call of complex function. A function must have exactly one return statement.', + ), + makeRelatedInformation(value.reason.node, 'Function is declared here.'), ]; } @@ -171,7 +182,7 @@ class TraceDynamicValueVisitor implements DynamicValueVisitor { private constructor( - readonly node: ts.Node, readonly reason: R, private code: DynamicValueReason) {} + readonly node: ts.Node, + readonly reason: R, + private code: DynamicValueReason, + ) {} static fromDynamicInput(node: ts.Node, input: DynamicValue): DynamicValue { return new DynamicValue(node, input, DynamicValueReason.DYNAMIC_INPUT); @@ -106,8 +109,10 @@ export class DynamicValue { return new DynamicValue(node, undefined, DynamicValueReason.DYNAMIC_STRING); } - static fromExternalReference(node: ts.Node, ref: Reference): - DynamicValue> { + static fromExternalReference( + node: ts.Node, + ref: Reference, + ): DynamicValue> { return new DynamicValue(node, ref, DynamicValueReason.EXTERNAL_REFERENCE); } @@ -123,8 +128,10 @@ export class DynamicValue { return new DynamicValue(node, value, DynamicValueReason.INVALID_EXPRESSION_TYPE); } - static fromComplexFunctionCall(node: ts.Node, fn: FunctionDefinition): - DynamicValue { + static fromComplexFunctionCall( + node: ts.Node, + fn: FunctionDefinition, + ): DynamicValue { return new DynamicValue(node, fn, DynamicValueReason.COMPLEX_FUNCTION_CALL); } @@ -132,8 +139,10 @@ export class DynamicValue { return new DynamicValue(node, undefined, DynamicValueReason.DYNAMIC_TYPE); } - static fromSyntheticInput(node: ts.Node, value: SyntheticValue): - DynamicValue> { + static fromSyntheticInput( + node: ts.Node, + value: SyntheticValue, + ): DynamicValue> { return new DynamicValue(node, value, DynamicValueReason.SYNTHETIC_INPUT); } @@ -185,7 +194,8 @@ export class DynamicValue { return visitor.visitDynamicString(this); case DynamicValueReason.EXTERNAL_REFERENCE: return visitor.visitExternalReference( - this as unknown as DynamicValue>); + this as unknown as DynamicValue>, + ); case DynamicValueReason.UNSUPPORTED_SYNTAX: return visitor.visitUnsupportedSyntax(this); case DynamicValueReason.UNKNOWN_IDENTIFIER: @@ -194,12 +204,14 @@ export class DynamicValue { return visitor.visitInvalidExpressionType(this); case DynamicValueReason.COMPLEX_FUNCTION_CALL: return visitor.visitComplexFunctionCall( - this as unknown as DynamicValue); + this as unknown as DynamicValue, + ); case DynamicValueReason.DYNAMIC_TYPE: return visitor.visitDynamicType(this); case DynamicValueReason.SYNTHETIC_INPUT: return visitor.visitSyntheticInput( - this as unknown as DynamicValue>); + this as unknown as DynamicValue>, + ); case DynamicValueReason.UNKNOWN: return visitor.visitUnknown(this); } diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interface.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interface.ts index 8fd46307d5b1d..90f8816b458bc 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interface.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interface.ts @@ -16,15 +16,19 @@ import {DynamicValue} from './dynamic'; import {StaticInterpreter} from './interpreter'; import {ResolvedValue} from './result'; -export type ForeignFunctionResolver = - (fn: Reference, - callExpr: ts.CallExpression, resolve: (expr: ts.Expression) => ResolvedValue, - unresolvable: DynamicValue) => ResolvedValue; +export type ForeignFunctionResolver = ( + fn: Reference, + callExpr: ts.CallExpression, + resolve: (expr: ts.Expression) => ResolvedValue, + unresolvable: DynamicValue, +) => ResolvedValue; export class PartialEvaluator { constructor( - private host: ReflectionHost, private checker: ts.TypeChecker, - private dependencyTracker: DependencyTracker|null) {} + private host: ReflectionHost, + private checker: ts.TypeChecker, + private dependencyTracker: DependencyTracker | null, + ) {} evaluate(expr: ts.Expression, foreignFunctionResolver?: ForeignFunctionResolver): ResolvedValue { const interpreter = new StaticInterpreter(this.host, this.checker, this.dependencyTracker); diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts index 3c3e1bccaf5c7..077f96791b011 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/interpreter.ts @@ -17,11 +17,16 @@ import {isDeclaration} from '../../util/src/typescript'; import {ArrayConcatBuiltinFn, ArraySliceBuiltinFn, StringConcatBuiltinFn} from './builtin'; import {DynamicValue} from './dynamic'; import {ForeignFunctionResolver} from './interface'; -import {EnumValue, KnownFn, ResolvedModule, ResolvedValue, ResolvedValueArray, ResolvedValueMap} from './result'; +import { + EnumValue, + KnownFn, + ResolvedModule, + ResolvedValue, + ResolvedValueArray, + ResolvedValueMap, +} from './result'; import {SyntheticValue} from './synthetic'; - - /** * Tracks the scope of a function body, which includes `ResolvedValue`s for the parameters of that * body. @@ -63,12 +68,14 @@ const BINARY_OPERATORS = new Map([ [ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken, literalBinaryOp((a, b) => a >>> b)], [ts.SyntaxKind.AsteriskAsteriskToken, literalBinaryOp((a, b) => Math.pow(a, b))], [ts.SyntaxKind.AmpersandAmpersandToken, referenceBinaryOp((a, b) => a && b)], - [ts.SyntaxKind.BarBarToken, referenceBinaryOp((a, b) => a || b)] + [ts.SyntaxKind.BarBarToken, referenceBinaryOp((a, b) => a || b)], ]); const UNARY_OPERATORS = new Map any>([ - [ts.SyntaxKind.TildeToken, a => ~a], [ts.SyntaxKind.MinusToken, a => -a], - [ts.SyntaxKind.PlusToken, a => +a], [ts.SyntaxKind.ExclamationToken, a => !a] + [ts.SyntaxKind.TildeToken, (a) => ~a], + [ts.SyntaxKind.MinusToken, (a) => -a], + [ts.SyntaxKind.PlusToken, (a) => +a], + [ts.SyntaxKind.ExclamationToken, (a) => !a], ]); interface Context { @@ -76,7 +83,7 @@ interface Context { /** * The module name (if any) which was used to reach the currently resolving symbols. */ - absoluteModuleName: string|null; + absoluteModuleName: string | null; /** * A file name representing the context in which the current `absoluteModuleName`, if any, was @@ -88,8 +95,10 @@ interface Context { } export class StaticInterpreter { constructor( - private host: ReflectionHost, private checker: ts.TypeChecker, - private dependencyTracker: DependencyTracker|null) {} + private host: ReflectionHost, + private checker: ts.TypeChecker, + private dependencyTracker: DependencyTracker | null, + ) {} visit(node: ts.Expression, context: Context): ResolvedValue { return this.visitExpression(node, context); @@ -146,8 +155,10 @@ export class StaticInterpreter { return result; } - private visitArrayLiteralExpression(node: ts.ArrayLiteralExpression, context: Context): - ResolvedValue { + private visitArrayLiteralExpression( + node: ts.ArrayLiteralExpression, + context: Context, + ): ResolvedValue { const array: ResolvedValueArray = []; for (let i = 0; i < node.elements.length; i++) { const element = node.elements[i]; @@ -160,8 +171,10 @@ export class StaticInterpreter { return array; } - protected visitObjectLiteralExpression(node: ts.ObjectLiteralExpression, context: Context): - ResolvedValue { + protected visitObjectLiteralExpression( + node: ts.ObjectLiteralExpression, + context: Context, + ): ResolvedValue { const map: ResolvedValueMap = new Map(); for (let i = 0; i < node.properties.length; i++) { const property = node.properties[i]; @@ -189,7 +202,9 @@ export class StaticInterpreter { spread.getExports().forEach((value, key) => map.set(key, value)); } else { return DynamicValue.fromDynamicInput( - node, DynamicValue.fromInvalidExpressionType(property, spread)); + node, + DynamicValue.fromInvalidExpressionType(property, spread), + ); } } else { return DynamicValue.fromUnknown(node); @@ -202,9 +217,9 @@ export class StaticInterpreter { const pieces: string[] = [node.head.text]; for (let i = 0; i < node.templateSpans.length; i++) { const span = node.templateSpans[i]; - const value = literal( - this.visit(span.expression, context), - () => DynamicValue.fromDynamicString(span.expression)); + const value = literal(this.visit(span.expression, context), () => + DynamicValue.fromDynamicString(span.expression), + ); if (value instanceof DynamicValue) { return DynamicValue.fromDynamicInput(node, value); } @@ -299,7 +314,7 @@ export class StaticInterpreter { private visitEnumDeclaration(node: ts.EnumDeclaration, context: Context): ResolvedValue { const enumRef = this.getReference(node, context); const map = new Map(); - node.members.forEach(member => { + node.members.forEach((member) => { const name = this.stringNameFromPropertyName(member.name, context); if (name !== undefined) { const resolved = member.initializer && this.visit(member.initializer, context); @@ -309,8 +324,10 @@ export class StaticInterpreter { return map; } - private visitElementAccessExpression(node: ts.ElementAccessExpression, context: Context): - ResolvedValue { + private visitElementAccessExpression( + node: ts.ElementAccessExpression, + context: Context, + ): ResolvedValue { const lhs = this.visitExpression(node.expression, context); if (lhs instanceof DynamicValue) { return DynamicValue.fromDynamicInput(node, lhs); @@ -326,8 +343,10 @@ export class StaticInterpreter { return this.accessHelper(node, lhs, rhs, context); } - private visitPropertyAccessExpression(node: ts.PropertyAccessExpression, context: Context): - ResolvedValue { + private visitPropertyAccessExpression( + node: ts.PropertyAccessExpression, + context: Context, + ): ResolvedValue { const lhs = this.visitExpression(node.expression, context); const rhs = node.name.text; // TODO: handle reference to class declaration. @@ -343,7 +362,7 @@ export class StaticInterpreter { return DynamicValue.fromUnknown(node); } - return new ResolvedModule(declarations, decl => { + return new ResolvedModule(declarations, (decl) => { const declContext = { ...context, ...joinModuleContext(context, node, decl), @@ -354,8 +373,12 @@ export class StaticInterpreter { }); } - private accessHelper(node: ts.Node, lhs: ResolvedValue, rhs: string|number, context: Context): - ResolvedValue { + private accessHelper( + node: ts.Node, + lhs: ResolvedValue, + rhs: string | number, + context: Context, + ): ResolvedValue { const strIndex = `${rhs}`; if (lhs instanceof Map) { if (lhs.has(strIndex)) { @@ -384,8 +407,9 @@ export class StaticInterpreter { if (this.host.isClass(ref)) { const module = owningModule(context, lhs.bestGuessOwningModule); let value: ResolvedValue = undefined; - const member = this.host.getMembersOfClass(ref).find( - member => member.isStatic && member.name === strIndex); + const member = this.host + .getMembersOfClass(ref) + .find((member) => member.isStatic && member.name === strIndex); if (member !== undefined) { if (member.value !== null) { value = this.visitExpression(member.value, context); @@ -398,7 +422,9 @@ export class StaticInterpreter { return value; } else if (isDeclaration(ref)) { return DynamicValue.fromDynamicInput( - node, DynamicValue.fromExternalReference(ref, lhs as Reference)); + node, + DynamicValue.fromExternalReference(ref, lhs as Reference), + ); } } else if (lhs instanceof DynamicValue) { return DynamicValue.fromDynamicInput(node, lhs); @@ -435,15 +461,18 @@ export class StaticInterpreter { const resolveFfrExpr = (expr: ts.Expression) => { let contextExtension: { - absoluteModuleName?: string|null, - resolutionContext?: string, + absoluteModuleName?: string | null; + resolutionContext?: string; } = {}; // TODO(alxhub): the condition `fn.body === null` here is vestigial - we probably _do_ want to // change the context like this even for non-null function bodies. But, this is being // redesigned as a refactoring with no behavior changes so that should be done as a follow-up. - if (fn.body === null && expr.getSourceFile() !== node.expression.getSourceFile() && - lhs.bestGuessOwningModule !== null) { + if ( + fn.body === null && + expr.getSourceFile() !== node.expression.getSourceFile() && + lhs.bestGuessOwningModule !== null + ) { contextExtension = { absoluteModuleName: lhs.bestGuessOwningModule.specifier, resolutionContext: lhs.bestGuessOwningModule.resolutionContext, @@ -457,7 +486,9 @@ export class StaticInterpreter { // foreignFunctionResolver, if one is specified. if (fn.body === null && context.foreignFunctionResolver !== undefined) { const unresolvable = DynamicValue.fromDynamicInput( - node, DynamicValue.fromExternalReference(node.expression, lhs)); + node, + DynamicValue.fromExternalReference(node.expression, lhs), + ); return context.foreignFunctionResolver(lhs, node, resolveFfrExpr, unresolvable); } @@ -491,8 +522,11 @@ export class StaticInterpreter { return res; } - private visitFunctionBody(node: ts.CallExpression, fn: FunctionDefinition, context: Context): - ResolvedValue { + private visitFunctionBody( + node: ts.CallExpression, + fn: FunctionDefinition, + context: Context, + ): ResolvedValue { if (fn.body === null) { return DynamicValue.fromUnknown(node); } else if (fn.body.length !== 1 || !ts.isReturnStatement(fn.body[0])) { @@ -514,12 +548,15 @@ export class StaticInterpreter { newScope.set(param.node, arg); }); - return ret.expression !== undefined ? this.visitExpression(ret.expression, calleeContext) : - undefined; + return ret.expression !== undefined + ? this.visitExpression(ret.expression, calleeContext) + : undefined; } - private visitConditionalExpression(node: ts.ConditionalExpression, context: Context): - ResolvedValue { + private visitConditionalExpression( + node: ts.ConditionalExpression, + context: Context, + ): ResolvedValue { const condition = this.visitExpression(node.condition, context); if (condition instanceof DynamicValue) { return DynamicValue.fromDynamicInput(node, condition); @@ -532,8 +569,10 @@ export class StaticInterpreter { } } - private visitPrefixUnaryExpression(node: ts.PrefixUnaryExpression, context: Context): - ResolvedValue { + private visitPrefixUnaryExpression( + node: ts.PrefixUnaryExpression, + context: Context, + ): ResolvedValue { const operatorKind = node.operator; if (!UNARY_OPERATORS.has(operatorKind)) { return DynamicValue.fromUnsupportedSyntax(node); @@ -557,12 +596,12 @@ export class StaticInterpreter { const opRecord = BINARY_OPERATORS.get(tokenKind)!; let lhs: ResolvedValue, rhs: ResolvedValue; if (opRecord.literal) { - lhs = literal( - this.visitExpression(node.left, context), - value => DynamicValue.fromInvalidExpressionType(node.left, value)); - rhs = literal( - this.visitExpression(node.right, context), - value => DynamicValue.fromInvalidExpressionType(node.right, value)); + lhs = literal(this.visitExpression(node.left, context), (value) => + DynamicValue.fromInvalidExpressionType(node.left, value), + ); + rhs = literal(this.visitExpression(node.right, context), (value) => + DynamicValue.fromInvalidExpressionType(node.right, value), + ); } else { lhs = this.visitExpression(node.left, context); rhs = this.visitExpression(node.right, context); @@ -576,8 +615,10 @@ export class StaticInterpreter { } } - private visitParenthesizedExpression(node: ts.ParenthesizedExpression, context: Context): - ResolvedValue { + private visitParenthesizedExpression( + node: ts.ParenthesizedExpression, + context: Context, + ): ResolvedValue { return this.visitExpression(node.expression, context); } @@ -608,9 +649,11 @@ export class StaticInterpreter { const path: ts.BindingElement[] = []; let closestDeclaration: ts.Node = node; - while (ts.isBindingElement(closestDeclaration) || - ts.isArrayBindingPattern(closestDeclaration) || - ts.isObjectBindingPattern(closestDeclaration)) { + while ( + ts.isBindingElement(closestDeclaration) || + ts.isArrayBindingPattern(closestDeclaration) || + ts.isObjectBindingPattern(closestDeclaration) + ) { if (ts.isBindingElement(closestDeclaration)) { path.unshift(closestDeclaration); } @@ -618,14 +661,16 @@ export class StaticInterpreter { closestDeclaration = closestDeclaration.parent; } - if (!ts.isVariableDeclaration(closestDeclaration) || - closestDeclaration.initializer === undefined) { + if ( + !ts.isVariableDeclaration(closestDeclaration) || + closestDeclaration.initializer === undefined + ) { return DynamicValue.fromUnknown(node); } let value = this.visit(closestDeclaration.initializer, context); for (const element of path) { - let key: number|string; + let key: number | string; if (ts.isArrayBindingPattern(element.parent)) { key = element.parent.elements.indexOf(element); } else { @@ -645,7 +690,7 @@ export class StaticInterpreter { return value; } - private stringNameFromPropertyName(node: ts.PropertyName, context: Context): string|undefined { + private stringNameFromPropertyName(node: ts.PropertyName, context: Context): string | undefined { if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) { return node.text; } else if (ts.isComputedPropertyName(node)) { @@ -701,19 +746,31 @@ export class StaticInterpreter { } } -function isFunctionOrMethodReference(ref: Reference): - ref is Reference { - return ts.isFunctionDeclaration(ref.node) || ts.isMethodDeclaration(ref.node) || - ts.isFunctionExpression(ref.node); +function isFunctionOrMethodReference( + ref: Reference, +): ref is Reference { + return ( + ts.isFunctionDeclaration(ref.node) || + ts.isMethodDeclaration(ref.node) || + ts.isFunctionExpression(ref.node) + ); } function literal( - value: ResolvedValue, reject: (value: ResolvedValue) => ResolvedValue): ResolvedValue { + value: ResolvedValue, + reject: (value: ResolvedValue) => ResolvedValue, +): ResolvedValue { if (value instanceof EnumValue) { value = value.resolved; } - if (value instanceof DynamicValue || value === null || value === undefined || - typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + if ( + value instanceof DynamicValue || + value === null || + value === undefined || + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { return value; } return reject(value); @@ -729,15 +786,20 @@ function isVariableDeclarationDeclared(node: ts.VariableDeclaration): boolean { } const varStmt = declList.parent; const modifiers = ts.getModifiers(varStmt); - return modifiers !== undefined && - modifiers.some(mod => mod.kind === ts.SyntaxKind.DeclareKeyword); + return ( + modifiers !== undefined && modifiers.some((mod) => mod.kind === ts.SyntaxKind.DeclareKeyword) + ); } const EMPTY = {}; -function joinModuleContext(existing: Context, node: ts.Node, decl: Declaration): { - absoluteModuleName?: string, - resolutionContext?: string, +function joinModuleContext( + existing: Context, + node: ts.Node, + decl: Declaration, +): { + absoluteModuleName?: string; + resolutionContext?: string; } { if (typeof decl.viaModule === 'string' && decl.viaModule !== existing.absoluteModuleName) { return { @@ -749,7 +811,7 @@ function joinModuleContext(existing: Context, node: ts.Node, decl: Declaration): } } -function owningModule(context: Context, override: OwningModule|null = null): OwningModule|null { +function owningModule(context: Context, override: OwningModule | null = null): OwningModule | null { let specifier = context.absoluteModuleName; if (override !== null) { specifier = override.specifier; diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/result.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/result.ts index 5dcddea8d3b86..32401f32f3a75 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/src/result.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/src/result.ts @@ -14,7 +14,6 @@ import {Declaration} from '../../reflection'; import {DynamicValue} from './dynamic'; import {SyntheticValue} from './synthetic'; - /** * A value resulting from static resolution. * @@ -23,8 +22,19 @@ import {SyntheticValue} from './synthetic'; * available statically. */ export type ResolvedValue = - number|boolean|string|null|undefined|Reference|EnumValue|ResolvedValueArray|ResolvedValueMap| - ResolvedModule|KnownFn|SyntheticValue|DynamicValue; + | number + | boolean + | string + | null + | undefined + | Reference + | EnumValue + | ResolvedValueArray + | ResolvedValueMap + | ResolvedModule + | KnownFn + | SyntheticValue + | DynamicValue; /** * An array of `ResolvedValue`s. @@ -48,8 +58,9 @@ export interface ResolvedValueMap extends Map {} */ export class ResolvedModule { constructor( - private exports: Map, - private evaluate: (decl: Declaration) => ResolvedValue) {} + private exports: Map, + private evaluate: (decl: Declaration) => ResolvedValue, + ) {} getExport(name: string): ResolvedValue { if (!this.exports.has(name)) { @@ -75,8 +86,10 @@ export class ResolvedModule { */ export class EnumValue { constructor( - readonly enumRef: Reference, readonly name: string, - readonly resolved: ResolvedValue) {} + readonly enumRef: Reference, + readonly name: string, + readonly resolved: ResolvedValue, + ) {} } /** diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/diagnostics_spec.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/diagnostics_spec.ts index a49859355a914..d692de56cee1e 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/diagnostics_spec.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/diagnostics_spec.ts @@ -20,7 +20,7 @@ import {DynamicValue} from '../src/dynamic'; import {PartialEvaluator} from '../src/interface'; import {EnumValue, ResolvedModule} from '../src/result'; -runInEachFileSystem(os => { +runInEachFileSystem((os) => { describe('partial evaluator', () => { describe('describeResolvedType()', () => { it('should describe primitives', () => { @@ -34,8 +34,14 @@ runInEachFileSystem(os => { it('should describe objects limited to a single level', () => { expect(describeResolvedType(new Map())).toBe('{}'); - expect(describeResolvedType(new Map([['a', 0], ['b', true]]))) - .toBe('{ a: number; b: boolean }'); + expect( + describeResolvedType( + new Map([ + ['a', 0], + ['b', true], + ]), + ), + ).toBe('{ a: number; b: boolean }'); expect(describeResolvedType(new Map([['a', new Map()]]))).toBe('{ a: object }'); expect(describeResolvedType(new Map([['a', [1, 2, 3]]]))).toBe('{ a: Array }'); }); @@ -43,40 +49,44 @@ runInEachFileSystem(os => { it('should describe arrays limited to a single level', () => { expect(describeResolvedType([])).toBe('[]'); expect(describeResolvedType([1, 2, 3])).toBe('[number, number, number]'); - expect(describeResolvedType([[1, 2], [3, 4]])).toBe('[Array, Array]'); + expect( + describeResolvedType([ + [1, 2], + [3, 4], + ]), + ).toBe('[Array, Array]'); expect(describeResolvedType([new Map([['a', 0]])])).toBe('[object]'); }); it('should describe references', () => { const namedFn = ts.factory.createFunctionDeclaration( - /* modifiers */ undefined, - /* asteriskToken */ undefined, - /* name */ 'test', - /* typeParameters */ undefined, - /* parameters */[], - /* type */ undefined, - /* body */ undefined, + /* modifiers */ undefined, + /* asteriskToken */ undefined, + /* name */ 'test', + /* typeParameters */ undefined, + /* parameters */ [], + /* type */ undefined, + /* body */ undefined, ); expect(describeResolvedType(new Reference(namedFn))).toBe('test'); const anonymousFn = ts.factory.createFunctionDeclaration( - /* modifiers */ undefined, - /* asteriskToken */ undefined, - /* name */ undefined, - /* typeParameters */ undefined, - /* parameters */[], - /* type */ undefined, - /* body */ undefined, + /* modifiers */ undefined, + /* asteriskToken */ undefined, + /* name */ undefined, + /* typeParameters */ undefined, + /* parameters */ [], + /* type */ undefined, + /* body */ undefined, ); expect(describeResolvedType(new Reference(anonymousFn))).toBe('(anonymous)'); }); it('should describe enum values', () => { const decl = ts.factory.createEnumDeclaration( - /* modifiers */ undefined, - /* name */ 'MyEnum', - /* members */[ts.factory.createEnumMember( - 'member', ts.factory.createNumericLiteral(1))], + /* modifiers */ undefined, + /* name */ 'MyEnum', + /* members */ [ts.factory.createEnumMember('member', ts.factory.createNumericLiteral(1))], ); const ref = new Reference(decl); expect(describeResolvedType(new EnumValue(ref, 'member', 1))).toBe('MyEnum'); @@ -84,8 +94,9 @@ runInEachFileSystem(os => { it('should describe dynamic values', () => { const node = ts.factory.createObjectLiteralExpression(); - expect(describeResolvedType(DynamicValue.fromUnsupportedSyntax(node))) - .toBe('(not statically analyzable)'); + expect(describeResolvedType(DynamicValue.fromUnsupportedSyntax(node))).toBe( + '(not statically analyzable)', + ); }); it('should describe known functions', () => { @@ -93,8 +104,9 @@ runInEachFileSystem(os => { }); it('should describe external modules', () => { - expect(describeResolvedType(new ResolvedModule(new Map(), () => undefined))) - .toBe('(module)'); + expect(describeResolvedType(new ResolvedModule(new Map(), () => undefined))).toBe( + '(module)', + ); }); }); @@ -138,9 +150,10 @@ runInEachFileSystem(os => { // Dynamic values exist for each node that has been visited, but only the initial dynamic // value within a statement is included in the trace. const trace = traceExpression( - `const firstChild = document.body.childNodes[0]; + `const firstChild = document.body.childNodes[0]; const child = firstChild.firstChild;`, - 'child !== undefined'); + 'child !== undefined', + ); expect(trace.length).toBe(4); expect(trace[0].messageText).toBe('Unable to evaluate this expression statically.'); @@ -155,9 +168,9 @@ runInEachFileSystem(os => { expect(absoluteFromSourceFile(trace[2].file!)).toBe(_('/entry.ts')); expect(getSourceCode(trace[2])).toBe('document.body'); - expect(trace[3].messageText) - .toBe( - `A value for 'document' cannot be determined statically, as it is an external declaration.`); + expect(trace[3].messageText).toBe( + `A value for 'document' cannot be determined statically, as it is an external declaration.`, + ); expect(absoluteFromSourceFile(trace[3].file!)).toBe(_('/lib.d.ts')); expect(getSourceCode(trace[3])).toBe('document: any'); }); @@ -186,22 +199,23 @@ runInEachFileSystem(os => { expect(trace.length).toBe(1); expect(trace[0].messageText).toBe('This syntax is not supported.'); expect(absoluteFromSourceFile(trace[0].file!)).toBe(_('/entry.ts')); - expect(getSourceCode(trace[0])).toBe('new String(\'test\')'); + expect(getSourceCode(trace[0])).toBe("new String('test')"); }); it('should trace complex function invocations', () => { const trace = traceExpression( - ` + ` function complex() { console.log('test'); return true; }`, - 'complex()'); + 'complex()', + ); expect(trace.length).toBe(2); - expect(trace[0].messageText) - .toBe( - 'Unable to evaluate function call of complex function. A function must have exactly one return statement.'); + expect(trace[0].messageText).toBe( + 'Unable to evaluate function call of complex function. A function must have exactly one return statement.', + ); expect(absoluteFromSourceFile(trace[0].file!)).toBe(_('/entry.ts')); expect(getSourceCode(trace[0])).toBe('complex()'); @@ -218,32 +232,36 @@ runInEachFileSystem(os => { expect(absoluteFromSourceFile(trace[0].file!)).toBe(_('/entry.ts')); expect(getSourceCode(trace[0])).toBe('body: {firstChild}'); - expect(trace[1].messageText) - .toBe( - `A value for 'document' cannot be determined statically, as it is an external declaration.`); + expect(trace[1].messageText).toBe( + `A value for 'document' cannot be determined statically, as it is an external declaration.`, + ); expect(absoluteFromSourceFile(trace[1].file!)).toBe(_('/lib.d.ts')); expect(getSourceCode(trace[1])).toBe('document: any'); }); it('should trace deep object destructuring of external reference', () => { - const trace = - traceExpression('const {doc: {body: {firstChild}}} = {doc: document};', 'firstChild'); + const trace = traceExpression( + 'const {doc: {body: {firstChild}}} = {doc: document};', + 'firstChild', + ); expect(trace.length).toBe(2); expect(trace[0].messageText).toBe('Unable to evaluate this expression statically.'); expect(absoluteFromSourceFile(trace[0].file!)).toBe(_('/entry.ts')); expect(getSourceCode(trace[0])).toBe('body: {firstChild}'); - expect(trace[1].messageText) - .toBe( - `A value for 'document' cannot be determined statically, as it is an external declaration.`); + expect(trace[1].messageText).toBe( + `A value for 'document' cannot be determined statically, as it is an external declaration.`, + ); expect(absoluteFromSourceFile(trace[1].file!)).toBe(_('/lib.d.ts')); expect(getSourceCode(trace[1])).toBe('document: any'); }); it('should trace array destructuring of dynamic value', () => { - const trace = - traceExpression('const [firstChild] = document.body.childNodes;', 'firstChild'); + const trace = traceExpression( + 'const [firstChild] = document.body.childNodes;', + 'firstChild', + ); expect(trace.length).toBe(3); expect(trace[0].messageText).toBe('Unable to evaluate this expression statically.'); @@ -254,9 +272,9 @@ runInEachFileSystem(os => { expect(absoluteFromSourceFile(trace[1].file!)).toBe(_('/entry.ts')); expect(getSourceCode(trace[1])).toBe('document.body'); - expect(trace[2].messageText) - .toBe( - `A value for 'document' cannot be determined statically, as it is an external declaration.`); + expect(trace[2].messageText).toBe( + `A value for 'document' cannot be determined statically, as it is an external declaration.`, + ); expect(absoluteFromSourceFile(trace[2].file!)).toBe(_('/lib.d.ts')); expect(getSourceCode(trace[2])).toBe('document: any'); }); @@ -272,11 +290,14 @@ function getSourceCode(diag: ts.DiagnosticRelatedInformation): string { function traceExpression(code: string, expr: string): ts.DiagnosticRelatedInformation[] { const {program} = makeProgram( - [ - {name: _('/entry.ts'), contents: `${code}; const target$ = ${expr};`}, - {name: _('/lib.d.ts'), contents: `declare const document: any;`}, - ], - /* options */ undefined, /* host */ undefined, /* checkForErrors */ false); + [ + {name: _('/entry.ts'), contents: `${code}; const target$ = ${expr};`}, + {name: _('/lib.d.ts'), contents: `declare const document: any;`}, + ], + /* options */ undefined, + /* host */ undefined, + /* checkForErrors */ false, + ); const checker = program.getTypeChecker(); const decl = getDeclaration(program, _('/entry.ts'), 'target$', ts.isVariableDeclaration); const valueExpr = decl.initializer!; diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts index 23349cc0e43fa..0fca87d9c2d36 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/evaluator_spec.ts @@ -15,27 +15,37 @@ import {getDeclaration, makeProgram} from '../../testing'; import {DynamicValue} from '../src/dynamic'; import {EnumValue} from '../src/result'; -import {arrowReturnValueFfr, evaluate, firstArgFfr, makeEvaluator, makeExpression, owningModuleOf, returnTypeFfr} from './utils'; +import { + arrowReturnValueFfr, + evaluate, + firstArgFfr, + makeEvaluator, + makeExpression, + owningModuleOf, + returnTypeFfr, +} from './utils'; runInEachFileSystem(() => { describe('ngtsc metadata', () => { let _: typeof absoluteFrom; - beforeEach(() => _ = absoluteFrom); + beforeEach(() => (_ = absoluteFrom)); it('reads a file correctly', () => { const value = evaluate( - ` + ` import {Y} from './other'; const A = Y; `, - 'A', [ - { - name: _('/other.ts'), - contents: ` + 'A', + [ + { + name: _('/other.ts'), + contents: ` export const Y = 'test'; - ` - }, - ]); + `, + }, + ], + ); expect(value).toEqual('test'); }); @@ -60,7 +70,8 @@ runInEachFileSystem(() => { it('function call spread works', () => { expect(evaluate(`function foo(a, ...b) { return [a, b]; }`, 'foo(1, ...[2, 3])')).toEqual([ - 1, [2, 3] + 1, + [2, 3], ]); }); @@ -77,15 +88,18 @@ runInEachFileSystem(() => { }); it('static property call works', () => { - expect(evaluate(`class Foo { static bar(test) { return test; } }`, 'Foo.bar("test")')) - .toEqual('test'); + expect( + evaluate(`class Foo { static bar(test) { return test; } }`, 'Foo.bar("test")'), + ).toEqual('test'); }); it('indirected static property call works', () => { expect( - evaluate( - `class Foo { static bar(test) { return test; } }; const fn = Foo.bar;`, 'fn("test")')) - .toEqual('test'); + evaluate( + `class Foo { static bar(test) { return test; } }; const fn = Foo.bar;`, + 'fn("test")', + ), + ).toEqual('test'); }); it('array works', () => { @@ -93,8 +107,9 @@ runInEachFileSystem(() => { }); it('array spread works', () => { - expect(evaluate(`const a = [1, 2]; const b = [4, 5]; const c = [...a, 3, ...b];`, 'c')) - .toEqual([1, 2, 3, 4, 5]); + expect( + evaluate(`const a = [1, 2]; const b = [4, 5]; const c = [...a, 3, ...b];`, 'c'), + ).toEqual([1, 2, 3, 4, 5]); }); it('&& operations work', () => { @@ -164,20 +179,20 @@ runInEachFileSystem(() => { }); it('array `length` property access works', () => { - expect(evaluate(`const a = [1, 2, 3];`, 'a[\'length\'] + 1')).toEqual(4); + expect(evaluate(`const a = [1, 2, 3];`, "a['length'] + 1")).toEqual(4); }); it('array `slice` function works', () => { - expect(evaluate(`const a = [1, 2, 3];`, 'a[\'slice\']()')).toEqual([1, 2, 3]); + expect(evaluate(`const a = [1, 2, 3];`, "a['slice']()")).toEqual([1, 2, 3]); }); it('array `concat` function works', () => { - expect(evaluate(`const a = [1, 2], b = [3, 4];`, 'a[\'concat\'](b)')).toEqual([1, 2, 3, 4]); - expect(evaluate(`const a = [1, 2], b = 3;`, 'a[\'concat\'](b)')).toEqual([1, 2, 3]); - expect(evaluate(`const a = [1, 2], b = 3, c = [4, 5];`, 'a[\'concat\'](b, c)')).toEqual([ - 1, 2, 3, 4, 5 + expect(evaluate(`const a = [1, 2], b = [3, 4];`, "a['concat'](b)")).toEqual([1, 2, 3, 4]); + expect(evaluate(`const a = [1, 2], b = 3;`, "a['concat'](b)")).toEqual([1, 2, 3]); + expect(evaluate(`const a = [1, 2], b = 3, c = [4, 5];`, "a['concat'](b, c)")).toEqual([ + 1, 2, 3, 4, 5, ]); - expect(evaluate(`const a = [1, 2], b = [3, 4]`, 'a[\'concat\'](...b)')).toEqual([1, 2, 3, 4]); + expect(evaluate(`const a = [1, 2], b = [3, 4]`, "a['concat'](...b)")).toEqual([1, 2, 3, 4]); }); it('negation works', () => { @@ -245,9 +260,9 @@ runInEachFileSystem(() => { }); it('resolves unknown values in a destructured variable declaration as dynamic values', () => { - const value = evaluate( - `const {a: {body}} = {a: window};`, 'body', - [{name: _('/window.ts'), contents: `declare const window: any;`}]); + const value = evaluate(`const {a: {body}} = {a: window};`, 'body', [ + {name: _('/window.ts'), contents: `declare const window: any;`}, + ]); if (!(value instanceof DynamicValue)) { return fail(`Should have resolved to a DynamicValue`); } @@ -368,7 +383,10 @@ runInEachFileSystem(() => { it('supports declarations of tuples', () => { expect(evaluate(`declare const x: ['foo', 42, null, true];`, `x`)).toEqual([ - 'foo', 42, null, true + 'foo', + 42, + null, + true, ]); expect(evaluate(`declare const x: ['bar'];`, `[...x]`)).toEqual(['bar']); }); @@ -376,16 +394,18 @@ runInEachFileSystem(() => { // https://github.com/angular/angular/issues/48089 it('supports declarations of readonly tuples with class references', () => { const tuple = evaluate( - ` + ` import {External} from 'external'; declare class Local {} declare const x: readonly [typeof External, typeof Local];`, - `x`, [ - { - name: _('/node_modules/external/index.d.ts'), - contents: 'export declare class External {}' - }, - ]); + `x`, + [ + { + name: _('/node_modules/external/index.d.ts'), + contents: 'export declare class External {}', + }, + ], + ); if (!Array.isArray(tuple)) { return fail('Should have evaluated tuple as an array'); } @@ -414,7 +434,6 @@ runInEachFileSystem(() => { expect(value[1].isFromDynamicType()).toBe(true); }); - it('imports work', () => { const {program} = makeProgram([ {name: _('/second.ts'), contents: 'export function foo(bar) { return bar; }'}, @@ -423,7 +442,7 @@ runInEachFileSystem(() => { contents: ` import {foo} from './second'; const target$ = foo; - ` + `, }, ]); const checker = program.getTypeChecker(); @@ -446,14 +465,14 @@ runInEachFileSystem(() => { const {program} = makeProgram([ { name: _('/node_modules/some_library/index.d.ts'), - contents: 'export declare function foo(bar);' + contents: 'export declare function foo(bar);', }, { name: _('/entry.ts'), contents: ` import {foo} from 'some_library'; const target$ = foo; - ` + `, }, ]); const checker = program.getTypeChecker(); @@ -473,12 +492,12 @@ runInEachFileSystem(() => { it('reads values from default exports', () => { const value = evaluate( - ` + ` import mod from './second'; `, - 'mod.property', [ - {name: _('/second.ts'), contents: 'export default {property: "test"}'}, - ]); + 'mod.property', + [{name: _('/second.ts'), contents: 'export default {property: "test"}'}], + ); expect(value).toEqual('test'); }); @@ -501,10 +520,12 @@ runInEachFileSystem(() => { it('map spread works', () => { const map: Map = evaluate>( - `const a = {a: 1}; const b = {b: 2, c: 1}; const c = {...a, ...b, c: 3};`, 'c'); + `const a = {a: 1}; const b = {b: 2, c: 1}; const c = {...a, ...b, c: 3};`, + 'c', + ); const obj: {[key: string]: number} = {}; - map.forEach((value, key) => obj[key] = value); + map.forEach((value, key) => (obj[key] = value)); expect(obj).toEqual({ a: 1, b: 2, @@ -514,12 +535,13 @@ runInEachFileSystem(() => { it('module spread works', () => { const map = evaluate>( - `import * as mod from './module'; const c = {...mod, c: 3};`, 'c', [ - {name: _('/module.ts'), contents: `export const a = 1; export const b = 2;`}, - ]); + `import * as mod from './module'; const c = {...mod, c: 3};`, + 'c', + [{name: _('/module.ts'), contents: `export const a = 1; export const b = 2;`}], + ); const obj: {[key: string]: number} = {}; - map.forEach((value, key) => obj[key] = value); + map.forEach((value, key) => (obj[key] = value)); expect(obj).toEqual({ a: 1, b: 2, @@ -534,24 +556,26 @@ runInEachFileSystem(() => { contents: ` import * as mod2 from './mod2'; export const primary = mod2.indirection; - export const secondary = 2;` + export const secondary = 2;`, }, { name: _('/mod2.ts'), - contents: `import * as mod1 from './mod1'; export const indirection = mod1.secondary;` + contents: `import * as mod1 from './mod1'; export const indirection = mod1.secondary;`, }, ]); expect(value).toEqual(2); }); it('indirected-via-object function call works', () => { - expect(evaluate( - ` + expect( + evaluate( + ` function fn(res) { return res; } const obj = {fn}; `, - 'obj.fn("test")')) - .toEqual('test'); + 'obj.fn("test")', + ), + ).toEqual('test'); }); it('template expressions work', () => { @@ -563,19 +587,21 @@ runInEachFileSystem(() => { }); it('string concatenation should resolve enums', () => { - expect(evaluate('enum Test { VALUE = "test" };', '"a." + Test.VALUE + ".b"')) - .toBe('a.test.b'); + expect(evaluate('enum Test { VALUE = "test" };', '"a." + Test.VALUE + ".b"')).toBe( + 'a.test.b', + ); }); it('string `concat` function works', () => { - expect(evaluate(`const a = '12', b = '34';`, 'a[\'concat\'](b)')).toBe('1234'); - expect(evaluate(`const a = '12', b = '3';`, 'a[\'concat\'](b)')).toBe('123'); - expect(evaluate(`const a = '12', b = '3', c = '45';`, 'a[\'concat\'](b,c)')).toBe('12345'); + expect(evaluate(`const a = '12', b = '34';`, "a['concat'](b)")).toBe('1234'); + expect(evaluate(`const a = '12', b = '3';`, "a['concat'](b)")).toBe('123'); + expect(evaluate(`const a = '12', b = '3', c = '45';`, "a['concat'](b,c)")).toBe('12345'); expect( - evaluate(`const a = '1', b = 2, c = '3', d = true, e = null;`, 'a[\'concat\'](b,c,d,e)')) - .toBe('123truenull'); - expect(evaluate('enum Test { VALUE = "test" };', '"a."[\'concat\'](Test.VALUE, ".b")')) - .toBe('a.test.b'); + evaluate(`const a = '1', b = 2, c = '3', d = true, e = null;`, "a['concat'](b,c,d,e)"), + ).toBe('123truenull'); + expect(evaluate('enum Test { VALUE = "test" };', '"a."[\'concat\'](Test.VALUE, ".b")')).toBe( + 'a.test.b', + ); }); it('should resolve non-literals as dynamic string', () => { @@ -596,7 +622,7 @@ runInEachFileSystem(() => { it('enum resolution works', () => { const result = evaluate( - ` + ` enum Foo { A, B, @@ -605,7 +631,8 @@ runInEachFileSystem(() => { const r = Foo.B; `, - 'r'); + 'r', + ); if (!(result instanceof EnumValue)) { return fail(`result is not an EnumValue`); } @@ -638,8 +665,7 @@ runInEachFileSystem(() => { {name: _('/decl.d.ts'), contents: 'export declare const fn: any;'}, { name: _('/entry.ts'), - contents: - `import {fn} from './decl'; const prop = fn.foo(); const target$ = {value: prop};` + contents: `import {fn} from './decl'; const prop = fn.foo(); const target$ = {value: prop};`, }, ]); const checker = program.getTypeChecker(); @@ -660,14 +686,17 @@ runInEachFileSystem(() => { it('should not attach identifiers to FFR-resolved values', () => { const value = evaluate( - ` + ` declare function foo(arg: any): any; class Target {} const indir = foo(Target); const value = indir; `, - 'value', [], firstArgFfr); + 'value', + [], + firstArgFfr, + ); if (!(value instanceof Reference)) { return fail('Expected value to be a Reference'); } @@ -678,95 +707,103 @@ runInEachFileSystem(() => { expect(id.text).toEqual('Target'); }); - it('should not associate an owning module when a FFR-resolved expression is within the originating source file', - () => { - const resolved = evaluate( - `import {forwardRef} from 'forward'; + it('should not associate an owning module when a FFR-resolved expression is within the originating source file', () => { + const resolved = evaluate( + `import {forwardRef} from 'forward'; class Foo {}`, - 'forwardRef(() => Foo)', [{ - name: _('/node_modules/forward/index.d.ts'), - contents: `export declare function forwardRef(fn: () => T): T;`, - }], - arrowReturnValueFfr); - if (!(resolved instanceof Reference)) { - return fail('Expected expression to resolve to a reference'); - } - expect((resolved.node as ts.ClassDeclaration).name!.text).toBe('Foo'); - expect(resolved.bestGuessOwningModule).toBeNull(); - }); - - it('should not associate an owning module when a FFR-resolved expression is imported using a relative import', - () => { - const resolved = evaluate( - `import {forwardRef} from 'forward'; + 'forwardRef(() => Foo)', + [ + { + name: _('/node_modules/forward/index.d.ts'), + contents: `export declare function forwardRef(fn: () => T): T;`, + }, + ], + arrowReturnValueFfr, + ); + if (!(resolved instanceof Reference)) { + return fail('Expected expression to resolve to a reference'); + } + expect((resolved.node as ts.ClassDeclaration).name!.text).toBe('Foo'); + expect(resolved.bestGuessOwningModule).toBeNull(); + }); + + it('should not associate an owning module when a FFR-resolved expression is imported using a relative import', () => { + const resolved = evaluate( + `import {forwardRef} from 'forward'; import {Foo} from './foo';`, - 'forwardRef(() => Foo)', - [ - { - name: _('/node_modules/forward/index.d.ts'), - contents: `export declare function forwardRef(fn: () => T): T;`, - }, - { - name: _('/foo.ts'), - contents: `export class Foo {}`, - } - ], - arrowReturnValueFfr); - if (!(resolved instanceof Reference)) { - return fail('Expected expression to resolve to a reference'); - } - expect((resolved.node as ts.ClassDeclaration).name!.text).toBe('Foo'); - expect(resolved.bestGuessOwningModule).toBeNull(); - }); - - it('should associate an owning module when a FFR-resolved expression is imported using an absolute import', - () => { - const {expression, checker} = makeExpression( - `import {forwardRef} from 'forward'; + 'forwardRef(() => Foo)', + [ + { + name: _('/node_modules/forward/index.d.ts'), + contents: `export declare function forwardRef(fn: () => T): T;`, + }, + { + name: _('/foo.ts'), + contents: `export class Foo {}`, + }, + ], + arrowReturnValueFfr, + ); + if (!(resolved instanceof Reference)) { + return fail('Expected expression to resolve to a reference'); + } + expect((resolved.node as ts.ClassDeclaration).name!.text).toBe('Foo'); + expect(resolved.bestGuessOwningModule).toBeNull(); + }); + + it('should associate an owning module when a FFR-resolved expression is imported using an absolute import', () => { + const {expression, checker} = makeExpression( + `import {forwardRef} from 'forward'; import {Foo} from 'external';`, - `forwardRef(() => Foo)`, [ - { - name: _('/node_modules/forward/index.d.ts'), - contents: `export declare function forwardRef(fn: () => T): T;`, - }, - { - name: _('/node_modules/external/index.d.ts'), - contents: `export declare class Foo {}`, - } - ]); - const evaluator = makeEvaluator(checker); - const resolved = evaluator.evaluate(expression, arrowReturnValueFfr); - if (!(resolved instanceof Reference)) { - return fail('Expected expression to resolve to a reference'); - } - expect((resolved.node as ts.ClassDeclaration).name!.text).toBe('Foo'); - expect(resolved.bestGuessOwningModule).toEqual({ - specifier: 'external', - resolutionContext: expression.getSourceFile().fileName, - }); - }); - - it('should associate an owning module when a FFR-resolved expression is within the foreign file', - () => { - const {expression, checker} = - makeExpression(`import {external} from 'external';`, `external()`, [{ - name: _('/node_modules/external/index.d.ts'), - contents: ` + `forwardRef(() => Foo)`, + [ + { + name: _('/node_modules/forward/index.d.ts'), + contents: `export declare function forwardRef(fn: () => T): T;`, + }, + { + name: _('/node_modules/external/index.d.ts'), + contents: `export declare class Foo {}`, + }, + ], + ); + const evaluator = makeEvaluator(checker); + const resolved = evaluator.evaluate(expression, arrowReturnValueFfr); + if (!(resolved instanceof Reference)) { + return fail('Expected expression to resolve to a reference'); + } + expect((resolved.node as ts.ClassDeclaration).name!.text).toBe('Foo'); + expect(resolved.bestGuessOwningModule).toEqual({ + specifier: 'external', + resolutionContext: expression.getSourceFile().fileName, + }); + }); + + it('should associate an owning module when a FFR-resolved expression is within the foreign file', () => { + const {expression, checker} = makeExpression( + `import {external} from 'external';`, + `external()`, + [ + { + name: _('/node_modules/external/index.d.ts'), + contents: ` export declare class Foo {} export declare function external(): Foo; - ` - }]); - const evaluator = makeEvaluator(checker); - const resolved = evaluator.evaluate(expression, returnTypeFfr); - if (!(resolved instanceof Reference)) { - return fail('Expected expression to resolve to a reference'); - } - expect((resolved.node as ts.ClassDeclaration).name!.text).toBe('Foo'); - expect(resolved.bestGuessOwningModule).toEqual({ - specifier: 'external', - resolutionContext: expression.getSourceFile().fileName, - }); - }); + `, + }, + ], + ); + const evaluator = makeEvaluator(checker); + const resolved = evaluator.evaluate(expression, returnTypeFfr); + if (!(resolved instanceof Reference)) { + return fail('Expected expression to resolve to a reference'); + } + expect((resolved.node as ts.ClassDeclaration).name!.text).toBe('Foo'); + expect(resolved.bestGuessOwningModule).toEqual({ + specifier: 'external', + resolutionContext: expression.getSourceFile().fileName, + }); + }); it('should resolve functions with more than one statement to a complex function call', () => { const value = evaluate(`function foo(bar) { const b = bar; return b; }`, 'foo("test")'); @@ -778,67 +815,81 @@ runInEachFileSystem(() => { return fail('Expected DynamicValue to be from complex function call'); } expect((value.node as ts.CallExpression).expression.getText()).toBe('foo'); - expect((value.reason.node as ts.FunctionDeclaration).getText()) - .toContain('const b = bar; return b;'); + expect((value.reason.node as ts.FunctionDeclaration).getText()).toContain( + 'const b = bar; return b;', + ); }); describe('(visited file tracking)', () => { it('should track each time a source file is visited', () => { const addDependency = - jasmine.createSpy('DependencyTracker'); + jasmine.createSpy('DependencyTracker'); const {expression, checker, program} = makeExpression( - `class A { static foo = 42; } function bar() { return A.foo; }`, 'bar()'); + `class A { static foo = 42; } function bar() { return A.foo; }`, + 'bar()', + ); const entryPath = getSourceFileOrError(program, _('/entry.ts')).fileName; const evaluator = makeEvaluator(checker, {...fakeDepTracker, addDependency}); evaluator.evaluate(expression); - expect(addDependency).toHaveBeenCalledTimes(2); // two declaration visited + expect(addDependency).toHaveBeenCalledTimes(2); // two declaration visited expect( - addDependency.calls.allArgs().map( - (args: Parameters) => [args[0].fileName, args[1].fileName])) - .toEqual([[entryPath, entryPath], [entryPath, entryPath]]); + addDependency.calls + .allArgs() + .map((args: Parameters) => [args[0].fileName, args[1].fileName]), + ).toEqual([ + [entryPath, entryPath], + [entryPath, entryPath], + ]); }); it('should track imported source files', () => { const addDependency = - jasmine.createSpy('DependencyTracker'); - const {expression, checker, program} = - makeExpression(`import {Y} from './other'; const A = Y;`, 'A', [ - {name: _('/other.ts'), contents: `export const Y = 'test';`}, - {name: _('/not-visited.ts'), contents: `export const Z = 'nope';`} - ]); + jasmine.createSpy('DependencyTracker'); + const {expression, checker, program} = makeExpression( + `import {Y} from './other'; const A = Y;`, + 'A', + [ + {name: _('/other.ts'), contents: `export const Y = 'test';`}, + {name: _('/not-visited.ts'), contents: `export const Z = 'nope';`}, + ], + ); const entryPath = getSourceFileOrError(program, _('/entry.ts')).fileName; const otherPath = getSourceFileOrError(program, _('/other.ts')).fileName; const evaluator = makeEvaluator(checker, {...fakeDepTracker, addDependency}); evaluator.evaluate(expression); expect(addDependency).toHaveBeenCalledTimes(2); expect( - addDependency.calls.allArgs().map( - (args: Parameters) => [args[0].fileName, args[1].fileName])) - .toEqual([ - [entryPath, entryPath], - [entryPath, otherPath], - ]); + addDependency.calls + .allArgs() + .map((args: Parameters) => [args[0].fileName, args[1].fileName]), + ).toEqual([ + [entryPath, entryPath], + [entryPath, otherPath], + ]); }); it('should track files passed through during re-exports', () => { const addDependency = - jasmine.createSpy('DependencyTracker'); - const {expression, checker, program} = - makeExpression(`import * as mod from './direct-reexport';`, 'mod.value.property', [ - {name: _('/const.ts'), contents: 'export const value = {property: "test"};'}, - { - name: _('/def.ts'), - contents: `import {value} from './const'; export default value;` - }, - { - name: _('/indirect-reexport.ts'), - contents: `import value from './def'; export {value};` - }, - { - name: _('/direct-reexport.ts'), - contents: `export {value} from './indirect-reexport';` - }, - ]); + jasmine.createSpy('DependencyTracker'); + const {expression, checker, program} = makeExpression( + `import * as mod from './direct-reexport';`, + 'mod.value.property', + [ + {name: _('/const.ts'), contents: 'export const value = {property: "test"};'}, + { + name: _('/def.ts'), + contents: `import {value} from './const'; export default value;`, + }, + { + name: _('/indirect-reexport.ts'), + contents: `import value from './def'; export {value};`, + }, + { + name: _('/direct-reexport.ts'), + contents: `export {value} from './indirect-reexport';`, + }, + ], + ); const evaluator = makeEvaluator(checker, {...fakeDepTracker, addDependency}); const entryPath = getSourceFileOrError(program, _('/entry.ts')).fileName; const directReexportPath = getSourceFileOrError(program, _('/direct-reexport.ts')).fileName; @@ -846,14 +897,15 @@ runInEachFileSystem(() => { evaluator.evaluate(expression); expect(addDependency).toHaveBeenCalledTimes(2); expect( - addDependency.calls.allArgs().map( - (args: Parameters) => [args[0].fileName, args[1].fileName])) - .toEqual([ - [entryPath, directReexportPath], - // Not '/indirect-reexport.ts' or '/def.ts'. - // TS skips through them when finding the original symbol for `value` - [entryPath, constPath], - ]); + addDependency.calls + .allArgs() + .map((args: Parameters) => [args[0].fileName, args[1].fileName]), + ).toEqual([ + [entryPath, directReexportPath], + // Not '/indirect-reexport.ts' or '/def.ts'. + // TS skips through them when finding the original symbol for `value` + [entryPath, constPath], + ]); }); }); }); diff --git a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/utils.ts b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/utils.ts index c030d5a5dfbfa..153f2b30f3f76 100644 --- a/packages/compiler-cli/src/ngtsc/partial_evaluator/test/utils.ts +++ b/packages/compiler-cli/src/ngtsc/partial_evaluator/test/utils.ts @@ -16,20 +16,28 @@ import {getDeclaration, makeProgram} from '../../testing'; import {ForeignFunctionResolver, PartialEvaluator} from '../src/interface'; import {ResolvedValue} from '../src/result'; -export function makeExpression(code: string, expr: string, supportingFiles: TestFile[] = []): { - expression: ts.Expression, - host: ts.CompilerHost, - checker: ts.TypeChecker, - program: ts.Program, - options: ts.CompilerOptions +export function makeExpression( + code: string, + expr: string, + supportingFiles: TestFile[] = [], +): { + expression: ts.Expression; + host: ts.CompilerHost; + checker: ts.TypeChecker; + program: ts.Program; + options: ts.CompilerOptions; } { const {program, options, host} = makeProgram([ {name: absoluteFrom('/entry.ts'), contents: `${code}; const target$ = ${expr};`}, - ...supportingFiles + ...supportingFiles, ]); const checker = program.getTypeChecker(); - const decl = - getDeclaration(program, absoluteFrom('/entry.ts'), 'target$', ts.isVariableDeclaration); + const decl = getDeclaration( + program, + absoluteFrom('/entry.ts'), + 'target$', + ts.isVariableDeclaration, + ); return { expression: decl.initializer!, host, @@ -40,26 +48,33 @@ export function makeExpression(code: string, expr: string, supportingFiles: Test } export function makeEvaluator( - checker: ts.TypeChecker, tracker?: DependencyTracker): PartialEvaluator { + checker: ts.TypeChecker, + tracker?: DependencyTracker, +): PartialEvaluator { const reflectionHost = new TypeScriptReflectionHost(checker); return new PartialEvaluator(reflectionHost, checker, tracker !== undefined ? tracker : null); } export function evaluate( - code: string, expr: string, supportingFiles: TestFile[] = [], - foreignFunctionResolver?: ForeignFunctionResolver): T { + code: string, + expr: string, + supportingFiles: TestFile[] = [], + foreignFunctionResolver?: ForeignFunctionResolver, +): T { const {expression, checker} = makeExpression(code, expr, supportingFiles); const evaluator = makeEvaluator(checker); return evaluator.evaluate(expression, foreignFunctionResolver) as T; } -export function owningModuleOf(ref: Reference): string|null { +export function owningModuleOf(ref: Reference): string | null { return ref.bestGuessOwningModule !== null ? ref.bestGuessOwningModule.specifier : null; } export function firstArgFfr( - fn: Reference, - expr: ts.CallExpression, resolve: (expr: ts.Expression) => ResolvedValue): ResolvedValue { + fn: Reference, + expr: ts.CallExpression, + resolve: (expr: ts.Expression) => ResolvedValue, +): ResolvedValue { return resolve(expr.arguments[0]); } @@ -71,5 +86,6 @@ export const arrowReturnValueFfr: ForeignFunctionResolver = (_fn, node, resolve) export const returnTypeFfr: ForeignFunctionResolver = (fn, node, resolve) => { // Extract the `Foo` from the return type of the `external` function declaration. return resolve( - ((fn.node as ts.FunctionDeclaration).type as ts.TypeReferenceNode).typeName as ts.Identifier); + ((fn.node as ts.FunctionDeclaration).type as ts.TypeReferenceNode).typeName as ts.Identifier, + ); }; diff --git a/packages/compiler-cli/src/ngtsc/perf/src/clock.ts b/packages/compiler-cli/src/ngtsc/perf/src/clock.ts index 5f5e23f1c7569..5d3da08e1d395 100644 --- a/packages/compiler-cli/src/ngtsc/perf/src/clock.ts +++ b/packages/compiler-cli/src/ngtsc/perf/src/clock.ts @@ -17,5 +17,5 @@ export function mark(): HrTime { export function timeSinceInMicros(mark: HrTime): number { const delta = process.hrtime(mark); - return (delta[0] * 1000000) + Math.floor(delta[1] / 1000); + return delta[0] * 1000000 + Math.floor(delta[1] / 1000); } diff --git a/packages/compiler-cli/src/ngtsc/perf/src/noop.ts b/packages/compiler-cli/src/ngtsc/perf/src/noop.ts index 8d5c8e0a416ed..cff7b85e61b66 100644 --- a/packages/compiler-cli/src/ngtsc/perf/src/noop.ts +++ b/packages/compiler-cli/src/ngtsc/perf/src/noop.ts @@ -23,5 +23,4 @@ class NoopPerfRecorder implements PerfRecorder { reset(): void {} } - export const NOOP_PERF_RECORDER: PerfRecorder = new NoopPerfRecorder(); diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 75dacbb1a8f08..1d0313cec47ea 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -13,7 +13,13 @@ import * as api from '../transformers/api'; import {i18nExtract} from '../transformers/i18n'; import {verifySupportedTypeScriptVersion} from '../typescript_support'; -import {CompilationTicket, freshCompilationTicket, incrementalFromCompilerTicket, NgCompiler, NgCompilerHost} from './core'; +import { + CompilationTicket, + freshCompilationTicket, + incrementalFromCompilerTicket, + NgCompiler, + NgCompilerHost, +} from './core'; import {NgCompilerOptions} from './core/api'; import {DocEntry} from './docs'; import {absoluteFrom, AbsoluteFsPath, getFileSystem, resolve} from './file_system'; @@ -42,8 +48,11 @@ export class NgtscProgram implements api.Program { private incrementalStrategy: TrackedIncrementalBuildStrategy; constructor( - rootNames: ReadonlyArray, private options: NgCompilerOptions, - delegateHost: api.CompilerHost, oldProgram?: NgtscProgram) { + rootNames: ReadonlyArray, + private options: NgCompilerOptions, + delegateHost: api.CompilerHost, + oldProgram?: NgtscProgram, + ) { const perfRecorder = ActivePerfRecorder.zeroedToNow(); perfRecorder.phase(PerfPhase.Setup); @@ -70,9 +79,9 @@ export class NgtscProgram implements api.Program { retagAllTsFiles(reuseProgram); } - this.tsProgram = perfRecorder.inPhase( - PerfPhase.TypeScriptProgramCreate, - () => ts.createProgram(this.host.inputFiles, options, this.host, reuseProgram)); + this.tsProgram = perfRecorder.inPhase(PerfPhase.TypeScriptProgramCreate, () => + ts.createProgram(this.host.inputFiles, options, this.host, reuseProgram), + ); perfRecorder.phase(PerfPhase.Unaccounted); perfRecorder.memory(PerfCheckpoint.TypeScriptProgramCreate); @@ -84,11 +93,16 @@ export class NgtscProgram implements api.Program { untagAllTsFiles(this.tsProgram); const programDriver = new TsCreateProgramDriver( - this.tsProgram, this.host, this.options, this.host.shimExtensionPrefixes); - - this.incrementalStrategy = oldProgram !== undefined ? - oldProgram.incrementalStrategy.toNextBuildStrategy() : - new TrackedIncrementalBuildStrategy(); + this.tsProgram, + this.host, + this.options, + this.host.shimExtensionPrefixes, + ); + + this.incrementalStrategy = + oldProgram !== undefined + ? oldProgram.incrementalStrategy.toNextBuildStrategy() + : new TrackedIncrementalBuildStrategy(); const modifiedResourceFiles = new Set(); if (this.host.getModifiedResourceFiles !== undefined) { const strings = this.host.getModifiedResourceFiles(); @@ -102,20 +116,25 @@ export class NgtscProgram implements api.Program { let ticket: CompilationTicket; if (oldProgram === undefined) { ticket = freshCompilationTicket( - this.tsProgram, options, this.incrementalStrategy, programDriver, perfRecorder, - /* enableTemplateTypeChecker */ false, /* usePoisonedData */ false); + this.tsProgram, + options, + this.incrementalStrategy, + programDriver, + perfRecorder, + /* enableTemplateTypeChecker */ false, + /* usePoisonedData */ false, + ); } else { ticket = incrementalFromCompilerTicket( - oldProgram.compiler, - this.tsProgram, - this.incrementalStrategy, - programDriver, - modifiedResourceFiles, - perfRecorder, + oldProgram.compiler, + this.tsProgram, + this.incrementalStrategy, + programDriver, + modifiedResourceFiles, + perfRecorder, ); } - // Create the NgCompiler which will drive the rest of the compilation. this.compiler = NgCompiler.fromTicket(ticket, this.host); } @@ -128,16 +147,18 @@ export class NgtscProgram implements api.Program { return this.compiler.getCurrentProgram(); } - getTsOptionDiagnostics(cancellationToken?: ts.CancellationToken| - undefined): readonly ts.Diagnostic[] { - return this.compiler.perfRecorder.inPhase( - PerfPhase.TypeScriptDiagnostics, - () => this.tsProgram.getOptionsDiagnostics(cancellationToken)); + getTsOptionDiagnostics( + cancellationToken?: ts.CancellationToken | undefined, + ): readonly ts.Diagnostic[] { + return this.compiler.perfRecorder.inPhase(PerfPhase.TypeScriptDiagnostics, () => + this.tsProgram.getOptionsDiagnostics(cancellationToken), + ); } getTsSyntacticDiagnostics( - sourceFile?: ts.SourceFile|undefined, - cancellationToken?: ts.CancellationToken|undefined): readonly ts.Diagnostic[] { + sourceFile?: ts.SourceFile | undefined, + cancellationToken?: ts.CancellationToken | undefined, + ): readonly ts.Diagnostic[] { return this.compiler.perfRecorder.inPhase(PerfPhase.TypeScriptDiagnostics, () => { const ignoredFiles = this.compiler.ignoreForDiagnostics; let res: readonly ts.Diagnostic[]; @@ -161,8 +182,9 @@ export class NgtscProgram implements api.Program { } getTsSemanticDiagnostics( - sourceFile?: ts.SourceFile|undefined, - cancellationToken?: ts.CancellationToken|undefined): readonly ts.Diagnostic[] { + sourceFile?: ts.SourceFile | undefined, + cancellationToken?: ts.CancellationToken | undefined, + ): readonly ts.Diagnostic[] { // No TS semantic check should be done in local compilation mode, as it is always full of errors // due to cross file imports. if (this.options.compilationMode === 'experimental-local') { @@ -191,20 +213,23 @@ export class NgtscProgram implements api.Program { }); } - getNgOptionDiagnostics(cancellationToken?: ts.CancellationToken| - undefined): readonly ts.Diagnostic[] { + getNgOptionDiagnostics( + cancellationToken?: ts.CancellationToken | undefined, + ): readonly ts.Diagnostic[] { return this.compiler.getOptionDiagnostics(); } - getNgStructuralDiagnostics(cancellationToken?: ts.CancellationToken| - undefined): readonly ts.Diagnostic[] { + getNgStructuralDiagnostics( + cancellationToken?: ts.CancellationToken | undefined, + ): readonly ts.Diagnostic[] { return []; } getNgSemanticDiagnostics( - fileName?: string|undefined, - cancellationToken?: ts.CancellationToken|undefined): readonly ts.Diagnostic[] { - let sf: ts.SourceFile|undefined = undefined; + fileName?: string | undefined, + cancellationToken?: ts.CancellationToken | undefined, + ): readonly ts.Diagnostic[] { + let sf: ts.SourceFile | undefined = undefined; if (fileName !== undefined) { sf = this.tsProgram.getSourceFile(fileName); if (sf === undefined) { @@ -232,7 +257,7 @@ export class NgtscProgram implements api.Program { return this.compiler.analyzeAsync(); } - listLazyRoutes(entryRoute?: string|undefined): api.LazyRoute[] { + listLazyRoutes(entryRoute?: string | undefined): api.LazyRoute[] { return []; } @@ -240,15 +265,24 @@ export class NgtscProgram implements api.Program { const ctx = new MessageBundle(new HtmlParser(), [], {}, this.options.i18nOutLocale ?? null); this.compiler.xi18n(ctx); i18nExtract( - this.options.i18nOutFormat ?? null, this.options.i18nOutFile ?? null, this.host, - this.options, ctx, resolve); + this.options.i18nOutFormat ?? null, + this.options.i18nOutFile ?? null, + this.host, + this.options, + ctx, + resolve, + ); } - emit(opts?: api.EmitOptions| - undefined): ts.EmitResult { + emit( + opts?: api.EmitOptions | undefined, + ): ts.EmitResult { // Check if emission of the i18n messages bundle was requested. - if (opts !== undefined && opts.emitFlags !== undefined && - opts.emitFlags & api.EmitFlags.I18nBundle) { + if ( + opts !== undefined && + opts.emitFlags !== undefined && + opts.emitFlags & api.EmitFlags.I18nBundle + ) { this.emitXi18n(); // `api.EmitFlags` is a View Engine compiler concept. We only pay attention to the absence of @@ -270,26 +304,29 @@ export class NgtscProgram implements api.Program { const res = this.compiler.perfRecorder.inPhase(PerfPhase.TypeScriptEmit, () => { const {transformers} = this.compiler.prepareEmit(); const ignoreFiles = this.compiler.ignoreForEmit; - const emitCallback = - (opts?.emitCallback ?? defaultEmitCallback) as api.TsEmitCallback; - - const writeFile: ts.WriteFileCallback = - (fileName: string, data: string, writeByteOrderMark: boolean, - onError: ((message: string) => void)|undefined, - sourceFiles: ReadonlyArray|undefined) => { - if (sourceFiles !== undefined) { - // Record successful writes for any `ts.SourceFile` (that's not a declaration file) - // that's an input to this write. - for (const writtenSf of sourceFiles) { - if (writtenSf.isDeclarationFile) { - continue; - } - - this.compiler.incrementalCompilation.recordSuccessfulEmit(writtenSf); - } + const emitCallback = (opts?.emitCallback ?? + defaultEmitCallback) as api.TsEmitCallback; + + const writeFile: ts.WriteFileCallback = ( + fileName: string, + data: string, + writeByteOrderMark: boolean, + onError: ((message: string) => void) | undefined, + sourceFiles: ReadonlyArray | undefined, + ) => { + if (sourceFiles !== undefined) { + // Record successful writes for any `ts.SourceFile` (that's not a declaration file) + // that's an input to this write. + for (const writtenSf of sourceFiles) { + if (writtenSf.isDeclarationFile) { + continue; } - this.host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles); - }; + + this.compiler.incrementalCompilation.recordSuccessfulEmit(writtenSf); + } + } + this.host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles); + }; const customTransforms = opts && opts.customTransformers; const beforeTransforms = transformers.before || []; @@ -313,19 +350,21 @@ export class NgtscProgram implements api.Program { this.compiler.perfRecorder.eventCount(PerfEvent.EmitSourceFile); - emitResults.push(emitCallback({ - targetSourceFile, - program: this.tsProgram, - host: this.host, - options: this.options, - emitOnlyDtsFiles: false, - writeFile, - customTransformers: { - before: beforeTransforms, - after: customTransforms && customTransforms.afterTs, - afterDeclarations: afterDeclarationsTransforms, - } as any, - })); + emitResults.push( + emitCallback({ + targetSourceFile, + program: this.tsProgram, + host: this.host, + options: this.options, + emitOnlyDtsFiles: false, + writeFile, + customTransformers: { + before: beforeTransforms, + after: customTransforms && customTransforms.afterTs, + afterDeclarations: afterDeclarationsTransforms, + } as any, + }), + ); } this.compiler.perfRecorder.memory(PerfCheckpoint.Emit); @@ -339,7 +378,9 @@ export class NgtscProgram implements api.Program { if (this.options.tracePerformance !== undefined) { const perf = this.compiler.perfRecorder.finalize(); getFileSystem().writeFile( - getFileSystem().resolve(this.options.tracePerformance), JSON.stringify(perf, null, 2)); + getFileSystem().resolve(this.options.tracePerformance), + JSON.stringify(perf, null, 2), + ); } return res; } @@ -371,10 +412,15 @@ const defaultEmitCallback: api.TsEmitCallback = ({ writeFile, cancellationToken, emitOnlyDtsFiles, - customTransformers + customTransformers, }) => - program.emit( - targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers); + program.emit( + targetSourceFile, + writeFile, + cancellationToken, + emitOnlyDtsFiles, + customTransformers, + ); function mergeEmitResults(emitResults: ts.EmitResult[]): ts.EmitResult { const diagnostics: ts.Diagnostic[] = []; diff --git a/packages/compiler-cli/src/ngtsc/program_driver/src/api.ts b/packages/compiler-cli/src/ngtsc/program_driver/src/api.ts index 33d1a8dfcf4a7..920e40f88c53a 100644 --- a/packages/compiler-cli/src/ngtsc/program_driver/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/program_driver/src/api.ts @@ -19,7 +19,7 @@ export interface FileUpdate { * Represents the source file from the original program that is being updated. If the file update * targets a shim file then this is null, as shim files do not have an associated original file. */ - originalFile: ts.SourceFile|null; + originalFile: ts.SourceFile | null; } export const NgOriginalFile = Symbol('NgOriginalFile'); diff --git a/packages/compiler-cli/src/ngtsc/program_driver/src/ts_create_program_driver.ts b/packages/compiler-cli/src/ngtsc/program_driver/src/ts_create_program_driver.ts index 4cbb92e338b22..122ec236c106c 100644 --- a/packages/compiler-cli/src/ngtsc/program_driver/src/ts_create_program_driver.ts +++ b/packages/compiler-cli/src/ngtsc/program_driver/src/ts_create_program_driver.ts @@ -12,7 +12,13 @@ import {AbsoluteFsPath} from '../../file_system'; import {copyFileShimData, retagAllTsFiles, ShimReferenceTagger, untagAllTsFiles} from '../../shims'; import {RequiredDelegations, toUnredirectedSourceFile} from '../../util/src/typescript'; -import {FileUpdate, MaybeSourceFileWithOriginalFile, NgOriginalFile, ProgramDriver, UpdateMode} from './api'; +import { + FileUpdate, + MaybeSourceFileWithOriginalFile, + NgOriginalFile, + ProgramDriver, + UpdateMode, +} from './api'; /** * Delegates all methods of `ts.CompilerHost` to a delegate, with the exception of @@ -21,8 +27,10 @@ import {FileUpdate, MaybeSourceFileWithOriginalFile, NgOriginalFile, ProgramDriv * If a new method is added to `ts.CompilerHost` which is not delegated, a type error will be * generated for this class. */ -export class DelegatingCompilerHost implements - Omit, 'getSourceFile'|'fileExists'|'writeFile'> { +export class DelegatingCompilerHost + implements + Omit, 'getSourceFile' | 'fileExists' | 'writeFile'> +{ createHash; directoryExists; getCancellationToken; @@ -84,13 +92,15 @@ export class DelegatingCompilerHost implements this.getModuleResolutionCache = this.delegateMethod('getModuleResolutionCache'); this.hasInvalidatedResolutions = this.delegateMethod('hasInvalidatedResolutions'); this.resolveModuleNameLiterals = this.delegateMethod('resolveModuleNameLiterals'); - this.resolveTypeReferenceDirectiveReferences = - this.delegateMethod('resolveTypeReferenceDirectiveReferences'); + this.resolveTypeReferenceDirectiveReferences = this.delegateMethod( + 'resolveTypeReferenceDirectiveReferences', + ); } private delegateMethod(name: M): ts.CompilerHost[M] { - return this.delegate[name] !== undefined ? (this.delegate[name] as any).bind(this.delegate) : - undefined; + return this.delegate[name] !== undefined + ? (this.delegate[name] as any).bind(this.delegate) + : undefined; } } @@ -116,25 +126,34 @@ class UpdatedProgramHost extends DelegatingCompilerHost { private shimTagger: ShimReferenceTagger; constructor( - sfMap: Map, private originalProgram: ts.Program, - delegate: ts.CompilerHost, private shimExtensionPrefixes: string[]) { + sfMap: Map, + private originalProgram: ts.Program, + delegate: ts.CompilerHost, + private shimExtensionPrefixes: string[], + ) { super(delegate); this.shimTagger = new ShimReferenceTagger(this.shimExtensionPrefixes); this.sfMap = sfMap; } getSourceFile( - fileName: string, languageVersionOrOptions: ts.ScriptTarget|ts.CreateSourceFileOptions, - onError?: ((message: string) => void)|undefined, - shouldCreateNewSourceFile?: boolean|undefined): ts.SourceFile|undefined { + fileName: string, + languageVersionOrOptions: ts.ScriptTarget | ts.CreateSourceFileOptions, + onError?: ((message: string) => void) | undefined, + shouldCreateNewSourceFile?: boolean | undefined, + ): ts.SourceFile | undefined { // Try to use the same `ts.SourceFile` as the original program, if possible. This guarantees // that program reuse will be as efficient as possible. - let delegateSf: ts.SourceFile|undefined = this.originalProgram.getSourceFile(fileName); + let delegateSf: ts.SourceFile | undefined = this.originalProgram.getSourceFile(fileName); if (delegateSf === undefined) { // Something went wrong and a source file is being requested that's not in the original // program. Just in case, try to retrieve it from the delegate. delegateSf = this.delegate.getSourceFile( - fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile)!; + fileName, + languageVersionOrOptions, + onError, + shouldCreateNewSourceFile, + )!; } if (delegateSf === undefined) { return undefined; @@ -169,7 +188,6 @@ class UpdatedProgramHost extends DelegatingCompilerHost { } } - /** * Updates a `ts.Program` instance with a new one that incorporates specific changes, using the * TypeScript compiler APIs for incremental program creation. @@ -186,8 +204,11 @@ export class TsCreateProgramDriver implements ProgramDriver { private program: ts.Program; constructor( - private originalProgram: ts.Program, private originalHost: ts.CompilerHost, - private options: ts.CompilerOptions, private shimExtensionPrefixes: string[]) { + private originalProgram: ts.Program, + private originalHost: ts.CompilerHost, + private options: ts.CompilerOptions, + private shimExtensionPrefixes: string[], + ) { this.program = this.originalProgram; } @@ -222,7 +243,11 @@ export class TsCreateProgramDriver implements ProgramDriver { } const host = new UpdatedProgramHost( - this.sfMap, this.originalProgram, this.originalHost, this.shimExtensionPrefixes); + this.sfMap, + this.originalProgram, + this.originalHost, + this.shimExtensionPrefixes, + ); const oldProgram = this.program; // Retag the old program's `ts.SourceFile`s with shim tags, to allow TypeScript to reuse the diff --git a/packages/compiler-cli/src/ngtsc/reflection/index.ts b/packages/compiler-cli/src/ngtsc/reflection/index.ts index 9899f8ca69b11..e7a10bd0a6360 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/index.ts +++ b/packages/compiler-cli/src/ngtsc/reflection/index.ts @@ -8,5 +8,16 @@ export * from './src/host'; export {typeNodeToValueExpr, entityNameToValue} from './src/type_to_value'; -export {TypeScriptReflectionHost, filterToMembersWithDecorator, reflectIdentifierOfDeclaration, reflectNameOfDeclaration, reflectObjectLiteral, reflectTypeEntityToDeclaration} from './src/typescript'; -export {isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration} from './src/util'; +export { + TypeScriptReflectionHost, + filterToMembersWithDecorator, + reflectIdentifierOfDeclaration, + reflectNameOfDeclaration, + reflectObjectLiteral, + reflectTypeEntityToDeclaration, +} from './src/typescript'; +export { + isNamedClassDeclaration, + isNamedFunctionDeclaration, + isNamedVariableDeclaration, +} from './src/util'; diff --git a/packages/compiler-cli/src/ngtsc/reflection/src/host.ts b/packages/compiler-cli/src/ngtsc/reflection/src/host.ts index 0b8a76182458f..5d3e0967bb281 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/src/host.ts +++ b/packages/compiler-cli/src/ngtsc/reflection/src/host.ts @@ -32,7 +32,7 @@ export interface Decorator { * Note: this field is declared using computed property syntax to work around a clang-format bug * that resulted in inconsistent indentation of this comment block. */ - ['import']: Import|null; + ['import']: Import | null; /** * TypeScript reference to the decorator itself. @@ -43,22 +43,25 @@ export interface Decorator { * Arguments of the invocation of the decorator, if the decorator is invoked, or `null` * otherwise. */ - args: ts.Expression[]|null; + args: ts.Expression[] | null; } /** * A decorator is identified by either a simple identifier (e.g. `Decorator`) or, in some cases, * a namespaced property access (e.g. `core.Decorator`). */ -export type DecoratorIdentifier = ts.Identifier|NamespacedIdentifier; -export type NamespacedIdentifier = ts.PropertyAccessExpression&{ +export type DecoratorIdentifier = ts.Identifier | NamespacedIdentifier; +export type NamespacedIdentifier = ts.PropertyAccessExpression & { expression: ts.Identifier; - name: ts.Identifier + name: ts.Identifier; }; export function isDecoratorIdentifier(exp: ts.Expression): exp is DecoratorIdentifier { - return ts.isIdentifier(exp) || - ts.isPropertyAccessExpression(exp) && ts.isIdentifier(exp.expression) && - ts.isIdentifier(exp.name); + return ( + ts.isIdentifier(exp) || + (ts.isPropertyAccessExpression(exp) && + ts.isIdentifier(exp.expression) && + ts.isIdentifier(exp.name)) + ); } /** @@ -76,7 +79,9 @@ export function isDecoratorIdentifier(exp: ts.Expression): exp is DecoratorIdent * For `ReflectionHost` purposes, a class declaration should always have a `name` identifier, * because we need to be able to reference it in other parts of the program. */ -export type ClassDeclaration = T&{name: ts.Identifier}; +export type ClassDeclaration = T & { + name: ts.Identifier; +}; /** * An enumeration of possible kinds of class members. @@ -105,7 +110,7 @@ export interface ClassMember { /** * TypeScript reference to the class member itself, or null if it is not applicable. */ - node: ts.Node|null; + node: ts.Node | null; /** * Indication of which type of member this is (property, method, etc). @@ -119,7 +124,7 @@ export interface ClassMember { * TypeScript `ts.TypeNode` representing the type of the member, or `null` if not present or * applicable. */ - type: ts.TypeNode|null; + type: ts.TypeNode | null; /** * Name of the class member. @@ -133,7 +138,7 @@ export interface ClassMember { * The `nameNode` is useful in writing references to this member that will be correctly source- * mapped back to the original file. */ - nameNode: ts.Identifier|ts.PrivateIdentifier|ts.StringLiteral|null; + nameNode: ts.Identifier | ts.PrivateIdentifier | ts.StringLiteral | null; /** * TypeScript `ts.Expression` which represents the value of the member. @@ -141,7 +146,7 @@ export interface ClassMember { * If the member is a property, this will be the property initializer if there is one, or null * otherwise. */ - value: ts.Expression|null; + value: ts.Expression | null; /** * TypeScript `ts.Declaration` which represents the implementation of the member. @@ -186,7 +191,7 @@ export interface ClassMember { * }, * ``` */ - implementation: ts.Declaration|null; + implementation: ts.Declaration | null; /** * Whether the member is static or not. @@ -196,7 +201,7 @@ export interface ClassMember { /** * Any `Decorator`s which are present on the member, or `null` if none are present. */ - decorators: Decorator[]|null; + decorators: Decorator[] | null; } export const enum TypeValueReferenceKind { @@ -222,7 +227,7 @@ export interface LocalTypeValueReference { * to track its usages, preventing the import from being elided if it was originally only used in * a type-position. See `DefaultImportTracker` for details. */ - defaultImportStatement: ts.ImportDeclaration|null; + defaultImportStatement: ts.ImportDeclaration | null; } /** @@ -248,10 +253,10 @@ export interface ImportedTypeValueReference { * If present, represents the symbol names that are referenced from the top-level import. * When `null` or empty, the `importedName` itself is the symbol being referenced. */ - nestedPath: string[]|null; + nestedPath: string[] | null; // This field can be null in local compilation mode when resolving is not possible. - valueDeclaration: DeclarationNode|null; + valueDeclaration: DeclarationNode | null; } /** @@ -303,7 +308,6 @@ export const enum ValueUnavailableKind { UNSUPPORTED, } - export interface UnsupportedType { kind: ValueUnavailableKind.UNSUPPORTED; typeNode: ts.TypeNode; @@ -312,13 +316,13 @@ export interface UnsupportedType { export interface NoValueDeclaration { kind: ValueUnavailableKind.NO_VALUE_DECLARATION; typeNode: ts.TypeNode; - decl: ts.Declaration|null; + decl: ts.Declaration | null; } export interface TypeOnlyImport { kind: ValueUnavailableKind.TYPE_ONLY_IMPORT; typeNode: ts.TypeNode; - node: ts.ImportClause|ts.ImportSpecifier; + node: ts.ImportClause | ts.ImportSpecifier; } export interface NamespaceImport { @@ -340,7 +344,12 @@ export interface MissingType { * The various reasons why a type node may not be referred to as a value. */ export type UnavailableValue = - UnsupportedType|NoValueDeclaration|TypeOnlyImport|NamespaceImport|UnknownReference|MissingType; + | UnsupportedType + | NoValueDeclaration + | TypeOnlyImport + | NamespaceImport + | UnknownReference + | MissingType; /** * A reference to a value that originated from a type position. @@ -352,7 +361,9 @@ export type UnavailableValue = * See the individual types for additional information. */ export type TypeValueReference = - LocalTypeValueReference|ImportedTypeValueReference|UnavailableTypeValueReference; + | LocalTypeValueReference + | ImportedTypeValueReference + | UnavailableTypeValueReference; /** * A parameter to a constructor. @@ -364,7 +375,7 @@ export interface CtorParameter { * Some parameters don't have a simple string name (for example, parameters which are destructured * into multiple variables). In these cases, `name` can be `null`. */ - name: string|null; + name: string | null; /** * TypeScript `ts.BindingName` representing the name of the parameter. @@ -390,12 +401,12 @@ export interface CtorParameter { * * Can be null, if the param has no type declared. */ - typeNode: ts.TypeNode|null; + typeNode: ts.TypeNode | null; /** * Any `Decorator`s which are present on the parameter, or `null` if none are present. */ - decorators: Decorator[]|null; + decorators: Decorator[] | null; } /** @@ -409,8 +420,12 @@ export interface FunctionDefinition { /** * A reference to the node which declares the function. */ - node: ts.MethodDeclaration|ts.FunctionDeclaration|ts.FunctionExpression|ts.VariableDeclaration| - ts.ArrowFunction; + node: + | ts.MethodDeclaration + | ts.FunctionDeclaration + | ts.FunctionExpression + | ts.VariableDeclaration + | ts.ArrowFunction; /** * Statements of the function body, if a body is present, or null if no body is present or the @@ -420,7 +435,7 @@ export interface FunctionDefinition { * This list may have been filtered to exclude statements which perform parameter default value * initialization. */ - body: ts.Statement[]|null; + body: ts.Statement[] | null; /** * Metadata regarding the function's parameters, including possible default value expressions. @@ -430,7 +445,7 @@ export interface FunctionDefinition { /** * Generic type parameters of the function. */ - typeParameters: ts.TypeParameterDeclaration[]|null; + typeParameters: ts.TypeParameterDeclaration[] | null; /** * Number of known signatures of the function. @@ -445,7 +460,7 @@ export interface Parameter { /** * Name of the parameter, if available. */ - name: string|null; + name: string | null; /** * Declaration which created this parameter. @@ -455,12 +470,12 @@ export interface Parameter { /** * Expression which represents the default value of the parameter, if any. */ - initializer: ts.Expression|null; + initializer: ts.Expression | null; /** * Type of the parameter. */ - type: ts.TypeNode|null; + type: ts.TypeNode | null; } /** @@ -492,7 +507,7 @@ export interface Import { export type DeclarationNode = ts.Declaration; export type AmbientImport = { - __brand: 'AmbientImport' + __brand: 'AmbientImport'; }; /** Indicates that a declaration is referenced through an ambient type. */ @@ -508,7 +523,7 @@ export interface Declaration { * was imported via an absolute module (even through a chain of re-exports). If the symbol is part * of the application and was not imported from an absolute path, this will be `null`. */ - viaModule: string|AmbientImport|null; + viaModule: string | AmbientImport | null; /** * TypeScript reference to the declaration itself, if one exists. @@ -541,7 +556,7 @@ export interface ReflectionHost { * @returns an array of `Decorator` metadata if decorators are present on the declaration, or * `null` if either no decorators were present or if the declaration is not of a decoratable type. */ - getDecoratorsOfDeclaration(declaration: DeclarationNode): Decorator[]|null; + getDecoratorsOfDeclaration(declaration: DeclarationNode): Decorator[] | null; /** * Examine a declaration which should be of a class, and return metadata about the members of the @@ -567,7 +582,7 @@ export interface ReflectionHost { * a constructor exists. If the constructor exists and has 0 parameters, this array will be empty. * If the class has no constructor, this method returns `null`. */ - getConstructorParameters(clazz: ClassDeclaration): CtorParameter[]|null; + getConstructorParameters(clazz: ClassDeclaration): CtorParameter[] | null; /** * Reflect over a function and return metadata about its parameters and body. @@ -589,7 +604,7 @@ export interface ReflectionHost { * * @returns a `FunctionDefinition` giving metadata about the function definition. */ - getDefinitionOfFunction(fn: ts.Node): FunctionDefinition|null; + getDefinitionOfFunction(fn: ts.Node): FunctionDefinition | null; /** * Determine if an identifier was imported from another module and return `Import` metadata @@ -600,7 +615,7 @@ export interface ReflectionHost { * @returns metadata about the `Import` if the identifier was imported from another module, or * `null` if the identifier doesn't resolve to an import but instead is locally defined. */ - getImportOfIdentifier(id: ts.Identifier): Import|null; + getImportOfIdentifier(id: ts.Identifier): Import | null; /** * Trace an identifier to its declaration, if possible. @@ -634,7 +649,7 @@ export interface ReflectionHost { * @returns metadata about the `Declaration` if the original declaration is found, or `null` * otherwise. */ - getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null; + getDeclarationOfIdentifier(id: ts.Identifier): Declaration | null; /** * Collect the declarations exported from a module by name. @@ -648,7 +663,7 @@ export interface ReflectionHost { * * @returns a map of `Declaration`s for the module's exports, by name. */ - getExportsOfModule(module: ts.Node): Map|null; + getExportsOfModule(module: ts.Node): Map | null; /** * Check whether the given node actually represents a class. @@ -670,7 +685,7 @@ export interface ReflectionHost { * * @param clazz the class whose base we want to get. */ - getBaseClassExpression(clazz: ClassDeclaration): ts.Expression|null; + getBaseClassExpression(clazz: ClassDeclaration): ts.Expression | null; /** * Get the number of generic type parameters of a given class. @@ -680,7 +695,7 @@ export interface ReflectionHost { * @returns the number of type parameters of the class, if known, or `null` if the declaration * is not a class or has an unknown number of type parameters. */ - getGenericArityOfClass(clazz: ClassDeclaration): number|null; + getGenericArityOfClass(clazz: ClassDeclaration): number | null; /** * Find the assigned value of a variable declaration. @@ -692,7 +707,7 @@ export interface ReflectionHost { * @returns the value of the variable, as a TypeScript expression node, or `undefined` * if the value cannot be computed. */ - getVariableValue(declaration: ts.VariableDeclaration): ts.Expression|null; + getVariableValue(declaration: ts.VariableDeclaration): ts.Expression | null; /** * Returns `true` if a declaration is exported from the module in which it's defined. diff --git a/packages/compiler-cli/src/ngtsc/reflection/src/type_to_value.ts b/packages/compiler-cli/src/ngtsc/reflection/src/type_to_value.ts index bbb56bf4fbf2e..220368efc6f27 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/src/type_to_value.ts +++ b/packages/compiler-cli/src/ngtsc/reflection/src/type_to_value.ts @@ -8,7 +8,12 @@ import ts from 'typescript'; -import {TypeValueReference, TypeValueReferenceKind, UnavailableTypeValueReference, ValueUnavailableKind} from './host'; +import { + TypeValueReference, + TypeValueReferenceKind, + UnavailableTypeValueReference, + ValueUnavailableKind, +} from './host'; /** * Potentially convert a `ts.TypeNode` to a `TypeValueReference`, which indicates how to use the @@ -18,8 +23,10 @@ import {TypeValueReference, TypeValueReferenceKind, UnavailableTypeValueReferenc * declaration, or if it is not possible to statically understand. */ export function typeToValue( - typeNode: ts.TypeNode|null, checker: ts.TypeChecker, - isLocalCompilation: boolean): TypeValueReference { + typeNode: ts.TypeNode | null, + checker: ts.TypeChecker, + isLocalCompilation: boolean, +): TypeValueReference { // It's not possible to get a value expression if the parameter doesn't even have a type. if (typeNode === null) { return missingType(); @@ -40,17 +47,22 @@ export function typeToValue( // has a value declaration associated with it. Note that const enums are an exception, // because while they do have a value declaration, they don't exist at runtime. if (decl.valueDeclaration === undefined || decl.flags & ts.SymbolFlags.ConstEnum) { - let typeOnlyDecl: ts.Declaration|null = null; + let typeOnlyDecl: ts.Declaration | null = null; if (decl.declarations !== undefined && decl.declarations.length > 0) { typeOnlyDecl = decl.declarations[0]; } // In local compilation mode a declaration is considered invalid only if it is a type related // declaration. - if (!isLocalCompilation || (typeOnlyDecl && [ - ts.SyntaxKind.TypeParameter, ts.SyntaxKind.TypeAliasDeclaration, - ts.SyntaxKind.InterfaceDeclaration - ].includes(typeOnlyDecl.kind))) { + if ( + !isLocalCompilation || + (typeOnlyDecl && + [ + ts.SyntaxKind.TypeParameter, + ts.SyntaxKind.TypeAliasDeclaration, + ts.SyntaxKind.InterfaceDeclaration, + ].includes(typeOnlyDecl.kind)) + ) { return noValueDeclaration(typeNode, typeOnlyDecl); } } @@ -107,7 +119,7 @@ export function typeToValue( valueDeclaration: decl.valueDeclaration ?? null, moduleName, importedName, - nestedPath + nestedPath, }; } else if (ts.isNamespaceImport(firstDecl)) { // The import is a namespace import @@ -134,7 +146,7 @@ export function typeToValue( valueDeclaration: decl.valueDeclaration ?? null, moduleName, importedName, - nestedPath + nestedPath, }; } } @@ -160,15 +172,19 @@ function unsupportedType(typeNode: ts.TypeNode): UnavailableTypeValueReference { } function noValueDeclaration( - typeNode: ts.TypeNode, decl: ts.Declaration|null): UnavailableTypeValueReference { + typeNode: ts.TypeNode, + decl: ts.Declaration | null, +): UnavailableTypeValueReference { return { kind: TypeValueReferenceKind.UNAVAILABLE, reason: {kind: ValueUnavailableKind.NO_VALUE_DECLARATION, typeNode, decl}, }; } -function typeOnlyImport(typeNode: ts.TypeNode, node: ts.ImportClause|ts.ImportSpecifier): - UnavailableTypeValueReference { +function typeOnlyImport( + typeNode: ts.TypeNode, + node: ts.ImportClause | ts.ImportSpecifier, +): UnavailableTypeValueReference { return { kind: TypeValueReferenceKind.UNAVAILABLE, reason: {kind: ValueUnavailableKind.TYPE_ONLY_IMPORT, typeNode, node}, @@ -183,7 +199,9 @@ function unknownReference(typeNode: ts.TypeNode): UnavailableTypeValueReference } function namespaceImport( - typeNode: ts.TypeNode, importClause: ts.ImportClause): UnavailableTypeValueReference { + typeNode: ts.TypeNode, + importClause: ts.ImportClause, +): UnavailableTypeValueReference { return { kind: TypeValueReferenceKind.UNAVAILABLE, reason: {kind: ValueUnavailableKind.NAMESPACE, typeNode, importClause}, @@ -203,7 +221,7 @@ function missingType(): UnavailableTypeValueReference { * * This will return `null` if an equivalent expression cannot be constructed. */ -export function typeNodeToValueExpr(node: ts.TypeNode): ts.Expression|null { +export function typeNodeToValueExpr(node: ts.TypeNode): ts.Expression | null { if (ts.isTypeReferenceNode(node)) { return entityNameToValue(node.typeName); } else { @@ -222,11 +240,13 @@ export function typeNodeToValueExpr(node: ts.TypeNode): ts.Expression|null { * All symbol names that make up the type reference are returned left-to-right into the * `symbolNames` array, which is guaranteed to include at least one entry. */ -function resolveTypeSymbols(typeRef: ts.TypeReferenceNode, checker: ts.TypeChecker): - {local: ts.Symbol, decl: ts.Symbol, symbolNames: string[]}|null { +function resolveTypeSymbols( + typeRef: ts.TypeReferenceNode, + checker: ts.TypeChecker, +): {local: ts.Symbol; decl: ts.Symbol; symbolNames: string[]} | null { const typeName = typeRef.typeName; // typeRefSymbol is the ts.Symbol of the entire type reference. - const typeRefSymbol: ts.Symbol|undefined = checker.getSymbolAtLocation(typeName); + const typeRefSymbol: ts.Symbol | undefined = checker.getSymbolAtLocation(typeName); if (typeRefSymbol === undefined) { return null; } @@ -272,7 +292,7 @@ function resolveTypeSymbols(typeRef: ts.TypeReferenceNode, checker: ts.TypeCheck return {local, decl, symbolNames}; } -export function entityNameToValue(node: ts.EntityName): ts.Expression|null { +export function entityNameToValue(node: ts.EntityName): ts.Expression | null { if (ts.isQualifiedName(node)) { const left = entityNameToValue(node.left); return left !== null ? ts.factory.createPropertyAccessExpression(left, node.right) : null; diff --git a/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts b/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts index d6e95bb93e078..443c77c0c4dd8 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts +++ b/packages/compiler-cli/src/ngtsc/reflection/src/typescript.ts @@ -8,7 +8,21 @@ import ts from 'typescript'; -import {AmbientImport, ClassDeclaration, ClassMember, ClassMemberAccessLevel, ClassMemberKind, CtorParameter, Declaration, DeclarationNode, Decorator, FunctionDefinition, Import, isDecoratorIdentifier, ReflectionHost} from './host'; +import { + AmbientImport, + ClassDeclaration, + ClassMember, + ClassMemberAccessLevel, + ClassMemberKind, + CtorParameter, + Declaration, + DeclarationNode, + Decorator, + FunctionDefinition, + Import, + isDecoratorIdentifier, + ReflectionHost, +} from './host'; import {typeToValue} from './type_to_value'; import {isNamedClassDeclaration} from './util'; @@ -17,35 +31,40 @@ import {isNamedClassDeclaration} from './util'; */ export class TypeScriptReflectionHost implements ReflectionHost { - constructor(protected checker: ts.TypeChecker, private readonly isLocalCompilation = false) {} + constructor( + protected checker: ts.TypeChecker, + private readonly isLocalCompilation = false, + ) {} - getDecoratorsOfDeclaration(declaration: DeclarationNode): Decorator[]|null { - const decorators = - ts.canHaveDecorators(declaration) ? ts.getDecorators(declaration) : undefined; + getDecoratorsOfDeclaration(declaration: DeclarationNode): Decorator[] | null { + const decorators = ts.canHaveDecorators(declaration) + ? ts.getDecorators(declaration) + : undefined; - return decorators !== undefined && decorators.length ? - decorators.map(decorator => this._reflectDecorator(decorator)) - .filter((dec): dec is Decorator => dec !== null) : - null; + return decorators !== undefined && decorators.length + ? decorators + .map((decorator) => this._reflectDecorator(decorator)) + .filter((dec): dec is Decorator => dec !== null) + : null; } getMembersOfClass(clazz: ClassDeclaration): ClassMember[] { const tsClazz = castDeclarationToClassOrDie(clazz); return tsClazz.members - .map(member => { - const result = reflectClassMember(member); - if (result === null) { - return null; - } - return { - ...result, - decorators: this.getDecoratorsOfDeclaration(member), - }; - }) - .filter((member): member is NonNullable => member !== null); - } - - getConstructorParameters(clazz: ClassDeclaration): CtorParameter[]|null { + .map((member) => { + const result = reflectClassMember(member); + if (result === null) { + return null; + } + return { + ...result, + decorators: this.getDecoratorsOfDeclaration(member), + }; + }) + .filter((member): member is NonNullable => member !== null); + } + + getConstructorParameters(clazz: ClassDeclaration): CtorParameter[] | null { const tsClazz = castDeclarationToClassOrDie(clazz); const isDeclaration = tsClazz.getSourceFile().isDeclarationFile; @@ -54,13 +73,14 @@ export class TypeScriptReflectionHost implements ReflectionHost { // be executed and which can have decorators. For declaration files, we take the first one that // we get. const ctor = tsClazz.members.find( - (member): member is ts.ConstructorDeclaration => - ts.isConstructorDeclaration(member) && (isDeclaration || member.body !== undefined)); + (member): member is ts.ConstructorDeclaration => + ts.isConstructorDeclaration(member) && (isDeclaration || member.body !== undefined), + ); if (ctor === undefined) { return null; } - return ctor.parameters.map(node => { + return ctor.parameters.map((node) => { // The name of the parameter is easy. const name = parameterName(node.name); @@ -78,9 +98,12 @@ export class TypeScriptReflectionHost implements ReflectionHost { // optional tokes that don't have providers. if (typeNode && ts.isUnionTypeNode(typeNode)) { let childTypeNodes = typeNode.types.filter( - childTypeNode => - !(ts.isLiteralTypeNode(childTypeNode) && - childTypeNode.literal.kind === ts.SyntaxKind.NullKeyword)); + (childTypeNode) => + !( + ts.isLiteralTypeNode(childTypeNode) && + childTypeNode.literal.kind === ts.SyntaxKind.NullKeyword + ), + ); if (childTypeNodes.length === 1) { typeNode = childTypeNodes[0]; @@ -99,7 +122,7 @@ export class TypeScriptReflectionHost implements ReflectionHost { }); } - getImportOfIdentifier(id: ts.Identifier): Import|null { + getImportOfIdentifier(id: ts.Identifier): Import | null { const directImport = this.getDirectImportOfIdentifier(id); if (directImport !== null) { return directImport; @@ -112,7 +135,7 @@ export class TypeScriptReflectionHost implements ReflectionHost { } } - getExportsOfModule(node: ts.Node): Map|null { + getExportsOfModule(node: ts.Node): Map | null { // In TypeScript code, modules are only ts.SourceFiles. Throw if the node isn't a module. if (!ts.isSourceFile(node)) { throw new Error(`getExportsOfModule() called on non-SourceFile in TS code`); @@ -126,7 +149,7 @@ export class TypeScriptReflectionHost implements ReflectionHost { } const map = new Map(); - this.checker.getExportsOfModule(symbol).forEach(exportSymbol => { + this.checker.getExportsOfModule(symbol).forEach((exportSymbol) => { // Map each exported Symbol to a Declaration and add it to the map. const decl = this.getDeclarationOfSymbol(exportSymbol, null); if (decl !== null) { @@ -146,13 +169,16 @@ export class TypeScriptReflectionHost implements ReflectionHost { return this.getBaseClassExpression(clazz) !== null; } - getBaseClassExpression(clazz: ClassDeclaration): ts.Expression|null { - if (!(ts.isClassDeclaration(clazz) || ts.isClassExpression(clazz)) || - clazz.heritageClauses === undefined) { + getBaseClassExpression(clazz: ClassDeclaration): ts.Expression | null { + if ( + !(ts.isClassDeclaration(clazz) || ts.isClassExpression(clazz)) || + clazz.heritageClauses === undefined + ) { return null; } - const extendsClause = - clazz.heritageClauses.find(clause => clause.token === ts.SyntaxKind.ExtendsKeyword); + const extendsClause = clazz.heritageClauses.find( + (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, + ); if (extendsClause === undefined) { return null; } @@ -163,27 +189,32 @@ export class TypeScriptReflectionHost implements ReflectionHost { return extendsType.expression; } - getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null { + getDeclarationOfIdentifier(id: ts.Identifier): Declaration | null { // Resolve the identifier to a Symbol, and return the declaration of that. - let symbol: ts.Symbol|undefined = this.checker.getSymbolAtLocation(id); + let symbol: ts.Symbol | undefined = this.checker.getSymbolAtLocation(id); if (symbol === undefined) { return null; } return this.getDeclarationOfSymbol(symbol, id); } - getDefinitionOfFunction(node: ts.Node): FunctionDefinition|null { - if (!ts.isFunctionDeclaration(node) && !ts.isMethodDeclaration(node) && - !ts.isFunctionExpression(node) && !ts.isArrowFunction(node)) { + getDefinitionOfFunction(node: ts.Node): FunctionDefinition | null { + if ( + !ts.isFunctionDeclaration(node) && + !ts.isMethodDeclaration(node) && + !ts.isFunctionExpression(node) && + !ts.isArrowFunction(node) + ) { return null; } - let body: ts.Statement[]|null = null; + let body: ts.Statement[] | null = null; if (node.body !== undefined) { // The body might be an expression if the node is an arrow function. - body = ts.isBlock(node.body) ? Array.from(node.body.statements) : - [ts.factory.createReturnStatement(node.body)]; + body = ts.isBlock(node.body) + ? Array.from(node.body.statements) + : [ts.factory.createReturnStatement(node.body)]; } const type = this.checker.getTypeAtLocation(node); @@ -194,7 +225,7 @@ export class TypeScriptReflectionHost implements ReflectionHost { body, signatureCount: signatures.length, typeParameters: node.typeParameters === undefined ? null : Array.from(node.typeParameters), - parameters: node.parameters.map(param => { + parameters: node.parameters.map((param) => { const name = parameterName(param.name); const initializer = param.initializer || null; return {name, node: param, initializer, type: param.type || null}; @@ -202,14 +233,14 @@ export class TypeScriptReflectionHost implements ReflectionHost { }; } - getGenericArityOfClass(clazz: ClassDeclaration): number|null { + getGenericArityOfClass(clazz: ClassDeclaration): number | null { if (!ts.isClassDeclaration(clazz)) { return null; } return clazz.typeParameters !== undefined ? clazz.typeParameters.length : 0; } - getVariableValue(declaration: ts.VariableDeclaration): ts.Expression|null { + getVariableValue(declaration: ts.VariableDeclaration): ts.Expression | null { return declaration.initializer || null; } @@ -220,8 +251,10 @@ export class TypeScriptReflectionHost implements ReflectionHost { topLevel = decl.parent.parent; } const modifiers = ts.canHaveModifiers(topLevel) ? ts.getModifiers(topLevel) : undefined; - if (modifiers !== undefined && - modifiers.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) { + if ( + modifiers !== undefined && + modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) + ) { // The node is part of a declaration that's directly exported. return true; } @@ -243,11 +276,14 @@ export class TypeScriptReflectionHost implements ReflectionHost { return localExports.has(decl as ts.Declaration); } - protected getDirectImportOfIdentifier(id: ts.Identifier): Import|null { + protected getDirectImportOfIdentifier(id: ts.Identifier): Import | null { const symbol = this.checker.getSymbolAtLocation(id); - if (symbol === undefined || symbol.declarations === undefined || - symbol.declarations.length !== 1) { + if ( + symbol === undefined || + symbol.declarations === undefined || + symbol.declarations.length !== 1 + ) { return null; } @@ -291,7 +327,9 @@ export class TypeScriptReflectionHost implements ReflectionHost { * @returns The import info if this is a namespaced import or `null`. */ protected getImportOfNamespacedIdentifier( - id: ts.Identifier, namespaceIdentifier: ts.Identifier|null): Import|null { + id: ts.Identifier, + namespaceIdentifier: ts.Identifier | null, + ): Import | null { if (namespaceIdentifier === null) { return null; } @@ -300,7 +338,7 @@ export class TypeScriptReflectionHost implements ReflectionHost { return null; } const declaration = - namespaceSymbol.declarations.length === 1 ? namespaceSymbol.declarations[0] : null; + namespaceSymbol.declarations.length === 1 ? namespaceSymbol.declarations[0] : null; if (!declaration) { return null; } @@ -325,10 +363,12 @@ export class TypeScriptReflectionHost implements ReflectionHost { /** * Resolve a `ts.Symbol` to its declaration, keeping track of the `viaModule` along the way. */ - protected getDeclarationOfSymbol(symbol: ts.Symbol, originalId: ts.Identifier|null): Declaration - |null { + protected getDeclarationOfSymbol( + symbol: ts.Symbol, + originalId: ts.Identifier | null, + ): Declaration | null { // If the symbol points to a ShorthandPropertyAssignment, resolve it. - let valueDeclaration: ts.Declaration|undefined = undefined; + let valueDeclaration: ts.Declaration | undefined = undefined; if (symbol.valueDeclaration !== undefined) { valueDeclaration = symbol.valueDeclaration; } else if (symbol.declarations !== undefined && symbol.declarations.length > 0) { @@ -372,12 +412,12 @@ export class TypeScriptReflectionHost implements ReflectionHost { } } - private _reflectDecorator(node: ts.Decorator): Decorator|null { + private _reflectDecorator(node: ts.Decorator): Decorator | null { // Attempt to resolve the decorator expression into a reference to a concrete Identifier. The // expression may contain a call to a function which returns the decorator function, in which // case we want to return the arguments. let decoratorExpr: ts.Expression = node.expression; - let args: ts.Expression[]|null = null; + let args: ts.Expression[] | null = null; // Check for call expressions. if (ts.isCallExpression(decoratorExpr)) { @@ -443,8 +483,10 @@ export class TypeScriptReflectionHost implements ReflectionHost { exportedSymbol = this.checker.getAliasedSymbol(exportedSymbol); } - if (exportedSymbol.valueDeclaration !== undefined && - exportedSymbol.valueDeclaration.getSourceFile() === file) { + if ( + exportedSymbol.valueDeclaration !== undefined && + exportedSymbol.valueDeclaration.getSourceFile() === file + ) { exportSet.add(exportedSymbol.valueDeclaration); } item = iter.next(); @@ -454,25 +496,30 @@ export class TypeScriptReflectionHost implements ReflectionHost { } private _viaModule( - declaration: ts.Declaration, originalId: ts.Identifier|null, importInfo: Import|null): string - |AmbientImport|null { - if (importInfo === null && originalId !== null && - declaration.getSourceFile() !== originalId.getSourceFile()) { + declaration: ts.Declaration, + originalId: ts.Identifier | null, + importInfo: Import | null, + ): string | AmbientImport | null { + if ( + importInfo === null && + originalId !== null && + declaration.getSourceFile() !== originalId.getSourceFile() + ) { return AmbientImport; } - return importInfo !== null && importInfo.from !== null && !importInfo.from.startsWith('.') ? - importInfo.from : - null; + return importInfo !== null && importInfo.from !== null && !importInfo.from.startsWith('.') + ? importInfo.from + : null; } } -export function reflectNameOfDeclaration(decl: ts.Declaration): string|null { +export function reflectNameOfDeclaration(decl: ts.Declaration): string | null { const id = reflectIdentifierOfDeclaration(decl); - return id && id.text || null; + return (id && id.text) || null; } -export function reflectIdentifierOfDeclaration(decl: ts.Declaration): ts.Identifier|null { +export function reflectIdentifierOfDeclaration(decl: ts.Declaration): ts.Identifier | null { if (ts.isClassDeclaration(decl) || ts.isFunctionDeclaration(decl)) { return decl.name || null; } else if (ts.isVariableDeclaration(decl)) { @@ -484,7 +531,9 @@ export function reflectIdentifierOfDeclaration(decl: ts.Declaration): ts.Identif } export function reflectTypeEntityToDeclaration( - type: ts.EntityName, checker: ts.TypeChecker): {node: ts.Declaration, from: string|null} { + type: ts.EntityName, + checker: ts.TypeChecker, +): {node: ts.Declaration; from: string | null} { let realSymbol = checker.getSymbolAtLocation(type); if (realSymbol === undefined) { throw new Error(`Cannot resolve type entity ${type.getText()} to symbol`); @@ -493,7 +542,7 @@ export function reflectTypeEntityToDeclaration( realSymbol = checker.getAliasedSymbol(realSymbol); } - let node: ts.Declaration|null = null; + let node: ts.Declaration | null = null; if (realSymbol.valueDeclaration !== undefined) { node = realSymbol.valueDeclaration; } else if (realSymbol.declarations !== undefined && realSymbol.declarations.length === 1) { @@ -507,8 +556,11 @@ export function reflectTypeEntityToDeclaration( throw new Error(`Cannot handle qualified name with non-identifier lhs`); } const symbol = checker.getSymbolAtLocation(type.left); - if (symbol === undefined || symbol.declarations === undefined || - symbol.declarations.length !== 1) { + if ( + symbol === undefined || + symbol.declarations === undefined || + symbol.declarations.length !== 1 + ) { throw new Error(`Cannot resolve qualified type entity lhs to symbol`); } const decl = symbol.declarations[0]; @@ -529,33 +581,39 @@ export function reflectTypeEntityToDeclaration( } } -export function filterToMembersWithDecorator(members: ClassMember[], name: string, module?: string): - {member: ClassMember, decorators: Decorator[]}[] { - return members.filter(member => !member.isStatic) - .map(member => { - if (member.decorators === null) { - return null; - } - - const decorators = member.decorators.filter(dec => { - if (dec.import !== null) { - return dec.import.name === name && (module === undefined || dec.import.from === module); - } else { - return dec.name === name && module === undefined; - } - }); +export function filterToMembersWithDecorator( + members: ClassMember[], + name: string, + module?: string, +): {member: ClassMember; decorators: Decorator[]}[] { + return members + .filter((member) => !member.isStatic) + .map((member) => { + if (member.decorators === null) { + return null; + } - if (decorators.length === 0) { - return null; + const decorators = member.decorators.filter((dec) => { + if (dec.import !== null) { + return dec.import.name === name && (module === undefined || dec.import.from === module); + } else { + return dec.name === name && module === undefined; } + }); - return {member, decorators}; - }) - .filter((value): value is {member: ClassMember, decorators: Decorator[]} => value !== null); + if (decorators.length === 0) { + return null; + } + + return {member, decorators}; + }) + .filter((value): value is {member: ClassMember; decorators: Decorator[]} => value !== null); } -function extractModifiersOfMember(node: ts.ClassElement&ts.HasModifiers): - {accessLevel: ClassMemberAccessLevel, isStatic: boolean} { +function extractModifiersOfMember(node: ts.ClassElement & ts.HasModifiers): { + accessLevel: ClassMemberAccessLevel; + isStatic: boolean; +} { const modifiers = ts.getModifiers(node); let isStatic = false; let isReadonly = false; @@ -597,11 +655,11 @@ function extractModifiersOfMember(node: ts.ClassElement&ts.HasModifiers): * Note: Decorator information is not included in this helper as it relies * on type checking to resolve originating import. */ -export function reflectClassMember(node: ts.ClassElement): Omit|null { - let kind: ClassMemberKind|null = null; - let value: ts.Expression|null = null; - let name: string|null = null; - let nameNode: ts.Identifier|ts.PrivateIdentifier|ts.StringLiteral|null = null; +export function reflectClassMember(node: ts.ClassElement): Omit | null { + let kind: ClassMemberKind | null = null; + let value: ts.Expression | null = null; + let name: string | null = null; + let nameNode: ts.Identifier | ts.PrivateIdentifier | ts.StringLiteral | null = null; if (ts.isPropertyDeclaration(node)) { kind = ClassMemberKind.Property; @@ -649,13 +707,16 @@ export function reflectClassMember(node: ts.ClassElement): Omit member.isStatic === isStatic && member.name === name) || null; + members: ClassMember[], + name: string, + isStatic: boolean = false, +): ClassMember | null { + return members.find((member) => member.isStatic === isStatic && member.name === name) || null; } export function reflectObjectLiteral(node: ts.ObjectLiteralExpression): Map { const map = new Map(); - node.properties.forEach(prop => { + node.properties.forEach((prop) => { if (ts.isPropertyAssignment(prop)) { const name = propertyNameToString(prop.name); if (name === null) { @@ -671,16 +732,18 @@ export function reflectObjectLiteral(node: ts.ObjectLiteralExpression): Map { +function castDeclarationToClassOrDie( + declaration: ClassDeclaration, +): ClassDeclaration { if (!ts.isClassDeclaration(declaration)) { throw new Error( - `Reflecting on a ${ts.SyntaxKind[declaration.kind]} instead of a ClassDeclaration.`); + `Reflecting on a ${ts.SyntaxKind[declaration.kind]} instead of a ClassDeclaration.`, + ); } return declaration; } -function parameterName(name: ts.BindingName): string|null { +function parameterName(name: ts.BindingName): string | null { if (ts.isIdentifier(name)) { return name.text; } else { @@ -688,7 +751,7 @@ function parameterName(name: ts.BindingName): string|null { } } -function propertyNameToString(node: ts.PropertyName): string|null { +function propertyNameToString(node: ts.PropertyName): string | null { if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) { return node.text; } else { @@ -702,7 +765,7 @@ function propertyNameToString(node: ts.PropertyName): string|null { * the left most identifier. * @returns the left most identifier in the chain or `null` if it is not an identifier. */ -function getQualifiedNameRoot(qualifiedName: ts.QualifiedName): ts.Identifier|null { +function getQualifiedNameRoot(qualifiedName: ts.QualifiedName): ts.Identifier | null { while (ts.isQualifiedName(qualifiedName.left)) { qualifiedName = qualifiedName.left; } @@ -715,7 +778,7 @@ function getQualifiedNameRoot(qualifiedName: ts.QualifiedName): ts.Identifier|nu * the left most identifier. * @returns the left most identifier in the chain or `null` if it is not an identifier. */ -function getFarLeftIdentifier(propertyAccess: ts.PropertyAccessExpression): ts.Identifier|null { +function getFarLeftIdentifier(propertyAccess: ts.PropertyAccessExpression): ts.Identifier | null { while (ts.isPropertyAccessExpression(propertyAccess.expression)) { propertyAccess = propertyAccess.expression; } @@ -725,7 +788,7 @@ function getFarLeftIdentifier(propertyAccess: ts.PropertyAccessExpression): ts.I /** * Gets the closest ancestor `ImportDeclaration` to a node. */ -export function getContainingImportDeclaration(node: ts.Node): ts.ImportDeclaration|null { +export function getContainingImportDeclaration(node: ts.Node): ts.ImportDeclaration | null { let parent = node.parent; while (parent && !ts.isSourceFile(parent)) { @@ -744,9 +807,9 @@ export function getContainingImportDeclaration(node: ts.Node): ts.ImportDeclarat * then fallback to the `originalId`. */ function getExportedName(decl: ts.Declaration, originalId: ts.Identifier): string { - return ts.isImportSpecifier(decl) ? - (decl.propertyName !== undefined ? decl.propertyName : decl.name).text : - originalId.text; + return ts.isImportSpecifier(decl) + ? (decl.propertyName !== undefined ? decl.propertyName : decl.name).text + : originalId.text; } const LocalExportedDeclarations = Symbol('LocalExportedDeclarations'); diff --git a/packages/compiler-cli/src/ngtsc/reflection/src/util.ts b/packages/compiler-cli/src/ngtsc/reflection/src/util.ts index f20747505d186..b5696823e6c29 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/reflection/src/util.ts @@ -10,22 +10,25 @@ import ts from 'typescript'; import {ClassDeclaration, ClassMemberAccessLevel} from './host'; -export function isNamedClassDeclaration(node: ts.Node): - node is ClassDeclaration { +export function isNamedClassDeclaration( + node: ts.Node, +): node is ClassDeclaration { return ts.isClassDeclaration(node) && isIdentifier(node.name); } -export function isNamedFunctionDeclaration(node: ts.Node): - node is ClassDeclaration { +export function isNamedFunctionDeclaration( + node: ts.Node, +): node is ClassDeclaration { return ts.isFunctionDeclaration(node) && isIdentifier(node.name); } -export function isNamedVariableDeclaration(node: ts.Node): - node is ClassDeclaration { +export function isNamedVariableDeclaration( + node: ts.Node, +): node is ClassDeclaration { return ts.isVariableDeclaration(node) && isIdentifier(node.name); } -function isIdentifier(node: ts.Node|undefined): node is ts.Identifier { +function isIdentifier(node: ts.Node | undefined): node is ts.Identifier { return node !== undefined && ts.isIdentifier(node); } diff --git a/packages/compiler-cli/src/ngtsc/reflection/test/ts_host_spec.ts b/packages/compiler-cli/src/ngtsc/reflection/test/ts_host_spec.ts index 0891d0e13e0a8..456005fcadd12 100644 --- a/packages/compiler-cli/src/ngtsc/reflection/test/ts_host_spec.ts +++ b/packages/compiler-cli/src/ngtsc/reflection/test/ts_host_spec.ts @@ -14,8 +14,8 @@ import {ClassMember, ClassMemberKind, CtorParameter, TypeValueReferenceKind} fro import {TypeScriptReflectionHost} from '../src/typescript'; import {isNamedClassDeclaration} from '../src/util'; -function findFirstImportDeclaration(node: ts.Node): ts.ImportDeclaration|null { - let found: ts.ImportDeclaration|null = null; +function findFirstImportDeclaration(node: ts.Node): ts.ImportDeclaration | null { + let found: ts.ImportDeclaration | null = null; const visit = (node: ts.Node): void => { if (found) return; if (ts.isImportDeclaration(node)) { @@ -32,20 +32,22 @@ runInEachFileSystem(() => { describe('reflector', () => { let _: typeof absoluteFrom; - beforeEach(() => _ = absoluteFrom); + beforeEach(() => (_ = absoluteFrom)); describe('getConstructorParameters()', () => { it('should reflect a single argument', () => { - const {program} = makeProgram([{ - name: _('/entry.ts'), - contents: ` + const {program} = makeProgram([ + { + name: _('/entry.ts'), + contents: ` class Bar {} class Foo { constructor(bar: Bar) {} } - ` - }]); + `, + }, + ]); const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration); const checker = program.getTypeChecker(); const host = new TypeScriptReflectionHost(checker); @@ -61,7 +63,7 @@ runInEachFileSystem(() => { contents: ` export function dec(target: any, key: string, index: number) { } - ` + `, }, { name: _('/entry.ts'), @@ -72,8 +74,8 @@ runInEachFileSystem(() => { class Foo { constructor(@dec bar: Bar) {} } - ` - } + `, + }, ]); const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration); const checker = program.getTypeChecker(); @@ -90,7 +92,7 @@ runInEachFileSystem(() => { contents: ` export function dec(target: any, key: string, index: number) { } - ` + `, }, { name: _('/entry.ts'), @@ -101,8 +103,8 @@ runInEachFileSystem(() => { class Foo { constructor(@dec bar: Bar) {} } - ` - } + `, + }, ]); const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration); const checker = program.getTypeChecker(); @@ -118,7 +120,7 @@ runInEachFileSystem(() => { name: _('/bar.ts'), contents: ` export class Bar {} - ` + `, }, { name: _('/entry.ts'), @@ -129,8 +131,8 @@ runInEachFileSystem(() => { class Foo { constructor(bar: Bar, otherBar: star.Bar) {} } - ` - } + `, + }, ]); const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration); const checker = program.getTypeChecker(); @@ -147,7 +149,7 @@ runInEachFileSystem(() => { name: _('/bar.ts'), contents: ` export class Bar {} - ` + `, }, { name: _('/entry.ts'), @@ -157,8 +159,8 @@ runInEachFileSystem(() => { class Foo { constructor(bar: LocalBar) {} } - ` - } + `, + }, ]); const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration); const checker = program.getTypeChecker(); @@ -169,9 +171,10 @@ runInEachFileSystem(() => { }); it('should reflect an argument from a namespace declarations', () => { - const {program} = makeProgram([{ - name: _('/entry.ts'), - contents: ` + const {program} = makeProgram([ + { + name: _('/entry.ts'), + contents: ` export declare class Bar {} declare namespace i1 { export { @@ -182,8 +185,9 @@ runInEachFileSystem(() => { class Foo { constructor(bar: i1.Bar) {} } - ` - }]); + `, + }, + ]); const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration); const checker = program.getTypeChecker(); const host = new TypeScriptReflectionHost(checker); @@ -198,7 +202,7 @@ runInEachFileSystem(() => { name: _('/bar.ts'), contents: ` export default class Bar {} - ` + `, }, { name: _('/entry.ts'), @@ -208,8 +212,8 @@ runInEachFileSystem(() => { class Foo { constructor(bar: Bar) {} } - ` - } + `, + }, ]); const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration); const checker = program.getTypeChecker(); @@ -230,7 +234,7 @@ runInEachFileSystem(() => { name: _('/bar.ts'), contents: ` export class Bar {} - ` + `, }, { name: _('/entry.ts'), @@ -240,8 +244,8 @@ runInEachFileSystem(() => { class Foo { constructor(bar: Bar|null) {} } - ` - } + `, + }, ]); const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration); const checker = program.getTypeChecker(); @@ -252,9 +256,10 @@ runInEachFileSystem(() => { }); it('should reflect the arguments from an overloaded constructor', () => { - const {program} = makeProgram([{ - name: _('/entry.ts'), - contents: ` + const {program} = makeProgram([ + { + name: _('/entry.ts'), + contents: ` class Bar {} class Baz {} @@ -262,8 +267,9 @@ runInEachFileSystem(() => { constructor(bar: Bar); constructor(bar: Bar, baz?: Baz) {} } - ` - }]); + `, + }, + ]); const clazz = getDeclaration(program, _('/entry.ts'), 'Foo', isNamedClassDeclaration); const checker = program.getTypeChecker(); const host = new TypeScriptReflectionHost(checker); @@ -274,7 +280,6 @@ runInEachFileSystem(() => { }); }); - describe('getImportOfIdentifier()', () => { it('should resolve a direct import', () => { const {program} = makeProgram([ @@ -284,15 +289,18 @@ runInEachFileSystem(() => { contents: ` import {Target} from 'absolute'; let foo: Target; - ` + `, }, ]); const checker = program.getTypeChecker(); const host = new TypeScriptReflectionHost(checker); const foo = getDeclaration(program, _('/entry.ts'), 'foo', ts.isVariableDeclaration); - if (foo.type === undefined || !ts.isTypeReferenceNode(foo.type) || - !ts.isIdentifier(foo.type.typeName)) { + if ( + foo.type === undefined || + !ts.isTypeReferenceNode(foo.type) || + !ts.isIdentifier(foo.type.typeName) + ) { return fail('Unexpected type for foo'); } const Target = foo.type.typeName; @@ -314,15 +322,18 @@ runInEachFileSystem(() => { contents: ` import * as abs from 'absolute'; let foo: abs.Target; - ` + `, }, ]); const checker = program.getTypeChecker(); const host = new TypeScriptReflectionHost(checker); const foo = getDeclaration(program, _('/entry.ts'), 'foo', ts.isVariableDeclaration); - if (foo.type === undefined || !ts.isTypeReferenceNode(foo.type) || - !ts.isQualifiedName(foo.type.typeName)) { + if ( + foo.type === undefined || + !ts.isTypeReferenceNode(foo.type) || + !ts.isQualifiedName(foo.type.typeName) + ) { return fail('Unexpected type for foo'); } const Target = foo.type.typeName.right; @@ -342,7 +353,8 @@ runInEachFileSystem(() => { const {program} = makeProgram([ {name: _('/node_modules/absolute/index.ts'), contents: 'export class Target {}'}, {name: _('/local1.ts'), contents: `export {Target as AliasTarget} from 'absolute';`}, - {name: _('/local2.ts'), contents: `export {AliasTarget as Target} from './local1';`}, { + {name: _('/local2.ts'), contents: `export {AliasTarget as Target} from './local1';`}, + { name: _('/entry.ts'), contents: ` import {Target} from './local2'; @@ -350,15 +362,19 @@ runInEachFileSystem(() => { const target = Target; const directTarget = DirectTarget; - ` - } + `, + }, ]); const target = getDeclaration(program, _('/entry.ts'), 'target', ts.isVariableDeclaration); if (target.initializer === undefined || !ts.isIdentifier(target.initializer)) { return fail('Unexpected initializer for target'); } - const directTarget = - getDeclaration(program, _('/entry.ts'), 'directTarget', ts.isVariableDeclaration); + const directTarget = getDeclaration( + program, + _('/entry.ts'), + 'directTarget', + ts.isVariableDeclaration, + ); if (directTarget.initializer === undefined || !ts.isIdentifier(directTarget.initializer)) { return fail('Unexpected initializer for directTarget'); } @@ -374,8 +390,9 @@ runInEachFileSystem(() => { } else if (directTargetDecl === null) { return fail('No declaration found for DirectTarget'); } - expect(targetDecl.node!.getSourceFile()) - .toBe(getSourceFileOrError(program, _('/node_modules/absolute/index.ts'))); + expect(targetDecl.node!.getSourceFile()).toBe( + getSourceFileOrError(program, _('/node_modules/absolute/index.ts')), + ); expect(ts.isClassDeclaration(targetDecl.node!)).toBe(true); expect(directTargetDecl.viaModule).toBe('absolute'); expect(directTargetDecl.node).toBe(targetDecl.node); @@ -389,17 +406,24 @@ runInEachFileSystem(() => { contents: ` import {Target} from 'absolute'; let foo: Target; - ` + `, }, ]); const checker = program.getTypeChecker(); const host = new TypeScriptReflectionHost(checker); const targetDecl = getDeclaration( - program, _('/node_modules/absolute/index.ts'), 'Target', ts.isClassDeclaration); + program, + _('/node_modules/absolute/index.ts'), + 'Target', + ts.isClassDeclaration, + ); const foo = getDeclaration(program, _('/entry.ts'), 'foo', ts.isVariableDeclaration); - if (foo.type === undefined || !ts.isTypeReferenceNode(foo.type) || - !ts.isIdentifier(foo.type.typeName)) { + if ( + foo.type === undefined || + !ts.isTypeReferenceNode(foo.type) || + !ts.isIdentifier(foo.type.typeName) + ) { return fail('Unexpected type for foo'); } const Target = foo.type.typeName; @@ -418,17 +442,24 @@ runInEachFileSystem(() => { contents: ` import * as abs from 'absolute'; let foo: abs.Target; - ` + `, }, ]); const checker = program.getTypeChecker(); const host = new TypeScriptReflectionHost(checker); const targetDecl = getDeclaration( - program, _('/node_modules/absolute/index.ts'), 'Target', ts.isClassDeclaration); + program, + _('/node_modules/absolute/index.ts'), + 'Target', + ts.isClassDeclaration, + ); const foo = getDeclaration(program, _('/entry.ts'), 'foo', ts.isVariableDeclaration); - if (foo.type === undefined || !ts.isTypeReferenceNode(foo.type) || - !ts.isQualifiedName(foo.type.typeName)) { + if ( + foo.type === undefined || + !ts.isTypeReferenceNode(foo.type) || + !ts.isQualifiedName(foo.type.typeName) + ) { return fail('Unexpected type for foo'); } const Target = foo.type.typeName.right; @@ -451,16 +482,21 @@ runInEachFileSystem(() => { export type T = string; export interface I {} export enum E {} - ` + `, }, ]); const checker = program.getTypeChecker(); const host = new TypeScriptReflectionHost(checker); - const exportedDeclarations = - host.getExportsOfModule(program.getSourceFile(_('/entry.ts'))!); + const exportedDeclarations = host.getExportsOfModule( + program.getSourceFile(_('/entry.ts'))!, + ); expect(Array.from(exportedDeclarations!.keys())).toEqual(['foo', 'x', 'T', 'I', 'E']); - expect(Array.from(exportedDeclarations!.values()).map(v => v.viaModule)).toEqual([ - null, null, null, null, null + expect(Array.from(exportedDeclarations!.values()).map((v) => v.viaModule)).toEqual([ + null, + null, + null, + null, + null, ]); }); @@ -476,78 +512,93 @@ runInEachFileSystem(() => { export {AliasTarget} from './local1'; export {Target as AliasTarget2} from './local2'; export * from 'absolute'; - ` + `, }, ]); const checker = program.getTypeChecker(); const host = new TypeScriptReflectionHost(checker); - const exportedDeclarations = - host.getExportsOfModule(program.getSourceFile(_('/entry.ts'))!); + const exportedDeclarations = host.getExportsOfModule( + program.getSourceFile(_('/entry.ts'))!, + ); expect(Array.from(exportedDeclarations!.keys())).toEqual([ - 'Target1', 'AliasTarget', 'AliasTarget2', 'Target' + 'Target1', + 'AliasTarget', + 'AliasTarget2', + 'Target', ]); - expect(Array.from(exportedDeclarations!.values()).map(v => v.viaModule)).toEqual([ - null, null, null, null + expect(Array.from(exportedDeclarations!.values()).map((v) => v.viaModule)).toEqual([ + null, + null, + null, + null, ]); }); }); describe('getMembersOfClass()', () => { it('should get string literal members of class', () => { - const {program} = makeProgram([{ - name: _('/entry.ts'), - contents: ` + const {program} = makeProgram([ + { + name: _('/entry.ts'), + contents: ` class Foo { 'string-literal-property-member' = 'my value'; } - ` - }]); + `, + }, + ]); const members = getMembers(program); expect(members.length).toBe(1); expectMember(members[0], 'string-literal-property-member', ClassMemberKind.Property); }); it('should retrieve method members', () => { - const {program} = makeProgram([{ - name: _('/entry.ts'), - contents: ` + const {program} = makeProgram([ + { + name: _('/entry.ts'), + contents: ` class Foo { myMethod(): void { } } - ` - }]); + `, + }, + ]); const members = getMembers(program); expect(members.length).toBe(1); expectMember(members[0], 'myMethod', ClassMemberKind.Method); }); it('should retrieve constructor as member', () => { - const {program} = makeProgram([{ - name: _('/entry.ts'), - contents: ` + const {program} = makeProgram([ + { + name: _('/entry.ts'), + contents: ` class Foo { constructor() {} } - ` - }]); + `, + }, + ]); const members = getMembers(program); expect(members.length).toBe(1); expectMember(members[0], 'constructor', ClassMemberKind.Constructor); }); it('should retrieve decorators of member', () => { - const {program} = makeProgram([{ - name: _('/entry.ts'), - contents: ` + const {program} = makeProgram([ + { + name: _('/entry.ts'), + contents: ` declare var Input; class Foo { @Input() prop: string; } - ` - }]); + `, + }, + ]); const members = getMembers(program); expect(members.length).toBe(1); expect(members[0].decorators).not.toBeNull(); @@ -555,14 +606,16 @@ runInEachFileSystem(() => { }); it('identifies static members', () => { - const {program} = makeProgram([{ - name: _('/entry.ts'), - contents: ` + const {program} = makeProgram([ + { + name: _('/entry.ts'), + contents: ` class Foo { static staticMember = ''; } - ` - }]); + `, + }, + ]); const members = getMembers(program); expect(members.length).toBe(1); expect(members[0].isStatic).toBeTrue(); @@ -583,8 +636,12 @@ runInEachFileSystem(() => { }); function expectParameter( - param: CtorParameter, name: string, type?: string|{name: string, moduleName: string}, - decorator?: string, decoratorFrom?: string): void { + param: CtorParameter, + name: string, + type?: string | {name: string; moduleName: string}, + decorator?: string, + decoratorFrom?: string, + ): void { expect(param.name!).toEqual(name); if (type === undefined) { expect(param.typeValueReference).toBeNull(); @@ -592,32 +649,38 @@ runInEachFileSystem(() => { if (param.typeValueReference.kind === TypeValueReferenceKind.UNAVAILABLE) { return fail(`Expected parameter ${name} to have a typeValueReference`); } - if (param.typeValueReference.kind === TypeValueReferenceKind.LOCAL && - typeof type === 'string') { + if ( + param.typeValueReference.kind === TypeValueReferenceKind.LOCAL && + typeof type === 'string' + ) { expect(argExpressionToString(param.typeValueReference.expression)).toEqual(type); } else if ( - param.typeValueReference.kind === TypeValueReferenceKind.IMPORTED && - typeof type !== 'string') { + param.typeValueReference.kind === TypeValueReferenceKind.IMPORTED && + typeof type !== 'string' + ) { expect(param.typeValueReference.moduleName).toEqual(type.moduleName); expect(param.typeValueReference.importedName).toEqual(type.name); } else { - return fail(`Mismatch between typeValueReference and expected type: ${param.name} / ${ - param.typeValueReference.kind}`); + return fail( + `Mismatch between typeValueReference and expected type: ${param.name} / ${param.typeValueReference.kind}`, + ); } } if (decorator !== undefined) { expect(param.decorators).not.toBeNull(); expect(param.decorators!.length).toBeGreaterThan(0); - expect(param.decorators!.some( - dec => dec.name === decorator && dec.import !== null && - dec.import.from === decoratorFrom)) - .toBe(true); + expect( + param.decorators!.some( + (dec) => + dec.name === decorator && dec.import !== null && dec.import.from === decoratorFrom, + ), + ).toBe(true); } } - function argExpressionToString(name: ts.Node|null): string { + function argExpressionToString(name: ts.Node | null): string { if (name == null) { - throw new Error('\'name\' argument can\'t be null'); + throw new Error("'name' argument can't be null"); } if (ts.isIdentifier(name)) { diff --git a/packages/compiler-cli/src/ngtsc/resource/src/loader.ts b/packages/compiler-cli/src/ngtsc/resource/src/loader.ts index 3aeeee240ca1e..fc8fdfceed340 100644 --- a/packages/compiler-cli/src/ngtsc/resource/src/loader.ts +++ b/packages/compiler-cli/src/ngtsc/resource/src/loader.ts @@ -29,7 +29,10 @@ export class AdapterResourceLoader implements ResourceLoader { canPreload: boolean; canPreprocess: boolean; - constructor(private adapter: NgCompilerAdapter, private options: ts.CompilerOptions) { + constructor( + private adapter: NgCompilerAdapter, + private options: ts.CompilerOptions, + ) { this.lookupResolutionHost = createLookupResolutionHost(this.adapter); this.canPreload = !!this.adapter.readResource; this.canPreprocess = !!this.adapter.transformResource; @@ -48,10 +51,13 @@ export class AdapterResourceLoader implements ResourceLoader { * @throws An error if the resource cannot be resolved. */ resolve(url: string, fromFile: string): string { - let resolvedUrl: string|null = null; + let resolvedUrl: string | null = null; if (this.adapter.resourceNameToFileName) { resolvedUrl = this.adapter.resourceNameToFileName( - url, fromFile, (url: string, fromFile: string) => this.fallbackResolve(url, fromFile)); + url, + fromFile, + (url: string, fromFile: string) => this.fallbackResolve(url, fromFile), + ); } else { resolvedUrl = this.fallbackResolve(url, fromFile); } @@ -73,10 +79,11 @@ export class AdapterResourceLoader implements ResourceLoader { * file has already been loaded. * @throws An Error if pre-loading is not available. */ - preload(resolvedUrl: string, context: ResourceLoaderContext): Promise|undefined { + preload(resolvedUrl: string, context: ResourceLoaderContext): Promise | undefined { if (!this.adapter.readResource) { throw new Error( - 'HostResourceLoader: the CompilerHost provided does not support pre-loading resources.'); + 'HostResourceLoader: the CompilerHost provided does not support pre-loading resources.', + ); } if (this.cache.has(resolvedUrl)) { return undefined; @@ -102,7 +109,7 @@ export class AdapterResourceLoader implements ResourceLoader { this.cache.set(resolvedUrl, result); return undefined; } else { - const fetchCompletion = result.then(str => { + const fetchCompletion = result.then((str) => { this.fetching.delete(resolvedUrl); this.cache.set(resolvedUrl, str); }); @@ -124,8 +131,11 @@ export class AdapterResourceLoader implements ResourceLoader { return data; } - const transformResult = await this.adapter.transformResource( - data, {type: 'style', containingFile: context.containingFile, resourceFile: null}); + const transformResult = await this.adapter.transformResource(data, { + type: 'style', + containingFile: context.containingFile, + resourceFile: null, + }); if (transformResult === null) { return data; } @@ -146,8 +156,9 @@ export class AdapterResourceLoader implements ResourceLoader { return this.cache.get(resolvedUrl)!; } - const result = this.adapter.readResource ? this.adapter.readResource(resolvedUrl) : - this.adapter.readFile(resolvedUrl); + const result = this.adapter.readResource + ? this.adapter.readResource(resolvedUrl) + : this.adapter.readFile(resolvedUrl); if (typeof result !== 'string') { throw new Error(`HostResourceLoader: loader(${resolvedUrl}) returned a Promise`); } @@ -166,7 +177,7 @@ export class AdapterResourceLoader implements ResourceLoader { * Attempt to resolve `url` in the context of `fromFile`, while respecting the rootDirs * option from the tsconfig. First, normalize the file name. */ - private fallbackResolve(url: string, fromFile: string): string|null { + private fallbackResolve(url: string, fromFile: string): string | null { let candidateLocations: string[]; if (url.startsWith('/')) { // This path is not really an absolute path, but instead the leading '/' means that it's @@ -203,7 +214,7 @@ export class AdapterResourceLoader implements ResourceLoader { private getRootedCandidateLocations(url: string): AbsoluteFsPath[] { // The path already starts with '/', so add a '.' to make it relative. const segment: PathSegment = ('.' + url) as PathSegment; - return this.adapter.rootDirs.map(rootDir => join(rootDir, segment)); + return this.adapter.rootDirs.map((rootDir) => join(rootDir, segment)); } /** @@ -217,21 +228,27 @@ export class AdapterResourceLoader implements ResourceLoader { // `failedLookupLocations` is in the name of the type ts.ResolvedModuleWithFailedLookupLocations // but is marked @internal in TypeScript. See // https://github.com/Microsoft/TypeScript/issues/28770. - type ResolvedModuleWithFailedLookupLocations = - ts.ResolvedModuleWithFailedLookupLocations&{failedLookupLocations: ReadonlyArray}; + type ResolvedModuleWithFailedLookupLocations = ts.ResolvedModuleWithFailedLookupLocations & { + failedLookupLocations: ReadonlyArray; + }; // clang-format off - const failedLookup = ts.resolveModuleName(url + RESOURCE_MARKER, fromFile, this.options, this.lookupResolutionHost) as ResolvedModuleWithFailedLookupLocations; + const failedLookup = ts.resolveModuleName( + url + RESOURCE_MARKER, + fromFile, + this.options, + this.lookupResolutionHost, + ) as ResolvedModuleWithFailedLookupLocations; // clang-format on if (failedLookup.failedLookupLocations === undefined) { throw new Error( - `Internal error: expected to find failedLookupLocations during resolution of resource '${ - url}' in context of ${fromFile}`); + `Internal error: expected to find failedLookupLocations during resolution of resource '${url}' in context of ${fromFile}`, + ); } return failedLookup.failedLookupLocations - .filter(candidate => candidate.endsWith(RESOURCE_MARKER_TS)) - .map(candidate => candidate.slice(0, -RESOURCE_MARKER_TS.length)); + .filter((candidate) => candidate.endsWith(RESOURCE_MARKER_TS)) + .map((candidate) => candidate.slice(0, -RESOURCE_MARKER_TS.length)); } } @@ -239,8 +256,9 @@ export class AdapterResourceLoader implements ResourceLoader { * Derives a `ts.ModuleResolutionHost` from a compiler adapter that recognizes the special resource * marker and does not go to the filesystem for these requests, as they are known not to exist. */ -function createLookupResolutionHost(adapter: NgCompilerAdapter): - RequiredDelegations { +function createLookupResolutionHost( + adapter: NgCompilerAdapter, +): RequiredDelegations { return { directoryExists(directoryName: string): boolean { if (directoryName.includes(RESOURCE_MARKER)) { @@ -265,8 +283,9 @@ function createLookupResolutionHost(adapter: NgCompilerAdapter): getDirectories: adapter.getDirectories?.bind(adapter), realpath: adapter.realpath?.bind(adapter), trace: adapter.trace?.bind(adapter), - useCaseSensitiveFileNames: typeof adapter.useCaseSensitiveFileNames === 'function' ? - adapter.useCaseSensitiveFileNames.bind(adapter) : - adapter.useCaseSensitiveFileNames + useCaseSensitiveFileNames: + typeof adapter.useCaseSensitiveFileNames === 'function' + ? adapter.useCaseSensitiveFileNames.bind(adapter) + : adapter.useCaseSensitiveFileNames, }; } diff --git a/packages/compiler-cli/src/ngtsc/scope/index.ts b/packages/compiler-cli/src/ngtsc/scope/index.ts index 46a7762cbc493..9c6f1335a0e1f 100644 --- a/packages/compiler-cli/src/ngtsc/scope/index.ts +++ b/packages/compiler-cli/src/ngtsc/scope/index.ts @@ -6,7 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -export {ComponentScopeKind, ComponentScopeReader, ExportScope, LocalModuleScope, ScopeData, StandaloneScope} from './src/api'; +export { + ComponentScopeKind, + ComponentScopeReader, + ExportScope, + LocalModuleScope, + ScopeData, + StandaloneScope, +} from './src/api'; export {CompoundComponentScopeReader} from './src/component_scope'; export {DtsModuleScopeResolver, MetadataDtsModuleScopeResolver} from './src/dependency'; export {DeclarationData, LocalModuleScopeRegistry, LocalNgModuleData} from './src/local'; diff --git a/packages/compiler-cli/src/ngtsc/scope/src/api.ts b/packages/compiler-cli/src/ngtsc/scope/src/api.ts index 3a1716e28a118..12c8b36730dba 100644 --- a/packages/compiler-cli/src/ngtsc/scope/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/scope/src/api.ts @@ -12,12 +12,11 @@ import {Reexport, Reference} from '../../imports'; import {DirectiveMeta, NgModuleMeta, PipeMeta} from '../../metadata'; import {ClassDeclaration} from '../../reflection'; - /** * Data for one of a given NgModule's scopes (either compilation scope or export scopes). */ export interface ScopeData { - dependencies: Array; + dependencies: Array; /** * Whether some module or component in this scope contains errors and is thus semantically @@ -58,31 +57,30 @@ export enum ComponentScopeKind { Standalone, } - export interface LocalModuleScope extends ExportScope { kind: ComponentScopeKind.NgModule; ngModule: ClassDeclaration; compilation: ScopeData; - reexports: Reexport[]|null; + reexports: Reexport[] | null; schemas: SchemaMetadata[]; } export interface StandaloneScope { kind: ComponentScopeKind.Standalone; - dependencies: Array; - deferredDependencies: Array; + dependencies: Array; + deferredDependencies: Array; component: ClassDeclaration; schemas: SchemaMetadata[]; isPoisoned: boolean; } -export type ComponentScope = LocalModuleScope|StandaloneScope; +export type ComponentScope = LocalModuleScope | StandaloneScope; /** * Read information about the compilation scope of components. */ export interface ComponentScopeReader { - getScopeForComponent(clazz: ClassDeclaration): ComponentScope|null; + getScopeForComponent(clazz: ClassDeclaration): ComponentScope | null; /** * Get the `RemoteScope` required for this component, if any. @@ -90,5 +88,5 @@ export interface ComponentScopeReader { * If the component requires remote scoping, then retrieve the directives/pipes registered for * that component. If remote scoping is not required (the common case), returns `null`. */ - getRemoteScope(clazz: ClassDeclaration): RemoteScope|null; + getRemoteScope(clazz: ClassDeclaration): RemoteScope | null; } diff --git a/packages/compiler-cli/src/ngtsc/scope/src/component_scope.ts b/packages/compiler-cli/src/ngtsc/scope/src/component_scope.ts index a66acd236b96b..45f988eec44a8 100644 --- a/packages/compiler-cli/src/ngtsc/scope/src/component_scope.ts +++ b/packages/compiler-cli/src/ngtsc/scope/src/component_scope.ts @@ -19,7 +19,7 @@ import {ComponentScope, ComponentScopeReader, LocalModuleScope, RemoteScope} fro export class CompoundComponentScopeReader implements ComponentScopeReader { constructor(private readers: ComponentScopeReader[]) {} - getScopeForComponent(clazz: ClassDeclaration): ComponentScope|null { + getScopeForComponent(clazz: ClassDeclaration): ComponentScope | null { for (const reader of this.readers) { const meta = reader.getScopeForComponent(clazz); if (meta !== null) { @@ -29,7 +29,7 @@ export class CompoundComponentScopeReader implements ComponentScopeReader { return null; } - getRemoteScope(clazz: ClassDeclaration): RemoteScope|null { + getRemoteScope(clazz: ClassDeclaration): RemoteScope | null { for (const reader of this.readers) { const remoteScope = reader.getRemoteScope(clazz); if (remoteScope !== null) { diff --git a/packages/compiler-cli/src/ngtsc/scope/src/dependency.ts b/packages/compiler-cli/src/ngtsc/scope/src/dependency.ts index 47bffa30a604e..0fa6a09a37ab3 100644 --- a/packages/compiler-cli/src/ngtsc/scope/src/dependency.ts +++ b/packages/compiler-cli/src/ngtsc/scope/src/dependency.ts @@ -15,7 +15,7 @@ import {ClassDeclaration} from '../../reflection'; import {ExportScope} from './api'; export interface DtsModuleScopeResolver { - resolve(ref: Reference): ExportScope|null; + resolve(ref: Reference): ExportScope | null; } /** @@ -29,12 +29,15 @@ export class MetadataDtsModuleScopeResolver implements DtsModuleScopeResolver { /** * Cache which holds fully resolved scopes for NgModule classes from .d.ts files. */ - private cache = new Map(); + private cache = new Map(); /** * @param dtsMetaReader a `MetadataReader` which can read metadata from `.d.ts` files. */ - constructor(private dtsMetaReader: MetadataReader, private aliasingHost: AliasingHost|null) {} + constructor( + private dtsMetaReader: MetadataReader, + private aliasingHost: AliasingHost | null, + ) {} /** * Resolve a `Reference`'d NgModule from a .d.ts file and produce a transitive `ExportScope` @@ -43,12 +46,13 @@ export class MetadataDtsModuleScopeResolver implements DtsModuleScopeResolver { * This operation relies on a `Reference` instead of a direct TypeScript node as the `Reference`s * produced depend on how the original NgModule was imported. */ - resolve(ref: Reference): ExportScope|null { + resolve(ref: Reference): ExportScope | null { const clazz = ref.node; const sourceFile = clazz.getSourceFile(); if (!sourceFile.isDeclarationFile) { - throw new Error(`Debug error: DtsModuleScopeResolver.read(${ref.debugName} from ${ - sourceFile.fileName}), but not a .d.ts file`); + throw new Error( + `Debug error: DtsModuleScopeResolver.read(${ref.debugName} from ${sourceFile.fileName}), but not a .d.ts file`, + ); } if (this.cache.has(clazz)) { @@ -56,7 +60,7 @@ export class MetadataDtsModuleScopeResolver implements DtsModuleScopeResolver { } // Build up the export scope - those directives and pipes made visible by this module. - const dependencies: Array = []; + const dependencies: Array = []; const meta = this.dtsMetaReader.getNgModuleMetadata(ref); if (meta === null) { @@ -127,8 +131,11 @@ export class MetadataDtsModuleScopeResolver implements DtsModuleScopeResolver { return exportScope; } - private maybeAlias( - dirOrPipe: T, maybeAliasFrom: ts.SourceFile, isReExport: boolean): T { + private maybeAlias( + dirOrPipe: T, + maybeAliasFrom: ts.SourceFile, + isReExport: boolean, + ): T { const ref = dirOrPipe.ref; if (this.aliasingHost === null || ref.node.getSourceFile() === maybeAliasFrom) { return dirOrPipe; diff --git a/packages/compiler-cli/src/ngtsc/scope/src/local.ts b/packages/compiler-cli/src/ngtsc/scope/src/local.ts index 333fd36e54257..b5e9d4d3a7018 100644 --- a/packages/compiler-cli/src/ngtsc/scope/src/local.ts +++ b/packages/compiler-cli/src/ngtsc/scope/src/local.ts @@ -10,12 +10,32 @@ import {ExternalExpr} from '@angular/compiler'; import ts from 'typescript'; import {ErrorCode, makeDiagnostic, makeRelatedInformation} from '../../diagnostics'; -import {AliasingHost, assertSuccessfulReferenceEmit, Reexport, Reference, ReferenceEmitter} from '../../imports'; -import {DirectiveMeta, MetadataReader, MetadataRegistry, MetaKind, NgModuleMeta, PipeMeta} from '../../metadata'; +import { + AliasingHost, + assertSuccessfulReferenceEmit, + Reexport, + Reference, + ReferenceEmitter, +} from '../../imports'; +import { + DirectiveMeta, + MetadataReader, + MetadataRegistry, + MetaKind, + NgModuleMeta, + PipeMeta, +} from '../../metadata'; import {ClassDeclaration, DeclarationNode} from '../../reflection'; import {identifierOfNode, nodeNameForError} from '../../util/src/typescript'; -import {ComponentScopeKind, ComponentScopeReader, ExportScope, LocalModuleScope, RemoteScope, ScopeData} from './api'; +import { + ComponentScopeKind, + ComponentScopeReader, + ExportScope, + LocalModuleScope, + RemoteScope, + ScopeData, +} from './api'; import {DtsModuleScopeResolver} from './dependency'; import {getDiagnosticNode, makeNotStandaloneDiagnostic} from './util'; @@ -65,8 +85,10 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop * This maps from the directive/pipe class to a map of data for each NgModule that declares the * directive/pipe. This data is needed to produce an error for the given class. */ - private duplicateDeclarations = - new Map>(); + private duplicateDeclarations = new Map< + ClassDeclaration, + Map + >(); private moduleToRef = new Map>(); @@ -74,7 +96,7 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop * A cache of calculated `LocalModuleScope`s for each NgModule declared in the current program. */ - private cache = new Map(); + private cache = new Map(); /** * Tracks the `RemoteScope` for components requiring "remote scoping". @@ -97,9 +119,12 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop private modulesWithStructuralErrors = new Set(); constructor( - private localReader: MetadataReader, private fullReader: MetadataReader, - private dependencyScopeReader: DtsModuleScopeResolver, private refEmitter: ReferenceEmitter, - private aliasingHost: AliasingHost|null) {} + private localReader: MetadataReader, + private fullReader: MetadataReader, + private dependencyScopeReader: DtsModuleScopeResolver, + private refEmitter: ReferenceEmitter, + private aliasingHost: AliasingHost | null, + ) {} /** * Add an NgModule's data to the registry. @@ -119,10 +144,10 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop registerPipeMetadata(pipe: PipeMeta): void {} - getScopeForComponent(clazz: ClassDeclaration): LocalModuleScope|null { - const scope = !this.declarationToModule.has(clazz) ? - null : - this.getScopeOfModule(this.declarationToModule.get(clazz)!.ngModule); + getScopeForComponent(clazz: ClassDeclaration): LocalModuleScope | null { + const scope = !this.declarationToModule.has(clazz) + ? null + : this.getScopeOfModule(this.declarationToModule.get(clazz)!.ngModule); return scope; } @@ -133,7 +158,7 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop * Ordinarily a class is only declared in one NgModule, in which case this function returns * `null`. */ - getDuplicateDeclarations(node: ClassDeclaration): DeclarationData[]|null { + getDuplicateDeclarations(node: ClassDeclaration): DeclarationData[] | null { if (!this.duplicateDeclarations.has(node)) { return null; } @@ -149,17 +174,17 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop * `LocalModuleScope` for the given NgModule if one can be produced, `null` if no scope was ever * defined, or the string `'error'` if the scope contained errors. */ - getScopeOfModule(clazz: ClassDeclaration): LocalModuleScope|null { - return this.moduleToRef.has(clazz) ? - this.getScopeOfModuleReference(this.moduleToRef.get(clazz)!) : - null; + getScopeOfModule(clazz: ClassDeclaration): LocalModuleScope | null { + return this.moduleToRef.has(clazz) + ? this.getScopeOfModuleReference(this.moduleToRef.get(clazz)!) + : null; } /** * Retrieves any `ts.Diagnostic`s produced during the calculation of the `LocalModuleScope` for * the given NgModule, or `null` if no errors were present. */ - getDiagnosticsOfModule(clazz: ClassDeclaration): ts.Diagnostic[]|null { + getDiagnosticsOfModule(clazz: ClassDeclaration): ts.Diagnostic[] | null { // Required to ensure the errors are populated for the given class. If it has been processed // before, this will be a no-op due to the scope cache. this.getScopeOfModule(clazz); @@ -172,8 +197,10 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop } private registerDeclarationOfModule( - ngModule: ClassDeclaration, decl: Reference, - rawDeclarations: ts.Expression|null): void { + ngModule: ClassDeclaration, + decl: Reference, + rawDeclarations: ts.Expression | null, + ): void { const declData: DeclarationData = { ngModule, ref: decl, @@ -186,8 +213,9 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop // map of modules for which a duplicate declaration exists. this.duplicateDeclarations.get(decl.node)!.set(ngModule, declData); } else if ( - this.declarationToModule.has(decl.node) && - this.declarationToModule.get(decl.node)!.ngModule !== ngModule) { + this.declarationToModule.has(decl.node) && + this.declarationToModule.get(decl.node)!.ngModule !== ngModule + ) { // This directive/pipe is already registered as declared in another module. Mark it as a // duplicate instead. const duplicateDeclMap = new Map(); @@ -215,7 +243,7 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop /** * Implementation of `getScopeOfModule` which accepts a reference to a class. */ - private getScopeOfModuleReference(ref: Reference): LocalModuleScope|null { + private getScopeOfModuleReference(ref: Reference): LocalModuleScope | null { if (this.cache.has(ref.node)) { return this.cache.get(ref.node)!; } @@ -304,8 +332,14 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop compilationDirectives.set(directive.ref.node, directive); } else { // Error: can't import a non-standalone component/directive. - diagnostics.push(makeNotStandaloneDiagnostic( - this, decl, ngModule.rawImports, directive.isComponent ? 'component' : 'directive')); + diagnostics.push( + makeNotStandaloneDiagnostic( + this, + decl, + ngModule.rawImports, + directive.isComponent ? 'component' : 'directive', + ), + ); isPoisoned = true; } @@ -337,12 +371,13 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop if (directive !== null) { if (directive.isStandalone) { const refType = directive.isComponent ? 'Component' : 'Directive'; - diagnostics.push(makeDiagnostic( + diagnostics.push( + makeDiagnostic( ErrorCode.NGMODULE_DECLARATION_IS_STANDALONE, decl.getOriginForDiagnostics(ngModule.rawDeclarations!), - `${refType} ${ - decl.node.name - .text} is standalone, and cannot be declared in an NgModule. Did you mean to import it instead?`)); + `${refType} ${decl.node.name.text} is standalone, and cannot be declared in an NgModule. Did you mean to import it instead?`, + ), + ); isPoisoned = true; continue; } @@ -354,27 +389,29 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop } } else if (pipe !== null) { if (pipe.isStandalone) { - diagnostics.push(makeDiagnostic( + diagnostics.push( + makeDiagnostic( ErrorCode.NGMODULE_DECLARATION_IS_STANDALONE, decl.getOriginForDiagnostics(ngModule.rawDeclarations!), - `Pipe ${ - decl.node.name - .text} is standalone, and cannot be declared in an NgModule. Did you mean to import it instead?`)); + `Pipe ${decl.node.name.text} is standalone, and cannot be declared in an NgModule. Did you mean to import it instead?`, + ), + ); isPoisoned = true; continue; } compilationPipes.set(decl.node, {...pipe, ref: decl}); } else { const errorNode = decl.getOriginForDiagnostics(ngModule.rawDeclarations!); - diagnostics.push(makeDiagnostic( - ErrorCode.NGMODULE_INVALID_DECLARATION, errorNode, + diagnostics.push( + makeDiagnostic( + ErrorCode.NGMODULE_INVALID_DECLARATION, + errorNode, `The class '${decl.node.name.text}' is listed in the declarations ` + - `of the NgModule '${ - ngModule.ref.node.name - .text}', but is not a directive, a component, or a pipe. ` + - `Either remove it from the NgModule's declarations, or add an appropriate Angular decorator.`, - [makeRelatedInformation( - decl.node.name, `'${decl.node.name.text}' is declared here.`)])); + `of the NgModule '${ngModule.ref.node.name.text}', but is not a directive, a component, or a pipe. ` + + `Either remove it from the NgModule's declarations, or add an appropriate Angular decorator.`, + [makeRelatedInformation(decl.node.name, `'${decl.node.name.text}' is declared here.`)], + ), + ); isPoisoned = true; continue; } @@ -437,9 +474,13 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop isPoisoned, }; - const reexports = - this.getReexports(ngModule, ref, declared, exported.dependencies, diagnostics); - + const reexports = this.getReexports( + ngModule, + ref, + declared, + exported.dependencies, + diagnostics, + ); // Finally, produce the `LocalModuleScope` with both the compilation and export scopes. const scope: LocalModuleScope = { @@ -470,7 +511,7 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop /** * Check whether a component requires remote scoping. */ - getRemoteScope(node: ClassDeclaration): RemoteScope|null { + getRemoteScope(node: ClassDeclaration): RemoteScope | null { return this.remoteScoping.has(node) ? this.remoteScoping.get(node)! : null; } @@ -478,8 +519,11 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop * Set a component as requiring remote scoping, with the given directives and pipes to be * registered remotely. */ - setComponentRemoteScope(node: ClassDeclaration, directives: Reference[], pipes: Reference[]): - void { + setComponentRemoteScope( + node: ClassDeclaration, + directives: Reference[], + pipes: Reference[], + ): void { this.remoteScoping.set(node, {directives, pipes}); } @@ -496,19 +540,27 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop * array parameter. */ private getExportedScope( - ref: Reference, diagnostics: ts.Diagnostic[], - ownerForErrors: DeclarationNode, type: 'import'|'export'): ExportScope|null|'invalid' { + ref: Reference, + diagnostics: ts.Diagnostic[], + ownerForErrors: DeclarationNode, + type: 'import' | 'export', + ): ExportScope | null | 'invalid' { if (ref.node.getSourceFile().isDeclarationFile) { // The NgModule is declared in a .d.ts file. Resolve it with the `DependencyScopeReader`. if (!ts.isClassDeclaration(ref.node)) { // The NgModule is in a .d.ts file but is not declared as a ts.ClassDeclaration. This is an // error in the .d.ts metadata. - const code = type === 'import' ? ErrorCode.NGMODULE_INVALID_IMPORT : - ErrorCode.NGMODULE_INVALID_EXPORT; - diagnostics.push(makeDiagnostic( - code, identifierOfNode(ref.node) || ref.node, - `Appears in the NgModule.${type}s of ${ - nodeNameForError(ownerForErrors)}, but could not be resolved to an NgModule`)); + const code = + type === 'import' ? ErrorCode.NGMODULE_INVALID_IMPORT : ErrorCode.NGMODULE_INVALID_EXPORT; + diagnostics.push( + makeDiagnostic( + code, + identifierOfNode(ref.node) || ref.node, + `Appears in the NgModule.${type}s of ${nodeNameForError( + ownerForErrors, + )}, but could not be resolved to an NgModule`, + ), + ); return 'invalid'; } return this.dependencyScopeReader.resolve(ref); @@ -519,9 +571,13 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop } private getReexports( - ngModule: NgModuleMeta, ref: Reference, declared: Set, - exported: Array, diagnostics: ts.Diagnostic[]): Reexport[]|null { - let reexports: Reexport[]|null = null; + ngModule: NgModuleMeta, + ref: Reference, + declared: Set, + exported: Array, + diagnostics: ts.Diagnostic[], + ): Reexport[] | null { + let reexports: Reexport[] | null = null; const sourceFile = ref.node.getSourceFile(); if (this.aliasingHost === null) { return null; @@ -538,7 +594,11 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop } const isReExport = !declared.has(exportRef.node); const exportName = this.aliasingHost!.maybeAliasSymbolAs( - exportRef, sourceFile, ngModule.ref.node.name.text, isReExport); + exportRef, + sourceFile, + ngModule.ref.node.name.text, + isReExport, + ); if (exportName === null) { return; } @@ -553,8 +613,11 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop const emittedRef = this.refEmitter.emit(exportRef.cloneWithNoIdentifiers(), sourceFile); assertSuccessfulReferenceEmit(emittedRef, ngModuleRef.node.name, 'class'); const expr = emittedRef.expression; - if (!(expr instanceof ExternalExpr) || expr.value.moduleName === null || - expr.value.name === null) { + if ( + !(expr instanceof ExternalExpr) || + expr.value.moduleName === null || + expr.value.name === null + ) { throw new Error('Expected ExternalExpr'); } reexports!.push({ @@ -587,10 +650,12 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop * Produce a `ts.Diagnostic` for an invalid import or export from an NgModule. */ function invalidRef( - decl: Reference, rawExpr: ts.Expression|null, - type: 'import'|'export'): ts.Diagnostic { + decl: Reference, + rawExpr: ts.Expression | null, + type: 'import' | 'export', +): ts.Diagnostic { const code = - type === 'import' ? ErrorCode.NGMODULE_INVALID_IMPORT : ErrorCode.NGMODULE_INVALID_EXPORT; + type === 'import' ? ErrorCode.NGMODULE_INVALID_IMPORT : ErrorCode.NGMODULE_INVALID_EXPORT; const resolveTarget = type === 'import' ? 'NgModule' : 'NgModule, Component, Directive, or Pipe'; const message = `'${decl.node.name.text}' does not appear to be an ${resolveTarget} class.`; const library = decl.ownedByModuleGuess !== null ? ` (${decl.ownedByModuleGuess})` : ''; @@ -606,33 +671,36 @@ function invalidRef( } else if (sf.fileName.indexOf('node_modules') !== -1) { // This file comes from a third-party library in node_modules. relatedMessage = - `This likely means that the library${library} which declares ${decl.debugName} is not ` + - 'compatible with Angular Ivy. Check if a newer version of the library is available, ' + - 'and update if so. Also consider checking with the library\'s authors to see if the ' + - 'library is expected to be compatible with Ivy.'; + `This likely means that the library${library} which declares ${decl.debugName} is not ` + + 'compatible with Angular Ivy. Check if a newer version of the library is available, ' + + "and update if so. Also consider checking with the library's authors to see if the " + + 'library is expected to be compatible with Ivy.'; } else { // This is a monorepo style local dependency. Unfortunately these are too different to really // offer much more advice than this. - relatedMessage = `This likely means that the dependency${library} which declares ${ - decl.debugName} is not compatible with Angular Ivy.`; + relatedMessage = `This likely means that the dependency${library} which declares ${decl.debugName} is not compatible with Angular Ivy.`; } - return makeDiagnostic( - code, getDiagnosticNode(decl, rawExpr), message, - [makeRelatedInformation(decl.node.name, relatedMessage)]); + return makeDiagnostic(code, getDiagnosticNode(decl, rawExpr), message, [ + makeRelatedInformation(decl.node.name, relatedMessage), + ]); } /** * Produce a `ts.Diagnostic` for an import or export which itself has errors. */ function invalidTransitiveNgModuleRef( - decl: Reference, rawExpr: ts.Expression|null, - type: 'import'|'export'): ts.Diagnostic { + decl: Reference, + rawExpr: ts.Expression | null, + type: 'import' | 'export', +): ts.Diagnostic { const code = - type === 'import' ? ErrorCode.NGMODULE_INVALID_IMPORT : ErrorCode.NGMODULE_INVALID_EXPORT; + type === 'import' ? ErrorCode.NGMODULE_INVALID_IMPORT : ErrorCode.NGMODULE_INVALID_EXPORT; return makeDiagnostic( - code, getDiagnosticNode(decl, rawExpr), - `This ${type} contains errors, which may affect components that depend on this NgModule.`); + code, + getDiagnosticNode(decl, rawExpr), + `This ${type} contains errors, which may affect components that depend on this NgModule.`, + ); } /** @@ -640,8 +708,10 @@ function invalidTransitiveNgModuleRef( * by the NgModule in question. */ function invalidReexport( - decl: Reference, rawExpr: ts.Expression|null, - isStandalone: boolean): ts.Diagnostic { + decl: Reference, + rawExpr: ts.Expression | null, + isStandalone: boolean, +): ts.Diagnostic { // The root error is the same here - this export is not valid. Give a helpful error message based // on the specific circumstance. let message = `Can't be exported from this NgModule, as `; @@ -656,38 +726,43 @@ function invalidReexport( // Local non-standalone types must either be declared directly by this NgModule, or imported as // above. message += - 'it must be either declared by this NgModule, or imported here via its NgModule first'; + 'it must be either declared by this NgModule, or imported here via its NgModule first'; } return makeDiagnostic( - ErrorCode.NGMODULE_INVALID_REEXPORT, getDiagnosticNode(decl, rawExpr), message); + ErrorCode.NGMODULE_INVALID_REEXPORT, + getDiagnosticNode(decl, rawExpr), + message, + ); } /** * Produce a `ts.Diagnostic` for a collision in re-export names between two directives/pipes. */ function reexportCollision( - module: ClassDeclaration, refA: Reference, - refB: Reference): ts.Diagnostic { - const childMessageText = `This directive/pipe is part of the exports of '${ - module.name.text}' and shares the same name as another exported directive/pipe.`; + module: ClassDeclaration, + refA: Reference, + refB: Reference, +): ts.Diagnostic { + const childMessageText = `This directive/pipe is part of the exports of '${module.name.text}' and shares the same name as another exported directive/pipe.`; return makeDiagnostic( - ErrorCode.NGMODULE_REEXPORT_NAME_COLLISION, module.name, - ` - There was a name collision between two classes named '${ - refA.node.name.text}', which are both part of the exports of '${module.name.text}'. + ErrorCode.NGMODULE_REEXPORT_NAME_COLLISION, + module.name, + ` + There was a name collision between two classes named '${refA.node.name.text}', which are both part of the exports of '${module.name.text}'. Angular generates re-exports of an NgModule's exported directives/pipes from the module's source file in certain cases, using the declared name of the class. If two classes of the same name are exported, this automatic naming does not work. To fix this problem please re-export one or both classes directly from this file. `.trim(), - [ - makeRelatedInformation(refA.node.name, childMessageText), - makeRelatedInformation(refB.node.name, childMessageText), - ]); + [ + makeRelatedInformation(refA.node.name, childMessageText), + makeRelatedInformation(refB.node.name, childMessageText), + ], + ); } export interface DeclarationData { ngModule: ClassDeclaration; ref: Reference; - rawDeclarations: ts.Expression|null; + rawDeclarations: ts.Expression | null; } diff --git a/packages/compiler-cli/src/ngtsc/scope/src/standalone.ts b/packages/compiler-cli/src/ngtsc/scope/src/standalone.ts index 96feae46fad77..b5c4b8836579c 100644 --- a/packages/compiler-cli/src/ngtsc/scope/src/standalone.ts +++ b/packages/compiler-cli/src/ngtsc/scope/src/standalone.ts @@ -10,7 +10,13 @@ import {Reference} from '../../imports'; import {DirectiveMeta, MetadataReader, NgModuleMeta, PipeMeta} from '../../metadata'; import {ClassDeclaration} from '../../reflection'; -import {ComponentScopeKind, ComponentScopeReader, ExportScope, RemoteScope, StandaloneScope} from './api'; +import { + ComponentScopeKind, + ComponentScopeReader, + ExportScope, + RemoteScope, + StandaloneScope, +} from './api'; import {DtsModuleScopeResolver} from './dependency'; import {LocalModuleScopeRegistry} from './local'; @@ -19,13 +25,15 @@ import {LocalModuleScopeRegistry} from './local'; * scopes where necessary. */ export class StandaloneComponentScopeReader implements ComponentScopeReader { - private cache = new Map(); + private cache = new Map(); constructor( - private metaReader: MetadataReader, private localModuleReader: LocalModuleScopeRegistry, - private dtsModuleReader: DtsModuleScopeResolver) {} + private metaReader: MetadataReader, + private localModuleReader: LocalModuleScopeRegistry, + private dtsModuleReader: DtsModuleScopeResolver, + ) {} - getScopeForComponent(clazz: ClassDeclaration): StandaloneScope|null { + getScopeForComponent(clazz: ClassDeclaration): StandaloneScope | null { if (!this.cache.has(clazz)) { const clazzRef = new Reference(clazz); const clazzMeta = this.metaReader.getDirectiveMetadata(clazzRef); @@ -37,8 +45,8 @@ export class StandaloneComponentScopeReader implements ComponentScopeReader { // A standalone component always has itself in scope, so add `clazzMeta` during // initialization. - const dependencies = new Set([clazzMeta]); - const deferredDependencies = new Set(); + const dependencies = new Set([clazzMeta]); + const deferredDependencies = new Set(); const seen = new Set([clazz]); let isPoisoned = clazzMeta.isPoisoned; @@ -67,7 +75,7 @@ export class StandaloneComponentScopeReader implements ComponentScopeReader { if (ngModuleMeta !== null) { dependencies.add({...ngModuleMeta, ref}); - let ngModuleScope: ExportScope|null; + let ngModuleScope: ExportScope | null; if (ref.node.getSourceFile().isDeclarationFile) { ngModuleScope = this.dtsModuleReader.resolve(ref); } else { diff --git a/packages/compiler-cli/src/ngtsc/scope/src/typecheck.ts b/packages/compiler-cli/src/ngtsc/scope/src/typecheck.ts index c7adfa051b078..6994ed50e45af 100644 --- a/packages/compiler-cli/src/ngtsc/scope/src/typecheck.ts +++ b/packages/compiler-cli/src/ngtsc/scope/src/typecheck.ts @@ -10,7 +10,14 @@ import {CssSelector, SchemaMetadata, SelectorMatcher} from '@angular/compiler'; import ts from 'typescript'; import {Reference} from '../../imports'; -import {DirectiveMeta, flattenInheritedDirectiveMetadata, HostDirectivesResolver, MetadataReader, MetaKind, PipeMeta} from '../../metadata'; +import { + DirectiveMeta, + flattenInheritedDirectiveMetadata, + HostDirectivesResolver, + MetadataReader, + MetaKind, + PipeMeta, +} from '../../metadata'; import {ClassDeclaration} from '../../reflection'; import {ComponentScopeKind, ComponentScopeReader} from './api'; @@ -63,8 +70,10 @@ export class TypeCheckScopeRegistry { private scopeCache = new Map(); constructor( - private scopeReader: ComponentScopeReader, private metaReader: MetadataReader, - private hostDirectivesResolver: HostDirectivesResolver) {} + private scopeReader: ComponentScopeReader, + private metaReader: MetadataReader, + private hostDirectivesResolver: HostDirectivesResolver, + ) {} /** * Computes the type-check scope information for the component declaration. If the NgModule @@ -96,8 +105,11 @@ export class TypeCheckScopeRegistry { } let allDependencies = dependencies; - if (!isNgModuleScope && Array.isArray(scope.deferredDependencies) && - scope.deferredDependencies.length > 0) { + if ( + !isNgModuleScope && + Array.isArray(scope.deferredDependencies) && + scope.deferredDependencies.length > 0 + ) { allDependencies = [...allDependencies, ...scope.deferredDependencies]; } for (const meta of allDependencies) { @@ -109,15 +121,19 @@ export class TypeCheckScopeRegistry { // Carry over the `isExplicitlyDeferred` flag from the dependency info. const directiveMeta = this.applyExplicitlyDeferredFlag(extMeta, meta.isExplicitlyDeferred); - matcher.addSelectables( - CssSelector.parse(meta.selector), - [...this.hostDirectivesResolver.resolve(directiveMeta), directiveMeta]); + matcher.addSelectables(CssSelector.parse(meta.selector), [ + ...this.hostDirectivesResolver.resolve(directiveMeta), + directiveMeta, + ]); directives.push(directiveMeta); } else if (meta.kind === MetaKind.Pipe) { if (!ts.isClassDeclaration(meta.ref.node)) { - throw new Error(`Unexpected non-class declaration ${ - ts.SyntaxKind[meta.ref.node.kind]} for pipe ${meta.ref.debugName}`); + throw new Error( + `Unexpected non-class declaration ${ + ts.SyntaxKind[meta.ref.node.kind] + } for pipe ${meta.ref.debugName}`, + ); } pipes.set(meta.name, meta); } @@ -128,15 +144,16 @@ export class TypeCheckScopeRegistry { directives, pipes, schemas: scope.schemas, - isPoisoned: scope.kind === ComponentScopeKind.NgModule ? - scope.compilation.isPoisoned || scope.exported.isPoisoned : - scope.isPoisoned, + isPoisoned: + scope.kind === ComponentScopeKind.NgModule + ? scope.compilation.isPoisoned || scope.exported.isPoisoned + : scope.isPoisoned, }; this.scopeCache.set(cacheKey, typeCheckScope); return typeCheckScope; } - getTypeCheckDirectiveMetadata(ref: Reference): DirectiveMeta|null { + getTypeCheckDirectiveMetadata(ref: Reference): DirectiveMeta | null { const clazz = ref.node; if (this.flattenedDirectiveMetaCache.has(clazz)) { return this.flattenedDirectiveMetaCache.get(clazz)!; @@ -150,8 +167,10 @@ export class TypeCheckScopeRegistry { return meta; } - private applyExplicitlyDeferredFlag( - meta: T, isExplicitlyDeferred: boolean): T { + private applyExplicitlyDeferredFlag( + meta: T, + isExplicitlyDeferred: boolean, + ): T { return isExplicitlyDeferred === true ? {...meta, isExplicitlyDeferred} : meta; } } diff --git a/packages/compiler-cli/src/ngtsc/scope/src/util.ts b/packages/compiler-cli/src/ngtsc/scope/src/util.ts index 8faf74298a924..87ea2349969ca 100644 --- a/packages/compiler-cli/src/ngtsc/scope/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/scope/src/util.ts @@ -15,7 +15,9 @@ import {ClassDeclaration} from '../../reflection'; import {ComponentScopeKind, ComponentScopeReader} from './api'; export function getDiagnosticNode( - ref: Reference, rawExpr: ts.Expression|null): ts.Expression { + ref: Reference, + rawExpr: ts.Expression | null, +): ts.Expression { // Show the diagnostic on the node within `rawExpr` which references the declaration // in question. `rawExpr` represents the raw expression from which `ref` was partially evaluated, // so use that to find the right node. Note that by the type system, `rawExpr` might be `null`, so @@ -25,21 +27,22 @@ export function getDiagnosticNode( } export function makeNotStandaloneDiagnostic( - scopeReader: ComponentScopeReader, ref: Reference, - rawExpr: ts.Expression|null, kind: 'component'|'directive'|'pipe'): ts.Diagnostic { + scopeReader: ComponentScopeReader, + ref: Reference, + rawExpr: ts.Expression | null, + kind: 'component' | 'directive' | 'pipe', +): ts.Diagnostic { const scope = scopeReader.getScopeForComponent(ref.node); - let message = `The ${kind} '${ - ref.node.name - .text}' appears in 'imports', but is not standalone and cannot be imported directly.`; - let relatedInformation: ts.DiagnosticRelatedInformation[]|undefined = undefined; + let message = `The ${kind} '${ref.node.name.text}' appears in 'imports', but is not standalone and cannot be imported directly.`; + let relatedInformation: ts.DiagnosticRelatedInformation[] | undefined = undefined; if (scope !== null && scope.kind === ComponentScopeKind.NgModule) { // The directive/pipe in question is declared in an NgModule. Check if it's also exported. - const isExported = scope.exported.dependencies.some(dep => dep.ref.node === ref.node); - const relatedInfoMessageText = isExported ? - `It can be imported using its '${scope.ngModule.name.text}' NgModule instead.` : - `It's declared in the '${scope.ngModule.name.text}' NgModule, but is not exported. ` + - 'Consider exporting it and importing the NgModule instead.'; + const isExported = scope.exported.dependencies.some((dep) => dep.ref.node === ref.node); + const relatedInfoMessageText = isExported + ? `It can be imported using its '${scope.ngModule.name.text}' NgModule instead.` + : `It's declared in the '${scope.ngModule.name.text}' NgModule, but is not exported. ` + + 'Consider exporting it and importing the NgModule instead.'; relatedInformation = [makeRelatedInformation(scope.ngModule.name, relatedInfoMessageText)]; } else { // TODO(alxhub): the above case handles directives/pipes in NgModules that are declared in the @@ -52,20 +55,31 @@ export function makeNotStandaloneDiagnostic( message += ' It must be imported via an NgModule.'; } return makeDiagnostic( - ErrorCode.COMPONENT_IMPORT_NOT_STANDALONE, getDiagnosticNode(ref, rawExpr), message, - relatedInformation); + ErrorCode.COMPONENT_IMPORT_NOT_STANDALONE, + getDiagnosticNode(ref, rawExpr), + message, + relatedInformation, + ); } export function makeUnknownComponentImportDiagnostic( - ref: Reference, rawExpr: ts.Expression) { + ref: Reference, + rawExpr: ts.Expression, +) { return makeDiagnostic( - ErrorCode.COMPONENT_UNKNOWN_IMPORT, getDiagnosticNode(ref, rawExpr), - `Component imports must be standalone components, directives, pipes, or must be NgModules.`); + ErrorCode.COMPONENT_UNKNOWN_IMPORT, + getDiagnosticNode(ref, rawExpr), + `Component imports must be standalone components, directives, pipes, or must be NgModules.`, + ); } export function makeUnknownComponentDeferredImportDiagnostic( - ref: Reference, rawExpr: ts.Expression) { + ref: Reference, + rawExpr: ts.Expression, +) { return makeDiagnostic( - ErrorCode.COMPONENT_UNKNOWN_DEFERRED_IMPORT, getDiagnosticNode(ref, rawExpr), - `Component deferred imports must be standalone components, directives or pipes.`); + ErrorCode.COMPONENT_UNKNOWN_DEFERRED_IMPORT, + getDiagnosticNode(ref, rawExpr), + `Component deferred imports must be standalone components, directives or pipes.`, + ); } diff --git a/packages/compiler-cli/src/ngtsc/scope/test/dependency_spec.ts b/packages/compiler-cli/src/ngtsc/scope/test/dependency_spec.ts index 8094a6ea2b894..16c1dadfba22f 100644 --- a/packages/compiler-cli/src/ngtsc/scope/test/dependency_spec.ts +++ b/packages/compiler-cli/src/ngtsc/scope/test/dependency_spec.ts @@ -21,10 +21,10 @@ import {MetadataDtsModuleScopeResolver} from '../src/dependency'; const MODULE_FROM_NODE_MODULES_PATH = /.*node_modules\/(\w+)\/index\.d\.ts$/; const testHost: UnifiedModulesHost = { - fileNameToModuleName: function(imported: string): string { + fileNameToModuleName: function (imported: string): string { const res = MODULE_FROM_NODE_MODULES_PATH.exec(imported)!; return 'root/' + res[1]; - } + }, }; /** @@ -44,12 +44,14 @@ export declare type PipeMeta = never; * destructured to retrieve references to specific declared classes. */ function makeTestEnv( - modules: {[module: string]: string}, aliasGenerator: AliasingHost|null = null): { - refs: {[name: string]: Reference}, - resolver: MetadataDtsModuleScopeResolver, + modules: {[module: string]: string}, + aliasGenerator: AliasingHost | null = null, +): { + refs: {[name: string]: Reference}; + resolver: MetadataDtsModuleScopeResolver; } { // Map the modules object to an array of files for `makeProgram`. - const files = Object.keys(modules).map(moduleName => { + const files = Object.keys(modules).map((moduleName) => { return { name: absoluteFrom(`/node_modules/${moduleName}/index.d.ts`), contents: PROLOG + (modules as any)[moduleName], @@ -58,8 +60,10 @@ function makeTestEnv( const {program} = makeProgram(files); const checker = program.getTypeChecker(); const reflector = new TypeScriptReflectionHost(checker); - const resolver = - new MetadataDtsModuleScopeResolver(new DtsMetadataReader(checker, reflector), aliasGenerator); + const resolver = new MetadataDtsModuleScopeResolver( + new DtsMetadataReader(checker, reflector), + aliasGenerator, + ); // Resolver for the refs object. const get = (target: {}, name: string): Reference => { @@ -93,7 +97,7 @@ runInEachFileSystem(() => { export declare class Module { static ɵmod: ModuleMeta; } - ` + `, }); const {Dir, Module} = refs; const scope = resolver.resolve(Module)!; @@ -114,7 +118,7 @@ runInEachFileSystem(() => { export declare class ModuleB { static ɵmod: ModuleMeta; } - ` + `, }); const {Dir, ModuleB} = refs; const scope = resolver.resolve(ModuleB)!; @@ -138,11 +142,11 @@ runInEachFileSystem(() => { export declare class ModuleB { static ɵmod: ModuleMeta; } - ` + `, }); const {Dir, ModuleB} = refs; const scope = resolver.resolve(ModuleB)!; - expect(scopeToRefs(scope).map(ref => ref.node)).toEqual([Dir.node]); + expect(scopeToRefs(scope).map((ref) => ref.node)).toEqual([Dir.node]); // Explicitly verify that the directive has the correct owning module. expect(scope.exported.dependencies[0].ref.bestGuessOwningModule).toEqual({ @@ -153,8 +157,8 @@ runInEachFileSystem(() => { it('should write correct aliases for deep dependencies', () => { const {resolver, refs} = makeTestEnv( - { - 'deep': ` + { + 'deep': ` export declare class DeepDir { static ɵdir: DirectiveMeta; } @@ -163,7 +167,7 @@ runInEachFileSystem(() => { static ɵmod: ModuleMeta; } `, - 'middle': ` + 'middle': ` import * as deep from 'deep'; export declare class MiddleDir { @@ -174,7 +178,7 @@ runInEachFileSystem(() => { static ɵmod: ModuleMeta; } `, - 'shallow': ` + 'shallow': ` import * as middle from 'middle'; export declare class ShallowDir { @@ -185,8 +189,9 @@ runInEachFileSystem(() => { static ɵmod: ModuleMeta; } `, - }, - new UnifiedModulesAliasingHost(testHost)); + }, + new UnifiedModulesAliasingHost(testHost), + ); const {ShallowModule} = refs; const scope = resolver.resolve(ShallowModule)!; const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope); @@ -203,8 +208,8 @@ runInEachFileSystem(() => { it('should write correct aliases for bare directives in exports', () => { const {resolver, refs} = makeTestEnv( - { - 'deep': ` + { + 'deep': ` export declare class DeepDir { static ɵdir: DirectiveMeta; } @@ -213,7 +218,7 @@ runInEachFileSystem(() => { static ɵmod: ModuleMeta; } `, - 'middle': ` + 'middle': ` import * as deep from 'deep'; export declare class MiddleDir { @@ -224,7 +229,7 @@ runInEachFileSystem(() => { static ɵmod: ModuleMeta; } `, - 'shallow': ` + 'shallow': ` import * as middle from 'middle'; export declare class ShallowDir { @@ -235,8 +240,9 @@ runInEachFileSystem(() => { static ɵmod: ModuleMeta; } `, - }, - new UnifiedModulesAliasingHost(testHost)); + }, + new UnifiedModulesAliasingHost(testHost), + ); const {ShallowModule} = refs; const scope = resolver.resolve(ShallowModule)!; const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope); @@ -251,11 +257,10 @@ runInEachFileSystem(() => { expect(getAlias(ShallowDir)).toBeNull(); }); - it('should not use an alias if a directive is declared in the same file as the re-exporting module', - () => { - const {resolver, refs} = makeTestEnv( - { - 'module': ` + it('should not use an alias if a directive is declared in the same file as the re-exporting module', () => { + const {resolver, refs} = makeTestEnv( + { + 'module': ` export declare class DeepDir { static ɵdir: DirectiveMeta; } @@ -268,21 +273,23 @@ runInEachFileSystem(() => { static ɵmod: ModuleMeta; } `, - }, - new UnifiedModulesAliasingHost(testHost)); - const {DeepExportModule} = refs; - const scope = resolver.resolve(DeepExportModule)!; - const [DeepDir] = scopeToRefs(scope); - expect(getAlias(DeepDir)).toBeNull(); - }); + }, + new UnifiedModulesAliasingHost(testHost), + ); + const {DeepExportModule} = refs; + const scope = resolver.resolve(DeepExportModule)!; + const [DeepDir] = scopeToRefs(scope); + expect(getAlias(DeepDir)).toBeNull(); + }); }); function scopeToRefs(scope: ExportScope): Reference[] { - return scope.exported.dependencies.map(dep => dep.ref) - .sort((a, b) => a.debugName!.localeCompare(b.debugName!)); + return scope.exported.dependencies + .map((dep) => dep.ref) + .sort((a, b) => a.debugName!.localeCompare(b.debugName!)); } - function getAlias(ref: Reference): ExternalReference|null { + function getAlias(ref: Reference): ExternalReference | null { if (ref.alias === null) { return null; } else { diff --git a/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts b/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts index bdc2f0dc6ac41..26e1bf8c50a4d 100644 --- a/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts +++ b/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts @@ -9,17 +9,32 @@ import ts from 'typescript'; import {Reference, ReferenceEmitter} from '../../imports'; -import {ClassPropertyMapping, CompoundMetadataRegistry, DirectiveMeta, LocalMetadataRegistry, MatchSource, MetadataRegistry, MetaKind, PipeMeta} from '../../metadata'; +import { + ClassPropertyMapping, + CompoundMetadataRegistry, + DirectiveMeta, + LocalMetadataRegistry, + MatchSource, + MetadataRegistry, + MetaKind, + PipeMeta, +} from '../../metadata'; import {ClassDeclaration} from '../../reflection'; import {LocalModuleScope, ScopeData} from '../src/api'; import {DtsModuleScopeResolver} from '../src/dependency'; import {LocalModuleScopeRegistry} from '../src/local'; -function registerFakeRefs(registry: MetadataRegistry): - {[name: string]: Reference} { +function registerFakeRefs(registry: MetadataRegistry): { + [name: string]: Reference; +} { const get = (target: {}, name: string): Reference => { const sf = ts.createSourceFile( - name + '.ts', `export class ${name} {}`, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); + name + '.ts', + `export class ${name} {}`, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TS, + ); const clazz = sf.statements[0] as unknown as ClassDeclaration; const ref = new Reference(clazz); if (name.startsWith('Dir') || name.startsWith('Cmp')) { @@ -40,7 +55,12 @@ describe('LocalModuleScopeRegistry', () => { beforeEach(() => { const localRegistry = new LocalMetadataRegistry(); scopeRegistry = new LocalModuleScopeRegistry( - localRegistry, localRegistry, new MockDtsModuleScopeResolver(), refEmitter, null); + localRegistry, + localRegistry, + new MockDtsModuleScopeResolver(), + refEmitter, + null, + ); metaRegistry = new CompoundMetadataRegistry([localRegistry, scopeRegistry]); }); @@ -225,7 +245,7 @@ describe('LocalModuleScopeRegistry', () => { expect(scope.compilation.dependencies[0].ref.getIdentityIn(idSf)).toBe(id); }); - it('should allow directly exporting a directive that\'s not imported', () => { + it("should allow directly exporting a directive that's not imported", () => { const {Dir, ModuleA, ModuleB} = registerFakeRefs(metaRegistry); metaRegistry.registerNgModuleMetadata({ @@ -259,7 +279,7 @@ describe('LocalModuleScopeRegistry', () => { expect(scopeToRefs(scopeA.exported)).toEqual([Dir]); }); - it('should not allow directly exporting a directive that\'s not imported', () => { + it("should not allow directly exporting a directive that's not imported", () => { const {Dir, ModuleA, ModuleB} = registerFakeRefs(metaRegistry); metaRegistry.registerNgModuleMetadata({ @@ -357,6 +377,7 @@ class MockDtsModuleScopeResolver implements DtsModuleScopeResolver { } function scopeToRefs(scopeData: ScopeData): Reference[] { - return scopeData.dependencies.map(dep => dep.ref) - .sort((a, b) => a.debugName!.localeCompare(b.debugName!)); + return scopeData.dependencies + .map((dep) => dep.ref) + .sort((a, b) => a.debugName!.localeCompare(b.debugName!)); } diff --git a/packages/compiler-cli/src/ngtsc/shims/api.ts b/packages/compiler-cli/src/ngtsc/shims/api.ts index cf27a1cc760cb..724dbd39b93c4 100644 --- a/packages/compiler-cli/src/ngtsc/shims/api.ts +++ b/packages/compiler-cli/src/ngtsc/shims/api.ts @@ -47,6 +47,8 @@ export interface PerFileShimGenerator { * Generate the shim for a given original `ts.SourceFile`, with the given filename. */ generateShimForFile( - sf: ts.SourceFile, genFilePath: AbsoluteFsPath, - priorShimSf: ts.SourceFile|null): ts.SourceFile; + sf: ts.SourceFile, + genFilePath: AbsoluteFsPath, + priorShimSf: ts.SourceFile | null, + ): ts.SourceFile; } diff --git a/packages/compiler-cli/src/ngtsc/shims/index.ts b/packages/compiler-cli/src/ngtsc/shims/index.ts index 16b9a81cfd8a0..b4c60d44144a8 100644 --- a/packages/compiler-cli/src/ngtsc/shims/index.ts +++ b/packages/compiler-cli/src/ngtsc/shims/index.ts @@ -9,5 +9,13 @@ /// export {ShimAdapter} from './src/adapter'; -export {copyFileShimData, isShim, retagAllTsFiles, retagTsFile, sfExtensionData, untagAllTsFiles, untagTsFile} from './src/expando'; +export { + copyFileShimData, + isShim, + retagAllTsFiles, + retagTsFile, + sfExtensionData, + untagAllTsFiles, + untagTsFile, +} from './src/expando'; export {ShimReferenceTagger} from './src/reference_tagger'; diff --git a/packages/compiler-cli/src/ngtsc/shims/src/adapter.ts b/packages/compiler-cli/src/ngtsc/shims/src/adapter.ts index 70871f1718340..a3bc70036fc3f 100644 --- a/packages/compiler-cli/src/ngtsc/shims/src/adapter.ts +++ b/packages/compiler-cli/src/ngtsc/shims/src/adapter.ts @@ -76,9 +76,12 @@ export class ShimAdapter { readonly extensionPrefixes: string[] = []; constructor( - private delegate: Pick, - tsRootFiles: AbsoluteFsPath[], topLevelGenerators: TopLevelShimGenerator[], - perFileGenerators: PerFileShimGenerator[], oldProgram: ts.Program|null) { + private delegate: Pick, + tsRootFiles: AbsoluteFsPath[], + topLevelGenerators: TopLevelShimGenerator[], + perFileGenerators: PerFileShimGenerator[], + oldProgram: ts.Program | null, + ) { // Initialize `this.generators` with a regex that matches each generator's paths. for (const gen of perFileGenerators) { // This regex matches paths for shims from this generator. The first (and only) capture group @@ -141,7 +144,7 @@ export class ShimAdapter { * If `fileName` does not refer to a potential shim file, `null` is returned. If a corresponding * base file could not be determined, `undefined` is returned instead. */ - maybeGenerate(fileName: AbsoluteFsPath): ts.SourceFile|null|undefined { + maybeGenerate(fileName: AbsoluteFsPath): ts.SourceFile | null | undefined { // Fast path: either this filename has been proven not to be a shim before, or it is a known // shim and no generation is required. if (this.notShims.has(fileName)) { @@ -201,9 +204,11 @@ export class ShimAdapter { } private generateSpecific( - fileName: AbsoluteFsPath, generator: PerFileShimGenerator, - inputFile: ts.SourceFile): ts.SourceFile { - let priorShimSf: ts.SourceFile|null = null; + fileName: AbsoluteFsPath, + generator: PerFileShimGenerator, + inputFile: ts.SourceFile, + ): ts.SourceFile { + let priorShimSf: ts.SourceFile | null = null; if (this.priorShims.has(fileName)) { // In the previous program a shim with this name already existed. It's passed to the shim // generator which may reuse it instead of generating a fresh shim. diff --git a/packages/compiler-cli/src/ngtsc/shims/src/expando.ts b/packages/compiler-cli/src/ngtsc/shims/src/expando.ts index 3868bda326bc6..56bbdf924e5d9 100644 --- a/packages/compiler-cli/src/ngtsc/shims/src/expando.ts +++ b/packages/compiler-cli/src/ngtsc/shims/src/expando.ts @@ -20,17 +20,17 @@ export const NgExtension = Symbol('NgExtension'); */ export interface NgExtensionData { isTopLevelShim: boolean; - fileShim: NgFileShimData|null; + fileShim: NgFileShimData | null; /** * The contents of the `referencedFiles` array, before modification by a `ShimReferenceTagger`. */ - originalReferencedFiles: ReadonlyArray|null; + originalReferencedFiles: ReadonlyArray | null; /** * The contents of the `referencedFiles` array, after modification by a `ShimReferenceTagger`. */ - taggedReferenceFiles: ReadonlyArray|null; + taggedReferenceFiles: ReadonlyArray | null; } /** @@ -92,8 +92,8 @@ export interface NgFileShimData { * An `NgExtendedSourceFile` that is a per-file shim and has `NgFileShimData`. */ export interface NgFileShimSourceFile extends NgExtendedSourceFile { - [NgExtension]: NgExtensionData&{ - fileShim: NgFileShimData, + [NgExtension]: NgExtensionData & { + fileShim: NgFileShimData; }; } diff --git a/packages/compiler-cli/src/ngtsc/shims/src/reference_tagger.ts b/packages/compiler-cli/src/ngtsc/shims/src/reference_tagger.ts index 7fed2d506d146..b39f57f44d914 100644 --- a/packages/compiler-cli/src/ngtsc/shims/src/reference_tagger.ts +++ b/packages/compiler-cli/src/ngtsc/shims/src/reference_tagger.ts @@ -36,15 +36,20 @@ export class ShimReferenceTagger { private enabled: boolean = true; constructor(shimExtensions: string[]) { - this.suffixes = shimExtensions.map(extension => `.${extension}.ts`); + this.suffixes = shimExtensions.map((extension) => `.${extension}.ts`); } /** * Tag `sf` with any needed references if it's not a shim itself. */ tag(sf: ts.SourceFile): void { - if (!this.enabled || sf.isDeclarationFile || isShim(sf) || this.tagged.has(sf) || - !isNonDeclarationTsPath(sf.fileName)) { + if ( + !this.enabled || + sf.isDeclarationFile || + isShim(sf) || + this.tagged.has(sf) || + !isNonDeclarationTsPath(sf.fileName) + ) { return; } @@ -58,7 +63,6 @@ export class ShimReferenceTagger { const referencedFiles = [...ext.originalReferencedFiles]; - const sfPath = absoluteFromSourceFile(sf); for (const suffix of this.suffixes) { referencedFiles.push({ diff --git a/packages/compiler-cli/src/ngtsc/shims/src/util.ts b/packages/compiler-cli/src/ngtsc/shims/src/util.ts index 77407e54a1f60..e958a8e7286a3 100644 --- a/packages/compiler-cli/src/ngtsc/shims/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/shims/src/util.ts @@ -18,7 +18,10 @@ export function makeShimFileName(fileName: AbsoluteFsPath, suffix: string): Abso } export function generatedModuleName( - originalModuleName: string, originalFileName: string, genSuffix: string): string { + originalModuleName: string, + originalFileName: string, + genSuffix: string, +): string { let moduleName: string; if (originalFileName.endsWith('/index.ts')) { moduleName = originalModuleName + '/index' + genSuffix; diff --git a/packages/compiler-cli/src/ngtsc/shims/test/adapter_spec.ts b/packages/compiler-cli/src/ngtsc/shims/test/adapter_spec.ts index 312325107bd18..b92e12f5015d8 100644 --- a/packages/compiler-cli/src/ngtsc/shims/test/adapter_spec.ts +++ b/packages/compiler-cli/src/ngtsc/shims/test/adapter_spec.ts @@ -17,13 +17,20 @@ import {TestShimGenerator} from './util'; runInEachFileSystem(() => { describe('ShimAdapter', () => { it('should recognize a basic shim name', () => { - const {host} = makeProgram([{ - name: _('/test.ts'), - contents: `export class A {}`, - }]); + const {host} = makeProgram([ + { + name: _('/test.ts'), + contents: `export class A {}`, + }, + ]); - const adapter = - new ShimAdapter(host, [], [], [new TestShimGenerator()], /* oldProgram */ null); + const adapter = new ShimAdapter( + host, + [], + [], + [new TestShimGenerator()], + /* oldProgram */ null, + ); const shimSf = adapter.maybeGenerate(_('/test.testshim.ts')); expect(shimSf).not.toBeNull(); expect(shimSf!.fileName).toBe(_('/test.testshim.ts')); @@ -31,25 +38,39 @@ runInEachFileSystem(() => { }); it('should not recognize a normal file in the program', () => { - const {host} = makeProgram([{ - name: _('/test.ts'), - contents: `export class A {}`, - }]); + const {host} = makeProgram([ + { + name: _('/test.ts'), + contents: `export class A {}`, + }, + ]); - const adapter = - new ShimAdapter(host, [], [], [new TestShimGenerator()], /* oldProgram */ null); + const adapter = new ShimAdapter( + host, + [], + [], + [new TestShimGenerator()], + /* oldProgram */ null, + ); const shimSf = adapter.maybeGenerate(_('/test.ts')); expect(shimSf).toBeNull(); }); it('should not recognize a shim-named file without a source file', () => { - const {host} = makeProgram([{ - name: _('/test.ts'), - contents: `export class A {}`, - }]); + const {host} = makeProgram([ + { + name: _('/test.ts'), + contents: `export class A {}`, + }, + ]); - const adapter = - new ShimAdapter(host, [], [], [new TestShimGenerator()], /* oldProgram */ null); + const adapter = new ShimAdapter( + host, + [], + [], + [new TestShimGenerator()], + /* oldProgram */ null, + ); const shimSf = adapter.maybeGenerate(_('/other.testshim.ts')); // Expect undefined, not null, since that indicates a valid shim path but an invalid source @@ -68,8 +89,13 @@ runInEachFileSystem(() => { contents: `export class A {}`, }, ]); - const adapter = - new ShimAdapter(host, [], [], [new TestShimGenerator()], /* oldProgram */ null); + const adapter = new ShimAdapter( + host, + [], + [], + [new TestShimGenerator()], + /* oldProgram */ null, + ); const originalShim = adapter.maybeGenerate(_('/test.testshim.ts'))!; const oldProgramStub = { getSourceFiles: () => [...program.getSourceFiles(), originalShim], diff --git a/packages/compiler-cli/src/ngtsc/shims/test/reference_tagger_spec.ts b/packages/compiler-cli/src/ngtsc/shims/test/reference_tagger_spec.ts index 0e70e94d66fa7..fbb7cb18db332 100644 --- a/packages/compiler-cli/src/ngtsc/shims/test/reference_tagger_spec.ts +++ b/packages/compiler-cli/src/ngtsc/shims/test/reference_tagger_spec.ts @@ -58,8 +58,13 @@ runInEachFileSystem(() => { const {host} = makeProgram([ {name: fileName, contents: 'export declare const UNIMPORTANT = true;'}, ]); - const shimAdapter = - new ShimAdapter(host, [], [], [new TestShimGenerator()], /* oldProgram */ null); + const shimAdapter = new ShimAdapter( + host, + [], + [], + [new TestShimGenerator()], + /* oldProgram */ null, + ); const shimSf = shimAdapter.maybeGenerate(_('/file.testshim.ts'))!; expect(shimSf.referencedFiles).toEqual([]); @@ -84,11 +89,13 @@ runInEachFileSystem(() => { const fileName = _('/file.ts'); const sf = makeArbitrarySf(fileName); - sf.referencedFiles = [{ - fileName: _('/other.ts'), - pos: 0, - end: 0, - }]; + sf.referencedFiles = [ + { + fileName: _('/other.ts'), + pos: 0, + end: 0, + }, + ]; tagger.tag(sf); expectReferencedFiles(sf, ['/other.ts', '/file.test.ts']); @@ -112,11 +119,13 @@ runInEachFileSystem(() => { const fileName = _('/file.ts'); const sf = makeArbitrarySf(fileName); - sf.referencedFiles = [{ - fileName: _('/other.ts'), - pos: 0, - end: 0, - }]; + sf.referencedFiles = [ + { + fileName: _('/other.ts'), + pos: 0, + end: 0, + }, + ]; tagger.tag(sf); expectReferencedFiles(sf, ['/other.ts', '/file.test.ts']); @@ -141,7 +150,7 @@ function makeArbitrarySf(fileName: AbsoluteFsPath): ts.SourceFile { } function expectReferencedFiles(sf: ts.SourceFile, files: string[]): void { - const actual = sf.referencedFiles.map(f => _(f.fileName)).sort(); - const expected = files.map(fileName => _(fileName)).sort(); + const actual = sf.referencedFiles.map((f) => _(f.fileName)).sort(); + const expected = files.map((fileName) => _(fileName)).sort(); expect(actual).toEqual(expected); } diff --git a/packages/compiler-cli/src/ngtsc/shims/test/util.ts b/packages/compiler-cli/src/ngtsc/shims/test/util.ts index 432675a5fc58d..604bf8617c2e5 100644 --- a/packages/compiler-cli/src/ngtsc/shims/test/util.ts +++ b/packages/compiler-cli/src/ngtsc/shims/test/util.ts @@ -15,13 +15,20 @@ export class TestShimGenerator implements PerFileShimGenerator { readonly shouldEmit = false; readonly extensionPrefix = 'testshim'; - generateShimForFile(sf: ts.SourceFile, genFilePath: AbsoluteFsPath, priorSf: ts.SourceFile|null): - ts.SourceFile { + generateShimForFile( + sf: ts.SourceFile, + genFilePath: AbsoluteFsPath, + priorSf: ts.SourceFile | null, + ): ts.SourceFile { if (priorSf !== null) { return priorSf; } const path = absoluteFromSourceFile(sf); return ts.createSourceFile( - genFilePath, `export const SHIM_FOR_FILE = '${path}';\n`, ts.ScriptTarget.Latest, true); + genFilePath, + `export const SHIM_FOR_FILE = '${path}';\n`, + ts.ScriptTarget.Latest, + true, + ); } } diff --git a/packages/compiler-cli/src/ngtsc/sourcemaps/src/raw_source_map.ts b/packages/compiler-cli/src/ngtsc/sourcemaps/src/raw_source_map.ts index c9ac934ac3b13..c6c0ac9633bfd 100644 --- a/packages/compiler-cli/src/ngtsc/sourcemaps/src/raw_source_map.ts +++ b/packages/compiler-cli/src/ngtsc/sourcemaps/src/raw_source_map.ts @@ -13,22 +13,21 @@ import {ContentOrigin} from './content_origin'; * disk. */ export interface RawSourceMap { - version: number|string; + version: number | string; file?: string; sourceRoot?: string; sources: string[]; names: string[]; - sourcesContent?: (string|null)[]; + sourcesContent?: (string | null)[]; mappings: string; } - /** * The path and content of a source-map. */ export interface MapAndPath { /** The path to the source map if it was external or `null` if it was inline. */ - mapPath: AbsoluteFsPath|null; + mapPath: AbsoluteFsPath | null; /** The raw source map itself. */ map: RawSourceMap; } diff --git a/packages/compiler-cli/src/ngtsc/sourcemaps/src/segment_marker.ts b/packages/compiler-cli/src/ngtsc/sourcemaps/src/segment_marker.ts index a325bc19c3515..3d0ed28364513 100644 --- a/packages/compiler-cli/src/ngtsc/sourcemaps/src/segment_marker.ts +++ b/packages/compiler-cli/src/ngtsc/sourcemaps/src/segment_marker.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ - /** * A marker that indicates the start of a segment in a mapping. * @@ -17,7 +16,7 @@ export interface SegmentMarker { readonly line: number; readonly column: number; readonly position: number; - next: SegmentMarker|undefined; + next: SegmentMarker | undefined; } /** @@ -39,7 +38,10 @@ export function compareSegments(a: SegmentMarker, b: SegmentMarker): number { * @param offset the number of character to offset by. */ export function offsetSegment( - startOfLinePositions: number[], marker: SegmentMarker, offset: number): SegmentMarker { + startOfLinePositions: number[], + marker: SegmentMarker, + offset: number, +): SegmentMarker { if (offset === 0) { return marker; } diff --git a/packages/compiler-cli/src/ngtsc/sourcemaps/src/source_file.ts b/packages/compiler-cli/src/ngtsc/sourcemaps/src/source_file.ts index 14472790fb2a8..a3fdddb14e5ca 100644 --- a/packages/compiler-cli/src/ngtsc/sourcemaps/src/source_file.ts +++ b/packages/compiler-cli/src/ngtsc/sourcemaps/src/source_file.ts @@ -14,8 +14,9 @@ import {RawSourceMap, SourceMapInfo} from './raw_source_map'; import {compareSegments, offsetSegment, SegmentMarker} from './segment_marker'; export function removeSourceMapComments(contents: string): string { - return mapHelpers.removeMapFileComments(mapHelpers.removeComments(contents)) - .replace(/\n\n$/, '\n'); + return mapHelpers + .removeMapFileComments(mapHelpers.removeComments(contents)) + .replace(/\n\n$/, '\n'); } export class SourceFile { @@ -30,15 +31,15 @@ export class SourceFile { readonly startOfLinePositions: number[]; constructor( - /** The path to this source file. */ - readonly sourcePath: AbsoluteFsPath, - /** The contents of this source file. */ - readonly contents: string, - /** The raw source map (if any) referenced by this source file. */ - readonly rawMap: SourceMapInfo|null, - /** Any source files referenced by the raw source map associated with this source file. */ - readonly sources: (SourceFile|null)[], - private fs: PathManipulation, + /** The path to this source file. */ + readonly sourcePath: AbsoluteFsPath, + /** The contents of this source file. */ + readonly contents: string, + /** The raw source map (if any) referenced by this source file. */ + readonly rawMap: SourceMapInfo | null, + /** Any source files referenced by the raw source map associated with this source file. */ + readonly sources: (SourceFile | null)[], + private fs: PathManipulation, ) { this.contents = removeSourceMapComments(contents); this.startOfLinePositions = computeStartOfLinePositions(this.contents); @@ -55,13 +56,15 @@ export class SourceFile { const sourcePathDir = this.fs.dirname(this.sourcePath); // Computing the relative path can be expensive, and we are likely to have the same path for // many (if not all!) mappings. - const relativeSourcePathCache = - new Cache(input => this.fs.relative(sourcePathDir, input)); + const relativeSourcePathCache = new Cache((input) => + this.fs.relative(sourcePathDir, input), + ); for (const mapping of this.flattenedMappings) { const sourceIndex = sources.set( - relativeSourcePathCache.get(mapping.originalSource.sourcePath), - mapping.originalSource.contents); + relativeSourcePathCache.get(mapping.originalSource.sourcePath), + mapping.originalSource.contents, + ); const mappingArray: SourceMapSegment = [ mapping.generatedSegment.column, sourceIndex, @@ -101,8 +104,10 @@ export class SourceFile { * segment. Finally we apply this offset to the original source segment to get the desired * original location. */ - getOriginalLocation(line: number, column: number): - {file: AbsoluteFsPath, line: number, column: number}|null { + getOriginalLocation( + line: number, + column: number, + ): {file: AbsoluteFsPath; line: number; column: number} | null { if (this.flattenedMappings.length === 0) { return null; } @@ -117,16 +122,23 @@ export class SourceFile { const locationSegment: SegmentMarker = {line, column, position, next: undefined}; - let mappingIndex = - findLastMappingIndexBefore(this.flattenedMappings, locationSegment, false, 0); + let mappingIndex = findLastMappingIndexBefore( + this.flattenedMappings, + locationSegment, + false, + 0, + ); if (mappingIndex < 0) { mappingIndex = 0; } const {originalSegment, originalSource, generatedSegment} = - this.flattenedMappings[mappingIndex]; + this.flattenedMappings[mappingIndex]; const offset = locationSegment.position - generatedSegment.position; - const offsetOriginalSegment = - offsetSegment(originalSource.startOfLinePositions, originalSegment, offset); + const offsetOriginalSegment = offsetSegment( + originalSource.startOfLinePositions, + originalSegment, + offset, + ); return { file: originalSource.sourcePath, @@ -140,8 +152,11 @@ export class SourceFile { * source files with no transitive source maps. */ private flattenMappings(): Mapping[] { - const mappings = - parseMappings(this.rawMap && this.rawMap.map, this.sources, this.startOfLinePositions); + const mappings = parseMappings( + this.rawMap && this.rawMap.map, + this.sources, + this.startOfLinePositions, + ); ensureOriginalSegmentLinks(mappings); const flattenedMappings: Mapping[] = []; for (let mappingIndex = 0; mappingIndex < mappings.length; mappingIndex++) { @@ -196,18 +211,30 @@ export class SourceFile { // The range with `incomingStart` at 2 and `incomingEnd` at 5 has outgoing start mapping of // [1,0] and outgoing end mapping of [4, 6], which also includes [4, 3]. // - let outgoingStartIndex = - findLastMappingIndexBefore(bSource.flattenedMappings, incomingStart, false, 0); + let outgoingStartIndex = findLastMappingIndexBefore( + bSource.flattenedMappings, + incomingStart, + false, + 0, + ); if (outgoingStartIndex < 0) { outgoingStartIndex = 0; } - const outgoingEndIndex = incomingEnd !== undefined ? - findLastMappingIndexBefore( - bSource.flattenedMappings, incomingEnd, true, outgoingStartIndex) : - bSource.flattenedMappings.length - 1; - - for (let bToCmappingIndex = outgoingStartIndex; bToCmappingIndex <= outgoingEndIndex; - bToCmappingIndex++) { + const outgoingEndIndex = + incomingEnd !== undefined + ? findLastMappingIndexBefore( + bSource.flattenedMappings, + incomingEnd, + true, + outgoingStartIndex, + ) + : bSource.flattenedMappings.length - 1; + + for ( + let bToCmappingIndex = outgoingStartIndex; + bToCmappingIndex <= outgoingEndIndex; + bToCmappingIndex++ + ) { const bToCmapping: Mapping = bSource.flattenedMappings[bToCmappingIndex]; flattenedMappings.push(mergeMappings(this, aToBmapping, bToCmapping)); } @@ -228,7 +255,11 @@ export class SourceFile { * index that is no lower than this. */ export function findLastMappingIndexBefore( - mappings: Mapping[], marker: SegmentMarker, exclusive: boolean, lowerIndex: number): number { + mappings: Mapping[], + marker: SegmentMarker, + exclusive: boolean, + lowerIndex: number, +): number { let upperIndex = mappings.length - 1; const test = exclusive ? -1 : 0; @@ -264,8 +295,6 @@ export interface Mapping { readonly name?: string; } - - /** * Merge two mappings that go from A to B and B to C, to result in a mapping that goes from A to C. */ @@ -316,8 +345,11 @@ export function mergeMappings(generatedSource: SourceFile, ab: Mapping, bc: Mapp if (diff > 0) { return { name, - generatedSegment: - offsetSegment(generatedSource.startOfLinePositions, ab.generatedSegment, diff), + generatedSegment: offsetSegment( + generatedSource.startOfLinePositions, + ab.generatedSegment, + diff, + ), originalSource: bc.originalSource, originalSegment: bc.originalSegment, }; @@ -326,8 +358,11 @@ export function mergeMappings(generatedSource: SourceFile, ab: Mapping, bc: Mapp name, generatedSegment: ab.generatedSegment, originalSource: bc.originalSource, - originalSegment: - offsetSegment(bc.originalSource.startOfLinePositions, bc.originalSegment, -diff), + originalSegment: offsetSegment( + bc.originalSource.startOfLinePositions, + bc.originalSegment, + -diff, + ), }; } } @@ -337,8 +372,10 @@ export function mergeMappings(generatedSource: SourceFile, ab: Mapping, bc: Mapp * in the `sources` parameter. */ export function parseMappings( - rawMap: RawSourceMap|null, sources: (SourceFile|null)[], - generatedSourceStartOfLinePositions: number[]): Mapping[] { + rawMap: RawSourceMap | null, + sources: (SourceFile | null)[], + generatedSourceStartOfLinePositions: number[], +): Mapping[] { if (rawMap === null) { return []; } @@ -399,7 +436,7 @@ export function extractOriginalSegments(mappings: Mapping[]): Map segmentMarkers.sort(compareSegments)); + originalSegments.forEach((segmentMarkers) => segmentMarkers.sort(compareSegments)); return originalSegments; } @@ -411,7 +448,7 @@ export function extractOriginalSegments(mappings: Mapping[]): Map { + segmentsBySource.forEach((markers) => { for (let i = 0; i < markers.length - 1; i++) { markers[i].next = markers[i + 1]; } @@ -426,7 +463,7 @@ export function computeStartOfLinePositions(str: string) { // so differences in length due to extra `\r` characters do not affect the algorithms. const NEWLINE_MARKER_OFFSET = 1; const lineLengths = computeLineLengths(str); - const startPositions = [0]; // First line starts at position 0 + const startPositions = [0]; // First line starts at position 0 for (let i = 0; i < lineLengths.length - 1; i++) { startPositions.push(startPositions[i] + lineLengths[i] + NEWLINE_MARKER_OFFSET); } @@ -434,7 +471,7 @@ export function computeStartOfLinePositions(str: string) { } function computeLineLengths(str: string): number[] { - return (str.split(/\n/)).map(s => s.length); + return str.split(/\n/).map((s) => s.length); } /** diff --git a/packages/compiler-cli/src/ngtsc/sourcemaps/src/source_file_loader.ts b/packages/compiler-cli/src/ngtsc/sourcemaps/src/source_file_loader.ts index 8684ef68746dc..bfe068be9d942 100644 --- a/packages/compiler-cli/src/ngtsc/sourcemaps/src/source_file_loader.ts +++ b/packages/compiler-cli/src/ngtsc/sourcemaps/src/source_file_loader.ts @@ -29,9 +29,11 @@ export class SourceFileLoader { private currentPaths: AbsoluteFsPath[] = []; constructor( - private fs: ReadonlyFileSystem, private logger: Logger, - /** A map of URL schemes to base paths. The scheme name should be lowercase. */ - private schemeMap: Record) {} + private fs: ReadonlyFileSystem, + private logger: Logger, + /** A map of URL schemes to base paths. The scheme name should be lowercase. */ + private schemeMap: Record, + ) {} /** * Load a source file from the provided content and source map, and recursively load any @@ -59,13 +61,17 @@ export class SourceFileLoader { * @param sourcePath The path to the source file to load. * @returns a SourceFile object if its contents could be loaded from disk, or null otherwise. */ - loadSourceFile(sourcePath: AbsoluteFsPath): SourceFile|null; + loadSourceFile(sourcePath: AbsoluteFsPath): SourceFile | null; loadSourceFile( - sourcePath: AbsoluteFsPath, contents: string|null = null, - mapAndPath: MapAndPath|null = null): SourceFile|null { + sourcePath: AbsoluteFsPath, + contents: string | null = null, + mapAndPath: MapAndPath | null = null, + ): SourceFile | null { const contentsOrigin = contents !== null ? ContentOrigin.Provided : ContentOrigin.FileSystem; - const sourceMapInfo: SourceMapInfo|null = - mapAndPath && {origin: ContentOrigin.Provided, ...mapAndPath}; + const sourceMapInfo: SourceMapInfo | null = mapAndPath && { + origin: ContentOrigin.Provided, + ...mapAndPath, + }; return this.loadSourceFileInternal(sourcePath, contents, contentsOrigin, sourceMapInfo); } @@ -86,8 +92,11 @@ export class SourceFileLoader { * `null` otherwise. */ private loadSourceFileInternal( - sourcePath: AbsoluteFsPath, contents: string|null, sourceOrigin: ContentOrigin, - sourceMapInfo: SourceMapInfo|null): SourceFile|null { + sourcePath: AbsoluteFsPath, + contents: string | null, + sourceOrigin: ContentOrigin, + sourceMapInfo: SourceMapInfo | null, + ): SourceFile | null { const previousPaths = this.currentPaths.slice(); try { if (contents === null) { @@ -102,7 +111,7 @@ export class SourceFileLoader { sourceMapInfo = this.loadSourceMap(sourcePath, contents, sourceOrigin); } - let sources: (SourceFile|null)[] = []; + let sources: (SourceFile | null)[] = []; if (sourceMapInfo !== null) { const basePath = sourceMapInfo.mapPath || sourcePath; sources = this.processSources(basePath, sourceMapInfo); @@ -111,7 +120,8 @@ export class SourceFileLoader { return new SourceFile(sourcePath, contents, sourceMapInfo, sources, this.fs); } catch (e) { this.logger.warn( - `Unable to fully load ${sourcePath} for source-map flattening: ${(e as Error).message}`); + `Unable to fully load ${sourcePath} for source-map flattening: ${(e as Error).message}`, + ); return null; } finally { // We are finished with this recursion so revert the paths being tracked @@ -133,8 +143,10 @@ export class SourceFileLoader { * otherwise. */ private loadSourceMap( - sourcePath: AbsoluteFsPath, sourceContents: string, - sourceOrigin: ContentOrigin): SourceMapInfo|null { + sourcePath: AbsoluteFsPath, + sourceContents: string, + sourceOrigin: ContentOrigin, + ): SourceMapInfo | null { // Only consider a source-map comment from the last non-empty line of the file, in case there // are embedded source-map comments elsewhere in the file (as can be the case with bundlers like // webpack). @@ -166,8 +178,9 @@ export class SourceFileLoader { origin: ContentOrigin.FileSystem, }; } catch (e) { - this.logger.warn(`Unable to fully load ${sourcePath} for source-map flattening: ${ - (e as Error).message}`); + this.logger.warn( + `Unable to fully load ${sourcePath} for source-map flattening: ${(e as Error).message}`, + ); return null; } } @@ -188,22 +201,27 @@ export class SourceFileLoader { * Iterate over each of the "sources" for this source file's source map, recursively loading each * source file and its associated source map. */ - private processSources(basePath: AbsoluteFsPath, {map, origin: sourceMapOrigin}: SourceMapInfo): - (SourceFile|null)[] { + private processSources( + basePath: AbsoluteFsPath, + {map, origin: sourceMapOrigin}: SourceMapInfo, + ): (SourceFile | null)[] { const sourceRoot = this.fs.resolve( - this.fs.dirname(basePath), this.replaceSchemeWithPath(map.sourceRoot || '')); + this.fs.dirname(basePath), + this.replaceSchemeWithPath(map.sourceRoot || ''), + ); return map.sources.map((source, index) => { const path = this.fs.resolve(sourceRoot, this.replaceSchemeWithPath(source)); - const content = map.sourcesContent && map.sourcesContent[index] || null; + const content = (map.sourcesContent && map.sourcesContent[index]) || null; // The origin of this source file is "inline" if we extracted it from the source-map's // `sourcesContent`, except when the source-map itself was "provided" in-memory. // An inline source file is treated as if it were from the file-system if the source-map that // contains it was provided in-memory. The first call to `loadSourceFile()` is special in that // if you "provide" the contents of the source-map in-memory then we don't want to block // loading sources from the file-system just because this source-map had an inline source. - const sourceOrigin = content !== null && sourceMapOrigin !== ContentOrigin.Provided ? - ContentOrigin.Inline : - ContentOrigin.FileSystem; + const sourceOrigin = + content !== null && sourceMapOrigin !== ContentOrigin.Provided + ? ContentOrigin.Inline + : ContentOrigin.FileSystem; return this.loadSourceFileInternal(path, content, sourceOrigin, null); }); } @@ -236,16 +254,18 @@ export class SourceFileLoader { private trackPath(path: AbsoluteFsPath): void { if (this.currentPaths.includes(path)) { throw new Error( - `Circular source file mapping dependency: ${this.currentPaths.join(' -> ')} -> ${path}`); + `Circular source file mapping dependency: ${this.currentPaths.join(' -> ')} -> ${path}`, + ); } this.currentPaths.push(path); } private getLastNonEmptyLine(contents: string): string { let trailingWhitespaceIndex = contents.length - 1; - while (trailingWhitespaceIndex > 0 && - (contents[trailingWhitespaceIndex] === '\n' || - contents[trailingWhitespaceIndex] === '\r')) { + while ( + trailingWhitespaceIndex > 0 && + (contents[trailingWhitespaceIndex] === '\n' || contents[trailingWhitespaceIndex] === '\r') + ) { trailingWhitespaceIndex--; } let lastRealLineIndex = contents.lastIndexOf('\n', trailingWhitespaceIndex - 1); @@ -265,6 +285,8 @@ export class SourceFileLoader { */ private replaceSchemeWithPath(path: string): string { return path.replace( - SCHEME_MATCHER, (_: string, scheme: string) => this.schemeMap[scheme.toLowerCase()] || ''); + SCHEME_MATCHER, + (_: string, scheme: string) => this.schemeMap[scheme.toLowerCase()] || '', + ); } } diff --git a/packages/compiler-cli/src/ngtsc/sourcemaps/test/segment_marker_spec.ts b/packages/compiler-cli/src/ngtsc/sourcemaps/test/segment_marker_spec.ts index b374cafea6ee6..09ab7c064241f 100644 --- a/packages/compiler-cli/src/ngtsc/sourcemaps/test/segment_marker_spec.ts +++ b/packages/compiler-cli/src/ngtsc/sourcemaps/test/segment_marker_spec.ts @@ -11,100 +11,174 @@ import {computeStartOfLinePositions} from '../src/source_file'; describe('SegmentMarker utils', () => { describe('compareSegments()', () => { it('should return 0 if the segments are the same', () => { - expect(compareSegments( - {line: 0, column: 0, position: 0, next: undefined}, - {line: 0, column: 0, position: 0, next: undefined})) - .toEqual(0); - expect(compareSegments( - {line: 123, column: 0, position: 200, next: undefined}, - {line: 123, column: 0, position: 200, next: undefined})) - .toEqual(0); - expect(compareSegments( - {line: 0, column: 45, position: 45, next: undefined}, - {line: 0, column: 45, position: 45, next: undefined})) - .toEqual(0); - expect(compareSegments( - {line: 123, column: 45, position: 245, next: undefined}, - {line: 123, column: 45, position: 245, next: undefined})) - .toEqual(0); + expect( + compareSegments( + {line: 0, column: 0, position: 0, next: undefined}, + {line: 0, column: 0, position: 0, next: undefined}, + ), + ).toEqual(0); + expect( + compareSegments( + {line: 123, column: 0, position: 200, next: undefined}, + {line: 123, column: 0, position: 200, next: undefined}, + ), + ).toEqual(0); + expect( + compareSegments( + {line: 0, column: 45, position: 45, next: undefined}, + {line: 0, column: 45, position: 45, next: undefined}, + ), + ).toEqual(0); + expect( + compareSegments( + {line: 123, column: 45, position: 245, next: undefined}, + {line: 123, column: 45, position: 245, next: undefined}, + ), + ).toEqual(0); }); it('should return a negative number if the first segment is before the second segment', () => { - expect(compareSegments( - {line: 0, column: 0, position: 0, next: undefined}, - {line: 0, column: 45, position: 45, next: undefined})) - .toBeLessThan(0); - expect(compareSegments( - {line: 123, column: 0, position: 200, next: undefined}, - {line: 123, column: 45, position: 245, next: undefined})) - .toBeLessThan(0); - expect(compareSegments( - {line: 13, column: 45, position: 75, next: undefined}, - {line: 123, column: 45, position: 245, next: undefined})) - .toBeLessThan(0); - expect(compareSegments( - {line: 13, column: 45, position: 75, next: undefined}, - {line: 123, column: 9, position: 209, next: undefined})) - .toBeLessThan(0); + expect( + compareSegments( + {line: 0, column: 0, position: 0, next: undefined}, + {line: 0, column: 45, position: 45, next: undefined}, + ), + ).toBeLessThan(0); + expect( + compareSegments( + {line: 123, column: 0, position: 200, next: undefined}, + {line: 123, column: 45, position: 245, next: undefined}, + ), + ).toBeLessThan(0); + expect( + compareSegments( + {line: 13, column: 45, position: 75, next: undefined}, + {line: 123, column: 45, position: 245, next: undefined}, + ), + ).toBeLessThan(0); + expect( + compareSegments( + {line: 13, column: 45, position: 75, next: undefined}, + {line: 123, column: 9, position: 209, next: undefined}, + ), + ).toBeLessThan(0); }); it('should return a positive number if the first segment is after the second segment', () => { - expect(compareSegments( - {line: 0, column: 45, position: 45, next: undefined}, - {line: 0, column: 0, position: 0, next: undefined})) - .toBeGreaterThan(0); - expect(compareSegments( - {line: 123, column: 45, position: 245, next: undefined}, - {line: 123, column: 0, position: 200, next: undefined})) - .toBeGreaterThan(0); - expect(compareSegments( - {line: 123, column: 45, position: 245, next: undefined}, - {line: 13, column: 45, position: 75, next: undefined})) - .toBeGreaterThan(0); - expect(compareSegments( - {line: 123, column: 9, position: 209, next: undefined}, - {line: 13, column: 45, position: 75, next: undefined})) - .toBeGreaterThan(0); + expect( + compareSegments( + {line: 0, column: 45, position: 45, next: undefined}, + {line: 0, column: 0, position: 0, next: undefined}, + ), + ).toBeGreaterThan(0); + expect( + compareSegments( + {line: 123, column: 45, position: 245, next: undefined}, + {line: 123, column: 0, position: 200, next: undefined}, + ), + ).toBeGreaterThan(0); + expect( + compareSegments( + {line: 123, column: 45, position: 245, next: undefined}, + {line: 13, column: 45, position: 75, next: undefined}, + ), + ).toBeGreaterThan(0); + expect( + compareSegments( + {line: 123, column: 9, position: 209, next: undefined}, + {line: 13, column: 45, position: 75, next: undefined}, + ), + ).toBeGreaterThan(0); }); }); describe('offsetSegment()', () => { it('should return an identical marker if offset is 0', () => { - const startOfLinePositions = - computeStartOfLinePositions('012345\n0123456789\r\n012*4567\n0123456'); + const startOfLinePositions = computeStartOfLinePositions( + '012345\n0123456789\r\n012*4567\n0123456', + ); const marker = {line: 2, column: 3, position: 20, next: undefined}; expect(offsetSegment(startOfLinePositions, marker, 0)).toBe(marker); }); it('should return a new marker offset by the given chars', () => { - const startOfLinePositions = - computeStartOfLinePositions('012345\n0123456789\r\n012*4567\n0123456'); + const startOfLinePositions = computeStartOfLinePositions( + '012345\n0123456789\r\n012*4567\n0123456', + ); const marker = {line: 2, column: 3, position: 22, next: undefined}; - expect(offsetSegment(startOfLinePositions, marker, 1)) - .toEqual({line: 2, column: 4, position: 23, next: undefined}); - expect(offsetSegment(startOfLinePositions, marker, 2)) - .toEqual({line: 2, column: 5, position: 24, next: undefined}); - expect(offsetSegment(startOfLinePositions, marker, 4)) - .toEqual({line: 2, column: 7, position: 26, next: undefined}); - expect(offsetSegment(startOfLinePositions, marker, 6)) - .toEqual({line: 3, column: 0, position: 28, next: undefined}); - expect(offsetSegment(startOfLinePositions, marker, 8)) - .toEqual({line: 3, column: 2, position: 30, next: undefined}); - expect(offsetSegment(startOfLinePositions, marker, 20)) - .toEqual({line: 3, column: 14, position: 42, next: undefined}); - expect(offsetSegment(startOfLinePositions, marker, -1)) - .toEqual({line: 2, column: 2, position: 21, next: undefined}); - expect(offsetSegment(startOfLinePositions, marker, -2)) - .toEqual({line: 2, column: 1, position: 20, next: undefined}); - expect(offsetSegment(startOfLinePositions, marker, -3)) - .toEqual({line: 2, column: 0, position: 19, next: undefined}); - expect(offsetSegment(startOfLinePositions, marker, -4)) - .toEqual({line: 1, column: 11, position: 18, next: undefined}); - expect(offsetSegment(startOfLinePositions, marker, -6)) - .toEqual({line: 1, column: 9, position: 16, next: undefined}); - expect(offsetSegment(startOfLinePositions, marker, -16)) - .toEqual({line: 0, column: 6, position: 6, next: undefined}); + expect(offsetSegment(startOfLinePositions, marker, 1)).toEqual({ + line: 2, + column: 4, + position: 23, + next: undefined, + }); + expect(offsetSegment(startOfLinePositions, marker, 2)).toEqual({ + line: 2, + column: 5, + position: 24, + next: undefined, + }); + expect(offsetSegment(startOfLinePositions, marker, 4)).toEqual({ + line: 2, + column: 7, + position: 26, + next: undefined, + }); + expect(offsetSegment(startOfLinePositions, marker, 6)).toEqual({ + line: 3, + column: 0, + position: 28, + next: undefined, + }); + expect(offsetSegment(startOfLinePositions, marker, 8)).toEqual({ + line: 3, + column: 2, + position: 30, + next: undefined, + }); + expect(offsetSegment(startOfLinePositions, marker, 20)).toEqual({ + line: 3, + column: 14, + position: 42, + next: undefined, + }); + expect(offsetSegment(startOfLinePositions, marker, -1)).toEqual({ + line: 2, + column: 2, + position: 21, + next: undefined, + }); + expect(offsetSegment(startOfLinePositions, marker, -2)).toEqual({ + line: 2, + column: 1, + position: 20, + next: undefined, + }); + expect(offsetSegment(startOfLinePositions, marker, -3)).toEqual({ + line: 2, + column: 0, + position: 19, + next: undefined, + }); + expect(offsetSegment(startOfLinePositions, marker, -4)).toEqual({ + line: 1, + column: 11, + position: 18, + next: undefined, + }); + expect(offsetSegment(startOfLinePositions, marker, -6)).toEqual({ + line: 1, + column: 9, + position: 16, + next: undefined, + }); + expect(offsetSegment(startOfLinePositions, marker, -16)).toEqual({ + line: 0, + column: 6, + position: 6, + next: undefined, + }); }); }); }); diff --git a/packages/compiler-cli/src/ngtsc/sourcemaps/test/source_file_loader_spec.ts b/packages/compiler-cli/src/ngtsc/sourcemaps/test/source_file_loader_spec.ts index 5be7e31247c05..83516407bd77d 100644 --- a/packages/compiler-cli/src/ngtsc/sourcemaps/test/source_file_loader_spec.ts +++ b/packages/compiler-cli/src/ngtsc/sourcemaps/test/source_file_loader_spec.ts @@ -56,7 +56,9 @@ runInEachFileSystem(() => { const sourceMap = createRawSourceMap({file: 'index.js'}); fs.writeFile(_('/foo/src/external.js.map'), JSON.stringify(sourceMap)); const sourceFile = registry.loadSourceFile( - _('/foo/src/index.js'), 'some inline content\n//# sourceMappingURL=external.js.map'); + _('/foo/src/index.js'), + 'some inline content\n//# sourceMappingURL=external.js.map', + ); if (sourceFile === null) { return fail('Expected source file to be defined'); } @@ -70,12 +72,15 @@ runInEachFileSystem(() => { fs.ensureDir(_('/foo/src')); const sourceMap = createRawSourceMap({file: 'index.js'}); fs.writeFile(_('/foo/src/external.js.map'), JSON.stringify(sourceMap)); - const sourceFile = registry.loadSourceFile(_('/foo/src/index.js'), [ - 'some content', - '//# sourceMappingURL=bad.js.map', - 'some more content', - '//# sourceMappingURL=external.js.map', - ].join('\n')); + const sourceFile = registry.loadSourceFile( + _('/foo/src/index.js'), + [ + 'some content', + '//# sourceMappingURL=bad.js.map', + 'some more content', + '//# sourceMappingURL=external.js.map', + ].join('\n'), + ); if (sourceFile === null) { return fail('Expected source file to be defined'); } @@ -87,33 +92,38 @@ runInEachFileSystem(() => { for (const eolMarker of ['\n', '\r\n']) { it(`should only read source-map comments from the last non-blank line of a file [EOL marker: ${ - eolMarker === '\n' ? '\\n' : '\\r\\n'}]`, - () => { - fs.ensureDir(_('/foo/src')); - const sourceMap = createRawSourceMap({file: 'index.js'}); - fs.writeFile(_('/foo/src/external.js.map'), JSON.stringify(sourceMap)); - const sourceFile = registry.loadSourceFile(_('/foo/src/index.js'), [ - 'some content', - '//# sourceMappingURL=bad.js.map', - 'some more content', - '//# sourceMappingURL=external.js.map', - '', - '', - ].join(eolMarker)); - if (sourceFile === null) { - return fail('Expected source file to be defined'); - } - if (sourceFile.rawMap === null) { - return fail('Expected source map to be defined'); - } - expect(sourceFile.rawMap.map).toEqual(sourceMap); - }); + eolMarker === '\n' ? '\\n' : '\\r\\n' + }]`, () => { + fs.ensureDir(_('/foo/src')); + const sourceMap = createRawSourceMap({file: 'index.js'}); + fs.writeFile(_('/foo/src/external.js.map'), JSON.stringify(sourceMap)); + const sourceFile = registry.loadSourceFile( + _('/foo/src/index.js'), + [ + 'some content', + '//# sourceMappingURL=bad.js.map', + 'some more content', + '//# sourceMappingURL=external.js.map', + '', + '', + ].join(eolMarker), + ); + if (sourceFile === null) { + return fail('Expected source file to be defined'); + } + if (sourceFile.rawMap === null) { + return fail('Expected source map to be defined'); + } + expect(sourceFile.rawMap.map).toEqual(sourceMap); + }); } it('should handle a missing external source map', () => { fs.ensureDir(_('/foo/src')); const sourceFile = registry.loadSourceFile( - _('/foo/src/index.js'), 'some inline content\n//# sourceMappingURL=external.js.map'); + _('/foo/src/index.js'), + 'some inline content\n//# sourceMappingURL=external.js.map', + ); if (sourceFile === null) { return fail('Expected source file to be defined'); } @@ -124,9 +134,9 @@ runInEachFileSystem(() => { const sourceMap = createRawSourceMap({file: 'index.js'}); const encodedSourceMap = Buffer.from(JSON.stringify(sourceMap)).toString('base64'); const sourceFile = registry.loadSourceFile( - _('/foo/src/index.js'), - `some inline content\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${ - encodedSourceMap}`); + _('/foo/src/index.js'), + `some inline content\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,${encodedSourceMap}`, + ); if (sourceFile === null) { return fail('Expected source file to be defined'); } @@ -159,73 +169,74 @@ runInEachFileSystem(() => { expect(sourceFile.rawMap).toBe(null); }); - it('should recurse into external original source files that are referenced from source maps', - () => { - // Setup a scenario where the generated files reference previous files: - // - // index.js - // -> x.js - // -> y.js - // -> a.js - // -> z.js (inline content) - fs.ensureDir(_('/foo/src')); - - const indexSourceMap = createRawSourceMap({ - file: 'index.js', - sources: ['x.js', 'y.js', 'z.js'], - 'sourcesContent': [null, null, 'z content'] - }); - fs.writeFile(_('/foo/src/index.js.map'), JSON.stringify(indexSourceMap)); - - fs.writeFile(_('/foo/src/x.js'), 'x content'); - - const ySourceMap = createRawSourceMap({file: 'y.js', sources: ['a.js']}); - fs.writeFile(_('/foo/src/y.js'), 'y content'); - fs.writeFile(_('/foo/src/y.js.map'), JSON.stringify(ySourceMap)); - fs.writeFile(_('/foo/src/z.js'), 'z content'); - fs.writeFile(_('/foo/src/a.js'), 'a content'); - - const sourceFile = registry.loadSourceFile(_('/foo/src/index.js'), 'index content'); - if (sourceFile === null) { - return fail('Expected source file to be defined'); - } - - expect(sourceFile.contents).toEqual('index content'); - expect(sourceFile.sourcePath).toEqual(_('/foo/src/index.js')); - if (sourceFile.rawMap === null) { - return fail('Expected source map to be defined'); - } - expect(sourceFile.rawMap.map).toEqual(indexSourceMap); - - expect(sourceFile.sources.length).toEqual(3); - - expect(sourceFile.sources[0]!.contents).toEqual('x content'); - expect(sourceFile.sources[0]!.sourcePath).toEqual(_('/foo/src/x.js')); - expect(sourceFile.sources[0]!.rawMap).toBe(null); - expect(sourceFile.sources[0]!.sources).toEqual([]); - - - expect(sourceFile.sources[1]!.contents).toEqual('y content'); - expect(sourceFile.sources[1]!.sourcePath).toEqual(_('/foo/src/y.js')); - expect(sourceFile.sources[1]!.rawMap!.map).toEqual(ySourceMap); - - expect(sourceFile.sources[1]!.sources.length).toEqual(1); - expect(sourceFile.sources[1]!.sources[0]!.contents).toEqual('a content'); - expect(sourceFile.sources[1]!.sources[0]!.sourcePath).toEqual(_('/foo/src/a.js')); - expect(sourceFile.sources[1]!.sources[0]!.rawMap).toBe(null); - expect(sourceFile.sources[1]!.sources[0]!.sources).toEqual([]); - - expect(sourceFile.sources[2]!.contents).toEqual('z content'); - expect(sourceFile.sources[2]!.sourcePath).toEqual(_('/foo/src/z.js')); - expect(sourceFile.sources[2]!.rawMap).toBe(null); - expect(sourceFile.sources[2]!.sources).toEqual([]); - }); + it('should recurse into external original source files that are referenced from source maps', () => { + // Setup a scenario where the generated files reference previous files: + // + // index.js + // -> x.js + // -> y.js + // -> a.js + // -> z.js (inline content) + fs.ensureDir(_('/foo/src')); + + const indexSourceMap = createRawSourceMap({ + file: 'index.js', + sources: ['x.js', 'y.js', 'z.js'], + 'sourcesContent': [null, null, 'z content'], + }); + fs.writeFile(_('/foo/src/index.js.map'), JSON.stringify(indexSourceMap)); + + fs.writeFile(_('/foo/src/x.js'), 'x content'); + + const ySourceMap = createRawSourceMap({file: 'y.js', sources: ['a.js']}); + fs.writeFile(_('/foo/src/y.js'), 'y content'); + fs.writeFile(_('/foo/src/y.js.map'), JSON.stringify(ySourceMap)); + fs.writeFile(_('/foo/src/z.js'), 'z content'); + fs.writeFile(_('/foo/src/a.js'), 'a content'); + + const sourceFile = registry.loadSourceFile(_('/foo/src/index.js'), 'index content'); + if (sourceFile === null) { + return fail('Expected source file to be defined'); + } + + expect(sourceFile.contents).toEqual('index content'); + expect(sourceFile.sourcePath).toEqual(_('/foo/src/index.js')); + if (sourceFile.rawMap === null) { + return fail('Expected source map to be defined'); + } + expect(sourceFile.rawMap.map).toEqual(indexSourceMap); + + expect(sourceFile.sources.length).toEqual(3); + + expect(sourceFile.sources[0]!.contents).toEqual('x content'); + expect(sourceFile.sources[0]!.sourcePath).toEqual(_('/foo/src/x.js')); + expect(sourceFile.sources[0]!.rawMap).toBe(null); + expect(sourceFile.sources[0]!.sources).toEqual([]); + + expect(sourceFile.sources[1]!.contents).toEqual('y content'); + expect(sourceFile.sources[1]!.sourcePath).toEqual(_('/foo/src/y.js')); + expect(sourceFile.sources[1]!.rawMap!.map).toEqual(ySourceMap); + + expect(sourceFile.sources[1]!.sources.length).toEqual(1); + expect(sourceFile.sources[1]!.sources[0]!.contents).toEqual('a content'); + expect(sourceFile.sources[1]!.sources[0]!.sourcePath).toEqual(_('/foo/src/a.js')); + expect(sourceFile.sources[1]!.sources[0]!.rawMap).toBe(null); + expect(sourceFile.sources[1]!.sources[0]!.sources).toEqual([]); + + expect(sourceFile.sources[2]!.contents).toEqual('z content'); + expect(sourceFile.sources[2]!.sourcePath).toEqual(_('/foo/src/z.js')); + expect(sourceFile.sources[2]!.rawMap).toBe(null); + expect(sourceFile.sources[2]!.sources).toEqual([]); + }); it('should handle a missing source file referenced from a source-map', () => { fs.ensureDir(_('/foo/src')); - const indexSourceMap = - createRawSourceMap({file: 'index.js', sources: ['x.js'], 'sourcesContent': [null]}); + const indexSourceMap = createRawSourceMap({ + file: 'index.js', + sources: ['x.js'], + 'sourcesContent': [null], + }); fs.writeFile(_('/foo/src/index.js.map'), JSON.stringify(indexSourceMap)); const sourceFile = registry.loadSourceFile(_('/foo/src/index.js'), 'index content'); @@ -244,92 +255,90 @@ runInEachFileSystem(() => { }); }); - it('should log a warning if there is a cyclic dependency in source files loaded from disk', - () => { - // a.js -> a.js.map -> b.js -> b.js.map -> c.js -> c.js.map -> (external) a.js - // ^^^^^^^^^^^^^^^ - // c.js.map incorrectly links to a.js, creating a cycle - - fs.ensureDir(_('/foo/src')); - - const aMap = createRawSourceMap({file: 'a.js', sources: ['b.js']}); - - const aPath = _('/foo/src/a.js'); - fs.writeFile(aPath, 'a content\n' + mapHelpers.fromObject(aMap).toComment()); - - const bPath = _('/foo/src/b.js'); - fs.writeFile( - bPath, - 'b content\n' + - mapHelpers.fromObject(createRawSourceMap({file: 'b.js', sources: ['c.js']})) - .toComment()); - - const cPath = _('/foo/src/c.js'); - fs.writeFile( - cPath, - 'c content\n' + - mapHelpers.fromObject(createRawSourceMap({file: 'c.js', sources: ['a.js']})) - .toComment()); - - const sourceFile = registry.loadSourceFile(aPath)!; - expect(sourceFile).not.toBe(null!); - expect(sourceFile.contents).toEqual('a content\n'); - expect(sourceFile.sourcePath).toEqual(_('/foo/src/a.js')); - if (sourceFile.rawMap === null) { - return fail('Expected source map to be defined'); - } - expect(sourceFile.rawMap.map).toEqual(aMap); - expect(sourceFile.sources.length).toEqual(1); - - expect(logger.logs.warn[0][0]) - .toContain( - `Circular source file mapping dependency: ` + - `${aPath} -> ${bPath} -> ${cPath} -> ${aPath}`); - }); - - it('should log a warning if there is a cyclic dependency in source maps loaded from disk', - () => { - // a.js -> a.js.map -> b.js -> a.js.map -> c.js - // ^^^^^^^^ - // b.js incorrectly links to a.js.map, creating a cycle - - fs.ensureDir(_('/foo/src')); - const aPath = _('/foo/src/a.js'); - fs.writeFile(aPath, 'a.js content\n//# sourceMappingURL=a.js.map'); - - const aMap = createRawSourceMap({file: 'a.js', sources: ['b.js']}); - const aMapPath = _('/foo/src/a.js.map'); - fs.writeFile(aMapPath, JSON.stringify(aMap)); - - const bPath = _('/foo/src/b.js'); - fs.writeFile(bPath, 'b.js content\n//# sourceMappingURL=a.js.map'); - - const sourceFile = registry.loadSourceFile(aPath); - if (sourceFile === null) { - return fail('Expected source file to be defined'); - } - expect(sourceFile.contents).toEqual('a.js content\n'); - expect(sourceFile.sourcePath).toEqual(_('/foo/src/a.js')); - if (sourceFile.rawMap === null) { - return fail('Expected source map to be defined'); - } - expect(sourceFile.rawMap.map).toEqual(aMap); - expect(sourceFile.sources.length).toEqual(1); - - expect(logger.logs.warn[0][0]) - .toContain( - `Circular source file mapping dependency: ` + - `${aPath} -> ${aMapPath} -> ${bPath} -> ${aMapPath}`); - const innerSourceFile = sourceFile.sources[0]; - if (innerSourceFile === null) { - return fail('Expected source file to be defined'); - } - expect(innerSourceFile.contents).toEqual('b.js content\n'); - expect(innerSourceFile.sourcePath).toEqual(_('/foo/src/b.js')); - // The source-map from b.js was not loaded as it would have caused a cycle - expect(innerSourceFile.rawMap).toBe(null); - expect(innerSourceFile.sources.length).toEqual(0); - }); + it('should log a warning if there is a cyclic dependency in source files loaded from disk', () => { + // a.js -> a.js.map -> b.js -> b.js.map -> c.js -> c.js.map -> (external) a.js + // ^^^^^^^^^^^^^^^ + // c.js.map incorrectly links to a.js, creating a cycle + + fs.ensureDir(_('/foo/src')); + + const aMap = createRawSourceMap({file: 'a.js', sources: ['b.js']}); + + const aPath = _('/foo/src/a.js'); + fs.writeFile(aPath, 'a content\n' + mapHelpers.fromObject(aMap).toComment()); + + const bPath = _('/foo/src/b.js'); + fs.writeFile( + bPath, + 'b content\n' + + mapHelpers.fromObject(createRawSourceMap({file: 'b.js', sources: ['c.js']})).toComment(), + ); + + const cPath = _('/foo/src/c.js'); + fs.writeFile( + cPath, + 'c content\n' + + mapHelpers.fromObject(createRawSourceMap({file: 'c.js', sources: ['a.js']})).toComment(), + ); + + const sourceFile = registry.loadSourceFile(aPath)!; + expect(sourceFile).not.toBe(null!); + expect(sourceFile.contents).toEqual('a content\n'); + expect(sourceFile.sourcePath).toEqual(_('/foo/src/a.js')); + if (sourceFile.rawMap === null) { + return fail('Expected source map to be defined'); + } + expect(sourceFile.rawMap.map).toEqual(aMap); + expect(sourceFile.sources.length).toEqual(1); + + expect(logger.logs.warn[0][0]).toContain( + `Circular source file mapping dependency: ` + + `${aPath} -> ${bPath} -> ${cPath} -> ${aPath}`, + ); + }); + + it('should log a warning if there is a cyclic dependency in source maps loaded from disk', () => { + // a.js -> a.js.map -> b.js -> a.js.map -> c.js + // ^^^^^^^^ + // b.js incorrectly links to a.js.map, creating a cycle + + fs.ensureDir(_('/foo/src')); + const aPath = _('/foo/src/a.js'); + fs.writeFile(aPath, 'a.js content\n//# sourceMappingURL=a.js.map'); + + const aMap = createRawSourceMap({file: 'a.js', sources: ['b.js']}); + const aMapPath = _('/foo/src/a.js.map'); + fs.writeFile(aMapPath, JSON.stringify(aMap)); + + const bPath = _('/foo/src/b.js'); + fs.writeFile(bPath, 'b.js content\n//# sourceMappingURL=a.js.map'); + + const sourceFile = registry.loadSourceFile(aPath); + if (sourceFile === null) { + return fail('Expected source file to be defined'); + } + expect(sourceFile.contents).toEqual('a.js content\n'); + expect(sourceFile.sourcePath).toEqual(_('/foo/src/a.js')); + if (sourceFile.rawMap === null) { + return fail('Expected source map to be defined'); + } + expect(sourceFile.rawMap.map).toEqual(aMap); + expect(sourceFile.sources.length).toEqual(1); + + expect(logger.logs.warn[0][0]).toContain( + `Circular source file mapping dependency: ` + + `${aPath} -> ${aMapPath} -> ${bPath} -> ${aMapPath}`, + ); + const innerSourceFile = sourceFile.sources[0]; + if (innerSourceFile === null) { + return fail('Expected source file to be defined'); + } + expect(innerSourceFile.contents).toEqual('b.js content\n'); + expect(innerSourceFile.sourcePath).toEqual(_('/foo/src/b.js')); + // The source-map from b.js was not loaded as it would have caused a cycle + expect(innerSourceFile.rawMap).toBe(null); + expect(innerSourceFile.sources.length).toEqual(0); + }); it('should not fail if the filename of an inline source looks like a cyclic dependency', () => { // a.js -> (inline) a.js.map -> (inline) a.js @@ -338,8 +347,11 @@ runInEachFileSystem(() => { fs.ensureDir(_('/foo/src')); const aPath = _('/foo/src/a.js'); - const aMap = createRawSourceMap( - {file: 'a.js', sources: ['a.js'], sourcesContent: ['inline original a.js content']}); + const aMap = createRawSourceMap({ + file: 'a.js', + sources: ['a.js'], + sourcesContent: ['inline original a.js content'], + }); fs.writeFile(aPath, 'a content\n' + mapHelpers.fromObject(aMap).toComment()); const sourceFile = registry.loadSourceFile(aPath); @@ -355,57 +367,56 @@ runInEachFileSystem(() => { expect(logger.logs.warn.length).toEqual(0); }); - it('should not load source-maps (after the initial map) from disk if the source file was inline', - () => { - // a.js -> (initial) a.js.map -> b.js -> b.js.map -> (inline) c.js -> c.js.map - // ^^^^^^^^ - // c.js.map is not loaded because the referencing source file (c.js) was inline - - fs.ensureDir(_('/foo/src')); - - const aPath = _('/foo/src/a.js'); - fs.writeFile(aPath, 'a.js content\n//# sourceMappingURL=a.js.map'); - const aMapPath = _('/foo/src/a.js.map'); - const aMap = createRawSourceMap({file: 'a.js', sources: ['b.js']}); - fs.writeFile(aMapPath, JSON.stringify(aMap)); - - const bPath = _('/foo/src/b.js'); - fs.writeFile(bPath, 'b.js content\n//# sourceMappingURL=b.js.map'); - const bMapPath = _('/foo/src/b.js.map'); - const bMap = createRawSourceMap({ - file: 'b.js', - sources: ['c.js'], - sourcesContent: ['c content\n//# sourceMappingURL=c.js.map'] - }); - fs.writeFile(bMapPath, JSON.stringify(bMap)); - - const cMapPath = _('/foo/src/c.js.map'); - const cMap = createRawSourceMap({file: 'c.js', sources: ['d.js']}); - fs.writeFile(cMapPath, JSON.stringify(cMap)); - - const sourceFile = registry.loadSourceFile(aPath); - if (sourceFile === null) { - return fail('Expected source file to be defined'); - } - const bSource = sourceFile.sources[0]; - if (!bSource) { - return fail('Expected source file to be defined'); - } - const cSource = bSource.sources[0]; - if (!cSource) { - return fail('Expected source file to be defined'); - } - // External c.js.map never gets loaded because c.js was inline source - expect(cSource.rawMap).toBe(null); - expect(cSource.sources).toEqual([]); - - expect(logger.logs.warn.length).toEqual(0); - }); - - for (const {scheme, mappedPath} of - [{scheme: 'WEBPACK://', mappedPath: '/foo/src/index.ts'}, - {scheme: 'webpack://', mappedPath: '/foo/src/index.ts'}, - {scheme: 'missing://', mappedPath: '/src/index.ts'}, + it('should not load source-maps (after the initial map) from disk if the source file was inline', () => { + // a.js -> (initial) a.js.map -> b.js -> b.js.map -> (inline) c.js -> c.js.map + // ^^^^^^^^ + // c.js.map is not loaded because the referencing source file (c.js) was inline + + fs.ensureDir(_('/foo/src')); + + const aPath = _('/foo/src/a.js'); + fs.writeFile(aPath, 'a.js content\n//# sourceMappingURL=a.js.map'); + const aMapPath = _('/foo/src/a.js.map'); + const aMap = createRawSourceMap({file: 'a.js', sources: ['b.js']}); + fs.writeFile(aMapPath, JSON.stringify(aMap)); + + const bPath = _('/foo/src/b.js'); + fs.writeFile(bPath, 'b.js content\n//# sourceMappingURL=b.js.map'); + const bMapPath = _('/foo/src/b.js.map'); + const bMap = createRawSourceMap({ + file: 'b.js', + sources: ['c.js'], + sourcesContent: ['c content\n//# sourceMappingURL=c.js.map'], + }); + fs.writeFile(bMapPath, JSON.stringify(bMap)); + + const cMapPath = _('/foo/src/c.js.map'); + const cMap = createRawSourceMap({file: 'c.js', sources: ['d.js']}); + fs.writeFile(cMapPath, JSON.stringify(cMap)); + + const sourceFile = registry.loadSourceFile(aPath); + if (sourceFile === null) { + return fail('Expected source file to be defined'); + } + const bSource = sourceFile.sources[0]; + if (!bSource) { + return fail('Expected source file to be defined'); + } + const cSource = bSource.sources[0]; + if (!cSource) { + return fail('Expected source file to be defined'); + } + // External c.js.map never gets loaded because c.js was inline source + expect(cSource.rawMap).toBe(null); + expect(cSource.sources).toEqual([]); + + expect(logger.logs.warn.length).toEqual(0); + }); + + for (const {scheme, mappedPath} of [ + {scheme: 'WEBPACK://', mappedPath: '/foo/src/index.ts'}, + {scheme: 'webpack://', mappedPath: '/foo/src/index.ts'}, + {scheme: 'missing://', mappedPath: '/src/index.ts'}, ]) { it(`should handle source paths that are protocol mapped [scheme:"${scheme}"]`, () => { fs.ensureDir(_('/foo/src')); @@ -413,7 +424,7 @@ runInEachFileSystem(() => { const indexSourceMap = createRawSourceMap({ file: 'index.js', sources: [`${scheme}/src/index.ts`], - 'sourcesContent': ['original content'] + 'sourcesContent': ['original content'], }); fs.writeFile(_('/foo/src/index.js.map'), JSON.stringify(indexSourceMap)); const sourceFile = registry.loadSourceFile(_('/foo/src/index.js'), 'generated content'); @@ -457,7 +468,6 @@ runInEachFileSystem(() => { }); }); - function createRawSourceMap(custom: Partial): RawSourceMap { return { 'version': 3, @@ -466,6 +476,6 @@ function createRawSourceMap(custom: Partial): RawSourceMap { 'sourcesContent': [], 'names': [], 'mappings': '', - ...custom + ...custom, }; } diff --git a/packages/compiler-cli/src/ngtsc/sourcemaps/test/source_file_spec.ts b/packages/compiler-cli/src/ngtsc/sourcemaps/test/source_file_spec.ts index fddb88ebbbfa0..0dbbf38c17042 100644 --- a/packages/compiler-cli/src/ngtsc/sourcemaps/test/source_file_spec.ts +++ b/packages/compiler-cli/src/ngtsc/sourcemaps/test/source_file_spec.ts @@ -12,7 +12,15 @@ import {runInEachFileSystem} from '../../file_system/testing'; import {ContentOrigin} from '../src/content_origin'; import {RawSourceMap, SourceMapInfo} from '../src/raw_source_map'; import {SegmentMarker} from '../src/segment_marker'; -import {computeStartOfLinePositions, ensureOriginalSegmentLinks, extractOriginalSegments, findLastMappingIndexBefore, Mapping, parseMappings, SourceFile} from '../src/source_file'; +import { + computeStartOfLinePositions, + ensureOriginalSegmentLinks, + extractOriginalSegments, + findLastMappingIndexBefore, + Mapping, + parseMappings, + SourceFile, +} from '../src/source_file'; runInEachFileSystem(() => { describe('SourceFile and utilities', () => { @@ -38,10 +46,15 @@ runInEachFileSystem(() => { it('should parse the mappings from the raw source map', () => { const rawSourceMap: RawSourceMap = { - mappings: encode([[[0, 0, 0, 0], [6, 0, 0, 3]]]), + mappings: encode([ + [ + [0, 0, 0, 0], + [6, 0, 0, 3], + ], + ]), names: [], sources: ['a.js'], - version: 3 + version: 3, }; const originalSource = new SourceFile(_('/foo/src/a.js'), 'abcdefg', null, [], fs); const mappings = parseMappings(rawSourceMap, [originalSource], [0, 8]); @@ -50,13 +63,13 @@ runInEachFileSystem(() => { generatedSegment: {line: 0, column: 0, position: 0, next: undefined}, originalSource, originalSegment: {line: 0, column: 0, position: 0, next: undefined}, - name: undefined + name: undefined, }, { generatedSegment: {line: 0, column: 6, position: 6, next: undefined}, originalSource, originalSegment: {line: 0, column: 3, position: 3, next: undefined}, - name: undefined + name: undefined, }, ]); }); @@ -72,36 +85,50 @@ runInEachFileSystem(() => { expect(extractOriginalSegments(parseMappings(rawSourceMap, [], []))).toEqual(new Map()); }); - it('should parse the segments in ascending order of original position from the raw source map', - () => { - const originalSource = new SourceFile(_('/foo/src/a.js'), 'abcdefg', null, [], fs); - const rawSourceMap: RawSourceMap = { - mappings: encode([[[0, 0, 0, 0], [2, 0, 0, 3], [4, 0, 0, 2]]]), - names: [], - sources: ['a.js'], - version: 3 - }; - const originalSegments = - extractOriginalSegments(parseMappings(rawSourceMap, [originalSource], [0, 8])); - expect(originalSegments.get(originalSource)).toEqual([ - {line: 0, column: 0, position: 0, next: undefined}, - {line: 0, column: 2, position: 2, next: undefined}, - {line: 0, column: 3, position: 3, next: undefined}, - ]); - }); + it('should parse the segments in ascending order of original position from the raw source map', () => { + const originalSource = new SourceFile(_('/foo/src/a.js'), 'abcdefg', null, [], fs); + const rawSourceMap: RawSourceMap = { + mappings: encode([ + [ + [0, 0, 0, 0], + [2, 0, 0, 3], + [4, 0, 0, 2], + ], + ]), + names: [], + sources: ['a.js'], + version: 3, + }; + const originalSegments = extractOriginalSegments( + parseMappings(rawSourceMap, [originalSource], [0, 8]), + ); + expect(originalSegments.get(originalSource)).toEqual([ + {line: 0, column: 0, position: 0, next: undefined}, + {line: 0, column: 2, position: 2, next: undefined}, + {line: 0, column: 3, position: 3, next: undefined}, + ]); + }); it('should create separate arrays for each original source file', () => { const sourceA = new SourceFile(_('/foo/src/a.js'), 'abcdefg', null, [], fs); const sourceB = new SourceFile(_('/foo/src/b.js'), '1234567', null, [], fs); const rawSourceMap: RawSourceMap = { - mappings: - encode([[[0, 0, 0, 0], [2, 1, 0, 3], [4, 0, 0, 2], [5, 1, 0, 5], [6, 1, 0, 2]]]), + mappings: encode([ + [ + [0, 0, 0, 0], + [2, 1, 0, 3], + [4, 0, 0, 2], + [5, 1, 0, 5], + [6, 1, 0, 2], + ], + ]), names: [], sources: ['a.js', 'b.js'], - version: 3 + version: 3, }; - const originalSegments = - extractOriginalSegments(parseMappings(rawSourceMap, [sourceA, sourceB], [0, 8])); + const originalSegments = extractOriginalSegments( + parseMappings(rawSourceMap, [sourceA, sourceB], [0, 8]), + ); expect(originalSegments.get(sourceA)).toEqual([ {line: 0, column: 0, position: 0, next: undefined}, {line: 0, column: 2, position: 2, next: undefined}, @@ -115,35 +142,35 @@ runInEachFileSystem(() => { }); describe('findLastMappingIndexBefore', () => { - it('should find the highest mapping index that has a segment marker below the given one if there is not an exact match', - () => { - const marker5: SegmentMarker = {line: 0, column: 50, position: 50, next: undefined}; - const marker4: SegmentMarker = {line: 0, column: 40, position: 40, next: marker5}; - const marker3: SegmentMarker = {line: 0, column: 30, position: 30, next: marker4}; - const marker2: SegmentMarker = {line: 0, column: 20, position: 20, next: marker3}; - const marker1: SegmentMarker = {line: 0, column: 10, position: 10, next: marker2}; - const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map( - marker => ({generatedSegment: marker} as Mapping)); - - const marker: SegmentMarker = {line: 0, column: 35, position: 35, next: undefined}; - const index = findLastMappingIndexBefore(mappings, marker, /* exclusive */ false, 0); - expect(index).toEqual(2); - }); - - it('should find the highest mapping index that has a segment marker (when there are duplicates) below the given one if there is not an exact match', - () => { - const marker5: SegmentMarker = {line: 0, column: 50, position: 50, next: undefined}; - const marker4: SegmentMarker = {line: 0, column: 30, position: 30, next: marker5}; - const marker3: SegmentMarker = {line: 0, column: 30, position: 30, next: marker4}; - const marker2: SegmentMarker = {line: 0, column: 20, position: 20, next: marker3}; - const marker1: SegmentMarker = {line: 0, column: 10, position: 10, next: marker2}; - const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map( - marker => ({generatedSegment: marker} as Mapping)); - - const marker: SegmentMarker = {line: 0, column: 35, position: 35, next: undefined}; - const index = findLastMappingIndexBefore(mappings, marker, /* exclusive */ false, 0); - expect(index).toEqual(3); - }); + it('should find the highest mapping index that has a segment marker below the given one if there is not an exact match', () => { + const marker5: SegmentMarker = {line: 0, column: 50, position: 50, next: undefined}; + const marker4: SegmentMarker = {line: 0, column: 40, position: 40, next: marker5}; + const marker3: SegmentMarker = {line: 0, column: 30, position: 30, next: marker4}; + const marker2: SegmentMarker = {line: 0, column: 20, position: 20, next: marker3}; + const marker1: SegmentMarker = {line: 0, column: 10, position: 10, next: marker2}; + const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map( + (marker) => ({generatedSegment: marker}) as Mapping, + ); + + const marker: SegmentMarker = {line: 0, column: 35, position: 35, next: undefined}; + const index = findLastMappingIndexBefore(mappings, marker, /* exclusive */ false, 0); + expect(index).toEqual(2); + }); + + it('should find the highest mapping index that has a segment marker (when there are duplicates) below the given one if there is not an exact match', () => { + const marker5: SegmentMarker = {line: 0, column: 50, position: 50, next: undefined}; + const marker4: SegmentMarker = {line: 0, column: 30, position: 30, next: marker5}; + const marker3: SegmentMarker = {line: 0, column: 30, position: 30, next: marker4}; + const marker2: SegmentMarker = {line: 0, column: 20, position: 20, next: marker3}; + const marker1: SegmentMarker = {line: 0, column: 10, position: 10, next: marker2}; + const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map( + (marker) => ({generatedSegment: marker}) as Mapping, + ); + + const marker: SegmentMarker = {line: 0, column: 35, position: 35, next: undefined}; + const index = findLastMappingIndexBefore(mappings, marker, /* exclusive */ false, 0); + expect(index).toEqual(3); + }); it('should find the last mapping if the segment marker is higher than all of them', () => { const marker5: SegmentMarker = {line: 0, column: 50, position: 50, next: undefined}; @@ -152,7 +179,8 @@ runInEachFileSystem(() => { const marker2: SegmentMarker = {line: 0, column: 20, position: 20, next: marker3}; const marker1: SegmentMarker = {line: 0, column: 10, position: 10, next: marker2}; const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map( - marker => ({generatedSegment: marker} as Mapping)); + (marker) => ({generatedSegment: marker}) as Mapping, + ); const marker: SegmentMarker = {line: 0, column: 60, position: 60, next: undefined}; @@ -167,7 +195,8 @@ runInEachFileSystem(() => { const marker2: SegmentMarker = {line: 0, column: 20, position: 20, next: marker3}; const marker1: SegmentMarker = {line: 0, column: 10, position: 10, next: marker2}; const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map( - marker => ({generatedSegment: marker} as Mapping)); + (marker) => ({generatedSegment: marker}) as Mapping, + ); const marker: SegmentMarker = {line: 0, column: 5, position: 5, next: undefined}; @@ -176,33 +205,33 @@ runInEachFileSystem(() => { }); describe('[exact match inclusive]', () => { - it('should find the matching segment marker mapping index if there is only one of them', - () => { - const marker5: SegmentMarker = {line: 0, column: 50, position: 50, next: undefined}; - const marker4: SegmentMarker = {line: 0, column: 40, position: 40, next: marker5}; - const marker3: SegmentMarker = {line: 0, column: 30, position: 30, next: marker4}; - const marker2: SegmentMarker = {line: 0, column: 20, position: 20, next: marker3}; - const marker1: SegmentMarker = {line: 0, column: 10, position: 10, next: marker2}; - - const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map( - marker => ({generatedSegment: marker} as Mapping)); - const index = findLastMappingIndexBefore(mappings, marker3, /* exclusive */ false, 0); - expect(index).toEqual(2); - }); - - it('should find the highest matching segment marker mapping index if there is more than one of them', - () => { - const marker5: SegmentMarker = {line: 0, column: 50, position: 50, next: undefined}; - const marker4: SegmentMarker = {line: 0, column: 30, position: 30, next: marker5}; - const marker3: SegmentMarker = {line: 0, column: 30, position: 30, next: marker4}; - const marker2: SegmentMarker = {line: 0, column: 20, position: 20, next: marker3}; - const marker1: SegmentMarker = {line: 0, column: 10, position: 10, next: marker2}; - - const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map( - marker => ({generatedSegment: marker} as Mapping)); - const index = findLastMappingIndexBefore(mappings, marker3, /* exclusive */ false, 0); - expect(index).toEqual(3); - }); + it('should find the matching segment marker mapping index if there is only one of them', () => { + const marker5: SegmentMarker = {line: 0, column: 50, position: 50, next: undefined}; + const marker4: SegmentMarker = {line: 0, column: 40, position: 40, next: marker5}; + const marker3: SegmentMarker = {line: 0, column: 30, position: 30, next: marker4}; + const marker2: SegmentMarker = {line: 0, column: 20, position: 20, next: marker3}; + const marker1: SegmentMarker = {line: 0, column: 10, position: 10, next: marker2}; + + const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map( + (marker) => ({generatedSegment: marker}) as Mapping, + ); + const index = findLastMappingIndexBefore(mappings, marker3, /* exclusive */ false, 0); + expect(index).toEqual(2); + }); + + it('should find the highest matching segment marker mapping index if there is more than one of them', () => { + const marker5: SegmentMarker = {line: 0, column: 50, position: 50, next: undefined}; + const marker4: SegmentMarker = {line: 0, column: 30, position: 30, next: marker5}; + const marker3: SegmentMarker = {line: 0, column: 30, position: 30, next: marker4}; + const marker2: SegmentMarker = {line: 0, column: 20, position: 20, next: marker3}; + const marker1: SegmentMarker = {line: 0, column: 10, position: 10, next: marker2}; + + const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map( + (marker) => ({generatedSegment: marker}) as Mapping, + ); + const index = findLastMappingIndexBefore(mappings, marker3, /* exclusive */ false, 0); + expect(index).toEqual(3); + }); }); describe('[exact match exclusive]', () => { @@ -214,71 +243,72 @@ runInEachFileSystem(() => { const marker1: SegmentMarker = {line: 0, column: 10, position: 10, next: marker2}; const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map( - marker => ({generatedSegment: marker} as Mapping)); + (marker) => ({generatedSegment: marker}) as Mapping, + ); const index = findLastMappingIndexBefore(mappings, marker3, /* exclusive */ true, 0); expect(index).toEqual(1); }); - it('should find the highest preceding mapping index if there is more than one matching segment marker', - () => { - const marker5: SegmentMarker = {line: 0, column: 50, position: 50, next: undefined}; - const marker4: SegmentMarker = {line: 0, column: 30, position: 30, next: marker5}; - const marker3: SegmentMarker = {line: 0, column: 30, position: 30, next: marker4}; - const marker2: SegmentMarker = {line: 0, column: 20, position: 20, next: marker3}; - const marker1: SegmentMarker = {line: 0, column: 10, position: 10, next: marker2}; - - const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map( - marker => ({generatedSegment: marker} as Mapping)); - const index = findLastMappingIndexBefore(mappings, marker3, /* exclusive */ false, 0); - expect(index).toEqual(3); - }); + it('should find the highest preceding mapping index if there is more than one matching segment marker', () => { + const marker5: SegmentMarker = {line: 0, column: 50, position: 50, next: undefined}; + const marker4: SegmentMarker = {line: 0, column: 30, position: 30, next: marker5}; + const marker3: SegmentMarker = {line: 0, column: 30, position: 30, next: marker4}; + const marker2: SegmentMarker = {line: 0, column: 20, position: 20, next: marker3}; + const marker1: SegmentMarker = {line: 0, column: 10, position: 10, next: marker2}; + + const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map( + (marker) => ({generatedSegment: marker}) as Mapping, + ); + const index = findLastMappingIndexBefore(mappings, marker3, /* exclusive */ false, 0); + expect(index).toEqual(3); + }); }); describe('[with lowerIndex hint', () => { - it('should find the highest mapping index above the lowerIndex hint that has a segment marker below the given one if there is not an exact match', - () => { - const marker5: SegmentMarker = {line: 0, column: 50, position: 50, next: undefined}; - const marker4: SegmentMarker = {line: 0, column: 40, position: 40, next: marker5}; - const marker3: SegmentMarker = {line: 0, column: 30, position: 30, next: marker4}; - const marker2: SegmentMarker = {line: 0, column: 20, position: 20, next: marker3}; - const marker1: SegmentMarker = {line: 0, column: 10, position: 10, next: marker2}; - const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map( - marker => ({generatedSegment: marker} as Mapping)); - - const marker: SegmentMarker = {line: 0, column: 35, position: 35, next: undefined}; - const index = findLastMappingIndexBefore(mappings, marker, /* exclusive */ false, 1); - expect(index).toEqual(2); - }); - - it('should return the lowerIndex mapping index if there is a single exact match and we are not exclusive', - () => { - const marker5: SegmentMarker = {line: 0, column: 50, position: 50, next: undefined}; - const marker4: SegmentMarker = {line: 0, column: 40, position: 40, next: marker5}; - const marker3: SegmentMarker = {line: 0, column: 30, position: 30, next: marker4}; - const marker2: SegmentMarker = {line: 0, column: 20, position: 20, next: marker3}; - const marker1: SegmentMarker = {line: 0, column: 10, position: 10, next: marker2}; - const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map( - marker => ({generatedSegment: marker} as Mapping)); - - const marker: SegmentMarker = {line: 0, column: 30, position: 30, next: undefined}; - const index = findLastMappingIndexBefore(mappings, marker, /* exclusive */ false, 2); - expect(index).toEqual(2); - }); - - it('should return the lowerIndex mapping index if there are multiple exact matches and we are not exclusive', - () => { - const marker5: SegmentMarker = {line: 0, column: 50, position: 50, next: undefined}; - const marker4: SegmentMarker = {line: 0, column: 30, position: 30, next: marker5}; - const marker3: SegmentMarker = {line: 0, column: 30, position: 30, next: marker4}; - const marker2: SegmentMarker = {line: 0, column: 20, position: 20, next: marker3}; - const marker1: SegmentMarker = {line: 0, column: 10, position: 10, next: marker2}; - const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map( - marker => ({generatedSegment: marker} as Mapping)); - - const marker: SegmentMarker = {line: 0, column: 30, position: 30, next: undefined}; - const index = findLastMappingIndexBefore(mappings, marker, /* exclusive */ false, 3); - expect(index).toEqual(3); - }); + it('should find the highest mapping index above the lowerIndex hint that has a segment marker below the given one if there is not an exact match', () => { + const marker5: SegmentMarker = {line: 0, column: 50, position: 50, next: undefined}; + const marker4: SegmentMarker = {line: 0, column: 40, position: 40, next: marker5}; + const marker3: SegmentMarker = {line: 0, column: 30, position: 30, next: marker4}; + const marker2: SegmentMarker = {line: 0, column: 20, position: 20, next: marker3}; + const marker1: SegmentMarker = {line: 0, column: 10, position: 10, next: marker2}; + const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map( + (marker) => ({generatedSegment: marker}) as Mapping, + ); + + const marker: SegmentMarker = {line: 0, column: 35, position: 35, next: undefined}; + const index = findLastMappingIndexBefore(mappings, marker, /* exclusive */ false, 1); + expect(index).toEqual(2); + }); + + it('should return the lowerIndex mapping index if there is a single exact match and we are not exclusive', () => { + const marker5: SegmentMarker = {line: 0, column: 50, position: 50, next: undefined}; + const marker4: SegmentMarker = {line: 0, column: 40, position: 40, next: marker5}; + const marker3: SegmentMarker = {line: 0, column: 30, position: 30, next: marker4}; + const marker2: SegmentMarker = {line: 0, column: 20, position: 20, next: marker3}; + const marker1: SegmentMarker = {line: 0, column: 10, position: 10, next: marker2}; + const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map( + (marker) => ({generatedSegment: marker}) as Mapping, + ); + + const marker: SegmentMarker = {line: 0, column: 30, position: 30, next: undefined}; + const index = findLastMappingIndexBefore(mappings, marker, /* exclusive */ false, 2); + expect(index).toEqual(2); + }); + + it('should return the lowerIndex mapping index if there are multiple exact matches and we are not exclusive', () => { + const marker5: SegmentMarker = {line: 0, column: 50, position: 50, next: undefined}; + const marker4: SegmentMarker = {line: 0, column: 30, position: 30, next: marker5}; + const marker3: SegmentMarker = {line: 0, column: 30, position: 30, next: marker4}; + const marker2: SegmentMarker = {line: 0, column: 20, position: 20, next: marker3}; + const marker1: SegmentMarker = {line: 0, column: 10, position: 10, next: marker2}; + const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map( + (marker) => ({generatedSegment: marker}) as Mapping, + ); + + const marker: SegmentMarker = {line: 0, column: 30, position: 30, next: undefined}; + const index = findLastMappingIndexBefore(mappings, marker, /* exclusive */ false, 3); + expect(index).toEqual(3); + }); it('should return -1 if the segment marker is lower than the lowerIndex hint', () => { const marker5: SegmentMarker = {line: 0, column: 50, position: 50, next: undefined}; @@ -287,7 +317,8 @@ runInEachFileSystem(() => { const marker2: SegmentMarker = {line: 0, column: 20, position: 20, next: marker3}; const marker1: SegmentMarker = {line: 0, column: 10, position: 10, next: marker2}; const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map( - marker => ({generatedSegment: marker} as Mapping)); + (marker) => ({generatedSegment: marker}) as Mapping, + ); const marker: SegmentMarker = {line: 0, column: 25, position: 25, next: undefined}; @@ -295,44 +326,50 @@ runInEachFileSystem(() => { expect(index).toEqual(-1); }); - it('should return -1 if the segment marker is equal to the lowerIndex hint and we are exclusive', - () => { - const marker5: SegmentMarker = {line: 0, column: 50, position: 50, next: undefined}; - const marker4: SegmentMarker = {line: 0, column: 40, position: 40, next: marker5}; - const marker3: SegmentMarker = {line: 0, column: 30, position: 30, next: marker4}; - const marker2: SegmentMarker = {line: 0, column: 20, position: 20, next: marker3}; - const marker1: SegmentMarker = {line: 0, column: 10, position: 10, next: marker2}; - const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map( - marker => ({generatedSegment: marker} as Mapping)); - - const marker: SegmentMarker = {line: 0, column: 30, position: 30, next: undefined}; - - const index = findLastMappingIndexBefore(mappings, marker, /* exclusive */ true, 2); - expect(index).toEqual(-1); - }); + it('should return -1 if the segment marker is equal to the lowerIndex hint and we are exclusive', () => { + const marker5: SegmentMarker = {line: 0, column: 50, position: 50, next: undefined}; + const marker4: SegmentMarker = {line: 0, column: 40, position: 40, next: marker5}; + const marker3: SegmentMarker = {line: 0, column: 30, position: 30, next: marker4}; + const marker2: SegmentMarker = {line: 0, column: 20, position: 20, next: marker3}; + const marker1: SegmentMarker = {line: 0, column: 10, position: 10, next: marker2}; + const mappings: Mapping[] = [marker1, marker2, marker3, marker4, marker5].map( + (marker) => ({generatedSegment: marker}) as Mapping, + ); + + const marker: SegmentMarker = {line: 0, column: 30, position: 30, next: undefined}; + + const index = findLastMappingIndexBefore(mappings, marker, /* exclusive */ true, 2); + expect(index).toEqual(-1); + }); }); }); describe('ensureOriginalSegmentLinks', () => { - it('should add `next` properties to each segment that point to the next segment in the same source file', - () => { - const sourceA = new SourceFile(_('/foo/src/a.js'), 'abcdefg', null, [], fs); - const sourceB = new SourceFile(_('/foo/src/b.js'), '1234567', null, [], fs); - const rawSourceMap: RawSourceMap = { - mappings: - encode([[[0, 0, 0, 0], [2, 1, 0, 3], [4, 0, 0, 2], [5, 1, 0, 5], [6, 1, 0, 2]]]), - names: [], - sources: ['a.js', 'b.js'], - version: 3 - }; - const mappings = parseMappings(rawSourceMap, [sourceA, sourceB], [0, 8]); - ensureOriginalSegmentLinks(mappings); - expect(mappings[0].originalSegment.next).toBe(mappings[2].originalSegment); - expect(mappings[1].originalSegment.next).toBe(mappings[3].originalSegment); - expect(mappings[2].originalSegment.next).toBeUndefined(); - expect(mappings[3].originalSegment.next).toBeUndefined(); - expect(mappings[4].originalSegment.next).toBe(mappings[1].originalSegment); - }); + it('should add `next` properties to each segment that point to the next segment in the same source file', () => { + const sourceA = new SourceFile(_('/foo/src/a.js'), 'abcdefg', null, [], fs); + const sourceB = new SourceFile(_('/foo/src/b.js'), '1234567', null, [], fs); + const rawSourceMap: RawSourceMap = { + mappings: encode([ + [ + [0, 0, 0, 0], + [2, 1, 0, 3], + [4, 0, 0, 2], + [5, 1, 0, 5], + [6, 1, 0, 2], + ], + ]), + names: [], + sources: ['a.js', 'b.js'], + version: 3, + }; + const mappings = parseMappings(rawSourceMap, [sourceA, sourceB], [0, 8]); + ensureOriginalSegmentLinks(mappings); + expect(mappings[0].originalSegment.next).toBe(mappings[2].originalSegment); + expect(mappings[1].originalSegment.next).toBe(mappings[3].originalSegment); + expect(mappings[2].originalSegment.next).toBeUndefined(); + expect(mappings[3].originalSegment.next).toBeUndefined(); + expect(mappings[4].originalSegment.next).toBe(mappings[1].originalSegment); + }); }); describe('SourceFile', () => { @@ -346,31 +383,46 @@ runInEachFileSystem(() => { const rawSourceMap: SourceMapInfo = { map: {mappings: '', names: [], sources: [], version: 3}, mapPath: null, - origin: ContentOrigin.Provided + origin: ContentOrigin.Provided, }; - const sourceFile = - new SourceFile(_('/foo/src/index.js'), 'index contents', rawSourceMap, [], fs); + const sourceFile = new SourceFile( + _('/foo/src/index.js'), + 'index contents', + rawSourceMap, + [], + fs, + ); expect(sourceFile.flattenedMappings).toEqual([]); }); - it('should be the same as non-flat mappings if there is only one level of source map', - () => { - const rawSourceMap: SourceMapInfo = { - mapPath: null, - map: { - mappings: encode([[[0, 0, 0, 0], [6, 0, 0, 3]]]), - names: [], - sources: ['a.js'], - version: 3 - }, - origin: ContentOrigin.Provided, - }; - const originalSource = new SourceFile(_('/foo/src/a.js'), 'abcdefg', null, [], fs); - const sourceFile = new SourceFile( - _('/foo/src/index.js'), 'abc123defg', rawSourceMap, [originalSource], fs); - expect(removeOriginalSegmentLinks(sourceFile.flattenedMappings)) - .toEqual(parseMappings(rawSourceMap.map, [originalSource], [0, 11])); - }); + it('should be the same as non-flat mappings if there is only one level of source map', () => { + const rawSourceMap: SourceMapInfo = { + mapPath: null, + map: { + mappings: encode([ + [ + [0, 0, 0, 0], + [6, 0, 0, 3], + ], + ]), + names: [], + sources: ['a.js'], + version: 3, + }, + origin: ContentOrigin.Provided, + }; + const originalSource = new SourceFile(_('/foo/src/a.js'), 'abcdefg', null, [], fs); + const sourceFile = new SourceFile( + _('/foo/src/index.js'), + 'abc123defg', + rawSourceMap, + [originalSource], + fs, + ); + expect(removeOriginalSegmentLinks(sourceFile.flattenedMappings)).toEqual( + parseMappings(rawSourceMap.map, [originalSource], [0, 11]), + ); + }); it('should merge mappings from flattened original source files', () => { const cSource = new SourceFile(_('/foo/src/c.js'), 'bcd123', null, [], fs); @@ -379,23 +431,41 @@ runInEachFileSystem(() => { const bSourceMap: SourceMapInfo = { mapPath: null, map: { - mappings: encode([[[0, 1, 0, 0], [1, 0, 0, 0], [4, 1, 0, 1]]]), + mappings: encode([ + [ + [0, 1, 0, 0], + [1, 0, 0, 0], + [4, 1, 0, 1], + ], + ]), names: [], sources: ['c.js', 'd.js'], - version: 3 + version: 3, }, origin: ContentOrigin.Provided, }; - const bSource = - new SourceFile(_('/foo/src/b.js'), 'abcdef', bSourceMap, [cSource, dSource], fs); + const bSource = new SourceFile( + _('/foo/src/b.js'), + 'abcdef', + bSourceMap, + [cSource, dSource], + fs, + ); const aSourceMap: SourceMapInfo = { mapPath: null, map: { - mappings: encode([[[0, 0, 0, 0], [2, 0, 0, 3], [4, 0, 0, 2], [5, 0, 0, 5]]]), + mappings: encode([ + [ + [0, 0, 0, 0], + [2, 0, 0, 3], + [4, 0, 0, 2], + [5, 0, 0, 5], + ], + ]), names: [], sources: ['b.js'], - version: 3 + version: 3, }, origin: ContentOrigin.Provided, }; @@ -406,37 +476,37 @@ runInEachFileSystem(() => { generatedSegment: {line: 0, column: 0, position: 0, next: undefined}, originalSource: dSource, originalSegment: {line: 0, column: 0, position: 0, next: undefined}, - name: undefined + name: undefined, }, { generatedSegment: {line: 0, column: 1, position: 1, next: undefined}, originalSource: cSource, originalSegment: {line: 0, column: 0, position: 0, next: undefined}, - name: undefined + name: undefined, }, { generatedSegment: {line: 0, column: 2, position: 2, next: undefined}, originalSource: cSource, originalSegment: {line: 0, column: 2, position: 2, next: undefined}, - name: undefined + name: undefined, }, { generatedSegment: {line: 0, column: 3, position: 3, next: undefined}, originalSource: dSource, originalSegment: {line: 0, column: 1, position: 1, next: undefined}, - name: undefined + name: undefined, }, { generatedSegment: {line: 0, column: 4, position: 4, next: undefined}, originalSource: cSource, originalSegment: {line: 0, column: 1, position: 1, next: undefined}, - name: undefined + name: undefined, }, { generatedSegment: {line: 0, column: 5, position: 5, next: undefined}, originalSource: dSource, originalSegment: {line: 0, column: 2, position: 2, next: undefined}, - name: undefined + name: undefined, }, ]); }); @@ -445,10 +515,17 @@ runInEachFileSystem(() => { const bSourceMap: SourceMapInfo = { mapPath: null, map: { - mappings: encode([[[1, 0, 0, 0], [4, 0, 0, 3], [4, 0, 0, 6], [5, 0, 0, 7]]]), + mappings: encode([ + [ + [1, 0, 0, 0], + [4, 0, 0, 3], + [4, 0, 0, 6], + [5, 0, 0, 7], + ], + ]), names: [], sources: ['c.js'], - version: 3 + version: 3, }, origin: ContentOrigin.Provided, }; @@ -456,10 +533,17 @@ runInEachFileSystem(() => { const aSourceMap: SourceMapInfo = { mapPath: null, map: { - mappings: encode([[[0, 0, 0, 0], [2, 0, 0, 3], [4, 0, 0, 2], [5, 0, 0, 5]]]), + mappings: encode([ + [ + [0, 0, 0, 0], + [2, 0, 0, 3], + [4, 0, 0, 2], + [5, 0, 0, 5], + ], + ]), names: [], sources: ['b.js'], - version: 3 + version: 3, }, origin: ContentOrigin.Provided, }; @@ -468,8 +552,9 @@ runInEachFileSystem(() => { // These flattened mappings are just the mappings from a to b. // (The mappings to c are dropped since there is no source file to map // to.) - expect(removeOriginalSegmentLinks(aSource.flattenedMappings)) - .toEqual(parseMappings(aSourceMap.map, [bSource], [0, 7])); + expect(removeOriginalSegmentLinks(aSource.flattenedMappings)).toEqual( + parseMappings(aSourceMap.map, [bSource], [0, 7]), + ); }); /** @@ -491,27 +576,51 @@ runInEachFileSystem(() => { const bToCSourceMap: SourceMapInfo = { mapPath: null, map: { - mappings: encode([[[1, 0, 0, 0], [4, 0, 0, 3], [4, 0, 0, 6], [5, 0, 0, 7]]]), + mappings: encode([ + [ + [1, 0, 0, 0], + [4, 0, 0, 3], + [4, 0, 0, 6], + [5, 0, 0, 7], + ], + ]), names: [], sources: ['c.js'], - version: 3 + version: 3, }, origin: ContentOrigin.Provided, }; - const bSource = - new SourceFile(_('/foo/src/b.js'), 'abcdef', bToCSourceMap, [cSource], fs); + const bSource = new SourceFile( + _('/foo/src/b.js'), + 'abcdef', + bToCSourceMap, + [cSource], + fs, + ); const aToBSourceMap: SourceMapInfo = { mapPath: null, map: { - mappings: encode([[[0, 0, 0, 0], [2, 0, 0, 3], [4, 0, 0, 2], [5, 0, 0, 5]]]), + mappings: encode([ + [ + [0, 0, 0, 0], + [2, 0, 0, 3], + [4, 0, 0, 2], + [5, 0, 0, 5], + ], + ]), names: [], sources: ['b.js'], - version: 3 + version: 3, }, origin: ContentOrigin.Provided, }; - const aSource = - new SourceFile(_('/foo/src/a.js'), 'abdecf', aToBSourceMap, [bSource], fs); + const aSource = new SourceFile( + _('/foo/src/a.js'), + 'abdecf', + aToBSourceMap, + [bSource], + fs, + ); const aTocSourceMap = aSource.renderFlattenedSourceMap(); expect(aTocSourceMap.version).toEqual(3); @@ -520,9 +629,18 @@ runInEachFileSystem(() => { expect(aTocSourceMap.sourceRoot).toBeUndefined(); expect(aTocSourceMap.sources).toEqual(['c.js']); expect(aTocSourceMap.sourcesContent).toEqual(['bcd123e']); - expect(aTocSourceMap.mappings).toEqual(encode([ - [[1, 0, 0, 0], [2, 0, 0, 2], [3, 0, 0, 3], [3, 0, 0, 6], [4, 0, 0, 1], [5, 0, 0, 7]] - ])); + expect(aTocSourceMap.mappings).toEqual( + encode([ + [ + [1, 0, 0, 0], + [2, 0, 0, 2], + [3, 0, 0, 3], + [3, 0, 0, 6], + [4, 0, 0, 1], + [5, 0, 0, 7], + ], + ]), + ); }); it('should handle mappings that map from lines outside of the actual content lines', () => { @@ -531,19 +649,29 @@ runInEachFileSystem(() => { mapPath: null, map: { mappings: encode([ - [[0, 0, 0, 0], [2, 0, 0, 3], [4, 0, 0, 2], [5, 0, 0, 5]], [ - [0, 0, 0, 0], // Extra mapping from a non-existent line - ] + [0, 0, 0, 0], + [2, 0, 0, 3], + [4, 0, 0, 2], + [5, 0, 0, 5], + ], + [ + [0, 0, 0, 0], // Extra mapping from a non-existent line + ], ]), names: [], sources: ['b.js'], - version: 3 + version: 3, }, origin: ContentOrigin.Provided, }; - const aSource = - new SourceFile(_('/foo/src/a.js'), 'abdecf', aToBSourceMap, [bSource], fs); + const aSource = new SourceFile( + _('/foo/src/a.js'), + 'abdecf', + aToBSourceMap, + [bSource], + fs, + ); const aTocSourceMap = aSource.renderFlattenedSourceMap(); expect(aTocSourceMap.version).toEqual(3); @@ -562,21 +690,40 @@ runInEachFileSystem(() => { const bToCSourceMap: SourceMapInfo = { mapPath: null, map: { - mappings: encode([[[1, 0, 0, 0], [4, 0, 0, 3], [4, 0, 0, 6], [5, 0, 0, 7]]]), + mappings: encode([ + [ + [1, 0, 0, 0], + [4, 0, 0, 3], + [4, 0, 0, 6], + [5, 0, 0, 7], + ], + ]), names: [], sources: ['c.js'], - version: 3 + version: 3, }, origin: ContentOrigin.Provided, }; - const bSource = - new SourceFile(_('/foo/src/lib/b.js'), 'abcdef', bToCSourceMap, [cSource1], fs); + const bSource = new SourceFile( + _('/foo/src/lib/b.js'), + 'abcdef', + bToCSourceMap, + [cSource1], + fs, + ); const aToBCSourceMap: SourceMapInfo = { mapPath: null, map: { - mappings: - encode([[[0, 0, 0, 0], [2, 0, 0, 3], [4, 0, 0, 2], [5, 0, 0, 5], [6, 1, 0, 3]]]), + mappings: encode([ + [ + [0, 0, 0, 0], + [2, 0, 0, 3], + [4, 0, 0, 2], + [5, 0, 0, 5], + [6, 1, 0, 3], + ], + ]), names: [], sources: ['lib/b.js', 'lib/c.js'], version: 3, @@ -584,7 +731,12 @@ runInEachFileSystem(() => { origin: ContentOrigin.Provided, }; const aSource = new SourceFile( - _('/foo/src/a.js'), 'abdecf123', aToBCSourceMap, [bSource, cSource2], fs); + _('/foo/src/a.js'), + 'abdecf123', + aToBCSourceMap, + [bSource, cSource2], + fs, + ); const aTocSourceMap = aSource.renderFlattenedSourceMap(); expect(aTocSourceMap.version).toEqual(3); @@ -593,10 +745,19 @@ runInEachFileSystem(() => { expect(aTocSourceMap.sourceRoot).toBeUndefined(); expect(aTocSourceMap.sources).toEqual(['lib/c.js']); expect(aTocSourceMap.sourcesContent).toEqual(['bcd123e']); - expect(aTocSourceMap.mappings).toEqual(encode([[ - [1, 0, 0, 0], [2, 0, 0, 2], [3, 0, 0, 3], [3, 0, 0, 6], [4, 0, 0, 1], [5, 0, 0, 7], - [6, 0, 0, 3] - ]])); + expect(aTocSourceMap.mappings).toEqual( + encode([ + [ + [1, 0, 0, 0], + [2, 0, 0, 2], + [3, 0, 0, 3], + [3, 0, 0, 6], + [4, 0, 0, 1], + [5, 0, 0, 7], + [6, 0, 0, 3], + ], + ]), + ); }); }); @@ -615,143 +776,163 @@ runInEachFileSystem(() => { map: { mappings: encode([ [ - [0, 1, 0, 0], // "a" is in d.js [source 1] - [1, 0, 0, 0], // "bcd" are in c.js [source 0] - [4, 1, 0, 1], // "ef" are in d.js [source 1] + [0, 1, 0, 0], // "a" is in d.js [source 1] + [1, 0, 0, 0], // "bcd" are in c.js [source 0] + [4, 1, 0, 1], // "ef" are in d.js [source 1] ], ]), names: [], sources: ['c.js', 'd.js'], - version: 3 + version: 3, }, origin: ContentOrigin.Provided, }; - const bSource = - new SourceFile(_('/foo/src/b.js'), 'abcdef', bSourceMap, [cSource, dSource], fs); + const bSource = new SourceFile( + _('/foo/src/b.js'), + 'abcdef', + bSourceMap, + [cSource, dSource], + fs, + ); const aSourceMap: SourceMapInfo = { mapPath: null, map: { mappings: encode([ [ - [0, 0, 0, 0], [2, 0, 0, 3], // "c" is missing from first line + [0, 0, 0, 0], + [2, 0, 0, 3], // "c" is missing from first line ], [ - [4, 0, 0, 2], // second line has new indentation, and starts - // with "c" - [5, 0, 0, 5], // "f" is here + [4, 0, 0, 2], // second line has new indentation, and starts + // with "c" + [5, 0, 0, 5], // "f" is here ], ]), names: [], sources: ['b.js'], - version: 3 + version: 3, }, origin: ContentOrigin.Provided, }; - const aSource = - new SourceFile(_('/foo/src/a.js'), 'abde\n cf', aSourceMap, [bSource], fs); + const aSource = new SourceFile( + _('/foo/src/a.js'), + 'abde\n cf', + aSourceMap, + [bSource], + fs, + ); // Line 0 - expect(aSource.getOriginalLocation(0, 0)) // a - .toEqual({file: dSource.sourcePath, line: 0, column: 0}); - expect(aSource.getOriginalLocation(0, 1)) // b - .toEqual({file: cSource.sourcePath, line: 0, column: 0}); - expect(aSource.getOriginalLocation(0, 2)) // d - .toEqual({file: cSource.sourcePath, line: 0, column: 2}); - expect(aSource.getOriginalLocation(0, 3)) // e - .toEqual({file: dSource.sourcePath, line: 0, column: 1}); - expect(aSource.getOriginalLocation(0, 4)) // off the end of the line - .toEqual({file: dSource.sourcePath, line: 0, column: 2}); + expect(aSource.getOriginalLocation(0, 0)) // a + .toEqual({file: dSource.sourcePath, line: 0, column: 0}); + expect(aSource.getOriginalLocation(0, 1)) // b + .toEqual({file: cSource.sourcePath, line: 0, column: 0}); + expect(aSource.getOriginalLocation(0, 2)) // d + .toEqual({file: cSource.sourcePath, line: 0, column: 2}); + expect(aSource.getOriginalLocation(0, 3)) // e + .toEqual({file: dSource.sourcePath, line: 0, column: 1}); + expect(aSource.getOriginalLocation(0, 4)) // off the end of the line + .toEqual({file: dSource.sourcePath, line: 0, column: 2}); // Line 1 - expect(aSource.getOriginalLocation(1, 0)) // indent - .toEqual({file: dSource.sourcePath, line: 0, column: 3}); - expect(aSource.getOriginalLocation(1, 1)) // indent - .toEqual({file: dSource.sourcePath, line: 0, column: 4}); - expect(aSource.getOriginalLocation(1, 2)) // indent - .toEqual({file: dSource.sourcePath, line: 0, column: 5}); - expect(aSource.getOriginalLocation(1, 3)) // indent - .toEqual({file: dSource.sourcePath, line: 0, column: 6}); - expect(aSource.getOriginalLocation(1, 4)) // c - .toEqual({file: cSource.sourcePath, line: 0, column: 1}); - expect(aSource.getOriginalLocation(1, 5)) // f - .toEqual({file: dSource.sourcePath, line: 0, column: 2}); - expect(aSource.getOriginalLocation(1, 6)) // off the end of the line - .toEqual({file: dSource.sourcePath, line: 0, column: 3}); + expect(aSource.getOriginalLocation(1, 0)) // indent + .toEqual({file: dSource.sourcePath, line: 0, column: 3}); + expect(aSource.getOriginalLocation(1, 1)) // indent + .toEqual({file: dSource.sourcePath, line: 0, column: 4}); + expect(aSource.getOriginalLocation(1, 2)) // indent + .toEqual({file: dSource.sourcePath, line: 0, column: 5}); + expect(aSource.getOriginalLocation(1, 3)) // indent + .toEqual({file: dSource.sourcePath, line: 0, column: 6}); + expect(aSource.getOriginalLocation(1, 4)) // c + .toEqual({file: cSource.sourcePath, line: 0, column: 1}); + expect(aSource.getOriginalLocation(1, 5)) // f + .toEqual({file: dSource.sourcePath, line: 0, column: 2}); + expect(aSource.getOriginalLocation(1, 6)) // off the end of the line + .toEqual({file: dSource.sourcePath, line: 0, column: 3}); }); it('should return offset locations across multiple lines', () => { - const originalSource = - new SourceFile(_('/foo/src/original.js'), 'abcdef\nghijk\nlmnop', null, [], fs); + const originalSource = new SourceFile( + _('/foo/src/original.js'), + 'abcdef\nghijk\nlmnop', + null, + [], + fs, + ); const generatedSourceMap: SourceMapInfo = { mapPath: null, map: { mappings: encode([ [ - [0, 0, 0, 0], // "ABC" [0,0] => [0,0] + [0, 0, 0, 0], // "ABC" [0,0] => [0,0] ], [ - [0, 0, 1, 0], // "GHIJ" [1, 0] => [1,0] - [4, 0, 0, 3], // "DEF" [1, 4] => [0,3] - [7, 0, 1, 4], // "K" [1, 7] => [1,4] + [0, 0, 1, 0], // "GHIJ" [1, 0] => [1,0] + [4, 0, 0, 3], // "DEF" [1, 4] => [0,3] + [7, 0, 1, 4], // "K" [1, 7] => [1,4] ], [ - [0, 0, 2, 0], // "LMNOP" [2,0] => [2,0] + [0, 0, 2, 0], // "LMNOP" [2,0] => [2,0] ], ]), names: [], sources: ['original.js'], - version: 3 + version: 3, }, origin: ContentOrigin.Provided, }; const generatedSource = new SourceFile( - _('/foo/src/generated.js'), 'ABC\nGHIJDEFK\nLMNOP', generatedSourceMap, - [originalSource], fs); + _('/foo/src/generated.js'), + 'ABC\nGHIJDEFK\nLMNOP', + generatedSourceMap, + [originalSource], + fs, + ); // Line 0 - expect(generatedSource.getOriginalLocation(0, 0)) // A - .toEqual({file: originalSource.sourcePath, line: 0, column: 0}); - expect(generatedSource.getOriginalLocation(0, 1)) // B - .toEqual({file: originalSource.sourcePath, line: 0, column: 1}); - expect(generatedSource.getOriginalLocation(0, 2)) // C - .toEqual({file: originalSource.sourcePath, line: 0, column: 2}); - expect(generatedSource.getOriginalLocation(0, 3)) // off the end of line 0 - .toEqual({file: originalSource.sourcePath, line: 0, column: 3}); + expect(generatedSource.getOriginalLocation(0, 0)) // A + .toEqual({file: originalSource.sourcePath, line: 0, column: 0}); + expect(generatedSource.getOriginalLocation(0, 1)) // B + .toEqual({file: originalSource.sourcePath, line: 0, column: 1}); + expect(generatedSource.getOriginalLocation(0, 2)) // C + .toEqual({file: originalSource.sourcePath, line: 0, column: 2}); + expect(generatedSource.getOriginalLocation(0, 3)) // off the end of line 0 + .toEqual({file: originalSource.sourcePath, line: 0, column: 3}); // Line 1 - expect(generatedSource.getOriginalLocation(1, 0)) // G - .toEqual({file: originalSource.sourcePath, line: 1, column: 0}); - expect(generatedSource.getOriginalLocation(1, 1)) // H - .toEqual({file: originalSource.sourcePath, line: 1, column: 1}); - expect(generatedSource.getOriginalLocation(1, 2)) // I - .toEqual({file: originalSource.sourcePath, line: 1, column: 2}); - expect(generatedSource.getOriginalLocation(1, 3)) // J - .toEqual({file: originalSource.sourcePath, line: 1, column: 3}); - expect(generatedSource.getOriginalLocation(1, 4)) // D - .toEqual({file: originalSource.sourcePath, line: 0, column: 3}); - expect(generatedSource.getOriginalLocation(1, 5)) // E - .toEqual({file: originalSource.sourcePath, line: 0, column: 4}); - expect(generatedSource.getOriginalLocation(1, 6)) // F - .toEqual({file: originalSource.sourcePath, line: 0, column: 5}); - expect(generatedSource.getOriginalLocation(1, 7)) // K - .toEqual({file: originalSource.sourcePath, line: 1, column: 4}); - expect(generatedSource.getOriginalLocation(1, 8)) // off the end of line 1 - .toEqual({file: originalSource.sourcePath, line: 1, column: 5}); + expect(generatedSource.getOriginalLocation(1, 0)) // G + .toEqual({file: originalSource.sourcePath, line: 1, column: 0}); + expect(generatedSource.getOriginalLocation(1, 1)) // H + .toEqual({file: originalSource.sourcePath, line: 1, column: 1}); + expect(generatedSource.getOriginalLocation(1, 2)) // I + .toEqual({file: originalSource.sourcePath, line: 1, column: 2}); + expect(generatedSource.getOriginalLocation(1, 3)) // J + .toEqual({file: originalSource.sourcePath, line: 1, column: 3}); + expect(generatedSource.getOriginalLocation(1, 4)) // D + .toEqual({file: originalSource.sourcePath, line: 0, column: 3}); + expect(generatedSource.getOriginalLocation(1, 5)) // E + .toEqual({file: originalSource.sourcePath, line: 0, column: 4}); + expect(generatedSource.getOriginalLocation(1, 6)) // F + .toEqual({file: originalSource.sourcePath, line: 0, column: 5}); + expect(generatedSource.getOriginalLocation(1, 7)) // K + .toEqual({file: originalSource.sourcePath, line: 1, column: 4}); + expect(generatedSource.getOriginalLocation(1, 8)) // off the end of line 1 + .toEqual({file: originalSource.sourcePath, line: 1, column: 5}); // Line 2 - expect(generatedSource.getOriginalLocation(2, 0)) // L - .toEqual({file: originalSource.sourcePath, line: 2, column: 0}); - expect(generatedSource.getOriginalLocation(2, 1)) // M - .toEqual({file: originalSource.sourcePath, line: 2, column: 1}); - expect(generatedSource.getOriginalLocation(2, 2)) // N - .toEqual({file: originalSource.sourcePath, line: 2, column: 2}); - expect(generatedSource.getOriginalLocation(2, 3)) // O - .toEqual({file: originalSource.sourcePath, line: 2, column: 3}); - expect(generatedSource.getOriginalLocation(2, 4)) // P - .toEqual({file: originalSource.sourcePath, line: 2, column: 4}); - expect(generatedSource.getOriginalLocation(2, 5)) // off the end of line 2 - .toEqual({file: originalSource.sourcePath, line: 2, column: 5}); + expect(generatedSource.getOriginalLocation(2, 0)) // L + .toEqual({file: originalSource.sourcePath, line: 2, column: 0}); + expect(generatedSource.getOriginalLocation(2, 1)) // M + .toEqual({file: originalSource.sourcePath, line: 2, column: 1}); + expect(generatedSource.getOriginalLocation(2, 2)) // N + .toEqual({file: originalSource.sourcePath, line: 2, column: 2}); + expect(generatedSource.getOriginalLocation(2, 3)) // O + .toEqual({file: originalSource.sourcePath, line: 2, column: 3}); + expect(generatedSource.getOriginalLocation(2, 4)) // P + .toEqual({file: originalSource.sourcePath, line: 2, column: 4}); + expect(generatedSource.getOriginalLocation(2, 5)) // off the end of line 2 + .toEqual({file: originalSource.sourcePath, line: 2, column: 5}); }); }); }); diff --git a/packages/compiler-cli/src/ngtsc/testing/fake_common/index.ts b/packages/compiler-cli/src/ngtsc/testing/fake_common/index.ts index af6baa3ecf82e..630dafba8962e 100644 --- a/packages/compiler-cli/src/ngtsc/testing/fake_common/index.ts +++ b/packages/compiler-cli/src/ngtsc/testing/fake_common/index.ts @@ -6,7 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import {NgIterable, TemplateRef, TrackByFunction, ɵɵDirectiveDeclaration, ɵɵNgModuleDeclaration, ɵɵPipeDeclaration} from '@angular/core'; +import { + NgIterable, + TemplateRef, + TrackByFunction, + ɵɵDirectiveDeclaration, + ɵɵNgModuleDeclaration, + ɵɵPipeDeclaration, +} from '@angular/core'; export interface NgForOfContext> { $implicit: T; @@ -28,54 +35,83 @@ export interface NgIfContext { * A fake version of the NgFor directive. */ export declare class NgForOf> { - ngForOf: U&NgIterable|null|undefined; + ngForOf: (U & NgIterable) | null | undefined; ngForTrackBy: TrackByFunction; ngForTemplate: TemplateRef>; - static ɵdir: ɵɵDirectiveDeclaration < NgForOf, '[ngFor][ngForOf]', never, { - 'ngForOf': 'ngForOf'; - 'ngForTrackBy': 'ngForTrackBy'; - 'ngForTemplate': 'ngForTemplate'; - } - , {}, never > ; - static ngTemplateContextGuard>(dir: NgForOf, ctx: any): - ctx is NgForOfContext; + static ɵdir: ɵɵDirectiveDeclaration< + NgForOf, + '[ngFor][ngForOf]', + never, + { + 'ngForOf': 'ngForOf'; + 'ngForTrackBy': 'ngForTrackBy'; + 'ngForTemplate': 'ngForTemplate'; + }, + {}, + never + >; + static ngTemplateContextGuard>( + dir: NgForOf, + ctx: any, + ): ctx is NgForOfContext; } export declare class NgIf { ngIf: T; - ngIfThen: TemplateRef>|null; - ngIfElse: TemplateRef>|null; - static ɵdir: ɵɵDirectiveDeclaration < NgIf, '[ngIf]', never, { - 'ngIf': 'ngIf'; - 'ngIfThen': 'ngIfThen'; - 'ngIfElse': 'ngIfElse'; - } - , {}, never > ; + ngIfThen: TemplateRef> | null; + ngIfElse: TemplateRef> | null; + static ɵdir: ɵɵDirectiveDeclaration< + NgIf, + '[ngIf]', + never, + { + 'ngIf': 'ngIf'; + 'ngIfThen': 'ngIfThen'; + 'ngIfElse': 'ngIfElse'; + }, + {}, + never + >; static ngTemplateGuard_ngIf: 'binding'; - static ngTemplateContextGuard(dir: NgIf, ctx: any): - ctx is NgIfContext>; + static ngTemplateContextGuard( + dir: NgIf, + ctx: any, + ): ctx is NgIfContext>; } export declare class NgTemplateOutlet { - ngTemplateOutlet: TemplateRef|null; - ngTemplateOutletContext: C|null; + ngTemplateOutlet: TemplateRef | null; + ngTemplateOutletContext: C | null; - static ɵdir: ɵɵDirectiveDeclaration < NgTemplateOutlet, '[ngTemplateOutlet]', never, { - 'ngTemplateOutlet': 'ngTemplateOutlet'; - 'ngTemplateOutletContext': 'ngTemplateOutletContext'; - } - , {}, never > ; + static ɵdir: ɵɵDirectiveDeclaration< + NgTemplateOutlet, + '[ngTemplateOutlet]', + never, + { + 'ngTemplateOutlet': 'ngTemplateOutlet'; + 'ngTemplateOutletContext': 'ngTemplateOutletContext'; + }, + {}, + never + >; static ngTemplateContextGuard(dir: NgTemplateOutlet, ctx: any): ctx is T; } export declare class DatePipe { - transform(value: Date|string|number, format?: string, timezone?: string, locale?: string): string - |null; - transform(value: null|undefined, format?: string, timezone?: string, locale?: string): null; transform( - value: Date|string|number|null|undefined, format?: string, timezone?: string, - locale?: string): string|null; + value: Date | string | number, + format?: string, + timezone?: string, + locale?: string, + ): string | null; + transform(value: null | undefined, format?: string, timezone?: string, locale?: string): null; + transform( + value: Date | string | number | null | undefined, + format?: string, + timezone?: string, + locale?: string, + ): string | null; static ɵpipe: ɵɵPipeDeclaration; } @@ -87,8 +123,9 @@ export declare class IndexPipe { export declare class CommonModule { static ɵmod: ɵɵNgModuleDeclaration< - CommonModule, - [typeof NgForOf, typeof NgIf, typeof DatePipe, typeof IndexPipe, typeof NgTemplateOutlet], - never, - [typeof NgForOf, typeof NgIf, typeof DatePipe, typeof IndexPipe, typeof NgTemplateOutlet]>; + CommonModule, + [typeof NgForOf, typeof NgIf, typeof DatePipe, typeof IndexPipe, typeof NgTemplateOutlet], + never, + [typeof NgForOf, typeof NgIf, typeof DatePipe, typeof IndexPipe, typeof NgTemplateOutlet] + >; } diff --git a/packages/compiler-cli/src/ngtsc/testing/src/cached_source_files.ts b/packages/compiler-cli/src/ngtsc/testing/src/cached_source_files.ts index 04d64fad921a2..18543e6053a0b 100644 --- a/packages/compiler-cli/src/ngtsc/testing/src/cached_source_files.ts +++ b/packages/compiler-cli/src/ngtsc/testing/src/cached_source_files.ts @@ -28,9 +28,13 @@ let sourceFileCache = new Map(); * is available to verify that the cached `ts.SourceFile` corresponds with the contents on disk. */ export function getCachedSourceFile( - fileName: string, load: () => string | undefined): ts.SourceFile|null { - if (!/^lib\..+\.d\.ts$/.test(basename(fileName)) && - !/\/node_modules\/(@angular|rxjs)\//.test(fileName)) { + fileName: string, + load: () => string | undefined, +): ts.SourceFile | null { + if ( + !/^lib\..+\.d\.ts$/.test(basename(fileName)) && + !/\/node_modules\/(@angular|rxjs)\//.test(fileName) + ) { return null; } diff --git a/packages/compiler-cli/src/ngtsc/testing/src/compiler_host.ts b/packages/compiler-cli/src/ngtsc/testing/src/compiler_host.ts index 8711b35396d96..934d42ec522ce 100644 --- a/packages/compiler-cli/src/ngtsc/testing/src/compiler_host.ts +++ b/packages/compiler-cli/src/ngtsc/testing/src/compiler_host.ts @@ -15,8 +15,10 @@ import {getCachedSourceFile} from './cached_source_files'; * reuse across tests. */ export class NgtscTestCompilerHost extends NgtscCompilerHost { - override getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile - |undefined { + override getSourceFile( + fileName: string, + languageVersion: ts.ScriptTarget, + ): ts.SourceFile | undefined { const cachedSf = getCachedSourceFile(fileName, () => this.readFile(fileName)); if (cachedSf !== null) { return cachedSf; diff --git a/packages/compiler-cli/src/ngtsc/testing/src/mock_file_loading.ts b/packages/compiler-cli/src/ngtsc/testing/src/mock_file_loading.ts index f44825def591b..b542d955dc342 100644 --- a/packages/compiler-cli/src/ngtsc/testing/src/mock_file_loading.ts +++ b/packages/compiler-cli/src/ngtsc/testing/src/mock_file_loading.ts @@ -17,7 +17,7 @@ import {getAngularPackagesFromRunfiles, resolveFromRunfiles} from './runfile_hel export function loadTestFiles(files: TestFile[]) { const fs = getFileSystem(); - files.forEach(file => { + files.forEach((file) => { fs.ensureDir(fs.dirname(file.name)); fs.writeFile(file.name, file.contents); }); @@ -27,7 +27,7 @@ export function loadTestFiles(files: TestFile[]) { * A folder that is lazily loaded upon first access and then cached. */ class CachedFolder { - private folder: Folder|null = null; + private folder: Folder | null = null; constructor(private loader: () => Folder) {} @@ -39,13 +39,16 @@ class CachedFolder { } } -const typescriptFolder = - new CachedFolder(() => loadFolder(resolveFromRunfiles('npm/node_modules/typescript'))); +const typescriptFolder = new CachedFolder(() => + loadFolder(resolveFromRunfiles('npm/node_modules/typescript')), +); const angularFolder = new CachedFolder(loadAngularFolder); const rxjsFolder = new CachedFolder(() => loadFolder(resolveFromRunfiles('npm/node_modules/rxjs'))); -export function loadStandardTestFiles( - {fakeCommon = false, rxjs = false}: {fakeCommon?: boolean, rxjs?: boolean} = {}): Folder { +export function loadStandardTestFiles({ + fakeCommon = false, + rxjs = false, +}: {fakeCommon?: boolean; rxjs?: boolean} = {}): Folder { const tmpFs = new MockFileSystemPosix(true); const basePath = '/' as AbsoluteFsPath; @@ -68,22 +71,26 @@ export function loadStandardTestFiles( export function loadTsLib(fs: FileSystem, basePath: string = '/') { loadTestDirectory( - fs, resolveFromRunfiles('npm/node_modules/tslib'), - fs.resolve(basePath, 'node_modules/tslib')); + fs, + resolveFromRunfiles('npm/node_modules/tslib'), + fs.resolve(basePath, 'node_modules/tslib'), + ); } export function loadFakeCommon(fs: FileSystem, basePath: string = '/') { loadTestDirectory( - fs, - resolveFromRunfiles( - 'angular/packages/compiler-cli/src/ngtsc/testing/fake_common/npm_package'), - fs.resolve(basePath, 'node_modules/@angular/common')); + fs, + resolveFromRunfiles('angular/packages/compiler-cli/src/ngtsc/testing/fake_common/npm_package'), + fs.resolve(basePath, 'node_modules/@angular/common'), + ); } export function loadAngularCore(fs: FileSystem, basePath: string = '/') { loadTestDirectory( - fs, resolveFromRunfiles('angular/packages/core/npm_package'), - fs.resolve(basePath, 'node_modules/@angular/core')); + fs, + resolveFromRunfiles('angular/packages/core/npm_package'), + fs.resolve(basePath, 'node_modules/@angular/core'), + ); } function loadFolder(path: string): Folder { @@ -114,8 +121,11 @@ function loadAngularFolder(): Folder { * @param mockPath the path within the mock file-system where the directory is to be loaded. */ export function loadTestDirectory( - fs: FileSystem, directoryPath: string, mockPath: AbsoluteFsPath): void { - readdirSync(directoryPath).forEach(item => { + fs: FileSystem, + directoryPath: string, + mockPath: AbsoluteFsPath, +): void { + readdirSync(directoryPath).forEach((item) => { const srcPath = resolve(directoryPath, item); const targetPath = fs.resolve(mockPath, item); try { diff --git a/packages/compiler-cli/src/ngtsc/testing/src/runfile_helpers.ts b/packages/compiler-cli/src/ngtsc/testing/src/runfile_helpers.ts index 3c4ebbb80da74..607990f775e3a 100644 --- a/packages/compiler-cli/src/ngtsc/testing/src/runfile_helpers.ts +++ b/packages/compiler-cli/src/ngtsc/testing/src/runfile_helpers.ts @@ -25,19 +25,21 @@ export function getAngularPackagesFromRunfiles() { if (!runfilesManifestPath) { const packageRunfilesDir = path.join(process.env['RUNFILES']!, 'angular/packages'); - return fs.readdirSync(packageRunfilesDir) - .map(name => ({name, pkgPath: path.join(packageRunfilesDir, name, 'npm_package/')})) - .filter(({pkgPath}) => fs.existsSync(pkgPath)); + return fs + .readdirSync(packageRunfilesDir) + .map((name) => ({name, pkgPath: path.join(packageRunfilesDir, name, 'npm_package/')})) + .filter(({pkgPath}) => fs.existsSync(pkgPath)); } - return fs.readFileSync(runfilesManifestPath, 'utf8') - .split('\n') - .map(mapping => mapping.split(' ')) - .filter(([runfilePath]) => runfilePath.match(/^angular\/packages\/[\w-]+\/npm_package$/)) - .map(([runfilePath, realPath]) => ({ - name: path.relative('angular/packages', runfilePath).split(path.sep)[0], - pkgPath: realPath, - })); + return fs + .readFileSync(runfilesManifestPath, 'utf8') + .split('\n') + .map((mapping) => mapping.split(' ')) + .filter(([runfilePath]) => runfilePath.match(/^angular\/packages\/[\w-]+\/npm_package$/)) + .map(([runfilePath, realPath]) => ({ + name: path.relative('angular/packages', runfilePath).split(path.sep)[0], + pkgPath: realPath, + })); } /** Resolves a file or directory from the Bazel runfiles. */ diff --git a/packages/compiler-cli/src/ngtsc/testing/src/utils.ts b/packages/compiler-cli/src/ngtsc/testing/src/utils.ts index 0baa2f31d46ab..608dfc16a7414 100644 --- a/packages/compiler-cli/src/ngtsc/testing/src/utils.ts +++ b/packages/compiler-cli/src/ngtsc/testing/src/utils.ts @@ -10,18 +10,26 @@ import ts from 'typescript'; -import {AbsoluteFsPath, dirname, getFileSystem, getSourceFileOrError, NgtscCompilerHost} from '../../file_system'; +import { + AbsoluteFsPath, + dirname, + getFileSystem, + getSourceFileOrError, + NgtscCompilerHost, +} from '../../file_system'; import {DeclarationNode} from '../../reflection'; import {getTokenAtPosition} from '../../util/src/typescript'; import {NgtscTestCompilerHost} from './compiler_host'; export function makeProgram( - files: {name: AbsoluteFsPath, contents: string, isRoot?: boolean}[], - options?: ts.CompilerOptions, host?: ts.CompilerHost, checkForErrors: boolean = true): - {program: ts.Program, host: ts.CompilerHost, options: ts.CompilerOptions} { + files: {name: AbsoluteFsPath; contents: string; isRoot?: boolean}[], + options?: ts.CompilerOptions, + host?: ts.CompilerHost, + checkForErrors: boolean = true, +): {program: ts.Program; host: ts.CompilerHost; options: ts.CompilerOptions} { const fs = getFileSystem(); - files.forEach(file => { + files.forEach((file) => { fs.ensureDir(dirname(file.name)); fs.writeFile(file.name, file.contents); }); @@ -30,20 +38,22 @@ export function makeProgram( noLib: true, experimentalDecorators: true, moduleResolution: ts.ModuleResolutionKind.Node10, - ...options + ...options, }; const compilerHost = new NgtscTestCompilerHost(fs, compilerOptions); - const rootNames = files.filter(file => file.isRoot !== false) - .map(file => compilerHost.getCanonicalFileName(file.name)); + const rootNames = files + .filter((file) => file.isRoot !== false) + .map((file) => compilerHost.getCanonicalFileName(file.name)); const program = ts.createProgram(rootNames, compilerOptions, compilerHost); if (checkForErrors) { const diags = [...program.getSyntacticDiagnostics(), ...program.getSemanticDiagnostics()]; if (diags.length > 0) { - const errors = diags.map(diagnostic => { + const errors = diags.map((diagnostic) => { let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); if (diagnostic.file) { - const {line, character} = - diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!); + const {line, character} = diagnostic.file.getLineAndCharacterOfPosition( + diagnostic.start!, + ); message = `${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`; } return `Error: ${message}`; @@ -62,8 +72,11 @@ export function makeProgram( * the `predicate` test. */ export function getDeclaration( - program: ts.Program, fileName: AbsoluteFsPath, name: string, - assert: (value: any) => value is T): T { + program: ts.Program, + fileName: AbsoluteFsPath, + name: string, + assert: (value: any) => value is T, +): T { const sf = getSourceFileOrError(program, fileName); const chosenDecls = walkForDeclarations(name, sf); @@ -72,9 +85,11 @@ export function getDeclaration( } const chosenDecl = chosenDecls.find(assert); if (chosenDecl === undefined) { - throw new Error(`Symbols with name ${name} in ${fileName} have types: ${ - chosenDecls.map(decl => ts.SyntaxKind[decl.kind])}. Expected one to pass predicate "${ - assert.name}()".`); + throw new Error( + `Symbols with name ${name} in ${fileName} have types: ${chosenDecls.map( + (decl) => ts.SyntaxKind[decl.kind], + )}. Expected one to pass predicate "${assert.name}()".`, + ); } return chosenDecl; } @@ -84,9 +99,9 @@ export function getDeclaration( */ export function walkForDeclarations(name: string, rootNode: ts.Node): DeclarationNode[] { const chosenDecls: DeclarationNode[] = []; - rootNode.forEachChild(node => { + rootNode.forEachChild((node) => { if (ts.isVariableStatement(node)) { - node.declarationList.declarations.forEach(decl => { + node.declarationList.declarations.forEach((decl) => { if (bindingNameEquals(decl.name, name)) { chosenDecls.push(decl); if (decl.initializer) { @@ -102,8 +117,11 @@ export function walkForDeclarations(name: string, rootNode: ts.Node): Declaratio } chosenDecls.push(...walkForDeclarations(name, node)); } else if ( - ts.isImportDeclaration(node) && node.importClause !== undefined && - node.importClause.name !== undefined && node.importClause.name.text === name) { + ts.isImportDeclaration(node) && + node.importClause !== undefined && + node.importClause.name !== undefined && + node.importClause.name.text === name + ) { chosenDecls.push(node.importClause); } else { chosenDecls.push(...walkForDeclarations(name, node)); @@ -112,13 +130,13 @@ export function walkForDeclarations(name: string, rootNode: ts.Node): Declaratio return chosenDecls; } -export function isNamedDeclaration(node: ts.Node): node is(ts.Declaration & {name: ts.Identifier}) { +export function isNamedDeclaration(node: ts.Node): node is ts.Declaration & {name: ts.Identifier} { const namedNode = node as {name?: ts.Identifier}; return namedNode.name !== undefined && ts.isIdentifier(namedNode.name); } const COMPLETE_REUSE_FAILURE_MESSAGE = - 'The original program was not reused completely, even though no changes should have been made to its structure'; + 'The original program was not reused completely, even though no changes should have been made to its structure'; /** * Extracted from TypeScript's internal enum `StructureIsReused`. @@ -132,8 +150,8 @@ enum TsStructureIsReused { export function expectCompleteReuse(program: ts.Program): void { // Assert complete reuse using TypeScript's private API. expect((program as any).structureIsReused) - .withContext(COMPLETE_REUSE_FAILURE_MESSAGE) - .toBe(TsStructureIsReused.Completely); + .withContext(COMPLETE_REUSE_FAILURE_MESSAGE) + .toBe(TsStructureIsReused.Completely); } function bindingNameEquals(node: ts.BindingName, name: string): boolean { @@ -146,15 +164,17 @@ function bindingNameEquals(node: ts.BindingName, name: string): boolean { export function getSourceCodeForDiagnostic(diag: ts.Diagnostic): string { if (diag.file === undefined || diag.start === undefined || diag.length === undefined) { throw new Error( - `Unable to get source code for diagnostic. Provided diagnostic instance doesn't contain "file", "start" and/or "length" properties.`); + `Unable to get source code for diagnostic. Provided diagnostic instance doesn't contain "file", "start" and/or "length" properties.`, + ); } const text = diag.file.text; return text.slice(diag.start, diag.start + diag.length); } export function diagnosticToNode( - diagnostic: ts.Diagnostic|ts.DiagnosticRelatedInformation, - guard: (node: ts.Node) => node is T): T { + diagnostic: ts.Diagnostic | ts.DiagnosticRelatedInformation, + guard: (node: ts.Node) => node is T, +): T { const diag = diagnostic as ts.Diagnostic | ts.DiagnosticRelatedInformation; if (diag.file === undefined) { throw new Error(`Expected ts.Diagnostic to have a file source`); diff --git a/packages/compiler-cli/src/ngtsc/transform/index.ts b/packages/compiler-cli/src/ngtsc/transform/index.ts index e3da19c3b15f3..1c6b2b5383447 100644 --- a/packages/compiler-cli/src/ngtsc/transform/index.ts +++ b/packages/compiler-cli/src/ngtsc/transform/index.ts @@ -9,6 +9,17 @@ export * from './src/api'; export {aliasTransformFactory} from './src/alias'; export {ClassRecord, TraitCompiler} from './src/compilation'; -export {declarationTransformFactory, DtsTransformRegistry, IvyDeclarationDtsTransform} from './src/declaration'; -export {AnalyzedTrait, PendingTrait, ResolvedTrait, SkippedTrait, Trait, TraitState} from './src/trait'; +export { + declarationTransformFactory, + DtsTransformRegistry, + IvyDeclarationDtsTransform, +} from './src/declaration'; +export { + AnalyzedTrait, + PendingTrait, + ResolvedTrait, + SkippedTrait, + Trait, + TraitState, +} from './src/trait'; export {ivyTransformFactory} from './src/transform'; diff --git a/packages/compiler-cli/src/ngtsc/transform/src/alias.ts b/packages/compiler-cli/src/ngtsc/transform/src/alias.ts index 122748f025fd8..68284e8075626 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/alias.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/alias.ts @@ -8,8 +8,9 @@ import ts from 'typescript'; -export function aliasTransformFactory(exportStatements: Map>): - ts.TransformerFactory { +export function aliasTransformFactory( + exportStatements: Map>, +): ts.TransformerFactory { return () => { return (file: ts.SourceFile) => { if (ts.isBundle(file) || !exportStatements.has(file.fileName)) { @@ -19,11 +20,13 @@ export function aliasTransformFactory(exportStatements: Map { const stmt = ts.factory.createExportDeclaration( - /* modifiers */ undefined, - /* isTypeOnly */ false, - /* exportClause */ ts.factory.createNamedExports([ts.factory.createExportSpecifier( - false, symbolName, aliasName)]), - /* moduleSpecifier */ ts.factory.createStringLiteral(moduleName)); + /* modifiers */ undefined, + /* isTypeOnly */ false, + /* exportClause */ ts.factory.createNamedExports([ + ts.factory.createExportSpecifier(false, symbolName, aliasName), + ]), + /* moduleSpecifier */ ts.factory.createStringLiteral(moduleName), + ); statements.push(stmt); }); diff --git a/packages/compiler-cli/src/ngtsc/transform/src/api.ts b/packages/compiler-cli/src/ngtsc/transform/src/api.ts index aeadd6e3810f7..17ee8b9dc1905 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/api.ts @@ -75,7 +75,7 @@ export enum HandlerPrecedence { * @param `A` The type of analysis metadata produced by `analyze`. * @param `R` The type of resolution metadata produced by `resolve`. */ -export interface DecoratorHandler { +export interface DecoratorHandler { readonly name: string; /** @@ -90,8 +90,7 @@ export interface DecoratorHandler { * Scan a set of reflected decorators and determine if this handler is responsible for compilation * of one of them. */ - detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult|undefined; - + detect(node: ClassDeclaration, decorators: Decorator[] | null): DetectResult | undefined; /** * Asynchronously perform pre-analysis on the decorator/class combination. @@ -99,7 +98,7 @@ export interface DecoratorHandler { * `preanalyze` is optional and is not guaranteed to be called through all compilation flows. It * will only be called if asynchronicity is supported in the CompilerHost. */ - preanalyze?(node: ClassDeclaration, metadata: Readonly): Promise|undefined; + preanalyze?(node: ClassDeclaration, metadata: Readonly): Promise | undefined; /** * Perform analysis on the decorator/class combination, extracting information from the class @@ -149,9 +148,12 @@ export interface DecoratorHandler { * `IndexingContext`, which stores information about components discovered in the * program. */ - index? - (context: IndexingContext, node: ClassDeclaration, analysis: Readonly, - resolution: Readonly): void; + index?( + context: IndexingContext, + node: ClassDeclaration, + analysis: Readonly, + resolution: Readonly, + ): void; /** * Perform resolution on the given decorator along with the result of analysis. @@ -168,17 +170,22 @@ export interface DecoratorHandler { */ xi18n?(bundle: Xi18nContext, node: ClassDeclaration, analysis: Readonly): void; - typeCheck? - (ctx: TypeCheckContext, node: ClassDeclaration, analysis: Readonly, - resolution: Readonly): void; + typeCheck?( + ctx: TypeCheckContext, + node: ClassDeclaration, + analysis: Readonly, + resolution: Readonly, + ): void; - extendedTemplateCheck? - (component: ts.ClassDeclaration, extendedTemplateChecker: ExtendedTemplateChecker): - ts.Diagnostic[]; + extendedTemplateCheck?( + component: ts.ClassDeclaration, + extendedTemplateChecker: ExtendedTemplateChecker, + ): ts.Diagnostic[]; - templateSemanticsCheck? - (component: ts.ClassDeclaration, templateSemanticsChecker: TemplateSemanticsChecker): - ts.Diagnostic[]; + templateSemanticsCheck?( + component: ts.ClassDeclaration, + templateSemanticsChecker: TemplateSemanticsChecker, + ): ts.Diagnostic[]; /** * Generate a description of the field which should be added to the class, including any @@ -188,8 +195,11 @@ export interface DecoratorHandler { * corresponding method is not provided, then this method is called as a fallback. */ compileFull( - node: ClassDeclaration, analysis: Readonly, resolution: Readonly, - constantPool: ConstantPool): CompileResult|CompileResult[]; + node: ClassDeclaration, + analysis: Readonly, + resolution: Readonly, + constantPool: ConstantPool, + ): CompileResult | CompileResult[]; /** * Generates code for the decorator using a stable, but intermediate format suitable to be @@ -199,17 +209,22 @@ export interface DecoratorHandler { * If present, this method is used if the compilation mode is configured as partial, otherwise * `compileFull` is. */ - compilePartial? - (node: ClassDeclaration, analysis: Readonly, resolution: Readonly): CompileResult - |CompileResult[]; + compilePartial?( + node: ClassDeclaration, + analysis: Readonly, + resolution: Readonly, + ): CompileResult | CompileResult[]; /** * Generates code based on each individual source file without using its * dependencies (suitable for local dev edit/refresh workflow) */ compileLocal( - node: ClassDeclaration, analysis: Readonly, resolution: Readonly>, - constantPool: ConstantPool): CompileResult|CompileResult[]; + node: ClassDeclaration, + analysis: Readonly, + resolution: Readonly>, + constantPool: ConstantPool, + ): CompileResult | CompileResult[]; } /** @@ -220,14 +235,14 @@ export interface DetectResult { /** * The node that triggered the match, which is typically a decorator. */ - trigger: ts.Node|null; + trigger: ts.Node | null; /** * Refers to the decorator that was recognized for this detection, if any. This can be a concrete * decorator that is actually present in a file, or a synthetic decorator as inserted * programmatically. */ - decorator: Decorator|null; + decorator: Decorator | null; /** * An arbitrary object to carry over from the detection phase into the analysis phase. @@ -251,10 +266,10 @@ export interface AnalysisOutput { */ export interface CompileResult { name: string; - initializer: Expression|null; + initializer: Expression | null; statements: Statement[]; type: Type; - deferrableImports: Set|null; + deferrableImports: Set | null; } export interface ResolveResult { @@ -265,10 +280,15 @@ export interface ResolveResult { export interface DtsTransform { transformClassElement?(element: ts.ClassElement, imports: ImportManager): ts.ClassElement; - transformFunctionDeclaration? - (element: ts.FunctionDeclaration, imports: ImportManager): ts.FunctionDeclaration; - transformClass? - (clazz: ts.ClassDeclaration, elements: ReadonlyArray, - reflector: ReflectionHost, refEmitter: ReferenceEmitter, - imports: ImportManager): ts.ClassDeclaration; + transformFunctionDeclaration?( + element: ts.FunctionDeclaration, + imports: ImportManager, + ): ts.FunctionDeclaration; + transformClass?( + clazz: ts.ClassDeclaration, + elements: ReadonlyArray, + reflector: ReflectionHost, + refEmitter: ReferenceEmitter, + imports: ImportManager, + ): ts.ClassDeclaration; } diff --git a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts index efe634f3a2567..98d71d3d45c3f 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -15,16 +15,28 @@ import {IncrementalBuild} from '../../incremental/api'; import {SemanticDepGraphUpdater, SemanticSymbol} from '../../incremental/semantic_graph'; import {IndexingContext} from '../../indexer'; import {PerfEvent, PerfRecorder} from '../../perf'; -import {ClassDeclaration, DeclarationNode, Decorator, isNamedClassDeclaration, ReflectionHost} from '../../reflection'; +import { + ClassDeclaration, + DeclarationNode, + Decorator, + isNamedClassDeclaration, + ReflectionHost, +} from '../../reflection'; import {ProgramTypeCheckAdapter, TypeCheckContext} from '../../typecheck/api'; import {getSourceFile} from '../../util/src/typescript'; import {Xi18nContext} from '../../xi18n'; -import {AnalysisOutput, CompilationMode, CompileResult, DecoratorHandler, HandlerPrecedence, ResolveResult} from './api'; +import { + AnalysisOutput, + CompilationMode, + CompileResult, + DecoratorHandler, + HandlerPrecedence, + ResolveResult, +} from './api'; import {DtsTransformRegistry} from './declaration'; import {PendingTrait, Trait, TraitState} from './trait'; - /** * Records information about a specific class that has matched traits. */ @@ -37,13 +49,13 @@ export interface ClassRecord { /** * All traits which matched on the class. */ - traits: Trait[]; + traits: Trait[]; /** * Meta-diagnostics about the class, which are usually related to whether certain combinations of * Angular decorators are not permitted. */ - metaDiagnostics: ts.Diagnostic[]|null; + metaDiagnostics: ts.Diagnostic[] | null; // Subsequent fields are "internal" and used during the matching of `DecoratorHandler`s. This is // mutable state during the `detect`/`analyze` phases of compilation. @@ -91,19 +103,21 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { private reexportMap = new Map>(); - private handlersByName = - new Map>(); + private handlersByName = new Map< + string, + DecoratorHandler + >(); constructor( - private handlers: DecoratorHandler[], - private reflector: ReflectionHost, - private perf: PerfRecorder, - private incrementalBuild: IncrementalBuild, - private compileNonExportedClasses: boolean, - private compilationMode: CompilationMode, - private dtsTransforms: DtsTransformRegistry, - private semanticDepGraphUpdater: SemanticDepGraphUpdater|null, - private sourceFileTypeIdentifier: SourceFileTypeIdentifier, + private handlers: DecoratorHandler[], + private reflector: ReflectionHost, + private perf: PerfRecorder, + private incrementalBuild: IncrementalBuild, + private compileNonExportedClasses: boolean, + private compilationMode: CompilationMode, + private dtsTransforms: DtsTransformRegistry, + private semanticDepGraphUpdater: SemanticDepGraphUpdater | null, + private sourceFileTypeIdentifier: SourceFileTypeIdentifier, ) { for (const handler of handlers) { this.handlersByName.set(handler.name, handler); @@ -114,16 +128,19 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { this.analyze(sf, false); } - analyzeAsync(sf: ts.SourceFile): Promise|undefined { + analyzeAsync(sf: ts.SourceFile): Promise | undefined { return this.analyze(sf, true); } private analyze(sf: ts.SourceFile, preanalyze: false): void; - private analyze(sf: ts.SourceFile, preanalyze: true): Promise|undefined; - private analyze(sf: ts.SourceFile, preanalyze: boolean): Promise|undefined { + private analyze(sf: ts.SourceFile, preanalyze: true): Promise | undefined; + private analyze(sf: ts.SourceFile, preanalyze: boolean): Promise | undefined { // We shouldn't analyze declaration, shim, or resource files. - if (sf.isDeclarationFile || this.sourceFileTypeIdentifier.isShim(sf) || - this.sourceFileTypeIdentifier.isResource(sf)) { + if ( + sf.isDeclarationFile || + this.sourceFileTypeIdentifier.isShim(sf) || + this.sourceFileTypeIdentifier.isResource(sf) + ) { return undefined; } @@ -132,9 +149,10 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { const promises: Promise[] = []; // Local compilation does not support incremental build. - const priorWork = (this.compilationMode !== CompilationMode.LOCAL) ? - this.incrementalBuild.priorAnalysisFor(sf) : - null; + const priorWork = + this.compilationMode !== CompilationMode.LOCAL + ? this.incrementalBuild.priorAnalysisFor(sf) + : null; if (priorWork !== null) { this.perf.eventCount(PerfEvent.SourceFileReuseAnalysis); @@ -175,7 +193,7 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { } } - recordFor(clazz: ClassDeclaration): ClassRecord|null { + recordFor(clazz: ClassDeclaration): ClassRecord | null { if (this.classes.has(clazz)) { return this.classes.get(clazz)!; } else { @@ -218,8 +236,10 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { for (const priorTrait of priorRecord.traits) { const handler = this.handlersByName.get(priorTrait.handler.name)!; - let trait: Trait = - Trait.pending(handler, priorTrait.detected); + let trait: Trait = Trait.pending( + handler, + priorTrait.detected, + ); if (priorTrait.state === TraitState.Analyzed || priorTrait.state === TraitState.Resolved) { const symbol = this.makeSymbolForTrait(handler, record.node, priorTrait.analysis); @@ -242,8 +262,9 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { this.fileToClasses.get(sf)!.add(record.node); } - private scanClassForTraits(clazz: ClassDeclaration): - PendingTrait[]|null { + private scanClassForTraits( + clazz: ClassDeclaration, + ): PendingTrait[] | null { if (!this.compileNonExportedClasses && !this.reflector.isStaticallyExported(clazz)) { return null; } @@ -253,15 +274,17 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { return this.detectTraits(clazz, decorators); } - protected detectTraits(clazz: ClassDeclaration, decorators: Decorator[]|null): - PendingTrait[]|null { - let record: ClassRecord|null = this.recordFor(clazz); - let foundTraits: PendingTrait[] = []; + protected detectTraits( + clazz: ClassDeclaration, + decorators: Decorator[] | null, + ): PendingTrait[] | null { + let record: ClassRecord | null = this.recordFor(clazz); + let foundTraits: PendingTrait[] = []; // A set to track the non-Angular decorators in local compilation mode. An error will be issued // if non-Angular decorators is found in local compilation mode. const nonNgDecoratorsInLocalMode = - this.compilationMode === CompilationMode.LOCAL ? new Set(decorators) : null; + this.compilationMode === CompilationMode.LOCAL ? new Set(decorators) : null; for (const handler of this.handlers) { const result = handler.detect(clazz, decorators); @@ -310,8 +333,9 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { if (!isWeakHandler && record.hasWeakHandlers) { // The current handler is not a WEAK handler, but the class has other WEAK handlers. // Remove them. - record.traits = - record.traits.filter(field => field.handler.precedence !== HandlerPrecedence.WEAK); + record.traits = record.traits.filter( + (field) => field.handler.precedence !== HandlerPrecedence.WEAK, + ); record.hasWeakHandlers = false; } else if (isWeakHandler && !record.hasWeakHandlers) { // The current handler is a WEAK handler, but the class has non-WEAK handlers already. @@ -321,14 +345,16 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { if (isPrimaryHandler && record.hasPrimaryHandler) { // The class already has a PRIMARY handler, and another one just matched. - record.metaDiagnostics = [{ - category: ts.DiagnosticCategory.Error, - code: Number('-99' + ErrorCode.DECORATOR_COLLISION), - file: getSourceFile(clazz), - start: clazz.getStart(undefined, false), - length: clazz.getWidth(), - messageText: 'Two incompatible decorators on class', - }]; + record.metaDiagnostics = [ + { + category: ts.DiagnosticCategory.Error, + code: Number('-99' + ErrorCode.DECORATOR_COLLISION), + file: getSourceFile(clazz), + start: clazz.getStart(undefined, false), + length: clazz.getWidth(), + messageText: 'Two incompatible decorators on class', + }, + ]; record.traits = foundTraits = []; break; } @@ -340,20 +366,23 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { } } - if (nonNgDecoratorsInLocalMode !== null && nonNgDecoratorsInLocalMode.size > 0 && - record !== null && record.metaDiagnostics === null) { + if ( + nonNgDecoratorsInLocalMode !== null && + nonNgDecoratorsInLocalMode.size > 0 && + record !== null && + record.metaDiagnostics === null + ) { // Custom decorators found in local compilation mode! In this mode we don't support custom // decorators yet. But will eventually do (b/320536434). For now a temporary error is thrown. - record.metaDiagnostics = [...nonNgDecoratorsInLocalMode].map( - decorator => ({ - category: ts.DiagnosticCategory.Error, - code: Number('-99' + ErrorCode.DECORATOR_UNEXPECTED), - file: getSourceFile(clazz), - start: decorator.node.getStart(), - length: decorator.node.getWidth(), - messageText: - 'In local compilation mode, Angular does not support custom decorators. Ensure all class decorators are from Angular.', - })); + record.metaDiagnostics = [...nonNgDecoratorsInLocalMode].map((decorator) => ({ + category: ts.DiagnosticCategory.Error, + code: Number('-99' + ErrorCode.DECORATOR_UNEXPECTED), + file: getSourceFile(clazz), + start: decorator.node.getStart(), + length: decorator.node.getWidth(), + messageText: + 'In local compilation mode, Angular does not support custom decorators. Ensure all class decorators are from Angular.', + })); record.traits = foundTraits = []; } @@ -361,8 +390,10 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { } private makeSymbolForTrait( - handler: DecoratorHandler, - decl: ClassDeclaration, analysis: Readonly|null): SemanticSymbol|null { + handler: DecoratorHandler, + decl: ClassDeclaration, + analysis: Readonly | null, + ): SemanticSymbol | null { if (analysis === null) { return null; } @@ -371,7 +402,8 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { const isPrimary = handler.precedence === HandlerPrecedence.PRIMARY; if (!isPrimary) { throw new Error( - `AssertionError: ${handler.name} returned a symbol but is not a primary handler.`); + `AssertionError: ${handler.name} returned a symbol but is not a primary handler.`, + ); } this.semanticDepGraphUpdater.registerSymbol(symbol); } @@ -379,7 +411,7 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { return symbol; } - private analyzeClass(clazz: ClassDeclaration, preanalyzeQueue: Promise[]|null): void { + private analyzeClass(clazz: ClassDeclaration, preanalyzeQueue: Promise[] | null): void { const traits = this.scanClassForTraits(clazz); if (traits === null) { @@ -390,7 +422,7 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { for (const trait of traits) { const analyze = () => this.analyzeTrait(clazz, trait); - let preanalysis: Promise|null = null; + let preanalysis: Promise | null = null; if (preanalyzeQueue !== null && trait.handler.preanalyze !== undefined) { // Attempt to run preanalysis. This could fail with a `FatalDiagnosticError`; catch it if it // does. @@ -414,10 +446,15 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { } private analyzeTrait( - clazz: ClassDeclaration, trait: Trait): void { + clazz: ClassDeclaration, + trait: Trait, + ): void { if (trait.state !== TraitState.Pending) { - throw new Error(`Attempt to analyze trait of ${clazz.name.text} in state ${ - TraitState[trait.state]} (expected DETECTED)`); + throw new Error( + `Attempt to analyze trait of ${clazz.name.text} in state ${ + TraitState[trait.state] + } (expected DETECTED)`, + ); } this.perf.eventCount(PerfEvent.TraitAnalyze); @@ -452,8 +489,9 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { case TraitState.Skipped: continue; case TraitState.Pending: - throw new Error(`Resolving a trait that hasn't been analyzed: ${clazz.name.text} / ${ - trait.handler.name}`); + throw new Error( + `Resolving a trait that hasn't been analyzed: ${clazz.name.text} / ${trait.handler.name}`, + ); case TraitState.Resolved: throw new Error(`Resolving an already resolved trait`); } @@ -522,11 +560,12 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { } runAdditionalChecks( - sf: ts.SourceFile, - check: - (clazz: ts.ClassDeclaration, - handler: DecoratorHandler) => - ts.Diagnostic[] | null): ts.Diagnostic[] { + sf: ts.SourceFile, + check: ( + clazz: ts.ClassDeclaration, + handler: DecoratorHandler, + ) => ts.Diagnostic[] | null, + ): ts.Diagnostic[] { if (this.compilationMode === CompilationMode.LOCAL) { return []; } @@ -591,8 +630,11 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { updateResources(clazz: DeclarationNode): void { // Local compilation does not support incremental - if (this.compilationMode === CompilationMode.LOCAL || !this.reflector.isClass(clazz) || - !this.classes.has(clazz)) { + if ( + this.compilationMode === CompilationMode.LOCAL || + !this.reflector.isClass(clazz) || + !this.classes.has(clazz) + ) { return; } const record = this.classes.get(clazz)!; @@ -605,10 +647,13 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { } } - compile(clazz: DeclarationNode, constantPool: ConstantPool): CompileResult[]|null { + compile(clazz: DeclarationNode, constantPool: ConstantPool): CompileResult[] | null { const original = ts.getOriginalNode(clazz) as typeof clazz; - if (!this.reflector.isClass(clazz) || !this.reflector.isClass(original) || - !this.classes.has(original)) { + if ( + !this.reflector.isClass(clazz) || + !this.reflector.isClass(original) || + !this.classes.has(original) + ) { return null; } @@ -617,10 +662,13 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { let res: CompileResult[] = []; for (const trait of record.traits) { - let compileRes: CompileResult|CompileResult[]; + let compileRes: CompileResult | CompileResult[]; - if (trait.state !== TraitState.Resolved || containsErrors(trait.analysisDiagnostics) || - containsErrors(trait.resolveDiagnostics)) { + if ( + trait.state !== TraitState.Resolved || + containsErrors(trait.analysisDiagnostics) || + containsErrors(trait.resolveDiagnostics) + ) { // Cannot compile a trait that is not resolved, or had any errors in its declaration. continue; } @@ -629,37 +677,48 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { // `trait.analysis` is non-null asserted here because TypeScript does not recognize that // `Readonly` is nullable (as `unknown` itself is nullable) due to the way that // `Readonly` works. - compileRes = - trait.handler.compileLocal(clazz, trait.analysis!, trait.resolution!, constantPool); + compileRes = trait.handler.compileLocal( + clazz, + trait.analysis!, + trait.resolution!, + constantPool, + ); } else { // `trait.resolution` is non-null asserted below because TypeScript does not recognize that // `Readonly` is nullable (as `unknown` itself is nullable) due to the way that // `Readonly` works. - if (this.compilationMode === CompilationMode.PARTIAL && - trait.handler.compilePartial !== undefined) { + if ( + this.compilationMode === CompilationMode.PARTIAL && + trait.handler.compilePartial !== undefined + ) { compileRes = trait.handler.compilePartial(clazz, trait.analysis, trait.resolution!); } else { - compileRes = - trait.handler.compileFull(clazz, trait.analysis, trait.resolution!, constantPool); + compileRes = trait.handler.compileFull( + clazz, + trait.analysis, + trait.resolution!, + constantPool, + ); } } const compileMatchRes = compileRes; if (Array.isArray(compileMatchRes)) { for (const result of compileMatchRes) { - if (!res.some(r => r.name === result.name)) { + if (!res.some((r) => r.name === result.name)) { res.push(result); } } - } else if (!res.some(result => result.name === compileMatchRes.name)) { + } else if (!res.some((result) => result.name === compileMatchRes.name)) { res.push(compileMatchRes); } } // Look up the .d.ts transformer for the input file and record that at least one field was // generated, which will allow the .d.ts to be transformed later. - this.dtsTransforms.getIvyDeclarationTransform(original.getSourceFile()) - .addFields(original, res); + this.dtsTransforms + .getIvyDeclarationTransform(original.getSourceFile()) + .addFields(original, res); // Return the instruction to the transformer so the fields will be added. return res.length > 0 ? res : null; @@ -696,8 +755,10 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { diagnostics.push(...record.metaDiagnostics); } for (const trait of record.traits) { - if ((trait.state === TraitState.Analyzed || trait.state === TraitState.Resolved) && - trait.analysisDiagnostics !== null) { + if ( + (trait.state === TraitState.Analyzed || trait.state === TraitState.Resolved) && + trait.analysisDiagnostics !== null + ) { diagnostics.push(...trait.analysisDiagnostics); } if (trait.state === TraitState.Resolved) { @@ -713,7 +774,9 @@ export class TraitCompiler implements ProgramTypeCheckAdapter { } } -function containsErrors(diagnostics: ts.Diagnostic[]|null): boolean { - return diagnostics !== null && - diagnostics.some(diag => diag.category === ts.DiagnosticCategory.Error); +function containsErrors(diagnostics: ts.Diagnostic[] | null): boolean { + return ( + diagnostics !== null && + diagnostics.some((diag) => diag.category === ts.DiagnosticCategory.Error) + ); } diff --git a/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts b/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts index 61cbd443ed2f6..788570ed5e52d 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts @@ -11,7 +11,11 @@ import ts from 'typescript'; import {ImportRewriter, ReferenceEmitter} from '../../imports'; import {ClassDeclaration, ReflectionHost} from '../../reflection'; -import {ImportManager, presetImportManagerForceNamespaceImports, translateType} from '../../translator'; +import { + ImportManager, + presetImportManagerForceNamespaceImports, + translateType, +} from '../../translator'; import {DtsTransform} from './api'; @@ -33,7 +37,7 @@ export class DtsTransformRegistry { * Gets the dts transforms to be applied for the given source file, or `null` if no transform is * necessary. */ - getAllTransforms(sf: ts.SourceFile): DtsTransform[]|null { + getAllTransforms(sf: ts.SourceFile): DtsTransform[] | null { // No need to transform if it's not a declarations file, or if no changes have been requested // to the input file. Due to the way TypeScript afterDeclarations transformers work, the // `ts.SourceFile` path is the same as the original .ts. The only way we know it's actually a @@ -43,7 +47,7 @@ export class DtsTransformRegistry { } const originalSf = ts.getOriginalNode(sf) as ts.SourceFile; - let transforms: DtsTransform[]|null = null; + let transforms: DtsTransform[] | null = null; if (this.ivyDeclarationTransforms.has(originalSf)) { transforms = []; transforms.push(this.ivyDeclarationTransforms.get(originalSf)!); @@ -53,9 +57,11 @@ export class DtsTransformRegistry { } export function declarationTransformFactory( - transformRegistry: DtsTransformRegistry, reflector: ReflectionHost, - refEmitter: ReferenceEmitter, - importRewriter: ImportRewriter): ts.TransformerFactory { + transformRegistry: DtsTransformRegistry, + reflector: ReflectionHost, + refEmitter: ReferenceEmitter, + importRewriter: ImportRewriter, +): ts.TransformerFactory { return (context: ts.TransformationContext) => { const transformer = new DtsTransformer(context, reflector, refEmitter, importRewriter); return (fileOrBundle) => { @@ -77,15 +83,20 @@ export function declarationTransformFactory( */ class DtsTransformer { constructor( - private ctx: ts.TransformationContext, private reflector: ReflectionHost, - private refEmitter: ReferenceEmitter, private importRewriter: ImportRewriter) {} + private ctx: ts.TransformationContext, + private reflector: ReflectionHost, + private refEmitter: ReferenceEmitter, + private importRewriter: ImportRewriter, + ) {} /** * Transform the declaration file and add any declarations which were recorded. */ transform(sf: ts.SourceFile, transforms: DtsTransform[]): ts.SourceFile { - const imports = new ImportManager( - {...presetImportManagerForceNamespaceImports, rewriter: this.importRewriter}); + const imports = new ImportManager({ + ...presetImportManagerForceNamespaceImports, + rewriter: this.importRewriter, + }); const visitor: ts.Visitor = (node: ts.Node): ts.VisitResult => { if (ts.isClassDeclaration(node)) { @@ -106,9 +117,11 @@ class DtsTransformer { } private transformClassDeclaration( - clazz: ts.ClassDeclaration, transforms: DtsTransform[], - imports: ImportManager): ts.ClassDeclaration { - let elements: ts.ClassElement[]|ReadonlyArray = clazz.members; + clazz: ts.ClassDeclaration, + transforms: DtsTransform[], + imports: ImportManager, + ): ts.ClassDeclaration { + let elements: ts.ClassElement[] | ReadonlyArray = clazz.members; let elementsChanged = false; for (const transform of transforms) { @@ -132,10 +145,15 @@ class DtsTransformer { if (transform.transformClass !== undefined) { // If no DtsTransform has changed the class yet, then the (possibly mutated) elements have // not yet been incorporated. Otherwise, `newClazz.members` holds the latest class members. - const inputMembers = (clazz === newClazz ? elements : newClazz.members); + const inputMembers = clazz === newClazz ? elements : newClazz.members; newClazz = transform.transformClass( - newClazz, inputMembers, this.reflector, this.refEmitter, imports); + newClazz, + inputMembers, + this.reflector, + this.refEmitter, + imports, + ); } } @@ -143,20 +161,23 @@ class DtsTransformer { // an updated class declaration with the updated elements. if (elementsChanged && clazz === newClazz) { newClazz = ts.factory.updateClassDeclaration( - /* node */ clazz, - /* modifiers */ clazz.modifiers, - /* name */ clazz.name, - /* typeParameters */ clazz.typeParameters, - /* heritageClauses */ clazz.heritageClauses, - /* members */ elements); + /* node */ clazz, + /* modifiers */ clazz.modifiers, + /* name */ clazz.name, + /* typeParameters */ clazz.typeParameters, + /* heritageClauses */ clazz.heritageClauses, + /* members */ elements, + ); } return newClazz; } private transformFunctionDeclaration( - declaration: ts.FunctionDeclaration, transforms: DtsTransform[], - imports: ImportManager): ts.FunctionDeclaration { + declaration: ts.FunctionDeclaration, + transforms: DtsTransform[], + imports: ImportManager, + ): ts.FunctionDeclaration { let newDecl = declaration; for (const transform of transforms) { @@ -182,9 +203,12 @@ export class IvyDeclarationDtsTransform implements DtsTransform { } transformClass( - clazz: ts.ClassDeclaration, members: ReadonlyArray, - reflector: ReflectionHost, refEmitter: ReferenceEmitter, - imports: ImportManager): ts.ClassDeclaration { + clazz: ts.ClassDeclaration, + members: ReadonlyArray, + reflector: ReflectionHost, + refEmitter: ReferenceEmitter, + imports: ImportManager, + ): ts.ClassDeclaration { const original = ts.getOriginalNode(clazz) as ClassDeclaration; if (!this.declarationFields.has(original)) { @@ -192,26 +216,33 @@ export class IvyDeclarationDtsTransform implements DtsTransform { } const fields = this.declarationFields.get(original)!; - const newMembers = fields.map(decl => { + const newMembers = fields.map((decl) => { const modifiers = [ts.factory.createModifier(ts.SyntaxKind.StaticKeyword)]; - const typeRef = - translateType(decl.type, original.getSourceFile(), reflector, refEmitter, imports); + const typeRef = translateType( + decl.type, + original.getSourceFile(), + reflector, + refEmitter, + imports, + ); markForEmitAsSingleLine(typeRef); return ts.factory.createPropertyDeclaration( - /* modifiers */ modifiers, - /* name */ decl.name, - /* questionOrExclamationToken */ undefined, - /* type */ typeRef, - /* initializer */ undefined); + /* modifiers */ modifiers, + /* name */ decl.name, + /* questionOrExclamationToken */ undefined, + /* type */ typeRef, + /* initializer */ undefined, + ); }); return ts.factory.updateClassDeclaration( - /* node */ clazz, - /* modifiers */ clazz.modifiers, - /* name */ clazz.name, - /* typeParameters */ clazz.typeParameters, - /* heritageClauses */ clazz.heritageClauses, - /* members */[...members, ...newMembers]); + /* node */ clazz, + /* modifiers */ clazz.modifiers, + /* name */ clazz.name, + /* typeParameters */ clazz.typeParameters, + /* heritageClauses */ clazz.heritageClauses, + /* members */ [...members, ...newMembers], + ); } } diff --git a/packages/compiler-cli/src/ngtsc/transform/src/trait.ts b/packages/compiler-cli/src/ngtsc/transform/src/trait.ts index 958dd0ccbeb24..d12fc0896fd35 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/trait.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/trait.ts @@ -47,23 +47,27 @@ export enum TraitState { * This not only simplifies the implementation, but ensures traits are monomorphic objects as * they're all just "views" in the type system of the same object (which never changes shape). */ -export type Trait = PendingTrait| - SkippedTrait|AnalyzedTrait|ResolvedTrait; +export type Trait = + | PendingTrait + | SkippedTrait + | AnalyzedTrait + | ResolvedTrait; /** * The value side of `Trait` exposes a helper to create a `Trait` in a pending state (by delegating * to `TraitImpl`). */ export const Trait = { - pending: ( - handler: DecoratorHandler, detected: DetectResult): PendingTrait => - TraitImpl.pending(handler, detected), + pending: ( + handler: DecoratorHandler, + detected: DetectResult, + ): PendingTrait => TraitImpl.pending(handler, detected), }; /** * The part of the `Trait` interface that's common to all trait states. */ -export interface TraitBase { +export interface TraitBase { /** * Current state of the trait. * @@ -90,16 +94,19 @@ export interface TraitBase { * * Pending traits have yet to be analyzed in any way. */ -export interface PendingTrait extends - TraitBase { +export interface PendingTrait + extends TraitBase { state: TraitState.Pending; /** * This pending trait has been successfully analyzed, and should transition to the "analyzed" * state. */ - toAnalyzed(analysis: A|null, diagnostics: ts.Diagnostic[]|null, symbol: S): - AnalyzedTrait; + toAnalyzed( + analysis: A | null, + diagnostics: ts.Diagnostic[] | null, + symbol: S, + ): AnalyzedTrait; /** * During analysis it was determined that this trait is not eligible for compilation after all, @@ -115,8 +122,8 @@ export interface PendingTrait extends * * This is a terminal state. */ -export interface SkippedTrait extends - TraitBase { +export interface SkippedTrait + extends TraitBase { state: TraitState.Skipped; } @@ -125,8 +132,8 @@ export interface SkippedTrait extends * * Analyzed traits have analysis results available, and are eligible for resolution. */ -export interface AnalyzedTrait extends - TraitBase { +export interface AnalyzedTrait + extends TraitBase { state: TraitState.Analyzed; symbol: S; @@ -134,18 +141,18 @@ export interface AnalyzedTrait extends * Analysis results of the given trait (if able to be produced), or `null` if analysis failed * completely. */ - analysis: Readonly|null; + analysis: Readonly | null; /** * Any diagnostics that resulted from analysis, or `null` if none. */ - analysisDiagnostics: ts.Diagnostic[]|null; + analysisDiagnostics: ts.Diagnostic[] | null; /** * This analyzed trait has been successfully resolved, and should be transitioned to the * "resolved" state. */ - toResolved(resolution: R|null, diagnostics: ts.Diagnostic[]|null): ResolvedTrait; + toResolved(resolution: R | null, diagnostics: ts.Diagnostic[] | null): ResolvedTrait; } /** @@ -156,8 +163,8 @@ export interface AnalyzedTrait extends * * This is a terminal state. */ -export interface ResolvedTrait extends - TraitBase { +export interface ResolvedTrait + extends TraitBase { state: TraitState.Resolved; symbol: S; @@ -169,42 +176,45 @@ export interface ResolvedTrait extends /** * Analysis may have still resulted in diagnostics. */ - analysisDiagnostics: ts.Diagnostic[]|null; + analysisDiagnostics: ts.Diagnostic[] | null; /** * Diagnostics resulting from resolution are tracked separately from */ - resolveDiagnostics: ts.Diagnostic[]|null; + resolveDiagnostics: ts.Diagnostic[] | null; /** * The results returned by a successful resolution of the given class/`DecoratorHandler` * combination. */ - resolution: Readonly|null; + resolution: Readonly | null; } /** * An implementation of the `Trait` type which transitions safely between the various * `TraitState`s. */ -class TraitImpl { +class TraitImpl { state: TraitState = TraitState.Pending; handler: DecoratorHandler; detected: DetectResult; - analysis: Readonly|null = null; - symbol: S|null = null; - resolution: Readonly|null = null; - analysisDiagnostics: ts.Diagnostic[]|null = null; - resolveDiagnostics: ts.Diagnostic[]|null = null; - typeCheckDiagnostics: ts.Diagnostic[]|null = null; + analysis: Readonly | null = null; + symbol: S | null = null; + resolution: Readonly | null = null; + analysisDiagnostics: ts.Diagnostic[] | null = null; + resolveDiagnostics: ts.Diagnostic[] | null = null; + typeCheckDiagnostics: ts.Diagnostic[] | null = null; constructor(handler: DecoratorHandler, detected: DetectResult) { this.handler = handler; this.detected = detected; } - toAnalyzed(analysis: A|null, diagnostics: ts.Diagnostic[]|null, symbol: S): - AnalyzedTrait { + toAnalyzed( + analysis: A | null, + diagnostics: ts.Diagnostic[] | null, + symbol: S, + ): AnalyzedTrait { // Only pending traits can be analyzed. this.assertTransitionLegal(TraitState.Pending, TraitState.Analyzed); this.analysis = analysis; @@ -214,7 +224,7 @@ class TraitImpl { return this as AnalyzedTrait; } - toResolved(resolution: R|null, diagnostics: ts.Diagnostic[]|null): ResolvedTrait { + toResolved(resolution: R | null, diagnostics: ts.Diagnostic[] | null): ResolvedTrait { // Only analyzed traits can be resolved. this.assertTransitionLegal(TraitState.Analyzed, TraitState.Resolved); if (this.analysis === null) { @@ -244,16 +254,21 @@ class TraitImpl { */ private assertTransitionLegal(allowedState: TraitState, transitionTo: TraitState): void { if (!(this.state === allowedState)) { - throw new Error(`Assertion failure: cannot transition from ${TraitState[this.state]} to ${ - TraitState[transitionTo]}.`); + throw new Error( + `Assertion failure: cannot transition from ${TraitState[this.state]} to ${ + TraitState[transitionTo] + }.`, + ); } } /** * Construct a new `TraitImpl` in the pending state. */ - static pending( - handler: DecoratorHandler, detected: DetectResult): PendingTrait { + static pending( + handler: DecoratorHandler, + detected: DetectResult, + ): PendingTrait { return new TraitImpl(handler, detected) as PendingTrait; } } diff --git a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts index 47921b4380cd2..42c068c6bdaff 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts @@ -9,11 +9,22 @@ import {ConstantPool} from '@angular/compiler'; import ts from 'typescript'; -import {DefaultImportTracker, ImportRewriter, LocalCompilationExtraImportsTracker} from '../../imports'; +import { + DefaultImportTracker, + ImportRewriter, + LocalCompilationExtraImportsTracker, +} from '../../imports'; import {getDefaultImportDeclaration} from '../../imports/src/default'; import {PerfPhase, PerfRecorder} from '../../perf'; import {Decorator, ReflectionHost} from '../../reflection'; -import {ImportManager, presetImportManagerForceNamespaceImports, RecordWrappedNodeFn, translateExpression, translateStatement, TranslatorOptions} from '../../translator'; +import { + ImportManager, + presetImportManagerForceNamespaceImports, + RecordWrappedNodeFn, + translateExpression, + translateStatement, + TranslatorOptions, +} from '../../translator'; import {visit, VisitListEntryResult, Visitor} from '../../util/src/visitor'; import {CompileResult} from './api'; @@ -33,19 +44,31 @@ interface FileOverviewMeta { } export function ivyTransformFactory( - compilation: TraitCompiler, reflector: ReflectionHost, importRewriter: ImportRewriter, - defaultImportTracker: DefaultImportTracker, - localCompilationExtraImportsTracker: LocalCompilationExtraImportsTracker|null, - perf: PerfRecorder, isCore: boolean, - isClosureCompilerEnabled: boolean): ts.TransformerFactory { + compilation: TraitCompiler, + reflector: ReflectionHost, + importRewriter: ImportRewriter, + defaultImportTracker: DefaultImportTracker, + localCompilationExtraImportsTracker: LocalCompilationExtraImportsTracker | null, + perf: PerfRecorder, + isCore: boolean, + isClosureCompilerEnabled: boolean, +): ts.TransformerFactory { const recordWrappedNode = createRecorderFn(defaultImportTracker); return (context: ts.TransformationContext): ts.Transformer => { return (file: ts.SourceFile): ts.SourceFile => { - return perf.inPhase( - PerfPhase.Compile, - () => transformIvySourceFile( - compilation, context, reflector, importRewriter, localCompilationExtraImportsTracker, - file, isCore, isClosureCompilerEnabled, recordWrappedNode)); + return perf.inPhase(PerfPhase.Compile, () => + transformIvySourceFile( + compilation, + context, + reflector, + importRewriter, + localCompilationExtraImportsTracker, + file, + isCore, + isClosureCompilerEnabled, + recordWrappedNode, + ), + ); }; }; } @@ -59,12 +82,16 @@ class IvyCompilationVisitor extends Visitor { public classCompilationMap = new Map(); public deferrableImports = new Set(); - constructor(private compilation: TraitCompiler, private constantPool: ConstantPool) { + constructor( + private compilation: TraitCompiler, + private constantPool: ConstantPool, + ) { super(); } - override visitClassDeclaration(node: ts.ClassDeclaration): - VisitListEntryResult { + override visitClassDeclaration( + node: ts.ClassDeclaration, + ): VisitListEntryResult { // Determine if this class has an Ivy field that needs to be added, and compile the field // to an expression if so. const result = this.compilation.compile(node, this.constantPool); @@ -76,8 +103,9 @@ class IvyCompilationVisitor extends Visitor { // corresponding regular import declarations. for (const classResult of result) { if (classResult.deferrableImports !== null && classResult.deferrableImports.size > 0) { - classResult.deferrableImports.forEach( - importDecl => this.deferrableImports.add(importDecl)); + classResult.deferrableImports.forEach((importDecl) => + this.deferrableImports.add(importDecl), + ); } } } @@ -91,17 +119,21 @@ class IvyCompilationVisitor extends Visitor { */ class IvyTransformationVisitor extends Visitor { constructor( - private compilation: TraitCompiler, - private classCompilationMap: Map, - private reflector: ReflectionHost, private importManager: ImportManager, - private recordWrappedNodeExpr: RecordWrappedNodeFn, - private isClosureCompilerEnabled: boolean, private isCore: boolean, - private deferrableImports: Set) { + private compilation: TraitCompiler, + private classCompilationMap: Map, + private reflector: ReflectionHost, + private importManager: ImportManager, + private recordWrappedNodeExpr: RecordWrappedNodeFn, + private isClosureCompilerEnabled: boolean, + private isCore: boolean, + private deferrableImports: Set, + ) { super(); } - override visitClassDeclaration(node: ts.ClassDeclaration): - VisitListEntryResult { + override visitClassDeclaration( + node: ts.ClassDeclaration, + ): VisitListEntryResult { // If this class is not registered in the map, it means that it doesn't have Angular decorators, // thus no further processing is required. if (!this.classCompilationMap.has(node)) { @@ -128,13 +160,21 @@ class IvyTransformationVisitor extends Visitor { } // Translate the initializer for the field into TS nodes. - const exprNode = - translateExpression(sourceFile, field.initializer, this.importManager, translateOptions); + const exprNode = translateExpression( + sourceFile, + field.initializer, + this.importManager, + translateOptions, + ); // Create a static property declaration for the new field. const property = ts.factory.createPropertyDeclaration( - [ts.factory.createToken(ts.SyntaxKind.StaticKeyword)], field.name, undefined, undefined, - exprNode); + [ts.factory.createToken(ts.SyntaxKind.StaticKeyword)], + field.name, + undefined, + undefined, + exprNode, + ); if (this.isClosureCompilerEnabled) { // Closure compiler transforms the form `Service.ɵprov = X` into `Service$ɵprov = X`. To @@ -142,23 +182,26 @@ class IvyTransformationVisitor extends Visitor { // Note that tsickle is typically responsible for adding such annotations, however it // doesn't yet handle synthetic fields added during other transformations. ts.addSyntheticLeadingComment( - property, ts.SyntaxKind.MultiLineCommentTrivia, '* @nocollapse ', - /* hasTrailingNewLine */ false); + property, + ts.SyntaxKind.MultiLineCommentTrivia, + '* @nocollapse ', + /* hasTrailingNewLine */ false, + ); } field.statements - .map(stmt => translateStatement(sourceFile, stmt, this.importManager, translateOptions)) - .forEach(stmt => statements.push(stmt)); + .map((stmt) => translateStatement(sourceFile, stmt, this.importManager, translateOptions)) + .forEach((stmt) => statements.push(stmt)); members.push(property); } const filteredDecorators = - // Remove the decorator which triggered this compilation, leaving the others alone. - maybeFilterDecorator(ts.getDecorators(node), this.compilation.decoratorsFor(node)); + // Remove the decorator which triggered this compilation, leaving the others alone. + maybeFilterDecorator(ts.getDecorators(node), this.compilation.decoratorsFor(node)); const nodeModifiers = ts.getModifiers(node); - let updatedModifiers: ts.ModifierLike[]|undefined; + let updatedModifiers: ts.ModifierLike[] | undefined; if (filteredDecorators?.length || nodeModifiers?.length) { updatedModifiers = [...(filteredDecorators || []), ...(nodeModifiers || [])]; @@ -166,9 +209,14 @@ class IvyTransformationVisitor extends Visitor { // Replace the class declaration with an updated version. node = ts.factory.updateClassDeclaration( - node, updatedModifiers, node.name, node.typeParameters, node.heritageClauses || [], - // Map over the class members and remove any Angular decorators from them. - members.map(member => this._stripAngularDecorators(member))); + node, + updatedModifiers, + node.name, + node.typeParameters, + node.heritageClauses || [], + // Map over the class members and remove any Angular decorators from them. + members.map((member) => this._stripAngularDecorators(member)), + ); return {node, after: statements}; } @@ -191,8 +239,9 @@ class IvyTransformationVisitor extends Visitor { if (decorators === null) { return NO_DECORATORS; } - const coreDecorators = decorators.filter(dec => this.isCore || isFromAngularCore(dec)) - .map(dec => dec.node as ts.Decorator); + const coreDecorators = decorators + .filter((dec) => this.isCore || isFromAngularCore(dec)) + .map((dec) => dec.node as ts.Decorator); if (coreDecorators.length > 0) { return new Set(coreDecorators); } else { @@ -200,7 +249,7 @@ class IvyTransformationVisitor extends Visitor { } } - private _nonCoreDecoratorsOnly(node: ts.HasDecorators): ts.NodeArray|undefined { + private _nonCoreDecoratorsOnly(node: ts.HasDecorators): ts.NodeArray | undefined { const decorators = ts.getDecorators(node); // Shortcut if the node has no decorators. @@ -219,7 +268,7 @@ class IvyTransformationVisitor extends Visitor { } // Filter out the core decorators. - const filtered = decorators.filter(dec => !coreDecorators.has(dec)); + const filtered = decorators.filter((dec) => !coreDecorators.has(dec)); // If no decorators survive, return `undefined`. This can only happen if a core decorator is // repeated on the node. @@ -239,43 +288,69 @@ class IvyTransformationVisitor extends Visitor { */ private _stripAngularDecorators(node: T): T { const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined; - const nonCoreDecorators = - ts.canHaveDecorators(node) ? this._nonCoreDecoratorsOnly(node) : undefined; + const nonCoreDecorators = ts.canHaveDecorators(node) + ? this._nonCoreDecoratorsOnly(node) + : undefined; const combinedModifiers = [...(nonCoreDecorators || []), ...(modifiers || [])]; if (ts.isParameter(node)) { // Strip decorators from parameters (probably of the constructor). node = ts.factory.updateParameterDeclaration( - node, combinedModifiers, node.dotDotDotToken, node.name, node.questionToken, - node.type, node.initializer) as T & - ts.ParameterDeclaration; + node, + combinedModifiers, + node.dotDotDotToken, + node.name, + node.questionToken, + node.type, + node.initializer, + ) as T & ts.ParameterDeclaration; } else if (ts.isMethodDeclaration(node)) { // Strip decorators of methods. node = ts.factory.updateMethodDeclaration( - node, combinedModifiers, node.asteriskToken, node.name, node.questionToken, - node.typeParameters, node.parameters, node.type, node.body) as T & - ts.MethodDeclaration; + node, + combinedModifiers, + node.asteriskToken, + node.name, + node.questionToken, + node.typeParameters, + node.parameters, + node.type, + node.body, + ) as T & ts.MethodDeclaration; } else if (ts.isPropertyDeclaration(node)) { // Strip decorators of properties. node = ts.factory.updatePropertyDeclaration( - node, combinedModifiers, node.name, node.questionToken, node.type, - node.initializer) as T & - ts.PropertyDeclaration; + node, + combinedModifiers, + node.name, + node.questionToken, + node.type, + node.initializer, + ) as T & ts.PropertyDeclaration; } else if (ts.isGetAccessor(node)) { // Strip decorators of getters. node = ts.factory.updateGetAccessorDeclaration( - node, combinedModifiers, node.name, node.parameters, node.type, node.body) as T & - ts.GetAccessorDeclaration; + node, + combinedModifiers, + node.name, + node.parameters, + node.type, + node.body, + ) as T & ts.GetAccessorDeclaration; } else if (ts.isSetAccessor(node)) { // Strip decorators of setters. node = ts.factory.updateSetAccessorDeclaration( - node, combinedModifiers, node.name, node.parameters, node.body) as T & - ts.SetAccessorDeclaration; + node, + combinedModifiers, + node.name, + node.parameters, + node.body, + ) as T & ts.SetAccessorDeclaration; } else if (ts.isConstructorDeclaration(node)) { // For constructors, strip decorators of the parameters. - const parameters = node.parameters.map(param => this._stripAngularDecorators(param)); + const parameters = node.parameters.map((param) => this._stripAngularDecorators(param)); node = ts.factory.updateConstructorDeclaration(node, modifiers, parameters, node.body) as T & - ts.ConstructorDeclaration; + ts.ConstructorDeclaration; } return node; } @@ -285,14 +360,21 @@ class IvyTransformationVisitor extends Visitor { * A transformer which operates on ts.SourceFiles and applies changes from an `IvyCompilation`. */ function transformIvySourceFile( - compilation: TraitCompiler, context: ts.TransformationContext, reflector: ReflectionHost, - importRewriter: ImportRewriter, - localCompilationExtraImportsTracker: LocalCompilationExtraImportsTracker|null, - file: ts.SourceFile, isCore: boolean, isClosureCompilerEnabled: boolean, - recordWrappedNode: RecordWrappedNodeFn): ts.SourceFile { + compilation: TraitCompiler, + context: ts.TransformationContext, + reflector: ReflectionHost, + importRewriter: ImportRewriter, + localCompilationExtraImportsTracker: LocalCompilationExtraImportsTracker | null, + file: ts.SourceFile, + isCore: boolean, + isClosureCompilerEnabled: boolean, + recordWrappedNode: RecordWrappedNodeFn, +): ts.SourceFile { const constantPool = new ConstantPool(isClosureCompilerEnabled); - const importManager = - new ImportManager({...presetImportManagerForceNamespaceImports, rewriter: importRewriter}); + const importManager = new ImportManager({ + ...presetImportManagerForceNamespaceImports, + rewriter: importRewriter, + }); // The transformation process consists of 2 steps: // @@ -311,20 +393,28 @@ function transformIvySourceFile( // Step 2. Scan through the AST again and perform transformations based on Ivy compilation // results obtained at Step 1. const transformationVisitor = new IvyTransformationVisitor( - compilation, compilationVisitor.classCompilationMap, reflector, importManager, - recordWrappedNode, isClosureCompilerEnabled, isCore, compilationVisitor.deferrableImports); + compilation, + compilationVisitor.classCompilationMap, + reflector, + importManager, + recordWrappedNode, + isClosureCompilerEnabled, + isCore, + compilationVisitor.deferrableImports, + ); let sf = visit(file, transformationVisitor, context); // Generate the constant statements first, as they may involve adding additional imports // to the ImportManager. const downlevelTranslatedCode = getLocalizeCompileTarget(context) < ts.ScriptTarget.ES2015; - const constants = - constantPool.statements.map(stmt => translateStatement(file, stmt, importManager, { - recordWrappedNode, - downlevelTaggedTemplates: downlevelTranslatedCode, - downlevelVariableDeclarations: downlevelTranslatedCode, - annotateForClosureCompiler: isClosureCompilerEnabled, - })); + const constants = constantPool.statements.map((stmt) => + translateStatement(file, stmt, importManager, { + recordWrappedNode, + downlevelTaggedTemplates: downlevelTranslatedCode, + downlevelVariableDeclarations: downlevelTranslatedCode, + annotateForClosureCompiler: isClosureCompilerEnabled, + }), + ); // Preserve @fileoverview comments required by Closure, since the location might change as a // result of adding extra imports and constant pool statements. @@ -357,13 +447,14 @@ function transformIvySourceFile( * be so that we can generate ES5 compliant `$localize` calls instead of relying upon TS to do the * downleveling for us. */ -function getLocalizeCompileTarget(context: ts.TransformationContext): - Exclude { +function getLocalizeCompileTarget( + context: ts.TransformationContext, +): Exclude { const target = context.getCompilerOptions().target || ts.ScriptTarget.ES2015; return target !== ts.ScriptTarget.JSON ? target : ts.ScriptTarget.ES2015; } -function getFileOverviewComment(statements: ts.NodeArray): FileOverviewMeta|null { +function getFileOverviewComment(statements: ts.NodeArray): FileOverviewMeta | null { if (statements.length > 0) { const host = statements[0]; let trailing = false; @@ -383,7 +474,9 @@ function getFileOverviewComment(statements: ts.NodeArray): FileOve } function insertFileOverviewComment( - sf: ts.SourceFile, fileoverview: FileOverviewMeta): ts.SourceFile { + sf: ts.SourceFile, + fileoverview: FileOverviewMeta, +): ts.SourceFile { const {comments, host, trailing} = fileoverview; // If host statement is no longer the first one, it means that extra statements were added at the // very beginning, so we need to relocate @fileoverview comment and cleanup the original statement @@ -401,20 +494,28 @@ function insertFileOverviewComment( ts.setSyntheticLeadingComments(commentNode, comments); return ts.factory.updateSourceFile( - sf, [commentNode, ...sf.statements], sf.isDeclarationFile, sf.referencedFiles, - sf.typeReferenceDirectives, sf.hasNoDefaultLib, sf.libReferenceDirectives); + sf, + [commentNode, ...sf.statements], + sf.isDeclarationFile, + sf.referencedFiles, + sf.typeReferenceDirectives, + sf.hasNoDefaultLib, + sf.libReferenceDirectives, + ); } return sf; } function maybeFilterDecorator( - decorators: readonly ts.Decorator[]|undefined, - toRemove: ts.Decorator[]): ts.NodeArray|undefined { + decorators: readonly ts.Decorator[] | undefined, + toRemove: ts.Decorator[], +): ts.NodeArray | undefined { if (decorators === undefined) { return undefined; } const filtered = decorators.filter( - dec => toRemove.find(decToRemove => ts.getOriginalNode(dec) === decToRemove) === undefined); + (dec) => toRemove.find((decToRemove) => ts.getOriginalNode(dec) === decToRemove) === undefined, + ); if (filtered.length === 0) { return undefined; } @@ -425,9 +526,10 @@ function isFromAngularCore(decorator: Decorator): boolean { return decorator.import !== null && decorator.import.from === '@angular/core'; } -function createRecorderFn(defaultImportTracker: DefaultImportTracker): - RecordWrappedNodeFn { - return node => { +function createRecorderFn( + defaultImportTracker: DefaultImportTracker, +): RecordWrappedNodeFn { + return (node) => { const importDecl = getDefaultImportDeclaration(node); if (importDecl !== null) { defaultImportTracker.recordUsedImport(importDecl); @@ -436,8 +538,9 @@ function createRecorderFn(defaultImportTracker: DefaultImportTracker): } /** Creates a `NodeArray` with the correct offsets from an array of decorators. */ -function nodeArrayFromDecoratorsArray(decorators: readonly ts.Decorator[]): - ts.NodeArray { +function nodeArrayFromDecoratorsArray( + decorators: readonly ts.Decorator[], +): ts.NodeArray { const array = ts.factory.createNodeArray(decorators); if (array.length > 0) { diff --git a/packages/compiler-cli/src/ngtsc/transform/test/compilation_spec.ts b/packages/compiler-cli/src/ngtsc/transform/test/compilation_spec.ts index 8e0f47a6dacdd..042e2c59f0443 100644 --- a/packages/compiler-cli/src/ngtsc/transform/test/compilation_spec.ts +++ b/packages/compiler-cli/src/ngtsc/transform/test/compilation_spec.ts @@ -12,41 +12,65 @@ import {absoluteFrom} from '../../file_system'; import {runInEachFileSystem} from '../../file_system/testing'; import {NOOP_INCREMENTAL_BUILD} from '../../incremental'; import {NOOP_PERF_RECORDER} from '../../perf'; -import {ClassDeclaration, Decorator, isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection'; +import { + ClassDeclaration, + Decorator, + isNamedClassDeclaration, + TypeScriptReflectionHost, +} from '../../reflection'; import {getDeclaration, makeProgram} from '../../testing'; import {CompilationMode, DetectResult, DtsTransformRegistry, TraitCompiler} from '../../transform'; -import {AnalysisOutput, CompileResult, DecoratorHandler, HandlerPrecedence, ResolveResult} from '../src/api'; +import { + AnalysisOutput, + CompileResult, + DecoratorHandler, + HandlerPrecedence, + ResolveResult, +} from '../src/api'; const fakeSfTypeIdentifier = { isShim: () => false, - isResource: () => false + isResource: () => false, }; runInEachFileSystem(() => { describe('TraitCompiler', () => { let _: typeof absoluteFrom; - beforeEach(() => _ = absoluteFrom); + beforeEach(() => (_ = absoluteFrom)); function setup( - programContents: string, handlers: DecoratorHandler<{}|null, unknown, null, unknown>[], - compilationMode: CompilationMode, makeDtsSourceFile = false) { + programContents: string, + handlers: DecoratorHandler<{} | null, unknown, null, unknown>[], + compilationMode: CompilationMode, + makeDtsSourceFile = false, + ) { const filename = makeDtsSourceFile ? 'test.d.ts' : 'test.ts'; - const {program} = makeProgram([{ - name: _('/' + filename), - contents: programContents, - }]); + const {program} = makeProgram([ + { + name: _('/' + filename), + contents: programContents, + }, + ]); const checker = program.getTypeChecker(); const reflectionHost = new TypeScriptReflectionHost(checker); const compiler = new TraitCompiler( - handlers, reflectionHost, NOOP_PERF_RECORDER, NOOP_INCREMENTAL_BUILD, true, - compilationMode, new DtsTransformRegistry(), null, fakeSfTypeIdentifier); + handlers, + reflectionHost, + NOOP_PERF_RECORDER, + NOOP_INCREMENTAL_BUILD, + true, + compilationMode, + new DtsTransformRegistry(), + null, + fakeSfTypeIdentifier, + ); const sourceFile = program.getSourceFile(filename)!; return {compiler, sourceFile, program, filename: _('/' + filename)}; } it('should not run decoration handlers against declaration files', () => { - class FakeDecoratorHandler implements DecoratorHandler<{}|null, unknown, null, unknown> { + class FakeDecoratorHandler implements DecoratorHandler<{} | null, unknown, null, unknown> { name = 'FakeDecoratorHandler'; precedence = HandlerPrecedence.PRIMARY; @@ -67,8 +91,12 @@ runInEachFileSystem(() => { } } const contents = `export declare class SomeDirective {}`; - const {compiler, sourceFile} = - setup(contents, [new FakeDecoratorHandler()], CompilationMode.FULL, true); + const {compiler, sourceFile} = setup( + contents, + [new FakeDecoratorHandler()], + CompilationMode.FULL, + true, + ); const analysis = compiler.analyzeSync(sourceFile); @@ -81,7 +109,10 @@ runInEachFileSystem(() => { name = 'LocalDecoratorHandler'; precedence = HandlerPrecedence.PRIMARY; - detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<{}>|undefined { + detect( + node: ClassDeclaration, + decorators: Decorator[] | null, + ): DetectResult<{}> | undefined { if (node.name.text !== 'Local') { return undefined; } @@ -121,7 +152,10 @@ runInEachFileSystem(() => { name = 'PartialDecoratorHandler'; precedence = HandlerPrecedence.PRIMARY; - detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<{}>|undefined { + detect( + node: ClassDeclaration, + decorators: Decorator[] | null, + ): DetectResult<{}> | undefined { if (node.name.text !== 'Partial') { return undefined; } @@ -171,7 +205,10 @@ runInEachFileSystem(() => { name = 'FullDecoratorHandler'; precedence = HandlerPrecedence.PRIMARY; - detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<{}>|undefined { + detect( + node: ClassDeclaration, + decorators: Decorator[] | null, + ): DetectResult<{}> | undefined { if (node.name.text !== 'Full') { return undefined; } @@ -213,8 +250,10 @@ runInEachFileSystem(() => { export class Partial {} `; const {compiler, sourceFile, program, filename} = setup( - contents, [new PartialDecoratorHandler(), new FullDecoratorHandler()], - CompilationMode.PARTIAL); + contents, + [new PartialDecoratorHandler(), new FullDecoratorHandler()], + CompilationMode.PARTIAL, + ); compiler.analyzeSync(sourceFile); compiler.resolve(); @@ -236,8 +275,10 @@ runInEachFileSystem(() => { export class Local {} `; const {compiler, sourceFile, program, filename} = setup( - contents, [new LocalDecoratorHandler(), new FullDecoratorHandler()], - CompilationMode.LOCAL); + contents, + [new LocalDecoratorHandler(), new FullDecoratorHandler()], + CompilationMode.LOCAL, + ); compiler.analyzeSync(sourceFile); compiler.resolve(); @@ -260,11 +301,10 @@ runInEachFileSystem(() => { export class Local {} `; const {compiler, sourceFile, program, filename} = setup( - contents, - [ - new LocalDecoratorHandler(), new PartialDecoratorHandler(), new FullDecoratorHandler() - ], - CompilationMode.FULL); + contents, + [new LocalDecoratorHandler(), new PartialDecoratorHandler(), new FullDecoratorHandler()], + CompilationMode.FULL, + ); compiler.analyzeSync(sourceFile); compiler.resolve(); @@ -291,7 +331,10 @@ runInEachFileSystem(() => { name = 'TestDecoratorHandler'; precedence = HandlerPrecedence.PRIMARY; - detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<{}>|undefined { + detect( + node: ClassDeclaration, + decorators: Decorator[] | null, + ): DetectResult<{}> | undefined { if (node.name.text !== 'Test') { return undefined; } @@ -369,8 +412,11 @@ runInEachFileSystem(() => { `; const handler = new TestDecoratorHandler(); spyOn(handler, 'updateResources'); - const {compiler, sourceFile, program, filename} = - setup(contents, [handler], CompilationMode.LOCAL); + const {compiler, sourceFile, program, filename} = setup( + contents, + [handler], + CompilationMode.LOCAL, + ); const decl = getDeclaration(program, filename, 'Test', isNamedClassDeclaration); compiler.analyzeSync(sourceFile); diff --git a/packages/compiler-cli/src/ngtsc/translator/index.ts b/packages/compiler-cli/src/ngtsc/translator/index.ts index 45edeedbb7b50..abd33e48c50b3 100644 --- a/packages/compiler-cli/src/ngtsc/translator/index.ts +++ b/packages/compiler-cli/src/ngtsc/translator/index.ts @@ -6,12 +6,36 @@ * found in the LICENSE file at https://angular.io/license */ -export {AstFactory, BinaryOperator, LeadingComment, ObjectLiteralProperty, SourceMapLocation, SourceMapRange, TemplateElement, TemplateLiteral, UnaryOperator, VariableDeclarationType} from './src/api/ast_factory'; +export { + AstFactory, + BinaryOperator, + LeadingComment, + ObjectLiteralProperty, + SourceMapLocation, + SourceMapRange, + TemplateElement, + TemplateLiteral, + UnaryOperator, + VariableDeclarationType, +} from './src/api/ast_factory'; export {ImportGenerator, ImportRequest} from './src/api/import_generator'; export {Context} from './src/context'; -export {ImportManager, ImportManagerConfig, presetImportManagerForceNamespaceImports} from './src/import_manager/import_manager'; -export {ExpressionTranslatorVisitor, RecordWrappedNodeFn, TranslatorOptions} from './src/translator'; +export { + ImportManager, + ImportManagerConfig, + presetImportManagerForceNamespaceImports, +} from './src/import_manager/import_manager'; +export { + ExpressionTranslatorVisitor, + RecordWrappedNodeFn, + TranslatorOptions, +} from './src/translator'; export {canEmitType, TypeEmitter, TypeReferenceTranslator} from './src/type_emitter'; export {translateType} from './src/type_translator'; -export {attachComments, createTemplateMiddle, createTemplateTail, TypeScriptAstFactory} from './src/typescript_ast_factory'; +export { + attachComments, + createTemplateMiddle, + createTemplateTail, + TypeScriptAstFactory, +} from './src/typescript_ast_factory'; export {translateExpression, translateStatement} from './src/typescript_translator'; 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 5875edac95aeb..6683cb5ffde3f 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 @@ -45,7 +45,10 @@ export interface AstFactory { * @param rightOperand an expression that will appear on the right of the operator. */ createBinaryExpression( - leftOperand: TExpression, operator: BinaryOperator, rightOperand: TExpression): TExpression; + leftOperand: TExpression, + operator: BinaryOperator, + rightOperand: TExpression, + ): TExpression; /** * Create a block of statements (e.g. `{ stmt1; stmt2; }`). @@ -71,8 +74,10 @@ export interface AstFactory { * @param elseExpression an expression that is executed if `condition` is falsy. */ createConditional( - condition: TExpression, thenExpression: TExpression, - elseExpression: TExpression): TExpression; + condition: TExpression, + thenExpression: TExpression, + elseExpression: TExpression, + ): TExpression; /** * Create an element access (e.g. `obj[expr]`). @@ -96,8 +101,11 @@ export interface AstFactory { * @param parameters the names of the function's parameters. * @param body a statement (or a block of statements) that are the body of the function. */ - createFunctionDeclaration(functionName: string, parameters: string[], body: TStatement): - TStatement; + createFunctionDeclaration( + functionName: string, + parameters: string[], + body: TStatement, + ): TStatement; /** * Create an expression that represents a function @@ -107,8 +115,11 @@ export interface AstFactory { * @param parameters the names of the function's parameters. * @param body a statement (or a block of statements) that are the body of the function. */ - createFunctionExpression(functionName: string|null, parameters: string[], body: TStatement): - TExpression; + createFunctionExpression( + functionName: string | null, + parameters: string[], + body: TStatement, + ): TExpression; /** * Create an expression that represents an arrow function @@ -117,7 +128,7 @@ 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: string[], body: TExpression | TStatement): TExpression; /** * Creates an expression that represents a dynamic import @@ -144,15 +155,17 @@ export interface AstFactory { * falsy. */ createIfStatement( - condition: TExpression, thenStatement: TStatement, - elseStatement: TStatement|null): TStatement; + condition: TExpression, + thenStatement: TStatement, + elseStatement: TStatement | null, + ): TStatement; /** * Create a simple literal (e.g. `"string"`, `123`, `false`, etc). * * @param value the value of the literal. */ - createLiteral(value: string|number|boolean|null|undefined): TExpression; + createLiteral(value: string | number | boolean | null | undefined): TExpression; /** * Create an expression that is instantiating the `expression` as a class. @@ -189,7 +202,7 @@ export interface AstFactory { * * @param expression the expression to be returned. */ - createReturnStatement(expression: TExpression|null): TStatement; + createReturnStatement(expression: TExpression | null): TStatement; /** * Create a tagged template literal string. E.g. @@ -234,8 +247,10 @@ export interface AstFactory { * @param type whether this variable should be declared as `var`, `let` or `const`. */ createVariableDeclaration( - variableName: string, initializer: TExpression|null, - type: VariableDeclarationType): TStatement; + variableName: string, + initializer: TExpression | null, + type: VariableDeclarationType, + ): TStatement; /** * Attach a source map range to the given node. @@ -244,25 +259,44 @@ export interface AstFactory { * @param sourceMapRange the range to attach to the node, or null if there is no range to attach. * @returns the `node` with the `sourceMapRange` attached. */ - setSourceMapRange(node: T, sourceMapRange: SourceMapRange|null): - T; + setSourceMapRange( + node: T, + sourceMapRange: SourceMapRange | null, + ): T; } /** * The type of a variable declaration. */ -export type VariableDeclarationType = 'const'|'let'|'var'; +export type VariableDeclarationType = 'const' | 'let' | 'var'; /** * The unary operators supported by the `AstFactory`. */ -export type UnaryOperator = '+'|'-'|'!'; +export type UnaryOperator = '+' | '-' | '!'; /** * The binary operators supported by the `AstFactory`. */ export type BinaryOperator = - '&&'|'>'|'>='|'&'|'|'|'/'|'=='|'==='|'<'|'<='|'-'|'%'|'*'|'!='|'!=='|'||'|'+'|'??'; + | '&&' + | '>' + | '>=' + | '&' + | '|' + | '/' + | '==' + | '===' + | '<' + | '<=' + | '-' + | '%' + | '*' + | '!=' + | '!==' + | '||' + | '+' + | '??'; /** * The original location of the start or end of a node created by the `AstFactory`. @@ -322,7 +356,7 @@ export interface TemplateElement { /** The parsed string, with escape codes etc processed. */ cooked: string; /** The original location of this piece of the template literal string. */ - range: SourceMapRange|null; + range: SourceMapRange | null; } /** diff --git a/packages/compiler-cli/src/ngtsc/translator/src/api/import_generator.ts b/packages/compiler-cli/src/ngtsc/translator/src/api/import_generator.ts index 4714beb192e1f..d61b776bb752e 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/api/import_generator.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/api/import_generator.ts @@ -14,7 +14,7 @@ export interface ImportRequest { * Name of the export to be imported. * May be `null` if a namespace import is requested. */ - exportSymbolName: string|null; + exportSymbolName: string | null; /** * Module specifier to be imported. diff --git a/packages/compiler-cli/src/ngtsc/translator/src/import_manager/check_unique_identifier_name.ts b/packages/compiler-cli/src/ngtsc/translator/src/import_manager/check_unique_identifier_name.ts index 66aa37c14439d..d151990aa2e05 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/import_manager/check_unique_identifier_name.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/import_manager/check_unique_identifier_name.ts @@ -20,8 +20,7 @@ interface SourceFileWithIdentifiers extends ts.SourceFile { * Generates a helper for `ImportManagerConfig` to generate unique identifiers * for a given source file. */ -export function createGenerateUniqueIdentifierHelper(): - ImportManagerConfig['generateUniqueIdentifier'] { +export function createGenerateUniqueIdentifierHelper(): ImportManagerConfig['generateUniqueIdentifier'] { const generatedIdentifiers = new Set(); return (sourceFile: ts.SourceFile, symbolName: string) => { @@ -31,7 +30,7 @@ export function createGenerateUniqueIdentifierHelper(): } const isUniqueIdentifier = (name: string) => - !sf.identifiers!.has(name) && !generatedIdentifiers.has(name); + !sf.identifiers!.has(name) && !generatedIdentifiers.has(name); if (isUniqueIdentifier(symbolName)) { generatedIdentifiers.add(symbolName); diff --git a/packages/compiler-cli/src/ngtsc/translator/src/import_manager/import_manager.ts b/packages/compiler-cli/src/ngtsc/translator/src/import_manager/import_manager.ts index dfe25e1527146..e0d0858481e31 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/import_manager/import_manager.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/import_manager/import_manager.ts @@ -13,14 +13,21 @@ import {ImportGenerator, ImportRequest} from '../api/import_generator'; import {createGenerateUniqueIdentifierHelper} from './check_unique_identifier_name'; import {createTsTransformForImportManager} from './import_typescript_transform'; -import {attemptToReuseGeneratedImports, captureGeneratedImport, ReuseGeneratedImportsTracker} from './reuse_generated_imports'; -import {attemptToReuseExistingSourceFileImports, ReuseExistingSourceFileImportsTracker} from './reuse_source_file_imports'; +import { + attemptToReuseGeneratedImports, + captureGeneratedImport, + ReuseGeneratedImportsTracker, +} from './reuse_generated_imports'; +import { + attemptToReuseExistingSourceFileImports, + ReuseExistingSourceFileImportsTracker, +} from './reuse_source_file_imports'; /** Configuration for the import manager. */ export interface ImportManagerConfig { - generateUniqueIdentifier(file: ts.SourceFile, baseName: string): ts.Identifier|null; + generateUniqueIdentifier(file: ts.SourceFile, baseName: string): ts.Identifier | null; shouldUseSingleQuotes(file: ts.SourceFile): boolean; - rewriter: ImportRewriter|null; + rewriter: ImportRewriter | null; namespaceImportPrefix: string; disableOriginalSourceFileReuse: boolean; forceGenerateNamespacesForNewImports: boolean; @@ -41,7 +48,7 @@ export const presetImportManagerForceNamespaceImports: Partial { +export class ImportManager + implements ImportGenerator +{ /** List of new imports that will be inserted into given source files. */ - private newImports: Map, - namedImports: Map, - sideEffectImports: Set, - }> = new Map(); + private newImports: Map< + ts.SourceFile, + { + namespaceImports: Map; + namedImports: Map; + sideEffectImports: Set; + } + > = new Map(); private nextUniqueIndex = 0; private config: ImportManagerConfig; @@ -81,7 +92,7 @@ export class ImportManager implements forceGenerateNamespacesForNewImports: false, namespaceImportPrefix: 'i', generateUniqueIdentifier: - this._config.generateUniqueIdentifier ?? createGenerateUniqueIdentifierHelper(), + this._config.generateUniqueIdentifier ?? createGenerateUniqueIdentifierHelper(), ...this._config, }; this.reuseSourceFileImportsTracker = { @@ -94,37 +105,49 @@ export class ImportManager implements /** Adds a side-effect import for the given module. */ addSideEffectImport(requestedFile: ts.SourceFile, moduleSpecifier: string) { if (this.config.rewriter !== null) { - moduleSpecifier = - this.config.rewriter.rewriteSpecifier(moduleSpecifier, requestedFile.fileName); + moduleSpecifier = this.config.rewriter.rewriteSpecifier( + moduleSpecifier, + requestedFile.fileName, + ); } - this._getNewImportsTrackerForFile(requestedFile) - .sideEffectImports.add(moduleSpecifier as ModuleName); + this._getNewImportsTrackerForFile(requestedFile).sideEffectImports.add( + moduleSpecifier as ModuleName, + ); } /** * Adds an import to the given source-file and returns a TypeScript * expression that can be used to access the newly imported symbol. */ - addImport(request: ImportRequest&{asTypeReference: true}): ts.Identifier - |ts.QualifiedName; - addImport(request: ImportRequest&{asTypeReference?: undefined}): ts.Identifier - |ts.PropertyAccessExpression; - addImport(request: ImportRequest&{asTypeReference?: boolean}): ts.Identifier - |ts.PropertyAccessExpression|ts.QualifiedName { + addImport( + request: ImportRequest & {asTypeReference: true}, + ): ts.Identifier | ts.QualifiedName; + addImport( + request: ImportRequest & {asTypeReference?: undefined}, + ): ts.Identifier | ts.PropertyAccessExpression; + addImport( + request: ImportRequest & {asTypeReference?: boolean}, + ): ts.Identifier | ts.PropertyAccessExpression | ts.QualifiedName { if (this.config.rewriter !== null) { if (request.exportSymbolName !== null) { request.exportSymbolName = this.config.rewriter.rewriteSymbol( - request.exportSymbolName, request.exportModuleSpecifier); + request.exportSymbolName, + request.exportModuleSpecifier, + ); } request.exportModuleSpecifier = this.config.rewriter.rewriteSpecifier( - request.exportModuleSpecifier, request.requestedFile.fileName); + request.exportModuleSpecifier, + request.requestedFile.fileName, + ); } // Attempt to re-use previous identical import requests. - const previousGeneratedImportRef = - attemptToReuseGeneratedImports(this.reuseGeneratedImportsTracker, request); + const previousGeneratedImportRef = attemptToReuseGeneratedImports( + this.reuseGeneratedImportsTracker, + request, + ); if (previousGeneratedImportRef !== null) { return createImportReference(!!request.asTypeReference, previousGeneratedImportRef); } @@ -135,8 +158,9 @@ export class ImportManager implements return createImportReference(!!request.asTypeReference, resultImportRef); } - private _generateNewImport(request: ImportRequest): ts.Identifier - |[ts.Identifier, ts.Identifier] { + private _generateNewImport( + request: ImportRequest, + ): ts.Identifier | [ts.Identifier, ts.Identifier] { const {requestedFile: sourceFile} = request; const disableOriginalSourceFileReuse = this.config.disableOriginalSourceFileReuse; const forceGenerateNamespacesForNewImports = this.config.forceGenerateNamespacesForNewImports; @@ -145,7 +169,10 @@ export class ImportManager implements // This may involve updates to existing import named bindings. if (!disableOriginalSourceFileReuse) { const reuseResult = attemptToReuseExistingSourceFileImports( - this.reuseSourceFileImportsTracker, sourceFile, request); + this.reuseSourceFileImportsTracker, + sourceFile, + request, + ); if (reuseResult !== null) { return reuseResult; } @@ -160,15 +187,18 @@ export class ImportManager implements if (request.exportSymbolName === null || forceGenerateNamespacesForNewImports) { const namespaceImportName = `${this.config.namespaceImportPrefix}${this.nextUniqueIndex++}`; const namespaceImport = ts.factory.createNamespaceImport( - this.config.generateUniqueIdentifier(sourceFile, namespaceImportName) ?? - ts.factory.createIdentifier(namespaceImportName)); + this.config.generateUniqueIdentifier(sourceFile, namespaceImportName) ?? + ts.factory.createIdentifier(namespaceImportName), + ); namespaceImports.set(request.exportModuleSpecifier as ModuleName, namespaceImport); // Capture the generated namespace import alone, to allow re-use. captureGeneratedImport( - {...request, exportSymbolName: null}, this.reuseGeneratedImportsTracker, - namespaceImport.name); + {...request, exportSymbolName: null}, + this.reuseGeneratedImportsTracker, + namespaceImport.name, + ); if (request.exportSymbolName !== null) { return [namespaceImport.name, ts.factory.createIdentifier(request.exportSymbolName)]; @@ -182,14 +212,22 @@ export class ImportManager implements } const exportSymbolName = ts.factory.createIdentifier(request.exportSymbolName); - const fileUniqueName = - this.config.generateUniqueIdentifier(sourceFile, request.exportSymbolName); + const fileUniqueName = this.config.generateUniqueIdentifier( + sourceFile, + request.exportSymbolName, + ); const needsAlias = fileUniqueName !== null; const specifierName = needsAlias ? fileUniqueName : exportSymbolName; - namedImports.get(request.exportModuleSpecifier as ModuleName)!.push( + namedImports + .get(request.exportModuleSpecifier as ModuleName)! + .push( ts.factory.createImportSpecifier( - false, needsAlias ? exportSymbolName : undefined, specifierName)); + false, + needsAlias ? exportSymbolName : undefined, + specifierName, + ), + ); return specifierName; } @@ -203,10 +241,10 @@ export class ImportManager implements * to be updated, and allows more trivial re-use of previous generated imports. */ finalize(): { - affectedFiles: Set, - updatedImports: Map, - newImports: Map, - reusedOriginalAliasDeclarations: Set, + affectedFiles: Set; + updatedImports: Map; + newImports: Map; + reusedOriginalAliasDeclarations: Set; } { const affectedFiles = new Set(); const updatedImportsResult = new Map(); @@ -225,11 +263,17 @@ export class ImportManager implements this.reuseSourceFileImportsTracker.updatedImports.forEach((expressions, importDecl) => { const namedBindings = importDecl.importClause!.namedBindings as ts.NamedImports; const newNamedBindings = ts.factory.updateNamedImports( - namedBindings, - namedBindings.elements.concat(expressions.map( - ({propertyName, fileUniqueAlias}) => ts.factory.createImportSpecifier( - false, fileUniqueAlias !== null ? propertyName : undefined, - fileUniqueAlias ?? propertyName)))); + namedBindings, + namedBindings.elements.concat( + expressions.map(({propertyName, fileUniqueAlias}) => + ts.factory.createImportSpecifier( + false, + fileUniqueAlias !== null ? propertyName : undefined, + fileUniqueAlias ?? propertyName, + ), + ), + ), + ); affectedFiles.add(importDecl.getSourceFile().fileName); updatedImportsResult.set(namedBindings, newNamedBindings); @@ -240,17 +284,23 @@ export class ImportManager implements const useSingleQuotes = this.config.shouldUseSingleQuotes(sourceFile); const fileName = sourceFile.fileName; - sideEffectImports.forEach(moduleName => { + sideEffectImports.forEach((moduleName) => { addNewImport( - fileName, - ts.factory.createImportDeclaration( - undefined, undefined, ts.factory.createStringLiteral(moduleName))); + fileName, + ts.factory.createImportDeclaration( + undefined, + undefined, + ts.factory.createStringLiteral(moduleName), + ), + ); }); namespaceImports.forEach((namespaceImport, moduleName) => { const newImport = ts.factory.createImportDeclaration( - undefined, ts.factory.createImportClause(false, undefined, namespaceImport), - ts.factory.createStringLiteral(moduleName, useSingleQuotes)); + undefined, + ts.factory.createImportClause(false, undefined, namespaceImport), + ts.factory.createStringLiteral(moduleName, useSingleQuotes), + ); // IMPORTANT: Set the original TS node to the `ts.ImportDeclaration`. This allows // downstream transforms such as tsickle to properly process references to this import. @@ -265,10 +315,14 @@ export class ImportManager implements namedImports.forEach((specifiers, moduleName) => { const newImport = ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause( + false, undefined, - ts.factory.createImportClause( - false, undefined, ts.factory.createNamedImports(specifiers)), - ts.factory.createStringLiteral(moduleName, useSingleQuotes)); + ts.factory.createNamedImports(specifiers), + ), + ts.factory.createStringLiteral(moduleName, useSingleQuotes), + ); addNewImport(fileName, newImport); }); @@ -288,8 +342,9 @@ export class ImportManager implements * @param extraStatementsMap Additional set of statements to be inserted * for given source files after their imports. E.g. top-level constants. */ - toTsTransform(extraStatementsMap?: Map): - ts.TransformerFactory { + toTsTransform( + extraStatementsMap?: Map, + ): ts.TransformerFactory { return createTsTransformForImportManager(this, extraStatementsMap); } @@ -300,16 +355,19 @@ export class ImportManager implements * for given source files after their imports. E.g. top-level constants. */ transformTsFile( - ctx: ts.TransformationContext, file: ts.SourceFile, - extraStatementsAfterImports?: ts.Statement[]): ts.SourceFile { - const extraStatementsMap = extraStatementsAfterImports ? - new Map([[file.fileName, extraStatementsAfterImports]]) : - undefined; + ctx: ts.TransformationContext, + file: ts.SourceFile, + extraStatementsAfterImports?: ts.Statement[], + ): ts.SourceFile { + const extraStatementsMap = extraStatementsAfterImports + ? new Map([[file.fileName, extraStatementsAfterImports]]) + : undefined; return this.toTsTransform(extraStatementsMap)(ctx)(file); } - private _getNewImportsTrackerForFile(file: ts.SourceFile): - NonNullable> { + private _getNewImportsTrackerForFile( + file: ts.SourceFile, + ): NonNullable> { if (!this.newImports.has(file)) { this.newImports.set(file, { namespaceImports: new Map(), @@ -323,8 +381,9 @@ export class ImportManager implements /** Creates an import reference based on the given identifier, or nested access. */ function createImportReference( - asTypeReference: boolean, ref: ts.Identifier|[ts.Identifier, ts.Identifier]): ts.Identifier| - ts.QualifiedName|ts.PropertyAccessExpression { + asTypeReference: boolean, + ref: ts.Identifier | [ts.Identifier, ts.Identifier], +): ts.Identifier | ts.QualifiedName | ts.PropertyAccessExpression { if (asTypeReference) { return Array.isArray(ref) ? ts.factory.createQualifiedName(ref[0], ref[1]) : ref; } else { diff --git a/packages/compiler-cli/src/ngtsc/translator/src/import_manager/import_typescript_transform.ts b/packages/compiler-cli/src/ngtsc/translator/src/import_manager/import_typescript_transform.ts index d6bd726cb6065..9939ced2ac009 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/import_manager/import_typescript_transform.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/import_manager/import_typescript_transform.ts @@ -20,18 +20,20 @@ import type {ImportManager} from './import_manager'; * - The transform inserts additional optional statements after imports. */ export function createTsTransformForImportManager( - manager: ImportManager, - extraStatementsForFiles?: Map): ts.TransformerFactory { + manager: ImportManager, + extraStatementsForFiles?: Map, +): ts.TransformerFactory { return (ctx) => { const {affectedFiles, newImports, updatedImports, reusedOriginalAliasDeclarations} = - manager.finalize(); + manager.finalize(); // If we re-used existing source file alias declarations, mark those as referenced so TypeScript // doesn't drop these thinking they are unused. if (reusedOriginalAliasDeclarations.size > 0) { const referencedAliasDeclarations = loadIsReferencedAliasDeclarationPatch(ctx); - reusedOriginalAliasDeclarations.forEach( - aliasDecl => referencedAliasDeclarations.add(aliasDecl)); + reusedOriginalAliasDeclarations.forEach((aliasDecl) => + referencedAliasDeclarations.add(aliasDecl), + ); } // Update the set of affected files to include files that need extra statements to be inserted. @@ -44,34 +46,50 @@ export function createTsTransformForImportManager( } const visitStatement: ts.Visitor = (node) => { - if (!ts.isImportDeclaration(node) || node.importClause === undefined || - !ts.isImportClause(node.importClause)) { + if ( + !ts.isImportDeclaration(node) || + node.importClause === undefined || + !ts.isImportClause(node.importClause) + ) { return node; } const clause = node.importClause; - if (clause.namedBindings === undefined || !ts.isNamedImports(clause.namedBindings) || - !updatedImports.has(clause.namedBindings)) { + if ( + clause.namedBindings === undefined || + !ts.isNamedImports(clause.namedBindings) || + !updatedImports.has(clause.namedBindings) + ) { return node; } const newClause = ctx.factory.updateImportClause( - clause, clause.isTypeOnly, clause.name, updatedImports.get(clause.namedBindings)); + clause, + clause.isTypeOnly, + clause.name, + updatedImports.get(clause.namedBindings), + ); const newImport = ctx.factory.updateImportDeclaration( - node, node.modifiers, newClause, node.moduleSpecifier, node.attributes); + node, + node.modifiers, + newClause, + node.moduleSpecifier, + node.attributes, + ); // This tricks TypeScript into thinking that the `importClause` is still optimizable. // By default, TS assumes, no specifiers are elide-able if the clause of the "original // node" has changed. google3: // typescript/unstable/src/compiler/transformers/ts.ts;l=456;rcl=611254538. - ts.setOriginalNode( - newImport, - {importClause: newClause, kind: newImport.kind} as Partialas any); + ts.setOriginalNode(newImport, { + importClause: newClause, + kind: newImport.kind, + } as Partial as any); return newImport; }; - return sourceFile => { + return (sourceFile) => { if (!affectedFiles.has(sourceFile.fileName)) { return sourceFile; } @@ -93,18 +111,18 @@ export function createTsTransformForImportManager( } return ctx.factory.updateSourceFile( - sourceFile, - [ - ...existingImports, - ...(newImports.get(sourceFile.fileName) ?? []), - ...extraStatements, - ...body, - ], - sourceFile.isDeclarationFile, - sourceFile.referencedFiles, - sourceFile.typeReferenceDirectives, - sourceFile.hasNoDefaultLib, - sourceFile.libReferenceDirectives, + sourceFile, + [ + ...existingImports, + ...(newImports.get(sourceFile.fileName) ?? []), + ...extraStatements, + ...body, + ], + sourceFile.isDeclarationFile, + sourceFile.referencedFiles, + sourceFile.typeReferenceDirectives, + sourceFile.hasNoDefaultLib, + sourceFile.libReferenceDirectives, ); }; }; @@ -112,6 +130,7 @@ export function createTsTransformForImportManager( /** Whether the given statement is an import statement. */ function isImportStatement(stmt: ts.Statement): boolean { - return ts.isImportDeclaration(stmt) || ts.isImportEqualsDeclaration(stmt) || - ts.isNamespaceImport(stmt); + return ( + ts.isImportDeclaration(stmt) || ts.isImportEqualsDeclaration(stmt) || ts.isNamespaceImport(stmt) + ); } diff --git a/packages/compiler-cli/src/ngtsc/translator/src/import_manager/reuse_generated_imports.ts b/packages/compiler-cli/src/ngtsc/translator/src/import_manager/reuse_generated_imports.ts index 48cb07340fb0e..1f1b98a29f8d0 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/import_manager/reuse_generated_imports.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/import_manager/reuse_generated_imports.ts @@ -13,7 +13,7 @@ import {ImportRequest} from '../api/import_generator'; import type {ModuleName} from './import_manager'; /** Branded string identifying a hashed {@link ImportRequest}. */ -type ImportRequestHash = string&{__importHash: string}; +type ImportRequestHash = string & {__importHash: string}; /** Tracker capturing re-used generated imports. */ export interface ReuseGeneratedImportsTracker { @@ -21,7 +21,7 @@ export interface ReuseGeneratedImportsTracker { * Map of previously resolved symbol imports. Cache can be re-used to return * the same identifier without checking the source-file again. */ - directReuseCache: Map; + directReuseCache: Map; /** * Map of module names and their potential namespace import @@ -32,8 +32,9 @@ export interface ReuseGeneratedImportsTracker { /** Attempts to efficiently re-use previous generated import requests. */ export function attemptToReuseGeneratedImports( - tracker: ReuseGeneratedImportsTracker, - request: ImportRequest): ts.Identifier|[ts.Identifier, ts.Identifier]|null { + tracker: ReuseGeneratedImportsTracker, + request: ImportRequest, +): ts.Identifier | [ts.Identifier, ts.Identifier] | null { const requestHash = hashImportRequest(request); // In case the given import has been already generated previously, we just return @@ -43,8 +44,9 @@ export function attemptToReuseGeneratedImports( return existingExactImport; } - const potentialNamespaceImport = - tracker.namespaceImportReuseCache.get(request.exportModuleSpecifier as ModuleName); + const potentialNamespaceImport = tracker.namespaceImportReuseCache.get( + request.exportModuleSpecifier as ModuleName, + ); if (potentialNamespaceImport === undefined) { return null; } @@ -58,18 +60,21 @@ export function attemptToReuseGeneratedImports( /** Captures the given import request and its generated reference node/path for future re-use. */ export function captureGeneratedImport( - request: ImportRequest, tracker: ReuseGeneratedImportsTracker, - referenceNode: ts.Identifier|[ts.Identifier, ts.Identifier]) { + request: ImportRequest, + tracker: ReuseGeneratedImportsTracker, + referenceNode: ts.Identifier | [ts.Identifier, ts.Identifier], +) { tracker.directReuseCache.set(hashImportRequest(request), referenceNode); if (request.exportSymbolName === null && !Array.isArray(referenceNode)) { tracker.namespaceImportReuseCache.set( - request.exportModuleSpecifier as ModuleName, referenceNode); + request.exportModuleSpecifier as ModuleName, + referenceNode, + ); } } /** Generates a unique hash for the given import request. */ function hashImportRequest(req: ImportRequest): ImportRequestHash { - return `${req.requestedFile.fileName}:${req.exportModuleSpecifier}:${req.exportSymbolName}` as - ImportRequestHash; + return `${req.requestedFile.fileName}:${req.exportModuleSpecifier}:${req.exportSymbolName}` as ImportRequestHash; } diff --git a/packages/compiler-cli/src/ngtsc/translator/src/import_manager/reuse_source_file_imports.ts b/packages/compiler-cli/src/ngtsc/translator/src/import_manager/reuse_source_file_imports.ts index b12dde8259fd1..34ce5491a54a3 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/import_manager/reuse_source_file_imports.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/import_manager/reuse_source_file_imports.ts @@ -23,9 +23,10 @@ export interface ReuseExistingSourceFileImportsTracker { * Map of import declarations that need to be updated to include the * given symbols. */ - updatedImports: - Map; + updatedImports: Map< + ts.ImportDeclaration, + {propertyName: ts.Identifier; fileUniqueAlias: ts.Identifier | null}[] + >; /** * Set of re-used alias import declarations. @@ -36,18 +37,20 @@ export interface ReuseExistingSourceFileImportsTracker { reusedAliasDeclarations: Set; /** Generates a unique identifier for a name in the given file. */ - generateUniqueIdentifier(file: ts.SourceFile, symbolName: string): ts.Identifier|null; + generateUniqueIdentifier(file: ts.SourceFile, symbolName: string): ts.Identifier | null; } /** Attempts to re-use original source file imports for the given request. */ export function attemptToReuseExistingSourceFileImports( - tracker: ReuseExistingSourceFileImportsTracker, sourceFile: ts.SourceFile, - request: ImportRequest): ts.Identifier|[ts.Identifier, ts.Identifier]|null { + tracker: ReuseExistingSourceFileImportsTracker, + sourceFile: ts.SourceFile, + request: ImportRequest, +): ts.Identifier | [ts.Identifier, ts.Identifier] | null { // Walk through all source-file top-level statements and search for import declarations // that already match the specified "moduleName" and can be updated to import the // given symbol. If no matching import can be found, the last import in the source-file // will be used as starting point for a new import that will be generated. - let candidateImportToBeUpdated: ts.ImportDeclaration|null = null; + let candidateImportToBeUpdated: ts.ImportDeclaration | null = null; for (let i = sourceFile.statements.length - 1; i >= 0; i--) { const statement = sourceFile.statements[i]; @@ -86,11 +89,14 @@ export function attemptToReuseExistingSourceFileImports( // Named imports can be re-used if a specific symbol is requested. if (ts.isNamedImports(namedBindings) && request.exportSymbolName !== null) { - const existingElement = namedBindings.elements.find(e => { + const existingElement = namedBindings.elements.find((e) => { // TODO: Consider re-using type-only imports efficiently. - return !e.isTypeOnly && - (e.propertyName ? e.propertyName.text === request.exportSymbolName : - e.name.text === request.exportSymbolName); + return ( + !e.isTypeOnly && + (e.propertyName + ? e.propertyName.text === request.exportSymbolName + : e.name.text === request.exportSymbolName) + ); }); if (existingElement !== undefined) { diff --git a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts index f0f5dae9a4c42..a9a385f48f2f1 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/translator.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/translator.ts @@ -7,7 +7,15 @@ */ import * as o from '@angular/compiler'; -import {AstFactory, BinaryOperator, ObjectLiteralProperty, SourceMapRange, TemplateElement, TemplateLiteral, UnaryOperator} from './api/ast_factory'; +import { + AstFactory, + BinaryOperator, + ObjectLiteralProperty, + SourceMapRange, + TemplateElement, + TemplateLiteral, + UnaryOperator, +} from './api/ast_factory'; import {ImportGenerator} from './api/import_generator'; import {Context} from './context'; @@ -46,64 +54,82 @@ export interface TranslatorOptions { annotateForClosureCompiler?: boolean; } -export class ExpressionTranslatorVisitor implements - o.ExpressionVisitor, o.StatementVisitor { +export class ExpressionTranslatorVisitor + implements o.ExpressionVisitor, o.StatementVisitor +{ private downlevelTaggedTemplates: boolean; private downlevelVariableDeclarations: boolean; private recordWrappedNode: RecordWrappedNodeFn; constructor( - private factory: AstFactory, - private imports: ImportGenerator, private contextFile: TFile, - options: TranslatorOptions) { + private factory: AstFactory, + private imports: ImportGenerator, + private contextFile: TFile, + options: TranslatorOptions, + ) { this.downlevelTaggedTemplates = options.downlevelTaggedTemplates === true; this.downlevelVariableDeclarations = options.downlevelVariableDeclarations === true; this.recordWrappedNode = options.recordWrappedNode || (() => {}); } visitDeclareVarStmt(stmt: o.DeclareVarStmt, context: Context): TStatement { - const varType = this.downlevelVariableDeclarations ? 'var' : - stmt.hasModifier(o.StmtModifier.Final) ? 'const' : - 'let'; + const varType = this.downlevelVariableDeclarations + ? 'var' + : stmt.hasModifier(o.StmtModifier.Final) + ? 'const' + : 'let'; return this.attachComments( - this.factory.createVariableDeclaration( - stmt.name, stmt.value?.visitExpression(this, context.withExpressionMode), varType), - stmt.leadingComments); + this.factory.createVariableDeclaration( + stmt.name, + stmt.value?.visitExpression(this, context.withExpressionMode), + varType, + ), + stmt.leadingComments, + ); } visitDeclareFunctionStmt(stmt: o.DeclareFunctionStmt, context: Context): TStatement { return this.attachComments( - this.factory.createFunctionDeclaration( - stmt.name, stmt.params.map(param => param.name), - this.factory.createBlock( - this.visitStatements(stmt.statements, context.withStatementMode))), - stmt.leadingComments); + this.factory.createFunctionDeclaration( + stmt.name, + stmt.params.map((param) => param.name), + this.factory.createBlock(this.visitStatements(stmt.statements, context.withStatementMode)), + ), + stmt.leadingComments, + ); } visitExpressionStmt(stmt: o.ExpressionStatement, context: Context): TStatement { return this.attachComments( - this.factory.createExpressionStatement( - stmt.expr.visitExpression(this, context.withStatementMode)), - stmt.leadingComments); + this.factory.createExpressionStatement( + stmt.expr.visitExpression(this, context.withStatementMode), + ), + stmt.leadingComments, + ); } visitReturnStmt(stmt: o.ReturnStatement, context: Context): TStatement { return this.attachComments( - this.factory.createReturnStatement( - stmt.value.visitExpression(this, context.withExpressionMode)), - stmt.leadingComments); + this.factory.createReturnStatement( + stmt.value.visitExpression(this, context.withExpressionMode), + ), + stmt.leadingComments, + ); } visitIfStmt(stmt: o.IfStmt, context: Context): TStatement { return this.attachComments( - this.factory.createIfStatement( - stmt.condition.visitExpression(this, context), - this.factory.createBlock( - this.visitStatements(stmt.trueCase, context.withStatementMode)), - stmt.falseCase.length > 0 ? this.factory.createBlock(this.visitStatements( - stmt.falseCase, context.withStatementMode)) : - null), - stmt.leadingComments); + this.factory.createIfStatement( + stmt.condition.visitExpression(this, context), + this.factory.createBlock(this.visitStatements(stmt.trueCase, context.withStatementMode)), + stmt.falseCase.length > 0 + ? this.factory.createBlock( + this.visitStatements(stmt.falseCase, context.withStatementMode), + ) + : null, + ), + stmt.leadingComments, + ); } visitReadVarExpr(ast: o.ReadVarExpr, _context: Context): TExpression { @@ -114,56 +140,69 @@ export class ExpressionTranslatorVisitor impleme visitWriteVarExpr(expr: o.WriteVarExpr, context: Context): TExpression { const assignment = this.factory.createAssignment( - this.setSourceMapRange(this.factory.createIdentifier(expr.name), expr.sourceSpan), - expr.value.visitExpression(this, context), + this.setSourceMapRange(this.factory.createIdentifier(expr.name), expr.sourceSpan), + expr.value.visitExpression(this, context), ); - return context.isStatement ? assignment : - this.factory.createParenthesizedExpression(assignment); + return context.isStatement + ? assignment + : this.factory.createParenthesizedExpression(assignment); } visitWriteKeyExpr(expr: o.WriteKeyExpr, context: Context): TExpression { const exprContext = context.withExpressionMode; const target = this.factory.createElementAccess( - expr.receiver.visitExpression(this, exprContext), - expr.index.visitExpression(this, exprContext), + expr.receiver.visitExpression(this, exprContext), + expr.index.visitExpression(this, exprContext), ); - const assignment = - this.factory.createAssignment(target, expr.value.visitExpression(this, exprContext)); - return context.isStatement ? assignment : - this.factory.createParenthesizedExpression(assignment); + const assignment = this.factory.createAssignment( + target, + expr.value.visitExpression(this, exprContext), + ); + return context.isStatement + ? assignment + : this.factory.createParenthesizedExpression(assignment); } visitWritePropExpr(expr: o.WritePropExpr, context: Context): TExpression { - const target = - this.factory.createPropertyAccess(expr.receiver.visitExpression(this, context), expr.name); + const target = this.factory.createPropertyAccess( + expr.receiver.visitExpression(this, context), + expr.name, + ); return this.factory.createAssignment(target, expr.value.visitExpression(this, context)); } visitInvokeFunctionExpr(ast: o.InvokeFunctionExpr, context: Context): TExpression { return this.setSourceMapRange( - this.factory.createCallExpression( - ast.fn.visitExpression(this, context), - ast.args.map(arg => arg.visitExpression(this, context)), ast.pure), - ast.sourceSpan); + this.factory.createCallExpression( + ast.fn.visitExpression(this, context), + ast.args.map((arg) => arg.visitExpression(this, context)), + ast.pure, + ), + ast.sourceSpan, + ); } visitTaggedTemplateExpr(ast: o.TaggedTemplateExpr, context: Context): TExpression { return this.setSourceMapRange( - this.createTaggedTemplateExpression(ast.tag.visitExpression(this, context), { - elements: ast.template.elements.map(e => createTemplateElement({ - cooked: e.text, - raw: e.rawText, - range: e.sourceSpan ?? ast.sourceSpan, - })), - expressions: ast.template.expressions.map(e => e.visitExpression(this, context)) - }), - ast.sourceSpan); + this.createTaggedTemplateExpression(ast.tag.visitExpression(this, context), { + elements: ast.template.elements.map((e) => + createTemplateElement({ + cooked: e.text, + raw: e.rawText, + range: e.sourceSpan ?? ast.sourceSpan, + }), + ), + expressions: ast.template.expressions.map((e) => e.visitExpression(this, context)), + }), + ast.sourceSpan, + ); } visitInstantiateExpr(ast: o.InstantiateExpr, context: Context): TExpression { return this.factory.createNewExpression( - ast.classExpr.visitExpression(this, context), - ast.args.map(arg => arg.visitExpression(this, context))); + ast.classExpr.visitExpression(this, context), + ast.args.map((arg) => arg.visitExpression(this, context)), + ); } visitLiteralExpr(ast: o.LiteralExpr, _context: Context): TExpression { @@ -188,20 +227,27 @@ export class ExpressionTranslatorVisitor impleme const expressions: TExpression[] = []; for (let i = 0; i < ast.expressions.length; i++) { const placeholder = this.setSourceMapRange( - ast.expressions[i].visitExpression(this, context), ast.getPlaceholderSourceSpan(i)); + ast.expressions[i].visitExpression(this, context), + ast.getPlaceholderSourceSpan(i), + ); expressions.push(placeholder); elements.push(createTemplateElement(ast.serializeI18nTemplatePart(i + 1))); } const localizeTag = this.factory.createIdentifier('$localize'); return this.setSourceMapRange( - this.createTaggedTemplateExpression(localizeTag, {elements, expressions}), ast.sourceSpan); + this.createTaggedTemplateExpression(localizeTag, {elements, expressions}), + ast.sourceSpan, + ); } - private createTaggedTemplateExpression(tag: TExpression, template: TemplateLiteral): - TExpression { - return this.downlevelTaggedTemplates ? this.createES5TaggedTemplateFunctionCall(tag, template) : - this.factory.createTaggedTemplate(tag, template); + private createTaggedTemplateExpression( + tag: TExpression, + template: TemplateLiteral, + ): TExpression { + return this.downlevelTaggedTemplates + ? this.createES5TaggedTemplateFunctionCall(tag, template) + : this.factory.createTaggedTemplate(tag, template); } /** @@ -209,7 +255,9 @@ export class ExpressionTranslatorVisitor impleme * imported `__makeTemplateObject` helper for ES5 formatted output. */ private createES5TaggedTemplateFunctionCall( - tagHandler: TExpression, {elements, expressions}: TemplateLiteral): TExpression { + tagHandler: TExpression, + {elements, expressions}: TemplateLiteral, + ): TExpression { // Ensure that the `__makeTemplateObject()` helper has been imported. const __makeTemplateObjectHelper = this.imports.addImport({ exportModuleSpecifier: 'tslib', @@ -221,23 +269,28 @@ export class ExpressionTranslatorVisitor impleme const cooked: TExpression[] = []; const raw: TExpression[] = []; for (const element of elements) { - cooked.push(this.factory.setSourceMapRange( - this.factory.createLiteral(element.cooked), element.range)); + cooked.push( + this.factory.setSourceMapRange(this.factory.createLiteral(element.cooked), element.range), + ); raw.push( - this.factory.setSourceMapRange(this.factory.createLiteral(element.raw), element.range)); + this.factory.setSourceMapRange(this.factory.createLiteral(element.raw), element.range), + ); } // Generate the helper call in the form: `__makeTemplateObject([cooked], [raw]);` const templateHelperCall = this.factory.createCallExpression( - __makeTemplateObjectHelper, - [this.factory.createArrayLiteral(cooked), this.factory.createArrayLiteral(raw)], - /* pure */ false); + __makeTemplateObjectHelper, + [this.factory.createArrayLiteral(cooked), this.factory.createArrayLiteral(raw)], + /* pure */ false, + ); // Finally create the tagged handler call in the form: // `tag(__makeTemplateObject([cooked], [raw]), ...expressions);` return this.factory.createCallExpression( - tagHandler, [templateHelperCall, ...expressions], - /* pure */ false); + tagHandler, + [templateHelperCall, ...expressions], + /* pure */ false, + ); } visitExternalExpr(ast: o.ExternalExpr, _context: Context): TExpression { @@ -296,8 +349,10 @@ export class ExpressionTranslatorVisitor impleme } return this.factory.createConditional( - cond, ast.trueCase.visitExpression(this, context), - ast.falseCase!.visitExpression(this, context)); + cond, + ast.trueCase.visitExpression(this, context), + ast.falseCase!.visitExpression(this, context), + ); } visitDynamicImportExpr(ast: o.DynamicImportExpr, context: any) { @@ -310,16 +365,19 @@ export class ExpressionTranslatorVisitor impleme visitFunctionExpr(ast: o.FunctionExpr, context: Context): TExpression { return this.factory.createFunctionExpression( - ast.name ?? null, ast.params.map(param => param.name), - this.factory.createBlock(this.visitStatements(ast.statements, context))); + ast.name ?? null, + ast.params.map((param) => param.name), + this.factory.createBlock(this.visitStatements(ast.statements, context)), + ); } visitArrowFunctionExpr(ast: o.ArrowFunctionExpr, context: any) { return this.factory.createArrowFunctionExpression( - ast.params.map(param => param.name), - Array.isArray(ast.body) ? - this.factory.createBlock(this.visitStatements(ast.body, context)) : - ast.body.visitExpression(this, context)); + ast.params.map((param) => param.name), + Array.isArray(ast.body) + ? this.factory.createBlock(this.visitStatements(ast.body, context)) + : ast.body.visitExpression(this, context), + ); } visitBinaryOperatorExpr(ast: o.BinaryOperatorExpr, context: Context): TExpression { @@ -327,9 +385,9 @@ export class ExpressionTranslatorVisitor impleme throw new Error(`Unknown binary operator: ${o.BinaryOperator[ast.operator]}`); } return this.factory.createBinaryExpression( - ast.lhs.visitExpression(this, context), - BINARY_OPERATORS.get(ast.operator)!, - ast.rhs.visitExpression(this, context), + ast.lhs.visitExpression(this, context), + BINARY_OPERATORS.get(ast.operator)!, + ast.rhs.visitExpression(this, context), ); } @@ -339,20 +397,25 @@ export class ExpressionTranslatorVisitor impleme visitReadKeyExpr(ast: o.ReadKeyExpr, context: Context): TExpression { return this.factory.createElementAccess( - ast.receiver.visitExpression(this, context), ast.index.visitExpression(this, context)); + ast.receiver.visitExpression(this, context), + ast.index.visitExpression(this, context), + ); } visitLiteralArrayExpr(ast: o.LiteralArrayExpr, context: Context): TExpression { - return this.factory.createArrayLiteral(ast.entries.map( - expr => this.setSourceMapRange(expr.visitExpression(this, context), ast.sourceSpan))); + return this.factory.createArrayLiteral( + ast.entries.map((expr) => + this.setSourceMapRange(expr.visitExpression(this, context), ast.sourceSpan), + ), + ); } visitLiteralMapExpr(ast: o.LiteralMapExpr, context: Context): TExpression { - const properties: ObjectLiteralProperty[] = ast.entries.map(entry => { + const properties: ObjectLiteralProperty[] = ast.entries.map((entry) => { return { propertyName: entry.key, quoted: entry.quoted, - value: entry.value.visitExpression(this, context) + value: entry.value.visitExpression(this, context), }; }); return this.setSourceMapRange(this.factory.createObjectLiteral(properties), ast.sourceSpan); @@ -376,21 +439,28 @@ export class ExpressionTranslatorVisitor impleme throw new Error(`Unknown unary operator: ${o.UnaryOperator[ast.operator]}`); } return this.factory.createUnaryExpression( - UNARY_OPERATORS.get(ast.operator)!, ast.expr.visitExpression(this, context)); + UNARY_OPERATORS.get(ast.operator)!, + ast.expr.visitExpression(this, context), + ); } private visitStatements(statements: o.Statement[], context: Context): TStatement[] { - return statements.map(stmt => stmt.visitStatement(this, context)) - .filter(stmt => stmt !== undefined); + return statements + .map((stmt) => stmt.visitStatement(this, context)) + .filter((stmt) => stmt !== undefined); } - private setSourceMapRange(ast: T, span: o.ParseSourceSpan|null): - T { + private setSourceMapRange( + ast: T, + span: o.ParseSourceSpan | null, + ): T { return this.factory.setSourceMapRange(ast, createRange(span)); } - private attachComments(statement: TStatement, leadingComments: o.LeadingComment[]|undefined): - TStatement { + private attachComments( + statement: TStatement, + leadingComments: o.LeadingComment[] | undefined, + ): TStatement { if (leadingComments !== undefined) { this.factory.attachComments(statement, leadingComments); } @@ -401,16 +471,22 @@ export class ExpressionTranslatorVisitor impleme /** * Convert a cooked-raw string object into one that can be used by the AST factories. */ -function createTemplateElement( - {cooked, raw, range}: {cooked: string, raw: string, range: o.ParseSourceSpan|null}): - TemplateElement { +function createTemplateElement({ + cooked, + raw, + range, +}: { + cooked: string; + raw: string; + range: o.ParseSourceSpan | null; +}): TemplateElement { return {cooked, raw, range: createRange(range)}; } /** * Convert an OutputAST source-span into a range that can be used by the AST factories. */ -function createRange(span: o.ParseSourceSpan|null): SourceMapRange|null { +function createRange(span: o.ParseSourceSpan | null): SourceMapRange | null { if (span === null) { return null; } diff --git a/packages/compiler-cli/src/ngtsc/translator/src/ts_util.ts b/packages/compiler-cli/src/ngtsc/translator/src/ts_util.ts index d1603700625f5..8cd1987ea72d9 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/ts_util.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/ts_util.ts @@ -11,7 +11,7 @@ import ts from 'typescript'; /** * Creates a TypeScript node representing a numeric value. */ -export function tsNumericExpression(value: number): ts.NumericLiteral|ts.PrefixUnaryExpression { +export function tsNumericExpression(value: number): ts.NumericLiteral | ts.PrefixUnaryExpression { // As of TypeScript 5.3 negative numbers are represented as `prefixUnaryOperator` and passing a // negative number (even as a string) into `createNumericLiteral` will result in an error. if (value < 0) { diff --git a/packages/compiler-cli/src/ngtsc/translator/src/type_emitter.ts b/packages/compiler-cli/src/ngtsc/translator/src/type_emitter.ts index c9b01d6161399..8dc934b21b9fb 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/type_emitter.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/type_emitter.ts @@ -12,7 +12,7 @@ import ts from 'typescript'; * origin source file into a type reference that is valid in the desired source file. If the type * cannot be translated to the desired source file, then null can be returned. */ -export type TypeReferenceTranslator = (type: ts.TypeReferenceNode) => ts.TypeReferenceNode|null; +export type TypeReferenceTranslator = (type: ts.TypeReferenceNode) => ts.TypeReferenceNode | null; /** * A marker to indicate that a type reference is ineligible for emitting. This needs to be truthy @@ -32,7 +32,9 @@ const INELIGIBLE: INELIGIBLE = {} as INELIGIBLE; * fail. */ export function canEmitType( - type: ts.TypeNode, canEmit: (type: ts.TypeReferenceNode) => boolean): boolean { + type: ts.TypeNode, + canEmit: (type: ts.TypeReferenceNode) => boolean, +): boolean { return canEmitTypeWorker(type); function canEmitTypeWorker(type: ts.TypeNode): boolean { @@ -45,7 +47,7 @@ export function canEmitType( // that case. Otherwise, the result of visiting all child nodes determines the result. If no // ineligible type reference node is found then the walk returns `undefined`, indicating that // no type node was visited that could not be emitted. - function visitNode(node: ts.Node): INELIGIBLE|undefined { + function visitNode(node: ts.Node): INELIGIBLE | undefined { // `import('module')` type nodes are not supported, as it may require rewriting the module // specifier which is currently not done. if (ts.isImportTypeNode(node)) { @@ -106,7 +108,7 @@ export class TypeEmitter { constructor(private translator: TypeReferenceTranslator) {} emitType(type: ts.TypeNode): ts.TypeNode { - const typeReferenceTransformer: ts.TransformerFactory = context => { + const typeReferenceTransformer: ts.TransformerFactory = (context) => { const visitNode = (node: ts.Node): ts.Node => { if (ts.isImportTypeNode(node)) { throw new Error('Unable to emit import type'); @@ -143,7 +145,7 @@ export class TypeEmitter { return ts.visitEachChild(node, visitNode, context); } }; - return node => ts.visitNode(node, visitNode, ts.isTypeNode); + return (node) => ts.visitNode(node, visitNode, ts.isTypeNode); }; return ts.transform(type, [typeReferenceTransformer]).transformed[0]; } @@ -156,10 +158,11 @@ export class TypeEmitter { } // Emit the type arguments, if any. - let typeArguments: ts.NodeArray|undefined = undefined; + let typeArguments: ts.NodeArray | undefined = undefined; if (type.typeArguments !== undefined) { - typeArguments = - ts.factory.createNodeArray(type.typeArguments.map(typeArg => this.emitType(typeArg))); + typeArguments = ts.factory.createNodeArray( + type.typeArguments.map((typeArg) => this.emitType(typeArg)), + ); } return ts.factory.updateTypeReferenceNode(type, translatedType.typeName, typeArguments); diff --git a/packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts index c678a2701569f..121abcbec3003 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts @@ -9,7 +9,13 @@ import * as o from '@angular/compiler'; import ts from 'typescript'; -import {assertSuccessfulReferenceEmit, ImportFlags, OwningModule, Reference, ReferenceEmitter} from '../../imports'; +import { + assertSuccessfulReferenceEmit, + ImportFlags, + OwningModule, + Reference, + ReferenceEmitter, +} from '../../imports'; import {AmbientImport, ReflectionHost} from '../../reflection'; import {Context} from './context'; @@ -17,18 +23,26 @@ import {ImportManager} from './import_manager/import_manager'; import {tsNumericExpression} from './ts_util'; import {TypeEmitter} from './type_emitter'; - export function translateType( - type: o.Type, contextFile: ts.SourceFile, reflector: ReflectionHost, - refEmitter: ReferenceEmitter, imports: ImportManager): ts.TypeNode { + type: o.Type, + contextFile: ts.SourceFile, + reflector: ReflectionHost, + refEmitter: ReferenceEmitter, + imports: ImportManager, +): ts.TypeNode { return type.visitType( - new TypeTranslatorVisitor(imports, contextFile, reflector, refEmitter), new Context(false)); + new TypeTranslatorVisitor(imports, contextFile, reflector, refEmitter), + new Context(false), + ); } class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor { constructor( - private imports: ImportManager, private contextFile: ts.SourceFile, - private reflector: ReflectionHost, private refEmitter: ReferenceEmitter) {} + private imports: ImportManager, + private contextFile: ts.SourceFile, + private reflector: ReflectionHost, + private refEmitter: ReferenceEmitter, + ) {} visitBuiltinType(type: o.BuiltinType, context: Context): ts.KeywordTypeNode { switch (type.name) { @@ -56,13 +70,15 @@ class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor { if (!ts.isTypeReferenceNode(typeNode)) { throw new Error( - 'An ExpressionType with type arguments must translate into a TypeReferenceNode'); + 'An ExpressionType with type arguments must translate into a TypeReferenceNode', + ); } else if (typeNode.typeArguments !== undefined) { throw new Error( - `An ExpressionType with type arguments cannot have multiple levels of type arguments`); + `An ExpressionType with type arguments cannot have multiple levels of type arguments`, + ); } - const typeArgs = type.typeParams.map(param => this.translateType(param, context)); + const typeArgs = type.typeParams.map((param) => this.translateType(param, context)); return ts.factory.createTypeReferenceNode(typeNode.typeName, typeArgs); } @@ -72,11 +88,16 @@ class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor { visitMapType(type: o.MapType, context: Context): ts.TypeLiteralNode { const parameter = ts.factory.createParameterDeclaration( - undefined, undefined, 'key', undefined, - ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)); - const typeArgs = type.valueType !== null ? - this.translateType(type.valueType, context) : - ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword); + undefined, + undefined, + 'key', + undefined, + ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + ); + const typeArgs = + type.valueType !== null + ? this.translateType(type.valueType, context) + : ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword); const indexSignature = ts.factory.createIndexSignature(undefined, [parameter], typeArgs); return ts.factory.createTypeLiteralNode([indexSignature]); } @@ -89,8 +110,9 @@ class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor { const viaModule = ast.type instanceof Reference ? ast.type.bestGuessOwningModule : null; - const emitter = - new TypeEmitter(typeRef => this.translateTypeReference(typeRef, context, viaModule)); + const emitter = new TypeEmitter((typeRef) => + this.translateTypeReference(typeRef, context, viaModule), + ); return emitter.emitType(node); } @@ -132,7 +154,8 @@ class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor { return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword); } else if (typeof ast.value === 'boolean') { return ts.factory.createLiteralTypeNode( - ast.value ? ts.factory.createTrue() : ts.factory.createFalse()); + ast.value ? ts.factory.createTrue() : ts.factory.createFalse(), + ); } else if (typeof ast.value === 'number') { return ts.factory.createLiteralTypeNode(tsNumericExpression(ast.value)); } else { @@ -144,7 +167,7 @@ class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor { throw new Error('Method not implemented.'); } - visitExternalExpr(ast: o.ExternalExpr, context: Context): ts.EntityName|ts.TypeReferenceNode { + visitExternalExpr(ast: o.ExternalExpr, context: Context): ts.EntityName | ts.TypeReferenceNode { if (ast.value.moduleName === null || ast.value.name === null) { throw new Error(`Import unknown module or symbol`); } @@ -152,12 +175,13 @@ class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor { exportModuleSpecifier: ast.value.moduleName, exportSymbolName: ast.value.name, requestedFile: this.contextFile, - asTypeReference: true + asTypeReference: true, }); - const typeArguments = ast.typeParams !== null ? - ast.typeParams.map(type => this.translateType(type, context)) : - undefined; + const typeArguments = + ast.typeParams !== null + ? ast.typeParams.map((type) => this.translateType(type, context)) + : undefined; return ts.factory.createTypeReferenceNode(typeName, typeArguments); } @@ -198,19 +222,20 @@ class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor { } visitLiteralArrayExpr(ast: o.LiteralArrayExpr, context: Context): ts.TupleTypeNode { - const values = ast.entries.map(expr => this.translateExpression(expr, context)); + const values = ast.entries.map((expr) => this.translateExpression(expr, context)); return ts.factory.createTupleTypeNode(values); } visitLiteralMapExpr(ast: o.LiteralMapExpr, context: Context): ts.TypeLiteralNode { - const entries = ast.entries.map(entry => { + const entries = ast.entries.map((entry) => { const {key, quoted} = entry; const type = this.translateExpression(entry.value, context); return ts.factory.createPropertySignature( - /* modifiers */ undefined, - /* name */ quoted ? ts.factory.createStringLiteral(key) : key, - /* questionToken */ undefined, - /* type */ type); + /* modifiers */ undefined, + /* name */ quoted ? ts.factory.createStringLiteral(key) : key, + /* questionToken */ undefined, + /* type */ type, + ); }); return ts.factory.createTypeLiteralNode(entries); } @@ -229,7 +254,8 @@ class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor { return ts.factory.createLiteralTypeNode(node); } else { throw new Error( - `Unsupported WrappedNodeExpr in TypeTranslatorVisitor: ${ts.SyntaxKind[node.kind]}`); + `Unsupported WrappedNodeExpr in TypeTranslatorVisitor: ${ts.SyntaxKind[node.kind]}`, + ); } } @@ -246,7 +272,8 @@ class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor { const typeNode = type.visitType(this, context); if (!ts.isTypeNode(typeNode)) { throw new Error( - `A Type must translate to a TypeNode, but was ${ts.SyntaxKind[typeNode.kind]}`); + `A Type must translate to a TypeNode, but was ${ts.SyntaxKind[typeNode.kind]}`, + ); } return typeNode; } @@ -255,19 +282,23 @@ class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor { const typeNode = expr.visitExpression(this, context); if (!ts.isTypeNode(typeNode)) { throw new Error( - `An Expression must translate to a TypeNode, but was ${ts.SyntaxKind[typeNode.kind]}`); + `An Expression must translate to a TypeNode, but was ${ts.SyntaxKind[typeNode.kind]}`, + ); } return typeNode; } private translateTypeReference( - type: ts.TypeReferenceNode, context: Context, - viaModule: OwningModule|null): ts.TypeReferenceNode|null { + type: ts.TypeReferenceNode, + context: Context, + viaModule: OwningModule | null, + ): ts.TypeReferenceNode | null { const target = ts.isIdentifier(type.typeName) ? type.typeName : type.typeName.right; const declaration = this.reflector.getDeclarationOfIdentifier(target); if (declaration === null) { throw new Error( - `Unable to statically determine the declaration file of type node ${target.text}`); + `Unable to statically determine the declaration file of type node ${target.text}`, + ); } let owningModule = viaModule; @@ -279,10 +310,14 @@ class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor { } const reference = new Reference( - declaration.node, declaration.viaModule === AmbientImport ? AmbientImport : owningModule); + declaration.node, + declaration.viaModule === AmbientImport ? AmbientImport : owningModule, + ); const emittedType = this.refEmitter.emit( - reference, this.contextFile, - ImportFlags.NoAliasing | ImportFlags.AllowTypeImports | ImportFlags.AllowAmbientReferences); + reference, + this.contextFile, + ImportFlags.NoAliasing | ImportFlags.AllowTypeImports | ImportFlags.AllowAmbientReferences, + ); assertSuccessfulReferenceEmit(emittedType, target, 'type'); @@ -290,7 +325,8 @@ class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor { if (!ts.isTypeReferenceNode(typeNode)) { throw new Error( - `Expected TypeReferenceNode for emitted reference, got ${ts.SyntaxKind[typeNode.kind]}.`); + `Expected TypeReferenceNode for emitted reference, got ${ts.SyntaxKind[typeNode.kind]}.`, + ); } return typeNode; } 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 6601544c6367d..b9fa23a1d116d 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 @@ -7,7 +7,16 @@ */ import ts from 'typescript'; -import {AstFactory, BinaryOperator, LeadingComment, ObjectLiteralProperty, SourceMapRange, TemplateLiteral, UnaryOperator, VariableDeclarationType} from './api/ast_factory'; +import { + AstFactory, + BinaryOperator, + LeadingComment, + ObjectLiteralProperty, + SourceMapRange, + TemplateLiteral, + UnaryOperator, + VariableDeclarationType, +} from './api/ast_factory'; import {tsNumericExpression} from './ts_util'; /** @@ -75,8 +84,10 @@ export class TypeScriptAstFactory implements AstFactory ts.factory.createParameterDeclaration(undefined, undefined, param)), - undefined, body); + undefined, + undefined, + functionName, + undefined, + parameters.map((param) => ts.factory.createParameterDeclaration(undefined, undefined, param)), + undefined, + body, + ); } - createFunctionExpression(functionName: string|null, parameters: string[], body: ts.Statement): - ts.Expression { + createFunctionExpression( + functionName: string | null, + parameters: string[], + body: ts.Statement, + ): ts.Expression { if (!ts.isBlock(body)) { throw new Error(`Invalid syntax, expected a block, but got ${ts.SyntaxKind[body.kind]}.`); } return ts.factory.createFunctionExpression( - undefined, undefined, functionName ?? undefined, undefined, - parameters.map(param => ts.factory.createParameterDeclaration(undefined, undefined, param)), - undefined, body); + undefined, + undefined, + functionName ?? undefined, + undefined, + parameters.map((param) => ts.factory.createParameterDeclaration(undefined, undefined, param)), + undefined, + body, + ); } - createArrowFunctionExpression(parameters: string[], body: ts.Statement|ts.Expression): - ts.Expression { + createArrowFunctionExpression( + parameters: string[], + body: ts.Statement | ts.Expression, + ): ts.Expression { if (ts.isStatement(body) && !ts.isBlock(body)) { throw new Error(`Invalid syntax, expected a block, but got ${ts.SyntaxKind[body.kind]}.`); } return ts.factory.createArrowFunction( - undefined, undefined, - parameters.map(param => ts.factory.createParameterDeclaration(undefined, undefined, param)), - undefined, undefined, body); + undefined, + undefined, + parameters.map((param) => ts.factory.createParameterDeclaration(undefined, undefined, param)), + undefined, + undefined, + body, + ); } createIdentifier = ts.factory.createIdentifier; createIfStatement( - condition: ts.Expression, thenStatement: ts.Statement, - elseStatement: ts.Statement|null): ts.Statement { + condition: ts.Expression, + thenStatement: ts.Statement, + elseStatement: ts.Statement | null, + ): ts.Statement { return ts.factory.createIfStatement(condition, thenStatement, elseStatement ?? undefined); } - createLiteral(value: string|number|boolean|null|undefined): ts.Expression { + createLiteral(value: string | number | boolean | null | undefined): ts.Expression { if (value === undefined) { return ts.factory.createIdentifier('undefined'); } else if (value === null) { @@ -174,23 +219,30 @@ export class TypeScriptAstFactory implements AstFactory[]): ts.Expression { - return ts.factory.createObjectLiteralExpression(properties.map( - prop => ts.factory.createPropertyAssignment( - prop.quoted ? ts.factory.createStringLiteral(prop.propertyName) : - ts.factory.createIdentifier(prop.propertyName), - prop.value))); + return ts.factory.createObjectLiteralExpression( + properties.map((prop) => + ts.factory.createPropertyAssignment( + prop.quoted + ? ts.factory.createStringLiteral(prop.propertyName) + : ts.factory.createIdentifier(prop.propertyName), + prop.value, + ), + ), + ); } createParenthesizedExpression = ts.factory.createParenthesizedExpression; createPropertyAccess = ts.factory.createPropertyAccessExpression; - createReturnStatement(expression: ts.Expression|null): ts.Statement { + createReturnStatement(expression: ts.Expression | null): ts.Statement { return ts.factory.createReturnStatement(expression ?? undefined); } - createTaggedTemplate(tag: ts.Expression, template: TemplateLiteral): - ts.Expression { + createTaggedTemplate( + tag: ts.Expression, + template: TemplateLiteral, + ): ts.Expression { let templateLiteral: ts.TemplateLiteral; const length = template.elements.length; const head = template.elements[0]; @@ -217,7 +269,9 @@ export class TypeScriptAstFactory implements AstFactory(node: T, sourceMapRange: SourceMapRange|null): T { + setSourceMapRange(node: T, sourceMapRange: SourceMapRange | null): T { if (sourceMapRange === null) { return node; } @@ -254,11 +316,16 @@ export class TypeScriptAstFactory implements AstFactory pos)); + url, + ts.createSourceMapSource(url, sourceMapRange.content, (pos) => pos), + ); } const source = this.externalSourceFiles.get(url); - ts.setSourceMapRange( - node, {pos: sourceMapRange.start.offset, end: sourceMapRange.end.offset, source}); + ts.setSourceMapRange(node, { + pos: sourceMapRange.start.offset, + end: sourceMapRange.end.offset, + source, + }); return node; } } @@ -287,11 +354,16 @@ export function createTemplateTail(cooked: string, raw: string): ts.TemplateTail */ export function attachComments(statement: ts.Statement, leadingComments: LeadingComment[]): void { for (const comment of leadingComments) { - const commentKind = comment.multiline ? ts.SyntaxKind.MultiLineCommentTrivia : - ts.SyntaxKind.SingleLineCommentTrivia; + const commentKind = comment.multiline + ? ts.SyntaxKind.MultiLineCommentTrivia + : ts.SyntaxKind.SingleLineCommentTrivia; if (comment.multiline) { ts.addSyntheticLeadingComment( - statement, commentKind, comment.toString(), comment.trailingNewline); + statement, + commentKind, + comment.toString(), + comment.trailingNewline, + ); } else { for (const line of comment.toString().split('\n')) { ts.addSyntheticLeadingComment(statement, commentKind, line, comment.trailingNewline); 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 5e6e4a8f4cef0..a2eb1f11e2a0d 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/typescript_translator.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/typescript_translator.ts @@ -15,23 +15,35 @@ import {ExpressionTranslatorVisitor, TranslatorOptions} from './translator'; import {TypeScriptAstFactory} from './typescript_ast_factory'; export function translateExpression( - contextFile: ts.SourceFile, expression: o.Expression, - imports: ImportGenerator, - options: TranslatorOptions = {}): ts.Expression { + contextFile: ts.SourceFile, + expression: o.Expression, + imports: ImportGenerator, + options: TranslatorOptions = {}, +): ts.Expression { return expression.visitExpression( - new ExpressionTranslatorVisitor( - new TypeScriptAstFactory(options.annotateForClosureCompiler === true), imports, - contextFile, options), - new Context(false)); + new ExpressionTranslatorVisitor( + new TypeScriptAstFactory(options.annotateForClosureCompiler === true), + imports, + contextFile, + options, + ), + new Context(false), + ); } export function translateStatement( - contextFile: ts.SourceFile, statement: o.Statement, - imports: ImportGenerator, - options: TranslatorOptions = {}): ts.Statement { + contextFile: ts.SourceFile, + statement: o.Statement, + imports: ImportGenerator, + options: TranslatorOptions = {}, +): ts.Statement { return statement.visitStatement( - new ExpressionTranslatorVisitor( - new TypeScriptAstFactory(options.annotateForClosureCompiler === true), imports, - contextFile, options), - new Context(true)); + new ExpressionTranslatorVisitor( + new TypeScriptAstFactory(options.annotateForClosureCompiler === true), + imports, + contextFile, + options, + ), + new Context(true), + ); } diff --git a/packages/compiler-cli/src/ngtsc/translator/test/import_manager_spec.ts b/packages/compiler-cli/src/ngtsc/translator/test/import_manager_spec.ts index 7f87d1e58c324..be6816330a7d0 100644 --- a/packages/compiler-cli/src/ngtsc/translator/test/import_manager_spec.ts +++ b/packages/compiler-cli/src/ngtsc/translator/test/import_manager_spec.ts @@ -23,14 +23,14 @@ describe('import manager', () => { requestedFile: testFile, }); - const res = emit(manager, [ - ts.factory.createExpressionStatement(ref), - ]); + const res = emit(manager, [ts.factory.createExpressionStatement(ref)]); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import { input } from "@angular/core"; input; - `)); + `), + ); }); it('should be possible to import a namespace', () => { @@ -43,14 +43,14 @@ describe('import manager', () => { requestedFile: testFile, }); - const res = emit(manager, [ - ts.factory.createExpressionStatement(ref), - ]); + const res = emit(manager, [ts.factory.createExpressionStatement(ref)]); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import * as i0 from "@angular/core"; i0; - `)); + `), + ); }); it('should be possible to import multiple symbols', () => { @@ -74,11 +74,13 @@ describe('import manager', () => { ts.factory.createExpressionStatement(outputRef), ]); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import { input, output } from "@angular/core"; input; output; - `)); + `), + ); }); it('should be possible to import multiple namespaces', () => { @@ -102,12 +104,14 @@ describe('import manager', () => { ts.factory.createExpressionStatement(interopNamespace), ]); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import * as i0 from "@angular/core"; import * as i1 from "@angular/core/rxjs-interop"; i0; i1; - `)); + `), + ); }); it('should be possible to generate a namespace import and re-use it for future symbols', () => { @@ -131,11 +135,13 @@ describe('import manager', () => { ts.factory.createExpressionStatement(outputRef), ]); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import * as i0 from "@angular/core"; i0; i0.output; - `)); + `), + ); }); it('should always generate a new namespace import if there is only a named import', () => { @@ -159,12 +165,14 @@ describe('import manager', () => { ts.factory.createExpressionStatement(coreNamespace), ]); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import * as i0 from "@angular/core"; import { input } from "@angular/core"; input; i0; - `)); + `), + ); }); it('should be able to re-use existing source file namespace imports for symbols', () => { @@ -179,14 +187,14 @@ describe('import manager', () => { requestedFile: testFile, }); - const res = emit(manager, [ - ts.factory.createExpressionStatement(inputRef), - ]); + const res = emit(manager, [ts.factory.createExpressionStatement(inputRef)]); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import * as existingImport from '@angular/core'; existingImport.input; - `)); + `), + ); }); it('should re-use existing source file namespace imports for a namespace request', () => { @@ -201,14 +209,14 @@ describe('import manager', () => { requestedFile: testFile, }); - const res = emit(manager, [ - ts.factory.createExpressionStatement(coreRef), - ]); + const res = emit(manager, [ts.factory.createExpressionStatement(coreRef)]); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import * as existingImport from '@angular/core'; existingImport; - `)); + `), + ); }); it('should be able to re-use existing source named bindings', () => { @@ -223,14 +231,14 @@ describe('import manager', () => { requestedFile: testFile, }); - const res = emit(manager, [ - ts.factory.createExpressionStatement(inputRef), - ]); + const res = emit(manager, [ts.factory.createExpressionStatement(inputRef)]); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import { input } from '@angular/core'; input; - `)); + `), + ); }); it('should be able to add symbols to an existing source file named import', () => { @@ -247,40 +255,42 @@ describe('import manager', () => { requestedFile: testFile, }); - const res = emit(manager, [ - ts.factory.createExpressionStatement(outputRef), - ]); + const res = emit(manager, [ts.factory.createExpressionStatement(outputRef)]); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import { input, output } from '@angular/core'; output; const x = input(); - `)); + `), + ); }); - it('should be able to add symbols to an existing source file named import, ' + - 'while still eliding unused specifiers of the updated import', - () => { - const {testFile, emit} = createTestProgram(` + it( + 'should be able to add symbols to an existing source file named import, ' + + 'while still eliding unused specifiers of the updated import', + () => { + const {testFile, emit} = createTestProgram(` import {input} from '@angular/core'; `); - const manager = new ImportManager(); + const manager = new ImportManager(); - const outputRef = manager.addImport({ - exportModuleSpecifier: '@angular/core', - exportSymbolName: 'output', - requestedFile: testFile, - }); + const outputRef = manager.addImport({ + exportModuleSpecifier: '@angular/core', + exportSymbolName: 'output', + requestedFile: testFile, + }); - const res = emit(manager, [ - ts.factory.createExpressionStatement(outputRef), - ]); + const res = emit(manager, [ts.factory.createExpressionStatement(outputRef)]); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import { output } from '@angular/core'; output; - `)); - }); + `), + ); + }, + ); it('should not re-use an original file import if re-use is disabled', () => { const {testFile, emit} = createTestProgram(` @@ -296,14 +306,14 @@ describe('import manager', () => { requestedFile: testFile, }); - const res = emit(manager, [ - ts.factory.createExpressionStatement(outputRef), - ]); + const res = emit(manager, [ts.factory.createExpressionStatement(outputRef)]); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import { output } from "@angular/core"; output; - `)); + `), + ); }); it('should not re-use an original namespace import if re-use is disabled', () => { @@ -320,14 +330,14 @@ describe('import manager', () => { requestedFile: testFile, }); - const res = emit(manager, [ - ts.factory.createExpressionStatement(outputRef), - ]); + const res = emit(manager, [ts.factory.createExpressionStatement(outputRef)]); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import { output } from "@angular/core"; output; - `)); + `), + ); }); it('should be able to always prefer namespace imports for new imports', () => { @@ -353,75 +363,83 @@ describe('import manager', () => { ts.factory.createExpressionStatement(outputRef), ]); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import * as i0 from "@angular/core"; i0.input; i0.output; - `)); + `), + ); }); - it('should be able to always prefer namespace imports for new imports, ' + - 'but still re-use source file namespace imports', - () => { - const {testFile, emit} = createTestProgram(` + it( + 'should be able to always prefer namespace imports for new imports, ' + + 'but still re-use source file namespace imports', + () => { + const {testFile, emit} = createTestProgram(` import * as existingNamespace from '@angular/core'; `); - const manager = new ImportManager({ - forceGenerateNamespacesForNewImports: true, - }); - - const inputRef = manager.addImport({ - exportModuleSpecifier: '@angular/core', - exportSymbolName: 'input', - requestedFile: testFile, - }); - - const outputRef = manager.addImport({ - exportModuleSpecifier: '@angular/core', - exportSymbolName: 'output', - requestedFile: testFile, - }); - - const res = emit(manager, [ - ts.factory.createExpressionStatement(inputRef), - ts.factory.createExpressionStatement(outputRef), - ]); - - expect(res).toBe(omitLeadingWhitespace(` + const manager = new ImportManager({ + forceGenerateNamespacesForNewImports: true, + }); + + const inputRef = manager.addImport({ + exportModuleSpecifier: '@angular/core', + exportSymbolName: 'input', + requestedFile: testFile, + }); + + const outputRef = manager.addImport({ + exportModuleSpecifier: '@angular/core', + exportSymbolName: 'output', + requestedFile: testFile, + }); + + const res = emit(manager, [ + ts.factory.createExpressionStatement(inputRef), + ts.factory.createExpressionStatement(outputRef), + ]); + + expect(res).toBe( + omitLeadingWhitespace(` import * as existingNamespace from '@angular/core'; existingNamespace.input; existingNamespace.output; - `)); - }); - - it('should be able to always prefer namespace imports for new imports, ' + - 'but still re-use source file individual imports', - () => { - const {testFile, emit} = createTestProgram(` + `), + ); + }, + ); + + it( + 'should be able to always prefer namespace imports for new imports, ' + + 'but still re-use source file individual imports', + () => { + const {testFile, emit} = createTestProgram(` import {Dir} from 'bla'; const x = new Dir(); `); - const manager = new ImportManager({ - forceGenerateNamespacesForNewImports: true, - }); + const manager = new ImportManager({ + forceGenerateNamespacesForNewImports: true, + }); - const blaRef = manager.addImport({ - exportModuleSpecifier: 'bla', - exportSymbolName: 'Dir', - requestedFile: testFile, - }); + const blaRef = manager.addImport({ + exportModuleSpecifier: 'bla', + exportSymbolName: 'Dir', + requestedFile: testFile, + }); - const res = emit(manager, [ - ts.factory.createExpressionStatement(blaRef), - ]); + const res = emit(manager, [ts.factory.createExpressionStatement(blaRef)]); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import { Dir } from 'bla'; Dir; const x = new Dir(); - `)); - }); + `), + ); + }, + ); it('should not change existing unrelated imports', () => { const {testFile, emit} = createTestProgram(` @@ -437,16 +455,16 @@ describe('import manager', () => { requestedFile: testFile, }); - const res = emit(manager, [ - ts.factory.createExpressionStatement(inputRef), - ]); + const res = emit(manager, [ts.factory.createExpressionStatement(inputRef)]); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import { MyComp } from './bla'; import { input } from "@angular/core"; input; console.log(MyComp); - `)); + `), + ); }); it('should be able to add a side effect import', () => { @@ -457,9 +475,11 @@ describe('import manager', () => { const res = emit(manager, []); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import "@angular/core"; - `)); + `), + ); }); it('should avoid conflicts with existing top-level identifiers', () => { @@ -474,15 +494,15 @@ describe('import manager', () => { requestedFile: testFile, }); - const res = emit(manager, [ - ts.factory.createExpressionStatement(inputRef), - ]); + const res = emit(manager, [ts.factory.createExpressionStatement(inputRef)]); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import { input as input_1 } from "@angular/core"; input_1; const input = 1; - `)); + `), + ); }); it('should avoid conflicts with existing deep identifiers', () => { @@ -501,11 +521,10 @@ describe('import manager', () => { requestedFile: testFile, }); - const res = emit(manager, [ - ts.factory.createExpressionStatement(inputRef), - ]); + const res = emit(manager, [ts.factory.createExpressionStatement(inputRef)]); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import { input as input_1 } from "@angular/core"; input_1; function x() { @@ -513,7 +532,8 @@ describe('import manager', () => { const input = 1; }; } - `)); + `), + ); }); it('should avoid an import alias specifier if identifier is free to use', () => { @@ -526,14 +546,14 @@ describe('import manager', () => { requestedFile: testFile, }); - const res = emit(manager, [ - ts.factory.createExpressionStatement(inputRef), - ]); + const res = emit(manager, [ts.factory.createExpressionStatement(inputRef)]); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import { input } from "@angular/core"; input; - `)); + `), + ); }); it('should avoid collisions with generated identifiers', () => { @@ -557,12 +577,14 @@ describe('import manager', () => { ts.factory.createExpressionStatement(inputRef2), ]); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import { input } from "@angular/core"; import { input as input_1 } from "@angular/core2"; input; input_1; - `)); + `), + ); }); it('should avoid collisions with generated identifiers', () => { @@ -586,12 +608,14 @@ describe('import manager', () => { ts.factory.createExpressionStatement(inputRef2), ]); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import { input } from "@angular/core"; import { input as input_1 } from "@angular/core2"; input; input_1; - `)); + `), + ); }); it('should re-use previous similar generated imports', () => { @@ -616,11 +640,13 @@ describe('import manager', () => { ]); expect(inputRef).toBe(inputRef2); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import { input } from "@angular/core"; input; input; - `)); + `), + ); }); it('should not re-use original source file type-only imports', () => { @@ -634,14 +660,14 @@ describe('import manager', () => { exportSymbolName: 'bla', requestedFile: testFile, }); - const res = emit(manager, [ - ts.factory.createExpressionStatement(ref), - ]); + const res = emit(manager, [ts.factory.createExpressionStatement(ref)]); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import { bla } from "@angular/core"; bla; - `)); + `), + ); }); it('should not re-use original source file type-only import specifiers', () => { @@ -655,20 +681,20 @@ describe('import manager', () => { exportSymbolName: 'bla', requestedFile: testFile, }); - const res = emit(manager, [ - ts.factory.createExpressionStatement(ref), - ]); + const res = emit(manager, [ts.factory.createExpressionStatement(ref)]); - expect(res).toBe(omitLeadingWhitespace(` + expect(res).toBe( + omitLeadingWhitespace(` import { bla } from '@angular/core'; // existing. bla; - `)); + `), + ); }); }); function createTestProgram(text: string): { - testFile: ts.SourceFile, - emit: (manager: ImportManager, extraStatements: ts.Statement[]) => string, + testFile: ts.SourceFile; + emit: (manager: ImportManager, extraStatements: ts.Statement[]) => string; } { const fs = initMockFileSystem('Native'); const options: ts.CompilerOptions = { @@ -696,12 +722,18 @@ function createTestProgram(text: string): { const emit = (manager: ImportManager, newStatements: ts.Statement[]) => { const transformer = manager.toTsTransform(new Map([[testFile.fileName, newStatements]])); - let emitResult: string|null = null; - const {emitSkipped} = program.emit(testFile, (fileName, resultText) => { - if (fileName === '/test.js') { - emitResult = resultText; - } - }, undefined, undefined, {before: [transformer]}); + let emitResult: string | null = null; + const {emitSkipped} = program.emit( + testFile, + (fileName, resultText) => { + if (fileName === '/test.js') { + emitResult = resultText; + } + }, + undefined, + undefined, + {before: [transformer]}, + ); if (emitSkipped || emitResult === null) { throw new Error(`Unexpected emit failure when emitting test file.`); diff --git a/packages/compiler-cli/src/ngtsc/translator/test/typescript_ast_factory_spec.ts b/packages/compiler-cli/src/ngtsc/translator/test/typescript_ast_factory_spec.ts index fc411d1ae1ce7..2b88955976871 100644 --- a/packages/compiler-cli/src/ngtsc/translator/test/typescript_ast_factory_spec.ts +++ b/packages/compiler-cli/src/ngtsc/translator/test/typescript_ast_factory_spec.ts @@ -12,25 +12,29 @@ import {TypeScriptAstFactory} from '../src/typescript_ast_factory'; describe('TypeScriptAstFactory', () => { let factory: TypeScriptAstFactory; - beforeEach(() => factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false)); + beforeEach(() => (factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ false))); describe('attachComments()', () => { it('should add the comments to the given statement', () => { - const {items: [stmt], generate} = setupStatements('x = 10;'); - factory.attachComments( - stmt, [leadingComment('comment 1', true), leadingComment('comment 2', false)]); + const { + items: [stmt], + generate, + } = setupStatements('x = 10;'); + factory.attachComments(stmt, [ + leadingComment('comment 1', true), + leadingComment('comment 2', false), + ]); - expect(generate(stmt)).toEqual([ - '/* comment 1 */', - '//comment 2', - 'x = 10;', - ].join('\n')); + expect(generate(stmt)).toEqual(['/* comment 1 */', '//comment 2', 'x = 10;'].join('\n')); }); }); describe('createArrayLiteral()', () => { it('should create an array node containing the provided expressions', () => { - const {items: [expr1, expr2], generate} = setupExpressions(`42`, '"moo"'); + const { + items: [expr1, expr2], + generate, + } = setupExpressions(`42`, '"moo"'); const array = factory.createArrayLiteral([expr1, expr2]); expect(generate(array)).toEqual('[42, "moo"]'); }); @@ -38,7 +42,10 @@ describe('TypeScriptAstFactory', () => { describe('createAssignment()', () => { it('should create an assignment node using the target and value expressions', () => { - const {items: [target, value], generate} = setupExpressions(`x`, `42`); + const { + items: [target, value], + generate, + } = setupExpressions(`x`, `42`); const assignment = factory.createAssignment(target, value); expect(generate(assignment)).toEqual('x = 42'); }); @@ -46,7 +53,10 @@ describe('TypeScriptAstFactory', () => { describe('createBinaryExpression()', () => { it('should create a binary operation node using the left and right expressions', () => { - const {items: [left, right], generate} = setupExpressions(`17`, `42`); + const { + items: [left, right], + generate, + } = setupExpressions(`17`, `42`); const assignment = factory.createBinaryExpression(left, '+', right); expect(generate(assignment)).toEqual('17 + 42'); }); @@ -65,24 +75,25 @@ describe('TypeScriptAstFactory', () => { it('should create a block statement containing the given statements', () => { const {items: stmts, generate} = setupStatements('x = 10; y = 20;'); const block = factory.createBlock(stmts); - expect(generate(block)).toEqual([ - '{', - ' x = 10;', - ' y = 20;', - '}', - ].join('\n')); + expect(generate(block)).toEqual(['{', ' x = 10;', ' y = 20;', '}'].join('\n')); }); }); describe('createCallExpression()', () => { it('should create a call on the `callee` with the given `args`', () => { - const {items: [callee, arg1, arg2], generate} = setupExpressions('foo', '42', '"moo"'); + const { + items: [callee, arg1, arg2], + generate, + } = setupExpressions('foo', '42', '"moo"'); const call = factory.createCallExpression(callee, [arg1, arg2], false); expect(generate(call)).toEqual('foo(42, "moo")'); }); it('should create a call marked with a PURE comment if `pure` is true', () => { - const {items: [callee, arg1, arg2], generate} = setupExpressions(`foo`, `42`, `"moo"`); + const { + items: [callee, arg1, arg2], + generate, + } = setupExpressions(`foo`, `42`, `"moo"`); const call = factory.createCallExpression(callee, [arg1, arg2], true); expect(generate(call)).toEqual('/*@__PURE__*/ foo(42, "moo")'); }); @@ -90,7 +101,10 @@ describe('TypeScriptAstFactory', () => { it('should create a call marked with a closure-style pure comment if `pure` is true', () => { factory = new TypeScriptAstFactory(/* annotateForClosureCompiler */ true); - const {items: [callee, arg1, arg2], generate} = setupExpressions(`foo`, `42`, `"moo"`); + const { + items: [callee, arg1, arg2], + generate, + } = setupExpressions(`foo`, `42`, `"moo"`); const call = factory.createCallExpression(callee, [arg1, arg2], true); expect(generate(call)).toEqual('/** @pureOrBreakMyCode */ foo(42, "moo")'); }); @@ -98,8 +112,10 @@ describe('TypeScriptAstFactory', () => { describe('createConditional()', () => { it('should create a condition expression', () => { - const {items: [test, thenExpr, elseExpr], generate} = - setupExpressions(`!test`, `42`, `"moo"`); + const { + items: [test, thenExpr, elseExpr], + generate, + } = setupExpressions(`!test`, `42`, `"moo"`); const conditional = factory.createConditional(test, thenExpr, elseExpr); expect(generate(conditional)).toEqual('!test ? 42 : "moo"'); }); @@ -107,7 +123,10 @@ describe('TypeScriptAstFactory', () => { describe('createElementAccess()', () => { it('should create an expression accessing the element of an array/object', () => { - const {items: [expr, element], generate} = setupExpressions(`obj`, `"moo"`); + const { + items: [expr, element], + generate, + } = setupExpressions(`obj`, `"moo"`); const access = factory.createElementAccess(expr, element); expect(generate(access)).toEqual('obj["moo"]'); }); @@ -115,7 +134,10 @@ describe('TypeScriptAstFactory', () => { describe('createExpressionStatement()', () => { it('should create a statement node from the given expression', () => { - const {items: [expr], generate} = setupExpressions(`x = 10`); + const { + items: [expr], + generate, + } = setupExpressions(`x = 10`); const stmt = factory.createExpressionStatement(expr); expect(ts.isExpressionStatement(stmt)).toBe(true); expect(generate(stmt)).toEqual('x = 10;'); @@ -123,28 +145,32 @@ describe('TypeScriptAstFactory', () => { }); describe('createFunctionDeclaration()', () => { - it('should create a function declaration node with the given name, parameters and body statements', - () => { - const {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; }', - ); - }); + it('should create a function declaration node with the given name, parameters and body statements', () => { + const { + 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; }'); + }); }); describe('createFunctionExpression()', () => { - it('should create a function expression node with the given name, parameters and body statements', - () => { - const {items: [body], generate} = setupStatements('{x = 10; y = 20;}'); - const fn = factory.createFunctionExpression('foo', ['arg1', 'arg2'], body); - expect(ts.isExpressionStatement(fn)).toBe(false); - expect(generate(fn)).toEqual('function foo(arg1, arg2) { x = 10; y = 20; }'); - }); + it('should create a function expression node with the given name, parameters and body statements', () => { + const { + items: [body], + generate, + } = setupStatements('{x = 10; y = 20;}'); + const fn = factory.createFunctionExpression('foo', ['arg1', 'arg2'], body); + expect(ts.isExpressionStatement(fn)).toBe(false); + expect(generate(fn)).toEqual('function foo(arg1, arg2) { x = 10; y = 20; }'); + }); it('should create an anonymous function expression node if the name is null', () => { - const {items: [body], generate} = setupStatements('{x = 10; y = 20;}'); + const { + 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; }'); }); @@ -160,49 +186,55 @@ describe('TypeScriptAstFactory', () => { describe('createIfStatement()', () => { it('should create an if-else statement', () => { - const {items: [testStmt, thenStmt, elseStmt], generate} = - setupStatements('!test;x = 10;x = 42;'); + const { + items: [testStmt, thenStmt, elseStmt], + generate, + } = setupStatements('!test;x = 10;x = 42;'); const test = (testStmt as ts.ExpressionStatement).expression; const ifStmt = factory.createIfStatement(test, thenStmt, elseStmt); - expect(generate(ifStmt)).toEqual([ - 'if (!test)', - ' x = 10;', - 'else', - ' x = 42;', - ].join('\n')); + expect(generate(ifStmt)).toEqual( + ['if (!test)', ' x = 10;', 'else', ' x = 42;'].join('\n'), + ); }); it('should create an if statement if the else expression is null', () => { - const {items: [testStmt, thenStmt], generate} = setupStatements('!test;x = 10;'); + const { + items: [testStmt, thenStmt], + generate, + } = setupStatements('!test;x = 10;'); const test = (testStmt as ts.ExpressionStatement).expression; const ifStmt = factory.createIfStatement(test, thenStmt, null); - expect(generate(ifStmt)).toEqual([ - 'if (!test)', - ' x = 10;', - ].join('\n')); + expect(generate(ifStmt)).toEqual(['if (!test)', ' x = 10;'].join('\n')); }); }); describe('createArrowFunctionExpression()', () => { - it('should create an arrow function with an implicit return if a single statement is provided', - () => { - const {items: [body], generate} = setupExpressions('arg2 + arg1'); - const fn = factory.createArrowFunctionExpression(['arg1', 'arg2'], body); - expect(generate(fn)).toEqual('(arg1, arg2) => arg2 + arg1'); - }); + it('should create an arrow function with an implicit return if a single statement is provided', () => { + const { + items: [body], + generate, + } = setupExpressions('arg2 + arg1'); + const fn = factory.createArrowFunctionExpression(['arg1', 'arg2'], body); + expect(generate(fn)).toEqual('(arg1, arg2) => arg2 + arg1'); + }); it('should create an arrow function with an implicit return object literal', () => { - const {items: [body], generate} = setupExpressions('{a: 1, b: 2}'); + const { + items: [body], + generate, + } = setupExpressions('{a: 1, b: 2}'); const fn = factory.createArrowFunctionExpression([], body); expect(generate(fn)).toEqual('() => ({ a: 1, b: 2 })'); }); - it('should create an arrow function with a body when an array of statements is provided', - () => { - const {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; }'); - }); + it('should create an arrow function with a body when an array of statements is provided', () => { + const { + 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; }'); + }); }); describe('createLiteral()', () => { @@ -257,17 +289,22 @@ describe('TypeScriptAstFactory', () => { }); describe('createNewExpression()', () => { - it('should create a `new` operation on the constructor `expression` with the given `args`', - () => { - const {items: [expr, arg1, arg2], generate} = setupExpressions('Foo', '42', '"moo"'); - const call = factory.createNewExpression(expr, [arg1, arg2]); - expect(generate(call)).toEqual('new Foo(42, "moo")'); - }); + it('should create a `new` operation on the constructor `expression` with the given `args`', () => { + const { + items: [expr, arg1, arg2], + generate, + } = setupExpressions('Foo', '42', '"moo"'); + const call = factory.createNewExpression(expr, [arg1, arg2]); + expect(generate(call)).toEqual('new Foo(42, "moo")'); + }); }); describe('createObjectLiteral()', () => { it('should create an object literal node, with the given properties', () => { - const {items: [prop1, prop2], generate} = setupExpressions('42', '"moo"'); + const { + items: [prop1, prop2], + generate, + } = setupExpressions('42', '"moo"'); const obj = factory.createObjectLiteral([ {propertyName: 'prop1', value: prop1, quoted: false}, {propertyName: 'prop2', value: prop2, quoted: true}, @@ -278,7 +315,10 @@ describe('TypeScriptAstFactory', () => { describe('createParenthesizedExpression()', () => { it('should add parentheses around the given expression', () => { - const {items: [expr], generate} = setupExpressions(`a + b`); + const { + items: [expr], + generate, + } = setupExpressions(`a + b`); const paren = factory.createParenthesizedExpression(expr); expect(generate(paren)).toEqual('(a + b)'); }); @@ -286,7 +326,10 @@ describe('TypeScriptAstFactory', () => { describe('createPropertyAccess()', () => { it('should create a property access expression node', () => { - const {items: [expr], generate} = setupExpressions(`obj`); + const { + items: [expr], + generate, + } = setupExpressions(`obj`); const access = factory.createPropertyAccess(expr, 'moo'); expect(generate(access)).toEqual('obj.moo'); }); @@ -294,7 +337,10 @@ describe('TypeScriptAstFactory', () => { describe('createReturnStatement()', () => { it('should create a return statement returning the given expression', () => { - const {items: [expr], generate} = setupExpressions(`42`); + const { + items: [expr], + generate, + } = setupExpressions(`42`); const returnStmt = factory.createReturnStatement(expr); expect(generate(returnStmt)).toEqual('return 42;'); }); @@ -313,7 +359,10 @@ describe('TypeScriptAstFactory', () => { {raw: 'raw\\n2', cooked: 'raw\n2', range: null}, {raw: 'raw\\n3', cooked: 'raw\n3', range: null}, ]; - const {items: [tag, ...expressions], generate} = setupExpressions('tagFn', '42', '"moo"'); + const { + items: [tag, ...expressions], + generate, + } = setupExpressions('tagFn', '42', '"moo"'); const template = factory.createTaggedTemplate(tag, {elements, expressions}); expect(generate(template)).toEqual('tagFn `raw\\n1${42}raw\\n2${"moo"}raw\\n3`'); }); @@ -321,7 +370,10 @@ describe('TypeScriptAstFactory', () => { describe('createThrowStatement()', () => { it('should create a throw statement, throwing the given expression', () => { - const {items: [expr], generate} = setupExpressions(`new Error("bad")`); + const { + items: [expr], + generate, + } = setupExpressions(`new Error("bad")`); const throwStmt = factory.createThrowStatement(expr); expect(generate(throwStmt)).toEqual('throw new Error("bad");'); }); @@ -329,7 +381,10 @@ describe('TypeScriptAstFactory', () => { describe('createTypeOfExpression()', () => { it('should create a typeof expression node', () => { - const {items: [expr], generate} = setupExpressions(`42`); + const { + items: [expr], + generate, + } = setupExpressions(`42`); const typeofExpr = factory.createTypeOfExpression(expr); expect(generate(typeofExpr)).toEqual('typeof 42'); }); @@ -337,60 +392,74 @@ describe('TypeScriptAstFactory', () => { describe('createUnaryExpression()', () => { it('should create a unary expression with the operator and operand', () => { - const {items: [expr], generate} = setupExpressions(`value`); + const { + items: [expr], + generate, + } = setupExpressions(`value`); const unaryExpr = factory.createUnaryExpression('!', expr); expect(generate(unaryExpr)).toEqual('!value'); }); }); describe('createVariableDeclaration()', () => { - it('should create a variable declaration statement node for the given variable name and initializer', - () => { - const {items: [initializer], generate} = setupExpressions(`42`); - const varDecl = factory.createVariableDeclaration('foo', initializer, 'let'); - expect(generate(varDecl)).toEqual('let foo = 42;'); - }); - - it('should create a constant declaration statement node for the given variable name and initializer', - () => { - const {items: [initializer], generate} = setupExpressions(`42`); - const varDecl = factory.createVariableDeclaration('foo', initializer, 'const'); - expect(generate(varDecl)).toEqual('const foo = 42;'); - }); - - it('should create a downleveled declaration statement node for the given variable name and initializer', - () => { - const {items: [initializer], generate} = setupExpressions(`42`); - const varDecl = factory.createVariableDeclaration('foo', initializer, 'var'); - 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'); - expect(generate(varDecl)).toEqual('let foo;'); - }); + it('should create a variable declaration statement node for the given variable name and initializer', () => { + const { + items: [initializer], + generate, + } = setupExpressions(`42`); + const varDecl = factory.createVariableDeclaration('foo', initializer, 'let'); + expect(generate(varDecl)).toEqual('let foo = 42;'); + }); + + it('should create a constant declaration statement node for the given variable name and initializer', () => { + const { + items: [initializer], + generate, + } = setupExpressions(`42`); + const varDecl = factory.createVariableDeclaration('foo', initializer, 'const'); + expect(generate(varDecl)).toEqual('const foo = 42;'); + }); + + it('should create a downleveled declaration statement node for the given variable name and initializer', () => { + const { + items: [initializer], + generate, + } = setupExpressions(`42`); + const varDecl = factory.createVariableDeclaration('foo', initializer, 'var'); + 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'); + expect(generate(varDecl)).toEqual('let foo;'); + }); }); describe('setSourceMapRange()', () => { it('should attach the `sourceMapRange` to the given `node`', () => { - const {items: [expr]} = setupExpressions(`42`); + const { + items: [expr], + } = setupExpressions(`42`); factory.setSourceMapRange(expr, { start: {line: 0, column: 1, offset: 1}, end: {line: 2, column: 3, offset: 15}, content: '-****\n*****\n****', - url: 'original.ts' + url: 'original.ts', }); const range = ts.getSourceMapRange(expr); expect(range.pos).toEqual(1); expect(range.end).toEqual(15); - expect(range.source?.getLineAndCharacterOfPosition(range.pos)) - .toEqual({line: 0, character: 1}); - expect(range.source?.getLineAndCharacterOfPosition(range.end)) - .toEqual({line: 2, character: 3}); + expect(range.source?.getLineAndCharacterOfPosition(range.pos)).toEqual({ + line: 0, + character: 1, + }); + expect(range.source?.getLineAndCharacterOfPosition(range.end)).toEqual({ + line: 2, + character: 3, + }); }); }); }); @@ -423,9 +492,13 @@ function setupStatements(stmts: string = ''): SetupResult { * See `setupStatements()` for more information about this helper function. */ function setupExpressions(...exprs: string[]): SetupResult { - const {items: [arrayStmt], generate} = setupStatements(`[${exprs.join(',')}];`); + const { + items: [arrayStmt], + generate, + } = setupStatements(`[${exprs.join(',')}];`); const expressions = Array.from( - ((arrayStmt as ts.ExpressionStatement).expression as ts.ArrayLiteralExpression).elements); + ((arrayStmt as ts.ExpressionStatement).expression as ts.ArrayLiteralExpression).elements, + ); return {items: expressions, generate}; } diff --git a/packages/compiler-cli/src/ngtsc/tsc_plugin.ts b/packages/compiler-cli/src/ngtsc/tsc_plugin.ts index a7f70bc4833ac..dcad9789e6b2e 100644 --- a/packages/compiler-cli/src/ngtsc/tsc_plugin.ts +++ b/packages/compiler-cli/src/ngtsc/tsc_plugin.ts @@ -8,7 +8,13 @@ import ts from 'typescript'; -import {CompilationTicket, freshCompilationTicket, incrementalFromStateTicket, NgCompiler, NgCompilerHost} from './core'; +import { + CompilationTicket, + freshCompilationTicket, + incrementalFromStateTicket, + NgCompiler, + NgCompilerHost, +} from './core'; import {NgCompilerOptions, UnifiedModulesHost} from './core/api'; import {AbsoluteFsPath, NodeJSFileSystem, resolve, setFileSystem} from './file_system'; import {PatchedProgramIncrementalBuildStrategy} from './incremental'; @@ -41,12 +47,17 @@ interface TscPlugin { readonly name: string; wrapHost( - host: ts.CompilerHost&Partial, inputFiles: ReadonlyArray, - options: ts.CompilerOptions): PluginCompilerHost; - - setupCompilation(program: ts.Program, oldProgram?: ts.Program): { - ignoreForDiagnostics: Set, - ignoreForEmit: Set, + host: ts.CompilerHost & Partial, + inputFiles: ReadonlyArray, + options: ts.CompilerOptions, + ): PluginCompilerHost; + + setupCompilation( + program: ts.Program, + oldProgram?: ts.Program, + ): { + ignoreForDiagnostics: Set; + ignoreForEmit: Set; }; getDiagnostics(file?: ts.SourceFile): ts.Diagnostic[]; @@ -64,9 +75,9 @@ interface TscPlugin { export class NgTscPlugin implements TscPlugin { name = 'ngtsc'; - private options: NgCompilerOptions|null = null; - private host: NgCompilerHost|null = null; - private _compiler: NgCompiler|null = null; + private options: NgCompilerOptions | null = null; + private host: NgCompilerHost | null = null; + private _compiler: NgCompiler | null = null; get compiler(): NgCompiler { if (this._compiler === null) { @@ -80,8 +91,10 @@ export class NgTscPlugin implements TscPlugin { } wrapHost( - host: ts.CompilerHost&Partial, inputFiles: readonly string[], - options: ts.CompilerOptions): PluginCompilerHost { + host: ts.CompilerHost & Partial, + inputFiles: readonly string[], + options: ts.CompilerOptions, + ): PluginCompilerHost { // TODO(alxhub): Eventually the `wrapHost()` API will accept the old `ts.Program` (if one is // available). When it does, its `ts.SourceFile`s need to be re-tagged to enable proper // incremental compilation. @@ -90,9 +103,12 @@ export class NgTscPlugin implements TscPlugin { return this.host; } - setupCompilation(program: ts.Program, oldProgram?: ts.Program): { - ignoreForDiagnostics: Set, - ignoreForEmit: Set, + setupCompilation( + program: ts.Program, + oldProgram?: ts.Program, + ): { + ignoreForDiagnostics: Set; + ignoreForEmit: Set; } { // TODO(alxhub): we provide a `PerfRecorder` to the compiler, but because we're not driving the // compilation, the information captured within it is incomplete, and may not include timings @@ -107,7 +123,11 @@ export class NgTscPlugin implements TscPlugin { this.host.postProgramCreationCleanup(); untagAllTsFiles(program); const programDriver = new TsCreateProgramDriver( - program, this.host, this.options, this.host.shimExtensionPrefixes); + program, + this.host, + this.options, + this.host.shimExtensionPrefixes, + ); const strategy = new PatchedProgramIncrementalBuildStrategy(); const oldState = oldProgram !== undefined ? strategy.getIncrementalState(oldProgram) : null; let ticket: CompilationTicket; @@ -121,13 +141,28 @@ export class NgTscPlugin implements TscPlugin { if (oldProgram === undefined || oldState === null) { ticket = freshCompilationTicket( - program, this.options, strategy, programDriver, perfRecorder, - /* enableTemplateTypeChecker */ false, /* usePoisonedData */ false); + program, + this.options, + strategy, + programDriver, + perfRecorder, + /* enableTemplateTypeChecker */ false, + /* usePoisonedData */ false, + ); } else { strategy.toNextBuildStrategy().getIncrementalState(oldProgram); ticket = incrementalFromStateTicket( - oldProgram, oldState, program, this.options, strategy, programDriver, - modifiedResourceFiles, perfRecorder, false, false); + oldProgram, + oldState, + program, + this.options, + strategy, + programDriver, + modifiedResourceFiles, + perfRecorder, + false, + false, + ); } this._compiler = NgCompiler.fromTicket(ticket, this.host); return { diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts index d961efcba06ef..b0d6591765854 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/api.ts @@ -6,15 +6,26 @@ * found in the LICENSE file at https://angular.io/license */ -import {AbsoluteSourceSpan, BoundTarget, DirectiveMeta, ParseSourceSpan, SchemaMetadata} from '@angular/compiler'; +import { + AbsoluteSourceSpan, + BoundTarget, + DirectiveMeta, + ParseSourceSpan, + SchemaMetadata, +} from '@angular/compiler'; import ts from 'typescript'; import {ErrorCode} from '../../diagnostics'; import {Reference} from '../../imports'; -import {ClassPropertyMapping, DirectiveTypeCheckMeta, HostDirectiveMeta, InputMapping, PipeMeta} from '../../metadata'; +import { + ClassPropertyMapping, + DirectiveTypeCheckMeta, + HostDirectiveMeta, + InputMapping, + PipeMeta, +} from '../../metadata'; import {ClassDeclaration} from '../../reflection'; - /** * Extension of `DirectiveMeta` that includes additional information required to type-check the * usage of a particular directive. @@ -26,12 +37,12 @@ export interface TypeCheckableDirectiveMeta extends DirectiveMeta, DirectiveType outputs: ClassPropertyMapping; isStandalone: boolean; isSignal: boolean; - hostDirectives: HostDirectiveMeta[]|null; - decorator: ts.Decorator|null; + hostDirectives: HostDirectiveMeta[] | null; + decorator: ts.Decorator | null; isExplicitlyDeferred: boolean; } -export type TemplateId = string&{__brand: 'TemplateId'}; +export type TemplateId = string & {__brand: 'TemplateId'}; /** * A `ts.Diagnostic` with additional information about the diagnostic related to template @@ -52,7 +63,7 @@ export interface TemplateDiagnostic extends ts.Diagnostic { /** * A `TemplateDiagnostic` with a specific error code. */ -export type NgTemplateDiagnostic = TemplateDiagnostic&{__ngCode: T}; +export type NgTemplateDiagnostic = TemplateDiagnostic & {__ngCode: T}; /** * Metadata required in addition to a component class in order to generate a type check block (TCB) @@ -106,7 +117,7 @@ export interface TypeCtorMetadata { /** * Input, output, and query field names in the type which should be included as constructor input. */ - fields: {inputs: ClassPropertyMapping; queries: string[];}; + fields: {inputs: ClassPropertyMapping; queries: string[]}; /** * `Set` of field names which have type coercion enabled. @@ -211,7 +222,6 @@ export interface TypeCheckingConfig { */ checkTypeOfDomReferences: boolean; - /** * Whether to infer the type of local references. * @@ -282,7 +292,7 @@ export interface TypeCheckingConfig { /** * Whether to check if control flow syntax will prevent a node from being projected. */ - controlFlowPreventingContentProjection: 'error'|'warning'|'suppress'; + controlFlowPreventingContentProjection: 'error' | 'warning' | 'suppress'; /** * Whether to use any generic types of the context component. @@ -342,9 +352,10 @@ export interface TypeCheckingConfig { checkControlFlowBodies: boolean; } - export type TemplateSourceMapping = - DirectTemplateSourceMapping|IndirectTemplateSourceMapping|ExternalTemplateSourceMapping; + | DirectTemplateSourceMapping + | IndirectTemplateSourceMapping + | ExternalTemplateSourceMapping; /** * A mapping to an inline template in a TS file. @@ -354,7 +365,7 @@ export type TemplateSourceMapping = */ export interface DirectTemplateSourceMapping { type: 'direct'; - node: ts.StringLiteral|ts.NoSubstitutionTemplateLiteral; + node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral; } /** diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts index bccff7774fd39..2abc51d638b43 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts @@ -6,7 +6,19 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, LiteralPrimitive, ParseSourceSpan, PropertyRead, SafePropertyRead, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler'; +import { + AST, + LiteralPrimitive, + ParseSourceSpan, + PropertyRead, + SafePropertyRead, + TmplAstElement, + TmplAstNode, + TmplAstReference, + TmplAstTemplate, + TmplAstTextAttribute, + TmplAstVariable, +} from '@angular/compiler'; import ts from 'typescript'; import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system'; @@ -37,7 +49,7 @@ export interface TemplateTypeChecker { /** * Retrieve the template in use for the given component. */ - getTemplate(component: ts.ClassDeclaration): TmplAstNode[]|null; + getTemplate(component: ts.ClassDeclaration): TmplAstNode[] | null; /** * Get all `ts.Diagnostic`s currently available for the given `ts.SourceFile`. @@ -59,7 +71,7 @@ export interface TemplateTypeChecker { * Given a `shim` and position within the file, returns information for mapping back to a template * location. */ - getTemplateMappingAtTcbLocation(tcbLocation: TcbLocation): FullTemplateMapping|null; + getTemplateMappingAtTcbLocation(tcbLocation: TcbLocation): FullTemplateMapping | null; /** * Get all `ts.Diagnostic`s currently available that pertain to the given component. @@ -88,7 +100,7 @@ export interface TemplateTypeChecker { * * This method always runs in `OptimizeFor.SingleFile` mode. */ - getTypeCheckBlock(component: ts.ClassDeclaration): ts.Node|null; + getTypeCheckBlock(component: ts.ClassDeclaration): ts.Node | null; /** * Retrieves a `Symbol` for the node in a component's template. @@ -97,9 +109,9 @@ export interface TemplateTypeChecker { * * @see Symbol */ - getSymbolOfNode(node: TmplAstElement, component: ts.ClassDeclaration): ElementSymbol|null; - getSymbolOfNode(node: TmplAstTemplate, component: ts.ClassDeclaration): TemplateSymbol|null; - getSymbolOfNode(node: AST|TmplAstNode, component: ts.ClassDeclaration): Symbol|null; + getSymbolOfNode(node: TmplAstElement, component: ts.ClassDeclaration): ElementSymbol | null; + getSymbolOfNode(node: TmplAstTemplate, component: ts.ClassDeclaration): TemplateSymbol | null; + getSymbolOfNode(node: AST | TmplAstNode, component: ts.ClassDeclaration): Symbol | null; /** * Get "global" `Completion`s in the given context. @@ -111,16 +123,19 @@ export interface TemplateTypeChecker { * template variables which are in scope for that expression. */ getGlobalCompletions( - context: TmplAstTemplate|null, component: ts.ClassDeclaration, - node: AST|TmplAstNode): GlobalCompletion|null; - + context: TmplAstTemplate | null, + component: ts.ClassDeclaration, + node: AST | TmplAstNode, + ): GlobalCompletion | null; /** * For the given expression node, retrieve a `TcbLocation` that can be used to perform * autocompletion at that point in the expression, if such a location exists. */ getExpressionCompletionLocation( - expr: PropertyRead|SafePropertyRead, component: ts.ClassDeclaration): TcbLocation|null; + expr: PropertyRead | SafePropertyRead, + component: ts.ClassDeclaration, + ): TcbLocation | null; /** * For the given node represents a `LiteralPrimitive`(the `TextAttribute` represents a string @@ -128,8 +143,9 @@ export interface TemplateTypeChecker { * the node, if such a location exists. */ getLiteralCompletionLocation( - strNode: LiteralPrimitive|TmplAstTextAttribute, component: ts.ClassDeclaration): TcbLocation - |null; + strNode: LiteralPrimitive | TmplAstTextAttribute, + component: ts.ClassDeclaration, + ): TcbLocation | null; /** * Get basic metadata on the directives which are in scope or can be imported for the given @@ -147,26 +163,28 @@ export interface TemplateTypeChecker { * declares them (if the tag is from a directive/component), or `null` if the tag originates from * the DOM schema. */ - getPotentialElementTags(component: ts.ClassDeclaration): Map; + getPotentialElementTags(component: ts.ClassDeclaration): Map; /** * In the context of an Angular trait, generate potential imports for a directive. */ getPotentialImportsFor( - toImport: Reference, inComponent: ts.ClassDeclaration, - importMode: PotentialImportMode): ReadonlyArray; + toImport: Reference, + inComponent: ts.ClassDeclaration, + importMode: PotentialImportMode, + ): ReadonlyArray; /** * Get the primary decorator for an Angular class (such as @Component). This does not work for * `@Injectable`. */ - getPrimaryAngularDecorator(target: ts.ClassDeclaration): ts.Decorator|null; + getPrimaryAngularDecorator(target: ts.ClassDeclaration): ts.Decorator | null; /** * Get the class of the NgModule that owns this Angular trait. If the result is `null`, that * probably means the provided component is standalone. */ - getOwningNgModule(component: ts.ClassDeclaration): ts.ClassDeclaration|null; + getOwningNgModule(component: ts.ClassDeclaration): ts.ClassDeclaration | null; /** * Retrieve any potential DOM bindings for the given element. @@ -175,7 +193,7 @@ export interface TemplateTypeChecker { * binding, which are usually identical but can vary if the HTML attribute name is for example a * reserved keyword in JS, like the `for` attribute which corresponds to the `htmlFor` property. */ - getPotentialDomBindings(tagName: string): {attribute: string, property: string}[]; + getPotentialDomBindings(tagName: string): {attribute: string; property: string}[]; /** * Retrieve any potential DOM events. @@ -185,27 +203,27 @@ export interface TemplateTypeChecker { /** * Retrieve the type checking engine's metadata for the given directive class, if available. */ - getDirectiveMetadata(dir: ts.ClassDeclaration): TypeCheckableDirectiveMeta|null; + getDirectiveMetadata(dir: ts.ClassDeclaration): TypeCheckableDirectiveMeta | null; /** * Retrieve the type checking engine's metadata for the given NgModule class, if available. */ - getNgModuleMetadata(module: ts.ClassDeclaration): NgModuleMeta|null; + getNgModuleMetadata(module: ts.ClassDeclaration): NgModuleMeta | null; /** * Retrieve the type checking engine's metadata for the given pipe class, if available. */ - getPipeMetadata(pipe: ts.ClassDeclaration): PipeMeta|null; + getPipeMetadata(pipe: ts.ClassDeclaration): PipeMeta | null; /** * Gets the directives that have been used in a component's template. */ - getUsedDirectives(component: ts.ClassDeclaration): TypeCheckableDirectiveMeta[]|null; + getUsedDirectives(component: ts.ClassDeclaration): TypeCheckableDirectiveMeta[] | null; /** * Gets the pipes that have been used in a component's template. */ - getUsedPipes(component: ts.ClassDeclaration): string[]|null; + getUsedPipes(component: ts.ClassDeclaration): string[] | null; /** * Reset the `TemplateTypeChecker`'s state for the given class, so that it will be recomputed on @@ -217,20 +235,27 @@ export interface TemplateTypeChecker { * Gets the target of a template expression, if possible. * See `BoundTarget.getExpressionTarget` for more information. */ - getExpressionTarget(expression: AST, clazz: ts.ClassDeclaration): TmplAstReference|TmplAstVariable - |null; + getExpressionTarget( + expression: AST, + clazz: ts.ClassDeclaration, + ): TmplAstReference | TmplAstVariable | null; /** * Constructs a `ts.Diagnostic` for a given `ParseSourceSpan` within a template. */ makeTemplateDiagnostic( - clazz: ts.ClassDeclaration, sourceSpan: ParseSourceSpan, category: ts.DiagnosticCategory, - errorCode: T, message: string, relatedInformation?: { - text: string, - start: number, - end: number, - sourceFile: ts.SourceFile, - }[]): NgTemplateDiagnostic; + clazz: ts.ClassDeclaration, + sourceSpan: ParseSourceSpan, + category: ts.DiagnosticCategory, + errorCode: T, + message: string, + relatedInformation?: { + text: string; + start: number; + end: number; + sourceFile: ts.SourceFile; + }[], + ): NgTemplateDiagnostic; } /** diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/completion.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/completion.ts index 65e4e5332044e..3bd2caf39e205 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/completion.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/completion.ts @@ -13,7 +13,7 @@ import {TcbLocation} from './symbols'; /** * An autocompletion source of any kind. */ -export type Completion = ReferenceCompletion|VariableCompletion; +export type Completion = ReferenceCompletion | VariableCompletion; /** * Discriminant of an autocompletion source (a `Completion`). @@ -70,11 +70,11 @@ export interface GlobalCompletion { * accounted for in the preparation of `templateContext`. Entries here shadow component members of * the same name (from the `componentContext` completions). */ - templateContext: Map; + templateContext: Map; /** * A location within the type-checking shim where TypeScript's completion APIs can be used to * access completions for the AST node of the cursor position (primitive constants). */ - nodeContext: TcbLocation|null; + nodeContext: TcbLocation | null; } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/context.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/context.ts index 830d1e78f888f..38b67cb7f619f 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/context.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/context.ts @@ -6,7 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {ParseError, ParseSourceFile, R3TargetBinder, SchemaMetadata, TmplAstNode} from '@angular/compiler'; +import { + ParseError, + ParseSourceFile, + R3TargetBinder, + SchemaMetadata, + TmplAstNode, +} from '@angular/compiler'; import ts from 'typescript'; import {Reference} from '../../imports'; @@ -42,11 +48,17 @@ export interface TypeCheckContext { * whitespaces. */ addTemplate( - ref: Reference>, - binder: R3TargetBinder, template: TmplAstNode[], - pipes: Map, schemas: SchemaMetadata[], sourceMapping: TemplateSourceMapping, - file: ParseSourceFile, parseErrors: ParseError[]|null, isStandalone: boolean, - preserveWhitespaces: boolean): void; + ref: Reference>, + binder: R3TargetBinder, + template: TmplAstNode[], + pipes: Map, + schemas: SchemaMetadata[], + sourceMapping: TemplateSourceMapping, + file: ParseSourceFile, + parseErrors: ParseError[] | null, + isStandalone: boolean, + preserveWhitespaces: boolean, + ): void; } /** diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts index 5e6ea1bfb930a..96627d11ca9ea 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/scope.ts @@ -46,12 +46,12 @@ export interface PotentialDirective { /** * The module which declares the directive. */ - ngModule: ClassDeclaration|null; + ngModule: ClassDeclaration | null; /** * The selector for the directive or component. */ - selector: string|null; + selector: string | null; /** * `true` if this directive is a component. diff --git a/packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts b/packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts index 4384a46be3a0a..692a1714dec22 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/api/symbols.ts @@ -6,7 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {TmplAstElement, TmplAstReference, TmplAstTemplate, TmplAstVariable} from '@angular/compiler'; +import { + TmplAstElement, + TmplAstReference, + TmplAstTemplate, + TmplAstVariable, +} from '@angular/compiler'; import ts from 'typescript'; import {AbsoluteFsPath} from '../../file_system'; @@ -31,13 +36,22 @@ export enum SymbolKind { /** * A representation of an entity in the `TemplateAst`. */ -export type Symbol = InputBindingSymbol|OutputBindingSymbol|ElementSymbol|ReferenceSymbol| - VariableSymbol|ExpressionSymbol|DirectiveSymbol|TemplateSymbol|DomBindingSymbol|PipeSymbol; +export type Symbol = + | InputBindingSymbol + | OutputBindingSymbol + | ElementSymbol + | ReferenceSymbol + | VariableSymbol + | ExpressionSymbol + | DirectiveSymbol + | TemplateSymbol + | DomBindingSymbol + | PipeSymbol; /** * A `Symbol` which declares a new named entity in the template scope. */ -export type TemplateDeclarationSymbol = ReferenceSymbol|VariableSymbol; +export type TemplateDeclarationSymbol = ReferenceSymbol | VariableSymbol; /** * Information about where a `ts.Node` can be found in the type check file. This can either be @@ -67,7 +81,7 @@ export interface TsNodeSymbolInfo { tsType: ts.Type; /** The `ts.Symbol` for the template node */ - tsSymbol: ts.Symbol|null; + tsSymbol: ts.Symbol | null; /** The position of the most relevant part of the template node. */ tcbLocation: TcbLocation; @@ -86,7 +100,7 @@ export interface ExpressionSymbol { * The `ts.Symbol` of the entity. This could be `null`, for example `AST` expression * `{{foo.bar + foo.baz}}` does not have a `ts.Symbol` but `foo.bar` and `foo.baz` both do. */ - tsSymbol: ts.Symbol|null; + tsSymbol: ts.Symbol | null; /** The position of the most relevant part of the expression. */ tcbLocation: TcbLocation; @@ -106,7 +120,7 @@ export interface BindingSymbol { * The `DirectiveSymbol` or `ElementSymbol` for the Directive, Component, or `HTMLElement` with * the binding. */ - target: DirectiveSymbol|ElementSymbol|TemplateSymbol; + target: DirectiveSymbol | ElementSymbol | TemplateSymbol; /** The location in the shim file where the field access for the binding appears. */ tcbLocation: TcbLocation; @@ -162,7 +176,7 @@ export interface ReferenceSymbol { * - `TmplAstTemplate` when the ref refers to an `ng-template` * - `ts.ClassDeclaration` when the local ref refers to a Directive instance (#ref="myExportAs") */ - target: TmplAstElement|TmplAstTemplate|ts.ClassDeclaration; + target: TmplAstElement | TmplAstTemplate | ts.ClassDeclaration; /** * The node in the `TemplateAst` where the symbol is declared. That is, node for the `#ref` or @@ -208,7 +222,7 @@ export interface VariableSymbol { * * This will be `null` if there is no `ngTemplateContextGuard`. */ - tsSymbol: ts.Symbol|null; + tsSymbol: ts.Symbol | null; /** * The node in the `TemplateAst` where the variable is declared. That is, the node for the `let-` @@ -238,7 +252,7 @@ export interface ElementSymbol { tsType: ts.Type; /** The `ts.Symbol` for the `HTMLElement`. */ - tsSymbol: ts.Symbol|null; + tsSymbol: ts.Symbol | null; /** A list of directives applied to the element. */ directives: DirectiveSymbol[]; @@ -273,11 +287,13 @@ interface DirectiveSymbolBase extends PotentialDirective { * A representation of a directive/component whose selector matches a node in a component * template. */ -export type DirectiveSymbol = (DirectiveSymbolBase&{isHostDirective: false})|(DirectiveSymbolBase&{ - isHostDirective: true; - exposedInputs: Record|null; - exposedOutputs: Record|null; -}); +export type DirectiveSymbol = + | (DirectiveSymbolBase & {isHostDirective: false}) + | (DirectiveSymbolBase & { + isHostDirective: true; + exposedInputs: Record | null; + exposedOutputs: Record | null; + }); /** * A representation of an attribute on an element or template. These bindings aren't currently @@ -288,7 +304,7 @@ export interface DomBindingSymbol { kind: SymbolKind.DomBinding; /** The symbol for the element or template of the text attribute. */ - host: ElementSymbol|TemplateSymbol; + host: ElementSymbol | TemplateSymbol; } /** @@ -304,7 +320,7 @@ export interface PipeSymbol { * The `ts.Symbol` for the transform call. This could be `null` when `checkTypeOfPipes` is set to * `false` because the transform call would be of the form `(_pipe1 as any).transform()` */ - tsSymbol: ts.Symbol|null; + tsSymbol: ts.Symbol | null; /** The position of the transform call in the template. */ tcbLocation: TcbLocation; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/diagnostics/src/diagnostic.ts b/packages/compiler-cli/src/ngtsc/typecheck/diagnostics/src/diagnostic.ts index 04059193bccd0..885756693549e 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/diagnostics/src/diagnostic.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/diagnostics/src/diagnostic.ts @@ -10,22 +10,33 @@ import {ParseSourceSpan} from '@angular/compiler'; import ts from 'typescript'; import {addDiagnosticChain, makeDiagnosticChain} from '../../../diagnostics'; -import {ExternalTemplateSourceMapping, IndirectTemplateSourceMapping, TemplateDiagnostic, TemplateId, TemplateSourceMapping} from '../../api'; +import { + ExternalTemplateSourceMapping, + IndirectTemplateSourceMapping, + TemplateDiagnostic, + TemplateId, + TemplateSourceMapping, +} from '../../api'; /** * Constructs a `ts.Diagnostic` for a given `ParseSourceSpan` within a template. */ export function makeTemplateDiagnostic( - templateId: TemplateId, mapping: TemplateSourceMapping, span: ParseSourceSpan, - category: ts.DiagnosticCategory, code: number, messageText: string|ts.DiagnosticMessageChain, - relatedMessages?: { - text: string, - start: number, - end: number, - sourceFile: ts.SourceFile, - }[]): TemplateDiagnostic { + templateId: TemplateId, + mapping: TemplateSourceMapping, + span: ParseSourceSpan, + category: ts.DiagnosticCategory, + code: number, + messageText: string | ts.DiagnosticMessageChain, + relatedMessages?: { + text: string; + start: number; + end: number; + sourceFile: ts.SourceFile; + }[], +): TemplateDiagnostic { if (mapping.type === 'direct') { - let relatedInformation: ts.DiagnosticRelatedInformation[]|undefined = undefined; + let relatedInformation: ts.DiagnosticRelatedInformation[] | undefined = undefined; if (relatedMessages !== undefined) { relatedInformation = []; for (const relatedMessage of relatedMessages) { @@ -61,9 +72,10 @@ export function makeTemplateDiagnostic( // For external temoplates, the HTML filename is used. const componentSf = mapping.componentClass.getSourceFile(); const componentName = mapping.componentClass.name.text; - const fileName = mapping.type === 'indirect' ? - `${componentSf.fileName} (${componentName} template)` : - mapping.templateUrl; + const fileName = + mapping.type === 'indirect' + ? `${componentSf.fileName} (${componentName} template)` + : mapping.templateUrl; let relatedInformation: ts.DiagnosticRelatedInformation[] = []; if (relatedMessages !== undefined) { @@ -84,11 +96,11 @@ export function makeTemplateDiagnostic( sf = getParsedTemplateSourceFile(fileName, mapping); } catch (e) { const failureChain = makeDiagnosticChain( - `Failed to report an error in '${fileName}' at ${span.start.line + 1}:${ - span.start.col + 1}`, - [ - makeDiagnosticChain((e as Error)?.stack ?? `${e}`), - ]); + `Failed to report an error in '${fileName}' at ${span.start.line + 1}:${ + span.start.col + 1 + }`, + [makeDiagnosticChain((e as Error)?.stack ?? `${e}`)], + ); return { source: 'ngtsc', category, @@ -136,13 +148,17 @@ export function makeTemplateDiagnostic( const TemplateSourceFile = Symbol('TemplateSourceFile'); -type TemplateSourceMappingWithSourceFile = - (ExternalTemplateSourceMapping|IndirectTemplateSourceMapping)&{ +type TemplateSourceMappingWithSourceFile = ( + | ExternalTemplateSourceMapping + | IndirectTemplateSourceMapping +) & { [TemplateSourceFile]?: ts.SourceFile; }; function getParsedTemplateSourceFile( - fileName: string, mapping: TemplateSourceMappingWithSourceFile): ts.SourceFile { + fileName: string, + mapping: TemplateSourceMappingWithSourceFile, +): ts.SourceFile { if (mapping[TemplateSourceFile] === undefined) { mapping[TemplateSourceFile] = parseTemplateAsSourceFile(fileName, mapping.template); } @@ -150,7 +166,7 @@ function getParsedTemplateSourceFile( return mapping[TemplateSourceFile]; } -let parseTemplateAsSourceFileForTest: typeof parseTemplateAsSourceFile|null = null; +let parseTemplateAsSourceFileForTest: typeof parseTemplateAsSourceFile | null = null; export function setParseTemplateAsSourceFileForTest(fn: typeof parseTemplateAsSourceFile): void { parseTemplateAsSourceFileForTest = fn; @@ -168,10 +184,16 @@ function parseTemplateAsSourceFile(fileName: string, template: string): ts.Sourc // TODO(alxhub): investigate creating a fake `ts.SourceFile` here instead of invoking the TS // parser against the template (HTML is just really syntactically invalid TypeScript code ;). return ts.createSourceFile( - fileName, template, ts.ScriptTarget.Latest, /* setParentNodes */ false, ts.ScriptKind.JSX); + fileName, + template, + ts.ScriptTarget.Latest, + /* setParentNodes */ false, + ts.ScriptKind.JSX, + ); } export function isTemplateDiagnostic(diagnostic: ts.Diagnostic): diagnostic is TemplateDiagnostic { - return diagnostic.hasOwnProperty('componentFile') && - ts.isSourceFile((diagnostic as any).componentFile); + return ( + diagnostic.hasOwnProperty('componentFile') && ts.isSourceFile((diagnostic as any).componentFile) + ); } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/diagnostics/src/id.ts b/packages/compiler-cli/src/ngtsc/typecheck/diagnostics/src/id.ts index f2b8d630f9522..5aee837fb4e4c 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/diagnostics/src/id.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/diagnostics/src/id.ts @@ -11,7 +11,6 @@ import {DeclarationNode} from '../../../reflection'; import {TemplateId} from '../../api'; - const TEMPLATE_ID = Symbol('ngTemplateId'); const NEXT_TEMPLATE_ID = Symbol('ngNextTemplateId'); @@ -31,9 +30,9 @@ export function getTemplateId(clazz: DeclarationNode): TemplateId { return node[TEMPLATE_ID]!; } -function allocateTemplateId(sf: ts.SourceFile&Partial): TemplateId { +function allocateTemplateId(sf: ts.SourceFile & Partial): TemplateId { if (sf[NEXT_TEMPLATE_ID] === undefined) { sf[NEXT_TEMPLATE_ID] = 1; } - return (`tcb${sf[NEXT_TEMPLATE_ID]!++}`) as TemplateId; + return `tcb${sf[NEXT_TEMPLATE_ID]!++}` as TemplateId; } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/api/api.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/api/api.ts index 545477b88baa4..37bf8131e4bc2 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/api/api.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/api/api.ts @@ -6,7 +6,38 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, ASTWithSource, ParseSourceSpan, RecursiveAstVisitor, TmplAstBoundAttribute, TmplAstBoundDeferredTrigger, TmplAstBoundEvent, TmplAstBoundText, TmplAstContent, TmplAstDeferredBlock, TmplAstDeferredBlockError, TmplAstDeferredBlockLoading, TmplAstDeferredBlockPlaceholder, TmplAstDeferredTrigger, TmplAstElement, TmplAstForLoopBlock, TmplAstForLoopBlockEmpty, TmplAstIcu, TmplAstIfBlock, TmplAstIfBlockBranch, TmplAstNode, TmplAstRecursiveVisitor, TmplAstReference, TmplAstSwitchBlock, TmplAstSwitchBlockCase, TmplAstTemplate, TmplAstText, TmplAstTextAttribute, TmplAstUnknownBlock, TmplAstVariable} from '@angular/compiler'; +import { + AST, + ASTWithSource, + ParseSourceSpan, + RecursiveAstVisitor, + TmplAstBoundAttribute, + TmplAstBoundDeferredTrigger, + TmplAstBoundEvent, + TmplAstBoundText, + TmplAstContent, + TmplAstDeferredBlock, + TmplAstDeferredBlockError, + TmplAstDeferredBlockLoading, + TmplAstDeferredBlockPlaceholder, + TmplAstDeferredTrigger, + TmplAstElement, + TmplAstForLoopBlock, + TmplAstForLoopBlockEmpty, + TmplAstIcu, + TmplAstIfBlock, + TmplAstIfBlockBranch, + TmplAstNode, + TmplAstRecursiveVisitor, + TmplAstReference, + TmplAstSwitchBlock, + TmplAstSwitchBlockCase, + TmplAstTemplate, + TmplAstText, + TmplAstTextAttribute, + TmplAstUnknownBlock, + TmplAstVariable, +} from '@angular/compiler'; import ts from 'typescript'; import {NgCompilerOptions} from '../../../core/api'; @@ -22,8 +53,11 @@ export interface TemplateCheck { code: Code; /** Runs check and returns information about the diagnostics to be generated. */ - run(ctx: TemplateContext, component: ts.ClassDeclaration, - template: TmplAstNode[]): NgTemplateDiagnostic[]; + run( + ctx: TemplateContext, + component: ts.ClassDeclaration, + template: TmplAstNode[], + ): NgTemplateDiagnostic[]; } /** @@ -43,12 +77,16 @@ export interface TemplateContext { * Creates a template diagnostic with the given information for the template being processed and * using the diagnostic category configured for the extended template diagnostic. */ - makeTemplateDiagnostic(sourceSpan: ParseSourceSpan, message: string, relatedInformation?: { - text: string, - start: number, - end: number, - sourceFile: ts.SourceFile, - }[]): NgTemplateDiagnostic; + makeTemplateDiagnostic( + sourceSpan: ParseSourceSpan, + message: string, + relatedInformation?: { + text: string; + start: number; + end: number; + sourceFile: ts.SourceFile; + }[], + ): NgTemplateDiagnostic; } /** @@ -61,22 +99,26 @@ export interface TemplateCheckFactory< > { code: Code; name: Name; - create(options: NgCompilerOptions): TemplateCheck|null; + create(options: NgCompilerOptions): TemplateCheck | null; } /** * This abstract class provides a base implementation for the run method. */ -export abstract class TemplateCheckWithVisitor implements - TemplateCheck { +export abstract class TemplateCheckWithVisitor + implements TemplateCheck +{ abstract code: Code; /** * Base implementation for run function, visits all nodes in template and calls * `visitNode()` for each one. */ - run(ctx: TemplateContext, component: ts.ClassDeclaration, - template: TmplAstNode[]): NgTemplateDiagnostic[] { + run( + ctx: TemplateContext, + component: ts.ClassDeclaration, + template: TmplAstNode[], + ): NgTemplateDiagnostic[] { const visitor = new TemplateVisitor(ctx, component, this); return visitor.getDiagnostics(template); } @@ -86,24 +128,30 @@ export abstract class TemplateCheckWithVisitor implement * method to implement the check and return diagnostics. */ abstract visitNode( - ctx: TemplateContext, component: ts.ClassDeclaration, - node: TmplAstNode|AST): NgTemplateDiagnostic[]; + ctx: TemplateContext, + component: ts.ClassDeclaration, + node: TmplAstNode | AST, + ): NgTemplateDiagnostic[]; } /** * Visits all nodes in a template (TmplAstNode and AST) and calls `visitNode` for each one. */ -class TemplateVisitor extends RecursiveAstVisitor implements - TmplAstRecursiveVisitor { +class TemplateVisitor + extends RecursiveAstVisitor + implements TmplAstRecursiveVisitor +{ diagnostics: NgTemplateDiagnostic[] = []; constructor( - private readonly ctx: TemplateContext, private readonly component: ts.ClassDeclaration, - private readonly check: TemplateCheckWithVisitor) { + private readonly ctx: TemplateContext, + private readonly component: ts.ClassDeclaration, + private readonly check: TemplateCheckWithVisitor, + ) { super(); } - override visit(node: AST|TmplAstNode, context?: any) { + override visit(node: AST | TmplAstNode, context?: any) { this.diagnostics.push(...this.check.visitNode(this.ctx, this.component, node)); node.visit(this); } @@ -162,7 +210,6 @@ class TemplateVisitor extends RecursiveAstVisitor implem } visitIcu(icu: TmplAstIcu): void {} - visitDeferredBlock(deferred: TmplAstDeferredBlock): void { deferred.visitAll(this); } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/interpolated_signal_not_invoked/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/interpolated_signal_not_invoked/index.ts index ffa464b92dc4b..b78746c4d6e30 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/interpolated_signal_not_invoked/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/interpolated_signal_not_invoked/index.ts @@ -14,7 +14,6 @@ import {NgTemplateDiagnostic, SymbolKind} from '../../../api'; import {isSignalReference} from '../../../src/symbol_util'; import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '../../api'; - /** Names of known signal instance properties. */ const SIGNAL_INSTANCE_PROPERTIES = new Set(['set', 'update', 'asReadonly']); @@ -27,17 +26,18 @@ const FUNCTION_INSTANCE_PROPERTIES = new Set(['name', 'length', 'prototype']); /** * Ensures Signals are invoked when used in template interpolations. */ -class InterpolatedSignalCheck extends - TemplateCheckWithVisitor { +class InterpolatedSignalCheck extends TemplateCheckWithVisitor { override code = ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED as const; override visitNode( - ctx: TemplateContext, - component: ts.ClassDeclaration, - node: TmplAstNode|AST): NgTemplateDiagnostic[] { + ctx: TemplateContext, + component: ts.ClassDeclaration, + node: TmplAstNode | AST, + ): NgTemplateDiagnostic[] { if (node instanceof Interpolation) { - return node.expressions.filter((item): item is PropertyRead => item instanceof PropertyRead) - .flatMap(item => buildDiagnosticForSignal(ctx, item, component)); + return node.expressions + .filter((item): item is PropertyRead => item instanceof PropertyRead) + .flatMap((item) => buildDiagnosticForSignal(ctx, item, component)); } return []; } @@ -52,14 +52,16 @@ function isSignalInstanceProperty(name: string): boolean { } function buildDiagnosticForSignal( - ctx: TemplateContext, node: PropertyRead, - component: ts.ClassDeclaration): - Array> { + ctx: TemplateContext, + node: PropertyRead, + component: ts.ClassDeclaration, +): Array> { // check for `{{ mySignal }}` const symbol = ctx.templateTypeChecker.getSymbolOfNode(node, component); if (symbol !== null && symbol.kind === SymbolKind.Expression && isSignalReference(symbol)) { - const templateMapping = - ctx.templateTypeChecker.getTemplateMappingAtTcbLocation(symbol.tcbLocation)!; + const templateMapping = ctx.templateTypeChecker.getTemplateMappingAtTcbLocation( + symbol.tcbLocation, + )!; const errorString = `${node.name} is a function and should be invoked: ${node.name}()`; const diagnostic = ctx.makeTemplateDiagnostic(templateMapping.span, errorString); return [diagnostic]; @@ -71,15 +73,19 @@ function buildDiagnosticForSignal( // We also check for `{{ mySignal.set }}` or `{{ mySignal.update }}` or // `{{ mySignal.asReadonly }}` as these are the names of instance properties of Signal const symbolOfReceiver = ctx.templateTypeChecker.getSymbolOfNode(node.receiver, component); - if ((isFunctionInstanceProperty(node.name) || isSignalInstanceProperty(node.name)) && - symbolOfReceiver !== null && symbolOfReceiver.kind === SymbolKind.Expression && - isSignalReference(symbolOfReceiver)) { - const templateMapping = - ctx.templateTypeChecker.getTemplateMappingAtTcbLocation(symbolOfReceiver.tcbLocation)!; + if ( + (isFunctionInstanceProperty(node.name) || isSignalInstanceProperty(node.name)) && + symbolOfReceiver !== null && + symbolOfReceiver.kind === SymbolKind.Expression && + isSignalReference(symbolOfReceiver) + ) { + const templateMapping = ctx.templateTypeChecker.getTemplateMappingAtTcbLocation( + symbolOfReceiver.tcbLocation, + )!; - const errorString = - `${(node.receiver as PropertyRead).name} is a function and should be invoked: ${ - (node.receiver as PropertyRead).name}()`; + const errorString = `${(node.receiver as PropertyRead).name} is a function and should be invoked: ${ + (node.receiver as PropertyRead).name + }()`; const diagnostic = ctx.makeTemplateDiagnostic(templateMapping.span, errorString); return [diagnostic]; } @@ -88,8 +94,9 @@ function buildDiagnosticForSignal( } export const factory: TemplateCheckFactory< - ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED, - ExtendedTemplateDiagnosticName.INTERPOLATED_SIGNAL_NOT_INVOKED> = { + ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED, + ExtendedTemplateDiagnosticName.INTERPOLATED_SIGNAL_NOT_INVOKED +> = { code: ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED, name: ExtendedTemplateDiagnosticName.INTERPOLATED_SIGNAL_NOT_INVOKED, create: () => new InterpolatedSignalCheck(), diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/invalid_banana_in_box/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/invalid_banana_in_box/index.ts index df6c0964e22b5..10fbe63030a1d 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/invalid_banana_in_box/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/invalid_banana_in_box/index.ts @@ -22,10 +22,10 @@ class InvalidBananaInBoxCheck extends TemplateCheckWithVisitor, - component: ts.ClassDeclaration, - node: TmplAstNode|AST, - ): NgTemplateDiagnostic[] { + ctx: TemplateContext, + component: ts.ClassDeclaration, + node: TmplAstNode | AST, + ): NgTemplateDiagnostic[] { if (!(node instanceof TmplAstBoundEvent)) return []; const name = node.name; @@ -34,16 +34,18 @@ class InvalidBananaInBoxCheck extends TemplateCheckWithVisitor = { + ErrorCode.INVALID_BANANA_IN_BOX, + ExtendedTemplateDiagnosticName.INVALID_BANANA_IN_BOX +> = { code: ErrorCode.INVALID_BANANA_IN_BOX, name: ExtendedTemplateDiagnosticName.INVALID_BANANA_IN_BOX, create: () => new InvalidBananaInBoxCheck(), diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_control_flow_directive/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_control_flow_directive/index.ts index e403005c31d74..f23b665249611 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_control_flow_directive/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_control_flow_directive/index.ts @@ -24,9 +24,10 @@ import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '. * `CommonModule` is included, the `ngSwitch` would also be covered. */ export const KNOWN_CONTROL_FLOW_DIRECTIVES = new Map([ - ['ngIf', {directive: 'NgIf', builtIn: '@if'}], ['ngFor', {directive: 'NgFor', builtIn: '@for'}], + ['ngIf', {directive: 'NgIf', builtIn: '@if'}], + ['ngFor', {directive: 'NgFor', builtIn: '@for'}], ['ngSwitchCase', {directive: 'NgSwitchCase', builtIn: '@switch with @case'}], - ['ngSwitchDefault', {directive: 'NgSwitchDefault', builtIn: '@switch with @default'}] + ['ngSwitchDefault', {directive: 'NgSwitchDefault', builtIn: '@switch with @default'}], ]); /** @@ -38,13 +39,14 @@ export const KNOWN_CONTROL_FLOW_DIRECTIVES = new Map([ * Regular binding syntax (e.g. `[ngIf]`) is handled separately in type checker and treated as a * hard error instead of a warning. */ -class MissingControlFlowDirectiveCheck extends - TemplateCheckWithVisitor { +class MissingControlFlowDirectiveCheck extends TemplateCheckWithVisitor { override code = ErrorCode.MISSING_CONTROL_FLOW_DIRECTIVE as const; override run( - ctx: TemplateContext, - component: ts.ClassDeclaration, template: TmplAstNode[]) { + ctx: TemplateContext, + component: ts.ClassDeclaration, + template: TmplAstNode[], + ) { const componentMetadata = ctx.templateTypeChecker.getDirectiveMetadata(component); // Avoid running this check for non-standalone components. if (!componentMetadata || !componentMetadata.isStandalone) { @@ -54,13 +56,15 @@ class MissingControlFlowDirectiveCheck extends } override visitNode( - ctx: TemplateContext, - component: ts.ClassDeclaration, - node: TmplAstNode|AST): NgTemplateDiagnostic[] { + ctx: TemplateContext, + component: ts.ClassDeclaration, + node: TmplAstNode | AST, + ): NgTemplateDiagnostic[] { if (!(node instanceof TmplAstTemplate)) return []; - const controlFlowAttr = - node.templateAttrs.find(attr => KNOWN_CONTROL_FLOW_DIRECTIVES.has(attr.name)); + const controlFlowAttr = node.templateAttrs.find((attr) => + KNOWN_CONTROL_FLOW_DIRECTIVES.has(attr.name), + ); if (!controlFlowAttr) return []; const symbol = ctx.templateTypeChecker.getSymbolOfNode(node, component); @@ -71,21 +75,20 @@ class MissingControlFlowDirectiveCheck extends const sourceSpan = controlFlowAttr.keySpan || controlFlowAttr.sourceSpan; const directiveAndBuiltIn = KNOWN_CONTROL_FLOW_DIRECTIVES.get(controlFlowAttr.name); const errorMessage = - `The \`*${controlFlowAttr.name}\` directive was used in the template, ` + - `but neither the \`${ - directiveAndBuiltIn?.directive}\` directive nor the \`CommonModule\` was imported. ` + - `Use Angular's built-in control flow ${directiveAndBuiltIn?.builtIn} or ` + - `make sure that either the \`${ - directiveAndBuiltIn?.directive}\` directive or the \`CommonModule\` ` + - `is included in the \`@Component.imports\` array of this component.`; + `The \`*${controlFlowAttr.name}\` directive was used in the template, ` + + `but neither the \`${directiveAndBuiltIn?.directive}\` directive nor the \`CommonModule\` was imported. ` + + `Use Angular's built-in control flow ${directiveAndBuiltIn?.builtIn} or ` + + `make sure that either the \`${directiveAndBuiltIn?.directive}\` directive or the \`CommonModule\` ` + + `is included in the \`@Component.imports\` array of this component.`; const diagnostic = ctx.makeTemplateDiagnostic(sourceSpan, errorMessage); return [diagnostic]; } } export const factory: TemplateCheckFactory< - ErrorCode.MISSING_CONTROL_FLOW_DIRECTIVE, - ExtendedTemplateDiagnosticName.MISSING_CONTROL_FLOW_DIRECTIVE> = { + ErrorCode.MISSING_CONTROL_FLOW_DIRECTIVE, + ExtendedTemplateDiagnosticName.MISSING_CONTROL_FLOW_DIRECTIVE +> = { code: ErrorCode.MISSING_CONTROL_FLOW_DIRECTIVE, name: ExtendedTemplateDiagnosticName.MISSING_CONTROL_FLOW_DIRECTIVE, create: (options: NgCompilerOptions) => { diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_ngforof_let/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_ngforof_let/index.ts index 51d84d76d376e..a468bc7324326 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_ngforof_let/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/missing_ngforof_let/index.ts @@ -21,8 +21,10 @@ class MissingNgForOfLetCheck extends TemplateCheckWithVisitor, component: ts.ClassDeclaration, - node: TmplAstNode|AST): NgTemplateDiagnostic[] { + ctx: TemplateContext, + component: ts.ClassDeclaration, + node: TmplAstNode | AST, + ): NgTemplateDiagnostic[] { const isTemplate = node instanceof TmplAstTemplate; if (!(node instanceof TmplAstTemplate)) { return []; @@ -31,7 +33,7 @@ class MissingNgForOfLetCheck extends TemplateCheckWithVisitor x.name === 'ngFor'); + const attr = node.templateAttrs.find((x) => x.name === 'ngFor'); if (attr === undefined) { return []; } @@ -46,7 +48,9 @@ class MissingNgForOfLetCheck extends TemplateCheckWithVisitor = { + ErrorCode.MISSING_NGFOROF_LET, + ExtendedTemplateDiagnosticName.MISSING_NGFOROF_LET +> = { code: ErrorCode.MISSING_NGFOROF_LET, name: ExtendedTemplateDiagnosticName.MISSING_NGFOROF_LET, create: () => new MissingNgForOfLetCheck(), diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/nullish_coalescing_not_nullable/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/nullish_coalescing_not_nullable/index.ts index d122b2764cdbf..0393c986bfbea 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/nullish_coalescing_not_nullable/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/nullish_coalescing_not_nullable/index.ts @@ -20,14 +20,14 @@ import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '. * This check should only be use if `strictNullChecks` is enabled, * otherwise it would produce inaccurate results. */ -class NullishCoalescingNotNullableCheck extends - TemplateCheckWithVisitor { +class NullishCoalescingNotNullableCheck extends TemplateCheckWithVisitor { override code = ErrorCode.NULLISH_COALESCING_NOT_NULLABLE as const; override visitNode( - ctx: TemplateContext, - component: ts.ClassDeclaration, - node: TmplAstNode|AST): NgTemplateDiagnostic[] { + ctx: TemplateContext, + component: ts.ClassDeclaration, + node: TmplAstNode | AST, + ): NgTemplateDiagnostic[] { if (!(node instanceof Binary) || node.operation !== '??') return []; const symbolLeft = ctx.templateTypeChecker.getSymbolOfNode(node.left, component); @@ -50,27 +50,30 @@ class NullishCoalescingNotNullableCheck extends if (symbol.kind !== SymbolKind.Expression) { return []; } - const templateMapping = - ctx.templateTypeChecker.getTemplateMappingAtTcbLocation(symbol.tcbLocation); + const templateMapping = ctx.templateTypeChecker.getTemplateMappingAtTcbLocation( + symbol.tcbLocation, + ); if (templateMapping === null) { return []; } const diagnostic = ctx.makeTemplateDiagnostic( - templateMapping.span, - `The left side of this nullish coalescing operation does not include 'null' or 'undefined' in its type, therefore the '??' operator can be safely removed.`); + templateMapping.span, + `The left side of this nullish coalescing operation does not include 'null' or 'undefined' in its type, therefore the '??' operator can be safely removed.`, + ); return [diagnostic]; } } export const factory: TemplateCheckFactory< - ErrorCode.NULLISH_COALESCING_NOT_NULLABLE, - ExtendedTemplateDiagnosticName.NULLISH_COALESCING_NOT_NULLABLE> = { + ErrorCode.NULLISH_COALESCING_NOT_NULLABLE, + ExtendedTemplateDiagnosticName.NULLISH_COALESCING_NOT_NULLABLE +> = { code: ErrorCode.NULLISH_COALESCING_NOT_NULLABLE, name: ExtendedTemplateDiagnosticName.NULLISH_COALESCING_NOT_NULLABLE, create: (options: NgCompilerOptions) => { // Require `strictNullChecks` to be enabled. const strictNullChecks = - options.strictNullChecks === undefined ? !!options.strict : !!options.strictNullChecks; + options.strictNullChecks === undefined ? !!options.strict : !!options.strictNullChecks; if (!strictNullChecks) { return null; } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/optional_chain_not_nullable/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/optional_chain_not_nullable/index.ts index 520145834d275..9b0f6328cd33f 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/optional_chain_not_nullable/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/optional_chain_not_nullable/index.ts @@ -20,15 +20,19 @@ import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '. * This check should only be use if `strictNullChecks` is enabled, * otherwise it would produce inaccurate results. */ -class OptionalChainNotNullableCheck extends - TemplateCheckWithVisitor { +class OptionalChainNotNullableCheck extends TemplateCheckWithVisitor { override code = ErrorCode.OPTIONAL_CHAIN_NOT_NULLABLE as const; override visitNode( - ctx: TemplateContext, component: ts.ClassDeclaration, - node: TmplAstNode|AST): NgTemplateDiagnostic[] { - if (!(node instanceof SafeCall) && !(node instanceof SafePropertyRead) && - !(node instanceof SafeKeyedRead)) + ctx: TemplateContext, + component: ts.ClassDeclaration, + node: TmplAstNode | AST, + ): NgTemplateDiagnostic[] { + if ( + !(node instanceof SafeCall) && + !(node instanceof SafePropertyRead) && + !(node instanceof SafeKeyedRead) + ) return []; const symbolLeft = ctx.templateTypeChecker.getSymbolOfNode(node.receiver, component); @@ -51,31 +55,34 @@ class OptionalChainNotNullableCheck extends if (symbol.kind !== SymbolKind.Expression) { return []; } - const templateMapping = - ctx.templateTypeChecker.getTemplateMappingAtTcbLocation(symbol.tcbLocation); + const templateMapping = ctx.templateTypeChecker.getTemplateMappingAtTcbLocation( + symbol.tcbLocation, + ); if (templateMapping === null) { return []; } - const advice = node instanceof SafePropertyRead ? - `the '?.' operator can be replaced with the '.' operator` : - `the '?.' operator can be safely removed`; + const advice = + node instanceof SafePropertyRead + ? `the '?.' operator can be replaced with the '.' operator` + : `the '?.' operator can be safely removed`; const diagnostic = ctx.makeTemplateDiagnostic( - templateMapping.span, - `The left side of this optional chain operation does not include 'null' or 'undefined' in its type, therefore ${ - advice}.`); + templateMapping.span, + `The left side of this optional chain operation does not include 'null' or 'undefined' in its type, therefore ${advice}.`, + ); return [diagnostic]; } } export const factory: TemplateCheckFactory< - ErrorCode.OPTIONAL_CHAIN_NOT_NULLABLE, - ExtendedTemplateDiagnosticName.OPTIONAL_CHAIN_NOT_NULLABLE> = { + ErrorCode.OPTIONAL_CHAIN_NOT_NULLABLE, + ExtendedTemplateDiagnosticName.OPTIONAL_CHAIN_NOT_NULLABLE +> = { code: ErrorCode.OPTIONAL_CHAIN_NOT_NULLABLE, name: ExtendedTemplateDiagnosticName.OPTIONAL_CHAIN_NOT_NULLABLE, create: (options: NgCompilerOptions) => { // Require `strictNullChecks` to be enabled. const strictNullChecks = - options.strictNullChecks === undefined ? !!options.strict : !!options.strictNullChecks; + options.strictNullChecks === undefined ? !!options.strict : !!options.strictNullChecks; if (!strictNullChecks) { return null; } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/skip_hydration_not_static/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/skip_hydration_not_static/index.ts index 50de4d876d12f..400e34b5aa76c 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/skip_hydration_not_static/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/skip_hydration_not_static/index.ts @@ -23,8 +23,10 @@ class NgSkipHydrationSpec extends TemplateCheckWithVisitor, component: ts.ClassDeclaration, - node: TmplAstNode|AST): NgTemplateDiagnostic[] { + ctx: TemplateContext, + component: ts.ClassDeclaration, + node: TmplAstNode | AST, + ): NgTemplateDiagnostic[] { /** Binding should always error */ if (node instanceof TmplAstBoundAttribute && node.name === NG_SKIP_HYDRATION_ATTR_NAME) { const errorString = `ngSkipHydration should not be used as a binding.`; @@ -34,10 +36,13 @@ class NgSkipHydrationSpec extends TemplateCheckWithVisitor = - { - code: ErrorCode.SKIP_HYDRATION_NOT_STATIC, - name: ExtendedTemplateDiagnosticName.SKIP_HYDRATION_NOT_STATIC, - create: () => new NgSkipHydrationSpec(), - }; + ErrorCode.SKIP_HYDRATION_NOT_STATIC, + ExtendedTemplateDiagnosticName.SKIP_HYDRATION_NOT_STATIC +> = { + code: ErrorCode.SKIP_HYDRATION_NOT_STATIC, + name: ExtendedTemplateDiagnosticName.SKIP_HYDRATION_NOT_STATIC, + create: () => new NgSkipHydrationSpec(), +}; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/suffix_not_supported/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/suffix_not_supported/index.ts index a3a323dd6fbf8..6781793915586 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/suffix_not_supported/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/suffix_not_supported/index.ts @@ -23,26 +23,33 @@ class SuffixNotSupportedCheck extends TemplateCheckWithVisitor, component: ts.ClassDeclaration, - node: TmplAstNode|AST): NgTemplateDiagnostic[] { + ctx: TemplateContext, + component: ts.ClassDeclaration, + node: TmplAstNode | AST, + ): NgTemplateDiagnostic[] { if (!(node instanceof TmplAstBoundAttribute)) return []; - if (!node.keySpan.toString().startsWith('attr.') || - !STYLE_SUFFIXES.some(suffix => node.name.endsWith(`.${suffix}`))) { + if ( + !node.keySpan.toString().startsWith('attr.') || + !STYLE_SUFFIXES.some((suffix) => node.name.endsWith(`.${suffix}`)) + ) { return []; } const diagnostic = ctx.makeTemplateDiagnostic( - node.keySpan, - `The ${ - STYLE_SUFFIXES.map(suffix => `'.${suffix}'`) - .join(', ')} suffixes are only supported on style bindings.`); + node.keySpan, + `The ${STYLE_SUFFIXES.map((suffix) => `'.${suffix}'`).join( + ', ', + )} suffixes are only supported on style bindings.`, + ); return [diagnostic]; } } export const factory: TemplateCheckFactory< - ErrorCode.SUFFIX_NOT_SUPPORTED, ExtendedTemplateDiagnosticName.SUFFIX_NOT_SUPPORTED> = { + ErrorCode.SUFFIX_NOT_SUPPORTED, + ExtendedTemplateDiagnosticName.SUFFIX_NOT_SUPPORTED +> = { code: ErrorCode.SUFFIX_NOT_SUPPORTED, name: ExtendedTemplateDiagnosticName.SUFFIX_NOT_SUPPORTED, create: () => new SuffixNotSupportedCheck(), diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/text_attribute_not_binding/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/text_attribute_not_binding/index.ts index ae11ce0fa7ec7..6bfe9e1adc9d3 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/text_attribute_not_binding/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/checks/text_attribute_not_binding/index.ts @@ -20,19 +20,18 @@ import {TemplateCheckFactory, TemplateCheckWithVisitor, TemplateContext} from '. * is likely not the intent of the developer. Instead, the intent is likely to have the `id` be set * to 'my-id'. */ -class TextAttributeNotBindingSpec extends - TemplateCheckWithVisitor { +class TextAttributeNotBindingSpec extends TemplateCheckWithVisitor { override code = ErrorCode.TEXT_ATTRIBUTE_NOT_BINDING as const; override visitNode( - ctx: TemplateContext, - component: ts.ClassDeclaration, - node: TmplAstNode|AST, - ): NgTemplateDiagnostic[] { + ctx: TemplateContext, + component: ts.ClassDeclaration, + node: TmplAstNode | AST, + ): NgTemplateDiagnostic[] { if (!(node instanceof TmplAstTextAttribute)) return []; const name = node.name; - if ((!name.startsWith('attr.') && !name.startsWith('style.') && !name.startsWith('class.'))) { + if (!name.startsWith('attr.') && !name.startsWith('style.') && !name.startsWith('class.')) { return []; } @@ -46,9 +45,9 @@ class TextAttributeNotBindingSpec extends } else { const expectedKey = `[${name}]`; const expectedValue = - // true/false are special cases because we don't want to convert them to strings but - // rather maintain the logical true/false when bound. - (node.value === 'true' || node.value === 'false') ? node.value : `'${node.value}'`; + // true/false are special cases because we don't want to convert them to strings but + // rather maintain the logical true/false when bound. + node.value === 'true' || node.value === 'false' ? node.value : `'${node.value}'`; errorString = 'Attribute, style, and class bindings should be enclosed with square braces.'; if (node.value) { errorString += ` For example, '${expectedKey}="${expectedValue}"'.`; @@ -60,8 +59,9 @@ class TextAttributeNotBindingSpec extends } export const factory: TemplateCheckFactory< - ErrorCode.TEXT_ATTRIBUTE_NOT_BINDING, - ExtendedTemplateDiagnosticName.TEXT_ATTRIBUTE_NOT_BINDING> = { + ErrorCode.TEXT_ATTRIBUTE_NOT_BINDING, + ExtendedTemplateDiagnosticName.TEXT_ATTRIBUTE_NOT_BINDING +> = { code: ErrorCode.TEXT_ATTRIBUTE_NOT_BINDING, name: ExtendedTemplateDiagnosticName.TEXT_ATTRIBUTE_NOT_BINDING, create: () => new TextAttributeNotBindingSpec(), diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts index 93b24e02895e0..2a38248efb718 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/index.ts @@ -20,16 +20,21 @@ import {factory as textAttributeNotBindingFactory} from './checks/text_attribute export {ExtendedTemplateCheckerImpl} from './src/extended_template_checker'; -export const ALL_DIAGNOSTIC_FACTORIES: - readonly TemplateCheckFactory[] = [ - invalidBananaInBoxFactory, nullishCoalescingNotNullableFactory, - optionalChainNotNullableFactory, missingControlFlowDirectiveFactory, - textAttributeNotBindingFactory, missingNgForOfLetFactory, suffixNotSupportedFactory, - interpolatedSignalNotInvoked - ]; - +export const ALL_DIAGNOSTIC_FACTORIES: readonly TemplateCheckFactory< + ErrorCode, + ExtendedTemplateDiagnosticName +>[] = [ + invalidBananaInBoxFactory, + nullishCoalescingNotNullableFactory, + optionalChainNotNullableFactory, + missingControlFlowDirectiveFactory, + textAttributeNotBindingFactory, + missingNgForOfLetFactory, + suffixNotSupportedFactory, + interpolatedSignalNotInvoked, +]; export const SUPPORTED_DIAGNOSTIC_NAMES = new Set([ ExtendedTemplateDiagnosticName.CONTROL_FLOW_PREVENTING_CONTENT_PROJECTION, - ...ALL_DIAGNOSTIC_FACTORIES.map(factory => factory.name) + ...ALL_DIAGNOSTIC_FACTORIES.map((factory) => factory.name), ]); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/src/extended_template_checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/src/extended_template_checker.ts index 763715db3ebac..723b7d3c93512 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/src/extended_template_checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/src/extended_template_checker.ts @@ -12,25 +12,36 @@ import ts from 'typescript'; import {DiagnosticCategoryLabel, NgCompilerOptions} from '../../../core/api'; import {ErrorCode, ExtendedTemplateDiagnosticName} from '../../../diagnostics'; import {NgTemplateDiagnostic, TemplateDiagnostic, TemplateTypeChecker} from '../../api'; -import {ExtendedTemplateChecker, TemplateCheck, TemplateCheckFactory, TemplateContext} from '../api'; +import { + ExtendedTemplateChecker, + TemplateCheck, + TemplateCheckFactory, + TemplateContext, +} from '../api'; export class ExtendedTemplateCheckerImpl implements ExtendedTemplateChecker { private readonly partialCtx: Omit, 'makeTemplateDiagnostic'>; private readonly templateChecks: Map, ts.DiagnosticCategory>; constructor( - templateTypeChecker: TemplateTypeChecker, typeChecker: ts.TypeChecker, - templateCheckFactories: - readonly TemplateCheckFactory[], - options: NgCompilerOptions) { + templateTypeChecker: TemplateTypeChecker, + typeChecker: ts.TypeChecker, + templateCheckFactories: readonly TemplateCheckFactory< + ErrorCode, + ExtendedTemplateDiagnosticName + >[], + options: NgCompilerOptions, + ) { this.partialCtx = {templateTypeChecker, typeChecker}; this.templateChecks = new Map, ts.DiagnosticCategory>(); for (const factory of templateCheckFactories) { // Read the diagnostic category from compiler options. const category = diagnosticLabelToCategory( - options?.extendedDiagnostics?.checks?.[factory.name] ?? - options?.extendedDiagnostics?.defaultCategory ?? DiagnosticCategoryLabel.Warning); + options?.extendedDiagnostics?.checks?.[factory.name] ?? + options?.extendedDiagnostics?.defaultCategory ?? + DiagnosticCategoryLabel.Warning, + ); // Skip the diagnostic if suppressed via compiler options. if (category === null) { @@ -67,14 +78,24 @@ export class ExtendedTemplateCheckerImpl implements ExtendedTemplateChecker { ...this.partialCtx, // Wrap `templateTypeChecker.makeTemplateDiagnostic()` to implicitly provide all the known // options. - makeTemplateDiagnostic: (span: ParseSourceSpan, message: string, relatedInformation?: { - text: string, - start: number, - end: number, - sourceFile: ts.SourceFile, - }[]): NgTemplateDiagnostic => { + makeTemplateDiagnostic: ( + span: ParseSourceSpan, + message: string, + relatedInformation?: { + text: string; + start: number; + end: number; + sourceFile: ts.SourceFile; + }[], + ): NgTemplateDiagnostic => { return this.partialCtx.templateTypeChecker.makeTemplateDiagnostic( - component, span, category, check.code, message, relatedInformation); + component, + span, + category, + check.code, + message, + relatedInformation, + ); }, }; @@ -89,7 +110,7 @@ export class ExtendedTemplateCheckerImpl implements ExtendedTemplateChecker { * Converts a `DiagnosticCategoryLabel` to its equivalent `ts.DiagnosticCategory` or `null` if * the label is `DiagnosticCategoryLabel.Suppress`. */ -function diagnosticLabelToCategory(label: DiagnosticCategoryLabel): ts.DiagnosticCategory|null { +function diagnosticLabelToCategory(label: DiagnosticCategoryLabel): ts.DiagnosticCategory | null { switch (label) { case DiagnosticCategoryLabel.Warning: return ts.DiagnosticCategory.Warning; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/interpolated_signal_not_invoked/interpolated_signal_not_invoked_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/interpolated_signal_not_invoked/interpolated_signal_not_invoked_spec.ts index a1be83e5a7f52..d6e9335144691 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/interpolated_signal_not_invoked/interpolated_signal_not_invoked_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/interpolated_signal_not_invoked/interpolated_signal_not_invoked_spec.ts @@ -20,8 +20,9 @@ runInEachFileSystem(() => { describe('Interpolated Signal', () => { it('binds the error code to its extended template diagnostic name', () => { expect(interpolatedSignalFactory.code).toBe(ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED); - expect(interpolatedSignalFactory.name) - .toBe(ExtendedTemplateDiagnosticName.INTERPOLATED_SIGNAL_NOT_INVOKED); + expect(interpolatedSignalFactory.name).toBe( + ExtendedTemplateDiagnosticName.INTERPOLATED_SIGNAL_NOT_INVOKED, + ); }); it('should not produce a warning when a signal getter is invoked', () => { @@ -43,8 +44,11 @@ runInEachFileSystem(() => { const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [interpolatedSignalFactory], {} - /* options */ + templateTypeChecker, + program.getTypeChecker(), + [interpolatedSignalFactory], + {}, + /* options */ ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(0); @@ -71,7 +75,10 @@ runInEachFileSystem(() => { const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [interpolatedSignalFactory], {} /* options */ + templateTypeChecker, + program.getTypeChecker(), + [interpolatedSignalFactory], + {} /* options */, ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(2); @@ -100,7 +107,10 @@ runInEachFileSystem(() => { const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [interpolatedSignalFactory], {} /* options */ + templateTypeChecker, + program.getTypeChecker(), + [interpolatedSignalFactory], + {} /* options */, ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); @@ -129,7 +139,10 @@ runInEachFileSystem(() => { const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [interpolatedSignalFactory], {} /* options */ + templateTypeChecker, + program.getTypeChecker(), + [interpolatedSignalFactory], + {} /* options */, ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); @@ -157,7 +170,10 @@ runInEachFileSystem(() => { const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [interpolatedSignalFactory], {} /* options */ + templateTypeChecker, + program.getTypeChecker(), + [interpolatedSignalFactory], + {} /* options */, ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); @@ -185,7 +201,10 @@ runInEachFileSystem(() => { const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [interpolatedSignalFactory], {} /* options */ + templateTypeChecker, + program.getTypeChecker(), + [interpolatedSignalFactory], + {} /* options */, ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); @@ -213,7 +232,10 @@ runInEachFileSystem(() => { const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [interpolatedSignalFactory], {} /* options */ + templateTypeChecker, + program.getTypeChecker(), + [interpolatedSignalFactory], + {} /* options */, ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); @@ -241,7 +263,10 @@ runInEachFileSystem(() => { const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [interpolatedSignalFactory], {} /* options */ + templateTypeChecker, + program.getTypeChecker(), + [interpolatedSignalFactory], + {} /* options */, ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); @@ -275,7 +300,10 @@ runInEachFileSystem(() => { const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [interpolatedSignalFactory], {} /* options */ + templateTypeChecker, + program.getTypeChecker(), + [interpolatedSignalFactory], + {} /* options */, ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); @@ -304,7 +332,10 @@ runInEachFileSystem(() => { const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [interpolatedSignalFactory], {} /* options */ + templateTypeChecker, + program.getTypeChecker(), + [interpolatedSignalFactory], + {} /* options */, ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(0); @@ -330,7 +361,10 @@ runInEachFileSystem(() => { const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [interpolatedSignalFactory], {} /* options */ + templateTypeChecker, + program.getTypeChecker(), + [interpolatedSignalFactory], + {} /* options */, ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(0); @@ -356,7 +390,10 @@ runInEachFileSystem(() => { const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [interpolatedSignalFactory], {} /* options */ + templateTypeChecker, + program.getTypeChecker(), + [interpolatedSignalFactory], + {} /* options */, ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(0); @@ -381,7 +418,10 @@ runInEachFileSystem(() => { const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [interpolatedSignalFactory], {} /* options */ + templateTypeChecker, + program.getTypeChecker(), + [interpolatedSignalFactory], + {} /* options */, ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); @@ -409,7 +449,10 @@ runInEachFileSystem(() => { const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [interpolatedSignalFactory], {} /* options */ + templateTypeChecker, + program.getTypeChecker(), + [interpolatedSignalFactory], + {} /* options */, ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(0); @@ -434,7 +477,10 @@ runInEachFileSystem(() => { const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [interpolatedSignalFactory], {} /* options */ + templateTypeChecker, + program.getTypeChecker(), + [interpolatedSignalFactory], + {} /* options */, ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); @@ -443,32 +489,34 @@ runInEachFileSystem(() => { expect(getSourceCodeForDiagnostic(diags[0])).toBe(`mySignal`); }); - it('should not produce a warning when signal is invoked in attribute binding interpolation ', - () => { - const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([ - { - fileName, - templates: { - 'TestCmp': `
`, - }, - source: ` + it('should not produce a warning when signal is invoked in attribute binding interpolation ', () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `
`, + }, + source: ` import {signal} from '@angular/core'; export class TestCmp { mySignal = signal(0); }`, - }, - ]); - const sf = getSourceFileOrError(program, fileName); - const component = getClass(sf, 'TestCmp'); - const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [interpolatedSignalFactory], {} - /* options */ - ); - const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); - expect(diags.length).toBe(0); - }); + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [interpolatedSignalFactory], + {}, + /* options */ + ); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + expect(diags.length).toBe(0); + }); it('should produce a warning when nested signal is not invoked on interpolated binding', () => { const fileName = absoluteFrom('/main.ts'); @@ -489,7 +537,10 @@ runInEachFileSystem(() => { const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [interpolatedSignalFactory], {} /* options */ + templateTypeChecker, + program.getTypeChecker(), + [interpolatedSignalFactory], + {} /* options */, ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); @@ -518,7 +569,10 @@ runInEachFileSystem(() => { const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [interpolatedSignalFactory], {} /* options */ + templateTypeChecker, + program.getTypeChecker(), + [interpolatedSignalFactory], + {} /* options */, ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(0); @@ -548,101 +602,106 @@ runInEachFileSystem(() => { const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [interpolatedSignalFactory], {} /* options */ + templateTypeChecker, + program.getTypeChecker(), + [interpolatedSignalFactory], + {} /* options */, ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(0); }); ['name', 'length', 'prototype', 'set', 'update', 'asReadonly'].forEach( - functionInstanceProperty => { - it(`should produce a warning when a property named '${ - functionInstanceProperty}' of a not invoked signal is used in interpolation`, - () => { - const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([ - { - fileName, - templates: { - 'TestCmp': `
{{myObject.mySignal.${functionInstanceProperty}}}
`, - }, - source: ` + (functionInstanceProperty) => { + it(`should produce a warning when a property named '${functionInstanceProperty}' of a not invoked signal is used in interpolation`, () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `
{{myObject.mySignal.${functionInstanceProperty}}}
`, + }, + source: ` import {signal} from '@angular/core'; export class TestCmp { - myObject = { mySignal: signal<{ ${functionInstanceProperty}: string }>({ ${ - functionInstanceProperty}: 'foo' }) }; + myObject = { mySignal: signal<{ ${functionInstanceProperty}: string }>({ ${functionInstanceProperty}: 'foo' }) }; }`, - }, - ]); - const sf = getSourceFileOrError(program, fileName); - const component = getClass(sf, 'TestCmp'); - const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [interpolatedSignalFactory], {} - /* options */ - ); - const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); - expect(diags.length).toBe(1); - expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); - expect(diags[0].code).toBe(ngErrorCode(ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED)); - expect(getSourceCodeForDiagnostic(diags[0])).toBe(`mySignal`); - }); - - it(`should not produce a warning when a property named ${ - functionInstanceProperty} of an invoked signal is used in interpolation`, - () => { - const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([ - { - fileName, - templates: { - 'TestCmp': `
{{mySignal().${functionInstanceProperty}}}
`, - }, - source: ` + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [interpolatedSignalFactory], + {}, + /* options */ + ); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + expect(diags.length).toBe(1); + expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); + expect(diags[0].code).toBe(ngErrorCode(ErrorCode.INTERPOLATED_SIGNAL_NOT_INVOKED)); + expect(getSourceCodeForDiagnostic(diags[0])).toBe(`mySignal`); + }); + + it(`should not produce a warning when a property named ${functionInstanceProperty} of an invoked signal is used in interpolation`, () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `
{{mySignal().${functionInstanceProperty}}}
`, + }, + source: ` import {signal} from '@angular/core'; export class TestCmp { - mySignal = signal<{ ${functionInstanceProperty}: string }>({ ${ - functionInstanceProperty}: 'foo' }); + mySignal = signal<{ ${functionInstanceProperty}: string }>({ ${functionInstanceProperty}: 'foo' }); }`, - }, - ]); - const sf = getSourceFileOrError(program, fileName); - const component = getClass(sf, 'TestCmp'); - const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [interpolatedSignalFactory], {} - /* options */ - ); - const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); - expect(diags.length).toBe(0); - }); - - it(`should not produce a warning when a property named ${ - functionInstanceProperty} of an object is used in interpolation`, - () => { - const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([ - { - fileName, - templates: { - 'TestCmp': `
{{myObject.${functionInstanceProperty}}}
`, - }, - source: ` + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [interpolatedSignalFactory], + {}, + /* options */ + ); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + expect(diags.length).toBe(0); + }); + + it(`should not produce a warning when a property named ${functionInstanceProperty} of an object is used in interpolation`, () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `
{{myObject.${functionInstanceProperty}}}
`, + }, + source: ` import {signal} from '@angular/core'; export class TestCmp { myObject = { ${functionInstanceProperty}: 'foo' }; }`, - }, - ]); - const sf = getSourceFileOrError(program, fileName); - const component = getClass(sf, 'TestCmp'); - const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [interpolatedSignalFactory], {} - /* options */ - ); - const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); - expect(diags.length).toBe(0); - }); + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [interpolatedSignalFactory], + {}, + /* options */ + ); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + expect(diags.length).toBe(0); }); + }, + ); }); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/invalid_banana_in_box/invalid_banana_in_box_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/invalid_banana_in_box/invalid_banana_in_box_spec.ts index e13c033f44dbc..b185427e10137 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/invalid_banana_in_box/invalid_banana_in_box_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/invalid_banana_in_box/invalid_banana_in_box_spec.ts @@ -21,24 +21,30 @@ runInEachFileSystem(() => { describe('TemplateChecks', () => { it('binds the error code to its extended template diagnostic name', () => { expect(invalidBananaInBoxFactory.code).toBe(ErrorCode.INVALID_BANANA_IN_BOX); - expect(invalidBananaInBoxFactory.name) - .toBe(ExtendedTemplateDiagnosticName.INVALID_BANANA_IN_BOX); + expect(invalidBananaInBoxFactory.name).toBe( + ExtendedTemplateDiagnosticName.INVALID_BANANA_IN_BOX, + ); }); it('should produce invalid banana in a box warning', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': '
', + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': '
', + }, + source: 'export class TestCmp { var1: string = "text"; }', }, - source: 'export class TestCmp { var1: string = "text"; }' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [invalidBananaInBoxFactory], - {} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [invalidBananaInBoxFactory], + {} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); @@ -48,60 +54,74 @@ runInEachFileSystem(() => { it('should not produce invalid banana in a box warning if written correctly', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': '
', + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': '
', + }, + source: 'export class TestCmp { var1: string = "text"; }', }, - source: 'export class TestCmp { var1: string = "text"; }' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [invalidBananaInBoxFactory], - {} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [invalidBananaInBoxFactory], + {} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(0); }); - it('should not produce invalid banana in a box warning with bracket in the middle of the name', - () => { - const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': '
', - }, - source: 'export class TestCmp { var1: string = "text"; }' - }]); - const sf = getSourceFileOrError(program, fileName); - const component = getClass(sf, 'TestCmp'); - const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [invalidBananaInBoxFactory], - {} /* options */); - const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); - expect(diags.length).toBe(0); - }); + it('should not produce invalid banana in a box warning with bracket in the middle of the name', () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': '
', + }, + source: 'export class TestCmp { var1: string = "text"; }', + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [invalidBananaInBoxFactory], + {} /* options */, + ); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + expect(diags.length).toBe(0); + }); it('should produce invalid banana in a box warnings for *ngIf and ng-template', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `
+ const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `
Content to render when condition is false.
`, - }, - source: `export class TestCmp { + }, + source: `export class TestCmp { var1: string = "text"; - }` - }]); + }`, + }, + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [invalidBananaInBoxFactory], - {} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [invalidBananaInBoxFactory], + {} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(2); expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); @@ -114,19 +134,24 @@ runInEachFileSystem(() => { it('should respect configured category', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': '
', + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': '
', + }, + source: 'export class TestCmp { var1: string = "text"; }', }, - source: 'export class TestCmp { var1: string = "text"; }' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [invalidBananaInBoxFactory], - {extendedDiagnostics: {checks: {invalidBananaInBox: DiagnosticCategoryLabel.Error}}}); + templateTypeChecker, + program.getTypeChecker(), + [invalidBananaInBoxFactory], + {extendedDiagnostics: {checks: {invalidBananaInBox: DiagnosticCategoryLabel.Error}}}, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/missing_control_flow_directive/missing_control_flow_directive_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/missing_control_flow_directive/missing_control_flow_directive_spec.ts index 6834febe51df8..b933416ac0dd4 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/missing_control_flow_directive/missing_control_flow_directive_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/missing_control_flow_directive/missing_control_flow_directive_spec.ts @@ -13,133 +13,168 @@ import {absoluteFrom, getSourceFileOrError} from '../../../../../file_system'; import {runInEachFileSystem} from '../../../../../file_system/testing'; import {getSourceCodeForDiagnostic} from '../../../../../testing'; import {getClass, setup} from '../../../../testing'; -import {factory as missingControlFlowDirectiveCheck, KNOWN_CONTROL_FLOW_DIRECTIVES} from '../../../checks/missing_control_flow_directive'; +import { + factory as missingControlFlowDirectiveCheck, + KNOWN_CONTROL_FLOW_DIRECTIVES, +} from '../../../checks/missing_control_flow_directive'; import {ExtendedTemplateCheckerImpl} from '../../../src/extended_template_checker'; runInEachFileSystem(() => { describe('MissingControlFlowDirectiveCheck', () => { KNOWN_CONTROL_FLOW_DIRECTIVES.forEach((correspondingImport, directive) => { - ['div', 'ng-template', 'ng-container', 'ng-content'].forEach(element => { - it(`should produce a warning when the '${directive}' directive is not imported ` + - `(when used on the '${element}' element)`, - () => { - const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `<${element} *${directive}="exp">`, - }, - declarations: [{ - name: 'TestCmp', - type: 'directive', - selector: `[test-cmp]`, - isStandalone: true, - }] - }]); - const sf = getSourceFileOrError(program, fileName); - const component = getClass(sf, 'TestCmp'); - const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [missingControlFlowDirectiveCheck], - {} /* options */); - const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); - expect(diags.length).toBe(1); - expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); - expect(diags[0].code).toBe(ngErrorCode(ErrorCode.MISSING_CONTROL_FLOW_DIRECTIVE)); - expect(diags[0].messageText).toContain(`The \`*${directive}\` directive was used`); - expect(diags[0].messageText) - .toContain(`neither the \`${ - correspondingImport - .directive}\` directive nor the \`CommonModule\` was imported. `); - expect(diags[0].messageText) - .toContain(`Use Angular's built-in control flow ${correspondingImport?.builtIn}`); - expect(getSourceCodeForDiagnostic(diags[0])).toBe(directive); - }); + ['div', 'ng-template', 'ng-container', 'ng-content'].forEach((element) => { + it( + `should produce a warning when the '${directive}' directive is not imported ` + + `(when used on the '${element}' element)`, + () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `<${element} *${directive}="exp">`, + }, + declarations: [ + { + name: 'TestCmp', + type: 'directive', + selector: `[test-cmp]`, + isStandalone: true, + }, + ], + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [missingControlFlowDirectiveCheck], + {} /* options */, + ); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + expect(diags.length).toBe(1); + expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); + expect(diags[0].code).toBe(ngErrorCode(ErrorCode.MISSING_CONTROL_FLOW_DIRECTIVE)); + expect(diags[0].messageText).toContain(`The \`*${directive}\` directive was used`); + expect(diags[0].messageText).toContain( + `neither the \`${correspondingImport.directive}\` directive nor the \`CommonModule\` was imported. `, + ); + expect(diags[0].messageText).toContain( + `Use Angular's built-in control flow ${correspondingImport?.builtIn}`, + ); + expect(getSourceCodeForDiagnostic(diags[0])).toBe(directive); + }, + ); - it(`should *not* produce a warning when the '${directive}' directive is not imported ` + - `into a non-standalone component scope (when used on the '${element}' element)`, - () => { - const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `<${element} *${directive}="exp">`, - }, - declarations: [{ - name: 'TestCmp', - type: 'directive', - selector: `[test-cmp]`, - isStandalone: false, - }] - }]); - const sf = getSourceFileOrError(program, fileName); - const component = getClass(sf, 'TestCmp'); - const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [missingControlFlowDirectiveCheck], - {} /* options */); - const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); - // No diagnostic messages are expected. - expect(diags.length).toBe(0); - }); + it( + `should *not* produce a warning when the '${directive}' directive is not imported ` + + `into a non-standalone component scope (when used on the '${element}' element)`, + () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `<${element} *${directive}="exp">`, + }, + declarations: [ + { + name: 'TestCmp', + type: 'directive', + selector: `[test-cmp]`, + isStandalone: false, + }, + ], + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [missingControlFlowDirectiveCheck], + {} /* options */, + ); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + // No diagnostic messages are expected. + expect(diags.length).toBe(0); + }, + ); - it(`should *not* produce a warning when the '${directive}' directive is imported ` + - `(when used on the '${element}' element)`, - () => { - const className = directive.charAt(0).toUpperCase() + directive.substr(1); - const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `<${element} *${directive}="exp">`, - }, - source: ` + it( + `should *not* produce a warning when the '${directive}' directive is imported ` + + `(when used on the '${element}' element)`, + () => { + const className = directive.charAt(0).toUpperCase() + directive.substr(1); + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `<${element} *${directive}="exp">`, + }, + source: ` export class TestCmp {} export class ${className} {} `, - declarations: [ - { - type: 'directive', - name: className, - selector: `[${directive}]`, - }, - { - name: 'TestCmp', - type: 'directive', - selector: `[test-cmp]`, - isStandalone: true, - } - ], - }]); - const sf = getSourceFileOrError(program, fileName); - const component = getClass(sf, 'TestCmp'); - const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [missingControlFlowDirectiveCheck], - {strictNullChecks: true} /* options */); - const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); - // No diagnostic messages are expected. - expect(diags.length).toBe(0); - }); + declarations: [ + { + type: 'directive', + name: className, + selector: `[${directive}]`, + }, + { + name: 'TestCmp', + type: 'directive', + selector: `[test-cmp]`, + isStandalone: true, + }, + ], + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [missingControlFlowDirectiveCheck], + {strictNullChecks: true} /* options */, + ); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + // No diagnostic messages are expected. + expect(diags.length).toBe(0); + }, + ); }); }); it(`should *not* produce a warning for other missing structural directives`, () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `
`, + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `
`, + }, + declarations: [ + { + name: 'TestCmp', + type: 'directive', + selector: `[test-cmp]`, + isStandalone: true, + }, + ], }, - declarations: [{ - name: 'TestCmp', - type: 'directive', - selector: `[test-cmp]`, - isStandalone: true, - }] - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [missingControlFlowDirectiveCheck], - {strictNullChecks: true} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [missingControlFlowDirectiveCheck], + {strictNullChecks: true} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); // No diagnostic messages are expected. expect(diags.length).toBe(0); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/missing_ngforof_let/missing_ngforof_let_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/missing_ngforof_let/missing_ngforof_let_spec.ts index 29c06e934f2b4..2b107b801916f 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/missing_ngforof_let/missing_ngforof_let_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/missing_ngforof_let/missing_ngforof_let_spec.ts @@ -25,18 +25,23 @@ runInEachFileSystem(() => { it('should produce missing ngforof let warning', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': '
  • {{thing["name"]}}
', + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': '
  • {{thing["name"]}}
', + }, + source: "export class TestCmp { items: [] = [{'name': 'diana'}, {'name': 'prince'}] }", }, - source: - 'export class TestCmp { items: [] = [{\'name\': \'diana\'}, {\'name\': \'prince\'}] }' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [missingNgForOfLet], {} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [missingNgForOfLet], + {} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); @@ -46,36 +51,47 @@ runInEachFileSystem(() => { it('should not produce missing ngforof let warning if written correctly', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': '
  • {{item["name"]}};
', + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': '
  • {{item["name"]}};
', + }, + source: "export class TestCmp { items: [] = [{'name': 'diana'}, {'name': 'prince'}] }", }, - source: - 'export class TestCmp { items: [] = [{\'name\': \'diana\'}, {\'name\': \'prince\'}] }' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [missingNgForOfLet], {} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [missingNgForOfLet], + {} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(0); }); it('should not produce missing ngforof let warning if written correctly in longhand', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': '{{item["name"]}}', + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': + '{{item["name"]}}', + }, + source: "export class TestCmp { items: [] = [{'name': 'diana'}, {'name': 'prince'}] }", }, - source: - 'export class TestCmp { items: [] = [{\'name\': \'diana\'}, {\'name\': \'prince\'}] }' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [missingNgForOfLet], {} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [missingNgForOfLet], + {} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(0); }); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/nullish_coalescing_not_nullable/nullish_coalescing_not_nullable_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/nullish_coalescing_not_nullable/nullish_coalescing_not_nullable_spec.ts index 4922199cb0d22..b0f9b869360ba 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/nullish_coalescing_not_nullable/nullish_coalescing_not_nullable_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/nullish_coalescing_not_nullable/nullish_coalescing_not_nullable_spec.ts @@ -20,46 +20,52 @@ import {ExtendedTemplateCheckerImpl} from '../../../src/extended_template_checke runInEachFileSystem(() => { describe('NullishCoalescingNotNullableCheck', () => { it('binds the error code to its extended template diagnostic name', () => { - expect(nullishCoalescingNotNullableFactory.code) - .toBe(ErrorCode.NULLISH_COALESCING_NOT_NULLABLE); - expect(nullishCoalescingNotNullableFactory.name) - .toBe(ExtendedTemplateDiagnosticName.NULLISH_COALESCING_NOT_NULLABLE); + expect(nullishCoalescingNotNullableFactory.code).toBe( + ErrorCode.NULLISH_COALESCING_NOT_NULLABLE, + ); + expect(nullishCoalescingNotNullableFactory.name).toBe( + ExtendedTemplateDiagnosticName.NULLISH_COALESCING_NOT_NULLABLE, + ); }); it('should return a check if `strictNullChecks` is enabled', () => { expect(nullishCoalescingNotNullableFactory.create({strictNullChecks: true})).toBeDefined(); }); - it('should return a check if `strictNullChecks` is not configured but `strict` is enabled', - () => { - expect(nullishCoalescingNotNullableFactory.create({strict: true})).toBeDefined(); - }); + it('should return a check if `strictNullChecks` is not configured but `strict` is enabled', () => { + expect(nullishCoalescingNotNullableFactory.create({strict: true})).toBeDefined(); + }); it('should not return a check if `strictNullChecks` is disabled', () => { expect(nullishCoalescingNotNullableFactory.create({strictNullChecks: false})).toBeNull(); - expect(nullishCoalescingNotNullableFactory.create({})).toBeNull(); // Defaults disabled. + expect(nullishCoalescingNotNullableFactory.create({})).toBeNull(); // Defaults disabled. }); - it('should not return a check if `strict` is enabled but `strictNullChecks` is disabled', - () => { - expect(nullishCoalescingNotNullableFactory.create({strict: true, strictNullChecks: false})) - .toBeNull(); - }); + it('should not return a check if `strict` is enabled but `strictNullChecks` is disabled', () => { + expect( + nullishCoalescingNotNullableFactory.create({strict: true, strictNullChecks: false}), + ).toBeNull(); + }); it('should produce nullish coalescing warning', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `{{ var1 ?? 'foo' }}`, + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `{{ var1 ?? 'foo' }}`, + }, + source: 'export class TestCmp { var1: string = "text"; }', }, - source: 'export class TestCmp { var1: string = "text"; }' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [nullishCoalescingNotNullableFactory], - {strictNullChecks: true} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [nullishCoalescingNotNullableFactory], + {strictNullChecks: true} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); @@ -70,19 +76,25 @@ runInEachFileSystem(() => { it('should produce nullish coalescing warning for classes with inline TCBs', () => { const fileName = absoluteFrom('/main.ts'); const {program, templateTypeChecker} = setup( - [{ + [ + { fileName, templates: { 'TestCmp': `{{ var1 ?? 'foo' }}`, }, - source: 'class TestCmp { var1: string = "text"; }' - }], - {inlining: true}); + source: 'class TestCmp { var1: string = "text"; }', + }, + ], + {inlining: true}, + ); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [nullishCoalescingNotNullableFactory], - {strictNullChecks: true} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [nullishCoalescingNotNullableFactory], + {strictNullChecks: true} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); @@ -92,107 +104,133 @@ runInEachFileSystem(() => { it('should not produce nullish coalescing warning for a nullable type', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `{{ var1 ?? 'foo' }}`, + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `{{ var1 ?? 'foo' }}`, + }, + source: 'export class TestCmp { var1: string | null = "text"; }', }, - source: 'export class TestCmp { var1: string | null = "text"; }' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [nullishCoalescingNotNullableFactory], - {strictNullChecks: true} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [nullishCoalescingNotNullableFactory], + {strictNullChecks: true} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(0); }); it('should not produce nullish coalescing warning for the any type', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `{{ var1 ?? 'foo' }}`, + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `{{ var1 ?? 'foo' }}`, + }, + source: 'export class TestCmp { var1: any; }', }, - source: 'export class TestCmp { var1: any; }' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [nullishCoalescingNotNullableFactory], - {strictNullChecks: true} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [nullishCoalescingNotNullableFactory], + {strictNullChecks: true} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(0); }); it('should not produce nullish coalescing warning for the unknown type', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `{{ var1 ?? 'foo' }}`, + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `{{ var1 ?? 'foo' }}`, + }, + source: 'export class TestCmp { var1: unknown; }', }, - source: 'export class TestCmp { var1: unknown; }' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [nullishCoalescingNotNullableFactory], - {strictNullChecks: true} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [nullishCoalescingNotNullableFactory], + {strictNullChecks: true} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(0); }); it('should not produce nullish coalescing warning for a type that includes undefined', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `{{ var1 ?? 'foo' }}`, + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `{{ var1 ?? 'foo' }}`, + }, + source: 'export class TestCmp { var1: string | undefined = "text"; }', }, - source: 'export class TestCmp { var1: string | undefined = "text"; }' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [nullishCoalescingNotNullableFactory], - {strictNullChecks: true} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [nullishCoalescingNotNullableFactory], + {strictNullChecks: true} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(0); }); - it('warns for pipe arguments which are likely configured incorrectly (?? operates on "format" here)', - () => { - const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `{{ 123 | date: 'format' ?? 'invalid date' }}`, - }, - source: ` + it('warns for pipe arguments which are likely configured incorrectly (?? operates on "format" here)', () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `{{ 123 | date: 'format' ?? 'invalid date' }}`, + }, + source: ` export class TestCmp { var1: string | undefined = "text"; } export class DatePipe { transform(value: string, format: string): string[] { } `, - declarations: [{ - type: 'pipe', - name: 'DatePipe', - pipeName: 'date', - }], - }]); - const sf = getSourceFileOrError(program, fileName); - const component = getClass(sf, 'TestCmp'); - const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [nullishCoalescingNotNullableFactory], - {strictNullChecks: true} /* options */); - const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); - expect(diags.length).toBe(1); - expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); - expect(diags[0].code).toBe(ngErrorCode(ErrorCode.NULLISH_COALESCING_NOT_NULLABLE)); - expect(getSourceCodeForDiagnostic(diags[0])).toBe(`'format' ?? 'invalid date'`); - }); + declarations: [ + { + type: 'pipe', + name: 'DatePipe', + pipeName: 'date', + }, + ], + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [nullishCoalescingNotNullableFactory], + {strictNullChecks: true} /* options */, + ); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + expect(diags.length).toBe(1); + expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); + expect(diags[0].code).toBe(ngErrorCode(ErrorCode.NULLISH_COALESCING_NOT_NULLABLE)); + expect(getSourceCodeForDiagnostic(diags[0])).toBe(`'format' ?? 'invalid date'`); + }); it('does not warn for pipe arguments when parens are used', () => { const fileName = absoluteFrom('/main.ts'); @@ -208,71 +246,80 @@ runInEachFileSystem(() => { transform(value: string, format: string): string[] { } `, - declarations: [{ - type: 'pipe', - name: 'DatePipe', - pipeName: 'date', - }], + declarations: [ + { + type: 'pipe', + name: 'DatePipe', + pipeName: 'date', + }, + ], }, ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [nullishCoalescingNotNullableFactory], - {strictNullChecks: true} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [nullishCoalescingNotNullableFactory], + {strictNullChecks: true} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(0); }); - it('should not produce nullish coalescing warning when the left side is a nullable expression', - () => { - const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([ - { - fileName, - templates: { - 'TestCmp': `{{ func() ?? 'foo' }}`, - }, - source: ` + it('should not produce nullish coalescing warning when the left side is a nullable expression', () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `{{ func() ?? 'foo' }}`, + }, + source: ` export class TestCmp { func = (): string | null => null; } `, - }, - ]); - const sf = getSourceFileOrError(program, fileName); - const component = getClass(sf, 'TestCmp'); - const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [nullishCoalescingNotNullableFactory], - {strictNullChecks: true} /* options */); - const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); - expect(diags.length).toBe(0); - }); + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [nullishCoalescingNotNullableFactory], + {strictNullChecks: true} /* options */, + ); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + expect(diags.length).toBe(0); + }); it('should respect configured diagnostic category', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `{{ var1 ?? 'foo' }}`, + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `{{ var1 ?? 'foo' }}`, + }, + source: 'export class TestCmp { var1: string = "text"; }', }, - source: 'export class TestCmp { var1: string = "text"; }' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, - program.getTypeChecker(), - [nullishCoalescingNotNullableFactory], - { - strictNullChecks: true, - extendedDiagnostics: { - checks: { - nullishCoalescingNotNullable: DiagnosticCategoryLabel.Error, - }, + templateTypeChecker, + program.getTypeChecker(), + [nullishCoalescingNotNullableFactory], + { + strictNullChecks: true, + extendedDiagnostics: { + checks: { + nullishCoalescingNotNullable: DiagnosticCategoryLabel.Error, }, }, + }, ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/optional_chain_not_nullable/optional_chain_not_nullable_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/optional_chain_not_nullable/optional_chain_not_nullable_spec.ts index b6bf8a1aee94a..60c13427d6763 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/optional_chain_not_nullable/optional_chain_not_nullable_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/optional_chain_not_nullable/optional_chain_not_nullable_spec.ts @@ -21,67 +21,78 @@ runInEachFileSystem(() => { describe('OptionalChainNotNullableCheck', () => { it('binds the error code to its extended template diagnostic name', () => { expect(optionalChainNotNullableFactory.code).toBe(ErrorCode.OPTIONAL_CHAIN_NOT_NULLABLE); - expect(optionalChainNotNullableFactory.name) - .toBe(ExtendedTemplateDiagnosticName.OPTIONAL_CHAIN_NOT_NULLABLE); + expect(optionalChainNotNullableFactory.name).toBe( + ExtendedTemplateDiagnosticName.OPTIONAL_CHAIN_NOT_NULLABLE, + ); }); it('should return a check if `strictNullChecks` is enabled', () => { expect(optionalChainNotNullableFactory.create({strictNullChecks: true})).toBeDefined(); }); - it('should return a check if `strictNullChecks` is not configured but `strict` is enabled', - () => { - expect(optionalChainNotNullableFactory.create({strict: true})).toBeDefined(); - }); + it('should return a check if `strictNullChecks` is not configured but `strict` is enabled', () => { + expect(optionalChainNotNullableFactory.create({strict: true})).toBeDefined(); + }); it('should not return a check if `strictNullChecks` is disabled', () => { expect(optionalChainNotNullableFactory.create({strictNullChecks: false})).toBeNull(); - expect(optionalChainNotNullableFactory.create({})).toBeNull(); // Defaults disabled. + expect(optionalChainNotNullableFactory.create({})).toBeNull(); // Defaults disabled. }); - it('should not return a check if `strict` is enabled but `strictNullChecks` is disabled', - () => { - expect(optionalChainNotNullableFactory.create({strict: true, strictNullChecks: false})) - .toBeNull(); - }); + it('should not return a check if `strict` is enabled but `strictNullChecks` is disabled', () => { + expect( + optionalChainNotNullableFactory.create({strict: true, strictNullChecks: false}), + ).toBeNull(); + }); it('should produce optional chain warning for property access', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `{{ var1?.bar }}`, + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `{{ var1?.bar }}`, + }, + source: 'export class TestCmp { var1: { foo: string } = { foo: "bar" }; }', }, - source: 'export class TestCmp { var1: { foo: string } = { foo: "bar" }; }' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [optionalChainNotNullableFactory], - {strictNullChecks: true} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [optionalChainNotNullableFactory], + {strictNullChecks: true} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); expect(diags[0].code).toBe(ngErrorCode(ErrorCode.OPTIONAL_CHAIN_NOT_NULLABLE)); - expect(diags[0].messageText) - .toContain(`the '?.' operator can be replaced with the '.' operator`); + expect(diags[0].messageText).toContain( + `the '?.' operator can be replaced with the '.' operator`, + ); expect(getSourceCodeForDiagnostic(diags[0])).toBe(`bar`); }); it('should produce optional chain warning for indexed access', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `{{ var1?.['bar'] }}`, + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `{{ var1?.['bar'] }}`, + }, + source: 'export class TestCmp { var1: { foo: string } = { foo: "bar" }; }', }, - source: 'export class TestCmp { var1: { foo: string } = { foo: "bar" }; }' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [optionalChainNotNullableFactory], - {strictNullChecks: true} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [optionalChainNotNullableFactory], + {strictNullChecks: true} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); @@ -92,18 +103,23 @@ runInEachFileSystem(() => { it('should produce optional chain warning for method call', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `{{ foo?.() }}`, + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `{{ foo?.() }}`, + }, + source: 'export class TestCmp { foo: () => string }', }, - source: 'export class TestCmp { foo: () => string }' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [optionalChainNotNullableFactory], - {strictNullChecks: true} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [optionalChainNotNullableFactory], + {strictNullChecks: true} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); @@ -115,19 +131,25 @@ runInEachFileSystem(() => { it('should produce optional chain warning for classes with inline TCBs', () => { const fileName = absoluteFrom('/main.ts'); const {program, templateTypeChecker} = setup( - [{ + [ + { fileName, templates: { 'TestCmp': `{{ var1?.bar }}`, }, - source: 'class TestCmp { var1: { foo: string } = { foo: "bar" }; }' - }], - {inlining: true}); + source: 'class TestCmp { var1: { foo: string } = { foo: "bar" }; }', + }, + ], + {inlining: true}, + ); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [optionalChainNotNullableFactory], - {strictNullChecks: true} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [optionalChainNotNullableFactory], + {strictNullChecks: true} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); @@ -137,125 +159,149 @@ runInEachFileSystem(() => { it('should not produce optional chain warning for a nullable type', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `{{ var1?.bar }}`, + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `{{ var1?.bar }}`, + }, + source: 'export class TestCmp { var1: string | null = "text"; }', }, - source: 'export class TestCmp { var1: string | null = "text"; }' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [optionalChainNotNullableFactory], - {strictNullChecks: true} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [optionalChainNotNullableFactory], + {strictNullChecks: true} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(0); }); it('should not produce optional chain warning for the any type', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `{{ var1?.bar }}`, + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `{{ var1?.bar }}`, + }, + source: 'export class TestCmp { var1: any; }', }, - source: 'export class TestCmp { var1: any; }' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [optionalChainNotNullableFactory], - {strictNullChecks: true} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [optionalChainNotNullableFactory], + {strictNullChecks: true} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(0); }); it('should not produce optional chain warning for the unknown type', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `{{ var1?.bar }}`, + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `{{ var1?.bar }}`, + }, + source: 'export class TestCmp { var1: unknown; }', }, - source: 'export class TestCmp { var1: unknown; }' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [optionalChainNotNullableFactory], - {strictNullChecks: true} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [optionalChainNotNullableFactory], + {strictNullChecks: true} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(0); }); it('should not produce optional chain warning for a type that includes undefined', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `{{ var1?.bar }}`, + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `{{ var1?.bar }}`, + }, + source: 'export class TestCmp { var1: string | undefined = "text"; }', }, - source: 'export class TestCmp { var1: string | undefined = "text"; }' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [optionalChainNotNullableFactory], - {strictNullChecks: true} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [optionalChainNotNullableFactory], + {strictNullChecks: true} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(0); }); - it('should not produce optional chain warning when the left side is a nullable expression', - () => { - const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([ - { - fileName, - templates: { - 'TestCmp': `{{ func()?.foo }}`, - }, - source: ` + it('should not produce optional chain warning when the left side is a nullable expression', () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `{{ func()?.foo }}`, + }, + source: ` export class TestCmp { func = (): { foo: string } | null => null; } `, - }, - ]); - const sf = getSourceFileOrError(program, fileName); - const component = getClass(sf, 'TestCmp'); - const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [optionalChainNotNullableFactory], - {strictNullChecks: true} /* options */); - const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); - expect(diags.length).toBe(0); - }); + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const component = getClass(sf, 'TestCmp'); + const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( + templateTypeChecker, + program.getTypeChecker(), + [optionalChainNotNullableFactory], + {strictNullChecks: true} /* options */, + ); + const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); + expect(diags.length).toBe(0); + }); it('should respect configured diagnostic category', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `{{ var1?.bar }}`, + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `{{ var1?.bar }}`, + }, + source: 'export class TestCmp { var1: { foo: string } = { foo: "bar" }; }', }, - source: 'export class TestCmp { var1: { foo: string } = { foo: "bar" }; }' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, - program.getTypeChecker(), - [optionalChainNotNullableFactory], - { - strictNullChecks: true, - extendedDiagnostics: { - checks: { - optionalChainNotNullable: DiagnosticCategoryLabel.Error, - }, + templateTypeChecker, + program.getTypeChecker(), + [optionalChainNotNullableFactory], + { + strictNullChecks: true, + extendedDiagnostics: { + checks: { + optionalChainNotNullable: DiagnosticCategoryLabel.Error, }, }, + }, ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/skip_hydration_not_static/skip_hydration_not_static_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/skip_hydration_not_static/skip_hydration_not_static_spec.ts index 012915eaca27f..72d6ca19be12d 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/skip_hydration_not_static/skip_hydration_not_static_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/skip_hydration_not_static/skip_hydration_not_static_spec.ts @@ -20,8 +20,9 @@ runInEachFileSystem(() => { describe('SkipHydrationNotStatic', () => { it('binds the error code to its extended template diagnostic name', () => { expect(skipHydrationNotStaticFactory.code).toBe(ErrorCode.SKIP_HYDRATION_NOT_STATIC); - expect(skipHydrationNotStaticFactory.name) - .toBe(ExtendedTemplateDiagnosticName.SKIP_HYDRATION_NOT_STATIC); + expect(skipHydrationNotStaticFactory.name).toBe( + ExtendedTemplateDiagnosticName.SKIP_HYDRATION_NOT_STATIC, + ); }); it('should produce class binding warning', () => { @@ -38,8 +39,11 @@ runInEachFileSystem(() => { const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [skipHydrationNotStaticFactory], {} - /* options */ + templateTypeChecker, + program.getTypeChecker(), + [skipHydrationNotStaticFactory], + {}, + /* options */ ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); @@ -62,8 +66,11 @@ runInEachFileSystem(() => { const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [skipHydrationNotStaticFactory], {} - /* options */ + templateTypeChecker, + program.getTypeChecker(), + [skipHydrationNotStaticFactory], + {}, + /* options */ ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); @@ -86,8 +93,11 @@ runInEachFileSystem(() => { const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [skipHydrationNotStaticFactory], {} - /* options */ + templateTypeChecker, + program.getTypeChecker(), + [skipHydrationNotStaticFactory], + {}, + /* options */ ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); @@ -110,8 +120,11 @@ runInEachFileSystem(() => { const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [skipHydrationNotStaticFactory], {} - /* options */ + templateTypeChecker, + program.getTypeChecker(), + [skipHydrationNotStaticFactory], + {}, + /* options */ ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(0); @@ -131,8 +144,11 @@ runInEachFileSystem(() => { const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [skipHydrationNotStaticFactory], {} - /* options */ + templateTypeChecker, + program.getTypeChecker(), + [skipHydrationNotStaticFactory], + {}, + /* options */ ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(0); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/suffix_not_supported/suffix_not_supported_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/suffix_not_supported/suffix_not_supported_spec.ts index 27dc183add612..18fce2540ccb3 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/suffix_not_supported/suffix_not_supported_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/suffix_not_supported/suffix_not_supported_spec.ts @@ -21,23 +21,30 @@ runInEachFileSystem(() => { describe('SuffixNotSupportedCheck', () => { it('binds the error code to its extended template diagnostic name', () => { expect(suffixNotSupportedFactory.code).toBe(ErrorCode.SUFFIX_NOT_SUPPORTED); - expect(suffixNotSupportedFactory.name) - .toBe(ExtendedTemplateDiagnosticName.SUFFIX_NOT_SUPPORTED); + expect(suffixNotSupportedFactory.name).toBe( + ExtendedTemplateDiagnosticName.SUFFIX_NOT_SUPPORTED, + ); }); it('should produce suffix not supported warning', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `
`, + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `
`, + }, + source: 'export class TestCmp {}', }, - source: 'export class TestCmp {}' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [suffixNotSupportedFactory], {}); + templateTypeChecker, + program.getTypeChecker(), + [suffixNotSupportedFactory], + {}, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); @@ -47,17 +54,23 @@ runInEachFileSystem(() => { it('should not produce suffix not supported warning on a style binding', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `
`, + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `
`, + }, + source: 'export class TestCmp {}', }, - source: 'export class TestCmp {}' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [suffixNotSupportedFactory], {}); + templateTypeChecker, + program.getTypeChecker(), + [suffixNotSupportedFactory], + {}, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(0); }); @@ -76,7 +89,11 @@ runInEachFileSystem(() => { const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [suffixNotSupportedFactory], {}); + templateTypeChecker, + program.getTypeChecker(), + [suffixNotSupportedFactory], + {}, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(0); }); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/text_attribute_not_binding/text_attribute_not_binding_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/text_attribute_not_binding/text_attribute_not_binding_spec.ts index b940a36df5c00..f1a44c7654a3e 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/text_attribute_not_binding/text_attribute_not_binding_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/extended/test/checks/text_attribute_not_binding/text_attribute_not_binding_spec.ts @@ -21,24 +21,30 @@ runInEachFileSystem(() => { describe('TextAttributeNotBindingCheck', () => { it('binds the error code to its extended template diagnostic name', () => { expect(textAttributeNotBindingFactory.code).toBe(ErrorCode.TEXT_ATTRIBUTE_NOT_BINDING); - expect(textAttributeNotBindingFactory.name) - .toBe(ExtendedTemplateDiagnosticName.TEXT_ATTRIBUTE_NOT_BINDING); + expect(textAttributeNotBindingFactory.name).toBe( + ExtendedTemplateDiagnosticName.TEXT_ATTRIBUTE_NOT_BINDING, + ); }); it('should produce class binding warning', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `
`, + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `
`, + }, + source: 'export class TestCmp { }', }, - source: 'export class TestCmp { }' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [textAttributeNotBindingFactory], - {} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [textAttributeNotBindingFactory], + {} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); @@ -48,18 +54,23 @@ runInEachFileSystem(() => { it('should produce an attribute binding warning', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `
`, + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `
`, + }, + source: 'export class TestCmp { }', }, - source: 'export class TestCmp { }' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [textAttributeNotBindingFactory], - {} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [textAttributeNotBindingFactory], + {} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); @@ -69,18 +80,23 @@ runInEachFileSystem(() => { it('should produce a style binding warning', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `
`, + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `
`, + }, + source: 'export class TestCmp { }', }, - source: 'export class TestCmp { }' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [textAttributeNotBindingFactory], - {} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [textAttributeNotBindingFactory], + {} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); expect(diags[0].category).toBe(ts.DiagnosticCategory.Warning); @@ -90,18 +106,23 @@ runInEachFileSystem(() => { it('should not produce a warning when there is no value', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'TestCmp': `
`, + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'TestCmp': `
`, + }, + source: 'export class TestCmp { }', }, - source: 'export class TestCmp { }' - }]); + ]); const sf = getSourceFileOrError(program, fileName); const component = getClass(sf, 'TestCmp'); const extendedTemplateChecker = new ExtendedTemplateCheckerImpl( - templateTypeChecker, program.getTypeChecker(), [textAttributeNotBindingFactory], - {} /* options */); + templateTypeChecker, + program.getTypeChecker(), + [textAttributeNotBindingFactory], + {} /* options */, + ); const diags = extendedTemplateChecker.getDiagnosticsForComponent(component); expect(diags.length).toBe(1); expect(diags[0].code).toBe(ngErrorCode(ErrorCode.TEXT_ATTRIBUTE_NOT_BINDING)); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts index 2f9e6e56d77ea..207ab29dd887d 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/checker.ts @@ -6,25 +6,80 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, CssSelector, DomElementSchemaRegistry, ExternalExpr, LiteralPrimitive, ParseSourceSpan, PropertyRead, SafePropertyRead, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable, WrappedNodeExpr} from '@angular/compiler'; +import { + AST, + CssSelector, + DomElementSchemaRegistry, + ExternalExpr, + LiteralPrimitive, + ParseSourceSpan, + PropertyRead, + SafePropertyRead, + TmplAstElement, + TmplAstNode, + TmplAstReference, + TmplAstTemplate, + TmplAstTextAttribute, + TmplAstVariable, + WrappedNodeExpr, +} from '@angular/compiler'; import ts from 'typescript'; import {ErrorCode, ngErrorCode} from '../../diagnostics'; import {absoluteFromSourceFile, AbsoluteFsPath, getSourceFileOrError} from '../../file_system'; import {Reference, ReferenceEmitKind, ReferenceEmitter} from '../../imports'; import {IncrementalBuild} from '../../incremental/api'; -import {DirectiveMeta, MetadataReader, MetadataReaderWithIndex, MetaKind, NgModuleIndex, NgModuleMeta, PipeMeta} from '../../metadata'; +import { + DirectiveMeta, + MetadataReader, + MetadataReaderWithIndex, + MetaKind, + NgModuleIndex, + NgModuleMeta, + PipeMeta, +} from '../../metadata'; import {PerfCheckpoint, PerfEvent, PerfPhase, PerfRecorder} from '../../perf'; import {ProgramDriver, UpdateMode} from '../../program_driver'; -import {ClassDeclaration, DeclarationNode, isNamedClassDeclaration, ReflectionHost} from '../../reflection'; +import { + ClassDeclaration, + DeclarationNode, + isNamedClassDeclaration, + ReflectionHost, +} from '../../reflection'; import {ComponentScopeKind, ComponentScopeReader, TypeCheckScopeRegistry} from '../../scope'; import {isShim} from '../../shims'; import {getSourceFileOrNull, isSymbolWithValueDeclaration} from '../../util/src/typescript'; -import {ElementSymbol, FullTemplateMapping, GlobalCompletion, NgTemplateDiagnostic, OptimizeFor, PotentialDirective, PotentialImport, PotentialImportKind, PotentialImportMode, PotentialPipe, ProgramTypeCheckAdapter, Symbol, TcbLocation, TemplateDiagnostic, TemplateId, TemplateSymbol, TemplateTypeChecker, TypeCheckableDirectiveMeta, TypeCheckingConfig} from '../api'; +import { + ElementSymbol, + FullTemplateMapping, + GlobalCompletion, + NgTemplateDiagnostic, + OptimizeFor, + PotentialDirective, + PotentialImport, + PotentialImportKind, + PotentialImportMode, + PotentialPipe, + ProgramTypeCheckAdapter, + Symbol, + TcbLocation, + TemplateDiagnostic, + TemplateId, + TemplateSymbol, + TemplateTypeChecker, + TypeCheckableDirectiveMeta, + TypeCheckingConfig, +} from '../api'; import {makeTemplateDiagnostic} from '../diagnostics'; import {CompletionEngine} from './completion'; -import {InliningMode, ShimTypeCheckingData, TemplateData, TypeCheckContextImpl, TypeCheckingHost} from './context'; +import { + InliningMode, + ShimTypeCheckingData, + TemplateData, + TypeCheckContextImpl, + TypeCheckingHost, +} from './context'; import {shouldReportDiagnostic, translateDiagnostic} from './diagnostics'; import {TypeCheckShimGenerator} from './shim'; import {TemplateSourceManager} from './source'; @@ -74,24 +129,28 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { * destroyed when the `ts.Program` changes and the `TemplateTypeCheckerImpl` as a whole is * destroyed and replaced. */ - private elementTagCache = new Map>(); + private elementTagCache = new Map>(); private isComplete = false; constructor( - private originalProgram: ts.Program, readonly programDriver: ProgramDriver, - private typeCheckAdapter: ProgramTypeCheckAdapter, private config: TypeCheckingConfig, - private refEmitter: ReferenceEmitter, private reflector: ReflectionHost, - private compilerHost: Pick, - private priorBuild: IncrementalBuild, - private readonly metaReader: MetadataReader, - private readonly localMetaReader: MetadataReaderWithIndex, - private readonly ngModuleIndex: NgModuleIndex, - private readonly componentScopeReader: ComponentScopeReader, - private readonly typeCheckScopeRegistry: TypeCheckScopeRegistry, - private readonly perf: PerfRecorder) {} - - getTemplate(component: ts.ClassDeclaration): TmplAstNode[]|null { + private originalProgram: ts.Program, + readonly programDriver: ProgramDriver, + private typeCheckAdapter: ProgramTypeCheckAdapter, + private config: TypeCheckingConfig, + private refEmitter: ReferenceEmitter, + private reflector: ReflectionHost, + private compilerHost: Pick, + private priorBuild: IncrementalBuild, + private readonly metaReader: MetadataReader, + private readonly localMetaReader: MetadataReaderWithIndex, + private readonly ngModuleIndex: NgModuleIndex, + private readonly componentScopeReader: ComponentScopeReader, + private readonly typeCheckScopeRegistry: TypeCheckScopeRegistry, + private readonly perf: PerfRecorder, + ) {} + + getTemplate(component: ts.ClassDeclaration): TmplAstNode[] | null { const {data} = this.getLatestComponentState(component); if (data === null) { return null; @@ -99,16 +158,20 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return data.template; } - getUsedDirectives(component: ts.ClassDeclaration): TypeCheckableDirectiveMeta[]|null { + getUsedDirectives(component: ts.ClassDeclaration): TypeCheckableDirectiveMeta[] | null { return this.getLatestComponentState(component).data?.boundTarget.getUsedDirectives() || null; } - getUsedPipes(component: ts.ClassDeclaration): string[]|null { + getUsedPipes(component: ts.ClassDeclaration): string[] | null { return this.getLatestComponentState(component).data?.boundTarget.getUsedPipes() || null; } - private getLatestComponentState(component: ts.ClassDeclaration): - {data: TemplateData|null, tcb: ts.Node|null, tcbPath: AbsoluteFsPath, tcbIsShim: boolean} { + private getLatestComponentState(component: ts.ClassDeclaration): { + data: TemplateData | null; + tcb: ts.Node | null; + tcbPath: AbsoluteFsPath; + tcbIsShim: boolean; + } { this.ensureShimForComponent(component); const sf = component.getSourceFile(); @@ -132,7 +195,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { throw new Error(`Error: no shim file in program: ${shimPath}`); } - let tcb: ts.Node|null = findTypeCheckBlock(shimSf, id, /*isDiagnosticsRequest*/ false); + let tcb: ts.Node | null = findTypeCheckBlock(shimSf, id, /*isDiagnosticsRequest*/ false); let tcbPath = shimPath; if (tcb === null) { @@ -145,7 +208,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { } } - let data: TemplateData|null = null; + let data: TemplateData | null = null; if (shimRecord.templates.has(templateId)) { data = shimRecord.templates.get(templateId)!; } @@ -157,8 +220,10 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return this.getFileAndShimRecordsForPath(filePath) !== null; } - private getFileRecordForTcbLocation({tcbPath, isShimFile}: TcbLocation): FileTypeCheckingData - |null { + private getFileRecordForTcbLocation({ + tcbPath, + isShimFile, + }: TcbLocation): FileTypeCheckingData | null { if (!isShimFile) { // The location is not within a shim file but corresponds with an inline TCB in an original // source file; we can obtain the record directly by its path. @@ -179,8 +244,9 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { } } - private getFileAndShimRecordsForPath(shimPath: AbsoluteFsPath): - {fileRecord: FileTypeCheckingData, shimRecord: ShimTypeCheckingData}|null { + private getFileAndShimRecordsForPath( + shimPath: AbsoluteFsPath, + ): {fileRecord: FileTypeCheckingData; shimRecord: ShimTypeCheckingData} | null { for (const fileRecord of this.state.values()) { if (fileRecord.shimData.has(shimPath)) { return {fileRecord, shimRecord: fileRecord.shimData.get(shimPath)!}; @@ -189,7 +255,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return null; } - getTemplateMappingAtTcbLocation(tcbLocation: TcbLocation): FullTemplateMapping|null { + getTemplateMappingAtTcbLocation(tcbLocation: TcbLocation): FullTemplateMapping | null { const fileRecord = this.getFileRecordForTcbLocation(tcbLocation); if (fileRecord === null) { return null; @@ -200,8 +266,11 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return null; } return getTemplateMapping( - shimSf, tcbLocation.positionInFile, fileRecord.sourceManager, - /*isDiagnosticsRequest*/ false); + shimSf, + tcbLocation.positionInFile, + fileRecord.sourceManager, + /*isDiagnosticsRequest*/ false, + ); } generateAllTypeCheckBlocks() { @@ -228,17 +297,23 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { const typeCheckProgram = this.programDriver.getProgram(); - const diagnostics: (ts.Diagnostic|null)[] = []; + const diagnostics: (ts.Diagnostic | null)[] = []; if (fileRecord.hasInlines) { const inlineSf = getSourceFileOrError(typeCheckProgram, sfPath); - diagnostics.push(...typeCheckProgram.getSemanticDiagnostics(inlineSf).map( - diag => convertDiagnostic(diag, fileRecord.sourceManager))); + diagnostics.push( + ...typeCheckProgram + .getSemanticDiagnostics(inlineSf) + .map((diag) => convertDiagnostic(diag, fileRecord.sourceManager)), + ); } for (const [shimPath, shimRecord] of fileRecord.shimData) { const shimSf = getSourceFileOrError(typeCheckProgram, shimPath); - diagnostics.push(...typeCheckProgram.getSemanticDiagnostics(shimSf).map( - diag => convertDiagnostic(diag, fileRecord.sourceManager))); + diagnostics.push( + ...typeCheckProgram + .getSemanticDiagnostics(shimSf) + .map((diag) => convertDiagnostic(diag, fileRecord.sourceManager)), + ); diagnostics.push(...shimRecord.genesisDiagnostics); for (const templateData of shimRecord.templates.values()) { @@ -246,7 +321,9 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { } } - return diagnostics.filter((diag: ts.Diagnostic|null): diag is ts.Diagnostic => diag !== null); + return diagnostics.filter( + (diag: ts.Diagnostic | null): diag is ts.Diagnostic => diag !== null, + ); }); } @@ -269,16 +346,22 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { const typeCheckProgram = this.programDriver.getProgram(); - const diagnostics: (TemplateDiagnostic|null)[] = []; + const diagnostics: (TemplateDiagnostic | null)[] = []; if (shimRecord.hasInlines) { const inlineSf = getSourceFileOrError(typeCheckProgram, sfPath); - diagnostics.push(...typeCheckProgram.getSemanticDiagnostics(inlineSf).map( - diag => convertDiagnostic(diag, fileRecord.sourceManager))); + diagnostics.push( + ...typeCheckProgram + .getSemanticDiagnostics(inlineSf) + .map((diag) => convertDiagnostic(diag, fileRecord.sourceManager)), + ); } const shimSf = getSourceFileOrError(typeCheckProgram, shimPath); - diagnostics.push(...typeCheckProgram.getSemanticDiagnostics(shimSf).map( - diag => convertDiagnostic(diag, fileRecord.sourceManager))); + diagnostics.push( + ...typeCheckProgram + .getSemanticDiagnostics(shimSf) + .map((diag) => convertDiagnostic(diag, fileRecord.sourceManager)), + ); diagnostics.push(...shimRecord.genesisDiagnostics); for (const templateData of shimRecord.templates.values()) { @@ -286,45 +369,54 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { } return diagnostics.filter( - (diag: TemplateDiagnostic|null): diag is TemplateDiagnostic => - diag !== null && diag.templateId === templateId); + (diag: TemplateDiagnostic | null): diag is TemplateDiagnostic => + diag !== null && diag.templateId === templateId, + ); }); } - getTypeCheckBlock(component: ts.ClassDeclaration): ts.Node|null { + getTypeCheckBlock(component: ts.ClassDeclaration): ts.Node | null { return this.getLatestComponentState(component).tcb; } getGlobalCompletions( - context: TmplAstTemplate|null, component: ts.ClassDeclaration, - node: AST|TmplAstNode): GlobalCompletion|null { + context: TmplAstTemplate | null, + component: ts.ClassDeclaration, + node: AST | TmplAstNode, + ): GlobalCompletion | null { const engine = this.getOrCreateCompletionEngine(component); if (engine === null) { return null; } - return this.perf.inPhase( - PerfPhase.TtcAutocompletion, () => engine.getGlobalCompletions(context, node)); + return this.perf.inPhase(PerfPhase.TtcAutocompletion, () => + engine.getGlobalCompletions(context, node), + ); } getExpressionCompletionLocation( - ast: PropertyRead|SafePropertyRead, component: ts.ClassDeclaration): TcbLocation|null { + ast: PropertyRead | SafePropertyRead, + component: ts.ClassDeclaration, + ): TcbLocation | null { const engine = this.getOrCreateCompletionEngine(component); if (engine === null) { return null; } - return this.perf.inPhase( - PerfPhase.TtcAutocompletion, () => engine.getExpressionCompletionLocation(ast)); + return this.perf.inPhase(PerfPhase.TtcAutocompletion, () => + engine.getExpressionCompletionLocation(ast), + ); } getLiteralCompletionLocation( - node: LiteralPrimitive|TmplAstTextAttribute, component: ts.ClassDeclaration): TcbLocation - |null { + node: LiteralPrimitive | TmplAstTextAttribute, + component: ts.ClassDeclaration, + ): TcbLocation | null { const engine = this.getOrCreateCompletionEngine(component); if (engine === null) { return null; } - return this.perf.inPhase( - PerfPhase.TtcAutocompletion, () => engine.getLiteralCompletionLocation(node)); + return this.perf.inPhase(PerfPhase.TtcAutocompletion, () => + engine.getLiteralCompletionLocation(node), + ); } invalidateClass(clazz: ts.ClassDeclaration): void { @@ -345,20 +437,28 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { this.isComplete = false; } - getExpressionTarget(expression: AST, clazz: ts.ClassDeclaration): TmplAstReference|TmplAstVariable - |null { - return this.getLatestComponentState(clazz).data?.boundTarget.getExpressionTarget(expression) || - null; + getExpressionTarget( + expression: AST, + clazz: ts.ClassDeclaration, + ): TmplAstReference | TmplAstVariable | null { + return ( + this.getLatestComponentState(clazz).data?.boundTarget.getExpressionTarget(expression) || null + ); } makeTemplateDiagnostic( - clazz: ts.ClassDeclaration, sourceSpan: ParseSourceSpan, category: ts.DiagnosticCategory, - errorCode: T, message: string, relatedInformation?: { - text: string, - start: number, - end: number, - sourceFile: ts.SourceFile, - }[]): NgTemplateDiagnostic { + clazz: ts.ClassDeclaration, + sourceSpan: ParseSourceSpan, + category: ts.DiagnosticCategory, + errorCode: T, + message: string, + relatedInformation?: { + text: string; + start: number; + end: number; + sourceFile: ts.SourceFile; + }[], + ): NgTemplateDiagnostic { const sfPath = absoluteFromSourceFile(clazz.getSourceFile()); const fileRecord = this.state.get(sfPath)!; const templateId = fileRecord.sourceManager.getTemplateId(clazz); @@ -366,13 +466,19 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return { ...makeTemplateDiagnostic( - templateId, mapping, sourceSpan, category, ngErrorCode(errorCode), message, - relatedInformation), - __ngCode: errorCode + templateId, + mapping, + sourceSpan, + category, + ngErrorCode(errorCode), + message, + relatedInformation, + ), + __ngCode: errorCode, }; } - private getOrCreateCompletionEngine(component: ts.ClassDeclaration): CompletionEngine|null { + private getOrCreateCompletionEngine(component: ts.ClassDeclaration): CompletionEngine | null { if (this.completionCache.has(component)) { return this.completionCache.get(component)!; } @@ -484,10 +590,18 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { } private newContext(host: TypeCheckingHost): TypeCheckContextImpl { - const inlining = - this.programDriver.supportsInlineOperations ? InliningMode.InlineOps : InliningMode.Error; + const inlining = this.programDriver.supportsInlineOperations + ? InliningMode.InlineOps + : InliningMode.Error; return new TypeCheckContextImpl( - this.config, this.compilerHost, this.refEmitter, this.reflector, host, inlining, this.perf); + this.config, + this.compilerHost, + this.refEmitter, + this.reflector, + host, + inlining, + this.perf, + ); } /** @@ -537,9 +651,9 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { } return this.state.get(path)!; } - getSymbolOfNode(node: TmplAstTemplate, component: ts.ClassDeclaration): TemplateSymbol|null; - getSymbolOfNode(node: TmplAstElement, component: ts.ClassDeclaration): ElementSymbol|null; - getSymbolOfNode(node: AST|TmplAstNode, component: ts.ClassDeclaration): Symbol|null { + getSymbolOfNode(node: TmplAstTemplate, component: ts.ClassDeclaration): TemplateSymbol | null; + getSymbolOfNode(node: TmplAstElement, component: ts.ClassDeclaration): ElementSymbol | null; + getSymbolOfNode(node: AST | TmplAstNode, component: ts.ClassDeclaration): Symbol | null { const builder = this.getOrCreateSymbolBuilder(component); if (builder === null) { return null; @@ -547,7 +661,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return this.perf.inPhase(PerfPhase.TtcSymbol, () => builder.getSymbol(node)); } - private getOrCreateSymbolBuilder(component: ts.ClassDeclaration): SymbolBuilder|null { + private getOrCreateSymbolBuilder(component: ts.ClassDeclaration): SymbolBuilder | null { if (this.symbolBuilderCache.has(component)) { return this.symbolBuilderCache.get(component)!; } @@ -558,8 +672,13 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { } const builder = new SymbolBuilder( - tcbPath, tcbIsShim, tcb, data, this.componentScopeReader, - () => this.programDriver.getProgram().getTypeChecker()); + tcbPath, + tcbIsShim, + tcb, + data, + this.componentScopeReader, + () => this.programDriver.getProgram().getTypeChecker(), + ); this.symbolBuilderCache.set(component, builder); return builder; } @@ -605,33 +724,33 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return Array.from(resultingPipes.values()); } - getDirectiveMetadata(dir: ts.ClassDeclaration): TypeCheckableDirectiveMeta|null { + getDirectiveMetadata(dir: ts.ClassDeclaration): TypeCheckableDirectiveMeta | null { if (!isNamedClassDeclaration(dir)) { return null; } return this.typeCheckScopeRegistry.getTypeCheckDirectiveMetadata(new Reference(dir)); } - getNgModuleMetadata(module: ts.ClassDeclaration): NgModuleMeta|null { + getNgModuleMetadata(module: ts.ClassDeclaration): NgModuleMeta | null { if (!isNamedClassDeclaration(module)) { return null; } return this.metaReader.getNgModuleMetadata(new Reference(module)); } - getPipeMetadata(pipe: ts.ClassDeclaration): PipeMeta|null { + getPipeMetadata(pipe: ts.ClassDeclaration): PipeMeta | null { if (!isNamedClassDeclaration(pipe)) { return null; } return this.metaReader.getPipeMetadata(new Reference(pipe)); } - getPotentialElementTags(component: ts.ClassDeclaration): Map { + getPotentialElementTags(component: ts.ClassDeclaration): Map { if (this.elementTagCache.has(component)) { return this.elementTagCache.get(component)!; } - const tagMap = new Map(); + const tagMap = new Map(); for (const tag of REGISTRY.allKnownElementNames()) { tagMap.set(tag, null); @@ -660,19 +779,19 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return tagMap; } - getPotentialDomBindings(tagName: string): {attribute: string, property: string}[] { + getPotentialDomBindings(tagName: string): {attribute: string; property: string}[] { const attributes = REGISTRY.allKnownAttributesOfElement(tagName); - return attributes.map(attribute => ({ - attribute, - property: REGISTRY.getMappedPropName(attribute), - })); + return attributes.map((attribute) => ({ + attribute, + property: REGISTRY.getMappedPropName(attribute), + })); } getPotentialDomEvents(tagName: string): string[] { return REGISTRY.allKnownEventsOfElement(tagName); } - getPrimaryAngularDecorator(target: ts.ClassDeclaration): ts.Decorator|null { + getPrimaryAngularDecorator(target: ts.ClassDeclaration): ts.Decorator | null { this.ensureAllShimsForOneFile(target.getSourceFile()); if (!isNamedClassDeclaration(target)) { @@ -697,7 +816,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return null; } - getOwningNgModule(component: ts.ClassDeclaration): ts.ClassDeclaration|null { + getOwningNgModule(component: ts.ClassDeclaration): ts.ClassDeclaration | null { if (!isNamedClassDeclaration(component)) { return null; } @@ -708,8 +827,11 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { } const scope = this.componentScopeReader.getScopeForComponent(component); - if (scope === null || scope.kind !== ComponentScopeKind.NgModule || - !isNamedClassDeclaration(scope.ngModule)) { + if ( + scope === null || + scope.kind !== ComponentScopeKind.NgModule || + !isNamedClassDeclaration(scope.ngModule) + ) { return null; } @@ -717,8 +839,10 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { } private emit( - kind: PotentialImportKind, refTo: Reference, - inContext: ts.ClassDeclaration): PotentialImport|null { + kind: PotentialImportKind, + refTo: Reference, + inContext: ts.ClassDeclaration, + ): PotentialImport | null { const emittedRef = this.refEmitter.emit(refTo, inContext.getSourceFile()); if (emittedRef.kind === ReferenceEmitKind.Failed) { return null; @@ -732,11 +856,11 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { let isForwardReference = false; if (emitted.node.getStart() > inContext.getStart()) { - const declaration = this.programDriver.getProgram() - .getTypeChecker() - .getTypeAtLocation(emitted.node) - .getSymbol() - ?.declarations?.[0]; + const declaration = this.programDriver + .getProgram() + .getTypeChecker() + .getTypeAtLocation(emitted.node) + .getSymbol()?.declarations?.[0]; if (declaration && declaration.getSourceFile() === inContext.getSourceFile()) { isForwardReference = true; } @@ -744,8 +868,10 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { // An appropriate identifier is already in scope. return {kind, symbolName: emitted.node.text, isForwardReference}; } else if ( - emitted instanceof ExternalExpr && emitted.value.moduleName !== null && - emitted.value.name !== null) { + emitted instanceof ExternalExpr && + emitted.value.moduleName !== null && + emitted.value.name !== null + ) { return { kind, moduleSpecifier: emitted.value.moduleName, @@ -757,12 +883,14 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { } getPotentialImportsFor( - toImport: Reference, inContext: ts.ClassDeclaration, - importMode: PotentialImportMode): ReadonlyArray { + toImport: Reference, + inContext: ts.ClassDeclaration, + importMode: PotentialImportMode, + ): ReadonlyArray { const imports: PotentialImport[] = []; const meta = - this.metaReader.getDirectiveMetadata(toImport) ?? this.metaReader.getPipeMetadata(toImport); + this.metaReader.getDirectiveMetadata(toImport) ?? this.metaReader.getPipeMetadata(toImport); if (meta === null) { return imports; } @@ -787,7 +915,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return imports; } - private getScopeData(component: ts.ClassDeclaration): ScopeData|null { + private getScopeData(component: ts.ClassDeclaration): ScopeData | null { if (this.scopeCache.has(component)) { return this.scopeCache.get(component)!; } @@ -801,15 +929,18 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return null; } - const dependencies = scope.kind === ComponentScopeKind.NgModule ? - scope.compilation.dependencies : - scope.dependencies; + const dependencies = + scope.kind === ComponentScopeKind.NgModule + ? scope.compilation.dependencies + : scope.dependencies; const data: ScopeData = { directives: [], pipes: [], - isPoisoned: scope.kind === ComponentScopeKind.NgModule ? scope.compilation.isPoisoned : - scope.isPoisoned, + isPoisoned: + scope.kind === ComponentScopeKind.NgModule + ? scope.compilation.isPoisoned + : scope.isPoisoned, }; const typeChecker = this.programDriver.getProgram().getTypeChecker(); @@ -829,8 +960,10 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return data; } - private scopeDataOfDirectiveMeta(typeChecker: ts.TypeChecker, dep: DirectiveMeta): - Omit|null { + private scopeDataOfDirectiveMeta( + typeChecker: ts.TypeChecker, + dep: DirectiveMeta, + ): Omit | null { if (dep.selector === null) { // Skip this directive, it can't be added to a template anyway. return null; @@ -840,7 +973,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { return null; } - let ngModule: ClassDeclaration|null = null; + let ngModule: ClassDeclaration | null = null; const moduleScopeOfDir = this.componentScopeReader.getScopeForComponent(dep.ref.node); if (moduleScopeOfDir !== null && moduleScopeOfDir.kind === ComponentScopeKind.NgModule) { ngModule = moduleScopeOfDir.ngModule; @@ -856,8 +989,10 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { }; } - private scopeDataOfPipeMeta(typeChecker: ts.TypeChecker, dep: PipeMeta): - Omit|null { + private scopeDataOfPipeMeta( + typeChecker: ts.TypeChecker, + dep: PipeMeta, + ): Omit | null { const tsSymbol = typeChecker.getSymbolAtLocation(dep.ref.node.name); if (tsSymbol === undefined) { return null; @@ -871,7 +1006,9 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker { } function convertDiagnostic( - diag: ts.Diagnostic, sourceResolver: TemplateSourceResolver): TemplateDiagnostic|null { + diag: ts.Diagnostic, + sourceResolver: TemplateSourceResolver, +): TemplateDiagnostic | null { if (!shouldReportDiagnostic(diag)) { return null; } @@ -948,8 +1085,10 @@ class SingleFileTypeCheckingHost implements TypeCheckingHost { private seenInlines = false; constructor( - protected sfPath: AbsoluteFsPath, protected fileData: FileTypeCheckingData, - protected impl: TemplateTypeCheckerImpl) {} + protected sfPath: AbsoluteFsPath, + protected fileData: FileTypeCheckingData, + protected impl: TemplateTypeCheckerImpl, + ) {} private assertPath(sfPath: AbsoluteFsPath): void { if (this.sfPath !== sfPath) { @@ -1005,8 +1144,11 @@ class SingleFileTypeCheckingHost implements TypeCheckingHost { */ class SingleShimTypeCheckingHost extends SingleFileTypeCheckingHost { constructor( - sfPath: AbsoluteFsPath, fileData: FileTypeCheckingData, impl: TemplateTypeCheckerImpl, - private shimPath: AbsoluteFsPath) { + sfPath: AbsoluteFsPath, + fileData: FileTypeCheckingData, + impl: TemplateTypeCheckerImpl, + private shimPath: AbsoluteFsPath, + ) { super(sfPath, fileData, impl); } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/comments.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/comments.ts index a28dd0d8bb2eb..700ea71e162c7 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/comments.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/comments.ts @@ -18,19 +18,23 @@ const parseSpanComment = /^(\d+),(\d+)$/; * Will return `null` if no trailing comments on the node match the expected form of a source span. */ export function readSpanComment( - node: ts.Node, sourceFile: ts.SourceFile = node.getSourceFile()): AbsoluteSourceSpan|null { - return ts.forEachTrailingCommentRange(sourceFile.text, node.getEnd(), (pos, end, kind) => { - if (kind !== ts.SyntaxKind.MultiLineCommentTrivia) { - return null; - } - const commentText = sourceFile.text.substring(pos + 2, end - 2); - const match = commentText.match(parseSpanComment); - if (match === null) { - return null; - } + node: ts.Node, + sourceFile: ts.SourceFile = node.getSourceFile(), +): AbsoluteSourceSpan | null { + return ( + ts.forEachTrailingCommentRange(sourceFile.text, node.getEnd(), (pos, end, kind) => { + if (kind !== ts.SyntaxKind.MultiLineCommentTrivia) { + return null; + } + const commentText = sourceFile.text.substring(pos + 2, end - 2); + const match = commentText.match(parseSpanComment); + if (match === null) { + return null; + } - return new AbsoluteSourceSpan(+match[1], +match[2]); - }) || null; + return new AbsoluteSourceSpan(+match[1], +match[2]); + }) || null + ); } /** Used to identify what type the comment is. */ @@ -50,9 +54,11 @@ export enum ExpressionIdentifier { /** Tags the node with the given expression identifier. */ export function addExpressionIdentifier(node: ts.Node, identifier: ExpressionIdentifier) { ts.addSyntheticTrailingComment( - node, ts.SyntaxKind.MultiLineCommentTrivia, - `${CommentTriviaType.EXPRESSION_TYPE_IDENTIFIER}:${identifier}`, - /* hasTrailingNewLine */ false); + node, + ts.SyntaxKind.MultiLineCommentTrivia, + `${CommentTriviaType.EXPRESSION_TYPE_IDENTIFIER}:${identifier}`, + /* hasTrailingNewLine */ false, + ); } const IGNORE_FOR_DIAGNOSTICS_MARKER = `${CommentTriviaType.DIAGNOSTIC}:ignore`; @@ -63,24 +69,30 @@ const IGNORE_FOR_DIAGNOSTICS_MARKER = `${CommentTriviaType.DIAGNOSTIC}:ignore`; */ export function markIgnoreDiagnostics(node: ts.Node): void { ts.addSyntheticTrailingComment( - node, ts.SyntaxKind.MultiLineCommentTrivia, IGNORE_FOR_DIAGNOSTICS_MARKER, - /* hasTrailingNewLine */ false); + node, + ts.SyntaxKind.MultiLineCommentTrivia, + IGNORE_FOR_DIAGNOSTICS_MARKER, + /* hasTrailingNewLine */ false, + ); } /** Returns true if the node has a marker that indicates diagnostics errors should be ignored. */ export function hasIgnoreForDiagnosticsMarker(node: ts.Node, sourceFile: ts.SourceFile): boolean { - return ts.forEachTrailingCommentRange(sourceFile.text, node.getEnd(), (pos, end, kind) => { - if (kind !== ts.SyntaxKind.MultiLineCommentTrivia) { - return null; - } - const commentText = sourceFile.text.substring(pos + 2, end - 2); - return commentText === IGNORE_FOR_DIAGNOSTICS_MARKER; - }) === true; + return ( + ts.forEachTrailingCommentRange(sourceFile.text, node.getEnd(), (pos, end, kind) => { + if (kind !== ts.SyntaxKind.MultiLineCommentTrivia) { + return null; + } + const commentText = sourceFile.text.substring(pos + 2, end - 2); + return commentText === IGNORE_FOR_DIAGNOSTICS_MARKER; + }) === true + ); } -function makeRecursiveVisitor(visitor: (node: ts.Node) => T | null): - (node: ts.Node) => T | undefined { - function recursiveVisitor(node: ts.Node): T|undefined { +function makeRecursiveVisitor( + visitor: (node: ts.Node) => T | null, +): (node: ts.Node) => T | undefined { + function recursiveVisitor(node: ts.Node): T | undefined { const res = visitor(node); return res !== null ? res : node.forEachChild(recursiveVisitor); } @@ -90,11 +102,11 @@ function makeRecursiveVisitor(visitor: (node: ts.Node) => T | export interface FindOptions { filter: (node: ts.Node) => node is T; withExpressionIdentifier?: ExpressionIdentifier; - withSpan?: AbsoluteSourceSpan|ParseSourceSpan; + withSpan?: AbsoluteSourceSpan | ParseSourceSpan; } function getSpanFromOptions(opts: FindOptions) { - let withSpan: {start: number, end: number}|null = null; + let withSpan: {start: number; end: number} | null = null; if (opts.withSpan !== undefined) { if (opts.withSpan instanceof AbsoluteSourceSpan) { withSpan = opts.withSpan; @@ -111,12 +123,14 @@ function getSpanFromOptions(opts: FindOptions) { * * Returns `null` when no `ts.Node` matches the given conditions. */ -export function findFirstMatchingNode(tcb: ts.Node, opts: FindOptions): T| - null { +export function findFirstMatchingNode( + tcb: ts.Node, + opts: FindOptions, +): T | null { const withSpan = getSpanFromOptions(opts); const withExpressionIdentifier = opts.withExpressionIdentifier; const sf = tcb.getSourceFile(); - const visitor = makeRecursiveVisitor(node => { + const visitor = makeRecursiveVisitor((node) => { if (!opts.filter(node)) { return null; } @@ -126,8 +140,10 @@ export function findFirstMatchingNode(tcb: ts.Node, opts: Fin return null; } } - if (withExpressionIdentifier !== undefined && - !hasExpressionIdentifier(sf, node, withExpressionIdentifier)) { + if ( + withExpressionIdentifier !== undefined && + !hasExpressionIdentifier(sf, node, withExpressionIdentifier) + ) { return null; } return node; @@ -164,8 +180,10 @@ export function findAllMatchingNodes(tcb: ts.Node, opts: Find continue; } } - if (withExpressionIdentifier !== undefined && - !hasExpressionIdentifier(sf, node, withExpressionIdentifier)) { + if ( + withExpressionIdentifier !== undefined && + !hasExpressionIdentifier(sf, node, withExpressionIdentifier) + ) { continue; } @@ -176,12 +194,17 @@ export function findAllMatchingNodes(tcb: ts.Node, opts: Find } export function hasExpressionIdentifier( - sourceFile: ts.SourceFile, node: ts.Node, identifier: ExpressionIdentifier): boolean { - return ts.forEachTrailingCommentRange(sourceFile.text, node.getEnd(), (pos, end, kind) => { - if (kind !== ts.SyntaxKind.MultiLineCommentTrivia) { - return false; - } - const commentText = sourceFile.text.substring(pos + 2, end - 2); - return commentText === `${CommentTriviaType.EXPRESSION_TYPE_IDENTIFIER}:${identifier}`; - }) || false; + sourceFile: ts.SourceFile, + node: ts.Node, + identifier: ExpressionIdentifier, +): boolean { + return ( + ts.forEachTrailingCommentRange(sourceFile.text, node.getEnd(), (pos, end, kind) => { + if (kind !== ts.SyntaxKind.MultiLineCommentTrivia) { + return false; + } + const commentText = sourceFile.text.substring(pos + 2, end - 2); + return commentText === `${CommentTriviaType.EXPRESSION_TYPE_IDENTIFIER}:${identifier}`; + }) || false + ); } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/completion.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/completion.ts index dec6ec016de78..52d0b64cc30f9 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/completion.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/completion.ts @@ -6,11 +6,29 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, EmptyExpr, ImplicitReceiver, LiteralPrimitive, PropertyRead, PropertyWrite, SafePropertyRead, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute} from '@angular/compiler'; +import { + AST, + EmptyExpr, + ImplicitReceiver, + LiteralPrimitive, + PropertyRead, + PropertyWrite, + SafePropertyRead, + TmplAstNode, + TmplAstReference, + TmplAstTemplate, + TmplAstTextAttribute, +} from '@angular/compiler'; import ts from 'typescript'; import {AbsoluteFsPath} from '../../file_system'; -import {CompletionKind, GlobalCompletion, ReferenceCompletion, TcbLocation, VariableCompletion} from '../api'; +import { + CompletionKind, + GlobalCompletion, + ReferenceCompletion, + TcbLocation, + VariableCompletion, +} from '../api'; import {ExpressionIdentifier, findFirstMatchingNode} from './comments'; import {TemplateData} from './context'; @@ -22,26 +40,32 @@ import {TemplateData} from './context'; * surrounding TS program have changed. */ export class CompletionEngine { - private componentContext: TcbLocation|null; + private componentContext: TcbLocation | null; /** * Cache of completions for various levels of the template, including the root template (`null`). * Memoizes `getTemplateContextCompletions`. */ - private templateContextCache = - new Map>(); - - private expressionCompletionCache = - new Map(); + private templateContextCache = new Map< + TmplAstTemplate | null, + Map + >(); + private expressionCompletionCache = new Map< + PropertyRead | SafePropertyRead | LiteralPrimitive | TmplAstTextAttribute, + TcbLocation + >(); constructor( - private tcb: ts.Node, private data: TemplateData, private tcbPath: AbsoluteFsPath, - private tcbIsShim: boolean) { + private tcb: ts.Node, + private data: TemplateData, + private tcbPath: AbsoluteFsPath, + private tcbIsShim: boolean, + ) { // Find the component completion expression within the TCB. This looks like: `ctx. /* ... */;` const globalRead = findFirstMatchingNode(this.tcb, { filter: ts.isPropertyAccessExpression, - withExpressionIdentifier: ExpressionIdentifier.COMPONENT_COMPLETION + withExpressionIdentifier: ExpressionIdentifier.COMPONENT_COMPLETION, }); if (globalRead !== null) { @@ -66,8 +90,10 @@ export class CompletionEngine { * template context. * @param node the given AST node */ - getGlobalCompletions(context: TmplAstTemplate|null, node: AST|TmplAstNode): GlobalCompletion - |null { + getGlobalCompletions( + context: TmplAstTemplate | null, + node: AST | TmplAstNode, + ): GlobalCompletion | null { if (this.componentContext === null) { return null; } @@ -77,7 +103,7 @@ export class CompletionEngine { return null; } - let nodeContext: TcbLocation|null = null; + let nodeContext: TcbLocation | null = null; if (node instanceof EmptyExpr) { const nodeLocation = findFirstMatchingNode(this.tcb, { filter: ts.isIdentifier, @@ -113,14 +139,15 @@ export class CompletionEngine { }; } - getExpressionCompletionLocation(expr: PropertyRead|PropertyWrite|SafePropertyRead): TcbLocation - |null { + getExpressionCompletionLocation( + expr: PropertyRead | PropertyWrite | SafePropertyRead, + ): TcbLocation | null { if (this.expressionCompletionCache.has(expr)) { return this.expressionCompletionCache.get(expr)!; } // Completion works inside property reads and method calls. - let tsExpr: ts.PropertyAccessExpression|null = null; + let tsExpr: ts.PropertyAccessExpression | null = null; if (expr instanceof PropertyRead || expr instanceof PropertyWrite) { // Non-safe navigation operations are trivial: `foo.bar` or `foo.bar()` tsExpr = findFirstMatchingNode(this.tcb, { @@ -142,7 +169,9 @@ export class CompletionEngine { if (ts.isPropertyAccessExpression(whenTrue)) { tsExpr = whenTrue; } else if ( - ts.isCallExpression(whenTrue) && ts.isPropertyAccessExpression(whenTrue.expression)) { + ts.isCallExpression(whenTrue) && + ts.isPropertyAccessExpression(whenTrue.expression) + ) { tsExpr = whenTrue.expression; } } @@ -160,12 +189,12 @@ export class CompletionEngine { return res; } - getLiteralCompletionLocation(expr: LiteralPrimitive|TmplAstTextAttribute): TcbLocation|null { + getLiteralCompletionLocation(expr: LiteralPrimitive | TmplAstTextAttribute): TcbLocation | null { if (this.expressionCompletionCache.has(expr)) { return this.expressionCompletionCache.get(expr)!; } - let tsExpr: ts.StringLiteral|ts.NumericLiteral|null = null; + let tsExpr: ts.StringLiteral | ts.NumericLiteral | null = null; if (expr instanceof TmplAstTextAttribute) { const strNode = findFirstMatchingNode(this.tcb, { @@ -178,7 +207,7 @@ export class CompletionEngine { } else { tsExpr = findFirstMatchingNode(this.tcb, { filter: (n: ts.Node): n is ts.NumericLiteral | ts.StringLiteral => - ts.isStringLiteral(n) || ts.isNumericLiteral(n), + ts.isStringLiteral(n) || ts.isNumericLiteral(n), withSpan: expr.sourceSpan, }); } @@ -205,13 +234,14 @@ export class CompletionEngine { * Get global completions within the given template context - either a `TmplAstTemplate` embedded * view, or `null` for the root context. */ - private getTemplateContextCompletions(context: TmplAstTemplate|null): - Map|null { + private getTemplateContextCompletions( + context: TmplAstTemplate | null, + ): Map | null { if (this.templateContextCache.has(context)) { return this.templateContextCache.get(context)!; } - const templateContext = new Map(); + const templateContext = new Map(); // The bound template already has details about the references and variables in scope in the // `context` template - they just need to be converted to `Completion`s. diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts index cf743b18ec211..0cc0ece7242d8 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/context.ts @@ -6,7 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import {BoundTarget, ParseError, ParseSourceFile, R3TargetBinder, SchemaMetadata, TmplAstNode} from '@angular/compiler'; +import { + BoundTarget, + ParseError, + ParseSourceFile, + R3TargetBinder, + SchemaMetadata, + TmplAstNode, +} from '@angular/compiler'; import MagicString from 'magic-string'; import ts from 'typescript'; @@ -18,7 +25,16 @@ import {PerfEvent, PerfRecorder} from '../../perf'; import {FileUpdate} from '../../program_driver'; import {ClassDeclaration, ReflectionHost} from '../../reflection'; import {ImportManager} from '../../translator'; -import {TemplateDiagnostic, TemplateId, TemplateSourceMapping, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata, TypeCheckContext, TypeCheckingConfig, TypeCtorMetadata} from '../api'; +import { + TemplateDiagnostic, + TemplateId, + TemplateSourceMapping, + TypeCheckableDirectiveMeta, + TypeCheckBlockMetadata, + TypeCheckContext, + TypeCheckingConfig, + TypeCtorMetadata, +} from '../api'; import {makeTemplateDiagnostic} from '../diagnostics'; import {DomSchemaChecker, RegistryDomSchemaChecker} from './dom'; @@ -115,7 +131,6 @@ export interface PendingShimData { */ file: TypeCheckFile; - /** * Map of `TemplateId` to information collected about the template as it's ingested. */ @@ -181,10 +196,14 @@ export class TypeCheckContextImpl implements TypeCheckContext { private fileMap = new Map(); constructor( - private config: TypeCheckingConfig, - private compilerHost: Pick, - private refEmitter: ReferenceEmitter, private reflector: ReflectionHost, - private host: TypeCheckingHost, private inlining: InliningMode, private perf: PerfRecorder) { + private config: TypeCheckingConfig, + private compilerHost: Pick, + private refEmitter: ReferenceEmitter, + private reflector: ReflectionHost, + private host: TypeCheckingHost, + private inlining: InliningMode, + private perf: PerfRecorder, + ) { if (inlining === InliningMode.Error && config.useInlineTypeConstructors) { // We cannot use inlining for type checking since this environment does not support it. throw new Error(`AssertionError: invalid inlining configuration.`); @@ -209,11 +228,17 @@ export class TypeCheckContextImpl implements TypeCheckContext { * Implements `TypeCheckContext.addTemplate`. */ addTemplate( - ref: Reference>, - binder: R3TargetBinder, template: TmplAstNode[], - pipes: Map, schemas: SchemaMetadata[], sourceMapping: TemplateSourceMapping, - file: ParseSourceFile, parseErrors: ParseError[]|null, isStandalone: boolean, - preserveWhitespaces: boolean): void { + ref: Reference>, + binder: R3TargetBinder, + template: TmplAstNode[], + pipes: Map, + schemas: SchemaMetadata[], + sourceMapping: TemplateSourceMapping, + file: ParseSourceFile, + parseErrors: ParseError[] | null, + isStandalone: boolean, + preserveWhitespaces: boolean, + ): void { if (!this.host.shouldCheckComponent(ref.node)) { return; } @@ -226,7 +251,8 @@ export class TypeCheckContextImpl implements TypeCheckContext { if (parseErrors !== null) { templateDiagnostics.push( - ...this.getTemplateDiagnostics(parseErrors, templateId, sourceMapping)); + ...this.getTemplateDiagnostics(parseErrors, templateId, sourceMapping), + ); } const boundTarget = binder.bind({template}); @@ -273,13 +299,19 @@ export class TypeCheckContextImpl implements TypeCheckContext { usedPipes.push(pipes.get(name)!.ref as Reference>); } - const inliningRequirement = - requiresInlineTypeCheckBlock(ref, shimData.file, usedPipes, this.reflector); + const inliningRequirement = requiresInlineTypeCheckBlock( + ref, + shimData.file, + usedPipes, + this.reflector, + ); // If inlining is not supported, but is required for either the TCB or one of its directive // dependencies, then exit here with an error. - if (this.inlining === InliningMode.Error && - inliningRequirement === TcbInliningRequirement.MustInline) { + if ( + this.inlining === InliningMode.Error && + inliningRequirement === TcbInliningRequirement.MustInline + ) { // This template cannot be supported because the underlying strategy does not support inlining // and inlining would be required. @@ -300,26 +332,37 @@ export class TypeCheckContextImpl implements TypeCheckContext { preserveWhitespaces, }; this.perf.eventCount(PerfEvent.GenerateTcb); - if (inliningRequirement !== TcbInliningRequirement.None && - this.inlining === InliningMode.InlineOps) { + if ( + inliningRequirement !== TcbInliningRequirement.None && + this.inlining === InliningMode.InlineOps + ) { // This class didn't meet the requirements for external type checking, so generate an inline // TCB for the class. this.addInlineTypeCheckBlock(fileData, shimData, ref, meta); } else if ( - inliningRequirement === TcbInliningRequirement.ShouldInlineForGenericBounds && - this.inlining === InliningMode.Error) { + inliningRequirement === TcbInliningRequirement.ShouldInlineForGenericBounds && + this.inlining === InliningMode.Error + ) { // It's suggested that this TCB should be generated inline due to the component's generic // bounds, but inlining is not supported by the current environment. Use a non-inline type // check block, but fall back to `any` generic parameters since the generic bounds can't be // referenced in that context. This will infer a less useful type for the component, but allow // for type-checking it in an environment where that would not be possible otherwise. shimData.file.addTypeCheckBlock( - ref, meta, shimData.domSchemaChecker, shimData.oobRecorder, - TcbGenericContextBehavior.FallbackToAny); + ref, + meta, + shimData.domSchemaChecker, + shimData.oobRecorder, + TcbGenericContextBehavior.FallbackToAny, + ); } else { shimData.file.addTypeCheckBlock( - ref, meta, shimData.domSchemaChecker, shimData.oobRecorder, - TcbGenericContextBehavior.UseEmitter); + ref, + meta, + shimData.domSchemaChecker, + shimData.oobRecorder, + TcbGenericContextBehavior.UseEmitter, + ); } } @@ -327,8 +370,11 @@ export class TypeCheckContextImpl implements TypeCheckContext { * Record a type constructor for the given `node` with the given `ctorMetadata`. */ addInlineTypeCtor( - fileData: PendingFileTypeCheckingData, sf: ts.SourceFile, - ref: Reference>, ctorMeta: TypeCtorMetadata): void { + fileData: PendingFileTypeCheckingData, + sf: ts.SourceFile, + ref: Reference>, + ctorMeta: TypeCtorMetadata, + ): void { if (this.typeCtorPending.has(ref.node)) { return; } @@ -351,7 +397,7 @@ export class TypeCheckContextImpl implements TypeCheckContext { * If this particular `ts.SourceFile` requires changes, the text representing its new contents * will be returned. Otherwise, a `null` return indicates no changes were necessary. */ - transform(sf: ts.SourceFile): string|null { + transform(sf: ts.SourceFile): string | null { // If there are no operations pending for this particular file, return `null` to indicate no // changes. if (!this.opMap.has(sf)) { @@ -373,19 +419,20 @@ export class TypeCheckContextImpl implements TypeCheckContext { // Execute ops. // Each Op has a splitPoint index into the text where it needs to be inserted. - const updates: {pos: number, deletePos?: number, text: string}[] = - this.opMap.get(sf)!.map(op => { - return { - pos: op.splitPoint, - text: op.execute(importManager, sf, this.refEmitter, printer), - }; - }); + const updates: {pos: number; deletePos?: number; text: string}[] = this.opMap + .get(sf)! + .map((op) => { + return { + pos: op.splitPoint, + text: op.execute(importManager, sf, this.refEmitter, printer), + }; + }); const {newImports, updatedImports} = importManager.finalize(); // Capture new imports if (newImports.has(sf.fileName)) { - newImports.get(sf.fileName)!.forEach(newImport => { + newImports.get(sf.fileName)!.forEach((newImport) => { updates.push({ pos: 0, text: printer.printNode(ts.EmitHint.Unspecified, newImport, sf), @@ -455,17 +502,26 @@ export class TypeCheckContextImpl implements TypeCheckContext { } private addInlineTypeCheckBlock( - fileData: PendingFileTypeCheckingData, shimData: PendingShimData, - ref: Reference>, - tcbMeta: TypeCheckBlockMetadata): void { + fileData: PendingFileTypeCheckingData, + shimData: PendingShimData, + ref: Reference>, + tcbMeta: TypeCheckBlockMetadata, + ): void { const sf = ref.node.getSourceFile(); if (!this.opMap.has(sf)) { this.opMap.set(sf, []); } const ops = this.opMap.get(sf)!; - ops.push(new InlineTcbOp( - ref, tcbMeta, this.config, this.reflector, shimData.domSchemaChecker, - shimData.oobRecorder)); + ops.push( + new InlineTcbOp( + ref, + tcbMeta, + this.config, + this.reflector, + shimData.domSchemaChecker, + shimData.oobRecorder, + ), + ); fileData.hasInlines = true; } @@ -477,7 +533,12 @@ export class TypeCheckContextImpl implements TypeCheckContext { domSchemaChecker: new RegistryDomSchemaChecker(fileData.sourceManager), oobRecorder: new OutOfBandDiagnosticRecorderImpl(fileData.sourceManager), file: new TypeCheckFile( - shimPath, this.config, this.refEmitter, this.reflector, this.compilerHost), + shimPath, + this.config, + this.refEmitter, + this.reflector, + this.compilerHost, + ), templates: new Map(), }); } @@ -500,9 +561,11 @@ export class TypeCheckContextImpl implements TypeCheckContext { } private getTemplateDiagnostics( - parseErrors: ParseError[], templateId: TemplateId, - sourceMapping: TemplateSourceMapping): TemplateDiagnostic[] { - return parseErrors.map(error => { + parseErrors: ParseError[], + templateId: TemplateId, + sourceMapping: TemplateSourceMapping, + ): TemplateDiagnostic[] { + return parseErrors.map((error) => { const span = error.span; if (span.start.offset === span.end.offset) { @@ -514,8 +577,13 @@ export class TypeCheckContextImpl implements TypeCheckContext { } return makeTemplateDiagnostic( - templateId, sourceMapping, span, ts.DiagnosticCategory.Error, - ngErrorCode(ErrorCode.TEMPLATE_PARSE_ERROR), error.msg); + templateId, + sourceMapping, + span, + ts.DiagnosticCategory.Error, + ngErrorCode(ErrorCode.TEMPLATE_PARSE_ERROR), + error.msg, + ); }); } } @@ -537,8 +605,12 @@ interface Op { /** * Execute the operation and return the generated code as text. */ - execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter, printer: ts.Printer): - string; + execute( + im: ImportManager, + sf: ts.SourceFile, + refEmitter: ReferenceEmitter, + printer: ts.Printer, + ): string; } /** @@ -546,10 +618,13 @@ interface Op { */ class InlineTcbOp implements Op { constructor( - readonly ref: Reference>, - readonly meta: TypeCheckBlockMetadata, readonly config: TypeCheckingConfig, - readonly reflector: ReflectionHost, readonly domSchemaChecker: DomSchemaChecker, - readonly oobRecorder: OutOfBandDiagnosticRecorder) {} + readonly ref: Reference>, + readonly meta: TypeCheckBlockMetadata, + readonly config: TypeCheckingConfig, + readonly reflector: ReflectionHost, + readonly domSchemaChecker: DomSchemaChecker, + readonly oobRecorder: OutOfBandDiagnosticRecorder, + ) {} /** * Type check blocks are inserted immediately after the end of the component class. @@ -558,16 +633,26 @@ class InlineTcbOp implements Op { return this.ref.node.end + 1; } - execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter, printer: ts.Printer): - string { + execute( + im: ImportManager, + sf: ts.SourceFile, + refEmitter: ReferenceEmitter, + printer: ts.Printer, + ): string { const env = new Environment(this.config, im, refEmitter, this.reflector, sf); const fnName = ts.factory.createIdentifier(`_tcb_${this.ref.node.pos}`); // 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. const fn = generateTypeCheckBlock( - env, this.ref, fnName, this.meta, this.domSchemaChecker, this.oobRecorder, - TcbGenericContextBehavior.CopyClassNodes); + env, + this.ref, + fnName, + this.meta, + this.domSchemaChecker, + this.oobRecorder, + TcbGenericContextBehavior.CopyClassNodes, + ); return printer.printNode(ts.EmitHint.Unspecified, fn, sf); } @@ -578,8 +663,10 @@ class InlineTcbOp implements Op { */ class TypeCtorOp implements Op { constructor( - readonly ref: Reference>, - readonly reflector: ReflectionHost, readonly meta: TypeCtorMetadata) {} + readonly ref: Reference>, + readonly reflector: ReflectionHost, + readonly meta: TypeCtorMetadata, + ) {} /** * Type constructor operations are inserted immediately before the end of the directive class. @@ -588,8 +675,12 @@ class TypeCtorOp implements Op { return this.ref.node.end - 1; } - execute(im: ImportManager, sf: ts.SourceFile, refEmitter: ReferenceEmitter, printer: ts.Printer): - string { + execute( + im: ImportManager, + sf: ts.SourceFile, + refEmitter: ReferenceEmitter, + printer: ts.Printer, + ): string { const emitEnv = new ReferenceEmitEnvironment(im, refEmitter, this.reflector, sf); const tcb = generateInlineTypeCtor(emitEnv, this.ref.node, this.meta); return printer.printNode(ts.EmitHint.Unspecified, tcb, sf); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/diagnostics.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/diagnostics.ts index fee9cfc868633..3fe00c913882f 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/diagnostics.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/diagnostics.ts @@ -13,7 +13,6 @@ import {makeTemplateDiagnostic} from '../diagnostics'; import {getTemplateMapping, TemplateSourceResolver} from './tcb_util'; - /** * Wraps the node in parenthesis such that inserted span comments become attached to the proper * node. This is an alias for `ts.factory.createParenthesizedExpression` with the benefit that it @@ -43,7 +42,7 @@ export function wrapForTypeChecker(expr: ts.Expression): ts.Expression { * Adds a synthetic comment to the expression that represents the parse span of the provided node. * This comment can later be retrieved as trivia of a node to recover original source locations. */ -export function addParseSpanInfo(node: ts.Node, span: AbsoluteSourceSpan|ParseSourceSpan): void { +export function addParseSpanInfo(node: ts.Node, span: AbsoluteSourceSpan | ParseSourceSpan): void { let commentText: string; if (span instanceof AbsoluteSourceSpan) { commentText = `${span.start},${span.end}`; @@ -51,7 +50,11 @@ export function addParseSpanInfo(node: ts.Node, span: AbsoluteSourceSpan|ParseSo commentText = `${span.start.offset},${span.end.offset}`; } ts.addSyntheticTrailingComment( - node, ts.SyntaxKind.MultiLineCommentTrivia, commentText, /* hasTrailingNewLine */ false); + node, + ts.SyntaxKind.MultiLineCommentTrivia, + commentText, + /* hasTrailingNewLine */ false, + ); } /** @@ -90,18 +93,29 @@ export function shouldReportDiagnostic(diagnostic: ts.Diagnostic): boolean { * file from being reported as type-check errors. */ export function translateDiagnostic( - diagnostic: ts.Diagnostic, resolver: TemplateSourceResolver): TemplateDiagnostic|null { + diagnostic: ts.Diagnostic, + resolver: TemplateSourceResolver, +): TemplateDiagnostic | null { if (diagnostic.file === undefined || diagnostic.start === undefined) { return null; } const fullMapping = getTemplateMapping( - diagnostic.file, diagnostic.start, resolver, /*isDiagnosticsRequest*/ true); + diagnostic.file, + diagnostic.start, + resolver, + /*isDiagnosticsRequest*/ true, + ); if (fullMapping === null) { return null; } const {sourceLocation, templateSourceMapping, span} = fullMapping; return makeTemplateDiagnostic( - sourceLocation.id, templateSourceMapping, span, diagnostic.category, diagnostic.code, - diagnostic.messageText); + sourceLocation.id, + templateSourceMapping, + span, + diagnostic.category, + diagnostic.code, + diagnostic.messageText, + ); } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/dom.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/dom.ts index ead2ddf7622b1..9b58d99223c7c 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/dom.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/dom.ts @@ -6,7 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {DomElementSchemaRegistry, ParseSourceSpan, SchemaMetadata, TmplAstElement} from '@angular/compiler'; +import { + DomElementSchemaRegistry, + ParseSourceSpan, + SchemaMetadata, + TmplAstElement, +} from '@angular/compiler'; import ts from 'typescript'; import {ErrorCode, ngErrorCode} from '../../diagnostics'; @@ -45,8 +50,11 @@ export interface DomSchemaChecker { * component. */ checkElement( - id: string, element: TmplAstElement, schemas: SchemaMetadata[], - hostIsStandalone: boolean): void; + id: string, + element: TmplAstElement, + schemas: SchemaMetadata[], + hostIsStandalone: boolean, + ): void; /** * Check a property binding on an element and record any diagnostics about it. @@ -60,8 +68,13 @@ export interface DomSchemaChecker { * property. */ checkProperty( - id: string, element: TmplAstElement, name: string, span: ParseSourceSpan, - schemas: SchemaMetadata[], hostIsStandalone: boolean): void; + id: string, + element: TmplAstElement, + name: string, + span: ParseSourceSpan, + schemas: SchemaMetadata[], + hostIsStandalone: boolean, + ): void; } /** @@ -78,8 +91,11 @@ export class RegistryDomSchemaChecker implements DomSchemaChecker { constructor(private resolver: TemplateSourceResolver) {} checkElement( - id: TemplateId, element: TmplAstElement, schemas: SchemaMetadata[], - hostIsStandalone: boolean): void { + id: TemplateId, + element: TmplAstElement, + schemas: SchemaMetadata[], + hostIsStandalone: boolean, + ): void { // HTML elements inside an SVG `foreignObject` are declared in the `xhtml` namespace. // We need to strip it before handing it over to the registry because all HTML tag names // in the registry are without a namespace. @@ -91,54 +107,65 @@ export class RegistryDomSchemaChecker implements DomSchemaChecker { const schemas = `'${hostIsStandalone ? '@Component' : '@NgModule'}.schemas'`; let errorMsg = `'${name}' is not a known element:\n`; errorMsg += `1. If '${name}' is an Angular component, then verify that it is ${ - hostIsStandalone ? 'included in the \'@Component.imports\' of this component' : - 'part of this module'}.\n`; + hostIsStandalone + ? "included in the '@Component.imports' of this component" + : 'part of this module' + }.\n`; if (name.indexOf('-') > -1) { - errorMsg += `2. If '${name}' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the ${ - schemas} of this component to suppress this message.`; + errorMsg += `2. If '${name}' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the ${schemas} of this component to suppress this message.`; } else { - errorMsg += - `2. To allow any element add 'NO_ERRORS_SCHEMA' to the ${schemas} of this component.`; + errorMsg += `2. To allow any element add 'NO_ERRORS_SCHEMA' to the ${schemas} of this component.`; } const diag = makeTemplateDiagnostic( - id, mapping, element.startSourceSpan, ts.DiagnosticCategory.Error, - ngErrorCode(ErrorCode.SCHEMA_INVALID_ELEMENT), errorMsg); + id, + mapping, + element.startSourceSpan, + ts.DiagnosticCategory.Error, + ngErrorCode(ErrorCode.SCHEMA_INVALID_ELEMENT), + errorMsg, + ); this._diagnostics.push(diag); } } checkProperty( - id: TemplateId, element: TmplAstElement, name: string, span: ParseSourceSpan, - schemas: SchemaMetadata[], hostIsStandalone: boolean): void { + id: TemplateId, + element: TmplAstElement, + name: string, + span: ParseSourceSpan, + schemas: SchemaMetadata[], + hostIsStandalone: boolean, + ): void { if (!REGISTRY.hasProperty(element.name, name, schemas)) { const mapping = this.resolver.getSourceMapping(id); const decorator = hostIsStandalone ? '@Component' : '@NgModule'; const schemas = `'${decorator}.schemas'`; - let errorMsg = - `Can't bind to '${name}' since it isn't a known property of '${element.name}'.`; + let errorMsg = `Can't bind to '${name}' since it isn't a known property of '${element.name}'.`; if (element.name.startsWith('ng-')) { - errorMsg += `\n1. If '${name}' is an Angular directive, then add 'CommonModule' to the '${ - decorator}.imports' of this component.` + - `\n2. To allow any property add 'NO_ERRORS_SCHEMA' to the ${ - schemas} of this component.`; + errorMsg += + `\n1. If '${name}' is an Angular directive, then add 'CommonModule' to the '${decorator}.imports' of this component.` + + `\n2. To allow any property add 'NO_ERRORS_SCHEMA' to the ${schemas} of this component.`; } else if (element.name.indexOf('-') > -1) { errorMsg += - `\n1. If '${element.name}' is an Angular component and it has '${ - name}' input, then verify that it is ${ - hostIsStandalone ? 'included in the \'@Component.imports\' of this component' : - 'part of this module'}.` + - `\n2. If '${ - element.name}' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the ${ - schemas} of this component to suppress this message.` + - `\n3. To allow any property add 'NO_ERRORS_SCHEMA' to the ${ - schemas} of this component.`; + `\n1. If '${element.name}' is an Angular component and it has '${name}' input, then verify that it is ${ + hostIsStandalone + ? "included in the '@Component.imports' of this component" + : 'part of this module' + }.` + + `\n2. If '${element.name}' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the ${schemas} of this component to suppress this message.` + + `\n3. To allow any property add 'NO_ERRORS_SCHEMA' to the ${schemas} of this component.`; } const diag = makeTemplateDiagnostic( - id, mapping, span, ts.DiagnosticCategory.Error, - ngErrorCode(ErrorCode.SCHEMA_INVALID_ATTRIBUTE), errorMsg); + id, + mapping, + span, + ts.DiagnosticCategory.Error, + ngErrorCode(ErrorCode.SCHEMA_INVALID_ATTRIBUTE), + errorMsg, + ); this._diagnostics.push(diag); } } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts index d1b184d49bcb2..c8a176904a95f 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/environment.ts @@ -8,7 +8,12 @@ import ts from 'typescript'; -import {assertSuccessfulReferenceEmit, ImportFlags, Reference, ReferenceEmitter} from '../../imports'; +import { + assertSuccessfulReferenceEmit, + ImportFlags, + Reference, + ReferenceEmitter, +} from '../../imports'; import {ClassDeclaration, ReflectionHost} from '../../reflection'; import {ImportManager, translateExpression} from '../../translator'; import {TypeCheckableDirectiveMeta, TypeCheckingConfig, TypeCtorMetadata} from '../api'; @@ -42,8 +47,12 @@ export class Environment extends ReferenceEmitEnvironment { protected pipeInstStatements: ts.Statement[] = []; constructor( - readonly config: TypeCheckingConfig, importManager: ImportManager, - refEmitter: ReferenceEmitter, reflector: ReflectionHost, contextFile: ts.SourceFile) { + readonly config: TypeCheckingConfig, + importManager: ImportManager, + refEmitter: ReferenceEmitter, + reflector: ReflectionHost, + contextFile: ts.SourceFile, + ) { super(importManager, refEmitter, reflector, contextFile); } @@ -126,16 +135,14 @@ export class Environment extends ReferenceEmitEnvironment { return translateExpression(this.contextFile, ngExpr.expression, this.importManager); } - private emitTypeParameters(declaration: ClassDeclaration): - ts.TypeParameterDeclaration[]|undefined { + private emitTypeParameters( + declaration: ClassDeclaration, + ): ts.TypeParameterDeclaration[] | undefined { const emitter = new TypeParameterEmitter(declaration.typeParameters, this.reflector); - return emitter.emit(ref => this.referenceType(ref)); + return emitter.emit((ref) => this.referenceType(ref)); } getPreludeStatements(): ts.Statement[] { - return [ - ...this.pipeInstStatements, - ...this.typeCtorStatements, - ]; + return [...this.pipeInstStatements, ...this.typeCtorStatements]; } } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts index ee39ae51c8942..29608617b27ad 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts @@ -6,7 +6,33 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Call, Chain, Conditional, EmptyExpr, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, SafeCall, SafeKeyedRead, SafePropertyRead, ThisReceiver, Unary} from '@angular/compiler'; +import { + AST, + AstVisitor, + ASTWithSource, + Binary, + BindingPipe, + Call, + Chain, + Conditional, + EmptyExpr, + ImplicitReceiver, + Interpolation, + KeyedRead, + KeyedWrite, + LiteralArray, + LiteralMap, + LiteralPrimitive, + NonNullAssert, + PrefixNot, + PropertyRead, + PropertyWrite, + SafeCall, + SafeKeyedRead, + SafePropertyRead, + ThisReceiver, + Unary, +} from '@angular/compiler'; import ts from 'typescript'; import {TypeCheckingConfig} from '../api'; @@ -15,7 +41,9 @@ import {addParseSpanInfo, wrapForDiagnostics, wrapForTypeChecker} from './diagno import {tsCastToAny, tsNumericExpression} from './ts_util'; export const NULL_AS_ANY = ts.factory.createAsExpression( - ts.factory.createNull(), ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); + ts.factory.createNull(), + ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), +); const UNDEFINED = ts.factory.createIdentifier('undefined'); const UNARY_OPS = new Map([ @@ -49,16 +77,19 @@ const BINARY_OPS = new Map([ * AST. */ export function astToTypescript( - ast: AST, maybeResolve: (ast: AST) => (ts.Expression | null), - config: TypeCheckingConfig): ts.Expression { + ast: AST, + maybeResolve: (ast: AST) => ts.Expression | null, + config: TypeCheckingConfig, +): ts.Expression { const translator = new AstTranslator(maybeResolve, config); return translator.translate(ast); } class AstTranslator implements AstVisitor { constructor( - private maybeResolve: (ast: AST) => (ts.Expression | null), - private config: TypeCheckingConfig) {} + private maybeResolve: (ast: AST) => ts.Expression | null, + private config: TypeCheckingConfig, + ) {} translate(ast: AST): ts.Expression { // Skip over an `ASTWithSource` as its `visit` method calls directly into its ast's `visit`, @@ -107,7 +138,7 @@ class AstTranslator implements AstVisitor { } visitChain(ast: Chain): ts.Expression { - const elements = ast.expressions.map(expr => this.translate(expr)); + const elements = ast.expressions.map((expr) => this.translate(expr)); const node = wrapForDiagnostics(ts.factory.createCommaListExpression(elements)); addParseSpanInfo(node, ast.sourceSpan); return node; @@ -124,8 +155,9 @@ class AstTranslator implements AstVisitor { // `conditional /*1,2*/ ? trueExpr /*3,4*/ : falseExpr /*5,6*/` // This should be instead be `conditional /*1,2*/ ? trueExpr /*3,4*/ : (falseExpr /*5,6*/)` const falseExpr = wrapForTypeChecker(this.translate(ast.falseExp)); - const node = ts.factory.createParenthesizedExpression(ts.factory.createConditionalExpression( - condExpr, undefined, trueExpr, undefined, falseExpr)); + const node = ts.factory.createParenthesizedExpression( + ts.factory.createConditionalExpression(condExpr, undefined, trueExpr, undefined, falseExpr), + ); addParseSpanInfo(node, ast.sourceSpan); return node; } @@ -143,9 +175,14 @@ class AstTranslator implements AstVisitor { // interpolation's expressions. The chain is started using an actual string literal to ensure // the type is inferred as 'string'. return ast.expressions.reduce( - (lhs: ts.Expression, ast: AST) => ts.factory.createBinaryExpression( - lhs, ts.SyntaxKind.PlusToken, wrapForTypeChecker(this.translate(ast))), - ts.factory.createStringLiteral('')); + (lhs: ts.Expression, ast: AST) => + ts.factory.createBinaryExpression( + lhs, + ts.SyntaxKind.PlusToken, + wrapForTypeChecker(this.translate(ast)), + ), + ts.factory.createStringLiteral(''), + ); } visitKeyedRead(ast: KeyedRead): ts.Expression { @@ -163,13 +200,14 @@ class AstTranslator implements AstVisitor { // available on `ast`. const right = wrapForTypeChecker(this.translate(ast.value)); const node = wrapForDiagnostics( - ts.factory.createBinaryExpression(left, ts.SyntaxKind.EqualsToken, right)); + ts.factory.createBinaryExpression(left, ts.SyntaxKind.EqualsToken, right), + ); addParseSpanInfo(node, ast.sourceSpan); return node; } visitLiteralArray(ast: LiteralArray): ts.Expression { - const elements = ast.expressions.map(expr => this.translate(expr)); + const elements = ast.expressions.map((expr) => this.translate(expr)); const literal = ts.factory.createArrayLiteralExpression(elements); // If strictLiteralTypes is disabled, array literals are cast to `any`. const node = this.config.strictLiteralTypes ? literal : tsCastToAny(literal); @@ -255,7 +293,8 @@ class AstTranslator implements AstVisitor { // We should instead generate `e = ($event /*0,10*/)` so we know the span 0,10 matches RHS. const right = wrapForTypeChecker(this.translate(ast.value)); const node = wrapForDiagnostics( - ts.factory.createBinaryExpression(leftWithPath, ts.SyntaxKind.EqualsToken, right)); + ts.factory.createBinaryExpression(leftWithPath, ts.SyntaxKind.EqualsToken, right), + ); addParseSpanInfo(node, ast.sourceSpan); return node; } @@ -270,10 +309,13 @@ class AstTranslator implements AstVisitor { // "a?.b" becomes (null as any ? a!.b : undefined) // The type of this expression is (typeof a!.b) | undefined, which is exactly as desired. const expr = ts.factory.createPropertyAccessExpression( - ts.factory.createNonNullExpression(receiver), ast.name); + ts.factory.createNonNullExpression(receiver), + ast.name, + ); addParseSpanInfo(expr, ast.nameSpan); - node = ts.factory.createParenthesizedExpression(ts.factory.createConditionalExpression( - NULL_AS_ANY, undefined, expr, undefined, UNDEFINED)); + node = ts.factory.createParenthesizedExpression( + ts.factory.createConditionalExpression(NULL_AS_ANY, undefined, expr, undefined, UNDEFINED), + ); } else if (VeSafeLhsInferenceBugDetector.veWillInferAnyFor(ast)) { // Emulate a View Engine bug where 'any' is inferred for the left-hand side of the safe // navigation operation. With this bug, the type of the left-hand side is regarded as any. @@ -286,7 +328,9 @@ class AstTranslator implements AstVisitor { // result is still inferred as `any`. // "a?.b" becomes (a!.b as any) const expr = ts.factory.createPropertyAccessExpression( - ts.factory.createNonNullExpression(receiver), ast.name); + ts.factory.createNonNullExpression(receiver), + ast.name, + ); addParseSpanInfo(expr, ast.nameSpan); node = tsCastToAny(expr); } @@ -303,17 +347,22 @@ class AstTranslator implements AstVisitor { if (this.config.strictSafeNavigationTypes) { // "a?.[...]" becomes (null as any ? a![...] : undefined) const expr = ts.factory.createElementAccessExpression( - ts.factory.createNonNullExpression(receiver), key); + ts.factory.createNonNullExpression(receiver), + key, + ); addParseSpanInfo(expr, ast.sourceSpan); - node = ts.factory.createParenthesizedExpression(ts.factory.createConditionalExpression( - NULL_AS_ANY, undefined, expr, undefined, UNDEFINED)); + node = ts.factory.createParenthesizedExpression( + ts.factory.createConditionalExpression(NULL_AS_ANY, undefined, expr, undefined, UNDEFINED), + ); } else if (VeSafeLhsInferenceBugDetector.veWillInferAnyFor(ast)) { // "a?.[...]" becomes (a as any)[...] node = ts.factory.createElementAccessExpression(tsCastToAny(receiver), key); } else { // "a?.[...]" becomes (a!.[...] as any) const expr = ts.factory.createElementAccessExpression( - ts.factory.createNonNullExpression(receiver), key); + ts.factory.createNonNullExpression(receiver), + key, + ); addParseSpanInfo(expr, ast.sourceSpan); node = tsCastToAny(expr); } @@ -322,7 +371,7 @@ class AstTranslator implements AstVisitor { } visitCall(ast: Call): ts.Expression { - const args = ast.args.map(expr => this.translate(expr)); + const args = ast.args.map((expr) => this.translate(expr)); let expr: ts.Expression; const receiver = ast.receiver; @@ -358,21 +407,28 @@ class AstTranslator implements AstVisitor { } visitSafeCall(ast: SafeCall): ts.Expression { - const args = ast.args.map(expr => this.translate(expr)); + const args = ast.args.map((expr) => this.translate(expr)); const expr = wrapForDiagnostics(this.translate(ast.receiver)); const node = this.convertToSafeCall(ast, expr, args); addParseSpanInfo(node, ast.sourceSpan); return node; } - private convertToSafeCall(ast: Call|SafeCall, expr: ts.Expression, args: ts.Expression[]): - ts.Expression { + private convertToSafeCall( + ast: Call | SafeCall, + expr: ts.Expression, + args: ts.Expression[], + ): ts.Expression { if (this.config.strictSafeNavigationTypes) { // "a?.method(...)" becomes (null as any ? a!.method(...) : undefined) const call = ts.factory.createCallExpression( - ts.factory.createNonNullExpression(expr), undefined, args); - return ts.factory.createParenthesizedExpression(ts.factory.createConditionalExpression( - NULL_AS_ANY, undefined, call, undefined, UNDEFINED)); + ts.factory.createNonNullExpression(expr), + undefined, + args, + ); + return ts.factory.createParenthesizedExpression( + ts.factory.createConditionalExpression(NULL_AS_ANY, undefined, call, undefined, UNDEFINED), + ); } if (VeSafeLhsInferenceBugDetector.veWillInferAnyFor(ast)) { @@ -382,7 +438,8 @@ class AstTranslator implements AstVisitor { // "a?.method(...)" becomes (a!.method(...) as any) return tsCastToAny( - ts.factory.createCallExpression(ts.factory.createNonNullExpression(expr), undefined, args)); + ts.factory.createCallExpression(ts.factory.createNonNullExpression(expr), undefined, args), + ); } } @@ -402,7 +459,7 @@ class AstTranslator implements AstVisitor { class VeSafeLhsInferenceBugDetector implements AstVisitor { private static SINGLETON = new VeSafeLhsInferenceBugDetector(); - static veWillInferAnyFor(ast: Call|SafeCall|SafePropertyRead|SafeKeyedRead) { + static veWillInferAnyFor(ast: Call | SafeCall | SafePropertyRead | SafeKeyedRead) { const visitor = VeSafeLhsInferenceBugDetector.SINGLETON; return ast instanceof Call ? ast.visit(visitor) : ast.receiver.visit(visitor); } @@ -432,7 +489,7 @@ class VeSafeLhsInferenceBugDetector implements AstVisitor { return false; } visitInterpolation(ast: Interpolation): boolean { - return ast.expressions.some(exp => exp.visit(this)); + return ast.expressions.some((exp) => exp.visit(this)); } visitKeyedRead(ast: KeyedRead): boolean { return false; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/line_mappings.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/line_mappings.ts index 56301b7c4c121..01444805dc3cc 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/line_mappings.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/line_mappings.ts @@ -43,7 +43,11 @@ export function computeLineStartsMap(text: string): number[] { /** Finds the closest line start for the given position. */ function findClosestLineStartPosition( - linesMap: T[], position: T, low = 0, high = linesMap.length - 1) { + linesMap: T[], + position: T, + low = 0, + high = linesMap.length - 1, +) { while (low <= high) { const pivotIdx = Math.floor((low + high) / 2); const pivotEl = linesMap[pivotIdx]; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts index 60f98d27bcb68..70a28c7e39d69 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/oob.ts @@ -6,7 +6,24 @@ * found in the LICENSE file at https://angular.io/license */ -import {AbsoluteSourceSpan, BindingPipe, PropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstForLoopBlock, TmplAstForLoopBlockEmpty, TmplAstHoverDeferredTrigger, TmplAstIfBlockBranch, TmplAstInteractionDeferredTrigger, TmplAstReference, TmplAstSwitchBlockCase, TmplAstTemplate, TmplAstVariable, TmplAstViewportDeferredTrigger} from '@angular/compiler'; +import { + AbsoluteSourceSpan, + BindingPipe, + PropertyRead, + TmplAstBoundAttribute, + TmplAstBoundEvent, + TmplAstElement, + TmplAstForLoopBlock, + TmplAstForLoopBlockEmpty, + TmplAstHoverDeferredTrigger, + TmplAstIfBlockBranch, + TmplAstInteractionDeferredTrigger, + TmplAstReference, + TmplAstSwitchBlockCase, + TmplAstTemplate, + TmplAstVariable, + TmplAstViewportDeferredTrigger, +} from '@angular/compiler'; import ts from 'typescript'; import {ErrorCode, makeDiagnostic, makeRelatedInformation, ngErrorCode} from '../../diagnostics'; @@ -16,8 +33,6 @@ import {makeTemplateDiagnostic} from '../diagnostics'; import {TemplateSourceResolver} from './tcb_util'; - - /** * Collects `ts.Diagnostic`s on problems which occur in the template which aren't directly sourced * from Type Check Blocks. @@ -79,12 +94,18 @@ export interface OutOfBandDiagnosticRecorder { * @param firstDecl the first variable declaration which uses the same name as `variable`. */ duplicateTemplateVar( - templateId: TemplateId, variable: TmplAstVariable, firstDecl: TmplAstVariable): void; + templateId: TemplateId, + variable: TmplAstVariable, + firstDecl: TmplAstVariable, + ): void; requiresInlineTcb(templateId: TemplateId, node: ClassDeclaration): void; requiresInlineTypeConstructors( - templateId: TemplateId, node: ClassDeclaration, directives: ClassDeclaration[]): void; + templateId: TemplateId, + node: ClassDeclaration, + directives: ClassDeclaration[], + ): void; /** * Report a warning when structural directives support context guards, but the current @@ -96,37 +117,58 @@ export interface OutOfBandDiagnosticRecorder { * Reports a split two way binding error message. */ splitTwoWayBinding( - templateId: TemplateId, input: TmplAstBoundAttribute, output: TmplAstBoundEvent, - inputConsumer: ClassDeclaration, outputConsumer: ClassDeclaration|TmplAstElement): void; + templateId: TemplateId, + input: TmplAstBoundAttribute, + output: TmplAstBoundEvent, + inputConsumer: ClassDeclaration, + outputConsumer: ClassDeclaration | TmplAstElement, + ): void; /** Reports required inputs that haven't been bound. */ missingRequiredInputs( - templateId: TemplateId, element: TmplAstElement|TmplAstTemplate, directiveName: string, - isComponent: boolean, inputAliases: string[]): void; + templateId: TemplateId, + element: TmplAstElement | TmplAstTemplate, + directiveName: string, + isComponent: boolean, + inputAliases: string[], + ): void; /** * Reports accesses of properties that aren't available in a `for` block's tracking expression. */ illegalForLoopTrackAccess( - templateId: TemplateId, block: TmplAstForLoopBlock, access: PropertyRead): void; + templateId: TemplateId, + block: TmplAstForLoopBlock, + access: PropertyRead, + ): void; /** * Reports deferred triggers that cannot access the element they're referring to. */ inaccessibleDeferredTriggerElement( - templateId: TemplateId, - trigger: TmplAstHoverDeferredTrigger|TmplAstInteractionDeferredTrigger| - TmplAstViewportDeferredTrigger): void; + templateId: TemplateId, + trigger: + | TmplAstHoverDeferredTrigger + | TmplAstInteractionDeferredTrigger + | TmplAstViewportDeferredTrigger, + ): void; /** * Reports cases where control flow nodes prevent content projection. */ controlFlowPreventingContentProjection( - templateId: TemplateId, category: ts.DiagnosticCategory, - projectionNode: TmplAstElement|TmplAstTemplate, componentName: string, slotSelector: string, - controlFlowNode: TmplAstIfBlockBranch|TmplAstSwitchBlockCase|TmplAstForLoopBlock| - TmplAstForLoopBlockEmpty, - preservesWhitespaces: boolean): void; + templateId: TemplateId, + category: ts.DiagnosticCategory, + projectionNode: TmplAstElement | TmplAstTemplate, + componentName: string, + slotSelector: string, + controlFlowNode: + | TmplAstIfBlockBranch + | TmplAstSwitchBlockCase + | TmplAstForLoopBlock + | TmplAstForLoopBlockEmpty, + preservesWhitespaces: boolean, + ): void; } export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecorder { @@ -149,9 +191,16 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor const value = ref.value.trim(); const errorMsg = `No directive found with exportAs '${value}'.`; - this._diagnostics.push(makeTemplateDiagnostic( - templateId, mapping, ref.valueSpan || ref.sourceSpan, ts.DiagnosticCategory.Error, - ngErrorCode(ErrorCode.MISSING_REFERENCE_TARGET), errorMsg)); + this._diagnostics.push( + makeTemplateDiagnostic( + templateId, + mapping, + ref.valueSpan || ref.sourceSpan, + ts.DiagnosticCategory.Error, + ngErrorCode(ErrorCode.MISSING_REFERENCE_TARGET), + errorMsg, + ), + ); } missingPipe(templateId: TemplateId, ast: BindingPipe): void { @@ -165,11 +214,19 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor const sourceSpan = this.resolver.toParseSourceSpan(templateId, ast.nameSpan); if (sourceSpan === null) { throw new Error( - `Assertion failure: no SourceLocation found for usage of pipe '${ast.name}'.`); + `Assertion failure: no SourceLocation found for usage of pipe '${ast.name}'.`, + ); } - this._diagnostics.push(makeTemplateDiagnostic( - templateId, mapping, sourceSpan, ts.DiagnosticCategory.Error, - ngErrorCode(ErrorCode.MISSING_PIPE), errorMsg)); + this._diagnostics.push( + makeTemplateDiagnostic( + templateId, + mapping, + sourceSpan, + ts.DiagnosticCategory.Error, + ngErrorCode(ErrorCode.MISSING_PIPE), + errorMsg, + ), + ); this.recordedPipes.add(ast); } @@ -179,84 +236,127 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor } const mapping = this.resolver.getSourceMapping(templateId); - const errorMsg = `Pipe '${ast.name}' was imported via \`@Component.deferredImports\`, ` + - `but was used outside of a \`@defer\` block in a template. To fix this, either ` + - `use the '${ast.name}' pipe inside of a \`@defer\` block or import this dependency ` + - `using the \`@Component.imports\` field.`; + const errorMsg = + `Pipe '${ast.name}' was imported via \`@Component.deferredImports\`, ` + + `but was used outside of a \`@defer\` block in a template. To fix this, either ` + + `use the '${ast.name}' pipe inside of a \`@defer\` block or import this dependency ` + + `using the \`@Component.imports\` field.`; const sourceSpan = this.resolver.toParseSourceSpan(templateId, ast.nameSpan); if (sourceSpan === null) { throw new Error( - `Assertion failure: no SourceLocation found for usage of pipe '${ast.name}'.`); + `Assertion failure: no SourceLocation found for usage of pipe '${ast.name}'.`, + ); } - this._diagnostics.push(makeTemplateDiagnostic( - templateId, mapping, sourceSpan, ts.DiagnosticCategory.Error, - ngErrorCode(ErrorCode.DEFERRED_PIPE_USED_EAGERLY), errorMsg)); + this._diagnostics.push( + makeTemplateDiagnostic( + templateId, + mapping, + sourceSpan, + ts.DiagnosticCategory.Error, + ngErrorCode(ErrorCode.DEFERRED_PIPE_USED_EAGERLY), + errorMsg, + ), + ); this.recordedPipes.add(ast); } deferredComponentUsedEagerly(templateId: TemplateId, element: TmplAstElement): void { const mapping = this.resolver.getSourceMapping(templateId); - const errorMsg = `Element '${element.name}' contains a component or a directive that ` + - `was imported via \`@Component.deferredImports\`, but the element itself is located ` + - `outside of a \`@defer\` block in a template. To fix this, either ` + - `use the '${element.name}' element inside of a \`@defer\` block or ` + - `import referenced component/directive dependency using the \`@Component.imports\` field.`; + const errorMsg = + `Element '${element.name}' contains a component or a directive that ` + + `was imported via \`@Component.deferredImports\`, but the element itself is located ` + + `outside of a \`@defer\` block in a template. To fix this, either ` + + `use the '${element.name}' element inside of a \`@defer\` block or ` + + `import referenced component/directive dependency using the \`@Component.imports\` field.`; const {start, end} = element.startSourceSpan; const absoluteSourceSpan = new AbsoluteSourceSpan(start.offset, end.offset); const sourceSpan = this.resolver.toParseSourceSpan(templateId, absoluteSourceSpan); if (sourceSpan === null) { throw new Error( - `Assertion failure: no SourceLocation found for usage of pipe '${element.name}'.`); + `Assertion failure: no SourceLocation found for usage of pipe '${element.name}'.`, + ); } - this._diagnostics.push(makeTemplateDiagnostic( - templateId, mapping, sourceSpan, ts.DiagnosticCategory.Error, - ngErrorCode(ErrorCode.DEFERRED_DIRECTIVE_USED_EAGERLY), errorMsg)); + this._diagnostics.push( + makeTemplateDiagnostic( + templateId, + mapping, + sourceSpan, + ts.DiagnosticCategory.Error, + ngErrorCode(ErrorCode.DEFERRED_DIRECTIVE_USED_EAGERLY), + errorMsg, + ), + ); } duplicateTemplateVar( - templateId: TemplateId, variable: TmplAstVariable, firstDecl: TmplAstVariable): void { + templateId: TemplateId, + variable: TmplAstVariable, + firstDecl: TmplAstVariable, + ): void { const mapping = this.resolver.getSourceMapping(templateId); - const errorMsg = `Cannot redeclare variable '${ - variable.name}' as it was previously declared elsewhere for the same template.`; + const errorMsg = `Cannot redeclare variable '${variable.name}' as it was previously declared elsewhere for the same template.`; // The allocation of the error here is pretty useless for variables declared in microsyntax, // since the sourceSpan refers to the entire microsyntax property, not a span for the specific // variable in question. // // TODO(alxhub): allocate to a tighter span once one is available. - this._diagnostics.push(makeTemplateDiagnostic( - templateId, mapping, variable.sourceSpan, ts.DiagnosticCategory.Error, - ngErrorCode(ErrorCode.DUPLICATE_VARIABLE_DECLARATION), errorMsg, [{ - text: `The variable '${firstDecl.name}' was first declared here.`, - start: firstDecl.sourceSpan.start.offset, - end: firstDecl.sourceSpan.end.offset, - sourceFile: mapping.node.getSourceFile(), - }])); + this._diagnostics.push( + makeTemplateDiagnostic( + templateId, + mapping, + variable.sourceSpan, + ts.DiagnosticCategory.Error, + ngErrorCode(ErrorCode.DUPLICATE_VARIABLE_DECLARATION), + errorMsg, + [ + { + text: `The variable '${firstDecl.name}' was first declared here.`, + start: firstDecl.sourceSpan.start.offset, + end: firstDecl.sourceSpan.end.offset, + sourceFile: mapping.node.getSourceFile(), + }, + ], + ), + ); } requiresInlineTcb(templateId: TemplateId, node: ClassDeclaration): void { - this._diagnostics.push(makeInlineDiagnostic( - templateId, ErrorCode.INLINE_TCB_REQUIRED, node.name, - `This component requires inline template type-checking, which is not supported by the current environment.`)); + this._diagnostics.push( + makeInlineDiagnostic( + templateId, + ErrorCode.INLINE_TCB_REQUIRED, + node.name, + `This component requires inline template type-checking, which is not supported by the current environment.`, + ), + ); } requiresInlineTypeConstructors( - templateId: TemplateId, node: ClassDeclaration, directives: ClassDeclaration[]): void { + templateId: TemplateId, + node: ClassDeclaration, + directives: ClassDeclaration[], + ): void { let message: string; if (directives.length > 1) { - message = - `This component uses directives which require inline type constructors, which are not supported by the current environment.`; + message = `This component uses directives which require inline type constructors, which are not supported by the current environment.`; } else { - message = - `This component uses a directive which requires an inline type constructor, which is not supported by the current environment.`; + message = `This component uses a directive which requires an inline type constructor, which is not supported by the current environment.`; } - this._diagnostics.push(makeInlineDiagnostic( - templateId, ErrorCode.INLINE_TYPE_CTOR_REQUIRED, node.name, message, - directives.map( - dir => makeRelatedInformation(dir.name, `Requires an inline type constructor.`)))); + this._diagnostics.push( + makeInlineDiagnostic( + templateId, + ErrorCode.INLINE_TYPE_CTOR_REQUIRED, + node.name, + message, + directives.map((dir) => + makeRelatedInformation(dir.name, `Requires an inline type constructor.`), + ), + ), + ); } suboptimalTypeInference(templateId: TemplateId, variables: TmplAstVariable[]): void { @@ -264,9 +364,9 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor // Select one of the template variables that's most suitable for reporting the diagnostic. Any // variable will do, but prefer one bound to the context's $implicit if present. - let diagnosticVar: TmplAstVariable|null = null; + let diagnosticVar: TmplAstVariable | null = null; for (const variable of variables) { - if (diagnosticVar === null || (variable.value === '' || variable.value === '$implicit')) { + if (diagnosticVar === null || variable.value === '' || variable.value === '$implicit') { diagnosticVar = variable; } } @@ -281,25 +381,33 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor } else if (variables.length > 2) { varIdentification += ` (and ${variables.length - 1} others)`; } - const message = - `This structural directive supports advanced type inference, but the current compiler configuration prevents its usage. The variable ${ - varIdentification} will have type 'any' as a result.\n\nConsider enabling the 'strictTemplates' option in your tsconfig.json for better type inference within this template.`; - - this._diagnostics.push(makeTemplateDiagnostic( - templateId, mapping, diagnosticVar.keySpan, ts.DiagnosticCategory.Suggestion, - ngErrorCode(ErrorCode.SUGGEST_SUBOPTIMAL_TYPE_INFERENCE), message)); + const message = `This structural directive supports advanced type inference, but the current compiler configuration prevents its usage. The variable ${varIdentification} will have type 'any' as a result.\n\nConsider enabling the 'strictTemplates' option in your tsconfig.json for better type inference within this template.`; + + this._diagnostics.push( + makeTemplateDiagnostic( + templateId, + mapping, + diagnosticVar.keySpan, + ts.DiagnosticCategory.Suggestion, + ngErrorCode(ErrorCode.SUGGEST_SUBOPTIMAL_TYPE_INFERENCE), + message, + ), + ); } splitTwoWayBinding( - templateId: TemplateId, input: TmplAstBoundAttribute, output: TmplAstBoundEvent, - inputConsumer: ClassDeclaration, outputConsumer: ClassDeclaration|TmplAstElement): void { + templateId: TemplateId, + input: TmplAstBoundAttribute, + output: TmplAstBoundEvent, + inputConsumer: ClassDeclaration, + outputConsumer: ClassDeclaration | TmplAstElement, + ): void { const mapping = this.resolver.getSourceMapping(templateId); - const errorMsg = `The property and event halves of the two-way binding '${ - input.name}' are not bound to the same target. + const errorMsg = `The property and event halves of the two-way binding '${input.name}' are not bound to the same target. Find more at https://angular.dev/guide/templates/two-way-binding#how-two-way-binding-works`; - const relatedMessages: {text: string; start: number; end: number; - sourceFile: ts.SourceFile;}[] = []; + const relatedMessages: {text: string; start: number; end: number; sourceFile: ts.SourceFile}[] = + []; relatedMessages.push({ text: `The property half of the binding is to the '${inputConsumer.name.text}' component.`, @@ -309,8 +417,7 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor }); if (outputConsumer instanceof TmplAstElement) { - let message = `The event half of the binding is to a native event called '${ - input.name}' on the <${outputConsumer.name}> DOM element.`; + let message = `The event half of the binding is to a native event called '${input.name}' on the <${outputConsumer.name}> DOM element.`; if (!mapping.node.getSourceFile().isDeclarationFile) { message += `\n \n Are you missing an output declaration called '${output.name}'?`; } @@ -329,108 +436,159 @@ export class OutOfBandDiagnosticRecorderImpl implements OutOfBandDiagnosticRecor }); } - - this._diagnostics.push(makeTemplateDiagnostic( - templateId, mapping, input.keySpan, ts.DiagnosticCategory.Error, - ngErrorCode(ErrorCode.SPLIT_TWO_WAY_BINDING), errorMsg, relatedMessages)); + this._diagnostics.push( + makeTemplateDiagnostic( + templateId, + mapping, + input.keySpan, + ts.DiagnosticCategory.Error, + ngErrorCode(ErrorCode.SPLIT_TWO_WAY_BINDING), + errorMsg, + relatedMessages, + ), + ); } missingRequiredInputs( - templateId: TemplateId, element: TmplAstElement|TmplAstTemplate, directiveName: string, - isComponent: boolean, inputAliases: string[]): void { - const message = `Required input${inputAliases.length === 1 ? '' : 's'} ${ - inputAliases.map(n => `'${n}'`).join(', ')} from ${ - isComponent ? 'component' : 'directive'} ${directiveName} must be specified.`; - - this._diagnostics.push(makeTemplateDiagnostic( - templateId, this.resolver.getSourceMapping(templateId), element.startSourceSpan, - ts.DiagnosticCategory.Error, ngErrorCode(ErrorCode.MISSING_REQUIRED_INPUTS), message)); + templateId: TemplateId, + element: TmplAstElement | TmplAstTemplate, + directiveName: string, + isComponent: boolean, + inputAliases: string[], + ): void { + const message = `Required input${inputAliases.length === 1 ? '' : 's'} ${inputAliases + .map((n) => `'${n}'`) + .join(', ')} from ${ + isComponent ? 'component' : 'directive' + } ${directiveName} must be specified.`; + + this._diagnostics.push( + makeTemplateDiagnostic( + templateId, + this.resolver.getSourceMapping(templateId), + element.startSourceSpan, + ts.DiagnosticCategory.Error, + ngErrorCode(ErrorCode.MISSING_REQUIRED_INPUTS), + message, + ), + ); } illegalForLoopTrackAccess( - templateId: TemplateId, block: TmplAstForLoopBlock, access: PropertyRead): void { + templateId: TemplateId, + block: TmplAstForLoopBlock, + access: PropertyRead, + ): void { const sourceSpan = this.resolver.toParseSourceSpan(templateId, access.sourceSpan); if (sourceSpan === null) { throw new Error(`Assertion failure: no SourceLocation found for property read.`); } - const messageVars = [block.item, ...block.contextVariables.filter(v => v.value === '$index')] - .map(v => `'${v.name}'`) - .join(', '); + const messageVars = [block.item, ...block.contextVariables.filter((v) => v.value === '$index')] + .map((v) => `'${v.name}'`) + .join(', '); const message = - `Cannot access '${access.name}' inside of a track expression. ` + - `Only ${ - messageVars} and properties on the containing component are available to this expression.`; - - this._diagnostics.push(makeTemplateDiagnostic( - templateId, this.resolver.getSourceMapping(templateId), sourceSpan, - ts.DiagnosticCategory.Error, ngErrorCode(ErrorCode.ILLEGAL_FOR_LOOP_TRACK_ACCESS), - message)); + `Cannot access '${access.name}' inside of a track expression. ` + + `Only ${messageVars} and properties on the containing component are available to this expression.`; + + this._diagnostics.push( + makeTemplateDiagnostic( + templateId, + this.resolver.getSourceMapping(templateId), + sourceSpan, + ts.DiagnosticCategory.Error, + ngErrorCode(ErrorCode.ILLEGAL_FOR_LOOP_TRACK_ACCESS), + message, + ), + ); } inaccessibleDeferredTriggerElement( - templateId: TemplateId, - trigger: TmplAstHoverDeferredTrigger|TmplAstInteractionDeferredTrigger| - TmplAstViewportDeferredTrigger): void { + templateId: TemplateId, + trigger: + | TmplAstHoverDeferredTrigger + | TmplAstInteractionDeferredTrigger + | TmplAstViewportDeferredTrigger, + ): void { let message: string; if (trigger.reference === null) { - message = `Trigger cannot find reference. Make sure that the @defer block has a ` + - `@placeholder with at least one root element node.`; + message = + `Trigger cannot find reference. Make sure that the @defer block has a ` + + `@placeholder with at least one root element node.`; } else { message = - `Trigger cannot find reference "${trigger.reference}".\nCheck that an element with #${ - trigger.reference} exists in the same template and it's accessible from the ` + - `@defer block.\nDeferred blocks can only access triggers in same view, a parent ` + - `embedded view or the root view of the @placeholder block.`; + `Trigger cannot find reference "${trigger.reference}".\nCheck that an element with #${trigger.reference} exists in the same template and it's accessible from the ` + + `@defer block.\nDeferred blocks can only access triggers in same view, a parent ` + + `embedded view or the root view of the @placeholder block.`; } - this._diagnostics.push(makeTemplateDiagnostic( - templateId, this.resolver.getSourceMapping(templateId), trigger.sourceSpan, - ts.DiagnosticCategory.Error, ngErrorCode(ErrorCode.INACCESSIBLE_DEFERRED_TRIGGER_ELEMENT), - message)); + this._diagnostics.push( + makeTemplateDiagnostic( + templateId, + this.resolver.getSourceMapping(templateId), + trigger.sourceSpan, + ts.DiagnosticCategory.Error, + ngErrorCode(ErrorCode.INACCESSIBLE_DEFERRED_TRIGGER_ELEMENT), + message, + ), + ); } controlFlowPreventingContentProjection( - templateId: TemplateId, category: ts.DiagnosticCategory, - projectionNode: TmplAstElement|TmplAstTemplate, componentName: string, slotSelector: string, - controlFlowNode: TmplAstIfBlockBranch|TmplAstSwitchBlockCase|TmplAstForLoopBlock| - TmplAstForLoopBlockEmpty, - preservesWhitespaces: boolean): void { + templateId: TemplateId, + category: ts.DiagnosticCategory, + projectionNode: TmplAstElement | TmplAstTemplate, + componentName: string, + slotSelector: string, + controlFlowNode: + | TmplAstIfBlockBranch + | TmplAstSwitchBlockCase + | TmplAstForLoopBlock + | TmplAstForLoopBlockEmpty, + preservesWhitespaces: boolean, + ): void { const blockName = controlFlowNode.nameSpan.toString().trim(); const lines = [ - `Node matches the "${slotSelector}" slot of the "${ - componentName}" component, but will not be projected into the specific slot because the surrounding ${ - blockName} has more than one node at its root. To project the node in the right slot, you can:\n`, - `1. Wrap the content of the ${blockName} block in an that matches the "${ - slotSelector}" selector.`, - `2. Split the content of the ${blockName} block across multiple ${ - blockName} blocks such that each one only has a single projectable node at its root.`, - `3. Remove all content from the ${blockName} block, except for the node being projected.` + `Node matches the "${slotSelector}" slot of the "${componentName}" component, but will not be projected into the specific slot because the surrounding ${blockName} has more than one node at its root. To project the node in the right slot, you can:\n`, + `1. Wrap the content of the ${blockName} block in an that matches the "${slotSelector}" selector.`, + `2. Split the content of the ${blockName} block across multiple ${blockName} blocks such that each one only has a single projectable node at its root.`, + `3. Remove all content from the ${blockName} block, except for the node being projected.`, ]; if (preservesWhitespaces) { lines.push( - 'Note: the host component has `preserveWhitespaces: true` which may ' + - 'cause whitespace to affect content projection.'); + 'Note: the host component has `preserveWhitespaces: true` which may ' + + 'cause whitespace to affect content projection.', + ); } lines.push( - '', - 'This check can be disabled using the `extendedDiagnostics.checks.' + - 'controlFlowPreventingContentProjection = "suppress" compiler option.`'); - - this._diagnostics.push(makeTemplateDiagnostic( - templateId, this.resolver.getSourceMapping(templateId), projectionNode.startSourceSpan, - category, ngErrorCode(ErrorCode.CONTROL_FLOW_PREVENTING_CONTENT_PROJECTION), - lines.join('\n'))); + '', + 'This check can be disabled using the `extendedDiagnostics.checks.' + + 'controlFlowPreventingContentProjection = "suppress" compiler option.`', + ); + + this._diagnostics.push( + makeTemplateDiagnostic( + templateId, + this.resolver.getSourceMapping(templateId), + projectionNode.startSourceSpan, + category, + ngErrorCode(ErrorCode.CONTROL_FLOW_PREVENTING_CONTENT_PROJECTION), + lines.join('\n'), + ), + ); } } function makeInlineDiagnostic( - templateId: TemplateId, code: ErrorCode.INLINE_TCB_REQUIRED|ErrorCode.INLINE_TYPE_CTOR_REQUIRED, - node: ts.Node, messageText: string|ts.DiagnosticMessageChain, - relatedInformation?: ts.DiagnosticRelatedInformation[]): TemplateDiagnostic { + templateId: TemplateId, + code: ErrorCode.INLINE_TCB_REQUIRED | ErrorCode.INLINE_TYPE_CTOR_REQUIRED, + node: ts.Node, + messageText: string | ts.DiagnosticMessageChain, + relatedInformation?: ts.DiagnosticRelatedInformation[], +): TemplateDiagnostic { return { ...makeDiagnostic(code, node, messageText, relatedInformation), componentFile: node.getSourceFile(), diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/reference_emit_environment.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/reference_emit_environment.ts index 1a70753c2883c..3358a831a8b86 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/reference_emit_environment.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/reference_emit_environment.ts @@ -6,10 +6,22 @@ * found in the LICENSE file at https://angular.io/license */ -import {ExpressionType, ExternalExpr, TransplantedType, Type, TypeModifier} from '@angular/compiler'; +import { + ExpressionType, + ExternalExpr, + TransplantedType, + Type, + TypeModifier, +} from '@angular/compiler'; import ts from 'typescript'; -import {assertSuccessfulReferenceEmit, ImportFlags, Reference, ReferenceEmitKind, ReferenceEmitter} from '../../imports'; +import { + assertSuccessfulReferenceEmit, + ImportFlags, + Reference, + ReferenceEmitKind, + ReferenceEmitter, +} from '../../imports'; import {ReflectionHost} from '../../reflection'; import {ImportManager, translateExpression, translateType} from '../../translator'; @@ -21,13 +33,18 @@ import {ImportManager, translateExpression, translateType} from '../../translato */ export class ReferenceEmitEnvironment { constructor( - readonly importManager: ImportManager, protected refEmitter: ReferenceEmitter, - readonly reflector: ReflectionHost, public contextFile: ts.SourceFile) {} + readonly importManager: ImportManager, + protected refEmitter: ReferenceEmitter, + readonly reflector: ReflectionHost, + public contextFile: ts.SourceFile, + ) {} canReferenceType( - ref: Reference, - flags: ImportFlags = ImportFlags.NoAliasing | ImportFlags.AllowTypeImports | - ImportFlags.AllowRelativeDtsImports): boolean { + ref: Reference, + flags: ImportFlags = ImportFlags.NoAliasing | + ImportFlags.AllowTypeImports | + ImportFlags.AllowRelativeDtsImports, + ): boolean { const result = this.refEmitter.emit(ref, this.contextFile, flags); return result.kind === ReferenceEmitKind.Success; } @@ -38,17 +55,23 @@ export class ReferenceEmitEnvironment { * This may involve importing the node into the file if it's not declared there already. */ referenceType( - ref: Reference, - flags: ImportFlags = ImportFlags.NoAliasing | ImportFlags.AllowTypeImports | - ImportFlags.AllowRelativeDtsImports): ts.TypeNode { + ref: Reference, + flags: ImportFlags = ImportFlags.NoAliasing | + ImportFlags.AllowTypeImports | + ImportFlags.AllowRelativeDtsImports, + ): ts.TypeNode { const ngExpr = this.refEmitter.emit(ref, this.contextFile, flags); assertSuccessfulReferenceEmit(ngExpr, this.contextFile, 'symbol'); // Create an `ExpressionType` from the `Expression` and translate it via `translateType`. // TODO(alxhub): support references to types with generic arguments in a clean way. return translateType( - new ExpressionType(ngExpr.expression), this.contextFile, this.reflector, this.refEmitter, - this.importManager); + new ExpressionType(ngExpr.expression), + this.contextFile, + this.reflector, + this.refEmitter, + this.importManager, + ); } /** @@ -69,8 +92,12 @@ export class ReferenceEmitEnvironment { referenceExternalType(moduleName: string, name: string, typeParams?: Type[]): ts.TypeNode { const external = new ExternalExpr({moduleName, name}); return translateType( - new ExpressionType(external, TypeModifier.None, typeParams), this.contextFile, - this.reflector, this.refEmitter, this.importManager); + new ExpressionType(external, TypeModifier.None, typeParams), + this.contextFile, + this.reflector, + this.refEmitter, + this.importManager, + ); } /** @@ -80,6 +107,11 @@ export class ReferenceEmitEnvironment { */ referenceTransplantedType(type: TransplantedType>): ts.TypeNode { return translateType( - type, this.contextFile, this.reflector, this.refEmitter, this.importManager); + type, + this.contextFile, + this.reflector, + this.refEmitter, + this.importManager, + ); } } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/shim.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/shim.ts index 92aab368312c0..d2c7543392654 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/shim.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/shim.ts @@ -23,8 +23,10 @@ export class TypeCheckShimGenerator implements PerFileShimGenerator { readonly shouldEmit = false; generateShimForFile( - sf: ts.SourceFile, genFilePath: AbsoluteFsPath, - priorShimSf: ts.SourceFile|null): ts.SourceFile { + sf: ts.SourceFile, + genFilePath: AbsoluteFsPath, + priorShimSf: ts.SourceFile | null, + ): ts.SourceFile { if (priorShimSf !== null) { // If this shim existed in the previous program, reuse it now. It might not be correct, but // reusing it in the main program allows the shape of its imports to potentially remain the @@ -34,8 +36,12 @@ export class TypeCheckShimGenerator implements PerFileShimGenerator { return priorShimSf; } return ts.createSourceFile( - genFilePath, 'export const USED_FOR_NG_TYPE_CHECKING = true;', ts.ScriptTarget.Latest, true, - ts.ScriptKind.TS); + genFilePath, + 'export const USED_FOR_NG_TYPE_CHECKING = true;', + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TS, + ); } static shimFor(fileName: AbsoluteFsPath): AbsoluteFsPath { diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/source.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/source.ts index e6d2c7d97e735..1c4fd034eec4b 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/source.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/source.ts @@ -6,7 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {AbsoluteSourceSpan, ParseLocation, ParseSourceFile, ParseSourceSpan} from '@angular/compiler'; +import { + AbsoluteSourceSpan, + ParseLocation, + ParseSourceFile, + ParseSourceSpan, +} from '@angular/compiler'; import ts from 'typescript'; import {TemplateId, TemplateSourceMapping} from '../api'; @@ -20,9 +25,12 @@ import {TemplateSourceResolver} from './tcb_util'; * used when translating parse offsets in diagnostics back to their original line/column location. */ export class TemplateSource { - private lineStarts: number[]|null = null; + private lineStarts: number[] | null = null; - constructor(readonly mapping: TemplateSourceMapping, private file: ParseSourceFile) {} + constructor( + readonly mapping: TemplateSourceMapping, + private file: ParseSourceFile, + ) {} toParseSourceSpan(start: number, end: number): ParseSourceSpan { const startLoc = this.toParseLocation(start); @@ -61,8 +69,11 @@ export class TemplateSourceManager implements TemplateSourceResolver { return getTemplateId(node); } - captureSource(node: ts.ClassDeclaration, mapping: TemplateSourceMapping, file: ParseSourceFile): - TemplateId { + captureSource( + node: ts.ClassDeclaration, + mapping: TemplateSourceMapping, + file: ParseSourceFile, + ): TemplateId { const id = getTemplateId(node); this.templateSources.set(id, new TemplateSource(mapping, file)); return id; @@ -75,7 +86,7 @@ export class TemplateSourceManager implements TemplateSourceResolver { return this.templateSources.get(id)!.mapping; } - toParseSourceSpan(id: TemplateId, span: AbsoluteSourceSpan): ParseSourceSpan|null { + toParseSourceSpan(id: TemplateId, span: AbsoluteSourceSpan): ParseSourceSpan | null { if (!this.templateSources.has(id)) { return null; } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/symbol_util.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/symbol_util.ts index 4a09a339ea5a1..d75af56482251 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/symbol_util.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/symbol_util.ts @@ -21,22 +21,29 @@ const SIGNAL_FNS = new Set([ /** Returns whether a symbol is a reference to a signal. */ export function isSignalReference(symbol: Symbol): boolean { - return (symbol.kind === SymbolKind.Expression || symbol.kind === SymbolKind.Variable) && - // Note that `tsType.symbol` isn't optional in the typings, - // but it appears that it can be undefined at runtime. - (symbol.tsType.symbol !== undefined && isSignalSymbol(symbol.tsType.symbol) || - (symbol.tsType.aliasSymbol !== undefined && isSignalSymbol(symbol.tsType.aliasSymbol))); + return ( + (symbol.kind === SymbolKind.Expression || symbol.kind === SymbolKind.Variable) && + // Note that `tsType.symbol` isn't optional in the typings, + // but it appears that it can be undefined at runtime. + ((symbol.tsType.symbol !== undefined && isSignalSymbol(symbol.tsType.symbol)) || + (symbol.tsType.aliasSymbol !== undefined && isSignalSymbol(symbol.tsType.aliasSymbol))) + ); } /** Checks whether a symbol points to a signal. */ function isSignalSymbol(symbol: ts.Symbol): boolean { const declarations = symbol.getDeclarations(); - return declarations !== undefined && declarations.some(decl => { - const fileName = decl.getSourceFile().fileName; + return ( + declarations !== undefined && + declarations.some((decl) => { + const fileName = decl.getSourceFile().fileName; - return (ts.isInterfaceDeclaration(decl) || ts.isTypeAliasDeclaration(decl)) && + return ( + (ts.isInterfaceDeclaration(decl) || ts.isTypeAliasDeclaration(decl)) && SIGNAL_FNS.has(decl.name.text) && - (fileName.includes('@angular/core') || fileName.includes('angular2/rc/packages/core')); - }); + (fileName.includes('@angular/core') || fileName.includes('angular2/rc/packages/core')) + ); + }) + ); } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/tcb_util.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/tcb_util.ts index 12814821dc28b..b778fdb6dcdb6 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/tcb_util.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/tcb_util.ts @@ -54,7 +54,7 @@ export interface TemplateSourceResolver { * `ParseSourceSpan`. The returned parse span has line and column numbers in addition to only * absolute offsets and gives access to the original template source. */ - toParseSourceSpan(id: TemplateId, span: AbsoluteSourceSpan): ParseSourceSpan|null; + toParseSourceSpan(id: TemplateId, span: AbsoluteSourceSpan): ParseSourceSpan | null; } /** @@ -82,9 +82,11 @@ export enum TcbInliningRequirement { } export function requiresInlineTypeCheckBlock( - ref: Reference>, env: ReferenceEmitEnvironment, - usedPipes: Reference>[], - reflector: ReflectionHost): TcbInliningRequirement { + ref: Reference>, + env: ReferenceEmitEnvironment, + usedPipes: Reference>[], + reflector: ReflectionHost, +): TcbInliningRequirement { // In order to qualify for a declared TCB (not inline) two conditions must be met: // 1) the class must be suitable to be referenced from `env` (e.g. it must be exported) // 2) it must not have contextual generic type bounds @@ -95,7 +97,7 @@ export function requiresInlineTypeCheckBlock( // Condition 2 is false, the class has constrained generic types. It should be checked with an // inline TCB if possible, but can potentially use fallbacks to avoid inlining if not. return TcbInliningRequirement.ShouldInlineForGenericBounds; - } else if (usedPipes.some(pipeRef => !env.canReferenceType(pipeRef))) { + } else if (usedPipes.some((pipeRef) => !env.canReferenceType(pipeRef))) { // If one of the pipes used by the component is not exported, a non-inline TCB will not be able // to import it, so this requires an inline TCB. return TcbInliningRequirement.MustInline; @@ -106,8 +108,11 @@ export function requiresInlineTypeCheckBlock( /** Maps a shim position back to a template location. */ export function getTemplateMapping( - shimSf: ts.SourceFile, position: number, resolver: TemplateSourceResolver, - isDiagnosticRequest: boolean): FullTemplateMapping|null { + shimSf: ts.SourceFile, + position: number, + resolver: TemplateSourceResolver, + isDiagnosticRequest: boolean, +): FullTemplateMapping | null { const node = getTokenAtPosition(shimSf, position); const sourceLocation = findSourceLocation(node, shimSf, isDiagnosticRequest); if (sourceLocation === null) { @@ -125,7 +130,10 @@ export function getTemplateMapping( } export function findTypeCheckBlock( - file: ts.SourceFile, id: TemplateId, isDiagnosticRequest: boolean): ts.Node|null { + file: ts.SourceFile, + id: TemplateId, + isDiagnosticRequest: boolean, +): ts.Node | null { for (const stmt of file.statements) { if (ts.isFunctionDeclaration(stmt) && getTemplateId(stmt, file, isDiagnosticRequest) === id) { return stmt; @@ -141,7 +149,10 @@ export function findTypeCheckBlock( * returns null. */ export function findSourceLocation( - node: ts.Node, sourceFile: ts.SourceFile, isDiagnosticsRequest: boolean): SourceLocation|null { + node: ts.Node, + sourceFile: ts.SourceFile, + isDiagnosticsRequest: boolean, +): SourceLocation | null { // Search for comments until the TCB's function declaration is encountered. while (node !== undefined && !ts.isFunctionDeclaration(node)) { if (hasIgnoreForDiagnosticsMarker(node, sourceFile) && isDiagnosticsRequest) { @@ -167,7 +178,10 @@ export function findSourceLocation( } function getTemplateId( - node: ts.Node, sourceFile: ts.SourceFile, isDiagnosticRequest: boolean): TemplateId|null { + node: ts.Node, + sourceFile: ts.SourceFile, + isDiagnosticRequest: boolean, +): TemplateId | null { // Walk up to the function declaration of the TCB, the file information is attached there. while (!ts.isFunctionDeclaration(node)) { if (hasIgnoreForDiagnosticsMarker(node, sourceFile) && isDiagnosticRequest) { @@ -183,13 +197,15 @@ function getTemplateId( } const start = node.getFullStart(); - return ts.forEachLeadingCommentRange(sourceFile.text, start, (pos, end, kind) => { - if (kind !== ts.SyntaxKind.MultiLineCommentTrivia) { - return null; - } - const commentText = sourceFile.text.substring(pos + 2, end - 2); - return commentText; - }) as TemplateId || null; + return ( + (ts.forEachLeadingCommentRange(sourceFile.text, start, (pos, end, kind) => { + if (kind !== ts.SyntaxKind.MultiLineCommentTrivia) { + return null; + } + const commentText = sourceFile.text.substring(pos + 2, end - 2); + return commentText; + }) as TemplateId) || null + ); } /** @@ -208,9 +224,11 @@ export function ensureTypeCheckFilePreparationImports(env: ReferenceEmitEnvironm } export function checkIfGenericTypeBoundsCanBeEmitted( - node: ClassDeclaration, reflector: ReflectionHost, - env: ReferenceEmitEnvironment): boolean { + node: ClassDeclaration, + reflector: ReflectionHost, + env: ReferenceEmitEnvironment, +): boolean { // Generic type parameters are considered context free if they can be emitted into any context. const emitter = new TypeParameterEmitter(node.typeParameters, reflector); - return emitter.canEmit(ref => env.canReferenceType(ref)); + return emitter.canEmit((ref) => env.canReferenceType(ref)); } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts index d3ae88c7826ca..8ce6ed6bdf777 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/template_symbol_builder.ts @@ -6,7 +6,24 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, ASTWithSource, BindingPipe, ParseSourceSpan, PropertyRead, PropertyWrite, R3Identifiers, SafePropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler'; +import { + AST, + ASTWithSource, + BindingPipe, + ParseSourceSpan, + PropertyRead, + PropertyWrite, + R3Identifiers, + SafePropertyRead, + TmplAstBoundAttribute, + TmplAstBoundEvent, + TmplAstElement, + TmplAstNode, + TmplAstReference, + TmplAstTemplate, + TmplAstTextAttribute, + TmplAstVariable, +} from '@angular/compiler'; import ts from 'typescript'; import {AbsoluteFsPath} from '../../file_system'; @@ -15,9 +32,31 @@ import {HostDirectiveMeta, isHostDirectiveMetaForGlobalMode} from '../../metadat import {ClassDeclaration} from '../../reflection'; import {ComponentScopeKind, ComponentScopeReader} from '../../scope'; import {isAssignment, isSymbolWithValueDeclaration} from '../../util/src/typescript'; -import {BindingSymbol, DirectiveSymbol, DomBindingSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, PipeSymbol, ReferenceSymbol, Symbol, SymbolKind, TcbLocation, TemplateSymbol, TsNodeSymbolInfo, TypeCheckableDirectiveMeta, VariableSymbol} from '../api'; - -import {ExpressionIdentifier, findAllMatchingNodes, findFirstMatchingNode, hasExpressionIdentifier} from './comments'; +import { + BindingSymbol, + DirectiveSymbol, + DomBindingSymbol, + ElementSymbol, + ExpressionSymbol, + InputBindingSymbol, + OutputBindingSymbol, + PipeSymbol, + ReferenceSymbol, + Symbol, + SymbolKind, + TcbLocation, + TemplateSymbol, + TsNodeSymbolInfo, + TypeCheckableDirectiveMeta, + VariableSymbol, +} from '../api'; + +import { + ExpressionIdentifier, + findAllMatchingNodes, + findFirstMatchingNode, + hasExpressionIdentifier, +} from './comments'; import {TemplateData} from './context'; import {isAccessExpression} from './ts_util'; @@ -28,28 +67,28 @@ import {isAccessExpression} from './ts_util'; * replaced if the component's template changes. */ export class SymbolBuilder { - private symbolCache = new Map(); + private symbolCache = new Map(); constructor( - private readonly tcbPath: AbsoluteFsPath, - private readonly tcbIsShim: boolean, - private readonly typeCheckBlock: ts.Node, - private readonly templateData: TemplateData, - private readonly componentScopeReader: ComponentScopeReader, - // The `ts.TypeChecker` depends on the current type-checking program, and so must be requested - // on-demand instead of cached. - private readonly getTypeChecker: () => ts.TypeChecker, + private readonly tcbPath: AbsoluteFsPath, + private readonly tcbIsShim: boolean, + private readonly typeCheckBlock: ts.Node, + private readonly templateData: TemplateData, + private readonly componentScopeReader: ComponentScopeReader, + // The `ts.TypeChecker` depends on the current type-checking program, and so must be requested + // on-demand instead of cached. + private readonly getTypeChecker: () => ts.TypeChecker, ) {} - getSymbol(node: TmplAstTemplate|TmplAstElement): TemplateSymbol|ElementSymbol|null; - getSymbol(node: TmplAstReference|TmplAstVariable): ReferenceSymbol|VariableSymbol|null; - getSymbol(node: AST|TmplAstNode): Symbol|null; - getSymbol(node: AST|TmplAstNode): Symbol|null { + getSymbol(node: TmplAstTemplate | TmplAstElement): TemplateSymbol | ElementSymbol | null; + getSymbol(node: TmplAstReference | TmplAstVariable): ReferenceSymbol | VariableSymbol | null; + getSymbol(node: AST | TmplAstNode): Symbol | null; + getSymbol(node: AST | TmplAstNode): Symbol | null { if (this.symbolCache.has(node)) { return this.symbolCache.get(node)!; } - let symbol: Symbol|null = null; + let symbol: Symbol | null = null; if (node instanceof TmplAstBoundAttribute || node instanceof TmplAstTextAttribute) { // TODO(atscott): input and output bindings only return the first directive match but should // return a list of bindings for all of them. @@ -76,16 +115,18 @@ export class SymbolBuilder { return symbol; } - private getSymbolOfAstTemplate(template: TmplAstTemplate): TemplateSymbol|null { + private getSymbolOfAstTemplate(template: TmplAstTemplate): TemplateSymbol | null { const directives = this.getDirectivesOfNode(template); return {kind: SymbolKind.Template, directives, templateNode: template}; } - private getSymbolOfElement(element: TmplAstElement): ElementSymbol|null { + private getSymbolOfElement(element: TmplAstElement): ElementSymbol | null { const elementSourceSpan = element.startSourceSpan ?? element.sourceSpan; - const node = findFirstMatchingNode( - this.typeCheckBlock, {withSpan: elementSourceSpan, filter: ts.isVariableDeclaration}); + const node = findFirstMatchingNode(this.typeCheckBlock, { + withSpan: elementSourceSpan, + filter: ts.isVariableDeclaration, + }); if (node === null) { return null; } @@ -107,24 +148,30 @@ export class SymbolBuilder { }; } - private getDirectivesOfNode(element: TmplAstElement|TmplAstTemplate): DirectiveSymbol[] { + private getDirectivesOfNode(element: TmplAstElement | TmplAstTemplate): DirectiveSymbol[] { const elementSourceSpan = element.startSourceSpan ?? element.sourceSpan; const tcbSourceFile = this.typeCheckBlock.getSourceFile(); // directives could be either: // - var _t1: TestDir /*T:D*/ = null! as TestDir; // - var _t1 /*T:D*/ = _ctor1({}); - const isDirectiveDeclaration = (node: ts.Node): node is ts.TypeNode|ts.Identifier => - (ts.isTypeNode(node) || ts.isIdentifier(node)) && ts.isVariableDeclaration(node.parent) && - hasExpressionIdentifier(tcbSourceFile, node, ExpressionIdentifier.DIRECTIVE); - - const nodes = findAllMatchingNodes( - this.typeCheckBlock, {withSpan: elementSourceSpan, filter: isDirectiveDeclaration}); + const isDirectiveDeclaration = (node: ts.Node): node is ts.TypeNode | ts.Identifier => + (ts.isTypeNode(node) || ts.isIdentifier(node)) && + ts.isVariableDeclaration(node.parent) && + hasExpressionIdentifier(tcbSourceFile, node, ExpressionIdentifier.DIRECTIVE); + + const nodes = findAllMatchingNodes(this.typeCheckBlock, { + withSpan: elementSourceSpan, + filter: isDirectiveDeclaration, + }); const symbols: DirectiveSymbol[] = []; for (const node of nodes) { const symbol = this.getSymbolOfTsNode(node.parent); - if (symbol === null || !isSymbolWithValueDeclaration(symbol.tsSymbol) || - !ts.isClassDeclaration(symbol.tsSymbol.valueDeclaration)) { + if ( + symbol === null || + !isSymbolWithValueDeclaration(symbol.tsSymbol) || + !ts.isClassDeclaration(symbol.tsSymbol.valueDeclaration) + ) { continue; } @@ -158,8 +205,10 @@ export class SymbolBuilder { } private addHostDirectiveSymbols( - host: TmplAstTemplate|TmplAstElement, hostDirectives: HostDirectiveMeta[], - symbols: DirectiveSymbol[]): void { + host: TmplAstTemplate | TmplAstElement, + hostDirectives: HostDirectiveMeta[], + symbols: DirectiveSymbol[], + ): void { for (const current of hostDirectives) { if (!isHostDirectiveMetaForGlobalMode(current)) { throw new Error('Impossible state: typecheck code path in local compilation mode.'); @@ -198,8 +247,9 @@ export class SymbolBuilder { } private getDirectiveMeta( - host: TmplAstTemplate|TmplAstElement, - directiveDeclaration: ts.Declaration): TypeCheckableDirectiveMeta|null { + host: TmplAstTemplate | TmplAstElement, + directiveDeclaration: ts.Declaration, + ): TypeCheckableDirectiveMeta | null { let directives = this.templateData.boundTarget.getDirectivesOfNode(host); // `getDirectivesOfNode` will not return the directives intended for an element @@ -207,8 +257,8 @@ export class SymbolBuilder { // the `dir` will be skipped, but it's needed in language service. const firstChild = host.children[0]; if (firstChild instanceof TmplAstElement) { - const isMicrosyntaxTemplate = host instanceof TmplAstTemplate && - sourceSpanEqual(firstChild.sourceSpan, host.sourceSpan); + const isMicrosyntaxTemplate = + host instanceof TmplAstTemplate && sourceSpanEqual(firstChild.sourceSpan, host.sourceSpan); if (isMicrosyntaxTemplate) { const firstChildDirectives = this.templateData.boundTarget.getDirectivesOfNode(firstChild); if (firstChildDirectives !== null && directives !== null) { @@ -222,10 +272,10 @@ export class SymbolBuilder { return null; } - return directives.find(m => m.ref.node === directiveDeclaration) ?? null; + return directives.find((m) => m.ref.node === directiveDeclaration) ?? null; } - private getDirectiveModule(declaration: ts.ClassDeclaration): ClassDeclaration|null { + private getDirectiveModule(declaration: ts.ClassDeclaration): ClassDeclaration | null { const scope = this.componentScopeReader.getScopeForComponent(declaration as ClassDeclaration); if (scope === null || scope.kind !== ComponentScopeKind.NgModule) { return null; @@ -233,7 +283,7 @@ export class SymbolBuilder { return scope.ngModule; } - private getSymbolOfBoundEvent(eventBinding: TmplAstBoundEvent): OutputBindingSymbol|null { + private getSymbolOfBoundEvent(eventBinding: TmplAstBoundEvent): OutputBindingSymbol | null { const consumer = this.templateData.boundTarget.getConsumerOfBinding(eventBinding); if (consumer === null) { return null; @@ -258,7 +308,7 @@ export class SymbolBuilder { expectedAccess = bindingPropertyNames[0].classPropertyName; } - function filter(n: ts.Node): n is ts.PropertyAccessExpression|ts.ElementAccessExpression { + function filter(n: ts.Node): n is ts.PropertyAccessExpression | ts.ElementAccessExpression { if (!isAccessExpression(n)) { return false; } @@ -266,12 +316,15 @@ export class SymbolBuilder { if (ts.isPropertyAccessExpression(n)) { return n.name.getText() === expectedAccess; } else { - return ts.isStringLiteral(n.argumentExpression) && - n.argumentExpression.text === expectedAccess; + return ( + ts.isStringLiteral(n.argumentExpression) && n.argumentExpression.text === expectedAccess + ); } } - const outputFieldAccesses = - findAllMatchingNodes(this.typeCheckBlock, {withSpan: eventBinding.keySpan, filter}); + const outputFieldAccesses = findAllMatchingNodes(this.typeCheckBlock, { + withSpan: eventBinding.keySpan, + filter, + }); const bindings: BindingSymbol[] = []; for (const outputFieldAccess of outputFieldAccesses) { @@ -305,8 +358,9 @@ export class SymbolBuilder { if (!ts.isElementAccessExpression(outputFieldAccess)) { continue; } - const tsSymbol = - this.getTypeChecker().getSymbolAtLocation(outputFieldAccess.argumentExpression); + const tsSymbol = this.getTypeChecker().getSymbolAtLocation( + outputFieldAccess.argumentExpression, + ); if (tsSymbol === undefined) { continue; } @@ -338,8 +392,9 @@ export class SymbolBuilder { return {kind: SymbolKind.Output, bindings}; } - private getSymbolOfInputBinding(binding: TmplAstBoundAttribute| - TmplAstTextAttribute): InputBindingSymbol|DomBindingSymbol|null { + private getSymbolOfInputBinding( + binding: TmplAstBoundAttribute | TmplAstTextAttribute, + ): InputBindingSymbol | DomBindingSymbol | null { const consumer = this.templateData.boundTarget.getConsumerOfBinding(binding); if (consumer === null) { return null; @@ -350,8 +405,10 @@ export class SymbolBuilder { return host !== null ? {kind: SymbolKind.DomBinding, host} : null; } - const nodes = findAllMatchingNodes( - this.typeCheckBlock, {withSpan: binding.sourceSpan, filter: isAssignment}); + const nodes = findAllMatchingNodes(this.typeCheckBlock, { + withSpan: binding.sourceSpan, + filter: isAssignment, + }); const bindings: BindingSymbol[] = []; for (const node of nodes) { if (!isAccessExpression(node.left)) { @@ -359,7 +416,7 @@ export class SymbolBuilder { } const signalInputAssignment = unwrapSignalInputWriteTAccessor(node.left); - let symbolInfo: TsNodeSymbolInfo|null = null; + let symbolInfo: TsNodeSymbolInfo | null = null; // Signal inputs need special treatment because they are generated with an extra keyed // access. E.g. `_t1.prop[WriteT_ACCESSOR_SYMBOL]`. Observations: @@ -370,11 +427,14 @@ export class SymbolBuilder { const fieldSymbol = this.getSymbolOfTsNode(signalInputAssignment.fieldExpr); const typeSymbol = this.getSymbolOfTsNode(signalInputAssignment.typeExpr); - symbolInfo = fieldSymbol === null || typeSymbol === null ? null : { - tcbLocation: fieldSymbol.tcbLocation, - tsSymbol: fieldSymbol.tsSymbol, - tsType: typeSymbol.tsType, - }; + symbolInfo = + fieldSymbol === null || typeSymbol === null + ? null + : { + tcbLocation: fieldSymbol.tcbLocation, + tsSymbol: fieldSymbol.tsSymbol, + tsType: typeSymbol.tsType, + }; } else { symbolInfo = this.getSymbolOfTsNode(node.left); } @@ -384,7 +444,9 @@ export class SymbolBuilder { } const target = this.getDirectiveSymbolForAccessExpression( - signalInputAssignment?.fieldExpr ?? node.left, consumer); + signalInputAssignment?.fieldExpr ?? node.left, + consumer, + ); if (target === null) { continue; } @@ -403,28 +465,39 @@ export class SymbolBuilder { } private getDirectiveSymbolForAccessExpression( - fieldAccessExpr: ts.ElementAccessExpression|ts.PropertyAccessExpression, - {isComponent, selector, isStructural}: TypeCheckableDirectiveMeta): DirectiveSymbol|null { + fieldAccessExpr: ts.ElementAccessExpression | ts.PropertyAccessExpression, + {isComponent, selector, isStructural}: TypeCheckableDirectiveMeta, + ): DirectiveSymbol | null { // In all cases, `_t1["index"]` or `_t1.index`, `node.expression` is _t1. const tsSymbol = this.getTypeChecker().getSymbolAtLocation(fieldAccessExpr.expression); - if (tsSymbol?.declarations === undefined || tsSymbol.declarations.length === 0 || - selector === null) { + if ( + tsSymbol?.declarations === undefined || + tsSymbol.declarations.length === 0 || + selector === null + ) { return null; } const [declaration] = tsSymbol.declarations; - if (!ts.isVariableDeclaration(declaration) || - !hasExpressionIdentifier( - // The expression identifier could be on the type (for regular directives) or the name - // (for generic directives and the ctor op). - declaration.getSourceFile(), declaration.type ?? declaration.name, - ExpressionIdentifier.DIRECTIVE)) { + if ( + !ts.isVariableDeclaration(declaration) || + !hasExpressionIdentifier( + // The expression identifier could be on the type (for regular directives) or the name + // (for generic directives and the ctor op). + declaration.getSourceFile(), + declaration.type ?? declaration.name, + ExpressionIdentifier.DIRECTIVE, + ) + ) { return null; } const symbol = this.getSymbolOfTsNode(declaration); - if (symbol === null || !isSymbolWithValueDeclaration(symbol.tsSymbol) || - !ts.isClassDeclaration(symbol.tsSymbol.valueDeclaration)) { + if ( + symbol === null || + !isSymbolWithValueDeclaration(symbol.tsSymbol) || + !ts.isClassDeclaration(symbol.tsSymbol.valueDeclaration) + ) { return null; } @@ -441,18 +514,20 @@ export class SymbolBuilder { selector, ngModule, isHostDirective: false, - isInScope: true, // TODO: this should always be in scope in this context, right? + isInScope: true, // TODO: this should always be in scope in this context, right? }; } - private getSymbolOfVariable(variable: TmplAstVariable): VariableSymbol|null { - const node = findFirstMatchingNode( - this.typeCheckBlock, {withSpan: variable.sourceSpan, filter: ts.isVariableDeclaration}); + private getSymbolOfVariable(variable: TmplAstVariable): VariableSymbol | null { + const node = findFirstMatchingNode(this.typeCheckBlock, { + withSpan: variable.sourceSpan, + filter: ts.isVariableDeclaration, + }); if (node === null) { return null; } - let nodeValueSymbol: TsNodeSymbolInfo|null = null; + let nodeValueSymbol: TsNodeSymbolInfo | null = null; if (ts.isForOfStatement(node.parent.parent)) { nodeValueSymbol = this.getSymbolOfTsNode(node); } else if (node.initializer !== undefined) { @@ -473,15 +548,17 @@ export class SymbolBuilder { tcbPath: this.tcbPath, isShimFile: this.tcbIsShim, positionInFile: this.getTcbPositionForNode(node.name), - } + }, }; } - private getSymbolOfReference(ref: TmplAstReference): ReferenceSymbol|null { + private getSymbolOfReference(ref: TmplAstReference): ReferenceSymbol | null { const target = this.templateData.boundTarget.getReferenceTarget(ref); // Find the node for the reference declaration, i.e. `var _t2 = _t1;` - let node = findFirstMatchingNode( - this.typeCheckBlock, {withSpan: ref.sourceSpan, filter: ts.isVariableDeclaration}); + let node = findFirstMatchingNode(this.typeCheckBlock, { + withSpan: ref.sourceSpan, + filter: ts.isVariableDeclaration, + }); if (node === null || target === null || node.initializer === undefined) { return null; } @@ -490,10 +567,11 @@ export class SymbolBuilder { // which are of the form var _t3 = (_t2 as any as i2.TemplateRef) // TODO(atscott): Consider adding an `ExpressionIdentifier` to tag variable declaration // initializers as invalid for symbol retrieval. - const originalDeclaration = ts.isParenthesizedExpression(node.initializer) && - ts.isAsExpression(node.initializer.expression) ? - this.getTypeChecker().getSymbolAtLocation(node.name) : - this.getTypeChecker().getSymbolAtLocation(node.initializer); + const originalDeclaration = + ts.isParenthesizedExpression(node.initializer) && + ts.isAsExpression(node.initializer.expression) + ? this.getTypeChecker().getSymbolAtLocation(node.name) + : this.getTypeChecker().getSymbolAtLocation(node.initializer); if (originalDeclaration === undefined || originalDeclaration.valueDeclaration === undefined) { return null; } @@ -534,10 +612,11 @@ export class SymbolBuilder { } } - private getSymbolOfPipe(expression: BindingPipe): PipeSymbol|null { - const methodAccess = findFirstMatchingNode( - this.typeCheckBlock, - {withSpan: expression.nameSpan, filter: ts.isPropertyAccessExpression}); + private getSymbolOfPipe(expression: BindingPipe): PipeSymbol | null { + const methodAccess = findFirstMatchingNode(this.typeCheckBlock, { + withSpan: expression.nameSpan, + filter: ts.isPropertyAccessExpression, + }); if (methodAccess === null) { return null; } @@ -571,8 +650,9 @@ export class SymbolBuilder { }; } - private getSymbolOfTemplateExpression(expression: AST): VariableSymbol|ReferenceSymbol - |ExpressionSymbol|null { + private getSymbolOfTemplateExpression( + expression: AST, + ): VariableSymbol | ReferenceSymbol | ExpressionSymbol | null { if (expression instanceof ASTWithSource) { expression = expression.ast; } @@ -590,13 +670,15 @@ export class SymbolBuilder { withSpan = expression.nameSpan; } - let node: ts.Node|null = null; + let node: ts.Node | null = null; // Property reads in templates usually map to a `PropertyAccessExpression` // (e.g. `ctx.foo`) so try looking for one first. if (expression instanceof PropertyRead) { - node = findFirstMatchingNode( - this.typeCheckBlock, {withSpan, filter: ts.isPropertyAccessExpression}); + node = findFirstMatchingNode(this.typeCheckBlock, { + withSpan, + filter: ts.isPropertyAccessExpression, + }); } // Otherwise fall back to searching for any AST node. @@ -628,7 +710,7 @@ export class SymbolBuilder { kind: SymbolKind.Expression, // Rather than using the type of only the `whenTrue` part of the expression, we should // still get the type of the whole conditional expression to include `|undefined`. - tsType: this.getTypeChecker().getTypeAtLocation(node) + tsType: this.getTypeChecker().getTypeAtLocation(node), }; } else { const symbolInfo = this.getSymbolOfTsNode(node); @@ -636,12 +718,12 @@ export class SymbolBuilder { } } - private getSymbolOfTsNode(node: ts.Node): TsNodeSymbolInfo|null { + private getSymbolOfTsNode(node: ts.Node): TsNodeSymbolInfo | null { while (ts.isParenthesizedExpression(node)) { node = node.expression; } - let tsSymbol: ts.Symbol|undefined; + let tsSymbol: ts.Symbol | undefined; if (ts.isPropertyAccessExpression(node)) { tsSymbol = this.getTypeChecker().getSymbolAtLocation(node.name); } else { @@ -688,29 +770,35 @@ function sourceSpanEqual(a: ParseSourceSpan, b: ParseSourceSpan) { return a.start.offset === b.start.offset && a.end.offset === b.end.offset; } -function unwrapSignalInputWriteTAccessor(expr: ts.LeftHandSideExpression): (null|{ - fieldExpr: ts.ElementAccessExpression | ts.PropertyAccessExpression, - typeExpr: ts.ElementAccessExpression -}) { +function unwrapSignalInputWriteTAccessor(expr: ts.LeftHandSideExpression): null | { + fieldExpr: ts.ElementAccessExpression | ts.PropertyAccessExpression; + typeExpr: ts.ElementAccessExpression; +} { // e.g. `_t2.inputA[i2.ɵINPUT_SIGNAL_BRAND_WRITE_TYPE]` // 1. Assert that we are dealing with an element access expression. // 2. Assert that we are dealing with a signal brand symbol access in the argument expression. - if (!ts.isElementAccessExpression(expr) || - !ts.isPropertyAccessExpression(expr.argumentExpression)) { + if ( + !ts.isElementAccessExpression(expr) || + !ts.isPropertyAccessExpression(expr.argumentExpression) + ) { return null; } // Assert that the property access in the element access is a simple identifier and // refers to `ɵINPUT_SIGNAL_BRAND_WRITE_TYPE`. - if (!ts.isIdentifier(expr.argumentExpression.name) || - expr.argumentExpression.name.text !== R3Identifiers.InputSignalBrandWriteType.name) { + if ( + !ts.isIdentifier(expr.argumentExpression.name) || + expr.argumentExpression.name.text !== R3Identifiers.InputSignalBrandWriteType.name + ) { return null; } // Assert that the `_t2.inputA` is actually either a keyed element access, or // property access expression. This is checked for type safety and to catch unexpected cases. - if (!ts.isPropertyAccessExpression(expr.expression) && - !ts.isElementAccessExpression(expr.expression)) { + if ( + !ts.isPropertyAccessExpression(expr.expression) && + !ts.isElementAccessExpression(expr.expression) + ) { throw new Error('Unexpected expression for signal input write type.'); } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/ts_util.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/ts_util.ts index 33aee30fb4112..42b466a361f04 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/ts_util.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/ts_util.ts @@ -10,8 +10,6 @@ import ts from 'typescript'; import {addExpressionIdentifier, ExpressionIdentifier} from './comments'; - - /** * A `Set` of `ts.SyntaxKind`s of `ts.Expression` which are safe to wrap in a `ts.AsExpression` * without needing to be wrapped in parentheses. @@ -52,11 +50,11 @@ export function tsCastToAny(expr: ts.Expression): ts.Expression { } // The outer expression is always wrapped in parentheses. - return ts.factory.createParenthesizedExpression(ts.factory.createAsExpression( - expr, ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword))); + return ts.factory.createParenthesizedExpression( + ts.factory.createAsExpression(expr, ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)), + ); } - /** * Create an expression which instantiates an element by its HTML tagName. * @@ -65,11 +63,14 @@ export function tsCastToAny(expr: ts.Expression): ts.Expression { */ export function tsCreateElement(tagName: string): ts.Expression { const createElement = ts.factory.createPropertyAccessExpression( - /* expression */ ts.factory.createIdentifier('document'), 'createElement'); + /* expression */ ts.factory.createIdentifier('document'), + 'createElement', + ); return ts.factory.createCallExpression( - /* expression */ createElement, - /* typeArguments */ undefined, - /* argumentsArray */[ts.factory.createStringLiteral(tagName)]); + /* expression */ createElement, + /* typeArguments */ undefined, + /* argumentsArray */ [ts.factory.createStringLiteral(tagName)], + ); } /** @@ -85,16 +86,20 @@ export function tsDeclareVariable(id: ts.Identifier, type: ts.TypeNode): ts.Vari // in the initializer, e.g. `var _t1 = null! as boolean;`. addExpressionIdentifier(type, ExpressionIdentifier.VARIABLE_AS_EXPRESSION); const initializer: ts.Expression = ts.factory.createAsExpression( - ts.factory.createNonNullExpression(ts.factory.createNull()), type); + ts.factory.createNonNullExpression(ts.factory.createNull()), + type, + ); const decl = ts.factory.createVariableDeclaration( - /* name */ id, - /* exclamationToken */ undefined, - /* type */ undefined, - /* initializer */ initializer); + /* name */ id, + /* exclamationToken */ undefined, + /* type */ undefined, + /* initializer */ initializer, + ); return ts.factory.createVariableStatement( - /* modifiers */ undefined, - /* declarationList */[decl]); + /* modifiers */ undefined, + /* declarationList */ [decl], + ); } /** @@ -107,9 +112,12 @@ export function tsDeclareVariable(id: ts.Identifier, type: ts.TypeNode): ts.Vari * @param coercedInputName The field name of the coerced input. */ export function tsCreateTypeQueryForCoercedInput( - typeName: ts.EntityName, coercedInputName: string): ts.TypeQueryNode { + typeName: ts.EntityName, + coercedInputName: string, +): ts.TypeQueryNode { return ts.factory.createTypeQueryNode( - ts.factory.createQualifiedName(typeName, `ngAcceptInputType_${coercedInputName}`)); + ts.factory.createQualifiedName(typeName, `ngAcceptInputType_${coercedInputName}`), + ); } /** @@ -119,38 +127,47 @@ export function tsCreateTypeQueryForCoercedInput( * expression. */ export function tsCreateVariable( - id: ts.Identifier, initializer: ts.Expression): ts.VariableStatement { + id: ts.Identifier, + initializer: ts.Expression, +): ts.VariableStatement { const decl = ts.factory.createVariableDeclaration( - /* name */ id, - /* exclamationToken */ undefined, - /* type */ undefined, - /* initializer */ initializer); + /* name */ id, + /* exclamationToken */ undefined, + /* type */ undefined, + /* initializer */ initializer, + ); return ts.factory.createVariableStatement( - /* modifiers */ undefined, - /* declarationList */[decl]); + /* modifiers */ undefined, + /* declarationList */ [decl], + ); } /** * Construct a `ts.CallExpression` that calls a method on a receiver. */ export function tsCallMethod( - receiver: ts.Expression, methodName: string, args: ts.Expression[] = []): ts.CallExpression { + receiver: ts.Expression, + methodName: string, + args: ts.Expression[] = [], +): ts.CallExpression { const methodAccess = ts.factory.createPropertyAccessExpression(receiver, methodName); return ts.factory.createCallExpression( - /* expression */ methodAccess, - /* typeArguments */ undefined, - /* argumentsArray */ args); + /* expression */ methodAccess, + /* typeArguments */ undefined, + /* argumentsArray */ args, + ); } -export function isAccessExpression(node: ts.Node): node is ts.ElementAccessExpression| - ts.PropertyAccessExpression { +export function isAccessExpression( + node: ts.Node, +): node is ts.ElementAccessExpression | ts.PropertyAccessExpression { return ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node); } /** * Creates a TypeScript node representing a numeric value. */ -export function tsNumericExpression(value: number): ts.NumericLiteral|ts.PrefixUnaryExpression { +export function tsNumericExpression(value: number): ts.NumericLiteral | ts.PrefixUnaryExpression { // As of TypeScript 5.3 negative numbers are represented as `prefixUnaryOperator` and passing a // negative number (even as a string) into `createNumericLiteral` will result in an error. if (value < 0) { diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts index bffc90ce3600e..d647e341435a7 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts @@ -6,7 +6,51 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, BindingPipe, BindingType, BoundTarget, Call, createCssSelectorFromNode, CssSelector, DYNAMIC_TYPE, ImplicitReceiver, ParsedEventType, ParseSourceSpan, PropertyRead, PropertyWrite, R3Identifiers, SafeCall, SafePropertyRead, SchemaMetadata, SelectorMatcher, ThisReceiver, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstBoundText, TmplAstContent, TmplAstDeferredBlock, TmplAstDeferredBlockTriggers, TmplAstElement, TmplAstForLoopBlock, TmplAstForLoopBlockEmpty, TmplAstHoverDeferredTrigger, TmplAstIcu, TmplAstIfBlock, TmplAstIfBlockBranch, TmplAstInteractionDeferredTrigger, TmplAstNode, TmplAstReference, TmplAstSwitchBlock, TmplAstSwitchBlockCase, TmplAstTemplate, TmplAstText, TmplAstTextAttribute, TmplAstVariable, TmplAstViewportDeferredTrigger, TransplantedType} from '@angular/compiler'; +import { + AST, + BindingPipe, + BindingType, + BoundTarget, + Call, + createCssSelectorFromNode, + CssSelector, + DYNAMIC_TYPE, + ImplicitReceiver, + ParsedEventType, + ParseSourceSpan, + PropertyRead, + PropertyWrite, + R3Identifiers, + SafeCall, + SafePropertyRead, + SchemaMetadata, + SelectorMatcher, + ThisReceiver, + TmplAstBoundAttribute, + TmplAstBoundEvent, + TmplAstBoundText, + TmplAstContent, + TmplAstDeferredBlock, + TmplAstDeferredBlockTriggers, + TmplAstElement, + TmplAstForLoopBlock, + TmplAstForLoopBlockEmpty, + TmplAstHoverDeferredTrigger, + TmplAstIcu, + TmplAstIfBlock, + TmplAstIfBlockBranch, + TmplAstInteractionDeferredTrigger, + TmplAstNode, + TmplAstReference, + TmplAstSwitchBlock, + TmplAstSwitchBlockCase, + TmplAstTemplate, + TmplAstText, + TmplAstTextAttribute, + TmplAstVariable, + TmplAstViewportDeferredTrigger, + TransplantedType, +} from '@angular/compiler'; import ts from 'typescript'; import {Reference} from '../../imports'; @@ -15,12 +59,24 @@ import {ClassDeclaration} from '../../reflection'; import {TemplateId, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata} from '../api'; import {addExpressionIdentifier, ExpressionIdentifier, markIgnoreDiagnostics} from './comments'; -import {addParseSpanInfo, addTemplateId, wrapForDiagnostics, wrapForTypeChecker} from './diagnostics'; +import { + addParseSpanInfo, + addTemplateId, + wrapForDiagnostics, + wrapForTypeChecker, +} from './diagnostics'; import {DomSchemaChecker} from './dom'; import {Environment} from './environment'; import {astToTypescript, NULL_AS_ANY} from './expression'; import {OutOfBandDiagnosticRecorder} from './oob'; -import {tsCallMethod, tsCastToAny, tsCreateElement, tsCreateTypeQueryForCoercedInput, tsCreateVariable, tsDeclareVariable} from './ts_util'; +import { + tsCallMethod, + tsCastToAny, + tsCreateElement, + tsCreateTypeQueryForCoercedInput, + tsCreateVariable, + tsDeclareVariable, +} from './ts_util'; import {requiresInlineTypeCtor} from './type_constructor'; import {TypeParameterEmitter} from './type_parameter_emitter'; @@ -77,22 +133,35 @@ export enum TcbGenericContextBehavior { * bounds) will be referenced from the generated TCB code. */ export function generateTypeCheckBlock( - env: Environment, ref: Reference>, name: ts.Identifier, - meta: TypeCheckBlockMetadata, domSchemaChecker: DomSchemaChecker, - oobRecorder: OutOfBandDiagnosticRecorder, - genericContextBehavior: TcbGenericContextBehavior): ts.FunctionDeclaration { + env: Environment, + ref: Reference>, + name: ts.Identifier, + meta: TypeCheckBlockMetadata, + domSchemaChecker: DomSchemaChecker, + oobRecorder: OutOfBandDiagnosticRecorder, + genericContextBehavior: TcbGenericContextBehavior, +): ts.FunctionDeclaration { const tcb = new Context( - env, domSchemaChecker, oobRecorder, meta.id, meta.boundTarget, meta.pipes, meta.schemas, - meta.isStandalone, meta.preserveWhitespaces); + env, + domSchemaChecker, + oobRecorder, + meta.id, + meta.boundTarget, + meta.pipes, + meta.schemas, + meta.isStandalone, + meta.preserveWhitespaces, + ); const scope = Scope.forNodes(tcb, null, null, tcb.boundTarget.target.template!, /* guard */ null); const ctxRawType = env.referenceType(ref); if (!ts.isTypeReferenceNode(ctxRawType)) { throw new Error( - `Expected TypeReferenceNode when referencing the ctx param for ${ref.debugName}`); + `Expected TypeReferenceNode when referencing the ctx param for ${ref.debugName}`, + ); } - let typeParameters: ts.TypeParameterDeclaration[]|undefined = undefined; - let typeArguments: ts.TypeNode[]|undefined = undefined; + let typeParameters: ts.TypeParameterDeclaration[] | undefined = undefined; + let typeArguments: ts.TypeNode[] | undefined = undefined; if (ref.node.typeParameters !== undefined) { if (!env.config.useContextGenericType) { @@ -102,17 +171,23 @@ export function generateTypeCheckBlock( switch (genericContextBehavior) { case TcbGenericContextBehavior.UseEmitter: // Guaranteed to emit type parameters since we checked that the class has them above. - typeParameters = new TypeParameterEmitter(ref.node.typeParameters, env.reflector) - .emit(typeRef => env.referenceType(typeRef))!; - typeArguments = typeParameters.map(param => ts.factory.createTypeReferenceNode(param.name)); + typeParameters = new TypeParameterEmitter(ref.node.typeParameters, env.reflector).emit( + (typeRef) => env.referenceType(typeRef), + )!; + typeArguments = typeParameters.map((param) => + ts.factory.createTypeReferenceNode(param.name), + ); break; case TcbGenericContextBehavior.CopyClassNodes: typeParameters = [...ref.node.typeParameters]; - typeArguments = typeParameters.map(param => ts.factory.createTypeReferenceNode(param.name)); + typeArguments = typeParameters.map((param) => + ts.factory.createTypeReferenceNode(param.name), + ); break; case TcbGenericContextBehavior.FallbackToAny: - typeArguments = ref.node.typeParameters.map( - () => ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); + typeArguments = ref.node.typeParameters.map(() => + ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), + ); break; } } @@ -120,23 +195,22 @@ export function generateTypeCheckBlock( const paramList = [tcbThisParam(ctxRawType.typeName, typeArguments)]; const scopeStatements = scope.render(); - const innerBody = ts.factory.createBlock([ - ...env.getPreludeStatements(), - ...scopeStatements, - ]); + const innerBody = ts.factory.createBlock([...env.getPreludeStatements(), ...scopeStatements]); // Wrap the body in an "if (true)" expression. This is unnecessary but has the effect of causing // the `ts.Printer` to format the type-check block nicely. - const body = ts.factory.createBlock( - [ts.factory.createIfStatement(ts.factory.createTrue(), innerBody, undefined)]); + const body = ts.factory.createBlock([ + ts.factory.createIfStatement(ts.factory.createTrue(), innerBody, undefined), + ]); const fnDecl = ts.factory.createFunctionDeclaration( - /* modifiers */ undefined, - /* asteriskToken */ undefined, - /* name */ name, - /* typeParameters */ env.config.useContextGenericType ? typeParameters : undefined, - /* parameters */ paramList, - /* type */ undefined, - /* body */ body); + /* modifiers */ undefined, + /* asteriskToken */ undefined, + /* name */ name, + /* typeParameters */ env.config.useContextGenericType ? typeParameters : undefined, + /* parameters */ paramList, + /* type */ undefined, + /* body */ body, + ); addTemplateId(fnDecl, meta.id); return fnDecl; } @@ -166,7 +240,7 @@ abstract class TcbOp { */ abstract readonly optional: boolean; - abstract execute(): ts.Expression|null; + abstract execute(): ts.Expression | null; /** * Replacement value or operation used while this `TcbOp` is executing (i.e. to resolve circular @@ -176,7 +250,7 @@ abstract class TcbOp { * `TcbOp` can be returned in cases where additional code generation is necessary to deal with * circular references. */ - circularFallback(): TcbOp|ts.Expression { + circularFallback(): TcbOp | ts.Expression { return INFER_TYPE_FOR_CIRCULAR_OP_EXPR; } } @@ -188,7 +262,11 @@ abstract class TcbOp { * Executing this operation returns a reference to the element variable. */ class TcbElementOp extends TcbOp { - constructor(private tcb: Context, private scope: Scope, private element: TmplAstElement) { + constructor( + private tcb: Context, + private scope: Scope, + private element: TmplAstElement, + ) { super(); } @@ -217,8 +295,11 @@ class TcbElementOp extends TcbOp { */ class TcbTemplateVariableOp extends TcbOp { constructor( - private tcb: Context, private scope: Scope, private template: TmplAstTemplate, - private variable: TmplAstVariable) { + private tcb: Context, + private scope: Scope, + private template: TmplAstTemplate, + private variable: TmplAstVariable, + ) { super(); } @@ -234,8 +315,9 @@ class TcbTemplateVariableOp extends TcbOp { // on the template context. const id = this.tcb.allocateId(); const initializer = ts.factory.createPropertyAccessExpression( - /* expression */ ctx, - /* name */ this.variable.value || '$implicit'); + /* expression */ ctx, + /* name */ this.variable.value || '$implicit', + ); addParseSpanInfo(id, this.variable.keySpan); // Declare the variable, and return its identifier. @@ -258,7 +340,10 @@ class TcbTemplateVariableOp extends TcbOp { * Executing this operation returns a reference to the template's context variable. */ class TcbTemplateContextOp extends TcbOp { - constructor(private tcb: Context, private scope: Scope) { + constructor( + private tcb: Context, + private scope: Scope, + ) { super(); } @@ -283,7 +368,11 @@ class TcbTemplateContextOp extends TcbOp { * or more type guard conditions that narrow types within the template body. */ class TcbTemplateBodyOp extends TcbOp { - constructor(private tcb: Context, private scope: Scope, private template: TmplAstTemplate) { + constructor( + private tcb: Context, + private scope: Scope, + private template: TmplAstTemplate, + ) { super(); } @@ -307,18 +396,21 @@ class TcbTemplateBodyOp extends TcbOp { if (directives !== null) { for (const dir of directives) { const dirInstId = this.scope.resolve(this.template, dir); - const dirId = - this.tcb.env.reference(dir.ref as Reference>); + const dirId = this.tcb.env.reference( + dir.ref as Reference>, + ); // There are two kinds of guards. Template guards (ngTemplateGuards) allow type narrowing of // the expression passed to an @Input of the directive. Scan the directive to see if it has // any template guards, and generate them if needed. - dir.ngTemplateGuards.forEach(guard => { + dir.ngTemplateGuards.forEach((guard) => { // For each template guard function on the directive, look for a binding to that input. - const boundInput = this.template.inputs.find(i => i.name === guard.inputName) || - this.template.templateAttrs.find( - (i: TmplAstTextAttribute|TmplAstBoundAttribute): i is TmplAstBoundAttribute => - i instanceof TmplAstBoundAttribute && i.name === guard.inputName); + const boundInput = + this.template.inputs.find((i) => i.name === guard.inputName) || + this.template.templateAttrs.find( + (i: TmplAstTextAttribute | TmplAstBoundAttribute): i is TmplAstBoundAttribute => + i instanceof TmplAstBoundAttribute && i.name === guard.inputName, + ); if (boundInput !== undefined) { // If there is such a binding, generate an expression for it. const expr = tcbExpression(boundInput.value, this.tcb, this.scope); @@ -352,8 +444,9 @@ class TcbTemplateBodyOp extends TcbOp { addParseSpanInfo(guardInvoke, this.template.sourceSpan); directiveGuards.push(guardInvoke); } else if ( - this.template.variables.length > 0 && - this.tcb.env.config.suggestionsForSuboptimalTypeInference) { + this.template.variables.length > 0 && + this.tcb.env.config.suggestionsForSuboptimalTypeInference + ) { // The compiler could have inferred a better type for the variables in this template, // but was prevented from doing so by the type-checking configuration. Issue a warning // diagnostic. @@ -364,22 +457,28 @@ class TcbTemplateBodyOp extends TcbOp { } // By default the guard is simply `true`. - let guard: ts.Expression|null = null; + let guard: ts.Expression | null = null; // If there are any guards from directives, use them instead. if (directiveGuards.length > 0) { // Pop the first value and use it as the initializer to reduce(). This way, a single guard // will be used on its own, but two or more will be combined into binary AND expressions. guard = directiveGuards.reduce( - (expr, dirGuard) => ts.factory.createBinaryExpression( - expr, ts.SyntaxKind.AmpersandAmpersandToken, dirGuard), - directiveGuards.pop()!); + (expr, dirGuard) => + ts.factory.createBinaryExpression(expr, ts.SyntaxKind.AmpersandAmpersandToken, dirGuard), + directiveGuards.pop()!, + ); } // Create a new Scope for the template. This constructs the list of operations for the template // children, as well as tracks bindings within the template. - const tmplScope = - Scope.forNodes(this.tcb, this.scope, this.template, this.template.children, guard); + const tmplScope = Scope.forNodes( + this.tcb, + this.scope, + this.template, + this.template.children, + guard, + ); // Render the template's `Scope` into its statements. const statements = tmplScope.render(); @@ -397,8 +496,10 @@ class TcbTemplateBodyOp extends TcbOp { if (guard !== null) { // The scope has a guard that needs to be applied, so wrap the template block into an `if` // statement containing the guard expression. - tmplBlock = - ts.factory.createIfStatement(/* expression */ guard, /* thenStatement */ tmplBlock); + tmplBlock = ts.factory.createIfStatement( + /* expression */ guard, + /* thenStatement */ tmplBlock, + ); } this.scope.addStatement(tmplBlock); @@ -412,7 +513,11 @@ class TcbTemplateBodyOp extends TcbOp { * Executing this operation returns nothing. */ class TcbExpressionOp extends TcbOp { - constructor(private tcb: Context, private scope: Scope, private expression: AST) { + constructor( + private tcb: Context, + private scope: Scope, + private expression: AST, + ) { super(); } @@ -433,8 +538,11 @@ class TcbExpressionOp extends TcbOp { */ abstract class TcbDirectiveTypeOpBase extends TcbOp { constructor( - protected tcb: Context, protected scope: Scope, - protected node: TmplAstTemplate|TmplAstElement, protected dir: TypeCheckableDirectiveMeta) { + protected tcb: Context, + protected scope: Scope, + protected node: TmplAstTemplate | TmplAstElement, + protected dir: TypeCheckableDirectiveMeta, + ) { super(); } @@ -456,10 +564,12 @@ abstract class TcbDirectiveTypeOpBase extends TcbOp { } else { if (!ts.isTypeReferenceNode(rawType)) { throw new Error( - `Expected TypeReferenceNode when referencing the type for ${this.dir.ref.debugName}`); + `Expected TypeReferenceNode when referencing the type for ${this.dir.ref.debugName}`, + ); } - const typeArguments = dirRef.node.typeParameters.map( - () => ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); + const typeArguments = dirRef.node.typeParameters.map(() => + ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), + ); type = ts.factory.createTypeReferenceNode(rawType.typeName, typeArguments); } @@ -506,8 +616,9 @@ class TcbGenericDirectiveTypeWithAnyParamsOp extends TcbDirectiveTypeOpBase { override execute(): ts.Identifier { const dirRef = this.dir.ref as Reference>; if (dirRef.node.typeParameters === undefined) { - throw new Error(`Assertion Error: expected typeParameters when creating a declaration for ${ - dirRef.debugName}`); + throw new Error( + `Assertion Error: expected typeParameters when creating a declaration for ${dirRef.debugName}`, + ); } return super.execute(); @@ -536,10 +647,12 @@ class TcbGenericDirectiveTypeWithAnyParamsOp extends TcbDirectiveTypeOpBase { */ class TcbReferenceOp extends TcbOp { constructor( - private readonly tcb: Context, private readonly scope: Scope, - private readonly node: TmplAstReference, - private readonly host: TmplAstElement|TmplAstTemplate, - private readonly target: TypeCheckableDirectiveMeta|TmplAstTemplate|TmplAstElement) { + private readonly tcb: Context, + private readonly scope: Scope, + private readonly node: TmplAstReference, + private readonly host: TmplAstElement | TmplAstTemplate, + private readonly target: TypeCheckableDirectiveMeta | TmplAstTemplate | TmplAstElement, + ) { super(); } @@ -550,28 +663,35 @@ class TcbReferenceOp extends TcbOp { override execute(): ts.Identifier { const id = this.tcb.allocateId(); let initializer: ts.Expression = - this.target instanceof TmplAstTemplate || this.target instanceof TmplAstElement ? - this.scope.resolve(this.target) : - this.scope.resolve(this.host, this.target); + this.target instanceof TmplAstTemplate || this.target instanceof TmplAstElement + ? this.scope.resolve(this.target) + : this.scope.resolve(this.host, this.target); // The reference is either to an element, an node, or to a directive on an // element or template. - if ((this.target instanceof TmplAstElement && !this.tcb.env.config.checkTypeOfDomReferences) || - !this.tcb.env.config.checkTypeOfNonDomReferences) { + if ( + (this.target instanceof TmplAstElement && !this.tcb.env.config.checkTypeOfDomReferences) || + !this.tcb.env.config.checkTypeOfNonDomReferences + ) { // References to DOM nodes are pinned to 'any' when `checkTypeOfDomReferences` is `false`. // References to `TemplateRef`s and directives are pinned to 'any' when // `checkTypeOfNonDomReferences` is `false`. initializer = ts.factory.createAsExpression( - initializer, ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); + initializer, + ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), + ); } else if (this.target instanceof TmplAstTemplate) { // Direct references to an node simply require a value of type // `TemplateRef`. To get this, an expression of the form // `(_t1 as any as TemplateRef)` is constructed. initializer = ts.factory.createAsExpression( - initializer, ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); + initializer, + ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), + ); initializer = ts.factory.createAsExpression( - initializer, - this.tcb.env.referenceExternalType('@angular/core', 'TemplateRef', [DYNAMIC_TYPE])); + initializer, + this.tcb.env.referenceExternalType('@angular/core', 'TemplateRef', [DYNAMIC_TYPE]), + ); initializer = ts.factory.createParenthesizedExpression(initializer); } addParseSpanInfo(initializer, this.node.sourceSpan); @@ -588,7 +708,10 @@ class TcbReferenceOp extends TcbOp { * itself is recorded out-of-band. */ class TcbInvalidReferenceOp extends TcbOp { - constructor(private readonly tcb: Context, private readonly scope: Scope) { + constructor( + private readonly tcb: Context, + private readonly scope: Scope, + ) { super(); } @@ -616,8 +739,11 @@ class TcbInvalidReferenceOp extends TcbOp { */ class TcbDirectiveCtorOp extends TcbOp { constructor( - private tcb: Context, private scope: Scope, private node: TmplAstTemplate|TmplAstElement, - private dir: TypeCheckableDirectiveMeta) { + private tcb: Context, + private scope: Scope, + private node: TmplAstTemplate | TmplAstElement, + private dir: TypeCheckableDirectiveMeta, + ) { super(); } @@ -637,8 +763,10 @@ class TcbDirectiveCtorOp extends TcbOp { for (const attr of boundAttrs) { // Skip text attributes if configured to do so. - if (!this.tcb.env.config.checkTypeOfAttributes && - attr.attribute instanceof TmplAstTextAttribute) { + if ( + !this.tcb.env.config.checkTypeOfAttributes && + attr.attribute instanceof TmplAstTextAttribute + ) { continue; } for (const {fieldName, isTwoWayBinding} of attr.inputs) { @@ -688,8 +816,11 @@ class TcbDirectiveCtorOp extends TcbOp { */ class TcbDirectiveInputsOp extends TcbOp { constructor( - private tcb: Context, private scope: Scope, private node: TmplAstTemplate|TmplAstElement, - private dir: TypeCheckableDirectiveMeta) { + private tcb: Context, + private scope: Scope, + private node: TmplAstTemplate | TmplAstElement, + private dir: TypeCheckableDirectiveMeta, + ) { super(); } @@ -698,7 +829,7 @@ class TcbDirectiveInputsOp extends TcbOp { } override execute(): null { - let dirId: ts.Expression|null = null; + let dirId: ts.Expression | null = null; // TODO(joost): report duplicate properties @@ -738,7 +869,8 @@ class TcbDirectiveInputsOp extends TcbOp { if (!ts.isTypeReferenceNode(dirTypeRef)) { throw new Error( - `Expected TypeReferenceNode from reference to ${this.dir.ref.debugName}`); + `Expected TypeReferenceNode from reference to ${this.dir.ref.debugName}`, + ); } type = tsCreateTypeQueryForCoercedInput(dirTypeRef.typeName, fieldName); @@ -754,8 +886,9 @@ class TcbDirectiveInputsOp extends TcbOp { // assignment target available, so this field is skipped. continue; } else if ( - !this.tcb.env.config.honorAccessModifiersForInputBindings && - this.dir.restrictedInputFields.has(fieldName)) { + !this.tcb.env.config.honorAccessModifiersForInputBindings && + this.dir.restrictedInputFields.has(fieldName) + ) { // If strict checking of access modifiers is disabled and the field is restricted // (i.e. private/protected/readonly), generate an assignment into a temporary variable // that has the type of the field. This achieves type-checking but circumvents the access @@ -768,11 +901,13 @@ class TcbDirectiveInputsOp extends TcbOp { const dirTypeRef = this.tcb.env.referenceType(this.dir.ref); if (!ts.isTypeReferenceNode(dirTypeRef)) { throw new Error( - `Expected TypeReferenceNode from reference to ${this.dir.ref.debugName}`); + `Expected TypeReferenceNode from reference to ${this.dir.ref.debugName}`, + ); } const type = ts.factory.createIndexedAccessTypeNode( - ts.factory.createTypeQueryNode(dirId as ts.Identifier), - ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(fieldName))); + ts.factory.createTypeQueryNode(dirId as ts.Identifier), + ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(fieldName)), + ); const temp = tsDeclareVariable(id, type); this.scope.addStatement(temp); target = id; @@ -784,11 +919,15 @@ class TcbDirectiveInputsOp extends TcbOp { // To get errors assign directly to the fields on the instance, using property access // when possible. String literal fields may not be valid JS identifiers so we use // literal element access instead for those cases. - target = this.dir.stringLiteralInputFields.has(fieldName) ? - ts.factory.createElementAccessExpression( - dirId, ts.factory.createStringLiteral(fieldName)) : - ts.factory.createPropertyAccessExpression( - dirId, ts.factory.createIdentifier(fieldName)); + target = this.dir.stringLiteralInputFields.has(fieldName) + ? ts.factory.createElementAccessExpression( + dirId, + ts.factory.createStringLiteral(fieldName), + ) + : ts.factory.createPropertyAccessExpression( + dirId, + ts.factory.createIdentifier(fieldName), + ); } // For signal inputs, we unwrap the target `InputSignal`. Note that @@ -798,12 +937,16 @@ class TcbDirectiveInputsOp extends TcbOp { // This is a significant requirement for language service auto-completion. if (isSignal) { const inputSignalBrandWriteSymbol = this.tcb.env.referenceExternalSymbol( - R3Identifiers.InputSignalBrandWriteType.moduleName, - R3Identifiers.InputSignalBrandWriteType.name); - if (!ts.isIdentifier(inputSignalBrandWriteSymbol) && - !ts.isPropertyAccessExpression(inputSignalBrandWriteSymbol)) { - throw new Error(`Expected identifier or property access for reference to ${ - R3Identifiers.InputSignalBrandWriteType.name}`); + R3Identifiers.InputSignalBrandWriteType.moduleName, + R3Identifiers.InputSignalBrandWriteType.name, + ); + if ( + !ts.isIdentifier(inputSignalBrandWriteSymbol) && + !ts.isPropertyAccessExpression(inputSignalBrandWriteSymbol) + ) { + throw new Error( + `Expected identifier or property access for reference to ${R3Identifiers.InputSignalBrandWriteType.name}`, + ); } target = ts.factory.createElementAccessExpression(target, inputSignalBrandWriteSymbol); @@ -819,14 +962,19 @@ class TcbDirectiveInputsOp extends TcbOp { } // Finally the assignment is extended by assigning it into the target expression. - assignment = - ts.factory.createBinaryExpression(target, ts.SyntaxKind.EqualsToken, assignment); + assignment = ts.factory.createBinaryExpression( + target, + ts.SyntaxKind.EqualsToken, + assignment, + ); } addParseSpanInfo(assignment, attr.attribute.sourceSpan); // Ignore diagnostics for text attributes if configured to do so. - if (!this.tcb.env.config.checkTypeOfAttributes && - attr.attribute instanceof TmplAstTextAttribute) { + if ( + !this.tcb.env.config.checkTypeOfAttributes && + attr.attribute instanceof TmplAstTextAttribute + ) { markIgnoreDiagnostics(assignment); } @@ -849,7 +997,12 @@ class TcbDirectiveInputsOp extends TcbOp { if (missing.length > 0) { this.tcb.oobRecorder.missingRequiredInputs( - this.tcb.id, this.node, this.dir.name, this.dir.isComponent, missing); + this.tcb.id, + this.node, + this.dir.name, + this.dir.isComponent, + missing, + ); } } } @@ -870,8 +1023,11 @@ class TcbDirectiveInputsOp extends TcbOp { */ class TcbDirectiveCtorCircularFallbackOp extends TcbOp { constructor( - private tcb: Context, private scope: Scope, private node: TmplAstTemplate|TmplAstElement, - private dir: TypeCheckableDirectiveMeta) { + private tcb: Context, + private scope: Scope, + private node: TmplAstTemplate | TmplAstElement, + private dir: TypeCheckableDirectiveMeta, + ) { super(); } @@ -883,8 +1039,10 @@ class TcbDirectiveCtorCircularFallbackOp extends TcbOp { const id = this.tcb.allocateId(); const typeCtor = this.tcb.env.typeCtorFor(this.dir); const circularPlaceholder = ts.factory.createCallExpression( - typeCtor, /* typeArguments */ undefined, - [ts.factory.createNonNullExpression(ts.factory.createNull())]); + typeCtor, + /* typeArguments */ undefined, + [ts.factory.createNonNullExpression(ts.factory.createNull())], + ); this.scope.addStatement(tsCreateVariable(id, circularPlaceholder)); return id; } @@ -902,8 +1060,11 @@ class TcbDirectiveCtorCircularFallbackOp extends TcbOp { */ class TcbDomSchemaCheckerOp extends TcbOp { constructor( - private tcb: Context, private element: TmplAstElement, private checkElement: boolean, - private claimedInputs: Set) { + private tcb: Context, + private element: TmplAstElement, + private checkElement: boolean, + private claimedInputs: Set, + ) { super(); } @@ -911,16 +1072,20 @@ class TcbDomSchemaCheckerOp extends TcbOp { return false; } - override execute(): ts.Expression|null { + override execute(): ts.Expression | null { if (this.checkElement) { this.tcb.domSchemaChecker.checkElement( - this.tcb.id, this.element, this.tcb.schemas, this.tcb.hostIsStandalone); + this.tcb.id, + this.element, + this.tcb.schemas, + this.tcb.hostIsStandalone, + ); } // TODO(alxhub): this could be more efficient. for (const binding of this.element.inputs) { const isPropertyBinding = - binding.type === BindingType.Property || binding.type === BindingType.TwoWay; + binding.type === BindingType.Property || binding.type === BindingType.TwoWay; if (isPropertyBinding && this.claimedInputs.has(binding.name)) { // Skip this binding as it was claimed by a directive. @@ -931,15 +1096,19 @@ class TcbDomSchemaCheckerOp extends TcbOp { // A direct binding to a property. const propertyName = ATTR_TO_PROP.get(binding.name) ?? binding.name; this.tcb.domSchemaChecker.checkProperty( - this.tcb.id, this.element, propertyName, binding.sourceSpan, this.tcb.schemas, - this.tcb.hostIsStandalone); + this.tcb.id, + this.element, + propertyName, + binding.sourceSpan, + this.tcb.schemas, + this.tcb.hostIsStandalone, + ); } } return null; } } - /** * A `TcbOp` that finds and flags control flow nodes that interfere with content projection. * @@ -956,15 +1125,19 @@ class TcbControlFlowContentProjectionOp extends TcbOp { private readonly category: ts.DiagnosticCategory; constructor( - private tcb: Context, private element: TmplAstElement, private ngContentSelectors: string[], - private componentName: string) { + private tcb: Context, + private element: TmplAstElement, + private ngContentSelectors: string[], + private componentName: string, + ) { super(); // We only need to account for `error` and `warning` since // this check won't be enabled for `suppress`. - this.category = tcb.env.config.controlFlowPreventingContentProjection === 'error' ? - ts.DiagnosticCategory.Error : - ts.DiagnosticCategory.Warning; + this.category = + tcb.env.config.controlFlowPreventingContentProjection === 'error' + ? ts.DiagnosticCategory.Error + : ts.DiagnosticCategory.Warning; } override readonly optional = false; @@ -987,8 +1160,14 @@ class TcbControlFlowContentProjectionOp extends TcbOp { if (child instanceof TmplAstElement || child instanceof TmplAstTemplate) { matcher.match(createCssSelectorFromNode(child), (_, originalSelector) => { this.tcb.oobRecorder.controlFlowPreventingContentProjection( - this.tcb.id, this.category, child, this.componentName, originalSelector, root, - this.tcb.hostPreserveWhitespaces); + this.tcb.id, + this.category, + child, + this.componentName, + originalSelector, + root, + this.tcb.hostPreserveWhitespaces, + ); }); } } @@ -999,8 +1178,9 @@ class TcbControlFlowContentProjectionOp extends TcbOp { } private findPotentialControlFlowNodes() { - const result: Array = []; + const result: Array< + TmplAstIfBlockBranch | TmplAstSwitchBlockCase | TmplAstForLoopBlock | TmplAstForLoopBlockEmpty + > = []; for (const child of this.element.children) { if (child instanceof TmplAstForLoopBlock) { @@ -1028,7 +1208,7 @@ class TcbControlFlowContentProjectionOp extends TcbOp { return result; } - private shouldCheck(node: TmplAstNode&{children: TmplAstNode[]}): boolean { + private shouldCheck(node: TmplAstNode & {children: TmplAstNode[]}): boolean { // Skip nodes with less than two children since it's impossible // for them to run into the issue that we're checking for. if (node.children.length < 2) { @@ -1044,8 +1224,11 @@ class TcbControlFlowContentProjectionOp extends TcbOp { // `preserveWhitespaces` to preserve the accuracy of source maps diagnostics. This means // that we have to account for it here since the presence of text nodes affects the // content projection behavior. - if (!(child instanceof TmplAstText) || this.tcb.hostPreserveWhitespaces || - child.value.trim().length > 0) { + if ( + !(child instanceof TmplAstText) || + this.tcb.hostPreserveWhitespaces || + child.value.trim().length > 0 + ) { // Content projection will be affected if there's more than one root node. if (hasSeenRootNode) { return true; @@ -1062,14 +1245,16 @@ class TcbControlFlowContentProjectionOp extends TcbOp { * Mapping between attributes names that don't correspond to their element property names. * Note: this mapping has to be kept in sync with the equally named mapping in the runtime. */ -const ATTR_TO_PROP = new Map(Object.entries({ - 'class': 'className', - 'for': 'htmlFor', - 'formaction': 'formAction', - 'innerHtml': 'innerHTML', - 'readonly': 'readOnly', - 'tabindex': 'tabIndex', -})); +const ATTR_TO_PROP = new Map( + Object.entries({ + 'class': 'className', + 'for': 'htmlFor', + 'formaction': 'formAction', + 'innerHtml': 'innerHTML', + 'readonly': 'readOnly', + 'tabindex': 'tabIndex', + }), +); /** * A `TcbOp` which generates code to check "unclaimed inputs" - bindings on an element which were @@ -1083,8 +1268,11 @@ const ATTR_TO_PROP = new Map(Object.entries({ */ class TcbUnclaimedInputsOp extends TcbOp { constructor( - private tcb: Context, private scope: Scope, private element: TmplAstElement, - private claimedInputs: Set) { + private tcb: Context, + private scope: Scope, + private element: TmplAstElement, + private claimedInputs: Set, + ) { super(); } @@ -1095,12 +1283,12 @@ class TcbUnclaimedInputsOp extends TcbOp { override execute(): null { // `this.inputs` contains only those bindings not matched by any directive. These bindings go to // the element itself. - let elId: ts.Expression|null = null; + let elId: ts.Expression | null = null; // TODO(alxhub): this could be more efficient. for (const binding of this.element.inputs) { const isPropertyBinding = - binding.type === BindingType.Property || binding.type === BindingType.TwoWay; + binding.type === BindingType.Property || binding.type === BindingType.TwoWay; if (isPropertyBinding && this.claimedInputs.has(binding.name)) { // Skip this binding as it was claimed by a directive. @@ -1117,9 +1305,14 @@ class TcbUnclaimedInputsOp extends TcbOp { // A direct binding to a property. const propertyName = ATTR_TO_PROP.get(binding.name) ?? binding.name; const prop = ts.factory.createElementAccessExpression( - elId, ts.factory.createStringLiteral(propertyName)); + elId, + ts.factory.createStringLiteral(propertyName), + ); const stmt = ts.factory.createBinaryExpression( - prop, ts.SyntaxKind.EqualsToken, wrapForDiagnostics(expr)); + prop, + ts.SyntaxKind.EqualsToken, + wrapForDiagnostics(expr), + ); addParseSpanInfo(stmt, binding.sourceSpan); this.scope.addStatement(ts.factory.createExpressionStatement(stmt)); } else { @@ -1145,8 +1338,11 @@ class TcbUnclaimedInputsOp extends TcbOp { */ export class TcbDirectiveOutputsOp extends TcbOp { constructor( - private tcb: Context, private scope: Scope, private node: TmplAstTemplate|TmplAstElement, - private dir: TypeCheckableDirectiveMeta) { + private tcb: Context, + private scope: Scope, + private node: TmplAstTemplate | TmplAstElement, + private dir: TypeCheckableDirectiveMeta, + ) { super(); } @@ -1155,12 +1351,14 @@ export class TcbDirectiveOutputsOp extends TcbOp { } override execute(): null { - let dirId: ts.Expression|null = null; + let dirId: ts.Expression | null = null; const outputs = this.dir.outputs; for (const output of this.node.outputs) { - if (output.type === ParsedEventType.Animation || - !outputs.hasBindingPropertyName(output.name)) { + if ( + output.type === ParsedEventType.Animation || + !outputs.hasBindingPropertyName(output.name) + ) { continue; } @@ -1174,8 +1372,10 @@ export class TcbDirectiveOutputsOp extends TcbOp { if (dirId === null) { dirId = this.scope.resolve(this.node, this.dir); } - const outputField = - ts.factory.createElementAccessExpression(dirId, ts.factory.createStringLiteral(field)); + const outputField = ts.factory.createElementAccessExpression( + dirId, + ts.factory.createStringLiteral(field), + ); addParseSpanInfo(outputField, output.keySpan); if (this.tcb.env.config.checkTypeOfOutputEvents) { // For strict checking of directive events, generate a call to the `subscribe` method @@ -1183,8 +1383,9 @@ export class TcbDirectiveOutputsOp extends TcbOp { // `$event` parameter. const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Infer); const subscribeFn = ts.factory.createPropertyAccessExpression(outputField, 'subscribe'); - const call = - ts.factory.createCallExpression(subscribeFn, /* typeArguments */ undefined, [handler]); + const call = ts.factory.createCallExpression(subscribeFn, /* typeArguments */ undefined, [ + handler, + ]); addParseSpanInfo(call, output.sourceSpan); this.scope.addStatement(ts.factory.createExpressionStatement(call)); } else { @@ -1213,8 +1414,11 @@ export class TcbDirectiveOutputsOp extends TcbOp { */ class TcbUnclaimedOutputsOp extends TcbOp { constructor( - private tcb: Context, private scope: Scope, private element: TmplAstElement, - private claimedOutputs: Set) { + private tcb: Context, + private scope: Scope, + private element: TmplAstElement, + private claimedOutputs: Set, + ) { super(); } @@ -1223,7 +1427,7 @@ class TcbUnclaimedOutputsOp extends TcbOp { } override execute(): null { - let elId: ts.Expression|null = null; + let elId: ts.Expression | null = null; // TODO(alxhub): this could be more efficient. for (const output of this.element.outputs) { @@ -1242,9 +1446,9 @@ class TcbUnclaimedOutputsOp extends TcbOp { if (output.type === ParsedEventType.Animation) { // Animation output bindings always have an `$event` parameter of type `AnimationEvent`. - const eventType = this.tcb.env.config.checkTypeOfAnimationEvents ? - this.tcb.env.referenceExternalType('@angular/animations', 'AnimationEvent') : - EventParamType.Any; + const eventType = this.tcb.env.config.checkTypeOfAnimationEvents + ? this.tcb.env.referenceExternalType('@angular/animations', 'AnimationEvent') + : EventParamType.Any; const handler = tcbCreateEventHandler(output, this.tcb, this.scope, eventType); this.scope.addStatement(ts.factory.createExpressionStatement(handler)); @@ -1262,9 +1466,10 @@ class TcbUnclaimedOutputsOp extends TcbOp { const propertyAccess = ts.factory.createPropertyAccessExpression(elId, 'addEventListener'); addParseSpanInfo(propertyAccess, output.keySpan); const call = ts.factory.createCallExpression( - /* expression */ propertyAccess, - /* typeArguments */ undefined, - /* arguments */[ts.factory.createStringLiteral(output.name), handler]); + /* expression */ propertyAccess, + /* typeArguments */ undefined, + /* arguments */ [ts.factory.createStringLiteral(output.name), handler], + ); addParseSpanInfo(call, output.sourceSpan); this.scope.addStatement(ts.factory.createExpressionStatement(call)); } else { @@ -1310,8 +1515,11 @@ class TcbComponentContextCompletionOp extends TcbOp { */ class TcbBlockVariableOp extends TcbOp { constructor( - private tcb: Context, private scope: Scope, private initializer: ts.Expression, - private variable: TmplAstVariable) { + private tcb: Context, + private scope: Scope, + private initializer: ts.Expression, + private variable: TmplAstVariable, + ) { super(); } @@ -1337,8 +1545,11 @@ class TcbBlockVariableOp extends TcbOp { */ class TcbBlockImplicitVariableOp extends TcbOp { constructor( - private tcb: Context, private scope: Scope, private type: ts.TypeNode, - private variable: TmplAstVariable) { + private tcb: Context, + private scope: Scope, + private type: ts.TypeNode, + private variable: TmplAstVariable, + ) { super(); } @@ -1354,7 +1565,6 @@ class TcbBlockImplicitVariableOp extends TcbOp { } } - /** * A `TcbOp` which renders an `if` template block as a TypeScript `if` statement. * @@ -1363,7 +1573,11 @@ class TcbBlockImplicitVariableOp extends TcbOp { class TcbIfOp extends TcbOp { private expressionScopes = new Map(); - constructor(private tcb: Context, private scope: Scope, private block: TmplAstIfBlock) { + constructor( + private tcb: Context, + private scope: Scope, + private block: TmplAstIfBlock, + ) { super(); } @@ -1377,7 +1591,7 @@ class TcbIfOp extends TcbOp { return null; } - private generateBranch(index: number): ts.Statement|undefined { + private generateBranch(index: number): ts.Statement | undefined { const branch = this.block.branches[index]; if (!branch) { @@ -1395,27 +1609,35 @@ class TcbIfOp extends TcbOp { // for the case where the expression has an alias _and_ because we need the processed // expression when generating the guard for the body. const expressionScope = Scope.forNodes(this.tcb, this.scope, branch, [], null); - expressionScope.render().forEach(stmt => this.scope.addStatement(stmt)); + expressionScope.render().forEach((stmt) => this.scope.addStatement(stmt)); this.expressionScopes.set(branch, expressionScope); - const expression = branch.expressionAlias === null ? - tcbExpression(branch.expression, this.tcb, expressionScope) : - expressionScope.resolve(branch.expressionAlias); + const expression = + branch.expressionAlias === null + ? tcbExpression(branch.expression, this.tcb, expressionScope) + : expressionScope.resolve(branch.expressionAlias); const bodyScope = this.getBranchScope(expressionScope, branch, index); return ts.factory.createIfStatement( - expression, ts.factory.createBlock(bodyScope.render()), this.generateBranch(index + 1)); + expression, + ts.factory.createBlock(bodyScope.render()), + this.generateBranch(index + 1), + ); } private getBranchScope(parentScope: Scope, branch: TmplAstIfBlockBranch, index: number): Scope { const checkBody = this.tcb.env.config.checkControlFlowBodies; return Scope.forNodes( - this.tcb, parentScope, null, checkBody ? branch.children : [], - checkBody ? this.generateBranchGuard(index) : null); + this.tcb, + parentScope, + null, + checkBody ? branch.children : [], + checkBody ? this.generateBranchGuard(index) : null, + ); } - private generateBranchGuard(index: number): ts.Expression|null { - let guard: ts.Expression|null = null; + private generateBranchGuard(index: number): ts.Expression | null { + let guard: ts.Expression | null = null; // Since event listeners are inside callbacks, type narrowing doesn't apply to them anymore. // To recreate the behavior, we generate an expression that negates all the values of the @@ -1452,30 +1674,40 @@ class TcbIfOp extends TcbOp { // The expressions of the preceding branches have to be negated // (e.g. `expr` becomes `!(expr)`) when comparing in the guard, except // for the branch's own expression which is preserved as is. - const comparisonExpression = i === index ? - expression : - ts.factory.createPrefixUnaryExpression( - ts.SyntaxKind.ExclamationToken, ts.factory.createParenthesizedExpression(expression)); + const comparisonExpression = + i === index + ? expression + : ts.factory.createPrefixUnaryExpression( + ts.SyntaxKind.ExclamationToken, + ts.factory.createParenthesizedExpression(expression), + ); // Finally add the expression to the guard with an && operator. - guard = guard === null ? - comparisonExpression : - ts.factory.createBinaryExpression( - guard, ts.SyntaxKind.AmpersandAmpersandToken, comparisonExpression); + guard = + guard === null + ? comparisonExpression + : ts.factory.createBinaryExpression( + guard, + ts.SyntaxKind.AmpersandAmpersandToken, + comparisonExpression, + ); } return guard; } } - /** * A `TcbOp` which renders a `switch` block as a TypeScript `switch` statement. * * Executing this operation returns nothing. */ class TcbSwitchOp extends TcbOp { - constructor(private tcb: Context, private scope: Scope, private block: TmplAstSwitchBlock) { + constructor( + private tcb: Context, + private scope: Scope, + private block: TmplAstSwitchBlock, + ) { super(); } @@ -1485,27 +1717,36 @@ class TcbSwitchOp extends TcbOp { override execute(): null { const switchExpression = tcbExpression(this.block.expression, this.tcb, this.scope); - const clauses = this.block.cases.map(current => { + const clauses = this.block.cases.map((current) => { const checkBody = this.tcb.env.config.checkControlFlowBodies; const clauseScope = Scope.forNodes( - this.tcb, this.scope, null, checkBody ? current.children : [], - checkBody ? this.generateGuard(current, switchExpression) : null); + this.tcb, + this.scope, + null, + checkBody ? current.children : [], + checkBody ? this.generateGuard(current, switchExpression) : null, + ); const statements = [...clauseScope.render(), ts.factory.createBreakStatement()]; - return current.expression === null ? - ts.factory.createDefaultClause(statements) : - ts.factory.createCaseClause( - tcbExpression(current.expression, this.tcb, clauseScope), statements); + return current.expression === null + ? ts.factory.createDefaultClause(statements) + : ts.factory.createCaseClause( + tcbExpression(current.expression, this.tcb, clauseScope), + statements, + ); }); this.scope.addStatement( - ts.factory.createSwitchStatement(switchExpression, ts.factory.createCaseBlock(clauses))); + ts.factory.createSwitchStatement(switchExpression, ts.factory.createCaseBlock(clauses)), + ); return null; } - private generateGuard(node: TmplAstSwitchBlockCase, switchValue: ts.Expression): ts.Expression - |null { + private generateGuard( + node: TmplAstSwitchBlockCase, + switchValue: ts.Expression, + ): ts.Expression | null { // For non-default cases, the guard needs to compare against the case value, e.g. // `switchExpression === caseExpression`. if (node.expression !== null) { @@ -1513,7 +1754,10 @@ class TcbSwitchOp extends TcbOp { const expression = tcbExpression(node.expression, this.tcb, this.scope); markIgnoreDiagnostics(expression); return ts.factory.createBinaryExpression( - switchValue, ts.SyntaxKind.EqualsEqualsEqualsToken, expression); + switchValue, + ts.SyntaxKind.EqualsEqualsEqualsToken, + expression, + ); } // To fully narrow the type in the default case, we need to generate an expression that negates @@ -1524,7 +1768,7 @@ class TcbSwitchOp extends TcbOp { // @default {} // } // Will produce the guard `expr !== 1 && expr !== 2`. - let guard: ts.Expression|null = null; + let guard: ts.Expression | null = null; for (const current of this.block.cases) { if (current.expression === null) { @@ -1535,13 +1779,19 @@ class TcbSwitchOp extends TcbOp { const expression = tcbExpression(current.expression, this.tcb, this.scope); markIgnoreDiagnostics(expression); const comparison = ts.factory.createBinaryExpression( - switchValue, ts.SyntaxKind.ExclamationEqualsEqualsToken, expression); + switchValue, + ts.SyntaxKind.ExclamationEqualsEqualsToken, + expression, + ); if (guard === null) { guard = comparison; } else { guard = ts.factory.createBinaryExpression( - guard, ts.SyntaxKind.AmpersandAmpersandToken, comparison); + guard, + ts.SyntaxKind.AmpersandAmpersandToken, + comparison, + ); } } @@ -1555,7 +1805,11 @@ class TcbSwitchOp extends TcbOp { * Executing this operation returns nothing. */ class TcbForOfOp extends TcbOp { - constructor(private tcb: Context, private scope: Scope, private block: TmplAstForLoopBlock) { + constructor( + private tcb: Context, + private scope: Scope, + private block: TmplAstForLoopBlock, + ) { super(); } @@ -1565,20 +1819,28 @@ class TcbForOfOp extends TcbOp { override execute(): null { const loopScope = Scope.forNodes( - this.tcb, this.scope, this.block, - this.tcb.env.config.checkControlFlowBodies ? this.block.children : [], null); + this.tcb, + this.scope, + this.block, + this.tcb.env.config.checkControlFlowBodies ? this.block.children : [], + null, + ); const initializerId = loopScope.resolve(this.block.item); if (!ts.isIdentifier(initializerId)) { throw new Error( - `Could not resolve for loop variable ${this.block.item.name} to an identifier`); + `Could not resolve for loop variable ${this.block.item.name} to an identifier`, + ); } const initializer = ts.factory.createVariableDeclarationList( - [ts.factory.createVariableDeclaration(initializerId)], ts.NodeFlags.Const); + [ts.factory.createVariableDeclaration(initializerId)], + ts.NodeFlags.Const, + ); addParseSpanInfo(initializer, this.block.item.keySpan); // It's common to have a for loop over a nullable value (e.g. produced by the `async` pipe). // Add a non-null expression to allow such values to be assigned. const expression = ts.factory.createNonNullExpression( - tcbExpression(this.block.expression, this.tcb, loopScope)); + tcbExpression(this.block.expression, this.tcb, loopScope), + ); const trackTranslator = new TcbForLoopTrackTranslator(this.tcb, loopScope, this.block); const trackExpression = trackTranslator.translate(this.block.trackBy); const statements = [ @@ -1586,8 +1848,14 @@ class TcbForOfOp extends TcbOp { ts.factory.createExpressionStatement(trackExpression), ]; - this.scope.addStatement(ts.factory.createForOfStatement( - undefined, initializer, expression, ts.factory.createBlock(statements))); + this.scope.addStatement( + ts.factory.createForOfStatement( + undefined, + initializer, + expression, + ts.factory.createBlock(statements), + ), + ); return null; } @@ -1613,11 +1881,16 @@ export class Context { private nextId = 1; constructor( - readonly env: Environment, readonly domSchemaChecker: DomSchemaChecker, - readonly oobRecorder: OutOfBandDiagnosticRecorder, readonly id: TemplateId, - readonly boundTarget: BoundTarget, - private pipes: Map, readonly schemas: SchemaMetadata[], - readonly hostIsStandalone: boolean, readonly hostPreserveWhitespaces: boolean) {} + readonly env: Environment, + readonly domSchemaChecker: DomSchemaChecker, + readonly oobRecorder: OutOfBandDiagnosticRecorder, + readonly id: TemplateId, + readonly boundTarget: BoundTarget, + private pipes: Map, + readonly schemas: SchemaMetadata[], + readonly hostIsStandalone: boolean, + readonly hostPreserveWhitespaces: boolean, + ) {} /** * Allocate a new variable name for use within the `Context`. @@ -1629,7 +1902,7 @@ export class Context { return ts.factory.createIdentifier(`_t${this.nextId++}`); } - getPipeByName(name: string): PipeMeta|null { + getPipeByName(name: string): PipeMeta | null { if (!this.pipes.has(name)) { return null; } @@ -1664,7 +1937,7 @@ class Scope { * that fits instead. This has the same semantics as TypeScript itself when types are referenced * circularly. */ - private opQueue: (TcbOp|ts.Expression|null)[] = []; + private opQueue: (TcbOp | ts.Expression | null)[] = []; /** * A map of `TmplAstElement`s to the index of their `TcbElementOp` in the `opQueue` @@ -1674,8 +1947,10 @@ class Scope { * A map of maps which tracks the index of `TcbDirectiveCtorOp`s in the `opQueue` for each * directive on a `TmplAstElement` or `TmplAstTemplate` node. */ - private directiveOpMap = - new Map>(); + private directiveOpMap = new Map< + TmplAstElement | TmplAstTemplate, + Map + >(); /** * A map of `TmplAstReference`s to the index of their `TcbReferenceOp` in the `opQueue` @@ -1693,7 +1968,7 @@ class Scope { * `TmplAstVariable` nodes) to the index of their `TcbVariableOp`s in the `opQueue`, or to * pre-resolved variable identifiers. */ - private varMap = new Map(); + private varMap = new Map(); /** * Statements for this template. @@ -1715,8 +1990,10 @@ class Scope { ]); private constructor( - private tcb: Context, private parent: Scope|null = null, - private guard: ts.Expression|null = null) {} + private tcb: Context, + private parent: Scope | null = null, + private guard: ts.Expression | null = null, + ) {} /** * Constructs a `Scope` given either a `TmplAstTemplate` or a list of `TmplAstNode`s. @@ -1730,9 +2007,12 @@ class Scope { * @param guard an expression that is applied to this scope for type narrowing purposes. */ static forNodes( - tcb: Context, parentScope: Scope|null, - scopedNode: TmplAstTemplate|TmplAstIfBlockBranch|TmplAstForLoopBlock|null, - children: TmplAstNode[], guard: ts.Expression|null): Scope { + tcb: Context, + parentScope: Scope | null, + scopedNode: TmplAstTemplate | TmplAstIfBlockBranch | TmplAstForLoopBlock | null, + children: TmplAstNode[], + guard: ts.Expression | null, + ): Scope { const scope = new Scope(tcb, parentScope, guard); if (parentScope === null && tcb.env.config.enableTemplateTypeChecker) { @@ -1760,9 +2040,15 @@ class Scope { const {expression, expressionAlias} = scopedNode; if (expression !== null && expressionAlias !== null) { this.registerVariable( - scope, expressionAlias, - new TcbBlockVariableOp( - tcb, scope, tcbExpression(expression, tcb, scope), expressionAlias)); + scope, + expressionAlias, + new TcbBlockVariableOp( + tcb, + scope, + tcbExpression(expression, tcb, scope), + expressionAlias, + ), + ); } } else if (scopedNode instanceof TmplAstForLoopBlock) { // Register the variable for the loop so it can be resolved by @@ -1776,10 +2062,14 @@ class Scope { throw new Error(`Unrecognized for loop context variable ${variable.name}`); } - const type = - ts.factory.createKeywordTypeNode(this.forLoopContextVariableTypes.get(variable.value)!); + const type = ts.factory.createKeywordTypeNode( + this.forLoopContextVariableTypes.get(variable.value)!, + ); this.registerVariable( - scope, variable, new TcbBlockImplicitVariableOp(tcb, scope, type, variable)); + scope, + variable, + new TcbBlockImplicitVariableOp(tcb, scope, type, variable), + ); } } for (const node of children) { @@ -1813,8 +2103,9 @@ class Scope { * look up instead of the default for an element or template node. */ resolve( - node: TmplAstElement|TmplAstTemplate|TmplAstVariable|TmplAstReference, - directive?: TypeCheckableDirectiveMeta): ts.Identifier|ts.NonNullExpression { + node: TmplAstElement | TmplAstTemplate | TmplAstVariable | TmplAstReference, + directive?: TypeCheckableDirectiveMeta, + ): ts.Identifier | ts.NonNullExpression { // Attempt to resolve the operation locally. const res = this.resolveLocal(node, directive); if (res !== null) { @@ -1826,7 +2117,7 @@ class Scope { // // In addition, returning a clone prevents the consumer of `Scope#resolve` from // attaching comments at the declaration site. - let clone: ts.Identifier|ts.NonNullExpression; + let clone: ts.Identifier | ts.NonNullExpression; if (ts.isIdentifier(res)) { clone = ts.factory.createIdentifier(res.text); @@ -1871,8 +2162,8 @@ class Scope { * Returns an expression of all template guards that apply to this scope, including those of * parent scopes. If no guards have been applied, null is returned. */ - guards(): ts.Expression|null { - let parentGuards: ts.Expression|null = null; + guards(): ts.Expression | null { + let parentGuards: ts.Expression | null = null; if (this.parent !== null) { // Start with the guards from the parent scope, if present. parentGuards = this.parent.guards(); @@ -1890,13 +2181,17 @@ class Scope { // It is important that the parent guard is used as left operand, given that it may provide // narrowing that is required for this scope's guard to be valid. return ts.factory.createBinaryExpression( - parentGuards, ts.SyntaxKind.AmpersandAmpersandToken, this.guard); + parentGuards, + ts.SyntaxKind.AmpersandAmpersandToken, + this.guard, + ); } } private resolveLocal( - ref: TmplAstElement|TmplAstTemplate|TmplAstVariable|TmplAstReference, - directive?: TypeCheckableDirectiveMeta): ts.Expression|null { + ref: TmplAstElement | TmplAstTemplate | TmplAstVariable | TmplAstReference, + directive?: TypeCheckableDirectiveMeta, + ): ts.Expression | null { if (ref instanceof TmplAstReference && this.referenceOpMap.has(ref)) { return this.resolveOp(this.referenceOpMap.get(ref)!); } else if (ref instanceof TmplAstVariable && this.varMap.has(ref)) { @@ -1905,14 +2200,18 @@ class Scope { const opIndexOrNode = this.varMap.get(ref)!; return typeof opIndexOrNode === 'number' ? this.resolveOp(opIndexOrNode) : opIndexOrNode; } else if ( - ref instanceof TmplAstTemplate && directive === undefined && - this.templateCtxOpMap.has(ref)) { + ref instanceof TmplAstTemplate && + directive === undefined && + this.templateCtxOpMap.has(ref) + ) { // Resolving the context of the given sub-template. // Execute the `TcbTemplateContextOp` for the template. return this.resolveOp(this.templateCtxOpMap.get(ref)!); } else if ( - (ref instanceof TmplAstElement || ref instanceof TmplAstTemplate) && - directive !== undefined && this.directiveOpMap.has(ref)) { + (ref instanceof TmplAstElement || ref instanceof TmplAstTemplate) && + directive !== undefined && + this.directiveOpMap.has(ref) + ) { // Resolving a directive on an element or sub-template. const dirMap = this.directiveOpMap.get(ref)!; if (dirMap.has(directive)) { @@ -1946,7 +2245,7 @@ class Scope { * and also protects against a circular dependency from the operation to itself by temporarily * setting the operation's result to a special expression. */ - private executeOp(opIndex: number, skipOptional: boolean): ts.Expression|null { + private executeOp(opIndex: number, skipOptional: boolean): ts.Expression | null { const op = this.opQueue[opIndex]; if (!(op instanceof TcbOp)) { return op; @@ -2007,13 +2306,13 @@ class Scope { } } - private appendChildren(node: TmplAstNode&{children: TmplAstNode[]}) { + private appendChildren(node: TmplAstNode & {children: TmplAstNode[]}) { for (const child of node.children) { this.appendNode(child); } } - private checkAndAppendReferencesOfNode(node: TmplAstElement|TmplAstTemplate): void { + private checkAndAppendReferencesOfNode(node: TmplAstElement | TmplAstTemplate): void { for (const ref of node.references) { const target = this.tcb.boundTarget.getReferenceTarget(ref); @@ -2028,13 +2327,13 @@ class Scope { ctxIndex = this.opQueue.push(new TcbReferenceOp(this.tcb, this, ref, node, target)) - 1; } else { ctxIndex = - this.opQueue.push(new TcbReferenceOp(this.tcb, this, ref, node, target.directive)) - 1; + this.opQueue.push(new TcbReferenceOp(this.tcb, this, ref, node, target.directive)) - 1; } this.referenceOpMap.set(ref, ctxIndex); } } - private appendDirectivesAndInputsOfNode(node: TmplAstElement|TmplAstTemplate): void { + private appendDirectivesAndInputsOfNode(node: TmplAstElement | TmplAstTemplate): void { // Collect all the inputs on the element. const claimedInputs = new Set(); const directives = this.tcb.boundTarget.getDirectivesOfNode(node); @@ -2044,7 +2343,8 @@ class Scope { if (node instanceof TmplAstElement) { this.opQueue.push(new TcbUnclaimedInputsOp(this.tcb, this, node, claimedInputs)); this.opQueue.push( - new TcbDomSchemaCheckerOp(this.tcb, node, /* checkElement */ true, claimedInputs)); + new TcbDomSchemaCheckerOp(this.tcb, node, /* checkElement */ true, claimedInputs), + ); } return; } else { @@ -2070,8 +2370,9 @@ class Scope { // `TcbNonDirectiveTypeOp`. directiveOp = new TcbNonGenericDirectiveTypeOp(this.tcb, this, node, dir); } else if ( - !requiresInlineTypeCtor(dirRef.node, host, this.tcb.env) || - this.tcb.env.config.useInlineTypeConstructors) { + !requiresInlineTypeCtor(dirRef.node, host, this.tcb.env) || + 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. @@ -2109,7 +2410,7 @@ class Scope { } } - private appendOutputsOfNode(node: TmplAstElement|TmplAstTemplate): void { + private appendOutputsOfNode(node: TmplAstElement | TmplAstTemplate): void { // Collect all the outputs on the element. const claimedOutputs = new Set(); const directives = this.tcb.boundTarget.getDirectivesOfNode(node); @@ -2181,7 +2482,7 @@ class Scope { private appendContentProjectionCheckOp(root: TmplAstElement): void { const meta = - this.tcb.boundTarget.getDirectivesOfNode(root)?.find(meta => meta.isComponent) || null; + this.tcb.boundTarget.getDirectivesOfNode(root)?.find((meta) => meta.isComponent) || null; if (meta !== null && meta.ngContentSelectors !== null && meta.ngContentSelectors.length > 0) { const selectors = meta.ngContentSelectors; @@ -2190,7 +2491,8 @@ class Scope { // slots, or they only have one catch-all slot (represented by `*`). if (selectors.length > 1 || (selectors.length === 1 && selectors[0] !== '*')) { this.opQueue.push( - new TcbControlFlowContentProjectionOp(this.tcb, root, selectors, meta.name)); + new TcbControlFlowContentProjectionOp(this.tcb, root, selectors, meta.name), + ); } } } @@ -2214,7 +2516,9 @@ class Scope { } private appendDeferredTriggers( - block: TmplAstDeferredBlock, triggers: TmplAstDeferredBlockTriggers): void { + block: TmplAstDeferredBlock, + triggers: TmplAstDeferredBlockTriggers, + ): void { if (triggers.when !== undefined) { this.opQueue.push(new TcbExpressionOp(this.tcb, this, triggers.when.value)); } @@ -2233,9 +2537,12 @@ class Scope { } private appendReferenceBasedDeferredTrigger( - block: TmplAstDeferredBlock, - trigger: TmplAstHoverDeferredTrigger|TmplAstInteractionDeferredTrigger| - TmplAstViewportDeferredTrigger): void { + block: TmplAstDeferredBlock, + trigger: + | TmplAstHoverDeferredTrigger + | TmplAstInteractionDeferredTrigger + | TmplAstViewportDeferredTrigger, + ): void { if (this.tcb.boundTarget.getDeferredTriggerTarget(block, trigger) === null) { this.tcb.oobRecorder.inaccessibleDeferredTriggerElement(this.tcb.id, trigger); } @@ -2243,13 +2550,13 @@ class Scope { } interface TcbBoundAttribute { - attribute: TmplAstBoundAttribute|TmplAstTextAttribute; + attribute: TmplAstBoundAttribute | TmplAstTextAttribute; inputs: { - fieldName: ClassPropertyName, - required: boolean, - isSignal: boolean, - transformType: Reference|null, - isTwoWayBinding: boolean, + fieldName: ClassPropertyName; + required: boolean; + isSignal: boolean; + transformType: Reference | null; + isTwoWayBinding: boolean; }[]; } @@ -2258,14 +2565,17 @@ interface TcbBoundAttribute { * arguments. */ function tcbThisParam( - name: ts.EntityName, typeArguments: ts.TypeNode[]|undefined): ts.ParameterDeclaration { + name: ts.EntityName, + typeArguments: ts.TypeNode[] | undefined, +): ts.ParameterDeclaration { return ts.factory.createParameterDeclaration( - /* modifiers */ undefined, - /* dotDotDotToken */ undefined, - /* name */ 'this', - /* questionToken */ undefined, - /* type */ ts.factory.createTypeReferenceNode(name, typeArguments), - /* initializer */ undefined); + /* modifiers */ undefined, + /* dotDotDotToken */ undefined, + /* name */ 'this', + /* questionToken */ undefined, + /* type */ ts.factory.createTypeReferenceNode(name, typeArguments), + /* initializer */ undefined, + ); } /** @@ -2278,13 +2588,16 @@ function tcbExpression(ast: AST, tcb: Context, scope: Scope): ts.Expression { } class TcbExpressionTranslator { - constructor(protected tcb: Context, protected scope: Scope) {} + constructor( + protected tcb: Context, + protected scope: Scope, + ) {} translate(ast: AST): ts.Expression { // `astToTypescript` actually does the conversion. A special resolver `tcbResolve` is passed // which interprets specific expression nodes that interact with the `ImplicitReceiver`. These // nodes actually refer to identifiers within the current scope. - return astToTypescript(ast, ast => this.resolve(ast), this.tcb.env.config); + return astToTypescript(ast, (ast) => this.resolve(ast), this.tcb.env.config); } /** @@ -2293,7 +2606,7 @@ class TcbExpressionTranslator { * Some `AST` expressions refer to top-level concepts (references, variables, the component * context). This method assists in resolving those. */ - protected resolve(ast: AST): ts.Expression|null { + protected resolve(ast: AST): ts.Expression | null { // TODO: this is actually a bug, because `ImplicitReceiver` extends `ThisReceiver`. Consider a // case when the explicit `this` read is inside a template with a context that also provides the // variable name being read: @@ -2322,7 +2635,8 @@ class TcbExpressionTranslator { const expr = this.translate(ast.value); const result = ts.factory.createParenthesizedExpression( - ts.factory.createBinaryExpression(target, ts.SyntaxKind.EqualsToken, expr)); + ts.factory.createBinaryExpression(target, ts.SyntaxKind.EqualsToken, expr), + ); addParseSpanInfo(result, ast.sourceSpan); return result; } else if (ast instanceof ImplicitReceiver) { @@ -2342,7 +2656,7 @@ class TcbExpressionTranslator { } else if (ast instanceof BindingPipe) { const expr = this.translate(ast.exp); const pipeMeta = this.tcb.getPipeByName(ast.name); - let pipe: ts.Expression|null; + let pipe: ts.Expression | null; if (pipeMeta === null) { // No pipe by that name exists in scope. Record this as an error. this.tcb.oobRecorder.missingPipe(this.tcb.id, ast); @@ -2350,8 +2664,9 @@ class TcbExpressionTranslator { // Use an 'any' value to at least allow the rest of the expression to be checked. pipe = NULL_AS_ANY; } else if ( - pipeMeta.isExplicitlyDeferred && - this.tcb.boundTarget.getEagerlyUsedPipes().includes(ast.name)) { + pipeMeta.isExplicitlyDeferred && + this.tcb.boundTarget.getEagerlyUsedPipes().includes(ast.name) + ) { // This pipe was defer-loaded (included into `@Component.deferredImports`), // but was used outside of a `@defer` block, which is the error. this.tcb.oobRecorder.deferredPipeUsedEagerly(this.tcb.id, ast); @@ -2360,35 +2675,47 @@ class TcbExpressionTranslator { pipe = NULL_AS_ANY; } else { // Use a variable declared as the pipe's type. - pipe = - this.tcb.env.pipeInst(pipeMeta.ref as Reference>); + pipe = this.tcb.env.pipeInst( + pipeMeta.ref as Reference>, + ); } - const args = ast.args.map(arg => this.translate(arg)); - let methodAccess: ts.Expression = - ts.factory.createPropertyAccessExpression(pipe, 'transform'); + const args = ast.args.map((arg) => this.translate(arg)); + let methodAccess: ts.Expression = ts.factory.createPropertyAccessExpression( + pipe, + 'transform', + ); addParseSpanInfo(methodAccess, ast.nameSpan); if (!this.tcb.env.config.checkTypeOfPipes) { methodAccess = ts.factory.createAsExpression( - methodAccess, ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); + methodAccess, + ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), + ); } const result = ts.factory.createCallExpression( - /* expression */ methodAccess, - /* typeArguments */ undefined, - /* argumentsArray */[expr, ...args]); + /* expression */ methodAccess, + /* typeArguments */ undefined, + /* argumentsArray */ [expr, ...args], + ); addParseSpanInfo(result, ast.sourceSpan); return result; } else if ( - (ast instanceof Call || ast instanceof SafeCall) && - (ast.receiver instanceof PropertyRead || ast.receiver instanceof SafePropertyRead)) { + (ast instanceof Call || ast instanceof SafeCall) && + (ast.receiver instanceof PropertyRead || ast.receiver instanceof SafePropertyRead) + ) { // Resolve the special `$any(expr)` syntax to insert a cast of the argument to type `any`. // `$any(expr)` -> `expr as any` - if (ast.receiver.receiver instanceof ImplicitReceiver && - !(ast.receiver.receiver instanceof ThisReceiver) && ast.receiver.name === '$any' && - ast.args.length === 1) { + if ( + ast.receiver.receiver instanceof ImplicitReceiver && + !(ast.receiver.receiver instanceof ThisReceiver) && + ast.receiver.name === '$any' && + ast.args.length === 1 + ) { const expr = this.translate(ast.args[0]); const exprAsAny = ts.factory.createAsExpression( - expr, ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); + expr, + ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), + ); const result = ts.factory.createParenthesizedExpression(exprAsAny); addParseSpanInfo(result, ast.sourceSpan); return result; @@ -2405,7 +2732,7 @@ class TcbExpressionTranslator { const method = wrapForDiagnostics(receiver); addParseSpanInfo(method, ast.receiver.nameSpan); - const args = ast.args.map(arg => this.translate(arg)); + const args = ast.args.map((arg) => this.translate(arg)); const node = ts.factory.createCallExpression(method, undefined, args); addParseSpanInfo(node, ast.sourceSpan); return node; @@ -2420,7 +2747,7 @@ class TcbExpressionTranslator { * appropriate `ts.Expression` that represents the bound target. If no target is available, * `null` is returned. */ - protected resolveTarget(ast: AST): ts.Expression|null { + protected resolveTarget(ast: AST): ts.Expression | null { const binding = this.tcb.boundTarget.getExpressionTarget(ast); if (binding === null) { return null; @@ -2437,11 +2764,14 @@ class TcbExpressionTranslator { * the directive instance from any bound inputs. */ function tcbCallTypeCtor( - dir: TypeCheckableDirectiveMeta, tcb: Context, inputs: TcbDirectiveInput[]): ts.Expression { + dir: TypeCheckableDirectiveMeta, + tcb: Context, + inputs: TcbDirectiveInput[], +): ts.Expression { const typeCtor = tcb.env.typeCtorFor(dir); // Construct an array of `ts.PropertyAssignment`s for each of the directive's inputs. - const members = inputs.map(input => { + const members = inputs.map((input) => { const propertyName = ts.factory.createStringLiteral(input.field); if (input.type === 'binding') { @@ -2452,8 +2782,10 @@ function tcbCallTypeCtor( expr = unwrapWritableSignal(expr, tcb); } - const assignment = - ts.factory.createPropertyAssignment(propertyName, wrapForDiagnostics(expr)); + const assignment = ts.factory.createPropertyAssignment( + propertyName, + wrapForDiagnostics(expr), + ); addParseSpanInfo(assignment, input.sourceSpan); return assignment; } else { @@ -2466,20 +2798,25 @@ function tcbCallTypeCtor( // Call the `ngTypeCtor` method on the directive class, with an object literal argument created // from the matched inputs. return ts.factory.createCallExpression( - /* expression */ typeCtor, - /* typeArguments */ undefined, - /* argumentsArray */[ts.factory.createObjectLiteralExpression(members)]); + /* expression */ typeCtor, + /* typeArguments */ undefined, + /* argumentsArray */ [ts.factory.createObjectLiteralExpression(members)], + ); } function getBoundAttributes( - directive: TypeCheckableDirectiveMeta, - node: TmplAstTemplate|TmplAstElement): TcbBoundAttribute[] { + directive: TypeCheckableDirectiveMeta, + node: TmplAstTemplate | TmplAstElement, +): TcbBoundAttribute[] { const boundInputs: TcbBoundAttribute[] = []; - const processAttribute = (attr: TmplAstBoundAttribute|TmplAstTextAttribute) => { + const processAttribute = (attr: TmplAstBoundAttribute | TmplAstTextAttribute) => { // Skip non-property bindings. - if (attr instanceof TmplAstBoundAttribute && attr.type !== BindingType.Property && - attr.type !== BindingType.TwoWay) { + if ( + attr instanceof TmplAstBoundAttribute && + attr.type !== BindingType.Property && + attr.type !== BindingType.TwoWay + ) { return; } @@ -2489,16 +2826,16 @@ function getBoundAttributes( if (inputs !== null) { boundInputs.push({ attribute: attr, - inputs: inputs.map(input => { - return ({ + inputs: inputs.map((input) => { + return { fieldName: input.classPropertyName, required: input.required, transformType: input.transform?.type || null, isSignal: input.isSignal, isTwoWayBinding: - attr instanceof TmplAstBoundAttribute && attr.type === BindingType.TwoWay, - }); - }) + attr instanceof TmplAstBoundAttribute && attr.type === BindingType.TwoWay, + }; + }), }); } }; @@ -2516,7 +2853,10 @@ function getBoundAttributes( * Translates the given attribute binding to a `ts.Expression`. */ function translateInput( - attr: TmplAstBoundAttribute|TmplAstTextAttribute, tcb: Context, scope: Scope): ts.Expression { + attr: TmplAstBoundAttribute | TmplAstTextAttribute, + tcb: Context, + scope: Scope, +): ts.Expression { if (attr instanceof TmplAstBoundAttribute) { // Produce an expression representing the value of the binding. return tcbExpression(attr.value, tcb, scope); @@ -2556,7 +2896,9 @@ function widenBinding(expr: ts.Expression, tcb: Context): ts.Expression { */ function unwrapWritableSignal(expression: ts.Expression, tcb: Context): ts.CallExpression { const unwrapRef = tcb.env.referenceExternalSymbol( - R3Identifiers.unwrapWritableSignal.moduleName, R3Identifiers.unwrapWritableSignal.name); + R3Identifiers.unwrapWritableSignal.moduleName, + R3Identifiers.unwrapWritableSignal.name, + ); return ts.factory.createCallExpression(unwrapRef, undefined, [expression]); } @@ -2599,7 +2941,7 @@ interface TcbDirectiveUnsetInput { field: string; } -type TcbDirectiveInput = TcbDirectiveBoundInput|TcbDirectiveUnsetInput; +type TcbDirectiveInput = TcbDirectiveBoundInput | TcbDirectiveUnsetInput; const EVENT_PARAMETER = '$event'; @@ -2623,11 +2965,14 @@ const enum EventParamType { * bindings. Alternatively, an explicit type can be passed for the `$event` parameter. */ function tcbCreateEventHandler( - event: TmplAstBoundEvent, tcb: Context, scope: Scope, - eventType: EventParamType|ts.TypeNode): ts.Expression { + event: TmplAstBoundEvent, + tcb: Context, + scope: Scope, + eventType: EventParamType | ts.TypeNode, +): ts.Expression { const handler = tcbEventHandlerExpression(event.handler, tcb, scope); - let eventParamType: ts.TypeNode|undefined; + let eventParamType: ts.TypeNode | undefined; if (eventType === EventParamType.Infer) { eventParamType = undefined; } else if (eventType === EventParamType.Any) { @@ -2647,21 +2992,23 @@ function tcbCreateEventHandler( } const eventParam = ts.factory.createParameterDeclaration( - /* modifiers */ undefined, - /* dotDotDotToken */ undefined, - /* name */ EVENT_PARAMETER, - /* questionToken */ undefined, - /* type */ eventParamType); + /* modifiers */ undefined, + /* dotDotDotToken */ undefined, + /* name */ EVENT_PARAMETER, + /* questionToken */ undefined, + /* type */ eventParamType, + ); addExpressionIdentifier(eventParam, ExpressionIdentifier.EVENT_PARAMETER); // Return an arrow function instead of a function expression to preserve the `this` context. return ts.factory.createArrowFunction( - /* modifiers */ undefined, - /* typeParameters */ undefined, - /* parameters */[eventParam], - /* type */ ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), - /* equalsGreaterThanToken */ undefined, - /* body */ ts.factory.createBlock([body])); + /* modifiers */ undefined, + /* typeParameters */ undefined, + /* parameters */ [eventParam], + /* type */ ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), + /* equalsGreaterThanToken */ undefined, + /* body */ ts.factory.createBlock([body]), + ); } /** @@ -2675,38 +3022,59 @@ function tcbEventHandlerExpression(ast: AST, tcb: Context, scope: Scope): ts.Exp } function isSplitTwoWayBinding( - inputName: string, output: TmplAstBoundEvent, inputs: TmplAstBoundAttribute[], tcb: Context) { - const input = inputs.find(input => input.name === inputName); + inputName: string, + output: TmplAstBoundEvent, + inputs: TmplAstBoundAttribute[], + tcb: Context, +) { + const input = inputs.find((input) => input.name === inputName); if (input === undefined || input.sourceSpan !== output.sourceSpan) { return false; } // Input consumer should be a directive because it's claimed const inputConsumer = tcb.boundTarget.getConsumerOfBinding(input) as TypeCheckableDirectiveMeta; const outputConsumer = tcb.boundTarget.getConsumerOfBinding(output); - if (outputConsumer === null || inputConsumer.ref === undefined || - outputConsumer instanceof TmplAstTemplate) { + if ( + outputConsumer === null || + inputConsumer.ref === undefined || + outputConsumer instanceof TmplAstTemplate + ) { return false; } if (outputConsumer instanceof TmplAstElement) { tcb.oobRecorder.splitTwoWayBinding( - tcb.id, input, output, inputConsumer.ref.node, outputConsumer); + tcb.id, + input, + output, + inputConsumer.ref.node, + outputConsumer, + ); return true; } else if (outputConsumer.ref !== inputConsumer.ref) { tcb.oobRecorder.splitTwoWayBinding( - tcb.id, input, output, inputConsumer.ref.node, outputConsumer.ref.node); + tcb.id, + input, + output, + inputConsumer.ref.node, + outputConsumer.ref.node, + ); return true; } return false; } class TcbEventHandlerTranslator extends TcbExpressionTranslator { - protected override resolve(ast: AST): ts.Expression|null { + protected override resolve(ast: AST): ts.Expression | null { // Recognize a property read on the implicit receiver corresponding with the event parameter // that is available in event bindings. Since this variable is a parameter of the handler // function that the converted expression becomes a child of, just create a reference to the // parameter by its name. - if (ast instanceof PropertyRead && ast.receiver instanceof ImplicitReceiver && - !(ast.receiver instanceof ThisReceiver) && ast.name === EVENT_PARAMETER) { + if ( + ast instanceof PropertyRead && + ast.receiver instanceof ImplicitReceiver && + !(ast.receiver instanceof ThisReceiver) && + ast.name === EVENT_PARAMETER + ) { const event = ts.factory.createIdentifier(EVENT_PARAMETER); addParseSpanInfo(event, ast.nameSpan); return event; @@ -2719,7 +3087,11 @@ class TcbEventHandlerTranslator extends TcbExpressionTranslator { class TcbForLoopTrackTranslator extends TcbExpressionTranslator { private allowedVariables: Set; - constructor(tcb: Context, scope: Scope, private block: TmplAstForLoopBlock) { + constructor( + tcb: Context, + scope: Scope, + private block: TmplAstForLoopBlock, + ) { super(tcb, scope); // Tracking expressions are only allowed to read the `$index`, @@ -2732,7 +3104,7 @@ class TcbForLoopTrackTranslator extends TcbExpressionTranslator { } } - protected override resolve(ast: AST): ts.Expression|null { + protected override resolve(ast: AST): ts.Expression | null { if (ast instanceof PropertyRead && ast.receiver instanceof ImplicitReceiver) { const target = this.tcb.boundTarget.getExpressionTarget(ast); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_file.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_file.ts index d343137949976..b9a021f7b8104 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_file.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_file.ts @@ -19,8 +19,6 @@ import {OutOfBandDiagnosticRecorder} from './oob'; import {ensureTypeCheckFilePreparationImports} from './tcb_util'; import {generateTypeCheckBlock, TcbGenericContextBehavior} from './type_check_block'; - - /** * An `Environment` representing the single type-checking file into which most (if not all) Type * Check Blocks (TCBs) will be generated. @@ -34,28 +32,49 @@ export class TypeCheckFile extends Environment { private tcbStatements: ts.Statement[] = []; constructor( - readonly fileName: AbsoluteFsPath, config: TypeCheckingConfig, refEmitter: ReferenceEmitter, - reflector: ReflectionHost, compilerHost: Pick) { + readonly fileName: AbsoluteFsPath, + config: TypeCheckingConfig, + refEmitter: ReferenceEmitter, + reflector: ReflectionHost, + compilerHost: Pick, + ) { super( - config, new ImportManager({ - // This minimizes noticeable changes with older versions of `ImportManager`. - forceGenerateNamespacesForNewImports: true, - // Type check block code affects code completion and fix suggestions. - // We want to encourage single quotes for now, like we always did. - shouldUseSingleQuotes: () => true - }), - refEmitter, reflector, - ts.createSourceFile( - compilerHost.getCanonicalFileName(fileName), '', ts.ScriptTarget.Latest, true)); + config, + new ImportManager({ + // This minimizes noticeable changes with older versions of `ImportManager`. + forceGenerateNamespacesForNewImports: true, + // Type check block code affects code completion and fix suggestions. + // We want to encourage single quotes for now, like we always did. + shouldUseSingleQuotes: () => true, + }), + refEmitter, + reflector, + ts.createSourceFile( + compilerHost.getCanonicalFileName(fileName), + '', + ts.ScriptTarget.Latest, + true, + ), + ); } addTypeCheckBlock( - ref: Reference>, meta: TypeCheckBlockMetadata, - domSchemaChecker: DomSchemaChecker, oobRecorder: OutOfBandDiagnosticRecorder, - genericContextBehavior: TcbGenericContextBehavior): void { + ref: Reference>, + meta: TypeCheckBlockMetadata, + domSchemaChecker: DomSchemaChecker, + oobRecorder: OutOfBandDiagnosticRecorder, + genericContextBehavior: TcbGenericContextBehavior, + ): void { const fnId = ts.factory.createIdentifier(`_tcb${this.nextTcbId++}`); const fn = generateTypeCheckBlock( - this, ref, fnId, meta, domSchemaChecker, oobRecorder, genericContextBehavior); + this, + ref, + fnId, + meta, + domSchemaChecker, + oobRecorder, + genericContextBehavior, + ); this.tcbStatements.push(fn); } @@ -69,7 +88,8 @@ export class TypeCheckFile extends Environment { const importChanges = this.importManager.finalize(); if (importChanges.updatedImports.size > 0) { throw new Error( - 'AssertionError: Expected no imports to be updated for a new type check file.'); + 'AssertionError: Expected no imports to be updated for a new type check file.', + ); } const printer = ts.createPrinter({removeComments}); @@ -77,8 +97,9 @@ export class TypeCheckFile extends Environment { const newImports = importChanges.newImports.get(this.contextFile.fileName); if (newImports !== undefined) { - source += newImports.map(i => printer.printNode(ts.EmitHint.Unspecified, i, this.contextFile)) - .join('\n'); + source += newImports + .map((i) => printer.printNode(ts.EmitHint.Unspecified, i, this.contextFile)) + .join('\n'); } source += '\n'; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts index e478e246df2a7..e460f7c1dc2a5 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts @@ -17,8 +17,11 @@ import {checkIfGenericTypeBoundsCanBeEmitted} from './tcb_util'; import {tsCreateTypeQueryForCoercedInput} from './ts_util'; export function generateTypeCtorDeclarationFn( - env: ReferenceEmitEnvironment, meta: TypeCtorMetadata, nodeTypeRef: ts.EntityName, - typeParams: ts.TypeParameterDeclaration[]|undefined): ts.Statement { + env: ReferenceEmitEnvironment, + meta: TypeCtorMetadata, + nodeTypeRef: ts.EntityName, + typeParams: ts.TypeParameterDeclaration[] | undefined, +): ts.Statement { const rawTypeArgs = typeParams !== undefined ? generateGenericArgs(typeParams) : undefined; const rawType = ts.factory.createTypeReferenceNode(nodeTypeRef, rawTypeArgs); @@ -28,29 +31,32 @@ export function generateTypeCtorDeclarationFn( if (meta.body) { const fnType = ts.factory.createFunctionTypeNode( - /* typeParameters */ typeParameters, - /* parameters */[initParam], - /* type */ rawType, + /* typeParameters */ typeParameters, + /* parameters */ [initParam], + /* type */ rawType, ); const decl = ts.factory.createVariableDeclaration( - /* name */ meta.fnName, - /* exclamationToken */ undefined, - /* type */ fnType, - /* body */ ts.factory.createNonNullExpression(ts.factory.createNull())); + /* name */ meta.fnName, + /* exclamationToken */ undefined, + /* type */ fnType, + /* body */ ts.factory.createNonNullExpression(ts.factory.createNull()), + ); const declList = ts.factory.createVariableDeclarationList([decl], ts.NodeFlags.Const); return ts.factory.createVariableStatement( - /* modifiers */ undefined, - /* declarationList */ declList); + /* modifiers */ undefined, + /* declarationList */ declList, + ); } else { return ts.factory.createFunctionDeclaration( - /* modifiers */[ts.factory.createModifier(ts.SyntaxKind.DeclareKeyword)], - /* asteriskToken */ undefined, - /* name */ meta.fnName, - /* typeParameters */ typeParameters, - /* parameters */[initParam], - /* type */ rawType, - /* body */ undefined); + /* modifiers */ [ts.factory.createModifier(ts.SyntaxKind.DeclareKeyword)], + /* asteriskToken */ undefined, + /* name */ meta.fnName, + /* typeParameters */ typeParameters, + /* parameters */ [initParam], + /* type */ rawType, + /* body */ undefined, + ); } } @@ -90,13 +96,15 @@ export function generateTypeCtorDeclarationFn( * @returns a `ts.MethodDeclaration` for the type constructor. */ export function generateInlineTypeCtor( - env: ReferenceEmitEnvironment, node: ClassDeclaration, - meta: TypeCtorMetadata): ts.MethodDeclaration { + env: ReferenceEmitEnvironment, + node: ClassDeclaration, + meta: TypeCtorMetadata, +): ts.MethodDeclaration { // Build rawType, a `ts.TypeNode` of the class with its generic parameters passed through from // the definition without any type bounds. For example, if the class is // `FooDirective`, its rawType would be `FooDirective`. const rawTypeArgs = - node.typeParameters !== undefined ? generateGenericArgs(node.typeParameters) : undefined; + node.typeParameters !== undefined ? generateGenericArgs(node.typeParameters) : undefined; const rawType = ts.factory.createTypeReferenceNode(node.name, rawTypeArgs); const initParam = constructTypeCtorParameter(env, meta, rawType); @@ -104,7 +112,7 @@ export function generateInlineTypeCtor( // If this constructor is being generated into a .ts file, then it needs a fake body. The body // is set to a return of `null!`. If the type constructor is being generated into a .d.ts file, // it needs no body. - let body: ts.Block|undefined = undefined; + let body: ts.Block | undefined = undefined; if (meta.body) { body = ts.factory.createBlock([ ts.factory.createReturnStatement(ts.factory.createNonNullExpression(ts.factory.createNull())), @@ -113,20 +121,22 @@ export function generateInlineTypeCtor( // Create the type constructor method declaration. return ts.factory.createMethodDeclaration( - /* modifiers */[ts.factory.createModifier(ts.SyntaxKind.StaticKeyword)], - /* asteriskToken */ undefined, - /* name */ meta.fnName, - /* questionToken */ undefined, - /* typeParameters */ typeParametersWithDefaultTypes(node.typeParameters), - /* parameters */[initParam], - /* type */ rawType, - /* body */ body, + /* modifiers */ [ts.factory.createModifier(ts.SyntaxKind.StaticKeyword)], + /* asteriskToken */ undefined, + /* name */ meta.fnName, + /* questionToken */ undefined, + /* typeParameters */ typeParametersWithDefaultTypes(node.typeParameters), + /* parameters */ [initParam], + /* type */ rawType, + /* body */ body, ); } function constructTypeCtorParameter( - env: ReferenceEmitEnvironment, meta: TypeCtorMetadata, - rawType: ts.TypeReferenceNode): ts.ParameterDeclaration { + env: ReferenceEmitEnvironment, + meta: TypeCtorMetadata, + rawType: ts.TypeReferenceNode, +): ts.ParameterDeclaration { // initType is the type of 'init', the single argument to the type constructor method. // If the Directive has any inputs, its initType will be: // @@ -136,7 +146,7 @@ function constructTypeCtorParameter( // directive will be inferred. // // In the special case there are no inputs, initType is set to {}. - let initType: ts.TypeNode|null = null; + let initType: ts.TypeNode | null = null; const plainKeys: ts.LiteralTypeNode[] = []; const coercedKeys: ts.PropertySignature[] = []; @@ -145,20 +155,26 @@ function constructTypeCtorParameter( for (const {classPropertyName, transform, isSignal} of meta.fields.inputs) { if (isSignal) { signalInputKeys.push( - ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(classPropertyName))); + ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(classPropertyName)), + ); } else if (!meta.coercedInputFields.has(classPropertyName)) { plainKeys.push( - ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(classPropertyName))); + ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(classPropertyName)), + ); } else { - const coercionType = transform != null ? - transform.type.node : - tsCreateTypeQueryForCoercedInput(rawType.typeName, classPropertyName); + const coercionType = + transform != null + ? transform.type.node + : tsCreateTypeQueryForCoercedInput(rawType.typeName, classPropertyName); - coercedKeys.push(ts.factory.createPropertySignature( + coercedKeys.push( + ts.factory.createPropertySignature( /* modifiers */ undefined, /* name */ classPropertyName, /* questionToken */ undefined, - /* type */ coercionType)); + /* type */ coercionType, + ), + ); } } if (plainKeys.length > 0) { @@ -171,26 +187,29 @@ function constructTypeCtorParameter( if (coercedKeys.length > 0) { const coercedLiteral = ts.factory.createTypeLiteralNode(coercedKeys); - initType = initType !== null ? - ts.factory.createIntersectionTypeNode([initType, coercedLiteral]) : - coercedLiteral; + initType = + initType !== null + ? ts.factory.createIntersectionTypeNode([initType, coercedLiteral]) + : coercedLiteral; } if (signalInputKeys.length > 0) { const keyTypeUnion = ts.factory.createUnionTypeNode(signalInputKeys); // Construct the UnwrapDirectiveSignalInputs. const unwrapDirectiveSignalInputsExpr = env.referenceExternalType( - R3Identifiers.UnwrapDirectiveSignalInputs.moduleName, - R3Identifiers.UnwrapDirectiveSignalInputs.name, [ - // TODO: - new ExpressionType(new WrappedNodeExpr(rawType)), - new ExpressionType(new WrappedNodeExpr(keyTypeUnion)) - ]); - + R3Identifiers.UnwrapDirectiveSignalInputs.moduleName, + R3Identifiers.UnwrapDirectiveSignalInputs.name, + [ + // TODO: + new ExpressionType(new WrappedNodeExpr(rawType)), + new ExpressionType(new WrappedNodeExpr(keyTypeUnion)), + ], + ); - initType = initType !== null ? - ts.factory.createIntersectionTypeNode([initType, unwrapDirectiveSignalInputsExpr]) : - unwrapDirectiveSignalInputsExpr; + initType = + initType !== null + ? ts.factory.createIntersectionTypeNode([initType, unwrapDirectiveSignalInputsExpr]) + : unwrapDirectiveSignalInputsExpr; } if (initType === null) { @@ -200,21 +219,24 @@ function constructTypeCtorParameter( // Create the 'init' parameter itself. return ts.factory.createParameterDeclaration( - /* modifiers */ undefined, - /* dotDotDotToken */ undefined, - /* name */ 'init', - /* questionToken */ undefined, - /* type */ initType, - /* initializer */ undefined); + /* modifiers */ undefined, + /* dotDotDotToken */ undefined, + /* name */ 'init', + /* questionToken */ undefined, + /* type */ initType, + /* initializer */ undefined, + ); } function generateGenericArgs(params: ReadonlyArray): ts.TypeNode[] { - return params.map(param => ts.factory.createTypeReferenceNode(param.name, undefined)); + return params.map((param) => ts.factory.createTypeReferenceNode(param.name, undefined)); } export function requiresInlineTypeCtor( - node: ClassDeclaration, host: ReflectionHost, - env: ReferenceEmitEnvironment): boolean { + node: ClassDeclaration, + host: ReflectionHost, + env: ReferenceEmitEnvironment, +): boolean { // The class requires an inline type constructor if it has generic type bounds that can not be // emitted into the provided type-check environment. return !checkIfGenericTypeBoundsCanBeEmitted(node, host, env); @@ -264,17 +286,22 @@ export function requiresInlineTypeCtor( * * This correctly infers `T` as `any`, and therefore `_t3` as `NgFor`. */ -function typeParametersWithDefaultTypes(params: ReadonlyArray| - undefined): ts.TypeParameterDeclaration[]|undefined { +function typeParametersWithDefaultTypes( + params: ReadonlyArray | undefined, +): ts.TypeParameterDeclaration[] | undefined { if (params === undefined) { return undefined; } - return params.map(param => { + return params.map((param) => { if (param.default === undefined) { return ts.factory.updateTypeParameterDeclaration( - param, param.modifiers, param.name, param.constraint, - ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); + param, + param.modifiers, + param.name, + param.constraint, + ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), + ); } else { return param; } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_parameter_emitter.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_parameter_emitter.ts index 426908062f791..e7a3c23f49b0a 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_parameter_emitter.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_parameter_emitter.ts @@ -16,8 +16,9 @@ import {canEmitType, TypeEmitter} from '../../translator'; */ export class TypeParameterEmitter { constructor( - private typeParameters: ts.NodeArray|undefined, - private reflector: ReflectionHost) {} + private typeParameters: ts.NodeArray | undefined, + private reflector: ReflectionHost, + ) {} /** * Determines whether the type parameters can be emitted. If this returns true, then a call to @@ -29,19 +30,23 @@ export class TypeParameterEmitter { return true; } - return this.typeParameters.every(typeParam => { - return this.canEmitType(typeParam.constraint, canEmitReference) && - this.canEmitType(typeParam.default, canEmitReference); + return this.typeParameters.every((typeParam) => { + return ( + this.canEmitType(typeParam.constraint, canEmitReference) && + this.canEmitType(typeParam.default, canEmitReference) + ); }); } - private canEmitType(type: ts.TypeNode|undefined, canEmitReference: (ref: Reference) => boolean): - boolean { + private canEmitType( + type: ts.TypeNode | undefined, + canEmitReference: (ref: Reference) => boolean, + ): boolean { if (type === undefined) { return true; } - return canEmitType(type, typeReference => { + return canEmitType(type, (typeReference) => { const reference = this.resolveTypeReference(typeReference); if (reference === null) { return false; @@ -58,25 +63,32 @@ export class TypeParameterEmitter { /** * Emits the type parameters using the provided emitter function for `Reference`s. */ - emit(emitReference: (ref: Reference) => ts.TypeNode): ts.TypeParameterDeclaration[]|undefined { + emit(emitReference: (ref: Reference) => ts.TypeNode): ts.TypeParameterDeclaration[] | undefined { if (this.typeParameters === undefined) { return undefined; } - const emitter = new TypeEmitter(type => this.translateTypeReference(type, emitReference)); + const emitter = new TypeEmitter((type) => this.translateTypeReference(type, emitReference)); - return this.typeParameters.map(typeParam => { + return this.typeParameters.map((typeParam) => { const constraint = - typeParam.constraint !== undefined ? emitter.emitType(typeParam.constraint) : undefined; + typeParam.constraint !== undefined ? emitter.emitType(typeParam.constraint) : undefined; const defaultType = - typeParam.default !== undefined ? emitter.emitType(typeParam.default) : undefined; + typeParam.default !== undefined ? emitter.emitType(typeParam.default) : undefined; return ts.factory.updateTypeParameterDeclaration( - typeParam, typeParam.modifiers, typeParam.name, constraint, defaultType); + typeParam, + typeParam.modifiers, + typeParam.name, + constraint, + defaultType, + ); }); } - private resolveTypeReference(type: ts.TypeReferenceNode): Reference|ts.TypeReferenceNode|null { + private resolveTypeReference( + type: ts.TypeReferenceNode, + ): Reference | ts.TypeReferenceNode | null { const target = ts.isIdentifier(type.typeName) ? type.typeName : type.typeName.right; const declaration = this.reflector.getDeclarationOfIdentifier(target); @@ -92,7 +104,7 @@ export class TypeParameterEmitter { return type; } - let owningModule: OwningModule|null = null; + let owningModule: OwningModule | null = null; if (typeof declaration.viaModule === 'string') { owningModule = { specifier: declaration.viaModule, @@ -101,12 +113,15 @@ export class TypeParameterEmitter { } return new Reference( - declaration.node, declaration.viaModule === AmbientImport ? AmbientImport : owningModule); + declaration.node, + declaration.viaModule === AmbientImport ? AmbientImport : owningModule, + ); } private translateTypeReference( - type: ts.TypeReferenceNode, - emitReference: (ref: Reference) => ts.TypeNode | null): ts.TypeReferenceNode|null { + type: ts.TypeReferenceNode, + emitReference: (ref: Reference) => ts.TypeNode | null, + ): ts.TypeReferenceNode | null { const reference = this.resolveTypeReference(type); if (!(reference instanceof Reference)) { return reference; @@ -119,7 +134,8 @@ export class TypeParameterEmitter { if (!ts.isTypeReferenceNode(typeNode)) { throw new Error( - `Expected TypeReferenceNode for emitted reference, got ${ts.SyntaxKind[typeNode.kind]}.`); + `Expected TypeReferenceNode for emitted reference, got ${ts.SyntaxKind[typeNode.kind]}.`, + ); } return typeNode; } @@ -127,6 +143,6 @@ export class TypeParameterEmitter { private isLocalTypeParameter(decl: DeclarationNode): boolean { // Checking for local type parameters only occurs during resolution of type parameters, so it is // guaranteed that type parameters are present. - return this.typeParameters!.some(param => param === decl); + return this.typeParameters!.some((param) => param === decl); } } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/template_semantics/src/template_semantics_checker.ts b/packages/compiler-cli/src/ngtsc/typecheck/template_semantics/src/template_semantics_checker.ts index 572fb47c56e64..b203b97aba212 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/template_semantics/src/template_semantics_checker.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/template_semantics/src/template_semantics_checker.ts @@ -6,7 +6,19 @@ * found in the LICENSE file at https://angular.io/license */ -import {AST, ASTWithSource, ImplicitReceiver, ParsedEventType, PropertyRead, PropertyWrite, RecursiveAstVisitor, TmplAstBoundEvent, TmplAstNode, TmplAstRecursiveVisitor, TmplAstVariable} from '@angular/compiler'; +import { + AST, + ASTWithSource, + ImplicitReceiver, + ParsedEventType, + PropertyRead, + PropertyWrite, + RecursiveAstVisitor, + TmplAstBoundEvent, + TmplAstNode, + TmplAstRecursiveVisitor, + TmplAstVariable, +} from '@angular/compiler'; import ts from 'typescript'; import {ErrorCode, ngErrorCode} from '../../../diagnostics'; @@ -19,9 +31,9 @@ export class TemplateSemanticsCheckerImpl implements TemplateSemanticsChecker { getDiagnosticsForComponent(component: ts.ClassDeclaration): TemplateDiagnostic[] { const template = this.templateTypeChecker.getTemplate(component); - return template !== null ? - TemplateSemanticsVisitor.visit(template, component, this.templateTypeChecker) : - []; + return template !== null + ? TemplateSemanticsVisitor.visit(template, component, this.templateTypeChecker) + : []; } } @@ -32,13 +44,18 @@ class TemplateSemanticsVisitor extends TmplAstRecursiveVisitor { } static visit( - nodes: TmplAstNode[], component: ts.ClassDeclaration, - templateTypeChecker: TemplateTypeChecker) { + nodes: TmplAstNode[], + component: ts.ClassDeclaration, + templateTypeChecker: TemplateTypeChecker, + ) { const diagnostics: TemplateDiagnostic[] = []; - const expressionVisitor = - new ExpressionsSemanticsVisitor(templateTypeChecker, component, diagnostics); + const expressionVisitor = new ExpressionsSemanticsVisitor( + templateTypeChecker, + component, + diagnostics, + ); const templateVisitor = new TemplateSemanticsVisitor(expressionVisitor); - nodes.forEach(node => node.visit(templateVisitor)); + nodes.forEach((node) => node.visit(templateVisitor)); return diagnostics; } @@ -51,8 +68,10 @@ class TemplateSemanticsVisitor extends TmplAstRecursiveVisitor { /** Visitor that verifies the semantics of the expressions within a template. */ class ExpressionsSemanticsVisitor extends RecursiveAstVisitor { constructor( - private templateTypeChecker: TemplateTypeChecker, private component: ts.ClassDeclaration, - private diagnostics: TemplateDiagnostic[]) { + private templateTypeChecker: TemplateTypeChecker, + private component: ts.ClassDeclaration, + private diagnostics: TemplateDiagnostic[], + ) { super(); } @@ -73,18 +92,19 @@ class ExpressionsSemanticsVisitor extends RecursiveAstVisitor { const target = this.templateTypeChecker.getExpressionTarget(ast, this.component); if (target instanceof TmplAstVariable) { - const errorMessage = `Cannot use variable '${ - target - .name}' as the left-hand side of an assignment expression. Template variables are read-only.`; + const errorMessage = `Cannot use variable '${target.name}' as the left-hand side of an assignment expression. Template variables are read-only.`; this.diagnostics.push(this.makeIllegalTemplateVarDiagnostic(target, context, errorMessage)); } } private checkForIllegalWriteInTwoWayBinding(ast: PropertyRead, context: TmplAstNode) { // Only check top-level property reads inside two-way bindings for illegal assignments. - if (!(context instanceof TmplAstBoundEvent) || context.type !== ParsedEventType.TwoWay || - !(ast.receiver instanceof ImplicitReceiver) || - ast !== unwrapAstWithSource(context.handler)) { + if ( + !(context instanceof TmplAstBoundEvent) || + context.type !== ParsedEventType.TwoWay || + !(ast.receiver instanceof ImplicitReceiver) || + ast !== unwrapAstWithSource(context.handler) + ) { return; } @@ -96,23 +116,31 @@ class ExpressionsSemanticsVisitor extends RecursiveAstVisitor { // Two-way bindings to template variables are only allowed if the variables are signals. const symbol = this.templateTypeChecker.getSymbolOfNode(target, this.component); if (symbol !== null && !isSignalReference(symbol)) { - const errorMessage = `Cannot use a non-signal variable '${ - target.name}' in a two-way binding expression. Template variables are read-only.`; + const errorMessage = `Cannot use a non-signal variable '${target.name}' in a two-way binding expression. Template variables are read-only.`; this.diagnostics.push(this.makeIllegalTemplateVarDiagnostic(target, context, errorMessage)); } } private makeIllegalTemplateVarDiagnostic( - target: TmplAstVariable, expressionNode: TmplAstBoundEvent, - errorMessage: string): TemplateDiagnostic { + target: TmplAstVariable, + expressionNode: TmplAstBoundEvent, + errorMessage: string, + ): TemplateDiagnostic { return this.templateTypeChecker.makeTemplateDiagnostic( - this.component, expressionNode.handlerSpan, ts.DiagnosticCategory.Error, - ngErrorCode(ErrorCode.WRITE_TO_READ_ONLY_VARIABLE), errorMessage, [{ + this.component, + expressionNode.handlerSpan, + ts.DiagnosticCategory.Error, + ngErrorCode(ErrorCode.WRITE_TO_READ_ONLY_VARIABLE), + errorMessage, + [ + { text: `The variable ${target.name} is declared here.`, start: target.valueSpan?.start.offset || target.sourceSpan.start.offset, end: target.valueSpan?.end.offset || target.sourceSpan.end.offset, sourceFile: this.component.getSourceFile(), - }]); + }, + ], + ); } } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/diagnostics_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/diagnostics_spec.ts index 33ae7c23c412f..0dc5d993c50ac 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/diagnostics_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/diagnostics_spec.ts @@ -6,16 +6,19 @@ * found in the LICENSE file at https://angular.io/license */ - import {runInEachFileSystem} from '../../file_system/testing'; -import {resetParseTemplateAsSourceFileForTest, setParseTemplateAsSourceFileForTest} from '../diagnostics'; +import { + resetParseTemplateAsSourceFileForTest, + setParseTemplateAsSourceFileForTest, +} from '../diagnostics'; import {diagnose, ngForDeclaration, ngForDts, ngIfDeclaration, ngIfDts} from '../testing'; runInEachFileSystem(() => { describe('template diagnostics', () => { it('works for directive bindings', () => { const messages = diagnose( - `
`, ` + `
`, + ` class Dir { input: number; } @@ -24,27 +27,34 @@ runInEachFileSystem(() => { name: string; }; }`, - [{ + [ + { type: 'directive', name: 'Dir', selector: '[dir]', exportAs: ['dir'], inputs: {input: 'input'}, - }]); + }, + ], + ); - expect(messages).toEqual( - [`TestComponent.html(1, 11): Type 'string' is not assignable to type 'number'.`]); + expect(messages).toEqual([ + `TestComponent.html(1, 11): Type 'string' is not assignable to type 'number'.`, + ]); }); it('infers type of template variables', () => { const messages = diagnose( - `
{{ render(idx) }}
`, ` + `
{{ render(idx) }}
`, + ` class TestComponent { persons: {}[]; render(input: string): string { return input; } }`, - [ngForDeclaration()], [ngForDts()]); + [ngForDeclaration()], + [ngForDts()], + ); expect(messages).toEqual([ `TestComponent.html(1, 62): Argument of type 'number' is not assignable to parameter of type 'string'.`, @@ -53,32 +63,39 @@ runInEachFileSystem(() => { it('infers any type when generic type inference fails', () => { const messages = diagnose( - `
{{ render(person.namme) }}
`, ` + `
{{ render(person.namme) }}
`, + ` class TestComponent { persons: any; render(input: string): string { return input; } }`, - [ngForDeclaration()], [ngForDts()]); + [ngForDeclaration()], + [ngForDts()], + ); expect(messages).toEqual([]); }); it('infers type of element references', () => { const messages = diagnose( - `
{{ render(el) }}
`, ` + `
{{ render(el) }}
`, + ` class Dir { value: number; } class TestComponent { render(input: string): string { return input; } }`, - [{ + [ + { type: 'directive', name: 'Dir', selector: '[dir]', exportAs: ['dir'], - }]); + }, + ], + ); expect(messages).toEqual([ `TestComponent.html(1, 24): Argument of type 'HTMLDivElement' is not assignable to parameter of type 'string'.`, @@ -87,19 +104,23 @@ runInEachFileSystem(() => { it('infers type of directive references', () => { const messages = diagnose( - `
{{ render(dir) }}
`, ` + `
{{ render(dir) }}
`, + ` class Dir { value: number; } class TestComponent { render(input: string): string { return input; } }`, - [{ + [ + { type: 'directive', name: 'Dir', selector: '[dir]', exportAs: ['dir'], - }]); + }, + ], + ); expect(messages).toEqual([ `TestComponent.html(1, 31): Argument of type 'Dir' is not assignable to parameter of type 'string'.`, @@ -107,10 +128,13 @@ runInEachFileSystem(() => { }); it('infers TemplateRef for ng-template references', () => { - const messages = diagnose(`{{ render(tmpl) }}`, ` + const messages = diagnose( + `{{ render(tmpl) }}`, + ` class TestComponent { render(input: string): string { return input; } - }`); + }`, + ); expect(messages).toEqual([ `TestComponent.html(1, 30): Argument of type 'TemplateRef' is not assignable to parameter of type 'string'.`, @@ -119,13 +143,16 @@ runInEachFileSystem(() => { it('infers type of template context', () => { const messages = diagnose( - `
{{ person.namme }}
`, ` + `
{{ person.namme }}
`, + ` class TestComponent { persons: { name: string; }[]; }`, - [ngForDeclaration()], [ngForDts()]); + [ngForDeclaration()], + [ngForDts()], + ); expect(messages).toEqual([ `TestComponent.html(1, 47): Property 'namme' does not exist on type '{ name: string; }'. Did you mean 'name'?`, @@ -133,20 +160,26 @@ runInEachFileSystem(() => { }); it('interprets interpolation as strings', () => { - const messages = diagnose(`
`, ` + const messages = diagnose( + `
`, + ` class TestComponent { person: {}; - }`); + }`, + ); expect(messages).toEqual([]); }); it('checks bindings to regular element', () => { - const messages = diagnose(``, ` + const messages = diagnose( + ``, + ` class TestComponent { src: string; height: number; - }`); + }`, + ); expect(messages).toEqual([ `TestComponent.html(1, 29): Property 'heihgt' does not exist on type 'TestComponent'. Did you mean 'height'?`, @@ -156,17 +189,21 @@ runInEachFileSystem(() => { it('checks text attributes that are consumed by bindings with literal string types', () => { const messages = diagnose( - `
`, ` + `
`, + ` class Dir { mode: 'dark'|'light'; } class TestComponent {}`, - [{ + [ + { type: 'directive', name: 'Dir', selector: '[dir]', inputs: {'mode': 'mode'}, - }]); + }, + ], + ); expect(messages).toEqual([ `TestComponent.html(1, 10): Type '"drak"' is not assignable to type '"dark" | "light"'.`, @@ -175,10 +212,11 @@ runInEachFileSystem(() => { it('checks expressions in ICUs', () => { const messages = diagnose( - `{switch, plural, other { {{interpolation}} + `{switch, plural, other { {{interpolation}} {nestedSwitch, plural, other { {{nestedInterpolation}} }} }}`, - `class TestComponent {}`); + `class TestComponent {}`, + ); expect(messages.sort()).toEqual([ `TestComponent.html(1, 13): Property 'switch' does not exist on type 'TestComponent'.`, @@ -190,7 +228,8 @@ runInEachFileSystem(() => { it('produces diagnostics for pipes', () => { const messages = diagnose( - `
{{ person.name | pipe:person.age:1 }}
`, ` + `
{{ person.name | pipe:person.age:1 }}
`, + ` class Pipe { transform(value: string, a: string, b: string): string { return a + b; } } @@ -200,7 +239,8 @@ runInEachFileSystem(() => { age: number; }; }`, - [{type: 'pipe', name: 'Pipe', pipeName: 'pipe'}]); + [{type: 'pipe', name: 'Pipe', pipeName: 'pipe'}], + ); expect(messages).toEqual([ `TestComponent.html(1, 35): Argument of type 'number' is not assignable to parameter of type 'string'.`, @@ -213,7 +253,8 @@ runInEachFileSystem(() => { // twice, and since it doesn't exist this operation will fail. The test is here to verify that // failing to resolve the pipe twice only produces a single diagnostic (no duplicates). const messages = diagnose( - '
', ` + '
', + ` class Dir { dirOf: T; } @@ -221,26 +262,32 @@ runInEachFileSystem(() => { class TestComponent { name: string; }`, - [{ + [ + { type: 'directive', name: 'Dir', selector: '[dir]', inputs: {'dirOf': 'dirOf'}, isGeneric: true, - }]); + }, + ], + ); expect(messages.length).toBe(1); expect(messages[0]).toContain(`No pipe found with name 'uppercase'.`); }); it('does not repeat diagnostics for errors within LHS of safe-navigation operator', () => { - const messages = diagnose(`{{ personn?.name }} {{ personn?.getName() }}`, ` + const messages = diagnose( + `{{ personn?.name }} {{ personn?.getName() }}`, + ` class TestComponent { person: { name: string; getName: () => string; } | null; - }`); + }`, + ); expect(messages).toEqual([ `TestComponent.html(1, 4): Property 'personn' does not exist on type 'TestComponent'. Did you mean 'person'?`, @@ -250,7 +297,8 @@ runInEachFileSystem(() => { it('does not repeat diagnostics for errors used in template guard expressions', () => { const messages = diagnose( - `
`, ` + `
`, + ` class GuardDir { static ngTemplateGuard_guard: 'binding'; } @@ -260,77 +308,94 @@ runInEachFileSystem(() => { name: string; }; }`, - [{ + [ + { type: 'directive', name: 'GuardDir', selector: '[guard]', inputs: {'guard': 'guard'}, ngTemplateGuards: [{inputName: 'guard', type: 'binding'}], undeclaredInputFields: ['guard'], - }]); + }, + ], + ); expect(messages).toEqual([ `TestComponent.html(1, 14): Property 'personn' does not exist on type 'TestComponent'. Did you mean 'person'?`, ]); }); - it('should retain literal types in object literals together if strictNullInputBindings is disabled', - () => { - const messages = diagnose( - `
`, ` + it('should retain literal types in object literals together if strictNullInputBindings is disabled', () => { + const messages = diagnose( + `
`, + ` class Dir { ngModelOptions: { updateOn: 'change'|'blur' }; } class TestComponent {}`, - [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - inputs: {'ngModelOptions': 'ngModelOptions'}, - }], - [], {strictNullInputBindings: false}); - - expect(messages).toEqual([]); - }); - - it('should retain literal types in array literals together if strictNullInputBindings is disabled', - () => { - const messages = diagnose( - `
`, ` + [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + inputs: {'ngModelOptions': 'ngModelOptions'}, + }, + ], + [], + {strictNullInputBindings: false}, + ); + + expect(messages).toEqual([]); + }); + + it('should retain literal types in array literals together if strictNullInputBindings is disabled', () => { + const messages = diagnose( + `
`, + ` class Dir { options!: Array<'literal'>; } class TestComponent {}`, - [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - inputs: {'options': 'options'}, - }], - [], {strictNullInputBindings: false}); + [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + inputs: {'options': 'options'}, + }, + ], + [], + {strictNullInputBindings: false}, + ); - expect(messages).toEqual([]); - }); + expect(messages).toEqual([]); + }); it('does not produce diagnostics for user code', () => { - const messages = diagnose(`{{ person.name }}`, ` + const messages = diagnose( + `{{ person.name }}`, + ` class TestComponent { person: { name: string; }; render(input: string): number { return input; } // <-- type error here should not be reported - }`); + }`, + ); expect(messages).toEqual([]); }); it('should treat unary operators as literal types', () => { - const messages = diagnose(`{{ test(-1) + test(+1) + test(-2) }}`, ` + const messages = diagnose( + `{{ test(-1) + test(+1) + test(-2) }}`, + ` class TestComponent { test(value: -1 | 1): number { return value; } - }`); + }`, + ); expect(messages).toEqual([ `TestComponent.html(1, 32): Argument of type '-2' is not assignable to parameter of type '1 | -1'.`, @@ -339,11 +404,14 @@ runInEachFileSystem(() => { it('should support type-narrowing for methods with type guards', () => { const messages = diagnose( - `
{{ success }}
`, ` + `
{{ success }}
`, + ` class TestComponent { hasSuccess(): this is { success: boolean }; }`, - [ngIfDeclaration()], [ngIfDts()]); + [ngIfDeclaration()], + [ngIfDts()], + ); expect(messages).toEqual([]); }); @@ -351,7 +419,8 @@ runInEachFileSystem(() => { describe('outputs', () => { it('should produce a diagnostic for directive outputs', () => { const messages = diagnose( - `
`, ` + `
`, + ` import {EventEmitter} from '@angular/core'; class Dir { out = new EventEmitter(); @@ -359,7 +428,8 @@ runInEachFileSystem(() => { class TestComponent { handleEvent(event: string): void {} }`, - [{type: 'directive', name: 'Dir', selector: '[dir]', outputs: {'out': 'event'}}]); + [{type: 'directive', name: 'Dir', selector: '[dir]', outputs: {'out': 'event'}}], + ); expect(messages).toEqual([ `TestComponent.html(1, 31): Argument of type 'number' is not assignable to parameter of type 'string'.`, @@ -367,10 +437,13 @@ runInEachFileSystem(() => { }); it('should produce a diagnostic for animation events', () => { - const messages = diagnose(`
`, ` + const messages = diagnose( + `
`, + ` class TestComponent { handleEvent(event: string): void {} - }`); + }`, + ); expect(messages).toEqual([ `TestComponent.html(1, 41): Argument of type 'AnimationEvent' is not assignable to parameter of type 'string'.`, @@ -378,11 +451,14 @@ runInEachFileSystem(() => { }); it('should produce a diagnostic for element outputs', () => { - const messages = diagnose(`
`, ` + const messages = diagnose( + `
`, + ` import {EventEmitter} from '@angular/core'; class TestComponent { handleEvent(event: string): void {} - }`); + }`, + ); expect(messages).toEqual([ `TestComponent.html(1, 27): Argument of type 'MouseEvent' is not assignable to parameter of type 'string'.`, @@ -391,14 +467,16 @@ runInEachFileSystem(() => { it('should not produce a diagnostic when $event implicitly has an any type', () => { const messages = diagnose( - `
`, ` + `
`, + ` class Dir { out: any; } class TestComponent { handleEvent(event: string): void {} }`, - [{type: 'directive', name: 'Dir', selector: '[dir]', outputs: {'out': 'event'}}]); + [{type: 'directive', name: 'Dir', selector: '[dir]', outputs: {'out': 'event'}}], + ); expect(messages).toEqual([]); }); @@ -406,14 +484,19 @@ runInEachFileSystem(() => { // https://github.com/angular/angular/issues/33528 it('should not produce a diagnostic for implicit any return types', () => { const messages = diagnose( - `
`, ` + `
`, + ` class TestComponent { state: any; }`, - // Disable strict DOM event checking and strict null checks, to infer an any return type - // that would be implicit if the handler function would not have an explicit return - // type. - [], [], {checkTypeOfDomEvents: false}, {strictNullChecks: false}); + // Disable strict DOM event checking and strict null checks, to infer an any return type + // that would be implicit if the handler function would not have an explicit return + // type. + [], + [], + {checkTypeOfDomEvents: false}, + {strictNullChecks: false}, + ); expect(messages).toEqual([]); }); @@ -421,60 +504,71 @@ runInEachFileSystem(() => { describe('strict null checks', () => { it('produces diagnostic for unchecked property access', () => { - const messages = - diagnose(`
`, ` + const messages = diagnose( + `
`, + ` export class TestComponent { person: { address?: { street: string; }; }; - }`); + }`, + ); expect(messages).toEqual([`TestComponent.html(1, 41): Object is possibly 'undefined'.`]); }); it('does not produce diagnostic for checked property access', () => { const messages = diagnose( - `
`, ` + `
`, + ` export class TestComponent { person: { address?: { street: string; }; }; - }`); + }`, + ); expect(messages).toEqual([]); }); it('does not produce diagnostic for fallback value using nullish coalescing', () => { - const messages = diagnose(`
{{ greet(name ?? 'Frodo') }}
`, ` + const messages = diagnose( + `
{{ greet(name ?? 'Frodo') }}
`, + ` export class TestComponent { name: string | null; greet(name: string) { return 'hello ' + name; } - }`); + }`, + ); expect(messages).toEqual([]); }); it('does not produce diagnostic for safe keyed access', () => { - const messages = - diagnose(`
`, ` + const messages = diagnose( + `
`, + ` export class TestComponent { person: { favoriteColors?: string[]; }; - }`); + }`, + ); expect(messages).toEqual([]); }); it('infers a safe keyed read as undefined', () => { - const messages = diagnose(`
`, ` + const messages = diagnose( + `
`, + ` export class TestComponent { person: { favoriteColors?: string[]; @@ -483,28 +577,33 @@ runInEachFileSystem(() => { log(color: string) { console.log(color); } - }`); + }`, + ); expect(messages).toEqual([ `TestComponent.html(1, 19): Argument of type 'string | undefined' is not assignable to parameter of type 'string'. - Type 'undefined' is not assignable to type 'string'.` + Type 'undefined' is not assignable to type 'string'.`, ]); }); it('does not produce diagnostic for safe calls', () => { - const messages = - diagnose(`
`, ` + const messages = diagnose( + `
`, + ` export class TestComponent { person: { getName?: () => string; }; - }`); + }`, + ); expect(messages).toEqual([]); }); it('infers a safe call return value as undefined', () => { - const messages = diagnose(`
`, ` + const messages = diagnose( + `
`, + ` export class TestComponent { person: { getName?: () => string; @@ -513,28 +612,30 @@ runInEachFileSystem(() => { log(name: string) { console.log(name); } - }`); + }`, + ); expect(messages).toEqual([ `TestComponent.html(1, 19): Argument of type 'string | undefined' is not assignable to parameter of type 'string'. - Type 'undefined' is not assignable to type 'string'.` + Type 'undefined' is not assignable to type 'string'.`, ]); }); }); it('computes line and column offsets', () => { const messages = diagnose( - ` + `
`, - ` + ` class TestComponent { src: string; height: number; -}`); +}`, + ); expect(messages).toEqual([ `TestComponent.html(3, 15): Property 'srcc' does not exist on type 'TestComponent'. Did you mean 'src'?`, @@ -544,93 +645,114 @@ class TestComponent { it('works for shorthand property declarations', () => { const messages = diagnose( - `
`, ` + `
`, + ` class Dir { input: {a: string, b: number}; } class TestComponent { a: number; }`, - [{ + [ + { type: 'directive', name: 'Dir', selector: '[dir]', exportAs: ['dir'], inputs: {input: 'input'}, - }]); + }, + ], + ); - expect(messages).toEqual( - [`TestComponent.html(1, 20): Type 'number' is not assignable to type 'string'.`]); + expect(messages).toEqual([ + `TestComponent.html(1, 20): Type 'number' is not assignable to type 'string'.`, + ]); }); it('works for shorthand property declarations referring to template variables', () => { const messages = diagnose( - ` + `
`, - ` + ` class Dir { input: {span: string, b: number}; } class TestComponent {}`, - [{ + [ + { type: 'directive', name: 'Dir', selector: '[dir]', exportAs: ['dir'], inputs: {input: 'input'}, - }]); + }, + ], + ); - expect(messages).toEqual( - [`TestComponent.html(3, 30): Type 'HTMLElement' is not assignable to type 'string'.`]); + expect(messages).toEqual([ + `TestComponent.html(3, 30): Type 'HTMLElement' is not assignable to type 'string'.`, + ]); }); it('allows access to protected members', () => { - const messages = diagnose(``, ` + const messages = diagnose( + ``, + ` class TestComponent { protected message = 'Hello world'; protected doFoo(): void {} - }`); + }`, + ); expect(messages).toEqual([]); }); it('disallows access to private members', () => { - const messages = diagnose(``, ` + const messages = diagnose( + ``, + ` class TestComponent { private message = 'Hello world'; private doFoo(): void {} - }`); + }`, + ); expect(messages).toEqual([ `TestComponent.html(1, 18): Property 'doFoo' is private and only accessible within class 'TestComponent'.`, - `TestComponent.html(1, 30): Property 'message' is private and only accessible within class 'TestComponent'.` + `TestComponent.html(1, 30): Property 'message' is private and only accessible within class 'TestComponent'.`, ]); }); }); describe('method call spans', () => { it('reports invalid method name on method name span', () => { - const messages = diagnose(`{{ person.getNName() }}`, ` + const messages = diagnose( + `{{ person.getNName() }}`, + ` export class TestComponent { person: { getName(): string; }; - }`); + }`, + ); expect(messages).toEqual([ - `TestComponent.html(1, 11): Property 'getNName' does not exist on type '{ getName(): string; }'. Did you mean 'getName'?` + `TestComponent.html(1, 11): Property 'getNName' does not exist on type '{ getName(): string; }'. Did you mean 'getName'?`, ]); }); it('reports invalid method call signature on parameter span', () => { - const messages = diagnose(`{{ person.getName('abcd') }}`, ` + const messages = diagnose( + `{{ person.getName('abcd') }}`, + ` export class TestComponent { person: { getName(): string; }; - }`); + }`, + ); expect(messages).toEqual([`TestComponent.html(1, 19): Expected 0 arguments, but got 1.`]); }); @@ -638,25 +760,31 @@ class TestComponent { describe('safe method call spans', () => { it('reports invalid method name on method name span', () => { - const messages = diagnose(`{{ person?.getNName() }}`, ` + const messages = diagnose( + `{{ person?.getNName() }}`, + ` export class TestComponent { person?: { getName(): string; }; - }`); + }`, + ); expect(messages).toEqual([ - `TestComponent.html(1, 12): Property 'getNName' does not exist on type '{ getName(): string; }'. Did you mean 'getName'?` + `TestComponent.html(1, 12): Property 'getNName' does not exist on type '{ getName(): string; }'. Did you mean 'getName'?`, ]); }); it('reports invalid method call signature on parameter span', () => { - const messages = diagnose(`{{ person?.getName('abcd') }}`, ` + const messages = diagnose( + `{{ person?.getName('abcd') }}`, + ` export class TestComponent { person?: { getName(): string; }; - }`); + }`, + ); expect(messages).toEqual([`TestComponent.html(1, 20): Expected 0 arguments, but got 1.`]); }); @@ -664,28 +792,35 @@ class TestComponent { describe('property write spans', () => { it('reports invalid receiver property access on property access name span', () => { - const messages = diagnose(`
`, ` + const messages = diagnose( + `
`, + ` export class TestComponent { person: { name: string; }; - }`); + }`, + ); expect(messages).toEqual([ - `TestComponent.html(1, 22): Property 'nname' does not exist on type '{ name: string; }'. Did you mean 'name'?` + `TestComponent.html(1, 22): Property 'nname' does not exist on type '{ name: string; }'. Did you mean 'name'?`, ]); }); it('reports unassignable value on property write span', () => { - const messages = diagnose(`
`, ` + const messages = diagnose( + `
`, + ` export class TestComponent { person: { name: string; }; - }`); + }`, + ); - expect(messages).toEqual( - [`TestComponent.html(1, 15): Type 'number' is not assignable to type 'string'.`]); + expect(messages).toEqual([ + `TestComponent.html(1, 15): Type 'number' is not assignable to type 'string'.`, + ]); }); }); @@ -694,17 +829,25 @@ class TestComponent { // This test configures a component that is located outside the configured `rootDir`. Such // configuration requires that an inline type-check block is used as the reference emitter does // not allow generating imports outside `rootDir`. - const messages = - diagnose(`{{invalid}}`, `export class TestComponent {}`, [], [], {}, {rootDir: '/root'}); - - expect(messages).toEqual( - [`TestComponent.html(1, 3): Property 'invalid' does not exist on type 'TestComponent'.`]); + const messages = diagnose( + `{{invalid}}`, + `export class TestComponent {}`, + [], + [], + {}, + {rootDir: '/root'}, + ); + + expect(messages).toEqual([ + `TestComponent.html(1, 3): Property 'invalid' does not exist on type 'TestComponent'.`, + ]); }); describe('host directives', () => { it('should produce a diagnostic for host directive input bindings', () => { const messages = diagnose( - `
`, ` + `
`, + ` class Dir { } class HostDir { @@ -717,35 +860,40 @@ class TestComponent { age: number; }; }`, - [{ + [ + { type: 'directive', name: 'Dir', selector: '[dir]', - hostDirectives: [{ - directive: { - type: 'directive', - name: 'HostDir', - selector: '', - inputs: {input: 'input', otherInput: 'otherInput'}, - isStandalone: true, + hostDirectives: [ + { + directive: { + type: 'directive', + name: 'HostDir', + selector: '', + inputs: {input: 'input', otherInput: 'otherInput'}, + isStandalone: true, + }, + inputs: ['input', 'otherInput: alias'], }, - inputs: ['input', 'otherInput: alias'] - }] - }]); + ], + }, + ], + ); expect(messages).toEqual([ `TestComponent.html(1, 11): Type 'string' is not assignable to type 'number'.`, - `TestComponent.html(1, 33): Type 'number' is not assignable to type 'string'.` + `TestComponent.html(1, 33): Type 'number' is not assignable to type 'string'.`, ]); }); it('should produce a diagnostic for directive outputs', () => { const messages = diagnose( - `
`, - ` + ` import {EventEmitter} from '@angular/core'; class HostDir { stringEvent = new EventEmitter(); @@ -757,32 +905,37 @@ class TestComponent { handleStringEvent(event: string): void {} handleNumberEvent(event: number): void {} }`, - [{ + [ + { type: 'directive', name: 'Dir', selector: '[dir]', - hostDirectives: [{ - directive: { - type: 'directive', - name: 'HostDir', - selector: '', - isStandalone: true, - outputs: {stringEvent: 'stringEvent', numberEvent: 'numberEvent'}, + hostDirectives: [ + { + directive: { + type: 'directive', + name: 'HostDir', + selector: '', + isStandalone: true, + outputs: {stringEvent: 'stringEvent', numberEvent: 'numberEvent'}, + }, + outputs: ['stringEvent', 'numberEvent: numberAlias'], }, - outputs: ['stringEvent', 'numberEvent: numberAlias'] - }] - }]); + ], + }, + ], + ); expect(messages).toEqual([ `TestComponent.html(3, 46): Argument of type 'number' is not assignable to parameter of type 'string'.`, - `TestComponent.html(4, 46): Argument of type 'string' is not assignable to parameter of type 'number'.` + `TestComponent.html(4, 46): Argument of type 'string' is not assignable to parameter of type 'number'.`, ]); }); - it('should produce a diagnostic for host directive inputs and outputs that have not been exposed', - () => { - const messages = diagnose( - `
`, ` + it('should produce a diagnostic for host directive inputs and outputs that have not been exposed', () => { + const messages = diagnose( + `
`, + ` class Dir { } class HostDir { @@ -795,36 +948,42 @@ class TestComponent { }; handleStringEvent(event: string): void {} }`, - [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - hostDirectives: [{ - directive: { - type: 'directive', - name: 'HostDir', - selector: '', - inputs: {input: 'input'}, - outputs: {output: 'output'}, - isStandalone: true, - }, - // Intentionally left blank. - inputs: [], - outputs: [] - }] - }]); - - expect(messages).toEqual([ - // These messages are expected to refer to the native - // typings since the inputs/outputs haven't been exposed. - `TestComponent.html(1, 60): Argument of type 'Event' is not assignable to parameter of type 'string'.`, - `TestComponent.html(1, 10): Can't bind to 'input' since it isn't a known property of 'div'.` - ]); - }); + [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + hostDirectives: [ + { + directive: { + type: 'directive', + name: 'HostDir', + selector: '', + inputs: {input: 'input'}, + outputs: {output: 'output'}, + isStandalone: true, + }, + // Intentionally left blank. + inputs: [], + outputs: [], + }, + ], + }, + ], + ); + + expect(messages).toEqual([ + // These messages are expected to refer to the native + // typings since the inputs/outputs haven't been exposed. + `TestComponent.html(1, 60): Argument of type 'Event' is not assignable to parameter of type 'string'.`, + `TestComponent.html(1, 10): Can't bind to 'input' since it isn't a known property of 'div'.`, + ]); + }); it('should infer the type of host directive references', () => { const messages = diagnose( - `
{{ render(hostDir) }}
`, ` + `
{{ render(hostDir) }}
`, + ` class Dir {} class HostDir { value: number; @@ -832,20 +991,25 @@ class TestComponent { class TestComponent { render(input: string): string { return input; } }`, - [{ + [ + { type: 'directive', name: 'Dir', selector: '[dir]', - hostDirectives: [{ - directive: { - type: 'directive', - selector: '', - isStandalone: true, - name: 'HostDir', - exportAs: ['hostDir'] - } - }] - }]); + hostDirectives: [ + { + directive: { + type: 'directive', + selector: '', + isStandalone: true, + name: 'HostDir', + exportAs: ['hostDir'], + }, + }, + ], + }, + ], + ); expect(messages).toEqual([ `TestComponent.html(1, 39): Argument of type 'HostDir' is not assignable to parameter of type 'string'.`, @@ -856,13 +1020,15 @@ class TestComponent { describe('required inputs', () => { it('should produce a diagnostic when a single required input is missing', () => { const messages = diagnose( - `
`, ` + `
`, + ` class Dir { input: any; } class TestComponent {} `, - [{ + [ + { type: 'directive', name: 'Dir', selector: '[dir]', @@ -875,16 +1041,19 @@ class TestComponent { isSignal: false, }, }, - }]); + }, + ], + ); expect(messages).toEqual([ - `TestComponent.html(1, 1): Required input 'input' from directive Dir must be specified.` + `TestComponent.html(1, 1): Required input 'input' from directive Dir must be specified.`, ]); }); it('should produce a diagnostic when a multiple required inputs are missing', () => { const messages = diagnose( - `
`, ` + `
`, + ` class Dir { input: any; otherInput: any; @@ -894,43 +1063,44 @@ class TestComponent { } class TestComponent {} `, - [ - { - type: 'directive', - name: 'Dir', - selector: '[dir]', - inputs: { - input: { - classPropertyName: 'input', - bindingPropertyName: 'input', - required: true, - transform: null, - isSignal: false, - }, - otherInput: { - classPropertyName: 'otherInput', - bindingPropertyName: 'otherInput', - required: true, - transform: null, - isSignal: false, - } - } + [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + inputs: { + input: { + classPropertyName: 'input', + bindingPropertyName: 'input', + required: true, + transform: null, + isSignal: false, + }, + otherInput: { + classPropertyName: 'otherInput', + bindingPropertyName: 'otherInput', + required: true, + transform: null, + isSignal: false, + }, }, - { - type: 'directive', - name: 'OtherDir', - selector: '[otherDir]', - inputs: { - otherDirInput: { - classPropertyName: 'otherDirInput', - bindingPropertyName: 'otherDirInput', - required: true, - transform: null, - isSignal: false, - } + }, + { + type: 'directive', + name: 'OtherDir', + selector: '[otherDir]', + inputs: { + otherDirInput: { + classPropertyName: 'otherDirInput', + bindingPropertyName: 'otherDirInput', + required: true, + transform: null, + isSignal: false, }, - } - ]); + }, + }, + ], + ); expect(messages).toEqual([ `TestComponent.html(1, 1): Required inputs 'input', 'otherInput' from directive Dir must be specified.`, @@ -940,13 +1110,15 @@ class TestComponent { it('should report the public name of a missing required input', () => { const messages = diagnose( - `
`, ` + `
`, + ` class Dir { input: any; } class TestComponent {} `, - [{ + [ + { type: 'directive', name: 'Dir', selector: '[dir]', @@ -957,18 +1129,21 @@ class TestComponent { required: true, transform: null, isSignal: false, - } - } - }]); + }, + }, + }, + ], + ); expect(messages).toEqual([ - `TestComponent.html(1, 1): Required input 'inputAlias' from directive Dir must be specified.` + `TestComponent.html(1, 1): Required input 'inputAlias' from directive Dir must be specified.`, ]); }); it('should not produce a diagnostic if a required input is used in a binding', () => { const messages = diagnose( - `
`, ` + `
`, + ` class Dir { input: any; } @@ -976,7 +1151,8 @@ class TestComponent { foo: any; } `, - [{ + [ + { type: 'directive', name: 'Dir', selector: '[dir]', @@ -988,16 +1164,18 @@ class TestComponent { transform: null, isSignal: false, }, - } - }]); + }, + }, + ], + ); expect(messages).toEqual([]); }); - it('should not produce a diagnostic if a required input is used in a binding through an alias', - () => { - const messages = diagnose( - `
`, ` + it('should not produce a diagnostic if a required input is used in a binding through an alias', () => { + const messages = diagnose( + `
`, + ` class Dir { input: any; } @@ -1005,33 +1183,38 @@ class TestComponent { foo: any; } `, - [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - inputs: { - input: { - classPropertyName: 'input', - bindingPropertyName: 'inputAlias', - required: true, - transform: null, - isSignal: false, - }, - }, - }]); - - expect(messages).toEqual([]); - }); + [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + inputs: { + input: { + classPropertyName: 'input', + bindingPropertyName: 'inputAlias', + required: true, + transform: null, + isSignal: false, + }, + }, + }, + ], + ); + + expect(messages).toEqual([]); + }); it('should not produce a diagnostic if a required input is used in a static binding', () => { const messages = diagnose( - `
`, ` + `
`, + ` class Dir { input: any; } class TestComponent {} `, - [{ + [ + { type: 'directive', name: 'Dir', selector: '[dir]', @@ -1043,15 +1226,18 @@ class TestComponent { transform: null, isSignal: false, }, - } - }]); + }, + }, + ], + ); expect(messages).toEqual([]); }); it('should not produce a diagnostic if a required input is used in a two-way binding', () => { const messages = diagnose( - `
`, ` + `
`, + ` class Dir { input: any; inputChange: any; @@ -1060,7 +1246,8 @@ class TestComponent { foo: any; } `, - [{ + [ + { type: 'directive', name: 'Dir', selector: '[dir]', @@ -1074,101 +1261,115 @@ class TestComponent { }, }, outputs: {inputChange: 'inputChange'}, - }]); + }, + ], + ); expect(messages).toEqual([]); }); - it('should not produce a diagnostic for a required input that is the same the directive selector', - () => { - const messages = diagnose( - `
`, ` + it('should not produce a diagnostic for a required input that is the same the directive selector', () => { + const messages = diagnose( + `
`, + ` class Dir { dir: any; } class TestComponent {} `, - [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - inputs: { - dir: { - classPropertyName: 'dir', - bindingPropertyName: 'dir', - required: true, - transform: null, - isSignal: false, - } - } - }]); - - expect(messages).toEqual([]); - }); + [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + inputs: { + dir: { + classPropertyName: 'dir', + bindingPropertyName: 'dir', + required: true, + transform: null, + isSignal: false, + }, + }, + }, + ], + ); + + expect(messages).toEqual([]); + }); it('should produce a diagnostic when a required input from a host directive is missing', () => { const messages = diagnose( - `
`, ` + `
`, + ` class Dir {} class HostDir { input: any; } class TestComponent {}`, - [{ + [ + { type: 'directive', name: 'Dir', selector: '[dir]', - hostDirectives: [{ - directive: { - type: 'directive', - name: 'HostDir', - selector: '', - inputs: { - input: { - classPropertyName: 'input', - bindingPropertyName: 'hostAlias', - required: true, - transform: null, - isSignal: false, + hostDirectives: [ + { + directive: { + type: 'directive', + name: 'HostDir', + selector: '', + inputs: { + input: { + classPropertyName: 'input', + bindingPropertyName: 'hostAlias', + required: true, + transform: null, + isSignal: false, + }, }, + isStandalone: true, }, - isStandalone: true, + inputs: ['hostAlias: customAlias'], }, - inputs: ['hostAlias: customAlias'] - }] - }]); + ], + }, + ], + ); expect(messages).toEqual([ - `TestComponent.html(1, 1): Required input 'customAlias' from directive HostDir must be specified.` + `TestComponent.html(1, 1): Required input 'customAlias' from directive HostDir must be specified.`, ]); }); - it('should not report missing required inputs for an attribute binding with the same name', - () => { - const messages = diagnose( - `
`, ` + it('should not report missing required inputs for an attribute binding with the same name', () => { + const messages = diagnose( + `
`, + ` class MaxLengthValidator { maxlength: string; } class TestComponent {} `, - [{ - type: 'directive', - name: 'MaxLengthValidator', - selector: '[maxlength]', - inputs: { - maxlength: { - classPropertyName: 'maxlength', - bindingPropertyName: 'maxlength', - required: true, - transform: null, - isSignal: false, - }, - }, - }]); - - expect(messages).toEqual([]); - }); + [ + { + type: 'directive', + name: 'MaxLengthValidator', + selector: '[maxlength]', + inputs: { + maxlength: { + classPropertyName: 'maxlength', + bindingPropertyName: 'maxlength', + required: true, + transform: null, + isSignal: false, + }, + }, + }, + ], + ); + + expect(messages).toEqual([]); + }); }); // https://github.com/angular/angular/issues/43970 @@ -1176,15 +1377,18 @@ class TestComponent { afterEach(resetParseTemplateAsSourceFileForTest); it('baseline test without parse failure', () => { - const messages = diagnose(`
`, ` + const messages = diagnose( + `
`, + ` export class TestComponent { name: string | undefined; test(n: string): void {} - }`); + }`, + ); expect(messages).toEqual([ `TestComponent.html(1, 20): Argument of type 'string | undefined' is not assignable to parameter of type 'string'. - Type 'undefined' is not assignable to type 'string'.` + Type 'undefined' is not assignable to type 'string'.`, ]); }); @@ -1193,19 +1397,22 @@ class TestComponent { throw new Error('Simulated parse failure'); }); - const messages = diagnose(`
`, ` + const messages = diagnose( + `
`, + ` export class TestComponent { name: string | undefined; test(n: string): void {} - }`); + }`, + ); expect(messages.length).toBe(1); - expect(messages[0]) - .toContain( - `main.ts(2, 20): Argument of type 'string | undefined' is not assignable to parameter of type 'string'. + expect(messages[0]).toContain( + `main.ts(2, 20): Argument of type 'string | undefined' is not assignable to parameter of type 'string'. Type 'undefined' is not assignable to type 'string'. Failed to report an error in 'TestComponent.html' at 1:20 - Error: Simulated parse failure`); + Error: Simulated parse failure`, + ); }); it('should handle non-Error failures gracefully', () => { @@ -1213,19 +1420,22 @@ class TestComponent { throw 'Simulated parse failure'; }); - const messages = diagnose(`
`, ` + const messages = diagnose( + `
`, + ` export class TestComponent { name: string | undefined; test(n: string): void {} - }`); + }`, + ); expect(messages.length).toBe(1); - expect(messages[0]) - .toContain( - `main.ts(2, 20): Argument of type 'string | undefined' is not assignable to parameter of type 'string'. + expect(messages[0]).toContain( + `main.ts(2, 20): Argument of type 'string | undefined' is not assignable to parameter of type 'string'. Type 'undefined' is not assignable to type 'string'. Failed to report an error in 'TestComponent.html' at 1:20 - Simulated parse failure`); + Simulated parse failure`, + ); }); }); }); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/input_signal_diagnostics_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/input_signal_diagnostics_spec.ts index 12fb492e517ef..7f77f8670c61d 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/input_signal_diagnostics_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/input_signal_diagnostics_spec.ts @@ -17,9 +17,7 @@ runInEachFileSystem(() => { id: 'binding via attribute', inputs: {'show': {type: 'InputSignal', isSignal: true}}, template: `
`, - expected: [ - `TestComponent.html(1, 10): Type 'string' is not assignable to type 'boolean'.`, - ], + expected: [`TestComponent.html(1, 10): Type 'string' is not assignable to type 'boolean'.`], }, { id: 'explicit inline true binding', @@ -52,7 +50,8 @@ runInEachFileSystem(() => { template: `
`, expected: [ jasmine.stringContaining( - `Object literal may only specify known properties, and '"extraField"' does not exist in type '{ works: boolean; }'.`) + `Object literal may only specify known properties, and '"extraField"' does not exist in type '{ works: boolean; }'.`, + ), ], }, { @@ -60,7 +59,7 @@ runInEachFileSystem(() => { inputs: {'show': {type: 'InputSignal<{works: boolean}>', isSignal: true}}, template: `
`, expected: [ - `TestComponent.html(1, 11): Property 'works' is missing in type '{}' but required in type '{ works: boolean; }'.` + `TestComponent.html(1, 11): Property 'works' is missing in type '{}' but required in type '{ works: boolean; }'.`, ], }, // mixing cases @@ -71,7 +70,7 @@ runInEachFileSystem(() => { signalProp: {type: 'InputSignal', isSignal: true}, }, template: `
`, - expected: [] + expected: [], }, { id: 'mixing zone and signal inputs, invalid zone binding', @@ -80,7 +79,7 @@ runInEachFileSystem(() => { signalProp: {type: 'InputSignal', isSignal: true}, }, template: `
`, - expected: [`TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`] + expected: [`TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`], }, { id: 'mixing zone and signal inputs, invalid signal binding', @@ -89,7 +88,7 @@ runInEachFileSystem(() => { signalProp: {type: 'InputSignal', isSignal: true}, }, template: `
`, - expected: [`TestComponent.html(1, 32): Type '{}' is not assignable to type 'string'.`] + expected: [`TestComponent.html(1, 32): Type '{}' is not assignable to type 'string'.`], }, { id: 'mixing zone and signal inputs, both invalid', @@ -100,28 +99,28 @@ runInEachFileSystem(() => { template: `
`, expected: [ `TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`, - `TestComponent.html(1, 30): Type '{}' is not assignable to type 'string'.` - ] + `TestComponent.html(1, 30): Type '{}' is not assignable to type 'string'.`, + ], }, // restricted fields { id: 'disallows access to private input', inputs: { - pattern: {type: 'InputSignal', isSignal: true, restrictionModifier: 'private'} + pattern: {type: 'InputSignal', isSignal: true, restrictionModifier: 'private'}, }, template: `
`, expected: [ - `TestComponent.html(1, 11): Property 'pattern' is private and only accessible within class 'Dir'.` + `TestComponent.html(1, 11): Property 'pattern' is private and only accessible within class 'Dir'.`, ], }, { id: 'disallows access to protected input', inputs: { - pattern: {type: 'InputSignal', isSignal: true, restrictionModifier: 'protected'} + pattern: {type: 'InputSignal', isSignal: true, restrictionModifier: 'protected'}, }, template: `
`, expected: [ - `TestComponent.html(1, 11): Property 'pattern' is protected and only accessible within class 'Dir' and its subclasses.` + `TestComponent.html(1, 11): Property 'pattern' is protected and only accessible within class 'Dir' and its subclasses.`, ], }, { @@ -130,7 +129,7 @@ runInEachFileSystem(() => { // be perfectly fine to keep the `input()` member as readonly. id: 'allows access to readonly input', inputs: { - pattern: {type: 'InputSignal', isSignal: true, restrictionModifier: 'readonly'} + pattern: {type: 'InputSignal', isSignal: true, restrictionModifier: 'readonly'}, }, template: `
`, expected: [], @@ -139,7 +138,7 @@ runInEachFileSystem(() => { { id: 'allow access to private input if modifiers are explicitly ignored', inputs: { - pattern: {type: 'InputSignal', isSignal: true, restrictionModifier: 'private'} + pattern: {type: 'InputSignal', isSignal: true, restrictionModifier: 'private'}, }, template: `
`, expected: [], @@ -150,7 +149,7 @@ runInEachFileSystem(() => { { id: 'allow access to protected input if modifiers are explicitly ignored', inputs: { - pattern: {type: 'InputSignal', isSignal: true, restrictionModifier: 'protected'} + pattern: {type: 'InputSignal', isSignal: true, restrictionModifier: 'protected'}, }, template: `
`, expected: [], @@ -161,7 +160,7 @@ runInEachFileSystem(() => { { id: 'allow access to readonly input if modifiers are explicitly ignored', inputs: { - pattern: {type: 'InputSignal', isSignal: true, restrictionModifier: 'readonly'} + pattern: {type: 'InputSignal', isSignal: true, restrictionModifier: 'readonly'}, }, template: `
`, expected: [], @@ -172,12 +171,10 @@ runInEachFileSystem(() => { { id: 'allow access to private input if modifiers are explicitly ignored, but error if not assignable', inputs: { - pattern: {type: 'InputSignal', isSignal: true, restrictionModifier: 'private'} + pattern: {type: 'InputSignal', isSignal: true, restrictionModifier: 'private'}, }, template: `
`, - expected: [ - `TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`, - ], + expected: [`TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`], options: { honorAccessModifiersForInputBindings: false, }, @@ -191,13 +188,9 @@ runInEachFileSystem(() => { isSignal: true, }, }, - extraDirectiveMembers: [ - 'static ngAcceptInputType_pattern: string|boolean', - ], + extraDirectiveMembers: ['static ngAcceptInputType_pattern: string|boolean'], template: `
`, - expected: [ - `TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`, - ], + expected: [`TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`], }, // transforms { @@ -222,13 +215,11 @@ runInEachFileSystem(() => { other: { type: 'InputSignal', isSignal: true, - } + }, }, directiveGenerics: '', template: `
`, - expected: [ - `TestComponent.html(1, 25): Type 'string' is not assignable to type 'boolean'.`, - ], + expected: [`TestComponent.html(1, 25): Type 'string' is not assignable to type 'boolean'.`], }, { id: 'generic inference and binding to directive, mix of zone and signal', @@ -240,13 +231,11 @@ runInEachFileSystem(() => { other: { type: 'T', isSignal: false, - } + }, }, directiveGenerics: '', template: `
`, - expected: [ - `TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`, - ], + expected: [`TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`], }, { id: 'generic inference and binding to directive (with `extends boolean`), all signal inputs', @@ -258,7 +247,7 @@ runInEachFileSystem(() => { other: { type: 'InputSignal', isSignal: true, - } + }, }, directiveGenerics: '', template: `
`, @@ -276,13 +265,11 @@ runInEachFileSystem(() => { other: { type: 'T', isSignal: false, - } + }, }, directiveGenerics: '', template: `
`, - expected: [ - `TestComponent.html(1, 25): Type 'string' is not assignable to type 'boolean'.`, - ], + expected: [`TestComponent.html(1, 25): Type 'string' is not assignable to type 'boolean'.`], }, { id: 'generic multi-inference and bindings to directive, all signal inputs', @@ -294,11 +281,9 @@ runInEachFileSystem(() => { other: { type: 'InputSignal', isSignal: true, - } + }, }, - extraDirectiveMembers: [ - 'tester: {t: T, u: U} = null!', - ], + extraDirectiveMembers: ['tester: {t: T, u: U} = null!'], directiveGenerics: '', template: `
{ other: { type: 'U', isSignal: false, - } + }, }, - extraDirectiveMembers: [ - 'tester: {t: T, u: U} = null!', - ], + extraDirectiveMembers: ['tester: {t: T, u: U} = null!'], directiveGenerics: '', template: `
{ other: { type: 'InputSignal<{u: U}>', isSignal: true, - } + }, }, - extraDirectiveMembers: [ - 'tester: {t: T, u: U} = null!', - ], + extraDirectiveMembers: ['tester: {t: T, u: U} = null!'], directiveGenerics: '', template: `
{ }, }, template: `
`, - expected: [ - `TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`, - ], + expected: [`TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`], }, { id: 'differing WriteT and ReadT, generic ctor inference', @@ -413,9 +392,7 @@ runInEachFileSystem(() => { isSignal: true, }, }, - extraDirectiveMembers: [ - `tester: {t: T, blaValue: never} = null!`, - ], + extraDirectiveMembers: [`tester: {t: T, blaValue: never} = null!`], directiveGenerics: '', template: `
{ extraFileContent: ` class SomeNonExportedClass {} `, - extraDirectiveMembers: [ - `tester: {t: T} = null!`, - ], + extraDirectiveMembers: [`tester: {t: T} = null!`], directiveGenerics: '', template: `
`, component: `prop: HTMLElement = null!`, diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/model_signal_diagnostics_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/model_signal_diagnostics_spec.ts index 960deb251ed29..546a118f59a5c 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/model_signal_diagnostics_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/model_signal_diagnostics_spec.ts @@ -18,9 +18,7 @@ runInEachFileSystem(() => { inputs: {show: {type: 'ModelSignal', isSignal: true}}, outputs: {showChange: {type: 'ModelSignal'}}, template: `
`, - expected: [ - `TestComponent.html(1, 10): Type 'string' is not assignable to type 'boolean'.`, - ], + expected: [`TestComponent.html(1, 10): Type 'string' is not assignable to type 'boolean'.`], }, { id: 'one-way property binding', @@ -44,7 +42,8 @@ runInEachFileSystem(() => { template: `
`, expected: [ jasmine.stringContaining( - `Object literal may only specify known properties, and '"extraField"' does not exist in type '{ works: boolean; }'.`) + `Object literal may only specify known properties, and '"extraField"' does not exist in type '{ works: boolean; }'.`, + ), ], }, { @@ -53,7 +52,7 @@ runInEachFileSystem(() => { outputs: {showChange: {type: 'ModelSignal'}}, template: `
`, expected: [ - `TestComponent.html(1, 11): Property 'works' is missing in type '{}' but required in type '{ works: boolean; }'.` + `TestComponent.html(1, 11): Property 'works' is missing in type '{}' but required in type '{ works: boolean; }'.`, ], }, // mixing cases @@ -65,7 +64,7 @@ runInEachFileSystem(() => { }, outputs: {signalPropChange: {type: 'ModelSignal'}}, template: `
`, - expected: [] + expected: [], }, { id: 'mixing zone input and model, invalid zone binding', @@ -75,7 +74,7 @@ runInEachFileSystem(() => { }, outputs: {signalPropChange: {type: 'ModelSignal'}}, template: `
`, - expected: [`TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`] + expected: [`TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`], }, { id: 'mixing zone input and model, invalid signal binding', @@ -85,7 +84,7 @@ runInEachFileSystem(() => { }, outputs: {signalPropChange: {type: 'ModelSignal'}}, template: `
`, - expected: [`TestComponent.html(1, 32): Type '{}' is not assignable to type 'string'.`] + expected: [`TestComponent.html(1, 32): Type '{}' is not assignable to type 'string'.`], }, { id: 'mixing zone input and model, both invalid', @@ -97,36 +96,36 @@ runInEachFileSystem(() => { template: `
`, expected: [ `TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`, - `TestComponent.html(1, 30): Type '{}' is not assignable to type 'string'.` - ] + `TestComponent.html(1, 30): Type '{}' is not assignable to type 'string'.`, + ], }, // restricted fields { id: 'disallows access to private model', inputs: { - pattern: {type: 'ModelSignal', isSignal: true, restrictionModifier: 'private'} + pattern: {type: 'ModelSignal', isSignal: true, restrictionModifier: 'private'}, }, outputs: {patternChange: {type: 'ModelSignal'}}, template: `
`, expected: [ - `TestComponent.html(1, 11): Property 'pattern' is private and only accessible within class 'Dir'.` + `TestComponent.html(1, 11): Property 'pattern' is private and only accessible within class 'Dir'.`, ], }, { id: 'disallows access to protected model', inputs: { - pattern: {type: 'ModelSignal', isSignal: true, restrictionModifier: 'protected'} + pattern: {type: 'ModelSignal', isSignal: true, restrictionModifier: 'protected'}, }, outputs: {patternChange: {type: 'ModelSignal'}}, template: `
`, expected: [ - `TestComponent.html(1, 11): Property 'pattern' is protected and only accessible within class 'Dir' and its subclasses.` + `TestComponent.html(1, 11): Property 'pattern' is protected and only accessible within class 'Dir' and its subclasses.`, ], }, { id: 'allows access to readonly model by default', inputs: { - pattern: {type: 'ModelSignal', isSignal: true, restrictionModifier: 'readonly'} + pattern: {type: 'ModelSignal', isSignal: true, restrictionModifier: 'readonly'}, }, outputs: {patternChange: {type: 'ModelSignal'}}, template: `
`, @@ -136,7 +135,7 @@ runInEachFileSystem(() => { { id: 'allow access to private input if modifiers are explicitly ignored', inputs: { - pattern: {type: 'ModelSignal', isSignal: true, restrictionModifier: 'private'} + pattern: {type: 'ModelSignal', isSignal: true, restrictionModifier: 'private'}, }, outputs: {patternChange: {type: 'ModelSignal'}}, template: `
`, @@ -148,7 +147,7 @@ runInEachFileSystem(() => { { id: 'allow access to protected model if modifiers are explicitly ignored', inputs: { - pattern: {type: 'ModelSignal', isSignal: true, restrictionModifier: 'protected'} + pattern: {type: 'ModelSignal', isSignal: true, restrictionModifier: 'protected'}, }, outputs: {patternChange: {type: 'ModelSignal'}}, template: `
`, @@ -160,7 +159,7 @@ runInEachFileSystem(() => { { id: 'allow access to readonly input if modifiers are explicitly ignored', inputs: { - pattern: {type: 'ModelSignal', isSignal: true, restrictionModifier: 'readonly'} + pattern: {type: 'ModelSignal', isSignal: true, restrictionModifier: 'readonly'}, }, outputs: {patternChange: {type: 'ModelSignal'}}, template: `
`, @@ -172,13 +171,11 @@ runInEachFileSystem(() => { { id: 'allow access to private model if modifiers are explicitly ignored, but error if not assignable', inputs: { - pattern: {type: 'ModelSignal', isSignal: true, restrictionModifier: 'private'} + pattern: {type: 'ModelSignal', isSignal: true, restrictionModifier: 'private'}, }, outputs: {patternChange: {type: 'ModelSignal'}}, template: `
`, - expected: [ - `TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`, - ], + expected: [`TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`], options: { honorAccessModifiersForInputBindings: false, }, @@ -188,13 +185,9 @@ runInEachFileSystem(() => { id: 'coercion members are not respected', inputs: {pattern: {type: 'ModelSignal', isSignal: true}}, outputs: {patternChange: {type: 'ModelSignal'}}, - extraDirectiveMembers: [ - 'static ngAcceptInputType_pattern: string|boolean', - ], + extraDirectiveMembers: ['static ngAcceptInputType_pattern: string|boolean'], template: `
`, - expected: [ - `TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`, - ], + expected: [`TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`], }, // with generics (type constructor tests) { @@ -207,7 +200,7 @@ runInEachFileSystem(() => { other: { type: 'ModelSignal', isSignal: true, - } + }, }, outputs: { genChange: {type: 'ModelSignal'}, @@ -215,9 +208,7 @@ runInEachFileSystem(() => { }, directiveGenerics: '', template: `
`, - expected: [ - `TestComponent.html(1, 25): Type 'string' is not assignable to type 'boolean'.`, - ], + expected: [`TestComponent.html(1, 25): Type 'string' is not assignable to type 'boolean'.`], }, { id: 'generic inference and one-way binding to directive, mix of zone input and model', @@ -229,14 +220,12 @@ runInEachFileSystem(() => { other: { type: 'T', isSignal: false, - } + }, }, outputs: {genChange: {type: 'ModelSignal'}}, directiveGenerics: '', template: `
`, - expected: [ - `TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`, - ], + expected: [`TestComponent.html(1, 11): Type 'boolean' is not assignable to type 'string'.`], }, { id: 'generic inference and one-way binding to directive (with `extends boolean`), all model inputs', @@ -248,7 +237,7 @@ runInEachFileSystem(() => { other: { type: 'ModelSignal', isSignal: true, - } + }, }, outputs: {genChange: {type: 'ModelSignal'}, otherChange: {type: 'ModelSignal'}}, directiveGenerics: '', @@ -267,14 +256,12 @@ runInEachFileSystem(() => { other: { type: 'T', isSignal: false, - } + }, }, outputs: {genChange: {type: 'ModelSignal'}}, directiveGenerics: '', template: `
`, - expected: [ - `TestComponent.html(1, 25): Type 'string' is not assignable to type 'boolean'.`, - ], + expected: [`TestComponent.html(1, 25): Type 'string' is not assignable to type 'boolean'.`], }, { id: 'generic multi-inference and one-way bindings to directive, all model inputs', @@ -286,15 +273,13 @@ runInEachFileSystem(() => { other: { type: 'ModelSignal', isSignal: true, - } + }, }, outputs: { genChange: {type: 'ModelSignal'}, otherChange: {type: 'ModelSignal'}, }, - extraDirectiveMembers: [ - 'tester: {t: T, u: U} = null!', - ], + extraDirectiveMembers: ['tester: {t: T, u: U} = null!'], directiveGenerics: '', template: `
{ other: { type: 'U', isSignal: false, - } + }, }, outputs: {genChange: {type: 'ModelSignal'}}, - extraDirectiveMembers: [ - 'tester: {t: T, u: U} = null!', - ], + extraDirectiveMembers: ['tester: {t: T, u: U} = null!'], directiveGenerics: '', template: `
{ other: { type: 'ModelSignal<{u: U}>', isSignal: true, - } + }, }, outputs: { genChange: {type: 'ModelSignal'}, otherChange: {type: 'ModelSignal<{u: U}>'}, }, - extraDirectiveMembers: [ - 'tester: {t: T, u: U} = null!', - ], + extraDirectiveMembers: ['tester: {t: T, u: U} = null!'], directiveGenerics: '', template: `
{ extraFileContent: ` class SomeNonExportedClass {} `, - extraDirectiveMembers: [ - `tester: {t: T} = null!`, - ], + extraDirectiveMembers: [`tester: {t: T} = null!`], directiveGenerics: '', template: `
`, component: `prop: HTMLElement = null!`, @@ -390,7 +369,7 @@ runInEachFileSystem(() => { other: { type: 'ModelSignal', isSignal: true, - } + }, }, outputs: { genChange: {type: 'ModelSignal'}, @@ -402,9 +381,7 @@ runInEachFileSystem(() => { otherVal!: string; `, template: `
`, - expected: [ - `TestComponent.html(1, 29): Type 'string' is not assignable to type 'boolean'.`, - ], + expected: [`TestComponent.html(1, 29): Type 'string' is not assignable to type 'boolean'.`], }, { id: 'generic inference and two-way binding to directive, mix of zone input and model', @@ -416,7 +393,7 @@ runInEachFileSystem(() => { other: { type: 'T', isSignal: false, - } + }, }, outputs: { genChange: {type: 'ModelSignal'}, @@ -428,9 +405,7 @@ runInEachFileSystem(() => { genVal!: boolean; otherVal!: string; `, - expected: [ - `TestComponent.html(1, 12): Type 'boolean' is not assignable to type 'string'.`, - ], + expected: [`TestComponent.html(1, 12): Type 'boolean' is not assignable to type 'string'.`], }, { id: 'generic inference and two-way binding to directive (with `extends boolean`), all model inputs', @@ -442,7 +417,7 @@ runInEachFileSystem(() => { other: { type: 'ModelSignal', isSignal: true, - } + }, }, outputs: {genChange: {type: 'ModelSignal'}, otherChange: {type: 'ModelSignal'}}, directiveGenerics: '', @@ -451,9 +426,7 @@ runInEachFileSystem(() => { genVal!: boolean; otherVal!: string; `, - expected: [ - `TestComponent.html(1, 29): Type 'string' is not assignable to type 'boolean'.`, - ], + expected: [`TestComponent.html(1, 29): Type 'string' is not assignable to type 'boolean'.`], }, { id: 'generic inference and two-way binding to directive (with `extends boolean`), mix of zone inputs and model', @@ -465,7 +438,7 @@ runInEachFileSystem(() => { other: { type: 'T', isSignal: false, - } + }, }, outputs: { genChange: {type: 'ModelSignal'}, @@ -477,9 +450,7 @@ runInEachFileSystem(() => { genVal!: boolean; otherVal!: string; `, - expected: [ - `TestComponent.html(1, 29): Type 'string' is not assignable to type 'boolean'.`, - ], + expected: [`TestComponent.html(1, 29): Type 'string' is not assignable to type 'boolean'.`], }, { id: 'generic multi-inference and two-way bindings to directive, all model inputs', @@ -491,15 +462,13 @@ runInEachFileSystem(() => { other: { type: 'ModelSignal', isSignal: true, - } + }, }, outputs: { genChange: {type: 'ModelSignal'}, otherChange: {type: 'ModelSignal'}, }, - extraDirectiveMembers: [ - 'tester: {t: T, u: U} = null!', - ], + extraDirectiveMembers: ['tester: {t: T, u: U} = null!'], directiveGenerics: '', template: `
{ other: { type: 'U', isSignal: false, - } + }, }, outputs: { genChange: {type: 'ModelSignal'}, otherChange: {type: 'EventEmitter'}, }, - extraDirectiveMembers: [ - 'tester: {t: T, u: U} = null!', - ], + extraDirectiveMembers: ['tester: {t: T, u: U} = null!'], directiveGenerics: '', template: `
{ other: { type: 'ModelSignal<{u: U}>', isSignal: true, - } + }, }, outputs: { genChange: {type: 'ModelSignal'}, otherChange: {type: 'ModelSignal<{u: U}>'}, }, - extraDirectiveMembers: [ - 'tester: {t: T, u: U} = null!', - ], + extraDirectiveMembers: ['tester: {t: T, u: U} = null!'], directiveGenerics: '', template: `
{ extraFileContent: ` class SomeNonExportedClass {} `, - extraDirectiveMembers: [ - `tester: {t: T} = null!`, - ], + extraDirectiveMembers: [`tester: {t: T} = null!`], directiveGenerics: '', template: `
`, component: `prop: HTMLElement = null!`, @@ -605,18 +568,14 @@ runInEachFileSystem(() => { inputs: {evt: {type: 'ModelSignal', isSignal: true}}, outputs: {evtChange: {type: 'ModelSignal'}}, template: `
`, - expected: [ - `TestComponent.html(1, 30): Property 'bla' does not exist on type 'string'.`, - ], + expected: [`TestComponent.html(1, 30): Property 'bla' does not exist on type 'string'.`], }, { id: 'one-way output to a model with a void type', inputs: {evt: {type: 'ModelSignal', isSignal: true}}, outputs: {evtChange: {type: 'ModelSignal'}}, template: `
`, - expected: [ - `TestComponent.html(1, 30): Property 'x' does not exist on type 'void'.`, - ], + expected: [`TestComponent.html(1, 30): Property 'x' does not exist on type 'void'.`], }, { id: 'two-way binding to primitive, invalid', @@ -624,9 +583,7 @@ runInEachFileSystem(() => { outputs: {valueChange: {type: 'ModelSignal'}}, template: `
`, component: `bla = true;`, - expected: [ - `TestComponent.html(1, 12): Type 'boolean' is not assignable to type 'string'.`, - ], + expected: [`TestComponent.html(1, 12): Type 'boolean' is not assignable to type 'string'.`], }, { id: 'two-way binding to primitive, valid', @@ -652,7 +609,7 @@ runInEachFileSystem(() => { expected: [ `TestComponent.html(1, 24): Type 'string' is not assignable to type 'never'.`, `TestComponent.html(1, 45): Type 'string' is not assignable to type 'never'.`, - ] + ], }, // restricted fields { @@ -662,7 +619,7 @@ runInEachFileSystem(() => { type: 'ModelSignal', isSignal: true, restrictionModifier: 'private', - } + }, }, outputs: {evtChange: {type: 'ModelSignal', restrictionModifier: 'private'}}, template: `
`, @@ -675,7 +632,7 @@ runInEachFileSystem(() => { type: 'ModelSignal', isSignal: true, restrictionModifier: 'protected', - } + }, }, outputs: {evt: {type: 'ModelSignal', restrictionModifier: 'protected'}}, template: `
`, @@ -695,9 +652,7 @@ runInEachFileSystem(() => { outputs: {valueChange: {type: 'ModelSignal'}}, template: `
`, component: `val!: WritableSignal;`, - expected: [ - `TestComponent.html(1, 12): Type 'string' is not assignable to type 'boolean'.`, - ], + expected: [`TestComponent.html(1, 12): Type 'string' is not assignable to type 'boolean'.`], }, { id: 'non-writable signal binding', @@ -725,7 +680,8 @@ runInEachFileSystem(() => { component: `val!: (v: string) => number;`, expected: [ jasmine.stringContaining( - `TestComponent.html(1, 12): Type '(v: string) => number' is not assignable to type '(v: number) => number`), + `TestComponent.html(1, 12): Type '(v: string) => number' is not assignable to type '(v: number) => number`, + ), ], }, ]; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/output_function_diagnostics.spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/output_function_diagnostics.spec.ts index 7f7a8813edc68..8b389bfa9e0c3 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/output_function_diagnostics.spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/output_function_diagnostics.spec.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ - import {runInEachFileSystem} from '../../file_system/testing'; import {generateDiagnoseJasmineSpecs, TestCase} from './test_case_helper'; @@ -18,17 +17,13 @@ runInEachFileSystem(() => { id: 'basic output', outputs: {'evt': {type: 'OutputEmitterRef'}}, template: `
`, - expected: [ - `TestComponent.html(1, 24): Property 'bla' does not exist on type 'string'.`, - ], + expected: [`TestComponent.html(1, 24): Property 'bla' does not exist on type 'string'.`], }, { id: 'output with void type', outputs: {'evt': {type: 'OutputEmitterRef'}}, template: `
`, - expected: [ - `TestComponent.html(1, 24): Property 'x' does not exist on type 'void'.`, - ], + expected: [`TestComponent.html(1, 24): Property 'x' does not exist on type 'void'.`], }, { id: 'two way data binding, invalid', @@ -36,9 +31,7 @@ runInEachFileSystem(() => { outputs: {'valueChange': {type: 'OutputEmitterRef'}}, template: `
`, component: `bla = true;`, - expected: [ - `TestComponent.html(1, 12): Type 'boolean' is not assignable to type 'string'.`, - ], + expected: [`TestComponent.html(1, 12): Type 'boolean' is not assignable to type 'string'.`], }, { id: 'two way data binding, valid', @@ -52,10 +45,8 @@ runInEachFileSystem(() => { id: 'complex output object', outputs: {'evt': {type: 'OutputEmitterRef<{works: boolean}>'}}, template: `
`, - component: `x: never = null!`, // to raise a diagnostic to check the type. - expected: [ - `TestComponent.html(1, 17): Type 'boolean' is not assignable to type 'never'.`, - ], + component: `x: never = null!`, // to raise a diagnostic to check the type. + expected: [`TestComponent.html(1, 17): Type 'boolean' is not assignable to type 'never'.`], }, // mixing cases { @@ -72,7 +63,7 @@ runInEachFileSystem(() => { expected: [ `TestComponent.html(1, 18): Type 'string' is not assignable to type 'never'.`, `TestComponent.html(1, 39): Type 'string' is not assignable to type 'never'.`, - ] + ], }, // restricted fields { diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/program_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/program_spec.ts index 0136a37e3ad65..78227329b146b 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/program_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/program_spec.ts @@ -21,11 +21,13 @@ runInEachFileSystem(() => { describe('template type-checking program', () => { it('should not be created if no components need to be checked', () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker, programStrategy} = setup([{ - fileName, - templates: {}, - source: `export class NotACmp {}`, - }]); + const {program, templateTypeChecker, programStrategy} = setup([ + { + fileName, + templates: {}, + source: `export class NotACmp {}`, + }, + ]); const sf = getSourceFileOrError(program, fileName); templateTypeChecker.getDiagnosticsForFile(sf, OptimizeFor.WholeProgram); @@ -42,8 +44,9 @@ runInEachFileSystem(() => { // Update /main.ngtypecheck.ts without changing its shape. Verify that the old program was // reused completely. programStrategy.updateFiles( - new Map([[typecheckPath, createUpdate('export const VERSION = 2;')]]), - UpdateMode.Complete); + new Map([[typecheckPath, createUpdate('export const VERSION = 2;')]]), + UpdateMode.Complete, + ); expectCompleteReuse(programStrategy.getProgram()); }); @@ -55,8 +58,9 @@ runInEachFileSystem(() => { // Update /main.ts without changing its shape. Verify that the old program was reused // completely. programStrategy.updateFiles( - new Map([[mainPath, createUpdate('export const STILL_NOT_A_COMPONENT = true;')]]), - UpdateMode.Complete); + new Map([[mainPath, createUpdate('export const STILL_NOT_A_COMPONENT = true;')]]), + UpdateMode.Complete, + ); expectCompleteReuse(programStrategy.getProgram()); }); @@ -71,11 +75,11 @@ function createUpdate(text: string): FileUpdate { } function makeSingleFileProgramWithTypecheckShim(): { - program: ts.Program, - host: ts.CompilerHost, - options: ts.CompilerOptions, - mainPath: AbsoluteFsPath, - typecheckPath: AbsoluteFsPath, + program: ts.Program; + host: ts.CompilerHost; + options: ts.CompilerOptions; + mainPath: AbsoluteFsPath; + typecheckPath: AbsoluteFsPath; } { const mainPath = absoluteFrom('/main.ts'); const typecheckPath = absoluteFrom('/main.ngtypecheck.ts'); @@ -87,7 +91,7 @@ function makeSingleFileProgramWithTypecheckShim(): { { name: typecheckPath, contents: 'export const VERSION = 1;', - } + }, ]); const sf = getSourceFileOrError(program, mainPath); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/span_comments_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/span_comments_spec.ts index 84b99065da44e..e4db7d93c51d5 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/span_comments_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/span_comments_spec.ts @@ -18,44 +18,46 @@ describe('type check blocks diagnostics', () => { }); it('should annotate binary ops', () => { - expect(tcbWithSpans('{{ a + b }}')) - .toContain('(((this).a /*3,4*/) /*3,4*/) + (((this).b /*7,8*/) /*7,8*/) /*3,8*/'); + expect(tcbWithSpans('{{ a + b }}')).toContain( + '(((this).a /*3,4*/) /*3,4*/) + (((this).b /*7,8*/) /*7,8*/) /*3,8*/', + ); }); it('should annotate conditions', () => { - expect(tcbWithSpans('{{ a ? b : c }}')) - .toContain( - '(((this).a /*3,4*/) /*3,4*/ ? ((this).b /*7,8*/) /*7,8*/ : (((this).c /*11,12*/) /*11,12*/)) /*3,12*/'); + expect(tcbWithSpans('{{ a ? b : c }}')).toContain( + '(((this).a /*3,4*/) /*3,4*/ ? ((this).b /*7,8*/) /*7,8*/ : (((this).c /*11,12*/) /*11,12*/)) /*3,12*/', + ); }); it('should annotate interpolations', () => { - expect(tcbWithSpans('{{ hello }} {{ world }}')) - .toContain( - '"" + (((this).hello /*3,8*/) /*3,8*/) + (((this).world /*15,20*/) /*15,20*/)'); + expect(tcbWithSpans('{{ hello }} {{ world }}')).toContain( + '"" + (((this).hello /*3,8*/) /*3,8*/) + (((this).world /*15,20*/) /*15,20*/)', + ); }); it('should annotate literal map expressions', () => { // The additional method call is present to avoid that the object literal is emitted as // statement, which would wrap it into parenthesis that clutter the expected output. const TEMPLATE = '{{ m({foo: a, bar: b}) }}'; - expect(tcbWithSpans(TEMPLATE)) - .toContain( - '(this).m /*3,4*/({ "foo": ((this).a /*11,12*/) /*11,12*/, "bar": ((this).b /*19,20*/) /*19,20*/ } /*5,21*/) /*3,22*/'); + expect(tcbWithSpans(TEMPLATE)).toContain( + '(this).m /*3,4*/({ "foo": ((this).a /*11,12*/) /*11,12*/, "bar": ((this).b /*19,20*/) /*19,20*/ } /*5,21*/) /*3,22*/', + ); }); it('should annotate literal map expressions with shorthand declarations', () => { // The additional method call is present to avoid that the object literal is emitted as // statement, which would wrap it into parenthesis that clutter the expected output. const TEMPLATE = '{{ m({a, b}) }}'; - expect(tcbWithSpans(TEMPLATE)) - .toContain( - '((this).m /*3,4*/({ "a": ((this).a /*6,7*/) /*6,7*/, "b": ((this).b /*9,10*/) /*9,10*/ } /*5,11*/) /*3,12*/)'); + expect(tcbWithSpans(TEMPLATE)).toContain( + '((this).m /*3,4*/({ "a": ((this).a /*6,7*/) /*6,7*/, "b": ((this).b /*9,10*/) /*9,10*/ } /*5,11*/) /*3,12*/)', + ); }); it('should annotate literal array expressions', () => { const TEMPLATE = '{{ [a, b] }}'; - expect(tcbWithSpans(TEMPLATE)) - .toContain('[((this).a /*4,5*/) /*4,5*/, ((this).b /*7,8*/) /*7,8*/] /*3,9*/'); + expect(tcbWithSpans(TEMPLATE)).toContain( + '[((this).a /*4,5*/) /*4,5*/, ((this).b /*7,8*/) /*7,8*/] /*3,9*/', + ); }); it('should annotate literals', () => { @@ -75,82 +77,86 @@ describe('type check blocks diagnostics', () => { it('should annotate method calls', () => { const TEMPLATE = `{{ method(a, b) }}`; - expect(tcbWithSpans(TEMPLATE)) - .toContain( - '(this).method /*3,9*/(((this).a /*10,11*/) /*10,11*/, ((this).b /*13,14*/) /*13,14*/) /*3,15*/'); + expect(tcbWithSpans(TEMPLATE)).toContain( + '(this).method /*3,9*/(((this).a /*10,11*/) /*10,11*/, ((this).b /*13,14*/) /*13,14*/) /*3,15*/', + ); }); it('should annotate safe calls', () => { const TEMPLATE = `{{ method?.(a, b) }}`; - expect(tcbWithSpans(TEMPLATE)) - .toContain( - '((null as any ? (((this).method /*3,9*/) /*3,9*/)!(((this).a /*12,13*/) /*12,13*/, ((this).b /*15,16*/) /*15,16*/) : undefined) /*3,17*/)'); + expect(tcbWithSpans(TEMPLATE)).toContain( + '((null as any ? (((this).method /*3,9*/) /*3,9*/)!(((this).a /*12,13*/) /*12,13*/, ((this).b /*15,16*/) /*15,16*/) : undefined) /*3,17*/)', + ); }); it('should annotate method calls of variables', () => { const TEMPLATE = `{{ method(a, b) }}`; - expect(tcbWithSpans(TEMPLATE)) - .toContain( - '_t2 /*27,33*/(((this).a /*34,35*/) /*34,35*/, ((this).b /*37,38*/) /*37,38*/) /*27,39*/'); + expect(tcbWithSpans(TEMPLATE)).toContain( + '_t2 /*27,33*/(((this).a /*34,35*/) /*34,35*/, ((this).b /*37,38*/) /*37,38*/) /*27,39*/', + ); }); it('should annotate function calls', () => { const TEMPLATE = `{{ method(a)(b, c) }}`; - expect(tcbWithSpans(TEMPLATE)) - .toContain( - '(this).method /*3,9*/(((this).a /*10,11*/) /*10,11*/) /*3,12*/(((this).b /*13,14*/) /*13,14*/, ((this).c /*16,17*/) /*16,17*/) /*3,18*/'); + expect(tcbWithSpans(TEMPLATE)).toContain( + '(this).method /*3,9*/(((this).a /*10,11*/) /*10,11*/) /*3,12*/(((this).b /*13,14*/) /*13,14*/, ((this).c /*16,17*/) /*16,17*/) /*3,18*/', + ); }); it('should annotate property access', () => { const TEMPLATE = `{{ a.b.c }}`; - expect(tcbWithSpans(TEMPLATE)) - .toContain('((((((this).a /*3,4*/) /*3,4*/).b /*5,6*/) /*3,6*/).c /*7,8*/) /*3,8*/'); + expect(tcbWithSpans(TEMPLATE)).toContain( + '((((((this).a /*3,4*/) /*3,4*/).b /*5,6*/) /*3,6*/).c /*7,8*/) /*3,8*/', + ); }); it('should annotate property writes', () => { const TEMPLATE = `
`; - expect(tcbWithSpans(TEMPLATE)) - .toContain( - '(((((((this).a /*14,15*/) /*14,15*/).b /*16,17*/) /*14,17*/).c /*18,19*/) /*14,23*/ = (((this).d /*22,23*/) /*22,23*/)) /*14,23*/'); + expect(tcbWithSpans(TEMPLATE)).toContain( + '(((((((this).a /*14,15*/) /*14,15*/).b /*16,17*/) /*14,17*/).c /*18,19*/) /*14,23*/ = (((this).d /*22,23*/) /*22,23*/)) /*14,23*/', + ); }); it('should $event property writes', () => { const TEMPLATE = `
`; - expect(tcbWithSpans(TEMPLATE)) - .toContain('(((this).a /*14,15*/) /*14,24*/ = ($event /*18,24*/)) /*14,24*/;'); + expect(tcbWithSpans(TEMPLATE)).toContain( + '(((this).a /*14,15*/) /*14,24*/ = ($event /*18,24*/)) /*14,24*/;', + ); }); it('should annotate keyed property access', () => { const TEMPLATE = `{{ a[b] }}`; - expect(tcbWithSpans(TEMPLATE)) - .toContain('(((this).a /*3,4*/) /*3,4*/)[((this).b /*5,6*/) /*5,6*/] /*3,7*/'); + expect(tcbWithSpans(TEMPLATE)).toContain( + '(((this).a /*3,4*/) /*3,4*/)[((this).b /*5,6*/) /*5,6*/] /*3,7*/', + ); }); it('should annotate keyed property writes', () => { const TEMPLATE = `
`; - expect(tcbWithSpans(TEMPLATE)) - .toContain( - '((((this).a /*14,15*/) /*14,15*/)[((this).b /*16,17*/) /*16,17*/] = (((this).c /*21,22*/) /*21,22*/)) /*14,22*/'); + expect(tcbWithSpans(TEMPLATE)).toContain( + '((((this).a /*14,15*/) /*14,15*/)[((this).b /*16,17*/) /*16,17*/] = (((this).c /*21,22*/) /*21,22*/)) /*14,22*/', + ); }); it('should annotate safe property access', () => { const TEMPLATE = `{{ a?.b }}`; - expect(tcbWithSpans(TEMPLATE)) - .toContain('(null as any ? (((this).a /*3,4*/) /*3,4*/)!.b /*6,7*/ : undefined) /*3,7*/'); + expect(tcbWithSpans(TEMPLATE)).toContain( + '(null as any ? (((this).a /*3,4*/) /*3,4*/)!.b /*6,7*/ : undefined) /*3,7*/', + ); }); it('should annotate safe method calls', () => { const TEMPLATE = `{{ a?.method(b) }}`; - expect(tcbWithSpans(TEMPLATE)) - .toContain( - '((null as any ? (null as any ? (((this).a /*3,4*/) /*3,4*/)!.method /*6,12*/ : undefined) /*3,12*/!(((this).b /*13,14*/) /*13,14*/) : undefined) /*3,15*/)'); + expect(tcbWithSpans(TEMPLATE)).toContain( + '((null as any ? (null as any ? (((this).a /*3,4*/) /*3,4*/)!.method /*6,12*/ : undefined) /*3,12*/!(((this).b /*13,14*/) /*13,14*/) : undefined) /*3,15*/)', + ); }); it('should annotate safe keyed reads', () => { const TEMPLATE = `{{ a?.[0] }}`; - expect(tcbWithSpans(TEMPLATE)) - .toContain( - '(null as any ? (((this).a /*3,4*/) /*3,4*/)![0 /*7,8*/] /*3,9*/ : undefined) /*3,9*/'); + expect(tcbWithSpans(TEMPLATE)).toContain( + '(null as any ? (((this).a /*3,4*/) /*3,4*/)![0 /*7,8*/] /*3,9*/ : undefined) /*3,9*/', + ); }); it('should annotate $any casts', () => { @@ -160,22 +166,25 @@ describe('type check blocks diagnostics', () => { it('should annotate chained expressions', () => { const TEMPLATE = `
`; - expect(tcbWithSpans(TEMPLATE)) - .toContain( - '(((this).a /*14,15*/) /*14,15*/, ((this).b /*17,18*/) /*17,18*/, ((this).c /*20,21*/) /*20,21*/) /*14,21*/'); + expect(tcbWithSpans(TEMPLATE)).toContain( + '(((this).a /*14,15*/) /*14,15*/, ((this).b /*17,18*/) /*17,18*/, ((this).c /*20,21*/) /*20,21*/) /*14,21*/', + ); }); it('should annotate pipe usages', () => { const TEMPLATE = `{{ a | test:b }}`; - const PIPES: TestDeclaration[] = [{ - type: 'pipe', - name: 'TestPipe', - pipeName: 'test', - }]; + const PIPES: TestDeclaration[] = [ + { + type: 'pipe', + name: 'TestPipe', + pipeName: 'test', + }, + ]; const block = tcbWithSpans(TEMPLATE, PIPES); expect(block).toContain('var _pipe1 = null! as i0.TestPipe'); expect(block).toContain( - '(_pipe1.transform /*7,11*/(((this).a /*3,4*/) /*3,4*/, ((this).b /*12,13*/) /*12,13*/) /*3,13*/);'); + '(_pipe1.transform /*7,11*/(((this).a /*3,4*/) /*3,4*/, ((this).b /*12,13*/) /*12,13*/) /*3,13*/);', + ); }); describe('attaching multiple comments for multiple references', () => { @@ -188,31 +197,37 @@ describe('type check blocks diagnostics', () => { expect(tcbWithSpans(TEMPLATE)).toContain('((_t2 /*26,27*/) || (_t2 /*31,32*/) /*26,32*/);'); }); it('should be correct for directive refs', () => { - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'MyComponent', - selector: 'my-cmp', - isComponent: true, - }]; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'MyComponent', + selector: 'my-cmp', + isComponent: true, + }, + ]; const TEMPLATE = `{{ a || a }}`; - expect(tcbWithSpans(TEMPLATE, DIRECTIVES)) - .toContain('((_t1 /*23,24*/) || (_t1 /*28,29*/) /*23,29*/);'); + expect(tcbWithSpans(TEMPLATE, DIRECTIVES)).toContain( + '((_t1 /*23,24*/) || (_t1 /*28,29*/) /*23,29*/);', + ); }); }); describe('attaching comments for generic directive inputs', () => { it('should be correct for directive refs', () => { - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'MyComponent', - selector: 'my-cmp', - isComponent: true, - isGeneric: true, - inputs: {'inputA': 'inputA'}, - }]; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'MyComponent', + selector: 'my-cmp', + isComponent: true, + isGeneric: true, + inputs: {'inputA': 'inputA'}, + }, + ]; const TEMPLATE = ``; - expect(tcbWithSpans(TEMPLATE, DIRECTIVES)) - .toContain('_t1.inputA /*9,15*/ = ("" /*18,20*/) /*8,21*/;'); + expect(tcbWithSpans(TEMPLATE, DIRECTIVES)).toContain( + '_t1.inputA /*9,15*/ = ("" /*18,20*/) /*8,21*/;', + ); }); }); @@ -223,21 +238,24 @@ describe('type check blocks diagnostics', () => { expect(tcbWithSpans(template, [])).toContain('const _t1 /*6,10*/'); expect(tcbWithSpans(template, [])).toContain('(this).users /*14,19*/'); // index variable - expect(tcbWithSpans(template, [])) - .toContain('var _t2 /*37,38*/ = null! as number /*T:VAE*/ /*37,47*/'); + expect(tcbWithSpans(template, [])).toContain( + 'var _t2 /*37,38*/ = null! as number /*T:VAE*/ /*37,47*/', + ); // track expression expect(tcbWithSpans(template, [])).toContain('_t1 /*27,31*/;'); }); it('@for with comma separated variables', () => { - const template = - `@for (x of y; track x; let i = $index, odd = $odd,e_v_e_n=$even) { {{i + odd + e_v_e_n}} }`; - expect(tcbWithSpans(template, [])) - .toContain('var _t2 /*27,28*/ = null! as number /*T:VAE*/ /*27,37*/'); - expect(tcbWithSpans(template, [])) - .toContain('var _t3 /*39,42*/ = null! as boolean /*T:VAE*/ /*39,54*/'); - expect(tcbWithSpans(template, [])) - .toContain('var _t4 /*55,62*/ = null! as boolean /*T:VAE*/ /*55,68*/'); + const template = `@for (x of y; track x; let i = $index, odd = $odd,e_v_e_n=$even) { {{i + odd + e_v_e_n}} }`; + expect(tcbWithSpans(template, [])).toContain( + 'var _t2 /*27,28*/ = null! as number /*T:VAE*/ /*27,37*/', + ); + expect(tcbWithSpans(template, [])).toContain( + 'var _t3 /*39,42*/ = null! as boolean /*T:VAE*/ /*39,54*/', + ); + expect(tcbWithSpans(template, [])).toContain( + 'var _t4 /*55,62*/ = null! as boolean /*T:VAE*/ /*55,68*/', + ); }); it('@if', () => { diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/test_case_helper.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/test_case_helper.ts index a7a95091e5a57..a63b19d331cd4 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/test_case_helper.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/test_case_helper.ts @@ -45,7 +45,7 @@ export interface TestCase { /** Test component class code. */ component?: string; /** Expected diagnostics. */ - expected: (string|jasmine.AsymmetricMatcher)[]; + expected: (string | jasmine.AsymmetricMatcher)[]; /** Additional type checking options to be used. */ options?: Partial; /** Whether the test case should exclusively run. */ @@ -61,11 +61,13 @@ export function typeCheckDiagnose(c: TestCase, compilerOptions?: ts.CompilerOpti const outputs = c.outputs ?? {}; const inputFields = Object.keys(inputs).map( - inputName => - `${inputs[inputName].restrictionModifier ?? ''} ${inputName}: ${inputs[inputName].type}`); + (inputName) => + `${inputs[inputName].restrictionModifier ?? ''} ${inputName}: ${inputs[inputName].type}`, + ); const outputFields = Object.keys(outputs).map( - name => `${outputs[name].restrictionModifier ?? ''} ${name}: ${outputs[name].type}`); + (name) => `${outputs[name].restrictionModifier ?? ''} ${name}: ${outputs[name].type}`, + ); const testComponent = ` import { @@ -111,22 +113,26 @@ export function typeCheckDiagnose(c: TestCase, compilerOptions?: ts.CompilerOpti }, {}); const messages = diagnose( - c.template, testComponent, - [ - { - type: 'directive', - name: 'Dir', - selector: '[dir]', - exportAs: ['dir'], - isGeneric: c.directiveGenerics !== undefined, - outputs: outputDeclarations, - inputs: inputDeclarations, - restrictedInputFields: Object.entries(inputs) - .filter(([_, i]) => i.restrictionModifier !== undefined) - .map(([name]) => name), - }, - ], - undefined, c.options, compilerOptions); + c.template, + testComponent, + [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + exportAs: ['dir'], + isGeneric: c.directiveGenerics !== undefined, + outputs: outputDeclarations, + inputs: inputDeclarations, + restrictedInputFields: Object.entries(inputs) + .filter(([_, i]) => i.restrictionModifier !== undefined) + .map(([name]) => name), + }, + ], + undefined, + c.options, + compilerOptions, + ); expect(messages).toEqual(c.expected); } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts index fd7dd43e40dce..dd9678a68979b 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts @@ -14,7 +14,6 @@ import {Reference} from '../../imports'; import {OptimizeFor, TypeCheckingConfig} from '../api'; import {ALL_ENABLED_CONFIG, setup, tcb, TestDeclaration, TestDirective} from '../testing'; - describe('type check blocks', () => { beforeEach(() => initMockFileSystem('Native')); @@ -49,36 +48,42 @@ describe('type check blocks', () => { it('should handle nested ternary expressions', () => { const TEMPLATE = `{{a ? b : c ? d : e}}`; - expect(tcb(TEMPLATE)) - .toContain('(((this).a) ? ((this).b) : ((((this).c) ? ((this).d) : (((this).e)))))'); + expect(tcb(TEMPLATE)).toContain( + '(((this).a) ? ((this).b) : ((((this).c) ? ((this).d) : (((this).e)))))', + ); }); it('should handle nullish coalescing operator', () => { expect(tcb('{{ a ?? b }}')).toContain('((((this).a)) ?? (((this).b)))'); expect(tcb('{{ a ?? b ?? c }}')).toContain('(((((this).a)) ?? (((this).b))) ?? (((this).c)))'); - expect(tcb('{{ (a ?? b) + (c ?? e) }}')) - .toContain('(((((this).a)) ?? (((this).b))) + ((((this).c)) ?? (((this).e))))'); + expect(tcb('{{ (a ?? b) + (c ?? e) }}')).toContain( + '(((((this).a)) ?? (((this).b))) + ((((this).c)) ?? (((this).e))))', + ); }); it('should handle attribute values for directive inputs', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'DirA', - selector: '[dir]', - inputs: {inputA: 'inputA'}, - }]; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'DirA', + selector: '[dir]', + inputs: {inputA: 'inputA'}, + }, + ]; expect(tcb(TEMPLATE, DIRECTIVES)).toContain('_t1 = null! as i0.DirA; _t1.inputA = ("value");'); }); it('should handle multiple bindings to the same property', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'DirA', - selector: '[dir-a]', - inputs: {inputA: 'inputA'}, - }]; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'DirA', + selector: '[dir-a]', + inputs: {inputA: 'inputA'}, + }, + ]; const block = tcb(TEMPLATE, DIRECTIVES); expect(block).toContain('_t1.inputA = (1);'); expect(block).toContain('_t1.inputA = (2);'); @@ -86,23 +91,27 @@ describe('type check blocks', () => { it('should handle empty bindings', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'DirA', - selector: '[dir-a]', - inputs: {inputA: 'inputA'}, - }]; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'DirA', + selector: '[dir-a]', + inputs: {inputA: 'inputA'}, + }, + ]; expect(tcb(TEMPLATE, DIRECTIVES)).toContain('_t1.inputA = (undefined);'); }); it('should handle bindings without value', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'DirA', - selector: '[dir-a]', - inputs: {inputA: 'inputA'}, - }]; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'DirA', + selector: '[dir-a]', + inputs: {inputA: 'inputA'}, + }, + ]; expect(tcb(TEMPLATE, DIRECTIVES)).toContain('_t1.inputA = (undefined);'); }); @@ -131,51 +140,58 @@ describe('type check blocks', () => { describe('type constructors', () => { it('should handle missing property bindings', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - inputs: { - fieldA: 'inputA', - fieldB: 'inputB', + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + inputs: { + fieldA: 'inputA', + fieldB: 'inputB', + }, + isGeneric: true, }, - isGeneric: true, - }]; + ]; const actual = tcb(TEMPLATE, DIRECTIVES); expect(actual).toContain( - 'const _ctor1: (init: Pick, "fieldA" | "fieldB">) => i0.Dir = null!;'); + 'const _ctor1: (init: Pick, "fieldA" | "fieldB">) => i0.Dir = null!;', + ); expect(actual).toContain( - 'var _t1 = _ctor1({ "fieldA": (((this).foo)), "fieldB": null as any });'); + 'var _t1 = _ctor1({ "fieldA": (((this).foo)), "fieldB": null as any });', + ); }); it('should handle multiple bindings to the same property', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - inputs: { - fieldA: 'inputA', + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + inputs: { + fieldA: 'inputA', + }, + isGeneric: true, }, - isGeneric: true, - }]; + ]; const block = tcb(TEMPLATE, DIRECTIVES); expect(block).toContain('"fieldA": (1)'); expect(block).not.toContain('"fieldA": (2)'); }); - it('should only apply property bindings to directives', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - inputs: {'color': 'color', 'strong': 'strong', 'enabled': 'enabled'}, - isGeneric: true, - }]; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + inputs: {'color': 'color', 'strong': 'strong', 'enabled': 'enabled'}, + isGeneric: true, + }, + ]; const block = tcb(TEMPLATE, DIRECTIVES); expect(block).not.toContain('Dir.ngTypeCtor'); expect(block).toContain('"blue"; false; true;'); @@ -185,22 +201,24 @@ describe('type check blocks', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDirective[] = [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - exportAs: ['dir'], - inputs: {input: 'input'}, - isGeneric: true, - }]; + const DIRECTIVES: TestDirective[] = [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + exportAs: ['dir'], + inputs: {input: 'input'}, + isGeneric: true, + }, + ]; const actual = tcb(TEMPLATE, DIRECTIVES); expect(actual).toContain( - 'const _ctor1: (init: Pick, "input">) => i0.Dir = null!;'); + 'const _ctor1: (init: Pick, "input">) => i0.Dir = null!;', + ); expect(actual).toContain( - 'var _t2 = _ctor1({ "input": (null!) }); ' + - 'var _t1 = _t2; ' + - '_t2.input = (_t1);'); + 'var _t2 = _ctor1({ "input": (null!) }); ' + 'var _t1 = _t2; ' + '_t2.input = (_t1);', + ); }); it('should generate circular references between two directives correctly', () => { @@ -224,60 +242,67 @@ describe('type check blocks', () => { exportAs: ['dirB'], inputs: {inputB: 'inputB'}, isGeneric: true, - } + }, ]; const actual = tcb(TEMPLATE, DIRECTIVES); expect(actual).toContain( - 'const _ctor1: (init: Pick, "inputA">) => i0.DirA = null!; const _ctor2: (init: Pick, "inputB">) => i0.DirB = null!;'); + 'const _ctor1: (init: Pick, "inputA">) => i0.DirA = null!; const _ctor2: (init: Pick, "inputB">) => i0.DirB = null!;', + ); expect(actual).toContain( - 'var _t4 = _ctor1({ "inputA": (null!) }); ' + + 'var _t4 = _ctor1({ "inputA": (null!) }); ' + 'var _t3 = _t4; ' + 'var _t2 = _ctor2({ "inputB": (_t3) }); ' + 'var _t1 = _t2; ' + '_t4.inputA = (_t1); ' + - '_t2.inputB = (_t3);'); + '_t2.inputB = (_t3);', + ); }); it('should handle empty bindings', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'DirA', - selector: '[dir-a]', - inputs: {inputA: 'inputA'}, - isGeneric: true, - }]; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'DirA', + selector: '[dir-a]', + inputs: {inputA: 'inputA'}, + isGeneric: true, + }, + ]; expect(tcb(TEMPLATE, DIRECTIVES)).toContain('"inputA": (undefined)'); }); it('should handle bindings without value', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'DirA', - selector: '[dir-a]', - inputs: {inputA: 'inputA'}, - isGeneric: true, - }]; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'DirA', + selector: '[dir-a]', + inputs: {inputA: 'inputA'}, + isGeneric: true, + }, + ]; expect(tcb(TEMPLATE, DIRECTIVES)).toContain('"inputA": (undefined)'); }); it('should use coercion types if declared', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - inputs: { - fieldA: 'inputA', + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + inputs: { + fieldA: 'inputA', + }, + isGeneric: true, + coercedInputFields: ['fieldA'], }, - isGeneric: true, - coercedInputFields: ['fieldA'], - }]; - expect(tcb(TEMPLATE, DIRECTIVES)) - .toContain( - 'var _t1 = null! as typeof i0.Dir.ngAcceptInputType_fieldA; ' + - '_t1 = (((this).foo));'); + ]; + expect(tcb(TEMPLATE, DIRECTIVES)).toContain( + 'var _t1 = null! as typeof i0.Dir.ngAcceptInputType_fieldA; ' + '_t1 = (((this).foo));', + ); }); }); @@ -289,9 +314,8 @@ describe('type check blocks', () => { const block = tcb(TEMPLATE); expect(block).not.toContain('"div"'); expect(block).toContain( - 'var _t2 = document.createElement("button"); ' + - 'var _t1 = _t2; ' + - '_t2.addEventListener'); + 'var _t2 = document.createElement("button"); ' + 'var _t1 = _t2; ' + '_t2.addEventListener', + ); }); it('should only generate directive declarations that have bindings or are referenced', () => { @@ -352,9 +376,9 @@ describe('type check blocks', () => { {{ i.value }} `; - expect(tcb(TEMPLATE)) - .toContain( - 'var _t2 = document.createElement("input"); var _t1 = _t2; "" + (((_t1).value));'); + expect(tcb(TEMPLATE)).toContain( + 'var _t2 = document.createElement("input"); var _t1 = _t2; "" + (((_t1).value));', + ); }); it('should generate a forward directive reference correctly', () => { @@ -362,17 +386,17 @@ describe('type check blocks', () => { {{d.value}}
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - exportAs: ['dir'], - }]; - expect(tcb(TEMPLATE, DIRECTIVES)) - .toContain( - 'var _t2 = null! as i0.Dir; ' + - 'var _t1 = _t2; ' + - '"" + (((_t1).value));'); + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + exportAs: ['dir'], + }, + ]; + expect(tcb(TEMPLATE, DIRECTIVES)).toContain( + 'var _t2 = null! as i0.Dir; ' + 'var _t1 = _t2; ' + '"" + (((_t1).value));', + ); }); it('should handle style and class bindings specially', () => { @@ -391,12 +415,14 @@ describe('type check blocks', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - inputs: {'color': 'color', 'strong': 'strong', 'enabled': 'enabled'}, - }]; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + inputs: {'color': 'color', 'strong': 'strong', 'enabled': 'enabled'}, + }, + ]; const block = tcb(TEMPLATE, DIRECTIVES); expect(block).not.toContain('var _t1 = null! as Dir;'); expect(block).not.toContain('"color"'); @@ -409,18 +435,18 @@ describe('type check blocks', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDirective[] = [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - exportAs: ['dir'], - inputs: {input: 'input'}, - }]; - expect(tcb(TEMPLATE, DIRECTIVES)) - .toContain( - 'var _t2 = null! as i0.Dir; ' + - 'var _t1 = _t2; ' + - '_t2.input = (_t1);'); + const DIRECTIVES: TestDirective[] = [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + exportAs: ['dir'], + inputs: {input: 'input'}, + }, + ]; + expect(tcb(TEMPLATE, DIRECTIVES)).toContain( + 'var _t2 = null! as i0.Dir; ' + 'var _t1 = _t2; ' + '_t2.input = (_t1);', + ); }); it('should generate circular references between two directives correctly', () => { @@ -442,29 +468,31 @@ describe('type check blocks', () => { selector: '[dir-b]', exportAs: ['dirB'], inputs: {inputA: 'inputB'}, - } + }, ]; - expect(tcb(TEMPLATE, DIRECTIVES)) - .toContain( - 'var _t2 = null! as i0.DirB; ' + - 'var _t1 = _t2; ' + - 'var _t3 = null! as i0.DirA; ' + - '_t3.inputA = (_t1); ' + - 'var _t4 = _t3; ' + - '_t2.inputA = (_t4);'); + expect(tcb(TEMPLATE, DIRECTIVES)).toContain( + 'var _t2 = null! as i0.DirB; ' + + 'var _t1 = _t2; ' + + 'var _t3 = null! as i0.DirA; ' + + '_t3.inputA = (_t1); ' + + 'var _t4 = _t3; ' + + '_t2.inputA = (_t4);', + ); }); it('should handle undeclared properties', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - inputs: { - fieldA: 'inputA', + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + inputs: { + fieldA: 'inputA', + }, + undeclaredInputFields: ['fieldA'], }, - undeclaredInputFields: ['fieldA'] - }]; + ]; const block = tcb(TEMPLATE, DIRECTIVES); expect(block).not.toContain('var _t1 = null! as Dir;'); expect(block).toContain('(((this).foo)); '); @@ -472,163 +500,181 @@ describe('type check blocks', () => { it('should assign restricted properties to temp variables by default', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - inputs: { - fieldA: 'inputA', + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + inputs: { + fieldA: 'inputA', + }, + restrictedInputFields: ['fieldA'], }, - restrictedInputFields: ['fieldA'] - }]; - expect(tcb(TEMPLATE, DIRECTIVES)) - .toContain( - 'var _t1 = null! as i0.Dir; ' + - 'var _t2 = null! as (typeof _t1)["fieldA"]; ' + - '_t2 = (((this).foo)); '); + ]; + expect(tcb(TEMPLATE, DIRECTIVES)).toContain( + 'var _t1 = null! as i0.Dir; ' + + 'var _t2 = null! as (typeof _t1)["fieldA"]; ' + + '_t2 = (((this).foo)); ', + ); }); - it('should assign properties via element access for field names that are not JS identifiers', - () => { - const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - inputs: { - 'some-input.xs': 'inputA', - }, - stringLiteralInputFields: ['some-input.xs'], - }]; - const block = tcb(TEMPLATE, DIRECTIVES); - expect(block).toContain( - 'var _t1 = null! as i0.Dir; ' + - '_t1["some-input.xs"] = (((this).foo)); '); - }); + it('should assign properties via element access for field names that are not JS identifiers', () => { + const TEMPLATE = `
`; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + inputs: { + 'some-input.xs': 'inputA', + }, + stringLiteralInputFields: ['some-input.xs'], + }, + ]; + const block = tcb(TEMPLATE, DIRECTIVES); + expect(block).toContain( + 'var _t1 = null! as i0.Dir; ' + '_t1["some-input.xs"] = (((this).foo)); ', + ); + }); it('should handle a single property bound to multiple fields', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - inputs: { - field1: 'inputA', - field2: 'inputA', + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + inputs: { + field1: 'inputA', + field2: 'inputA', + }, }, - }]; - expect(tcb(TEMPLATE, DIRECTIVES)) - .toContain( - 'var _t1 = null! as i0.Dir; ' + - '_t1.field2 = _t1.field1 = (((this).foo));'); + ]; + expect(tcb(TEMPLATE, DIRECTIVES)).toContain( + 'var _t1 = null! as i0.Dir; ' + '_t1.field2 = _t1.field1 = (((this).foo));', + ); }); - it('should handle a single property bound to multiple fields, where one of them is coerced', - () => { - const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - inputs: { - field1: 'inputA', - field2: 'inputA', - }, - coercedInputFields: ['field1'], - }]; - expect(tcb(TEMPLATE, DIRECTIVES)) - .toContain( - 'var _t1 = null! as typeof i0.Dir.ngAcceptInputType_field1; ' + - 'var _t2 = null! as i0.Dir; ' + - '_t2.field2 = _t1 = (((this).foo));'); - }); - - it('should handle a single property bound to multiple fields, where one of them is undeclared', - () => { - const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - inputs: { - field1: 'inputA', - field2: 'inputA', - }, - undeclaredInputFields: ['field1'], - }]; - expect(tcb(TEMPLATE, DIRECTIVES)) - .toContain( - 'var _t1 = null! as i0.Dir; ' + - '_t1.field2 = (((this).foo));'); - }); + it('should handle a single property bound to multiple fields, where one of them is coerced', () => { + const TEMPLATE = `
`; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + inputs: { + field1: 'inputA', + field2: 'inputA', + }, + coercedInputFields: ['field1'], + }, + ]; + expect(tcb(TEMPLATE, DIRECTIVES)).toContain( + 'var _t1 = null! as typeof i0.Dir.ngAcceptInputType_field1; ' + + 'var _t2 = null! as i0.Dir; ' + + '_t2.field2 = _t1 = (((this).foo));', + ); + }); + + it('should handle a single property bound to multiple fields, where one of them is undeclared', () => { + const TEMPLATE = `
`; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + inputs: { + field1: 'inputA', + field2: 'inputA', + }, + undeclaredInputFields: ['field1'], + }, + ]; + expect(tcb(TEMPLATE, DIRECTIVES)).toContain( + 'var _t1 = null! as i0.Dir; ' + '_t1.field2 = (((this).foo));', + ); + }); it('should use coercion types if declared', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - inputs: { - fieldA: 'inputA', + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + inputs: { + fieldA: 'inputA', + }, + coercedInputFields: ['fieldA'], }, - coercedInputFields: ['fieldA'], - }]; + ]; const block = tcb(TEMPLATE, DIRECTIVES); expect(block).not.toContain('var _t1 = null! as Dir;'); expect(block).toContain( - 'var _t1 = null! as typeof i0.Dir.ngAcceptInputType_fieldA; ' + - '_t1 = (((this).foo));'); + 'var _t1 = null! as typeof i0.Dir.ngAcceptInputType_fieldA; ' + '_t1 = (((this).foo));', + ); }); it('should use coercion types if declared, even when backing field is not declared', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - inputs: { - fieldA: 'inputA', + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + inputs: { + fieldA: 'inputA', + }, + coercedInputFields: ['fieldA'], + undeclaredInputFields: ['fieldA'], }, - coercedInputFields: ['fieldA'], - undeclaredInputFields: ['fieldA'], - }]; + ]; const block = tcb(TEMPLATE, DIRECTIVES); expect(block).not.toContain('var _t1 = null! as Dir;'); expect(block).toContain( - 'var _t1 = null! as typeof i0.Dir.ngAcceptInputType_fieldA; ' + - '_t1 = (((this).foo));'); + 'var _t1 = null! as typeof i0.Dir.ngAcceptInputType_fieldA; ' + '_t1 = (((this).foo));', + ); }); it('should use transform type if an input has one', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - inputs: { - fieldA: { - bindingPropertyName: 'fieldA', - classPropertyName: 'fieldA', - required: false, - isSignal: false, - transform: { - node: ts.factory.createFunctionDeclaration( - undefined, undefined, undefined, undefined, [], undefined, undefined), - type: new Reference(ts.factory.createUnionTypeNode([ - ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), - ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), - ])) + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + inputs: { + fieldA: { + bindingPropertyName: 'fieldA', + classPropertyName: 'fieldA', + required: false, + isSignal: false, + transform: { + node: ts.factory.createFunctionDeclaration( + undefined, + undefined, + undefined, + undefined, + [], + undefined, + undefined, + ), + type: new Reference( + ts.factory.createUnionTypeNode([ + ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), + ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + ]), + ), + }, }, }, + coercedInputFields: ['fieldA'], }, - coercedInputFields: ['fieldA'], - }]; + ]; const block = tcb(TEMPLATE, DIRECTIVES); - expect(block).toContain( - 'var _t1 = null! as boolean | string; ' + - '_t1 = (((this).expr));'); + expect(block).toContain('var _t1 = null! as boolean | string; ' + '_t1 = (((this).expr));'); }); it('should handle $any casts', () => { @@ -651,13 +697,15 @@ describe('type check blocks', () => { it('should handle a two-way binding to an input/output pair', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'TwoWay', - selector: '[twoWay]', - inputs: {input: 'input'}, - outputs: {inputChange: 'inputChange'}, - }]; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'TwoWay', + selector: '[twoWay]', + inputs: {input: 'input'}, + outputs: {inputChange: 'inputChange'}, + }, + ]; const block = tcb(TEMPLATE, DIRECTIVES); expect(block).toContain('var _t1 = null! as i0.TwoWay;'); expect(block).toContain('_t1.input = i1.ɵunwrapWritableSignal((((this).value)));'); @@ -665,70 +713,88 @@ describe('type check blocks', () => { it('should handle a two-way binding to an input/output pair of a generic directive', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'TwoWay', - selector: '[twoWay]', - inputs: {input: 'input'}, - outputs: {inputChange: 'inputChange'}, - isGeneric: true, - }]; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'TwoWay', + selector: '[twoWay]', + inputs: {input: 'input'}, + outputs: {inputChange: 'inputChange'}, + isGeneric: true, + }, + ]; const block = tcb(TEMPLATE, DIRECTIVES); expect(block).toContain( - 'const _ctor1: (init: Pick, "input">) => i0.TwoWay = null!'); + 'const _ctor1: (init: Pick, "input">) => i0.TwoWay = null!', + ); expect(block).toContain( - 'var _t1 = _ctor1({ "input": (i1.ɵunwrapWritableSignal(((this).value))) });'); + 'var _t1 = _ctor1({ "input": (i1.ɵunwrapWritableSignal(((this).value))) });', + ); expect(block).toContain('_t1.input = i1.ɵunwrapWritableSignal((((this).value)));'); }); it('should handle a two-way binding to a model()', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'TwoWay', - selector: '[twoWay]', - inputs: { - input: { - classPropertyName: 'input', - bindingPropertyName: 'input', - required: false, - isSignal: true, - transform: null, - } + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'TwoWay', + selector: '[twoWay]', + inputs: { + input: { + classPropertyName: 'input', + bindingPropertyName: 'input', + required: false, + isSignal: true, + transform: null, + }, + }, + outputs: {inputChange: 'inputChange'}, }, - outputs: {inputChange: 'inputChange'}, - }]; + ]; const block = tcb(TEMPLATE, DIRECTIVES); expect(block).toContain('var _t1 = null! as i0.TwoWay;'); expect(block).toContain( - '_t1.input[i1.ɵINPUT_SIGNAL_BRAND_WRITE_TYPE] = i1.ɵunwrapWritableSignal((((this).value)));'); + '_t1.input[i1.ɵINPUT_SIGNAL_BRAND_WRITE_TYPE] = i1.ɵunwrapWritableSignal((((this).value)));', + ); }); it('should handle a two-way binding to an input with a transform', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'TwoWay', - selector: '[twoWay]', - inputs: { - input: { - classPropertyName: 'input', - bindingPropertyName: 'input', - required: false, - isSignal: false, - transform: { - node: ts.factory.createFunctionDeclaration( - undefined, undefined, undefined, undefined, [], undefined, undefined), - type: new Reference(ts.factory.createUnionTypeNode([ - ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), - ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), - ])) + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'TwoWay', + selector: '[twoWay]', + inputs: { + input: { + classPropertyName: 'input', + bindingPropertyName: 'input', + required: false, + isSignal: false, + transform: { + node: ts.factory.createFunctionDeclaration( + undefined, + undefined, + undefined, + undefined, + [], + undefined, + undefined, + ), + type: new Reference( + ts.factory.createUnionTypeNode([ + ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), + ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), + ]), + ), + }, }, - } + }, + outputs: {inputChange: 'inputChange'}, + coercedInputFields: ['input'], }, - outputs: {inputChange: 'inputChange'}, - coercedInputFields: ['input'], - }]; + ]; const block = tcb(TEMPLATE, DIRECTIVES); expect(block).toContain('var _t1 = null! as boolean | string;'); expect(block).toContain('_t1 = i1.ɵunwrapWritableSignal((((this).value)));'); @@ -738,55 +804,68 @@ describe('type check blocks', () => { it('should translate unclaimed bindings to their property equivalent', () => { const TEMPLATE = ``; const CONFIG = {...ALL_ENABLED_CONFIG, checkTypeOfDomBindings: true}; - expect(tcb(TEMPLATE, /* declarations */ undefined, CONFIG)) - .toContain('_t1["htmlFor"] = ("test");'); + expect(tcb(TEMPLATE, /* declarations */ undefined, CONFIG)).toContain( + '_t1["htmlFor"] = ("test");', + ); }); }); describe('template guards', () => { it('should emit invocation guards', () => { - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'NgIf', - selector: '[ngIf]', - inputs: {'ngIf': 'ngIf'}, - ngTemplateGuards: [{ - inputName: 'ngIf', - type: 'invocation', - }] - }]; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'NgIf', + selector: '[ngIf]', + inputs: {'ngIf': 'ngIf'}, + ngTemplateGuards: [ + { + inputName: 'ngIf', + type: 'invocation', + }, + ], + }, + ]; const TEMPLATE = `
{{person.name}}
`; const block = tcb(TEMPLATE, DIRECTIVES); expect(block).toContain('if (i0.NgIf.ngTemplateGuard_ngIf(_t1, ((this).person)))'); }); it('should emit binding guards', () => { - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'NgIf', - selector: '[ngIf]', - inputs: {'ngIf': 'ngIf'}, - ngTemplateGuards: [{ - inputName: 'ngIf', - type: 'binding', - }] - }]; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'NgIf', + selector: '[ngIf]', + inputs: {'ngIf': 'ngIf'}, + ngTemplateGuards: [ + { + inputName: 'ngIf', + type: 'binding', + }, + ], + }, + ]; const TEMPLATE = `
{{person.name}}
`; const block = tcb(TEMPLATE, DIRECTIVES); expect(block).toContain('if ((((this).person)) !== (null))'); }); it('should not emit guards when the child scope is empty', () => { - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'NgIf', - selector: '[ngIf]', - inputs: {'ngIf': 'ngIf'}, - ngTemplateGuards: [{ - inputName: 'ngIf', - type: 'invocation', - }] - }]; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'NgIf', + selector: '[ngIf]', + inputs: {'ngIf': 'ngIf'}, + ngTemplateGuards: [ + { + inputName: 'ngIf', + type: 'invocation', + }, + ], + }, + ]; const TEMPLATE = `
static
`; const block = tcb(TEMPLATE, DIRECTIVES); expect(block).not.toContain('NgIf.ngTemplateGuard_ngIf'); @@ -795,16 +874,19 @@ describe('type check blocks', () => { describe('outputs', () => { it('should emit subscribe calls for directive outputs', () => { - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - outputs: {'outputField': 'dirOutput'}, - }]; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + outputs: {'outputField': 'dirOutput'}, + }, + ]; const TEMPLATE = `
`; const block = tcb(TEMPLATE, DIRECTIVES); expect(block).toContain( - '_t1["outputField"].subscribe(($event): any => { (this).foo($event); });'); + '_t1["outputField"].subscribe(($event): any => { (this).foo($event); });', + ); }); it('should emit a listener function with AnimationEvent for animation events', () => { @@ -817,14 +899,16 @@ describe('type check blocks', () => { const TEMPLATE = `
`; const block = tcb(TEMPLATE); expect(block).toContain( - '_t1.addEventListener("event", ($event): any => { (this).foo($event); });'); + '_t1.addEventListener("event", ($event): any => { (this).foo($event); });', + ); }); it('should allow to cast $event using $any', () => { const TEMPLATE = `
`; const block = tcb(TEMPLATE); expect(block).toContain( - '_t1.addEventListener("event", ($event): any => { (this).foo(($event as any)); });'); + '_t1.addEventListener("event", ($event): any => { (this).foo(($event as any)); });', + ); }); it('should detect writes to template variables', () => { @@ -838,20 +922,23 @@ describe('type check blocks', () => { const block = tcb(TEMPLATE); expect(block).toContain( - '_t1.addEventListener("event", ($event): any => { (this).foo(((this).$event)); });'); + '_t1.addEventListener("event", ($event): any => { (this).foo(((this).$event)); });', + ); }); }); describe('config', () => { - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - exportAs: ['dir'], - inputs: {'dirInput': 'dirInput'}, - outputs: {'outputField': 'dirOutput'}, - hasNgTemplateContextGuard: true, - }]; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + exportAs: ['dir'], + inputs: {'dirInput': 'dirInput'}, + outputs: {'outputField': 'dirOutput'}, + hasNgTemplateContextGuard: true, + }, + ]; const BASE_CONFIG: TypeCheckingConfig = { applyTemplateContextGuards: true, checkQueries: false, @@ -888,8 +975,10 @@ describe('type check blocks', () => { expect(block).toContain(GUARD_APPLIED); }); it('should not apply template context guards when disabled', () => { - const DISABLED_CONFIG: - TypeCheckingConfig = {...BASE_CONFIG, applyTemplateContextGuards: false}; + const DISABLED_CONFIG: TypeCheckingConfig = { + ...BASE_CONFIG, + applyTemplateContextGuards: false, + }; const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG); expect(block).not.toContain(GUARD_APPLIED); }); @@ -929,8 +1018,10 @@ describe('type check blocks', () => { expect(block).toContain('((this).b);'); }); it('should use the non-null assertion operator when disabled', () => { - const DISABLED_CONFIG: - TypeCheckingConfig = {...BASE_CONFIG, strictNullInputBindings: false}; + const DISABLED_CONFIG: TypeCheckingConfig = { + ...BASE_CONFIG, + strictNullInputBindings: false, + }; const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG); expect(block).toContain('_t1.dirInput = (((this).a)!);'); expect(block).toContain('((this).b)!;'); @@ -947,8 +1038,10 @@ describe('type check blocks', () => { it('should not check types of bindings when disabled', () => { const TEMPLATE = `
`; - const DISABLED_CONFIG: - TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfInputBindings: false}; + const DISABLED_CONFIG: TypeCheckingConfig = { + ...BASE_CONFIG, + checkTypeOfInputBindings: false, + }; const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG); expect(block).toContain('_t1.dirInput = ((((this).a) as any));'); expect(block).toContain('(((this).b) as any);'); @@ -956,8 +1049,10 @@ describe('type check blocks', () => { it('should wrap the cast to any in parentheses when required', () => { const TEMPLATE = `
`; - const DISABLED_CONFIG: - TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfInputBindings: false}; + const DISABLED_CONFIG: TypeCheckingConfig = { + ...BASE_CONFIG, + checkTypeOfInputBindings: false, + }; const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG); expect(block).toContain('_t1.dirInput = ((((((this).a)) === (((this).b))) as any));'); }); @@ -969,18 +1064,23 @@ describe('type check blocks', () => { it('should check types of directive outputs when enabled', () => { const block = tcb(TEMPLATE, DIRECTIVES); expect(block).toContain( - '_t1["outputField"].subscribe(($event): any => { (this).foo($event); });'); + '_t1["outputField"].subscribe(($event): any => { (this).foo($event); });', + ); expect(block).toContain( - '_t2.addEventListener("nonDirOutput", ($event): any => { (this).foo($event); });'); + '_t2.addEventListener("nonDirOutput", ($event): any => { (this).foo($event); });', + ); }); it('should not check types of directive outputs when disabled', () => { - const DISABLED_CONFIG: - TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfOutputEvents: false}; + const DISABLED_CONFIG: TypeCheckingConfig = { + ...BASE_CONFIG, + checkTypeOfOutputEvents: false, + }; const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG); expect(block).toContain('($event: any): any => { (this).foo($event); }'); // Note that DOM events are still checked, that is controlled by `checkTypeOfDomEvents` expect(block).toContain( - 'addEventListener("nonDirOutput", ($event): any => { (this).foo($event); });'); + 'addEventListener("nonDirOutput", ($event): any => { (this).foo($event); });', + ); }); }); @@ -992,8 +1092,10 @@ describe('type check blocks', () => { expect(block).toContain('($event: i1.AnimationEvent): any => { (this).foo($event); }'); }); it('should not check types of animation events when disabled', () => { - const DISABLED_CONFIG: - TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfAnimationEvents: false}; + const DISABLED_CONFIG: TypeCheckingConfig = { + ...BASE_CONFIG, + checkTypeOfAnimationEvents: false, + }; const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG); expect(block).toContain('($event: any): any => { (this).foo($event); }'); }); @@ -1005,9 +1107,11 @@ describe('type check blocks', () => { it('should check types of DOM events when enabled', () => { const block = tcb(TEMPLATE, DIRECTIVES); expect(block).toContain( - '_t1["outputField"].subscribe(($event): any => { (this).foo($event); });'); + '_t1["outputField"].subscribe(($event): any => { (this).foo($event); });', + ); expect(block).toContain( - '_t2.addEventListener("nonDirOutput", ($event): any => { (this).foo($event); });'); + '_t2.addEventListener("nonDirOutput", ($event): any => { (this).foo($event); });', + ); }); it('should not check types of DOM events when disabled', () => { const DISABLED_CONFIG: TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfDomEvents: false}; @@ -1015,7 +1119,8 @@ describe('type check blocks', () => { // Note that directive outputs are still checked, that is controlled by // `checkTypeOfOutputEvents` expect(block).toContain( - '_t1["outputField"].subscribe(($event): any => { (this).foo($event); });'); + '_t1["outputField"].subscribe(($event): any => { (this).foo($event); });', + ); expect(block).toContain('($event: any): any => { (this).foo($event); }'); }); }); @@ -1029,27 +1134,28 @@ describe('type check blocks', () => { }); it('should use any for reference types when disabled', () => { - const DISABLED_CONFIG: - TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfDomReferences: false}; + const DISABLED_CONFIG: TypeCheckingConfig = { + ...BASE_CONFIG, + checkTypeOfDomReferences: false, + }; const block = tcb(TEMPLATE, [], DISABLED_CONFIG); - expect(block).toContain( - 'var _t1 = _t2 as any; ' + - '"" + (((_t1).value));'); + expect(block).toContain('var _t1 = _t2 as any; ' + '"" + (((_t1).value));'); }); }); describe('config.checkTypeOfNonDomReferences', () => { - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - exportAs: ['dir'], - inputs: {'dirInput': 'dirInput'}, - outputs: {'outputField': 'dirOutput'}, - hasNgTemplateContextGuard: true, - }]; - const TEMPLATE = - `
{{ref.value}}
{{ref2.value2}}`; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + exportAs: ['dir'], + inputs: {'dirInput': 'dirInput'}, + outputs: {'outputField': 'dirOutput'}, + hasNgTemplateContextGuard: true, + }, + ]; + const TEMPLATE = `
{{ref.value}}
{{ref2.value2}}`; it('should trace references to a directive when enabled', () => { const block = tcb(TEMPLATE, DIRECTIVES); @@ -1059,28 +1165,30 @@ describe('type check blocks', () => { it('should trace references to an when enabled', () => { const block = tcb(TEMPLATE, DIRECTIVES); expect(block).toContain( - 'var _t3 = (_t4 as any as i1.TemplateRef); ' + - '"" + (((_t3).value2));'); + 'var _t3 = (_t4 as any as i1.TemplateRef); ' + '"" + (((_t3).value2));', + ); }); it('should use any for reference types when disabled', () => { - const DISABLED_CONFIG: - TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfNonDomReferences: false}; + const DISABLED_CONFIG: TypeCheckingConfig = { + ...BASE_CONFIG, + checkTypeOfNonDomReferences: false, + }; const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG); - expect(block).toContain( - 'var _t1 = _t2 as any; ' + - '"" + (((_t1).value));'); + expect(block).toContain('var _t1 = _t2 as any; ' + '"" + (((_t1).value));'); }); }); describe('config.checkTypeOfAttributes', () => { const TEMPLATE = ``; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - inputs: {'disabled': 'disabled', 'cols': 'cols', 'rows': 'rows'}, - }]; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + inputs: {'disabled': 'disabled', 'cols': 'cols', 'rows': 'rows'}, + }, + ]; it('should assign string value to the input when enabled', () => { const block = tcb(TEMPLATE, DIRECTIVES); @@ -1100,11 +1208,13 @@ describe('type check blocks', () => { describe('config.checkTypeOfPipes', () => { const TEMPLATE = `{{a | test:b:c}}`; - const PIPES: TestDeclaration[] = [{ - type: 'pipe', - name: 'TestPipe', - pipeName: 'test', - }]; + const PIPES: TestDeclaration[] = [ + { + type: 'pipe', + name: 'TestPipe', + pipeName: 'test', + }, + ]; it('should check types of pipes when enabled', () => { const block = tcb(TEMPLATE, PIPES); @@ -1125,14 +1235,17 @@ describe('type check blocks', () => { it('should use undefined for safe navigation operations when enabled', () => { const block = tcb(TEMPLATE, DIRECTIVES); expect(block).toContain( - '(null as any ? (null as any ? (((this).a))!.method : undefined)!() : undefined)'); + '(null as any ? (null as any ? (((this).a))!.method : undefined)!() : undefined)', + ); expect(block).toContain('(null as any ? (((this).a))!.b : undefined)'); expect(block).toContain('(null as any ? (((this).a))![0] : undefined)'); expect(block).toContain('(null as any ? (((((this).a)).optionalMethod))!() : undefined)'); }); - it('should use an \'any\' type for safe navigation operations when disabled', () => { - const DISABLED_CONFIG: - TypeCheckingConfig = {...BASE_CONFIG, strictSafeNavigationTypes: false}; + it("should use an 'any' type for safe navigation operations when disabled", () => { + const DISABLED_CONFIG: TypeCheckingConfig = { + ...BASE_CONFIG, + strictSafeNavigationTypes: false, + }; const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG); expect(block).toContain('((((((this).a))!.method as any) as any)())'); expect(block).toContain('((((this).a))!.b as any)'); @@ -1142,20 +1255,23 @@ describe('type check blocks', () => { }); describe('config.strictSafeNavigationTypes (View Engine bug emulation)', () => { - const TEMPLATE = - `{{a.method()?.b}} {{a()?.method()}} {{a.method()?.[0]}} {{a.method()?.otherMethod?.()}}`; + const TEMPLATE = `{{a.method()?.b}} {{a()?.method()}} {{a.method()?.[0]}} {{a.method()?.otherMethod?.()}}`; it('should check the presence of a property/method on the receiver when enabled', () => { const block = tcb(TEMPLATE, DIRECTIVES); expect(block).toContain('(null as any ? ((((this).a)).method())!.b : undefined)'); expect(block).toContain( - '(null as any ? (null as any ? ((this).a())!.method : undefined)!() : undefined)'); + '(null as any ? (null as any ? ((this).a())!.method : undefined)!() : undefined)', + ); expect(block).toContain('(null as any ? ((((this).a)).method())![0] : undefined)'); expect(block).toContain( - '(null as any ? ((null as any ? ((((this).a)).method())!.otherMethod : undefined))!() : undefined)'); + '(null as any ? ((null as any ? ((((this).a)).method())!.otherMethod : undefined))!() : undefined)', + ); }); it('should not check the presence of a property/method on the receiver when disabled', () => { - const DISABLED_CONFIG: - TypeCheckingConfig = {...BASE_CONFIG, strictSafeNavigationTypes: false}; + const DISABLED_CONFIG: TypeCheckingConfig = { + ...BASE_CONFIG, + strictSafeNavigationTypes: false, + }; const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG); expect(block).toContain('(((((this).a)).method()) as any).b'); expect(block).toContain('(((((this).a()) as any).method as any)())'); @@ -1182,128 +1298,149 @@ describe('type check blocks', () => { describe('config.checkAccessModifiersForInputBindings', () => { const TEMPLATE = `
`; - it('should assign restricted properties via element access for field names that are not JS identifiers', - () => { - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - inputs: { - 'some-input.xs': 'inputA', - }, - restrictedInputFields: ['some-input.xs'], - stringLiteralInputFields: ['some-input.xs'], - }]; - const enableChecks: - TypeCheckingConfig = {...BASE_CONFIG, honorAccessModifiersForInputBindings: true}; - const block = tcb(TEMPLATE, DIRECTIVES, enableChecks); - expect(block).toContain( - 'var _t1 = null! as i0.Dir; ' + - '_t1["some-input.xs"] = (((this).foo)); '); - }); + it('should assign restricted properties via element access for field names that are not JS identifiers', () => { + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + inputs: { + 'some-input.xs': 'inputA', + }, + restrictedInputFields: ['some-input.xs'], + stringLiteralInputFields: ['some-input.xs'], + }, + ]; + const enableChecks: TypeCheckingConfig = { + ...BASE_CONFIG, + honorAccessModifiersForInputBindings: true, + }; + const block = tcb(TEMPLATE, DIRECTIVES, enableChecks); + expect(block).toContain( + 'var _t1 = null! as i0.Dir; ' + '_t1["some-input.xs"] = (((this).foo)); ', + ); + }); it('should assign restricted properties via property access', () => { - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'Dir', - selector: '[dir]', - inputs: { - fieldA: 'inputA', + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'Dir', + selector: '[dir]', + inputs: { + fieldA: 'inputA', + }, + restrictedInputFields: ['fieldA'], }, - restrictedInputFields: ['fieldA'] - }]; - const enableChecks: - TypeCheckingConfig = {...BASE_CONFIG, honorAccessModifiersForInputBindings: true}; + ]; + const enableChecks: TypeCheckingConfig = { + ...BASE_CONFIG, + honorAccessModifiersForInputBindings: true, + }; const block = tcb(TEMPLATE, DIRECTIVES, enableChecks); - expect(block).toContain( - 'var _t1 = null! as i0.Dir; ' + - '_t1.fieldA = (((this).foo)); '); + expect(block).toContain('var _t1 = null! as i0.Dir; ' + '_t1.fieldA = (((this).foo)); '); }); }); describe('config.allowSignalsInTwoWayBindings', () => { it('should not unwrap signals in two-way binding expressions', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'TwoWay', - selector: '[twoWay]', - inputs: {input: 'input'}, - outputs: {inputChange: 'inputChange'}, - }]; - const block = - tcb(TEMPLATE, DIRECTIVES, {...BASE_CONFIG, allowSignalsInTwoWayBindings: false}); + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'TwoWay', + selector: '[twoWay]', + inputs: {input: 'input'}, + outputs: {inputChange: 'inputChange'}, + }, + ]; + const block = tcb(TEMPLATE, DIRECTIVES, { + ...BASE_CONFIG, + allowSignalsInTwoWayBindings: false, + }); expect(block).not.toContain('ɵunwrapWritableSignal'); }); it('should not unwrap signals in two-way bindings to generic directives', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'TwoWay', - selector: '[twoWay]', - inputs: {input: 'input'}, - outputs: {inputChange: 'inputChange'}, - isGeneric: true, - }]; - const block = - tcb(TEMPLATE, DIRECTIVES, {...BASE_CONFIG, allowSignalsInTwoWayBindings: false}); + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'TwoWay', + selector: '[twoWay]', + inputs: {input: 'input'}, + outputs: {inputChange: 'inputChange'}, + isGeneric: true, + }, + ]; + const block = tcb(TEMPLATE, DIRECTIVES, { + ...BASE_CONFIG, + allowSignalsInTwoWayBindings: false, + }); expect(block).not.toContain('ɵunwrapWritableSignal'); }); }); }); - it('should use `any` type for type constructors with bound generic params ' + - 'when `useInlineTypeConstructors` is `false`', - () => { - const template = ` + it( + 'should use `any` type for type constructors with bound generic params ' + + 'when `useInlineTypeConstructors` is `false`', + () => { + const template = `
`; - const declarations: TestDeclaration[] = [{ - code: ` + const declarations: TestDeclaration[] = [ + { + code: ` interface PrivateInterface{}; export class Dir {}; `, - type: 'directive', - name: 'Dir', - selector: '[dir]', - inputs: { - inputA: 'inputA', - inputB: 'inputB', - }, - isGeneric: true - }]; - - const renderedTcb = tcb(template, declarations, {useInlineTypeConstructors: false}); - - expect(renderedTcb).toContain(`var _t1 = null! as i0.Dir;`); - expect(renderedTcb).toContain(`_t1.inputA = (((this).foo));`); - expect(renderedTcb).toContain(`_t1.inputB = (((this).bar));`); - }); + type: 'directive', + name: 'Dir', + selector: '[dir]', + inputs: { + inputA: 'inputA', + inputB: 'inputB', + }, + isGeneric: true, + }, + ]; + + const renderedTcb = tcb(template, declarations, {useInlineTypeConstructors: false}); + + expect(renderedTcb).toContain(`var _t1 = null! as i0.Dir;`); + expect(renderedTcb).toContain(`_t1.inputA = (((this).foo));`); + expect(renderedTcb).toContain(`_t1.inputB = (((this).bar));`); + }, + ); describe('host directives', () => { it('should generate bindings to host directive inputs/outputs', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'DirA', - selector: '[dir-a]', - hostDirectives: [{ - directive: { - type: 'directive', - name: 'HostDir', - selector: '', - inputs: {hostInput: 'hostInput'}, - outputs: {hostOutput: 'hostOutput'}, - isStandalone: true, - }, - inputs: ['hostInput'], - outputs: ['hostOutput'] - }] - }]; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'DirA', + selector: '[dir-a]', + hostDirectives: [ + { + directive: { + type: 'directive', + name: 'HostDir', + selector: '', + inputs: {hostInput: 'hostInput'}, + outputs: {hostOutput: 'hostOutput'}, + isStandalone: true, + }, + inputs: ['hostInput'], + outputs: ['hostOutput'], + }, + ], + }, + ]; const block = tcb(TEMPLATE, DIRECTIVES); expect(block).toContain('var _t1 = null! as i0.HostDir'); expect(block).toContain('_t1.hostInput = (1)'); @@ -1312,23 +1449,27 @@ describe('type check blocks', () => { it('should generate bindings to aliased host directive inputs/outputs', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'DirA', - selector: '[dir-a]', - hostDirectives: [{ - directive: { - type: 'directive', - name: 'HostDir', - selector: '', - inputs: {hostInput: 'hostInput'}, - outputs: {hostOutput: 'hostOutput'}, - isStandalone: true, - }, - inputs: ['hostInput: inputAlias'], - outputs: ['hostOutput: outputAlias'] - }] - }]; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'DirA', + selector: '[dir-a]', + hostDirectives: [ + { + directive: { + type: 'directive', + name: 'HostDir', + selector: '', + inputs: {hostInput: 'hostInput'}, + outputs: {hostOutput: 'hostOutput'}, + isStandalone: true, + }, + inputs: ['hostInput: inputAlias'], + outputs: ['hostOutput: outputAlias'], + }, + ], + }, + ]; const block = tcb(TEMPLATE, DIRECTIVES); expect(block).toContain('var _t1 = null! as i0.HostDir'); expect(block).toContain('_t1.hostInput = (1)'); @@ -1337,29 +1478,35 @@ describe('type check blocks', () => { it('should generate bindings to an input from a multi-level host directive', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'DirA', - selector: '[dir-a]', - hostDirectives: [{ - directive: { - type: 'directive', - name: 'HostDir', - selector: '', - isStandalone: true, - hostDirectives: [{ + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'DirA', + selector: '[dir-a]', + hostDirectives: [ + { directive: { type: 'directive', - name: 'MultiLevelHostDir', + name: 'HostDir', selector: '', isStandalone: true, - inputs: {'multiLevelHostInput': 'multiLevelHostInput'} + hostDirectives: [ + { + directive: { + type: 'directive', + name: 'MultiLevelHostDir', + selector: '', + isStandalone: true, + inputs: {'multiLevelHostInput': 'multiLevelHostInput'}, + }, + inputs: ['multiLevelHostInput'], + }, + ], }, - inputs: ['multiLevelHostInput'] - }] - }, - }] - }]; + }, + ], + }, + ]; const block = tcb(TEMPLATE, DIRECTIVES); expect(block).toContain('var _t1 = null! as i0.MultiLevelHostDir;'); expect(block).toContain('_t1.multiLevelHostInput = (1)'); @@ -1367,31 +1514,33 @@ describe('type check blocks', () => { it('should generate references to host directives', () => { const TEMPLATE = `
{{a.propA}} {{b.propB}}
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'DirA', - selector: '[dir-a]', - hostDirectives: [ - { - directive: { - type: 'directive', - name: 'HostA', - selector: '', - isStandalone: true, - exportAs: ['hostA'], + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'DirA', + selector: '[dir-a]', + hostDirectives: [ + { + directive: { + type: 'directive', + name: 'HostA', + selector: '', + isStandalone: true, + exportAs: ['hostA'], + }, }, - }, - { - directive: { - type: 'directive', - name: 'HostB', - selector: '', - isStandalone: true, - exportAs: ['hostB'], + { + directive: { + type: 'directive', + name: 'HostB', + selector: '', + isStandalone: true, + exportAs: ['hostB'], + }, }, - } - ] - }]; + ], + }, + ]; const block = tcb(TEMPLATE, DIRECTIVES); expect(block).toContain('var _t2 = null! as i0.HostA;'); expect(block).toContain('var _t4 = null! as i0.HostB;'); @@ -1400,22 +1549,26 @@ describe('type check blocks', () => { it('should generate bindings to the same input both from the host and host input', () => { const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'DirA', - selector: '[dir-a]', - inputs: {input: 'input'}, - hostDirectives: [{ - directive: { - type: 'directive', - name: 'HostDir', - selector: '', - inputs: {input: 'input'}, - isStandalone: true, - }, - inputs: ['input'] - }] - }]; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'DirA', + selector: '[dir-a]', + inputs: {input: 'input'}, + hostDirectives: [ + { + directive: { + type: 'directive', + name: 'HostDir', + selector: '', + inputs: {input: 'input'}, + isStandalone: true, + }, + inputs: ['input'], + }, + ], + }, + ]; const block = tcb(TEMPLATE, DIRECTIVES); expect(block).toContain('var _t1 = null! as i0.HostDir'); expect(block).toContain('var _t2 = null! as i0.DirA;'); @@ -1423,59 +1576,65 @@ describe('type check blocks', () => { expect(block).toContain('_t2.input = (1)'); }); - it('should not generate bindings to host directive inputs/outputs that have not been exposed', - () => { - const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'DirA', - selector: '[dir-a]', - hostDirectives: [{ - directive: { - type: 'directive', - name: 'HostDir', - selector: '', - inputs: {hostInput: 'hostInput'}, - outputs: {hostOutput: 'hostOutput'}, - isStandalone: true, - }, - // Intentionally left blank. - inputs: [], - outputs: [] - }] - }]; - const block = tcb(TEMPLATE, DIRECTIVES); - expect(block).not.toContain('var _t1 = null! i0.HostDir'); - expect(block).not.toContain('_t1.hostInput = (1)'); - expect(block).not.toContain('_t1["hostOutput"].subscribe'); - expect(block).toContain('_t1.addEventListener("hostOutput"'); - }); - - it('should generate bindings to aliased host directive inputs/outputs on a host with its own aliases', - () => { - const TEMPLATE = `
`; - const DIRECTIVES: TestDeclaration[] = [{ - type: 'directive', - name: 'DirA', - selector: '[dir-a]', - hostDirectives: [{ - directive: { - type: 'directive', - name: 'HostDir', - selector: '', - inputs: {hostInput: 'hostInputAlias'}, - outputs: {hostOutput: 'hostOutputAlias'}, - isStandalone: true, - }, - inputs: ['hostInputAlias: inputAlias'], - outputs: ['hostOutputAlias: outputAlias'] - }] - }]; - const block = tcb(TEMPLATE, DIRECTIVES); - expect(block).toContain('var _t1 = null! as i0.HostDir'); - expect(block).toContain('_t1.hostInput = (1)'); - expect(block).toContain('_t1["hostOutput"].subscribe'); - }); + it('should not generate bindings to host directive inputs/outputs that have not been exposed', () => { + const TEMPLATE = `
`; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'DirA', + selector: '[dir-a]', + hostDirectives: [ + { + directive: { + type: 'directive', + name: 'HostDir', + selector: '', + inputs: {hostInput: 'hostInput'}, + outputs: {hostOutput: 'hostOutput'}, + isStandalone: true, + }, + // Intentionally left blank. + inputs: [], + outputs: [], + }, + ], + }, + ]; + const block = tcb(TEMPLATE, DIRECTIVES); + expect(block).not.toContain('var _t1 = null! i0.HostDir'); + expect(block).not.toContain('_t1.hostInput = (1)'); + expect(block).not.toContain('_t1["hostOutput"].subscribe'); + expect(block).toContain('_t1.addEventListener("hostOutput"'); + }); + + it('should generate bindings to aliased host directive inputs/outputs on a host with its own aliases', () => { + const TEMPLATE = `
`; + const DIRECTIVES: TestDeclaration[] = [ + { + type: 'directive', + name: 'DirA', + selector: '[dir-a]', + hostDirectives: [ + { + directive: { + type: 'directive', + name: 'HostDir', + selector: '', + inputs: {hostInput: 'hostInputAlias'}, + outputs: {hostOutput: 'hostOutputAlias'}, + isStandalone: true, + }, + inputs: ['hostInputAlias: inputAlias'], + outputs: ['hostOutputAlias: outputAlias'], + }, + ], + }, + ]; + const block = tcb(TEMPLATE, DIRECTIVES); + expect(block).toContain('var _t1 = null! as i0.HostDir'); + expect(block).toContain('_t1.hostInput = (1)'); + expect(block).toContain('_t1["hostOutput"].subscribe'); + }); }); describe('deferred blocks', () => { @@ -1492,9 +1651,9 @@ describe('type check blocks', () => { } `; - expect(tcb(TEMPLATE)) - .toContain( - '"" + ((this).main()); "" + ((this).placeholder()); "" + ((this).loading()); "" + ((this).error());'); + expect(tcb(TEMPLATE)).toContain( + '"" + ((this).main()); "" + ((this).placeholder()); "" + ((this).loading()); "" + ((this).error());', + ); }); it('should generate `when` trigger', () => { @@ -1532,12 +1691,12 @@ describe('type check blocks', () => { } `; - expect(tcb(TEMPLATE)) - .toContain( - 'if ((((this).expr)) === (0)) { "" + ((this).main()); } ' + - 'else if ((((this).expr1)) === (1)) { "" + ((this).one()); } ' + - 'else if ((((this).expr2)) === (2)) { "" + ((this).two()); } ' + - 'else { "" + ((this).other()); }'); + expect(tcb(TEMPLATE)).toContain( + 'if ((((this).expr)) === (0)) { "" + ((this).main()); } ' + + 'else if ((((this).expr1)) === (1)) { "" + ((this).one()); } ' + + 'else if ((((this).expr2)) === (2)) { "" + ((this).two()); } ' + + 'else { "" + ((this).other()); }', + ); }); it('should generate a guard expression for listener inside conditional', () => { @@ -1557,11 +1716,14 @@ describe('type check blocks', () => { expect(result).toContain(`if ((((this).expr)) === (0)) (this).zero();`); expect(result).toContain( - `if (!((((this).expr)) === (0)) && (((this).expr)) === (1)) (this).one();`); + `if (!((((this).expr)) === (0)) && (((this).expr)) === (1)) (this).one();`, + ); expect(result).toContain( - `if (!((((this).expr)) === (0)) && !((((this).expr)) === (1)) && (((this).expr)) === (2)) (this).two();`); + `if (!((((this).expr)) === (0)) && !((((this).expr)) === (1)) && (((this).expr)) === (2)) (this).two();`, + ); expect(result).toContain( - `if (!((((this).expr)) === (0)) && !((((this).expr)) === (1)) && !((((this).expr)) === (2))) (this).otherwise();`); + `if (!((((this).expr)) === (0)) && !((((this).expr)) === (1)) && !((((this).expr)) === (2))) (this).otherwise();`, + ); }); it('should generate an if block with an `as` expression', () => { @@ -1569,13 +1731,13 @@ describe('type check blocks', () => { {{alias}} }`; - expect(tcb(TEMPLATE)) - .toContain('var _t1 = ((((this).expr)) === (1)); if (_t1) { "" + (_t1); } } }'); + expect(tcb(TEMPLATE)).toContain( + 'var _t1 = ((((this).expr)) === (1)); if (_t1) { "" + (_t1); } } }', + ); }); - it('should not generate the body of if blocks when `checkControlFlowBodies` is disabled', - () => { - const TEMPLATE = ` + it('should not generate the body of if blocks when `checkControlFlowBodies` is disabled', () => { + const TEMPLATE = ` @if (expr === 0) { {{main()}} } @else if (expr1 === 1) { @@ -1587,13 +1749,13 @@ describe('type check blocks', () => { } `; - expect(tcb(TEMPLATE, undefined, {checkControlFlowBodies: false})) - .toContain( - 'if ((((this).expr)) === (0)) { } ' + - 'else if ((((this).expr1)) === (1)) { } ' + - 'else if ((((this).expr2)) === (2)) { } ' + - 'else { }'); - }); + expect(tcb(TEMPLATE, undefined, {checkControlFlowBodies: false})).toContain( + 'if ((((this).expr)) === (0)) { } ' + + 'else if ((((this).expr1)) === (1)) { } ' + + 'else if ((((this).expr2)) === (2)) { } ' + + 'else { }', + ); + }); it('should generate a switch block', () => { const TEMPLATE = ` @@ -1610,12 +1772,12 @@ describe('type check blocks', () => { } `; - expect(tcb(TEMPLATE)) - .toContain( - 'switch (((this).expr)) { ' + - 'case 1: "" + ((this).one()); break; ' + - 'case 2: "" + ((this).two()); break; ' + - 'default: "" + ((this).default()); break; }'); + expect(tcb(TEMPLATE)).toContain( + 'switch (((this).expr)) { ' + + 'case 1: "" + ((this).one()); break; ' + + 'case 2: "" + ((this).two()); break; ' + + 'default: "" + ((this).default()); break; }', + ); }); it('should generate a switch block that only has a default case', () => { @@ -1627,8 +1789,9 @@ describe('type check blocks', () => { } `; - expect(tcb(TEMPLATE)) - .toContain('switch (((this).expr)) { default: "" + ((this).default()); break; }'); + expect(tcb(TEMPLATE)).toContain( + 'switch (((this).expr)) { default: "" + ((this).default()); break; }', + ); }); it('should generate a guard expression for a listener inside a switch case', () => { @@ -1670,21 +1833,20 @@ describe('type check blocks', () => {
`; - expect(tcb(TEMPLATE)) - .toContain( - 'var _t1 = null! as any; { var _t2 = (_t1.exp); switch (_t2()) { ' + - 'case "one": "" + ((this).one()); break; ' + - 'case "two": "" + ((this).two()); break; ' + - 'default: "" + ((this).default()); break; } }'); + expect(tcb(TEMPLATE)).toContain( + 'var _t1 = null! as any; { var _t2 = (_t1.exp); switch (_t2()) { ' + + 'case "one": "" + ((this).one()); break; ' + + 'case "two": "" + ((this).two()); break; ' + + 'default: "" + ((this).default()); break; } }', + ); }); it('should handle an empty switch block', () => { expect(tcb('@switch (expr) {}')).toContain('if (true) { switch (((this).expr)) { } }'); }); - it('should not generate the body of a switch block if checkControlFlowBodies is disabled', - () => { - const TEMPLATE = ` + it('should not generate the body of a switch block if checkControlFlowBodies is disabled', () => { + const TEMPLATE = ` @switch (expr) { @case (1) { {{one()}} @@ -1698,13 +1860,10 @@ describe('type check blocks', () => { } `; - expect(tcb(TEMPLATE, undefined, {checkControlFlowBodies: false})) - .toContain( - 'switch (((this).expr)) { ' + - 'case 1: break; ' + - 'case 2: break; ' + - 'default: break; }'); - }); + expect(tcb(TEMPLATE, undefined, {checkControlFlowBodies: false})).toContain( + 'switch (((this).expr)) { ' + 'case 1: break; ' + 'case 2: break; ' + 'default: break; }', + ); + }); }); describe('for loop blocks', () => { @@ -1795,9 +1954,8 @@ describe('type check blocks', () => { expect(result).toContain('(this).trackingFn(_t2, _t1, ((this).prop));'); }); - it('should not generate the body of a for block when checkControlFlowBodies is disabled', - () => { - const TEMPLATE = ` + it('should not generate the body of a for block when checkControlFlowBodies is disabled', () => { + const TEMPLATE = ` @for (item of items; track item) { {{main(item)}} } @empty { @@ -1805,11 +1963,11 @@ describe('type check blocks', () => { } `; - const result = tcb(TEMPLATE, undefined, {checkControlFlowBodies: false}); - expect(result).toContain('for (const _t1 of ((this).items)!) {'); - expect(result).not.toContain('.main'); - expect(result).not.toContain('.empty'); - }); + const result = tcb(TEMPLATE, undefined, {checkControlFlowBodies: false}); + expect(result).toContain('for (const _t1 of ((this).items)!) {'); + expect(result).not.toContain('.main'); + expect(result).not.toContain('.empty'); + }); }); describe('import generation', () => { @@ -1825,8 +1983,8 @@ describe('type check blocks', () => { classPropertyName: 'test', required: true, transform: null, - } - } + }, + }, }; it('should prefer namespace imports in type check files for new imports', () => { @@ -1839,26 +1997,30 @@ describe('type check blocks', () => { it('should re-use existing imports from original source files', () => { // This is especially important for inline type check blocks. // See: https://github.com/angular/angular/pull/53521#pullrequestreview-1778130879. - const {templateTypeChecker, program, programStrategy} = setup([{ - fileName: absoluteFrom('/test.ts'), - templates: {'AppComponent': TEMPLATE}, - declarations: [DIRECTIVE], - source: ` + const {templateTypeChecker, program, programStrategy} = setup([ + { + fileName: absoluteFrom('/test.ts'), + templates: {'AppComponent': TEMPLATE}, + declarations: [DIRECTIVE], + source: ` import {Component} from '@angular/core'; // should be re-used class AppComponent {} export class Dir {} `, - }]); + }, + ]); // Trigger type check block generation. templateTypeChecker.getDiagnosticsForFile( - getSourceFileOrError(program, absoluteFrom('/test.ts')), OptimizeFor.SingleFile); + getSourceFileOrError(program, absoluteFrom('/test.ts')), + OptimizeFor.SingleFile, + ); const testSf = getSourceFileOrError(programStrategy.getProgram(), absoluteFrom('/test.ts')); - expect(testSf.text) - .toContain( - `import { Component, ɵINPUT_SIGNAL_BRAND_WRITE_TYPE } from '@angular/core'; // should be re-used`); + expect(testSf.text).toContain( + `import { Component, ɵINPUT_SIGNAL_BRAND_WRITE_TYPE } from '@angular/core'; // should be re-used`, + ); expect(testSf.text).toContain(`[ɵINPUT_SIGNAL_BRAND_WRITE_TYPE]`); }); }); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__completion_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__completion_spec.ts index d628f4ef66dc1..f8674f2e34641 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__completion_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__completion_spec.ts @@ -44,9 +44,9 @@ runInEachFileSystem(() => { const members = `users: string[];`; // Embedded view in question is the first node in the template (index 0). const {completions} = setupCompletions(template, members, 0); - expect(new Set(completions.templateContext.keys())).toEqual(new Set([ - 'innerRef', 'user', 'topLevelRef' - ])); + expect(new Set(completions.templateContext.keys())).toEqual( + new Set(['innerRef', 'user', 'topLevelRef']), + ); }); it('should support shadowing between outer and inner templates ', () => { @@ -75,68 +75,77 @@ runInEachFileSystem(() => { describe('TemplateTypeChecker scopes', () => { it('should get directives and pipes in scope for a component', () => { const MAIN_TS = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName: MAIN_TS, - templates: { - 'SomeCmp': 'Not important', - }, - declarations: [ - { - type: 'directive', - file: MAIN_TS, - name: 'OtherDir', - selector: 'other-dir', + const {program, templateTypeChecker} = setup([ + { + fileName: MAIN_TS, + templates: { + 'SomeCmp': 'Not important', }, - { - type: 'pipe', - file: MAIN_TS, - name: 'OtherPipe', - pipeName: 'otherPipe', - } - ], - source: ` + declarations: [ + { + type: 'directive', + file: MAIN_TS, + name: 'OtherDir', + selector: 'other-dir', + }, + { + type: 'pipe', + file: MAIN_TS, + name: 'OtherPipe', + pipeName: 'otherPipe', + }, + ], + source: ` export class SomeCmp {} export class OtherDir {} export class OtherPipe {} export class SomeCmpModule {} - ` - }]); + `, + }, + ]); const sf = getSourceFileOrError(program, MAIN_TS); const SomeCmp = getClass(sf, 'SomeCmp'); let directives = templateTypeChecker.getPotentialTemplateDirectives(SomeCmp) ?? []; - directives = directives.filter(d => d.isInScope); + directives = directives.filter((d) => d.isInScope); const pipes = templateTypeChecker.getPotentialPipes(SomeCmp) ?? []; - expect(directives.map(dir => dir.selector)).toEqual(['other-dir']); - expect(pipes.map(pipe => pipe.name)).toEqual(['otherPipe']); + expect(directives.map((dir) => dir.selector)).toEqual(['other-dir']); + expect(pipes.map((pipe) => pipe.name)).toEqual(['otherPipe']); }); }); }); function setupCompletions( - template: string, componentMembers: string = '', inChildTemplateAtIndex: number|null = null): { - completions: GlobalCompletion, - program: ts.Program, - templateTypeChecker: TemplateTypeChecker, - component: ts.ClassDeclaration, + template: string, + componentMembers: string = '', + inChildTemplateAtIndex: number | null = null, +): { + completions: GlobalCompletion; + program: ts.Program; + templateTypeChecker: TemplateTypeChecker; + component: ts.ClassDeclaration; } { const MAIN_TS = absoluteFrom('/main.ts'); const {templateTypeChecker, programStrategy} = setup( - [{ + [ + { fileName: MAIN_TS, templates: {'SomeCmp': template}, source: `export class SomeCmp { ${componentMembers} }`, - }], - ({inlining: false, config: {enableTemplateTypeChecker: true}})); + }, + ], + {inlining: false, config: {enableTemplateTypeChecker: true}}, + ); const sf = getSourceFileOrError(programStrategy.getProgram(), MAIN_TS); const SomeCmp = getClass(sf, 'SomeCmp'); - let context: TmplAstTemplate|null = null; + let context: TmplAstTemplate | null = null; if (inChildTemplateAtIndex !== null) { const tmpl = templateTypeChecker.getTemplate(SomeCmp)![inChildTemplateAtIndex]; if (!(tmpl instanceof TmplAstTemplate)) { throw new Error( - `AssertionError: expected TmplAstTemplate at index ${inChildTemplateAtIndex}`); + `AssertionError: expected TmplAstTemplate at index ${inChildTemplateAtIndex}`, + ); } context = tmpl; } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts index 4d77fad4272dd..90084ce62d825 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts @@ -6,30 +6,63 @@ * found in the LICENSE file at https://angular.io/license */ -import {ASTWithSource, Binary, BindingPipe, Conditional, Interpolation, PropertyRead, TmplAstBoundAttribute, TmplAstBoundText, TmplAstElement, TmplAstForLoopBlock, TmplAstNode, TmplAstReference, TmplAstTemplate} from '@angular/compiler'; +import { + ASTWithSource, + Binary, + BindingPipe, + Conditional, + Interpolation, + PropertyRead, + TmplAstBoundAttribute, + TmplAstBoundText, + TmplAstElement, + TmplAstForLoopBlock, + TmplAstNode, + TmplAstReference, + TmplAstTemplate, +} from '@angular/compiler'; import {AST, LiteralArray, LiteralMap, TmplAstIfBlock} from '@angular/compiler/src/compiler'; import ts from 'typescript'; import {absoluteFrom, AbsoluteFsPath, getSourceFileOrError} from '../../file_system'; import {runInEachFileSystem} from '../../file_system/testing'; import {ClassDeclaration} from '../../reflection'; -import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, ExpressionSymbol, InputBindingSymbol, OutputBindingSymbol, PipeSymbol, ReferenceSymbol, Symbol, SymbolKind, TemplateSymbol, TemplateTypeChecker, TypeCheckingConfig, VariableSymbol} from '../api'; -import {getClass, ngForDeclaration, ngForTypeCheckTarget, setup as baseTestSetup, TypeCheckingTarget} from '../testing'; +import { + DirectiveSymbol, + DomBindingSymbol, + ElementSymbol, + ExpressionSymbol, + InputBindingSymbol, + OutputBindingSymbol, + PipeSymbol, + ReferenceSymbol, + Symbol, + SymbolKind, + TemplateSymbol, + TemplateTypeChecker, + TypeCheckingConfig, + VariableSymbol, +} from '../api'; +import { + getClass, + ngForDeclaration, + ngForTypeCheckTarget, + setup as baseTestSetup, + TypeCheckingTarget, +} from '../testing'; runInEachFileSystem(() => { describe('TemplateTypeChecker.getSymbolOfNode', () => { it('should get a symbol for regular attributes', () => { const fileName = absoluteFrom('/main.ts'); const templateString = `
`; - const {templateTypeChecker, program} = setup( - [ - { - fileName, - templates: {'Cmp': templateString}, - source: `export class Cmp {}`, - }, - ], - ); + const {templateTypeChecker, program} = setup([ + { + fileName, + templates: {'Cmp': templateString}, + source: `export class Cmp {}`, + }, + ]); const sf = getSourceFileOrError(program, fileName); const cmp = getClass(sf, 'Cmp'); const {attributes} = getAstElements(templateTypeChecker, cmp)[0]; @@ -50,19 +83,21 @@ runInEachFileSystem(() => { { fileName, templates: {'Cmp': templateString} as {[key: string]: string}, - declarations: [{ - name: 'NameDiv', - selector: 'div[name]', - file: dirFile, - type: 'directive' as const, - inputs: {name: 'name'}, - }] + declarations: [ + { + name: 'NameDiv', + selector: 'div[name]', + file: dirFile, + type: 'directive' as const, + inputs: {name: 'name'}, + }, + ], }, { fileName: dirFile, source: `export class NameDiv {name!: string;}`, templates: {}, - } + }, ]; }); @@ -73,13 +108,14 @@ runInEachFileSystem(() => { const {attributes} = getAstElements(templateTypeChecker, cmp)[0]; const symbol = templateTypeChecker.getSymbolOfNode(attributes[0], cmp)!; assertInputBindingSymbol(symbol); - expect((symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration) - .name.getText()) - .toEqual('name'); + expect( + (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ).toEqual('name'); // Ensure we can go back to the original location using the shim location - const mapping = - templateTypeChecker.getTemplateMappingAtTcbLocation(symbol.bindings[0].tcbLocation)!; + const mapping = templateTypeChecker.getTemplateMappingAtTcbLocation( + symbol.bindings[0].tcbLocation, + )!; expect(mapping.span.toString()).toEqual('name'); }); @@ -90,9 +126,9 @@ runInEachFileSystem(() => { const {attributes} = getAstElements(templateTypeChecker, cmp)[0]; const symbol = templateTypeChecker.getSymbolOfNode(attributes[0], cmp)!; assertInputBindingSymbol(symbol); - expect((symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration) - .name.getText()) - .toEqual('name'); + expect( + (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ).toEqual('name'); }); }); @@ -116,19 +152,21 @@ runInEachFileSystem(() => { templates: {'Cmp': templateString}, source: ` export class Cmp { }`, - declarations: [{ - name: 'TestDir', - selector: '[dir]', - file: dirFile, - type: 'directive', - exportAs: ['dir'], - }] + declarations: [ + { + name: 'TestDir', + selector: '[dir]', + file: dirFile, + type: 'directive', + exportAs: ['dir'], + }, + ], }, { fileName: dirFile, source: `export class TestDir {}`, templates: {}, - } + }, ]); templateTypeChecker = testValues.templateTypeChecker; program = testValues.program; @@ -146,17 +184,21 @@ runInEachFileSystem(() => { it('should get symbol for variables when used', () => { const symbol = templateTypeChecker.getSymbolOfNode( - (templateNode.children[0] as TmplAstTemplate).inputs[0].value, cmp)!; + (templateNode.children[0] as TmplAstTemplate).inputs[0].value, + cmp, + )!; assertVariableSymbol(symbol); expect(program.getTypeChecker().typeToString(symbol.tsType!)).toEqual('any'); expect(symbol.declaration.name).toEqual('contextFoo'); // Ensure we can map the shim locations back to the template - const initializerMapping = - templateTypeChecker.getTemplateMappingAtTcbLocation(symbol.initializerLocation)!; + const initializerMapping = templateTypeChecker.getTemplateMappingAtTcbLocation( + symbol.initializerLocation, + )!; expect(initializerMapping.span.toString()).toEqual('bar'); - const localVarMapping = - templateTypeChecker.getTemplateMappingAtTcbLocation(symbol.localVarLocation)!; + const localVarMapping = templateTypeChecker.getTemplateMappingAtTcbLocation( + symbol.localVarLocation, + )!; expect(localVarMapping.span.toString()).toEqual('contextFoo'); }); @@ -169,14 +211,17 @@ runInEachFileSystem(() => { it('should get a symbol for usage local ref which refers to a directive', () => { const symbol = templateTypeChecker.getSymbolOfNode( - (templateNode.children[0] as TmplAstTemplate).inputs[2].value, cmp)!; + (templateNode.children[0] as TmplAstTemplate).inputs[2].value, + cmp, + )!; assertReferenceSymbol(symbol); expect(program.getTypeChecker().symbolToString(symbol.tsSymbol)).toEqual('TestDir'); assertDirectiveReference(symbol); // Ensure we can map the var shim location back to the template - const localVarMapping = - templateTypeChecker.getTemplateMappingAtTcbLocation(symbol.referenceVarLocation); + const localVarMapping = templateTypeChecker.getTemplateMappingAtTcbLocation( + symbol.referenceVarLocation, + ); expect(localVarMapping!.span.toString()).toEqual('ref1'); }); @@ -194,7 +239,9 @@ runInEachFileSystem(() => { it('should get a symbol for usage local ref which refers to a template', () => { const symbol = templateTypeChecker.getSymbolOfNode( - (templateNode.children[0] as TmplAstTemplate).inputs[1].value, cmp)!; + (templateNode.children[0] as TmplAstTemplate).inputs[1].value, + cmp, + )!; assertReferenceSymbol(symbol); assertTemplateReference(symbol); }); @@ -246,7 +293,7 @@ runInEachFileSystem(() => { selector: '[dir]', file: dirFile, type: 'directive', - inputs: {name: 'name'} + inputs: {name: 'name'}, }, ], }, @@ -266,77 +313,83 @@ runInEachFileSystem(() => { it('should retrieve a symbol for a directive on a microsyntax template', () => { const symbol = templateTypeChecker.getSymbolOfNode(templateNode, cmp); - const testDir = symbol?.directives.find(dir => dir.selector === '[dir]'); + const testDir = symbol?.directives.find((dir) => dir.selector === '[dir]'); expect(testDir).toBeDefined(); expect(program.getTypeChecker().symbolToString(testDir!.tsSymbol)).toEqual('TestDir'); }); it('should retrieve a symbol for an expression inside structural binding', () => { - const ngForOfBinding = - templateNode.templateAttrs.find(a => a.name === 'ngForOf')! as TmplAstBoundAttribute; + const ngForOfBinding = templateNode.templateAttrs.find( + (a) => a.name === 'ngForOf', + )! as TmplAstBoundAttribute; const symbol = templateTypeChecker.getSymbolOfNode(ngForOfBinding.value, cmp)!; assertExpressionSymbol(symbol); expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('users'); expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual('Array'); }); - it('should retrieve a symbol for property reads of implicit variable inside structural binding', - () => { - const boundText = - (templateNode.children[0] as TmplAstElement).children[0] as TmplAstBoundText; - const interpolation = (boundText.value as ASTWithSource).ast as Interpolation; - const namePropRead = interpolation.expressions[0] as PropertyRead; - const streetNumberPropRead = interpolation.expressions[1] as PropertyRead; - - const nameSymbol = templateTypeChecker.getSymbolOfNode(namePropRead, cmp)!; - assertExpressionSymbol(nameSymbol); - expect(program.getTypeChecker().symbolToString(nameSymbol.tsSymbol!)).toEqual('name'); - expect(program.getTypeChecker().typeToString(nameSymbol.tsType)).toEqual('string'); - - const streetSymbol = templateTypeChecker.getSymbolOfNode(streetNumberPropRead, cmp)!; - assertExpressionSymbol(streetSymbol); - expect(program.getTypeChecker().symbolToString(streetSymbol.tsSymbol!)) - .toEqual('streetNumber'); - expect(program.getTypeChecker().typeToString(streetSymbol.tsType)).toEqual('number'); - - const userSymbol = templateTypeChecker.getSymbolOfNode(namePropRead.receiver, cmp)!; - expectUserSymbol(userSymbol); - }); + it('should retrieve a symbol for property reads of implicit variable inside structural binding', () => { + const boundText = (templateNode.children[0] as TmplAstElement) + .children[0] as TmplAstBoundText; + const interpolation = (boundText.value as ASTWithSource).ast as Interpolation; + const namePropRead = interpolation.expressions[0] as PropertyRead; + const streetNumberPropRead = interpolation.expressions[1] as PropertyRead; + + const nameSymbol = templateTypeChecker.getSymbolOfNode(namePropRead, cmp)!; + assertExpressionSymbol(nameSymbol); + expect(program.getTypeChecker().symbolToString(nameSymbol.tsSymbol!)).toEqual('name'); + expect(program.getTypeChecker().typeToString(nameSymbol.tsType)).toEqual('string'); + + const streetSymbol = templateTypeChecker.getSymbolOfNode(streetNumberPropRead, cmp)!; + assertExpressionSymbol(streetSymbol); + expect(program.getTypeChecker().symbolToString(streetSymbol.tsSymbol!)).toEqual( + 'streetNumber', + ); + expect(program.getTypeChecker().typeToString(streetSymbol.tsType)).toEqual('number'); + + const userSymbol = templateTypeChecker.getSymbolOfNode(namePropRead.receiver, cmp)!; + expectUserSymbol(userSymbol); + }); it('finds symbols for variables', () => { - const userVar = templateNode.variables.find(v => v.name === 'user')!; + const userVar = templateNode.variables.find((v) => v.name === 'user')!; const userSymbol = templateTypeChecker.getSymbolOfNode(userVar, cmp)!; expectUserSymbol(userSymbol); - const iVar = templateNode.variables.find(v => v.name === 'i')!; + const iVar = templateNode.variables.find((v) => v.name === 'i')!; const iSymbol = templateTypeChecker.getSymbolOfNode(iVar, cmp)!; expectIndexSymbol(iSymbol); }); it('finds symbol when using a template variable', () => { - const innerElementNodes = - onlyAstElements((templateNode.children[0] as TmplAstElement).children); - const indexSymbol = - templateTypeChecker.getSymbolOfNode(innerElementNodes[0].inputs[0].value, cmp)!; + const innerElementNodes = onlyAstElements( + (templateNode.children[0] as TmplAstElement).children, + ); + const indexSymbol = templateTypeChecker.getSymbolOfNode( + innerElementNodes[0].inputs[0].value, + cmp, + )!; expectIndexSymbol(indexSymbol); }); function expectUserSymbol(userSymbol: Symbol) { assertVariableSymbol(userSymbol); expect(userSymbol.tsSymbol!.escapedName).toContain('$implicit'); - expect(userSymbol.tsSymbol!.declarations![0].parent!.getText()) - .toContain('NgForOfContext'); + expect(userSymbol.tsSymbol!.declarations![0].parent!.getText()).toContain( + 'NgForOfContext', + ); expect(program.getTypeChecker().typeToString(userSymbol.tsType!)).toEqual('User'); - expect((userSymbol).declaration).toEqual(templateNode.variables[0]); + expect(userSymbol.declaration).toEqual(templateNode.variables[0]); } function expectIndexSymbol(indexSymbol: Symbol) { assertVariableSymbol(indexSymbol); expect(indexSymbol.tsSymbol!.escapedName).toContain('index'); - expect(indexSymbol.tsSymbol!.declarations![0].parent!.getText()) - .toContain('NgForOfContext'); + expect(indexSymbol.tsSymbol!.declarations![0].parent!.getText()).toContain( + 'NgForOfContext', + ); expect(program.getTypeChecker().typeToString(indexSymbol.tsType!)).toEqual('number'); - expect((indexSymbol).declaration).toEqual(templateNode.variables[1]); + expect(indexSymbol.declaration).toEqual(templateNode.variables[1]); } }); @@ -372,25 +425,29 @@ runInEachFileSystem(() => { ifBlockNode = templateTypeChecker.getTemplate(cmp)![0] as unknown as TmplAstIfBlock; }); - it('should retrieve a symbol for the loop expression', () => { - const symbol = - templateTypeChecker.getSymbolOfNode(ifBlockNode.branches[0].expression!, cmp)!; + const symbol = templateTypeChecker.getSymbolOfNode( + ifBlockNode.branches[0].expression!, + cmp, + )!; assertExpressionSymbol(symbol); expectUserSymbol(symbol); }); it('should retrieve a symbol for the track expression', () => { - const symbol = - templateTypeChecker.getSymbolOfNode(ifBlockNode.branches[0].expressionAlias!, cmp)!; + const symbol = templateTypeChecker.getSymbolOfNode( + ifBlockNode.branches[0].expressionAlias!, + cmp, + )!; assertVariableSymbol(symbol); expectUserSymbol(symbol); }); - function expectUserSymbol(userSymbol: VariableSymbol|ExpressionSymbol) { + function expectUserSymbol(userSymbol: VariableSymbol | ExpressionSymbol) { expect(userSymbol.tsSymbol!.escapedName).toContain('user'); - expect(program.getTypeChecker().typeToString(userSymbol.tsType!)) - .toEqual('User | undefined'); + expect(program.getTypeChecker().typeToString(userSymbol.tsType!)).toEqual( + 'User | undefined', + ); } }); @@ -435,7 +492,6 @@ runInEachFileSystem(() => { forLoopNode = templateTypeChecker.getTemplate(cmp)![0] as unknown as TmplAstForLoopBlock; }); - it('should retrieve a symbol for the loop expression', () => { const symbol = templateTypeChecker.getSymbolOfNode(forLoopNode.expression.ast, cmp)!; assertExpressionSymbol(symbol); @@ -449,8 +505,8 @@ runInEachFileSystem(() => { }); it('should retrieve a symbol for property reads of the loop variable', () => { - const boundText = - (forLoopNode.children[0] as TmplAstElement).children[0] as TmplAstBoundText; + const boundText = (forLoopNode.children[0] as TmplAstElement) + .children[0] as TmplAstBoundText; const interpolation = (boundText.value as ASTWithSource).ast as Interpolation; const namePropRead = interpolation.expressions[0] as PropertyRead; const streetNumberPropRead = interpolation.expressions[1] as PropertyRead; @@ -462,8 +518,9 @@ runInEachFileSystem(() => { const streetSymbol = templateTypeChecker.getSymbolOfNode(streetNumberPropRead, cmp)!; assertExpressionSymbol(streetSymbol); - expect(program.getTypeChecker().symbolToString(streetSymbol.tsSymbol!)) - .toEqual('streetNumber'); + expect(program.getTypeChecker().symbolToString(streetSymbol.tsSymbol!)).toEqual( + 'streetNumber', + ); expect(program.getTypeChecker().typeToString(streetSymbol.tsType)).toEqual('number'); const userSymbol = templateTypeChecker.getSymbolOfNode(namePropRead.receiver, cmp)!; @@ -477,17 +534,20 @@ runInEachFileSystem(() => { }); it('finds symbols for $index variable', () => { - const iVar = forLoopNode.contextVariables.find(v => v.name === '$index')!; + const iVar = forLoopNode.contextVariables.find((v) => v.name === '$index')!; const iSymbol = templateTypeChecker.getSymbolOfNode(iVar, cmp)!; expect(iVar).toBeTruthy(); expectIndexSymbol(iSymbol, '$index'); }); it('finds symbol when using the index in the body', () => { - const innerElementNodes = - onlyAstElements((forLoopNode.children[0] as TmplAstElement).children); - const indexSymbol = - templateTypeChecker.getSymbolOfNode(innerElementNodes[0].inputs[0].value, cmp)!; + const innerElementNodes = onlyAstElements( + (forLoopNode.children[0] as TmplAstElement).children, + ); + const indexSymbol = templateTypeChecker.getSymbolOfNode( + innerElementNodes[0].inputs[0].value, + cmp, + )!; expectIndexSymbol(indexSymbol, 'i'); }); @@ -495,18 +555,18 @@ runInEachFileSystem(() => { assertVariableSymbol(userSymbol); expect(userSymbol.tsSymbol!.escapedName).toContain('User'); expect(program.getTypeChecker().typeToString(userSymbol.tsType!)).toEqual('User'); - expect((userSymbol).declaration).toEqual(forLoopNode.item); + expect(userSymbol.declaration).toEqual(forLoopNode.item); } function expectIndexSymbol(indexSymbol: Symbol, localName: string) { - const indexVar = - forLoopNode.contextVariables.find(v => v.value === '$index' && v.name === localName)!; + const indexVar = forLoopNode.contextVariables.find( + (v) => v.value === '$index' && v.name === localName, + )!; assertVariableSymbol(indexSymbol); expect(indexVar).toBeTruthy(); - expect(indexSymbol.tsSymbol) - .toBeNull(); // implicit variable doesn't have a TS definition location + expect(indexSymbol.tsSymbol).toBeNull(); // implicit variable doesn't have a TS definition location expect(program.getTypeChecker().typeToString(indexSymbol.tsType!)).toEqual('number'); - expect((indexSymbol).declaration).toEqual(indexVar); + expect(indexSymbol.declaration).toEqual(indexVar); } }); }); @@ -529,8 +589,9 @@ runInEachFileSystem(() => { const symbol = templateTypeChecker.getSymbolOfNode(nodes[0].inputs[0].value, cmp)!; assertExpressionSymbol(symbol); expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('helloWorld'); - expect(program.getTypeChecker().typeToString(symbol.tsType)) - .toEqual('false | true | undefined'); + expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual( + 'false | true | undefined', + ); }); it('should get a symbol for properties several levels deep', () => { @@ -561,16 +622,20 @@ runInEachFileSystem(() => { const symbol = templateTypeChecker.getSymbolOfNode(inputNode, cmp)!; assertExpressionSymbol(symbol); expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('street'); - expect((symbol.tsSymbol!.declarations![0] as ts.PropertyDeclaration).parent.name!.getText()) - .toEqual('Address'); + expect( + (symbol.tsSymbol!.declarations![0] as ts.PropertyDeclaration).parent.name!.getText(), + ).toEqual('Address'); expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual('string'); const personSymbol = templateTypeChecker.getSymbolOfNode( - ((inputNode.ast as PropertyRead).receiver as PropertyRead).receiver, cmp)!; + ((inputNode.ast as PropertyRead).receiver as PropertyRead).receiver, + cmp, + )!; assertExpressionSymbol(personSymbol); expect(program.getTypeChecker().symbolToString(personSymbol.tsSymbol!)).toEqual('person'); - expect(program.getTypeChecker().typeToString(personSymbol.tsType)) - .toEqual('Person | undefined'); + expect(program.getTypeChecker().typeToString(personSymbol.tsType)).toEqual( + 'Person | undefined', + ); }); describe('should get symbols for conditionals', () => { @@ -587,12 +652,11 @@ runInEachFileSystem(() => {
`; - const testValues = setup( - [ - { - fileName, - templates: {'Cmp': templateString}, - source: ` + const testValues = setup([ + { + fileName, + templates: {'Cmp': templateString}, + source: ` interface Address { street: string; } @@ -608,9 +672,8 @@ runInEachFileSystem(() => { } export class Cmp {person?: Person; noPersonError = 'no person'} `, - }, - ], - ); + }, + ]); templateTypeChecker = testValues.templateTypeChecker; program = testValues.program; const sf = getSourceFileOrError(program, fileName); @@ -622,13 +685,17 @@ runInEachFileSystem(() => { const safePropertyRead = nodes[0].inputs[0].value as ASTWithSource; const propReadSymbol = templateTypeChecker.getSymbolOfNode(safePropertyRead, cmp)!; assertExpressionSymbol(propReadSymbol); - expect(program.getTypeChecker().symbolToString(propReadSymbol.tsSymbol!)) - .toEqual('street'); - expect((propReadSymbol.tsSymbol!.declarations![0] as ts.PropertyDeclaration) - .parent.name!.getText()) - .toEqual('Address'); - expect(program.getTypeChecker().typeToString(propReadSymbol.tsType)) - .toEqual('string | undefined'); + expect(program.getTypeChecker().symbolToString(propReadSymbol.tsSymbol!)).toEqual( + 'street', + ); + expect( + ( + propReadSymbol.tsSymbol!.declarations![0] as ts.PropertyDeclaration + ).parent.name!.getText(), + ).toEqual('Address'); + expect(program.getTypeChecker().typeToString(propReadSymbol.tsType)).toEqual( + 'string | undefined', + ); }); it('safe method calls', () => { @@ -638,8 +705,9 @@ runInEachFileSystem(() => { assertExpressionSymbol(methodCallSymbol); // Note that the symbol returned is for the return value of the safe method call. expect(methodCallSymbol.tsSymbol).toBeNull(); - expect(program.getTypeChecker().typeToString(methodCallSymbol.tsType)) - .toBe('string | undefined'); + expect(program.getTypeChecker().typeToString(methodCallSymbol.tsType)).toBe( + 'string | undefined', + ); }); it('safe keyed reads', () => { @@ -647,11 +715,14 @@ runInEachFileSystem(() => { const safeKeyedRead = nodes[3].inputs[0].value as ASTWithSource; const keyedReadSymbol = templateTypeChecker.getSymbolOfNode(safeKeyedRead, cmp)!; assertExpressionSymbol(keyedReadSymbol); - expect(program.getTypeChecker().symbolToString(keyedReadSymbol.tsSymbol!)) - .toEqual('engine'); - expect((keyedReadSymbol.tsSymbol!.declarations![0] as ts.PropertyDeclaration) - .parent.name!.getText()) - .toEqual('Car'); + expect(program.getTypeChecker().symbolToString(keyedReadSymbol.tsSymbol!)).toEqual( + 'engine', + ); + expect( + ( + keyedReadSymbol.tsSymbol!.declarations![0] as ts.PropertyDeclaration + ).parent.name!.getText(), + ).toEqual('Car'); expect(program.getTypeChecker().typeToString(keyedReadSymbol.tsType)).toEqual('string'); }); @@ -662,8 +733,9 @@ runInEachFileSystem(() => { const ternarySymbol = templateTypeChecker.getSymbolOfNode(ternary, cmp)!; assertExpressionSymbol(ternarySymbol); expect(ternarySymbol.tsSymbol).toBeNull(); - expect(program.getTypeChecker().typeToString(ternarySymbol.tsType)) - .toEqual('string | Address'); + expect(program.getTypeChecker().typeToString(ternarySymbol.tsType)).toEqual( + 'string | Address', + ); const addrSymbol = templateTypeChecker.getSymbolOfNode(ternary.trueExp, cmp)!; assertExpressionSymbol(addrSymbol); expect(program.getTypeChecker().symbolToString(addrSymbol.tsSymbol!)).toEqual('address'); @@ -671,8 +743,9 @@ runInEachFileSystem(() => { const noPersonSymbol = templateTypeChecker.getSymbolOfNode(ternary.falseExp, cmp)!; assertExpressionSymbol(noPersonSymbol); - expect(program.getTypeChecker().symbolToString(noPersonSymbol.tsSymbol!)) - .toEqual('noPersonError'); + expect(program.getTypeChecker().symbolToString(noPersonSymbol.tsSymbol!)).toEqual( + 'noPersonError', + ); expect(program.getTypeChecker().typeToString(noPersonSymbol.tsType)).toEqual('string'); }); }); @@ -724,14 +797,18 @@ runInEachFileSystem(() => { expect(wholeExprSymbol.tsSymbol).toBeNull(); expect(program.getTypeChecker().typeToString(wholeExprSymbol.tsType)).toEqual('string'); - const aSymbol = - templateTypeChecker.getSymbolOfNode((valueAssignment.ast as Binary).left, cmp)!; + const aSymbol = templateTypeChecker.getSymbolOfNode( + (valueAssignment.ast as Binary).left, + cmp, + )!; assertExpressionSymbol(aSymbol); expect(program.getTypeChecker().symbolToString(aSymbol.tsSymbol!)).toBe('a'); expect(program.getTypeChecker().typeToString(aSymbol.tsType)).toEqual('string'); - const bSymbol = - templateTypeChecker.getSymbolOfNode((valueAssignment.ast as Binary).right, cmp)!; + const bSymbol = templateTypeChecker.getSymbolOfNode( + (valueAssignment.ast as Binary).right, + cmp, + )!; assertExpressionSymbol(bSymbol); expect(program.getTypeChecker().symbolToString(bSymbol.tsSymbol!)).toBe('b'); expect(program.getTypeChecker().typeToString(bSymbol.tsType)).toEqual('number'); @@ -746,7 +823,7 @@ runInEachFileSystem(() => { templates: { 'Cmp': ` -
` +
`, }, }, ]); @@ -768,17 +845,18 @@ runInEachFileSystem(() => { it('checkTypeOfDomReferences = false', () => { const fileName = absoluteFrom('/main.ts'); const {templateTypeChecker, program} = setup( - [ - { - fileName, - templates: { - 'Cmp': ` + [ + { + fileName, + templates: { + 'Cmp': ` -
` - }, +
`, }, - ], - {checkTypeOfDomReferences: false}); + }, + ], + {checkTypeOfDomReferences: false}, + ); const sf = getSourceFileOrError(program, fileName); const cmp = getClass(sf, 'Cmp'); const nodes = getAstElements(templateTypeChecker, cmp); @@ -802,19 +880,21 @@ runInEachFileSystem(() => { { fileName, templates: {'Cmp': templateString}, - declarations: [{ - name: 'TestDir', - selector: '[dir]', - file: dirFile, - type: 'directive', - exportAs: ['dir'], - }] + declarations: [ + { + name: 'TestDir', + selector: '[dir]', + file: dirFile, + type: 'directive', + exportAs: ['dir'], + }, + ], }, { fileName: dirFile, source: `export class TestDir { dirValue = 'helloWorld' }`, - templates: {} - } + templates: {}, + }, ]); const sf = getSourceFileOrError(program, fileName); const cmp = getClass(sf, 'Cmp'); @@ -872,9 +952,9 @@ runInEachFileSystem(() => { program = testValues.program; const sf = getSourceFileOrError(testValues.program, fileName); cmp = getClass(sf, 'Cmp'); - interpolation = ((templateTypeChecker.getTemplate(cmp)![0] as TmplAstBoundText).value as - ASTWithSource) - .ast as Interpolation; + interpolation = ( + (templateTypeChecker.getTemplate(cmp)![0] as TmplAstBoundText).value as ASTWithSource + ).ast as Interpolation; }); it('literal array', () => { @@ -890,13 +970,14 @@ runInEachFileSystem(() => { const symbol = templateTypeChecker.getSymbolOfNode(literalMap, cmp)!; assertExpressionSymbol(symbol); expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('__object'); - expect(program.getTypeChecker().typeToString(symbol.tsType)) - .toEqual('{ hello: string; }'); + expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual( + '{ hello: string; }', + ); }); it('literal map shorthand property', () => { - const shorthandProp = - (interpolation.expressions[2] as LiteralMap).values[0] as PropertyRead; + const shorthandProp = (interpolation.expressions[2] as LiteralMap) + .values[0] as PropertyRead; const symbol = templateTypeChecker.getSymbolOfNode(shorthandProp, cmp)!; assertExpressionSymbol(symbol); expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('foo'); @@ -914,32 +995,34 @@ runInEachFileSystem(() => { const fileName = absoluteFrom('/main.ts'); const templateString = `
`; const testValues = setup( - [ - { - fileName, - templates: {'Cmp': templateString}, - source: ` + [ + { + fileName, + templates: {'Cmp': templateString}, + source: ` export class Cmp { a: string; b: number; c: boolean } export class TestPipe { transform(value: string, repeat: number, commaSeparate: boolean): string[] { } } `, - declarations: [{ + declarations: [ + { type: 'pipe', name: 'TestPipe', pipeName: 'test', - }], - }, - ], - {checkTypeOfPipes}); + }, + ], + }, + ], + {checkTypeOfPipes}, + ); program = testValues.program; templateTypeChecker = testValues.templateTypeChecker; const sf = getSourceFileOrError(testValues.program, fileName); cmp = getClass(sf, 'Cmp'); - binding = - (getAstElements(templateTypeChecker, cmp)[0].inputs[0].value as ASTWithSource).ast as - BindingPipe; + binding = (getAstElements(templateTypeChecker, cmp)[0].inputs[0].value as ASTWithSource) + .ast as BindingPipe; } for (const checkTypeOfPipes of [true, false]) { @@ -947,12 +1030,15 @@ runInEachFileSystem(() => { setupPipesTest(checkTypeOfPipes); const pipeSymbol = templateTypeChecker.getSymbolOfNode(binding, cmp)!; assertPipeSymbol(pipeSymbol); - expect(program.getTypeChecker().symbolToString(pipeSymbol.tsSymbol!)) - .toEqual('transform'); - expect(program.getTypeChecker().symbolToString(pipeSymbol.classSymbol.tsSymbol)) - .toEqual('TestPipe'); - expect(program.getTypeChecker().typeToString(pipeSymbol.tsType!)) - .toEqual('(value: string, repeat: number, commaSeparate: boolean) => string[]'); + expect(program.getTypeChecker().symbolToString(pipeSymbol.tsSymbol!)).toEqual( + 'transform', + ); + expect( + program.getTypeChecker().symbolToString(pipeSymbol.classSymbol.tsSymbol), + ).toEqual('TestPipe'); + expect(program.getTypeChecker().typeToString(pipeSymbol.tsType!)).toEqual( + '(value: string, repeat: number, commaSeparate: boolean) => string[]', + ); }); } @@ -1004,7 +1090,7 @@ runInEachFileSystem(() => { { fileName, templates: {'Cmp': '
'}, - source: `export class Cmp { lastEvent: any; }` + source: `export class Cmp { lastEvent: any; }`, }, ]); const sf = getSourceFileOrError(program, fileName); @@ -1027,7 +1113,7 @@ runInEachFileSystem(() => { { fileName, templates: {'Cmp': '
'}, - source: `export class Cmp { toString(v: any): string { return String(v); } }` + source: `export class Cmp { toString(v: any): string { return String(v); } }`, }, ]); const sf = getSourceFileOrError(program, fileName); @@ -1046,7 +1132,7 @@ runInEachFileSystem(() => { { fileName, templates: {'Cmp': '
'}, - source: `export class Cmp { toString?: (value: number) => string; }` + source: `export class Cmp { toString?: (value: number) => string; }`, }, ]); const sf = getSourceFileOrError(program, fileName); @@ -1056,8 +1142,9 @@ runInEachFileSystem(() => { assertExpressionSymbol(safeCallSymbol); // Note that the symbol returned is for the return value of the SafeCall. expect(safeCallSymbol.tsSymbol).toBeNull(); - expect(program.getTypeChecker().typeToString(safeCallSymbol.tsType)) - .toBe('string | undefined'); + expect(program.getTypeChecker().typeToString(safeCallSymbol.tsType)).toBe( + 'string | undefined', + ); }); }); @@ -1069,19 +1156,21 @@ runInEachFileSystem(() => { { fileName, templates: {'Cmp': `
`}, - declarations: [{ - name: 'TestDir', - selector: '[dir]', - file: dirFile, - type: 'directive', - inputs: {inputA: 'inputA'}, - }] + declarations: [ + { + name: 'TestDir', + selector: '[dir]', + file: dirFile, + type: 'directive', + inputs: {inputA: 'inputA'}, + }, + ], }, { fileName: dirFile, source: `export class TestDir {inputA?: string; }`, templates: {}, - } + }, ]); const sf = getSourceFileOrError(program, fileName); const cmp = getClass(sf, 'Cmp'); @@ -1091,33 +1180,34 @@ runInEachFileSystem(() => { const inputAbinding = (nodes[0] as TmplAstElement).inputs[0]; const aSymbol = templateTypeChecker.getSymbolOfNode(inputAbinding, cmp)!; assertInputBindingSymbol(aSymbol); - expect((aSymbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration) - .name.getText()) - .toEqual('inputA'); + expect( + (aSymbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ).toEqual('inputA'); }); it('can retrieve a symbol for an input binding', () => { const fileName = absoluteFrom('/main.ts'); const dirFile = absoluteFrom('/dir.ts'); - const templateString = - `
`; + const templateString = `
`; const {program, templateTypeChecker} = setup([ { fileName, templates: {'Cmp': templateString}, - declarations: [{ - name: 'TestDir', - selector: '[dir]', - file: dirFile, - type: 'directive', - inputs: {inputA: 'inputA', inputB: 'inputBRenamed'}, - }] + declarations: [ + { + name: 'TestDir', + selector: '[dir]', + file: dirFile, + type: 'directive', + inputs: {inputA: 'inputA', inputB: 'inputBRenamed'}, + }, + ], }, { fileName: dirFile, source: `export class TestDir {inputA!: string; inputB!: string}`, templates: {}, - } + }, ]); const sf = getSourceFileOrError(program, fileName); const cmp = getClass(sf, 'Cmp'); @@ -1127,16 +1217,16 @@ runInEachFileSystem(() => { const inputAbinding = (nodes[0] as TmplAstElement).inputs[0]; const aSymbol = templateTypeChecker.getSymbolOfNode(inputAbinding, cmp)!; assertInputBindingSymbol(aSymbol); - expect((aSymbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration) - .name.getText()) - .toEqual('inputA'); + expect( + (aSymbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ).toEqual('inputA'); const inputBbinding = (nodes[0] as TmplAstElement).inputs[1]; const bSymbol = templateTypeChecker.getSymbolOfNode(inputBbinding, cmp)!; assertInputBindingSymbol(bSymbol); - expect((bSymbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration) - .name.getText()) - .toEqual('inputB'); + expect( + (bSymbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ).toEqual('inputB'); }); it('can retrieve a symbol for a signal-input binding', () => { @@ -1147,28 +1237,30 @@ runInEachFileSystem(() => { { fileName, templates: {'Cmp': templateString}, - declarations: [{ - name: 'TestDir', - selector: '[dir]', - file: dirFile, - type: 'directive', - inputs: { - inputA: { - bindingPropertyName: 'inputA', - isSignal: true, - classPropertyName: 'inputA', - required: false, - transform: null, + declarations: [ + { + name: 'TestDir', + selector: '[dir]', + file: dirFile, + type: 'directive', + inputs: { + inputA: { + bindingPropertyName: 'inputA', + isSignal: true, + classPropertyName: 'inputA', + required: false, + transform: null, + }, + inputB: { + bindingPropertyName: 'aliased', + isSignal: true, + classPropertyName: 'inputB', + required: true, + transform: null, + }, }, - inputB: { - bindingPropertyName: 'aliased', - isSignal: true, - classPropertyName: 'inputB', - required: true, - transform: null, - } }, - }] + ], }, { fileName: dirFile, @@ -1180,7 +1272,7 @@ runInEachFileSystem(() => { inputB: InputSignal = null!; }`, templates: {}, - } + }, ]); const sf = getSourceFileOrError(program, fileName); const cmp = getClass(sf, 'Cmp'); @@ -1190,16 +1282,16 @@ runInEachFileSystem(() => { const inputAbinding = (nodes[0] as TmplAstElement).inputs[0]; const aSymbol = templateTypeChecker.getSymbolOfNode(inputAbinding, cmp)!; assertInputBindingSymbol(aSymbol); - expect((aSymbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration) - .name.getText()) - .toEqual('inputA'); + expect( + (aSymbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ).toEqual('inputA'); const inputBbinding = (nodes[0] as TmplAstElement).inputs[1]; const bSymbol = templateTypeChecker.getSymbolOfNode(inputBbinding, cmp)!; assertInputBindingSymbol(bSymbol); - expect((bSymbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration) - .name.getText()) - .toEqual('inputB'); + expect( + (bSymbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ).toEqual('inputB'); }); it('does not retrieve a symbol for an input when undeclared', () => { @@ -1210,19 +1302,21 @@ runInEachFileSystem(() => { { fileName, templates: {'Cmp': templateString}, - declarations: [{ - name: 'TestDir', - selector: '[dir]', - file: dirFile, - type: 'directive', - inputs: {inputA: 'inputA'}, - }] + declarations: [ + { + name: 'TestDir', + selector: '[dir]', + file: dirFile, + type: 'directive', + inputs: {inputA: 'inputA'}, + }, + ], }, { fileName: dirFile, source: `export class TestDir {}`, templates: {}, - } + }, ]); const sf = getSourceFileOrError(program, fileName); const cmp = getClass(sf, 'Cmp'); @@ -1246,14 +1340,14 @@ runInEachFileSystem(() => { const nodes = templateTypeChecker.getTemplate(cmp)!; - const ngForOfBinding = - (nodes[0] as TmplAstTemplate).templateAttrs.find(a => a.name === 'ngForOf')! as - TmplAstBoundAttribute; + const ngForOfBinding = (nodes[0] as TmplAstTemplate).templateAttrs.find( + (a) => a.name === 'ngForOf', + )! as TmplAstBoundAttribute; const symbol = templateTypeChecker.getSymbolOfNode(ngForOfBinding, cmp)!; assertInputBindingSymbol(symbol); - expect((symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration) - .name.getText()) - .toEqual('ngForOf'); + expect( + (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ).toEqual('ngForOf'); }); it('returns dom binding input binds only to the dom element', () => { @@ -1281,19 +1375,21 @@ runInEachFileSystem(() => { { fileName, templates: {'Cmp': templateString}, - declarations: [{ - name: 'TestDir', - selector: '[dir]', - file: dirFile, - type: 'directive', - inputs: {}, - }] + declarations: [ + { + name: 'TestDir', + selector: '[dir]', + file: dirFile, + type: 'directive', + inputs: {}, + }, + ], }, { fileName: dirFile, source: `export class TestDir {}`, templates: {}, - } + }, ]); const sf = getSourceFileOrError(program, fileName); const cmp = getClass(sf, 'Cmp'); @@ -1328,8 +1424,8 @@ runInEachFileSystem(() => { file: dirFile, type: 'directive', inputs: {}, - } - ] + }, + ], }, { fileName: dirFile, @@ -1338,7 +1434,7 @@ runInEachFileSystem(() => { export class OtherDir {} `, templates: {}, - } + }, ]); const sf = getSourceFileOrError(program, fileName); const cmp = getClass(sf, 'Cmp'); @@ -1348,12 +1444,13 @@ runInEachFileSystem(() => { const inputAbinding = (nodes[0] as TmplAstElement).inputs[0]; const symbol = templateTypeChecker.getSymbolOfNode(inputAbinding, cmp)!; assertInputBindingSymbol(symbol); - expect((symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration) - .name.getText()) - .toEqual('inputA'); - expect((symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration) - .parent.name?.text) - .toEqual('TestDir'); + expect( + (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ).toEqual('inputA'); + expect( + (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).parent.name + ?.text, + ).toEqual('TestDir'); }); it('returns the first field match when directive maps same input to two fields', () => { @@ -1371,7 +1468,7 @@ runInEachFileSystem(() => { type: 'directive', inputs: {inputA: 'inputA', otherInputA: 'inputA'}, }, - ] + ], }, { fileName: dirFile, @@ -1379,7 +1476,7 @@ runInEachFileSystem(() => { export class TestDir {inputA!: string; otherInputA!: string;} `, templates: {}, - } + }, ]); const sf = getSourceFileOrError(program, fileName); const cmp = getClass(sf, 'Cmp'); @@ -1389,12 +1486,13 @@ runInEachFileSystem(() => { const inputAbinding = (nodes[0] as TmplAstElement).inputs[0]; const symbol = templateTypeChecker.getSymbolOfNode(inputAbinding, cmp)!; assertInputBindingSymbol(symbol); - expect((symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration) - .name.getText()) - .toEqual('otherInputA'); - expect((symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration) - .parent.name?.text) - .toEqual('TestDir'); + expect( + (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ).toEqual('otherInputA'); + expect( + (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).parent.name + ?.text, + ).toEqual('TestDir'); }); it('returns the all inputs when two directives have the same input', () => { @@ -1419,8 +1517,8 @@ runInEachFileSystem(() => { file: dirFile, type: 'directive', inputs: {otherDirInputA: 'inputA'}, - } - ] + }, + ], }, { fileName: dirFile, @@ -1429,7 +1527,7 @@ runInEachFileSystem(() => { export class OtherDir {otherDirInputA!: string;} `, templates: {}, - } + }, ]); const sf = getSourceFileOrError(program, fileName); const cmp = getClass(sf, 'Cmp'); @@ -1439,13 +1537,20 @@ runInEachFileSystem(() => { const inputAbinding = (nodes[0] as TmplAstElement).inputs[0]; const symbol = templateTypeChecker.getSymbolOfNode(inputAbinding, cmp)!; assertInputBindingSymbol(symbol); - expect(new Set(symbol.bindings.map( - b => (b.tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText()))) - .toEqual(new Set(['inputA', 'otherDirInputA'])); expect( - new Set(symbol.bindings.map( - b => (b.tsSymbol!.declarations![0] as ts.PropertyDeclaration).parent.name?.text))) - .toEqual(new Set(['TestDir', 'OtherDir'])); + new Set( + symbol.bindings.map((b) => + (b.tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ), + ), + ).toEqual(new Set(['inputA', 'otherDirInputA'])); + expect( + new Set( + symbol.bindings.map( + (b) => (b.tsSymbol!.declarations![0] as ts.PropertyDeclaration).parent.name?.text, + ), + ), + ).toEqual(new Set(['TestDir', 'OtherDir'])); }); }); @@ -1453,8 +1558,7 @@ runInEachFileSystem(() => { it('should find symbol for output binding', () => { const fileName = absoluteFrom('/main.ts'); const dirFile = absoluteFrom('/dir.ts'); - const templateString = - `
`; + const templateString = `
`; const {program, templateTypeChecker} = setup([ { fileName, @@ -1467,7 +1571,7 @@ runInEachFileSystem(() => { type: 'directive', outputs: {outputA: 'outputA', outputB: 'renamedOutputB'}, }, - ] + ], }, { fileName: dirFile, @@ -1475,7 +1579,7 @@ runInEachFileSystem(() => { export class TestDir {outputA!: EventEmitter; outputB!: EventEmitter} `, templates: {}, - } + }, ]); const sf = getSourceFileOrError(program, fileName); const cmp = getClass(sf, 'Cmp'); @@ -1485,16 +1589,16 @@ runInEachFileSystem(() => { const outputABinding = (nodes[0] as TmplAstElement).outputs[0]; const aSymbol = templateTypeChecker.getSymbolOfNode(outputABinding, cmp)!; assertOutputBindingSymbol(aSymbol); - expect((aSymbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration) - .name.getText()) - .toEqual('outputA'); + expect( + (aSymbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ).toEqual('outputA'); const outputBBinding = (nodes[0] as TmplAstElement).outputs[1]; const bSymbol = templateTypeChecker.getSymbolOfNode(outputBBinding, cmp)!; assertOutputBindingSymbol(bSymbol); - expect((bSymbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration) - .name.getText()) - .toEqual('outputB'); + expect( + (bSymbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ).toEqual('outputB'); }); it('should find symbol for output binding when there are multiple directives', () => { @@ -1519,7 +1623,7 @@ runInEachFileSystem(() => { type: 'directive', outputs: {unusedOutput: 'unusedOutput'}, }, - ] + ], }, { fileName: dirFile, @@ -1528,7 +1632,7 @@ runInEachFileSystem(() => { export class OtherDir {unusedOutput!: EventEmitter;} `, templates: {}, - } + }, ]); const sf = getSourceFileOrError(program, fileName); const cmp = getClass(sf, 'Cmp'); @@ -1538,63 +1642,65 @@ runInEachFileSystem(() => { const outputABinding = (nodes[0] as TmplAstElement).outputs[0]; const symbol = templateTypeChecker.getSymbolOfNode(outputABinding, cmp)!; assertOutputBindingSymbol(symbol); - expect((symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration) - .name.getText()) - .toEqual('outputA'); - expect((symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration) - .parent.name?.text) - .toEqual('TestDir'); + expect( + (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ).toEqual('outputA'); + expect( + (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).parent.name + ?.text, + ).toEqual('TestDir'); }); - it('returns addEventListener binding to native element when no match to any directive output', - () => { - const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([ - { - fileName, - templates: {'Cmp': `
`}, - }, - ]); - const sf = getSourceFileOrError(program, fileName); - const cmp = getClass(sf, 'Cmp'); - - const nodes = templateTypeChecker.getTemplate(cmp)!; - - const outputABinding = (nodes[0] as TmplAstElement).outputs[0]; - const symbol = templateTypeChecker.getSymbolOfNode(outputABinding, cmp)!; - assertOutputBindingSymbol(symbol); - expect(program.getTypeChecker().symbolToString(symbol.bindings[0].tsSymbol!)) - .toEqual('addEventListener'); - - const eventSymbol = templateTypeChecker.getSymbolOfNode(outputABinding.handler, cmp)!; - assertExpressionSymbol(eventSymbol); - }); + it('returns addEventListener binding to native element when no match to any directive output', () => { + const fileName = absoluteFrom('/main.ts'); + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: {'Cmp': `
`}, + }, + ]); + const sf = getSourceFileOrError(program, fileName); + const cmp = getClass(sf, 'Cmp'); + + const nodes = templateTypeChecker.getTemplate(cmp)!; + + const outputABinding = (nodes[0] as TmplAstElement).outputs[0]; + const symbol = templateTypeChecker.getSymbolOfNode(outputABinding, cmp)!; + assertOutputBindingSymbol(symbol); + expect(program.getTypeChecker().symbolToString(symbol.bindings[0].tsSymbol!)).toEqual( + 'addEventListener', + ); + + const eventSymbol = templateTypeChecker.getSymbolOfNode(outputABinding.handler, cmp)!; + assertExpressionSymbol(eventSymbol); + }); it('still returns binding when checkTypeOfOutputEvents is false', () => { const fileName = absoluteFrom('/main.ts'); const dirFile = absoluteFrom('/dir.ts'); const {program, templateTypeChecker} = setup( - [ - { - fileName, - templates: {'Cmp': `
`}, - declarations: [ - { - name: 'TestDir', - selector: '[dir]', - file: dirFile, - type: 'directive', - outputs: {outputA: 'outputA'}, - }, - ] - }, - { - fileName: dirFile, - source: `export class TestDir {outputA!: EventEmitter;}`, - templates: {}, - } - ], - {checkTypeOfOutputEvents: false}); + [ + { + fileName, + templates: {'Cmp': `
`}, + declarations: [ + { + name: 'TestDir', + selector: '[dir]', + file: dirFile, + type: 'directive', + outputs: {outputA: 'outputA'}, + }, + ], + }, + { + fileName: dirFile, + source: `export class TestDir {outputA!: EventEmitter;}`, + templates: {}, + }, + ], + {checkTypeOfOutputEvents: false}, + ); const sf = getSourceFileOrError(program, fileName); const cmp = getClass(sf, 'Cmp'); @@ -1603,15 +1709,15 @@ runInEachFileSystem(() => { const outputABinding = (nodes[0] as TmplAstElement).outputs[0]; const symbol = templateTypeChecker.getSymbolOfNode(outputABinding, cmp)!; assertOutputBindingSymbol(symbol); - expect((symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration) - .name.getText()) - .toEqual('outputA'); - expect((symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration) - .parent.name?.text) - .toEqual('TestDir'); + expect( + (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ).toEqual('outputA'); + expect( + (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).parent.name + ?.text, + ).toEqual('TestDir'); }); - it('returns output symbol for two way binding', () => { const fileName = absoluteFrom('/main.ts'); const dirFile = absoluteFrom('/dir.ts'); @@ -1632,7 +1738,7 @@ runInEachFileSystem(() => { inputs: {ngModel: 'ngModel'}, outputs: {ngModelChange: 'ngModelChange'}, }, - ] + ], }, { fileName: dirFile, @@ -1642,7 +1748,7 @@ runInEachFileSystem(() => { ngModelChange!: EventEmitter; }`, templates: {}, - } + }, ]); const sf = getSourceFileOrError(program, fileName); const cmp = getClass(sf, 'Cmp'); @@ -1652,12 +1758,13 @@ runInEachFileSystem(() => { const outputABinding = (nodes[0] as TmplAstElement).outputs[0]; const symbol = templateTypeChecker.getSymbolOfNode(outputABinding, cmp)!; assertOutputBindingSymbol(symbol); - expect((symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration) - .name.getText()) - .toEqual('ngModelChange'); - expect((symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration) - .parent.name?.text) - .toEqual('TestDir'); + expect( + (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).name.getText(), + ).toEqual('ngModelChange'); + expect( + (symbol.bindings[0].tsSymbol!.declarations![0] as ts.PropertyDeclaration).parent.name + ?.text, + ).toEqual('TestDir'); }); }); @@ -1665,30 +1772,28 @@ runInEachFileSystem(() => { it('for elements that are components with no inputs', () => { const fileName = absoluteFrom('/main.ts'); const dirFile = absoluteFrom('/dir.ts'); - const {program, templateTypeChecker} = setup( - [ + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: {'Cmp': ``}, + declarations: [ { - fileName, - templates: {'Cmp': ``}, - declarations: [ - { - name: 'ChildComponent', - selector: 'child-component', - isComponent: true, - file: dirFile, - type: 'directive', - }, - ] + name: 'ChildComponent', + selector: 'child-component', + isComponent: true, + file: dirFile, + type: 'directive', }, - { - fileName: dirFile, - source: ` + ], + }, + { + fileName: dirFile, + source: ` export class ChildComponent {} `, - templates: {'ChildComponent': ''}, - } - ], - ); + templates: {'ChildComponent': ''}, + }, + ]); const sf = getSourceFileOrError(program, fileName); const cmp = getClass(sf, 'Cmp'); @@ -1698,43 +1803,43 @@ runInEachFileSystem(() => { assertElementSymbol(symbol); expect(symbol.directives.length).toBe(1); assertDirectiveSymbol(symbol.directives[0]); - expect(program.getTypeChecker().typeToString(symbol.directives[0].tsType)) - .toEqual('ChildComponent'); + expect(program.getTypeChecker().typeToString(symbol.directives[0].tsType)).toEqual( + 'ChildComponent', + ); expect(symbol.directives[0].isComponent).toBe(true); }); it('element with directive matches', () => { const fileName = absoluteFrom('/main.ts'); const dirFile = absoluteFrom('/dir.ts'); - const {program, templateTypeChecker} = setup( - [ + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: {'Cmp': `
`}, + declarations: [ { - fileName, - templates: {'Cmp': `
`}, - declarations: [ - { - name: 'TestDir', - selector: '[dir]', - file: dirFile, - type: 'directive', - }, - { - name: 'TestDir2', - selector: '[dir2]', - file: dirFile, - type: 'directive', - }, - { - name: 'TestDirAllDivs', - selector: 'div', - file: dirFile, - type: 'directive', - }, - ] + name: 'TestDir', + selector: '[dir]', + file: dirFile, + type: 'directive', }, { - fileName: dirFile, - source: ` + name: 'TestDir2', + selector: '[dir2]', + file: dirFile, + type: 'directive', + }, + { + name: 'TestDirAllDivs', + selector: 'div', + file: dirFile, + type: 'directive', + }, + ], + }, + { + fileName: dirFile, + source: ` export class TestDir {} // Allow the fake ComponentScopeReader to return a module for TestDir export class TestDirModule {} @@ -1743,10 +1848,9 @@ runInEachFileSystem(() => { export class TestDir2Module {} export class TestDirAllDivs {} `, - templates: {}, - } - ], - ); + templates: {}, + }, + ]); const sf = getSourceFileOrError(program, fileName); const cmp = getClass(sf, 'Cmp'); @@ -1756,12 +1860,13 @@ runInEachFileSystem(() => { assertElementSymbol(symbol); expect(symbol.directives.length).toBe(3); const expectedDirectives = ['TestDir', 'TestDir2', 'TestDirAllDivs'].sort(); - const actualDirectives = - symbol.directives.map(dir => program.getTypeChecker().typeToString(dir.tsType)).sort(); + const actualDirectives = symbol.directives + .map((dir) => program.getTypeChecker().typeToString(dir.tsType)) + .sort(); expect(actualDirectives).toEqual(expectedDirectives); const expectedSelectors = ['[dir]', '[dir2]', 'div'].sort(); - const actualSelectors = symbol.directives.map(dir => dir.selector).sort(); + const actualSelectors = symbol.directives.map((dir) => dir.selector).sort(); expect(actualSelectors).toEqual(expectedSelectors); // Testing this fully requires an integration test with a real `NgCompiler` (like in the @@ -1769,8 +1874,9 @@ runInEachFileSystem(() => { // assert that we are able to handle when the scope reader returns `null` or a class from // the fake implementation. const expectedModules = new Set([null, 'TestDirModule', 'TestDir2Module']); - const actualModules = - new Set(symbol.directives.map(dir => dir.ngModule?.name.getText() ?? null)); + const actualModules = new Set( + symbol.directives.map((dir) => dir.ngModule?.name.getText() ?? null), + ); expect(actualModules).toEqual(expectedModules); }); }); @@ -1778,30 +1884,28 @@ runInEachFileSystem(() => { it('elements with generic directives', () => { const fileName = absoluteFrom('/main.ts'); const dirFile = absoluteFrom('/dir.ts'); - const {program, templateTypeChecker} = setup( - [ + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: {'Cmp': `
`}, + declarations: [ { - fileName, - templates: {'Cmp': `
`}, - declarations: [ - { - name: 'GenericDir', - selector: '[genericDir]', - file: dirFile, - type: 'directive', - isGeneric: true - }, - ] + name: 'GenericDir', + selector: '[genericDir]', + file: dirFile, + type: 'directive', + isGeneric: true, }, - { - fileName: dirFile, - source: ` + ], + }, + { + fileName: dirFile, + source: ` export class GenericDir{} `, - templates: {}, - } - ], - ); + templates: {}, + }, + ]); const sf = getSourceFileOrError(program, fileName); const cmp = getClass(sf, 'Cmp'); @@ -1810,23 +1914,25 @@ runInEachFileSystem(() => { const symbol = templateTypeChecker.getSymbolOfNode(nodes[0] as TmplAstElement, cmp)!; assertElementSymbol(symbol); expect(symbol.directives.length).toBe(1); - const actualDirectives = - symbol.directives.map(dir => program.getTypeChecker().typeToString(dir.tsType)).sort(); + const actualDirectives = symbol.directives + .map((dir) => program.getTypeChecker().typeToString(dir.tsType)) + .sort(); expect(actualDirectives).toEqual(['GenericDir']); }); it('has correct tcb location for components with inline TCBs', () => { const fileName = absoluteFrom('/main.ts'); const {program, templateTypeChecker} = baseTestSetup( - [ - { - fileName, - templates: {'Cmp': '
'}, - // Force an inline TCB by using a non-exported component class - source: `class Cmp {}`, - }, - ], - {inlining: true, config: {enableTemplateTypeChecker: true}}); + [ + { + fileName, + templates: {'Cmp': '
'}, + // Force an inline TCB by using a non-exported component class + source: `class Cmp {}`, + }, + ], + {inlining: true, config: {enableTemplateTypeChecker: true}}, + ); const sf = getSourceFileOrError(program, fileName); const cmp = getClass(sf, 'Cmp'); @@ -1868,12 +1974,16 @@ function onlyAstElements(nodes: TmplAstNode[]): TmplAstElement[] { } function getAstElements( - templateTypeChecker: TemplateTypeChecker, cmp: ts.ClassDeclaration&{name: ts.Identifier}) { + templateTypeChecker: TemplateTypeChecker, + cmp: ts.ClassDeclaration & {name: ts.Identifier}, +) { return onlyAstElements(templateTypeChecker.getTemplate(cmp)!); } function getAstTemplates( - templateTypeChecker: TemplateTypeChecker, cmp: ts.ClassDeclaration&{name: ts.Identifier}) { + templateTypeChecker: TemplateTypeChecker, + cmp: ts.ClassDeclaration & {name: ts.Identifier}, +) { return onlyAstTemplates(templateTypeChecker.getTemplate(cmp)!); } @@ -1920,6 +2030,6 @@ function assertDomBindingSymbol(tSymbol: Symbol): asserts tSymbol is DomBindingS export function setup(targets: TypeCheckingTarget[], config?: Partial) { return baseTestSetup(targets, { inlining: false, - config: {...config, enableTemplateTypeChecker: true, useInlineTypeConstructors: false} + config: {...config, enableTemplateTypeChecker: true, useInlineTypeConstructors: false}, }); } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker_spec.ts index d181d7e954e37..f15f19a34488b 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker_spec.ts @@ -20,14 +20,18 @@ runInEachFileSystem(() => { const file2 = absoluteFrom('/file2.ts'); const {program, templateTypeChecker, programStrategy} = setup([ {fileName: file1, templates: {'Cmp1': '
'}}, - {fileName: file2, templates: {'Cmp2': ''}} + {fileName: file2, templates: {'Cmp2': ''}}, ]); templateTypeChecker.getDiagnosticsForFile( - getSourceFileOrError(program, file1), OptimizeFor.WholeProgram); + getSourceFileOrError(program, file1), + OptimizeFor.WholeProgram, + ); const ttcProgram1 = programStrategy.getProgram(); templateTypeChecker.getDiagnosticsForFile( - getSourceFileOrError(program, file2), OptimizeFor.WholeProgram); + getSourceFileOrError(program, file2), + OptimizeFor.WholeProgram, + ); const ttcProgram2 = programStrategy.getProgram(); expect(ttcProgram1).toBe(ttcProgram2); @@ -38,11 +42,13 @@ runInEachFileSystem(() => { const file2 = absoluteFrom('/file2.ts'); const {program, templateTypeChecker, programStrategy} = setup([ {fileName: file1, templates: {'Cmp1': '
'}}, - {fileName: file2, templates: {'Cmp2': ''}} + {fileName: file2, templates: {'Cmp2': ''}}, ]); templateTypeChecker.getDiagnosticsForFile( - getSourceFileOrError(program, file1), OptimizeFor.SingleFile); + getSourceFileOrError(program, file1), + OptimizeFor.SingleFile, + ); const ttcProgram1 = programStrategy.getProgram(); // ttcProgram1 should not contain a type check block for Cmp2. @@ -50,7 +56,9 @@ runInEachFileSystem(() => { expect(ttcSf2Before.text).not.toContain('Cmp2'); templateTypeChecker.getDiagnosticsForFile( - getSourceFileOrError(program, file2), OptimizeFor.SingleFile); + getSourceFileOrError(program, file2), + OptimizeFor.SingleFile, + ); const ttcProgram2 = programStrategy.getProgram(); // ttcProgram2 should now contain a type check block for Cmp2. @@ -65,7 +73,7 @@ runInEachFileSystem(() => { const file2 = absoluteFrom('/file2.ts'); const {program, templateTypeChecker, programStrategy} = setup([ {fileName: file1, templates: {'Cmp1': '
{{value}}
'}}, - {fileName: file2, templates: {'Cmp2': ''}} + {fileName: file2, templates: {'Cmp2': ''}}, ]); const cmp1 = getClass(getSourceFileOrError(program, file1), 'Cmp1'); @@ -130,25 +138,28 @@ runInEachFileSystem(() => { const fileName = absoluteFrom('/main.ts'); const dirFile = absoluteFrom('/dir.ts'); const {program, templateTypeChecker} = setup( - [ - { - fileName, - source: `export class Cmp {}`, - templates: {'Cmp': '
'}, - declarations: [{ + [ + { + fileName, + source: `export class Cmp {}`, + templates: {'Cmp': '
'}, + declarations: [ + { name: 'TestDir', selector: '[dir]', file: dirFile, type: 'directive', - }] - }, - { - fileName: dirFile, - source: `export class TestDir {}`, - templates: {}, - } - ], - {inlining: false}); + }, + ], + }, + { + fileName: dirFile, + source: `export class TestDir {}`, + templates: {}, + }, + ], + {inlining: false}, + ); const sf = getSourceFileOrError(program, fileName); const diags = templateTypeChecker.getDiagnosticsForFile(sf, OptimizeFor.WholeProgram); expect(diags.length).toBe(0); @@ -157,12 +168,15 @@ runInEachFileSystem(() => { it('should produce errors for components that require TCB inlining', () => { const fileName = absoluteFrom('/main.ts'); const {program, templateTypeChecker} = setup( - [{ + [ + { fileName, source: `abstract class Cmp {} // not exported, so requires inline`, - templates: {'Cmp': '
'} - }], - {inlining: false}); + templates: {'Cmp': '
'}, + }, + ], + {inlining: false}, + ); const sf = getSourceFileOrError(program, fileName); const diags = templateTypeChecker.getDiagnosticsForFile(sf, OptimizeFor.WholeProgram); expect(diags.length).toBe(1); @@ -171,14 +185,16 @@ runInEachFileSystem(() => { }); describe('getTemplateOfComponent()', () => { - it('should provide access to a component\'s real template', () => { + it("should provide access to a component's real template", () => { const fileName = absoluteFrom('/main.ts'); - const {program, templateTypeChecker} = setup([{ - fileName, - templates: { - 'Cmp': '
Template
', + const {program, templateTypeChecker} = setup([ + { + fileName, + templates: { + 'Cmp': '
Template
', + }, }, - }]); + ]); const cmp = getClass(getSourceFileOrError(program, fileName), 'Cmp'); const nodes = templateTypeChecker.getTemplate(cmp)!; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts index 09f6c84c3aeb8..e2651bf87d9b9 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_constructor_spec.ts @@ -7,16 +7,34 @@ */ import ts from 'typescript'; -import {absoluteFrom, getFileSystem, getSourceFileOrError, LogicalFileSystem, NgtscCompilerHost} from '../../file_system'; +import { + absoluteFrom, + getFileSystem, + getSourceFileOrError, + LogicalFileSystem, + NgtscCompilerHost, +} from '../../file_system'; import {runInEachFileSystem, TestFile} from '../../file_system/testing'; -import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, Reference, ReferenceEmitter} from '../../imports'; +import { + AbsoluteModuleStrategy, + LocalIdentifierStrategy, + LogicalProjectStrategy, + ModuleResolver, + Reference, + ReferenceEmitter, +} from '../../imports'; import {ClassPropertyMapping, InputMapping} from '../../metadata'; import {NOOP_PERF_RECORDER} from '../../perf'; import {TsCreateProgramDriver, UpdateMode} from '../../program_driver'; import {isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection'; import {getDeclaration, makeProgram} from '../../testing'; import {getRootDirs} from '../../util/src/typescript'; -import {InliningMode, PendingFileTypeCheckingData, TypeCheckContextImpl, TypeCheckingHost} from '../src/context'; +import { + InliningMode, + PendingFileTypeCheckingData, + TypeCheckContextImpl, + TypeCheckingHost, +} from '../src/context'; import {TemplateSourceManager} from '../src/source'; import {TypeCheckFile} from '../src/type_check_file'; import {ALL_ENABLED_CONFIG} from '../testing'; @@ -33,15 +51,19 @@ runInEachFileSystem(() => { contents: ` type Partial = { [P in keyof T]?: T[P]; }; type Pick = { [P in K]: T[P]; }; - type NonNullable = T extends null | undefined ? never : T;` + type NonNullable = T extends null | undefined ? never : T;`, }; }); it('should not produce an empty SourceFile when there is nothing to typecheck', () => { const host = new NgtscCompilerHost(getFileSystem()); const file = new TypeCheckFile( - _('/_typecheck_.ts'), ALL_ENABLED_CONFIG, new ReferenceEmitter([]), - /* reflector */ null!, host); + _('/_typecheck_.ts'), + ALL_ENABLED_CONFIG, + new ReferenceEmitter([]), + /* reflector */ null!, + host, + ); const sf = file.render(false /* removeComments */); expect(sf).toContain('export const IS_A_MODULE = true;'); }); @@ -49,7 +71,8 @@ runInEachFileSystem(() => { describe('ctors', () => { it('compiles a basic type constructor', () => { const files: TestFile[] = [ - LIB_D_TS, { + LIB_D_TS, + { name: _('/main.ts'), contents: ` class TestClass { @@ -57,52 +80,75 @@ class TestClass { } TestClass.ngTypeCtor({value: 'test'}); - ` - } + `, + }, ]; const {program, host, options} = makeProgram(files, undefined, undefined, false); const checker = program.getTypeChecker(); const reflectionHost = new TypeScriptReflectionHost(checker); const logicalFs = new LogicalFileSystem(getRootDirs(host, options), host); - const moduleResolver = - new ModuleResolver(program, options, host, /* moduleResolutionCache */ null); + const moduleResolver = new ModuleResolver( + program, + options, + host, + /* moduleResolutionCache */ null, + ); const emitter = new ReferenceEmitter([ new LocalIdentifierStrategy(), new AbsoluteModuleStrategy(program, checker, moduleResolver, reflectionHost), new LogicalProjectStrategy(reflectionHost, logicalFs), ]); const ctx = new TypeCheckContextImpl( - ALL_ENABLED_CONFIG, host, emitter, reflectionHost, new TestTypeCheckingHost(), - InliningMode.InlineOps, NOOP_PERF_RECORDER); - const TestClass = - getDeclaration(program, _('/main.ts'), 'TestClass', isNamedClassDeclaration); + ALL_ENABLED_CONFIG, + host, + emitter, + reflectionHost, + new TestTypeCheckingHost(), + InliningMode.InlineOps, + NOOP_PERF_RECORDER, + ); + const TestClass = getDeclaration( + program, + _('/main.ts'), + 'TestClass', + isNamedClassDeclaration, + ); const pendingFile = makePendingFile(); ctx.addInlineTypeCtor( - pendingFile, getSourceFileOrError(program, _('/main.ts')), new Reference(TestClass), { - fnName: 'ngTypeCtor', - body: true, - fields: { - inputs: ClassPropertyMapping.fromMappedObject({value: 'value'}), - queries: [], - }, - coercedInputFields: new Set(), - }); + pendingFile, + getSourceFileOrError(program, _('/main.ts')), + new Reference(TestClass), + { + fnName: 'ngTypeCtor', + body: true, + fields: { + inputs: ClassPropertyMapping.fromMappedObject({value: 'value'}), + queries: [], + }, + coercedInputFields: new Set(), + }, + ); ctx.finalize(); }); it('should not consider query fields', () => { const files: TestFile[] = [ - LIB_D_TS, { + LIB_D_TS, + { name: _('/main.ts'), contents: `class TestClass { value: any; }`, - } + }, ]; const {program, host, options} = makeProgram(files, undefined, undefined, false); const checker = program.getTypeChecker(); const reflectionHost = new TypeScriptReflectionHost(checker); const logicalFs = new LogicalFileSystem(getRootDirs(host, options), host); - const moduleResolver = - new ModuleResolver(program, options, host, /* moduleResolutionCache */ null); + const moduleResolver = new ModuleResolver( + program, + options, + host, + /* moduleResolutionCache */ null, + ); const emitter = new ReferenceEmitter([ new LocalIdentifierStrategy(), new AbsoluteModuleStrategy(program, checker, moduleResolver, reflectionHost), @@ -110,24 +156,42 @@ TestClass.ngTypeCtor({value: 'test'}); ]); const pendingFile = makePendingFile(); const ctx = new TypeCheckContextImpl( - ALL_ENABLED_CONFIG, host, emitter, reflectionHost, new TestTypeCheckingHost(), - InliningMode.InlineOps, NOOP_PERF_RECORDER); - const TestClass = - getDeclaration(program, _('/main.ts'), 'TestClass', isNamedClassDeclaration); + ALL_ENABLED_CONFIG, + host, + emitter, + reflectionHost, + new TestTypeCheckingHost(), + InliningMode.InlineOps, + NOOP_PERF_RECORDER, + ); + const TestClass = getDeclaration( + program, + _('/main.ts'), + 'TestClass', + isNamedClassDeclaration, + ); ctx.addInlineTypeCtor( - pendingFile, getSourceFileOrError(program, _('/main.ts')), new Reference(TestClass), { - fnName: 'ngTypeCtor', - body: true, - fields: { - inputs: ClassPropertyMapping.fromMappedObject({value: 'value'}), - queries: ['queryField'], - }, - coercedInputFields: new Set(), - }); + pendingFile, + getSourceFileOrError(program, _('/main.ts')), + new Reference(TestClass), + { + fnName: 'ngTypeCtor', + body: true, + fields: { + inputs: ClassPropertyMapping.fromMappedObject({value: 'value'}), + queries: ['queryField'], + }, + coercedInputFields: new Set(), + }, + ); const programStrategy = new TsCreateProgramDriver(program, host, options, []); programStrategy.updateFiles(ctx.finalize(), UpdateMode.Complete); const TestClassWithCtor = getDeclaration( - programStrategy.getProgram(), _('/main.ts'), 'TestClass', isNamedClassDeclaration); + programStrategy.getProgram(), + _('/main.ts'), + 'TestClass', + isNamedClassDeclaration, + ); const typeCtor = TestClassWithCtor.members.find(isTypeCtor)!; expect(typeCtor.getText()).not.toContain('queryField'); }); @@ -136,17 +200,22 @@ TestClass.ngTypeCtor({value: 'test'}); describe('input type coercion', () => { it('should coerce input types', () => { const files: TestFile[] = [ - LIB_D_TS, { + LIB_D_TS, + { name: _('/main.ts'), contents: `class TestClass { value: any; }`, - } + }, ]; const {program, host, options} = makeProgram(files, undefined, undefined, false); const checker = program.getTypeChecker(); const reflectionHost = new TypeScriptReflectionHost(checker); const logicalFs = new LogicalFileSystem(getRootDirs(host, options), host); - const moduleResolver = - new ModuleResolver(program, options, host, /* moduleResolutionCache */ null); + const moduleResolver = new ModuleResolver( + program, + options, + host, + /* moduleResolutionCache */ null, + ); const emitter = new ReferenceEmitter([ new LocalIdentifierStrategy(), new AbsoluteModuleStrategy(program, checker, moduleResolver, reflectionHost), @@ -154,45 +223,73 @@ TestClass.ngTypeCtor({value: 'test'}); ]); const pendingFile = makePendingFile(); const ctx = new TypeCheckContextImpl( - ALL_ENABLED_CONFIG, host, emitter, reflectionHost, new TestTypeCheckingHost(), - InliningMode.InlineOps, NOOP_PERF_RECORDER); - const TestClass = - getDeclaration(program, _('/main.ts'), 'TestClass', isNamedClassDeclaration); + ALL_ENABLED_CONFIG, + host, + emitter, + reflectionHost, + new TestTypeCheckingHost(), + InliningMode.InlineOps, + NOOP_PERF_RECORDER, + ); + const TestClass = getDeclaration( + program, + _('/main.ts'), + 'TestClass', + isNamedClassDeclaration, + ); ctx.addInlineTypeCtor( - pendingFile, getSourceFileOrError(program, _('/main.ts')), new Reference(TestClass), { - fnName: 'ngTypeCtor', - body: true, - fields: { - inputs: ClassPropertyMapping.fromMappedObject({ - foo: 'foo', - bar: 'bar', - baz: { - classPropertyName: 'baz', - bindingPropertyName: 'baz', - required: false, - isSignal: false, - transform: { - type: new Reference(ts.factory.createUnionTypeNode([ + pendingFile, + getSourceFileOrError(program, _('/main.ts')), + new Reference(TestClass), + { + fnName: 'ngTypeCtor', + body: true, + fields: { + inputs: ClassPropertyMapping.fromMappedObject({ + foo: 'foo', + bar: 'bar', + baz: { + classPropertyName: 'baz', + bindingPropertyName: 'baz', + required: false, + isSignal: false, + transform: { + type: new Reference( + ts.factory.createUnionTypeNode([ ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), - ])), - node: ts.factory.createFunctionDeclaration( - undefined, undefined, undefined, undefined, [], undefined, undefined) - } - } - }), - queries: [], - }, - coercedInputFields: new Set(['bar', 'baz']), - }); + ]), + ), + node: ts.factory.createFunctionDeclaration( + undefined, + undefined, + undefined, + undefined, + [], + undefined, + undefined, + ), + }, + }, + }), + queries: [], + }, + coercedInputFields: new Set(['bar', 'baz']), + }, + ); const programStrategy = new TsCreateProgramDriver(program, host, options, []); programStrategy.updateFiles(ctx.finalize(), UpdateMode.Complete); const TestClassWithCtor = getDeclaration( - programStrategy.getProgram(), _('/main.ts'), 'TestClass', isNamedClassDeclaration); + programStrategy.getProgram(), + _('/main.ts'), + 'TestClass', + isNamedClassDeclaration, + ); const typeCtor = TestClassWithCtor.members.find(isTypeCtor)!; const ctorText = typeCtor.getText().replace(/[ \r\n]+/g, ' '); expect(ctorText).toContain( - 'init: Pick & { bar: typeof TestClass.ngAcceptInputType_bar; baz: boolean | string; }'); + 'init: Pick & { bar: typeof TestClass.ngAcceptInputType_bar; baz: boolean | string; }', + ); }); }); }); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_parameter_emitter_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_parameter_emitter_spec.ts index c7b22eacbd05a..3b646156e98cb 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_parameter_emitter_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_parameter_emitter_spec.ts @@ -9,7 +9,14 @@ import ts from 'typescript'; import {absoluteFrom, LogicalFileSystem} from '../../file_system'; import {runInEachFileSystem, TestFile} from '../../file_system/testing'; -import {AbsoluteModuleStrategy, ImportFlags, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, ReferenceEmitter} from '../../imports'; +import { + AbsoluteModuleStrategy, + ImportFlags, + LocalIdentifierStrategy, + LogicalProjectStrategy, + ModuleResolver, + ReferenceEmitter, +} from '../../imports'; import {isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection'; import {getDeclaration, makeProgram} from '../../testing'; import {Environment} from '../src/environment'; @@ -17,7 +24,6 @@ import {TypeCheckFile} from '../src/type_check_file'; import {TypeParameterEmitter} from '../src/type_parameter_emitter'; import {ALL_ENABLED_CONFIG, angularCoreDtsFiles, typescriptLibDts} from '../testing'; - runInEachFileSystem(() => { describe('type parameter emitter', () => { function createEmitter(source: string, additionalFiles: TestFile[] = []) { @@ -31,10 +37,18 @@ runInEachFileSystem(() => { const reflector = new TypeScriptReflectionHost(checker); const TestClass = getDeclaration( - program, absoluteFrom('/app/main.ts'), 'TestClass', isNamedClassDeclaration); + program, + absoluteFrom('/app/main.ts'), + 'TestClass', + isNamedClassDeclaration, + ); const moduleResolver = new ModuleResolver( - program, program.getCompilerOptions(), host, /* moduleResolutionCache */ null); + program, + program.getCompilerOptions(), + host, + /* moduleResolutionCache */ null, + ); const refEmitter = new ReferenceEmitter([ new LocalIdentifierStrategy(), new AbsoluteModuleStrategy(program, checker, moduleResolver, reflector), @@ -42,19 +56,25 @@ runInEachFileSystem(() => { ]); const env = new TypeCheckFile( - absoluteFrom('/app/main.ngtypecheck.ts'), ALL_ENABLED_CONFIG, refEmitter, reflector, - host); + absoluteFrom('/app/main.ngtypecheck.ts'), + ALL_ENABLED_CONFIG, + refEmitter, + reflector, + host, + ); const emitter = new TypeParameterEmitter(TestClass.typeParameters, reflector); return {emitter, env}; } function emit( - {emitter, env}: {emitter: TypeParameterEmitter; env: Environment}, flags?: ImportFlags) { - const canEmit = emitter.canEmit(ref => env.canReferenceType(ref, flags)); + {emitter, env}: {emitter: TypeParameterEmitter; env: Environment}, + flags?: ImportFlags, + ) { + const canEmit = emitter.canEmit((ref) => env.canReferenceType(ref, flags)); - let emitted: ts.TypeParameterDeclaration[]|undefined; + let emitted: ts.TypeParameterDeclaration[] | undefined; try { - emitted = emitter.emit(ref => env.referenceType(ref, flags)); + emitted = emitter.emit((ref) => env.referenceType(ref, flags)); expect(canEmit).toBe(true); } catch (e) { expect(canEmit).toBe(false); @@ -67,8 +87,9 @@ runInEachFileSystem(() => { const printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed}); const sf = ts.createSourceFile('test.ts', '', ts.ScriptTarget.Latest); - const generics = - emitted.map(param => printer.printNode(ts.EmitHint.Unspecified, param, sf)).join(', '); + const generics = emitted + .map((param) => printer.printNode(ts.EmitHint.Unspecified, param, sf)) + .join(', '); return `<${generics}>`; } @@ -76,47 +97,66 @@ runInEachFileSystem(() => { it('can emit for simple generic types', () => { expect(emit(createEmitter(`export class TestClass {}`))).toEqual(''); expect(emit(createEmitter(`export class TestClass {}`))).toEqual(''); - expect(emit(createEmitter(`export class TestClass {}`))) - .toEqual(''); - expect(emit(createEmitter(`export class TestClass {}`))) - .toEqual(''); - expect(emit(createEmitter(`export class TestClass {}`))) - .toEqual(''); - expect(emit(createEmitter(`export class TestClass {}`))) - .toEqual(''); - expect(emit(createEmitter(`export class TestClass {}`))) - .toEqual(''); - expect(emit(createEmitter(`export class TestClass {}`))) - .toEqual(''); - expect(emit(createEmitter(`export class TestClass {}`))) - .toEqual(''); - expect(emit(createEmitter(`export class TestClass {}`))) - .toEqual(''); - expect(emit(createEmitter(`export class TestClass {}`))) - .toEqual(''); - expect(emit(createEmitter(`export class TestClass {}`))) - .toEqual(''); - expect(emit(createEmitter(`export class TestClass {}`))) - .toEqual(''); - expect(emit(createEmitter(`export class TestClass {}`))) - .toEqual(''); - expect(emit(createEmitter(`export class TestClass {}`))) - .toEqual(''); + expect(emit(createEmitter(`export class TestClass {}`))).toEqual( + '', + ); + expect(emit(createEmitter(`export class TestClass {}`))).toEqual( + '', + ); + expect(emit(createEmitter(`export class TestClass {}`))).toEqual( + '', + ); + expect(emit(createEmitter(`export class TestClass {}`))).toEqual( + '', + ); + expect(emit(createEmitter(`export class TestClass {}`))).toEqual( + '', + ); + expect(emit(createEmitter(`export class TestClass {}`))).toEqual( + '', + ); + expect(emit(createEmitter(`export class TestClass {}`))).toEqual( + '', + ); + expect(emit(createEmitter(`export class TestClass {}`))).toEqual( + '', + ); + expect(emit(createEmitter(`export class TestClass {}`))).toEqual( + '', + ); + expect(emit(createEmitter(`export class TestClass {}`))).toEqual( + '', + ); + expect(emit(createEmitter(`export class TestClass {}`))).toEqual( + '', + ); + expect(emit(createEmitter(`export class TestClass {}`))).toEqual( + '', + ); + expect( + emit(createEmitter(`export class TestClass {}`)), + ).toEqual(''); }); it('can emit literal types', () => { - expect(emit(createEmitter(`export class TestClass {}`))) - .toEqual(``); - expect(emit(createEmitter(`export class TestClass {}`))) - .toEqual(``); - expect(emit(createEmitter(`export class TestClass {}`))) - .toEqual(``); - expect(emit(createEmitter(`export class TestClass {}`))) - .toEqual(``); - expect(emit(createEmitter(`export class TestClass {}`))) - .toEqual(``); - expect(emit(createEmitter(`export class TestClass {}`))) - .toEqual(``); + expect(emit(createEmitter(`export class TestClass {}`))).toEqual( + ``, + ); + expect(emit(createEmitter(`export class TestClass {}`))).toEqual( + ``, + ); + expect(emit(createEmitter(`export class TestClass {}`))).toEqual( + ``, + ); + expect(emit(createEmitter(`export class TestClass {}`))).toEqual( + ``, + ); + expect(emit(createEmitter(`export class TestClass {}`))).toEqual( + ``, + ); + expect(emit(createEmitter(`export class TestClass {}`))).toEqual( + ``, + ); }); it('cannot emit import types', () => { @@ -197,31 +237,37 @@ runInEachFileSystem(() => { }); it('can emit references into relative files', () => { - const additionalFiles: TestFile[] = [{ - name: absoluteFrom('/app/internal.ts'), - contents: `export class Internal {}`, - }]; + const additionalFiles: TestFile[] = [ + { + name: absoluteFrom('/app/internal.ts'), + contents: `export class Internal {}`, + }, + ]; const emitter = createEmitter( - ` + ` import {Internal} from './internal'; export class TestClass {}`, - additionalFiles); + additionalFiles, + ); expect(emit(emitter)).toEqual(''); }); it('cannot emit references into relative source files that are outside of rootDirs', () => { - const additionalFiles: TestFile[] = [{ - name: absoluteFrom('/internal.ts'), - contents: `export class Internal {}`, - }]; + const additionalFiles: TestFile[] = [ + { + name: absoluteFrom('/internal.ts'), + contents: `export class Internal {}`, + }, + ]; const emitter = createEmitter( - ` + ` import {Internal} from '../internal'; export class TestClass {}`, - additionalFiles); + additionalFiles, + ); // The `internal.ts` source file is outside `rootDir` and importing from it would trigger // TS6059, so this emit should fail. @@ -229,16 +275,19 @@ runInEachFileSystem(() => { }); it('can emit references into relative declaration files that are outside of rootDirs', () => { - const additionalFiles: TestFile[] = [{ - name: absoluteFrom('/internal.d.ts'), - contents: `export class Internal {}`, - }]; + const additionalFiles: TestFile[] = [ + { + name: absoluteFrom('/internal.d.ts'), + contents: `export class Internal {}`, + }, + ]; const emitter = createEmitter( - ` + ` import {Internal} from '../internal'; export class TestClass {}`, - additionalFiles); + additionalFiles, + ); // The `internal.d.ts` is outside `rootDir` but declaration files do not trigger TS6059, so we // allow such an import to be created. @@ -255,114 +304,135 @@ runInEachFileSystem(() => { }); it('can emit references to exported classes imported using a namespace import', () => { - const additionalFiles: TestFile[] = [{ - name: absoluteFrom('/app/internal.ts'), - contents: `export class Internal {}`, - }]; + const additionalFiles: TestFile[] = [ + { + name: absoluteFrom('/app/internal.ts'), + contents: `export class Internal {}`, + }, + ]; const emitter = createEmitter( - ` + ` import * as ns from './internal'; export class TestClass {}`, - additionalFiles); + additionalFiles, + ); expect(emit(emitter)).toEqual(''); }); it('cannot emit references to local classes exported within a namespace', () => { - const additionalFiles: TestFile[] = [{ - name: absoluteFrom('/app/ns.ts'), - contents: ` + const additionalFiles: TestFile[] = [ + { + name: absoluteFrom('/app/ns.ts'), + contents: ` export namespace ns { export class Nested {} } `, - }]; + }, + ]; const emitter = createEmitter( - ` + ` import {ns} from './ns'; export class TestClass {}`, - additionalFiles); + additionalFiles, + ); expect(() => emit(emitter)).toThrow(); }); it('cannot emit references to external classes exported within a namespace', () => { - const additionalFiles: TestFile[] = [{ - name: absoluteFrom('/node_modules/ns/index.d.ts'), - contents: ` + const additionalFiles: TestFile[] = [ + { + name: absoluteFrom('/node_modules/ns/index.d.ts'), + contents: ` export namespace ns { export declare class Nested {} } `, - }]; + }, + ]; const emitter = createEmitter( - ` + ` import {ns} from 'ns'; export class TestClass {}`, - additionalFiles); + additionalFiles, + ); expect(() => emit(emitter)).toThrow(); }); it('can emit references to interfaces', () => { - const additionalFiles: TestFile[] = [{ - name: absoluteFrom('/node_modules/types/index.d.ts'), - contents: `export declare interface MyInterface {}`, - }]; + const additionalFiles: TestFile[] = [ + { + name: absoluteFrom('/node_modules/types/index.d.ts'), + contents: `export declare interface MyInterface {}`, + }, + ]; const emitter = createEmitter( - ` + ` import {MyInterface} from 'types'; export class TestClass {}`, - additionalFiles); + additionalFiles, + ); expect(emit(emitter)).toEqual(''); }); it('can emit references to enums', () => { - const additionalFiles: TestFile[] = [{ - name: absoluteFrom('/node_modules/types/index.d.ts'), - contents: `export declare enum MyEnum {}`, - }]; + const additionalFiles: TestFile[] = [ + { + name: absoluteFrom('/node_modules/types/index.d.ts'), + contents: `export declare enum MyEnum {}`, + }, + ]; const emitter = createEmitter( - ` + ` import {MyEnum} from 'types'; export class TestClass {}`, - additionalFiles); + additionalFiles, + ); expect(emit(emitter)).toEqual(''); }); it('can emit references to type aliases', () => { - const additionalFiles: TestFile[] = [{ - name: absoluteFrom('/node_modules/types/index.d.ts'), - contents: `export declare type MyType = string;`, - }]; + const additionalFiles: TestFile[] = [ + { + name: absoluteFrom('/node_modules/types/index.d.ts'), + contents: `export declare type MyType = string;`, + }, + ]; const emitter = createEmitter( - ` + ` import {MyType} from 'types'; export class TestClass {}`, - additionalFiles); + additionalFiles, + ); expect(emit(emitter)).toEqual(''); }); it('transforms generic type parameter defaults', () => { - const additionalFiles: TestFile[] = [{ - name: absoluteFrom('/node_modules/types/index.d.ts'), - contents: `export declare type MyType = string;`, - }]; + const additionalFiles: TestFile[] = [ + { + name: absoluteFrom('/node_modules/types/index.d.ts'), + contents: `export declare type MyType = string;`, + }, + ]; const emitter = createEmitter( - ` + ` import {MyType} from 'types'; export class TestClass {}`, - additionalFiles); + additionalFiles, + ); expect(emit(emitter)).toEqual(''); }); @@ -377,8 +447,9 @@ runInEachFileSystem(() => { }); it('can opt into emitting references to ambient types', () => { - const emitter = - createEmitter(`export class TestClass {}`, [typescriptLibDts()]); + const emitter = createEmitter(`export class TestClass {}`, [ + typescriptLibDts(), + ]); expect(emit(emitter, ImportFlags.AllowAmbientReferences)).toBe(''); }); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts b/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts index fca1263aa660e..1052e75db9c15 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/testing/index.ts @@ -6,24 +6,82 @@ * found in the LICENSE file at https://angular.io/license */ -import {BindingPipe, CssSelector, ParseSourceFile, ParseSourceSpan, parseTemplate, ParseTemplateOptions, R3TargetBinder, SchemaMetadata, SelectorMatcher, TmplAstElement} from '@angular/compiler'; +import { + BindingPipe, + CssSelector, + ParseSourceFile, + ParseSourceSpan, + parseTemplate, + ParseTemplateOptions, + R3TargetBinder, + SchemaMetadata, + SelectorMatcher, + TmplAstElement, +} from '@angular/compiler'; import {readFileSync} from 'fs'; import path from 'path'; import ts from 'typescript'; -import {absoluteFrom, AbsoluteFsPath, getSourceFileOrError, LogicalFileSystem} from '../../file_system'; +import { + absoluteFrom, + AbsoluteFsPath, + getSourceFileOrError, + LogicalFileSystem, +} from '../../file_system'; import {TestFile} from '../../file_system/testing'; -import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, Reference, ReferenceEmitter, RelativePathStrategy} from '../../imports'; +import { + AbsoluteModuleStrategy, + LocalIdentifierStrategy, + LogicalProjectStrategy, + ModuleResolver, + Reference, + ReferenceEmitter, + RelativePathStrategy, +} from '../../imports'; import {NOOP_INCREMENTAL_BUILD} from '../../incremental'; -import {ClassPropertyMapping, CompoundMetadataReader, DecoratorInputTransform, DirectiveMeta, HostDirectivesResolver, InputMapping, MatchSource, MetadataReaderWithIndex, MetaKind, NgModuleIndex, PipeMeta} from '../../metadata'; +import { + ClassPropertyMapping, + CompoundMetadataReader, + DecoratorInputTransform, + DirectiveMeta, + HostDirectivesResolver, + InputMapping, + MatchSource, + MetadataReaderWithIndex, + MetaKind, + NgModuleIndex, + PipeMeta, +} from '../../metadata'; import {NOOP_PERF_RECORDER} from '../../perf'; import {TsCreateProgramDriver} from '../../program_driver'; -import {ClassDeclaration, isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection'; -import {ComponentScopeKind, ComponentScopeReader, LocalModuleScope, ScopeData, TypeCheckScopeRegistry} from '../../scope'; +import { + ClassDeclaration, + isNamedClassDeclaration, + TypeScriptReflectionHost, +} from '../../reflection'; +import { + ComponentScopeKind, + ComponentScopeReader, + LocalModuleScope, + ScopeData, + TypeCheckScopeRegistry, +} from '../../scope'; import {makeProgram, resolveFromRunfiles} from '../../testing'; import {getRootDirs} from '../../util/src/typescript'; -import {OptimizeFor, ProgramTypeCheckAdapter, TemplateDiagnostic, TemplateTypeChecker, TypeCheckContext} from '../api'; -import {TemplateId, TemplateSourceMapping, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata, TypeCheckingConfig} from '../api/api'; +import { + OptimizeFor, + ProgramTypeCheckAdapter, + TemplateDiagnostic, + TemplateTypeChecker, + TypeCheckContext, +} from '../api'; +import { + TemplateId, + TemplateSourceMapping, + TypeCheckableDirectiveMeta, + TypeCheckBlockMetadata, + TypeCheckingConfig, +} from '../api/api'; import {TemplateTypeCheckerImpl} from '../src/checker'; import {DomSchemaChecker} from '../src/dom'; import {OutOfBandDiagnosticRecorder} from '../src/oob'; @@ -87,7 +145,7 @@ export function typescriptLibDts(): TestFile { createElement(tagName: string): HTMLElement; } declare const document: Document; - ` + `, }; } @@ -113,7 +171,7 @@ export function angularAnimationsDts(): TestFile { export declare class AnimationEvent { element: any; } - ` + `, }; } @@ -224,25 +282,37 @@ export const ALL_ENABLED_CONFIG: Readonly = { }; // Remove 'ref' from TypeCheckableDirectiveMeta and add a 'selector' instead. -export interface TestDirective extends Partial>> { + | 'ref' + | 'coercedInputFields' + | 'restrictedInputFields' + | 'stringLiteralInputFields' + | 'undeclaredInputFields' + | 'inputs' + | 'outputs' + | 'hostDirectives' + > + > + > { selector: string; name: string; file?: AbsoluteFsPath; type: 'directive'; inputs?: { [fieldName: string]: - string|{ + | string + | { classPropertyName: string; bindingPropertyName: string; required: boolean; isSignal: boolean; - transform: DecoratorInputTransform|null; - } + transform: DecoratorInputTransform | null; + }; }; outputs?: {[fieldName: string]: string}; coercedInputFields?: string[]; @@ -251,12 +321,12 @@ export interface TestDirective extends Partial, - options?: {emitSpans?: boolean}, templateParserOptions?: ParseTemplateOptions): string { + template: string, + declarations: TestDeclaration[] = [], + config?: Partial, + options?: {emitSpans?: boolean}, + templateParserOptions?: ParseTemplateOptions, +): string { const codeLines = [`export class Test {}`]; (function addCodeLines(currentDeclarations) { for (const decl of currentDeclarations) { if (decl.type === 'directive' && decl.hostDirectives) { - addCodeLines(decl.hostDirectives.map(hostDir => hostDir.directive)); + addCodeLines(decl.hostDirectives.map((hostDir) => hostDir.directive)); } codeLines.push(decl.code ?? `export class ${decl.name} {}`); @@ -300,14 +374,23 @@ export function tcb( throw new Error('Template parse errors: \n' + errors.join('\n')); } - const {matcher, pipes} = - prepareDeclarations(declarations, decl => getClass(sf, decl.name), new Map()); + const {matcher, pipes} = prepareDeclarations( + declarations, + (decl) => getClass(sf, decl.name), + new Map(), + ); const binder = new R3TargetBinder(matcher); const boundTarget = binder.bind({template: nodes}); const id = 'tcb' as TemplateId; - const meta: TypeCheckBlockMetadata = - {id, boundTarget, pipes, schemas: [], isStandalone: false, preserveWhitespaces: false}; + const meta: TypeCheckBlockMetadata = { + id, + boundTarget, + pipes, + schemas: [], + isStandalone: false, + preserveWhitespaces: false, + }; const fullConfig: TypeCheckingConfig = { applyTemplateContextGuards: true, @@ -334,7 +417,7 @@ export function tcb( useInlineTypeConstructors: true, suggestionsForSuboptimalTypeInference: false, allowSignalsInTwoWayBindings: true, - ...config + ...config, }; options = options || { emitSpans: false, @@ -344,14 +427,20 @@ export function tcb( const reflectionHost = new TypeScriptReflectionHost(program.getTypeChecker()); - const refEmmiter: ReferenceEmitter = new ReferenceEmitter( - [new LocalIdentifierStrategy(), new RelativePathStrategy(reflectionHost)]); + const refEmmiter: ReferenceEmitter = new ReferenceEmitter([ + new LocalIdentifierStrategy(), + new RelativePathStrategy(reflectionHost), + ]); const env = new TypeCheckFile(fileName, fullConfig, refEmmiter, reflectionHost, host); env.addTypeCheckBlock( - new Reference(clazz), meta, new NoopSchemaChecker(), new NoopOobRecorder(), - TcbGenericContextBehavior.UseEmitter); + new Reference(clazz), + meta, + new NoopSchemaChecker(), + new NoopOobRecorder(), + TcbGenericContextBehavior.UseEmitter, + ); const rendered = env.render(!options.emitSpans /* removeComments */); return rendered.replace(/\s+/g, ' '); @@ -395,20 +484,19 @@ export interface TypeCheckingTarget { * configuration. In many cases, it's not even necessary to include source code for test files, as * that can be auto-generated based on the provided target configuration. */ -export function setup(targets: TypeCheckingTarget[], overrides: { - config?: Partial, - options?: ts.CompilerOptions, - inlining?: boolean, -} = {}): { - templateTypeChecker: TemplateTypeChecker, - program: ts.Program, - programStrategy: TsCreateProgramDriver, +export function setup( + targets: TypeCheckingTarget[], + overrides: { + config?: Partial; + options?: ts.CompilerOptions; + inlining?: boolean; + } = {}, +): { + templateTypeChecker: TemplateTypeChecker; + program: ts.Program; + programStrategy: TsCreateProgramDriver; } { - const files = [ - typescriptLibDts(), - ...angularCoreDtsFiles(), - angularAnimationsDts(), - ]; + const files = [typescriptLibDts(), ...angularCoreDtsFiles(), angularAnimationsDts()]; const fakeMetadataRegistry = new Map(); for (const target of targets) { @@ -439,32 +527,43 @@ export function setup(targets: TypeCheckingTarget[], overrides: { const config = overrides.config ?? {}; const {program, host, options} = makeProgram( - files, { - strictNullChecks: true, - skipLibCheck: true, - noImplicitAny: true, - ...opts, - }, - /* host */ undefined, - /* checkForErrors */ false); + files, + { + strictNullChecks: true, + skipLibCheck: true, + noImplicitAny: true, + ...opts, + }, + /* host */ undefined, + /* checkForErrors */ false, + ); const checker = program.getTypeChecker(); const logicalFs = new LogicalFileSystem(getRootDirs(host, options), host); const reflectionHost = new TypeScriptReflectionHost(checker); - const moduleResolver = - new ModuleResolver(program, options, host, /* moduleResolutionCache */ null); + const moduleResolver = new ModuleResolver( + program, + options, + host, + /* moduleResolutionCache */ null, + ); const emitter = new ReferenceEmitter([ new LocalIdentifierStrategy(), new AbsoluteModuleStrategy( - program, checker, moduleResolver, new TypeScriptReflectionHost(checker)), + program, + checker, + moduleResolver, + new TypeScriptReflectionHost(checker), + ), new LogicalProjectStrategy(reflectionHost, logicalFs), ]); const fullConfig = { ...ALL_ENABLED_CONFIG, - useInlineTypeConstructors: overrides.inlining !== undefined ? - overrides.inlining : - ALL_ENABLED_CONFIG.useInlineTypeConstructors, - ...config + useInlineTypeConstructors: + overrides.inlining !== undefined + ? overrides.inlining + : ALL_ENABLED_CONFIG.useInlineTypeConstructors, + ...config, }; // Map out the scope of each target component, which is needed for the ComponentScopeReader. @@ -497,16 +596,20 @@ export function setup(targets: TypeCheckingTarget[], overrides: { throw new Error('Template parse errors: \n' + errors.join('\n')); } - const {matcher, pipes} = prepareDeclarations(declarations, decl => { - let declFile = sf; - if (decl.file !== undefined) { - declFile = program.getSourceFile(decl.file)!; - if (declFile === undefined) { - throw new Error(`Unable to locate ${decl.file} for ${decl.type} ${decl.name}`); + const {matcher, pipes} = prepareDeclarations( + declarations, + (decl) => { + let declFile = sf; + if (decl.file !== undefined) { + declFile = program.getSourceFile(decl.file)!; + if (declFile === undefined) { + throw new Error(`Unable to locate ${decl.file} for ${decl.type} ${decl.name}`); + } } - } - return getClass(declFile, decl.name); - }, fakeMetadataRegistry); + return getClass(declFile, decl.name); + }, + fakeMetadataRegistry, + ); const binder = new R3TargetBinder(matcher); const classRef = new Reference(classDecl); @@ -520,7 +623,17 @@ export function setup(targets: TypeCheckingTarget[], overrides: { }; ctx.addTemplate( - classRef, binder, nodes, pipes, [], sourceMapping, templateFile, errors, false, false); + classRef, + binder, + nodes, + pipes, + [], + sourceMapping, + templateFile, + errors, + false, + false, + ); } } }); @@ -536,55 +649,67 @@ export function setup(targets: TypeCheckingTarget[], overrides: { }, // If there is a module with [className] + 'Module' in the same source file, that will be // returned as the NgModule for the class. - getScopeForComponent(clazz: ClassDeclaration): LocalModuleScope | - null { - try { - const ngModule = getClass(clazz.getSourceFile(), `${clazz.name.getText()}Module`); - - if (!scopeMap.has(clazz)) { - // This class wasn't part of the target set of components with templates, but is - // probably a declaration used in one of them. Return an empty scope. - const emptyScope: ScopeData = { - dependencies: [], - isPoisoned: false, - }; - return { - kind: ComponentScopeKind.NgModule, - ngModule, - compilation: emptyScope, - reexports: [], - schemas: [], - exported: emptyScope, - - }; - } - const scope = scopeMap.get(clazz)!; - - return { - kind: ComponentScopeKind.NgModule, - ngModule, - compilation: scope, - reexports: [], - schemas: [], - exported: scope, - }; - } catch (e) { - // No NgModule was found for this class, so it has no scope. - return null; - } + getScopeForComponent(clazz: ClassDeclaration): LocalModuleScope | null { + try { + const ngModule = getClass(clazz.getSourceFile(), `${clazz.name.getText()}Module`); + + if (!scopeMap.has(clazz)) { + // This class wasn't part of the target set of components with templates, but is + // probably a declaration used in one of them. Return an empty scope. + const emptyScope: ScopeData = { + dependencies: [], + isPoisoned: false, + }; + return { + kind: ComponentScopeKind.NgModule, + ngModule, + compilation: emptyScope, + reexports: [], + schemas: [], + exported: emptyScope, + }; } + const scope = scopeMap.get(clazz)!; + + return { + kind: ComponentScopeKind.NgModule, + ngModule, + compilation: scope, + reexports: [], + schemas: [], + exported: scope, + }; + } catch (e) { + // No NgModule was found for this class, so it has no scope. + return null; + } + }, }; const fakeMetadataReader = getFakeMetadataReader(fakeMetadataRegistry); const fakeNgModuleIndex = getFakeNgModuleIndex(fakeMetadataRegistry); const typeCheckScopeRegistry = new TypeCheckScopeRegistry( - fakeScopeReader, new CompoundMetadataReader([fakeMetadataReader]), - new HostDirectivesResolver(fakeMetadataReader)); + fakeScopeReader, + new CompoundMetadataReader([fakeMetadataReader]), + new HostDirectivesResolver(fakeMetadataReader), + ); const templateTypeChecker = new TemplateTypeCheckerImpl( - program, programStrategy, checkAdapter, fullConfig, emitter, reflectionHost, host, - NOOP_INCREMENTAL_BUILD, fakeMetadataReader, fakeMetadataReader, fakeNgModuleIndex, - fakeScopeReader, typeCheckScopeRegistry, NOOP_PERF_RECORDER); + program, + programStrategy, + checkAdapter, + fullConfig, + emitter, + reflectionHost, + host, + NOOP_INCREMENTAL_BUILD, + fakeMetadataReader, + fakeMetadataReader, + fakeNgModuleIndex, + fakeScopeReader, + typeCheckScopeRegistry, + NOOP_PERF_RECORDER, + ); return { templateTypeChecker, program, @@ -598,31 +723,36 @@ export function setup(targets: TypeCheckingTarget[], overrides: { * @returns a list of error diagnostics. */ export function diagnose( - template: string, source: string, declarations?: TestDeclaration[], - additionalSources: TestFile[] = [], config?: Partial, - options?: ts.CompilerOptions): string[] { + template: string, + source: string, + declarations?: TestDeclaration[], + additionalSources: TestFile[] = [], + config?: Partial, + options?: ts.CompilerOptions, +): string[] { const sfPath = absoluteFrom('/main.ts'); const {program, templateTypeChecker} = setup( - [ - { - fileName: sfPath, - templates: { - 'TestComponent': template, - }, - source, - declarations, + [ + { + fileName: sfPath, + templates: { + 'TestComponent': template, }, - ...additionalSources.map(testFile => ({ - fileName: testFile.name, - source: testFile.contents, - templates: {}, - })), - ], - {config, options}); + source, + declarations, + }, + ...additionalSources.map((testFile) => ({ + fileName: testFile.name, + source: testFile.contents, + templates: {}, + })), + ], + {config, options}, + ); const sf = getSourceFileOrError(program, sfPath); const diagnostics = templateTypeChecker.getDiagnosticsForFile(sf, OptimizeFor.WholeProgram); - return diagnostics.map(diag => { + return diagnostics.map((diag) => { const text = ts.flattenDiagnosticMessageText(diag.messageText, '\n'); const fileName = diag.file!.fileName; const {line, character} = ts.getLineAndCharacterOfPosition(diag.file!, diag.start!); @@ -630,51 +760,55 @@ export function diagnose( }); } -function createTypeCheckAdapter(fn: (sf: ts.SourceFile, ctx: TypeCheckContext) => void): - ProgramTypeCheckAdapter { +function createTypeCheckAdapter( + fn: (sf: ts.SourceFile, ctx: TypeCheckContext) => void, +): ProgramTypeCheckAdapter { return {typeCheck: fn}; } -function getFakeMetadataReader(fakeMetadataRegistry: Map): - MetadataReaderWithIndex { +function getFakeMetadataReader( + fakeMetadataRegistry: Map, +): MetadataReaderWithIndex { return { - getDirectiveMetadata(node: Reference): DirectiveMeta | - null { - return fakeMetadataRegistry.get(node.debugName) ?? null; - }, + getDirectiveMetadata(node: Reference): DirectiveMeta | null { + return fakeMetadataRegistry.get(node.debugName) ?? null; + }, getKnown(kind: MetaKind): Array { switch (kind) { // TODO: This is not needed for these ngtsc tests, but may be wanted in the future. default: return []; } - } + }, } as MetadataReaderWithIndex; } -function getFakeNgModuleIndex(fakeMetadataRegistry: Map): NgModuleIndex { +function getFakeNgModuleIndex(fakeMetadataRegistry: Map): NgModuleIndex { return { getNgModulesExporting(trait: ClassDeclaration): Array> { return []; - } + }, } as NgModuleIndex; } type DeclarationResolver = (decl: TestDeclaration) => ClassDeclaration; function prepareDeclarations( - declarations: TestDeclaration[], resolveDeclaration: DeclarationResolver, - metadataRegistry: Map) { + declarations: TestDeclaration[], + resolveDeclaration: DeclarationResolver, + metadataRegistry: Map, +) { const matcher = new SelectorMatcher(); const pipes = new Map(); const hostDirectiveResolder = new HostDirectivesResolver( - getFakeMetadataReader(metadataRegistry as Map)); + getFakeMetadataReader(metadataRegistry as Map), + ); const directives: DirectiveMeta[] = []; const registerDirective = (decl: TestDirective) => { const meta = getDirectiveMetaFromDeclaration(decl, resolveDeclaration); directives.push(meta as DirectiveMeta); metadataRegistry.set(decl.name, meta); - decl.hostDirectives?.forEach(hostDecl => registerDirective(hostDecl.directive)); + decl.hostDirectives?.forEach((hostDecl) => registerDirective(hostDecl.directive)); }; for (const decl of declarations) { @@ -688,7 +822,7 @@ function prepareDeclarations( nameExpr: null, isStandalone: false, decorator: null, - isExplicitlyDeferred: false + isExplicitlyDeferred: false, }); } } @@ -714,7 +848,9 @@ export function getClass(sf: ts.SourceFile, name: string): ClassDeclaration { - return { - directive: new Reference(resolveDeclaration(hostDecl.directive)), - inputs: parseInputOutputMappingArray(hostDecl.inputs || []), - outputs: parseInputOutputMappingArray(hostDecl.outputs || []) - }; - }), + hostDirectives: + decl.hostDirectives === undefined + ? null + : decl.hostDirectives.map((hostDecl) => { + return { + directive: new Reference(resolveDeclaration(hostDecl.directive)), + inputs: parseInputOutputMappingArray(hostDecl.inputs || []), + outputs: parseInputOutputMappingArray(hostDecl.outputs || []), + }; + }), } as TypeCheckableDirectiveMeta; } @@ -800,19 +939,24 @@ function makeScope(program: ts.Program, sf: ts.SourceFile, decls: TestDeclaratio preserveWhitespaces: decl.preserveWhitespaces ?? false, isExplicitlyDeferred: false, hostDirectives: - decl.hostDirectives === undefined ? null : decl.hostDirectives.map(hostDecl => { - return { - directive: new Reference(getClass( - hostDecl.directive.file ? - getSourceFileOrError(program, hostDecl.directive.file) : - sf, - hostDecl.directive.name)), - origin: sf, - isForwardReference: false, - inputs: hostDecl.inputs || {}, - outputs: hostDecl.outputs || {}, - }; - }), + decl.hostDirectives === undefined + ? null + : decl.hostDirectives.map((hostDecl) => { + return { + directive: new Reference( + getClass( + hostDecl.directive.file + ? getSourceFileOrError(program, hostDecl.directive.file) + : sf, + hostDecl.directive.name, + ), + ), + origin: sf, + isForwardReference: false, + inputs: hostDecl.inputs || {}, + outputs: hostDecl.outputs || {}, + }; + }), }); } else if (decl.type === 'pipe') { scope.dependencies.push({ @@ -831,13 +975,16 @@ function makeScope(program: ts.Program, sf: ts.SourceFile, decls: TestDeclaratio } function parseInputOutputMappingArray(values: string[]) { - return values.reduce((results, value) => { - // Either the value is 'field' or 'field: property'. In the first case, `property` will - // be undefined, in which case the field name should also be used as the property name. - const [field, property] = value.split(':', 2).map(str => str.trim()); - results[field] = property || field; - return results; - }, {} as {[field: string]: string}); + return values.reduce( + (results, value) => { + // Either the value is 'field' or 'field: property'. In the first case, `property` will + // be undefined, in which case the field name should also be used as the property name. + const [field, property] = value.split(':', 2).map((str) => str.trim()); + results[field] = property || field; + return results; + }, + {} as {[field: string]: string}, + ); } export class NoopSchemaChecker implements DomSchemaChecker { @@ -846,11 +993,19 @@ export class NoopSchemaChecker implements DomSchemaChecker { } checkElement( - id: string, element: TmplAstElement, schemas: SchemaMetadata[], - hostIsStandalone: boolean): void {} + id: string, + element: TmplAstElement, + schemas: SchemaMetadata[], + hostIsStandalone: boolean, + ): void {} checkProperty( - id: string, element: TmplAstElement, name: string, span: ParseSourceSpan, - schemas: SchemaMetadata[], hostIsStandalone: boolean): void {} + id: string, + element: TmplAstElement, + name: string, + span: ParseSourceSpan, + schemas: SchemaMetadata[], + hostIsStandalone: boolean, + ): void {} } export class NoopOobRecorder implements OutOfBandDiagnosticRecorder { diff --git a/packages/compiler-cli/src/ngtsc/util/src/path.ts b/packages/compiler-cli/src/ngtsc/util/src/path.ts index 81bf17f5a91ad..0394052e695d6 100644 --- a/packages/compiler-cli/src/ngtsc/util/src/path.ts +++ b/packages/compiler-cli/src/ngtsc/util/src/path.ts @@ -8,7 +8,7 @@ import {dirname, relative, resolve, toRelativeImport} from '../../file_system'; import {stripExtension} from '../../file_system/src/util'; -export function relativePathBetween(from: string, to: string): string|null { +export function relativePathBetween(from: string, to: string): string | null { const relativePath = stripExtension(relative(dirname(resolve(from)), resolve(to))); return relativePath !== '' ? toRelativeImport(relativePath) : null; } diff --git a/packages/compiler-cli/src/ngtsc/util/src/typescript.ts b/packages/compiler-cli/src/ngtsc/util/src/typescript.ts index 31e2553a0430a..b3233caa3644e 100644 --- a/packages/compiler-cli/src/ngtsc/util/src/typescript.ts +++ b/packages/compiler-cli/src/ngtsc/util/src/typescript.ts @@ -16,18 +16,20 @@ import {DeclarationNode} from '../../reflection'; /** * Type describing a symbol that is guaranteed to have a value declaration. */ -export type SymbolWithValueDeclaration = ts.Symbol&{ +export type SymbolWithValueDeclaration = ts.Symbol & { valueDeclaration: ts.Declaration; declarations: ts.Declaration[]; }; -export function isSymbolWithValueDeclaration(symbol: ts.Symbol|null| - undefined): symbol is SymbolWithValueDeclaration { +export function isSymbolWithValueDeclaration( + symbol: ts.Symbol | null | undefined, +): symbol is SymbolWithValueDeclaration { // If there is a value declaration set, then the `declarations` property is never undefined. We // still check for the property to exist as this matches with the type that `symbol` is narrowed // to. - return symbol != null && symbol.valueDeclaration !== undefined && - symbol.declarations !== undefined; + return ( + symbol != null && symbol.valueDeclaration !== undefined && symbol.declarations !== undefined + ); } export function isDtsPath(filePath: string): boolean { @@ -39,20 +41,22 @@ export function isNonDeclarationTsPath(filePath: string): boolean { } export function isFromDtsFile(node: ts.Node): boolean { - let sf: ts.SourceFile|undefined = node.getSourceFile(); + let sf: ts.SourceFile | undefined = node.getSourceFile(); if (sf === undefined) { sf = ts.getOriginalNode(node).getSourceFile(); } return sf !== undefined && sf.isDeclarationFile; } -export function nodeNameForError(node: ts.Node&{name?: ts.Node}): string { +export function nodeNameForError(node: ts.Node & {name?: ts.Node}): string { if (node.name !== undefined && ts.isIdentifier(node.name)) { return node.name.text; } else { const kind = ts.SyntaxKind[node.kind]; - const {line, character} = - ts.getLineAndCharacterOfPosition(node.getSourceFile(), node.getStart()); + const {line, character} = ts.getLineAndCharacterOfPosition( + node.getSourceFile(), + node.getStart(), + ); return `${kind}@${line}:${character}`; } } @@ -65,18 +69,19 @@ export function getSourceFile(node: ts.Node): ts.SourceFile { return directSf !== undefined ? directSf : ts.getOriginalNode(node).getSourceFile(); } -export function getSourceFileOrNull(program: ts.Program, fileName: AbsoluteFsPath): ts.SourceFile| - null { +export function getSourceFileOrNull( + program: ts.Program, + fileName: AbsoluteFsPath, +): ts.SourceFile | null { return program.getSourceFile(fileName) || null; } - export function getTokenAtPosition(sf: ts.SourceFile, pos: number): ts.Node { // getTokenAtPosition is part of TypeScript's private API. return (ts as any).getTokenAtPosition(sf, pos); } -export function identifierOfNode(decl: ts.Node&{name?: ts.Node}): ts.Identifier|null { +export function identifierOfNode(decl: ts.Node & {name?: ts.Node}): ts.Identifier | null { if (decl.name !== undefined && ts.isIdentifier(decl.name)) { return decl.name; } else { @@ -88,19 +93,23 @@ export function isDeclaration(node: ts.Node): node is ts.Declaration { return isValueDeclaration(node) || isTypeDeclaration(node); } -export function isValueDeclaration(node: ts.Node): node is ts.ClassDeclaration| - ts.FunctionDeclaration|ts.VariableDeclaration { - return ts.isClassDeclaration(node) || ts.isFunctionDeclaration(node) || - ts.isVariableDeclaration(node); +export function isValueDeclaration( + node: ts.Node, +): node is ts.ClassDeclaration | ts.FunctionDeclaration | ts.VariableDeclaration { + return ( + ts.isClassDeclaration(node) || ts.isFunctionDeclaration(node) || ts.isVariableDeclaration(node) + ); } -export function isTypeDeclaration(node: ts.Node): node is ts.EnumDeclaration| - ts.TypeAliasDeclaration|ts.InterfaceDeclaration { - return ts.isEnumDeclaration(node) || ts.isTypeAliasDeclaration(node) || - ts.isInterfaceDeclaration(node); +export function isTypeDeclaration( + node: ts.Node, +): node is ts.EnumDeclaration | ts.TypeAliasDeclaration | ts.InterfaceDeclaration { + return ( + ts.isEnumDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node) + ); } -export function isNamedDeclaration(node: ts.Node): node is ts.Declaration&{name: ts.Identifier} { +export function isNamedDeclaration(node: ts.Node): node is ts.Declaration & {name: ts.Identifier} { const namedNode = node as {name?: ts.Identifier}; return namedNode.name !== undefined && ts.isIdentifier(namedNode.name); } @@ -111,13 +120,16 @@ export function isExported(node: DeclarationNode): boolean { topLevel = node.parent.parent; } const modifiers = ts.canHaveModifiers(topLevel) ? ts.getModifiers(topLevel) : undefined; - return modifiers !== undefined && - modifiers.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword); + return ( + modifiers !== undefined && + modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) + ); } export function getRootDirs( - host: Pick, - options: ts.CompilerOptions): AbsoluteFsPath[] { + host: Pick, + options: ts.CompilerOptions, +): AbsoluteFsPath[] { const rootDirs: string[] = []; const cwd = host.getCurrentDirectory(); const fs = getFileSystem(); @@ -133,7 +145,7 @@ export function getRootDirs( // See: // https://github.com/Microsoft/TypeScript/blob/3f7357d37f66c842d70d835bc925ec2a873ecfec/src/compiler/sys.ts#L650 // Also compiler options might be set via an API which doesn't normalize paths - return rootDirs.map(rootDir => fs.resolve(cwd, host.getCanonicalFileName(rootDir))); + return rootDirs.map((rootDir) => fs.resolve(cwd, host.getCanonicalFileName(rootDir))); } export function nodeDebugInfo(node: ts.Node): string { @@ -149,21 +161,28 @@ export function nodeDebugInfo(node: ts.Node): string { * Otherwise it will fallback on the `ts.ResolveModuleName()` function. */ export function resolveModuleName( - moduleName: string, containingFile: string, compilerOptions: ts.CompilerOptions, - compilerHost: ts.ModuleResolutionHost&Pick, - moduleResolutionCache: ts.ModuleResolutionCache|null): ts.ResolvedModule|undefined { + moduleName: string, + containingFile: string, + compilerOptions: ts.CompilerOptions, + compilerHost: ts.ModuleResolutionHost & Pick, + moduleResolutionCache: ts.ModuleResolutionCache | null, +): ts.ResolvedModule | undefined { if (compilerHost.resolveModuleNames) { return compilerHost.resolveModuleNames( - [moduleName], containingFile, - undefined, // reusedNames - undefined, // redirectedReference - compilerOptions)[0]; + [moduleName], + containingFile, + undefined, // reusedNames + undefined, // redirectedReference + compilerOptions, + )[0]; } else { - return ts - .resolveModuleName( - moduleName, containingFile, compilerOptions, compilerHost, - moduleResolutionCache !== null ? moduleResolutionCache : undefined) - .resolvedModule; + return ts.resolveModuleName( + moduleName, + containingFile, + compilerOptions, + compilerHost, + moduleResolutionCache !== null ? moduleResolutionCache : undefined, + ).resolvedModule; } } @@ -191,7 +210,7 @@ export type RequiredDelegations = { * `redirectInfo` property that refers to the original source file. */ interface RedirectedSourceFile extends ts.SourceFile { - redirectInfo?: {unredirected: ts.SourceFile;}; + redirectInfo?: {unredirected: ts.SourceFile}; } /** diff --git a/packages/compiler-cli/src/ngtsc/util/src/visitor.ts b/packages/compiler-cli/src/ngtsc/util/src/visitor.ts index e41ddac539564..b471d411bcd43 100644 --- a/packages/compiler-cli/src/ngtsc/util/src/visitor.ts +++ b/packages/compiler-cli/src/ngtsc/util/src/visitor.ts @@ -13,16 +13,19 @@ import ts from 'typescript'; * nodes should be added before the visited node in the output. */ export type VisitListEntryResult = { - node: T, - before?: B[], - after?: B[], + node: T; + before?: B[]; + after?: B[]; }; /** * Visit a node with the given visitor and return a transformed copy. */ export function visit( - node: T, visitor: Visitor, context: ts.TransformationContext): T { + node: T, + visitor: Visitor, + context: ts.TransformationContext, +): T { return visitor._visit(node, context); } @@ -45,11 +48,14 @@ export abstract class Visitor { * Visit a class declaration, returning at least the transformed declaration and optionally other * nodes to insert before the declaration. */ - abstract visitClassDeclaration(node: ts.ClassDeclaration): - VisitListEntryResult; + abstract visitClassDeclaration( + node: ts.ClassDeclaration, + ): VisitListEntryResult; private _visitListEntryNode( - node: T, visitor: (node: T) => VisitListEntryResult): T { + node: T, + visitor: (node: T) => VisitListEntryResult, + ): T { const result = visitor(node); if (result.before !== undefined) { // Record that some nodes should be inserted before the given declaration. The declaration's @@ -76,14 +82,14 @@ export abstract class Visitor { _visit(node: T, context: ts.TransformationContext): T { // First, visit the node. visitedNode starts off as `null` but should be set after visiting // is completed. - let visitedNode: T|null = null; + let visitedNode: T | null = null; - node = ts.visitEachChild(node, child => child && this._visit(child, context), context) as T; + node = ts.visitEachChild(node, (child) => child && this._visit(child, context), context) as T; if (ts.isClassDeclaration(node)) { - visitedNode = - this._visitListEntryNode( - node, (node: ts.ClassDeclaration) => this.visitClassDeclaration(node)) as typeof node; + visitedNode = this._visitListEntryNode(node, (node: ts.ClassDeclaration) => + this.visitClassDeclaration(node), + ) as typeof node; } else { visitedNode = this.visitOtherNode(node); } @@ -97,16 +103,16 @@ export abstract class Visitor { return visitedNode; } - private _maybeProcessStatements(node: T): T { + private _maybeProcessStatements(node: T): T { // Shortcut - if every statement doesn't require nodes to be prepended or appended, // this is a no-op. - if (node.statements.every(stmt => !this._before.has(stmt) && !this._after.has(stmt))) { + if (node.statements.every((stmt) => !this._before.has(stmt) && !this._after.has(stmt))) { return node; } // Build a new list of statements and patch it onto the clone. const newStatements: ts.Statement[] = []; - node.statements.forEach(stmt => { + node.statements.forEach((stmt) => { if (this._before.has(stmt)) { newStatements.push(...(this._before.get(stmt)! as ts.Statement[])); this._before.delete(stmt); @@ -118,16 +124,23 @@ export abstract class Visitor { } }); - const statementsArray = - ts.factory.createNodeArray(newStatements, node.statements.hasTrailingComma); + const statementsArray = ts.factory.createNodeArray( + newStatements, + node.statements.hasTrailingComma, + ); if (ts.isBlock(node)) { return ts.factory.updateBlock(node, statementsArray) as T; } else { return ts.factory.updateSourceFile( - node, statementsArray, node.isDeclarationFile, node.referencedFiles, - node.typeReferenceDirectives, node.hasNoDefaultLib, node.libReferenceDirectives) as - T; + node, + statementsArray, + node.isDeclarationFile, + node.referencedFiles, + node.typeReferenceDirectives, + node.hasNoDefaultLib, + node.libReferenceDirectives, + ) as T; } } } diff --git a/packages/compiler-cli/src/ngtsc/util/test/typescript_spec.ts b/packages/compiler-cli/src/ngtsc/util/test/typescript_spec.ts index 31aa679625d7c..cff7cefe261c3 100644 --- a/packages/compiler-cli/src/ngtsc/util/test/typescript_spec.ts +++ b/packages/compiler-cli/src/ngtsc/util/test/typescript_spec.ts @@ -21,7 +21,7 @@ runInEachFileSystem(() => { it('should allow relative root directories', () => { const mockCompilerHost = { getCanonicalFileName: (val: string) => val, - getCurrentDirectory: () => '/fs-root/projects' + getCurrentDirectory: () => '/fs-root/projects', }; const result = getRootDirs(mockCompilerHost, {rootDir: './test-project-root'}); expect(result).toEqual([fs.resolve('/fs-root/projects/test-project-root')]); diff --git a/packages/compiler-cli/src/ngtsc/util/test/visitor_spec.ts b/packages/compiler-cli/src/ngtsc/util/test/visitor_spec.ts index ebe9c2551e48d..4c61789118ac9 100644 --- a/packages/compiler-cli/src/ngtsc/util/test/visitor_spec.ts +++ b/packages/compiler-cli/src/ngtsc/util/test/visitor_spec.ts @@ -13,27 +13,29 @@ import {makeProgram} from '../../testing'; import {visit, VisitListEntryResult, Visitor} from '../src/visitor'; class TestAstVisitor extends Visitor { - override visitClassDeclaration(node: ts.ClassDeclaration): - VisitListEntryResult { + override visitClassDeclaration( + node: ts.ClassDeclaration, + ): VisitListEntryResult { const name = node.name!.text; - const statics = node.members.filter(member => { + const statics = node.members.filter((member) => { const modifiers = ts.canHaveModifiers(member) ? ts.getModifiers(member) : undefined; - return (modifiers || []).some(mod => mod.kind === ts.SyntaxKind.StaticKeyword); + return (modifiers || []).some((mod) => mod.kind === ts.SyntaxKind.StaticKeyword); }); const idStatic = statics.find( - el => ts.isPropertyDeclaration(el) && ts.isIdentifier(el.name) && - el.name.text === 'id') as ts.PropertyDeclaration | - undefined; + (el) => ts.isPropertyDeclaration(el) && ts.isIdentifier(el.name) && el.name.text === 'id', + ) as ts.PropertyDeclaration | undefined; if (idStatic !== undefined) { return { node, before: [ - ts.factory.createVariableStatement( + ts.factory.createVariableStatement(undefined, [ + ts.factory.createVariableDeclaration( + `${name}_id`, undefined, - [ - ts.factory.createVariableDeclaration( - `${name}_id`, undefined, undefined, idStatic.initializer), - ]), + undefined, + idStatic.initializer, + ), + ]), ], }; } @@ -48,11 +50,12 @@ function testTransformerFactory(context: ts.TransformationContext): ts.Transform runInEachFileSystem(() => { describe('AST Visitor', () => { let _: typeof absoluteFrom; - beforeEach(() => _ = absoluteFrom); + beforeEach(() => (_ = absoluteFrom)); it('should add a statement before class in plain file', () => { - const {program, host} = - makeProgram([{name: _('/main.ts'), contents: `class A { static id = 3; }`}]); + const {program, host} = makeProgram([ + {name: _('/main.ts'), contents: `class A { static id = 3; }`}, + ]); const sf = getSourceFileOrError(program, _('/main.ts')); program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]}); const main = host.readFile('/main.js'); @@ -60,16 +63,18 @@ runInEachFileSystem(() => { }); it('should add a statement before class inside function definition', () => { - const {program, host} = makeProgram([{ - name: _('/main.ts'), - contents: ` + const {program, host} = makeProgram([ + { + name: _('/main.ts'), + contents: ` export function foo() { var x = 3; class A { static id = 2; } return A; } - ` - }]); + `, + }, + ]); const sf = getSourceFileOrError(program, _('/main.ts')); program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]}); const main = host.readFile(_('/main.js')); @@ -77,9 +82,10 @@ runInEachFileSystem(() => { }); it('handles nested statements', () => { - const {program, host} = makeProgram([{ - name: _('/main.ts'), - contents: ` + const {program, host} = makeProgram([ + { + name: _('/main.ts'), + contents: ` export class A { static id = 3; @@ -89,8 +95,9 @@ runInEachFileSystem(() => { } return B; } - }` - }]); + }`, + }, + ]); const sf = getSourceFileOrError(program, _('/main.ts')); program.emit(sf, undefined, undefined, undefined, {before: [testTransformerFactory]}); const main = host.readFile(_('/main.js')); diff --git a/packages/compiler-cli/src/ngtsc/validation/src/rules/api.ts b/packages/compiler-cli/src/ngtsc/validation/src/rules/api.ts index 0ebb035b2d6e4..3181dfa547063 100644 --- a/packages/compiler-cli/src/ngtsc/validation/src/rules/api.ts +++ b/packages/compiler-cli/src/ngtsc/validation/src/rules/api.ts @@ -23,5 +23,5 @@ export interface SourceFileValidatorRule { * contain the issue that the rule is enforcing. * @param node Node to be checked. */ - checkNode(node: ts.Node): ts.Diagnostic|ts.Diagnostic[]|null; + checkNode(node: ts.Node): ts.Diagnostic | ts.Diagnostic[] | null; } diff --git a/packages/compiler-cli/src/ngtsc/validation/src/rules/initializer_api_usage_rule.ts b/packages/compiler-cli/src/ngtsc/validation/src/rules/initializer_api_usage_rule.ts index 943117ac4c446..2e59f3769204d 100644 --- a/packages/compiler-cli/src/ngtsc/validation/src/rules/initializer_api_usage_rule.ts +++ b/packages/compiler-cli/src/ngtsc/validation/src/rules/initializer_api_usage_rule.ts @@ -8,7 +8,14 @@ import ts from 'typescript'; -import {InitializerApiFunction, INPUT_INITIALIZER_FN, MODEL_INITIALIZER_FN, OUTPUT_INITIALIZER_FNS, QUERY_INITIALIZER_FNS, tryParseInitializerApi} from '../../../annotations'; +import { + InitializerApiFunction, + INPUT_INITIALIZER_FN, + MODEL_INITIALIZER_FN, + OUTPUT_INITIALIZER_FNS, + QUERY_INITIALIZER_FNS, + tryParseInitializerApi, +} from '../../../annotations'; import {ErrorCode, makeDiagnostic} from '../../../diagnostics'; import {ImportedSymbolsTracker} from '../../../imports'; import {ReflectionHost} from '../../../reflection'; @@ -28,25 +35,31 @@ const APIS_TO_CHECK: InitializerApiFunction[] = [ */ export class InitializerApiUsageRule implements SourceFileValidatorRule { constructor( - private reflector: ReflectionHost, private importedSymbolsTracker: ImportedSymbolsTracker) {} + private reflector: ReflectionHost, + private importedSymbolsTracker: ImportedSymbolsTracker, + ) {} shouldCheck(sourceFile: ts.SourceFile): boolean { // Skip the traversal if there are no imports of the initializer APIs. return APIS_TO_CHECK.some(({functionName, owningModule}) => { - return this.importedSymbolsTracker.hasNamedImport(sourceFile, functionName, owningModule) || - this.importedSymbolsTracker.hasNamespaceImport(sourceFile, owningModule); + return ( + this.importedSymbolsTracker.hasNamedImport(sourceFile, functionName, owningModule) || + this.importedSymbolsTracker.hasNamespaceImport(sourceFile, owningModule) + ); }); } - checkNode(node: ts.Node): ts.Diagnostic|null { + checkNode(node: ts.Node): ts.Diagnostic | null { // We only care about call expressions. if (!ts.isCallExpression(node)) { return null; } // Unwrap any parenthesized and `as` expressions since they don't affect the runtime behavior. - while (node.parent && - (ts.isParenthesizedExpression(node.parent) || ts.isAsExpression(node.parent))) { + while ( + node.parent && + (ts.isParenthesizedExpression(node.parent) || ts.isAsExpression(node.parent)) + ) { node = node.parent; } @@ -54,14 +67,19 @@ export class InitializerApiUsageRule implements SourceFileValidatorRule { return null; } - const identifiedInitializer = - tryParseInitializerApi(APIS_TO_CHECK, node, this.reflector, this.importedSymbolsTracker); + const identifiedInitializer = tryParseInitializerApi( + APIS_TO_CHECK, + node, + this.reflector, + this.importedSymbolsTracker, + ); if (identifiedInitializer === null) { return null; } - const functionName = identifiedInitializer.api.functionName + - (identifiedInitializer.isRequired ? '.required' : ''); + const functionName = + identifiedInitializer.api.functionName + + (identifiedInitializer.isRequired ? '.required' : ''); if (ts.isPropertyDeclaration(node.parent) && node.parent.initializer === node) { let closestClass: ts.Node = node.parent; @@ -72,24 +90,30 @@ export class InitializerApiUsageRule implements SourceFileValidatorRule { if (closestClass && ts.isClassDeclaration(closestClass)) { const decorators = this.reflector.getDecoratorsOfDeclaration(closestClass); - const isComponentOrDirective = decorators !== null && decorators.some(decorator => { - return decorator.import?.from === '@angular/core' && - (decorator.name === 'Component' || decorator.name === 'Directive'); - }); - - return isComponentOrDirective ? - null : - makeDiagnostic( - ErrorCode.UNSUPPORTED_INITIALIZER_API_USAGE, node, - `Unsupported call to the ${ - functionName} function. This function can only be used as the initializer ` + - `of a property on a @Component or @Directive class.`); + const isComponentOrDirective = + decorators !== null && + decorators.some((decorator) => { + return ( + decorator.import?.from === '@angular/core' && + (decorator.name === 'Component' || decorator.name === 'Directive') + ); + }); + + return isComponentOrDirective + ? null + : makeDiagnostic( + ErrorCode.UNSUPPORTED_INITIALIZER_API_USAGE, + node, + `Unsupported call to the ${functionName} function. This function can only be used as the initializer ` + + `of a property on a @Component or @Directive class.`, + ); } } return makeDiagnostic( - ErrorCode.UNSUPPORTED_INITIALIZER_API_USAGE, node, - `Unsupported call to the ${ - functionName} function. This function can only be called in the initializer of a class member.`); + ErrorCode.UNSUPPORTED_INITIALIZER_API_USAGE, + node, + `Unsupported call to the ${functionName} function. This function can only be called in the initializer of a class member.`, + ); } } diff --git a/packages/compiler-cli/src/ngtsc/validation/src/source_file_validator.ts b/packages/compiler-cli/src/ngtsc/validation/src/source_file_validator.ts index d1d772a9fd33e..63133ca0174c2 100644 --- a/packages/compiler-cli/src/ngtsc/validation/src/source_file_validator.ts +++ b/packages/compiler-cli/src/ngtsc/validation/src/source_file_validator.ts @@ -28,12 +28,12 @@ export class SourceFileValidator { * Gets the diagnostics for a specific file, or null if the file is valid. * @param sourceFile File to be checked. */ - getDiagnosticsForFile(sourceFile: ts.SourceFile): ts.Diagnostic[]|null { + getDiagnosticsForFile(sourceFile: ts.SourceFile): ts.Diagnostic[] | null { if (sourceFile.isDeclarationFile || sourceFile.fileName.endsWith('.ngtypecheck.ts')) { return null; } - let rulesToRun: SourceFileValidatorRule[]|null = null; + let rulesToRun: SourceFileValidatorRule[] | null = null; for (const rule of this.rules) { if (rule.shouldCheck(sourceFile)) { rulesToRun ??= []; @@ -45,7 +45,7 @@ export class SourceFileValidator { return null; } - let fileDiagnostics: ts.Diagnostic[]|null = null; + let fileDiagnostics: ts.Diagnostic[] | null = null; sourceFile.forEachChild(function walk(node) { // Note: non-null assertion is here because of g3. for (const rule of rulesToRun!) { diff --git a/packages/compiler-cli/src/perform_compile.ts b/packages/compiler-cli/src/perform_compile.ts index 11d0732cafef9..c20e0ca34780a 100644 --- a/packages/compiler-cli/src/perform_compile.ts +++ b/packages/compiler-cli/src/perform_compile.ts @@ -8,7 +8,13 @@ import ts from 'typescript'; -import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem, ReadonlyFileSystem} from '../src/ngtsc/file_system'; +import { + absoluteFrom, + AbsoluteFsPath, + FileSystem, + getFileSystem, + ReadonlyFileSystem, +} from '../src/ngtsc/file_system'; import {NgCompilerOptions} from './ngtsc/core/api'; import {replaceTsWithNgInErrors} from './ngtsc/diagnostics'; @@ -18,19 +24,20 @@ import {createMessageDiagnostic} from './transformers/util'; const defaultFormatHost: ts.FormatDiagnosticsHost = { getCurrentDirectory: () => ts.sys.getCurrentDirectory(), - getCanonicalFileName: fileName => fileName, - getNewLine: () => ts.sys.newLine + getCanonicalFileName: (fileName) => fileName, + getNewLine: () => ts.sys.newLine, }; export function formatDiagnostics( - diags: ReadonlyArray, - host: ts.FormatDiagnosticsHost = defaultFormatHost): string { + diags: ReadonlyArray, + host: ts.FormatDiagnosticsHost = defaultFormatHost, +): string { if (diags && diags.length) { return diags - .map( - diagnostic => replaceTsWithNgInErrors( - ts.formatDiagnosticsWithColorAndContext([diagnostic], host))) - .join(''); + .map((diagnostic) => + replaceTsWithNgInErrors(ts.formatDiagnosticsWithColorAndContext([diagnostic], host)), + ) + .join(''); } else { return ''; } @@ -38,20 +45,23 @@ export function formatDiagnostics( /** Used to read configuration files. */ export type ConfigurationHost = Pick< - ReadonlyFileSystem, 'readFile'|'exists'|'lstat'|'resolve'|'join'|'dirname'|'extname'|'pwd'>; + ReadonlyFileSystem, + 'readFile' | 'exists' | 'lstat' | 'resolve' | 'join' | 'dirname' | 'extname' | 'pwd' +>; export interface ParsedConfiguration { project: string; options: api.CompilerOptions; rootNames: string[]; - projectReferences?: readonly ts.ProjectReference[]|undefined; + projectReferences?: readonly ts.ProjectReference[] | undefined; emitFlags: api.EmitFlags; errors: ts.Diagnostic[]; } export function calcProjectFileAndBasePath( - project: string, host: ConfigurationHost = getFileSystem()): - {projectFile: AbsoluteFsPath, basePath: AbsoluteFsPath} { + project: string, + host: ConfigurationHost = getFileSystem(), +): {projectFile: AbsoluteFsPath; basePath: AbsoluteFsPath} { const absProject = host.resolve(project); const projectIsDir = host.lstat(absProject).isDirectory(); const projectFile = projectIsDir ? host.join(absProject, 'tsconfig.json') : absProject; @@ -62,47 +72,46 @@ export function calcProjectFileAndBasePath( } export function readConfiguration( - project: string, existingOptions?: api.CompilerOptions, - host: ConfigurationHost = getFileSystem()): ParsedConfiguration { + project: string, + existingOptions?: api.CompilerOptions, + host: ConfigurationHost = getFileSystem(), +): ParsedConfiguration { try { const fs = getFileSystem(); const readConfigFile = (configFile: string) => - ts.readConfigFile(configFile, file => host.readFile(host.resolve(file))); - const readAngularCompilerOptions = - (configFile: string, parentOptions: NgCompilerOptions = {}): NgCompilerOptions => { - const {config, error} = readConfigFile(configFile); - - if (error) { - // Errors are handled later on by 'parseJsonConfigFileContent' - return parentOptions; - } - - // we are only interested into merging 'angularCompilerOptions' as - // other options like 'compilerOptions' are merged by TS - let existingNgCompilerOptions = {...config.angularCompilerOptions, ...parentOptions}; - if (!config.extends) { - return existingNgCompilerOptions; - } - - const extendsPaths: string[] = - typeof config.extends === 'string' ? [config.extends] : config.extends; - - // Call readAngularCompilerOptions recursively to merge NG Compiler options - // Reverse the array so the overrides happen from right to left. - return [...extendsPaths].reverse().reduce((prevOptions, extendsPath) => { - const extendedConfigPath = getExtendedConfigPath( - configFile, - extendsPath, - host, - fs, - ); - - return extendedConfigPath === null ? - prevOptions : - readAngularCompilerOptions(extendedConfigPath, prevOptions); - }, existingNgCompilerOptions); - }; + ts.readConfigFile(configFile, (file) => host.readFile(host.resolve(file))); + const readAngularCompilerOptions = ( + configFile: string, + parentOptions: NgCompilerOptions = {}, + ): NgCompilerOptions => { + const {config, error} = readConfigFile(configFile); + + if (error) { + // Errors are handled later on by 'parseJsonConfigFileContent' + return parentOptions; + } + + // we are only interested into merging 'angularCompilerOptions' as + // other options like 'compilerOptions' are merged by TS + let existingNgCompilerOptions = {...config.angularCompilerOptions, ...parentOptions}; + if (!config.extends) { + return existingNgCompilerOptions; + } + + const extendsPaths: string[] = + typeof config.extends === 'string' ? [config.extends] : config.extends; + + // Call readAngularCompilerOptions recursively to merge NG Compiler options + // Reverse the array so the overrides happen from right to left. + return [...extendsPaths].reverse().reduce((prevOptions, extendsPath) => { + const extendedConfigPath = getExtendedConfigPath(configFile, extendsPath, host, fs); + + return extendedConfigPath === null + ? prevOptions + : readAngularCompilerOptions(extendedConfigPath, prevOptions); + }, existingNgCompilerOptions); + }; const {projectFile, basePath} = calcProjectFileAndBasePath(project, host); const configFileName = host.resolve(host.pwd(), projectFile); @@ -114,7 +123,7 @@ export function readConfiguration( errors: [error], rootNames: [], options: {}, - emitFlags: api.EmitFlags.Default + emitFlags: api.EmitFlags.Default, }; } @@ -126,9 +135,18 @@ export function readConfiguration( }; const parseConfigHost = createParseConfigHost(host, fs); - const {options, errors, fileNames: rootNames, projectReferences} = - ts.parseJsonConfigFileContent( - config, parseConfigHost, basePath, existingCompilerOptions, configFileName); + const { + options, + errors, + fileNames: rootNames, + projectReferences, + } = ts.parseJsonConfigFileContent( + config, + parseConfigHost, + basePath, + existingCompilerOptions, + configFileName, + ); let emitFlags = api.EmitFlags.Default; if (!(options['skipMetadataEmit'] || options['flatModuleOutFile'])) { @@ -139,15 +157,17 @@ export function readConfiguration( } return {project: projectFile, rootNames, projectReferences, options, errors, emitFlags}; } catch (e) { - const errors: ts.Diagnostic[] = [{ - category: ts.DiagnosticCategory.Error, - messageText: (e as Error).stack ?? (e as Error).message, - file: undefined, - start: undefined, - length: undefined, - source: 'angular', - code: api.UNKNOWN_ERROR_CODE, - }]; + const errors: ts.Diagnostic[] = [ + { + category: ts.DiagnosticCategory.Error, + messageText: (e as Error).stack ?? (e as Error).message, + file: undefined, + start: undefined, + length: undefined, + source: 'angular', + code: api.UNKNOWN_ERROR_CODE, + }, + ]; return {project: '', errors, rootNames: [], options: {}, emitFlags: api.EmitFlags.Default}; } } @@ -162,8 +182,11 @@ function createParseConfigHost(host: ConfigurationHost, fs = getFileSystem()): t } function getExtendedConfigPath( - configFile: string, extendsValue: string, host: ConfigurationHost, - fs: FileSystem): AbsoluteFsPath|null { + configFile: string, + extendsValue: string, + host: ConfigurationHost, + fs: FileSystem, +): AbsoluteFsPath | null { const result = getExtendedConfigPathWorker(configFile, extendsValue, host, fs); if (result !== null) { return result; @@ -176,8 +199,11 @@ function getExtendedConfigPath( } function getExtendedConfigPathWorker( - configFile: string, extendsValue: string, host: ConfigurationHost, - fs: FileSystem): AbsoluteFsPath|null { + configFile: string, + extendsValue: string, + host: ConfigurationHost, + fs: FileSystem, +): AbsoluteFsPath | null { if (extendsValue.startsWith('.') || fs.isRooted(extendsValue)) { const extendedConfigPath = host.resolve(host.dirname(configFile), extendsValue); if (host.exists(extendedConfigPath)) { @@ -187,13 +213,12 @@ function getExtendedConfigPathWorker( const parseConfigHost = createParseConfigHost(host, fs); // Path isn't a rooted or relative path, resolve like a module. - const { - resolvedModule, - } = - ts.nodeModuleNameResolver( - extendsValue, configFile, - {moduleResolution: ts.ModuleResolutionKind.Node10, resolveJsonModule: true}, - parseConfigHost); + const {resolvedModule} = ts.nodeModuleNameResolver( + extendsValue, + configFile, + {moduleResolution: ts.ModuleResolutionKind.Node10, resolveJsonModule: true}, + parseConfigHost, + ); if (resolvedModule) { return absoluteFrom(resolvedModule.resolvedFileName); } @@ -208,7 +233,7 @@ export interface PerformCompilationResult { emitResult?: ts.EmitResult; } -export function exitCodeFromResult(diags: ReadonlyArray|undefined): number { +export function exitCodeFromResult(diags: ReadonlyArray | undefined): number { if (!diags) return 0; if (diags.every((diag) => diag.category !== ts.DiagnosticCategory.Error)) { // If we have a result and didn't get any errors, we succeeded. @@ -216,7 +241,7 @@ export function exitCodeFromResult(diags: ReadonlyArray|undefined } // Return 2 if any of the errors were unknown. - return diags.some(d => d.source === 'angular' && d.code === api.UNKNOWN_ERROR_CODE) ? 2 : 1; + return diags.some((d) => d.source === 'angular' && d.code === api.UNKNOWN_ERROR_CODE) ? 2 : 1; } export function performCompilation({ @@ -230,22 +255,22 @@ export function performCompilation, - mergeEmitResultsCallback?: api.TsMergeEmitResultsCallback, - gatherDiagnostics?: (program: api.Program) => ReadonlyArray, - customTransformers?: api.CustomTransformers, - emitFlags?: api.EmitFlags, - forceEmit?: boolean, - modifiedResourceFiles?: Set| null, + rootNames: string[]; + options: api.CompilerOptions; + host?: api.CompilerHost; + oldProgram?: api.Program; + emitCallback?: api.TsEmitCallback; + mergeEmitResultsCallback?: api.TsMergeEmitResultsCallback; + gatherDiagnostics?: (program: api.Program) => ReadonlyArray; + customTransformers?: api.CustomTransformers; + emitFlags?: api.EmitFlags; + forceEmit?: boolean; + modifiedResourceFiles?: Set | null; }): PerformCompilationResult { - let program: api.Program|undefined; - let emitResult: ts.EmitResult|undefined; + let program: api.Program | undefined; + let emitResult: ts.EmitResult | undefined; let allDiagnostics: Array = []; try { if (!host) { @@ -262,12 +287,18 @@ export function performCompilation { const allDiagnostics: Array = []; - function checkDiagnostics(diags: ReadonlyArray|undefined) { + function checkDiagnostics(diags: ReadonlyArray | undefined) { if (diags) { allDiagnostics.push(...diags); return !hasErrors(diags); @@ -299,26 +330,29 @@ export function defaultGatherDiagnostics(program: api.Program): ReadonlyArray) { - return diags.some(d => d.category === ts.DiagnosticCategory.Error); + return diags.some((d) => d.category === ts.DiagnosticCategory.Error); } diff --git a/packages/compiler-cli/src/perform_watch.ts b/packages/compiler-cli/src/perform_watch.ts index 1e3970d9a7cea..6d467923b6957 100644 --- a/packages/compiler-cli/src/perform_watch.ts +++ b/packages/compiler-cli/src/perform_watch.ts @@ -10,7 +10,13 @@ import * as chokidar from 'chokidar'; import * as path from 'path'; import ts from 'typescript'; -import {exitCodeFromResult, ParsedConfiguration, performCompilation, PerformCompilationResult, readConfiguration} from './perform_compile'; +import { + exitCodeFromResult, + ParsedConfiguration, + performCompilation, + PerformCompilationResult, + readConfiguration, +} from './perform_compile'; import * as api from './transformers/api'; import {createCompilerHost} from './transformers/entry_points'; import {createMessageDiagnostic} from './transformers/util'; @@ -43,35 +49,40 @@ export interface PerformWatchHost): void; readConfiguration(): ParsedConfiguration; createCompilerHost(options: api.CompilerOptions): api.CompilerHost; - createEmitCallback(options: api.CompilerOptions): api.TsEmitCallback|undefined; + createEmitCallback(options: api.CompilerOptions): api.TsEmitCallback | undefined; onFileChange( - options: api.CompilerOptions, listener: (event: FileChangeEvent, fileName: string) => void, - ready: () => void): {close: () => void}; + options: api.CompilerOptions, + listener: (event: FileChangeEvent, fileName: string) => void, + ready: () => void, + ): {close: () => void}; setTimeout(callback: () => void, ms: number): any; clearTimeout(timeoutId: any): void; } export function createPerformWatchHost( - configFileName: string, reportDiagnostics: (diagnostics: ReadonlyArray) => void, - existingOptions?: ts.CompilerOptions, - createEmitCallback?: (options: api.CompilerOptions) => - api.TsEmitCallback| undefined): PerformWatchHost { + configFileName: string, + reportDiagnostics: (diagnostics: ReadonlyArray) => void, + existingOptions?: ts.CompilerOptions, + createEmitCallback?: (options: api.CompilerOptions) => api.TsEmitCallback | undefined, +): PerformWatchHost { return { reportDiagnostics: reportDiagnostics, - createCompilerHost: options => createCompilerHost({options}), + createCompilerHost: (options) => createCompilerHost({options}), readConfiguration: () => readConfiguration(configFileName, existingOptions), - createEmitCallback: options => createEmitCallback ? createEmitCallback(options) : undefined, + createEmitCallback: (options) => (createEmitCallback ? createEmitCallback(options) : undefined), onFileChange: (options, listener, ready: () => void) => { if (!options.basePath) { - reportDiagnostics([{ - category: ts.DiagnosticCategory.Error, - messageText: 'Invalid configuration option. baseDir not specified', - source: api.SOURCE, - code: api.DEFAULT_ERROR_CODE, - file: undefined, - start: undefined, - length: undefined, - }]); + reportDiagnostics([ + { + category: ts.DiagnosticCategory.Error, + messageText: 'Invalid configuration option. baseDir not specified', + source: api.SOURCE, + code: api.DEFAULT_ERROR_CODE, + file: undefined, + start: undefined, + length: undefined, + }, + ]); return {close: () => {}}; } const watcher = chokidar.watch(options.basePath, { @@ -119,15 +130,14 @@ interface QueuedCompilationInfo { * The logic in this function is adapted from `tsc.ts` from TypeScript. */ export function performWatchCompilation(host: PerformWatchHost): { - close: () => void, - ready: (cb: () => void) => void, - firstCompileResult: ReadonlyArray + close: () => void; + ready: (cb: () => void) => void; + firstCompileResult: ReadonlyArray; } { - let cachedProgram: api.Program|undefined; // Program cached from last compilation - let cachedCompilerHost: api.CompilerHost|undefined; // CompilerHost cached from last compilation - let cachedOptions: ParsedConfiguration|undefined; // CompilerOptions cached from last compilation - let timerHandleForRecompilation: QueuedCompilationInfo| - undefined; // Handle for 0.25s wait timer to trigger recompilation + let cachedProgram: api.Program | undefined; // Program cached from last compilation + let cachedCompilerHost: api.CompilerHost | undefined; // CompilerHost cached from last compilation + let cachedOptions: ParsedConfiguration | undefined; // CompilerOptions cached from last compilation + let timerHandleForRecompilation: QueuedCompilationInfo | undefined; // Handle for 0.25s wait timer to trigger recompilation const ignoreFilesForWatch = new Set(); const fileCache = new Map(); @@ -136,13 +146,16 @@ export function performWatchCompilation(host: PerformWatchHost): { // Watch basePath, ignoring .dotfiles let resolveReadyPromise: () => void; - const readyPromise = new Promise(resolve => resolveReadyPromise = resolve); + const readyPromise = new Promise((resolve) => (resolveReadyPromise = resolve)); // Note: ! is ok as options are filled after the first compilation // Note: ! is ok as resolvedReadyPromise is filled by the previous call - const fileWatcher = - host.onFileChange(cachedOptions!.options, watchedFileChanged, resolveReadyPromise!); + const fileWatcher = host.onFileChange( + cachedOptions!.options, + watchedFileChanged, + resolveReadyPromise!, + ); - return {close, ready: cb => readyPromise.then(cb), firstCompileResult}; + return {close, ready: (cb) => readyPromise.then(cb), firstCompileResult}; function cacheEntry(fileName: string): CacheEntry { fileName = path.normalize(fileName); @@ -175,14 +188,18 @@ export function performWatchCompilation(host: PerformWatchHost): { if (!cachedCompilerHost) { cachedCompilerHost = host.createCompilerHost(cachedOptions.options); const originalWriteFileCallback = cachedCompilerHost.writeFile; - cachedCompilerHost.writeFile = function( - fileName: string, data: string, writeByteOrderMark: boolean, - onError?: (message: string) => void, sourceFiles: ReadonlyArray = []) { + cachedCompilerHost.writeFile = function ( + fileName: string, + data: string, + writeByteOrderMark: boolean, + onError?: (message: string) => void, + sourceFiles: ReadonlyArray = [], + ) { ignoreFilesForWatch.add(path.normalize(fileName)); return originalWriteFileCallback(fileName, data, writeByteOrderMark, onError, sourceFiles); }; const originalFileExists = cachedCompilerHost.fileExists; - cachedCompilerHost.fileExists = function(fileName: string) { + cachedCompilerHost.fileExists = function (fileName: string) { const ce = cacheEntry(fileName); if (ce.exists == null) { ce.exists = originalFileExists.call(this, fileName); @@ -190,8 +207,10 @@ export function performWatchCompilation(host: PerformWatchHost): { return ce.exists!; }; const originalGetSourceFile = cachedCompilerHost.getSourceFile; - cachedCompilerHost.getSourceFile = function( - fileName: string, languageVersion: ts.ScriptTarget) { + cachedCompilerHost.getSourceFile = function ( + fileName: string, + languageVersion: ts.ScriptTarget, + ) { const ce = cacheEntry(fileName); if (!ce.sf) { ce.sf = originalGetSourceFile.call(this, fileName, languageVersion); @@ -199,7 +218,7 @@ export function performWatchCompilation(host: PerformWatchHost): { return ce.sf!; }; const originalReadFile = cachedCompilerHost.readFile; - cachedCompilerHost.readFile = function(fileName: string) { + cachedCompilerHost.readFile = function (fileName: string) { const ce = cacheEntry(fileName); if (ce.content == null) { ce.content = originalReadFile.call(this, fileName); @@ -207,7 +226,7 @@ export function performWatchCompilation(host: PerformWatchHost): { return ce.content!; }; // Provide access to the file paths that triggered this rebuild - cachedCompilerHost.getModifiedResourceFiles = function() { + cachedCompilerHost.getModifiedResourceFiles = function () { if (timerHandleForRecompilation === undefined) { return undefined; } @@ -224,7 +243,7 @@ export function performWatchCompilation(host: PerformWatchHost): { options: cachedOptions.options, host: cachedCompilerHost, oldProgram: oldProgram, - emitCallback: host.createEmitCallback(cachedOptions.options) + emitCallback: host.createEmitCallback(cachedOptions.options), }); if (compileResult.diagnostics.length) { @@ -239,11 +258,13 @@ export function performWatchCompilation(host: PerformWatchHost): { const exitCode = exitCodeFromResult(compileResult.diagnostics); if (exitCode == 0) { cachedProgram = compileResult.program; - host.reportDiagnostics( - [createMessageDiagnostic('Compilation complete. Watching for file changes.')]); + host.reportDiagnostics([ + createMessageDiagnostic('Compilation complete. Watching for file changes.'), + ]); } else { - host.reportDiagnostics( - [createMessageDiagnostic('Compilation failed. Watching for file changes.')]); + host.reportDiagnostics([ + createMessageDiagnostic('Compilation failed. Watching for file changes.'), + ]); } return compileResult.diagnostics; @@ -258,15 +279,20 @@ export function performWatchCompilation(host: PerformWatchHost): { function watchedFileChanged(event: FileChangeEvent, fileName: string) { const normalizedPath = path.normalize(fileName); - if (cachedOptions && event === FileChangeEvent.Change && - // TODO(chuckj): validate that this is sufficient to skip files that were written. - // This assumes that the file path we write is the same file path we will receive in the - // change notification. - normalizedPath === path.normalize(cachedOptions.project)) { + if ( + cachedOptions && + event === FileChangeEvent.Change && + // TODO(chuckj): validate that this is sufficient to skip files that were written. + // This assumes that the file path we write is the same file path we will receive in the + // change notification. + normalizedPath === path.normalize(cachedOptions.project) + ) { // If the configuration file changes, forget everything and start the recompilation timer resetOptions(); } else if ( - event === FileChangeEvent.CreateDelete || event === FileChangeEvent.CreateDeleteDir) { + event === FileChangeEvent.CreateDelete || + event === FileChangeEvent.CreateDeleteDir + ) { // If a file was added or removed, reread the configuration // to determine the new list of root files. cachedOptions = undefined; @@ -293,7 +319,7 @@ export function performWatchCompilation(host: PerformWatchHost): { } else { timerHandleForRecompilation = { modifiedResourceFiles: new Set(), - timerHandle: undefined + timerHandle: undefined, }; } timerHandleForRecompilation.timerHandle = host.setTimeout(recompile, 250); @@ -301,8 +327,9 @@ export function performWatchCompilation(host: PerformWatchHost): { } function recompile() { - host.reportDiagnostics( - [createMessageDiagnostic('File change detected. Starting incremental compilation.')]); + host.reportDiagnostics([ + createMessageDiagnostic('File change detected. Starting incremental compilation.'), + ]); doCompilation(); timerHandleForRecompilation = undefined; } diff --git a/packages/compiler-cli/src/transformers/api.ts b/packages/compiler-cli/src/transformers/api.ts index 4f248213febdd..15b179e17265d 100644 --- a/packages/compiler-cli/src/transformers/api.ts +++ b/packages/compiler-cli/src/transformers/api.ts @@ -60,7 +60,7 @@ export interface CompilerOptions extends NgCompilerOptions, ts.CompilerOptions { // static fields: Replace decorators with a static field in the class. // Allows advanced tree-shakers like Closure Compiler to remove // unused classes. - annotationsAs?: 'decorators'|'static fields'; + annotationsAs?: 'decorators' | 'static fields'; // Print extra information while running the compiler trace?: boolean; @@ -74,7 +74,7 @@ export interface CompilerOptions extends NgCompilerOptions, ts.CompilerOptions { // Path to the translation file i18nInFile?: string; // How to handle missing messages - i18nInMissingTranslations?: 'error'|'warning'|'ignore'; + i18nInMissingTranslations?: 'error' | 'warning' | 'ignore'; /** * Whether to replace the `templateUrl` and `styleUrls` property in all @@ -104,7 +104,7 @@ export interface CompilerHost extends ts.CompilerHost, ExtendedTsCompilerHost { * Converts a module name that is used in an `import` to a file path. * I.e. `path/to/containingFile.ts` containing `import {...} from 'module-name'`. */ - moduleNameToFileName?(moduleName: string, containingFile: string): string|null; + moduleNameToFileName?(moduleName: string, containingFile: string): string | null; /** * Converts a file name into a representation that should be stored in a summary file. * This has to include changing the suffix as well. @@ -125,7 +125,7 @@ export interface CompilerHost extends ts.CompilerHost, ExtendedTsCompilerHost { * An AMD module can have an arbitrary name, so that it is require'd by name * rather than by path. See https://requirejs.org/docs/whyamd.html#namedmodules */ - amdModuleName?(sf: ts.SourceFile): string|undefined; + amdModuleName?(sf: ts.SourceFile): string | undefined; } export enum EmitFlags { @@ -164,8 +164,8 @@ export interface TsMergeEmitResultsCallback { export interface LazyRoute { route: string; - module: {name: string, filePath: string}; - referencedModule: {name: string, filePath: string}; + module: {name: string; filePath: string}; + referencedModule: {name: string; filePath: string}; } export interface EmitOptions { @@ -202,8 +202,10 @@ export interface Program { * `getTsProgram().getSyntacticDiagnostics()` since it does not need to collect Angular structural * information to produce the errors. */ - getTsSyntacticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): - ReadonlyArray; + getTsSyntacticDiagnostics( + sourceFile?: ts.SourceFile, + cancellationToken?: ts.CancellationToken, + ): ReadonlyArray; /** * Retrieve the diagnostics for the structure of an Angular application is correctly formed. @@ -216,23 +218,28 @@ export interface Program { * * Angular structural information is required to produce these diagnostics. */ - getNgStructuralDiagnostics(cancellationToken?: ts.CancellationToken): - ReadonlyArray; + getNgStructuralDiagnostics( + cancellationToken?: ts.CancellationToken, + ): ReadonlyArray; /** * Retrieve the semantic diagnostics from TypeScript. This is equivalent to calling * `getTsProgram().getSemanticDiagnostics()` directly and is included for completeness. */ - getTsSemanticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): - ReadonlyArray; + getTsSemanticDiagnostics( + sourceFile?: ts.SourceFile, + cancellationToken?: ts.CancellationToken, + ): ReadonlyArray; /** * Retrieve the Angular semantic diagnostics. * * Angular structural information is required to produce these diagnostics. */ - getNgSemanticDiagnostics(fileName?: string, cancellationToken?: ts.CancellationToken): - ReadonlyArray; + getNgSemanticDiagnostics( + fileName?: string, + cancellationToken?: ts.CancellationToken, + ): ReadonlyArray; /** * Load Angular structural information asynchronously. If this method is not called then the @@ -252,7 +259,7 @@ export interface Program { * * Angular structural information is required to emit files. */ - emit(opts?: EmitOptions|undefined): ts.EmitResult; + emit(opts?: EmitOptions | undefined): ts.EmitResult; /** * @internal diff --git a/packages/compiler-cli/src/transformers/compiler_host.ts b/packages/compiler-cli/src/transformers/compiler_host.ts index f718309b3ce92..608da42ccd2cf 100644 --- a/packages/compiler-cli/src/transformers/compiler_host.ts +++ b/packages/compiler-cli/src/transformers/compiler_host.ts @@ -10,16 +10,21 @@ import ts from 'typescript'; import {CompilerHost, CompilerOptions} from './api'; -let wrapHostForTest: ((host: ts.CompilerHost) => ts.CompilerHost)|null = null; +let wrapHostForTest: ((host: ts.CompilerHost) => ts.CompilerHost) | null = null; -export function setWrapHostForTest(wrapFn: ((host: ts.CompilerHost) => ts.CompilerHost)| - null): void { +export function setWrapHostForTest( + wrapFn: ((host: ts.CompilerHost) => ts.CompilerHost) | null, +): void { wrapHostForTest = wrapFn; } -export function createCompilerHost( - {options, tsHost = ts.createCompilerHost(options, true)}: - {options: CompilerOptions, tsHost?: ts.CompilerHost}): CompilerHost { +export function createCompilerHost({ + options, + tsHost = ts.createCompilerHost(options, true), +}: { + options: CompilerOptions; + tsHost?: ts.CompilerHost; +}): CompilerHost { if (wrapHostForTest !== null) { tsHost = wrapHostForTest(tsHost); } diff --git a/packages/compiler-cli/src/transformers/i18n.ts b/packages/compiler-cli/src/transformers/i18n.ts index a9507f51dcac6..cf2af710fd5a9 100644 --- a/packages/compiler-cli/src/transformers/i18n.ts +++ b/packages/compiler-cli/src/transformers/i18n.ts @@ -30,9 +30,13 @@ export function i18nGetExtension(formatName: string): string { } export function i18nExtract( - formatName: string|null, outFile: string|null, host: ts.CompilerHost, options: CompilerOptions, - bundle: MessageBundle, - pathResolve: (...segments: string[]) => string = path.resolve): string[] { + formatName: string | null, + outFile: string | null, + host: ts.CompilerHost, + options: CompilerOptions, + bundle: MessageBundle, + pathResolve: (...segments: string[]) => string = path.resolve, +): string[] { formatName = formatName || 'xlf'; // Checks the format and returns the extension const ext = i18nGetExtension(formatName); @@ -44,7 +48,10 @@ export function i18nExtract( } export function i18nSerialize( - bundle: MessageBundle, formatName: string, options: CompilerOptions): string { + bundle: MessageBundle, + formatName: string, + options: CompilerOptions, +): string { const format = formatName.toLowerCase(); let serializer: Serializer; diff --git a/packages/compiler-cli/src/transformers/jit_transforms/downlevel_decorators_transform.ts b/packages/compiler-cli/src/transformers/jit_transforms/downlevel_decorators_transform.ts index 73480a2725a8f..e3b7684aa28cf 100644 --- a/packages/compiler-cli/src/transformers/jit_transforms/downlevel_decorators_transform.ts +++ b/packages/compiler-cli/src/transformers/jit_transforms/downlevel_decorators_transform.ts @@ -51,7 +51,9 @@ const DECORATOR_INVOCATION_JSDOC_TYPE = '!Array<{type: !Function, args: (undefin * { type: decorator, args: [arg1, arg2] } */ function extractMetadataFromSingleDecorator( - decorator: ts.Decorator, diagnostics: ts.Diagnostic[]): ts.ObjectLiteralExpression { + decorator: ts.Decorator, + diagnostics: ts.Diagnostic[], +): ts.ObjectLiteralExpression { const metadataProperties: ts.ObjectLiteralElementLike[] = []; const expr = decorator.expression; switch (expr.kind) { @@ -68,8 +70,9 @@ function extractMetadataFromSingleDecorator( for (const arg of call.arguments) { args.push(arg); } - const argsArrayLiteral = - ts.factory.createArrayLiteralExpression(ts.factory.createNodeArray(args, true)); + const argsArrayLiteral = ts.factory.createArrayLiteralExpression( + ts.factory.createNodeArray(args, true), + ); metadataProperties.push(ts.factory.createPropertyAssignment('args', argsArrayLiteral)); } break; @@ -78,8 +81,7 @@ function extractMetadataFromSingleDecorator( file: decorator.getSourceFile(), start: decorator.getStart(), length: decorator.getEnd() - decorator.getStart(), - messageText: - `${ts.SyntaxKind[decorator.kind]} not implemented in gathering decorator metadata.`, + messageText: `${ts.SyntaxKind[decorator.kind]} not implemented in gathering decorator metadata.`, category: ts.DiagnosticCategory.Error, code: 0, }); @@ -102,10 +104,11 @@ function extractMetadataFromSingleDecorator( * }]; */ function createCtorParametersClassProperty( - diagnostics: ts.Diagnostic[], - entityNameToExpression: (n: ts.EntityName) => ts.Expression | undefined, - ctorParameters: ParameterDecorationInfo[], - isClosureCompilerEnabled: boolean): ts.PropertyDeclaration { + diagnostics: ts.Diagnostic[], + entityNameToExpression: (n: ts.EntityName) => ts.Expression | undefined, + ctorParameters: ParameterDecorationInfo[], + isClosureCompilerEnabled: boolean, +): ts.PropertyDeclaration { const params: ts.Expression[] = []; for (const ctorParam of ctorParameters) { @@ -114,30 +117,46 @@ function createCtorParametersClassProperty( continue; } - const paramType = ctorParam.type ? - typeReferenceToExpression(entityNameToExpression, ctorParam.type) : - undefined; - const members = [ts.factory.createPropertyAssignment( - 'type', paramType || ts.factory.createIdentifier('undefined'))]; + const paramType = ctorParam.type + ? typeReferenceToExpression(entityNameToExpression, ctorParam.type) + : undefined; + const members = [ + ts.factory.createPropertyAssignment( + 'type', + paramType || ts.factory.createIdentifier('undefined'), + ), + ]; const decorators: ts.ObjectLiteralExpression[] = []; for (const deco of ctorParam.decorators) { decorators.push(extractMetadataFromSingleDecorator(deco, diagnostics)); } if (decorators.length) { - members.push(ts.factory.createPropertyAssignment( - 'decorators', ts.factory.createArrayLiteralExpression(decorators))); + members.push( + ts.factory.createPropertyAssignment( + 'decorators', + ts.factory.createArrayLiteralExpression(decorators), + ), + ); } params.push(ts.factory.createObjectLiteralExpression(members)); } const initializer = ts.factory.createArrowFunction( - undefined, undefined, [], undefined, - ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), - ts.factory.createArrayLiteralExpression(params, true)); + undefined, + undefined, + [], + undefined, + ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + ts.factory.createArrayLiteralExpression(params, true), + ); const ctorProp = ts.factory.createPropertyDeclaration( - [ts.factory.createToken(ts.SyntaxKind.StaticKeyword)], 'ctorParameters', undefined, undefined, - initializer); + [ts.factory.createToken(ts.SyntaxKind.StaticKeyword)], + 'ctorParameters', + undefined, + undefined, + initializer, + ); if (isClosureCompilerEnabled) { ts.setSyntheticLeadingComments(ctorProp, [ { @@ -169,8 +188,9 @@ function createCtorParametersClassProperty( * metadata. */ function typeReferenceToExpression( - entityNameToExpression: (n: ts.EntityName) => ts.Expression | undefined, - node: ts.TypeNode): ts.Expression|undefined { + entityNameToExpression: (n: ts.EntityName) => ts.Expression | undefined, + node: ts.TypeNode, +): ts.Expression | undefined { let kind = node.kind; if (ts.isLiteralTypeNode(node)) { // Treat literal types like their base type (boolean, string, number). @@ -201,13 +221,12 @@ function typeReferenceToExpression( // Ignore any generic types, just return the base type. return entityNameToExpression(typeRef.typeName); case ts.SyntaxKind.UnionType: - const childTypeNodes = - (node as ts.UnionTypeNode) - .types.filter( - t => !(ts.isLiteralTypeNode(t) && t.literal.kind === ts.SyntaxKind.NullKeyword)); - return childTypeNodes.length === 1 ? - typeReferenceToExpression(entityNameToExpression, childTypeNodes[0]) : - undefined; + const childTypeNodes = (node as ts.UnionTypeNode).types.filter( + (t) => !(ts.isLiteralTypeNode(t) && t.literal.kind === ts.SyntaxKind.NullKeyword), + ); + return childTypeNodes.length === 1 + ? typeReferenceToExpression(entityNameToExpression, childTypeNodes[0]) + : undefined; default: return undefined; } @@ -236,7 +255,7 @@ interface ParameterDecorationInfo { * The type declaration for the parameter. Only set if the type is a value (e.g. a class, not an * interface). */ - type: ts.TypeNode|null; + type: ts.TypeNode | null; /** The list of decorators found on the parameter, null if none. */ decorators: ts.Decorator[]; } @@ -254,8 +273,12 @@ interface ParameterDecorationInfo { * @param isClosureCompilerEnabled Whether closure annotations need to be added where needed. */ export function getDownlevelDecoratorsTransform( - typeChecker: ts.TypeChecker, host: ReflectionHost, diagnostics: ts.Diagnostic[], - isCore: boolean, isClosureCompilerEnabled: boolean): ts.TransformerFactory { + typeChecker: ts.TypeChecker, + host: ReflectionHost, + diagnostics: ts.Diagnostic[], + isCore: boolean, + isClosureCompilerEnabled: boolean, +): ts.TransformerFactory { function addJSDocTypeAnnotation(node: ts.Node, jsdocType: string): void { if (!isClosureCompilerEnabled) { return; @@ -283,21 +306,30 @@ export function getDownlevelDecoratorsTransform( * }; */ function createPropDecoratorsClassProperty( - diagnostics: ts.Diagnostic[], - properties: Map): ts.PropertyDeclaration { + diagnostics: ts.Diagnostic[], + properties: Map, + ): ts.PropertyDeclaration { // `static propDecorators: {[key: string]: ` + {type: Function, args?: // any[]}[] + `} = {\n`); const entries: ts.ObjectLiteralElementLike[] = []; for (const [name, decorators] of properties.entries()) { - entries.push(ts.factory.createPropertyAssignment( + entries.push( + ts.factory.createPropertyAssignment( name, ts.factory.createArrayLiteralExpression( - decorators.map(deco => extractMetadataFromSingleDecorator(deco, diagnostics))))); + decorators.map((deco) => extractMetadataFromSingleDecorator(deco, diagnostics)), + ), + ), + ); } const initializer = ts.factory.createObjectLiteralExpression(entries, true); const prop = ts.factory.createPropertyDeclaration( - [ts.factory.createToken(ts.SyntaxKind.StaticKeyword)], 'propDecorators', undefined, - undefined, initializer); + [ts.factory.createToken(ts.SyntaxKind.StaticKeyword)], + 'propDecorators', + undefined, + undefined, + initializer, + ); addJSDocTypeAnnotation(prop, `!Object`); return prop; } @@ -316,12 +348,16 @@ export function getDownlevelDecoratorsTransform( * For a given qualified name, this walks depth first to find the leftmost identifier, * and then converts the path into a property access that can be used as expression. */ - function entityNameToExpression(name: ts.EntityName): ts.Expression|undefined { + function entityNameToExpression(name: ts.EntityName): ts.Expression | undefined { const symbol = typeChecker.getSymbolAtLocation(name); // Check if the entity name references a symbol that is an actual value. If it is not, it // cannot be referenced by an expression, so return undefined. - if (!symbol || !symbolIsRuntimeValue(typeChecker, symbol) || !symbol.declarations || - symbol.declarations.length === 0) { + if ( + !symbol || + !symbolIsRuntimeValue(typeChecker, symbol) || + !symbol.declarations || + symbol.declarations.length === 0 + ) { return undefined; } // If we deal with a qualified name, build up a property access expression @@ -364,8 +400,9 @@ export function getDownlevelDecoratorsTransform( * decorators found. Returns an undefined name if there are no decorators to lower on the * element, or the element has an exotic name. */ - function transformClassElement(element: ts.ClassElement): - [string|undefined, ts.ClassElement, ts.Decorator[]] { + function transformClassElement( + element: ts.ClassElement, + ): [string | undefined, ts.ClassElement, ts.Decorator[]] { element = ts.visitEachChild(element, decoratorDownlevelVisitor, context); const decoratorsToKeep: ts.Decorator[] = []; const toLower: ts.Decorator[] = []; @@ -397,12 +434,13 @@ export function getDownlevelDecoratorsTransform( } const elementModifiers = ts.canHaveModifiers(element) ? ts.getModifiers(element) : undefined; - let modifiers: ts.NodeArray|undefined; + let modifiers: ts.NodeArray | undefined; if (decoratorsToKeep.length || elementModifiers?.length) { modifiers = ts.setTextRange( - ts.factory.createNodeArray([...decoratorsToKeep, ...(elementModifiers || [])]), - (element as ts.HasModifiers).modifiers); + ts.factory.createNodeArray([...decoratorsToKeep, ...(elementModifiers || [])]), + (element as ts.HasModifiers).modifiers, + ); } return [element.name.text, cloneClassElementWithModifiers(element, modifiers), toLower]; @@ -412,8 +450,9 @@ export function getDownlevelDecoratorsTransform( * Transforms a constructor. Returns the transformed constructor and the list of parameter * information collected, consisting of decorators and optional type. */ - function transformConstructor(ctor: ts.ConstructorDeclaration): - [ts.ConstructorDeclaration, ParameterDecorationInfo[]] { + function transformConstructor( + ctor: ts.ConstructorDeclaration, + ): [ts.ConstructorDeclaration, ParameterDecorationInfo[]] { ctor = ts.visitEachChild(ctor, decoratorDownlevelVisitor, context); const newParameters: ts.ParameterDeclaration[] = []; @@ -445,7 +484,7 @@ export function getDownlevelDecoratorsTransform( parametersInfo.push(paramInfo); // Must pass 'undefined' to avoid emitting decorator metadata. - let modifiers: ts.ModifierLike[]|undefined; + let modifiers: ts.ModifierLike[] | undefined; const paramModifiers = ts.getModifiers(param); if (decoratorsToKeep.length || paramModifiers?.length) { @@ -453,12 +492,22 @@ export function getDownlevelDecoratorsTransform( } const newParam = ts.factory.updateParameterDeclaration( - param, modifiers, param.dotDotDotToken, param.name, param.questionToken, param.type, - param.initializer); + param, + modifiers, + param.dotDotDotToken, + param.name, + param.questionToken, + param.type, + param.initializer, + ); newParameters.push(newParam); } const updated = ts.factory.updateConstructorDeclaration( - ctor, ts.getModifiers(ctor), newParameters, ctor.body); + ctor, + ts.getModifiers(ctor), + newParameters, + ctor.body, + ); return [updated, parametersInfo]; } @@ -472,7 +521,7 @@ export function getDownlevelDecoratorsTransform( function transformClassDeclaration(classDecl: ts.ClassDeclaration): ts.ClassDeclaration { const newMembers: ts.ClassElement[] = []; const decoratedProperties = new Map(); - let classParameters: ParameterDecorationInfo[]|null = null; + let classParameters: ParameterDecorationInfo[] | null = null; for (const member of classDecl.members) { switch (member.kind) { @@ -488,8 +537,9 @@ export function getDownlevelDecoratorsTransform( case ts.SyntaxKind.Constructor: { const ctor = member as ts.ConstructorDeclaration; if (!ctor.body) break; - const [newMember, parametersInfo] = - transformConstructor(member as ts.ConstructorDeclaration); + const [newMember, parametersInfo] = transformConstructor( + member as ts.ConstructorDeclaration, + ); classParameters = parametersInfo; newMembers.push(newMember); continue; @@ -506,15 +556,22 @@ export function getDownlevelDecoratorsTransform( // Keep track if we come across an Angular class decorator. This is used // to determine whether constructor parameters should be captured or not. - const hasAngularDecorator = - possibleAngularDecorators.some(d => isAngularDecorator(d, isCore)); + const hasAngularDecorator = possibleAngularDecorators.some((d) => + isAngularDecorator(d, isCore), + ); if (classParameters) { - if (hasAngularDecorator || classParameters.some(p => !!p.decorators.length)) { + if (hasAngularDecorator || classParameters.some((p) => !!p.decorators.length)) { // Capture constructor parameters if the class has Angular decorator applied, // or if any of the parameters has decorators applied directly. - newMembers.push(createCtorParametersClassProperty( - diagnostics, entityNameToExpression, classParameters, isClosureCompilerEnabled)); + newMembers.push( + createCtorParametersClassProperty( + diagnostics, + entityNameToExpression, + classParameters, + isClosureCompilerEnabled, + ), + ); } } if (decoratedProperties.size) { @@ -522,12 +579,18 @@ export function getDownlevelDecoratorsTransform( } const members = ts.setTextRange( - ts.factory.createNodeArray(newMembers, classDecl.members.hasTrailingComma), - classDecl.members); + ts.factory.createNodeArray(newMembers, classDecl.members.hasTrailingComma), + classDecl.members, + ); return ts.factory.updateClassDeclaration( - classDecl, classDecl.modifiers, classDecl.name, classDecl.typeParameters, - classDecl.heritageClauses, members); + classDecl, + classDecl.modifiers, + classDecl.name, + classDecl.typeParameters, + classDecl.heritageClauses, + members, + ); } /** @@ -552,22 +615,45 @@ export function getDownlevelDecoratorsTransform( } function cloneClassElementWithModifiers( - node: ts.ClassElement, modifiers: readonly ts.ModifierLike[]|undefined): ts.ClassElement { + node: ts.ClassElement, + modifiers: readonly ts.ModifierLike[] | undefined, +): ts.ClassElement { let clone: ts.ClassElement; if (ts.isMethodDeclaration(node)) { clone = ts.factory.createMethodDeclaration( - modifiers, node.asteriskToken, node.name, node.questionToken, node.typeParameters, - node.parameters, node.type, node.body); + modifiers, + node.asteriskToken, + node.name, + node.questionToken, + node.typeParameters, + node.parameters, + node.type, + node.body, + ); } else if (ts.isPropertyDeclaration(node)) { clone = ts.factory.createPropertyDeclaration( - modifiers, node.name, node.questionToken, node.type, node.initializer); + modifiers, + node.name, + node.questionToken, + node.type, + node.initializer, + ); } else if (ts.isGetAccessor(node)) { clone = ts.factory.createGetAccessorDeclaration( - modifiers, node.name, node.parameters, node.type, node.body); + modifiers, + node.name, + node.parameters, + node.type, + node.body, + ); } else if (ts.isSetAccessor(node)) { - clone = - ts.factory.createSetAccessorDeclaration(modifiers, node.name, node.parameters, node.body); + clone = ts.factory.createSetAccessorDeclaration( + modifiers, + node.name, + node.parameters, + node.body, + ); } else { throw new Error(`Unsupported decorated member with kind ${ts.SyntaxKind[node.kind]}`); } diff --git a/packages/compiler-cli/src/transformers/jit_transforms/index.ts b/packages/compiler-cli/src/transformers/jit_transforms/index.ts index b03c8d7490e2a..e1342db7bf4ab 100644 --- a/packages/compiler-cli/src/transformers/jit_transforms/index.ts +++ b/packages/compiler-cli/src/transformers/jit_transforms/index.ts @@ -36,17 +36,26 @@ export {getInitializerApiJitTransform} from './initializer_api_transforms/transf * metadata */ export function angularJitApplicationTransform( - program: ts.Program, isCore = false): ts.TransformerFactory { + program: ts.Program, + isCore = false, +): ts.TransformerFactory { const typeChecker = program.getTypeChecker(); const reflectionHost = new TypeScriptReflectionHost(typeChecker); const importTracker = new ImportedSymbolsTracker(); const downlevelDecoratorTransform = getDownlevelDecoratorsTransform( - typeChecker, reflectionHost, [], isCore, - /* enableClosureCompiler */ false); + typeChecker, + reflectionHost, + [], + isCore, + /* enableClosureCompiler */ false, + ); - const initializerApisJitTransform = - getInitializerApiJitTransform(reflectionHost, importTracker, isCore); + const initializerApisJitTransform = getInitializerApiJitTransform( + reflectionHost, + importTracker, + isCore, + ); return (ctx) => { return (sourceFile) => { diff --git a/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/input_function.ts b/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/input_function.ts index 93f1c66e45de1..3b829aec2168d 100644 --- a/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/input_function.ts +++ b/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/input_function.ts @@ -11,7 +11,11 @@ import ts from 'typescript'; import {isAngularDecorator, tryParseSignalInputMapping} from '../../../ngtsc/annotations'; -import {castAsAny, createSyntheticAngularCoreDecoratorAccess, PropertyTransform} from './transform_api'; +import { + castAsAny, + createSyntheticAngularCoreDecoratorAccess, + PropertyTransform, +} from './transform_api'; /** * Transform that will automatically add an `@Input` decorator for all signal @@ -25,17 +29,20 @@ import {castAsAny, createSyntheticAngularCoreDecoratorAccess, PropertyTransform} * the class needing to be instantiated. */ export const signalInputsTransform: PropertyTransform = ( - member, - host, - factory, - importTracker, - importManager, - classDecorator, - isCore, - ) => { + member, + host, + factory, + importTracker, + importManager, + classDecorator, + isCore, +) => { // If the field already is decorated, we handle this gracefully and skip it. - if (host.getDecoratorsOfDeclaration(member.node) - ?.some(d => isAngularDecorator(d, 'Input', isCore))) { + if ( + host + .getDecoratorsOfDeclaration(member.node) + ?.some((d) => isAngularDecorator(d, 'Input', isCore)) + ) { return member.node; } @@ -56,27 +63,37 @@ export const signalInputsTransform: PropertyTransform = ( const sourceFile = member.node.getSourceFile(); const newDecorator = factory.createDecorator( - factory.createCallExpression( - createSyntheticAngularCoreDecoratorAccess( - factory, importManager, classDecorator, sourceFile, 'Input'), - undefined, - [ - // Cast to `any` because `isSignal` will be private, and in case this - // transform is used directly as a pre-compilation step, the decorator should - // not fail. It is already validated now due to us parsing the input metadata. - castAsAny( - factory, - factory.createObjectLiteralExpression(Object.entries(fields).map( - ([name, value]) => factory.createPropertyAssignment(name, value)))), - ]), + factory.createCallExpression( + createSyntheticAngularCoreDecoratorAccess( + factory, + importManager, + classDecorator, + sourceFile, + 'Input', + ), + undefined, + [ + // Cast to `any` because `isSignal` will be private, and in case this + // transform is used directly as a pre-compilation step, the decorator should + // not fail. It is already validated now due to us parsing the input metadata. + castAsAny( + factory, + factory.createObjectLiteralExpression( + Object.entries(fields).map(([name, value]) => + factory.createPropertyAssignment(name, value), + ), + ), + ), + ], + ), ); return factory.updatePropertyDeclaration( - member.node, - [newDecorator, ...(member.node.modifiers ?? [])], - member.name, - member.node.questionToken, - member.node.type, - member.node.initializer, + member.node, + [newDecorator, ...(member.node.modifiers ?? [])], + member.name, + member.node.questionToken, + member.node.type, + member.node.initializer, ); }; diff --git a/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/model_function.ts b/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/model_function.ts index 9e40eb8b52683..aa23b92e40a9d 100644 --- a/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/model_function.ts +++ b/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/model_function.ts @@ -19,25 +19,23 @@ import {createSyntheticAngularCoreDecoratorAccess, PropertyTransform} from './tr * It is useful for JIT environments where models can't be recognized based on the initializer. */ export const signalModelTransform: PropertyTransform = ( - member, - host, - factory, - importTracker, - importManager, - classDecorator, - isCore, - ) => { - if (host.getDecoratorsOfDeclaration(member.node)?.some(d => { - return isAngularDecorator(d, 'Input', isCore) || isAngularDecorator(d, 'Output', isCore); - })) { + member, + host, + factory, + importTracker, + importManager, + classDecorator, + isCore, +) => { + if ( + host.getDecoratorsOfDeclaration(member.node)?.some((d) => { + return isAngularDecorator(d, 'Input', isCore) || isAngularDecorator(d, 'Output', isCore); + }) + ) { return member.node; } - const modelMapping = tryParseSignalModelMapping( - member, - host, - importTracker, - ); + const modelMapping = tryParseSignalModelMapping(member, host, importTracker); if (modelMapping === null) { return member.node; @@ -45,42 +43,69 @@ export const signalModelTransform: PropertyTransform = ( const inputConfig = factory.createObjectLiteralExpression([ factory.createPropertyAssignment( - 'isSignal', modelMapping.input.isSignal ? factory.createTrue() : factory.createFalse()), + 'isSignal', + modelMapping.input.isSignal ? factory.createTrue() : factory.createFalse(), + ), factory.createPropertyAssignment( - 'alias', factory.createStringLiteral(modelMapping.input.bindingPropertyName)), + 'alias', + factory.createStringLiteral(modelMapping.input.bindingPropertyName), + ), factory.createPropertyAssignment( - 'required', modelMapping.input.required ? factory.createTrue() : factory.createFalse()), + 'required', + modelMapping.input.required ? factory.createTrue() : factory.createFalse(), + ), ]); const sourceFile = member.node.getSourceFile(); const inputDecorator = createDecorator( - 'Input', - // Config is cast to `any` because `isSignal` will be private, and in case this - // transform is used directly as a pre-compilation step, the decorator should - // not fail. It is already validated now due to us parsing the input metadata. - factory.createAsExpression( - inputConfig, factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)), - classDecorator, factory, sourceFile, importManager); + 'Input', + // Config is cast to `any` because `isSignal` will be private, and in case this + // transform is used directly as a pre-compilation step, the decorator should + // not fail. It is already validated now due to us parsing the input metadata. + factory.createAsExpression( + inputConfig, + factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword), + ), + classDecorator, + factory, + sourceFile, + importManager, + ); const outputDecorator = createDecorator( - 'Output', factory.createStringLiteral(modelMapping.output.bindingPropertyName), - classDecorator, factory, sourceFile, importManager); + 'Output', + factory.createStringLiteral(modelMapping.output.bindingPropertyName), + classDecorator, + factory, + sourceFile, + importManager, + ); return factory.updatePropertyDeclaration( - member.node, - [inputDecorator, outputDecorator, ...(member.node.modifiers ?? [])], - member.node.name, - member.node.questionToken, - member.node.type, - member.node.initializer, + member.node, + [inputDecorator, outputDecorator, ...(member.node.modifiers ?? [])], + member.node.name, + member.node.questionToken, + member.node.type, + member.node.initializer, ); }; function createDecorator( - name: string, config: ts.Expression, classDecorator: Decorator, factory: ts.NodeFactory, - sourceFile: ts.SourceFile, importManager: ImportManager): ts.Decorator { + name: string, + config: ts.Expression, + classDecorator: Decorator, + factory: ts.NodeFactory, + sourceFile: ts.SourceFile, + importManager: ImportManager, +): ts.Decorator { const callTarget = createSyntheticAngularCoreDecoratorAccess( - factory, importManager, classDecorator, sourceFile, name); + factory, + importManager, + classDecorator, + sourceFile, + name, + ); return factory.createDecorator(factory.createCallExpression(callTarget, undefined, [config])); } diff --git a/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/output_function.ts b/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/output_function.ts index 41186478d1d8d..77c0ccb01a919 100644 --- a/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/output_function.ts +++ b/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/output_function.ts @@ -21,43 +21,49 @@ import {createSyntheticAngularCoreDecoratorAccess, PropertyTransform} from './tr * the class needing to be instantiated. */ export const initializerApiOutputTransform: PropertyTransform = ( - member, - host, - factory, - importTracker, - importManager, - classDecorator, - isCore, - ) => { + member, + host, + factory, + importTracker, + importManager, + classDecorator, + isCore, +) => { // If the field already is decorated, we handle this gracefully and skip it. - if (host.getDecoratorsOfDeclaration(member.node) - ?.some(d => isAngularDecorator(d, 'Output', isCore))) { + if ( + host + .getDecoratorsOfDeclaration(member.node) + ?.some((d) => isAngularDecorator(d, 'Output', isCore)) + ) { return member.node; } - const output = tryParseInitializerBasedOutput( - member, - host, - importTracker, - ); + const output = tryParseInitializerBasedOutput(member, host, importTracker); if (output === null) { return member.node; } const sourceFile = member.node.getSourceFile(); const newDecorator = factory.createDecorator( - factory.createCallExpression( - createSyntheticAngularCoreDecoratorAccess( - factory, importManager, classDecorator, sourceFile, 'Output'), - undefined, [factory.createStringLiteral(output.metadata.bindingPropertyName)]), + factory.createCallExpression( + createSyntheticAngularCoreDecoratorAccess( + factory, + importManager, + classDecorator, + sourceFile, + 'Output', + ), + undefined, + [factory.createStringLiteral(output.metadata.bindingPropertyName)], + ), ); return factory.updatePropertyDeclaration( - member.node, - [newDecorator, ...(member.node.modifiers ?? [])], - member.node.name, - member.node.questionToken, - member.node.type, - member.node.initializer, + member.node, + [newDecorator, ...(member.node.modifiers ?? [])], + member.node.name, + member.node.questionToken, + member.node.type, + member.node.initializer, ); }; diff --git a/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/query_functions.ts b/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/query_functions.ts index 6b0d023d79896..5efe94b2962ad 100644 --- a/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/query_functions.ts +++ b/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/query_functions.ts @@ -6,9 +6,18 @@ * found in the LICENSE file at https://angular.io/license */ -import {getAngularDecorators, queryDecoratorNames, QueryFunctionName, tryParseSignalQueryFromInitializer} from '../../../ngtsc/annotations'; +import { + getAngularDecorators, + queryDecoratorNames, + QueryFunctionName, + tryParseSignalQueryFromInitializer, +} from '../../../ngtsc/annotations'; -import {castAsAny, createSyntheticAngularCoreDecoratorAccess, PropertyTransform} from './transform_api'; +import { + castAsAny, + createSyntheticAngularCoreDecoratorAccess, + PropertyTransform, +} from './transform_api'; /** Maps a query function to its decorator. */ const queryFunctionToDecorator: Record = { @@ -30,28 +39,24 @@ const queryFunctionToDecorator: Record = { * information to the class without the class needing to be instantiated. */ export const queryFunctionsTransforms: PropertyTransform = ( - member, - host, - factory, - importTracker, - importManager, - classDecorator, - isCore, - ) => { + member, + host, + factory, + importTracker, + importManager, + classDecorator, + isCore, +) => { const decorators = host.getDecoratorsOfDeclaration(member.node); // If the field already is decorated, we handle this gracefully and skip it. const queryDecorators = - decorators && getAngularDecorators(decorators, queryDecoratorNames, isCore); + decorators && getAngularDecorators(decorators, queryDecoratorNames, isCore); if (queryDecorators !== null && queryDecorators.length > 0) { return member.node; } - const queryDefinition = tryParseSignalQueryFromInitializer( - member, - host, - importTracker, - ); + const queryDefinition = tryParseSignalQueryFromInitializer(member, host, importTracker); if (queryDefinition === null) { return member.node; } @@ -59,30 +64,38 @@ export const queryFunctionsTransforms: PropertyTransform = ( const sourceFile = member.node.getSourceFile(); const callArgs = queryDefinition.call.arguments; const newDecorator = factory.createDecorator( - factory.createCallExpression( - createSyntheticAngularCoreDecoratorAccess( - factory, importManager, classDecorator, sourceFile, - queryFunctionToDecorator[queryDefinition.name]), - undefined, - // All positional arguments of the query functions can be mostly re-used as is - // for the decorator. i.e. predicate is always first argument. Options are second. - [ - queryDefinition.call.arguments[0], - // Note: Casting as `any` because `isSignal` is not publicly exposed and this - // transform might pre-transform TS sources. - castAsAny(factory, factory.createObjectLiteralExpression([ - ...(callArgs.length > 1 ? [factory.createSpreadAssignment(callArgs[1])] : []), - factory.createPropertyAssignment('isSignal', factory.createTrue()), - ])), + factory.createCallExpression( + createSyntheticAngularCoreDecoratorAccess( + factory, + importManager, + classDecorator, + sourceFile, + queryFunctionToDecorator[queryDefinition.name], + ), + undefined, + // All positional arguments of the query functions can be mostly re-used as is + // for the decorator. i.e. predicate is always first argument. Options are second. + [ + queryDefinition.call.arguments[0], + // Note: Casting as `any` because `isSignal` is not publicly exposed and this + // transform might pre-transform TS sources. + castAsAny( + factory, + factory.createObjectLiteralExpression([ + ...(callArgs.length > 1 ? [factory.createSpreadAssignment(callArgs[1])] : []), + factory.createPropertyAssignment('isSignal', factory.createTrue()), ]), + ), + ], + ), ); return factory.updatePropertyDeclaration( - member.node, - [newDecorator, ...(member.node.modifiers ?? [])], - member.node.name, - member.node.questionToken, - member.node.type, - member.node.initializer, + member.node, + [newDecorator, ...(member.node.modifiers ?? [])], + member.node.name, + member.node.questionToken, + member.node.type, + member.node.initializer, ); }; diff --git a/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/transform.ts b/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/transform.ts index a139ba0a46f7e..50d2aee419ada 100644 --- a/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/transform.ts +++ b/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/transform.ts @@ -42,18 +42,18 @@ const propertyTransforms: PropertyTransform[] = [ * decorator for JIT. */ export function getInitializerApiJitTransform( - host: ReflectionHost, - importTracker: ImportedSymbolsTracker, - isCore: boolean, - ): ts.TransformerFactory { - return ctx => { - return sourceFile => { + host: ReflectionHost, + importTracker: ImportedSymbolsTracker, + isCore: boolean, +): ts.TransformerFactory { + return (ctx) => { + return (sourceFile) => { const importManager = new ImportManager(); sourceFile = ts.visitNode( - sourceFile, - createTransformVisitor(ctx, host, importManager, importTracker, isCore), - ts.isSourceFile, + sourceFile, + createTransformVisitor(ctx, host, importManager, importTracker, isCore), + ts.isSourceFile, ); return importManager.transformTsFile(ctx, sourceFile); @@ -62,21 +62,22 @@ export function getInitializerApiJitTransform( } function createTransformVisitor( - ctx: ts.TransformationContext, - host: ReflectionHost, - importManager: ImportManager, - importTracker: ImportedSymbolsTracker, - isCore: boolean, - ): ts.Visitor { + ctx: ts.TransformationContext, + host: ReflectionHost, + importManager: ImportManager, + importTracker: ImportedSymbolsTracker, + isCore: boolean, +): ts.Visitor { const visitor: ts.Visitor = (node: ts.Node): ts.Node => { if (ts.isClassDeclaration(node) && node.name !== undefined) { - const angularDecorator = host.getDecoratorsOfDeclaration(node)?.find( - (d) => decoratorsWithInputs.some(name => isAngularDecorator(d, name, isCore))); + const angularDecorator = host + .getDecoratorsOfDeclaration(node) + ?.find((d) => decoratorsWithInputs.some((name) => isAngularDecorator(d, name, isCore))); if (angularDecorator !== undefined) { let hasChanged = false; - const members = node.members.map(memberNode => { + const members = node.members.map((memberNode) => { if (!ts.isPropertyDeclaration(memberNode)) { return memberNode; } @@ -88,8 +89,14 @@ function createTransformVisitor( // Find the first matching transform and update the class member. for (const transform of propertyTransforms) { const newNode = transform( - {...member, node: memberNode}, host, ctx.factory, importTracker, importManager, - angularDecorator, isCore); + {...member, node: memberNode}, + host, + ctx.factory, + importTracker, + importManager, + angularDecorator, + isCore, + ); if (newNode !== member.node) { hasChanged = true; @@ -102,7 +109,13 @@ function createTransformVisitor( if (hasChanged) { return ctx.factory.updateClassDeclaration( - node, node.modifiers, node.name, node.typeParameters, node.heritageClauses, members); + node, + node.modifiers, + node.name, + node.typeParameters, + node.heritageClauses, + members, + ); } } } diff --git a/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/transform_api.ts b/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/transform_api.ts index df4cbcb8f1f28..6f108d6b6c688 100644 --- a/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/transform_api.ts +++ b/packages/compiler-cli/src/transformers/jit_transforms/initializer_api_transforms/transform_api.ts @@ -13,11 +13,15 @@ import {ClassMember, Decorator, ReflectionHost} from '../../../ngtsc/reflection' import {ImportManager} from '../../../ngtsc/translator'; /** Function that can be used to transform class properties. */ -export type PropertyTransform = - (member: Pick&{node: ts.PropertyDeclaration}, - host: ReflectionHost, factory: ts.NodeFactory, importTracker: ImportedSymbolsTracker, - importManager: ImportManager, classDecorator: Decorator, isCore: boolean) => - ts.PropertyDeclaration; +export type PropertyTransform = ( + member: Pick & {node: ts.PropertyDeclaration}, + host: ReflectionHost, + factory: ts.NodeFactory, + importTracker: ImportedSymbolsTracker, + importManager: ImportManager, + classDecorator: Decorator, + isCore: boolean, +) => ts.PropertyDeclaration; /** * Creates an import and access for a given Angular core import while @@ -26,22 +30,27 @@ export type PropertyTransform = * decorator downlevel transform. */ export function createSyntheticAngularCoreDecoratorAccess( - factory: ts.NodeFactory, importManager: ImportManager, ngClassDecorator: Decorator, - sourceFile: ts.SourceFile, decoratorName: string): ts.PropertyAccessExpression { - const classDecoratorIdentifier = ts.isIdentifier(ngClassDecorator.identifier) ? - ngClassDecorator.identifier : - ngClassDecorator.identifier.expression; + factory: ts.NodeFactory, + importManager: ImportManager, + ngClassDecorator: Decorator, + sourceFile: ts.SourceFile, + decoratorName: string, +): ts.PropertyAccessExpression { + const classDecoratorIdentifier = ts.isIdentifier(ngClassDecorator.identifier) + ? ngClassDecorator.identifier + : ngClassDecorator.identifier.expression; return factory.createPropertyAccessExpression( - importManager.addImport({ - exportModuleSpecifier: '@angular/core', - exportSymbolName: null, - requestedFile: sourceFile, - }), - // The synthetic identifier may be checked later by the downlevel decorators - // transform to resolve to an Angular import using `getSymbolAtLocation`. We trick - // the transform to think it's not synthetic and comes from Angular core. - ts.setOriginalNode(factory.createIdentifier(decoratorName), classDecoratorIdentifier)); + importManager.addImport({ + exportModuleSpecifier: '@angular/core', + exportSymbolName: null, + requestedFile: sourceFile, + }), + // The synthetic identifier may be checked later by the downlevel decorators + // transform to resolve to an Angular import using `getSymbolAtLocation`. We trick + // the transform to think it's not synthetic and comes from Angular core. + ts.setOriginalNode(factory.createIdentifier(decoratorName), classDecoratorIdentifier), + ); } /** Casts the given expression as `any`. */ diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index b4c838c45199f..e30685b6d4902 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -1,4 +1,3 @@ - /** * @license * Copyright Google LLC All Rights Reserved. @@ -11,11 +10,16 @@ import {NgtscProgram} from '../ngtsc/program'; import {CompilerHost, CompilerOptions, Program} from './api'; -export function createProgram({rootNames, options, host, oldProgram}: { - rootNames: ReadonlyArray, - options: CompilerOptions, - host: CompilerHost, - oldProgram?: Program +export function createProgram({ + rootNames, + options, + host, + oldProgram, +}: { + rootNames: ReadonlyArray; + options: CompilerOptions; + host: CompilerHost; + oldProgram?: Program; }): Program { return new NgtscProgram(rootNames, options, host, oldProgram as NgtscProgram | undefined); } diff --git a/packages/compiler-cli/src/transformers/util.ts b/packages/compiler-cli/src/transformers/util.ts index f6a2078873fff..28f433fdbe086 100644 --- a/packages/compiler-cli/src/transformers/util.ts +++ b/packages/compiler-cli/src/transformers/util.ts @@ -10,7 +10,6 @@ import ts from 'typescript'; import {DEFAULT_ERROR_CODE, SOURCE} from './api'; - export function error(msg: string): never { throw new Error(`Internal error: ${msg}`); } @@ -33,5 +32,8 @@ export function createMessageDiagnostic(messageText: string): ts.Diagnostic { * This will also strip the JSDOC comment start marker (`/**`). */ export function stripComment(commentText: string): string { - return commentText.replace(/^\/\*\*?/, '').replace(/\*\/$/, '').trim(); + return commentText + .replace(/^\/\*\*?/, '') + .replace(/\*\/$/, '') + .trim(); } diff --git a/packages/compiler-cli/src/typescript_support.ts b/packages/compiler-cli/src/typescript_support.ts index 82051d8a13008..75584a4d9b18b 100644 --- a/packages/compiler-cli/src/typescript_support.ts +++ b/packages/compiler-cli/src/typescript_support.ts @@ -54,9 +54,10 @@ export function restoreTypeScriptVersionForTesting(): void { * @throws Will throw an error if the given version ∉ [minVersion, maxVersion[ */ export function checkVersion(version: string, minVersion: string, maxVersion: string) { - if ((compareVersions(version, minVersion) < 0 || compareVersions(version, maxVersion) >= 0)) { - throw new Error(`The Angular Compiler requires TypeScript >=${minVersion} and <${ - maxVersion} but ${version} was found instead.`); + if (compareVersions(version, minVersion) < 0 || compareVersions(version, maxVersion) >= 0) { + throw new Error( + `The Angular Compiler requires TypeScript >=${minVersion} and <${maxVersion} but ${version} was found instead.`, + ); } } diff --git a/packages/compiler-cli/src/version_helpers.ts b/packages/compiler-cli/src/version_helpers.ts index 6f5fcdc9423c2..5f65ac8035d34 100644 --- a/packages/compiler-cli/src/version_helpers.ts +++ b/packages/compiler-cli/src/version_helpers.ts @@ -14,15 +14,18 @@ export function toNumbers(value: string): number[] { // Drop any suffixes starting with `-` so that versions like `1.2.3-rc.5` are treated as `1.2.3`. const suffixIndex = value.lastIndexOf('-'); - return value.slice(0, suffixIndex === -1 ? value.length : suffixIndex).split('.').map(segment => { - const parsed = parseInt(segment, 10); + return value + .slice(0, suffixIndex === -1 ? value.length : suffixIndex) + .split('.') + .map((segment) => { + const parsed = parseInt(segment, 10); - if (isNaN(parsed)) { - throw Error(`Unable to parse version string ${value}.`); - } + if (isNaN(parsed)) { + throw Error(`Unable to parse version string ${value}.`); + } - return parsed; - }); + return parsed; + }); } /** @@ -36,7 +39,7 @@ export function toNumbers(value: string): number[] { * @returns {-1|0|1} The comparison result: 1 if a is greater, -1 if b is greater, 0 is the two * arrays are equals */ -export function compareNumbers(a: number[], b: number[]): -1|0|1 { +export function compareNumbers(a: number[], b: number[]): -1 | 0 | 1 { const max = Math.max(a.length, b.length); const min = Math.min(a.length, b.length); @@ -76,8 +79,10 @@ export function compareNumbers(a: number[], b: number[]): -1|0|1 { export function isVersionBetween(version: string, low: string, high?: string): boolean { const tsNumbers = toNumbers(version); if (high !== undefined) { - return compareNumbers(toNumbers(low), tsNumbers) <= 0 && - compareNumbers(toNumbers(high), tsNumbers) >= 0; + return ( + compareNumbers(toNumbers(low), tsNumbers) <= 0 && + compareNumbers(toNumbers(high), tsNumbers) >= 0 + ); } return compareNumbers(toNumbers(low), tsNumbers) <= 0; } @@ -90,6 +95,6 @@ export function isVersionBetween(version: string, low: string, high?: string): b * @returns {-1|0|1} The comparison result: 1 if v1 is greater, -1 if v2 is greater, 0 is the two * versions are equals */ -export function compareVersions(v1: string, v2: string): -1|0|1 { +export function compareVersions(v1: string, v2: string): -1 | 0 | 1 { return compareNumbers(toNumbers(v1), toNumbers(v2)); } diff --git a/packages/compiler-cli/test/compliance/linked/linked_compile_spec.ts b/packages/compiler-cli/test/compliance/linked/linked_compile_spec.ts index d1bc051bd7ec9..933b916757137 100644 --- a/packages/compiler-cli/test/compliance/linked/linked_compile_spec.ts +++ b/packages/compiler-cli/test/compliance/linked/linked_compile_spec.ts @@ -36,21 +36,22 @@ function linkPartials(fileSystem: FileSystem, test: ComplianceTest): CompileResu fileSystem, logger, sourceMapping: test.compilerOptions?.['sourceMap'] === true, - ...test.angularCompilerOptions + ...test.angularCompilerOptions, }); const goldenPartialPath = fileSystem.resolve('/GOLDEN_PARTIAL.js'); if (!fileSystem.exists(goldenPartialPath)) { throw new Error( - 'Golden partial does not exist for this test\n' + + 'Golden partial does not exist for this test\n' + 'Try generating it by running:\n' + - `bazel run //packages/compiler-cli/test/compliance/test_cases:${ - test.relativePath}.golden.update`); + `bazel run //packages/compiler-cli/test/compliance/test_cases:${test.relativePath}.golden.update`, + ); } const partialFile = fileSystem.readFile(goldenPartialPath); const partialFiles = parseGoldenPartial(partialFile); - partialFiles.forEach( - f => safeWrite(fileSystem, fileSystem.resolve(builtDirectory, f.path), f.content)); + partialFiles.forEach((f) => + safeWrite(fileSystem, fileSystem.resolve(builtDirectory, f.path), f.content), + ); for (const expectation of test.expectations) { for (const {generated} of expectation.files) { @@ -60,11 +61,16 @@ function linkPartials(fileSystem: FileSystem, test: ComplianceTest): CompileResu } const source = fileSystem.readFile(fileName); const sourceMapPath = fileSystem.resolve(fileName + '.map'); - const sourceMap = fileSystem.exists(sourceMapPath) ? - JSON.parse(fileSystem.readFile(sourceMapPath)) as RawSourceMap : - undefined; - const {linkedSource, linkedSourceMap} = - applyLinker(builtDirectory, fileName, source, sourceMap, linkerPlugin); + const sourceMap = fileSystem.exists(sourceMapPath) + ? (JSON.parse(fileSystem.readFile(sourceMapPath)) as RawSourceMap) + : undefined; + const {linkedSource, linkedSourceMap} = applyLinker( + builtDirectory, + fileName, + source, + sourceMap, + linkerPlugin, + ); if (linkedSourceMap !== undefined) { const mapAndPath: MapAndPath = {map: linkedSourceMap, mapPath: sourceMapPath}; @@ -87,8 +93,12 @@ function linkPartials(fileSystem: FileSystem, test: ComplianceTest): CompileResu * @returns The file's source content, which has been transformed using the linker if necessary. */ function applyLinker( - cwd: string, filename: string, source: string, sourceMap: RawSourceMap|undefined, - linkerPlugin: PluginObj): {linkedSource: string, linkedSourceMap: RawSourceMap|undefined} { + cwd: string, + filename: string, + source: string, + sourceMap: RawSourceMap | undefined, + linkerPlugin: PluginObj, +): {linkedSource: string; linkedSourceMap: RawSourceMap | undefined} { if (!filename.endsWith('.js') || !needsLinking(filename, source)) { return {linkedSource: source, linkedSourceMap: sourceMap}; } diff --git a/packages/compiler-cli/test/compliance/partial/generate_golden_partial.ts b/packages/compiler-cli/test/compliance/partial/generate_golden_partial.ts index 7be0b7f2868d1..8ac80c8e85ada 100644 --- a/packages/compiler-cli/test/compliance/partial/generate_golden_partial.ts +++ b/packages/compiler-cli/test/compliance/partial/generate_golden_partial.ts @@ -6,7 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ import {AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system'; -import {compileTest, getBuildOutputDirectory, initMockTestFileSystem} from '../test_helpers/compile_test'; +import { + compileTest, + getBuildOutputDirectory, + initMockTestFileSystem, +} from '../test_helpers/compile_test'; import {ComplianceTest, getComplianceTests} from '../test_helpers/get_compliance_tests'; import {PartiallyCompiledFile, renderGoldenPartial} from '../test_helpers/golden_partials'; @@ -35,13 +39,15 @@ export function generateGoldenPartial(absTestConfigPath: AbsoluteFsPath): void { */ function* compilePartials(fs: FileSystem, test: ComplianceTest): Generator { const builtDirectory = getBuildOutputDirectory(fs); - const result = compileTest( - fs, test.inputFiles, test.compilerOptions, - {compilationMode: 'partial', ...test.angularCompilerOptions}); + const result = compileTest(fs, test.inputFiles, test.compilerOptions, { + compilationMode: 'partial', + ...test.angularCompilerOptions, + }); if (result.errors.length > 0) { throw new Error( - `Unexpected compilation errors: ${result.errors.map(e => ` - ${e}`).join('\n')}`); + `Unexpected compilation errors: ${result.errors.map((e) => ` - ${e}`).join('\n')}`, + ); } for (const generatedPath of result.emittedFiles) { diff --git a/packages/compiler-cli/test/compliance/test_helpers/check_errors.ts b/packages/compiler-cli/test/compliance/test_helpers/check_errors.ts index 5eccb24c44ac3..ca5091f146790 100644 --- a/packages/compiler-cli/test/compliance/test_helpers/check_errors.ts +++ b/packages/compiler-cli/test/compliance/test_helpers/check_errors.ts @@ -9,16 +9,25 @@ import {inspect} from 'util'; import {ExpectedError} from './get_compliance_tests'; export function checkErrors( - testPath: string, failureMessage: string, expectedErrors: ExpectedError[], - actualErrors: string[]): void { + testPath: string, + failureMessage: string, + expectedErrors: ExpectedError[], + actualErrors: string[], +): void { for (const expectedError of expectedErrors) { - if (!actualErrors.some( - actualError => expectedError.message.test(actualError) && - expectedError.location.test(actualError))) { + if ( + !actualErrors.some( + (actualError) => + expectedError.message.test(actualError) && expectedError.location.test(actualError), + ) + ) { throw new Error( - `When checking expected errors for test case at "${testPath}"\n` + failureMessage + '\n' + + `When checking expected errors for test case at "${testPath}"\n` + + failureMessage + + '\n' + `Expected errors: ${inspect(expectedErrors)}\n` + - `Actual errors: ${inspect(actualErrors)}.`); + `Actual errors: ${inspect(actualErrors)}.`, + ); } } } @@ -26,7 +35,8 @@ export function checkErrors( export function checkNoUnexpectedErrors(testPath: string, actualErrors: string[]): void { if (actualErrors.length > 0) { throw new Error( - `Unexpected errors occurred for test case at "${testPath}"\n` + - `Errors: ${inspect(actualErrors)}.`); + `Unexpected errors occurred for test case at "${testPath}"\n` + + `Errors: ${inspect(actualErrors)}.`, + ); } } diff --git a/packages/compiler-cli/test/compliance/test_helpers/check_expectations.ts b/packages/compiler-cli/test/compliance/test_helpers/check_expectations.ts index deebc54dbf674..aeb9f730dc9ea 100644 --- a/packages/compiler-cli/test/compliance/test_helpers/check_expectations.ts +++ b/packages/compiler-cli/test/compliance/test_helpers/check_expectations.ts @@ -36,24 +36,30 @@ const EXTRA_CHECK_FUNCTIONS: Record = { * https://github.com/angular/angular/issues/51647. */ export function checkExpectations( - fs: ReadonlyFileSystem, testPath: string, failureMessage: string, expectedFiles: ExpectedFile[], - extraChecks: ExtraCheck[], skipMappingCheck = false): void { + fs: ReadonlyFileSystem, + testPath: string, + failureMessage: string, + expectedFiles: ExpectedFile[], + extraChecks: ExtraCheck[], + skipMappingCheck = false, +): void { const builtDirectory = getBuildOutputDirectory(fs); for (const expectedFile of expectedFiles) { const expectedPath = fs.resolve(getRootDirectory(fs), expectedFile.expected); if (!fs.exists(expectedPath)) { - throw new Error(`The expected file at ${ - expectedPath} does not exist. Please check the TEST_CASES.json file for this test case.`); + throw new Error( + `The expected file at ${expectedPath} does not exist. Please check the TEST_CASES.json file for this test case.`, + ); } const generatedPath = fs.resolve(builtDirectory, expectedFile.generated); if (!fs.exists(generatedPath)) { const error = new Error( - `The generated file at ${generatedPath} does not exist.\n` + + `The generated file at ${generatedPath} does not exist.\n` + 'Perhaps there is no matching input source file in the TEST_CASES.json file for this test case.\n' + 'Or maybe you need to regenerate the GOLDEN_PARTIAL.js file by running:\n\n' + - ` yarn bazel run //packages/compiler-cli/test/compliance/test_cases:${ - testPath}.golden.update`); + ` yarn bazel run //packages/compiler-cli/test/compliance/test_cases:${testPath}.golden.update`, + ); // Clear the stack so that we get a nice error message error.stack = ''; throw error; @@ -62,20 +68,30 @@ export function checkExpectations( let expected = fs.readFile(expectedPath); expected = replaceMacros(expected); expected = stripAndCheckMappings( - fs, generated, generatedPath, expected, expectedPath, - /** skipMappingCheck */ !!skipMappingCheck); + fs, + generated, + generatedPath, + expected, + expectedPath, + /** skipMappingCheck */ !!skipMappingCheck, + ); expectEmit( - generated, expected, - `When checking against expected file "${testPath}/${expectedFile.expected}"\n` + - failureMessage); + generated, + expected, + `When checking against expected file "${testPath}/${expectedFile.expected}"\n` + + failureMessage, + ); runExtraChecks(testPath, generated, extraChecks); } } function runExtraChecks( - testPath: string, generated: string, extraChecks: (string|[string, ...any])[]): void { + testPath: string, + generated: string, + extraChecks: (string | [string, ...any])[], +): void { for (const check of extraChecks) { let fnName: string; let args: any[]; @@ -88,13 +104,14 @@ function runExtraChecks( const fn = EXTRA_CHECK_FUNCTIONS[fnName]; if (fn === undefined) { throw new Error( - `Unknown extra-check function: "${fnName}" in ${testPath}.\n` + - `Possible choices are: ${Object.keys(EXTRA_CHECK_FUNCTIONS).map(f => `\n - ${f}`)}.`); + `Unknown extra-check function: "${fnName}" in ${testPath}.\n` + + `Possible choices are: ${Object.keys(EXTRA_CHECK_FUNCTIONS).map((f) => `\n - ${f}`)}.`, + ); } if (!fn(generated, ...args)) { throw new Error( - `Extra check ${fnName}(${args.map(arg => JSON.stringify(arg)).join(',')}) in ${ - testPath} failed for generated code:\n\n${generated}`); + `Extra check ${fnName}(${args.map((arg) => JSON.stringify(arg)).join(',')}) in ${testPath} failed for generated code:\n\n${generated}`, + ); } } } diff --git a/packages/compiler-cli/test/compliance/test_helpers/compile_test.ts b/packages/compiler-cli/test/compliance/test_helpers/compile_test.ts index c32308ddf8ad4..1ae10627e13c9 100644 --- a/packages/compiler-cli/test/compliance/test_helpers/compile_test.ts +++ b/packages/compiler-cli/test/compliance/test_helpers/compile_test.ts @@ -7,9 +7,18 @@ */ import ts from 'typescript'; -import {AbsoluteFsPath, FileSystem, PathManipulation, ReadonlyFileSystem} from '../../../src/ngtsc/file_system'; +import { + AbsoluteFsPath, + FileSystem, + PathManipulation, + ReadonlyFileSystem, +} from '../../../src/ngtsc/file_system'; import {initMockFileSystem} from '../../../src/ngtsc/file_system/testing'; -import {loadStandardTestFiles, loadTestDirectory, NgtscTestCompilerHost} from '../../../src/ngtsc/testing'; +import { + loadStandardTestFiles, + loadTestDirectory, + NgtscTestCompilerHost, +} from '../../../src/ngtsc/testing'; import {performCompilation} from '../../../src/perform_compile'; import {CompilerOptions} from '../../../src/transformers/api'; @@ -46,15 +55,20 @@ export interface CompileResult { * @returns A collection of paths of the generated files (absolute within the mock file-system). */ export function compileTest( - fs: FileSystem, files: string[], compilerOptions: ConfigOptions|undefined, - angularCompilerOptions: ConfigOptions|undefined): CompileResult { + fs: FileSystem, + files: string[], + compilerOptions: ConfigOptions | undefined, + angularCompilerOptions: ConfigOptions | undefined, +): CompileResult { const rootDir = getRootDirectory(fs); const outDir = getBuildOutputDirectory(fs); const options = getOptions(rootDir, outDir, compilerOptions, angularCompilerOptions); - const rootNames = files.map(f => fs.resolve(f)); + const rootNames = files.map((f) => fs.resolve(f)); const host = new NgtscTestCompilerHost(fs, options); const {diagnostics, emitResult} = performCompilation({rootNames, host, options}); - const emittedFiles = emitResult ? emitResult.emittedFiles!.map(p => fs.resolve(rootDir, p)) : []; + const emittedFiles = emitResult + ? emitResult.emittedFiles!.map((p) => fs.resolve(rootDir, p)) + : []; const errors = parseDiagnostics(diagnostics); return {errors, emittedFiles}; } @@ -90,13 +104,17 @@ export function getBuildOutputDirectory(fs: PathManipulation): AbsoluteFsPath { * @param angularCompilerOptions Additional options for the Angular compiler. */ function getOptions( - rootDir: AbsoluteFsPath, outDir: AbsoluteFsPath, compilerOptions: ConfigOptions|undefined, - angularCompilerOptions: ConfigOptions|undefined): CompilerOptions { + rootDir: AbsoluteFsPath, + outDir: AbsoluteFsPath, + compilerOptions: ConfigOptions | undefined, + angularCompilerOptions: ConfigOptions | undefined, +): CompilerOptions { const convertedCompilerOptions = ts.convertCompilerOptionsFromJson(compilerOptions, rootDir); if (convertedCompilerOptions.errors.length > 0) { throw new Error( - 'Invalid compilerOptions in test-case::\n' + - convertedCompilerOptions.errors.map(d => d.messageText).join('\n')); + 'Invalid compilerOptions in test-case::\n' + + convertedCompilerOptions.errors.map((d) => d.messageText).join('\n'), + ); } return { emitDecoratorMetadata: true, @@ -132,13 +150,15 @@ function monkeyPatchReadFile(fs: ReadonlyFileSystem): void { const originalReadFile = fs.readFile; fs.readFile = (path: AbsoluteFsPath): string => { const file = originalReadFile.call(fs, path); - return file + return ( + file // First convert actual `\r\n` sequences to `\n` .replace(/\r\n/g, '\n') // unescape `\r\n` at the end of a line .replace(/\\r\\n\n/g, '\r\n') // unescape `\\r\\n`, at the end of a line, to `\r\n` - .replace(/\\\\r\\\\n(\r?\n)/g, '\\r\\n$1'); + .replace(/\\\\r\\\\n(\r?\n)/g, '\\r\\n$1') + ); }; } @@ -150,7 +170,7 @@ function monkeyPatchReadFile(fs: ReadonlyFileSystem): void { * @param diagnostics The diagnostics to parse. */ function parseDiagnostics(diagnostics: readonly ts.Diagnostic[]): string[] { - return diagnostics.map(diagnostic => { + return diagnostics.map((diagnostic) => { const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); if ('file' in diagnostic && diagnostic.file !== undefined && diagnostic.start !== undefined) { const {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); diff --git a/packages/compiler-cli/test/compliance/test_helpers/expect_emit.ts b/packages/compiler-cli/test/compliance/test_helpers/expect_emit.ts index e23eee8beb959..eee0a8713a20d 100644 --- a/packages/compiler-cli/test/compliance/test_helpers/expect_emit.ts +++ b/packages/compiler-cli/test/compliance/test_helpers/expect_emit.ts @@ -11,7 +11,7 @@ const IDENTIFIER = /[A-Za-z_$ɵ][A-Za-z0-9_$]*/; const COMMENT_START = /\/\*/; const COMMENT_END = /\*\//; const OPERATOR = - /!|\?|%|\*|\/|\^|&&?|\|\|?|\(|\)|\{|\}|\[|\]|:|;|<=?|>=?|={1,3}|!==?|=>|\+\+?|--?|@|,|\.|\.\.\.|\\`|\\'/; + /!|\?|%|\*|\/|\^|&&?|\|\|?|\(|\)|\{|\}|\[|\]|:|;|<=?|>=?|={1,3}|!==?|=>|\+\+?|--?|@|,|\.|\.\.\.|\\`|\\'/; const STRING = /'(\\'|[^'])*'|"(\\"|[^"])*"/; const INLINE_BACKTICK_STRING = /`(?:[\s\S]|(?:\$\{[^}]*?\}))*?[^\\]`/; const SINGLE_BACKTICK_STRING = new RegExp('^' + INLINE_BACKTICK_STRING.source); @@ -20,12 +20,11 @@ const NUMBER = /\d+/; const ELLIPSIS = '…'; const TOKEN = new RegExp( - `\\s*((${COMMENT_START.source})|(${COMMENT_END.source})|(${IDENTIFIER.source})|(${ - INLINE_BACKTICK_STRING.source})|(${OPERATOR.source})|(${STRING.source})|${NUMBER.source}|${ - ELLIPSIS})\\s*`, - 'y'); + `\\s*((${COMMENT_START.source})|(${COMMENT_END.source})|(${IDENTIFIER.source})|(${INLINE_BACKTICK_STRING.source})|(${OPERATOR.source})|(${STRING.source})|${NUMBER.source}|${ELLIPSIS})\\s*`, + 'y', +); -type Piece = string|RegExp; +type Piece = string | RegExp; const SKIP = /(?:.|\n|\r)*/; @@ -36,7 +35,7 @@ function tokenize(text: string): Piece[] { const lastIndex = TOKEN.lastIndex; TOKEN.lastIndex = 0; - let match: RegExpMatchArray|null; + let match: RegExpMatchArray | null; let tokenizedTextEnd = 0; let inComment = false; const pieces: Piece[] = []; @@ -46,7 +45,7 @@ function tokenize(text: string): Piece[] { pieces.push(IDENTIFIER); } else if (token === ELLIPSIS) { pieces.push(SKIP); - } else if (match = SINGLE_BACKTICK_STRING.exec(token)) { + } else if ((match = SINGLE_BACKTICK_STRING.exec(token))) { if (inComment) { // We are in a comment block so just treat a backtick as a normal token. // Store the token and reset the matcher. @@ -68,8 +67,9 @@ function tokenize(text: string): Piece[] { const from = tokenizedTextEnd; const to = from + ERROR_CONTEXT_WIDTH; throw Error( - `Invalid test, no token found for "${text[tokenizedTextEnd]}" ` + - `(context = '${text.slice(from, to)}...'`); + `Invalid test, no token found for "${text[tokenizedTextEnd]}" ` + + `(context = '${text.slice(from, to)}...'`, + ); } // Reset the lastIndex in case we are in a recursive `tokenize()` call. TOKEN.lastIndex = lastIndex; @@ -112,15 +112,18 @@ const RED = '\x1b[31m'; const GREEN = '\x1b[32m'; export function expectEmit( - source: string, expected: string, description: string, - assertIdentifiers?: {[name: string]: RegExp}) { + source: string, + expected: string, + description: string, + assertIdentifiers?: {[name: string]: RegExp}, +) { expected = expected - // turns `// ...` into `…` - .replace(/\/\/\s*\.\.\./g, ELLIPSIS) - // remove `// TODO` comment lines - .replace(/\/\/\s*TODO.*?\n/g, '') - // remove `// NOTE` comment lines - .replace(/\/\/\s*NOTE.*?\n/g, ''); + // turns `// ...` into `…` + .replace(/\/\/\s*\.\.\./g, ELLIPSIS) + // remove `// TODO` comment lines + .replace(/\/\/\s*TODO.*?\n/g, '') + // remove `// NOTE` comment lines + .replace(/\/\/\s*NOTE.*?\n/g, ''); const pieces = tokenize(expected); const {regexp, groups} = buildMatcher(pieces); @@ -135,24 +138,26 @@ export function expectEmit( // display at most `contextLength` characters of the line preceding the error location const contextLength = 50; const fullContext = source.substring(source.lastIndexOf('\n', last) + 1, last); - const context = fullContext.length > contextLength ? - `...${fullContext.slice(-contextLength)}` : - fullContext; + const context = + fullContext.length > contextLength + ? `...${fullContext.slice(-contextLength)}` + : fullContext; throw new Error( - `${RED}${description}:\n${RESET}${BLUE}Failed to find${RESET} "${expectedPiece}"\n` + + `${RED}${description}:\n${RESET}${BLUE}Failed to find${RESET} "${expectedPiece}"\n` + `${BLUE}After ${RESET}"${context}"\n` + `${BLUE}In generated file:${RESET}\n\n` + `${source.slice(0, last)}` + `${RED}[[[ <<<<---HERE expected "${GREEN}${expectedPiece}${RED}" ]]]${RESET}` + - `${source.slice(last)}`); + `${source.slice(last)}`, + ); } else { last = (m.index || 0) + m[0].length; } } throw new Error( - `Test helper failure: Expected expression failed but the reporting logic could not find where it failed in: ${ - source}`); + `Test helper failure: Expected expression failed but the reporting logic could not find where it failed in: ${source}`, + ); } else { if (assertIdentifiers) { // It might be possible to add the constraints in the original regexp (see `buildMatcher`) @@ -169,8 +174,9 @@ export function expectEmit( const name = matches[groups.get(id) as number]; const regexp = assertIdentifiers[id]; if (!regexp.test(name)) { - throw Error(`${description}: The matching identifier "${id}" is "${ - name}" which doesn't match ${regexp}`); + throw Error( + `${description}: The matching identifier "${id}" is "${name}" which doesn't match ${regexp}`, + ); } } } @@ -188,7 +194,7 @@ const MATCHING_IDENT = /^\$.*\$$/; * - the `regexp` to be used to match the generated code, * - the `groups` which maps `$...$` identifier to their position in the regexp matches. */ -function buildMatcher(pieces: (string|RegExp)[]): {regexp: RegExp, groups: Map} { +function buildMatcher(pieces: (string | RegExp)[]): {regexp: RegExp; groups: Map} { const results: string[] = []; let first = true; let group = 0; diff --git a/packages/compiler-cli/test/compliance/test_helpers/expected_file_macros.ts b/packages/compiler-cli/test/compliance/test_helpers/expected_file_macros.ts index 7c1fbe8552013..5e4f47784416e 100644 --- a/packages/compiler-cli/test/compliance/test_helpers/expected_file_macros.ts +++ b/packages/compiler-cli/test/compliance/test_helpers/expected_file_macros.ts @@ -8,31 +8,52 @@ import {AttributeMarker, SelectorFlags} from '@angular/compiler/src/core'; import {QueryFlags} from '@angular/compiler/src/render3/view/query_generation'; -import {i18nIcuMsg, i18nMsg, i18nMsgWithPostprocess, Options, Placeholder, resetMessageIndex} from './i18n_helpers'; +import { + i18nIcuMsg, + i18nMsg, + i18nMsgWithPostprocess, + Options, + Placeholder, + resetMessageIndex, +} from './i18n_helpers'; const EXPECTED_FILE_MACROS: [RegExp, (...args: string[]) => string][] = [ [ // E.g. `__i18nMsg__('message string', [ ['placeholder', 'pair'] // ], {original_code: {'placeholder': '{{ foo }}'}}, {meta: 'properties'})` macroFn(/__i18nMsg__/, stringParam(), arrayParam(), objectParam(), objectParam()), - (_match, message, placeholders, options, meta) => i18nMsg( - message, parsePlaceholders(placeholders), parseOptions(options), parseMetaProperties(meta)), + (_match, message, placeholders, options, meta) => + i18nMsg( + message, + parsePlaceholders(placeholders), + parseOptions(options), + parseMetaProperties(meta), + ), ], [ // E.g. `__i18nMsgWithPostprocess__('message', [ ['placeholder', 'pair'] ], { meta: 'props'})` macroFn( - /__i18nMsgWithPostprocess__/, stringParam(), arrayParam(), objectParam(), objectParam(), - arrayParam()), + /__i18nMsgWithPostprocess__/, + stringParam(), + arrayParam(), + objectParam(), + objectParam(), + arrayParam(), + ), (_match, message, placeholders, options, meta, postProcessPlaceholders) => - i18nMsgWithPostprocess( - message, parsePlaceholders(placeholders), parseOptions(options), - parseMetaProperties(meta), parsePlaceholders(postProcessPlaceholders)), + i18nMsgWithPostprocess( + message, + parsePlaceholders(placeholders), + parseOptions(options), + parseMetaProperties(meta), + parsePlaceholders(postProcessPlaceholders), + ), ], [ // E.g. `__i18nIcuMsg__('message string', [ ['placeholder', 'pair'] ])` macroFn(/__i18nIcuMsg__/, stringParam(), arrayParam(), objectParam()), (_match, message, placeholders, options) => - i18nIcuMsg(message, parsePlaceholders(placeholders), parseOptions(options)), + i18nIcuMsg(message, parsePlaceholders(placeholders), parseOptions(options)), ], [ // E.g. `__AttributeMarker.Bindings__` @@ -63,13 +84,21 @@ export function replaceMacros(expectedContent: string): string { function parsePlaceholders(str: string): Placeholder[] { const placeholders = eval(`(${str})`); - if (!Array.isArray(placeholders) || - !placeholders.every( - p => Array.isArray(p) && p.length >= 2 && typeof p[0] === 'string' && - typeof p[1] === 'string' && (p.length === 2 || typeof p[2] === 'string'))) { + if ( + !Array.isArray(placeholders) || + !placeholders.every( + (p) => + Array.isArray(p) && + p.length >= 2 && + typeof p[0] === 'string' && + typeof p[1] === 'string' && + (p.length === 2 || typeof p[2] === 'string'), + ) + ) { throw new Error( - 'Expected an array of Placeholder arrays (`[name: string, identifier: string, associatedId?: string]`) but got ' + - str); + 'Expected an array of Placeholder arrays (`[name: string, identifier: string, associatedId?: string]`) but got ' + + str, + ); } return placeholders; } @@ -85,21 +114,29 @@ function parseOptions(str: string): Options { // authored incorrectly. const unexpectedKeys = Object.keys(obj).filter((key) => key !== 'original_code'); if (unexpectedKeys.length > 0) { - throw new Error(`Expected an i18n options object with \`original_code\`, but got ${ - unexpectedKeys.join(', ')}`); + throw new Error( + `Expected an i18n options object with \`original_code\`, but got ${unexpectedKeys.join( + ', ', + )}`, + ); } // Validate `original_code`. const original = obj?.['original_code']; if (typeof original !== 'undefined' && typeof original !== 'object') { throw new Error( - `Expected an i18n options object with \`original_code\`, as a nested object, but got ${ - JSON.stringify(obj, null, 4)}`); + `Expected an i18n options object with \`original_code\`, as a nested object, but got ${JSON.stringify( + obj, + null, + 4, + )}`, + ); } for (const [key, value] of Object.entries(original ?? {})) { if (typeof value !== 'string') { - throw new Error(`Expected an object whose values are strings, but property ${key} has type ${ - typeof value}, when parsing:\n\n${str}`); + throw new Error( + `Expected an object whose values are strings, but property ${key} has type ${typeof value}, when parsing:\n\n${str}`, + ); } } @@ -113,8 +150,11 @@ function parseMetaProperties(str: string): Record { } for (const key in obj) { if (typeof obj[key] !== 'string') { - throw new Error(`Expected an object whose values are strings, but property ${key} has type ${ - typeof obj[key]}, when parsing:\n\n${str}`); + throw new Error( + `Expected an object whose values are strings, but property ${key} has type ${typeof obj[ + key + ]}, when parsing:\n\n${str}`, + ); } } return obj; @@ -188,8 +228,9 @@ function objectParam() { function macroFn(fnName: RegExp, ...args: RegExp[]): RegExp { const ws = /[\s\r\n]*/.source; return new RegExp( - ws + fnName.source + '\\(' + args.map(r => `${ws}${r.source}${ws}`).join(',') + '\\)' + ws, - 'g'); + ws + fnName.source + '\\(' + args.map((r) => `${ws}${r.source}${ws}`).join(',') + '\\)' + ws, + 'g', + ); } /** @@ -198,17 +239,22 @@ function macroFn(fnName: RegExp, ...args: RegExp[]): RegExp { * @param pattern The regex to match a single occurrence of the flag. * @param getFlagValue A function to extract the numeric flag value from the pattern. */ -function flagUnion(pattern: RegExp, getFlagValue: (...match: string[]) => number): - typeof EXPECTED_FILE_MACROS[number] { +function flagUnion( + pattern: RegExp, + getFlagValue: (...match: string[]) => number, +): (typeof EXPECTED_FILE_MACROS)[number] { return [ // Match at least one occurrence of the pattern, optionally followed by more occurrences // separated by a pipe. - new RegExp(pattern.source + '(?:s*\\\|s*' + pattern.source + ')*', 'g'), + new RegExp(pattern.source + '(?:s*\\|s*' + pattern.source + ')*', 'g'), (match: string) => { // Replace all matches with the union of the individually matched flags. - return String(match.split('|') - .map(flag => getFlagValue(...flag.trim().match(pattern)!)) - .reduce((accumulator, flagValue) => accumulator | flagValue, 0)); + return String( + match + .split('|') + .map((flag) => getFlagValue(...flag.trim().match(pattern)!)) + .reduce((accumulator, flagValue) => accumulator | flagValue, 0), + ); }, ]; } diff --git a/packages/compiler-cli/test/compliance/test_helpers/function_checks.ts b/packages/compiler-cli/test/compliance/test_helpers/function_checks.ts index 6ed3b39579928..4765c93084500 100644 --- a/packages/compiler-cli/test/compliance/test_helpers/function_checks.ts +++ b/packages/compiler-cli/test/compliance/test_helpers/function_checks.ts @@ -14,16 +14,23 @@ * @param expectedCount Expected number of functions. */ export function verifyUniqueFunctions( - output: string, functionNamePattern?: string, expectedCount?: number): boolean { + output: string, + functionNamePattern?: string, + expectedCount?: number, +): boolean { const pattern = functionNamePattern ? new RegExp(functionNamePattern) : null; const allTemplateFunctionsNames = (output.match(/function ([^\s(]+)/g) || []) - .map(match => match.slice(9)) - .filter(name => !pattern || pattern.test(name)); + .map((match) => match.slice(9)) + .filter((name) => !pattern || pattern.test(name)); const uniqueTemplateFunctionNames = new Set(allTemplateFunctionsNames); const lengthMatches = allTemplateFunctionsNames.length === uniqueTemplateFunctionNames.size; const expectedCountMatches = - (expectedCount == null ? allTemplateFunctionsNames.length > 0 : - allTemplateFunctionsNames.length === expectedCount); - return lengthMatches && expectedCountMatches && - allTemplateFunctionsNames.every(name => uniqueTemplateFunctionNames.has(name)); + expectedCount == null + ? allTemplateFunctionsNames.length > 0 + : allTemplateFunctionsNames.length === expectedCount; + return ( + lengthMatches && + expectedCountMatches && + allTemplateFunctionsNames.every((name) => uniqueTemplateFunctionNames.has(name)) + ); } diff --git a/packages/compiler-cli/test/compliance/test_helpers/get_compliance_tests.ts b/packages/compiler-cli/test/compliance/test_helpers/get_compliance_tests.ts index da7d58d10ac2e..ea9eee08288f5 100644 --- a/packages/compiler-cli/test/compliance/test_helpers/get_compliance_tests.ts +++ b/packages/compiler-cli/test/compliance/test_helpers/get_compliance_tests.ts @@ -7,13 +7,19 @@ */ import {runfiles} from '@bazel/runfiles'; -import {AbsoluteFsPath, NodeJSFileSystem, PathSegment, ReadonlyFileSystem} from '../../../src/ngtsc/file_system'; +import { + AbsoluteFsPath, + NodeJSFileSystem, + PathSegment, + ReadonlyFileSystem, +} from '../../../src/ngtsc/file_system'; export const fs = new NodeJSFileSystem(); /** Path to the test case sources. */ const basePath = fs.resolve( - runfiles.resolveWorkspaceRelative('packages/compiler-cli/test/compliance/test_cases')); + runfiles.resolveWorkspaceRelative('packages/compiler-cli/test/compliance/test_cases'), +); /** * Search the `test_cases` directory, in the real file-system, for all the compliance tests. @@ -21,7 +27,7 @@ const basePath = fs.resolve( * Test are indicated by a `TEST_CASES.json` file which contains one or more test cases. */ export function* getAllComplianceTests(): Generator { - const testConfigPaths = collectPaths(basePath, segment => segment === 'TEST_CASES.json'); + const testConfigPaths = collectPaths(basePath, (segment) => segment === 'TEST_CASES.json'); for (const testConfigPath of testConfigPaths) { yield* getComplianceTests(testConfigPath); } @@ -40,8 +46,11 @@ export function* getComplianceTests(absTestConfigPath: AbsoluteFsPath): Generato for (const test of testConfig) { const inputFiles = getStringArrayOrDefault(test, 'inputFiles', realTestPath, ['test.ts']); const compilationModeFilter = getStringArrayOrDefault( - test, 'compilationModeFilter', realTestPath, - ['linked compile', 'full compile']) as CompilationMode[]; + test, + 'compilationModeFilter', + realTestPath, + ['linked compile', 'full compile'], + ) as CompilationMode[]; yield { relativePath: fs.relative(basePath, realTestPath), @@ -60,21 +69,28 @@ export function* getComplianceTests(absTestConfigPath: AbsoluteFsPath): Generato } function loadTestCasesFile( - fs: ReadonlyFileSystem, testCasesPath: AbsoluteFsPath, basePath: AbsoluteFsPath) { + fs: ReadonlyFileSystem, + testCasesPath: AbsoluteFsPath, + basePath: AbsoluteFsPath, +) { try { return JSON.parse(fs.readFile(testCasesPath)) as {cases: TestCaseJson | TestCaseJson[]}; } catch (e) { - throw new Error(`Failed to load test-cases at "${fs.relative(basePath, testCasesPath)}":\n ${ - (e as Error).message}`); + throw new Error( + `Failed to load test-cases at "${fs.relative(basePath, testCasesPath)}":\n ${ + (e as Error).message + }`, + ); } } /** * Search the file-system from the `current` path to find all paths that satisfy the `predicate`. */ -function* - collectPaths(current: AbsoluteFsPath, predicate: (segment: PathSegment) => boolean): - Generator { +function* collectPaths( + current: AbsoluteFsPath, + predicate: (segment: PathSegment) => boolean, +): Generator { if (!fs.exists(current)) { return; } @@ -99,35 +115,45 @@ function getStringOrFail(container: any, property: string, testPath: AbsoluteFsP } function getStringArrayOrDefault( - container: any, property: string, testPath: AbsoluteFsPath, defaultValue: string[]): string[] { + container: any, + property: string, + testPath: AbsoluteFsPath, + defaultValue: string[], +): string[] { const value = container[property]; if (typeof value === 'undefined') { return defaultValue; } - if (!Array.isArray(value) || !value.every(item => typeof item === 'string')) { + if (!Array.isArray(value) || !value.every((item) => typeof item === 'string')) { throw new Error( - `Test has invalid "${property}" property in TEST_CASES.json - expected array of strings: ` + - testPath); + `Test has invalid "${property}" property in TEST_CASES.json - expected array of strings: ` + + testPath, + ); } return value; } function parseExpectations( - value: any, testPath: AbsoluteFsPath, inputFiles: string[]): Expectation[] { + value: any, + testPath: AbsoluteFsPath, + inputFiles: string[], +): Expectation[] { const defaultFailureMessage = 'Incorrect generated output.'; - const tsFiles = inputFiles.filter(f => f.endsWith('.ts') && !f.endsWith('.d.ts')); - const defaultFiles = tsFiles.map(inputFile => { + const tsFiles = inputFiles.filter((f) => f.endsWith('.ts') && !f.endsWith('.d.ts')); + const defaultFiles = tsFiles.map((inputFile) => { const outputFile = inputFile.replace(/\.ts$/, '.js'); return {expected: outputFile, generated: outputFile}; }); if (typeof value === 'undefined') { - return [{ - failureMessage: defaultFailureMessage, - files: defaultFiles, - expectedErrors: [], - extraChecks: [] - }]; + return [ + { + failureMessage: defaultFailureMessage, + files: defaultFiles, + expectedErrors: [], + extraChecks: [], + }, + ]; } if (!Array.isArray(value)) { @@ -137,8 +163,8 @@ function parseExpectations( return value.map((expectation, i) => { if (typeof expectation !== 'object') { throw new Error( - `Test has invalid "expectations" property in TEST_CASES.json - expected array of "expectation" objects: ${ - testPath}`); + `Test has invalid "expectations" property in TEST_CASES.json - expected array of "expectation" objects: ${testPath}`, + ); } const failureMessage: string = expectation.failureMessage ?? defaultFailureMessage; @@ -150,21 +176,24 @@ function parseExpectations( } if (!Array.isArray(expectation.files)) { - throw new Error(`Test has invalid "expectations[${ - i}].files" property in TEST_CASES.json - expected array of "expected files": ${ - testPath}`); + throw new Error( + `Test has invalid "expectations[${i}].files" property in TEST_CASES.json - expected array of "expected files": ${testPath}`, + ); } const files: ExpectedFile[] = expectation.files.map((file: any) => { if (typeof file === 'string') { return {expected: file, generated: file}; } - if (typeof file === 'object' && typeof file.expected === 'string' && - typeof file.generated === 'string') { + if ( + typeof file === 'object' && + typeof file.expected === 'string' && + typeof file.generated === 'string' + ) { return file; } - throw new Error(`Test has invalid "expectations[${ - i}].files" property in TEST_CASES.json - expected each item to be a string or an "expected file" object: ${ - testPath}`); + throw new Error( + `Test has invalid "expectations[${i}].files" property in TEST_CASES.json - expected each item to be a string or an "expected file" object: ${testPath}`, + ); }); return {failureMessage, files, expectedErrors, extraChecks}; @@ -174,43 +203,54 @@ function parseExpectations( function parseExpectedErrors(expectedErrors: any = [], testPath: AbsoluteFsPath): ExpectedError[] { if (!Array.isArray(expectedErrors)) { throw new Error( - 'Test has invalid "expectedErrors" property in TEST_CASES.json - expected an array: ' + - testPath); + 'Test has invalid "expectedErrors" property in TEST_CASES.json - expected an array: ' + + testPath, + ); } - return expectedErrors.map(error => { - if (typeof error !== 'object' || typeof error.message !== 'string' || - (error.location && typeof error.location !== 'string')) { + return expectedErrors.map((error) => { + if ( + typeof error !== 'object' || + typeof error.message !== 'string' || + (error.location && typeof error.location !== 'string') + ) { throw new Error( - `Test has invalid "expectedErrors" property in TEST_CASES.json - expected an array of ExpectedError objects: ` + - testPath); + `Test has invalid "expectedErrors" property in TEST_CASES.json - expected an array of ExpectedError objects: ` + + testPath, + ); } return {message: parseRegExp(error.message), location: parseRegExp(error.location)}; }); } function parseExtraChecks(extraChecks: any = [], testPath: AbsoluteFsPath): ExtraCheck[] { - if (!Array.isArray(extraChecks) || - !extraChecks.every(i => typeof i === 'string' || Array.isArray(i))) { + if ( + !Array.isArray(extraChecks) || + !extraChecks.every((i) => typeof i === 'string' || Array.isArray(i)) + ) { throw new Error( - `Test has invalid "extraChecks" property in TEST_CASES.json - expected an array of strings or arrays: ` + - testPath); + `Test has invalid "extraChecks" property in TEST_CASES.json - expected an array of strings or arrays: ` + + testPath, + ); } return extraChecks; } -function parseRegExp(str: string|undefined): RegExp { +function parseRegExp(str: string | undefined): RegExp { return new RegExp(str || ''); } function getConfigOptions( - container: any, property: string, testPath: AbsoluteFsPath): ConfigOptions|undefined { + container: any, + property: string, + testPath: AbsoluteFsPath, +): ConfigOptions | undefined { const options = container[property]; if (options !== undefined && typeof options !== 'object') { throw new Error( - `Test have invalid "${ - property}" property in TEST_CASES.json - expected config option object: ` + - testPath); + `Test have invalid "${property}" property in TEST_CASES.json - expected config option object: ` + + testPath, + ); } return options; } @@ -252,7 +292,7 @@ export interface ComplianceTest { excludeTest?: boolean; } -export type CompilationMode = 'linked compile'|'full compile'|'local compile'; +export type CompilationMode = 'linked compile' | 'full compile' | 'local compile'; export interface Expectation { /** The message to display if this expectation fails. */ @@ -285,26 +325,24 @@ export interface ExpectedError { * The name (or name and arguments) of a function to call to run additional checks against the * generated code. */ -export type ExtraCheck = (string|[string, ...any]); +export type ExtraCheck = string | [string, ...any]; /** * Options to pass to configure the compiler. */ -export type ConfigOptions = Record; - - +export type ConfigOptions = Record; /** * Interface espressing the type for the json object found at ../test_cases/test_case_schema.json. */ export interface TestCaseJson { description: string; - compilationModeFilter?: ('fulll compile'|'linked compile')[]; + compilationModeFilter?: ('fulll compile' | 'linked compile')[]; inputFiles?: string[]; expectations?: { failureMessage?: string; files?: ExpectedFile[] | string; - expectedErrors?: {message: string, location?: string}; + expectedErrors?: {message: string; location?: string}; extraChecks?: (string | string[])[]; }; compilerOptions?: ConfigOptions; diff --git a/packages/compiler-cli/test/compliance/test_helpers/golden_partials.ts b/packages/compiler-cli/test/compliance/test_helpers/golden_partials.ts index 680d7cdf777ba..f09e413c172d8 100644 --- a/packages/compiler-cli/test/compliance/test_helpers/golden_partials.ts +++ b/packages/compiler-cli/test/compliance/test_helpers/golden_partials.ts @@ -7,11 +7,11 @@ */ const headerStart = - '/****************************************************************************************************\n' + - ' * PARTIAL FILE: '; + '/****************************************************************************************************\n' + + ' * PARTIAL FILE: '; const headerEnd = - '\n ****************************************************************************************************/\n'; + '\n ****************************************************************************************************/\n'; /** * Render the partially compiled files into a single golden partial output string. @@ -19,7 +19,7 @@ const headerEnd = * @param files The partially compiled files to be rendered. */ export function renderGoldenPartial(files: PartiallyCompiledFile[]): string { - return files.map(file => `${headerStart + file.path + headerEnd}${file.content}`).join('\n'); + return files.map((file) => `${headerStart + file.path + headerEnd}${file.content}`).join('\n'); } /** diff --git a/packages/compiler-cli/test/compliance/test_helpers/i18n_checks.ts b/packages/compiler-cli/test/compliance/test_helpers/i18n_checks.ts index dd1ec9bdf14da..7db8b14c94539 100644 --- a/packages/compiler-cli/test/compliance/test_helpers/i18n_checks.ts +++ b/packages/compiler-cli/test/compliance/test_helpers/i18n_checks.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ const EXTRACT_GENERATED_TRANSLATIONS_REGEXP = - /const\s*(.*?)\s*=\s*goog\.getMsg\("(.*?)",?\s*(.*?)\)/g; + /const\s*(.*?)\s*=\s*goog\.getMsg\("(.*?)",?\s*(.*?)\)/g; /** * Verify that placeholders in translation strings match placeholders in the object defined in the @@ -30,27 +30,29 @@ export function verifyPlaceholdersIntegrity(output: string): boolean { */ export function verifyUniqueConsts(output: string): boolean { extract( - output, EXTRACT_GENERATED_TRANSLATIONS_REGEXP, - (current: string[], state: Set): string => { - const key = current[1]; - if (state.has(key)) { - throw new Error(`Duplicate const ${key} found in generated output!`); - } - return key; - }); + output, + EXTRACT_GENERATED_TRANSLATIONS_REGEXP, + (current: string[], state: Set): string => { + const key = current[1]; + if (state.has(key)) { + throw new Error(`Duplicate const ${key} found in generated output!`); + } + return key; + }, + ); return true; } - /** * Extract pairs of `[msg, placeholders]`, in calls to `goog.getMsg()`, from the `source`. * * @param source The source code to parse. */ function extractTranslations(source: string): Set { - return extract( - source, EXTRACT_GENERATED_TRANSLATIONS_REGEXP, - ([, , msg, placeholders]) => [msg, placeholders]); + return extract(source, EXTRACT_GENERATED_TRANSLATIONS_REGEXP, ([, , msg, placeholders]) => [ + msg, + placeholders, + ]); } /** @@ -75,9 +77,12 @@ function extractPlaceholdersFromArgs(args: string): Set { } function extract( - from: string, regex: RegExp, transformFn: (match: string[], state: Set) => T): Set { + from: string, + regex: RegExp, + transformFn: (match: string[], state: Set) => T, +): Set { const result = new Set(); - let item: RegExpExecArray|null; + let item: RegExpExecArray | null; while ((item = regex.exec(from)) !== null) { result.add(transformFn(item, result)); } @@ -85,5 +90,5 @@ function extract( } function diff(a: Set, b: Set): Set { - return new Set(Array.from(a).filter(x => !b.has(x))); + return new Set(Array.from(a).filter((x) => !b.has(x))); } diff --git a/packages/compiler-cli/test/compliance/test_helpers/i18n_helpers.ts b/packages/compiler-cli/test/compliance/test_helpers/i18n_helpers.ts index e7de55ed5cdef..6f67290df83f5 100644 --- a/packages/compiler-cli/test/compliance/test_helpers/i18n_helpers.ts +++ b/packages/compiler-cli/test/compliance/test_helpers/i18n_helpers.ts @@ -20,7 +20,11 @@ export function resetMessageIndex(): void { * Generate a string that represents expected i18n block content for a simple message. */ export function i18nMsg( - message: string, placeholders: Placeholder[], options: Options, meta: Meta): string { + message: string, + placeholders: Placeholder[], + options: Options, + meta: Meta, +): string { const varName = `$I18N_${msgIndex++}$`; const closurePlaceholders = i18nPlaceholdersToString(placeholders); const closureOptions = i18nOptionsToString(options); @@ -29,8 +33,7 @@ export function i18nMsg( let ${varName}; if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) { ${i18nMsgClosureMeta(meta)} - const $MSG_EXTERNAL_${msgIndex}$ = goog.getMsg("${message}"${closurePlaceholders}${ - closureOptions}); + const $MSG_EXTERNAL_${msgIndex}$ = goog.getMsg("${message}"${closurePlaceholders}${closureOptions}); ${varName} = $MSG_EXTERNAL_${msgIndex}$; } else { @@ -48,8 +51,12 @@ export interface Options { * post-processing. */ export function i18nMsgWithPostprocess( - message: string, placeholders: Placeholder[], options: Options, meta: Meta, - postprocessPlaceholders: Placeholder[]): string { + message: string, + placeholders: Placeholder[], + options: Options, + meta: Meta, + postprocessPlaceholders: Placeholder[], +): string { const varName = `$I18N_${msgIndex}$`; const ppPlaceholders = i18nPlaceholdersToString(postprocessPlaceholders); return String.raw` @@ -84,7 +91,6 @@ interface Meta { id?: string; } - /** * Convert a set of placeholders to a string (as it's expected from compiler). */ @@ -106,13 +112,12 @@ function i18nOptionsToString({original_code: originals = {}}: Options = {}): str */ function i18nMsgInsertLocalizePlaceholders(message: string, placeholders: Placeholder[]): string { if (placeholders.length > 0) { - message = message.replace(/{\$(.*?)}/g, function(_, name) { + message = message.replace(/{\$(.*?)}/g, function (_, name) { const placeholder = placeholders.find((p) => p[0] === name)!; // e.g. startDivTag -> START_DIV_TAG const key = name.replace(/[A-Z]/g, (ch: string) => '_' + ch).toUpperCase(); const associated = placeholder.length === 3 ? `@@${placeholder[2]}` : ''; - return '$' + - `{${quotedValue(placeholder[1])}}:${key}${associated}:`; + return '$' + `{${quotedValue(placeholder[1])}}:${key}${associated}:`; }); } return message; diff --git a/packages/compiler-cli/test/compliance/test_helpers/sourcemap_helpers.ts b/packages/compiler-cli/test/compliance/test_helpers/sourcemap_helpers.ts index 1b5bed45a92d7..c56d17d244753 100644 --- a/packages/compiler-cli/test/compliance/test_helpers/sourcemap_helpers.ts +++ b/packages/compiler-cli/test/compliance/test_helpers/sourcemap_helpers.ts @@ -33,8 +33,13 @@ import {SourceFileLoader} from '../../../src/ngtsc/sourcemaps'; * @returns The content of the expected source file, stripped of the mapping information. */ export function stripAndCheckMappings( - fs: ReadonlyFileSystem, generated: string, generatedPath: AbsoluteFsPath, - expectedSource: string, expectedPath: AbsoluteFsPath, skipMappingCheck: boolean): string { + fs: ReadonlyFileSystem, + generated: string, + generatedPath: AbsoluteFsPath, + expectedSource: string, + expectedPath: AbsoluteFsPath, + skipMappingCheck: boolean, +): string { // Generate the candidate source maps. const actualMappings = getMappedSegments(fs, generatedPath, generated); @@ -55,9 +60,10 @@ export function stripAndCheckMappings( if (failures.length > 0) { throw new Error( - `When checking mappings for ${generatedPath} against ${expectedPath} expected...\n\n` + + `When checking mappings for ${generatedPath} against ${expectedPath} expected...\n\n` + `${failures.join('\n\n')}\n\n` + - `All the mappings:\n\n${dumpMappings(actualMappings)}`); + `All the mappings:\n\n${dumpMappings(actualMappings)}`, + ); } return expected; @@ -83,31 +89,34 @@ interface SegmentMapping { * @param expected The content of the expected file containing source-map information. */ function extractMappings( - fs: ReadonlyFileSystem, expected: string): {expected: string, mappings: SegmentMapping[]} { + fs: ReadonlyFileSystem, + expected: string, +): {expected: string; mappings: SegmentMapping[]} { const mappings: SegmentMapping[] = []; // capture and remove source mapping info // Any newline at the end of an expectation line is removed, as a mapping expectation for a // segment within a template literal would otherwise force a newline to be matched in the template // literal. expected = expected.replace( - /^(.*?) \/\/ SOURCE: "([^"]*?)" "(.*?)"(?:\n|$)/gm, - (_, rawGenerated: string, rawSourceUrl: string, rawSource: string) => { - // Since segments need to appear on a single line in the expected file, any newlines in the - // segment being checked must be escaped in the expected file and then unescaped here before - // being checked. - const generated = unescape(rawGenerated); - const source = unescape(rawSource); - const sourceUrl = fs.resolve(rawSourceUrl); - - mappings.push({generated, sourceUrl, source}); - return generated; - }); + /^(.*?) \/\/ SOURCE: "([^"]*?)" "(.*?)"(?:\n|$)/gm, + (_, rawGenerated: string, rawSourceUrl: string, rawSource: string) => { + // Since segments need to appear on a single line in the expected file, any newlines in the + // segment being checked must be escaped in the expected file and then unescaped here before + // being checked. + const generated = unescape(rawGenerated); + const source = unescape(rawSource); + const sourceUrl = fs.resolve(rawSourceUrl); + + mappings.push({generated, sourceUrl, source}); + return generated; + }, + ); return {expected, mappings}; } function unescape(str: string): string { - const replacements: Record = {'\\n': '\n', '\\r': '\r', '\\\\': '\\', '\\"': '\"'}; - return str.replace(/\\[rn"\\]/g, match => replacements[match]); + const replacements: Record = {'\\n': '\n', '\\r': '\r', '\\\\': '\\', '\\"': '"'}; + return str.replace(/\\[rn"\\]/g, (match) => replacements[match]); } /** @@ -122,8 +131,10 @@ function unescape(str: string): string { * empty array is returned if there is no source-map file found. */ function getMappedSegments( - fs: ReadonlyFileSystem, generatedPath: AbsoluteFsPath, - generatedContents: string): SegmentMapping[] { + fs: ReadonlyFileSystem, + generatedPath: AbsoluteFsPath, + generatedContents: string, +): SegmentMapping[] { const logger = new ConsoleLogger(LogLevel.debug); const loader = new SourceFileLoader(fs, logger, {}); const generatedFile = loader.loadSourceFile(generatedPath, generatedContents); @@ -140,8 +151,11 @@ function getMappedSegments( const originalStart = mapping.originalSegment; let originalEnd = originalStart.next; // Skip until we find an end segment that is after the start segment - while (originalEnd !== undefined && originalEnd.next !== originalEnd && - originalEnd.position === originalStart.position) { + while ( + originalEnd !== undefined && + originalEnd.next !== originalEnd && + originalEnd.position === originalStart.position + ) { originalEnd = originalEnd.next; } if (originalEnd === undefined || originalEnd.next === originalEnd) { @@ -151,7 +165,7 @@ function getMappedSegments( const segment = { generated: generatedFile.contents.substring(generatedStart.position, generatedEnd.position), source: originalFile.contents.substring(originalStart.position, originalEnd!.position), - sourceUrl: originalFile.sourcePath + sourceUrl: originalFile.sourcePath, }; segments.push(segment); } @@ -164,14 +178,19 @@ function getMappedSegments( * * @returns An error message if a matching segment cannot be found, or null if it can. */ -function checkMapping(mappings: SegmentMapping[], expected: SegmentMapping): string|null { - if (mappings.some( - m => m.generated === expected.generated && m.source === expected.source && - m.sourceUrl === expected.sourceUrl)) { +function checkMapping(mappings: SegmentMapping[], expected: SegmentMapping): string | null { + if ( + mappings.some( + (m) => + m.generated === expected.generated && + m.source === expected.source && + m.sourceUrl === expected.sourceUrl, + ) + ) { return null; } - const matchingGenerated = mappings.filter(m => m.generated === expected.generated); - const matchingSource = mappings.filter(m => m.source === expected.source); + const matchingGenerated = mappings.filter((m) => m.generated === expected.generated); + const matchingSource = mappings.filter((m) => m.source === expected.source); const message = [ 'Expected mappings to contain the following mapping', @@ -180,12 +199,12 @@ function checkMapping(mappings: SegmentMapping[], expected: SegmentMapping): str if (matchingGenerated.length > 0) { message.push(''); message.push('There are the following mappings that match the generated text:'); - matchingGenerated.forEach(m => message.push(prettyPrintMapping(m))); + matchingGenerated.forEach((m) => message.push(prettyPrintMapping(m))); } if (matchingSource.length > 0) { message.push(''); message.push('There are the following mappings that match the source text:'); - matchingSource.forEach(m => message.push(prettyPrintMapping(m))); + matchingSource.forEach((m) => message.push(prettyPrintMapping(m))); } return message.join('\n'); @@ -207,16 +226,19 @@ function prettyPrintMapping(mapping: SegmentMapping): string { */ function dumpMappings(mappings: SegmentMapping[]): string { return mappings - .map( - mapping => padValue(mapping.sourceUrl, 20, 0) + ' : ' + - padValue(JSON.stringify(mapping.source), 100, 23) + ' : ' + - JSON.stringify(mapping.generated)) - .join('\n'); + .map( + (mapping) => + padValue(mapping.sourceUrl, 20, 0) + + ' : ' + + padValue(JSON.stringify(mapping.source), 100, 23) + + ' : ' + + JSON.stringify(mapping.generated), + ) + .join('\n'); } function padValue(value: string, max: number, start: number): string { - const padding = value.length > max ? ('\n' + - ' '.repeat(max + start)) : - ' '.repeat(max - value.length); + const padding = + value.length > max ? '\n' + ' '.repeat(max + start) : ' '.repeat(max - value.length); return value + padding; } diff --git a/packages/compiler-cli/test/compliance/test_helpers/test_runner.ts b/packages/compiler-cli/test/compliance/test_helpers/test_runner.ts index 64cea8d2b7a8f..c1b3c99544149 100644 --- a/packages/compiler-cli/test/compliance/test_helpers/test_runner.ts +++ b/packages/compiler-cli/test/compliance/test_helpers/test_runner.ts @@ -10,20 +10,24 @@ import {FileSystem} from '../../../src/ngtsc/file_system'; import {checkErrors, checkNoUnexpectedErrors} from './check_errors'; import {checkExpectations} from './check_expectations'; import {CompileResult, initMockTestFileSystem} from './compile_test'; -import {CompilationMode, ComplianceTest, Expectation, getAllComplianceTests} from './get_compliance_tests'; +import { + CompilationMode, + ComplianceTest, + Expectation, + getAllComplianceTests, +} from './get_compliance_tests'; function transformExpectation(expectation: Expectation, isLocalCompilation: boolean): void { - expectation.files = expectation.files.map(pair => ({ - expected: pair.expected, - generated: pair.generated, - })); + expectation.files = expectation.files.map((pair) => ({ + expected: pair.expected, + generated: pair.generated, + })); if (isLocalCompilation) { - expectation.files = - expectation.files.map(pair => ({ - expected: getFilenameForLocalCompilation(pair.expected), - generated: pair.generated, - })); + expectation.files = expectation.files.map((pair) => ({ + expected: getFilenameForLocalCompilation(pair.expected), + generated: pair.generated, + })); } } @@ -41,8 +45,10 @@ function getFilenameForLocalCompilation(fileName: string): string { * indicates whether we are testing in local compilation mode. */ export function runTests( - type: CompilationMode, compileFn: (fs: FileSystem, test: ComplianceTest) => CompileResult, - options: {isLocalCompilation?: boolean, skipMappingChecks?: boolean} = {}) { + type: CompilationMode, + compileFn: (fs: FileSystem, test: ComplianceTest) => CompileResult, + options: {isLocalCompilation?: boolean; skipMappingChecks?: boolean} = {}, +) { describe(`compliance tests (${type})`, () => { for (const test of getAllComplianceTests()) { if (!test.compilationModeFilter.includes(type)) { @@ -57,9 +63,9 @@ export function runTests( itFn(test.description, () => { if (type === 'linked compile' && test.compilerOptions?.['target'] === 'ES5') { throw new Error( - `The "${type}" scenario does not support ES5 output.\n` + - `Did you mean to set \`"compilationModeFilter": ["full compile"]\` in "${ - test.relativePath}"?`); + `The "${type}" scenario does not support ES5 output.\n` + + `Did you mean to set \`"compilationModeFilter": ["full compile"]\` in "${test.relativePath}"?`, + ); } const fs = initMockTestFileSystem(test.realTestPath); @@ -68,13 +74,21 @@ export function runTests( transformExpectation(expectation, !!options.isLocalCompilation); if (expectation.expectedErrors.length > 0) { checkErrors( - test.relativePath, expectation.failureMessage, expectation.expectedErrors, - errors); + test.relativePath, + expectation.failureMessage, + expectation.expectedErrors, + errors, + ); } else { checkNoUnexpectedErrors(test.relativePath, errors); checkExpectations( - fs, test.relativePath, expectation.failureMessage, expectation.files, - expectation.extraChecks, options.skipMappingChecks); + fs, + test.relativePath, + expectation.failureMessage, + expectation.files, + expectation.extraChecks, + options.skipMappingChecks, + ); } } }); diff --git a/packages/compiler-cli/test/compliance/update_all_goldens.js b/packages/compiler-cli/test/compliance/update_all_goldens.js index 77bc1c6ea964a..b0be7c4fb25cf 100644 --- a/packages/compiler-cli/test/compliance/update_all_goldens.js +++ b/packages/compiler-cli/test/compliance/update_all_goldens.js @@ -13,10 +13,11 @@ import shelljs from 'shelljs'; const {exec} = shelljs; process.stdout.write('Gathering all partial golden update targets'); -const queryCommand = - `yarn bazel query --output label 'filter('golden.update', kind(nodejs_binary, //packages/compiler-cli/test/compliance/test_cases:*))'`; -const allUpdateTargets = - exec(queryCommand, {silent: true}).trim().split('\n').map(test => test.trim()); +const queryCommand = `yarn bazel query --output label 'filter('golden.update', kind(nodejs_binary, //packages/compiler-cli/test/compliance/test_cases:*))'`; +const allUpdateTargets = exec(queryCommand, {silent: true}) + .trim() + .split('\n') + .map((test) => test.trim()); process.stdout.clearLine(); process.stdout.cursorTo(0); diff --git a/packages/compiler-cli/test/downlevel_decorators_transform_spec.ts b/packages/compiler-cli/test/downlevel_decorators_transform_spec.ts index dcd22d39a41f6..582d09986cafa 100644 --- a/packages/compiler-cli/test/downlevel_decorators_transform_spec.ts +++ b/packages/compiler-cli/test/downlevel_decorators_transform_spec.ts @@ -29,28 +29,32 @@ describe('downlevel decorator transform', () => { 'dom_globals.d.ts': ` declare class HTMLElement {}; declare class Document {}; - ` + `, }); host = new MockCompilerHost(context); isClosureEnabled = false; }); function transform( - contents: string, compilerOptions: ts.CompilerOptions = {}, - preTransformers: ts.TransformerFactory[] = []) { + contents: string, + compilerOptions: ts.CompilerOptions = {}, + preTransformers: ts.TransformerFactory[] = [], + ) { context.writeFile(TEST_FILE_INPUT, contents); const program = ts.createProgram( - [TEST_FILE_INPUT, '/dom_globals.d.ts'], { - module: ts.ModuleKind.CommonJS, - importHelpers: true, - lib: ['dom', 'es2015'], - target: ts.ScriptTarget.ES2017, - declaration: true, - experimentalDecorators: true, - emitDecoratorMetadata: false, - ...compilerOptions - }, - host); + [TEST_FILE_INPUT, '/dom_globals.d.ts'], + { + module: ts.ModuleKind.CommonJS, + importHelpers: true, + lib: ['dom', 'es2015'], + target: ts.ScriptTarget.ES2017, + declaration: true, + experimentalDecorators: true, + emitDecoratorMetadata: false, + ...compilerOptions, + }, + host, + ); const testFile = program.getSourceFile(TEST_FILE_INPUT); const typeChecker = program.getTypeChecker(); const reflectionHost = new TypeScriptReflectionHost(typeChecker); @@ -58,26 +62,34 @@ describe('downlevel decorator transform', () => { before: [ ...preTransformers, getDownlevelDecoratorsTransform( - program.getTypeChecker(), reflectionHost, diagnostics, - /* isCore */ false, isClosureEnabled) - ] + program.getTypeChecker(), + reflectionHost, + diagnostics, + /* isCore */ false, + isClosureEnabled, + ), + ], }; - let output: string|null = null; - let dtsOutput: string|null = null; + let output: string | null = null; + let dtsOutput: string | null = null; const emitResult = program.emit( - testFile, ((fileName, outputText) => { - if (fileName === TEST_FILE_OUTPUT) { - output = outputText; - } else if (fileName === TEST_FILE_DTS_OUTPUT) { - dtsOutput = outputText; - } - }), - undefined, undefined, transformers); + testFile, + (fileName, outputText) => { + if (fileName === TEST_FILE_OUTPUT) { + output = outputText; + } else if (fileName === TEST_FILE_DTS_OUTPUT) { + dtsOutput = outputText; + } + }, + undefined, + undefined, + transformers, + ); diagnostics.push(...emitResult.diagnostics); expect(output).not.toBeNull(); return { output: omitLeadingWhitespace(output!), - dtsOutput: dtsOutput ? omitLeadingWhitespace(dtsOutput) : null + dtsOutput: dtsOutput ? omitLeadingWhitespace(dtsOutput) : null, }; } @@ -308,7 +320,7 @@ describe('downlevel decorator transform', () => { it('should capture constructor type metadata with `emitDecoratorMetadata` enabled', () => { context.writeFile('/other-file.ts', `export class MyOtherClass {}`); const {output} = transform( - ` + ` import {Directive} from '@angular/core'; import {MyOtherClass} from './other-file'; @@ -317,7 +329,8 @@ describe('downlevel decorator transform', () => { constructor(other: MyOtherClass) {} } `, - {emitDecoratorMetadata: true}); + {emitDecoratorMetadata: true}, + ); expect(diagnostics.length).toBe(0); expect(output).toContain('const other_file_1 = require("./other-file");'); @@ -335,7 +348,7 @@ describe('downlevel decorator transform', () => { it('should capture constructor type metadata with `emitDecoratorMetadata` disabled', () => { context.writeFile('/other-file.ts', `export class MyOtherClass {}`); const {output, dtsOutput} = transform( - ` + ` import {Directive} from '@angular/core'; import {MyOtherClass} from './other-file'; @@ -344,7 +357,8 @@ describe('downlevel decorator transform', () => { constructor(other: MyOtherClass) {} } `, - {emitDecoratorMetadata: false}); + {emitDecoratorMetadata: false}, + ); expect(diagnostics.length).toBe(0); expect(output).toContain('const other_file_1 = require("./other-file");'); @@ -484,15 +498,19 @@ describe('downlevel decorator transform', () => { `); }); - it('should not retain unused type imports due to decorator downleveling with ' + - '`emitDecoratorMetadata` enabled.', - () => { - context.writeFile('/external.ts', ` + it( + 'should not retain unused type imports due to decorator downleveling with ' + + '`emitDecoratorMetadata` enabled.', + () => { + context.writeFile( + '/external.ts', + ` export class ErrorHandler {} export class ClassInject {} - `); - const {output} = transform( - ` + `, + ); + const {output} = transform( + ` import {Directive, Inject} from '@angular/core'; import {ErrorHandler, ClassInject} from './external'; @@ -501,22 +519,28 @@ describe('downlevel decorator transform', () => { constructor(@Inject(ClassInject) i: ClassInject) {} } `, - {module: ts.ModuleKind.ES2015, emitDecoratorMetadata: true}); - - expect(diagnostics.length).toBe(0); - expect(output).not.toContain('Directive'); - expect(output).not.toContain('ErrorHandler'); - }); - - it('should not retain unused type imports due to decorator downleveling with ' + - '`emitDecoratorMetadata` disabled', - () => { - context.writeFile('/external.ts', ` + {module: ts.ModuleKind.ES2015, emitDecoratorMetadata: true}, + ); + + expect(diagnostics.length).toBe(0); + expect(output).not.toContain('Directive'); + expect(output).not.toContain('ErrorHandler'); + }, + ); + + it( + 'should not retain unused type imports due to decorator downleveling with ' + + '`emitDecoratorMetadata` disabled', + () => { + context.writeFile( + '/external.ts', + ` export class ErrorHandler {} export class ClassInject {} - `); - const {output} = transform( - ` + `, + ); + const {output} = transform( + ` import {Directive, Inject} from '@angular/core'; import {ErrorHandler, ClassInject} from './external'; @@ -525,21 +549,26 @@ describe('downlevel decorator transform', () => { constructor(@Inject(ClassInject) i: ClassInject) {} } `, - {module: ts.ModuleKind.ES2015, emitDecoratorMetadata: false}); + {module: ts.ModuleKind.ES2015, emitDecoratorMetadata: false}, + ); - expect(diagnostics.length).toBe(0); - expect(output).not.toContain('Directive'); - expect(output).not.toContain('ErrorHandler'); - }); + expect(diagnostics.length).toBe(0); + expect(output).not.toContain('Directive'); + expect(output).not.toContain('ErrorHandler'); + }, + ); it('should not generate invalid reference due to conflicting parameter name', () => { - context.writeFile('/external.ts', ` + context.writeFile( + '/external.ts', + ` export class Dep { greet() {} } - `); + `, + ); const {output} = transform( - ` + ` import {Directive} from '@angular/core'; import {Dep} from './external'; @@ -550,7 +579,8 @@ describe('downlevel decorator transform', () => { } } `, - {emitDecoratorMetadata: false}); + {emitDecoratorMetadata: false}, + ); expect(diagnostics.length).toBe(0); expect(output).toContain(`external_1 = require("./external");`); @@ -600,19 +630,23 @@ describe('downlevel decorator transform', () => { `); expect(diagnostics.length).toBe(1); - expect(diagnostics[0].messageText as string) - .toBe(`Cannot process decorators for class element with non-analyzable name.`); + expect(diagnostics[0].messageText as string).toBe( + `Cannot process decorators for class element with non-analyzable name.`, + ); }); it('should not capture constructor parameter types when not resolving to a value', () => { - context.writeFile('/external.ts', ` + context.writeFile( + '/external.ts', + ` export interface IState {} export type IOverlay = {hello: true}&IState; export default interface { hello: false; } export const enum KeyCodes {A, B} - `); + `, + ); const {output} = transform(` import {Directive, Inject} from '@angular/core'; import * as angular from './external'; @@ -644,13 +678,17 @@ describe('downlevel decorator transform', () => { }); it('should allow preceding custom transformers to strip decorators', () => { - const stripAllDecoratorsTransform: ts.TransformerFactory = context => { + const stripAllDecoratorsTransform: ts.TransformerFactory = (context) => { return (sourceFile: ts.SourceFile) => { const visitNode = (node: ts.Node): ts.Node => { if (ts.isClassDeclaration(node)) { return ts.factory.createClassDeclaration( - ts.getModifiers(node), node.name, node.typeParameters, node.heritageClauses, - node.members); + ts.getModifiers(node), + node.name, + node.typeParameters, + node.heritageClauses, + node.members, + ); } return ts.visitEachChild(node, visitNode, context); }; @@ -659,7 +697,7 @@ describe('downlevel decorator transform', () => { }; const {output} = transform( - ` + ` import {Directive} from '@angular/core'; export class MyInjectedClass {} @@ -669,7 +707,9 @@ describe('downlevel decorator transform', () => { constructor(someToken: MyInjectedClass) {} } `, - {}, [stripAllDecoratorsTransform]); + {}, + [stripAllDecoratorsTransform], + ); expect(diagnostics.length).toBe(0); expect(output).not.toContain('MyDir.decorators'); @@ -700,16 +740,18 @@ describe('downlevel decorator transform', () => { `); }); - it('should allow for type-only references to be removed with `emitDecoratorMetadata` from custom decorators', - () => { - context.writeFile('/external-interface.ts', ` + it('should allow for type-only references to be removed with `emitDecoratorMetadata` from custom decorators', () => { + context.writeFile( + '/external-interface.ts', + ` export interface ExternalInterface { id?: string; } - `); + `, + ); - const {output} = transform( - ` + const {output} = transform( + ` import { ExternalInterface } from './external-interface'; export function CustomDecorator() { @@ -720,19 +762,25 @@ describe('downlevel decorator transform', () => { @CustomDecorator() static test(): ExternalInterface { return {}; } } `, - {emitDecoratorMetadata: true}); + {emitDecoratorMetadata: true}, + ); - expect(diagnostics.length).toBe(0); - expect(output).not.toContain('ExternalInterface'); - expect(output).toContain('metadata("design:returntype", Object)'); - }); + expect(diagnostics.length).toBe(0); + expect(output).not.toContain('ExternalInterface'); + expect(output).toContain('metadata("design:returntype", Object)'); + }); describe('transforming multiple files', () => { it('should work correctly for multiple files that import distinct declarations', () => { - context.writeFile('foo_service.d.ts', ` + context.writeFile( + 'foo_service.d.ts', + ` export declare class Foo {}; - `); - context.writeFile('foo.ts', ` + `, + ); + context.writeFile( + 'foo.ts', + ` import {Injectable} from '@angular/core'; import {Foo} from './foo_service'; @@ -740,12 +788,18 @@ describe('downlevel decorator transform', () => { export class MyService { constructor(foo: Foo) {} } - `); + `, + ); - context.writeFile('bar_service.d.ts', ` + context.writeFile( + 'bar_service.d.ts', + ` export declare class Bar {}; - `); - context.writeFile('bar.ts', ` + `, + ); + context.writeFile( + 'bar.ts', + ` import {Injectable} from '@angular/core'; import {Bar} from './bar_service'; @@ -753,7 +807,8 @@ describe('downlevel decorator transform', () => { export class MyService { constructor(bar: Bar) {} } - `); + `, + ); const {program, transformers} = createProgramWithTransform(['/foo.ts', '/bar.ts']); program.emit(undefined, undefined, undefined, undefined, transformers); @@ -767,9 +822,12 @@ describe('downlevel decorator transform', () => { // repeatedly for each source file in the program, causing a stack overflow once a large // number of source files was reached. This test verifies that emit succeeds even when there's // lots of source files. See https://github.com/angular/angular/issues/40276. - context.writeFile('foo.d.ts', ` + context.writeFile( + 'foo.d.ts', + ` export declare class Foo {}; - `); + `, + ); // A somewhat minimal number of source files that used to trigger a stack overflow. const numberOfTestFiles = 6500; @@ -777,7 +835,9 @@ describe('downlevel decorator transform', () => { for (let i = 0; i < numberOfTestFiles; i++) { const file = `/${i}.ts`; files.push(file); - context.writeFile(file, ` + context.writeFile( + file, + ` import {Injectable} from '@angular/core'; import {Foo} from './foo'; @@ -785,44 +845,61 @@ describe('downlevel decorator transform', () => { export class MyService { constructor(foo: Foo) {} } - `); + `, + ); } const {program, transformers} = createProgramWithTransform(files); let written = 0; - program.emit(undefined, (fileName, outputText) => { - written++; - - // The below assertion throws an explicit error instead of using a Jasmine expectation, - // as we want to abort on the first failure, if any. This avoids as many as `numberOfFiles` - // expectation failures, which would bloat the test output. - if (!outputText.includes(`import { Foo } from './foo';`)) { - throw new Error(`Transform failed to preserve the import in ${fileName}:\n${outputText}`); - } - }, undefined, undefined, transformers); + program.emit( + undefined, + (fileName, outputText) => { + written++; + + // The below assertion throws an explicit error instead of using a Jasmine expectation, + // as we want to abort on the first failure, if any. This avoids as many as `numberOfFiles` + // expectation failures, which would bloat the test output. + if (!outputText.includes(`import { Foo } from './foo';`)) { + throw new Error( + `Transform failed to preserve the import in ${fileName}:\n${outputText}`, + ); + } + }, + undefined, + undefined, + transformers, + ); expect(written).toBe(numberOfTestFiles); }); function createProgramWithTransform(files: string[]) { const program = ts.createProgram( - files, { - moduleResolution: ts.ModuleResolutionKind.Node10, - importHelpers: true, - lib: [], - module: ts.ModuleKind.ESNext, - target: ts.ScriptTarget.Latest, - declaration: false, - experimentalDecorators: true, - emitDecoratorMetadata: false, - }, - host); + files, + { + moduleResolution: ts.ModuleResolutionKind.Node10, + importHelpers: true, + lib: [], + module: ts.ModuleKind.ESNext, + target: ts.ScriptTarget.Latest, + declaration: false, + experimentalDecorators: true, + emitDecoratorMetadata: false, + }, + host, + ); const typeChecker = program.getTypeChecker(); const reflectionHost = new TypeScriptReflectionHost(typeChecker); const transformers: ts.CustomTransformers = { - before: [getDownlevelDecoratorsTransform( - program.getTypeChecker(), reflectionHost, diagnostics, - /* isCore */ false, isClosureEnabled)] + before: [ + getDownlevelDecoratorsTransform( + program.getTypeChecker(), + reflectionHost, + diagnostics, + /* isCore */ false, + isClosureEnabled, + ), + ], }; return {program, transformers}; } diff --git a/packages/compiler-cli/test/extract_i18n_spec.ts b/packages/compiler-cli/test/extract_i18n_spec.ts index 4883fe1950f7b..b3eb6a74d586f 100644 --- a/packages/compiler-cli/test/extract_i18n_spec.ts +++ b/packages/compiler-cli/test/extract_i18n_spec.ts @@ -199,7 +199,7 @@ describe('extract_i18n command line', () => { let basePath: string; let outDir: string; let write: (fileName: string, content: string) => void; - let errorSpy: jasmine.Spy&((s: string) => void); + let errorSpy: jasmine.Spy & ((s: string) => void); function writeConfig(tsconfig = '{"extends": "./tsconfig-base.json"}') { write('tsconfig.json', tsconfig); @@ -213,7 +213,9 @@ describe('extract_i18n command line', () => { }; basePath = support.basePath; outDir = path.join(basePath, 'built'); - write('tsconfig-base.json', `{ + write( + 'tsconfig-base.json', + `{ "compilerOptions": { "experimentalDecorators": true, "skipLibCheck": true, @@ -229,7 +231,8 @@ describe('extract_i18n command line', () => { "lib": ["es2015", "dom"], "typeRoots": ["node_modules/@types"] } - }`); + }`, + ); }); function writeSources() { @@ -237,19 +240,27 @@ describe('extract_i18n command line', () => { Welcome `; - write('src/basic.html', `
-

${welcomeMessage}

`); - - write('src/comp1.ts', ` + write( + 'src/basic.html', + `
+

${welcomeMessage}

`, + ); + + write( + 'src/comp1.ts', + ` import {Component} from '@angular/core'; @Component({ selector: 'basic', templateUrl: './basic.html', }) - export class BasicCmp1 {}`); + export class BasicCmp1 {}`, + ); - write('src/comp2.ts', ` + write( + 'src/comp2.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -262,47 +273,65 @@ describe('extract_i18n command line', () => { selector: 'basic4', template: \`

${welcomeMessage}

\`, }) - export class BasicCmp4 {}`); + export class BasicCmp4 {}`, + ); - write('src/comp3.ts', ` + write( + 'src/comp3.ts', + ` import {Component} from '@angular/core'; @Component({ selector: 'basic3', templateUrl: './basic.html', }) - export class BasicCmp3 {}`); + export class BasicCmp3 {}`, + ); - write('src/placeholders.html', `
Name: {{ + write( + 'src/placeholders.html', + `
Name: {{ name // i18n(ph="name") - }}
`); + }}
`, + ); - write('src/placeholder_cmp.ts', ` + write( + 'src/placeholder_cmp.ts', + ` import {Component} from '@angular/core'; @Component({ selector: 'placeholders', templateUrl: './placeholders.html', }) - export class PlaceholderCmp { name = 'whatever'; }`); + export class PlaceholderCmp { name = 'whatever'; }`, + ); - write('src/icu.html', `
{ + write( + 'src/icu.html', + `
{ count, plural, =1 {book} other {books} }
foo { count, plural, =1 {book} other {books} } -
`); +
`, + ); - write('src/icu_cmp.ts', ` + write( + 'src/icu_cmp.ts', + ` import {Component} from '@angular/core'; @Component({ selector: 'icu', templateUrl: './icu.html', }) - export class IcuCmp { count = 3; }`); + export class IcuCmp { count = 3; }`, + ); - write('src/module.ts', ` + write( + 'src/module.ts', + ` import {NgModule} from '@angular/core'; import {CommonModule} from '@angular/common'; import {BasicCmp1} from './comp1'; @@ -323,15 +352,18 @@ describe('extract_i18n command line', () => { imports: [CommonModule], }) export class I18nModule {} - `); + `, + ); } it('should extract xmb', () => { writeConfig(); writeSources(); - const exitCode = - mainXi18n(['-p', basePath, '--i18nFormat=xmb', '--outFile=custom_file.xmb'], errorSpy); + const exitCode = mainXi18n( + ['-p', basePath, '--i18nFormat=xmb', '--outFile=custom_file.xmb'], + errorSpy, + ); expect(errorSpy).not.toHaveBeenCalled(); expect(exitCode).toBe(0); @@ -359,8 +391,10 @@ describe('extract_i18n command line', () => { writeConfig(); writeSources(); - const exitCode = - mainXi18n(['-p', basePath, '--i18nFormat=xlf2', '--outFile=messages.xliff2.xlf'], errorSpy); + const exitCode = mainXi18n( + ['-p', basePath, '--i18nFormat=xlf2', '--outFile=messages.xliff2.xlf'], + errorSpy, + ); expect(errorSpy).not.toHaveBeenCalled(); expect(exitCode).toBe(0); @@ -374,8 +408,10 @@ describe('extract_i18n command line', () => { writeConfig(); writeSources(); - const exitCode = - mainXi18n(['-p', basePath, '--i18nFormat=xlf2', '--outFile=messages.xliff2.xlf'], errorSpy); + const exitCode = mainXi18n( + ['-p', basePath, '--i18nFormat=xlf2', '--outFile=messages.xliff2.xlf'], + errorSpy, + ); expect(errorSpy).not.toHaveBeenCalled(); expect(exitCode).toBe(0); diff --git a/packages/compiler-cli/test/initializer_api_transforms_spec.ts b/packages/compiler-cli/test/initializer_api_transforms_spec.ts index 2d0acb1a00f9a..c74a298143760 100644 --- a/packages/compiler-cli/test/initializer_api_transforms_spec.ts +++ b/packages/compiler-cli/test/initializer_api_transforms_spec.ts @@ -10,7 +10,10 @@ import ts from 'typescript'; import {ImportedSymbolsTracker} from '../src/ngtsc/imports'; import {TypeScriptReflectionHost} from '../src/ngtsc/reflection'; -import {getDownlevelDecoratorsTransform, getInitializerApiJitTransform} from '../src/transformers/jit_transforms'; +import { + getDownlevelDecoratorsTransform, + getInitializerApiJitTransform, +} from '../src/transformers/jit_transforms'; import {MockAotContext, MockCompilerHost} from './mocks'; @@ -37,42 +40,52 @@ describe('initializer API metadata transform', () => { context.writeFile(TEST_FILE_INPUT, contents); const program = ts.createProgram( - [TEST_FILE_INPUT], { - module: ts.ModuleKind.ESNext, - lib: ['dom', 'es2022'], - target: ts.ScriptTarget.ES2022, - traceResolution: true, - experimentalDecorators: true, - paths: { - '@angular/core': ['./core.d.ts'], - }, + [TEST_FILE_INPUT], + { + module: ts.ModuleKind.ESNext, + lib: ['dom', 'es2022'], + target: ts.ScriptTarget.ES2022, + traceResolution: true, + experimentalDecorators: true, + paths: { + '@angular/core': ['./core.d.ts'], }, - host); + }, + host, + ); const testFile = program.getSourceFile(TEST_FILE_INPUT); const typeChecker = program.getTypeChecker(); const reflectionHost = new TypeScriptReflectionHost(typeChecker); const importTracker = new ImportedSymbolsTracker(); const transformers: ts.CustomTransformers = { - before: [ - getInitializerApiJitTransform(reflectionHost, importTracker, /* isCore */ false), - ] + before: [getInitializerApiJitTransform(reflectionHost, importTracker, /* isCore */ false)], }; if (postDownlevelDecoratorsTransform) { - transformers.before!.push(getDownlevelDecoratorsTransform( - typeChecker, reflectionHost, [], /* isCore */ false, - /* isClosureCompilerEnabled */ false)); + transformers.before!.push( + getDownlevelDecoratorsTransform( + typeChecker, + reflectionHost, + [], + /* isCore */ false, + /* isClosureCompilerEnabled */ false, + ), + ); } - let output: string|null = null; + let output: string | null = null; const emitResult = program.emit( - testFile, ((fileName, outputText) => { - if (fileName === TEST_FILE_OUTPUT) { - output = outputText; - } - }), - undefined, undefined, transformers); + testFile, + (fileName, outputText) => { + if (fileName === TEST_FILE_OUTPUT) { + output = outputText; + } + }, + undefined, + undefined, + transformers, + ); expect(emitResult.diagnostics.length).toBe(0); expect(output).not.toBeNull(); @@ -91,11 +104,13 @@ describe('initializer API metadata transform', () => { } `); - expect(result).toContain(omitLeadingWhitespace(` + expect(result).toContain( + omitLeadingWhitespace(` __decorate([ i0.Input({ isSignal: true, alias: "someInput", required: false, transform: undefined }) ], MyDir.prototype, "someInput", void 0); - `)); + `), + ); }); it('should add `@Input` decorator for a required signal input', () => { @@ -108,11 +123,13 @@ describe('initializer API metadata transform', () => { } `); - expect(result).toContain(omitLeadingWhitespace(` + expect(result).toContain( + omitLeadingWhitespace(` __decorate([ i0.Input({ isSignal: true, alias: "someInput", required: true, transform: undefined }) ], MyDir.prototype, "someInput", void 0); - `)); + `), + ); }); it('should add `@Input` decorator for signal inputs with alias options', () => { @@ -126,14 +143,16 @@ describe('initializer API metadata transform', () => { } `); - expect(result).toContain(omitLeadingWhitespace(` + expect(result).toContain( + omitLeadingWhitespace(` __decorate([ i0.Input({ isSignal: true, alias: "public1", required: false, transform: undefined }) ], MyDir.prototype, "someInput", void 0); __decorate([ i0.Input({ isSignal: true, alias: "public2", required: true, transform: undefined }) ], MyDir.prototype, "someInput2", void 0); - `)); + `), + ); }); it('should add `@Input` decorator for signal inputs with transforms', () => { @@ -149,14 +168,16 @@ describe('initializer API metadata transform', () => { // Transform functions are never captured because the input signal already captures // them and will run these independently of whether a `transform` is specified here. - expect(result).toContain(omitLeadingWhitespace(` + expect(result).toContain( + omitLeadingWhitespace(` __decorate([ i0.Input({ isSignal: true, alias: "someInput", required: false, transform: undefined }) ], MyDir.prototype, "someInput", void 0); __decorate([ i0.Input({ isSignal: true, alias: "someInput2", required: true, transform: undefined }) ], MyDir.prototype, "someInput2", void 0); - `)); + `), + ); }); it('should not transform `@Input` decorator for non-signal inputs', () => { @@ -170,14 +191,16 @@ describe('initializer API metadata transform', () => { } `); - expect(result).toContain(omitLeadingWhitespace(` + expect(result).toContain( + omitLeadingWhitespace(` __decorate([ i0.Input({ isSignal: true, alias: "someInput", required: true, transform: undefined }) ], MyDir.prototype, "someInput", void 0); __decorate([ Input({ someOptionIndicatingThatNothingChanged: true }) ], MyDir.prototype, "nonSignalInput", void 0); - `)); + `), + ); }); it('should not transform signal inputs with an existing `@Input` decorator', () => { @@ -194,11 +217,13 @@ describe('initializer API metadata transform', () => { } `); - expect(result).toContain(omitLeadingWhitespace(` + expect(result).toContain( + omitLeadingWhitespace(` __decorate([ Input() ], MyDir.prototype, "someInput", void 0); - `)); + `), + ); }); it('should preserve existing decorators applied on signal inputs fields', () => { @@ -213,17 +238,19 @@ describe('initializer API metadata transform', () => { } `); - expect(result).toContain(omitLeadingWhitespace(` + expect(result).toContain( + omitLeadingWhitespace(` __decorate([ i0.Input({ isSignal: true, alias: "someInput", required: true, transform: undefined }), MyCustomDecorator() ], MyDir.prototype, "someInput", void 0); - `)); + `), + ); }); it('should work with decorator downleveling post-transform', () => { const result = transform( - ` + ` import {input, Directive} from '@angular/core'; @Directive({}) @@ -231,13 +258,16 @@ describe('initializer API metadata transform', () => { someInput = input(0); } `, - /* postDownlevelDecoratorsTransform */ true); + /* postDownlevelDecoratorsTransform */ true, + ); - expect(result).toContain(omitLeadingWhitespace(` + expect(result).toContain( + omitLeadingWhitespace(` static propDecorators = { someInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "someInput", required: false, transform: undefined },] }] }; - `)); + `), + ); }); }); @@ -252,12 +282,14 @@ describe('initializer API metadata transform', () => { } `); - expect(result).toContain(omitLeadingWhitespace(` + expect(result).toContain( + omitLeadingWhitespace(` __decorate([ i0.Input({ isSignal: true, alias: "value", required: false }), i0.Output("valueChange") ], MyDir.prototype, "value", void 0); - `)); + `), + ); }); it('should add `@Input` and `@Output` decorators for a required model input', () => { @@ -270,12 +302,14 @@ describe('initializer API metadata transform', () => { } `); - expect(result).toContain(omitLeadingWhitespace(` + expect(result).toContain( + omitLeadingWhitespace(` __decorate([ i0.Input({ isSignal: true, alias: "value", required: true }), i0.Output("valueChange") ], MyDir.prototype, "value", void 0); - `)); + `), + ); }); it('should add `@Input` and `@Output` decorators for an aliased model input', () => { @@ -289,7 +323,8 @@ describe('initializer API metadata transform', () => { } `); - expect(result).toContain(omitLeadingWhitespace(` + expect(result).toContain( + omitLeadingWhitespace(` __decorate([ i0.Input({ isSignal: true, alias: "alias", required: false }), i0.Output("aliasChange") @@ -298,7 +333,8 @@ describe('initializer API metadata transform', () => { i0.Input({ isSignal: true, alias: "alias2", required: true }), i0.Output("alias2Change") ], MyDir.prototype, "value2", void 0); - `)); + `), + ); }); it('should not transform model inputs with an existing `@Input` decorator', () => { @@ -311,11 +347,13 @@ describe('initializer API metadata transform', () => { } `); - expect(result).toContain(omitLeadingWhitespace(` + expect(result).toContain( + omitLeadingWhitespace(` __decorate([ Input() ], MyDir.prototype, "value", void 0); - `)); + `), + ); }); it('should not transform model inputs with an existing `@Output` decorator', () => { @@ -328,11 +366,13 @@ describe('initializer API metadata transform', () => { } `); - expect(result).toContain(omitLeadingWhitespace(` + expect(result).toContain( + omitLeadingWhitespace(` __decorate([ Output() ], MyDir.prototype, "value", void 0); - `)); + `), + ); }); it('should preserve existing decorators applied on model input fields', () => { @@ -347,18 +387,20 @@ describe('initializer API metadata transform', () => { } `); - expect(result).toContain(omitLeadingWhitespace(` + expect(result).toContain( + omitLeadingWhitespace(` __decorate([ i0.Input({ isSignal: true, alias: "value", required: true }), i0.Output("valueChange"), MyCustomDecorator() ], MyDir.prototype, "value", void 0); - `)); + `), + ); }); it('should work with decorator downleveling post-transform', () => { const result = transform( - ` + ` import {model, Directive} from '@angular/core'; @Directive({}) @@ -366,13 +408,16 @@ describe('initializer API metadata transform', () => { someInput = model(0); } `, - /* postDownlevelDecoratorsTransform */ true); + /* postDownlevelDecoratorsTransform */ true, + ); - expect(result).toContain(omitLeadingWhitespace(` + expect(result).toContain( + omitLeadingWhitespace(` static propDecorators = { someInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "someInput", required: false },] }, { type: i0.Output, args: ["someInputChange",] }] }; - `)); + `), + ); }); }); @@ -387,11 +432,13 @@ describe('initializer API metadata transform', () => { } `); - expect(result).toContain(omitLeadingWhitespace(` + expect(result).toContain( + omitLeadingWhitespace(` __decorate([ i0.Output("someInput") ], MyDir.prototype, "someInput", void 0); - `)); + `), + ); }); it('should insert an `@Output` decorator with aliases', () => { @@ -404,11 +451,13 @@ describe('initializer API metadata transform', () => { } `); - expect(result).toContain(omitLeadingWhitespace(` + expect(result).toContain( + omitLeadingWhitespace(` __decorate([ i0.Output("someAlias") ], MyDir.prototype, "someInput", void 0); - `)); + `), + ); }); it('should not change an existing `@Output` decorator', () => { @@ -423,11 +472,13 @@ describe('initializer API metadata transform', () => { } `); - expect(result).toContain(omitLeadingWhitespace(` + expect(result).toContain( + omitLeadingWhitespace(` __decorate([ Output(bla) ], MyDir.prototype, "someInput", void 0); - `)); + `), + ); }); }); }); diff --git a/packages/compiler-cli/test/mocks.ts b/packages/compiler-cli/test/mocks.ts index e5a30fad9c431..f8bb5d452b84b 100644 --- a/packages/compiler-cli/test/mocks.ts +++ b/packages/compiler-cli/test/mocks.ts @@ -8,7 +8,7 @@ import ts from 'typescript'; -export type Entry = string|Directory; +export type Entry = string | Directory; export interface Directory { [name: string]: Entry; @@ -17,7 +17,10 @@ export interface Directory { export class MockAotContext { private files: Entry[]; - constructor(public currentDirectory: string, ...files: Entry[]) { + constructor( + public currentDirectory: string, + ...files: Entry[] + ) { this.files = files; } @@ -58,14 +61,14 @@ export class MockAotContext { this.writeFile(fileName, ''); } - getEntry(fileName: string|string[]): Entry|undefined { + getEntry(fileName: string | string[]): Entry | undefined { let parts = typeof fileName === 'string' ? fileName.split('/') : fileName; if (parts[0]) { parts = this.currentDirectory.split('/').concat(parts); } parts.shift(); parts = normalize(parts); - return first(this.files, files => getEntryFromFiles(parts, files)); + return first(this.files, (files) => getEntryFromFiles(parts, files)); } getDirectories(path: string): string[] { @@ -73,7 +76,7 @@ export class MockAotContext { if (typeof dir !== 'object') { return []; } else { - return Object.keys(dir).filter(key => typeof dir[key] === 'object'); + return Object.keys(dir).filter((key) => typeof dir[key] === 'object'); } } @@ -82,7 +85,7 @@ export class MockAotContext { } } -function first(a: T[], cb: (value: T) => T | undefined): T|undefined { +function first(a: T[], cb: (value: T) => T | undefined): T | undefined { for (const value of a) { const result = cb(value); if (result != null) return result; @@ -139,8 +142,10 @@ export class MockCompilerHost implements ts.CompilerHost { } getSourceFile( - fileName: string, languageVersion: ts.ScriptTarget, - onError?: (message: string) => void): ts.SourceFile { + fileName: string, + languageVersion: ts.ScriptTarget, + onError?: (message: string) => void, + ): ts.SourceFile { const sourceText = this.context.readFile(fileName); if (sourceText != null) { return ts.createSourceFile(fileName, sourceText, languageVersion); @@ -153,10 +158,9 @@ export class MockCompilerHost implements ts.CompilerHost { return ts.getDefaultLibFileName(options); } - writeFile: ts.WriteFileCallback = - (fileName, text) => { - this.context.writeFile(fileName, text); - } + writeFile: ts.WriteFileCallback = (fileName, text) => { + this.context.writeFile(fileName, text); + }; getCurrentDirectory(): string { return this.context.currentDirectory; diff --git a/packages/compiler-cli/test/ngtsc/authoring_diagnostics_spec.ts b/packages/compiler-cli/test/ngtsc/authoring_diagnostics_spec.ts index 9c3454f4ea007..0f7988495f65f 100644 --- a/packages/compiler-cli/test/ngtsc/authoring_diagnostics_spec.ts +++ b/packages/compiler-cli/test/ngtsc/authoring_diagnostics_spec.ts @@ -23,7 +23,9 @@ runInEachFileSystem(() => { }); it('should report when an initializer function is used outside of an initializer', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, input} from '@angular/core'; function myInput() { @@ -34,17 +36,20 @@ runInEachFileSystem(() => { export class TestDir { inp = myInput(); } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toContain( - 'Unsupported call to the input function. This function can only be called in the initializer of a class member'); + expect(diags[0].messageText).toContain( + 'Unsupported call to the input function. This function can only be called in the initializer of a class member', + ); }); it('should report when a required initializer function is used outside of an initializer', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, input} from '@angular/core'; function myInput() { @@ -55,17 +60,20 @@ runInEachFileSystem(() => { export class TestDir { inp = myInput(); } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toContain( - 'Unsupported call to the input.required function. This function can only be called in the initializer of a class member'); + expect(diags[0].messageText).toContain( + 'Unsupported call to the input.required function. This function can only be called in the initializer of a class member', + ); }); it('should report when an aliased initializer function is used outside of an initializer', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, input as notInput} from '@angular/core'; function myInput() { @@ -76,18 +84,20 @@ runInEachFileSystem(() => { export class TestDir { inp = myInput(); } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toContain( - 'Unsupported call to the input function. This function can only be called in the initializer of a class member'); + expect(diags[0].messageText).toContain( + 'Unsupported call to the input function. This function can only be called in the initializer of a class member', + ); }); - it('should report when an initializer function accessed through a namespace import is used outside of an initializer', - () => { - env.write('test.ts', ` + it('should report when an initializer function accessed through a namespace import is used outside of an initializer', () => { + env.write( + 'test.ts', + ` import * as ng from '@angular/core'; function myInput() { @@ -98,35 +108,39 @@ runInEachFileSystem(() => { export class TestDir { inp = myInput(); } - `); - - const diags = env.driveDiagnostics(); - expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toContain( - 'Unsupported call to the input function. This function can only be called in the initializer of a class member'); - }); - - it('should report when an initializer function is used outside of an initializer in a file that does not have any decorated classes', - () => { - env.write('test.ts', ` + `, + ); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].messageText).toContain( + 'Unsupported call to the input function. This function can only be called in the initializer of a class member', + ); + }); + + it('should report when an initializer function is used outside of an initializer in a file that does not have any decorated classes', () => { + env.write( + 'test.ts', + ` import {input} from '@angular/core'; export function myInput() { return input(); } - `); - - const diags = env.driveDiagnostics(); - expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toContain( - 'Unsupported call to the input function. This function can only be called in the initializer of a class member'); - }); + `, + ); + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].messageText).toContain( + 'Unsupported call to the input function. This function can only be called in the initializer of a class member', + ); + }); it('should report when an initializer function is used in a constructor', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, input} from '@angular/core'; @Component({template: ''}) @@ -137,17 +151,20 @@ runInEachFileSystem(() => { this.inp = input(); } } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toContain( - 'Unsupported call to the input function. This function can only be called in the initializer of a class member'); + expect(diags[0].messageText).toContain( + 'Unsupported call to the input function. This function can only be called in the initializer of a class member', + ); }); it('should report when an initializer function is an indirect descendant of the initializer', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, input} from '@angular/core'; @Component({template: ''}) @@ -156,32 +173,37 @@ runInEachFileSystem(() => { return input(); })(); } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toContain( - 'Unsupported call to the input function. This function can only be called in the initializer of a class member'); + expect(diags[0].messageText).toContain( + 'Unsupported call to the input function. This function can only be called in the initializer of a class member', + ); }); it('should not report a correct usage of an initializer API', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, input} from '@angular/core'; @Component({template: ''}) export class TestDir { inp = input(); } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(0); }); - it('should not report if an initializer function is wrapped in a parenthesized expression', - () => { - env.write('test.ts', ` + it('should not report if an initializer function is wrapped in a parenthesized expression', () => { + env.write( + 'test.ts', + ` import {Component, input} from '@angular/core'; @Component({template: ''}) @@ -189,14 +211,17 @@ runInEachFileSystem(() => { inp = (input()); inp2 = (((((((((input()))))))))); } - `); + `, + ); - const diags = env.driveDiagnostics(); - expect(diags.length).toBe(0); - }); + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(0); + }); it('should not report if an initializer function is wrapped in an `as` expression', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, input} from '@angular/core'; @Component({template: ''}) @@ -204,43 +229,50 @@ runInEachFileSystem(() => { inp = input() as any; inp2 = input() as unknown as any as {}; } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(0); }); it('should report initializer function being used in an undecorated class', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {input} from '@angular/core'; export class Test { inp = input(); } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toContain( - 'Unsupported call to the input function. This function can only be used as the initializer of a property on a @Component or @Directive class.'); + expect(diags[0].messageText).toContain( + 'Unsupported call to the input function. This function can only be used as the initializer of a property on a @Component or @Directive class.', + ); }); it('should report initializer function being used in an unsupported Angular class', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {input, Pipe} from '@angular/core'; @Pipe({name: 'test'}) export class Test { inp = input(); } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toContain( - 'Unsupported call to the input function. This function can only be used as the initializer of a property on a @Component or @Directive class.'); + expect(diags[0].messageText).toContain( + 'Unsupported call to the input function. This function can only be used as the initializer of a property on a @Component or @Directive class.', + ); }); }); }); diff --git a/packages/compiler-cli/test/ngtsc/authoring_inputs_spec.ts b/packages/compiler-cli/test/ngtsc/authoring_inputs_spec.ts index 6a675f995b61d..90945da11a253 100644 --- a/packages/compiler-cli/test/ngtsc/authoring_inputs_spec.ts +++ b/packages/compiler-cli/test/ngtsc/authoring_inputs_spec.ts @@ -25,36 +25,46 @@ runInEachFileSystem(() => { }); it('should handle a basic, primitive valued input', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, input} from '@angular/core'; @Directive() export class TestDir { data = input('test'); } - `); + `, + ); env.driveMain(); const js = env.getContents('test.js'); expect(js).toContain('inputs: { data: [1, "data"] }'); }); it('should fail if @Input is applied on signal input member', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Input, input} from '@angular/core'; @Directive() export class TestDir { @Input() data = input('test'); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); - expect(diagnostics).toEqual([jasmine.objectContaining({ - messageText: `Using @Input with a signal input is not allowed.`, - })]); + expect(diagnostics).toEqual([ + jasmine.objectContaining({ + messageText: `Using @Input with a signal input is not allowed.`, + }), + ]); }); it('should fail if signal input is also declared in `inputs` decorator field.', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, input} from '@angular/core'; @Directive({ @@ -63,7 +73,8 @@ runInEachFileSystem(() => { export class TestDir { data = input('test'); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics).toEqual([ jasmine.objectContaining({ @@ -73,7 +84,9 @@ runInEachFileSystem(() => { }); it('should fail if signal input declares a non-statically analyzable alias', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, input} from '@angular/core'; const ALIAS = 'bla'; @@ -84,7 +97,8 @@ runInEachFileSystem(() => { export class TestDir { data = input('test', {alias: ALIAS}); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics).toEqual([ jasmine.objectContaining({ @@ -94,7 +108,9 @@ runInEachFileSystem(() => { }); it('should fail if signal input declares a non-statically analyzable options', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, input} from '@angular/core'; const OPTIONS = {}; @@ -105,7 +121,8 @@ runInEachFileSystem(() => { export class TestDir { data = input('test', OPTIONS); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics).toEqual([ jasmine.objectContaining({ @@ -115,14 +132,17 @@ runInEachFileSystem(() => { }); it('should fail if signal input is declared on static member', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, input} from '@angular/core'; @Directive() export class TestDir { static data = input('test'); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics).toEqual([ jasmine.objectContaining({ @@ -132,7 +152,9 @@ runInEachFileSystem(() => { }); it('should handle an alias configured, primitive valued input', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, input} from '@angular/core'; @Directive() @@ -141,14 +163,17 @@ runInEachFileSystem(() => { alias: 'publicName', }); } - `); + `, + ); env.driveMain(); const js = env.getContents('test.js'); expect(js).toContain('inputs: { data: [1, "publicName", "data"] }'); }); it('should error if a required input declares an initial value', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, input} from '@angular/core'; @Directive() @@ -157,16 +182,20 @@ runInEachFileSystem(() => { initialValue: 'bla', }); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); - expect(diagnostics[0].messageText).toEqual(jasmine.objectContaining({ - messageText: 'No overload matches this call.', - })); + expect(diagnostics[0].messageText).toEqual( + jasmine.objectContaining({ + messageText: 'No overload matches this call.', + }), + ); }); - it('should handle a transform and required input', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, input} from '@angular/core'; @Directive() @@ -175,32 +204,38 @@ runInEachFileSystem(() => { transform: (v: string|number) => 'works', }); } - `); + `, + ); env.driveMain(); expect(env.getContents('test.js')).toContain(`inputs: { data: [1, "data"] }`); expect(env.getContents('test.d.ts')).toContain('"required": true; "isSignal": true;'); expect(env.getContents('test.d.ts')) - .withContext( - 'Expected no coercion member as input signal captures the write type of the transform') - .not.toContain('ngAcceptInputType'); + .withContext( + 'Expected no coercion member as input signal captures the write type of the transform', + ) + .not.toContain('ngAcceptInputType'); }); - it('should handle a non-primitive initial value', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, input} from '@angular/core'; @Directive() export class TestDir { data = input(/default pattern/); } - `); + `, + ); env.driveMain(); expect(env.getContents('test.js')).toContain(`inputs: { data: [1, "data"] }`); }); it('should report mixed two-way binding with a signal input', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, input, Output, EventEmitter} from '@angular/core'; @Directive({standalone: true, selector: '[dir]'}) @@ -217,7 +252,8 @@ runInEachFileSystem(() => { export class TestComp { value = 1; } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); @@ -226,7 +262,9 @@ runInEachFileSystem(() => { describe('type checking', () => { it('should work', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, input} from '@angular/core'; @Directive({ @@ -244,16 +282,20 @@ runInEachFileSystem(() => { }) export class TestComp { } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(1); - expect(diagnostics[0].messageText) - .toBe(`Type 'boolean' is not assignable to type 'number'.`); + expect(diagnostics[0].messageText).toBe( + `Type 'boolean' is not assignable to type 'number'.`, + ); }); it('should work with transforms', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, input} from '@angular/core'; @Directive({ @@ -273,16 +315,20 @@ runInEachFileSystem(() => { }) export class TestComp { } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(1); - expect(diagnostics[0].messageText) - .toBe(`Type 'boolean' is not assignable to type 'string | number'.`); + expect(diagnostics[0].messageText).toBe( + `Type 'boolean' is not assignable to type 'string | number'.`, + ); }); it('should report unset required inputs', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, input} from '@angular/core'; @Directive({ @@ -300,18 +346,22 @@ runInEachFileSystem(() => { }) export class TestComp { } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(1); - expect(diagnostics[0].messageText) - .toBe(`Required input 'data' from directive TestDir must be specified.`); + expect(diagnostics[0].messageText).toBe( + `Required input 'data' from directive TestDir must be specified.`, + ); }); }); describe('diagnostics', () => { it('should error when declared using an ES private field', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, input} from '@angular/core'; @Directive({ @@ -321,19 +371,24 @@ runInEachFileSystem(() => { export class TestDir { #data = input.required(); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(1); - expect(diagnostics).toEqual([jasmine.objectContaining({ - messageText: jasmine.objectContaining({ - messageText: `Cannot use "input" on a class member that is declared as ES private.`, + expect(diagnostics).toEqual([ + jasmine.objectContaining({ + messageText: jasmine.objectContaining({ + messageText: `Cannot use "input" on a class member that is declared as ES private.`, + }), }), - })]); + ]); }); it('should error when declared using a `private` field', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, input} from '@angular/core'; @Directive({ @@ -343,19 +398,24 @@ runInEachFileSystem(() => { export class TestDir { private data = input.required(); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(1); - expect(diagnostics).toEqual([jasmine.objectContaining({ - messageText: jasmine.objectContaining({ - messageText: `Cannot use "input" on a class member that is declared as private.`, + expect(diagnostics).toEqual([ + jasmine.objectContaining({ + messageText: jasmine.objectContaining({ + messageText: `Cannot use "input" on a class member that is declared as private.`, + }), }), - })]); + ]); }); it('should allow declaring using a `protected` field', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, input} from '@angular/core'; @Directive({ @@ -365,7 +425,8 @@ runInEachFileSystem(() => { export class TestDir { protected data = input.required(); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(0); diff --git a/packages/compiler-cli/test/ngtsc/authoring_models_spec.ts b/packages/compiler-cli/test/ngtsc/authoring_models_spec.ts index 8a0458c7737a9..7c50e6e12f206 100644 --- a/packages/compiler-cli/test/ngtsc/authoring_models_spec.ts +++ b/packages/compiler-cli/test/ngtsc/authoring_models_spec.ts @@ -25,14 +25,17 @@ runInEachFileSystem(() => { }); it('should declare an input/output pair for a field initialized to a model()', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, model} from '@angular/core'; @Directive() export class TestDir { value = model(1); } - `); + `, + ); env.driveMain(); const js = env.getContents('test.js'); @@ -41,20 +44,24 @@ runInEachFileSystem(() => { expect(js).toContain('inputs: { value: [1, "value"] }'); expect(js).toContain('outputs: { value: "valueChange" }'); expect(dts).toContain( - 'static ɵdir: i0.ɵɵDirectiveDeclaration;'); + '{ "value": "valueChange"; }, never, never, false, never>;', + ); }); it('should declare an input/output pair for a field initialized to an aliased model()', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, model} from '@angular/core'; @Directive() export class TestDir { value = model(1, {alias: 'alias'}); } - `); + `, + ); env.driveMain(); const js = env.getContents('test.js'); @@ -63,20 +70,24 @@ runInEachFileSystem(() => { expect(js).toContain('inputs: { value: [1, "alias", "value"] }'); expect(js).toContain('outputs: { value: "aliasChange" }'); expect(dts).toContain( - 'static ɵdir: i0.ɵɵDirectiveDeclaration;'); + '{ "value": "aliasChange"; }, never, never, false, never>;', + ); }); it('should declare an input/output pair for a field initialized to a required model()', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, model} from '@angular/core'; @Directive() export class TestDir { value = model.required(); } - `); + `, + ); env.driveMain(); const js = env.getContents('test.js'); @@ -85,41 +96,50 @@ runInEachFileSystem(() => { expect(js).toContain('inputs: { value: [1, "value"] }'); expect(js).toContain('outputs: { value: "valueChange" }'); expect(dts).toContain( - 'static ɵdir: i0.ɵɵDirectiveDeclaration;'); + '{ "value": "valueChange"; }, never, never, false, never>;', + ); }); it('should report a diagnostic if a model field is decorated with @Input', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Input, model} from '@angular/core'; @Directive() export class TestDir { @Input() value = model('test'); } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(diags[0].messageText).toBe('Using @Input with a model input is not allowed.'); }); it('should report a diagnostic if a model field is decorated with @Output', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Output, model} from '@angular/core'; @Directive() export class TestDir { @Output() value = model('test'); } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(diags[0].messageText).toBe('Using @Output with a model input is not allowed.'); }); it('should report a diagnostic if a model input is also declared in the `inputs` field', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, model} from '@angular/core'; @Directive({ @@ -128,15 +148,19 @@ runInEachFileSystem(() => { export class TestDir { value = model('test'); } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toBe('Input "value" is also declared as non-signal in @Directive.'); + expect(diags[0].messageText).toBe( + 'Input "value" is also declared as non-signal in @Directive.', + ); }); it('should produce a diagnostic if the alias of a model cannot be analyzed', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, model} from '@angular/core'; const ALIAS = 'bla'; @@ -145,16 +169,20 @@ runInEachFileSystem(() => { export class TestDir { value = model('test', {alias: ALIAS}); } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toBe('Alias needs to be a string that is statically analyzable.'); + expect(diags[0].messageText).toBe( + 'Alias needs to be a string that is statically analyzable.', + ); }); it('should report a diagnostic if the options of a model signal cannot be analyzed', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, model} from '@angular/core'; const OPTIONS = {}; @@ -163,30 +191,38 @@ runInEachFileSystem(() => { export class TestDir { value = model('test', OPTIONS); } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toBe('Argument needs to be an object literal that is statically analyzable.'); + expect(diags[0].messageText).toBe( + 'Argument needs to be an object literal that is statically analyzable.', + ); }); it('should report a diagnostic if a model input is declared on a static member', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, model} from '@angular/core'; @Directive() export class TestDir { static value = model('test'); } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toBe('Input "value" is incorrectly declared as static member of "TestDir".'); + expect(diags[0].messageText).toBe( + 'Input "value" is incorrectly declared as static member of "TestDir".', + ); }); it('should a diagnostic if a required model input declares an initial value', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, model} from '@angular/core'; @Directive() @@ -195,17 +231,20 @@ runInEachFileSystem(() => { initialValue: 'bla', }); } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toBe( - `Object literal may only specify known properties, ` + - `and 'initialValue' does not exist in type 'ModelOptions'.`); + expect(diags[0].messageText).toBe( + `Object literal may only specify known properties, ` + + `and 'initialValue' does not exist in type 'ModelOptions'.`, + ); }); it('should report if a signal getter is invoked in a two-way binding', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, model, signal} from '@angular/core'; @Directive({ @@ -224,7 +263,8 @@ runInEachFileSystem(() => { export class TestComp { value = signal(0); } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); @@ -233,7 +273,9 @@ runInEachFileSystem(() => { describe('type checking', () => { it('should check a primitive value bound to a model input', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, model} from '@angular/core'; @Directive({ @@ -252,7 +294,8 @@ runInEachFileSystem(() => { export class TestComp { value = false; } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); @@ -260,7 +303,9 @@ runInEachFileSystem(() => { }); it('should check a signal value bound to a model input via a two-way binding', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, model, signal} from '@angular/core'; @Directive({ @@ -279,7 +324,8 @@ runInEachFileSystem(() => { export class TestComp { value = signal(false); } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); @@ -287,7 +333,9 @@ runInEachFileSystem(() => { }); it('should check two-way binding of a signal to a decorator-based input/output pair', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, Input, Output, signal, EventEmitter} from '@angular/core'; @Directive({ @@ -307,7 +355,8 @@ runInEachFileSystem(() => { export class TestComp { value = signal(false); } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); @@ -315,7 +364,9 @@ runInEachFileSystem(() => { }); it('should not allow a non-writable signal to be assigned to a model', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, model, input} from '@angular/core'; @Directive({ @@ -334,16 +385,20 @@ runInEachFileSystem(() => { export class TestComp { value = input(0); } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toBe(`Type 'InputSignal' is not assignable to type 'number'.`); + expect(diags[0].messageText).toBe( + `Type 'InputSignal' is not assignable to type 'number'.`, + ); }); it('should allow a model signal to be bound to another model signal', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, model} from '@angular/core'; @Directive({ @@ -362,14 +417,17 @@ runInEachFileSystem(() => { export class TestComp { value = model(0); } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(0); }); it('should check the event declared by a model input', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, model} from '@angular/core'; @Directive({ @@ -388,16 +446,20 @@ runInEachFileSystem(() => { export class TestComp { acceptsString(value: string) {} } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toBe(`Argument of type 'number' is not assignable to parameter of type 'string'.`); + expect(diags[0].messageText).toBe( + `Argument of type 'number' is not assignable to parameter of type 'string'.`, + ); }); it('should report unset required model inputs', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, model} from '@angular/core'; @Directive({ @@ -415,16 +477,20 @@ runInEachFileSystem(() => { }) export class TestComp { } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(1); - expect(diagnostics[0].messageText) - .toBe(`Required input 'value' from directive TestDir must be specified.`); + expect(diagnostics[0].messageText).toBe( + `Required input 'value' from directive TestDir must be specified.`, + ); }); it('should check generic two-way model binding with a primitive value', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, model} from '@angular/core'; @Directive({ @@ -443,16 +509,20 @@ runInEachFileSystem(() => { export class TestComp { value = {id: 1}; } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toBe(`Type '{ id: number; }' is not assignable to type '{ id: string; }'.`); + expect(diags[0].messageText).toBe( + `Type '{ id: number; }' is not assignable to type '{ id: string; }'.`, + ); }); it('should check generic two-way model binding with a signal value', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, model, signal} from '@angular/core'; @Directive({ @@ -471,16 +541,20 @@ runInEachFileSystem(() => { export class TestComp { value = signal({id: 1}); } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toEqual(`Type '{ id: number; }' is not assignable to type '{ id: string; }'.`); + expect(diags[0].messageText).toEqual( + `Type '{ id: number; }' is not assignable to type '{ id: string; }'.`, + ); }); it('should report unwrapped signals assigned to a model in a one-way binding', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, model, signal} from '@angular/core'; @Directive({ @@ -499,17 +573,21 @@ runInEachFileSystem(() => { export class TestComp { value = signal(1); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(1); - expect(diagnostics[0].messageText) - .toBe(`Type 'WritableSignal' is not assignable to type 'number'.`); + expect(diagnostics[0].messageText).toBe( + `Type 'WritableSignal' is not assignable to type 'number'.`, + ); }); }); it('should allow two-way binding to a generic model input', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, model, signal} from '@angular/core'; @Directive({ @@ -528,7 +606,8 @@ runInEachFileSystem(() => { export class TestComp { value = signal(1); } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags).toEqual([]); @@ -537,7 +616,9 @@ runInEachFileSystem(() => { // TODO(atscott): fix this test which was broken by #54711 xit('should not widen the type of two-way bindings on Angular versions less than 17.2', () => { env.tsconfig({_angularCoreVersion: '16.50.60', strictTemplates: true}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, Input, Output, EventEmitter, signal} from '@angular/core'; @Directive({ @@ -557,18 +638,22 @@ runInEachFileSystem(() => { export class TestComp { value = signal(1); } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toBe(`Type 'WritableSignal' is not assignable to type 'number'.`); + expect(diags[0].messageText).toBe( + `Type 'WritableSignal' is not assignable to type 'number'.`, + ); }); it('should widen the type of two-way bindings on supported Angular versions', () => { - ['17.2.0', '17.2.0-rc.0', '0.0.0-PLACEHOLDER', '18.0.0'].forEach(version => { + ['17.2.0', '17.2.0-rc.0', '0.0.0-PLACEHOLDER', '18.0.0'].forEach((version) => { env.tsconfig({_angularCoreVersion: version, strictTemplates: true}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, Input, Output, EventEmitter, signal} from '@angular/core'; @Directive({ @@ -588,7 +673,8 @@ runInEachFileSystem(() => { export class TestComp { value = signal(1); } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags).withContext(`On version ${version}`).toEqual([]); @@ -597,7 +683,9 @@ runInEachFileSystem(() => { describe('diagnostics', () => { it('should error when declared using an ES private field', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, model} from '@angular/core'; @Directive({ @@ -607,19 +695,24 @@ runInEachFileSystem(() => { export class TestDir { #data = model.required(); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(1); - expect(diagnostics).toEqual([jasmine.objectContaining({ - messageText: jasmine.objectContaining({ - messageText: `Cannot use "model" on a class member that is declared as ES private.`, + expect(diagnostics).toEqual([ + jasmine.objectContaining({ + messageText: jasmine.objectContaining({ + messageText: `Cannot use "model" on a class member that is declared as ES private.`, + }), }), - })]); + ]); }); it('should error when declared using a `private` field', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, model} from '@angular/core'; @Directive({ @@ -629,19 +722,24 @@ runInEachFileSystem(() => { export class TestDir { private data = model.required(); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(1); - expect(diagnostics).toEqual([jasmine.objectContaining({ - messageText: jasmine.objectContaining({ - messageText: `Cannot use "model" on a class member that is declared as private.`, + expect(diagnostics).toEqual([ + jasmine.objectContaining({ + messageText: jasmine.objectContaining({ + messageText: `Cannot use "model" on a class member that is declared as private.`, + }), }), - })]); + ]); }); it('should allow using a `protected` field', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, model} from '@angular/core'; @Directive({ @@ -651,7 +749,8 @@ runInEachFileSystem(() => { export class TestDir { protected data = model.required(); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(0); diff --git a/packages/compiler-cli/test/ngtsc/authoring_outputs_spec.ts b/packages/compiler-cli/test/ngtsc/authoring_outputs_spec.ts index 60f8aabe92c09..e5841c212d533 100644 --- a/packages/compiler-cli/test/ngtsc/authoring_outputs_spec.ts +++ b/packages/compiler-cli/test/ngtsc/authoring_outputs_spec.ts @@ -25,14 +25,17 @@ runInEachFileSystem(() => { }); it('should handle a basic output()', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, output} from '@angular/core'; @Component({selector: 'test', template: ''}) export class TestDir { click = output(); } - `); + `, + ); env.driveMain(); const js = env.getContents('test.js'); @@ -43,7 +46,9 @@ runInEachFileSystem(() => { }); it('should handle outputFromObservable()', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, EventEmitter} from '@angular/core'; import {outputFromObservable} from '@angular/core/rxjs-interop'; @@ -51,7 +56,8 @@ runInEachFileSystem(() => { export class TestDir { click = outputFromObservable(new EventEmitter()); } - `); + `, + ); env.driveMain(); const js = env.getContents('test.js'); @@ -62,14 +68,17 @@ runInEachFileSystem(() => { }); it('should handle an aliased output()', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, output} from '@angular/core'; @Component({selector: 'test', template: ''}) export class TestDir { click = output({alias: 'publicClick'}); } - `); + `, + ); env.driveMain(); const js = env.getContents('test.js'); @@ -80,7 +89,9 @@ runInEachFileSystem(() => { }); it('should handle an aliased outputFromObservable()', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, EventEmitter} from '@angular/core'; import {outputFromObservable} from '@angular/core/rxjs-interop'; @@ -89,7 +100,8 @@ runInEachFileSystem(() => { source$ = new EventEmitter(); click = outputFromObservable(this.source$, {alias: 'publicClick'}); } - `); + `, + ); env.driveMain(); const js = env.getContents('test.js'); @@ -101,24 +113,30 @@ runInEachFileSystem(() => { describe('diagnostics', () => { it('should fail when output() is used with @Output decorator', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Output, output} from '@angular/core'; @Component({selector: 'test', template: ''}) export class TestDir { @Output() click = output({alias: 'publicClick'}); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics).toEqual([ - jasmine.objectContaining( - {messageText: 'Using "@Output" with "output()" is not allowed.'}), + jasmine.objectContaining({ + messageText: 'Using "@Output" with "output()" is not allowed.', + }), ]); }); it('should fail when outputFromObservable() is used with @Output decorator', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Output, EventEmitter} from '@angular/core'; import {outputFromObservable} from '@angular/core/rxjs-interop'; @@ -126,17 +144,21 @@ runInEachFileSystem(() => { export class TestDir { @Output() click = outputFromObservable(new EventEmitter()); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics).toEqual([ - jasmine.objectContaining( - {messageText: 'Using "@Output" with "output()" is not allowed.'}), + jasmine.objectContaining({ + messageText: 'Using "@Output" with "output()" is not allowed.', + }), ]); }); it('should fail if used with output declared in @Directive metadata', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Output, output} from '@angular/core'; @Directive({ @@ -146,17 +168,21 @@ runInEachFileSystem(() => { export class TestDir { click = output({alias: 'publicClick'}); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics).toEqual([ - jasmine.objectContaining( - {messageText: 'Output "click" is unexpectedly declared in @Directive as well.'}), + jasmine.objectContaining({ + messageText: 'Output "click" is unexpectedly declared in @Directive as well.', + }), ]); }); it('should fail if used with output declared in @Component metadata', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Output, output} from '@angular/core'; @Component({ @@ -167,17 +193,21 @@ runInEachFileSystem(() => { export class TestDir { click = output({alias: 'publicClick'}); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics).toEqual([ - jasmine.objectContaining( - {messageText: 'Output "click" is unexpectedly declared in @Component as well.'}), + jasmine.objectContaining({ + messageText: 'Output "click" is unexpectedly declared in @Component as well.', + }), ]); }); it('should fail if declared on a static member', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, output} from '@angular/core'; @Component({ @@ -187,17 +217,21 @@ runInEachFileSystem(() => { export class TestDir { static click = output({alias: 'publicClick'}); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics).toEqual([ - jasmine.objectContaining( - {messageText: 'Output is incorrectly declared on a static class member.'}), + jasmine.objectContaining({ + messageText: 'Output is incorrectly declared on a static class member.', + }), ]); }); it('should fail if `.required` method is used (even though not supported via types)', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, output} from '@angular/core'; @Component({ @@ -208,7 +242,8 @@ runInEachFileSystem(() => { // @ts-ignore click = output.required({alias: 'publicClick'}); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics).toEqual([ @@ -217,7 +252,9 @@ runInEachFileSystem(() => { }); it('should report an error when using an ES private field', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, output} from '@angular/core'; @Component({ @@ -227,19 +264,24 @@ runInEachFileSystem(() => { export class TestDir { #click = output(); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(1); - expect(diagnostics).toEqual([jasmine.objectContaining({ - messageText: jasmine.objectContaining({ - messageText: `Cannot use "output" on a class member that is declared as ES private.`, + expect(diagnostics).toEqual([ + jasmine.objectContaining({ + messageText: jasmine.objectContaining({ + messageText: `Cannot use "output" on a class member that is declared as ES private.`, + }), }), - })]); + ]); }); it('should report an error when using a `private` field', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, output} from '@angular/core'; @Component({ @@ -249,19 +291,24 @@ runInEachFileSystem(() => { export class TestDir { private click = output(); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(1); - expect(diagnostics).toEqual([jasmine.objectContaining({ - messageText: jasmine.objectContaining({ - messageText: `Cannot use "output" on a class member that is declared as private.`, + expect(diagnostics).toEqual([ + jasmine.objectContaining({ + messageText: jasmine.objectContaining({ + messageText: `Cannot use "output" on a class member that is declared as private.`, + }), }), - })]); + ]); }); it('should allow an output using a `protected` field', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, output} from '@angular/core'; @Component({ @@ -271,7 +318,8 @@ runInEachFileSystem(() => { export class TestDir { protected click = output(); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(0); }); diff --git a/packages/compiler-cli/test/ngtsc/authoring_queries_spec.ts b/packages/compiler-cli/test/ngtsc/authoring_queries_spec.ts index 7e2f54063d92c..7bf42a4e01fdb 100644 --- a/packages/compiler-cli/test/ngtsc/authoring_queries_spec.ts +++ b/packages/compiler-cli/test/ngtsc/authoring_queries_spec.ts @@ -25,14 +25,17 @@ runInEachFileSystem(() => { }); it('should handle a basic viewChild', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, viewChild} from '@angular/core'; @Component({selector: 'test', template: ''}) export class TestDir { el = viewChild('myLocator'); } - `); + `, + ); env.driveMain(); const js = env.getContents('test.js'); @@ -42,7 +45,9 @@ runInEachFileSystem(() => { it('should support viewChild with `read` options', () => { env.write('other-file.ts', `export class X {}`); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, viewChild} from '@angular/core'; import * as fromOtherFile from './other-file'; @@ -53,7 +58,8 @@ runInEachFileSystem(() => { el = viewChild('myLocator', {read: X}); el2 = viewChild('myLocator', {read: fromOtherFile.X}); } - `); + `, + ); env.driveMain(); const js = env.getContents('test.js'); @@ -63,14 +69,17 @@ runInEachFileSystem(() => { }); it('should support viewChild with `read` pointing to an expression with a generic', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, viewChild, ElementRef} from '@angular/core'; @Component({selector: 'test', template: ''}) export class TestDir { el = viewChild('myLocator', {read: ElementRef}); } - `); + `, + ); env.driveMain(); const js = env.getContents('test.js'); @@ -79,14 +88,17 @@ runInEachFileSystem(() => { }); it('should support viewChild with `read` pointing to a parenthesized expression', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, viewChild, ElementRef} from '@angular/core'; @Component({selector: 'test', template: ''}) export class TestDir { el = viewChild('myLocator', {read: ((((ElementRef))))}); } - `); + `, + ); env.driveMain(); const js = env.getContents('test.js'); @@ -95,14 +107,17 @@ runInEachFileSystem(() => { }); it('should support viewChild with `read` pointing to an `as` expression', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, viewChild, ElementRef} from '@angular/core'; @Component({selector: 'test', template: ''}) export class TestDir { el = viewChild('myLocator', {read: ElementRef as any}); } - `); + `, + ); env.driveMain(); const js = env.getContents('test.js'); @@ -111,14 +126,17 @@ runInEachFileSystem(() => { }); it('should handle a basic viewChildren', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, viewChildren} from '@angular/core'; @Component({selector: 'test', template: ''}) export class TestDir { el = viewChildren('myLocator'); } - `); + `, + ); env.driveMain(); const js = env.getContents('test.js'); @@ -127,14 +145,17 @@ runInEachFileSystem(() => { }); it('should handle a basic contentChild', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, contentChild} from '@angular/core'; @Component({selector: 'test', template: ''}) export class TestDir { el = contentChild('myLocator'); } - `); + `, + ); env.driveMain(); const js = env.getContents('test.js'); @@ -143,14 +164,17 @@ runInEachFileSystem(() => { }); it('should handle a basic contentChildren', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, contentChildren} from '@angular/core'; @Component({selector: 'test', template: ''}) export class TestDir { el = contentChildren('myLocator'); } - `); + `, + ); env.driveMain(); const js = env.getContents('test.js'); @@ -160,39 +184,51 @@ runInEachFileSystem(() => { describe('diagnostics', () => { it('should report an error when used with query decorator', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, viewChild, ViewChild} from '@angular/core'; @Component({selector: 'test', template: ''}) export class TestDir { @ViewChild('myLocator') el = viewChild('myLocator'); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(1); - expect(diagnostics).toEqual([jasmine.objectContaining({ - messageText: `Using @ViewChild with a signal-based query is not allowed.`, - })]); + expect(diagnostics).toEqual([ + jasmine.objectContaining({ + messageText: `Using @ViewChild with a signal-based query is not allowed.`, + }), + ]); }); it('should report an error when used on a static field', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, viewChild} from '@angular/core'; @Component({selector: 'test', template: ''}) export class TestDir { static el = viewChild('myLocator'); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(1); - expect(diagnostics).toEqual([jasmine.objectContaining({ - messageText: `Query is incorrectly declared on a static class member.`, - })]); + expect(diagnostics).toEqual([ + jasmine.objectContaining({ + messageText: `Query is incorrectly declared on a static class member.`, + }), + ]); }); it('should report an error when declared in @Directive metadata', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, ViewChild, viewChild} from '@angular/core'; @Directive({ @@ -204,17 +240,21 @@ runInEachFileSystem(() => { export class TestDir { el = viewChild('myLocator'); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(1); - expect(diagnostics).toEqual([jasmine.objectContaining({ - messageText: - `Query is declared multiple times. "@Directive" declares a query for the same property.`, - })]); + expect(diagnostics).toEqual([ + jasmine.objectContaining({ + messageText: `Query is declared multiple times. "@Directive" declares a query for the same property.`, + }), + ]); }); it('should report an error when declared in @Component metadata', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, ViewChild, viewChild} from '@angular/core'; @Component({ @@ -227,17 +267,21 @@ runInEachFileSystem(() => { export class TestComp { el = viewChild('myLocator'); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(1); - expect(diagnostics).toEqual([jasmine.objectContaining({ - messageText: - `Query is declared multiple times. "@Component" declares a query for the same property.`, - })]); + expect(diagnostics).toEqual([ + jasmine.objectContaining({ + messageText: `Query is declared multiple times. "@Component" declares a query for the same property.`, + }), + ]); }); it('should report an error when a signal-based query function is used in metadata', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, viewChild} from '@angular/core'; @Component({ @@ -249,17 +293,22 @@ runInEachFileSystem(() => { }, }) export class TestComp {} - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(1); - expect(diagnostics).toEqual([jasmine.objectContaining({ - messageText: `Decorator query metadata must be an instance of a query type`, - })]); + expect(diagnostics).toEqual([ + jasmine.objectContaining({ + messageText: `Decorator query metadata must be an instance of a query type`, + }), + ]); }); it('should report an error when `read` option is complex', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, viewChild} from '@angular/core'; @Directive({ @@ -269,16 +318,21 @@ runInEachFileSystem(() => { something = null!; el = viewChild('myLocator', {read: this.something}); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(1); - expect(diagnostics).toEqual([jasmine.objectContaining({ - messageText: `Query "read" option expected a literal class reference.`, - })]); + expect(diagnostics).toEqual([ + jasmine.objectContaining({ + messageText: `Query "read" option expected a literal class reference.`, + }), + ]); }); it('should error when a query is declared using an ES private field', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, viewChild} from '@angular/core'; @Directive({ @@ -287,18 +341,23 @@ runInEachFileSystem(() => { export class TestDir { #el = viewChild('myLocator'); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(1); - expect(diagnostics).toEqual([jasmine.objectContaining({ - messageText: jasmine.objectContaining({ - messageText: `Cannot use "viewChild" on a class member that is declared as ES private.`, + expect(diagnostics).toEqual([ + jasmine.objectContaining({ + messageText: jasmine.objectContaining({ + messageText: `Cannot use "viewChild" on a class member that is declared as ES private.`, + }), }), - })]); + ]); }); it('should allow query is declared on a `private` field', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, viewChild} from '@angular/core'; @Directive({ @@ -307,13 +366,16 @@ runInEachFileSystem(() => { export class TestDir { private el = viewChild('myLocator'); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(0); }); it('should allow query is declared on a `protected` field', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, viewChild} from '@angular/core'; @Directive({ @@ -322,7 +384,8 @@ runInEachFileSystem(() => { export class TestDir { protected el = viewChild('myLocator'); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(0); }); diff --git a/packages/compiler-cli/test/ngtsc/component_indexing_spec.ts b/packages/compiler-cli/test/ngtsc/component_indexing_spec.ts index d6734a46e3533..33e3cd35b9660 100644 --- a/packages/compiler-cli/test/ngtsc/component_indexing_spec.ts +++ b/packages/compiler-cli/test/ngtsc/component_indexing_spec.ts @@ -5,9 +5,18 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {AbsoluteFsPath, getFileSystem, PathManipulation} from '@angular/compiler-cli/src/ngtsc/file_system'; +import { + AbsoluteFsPath, + getFileSystem, + PathManipulation, +} from '@angular/compiler-cli/src/ngtsc/file_system'; import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; -import {AbsoluteSourceSpan, IdentifierKind, IndexedComponent, TopLevelIdentifier} from '@angular/compiler-cli/src/ngtsc/indexer'; +import { + AbsoluteSourceSpan, + IdentifierKind, + IndexedComponent, + TopLevelIdentifier, +} from '@angular/compiler-cli/src/ngtsc/indexer'; import {ParseSourceFile} from '@angular/compiler/src/compiler'; import {NgtscTestEnvironment} from './env'; @@ -45,11 +54,13 @@ runInEachFileSystem(() => { const [[decl, indexedComp]] = Array.from(indexed.entries()); expect(decl.getText()).toContain('export class TestCmp {}'); - expect(indexedComp).toEqual(jasmine.objectContaining({ - name: 'TestCmp', - selector: 'test-cmp', - file: new ParseSourceFile(componentContent, testSourceFile), - })); + expect(indexedComp).toEqual( + jasmine.objectContaining({ + name: 'TestCmp', + selector: 'test-cmp', + file: new ParseSourceFile(componentContent, testSourceFile), + }), + ); }); it('should index inline templates', () => { @@ -68,12 +79,14 @@ runInEachFileSystem(() => { const template = indexedComp.template; expect(template).toEqual({ - identifiers: new Set([{ - name: 'foo', - kind: IdentifierKind.Property, - span: new AbsoluteSourceSpan(127, 130), - target: null, - }]), + identifiers: new Set([ + { + name: 'foo', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(127, 130), + target: null, + }, + ]), usedComponents: new Set(), isInline: true, file: new ParseSourceFile(componentContent, testSourceFile), @@ -81,7 +94,9 @@ runInEachFileSystem(() => { }); it('should index external templates', () => { - env.write(testSourceFile, ` + env.write( + testSourceFile, + ` import {Component} from '@angular/core'; @Component({ @@ -89,19 +104,22 @@ runInEachFileSystem(() => { templateUrl: './test.html', }) export class TestCmp { foo = 0; } - `); + `, + ); env.write(testTemplateFile, '{{foo}}'); const indexed = env.driveIndexer(); const [[_, indexedComp]] = Array.from(indexed.entries()); const template = indexedComp.template; expect(template).toEqual({ - identifiers: new Set([{ - name: 'foo', - kind: IdentifierKind.Property, - span: new AbsoluteSourceSpan(2, 5), - target: null, - }]), + identifiers: new Set([ + { + name: 'foo', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(2, 5), + target: null, + }, + ]), usedComponents: new Set(), isInline: false, file: new ParseSourceFile('{{foo}}', testTemplateFile), @@ -113,7 +131,9 @@ runInEachFileSystem(() => { preserveWhitespaces: false, }); - env.write(testSourceFile, ` + env.write( + testSourceFile, + ` import {Component} from '@angular/core'; @Component({ @@ -121,19 +141,22 @@ runInEachFileSystem(() => { templateUrl: './test.html', }) export class TestCmp { foo = 0; } - `); + `, + ); env.write(testTemplateFile, ' \n {{foo}}'); const indexed = env.driveIndexer(); const [[_, indexedComp]] = Array.from(indexed.entries()); const template = indexedComp.template; expect(template).toEqual({ - identifiers: new Set([{ - name: 'foo', - kind: IdentifierKind.Property, - span: new AbsoluteSourceSpan(7, 10), - target: null, - }]), + identifiers: new Set([ + { + name: 'foo', + kind: IdentifierKind.Property, + span: new AbsoluteSourceSpan(7, 10), + target: null, + }, + ]), usedComponents: new Set(), isInline: false, file: new ParseSourceFile(' \n {{foo}}', testTemplateFile), @@ -141,7 +164,9 @@ runInEachFileSystem(() => { }); it('should generate information about used components', () => { - env.write(testSourceFile, ` + env.write( + testSourceFile, + ` import {Component} from '@angular/core'; @Component({ @@ -149,9 +174,12 @@ runInEachFileSystem(() => { templateUrl: './test.html', }) export class TestCmp {} - `); + `, + ); env.write(testTemplateFile, '
'); - env.write('test_import.ts', ` + env.write( + 'test_import.ts', + ` import {Component, NgModule} from '@angular/core'; import {TestCmp} from './test'; @@ -168,14 +196,15 @@ runInEachFileSystem(() => { bootstrap: [TestImportCmp] }) export class TestModule {} - `); + `, + ); env.write('test_import.html', ''); const indexed = env.driveIndexer(); expect(indexed.size).toBe(2); const indexedComps = Array.from(indexed.values()); - const testComp = indexedComps.find(comp => comp.name === 'TestCmp'); - const testImportComp = indexedComps.find(cmp => cmp.name === 'TestImportCmp'); + const testComp = indexedComps.find((comp) => comp.name === 'TestCmp'); + const testImportComp = indexedComps.find((cmp) => cmp.name === 'TestImportCmp'); expect(testComp).toBeDefined(); expect(testImportComp).toBeDefined(); diff --git a/packages/compiler-cli/test/ngtsc/defer_spec.ts b/packages/compiler-cli/test/ngtsc/defer_spec.ts index e96f90ed017de..84ca72772fa1a 100644 --- a/packages/compiler-cli/test/ngtsc/defer_spec.ts +++ b/packages/compiler-cli/test/ngtsc/defer_spec.ts @@ -25,7 +25,9 @@ runInEachFileSystem(() => { }); it('should handle deferred blocks', () => { - env.write('cmp-a.ts', ` + env.write( + 'cmp-a.ts', + ` import { Component } from '@angular/core'; @Component({ @@ -34,9 +36,12 @@ runInEachFileSystem(() => { template: 'CmpA!' }) export class CmpA {} - `); + `, + ); - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import { Component } from '@angular/core'; import { CmpA } from './cmp-a'; @@ -59,7 +64,8 @@ runInEachFileSystem(() => { \`, }) export class TestCmp {} - `); + `, + ); env.driveMain(); @@ -73,10 +79,12 @@ runInEachFileSystem(() => { expect(jsContents).not.toContain('import { CmpA }'); }); - it('should include timer scheduler function when ' + - '`after` or `minimum` parameters are used', - () => { - env.write('cmp-a.ts', ` + it( + 'should include timer scheduler function when ' + '`after` or `minimum` parameters are used', + () => { + env.write( + 'cmp-a.ts', + ` import { Component } from '@angular/core'; @Component({ @@ -85,9 +93,12 @@ runInEachFileSystem(() => { template: 'CmpA!' }) export class CmpA {} - `); + `, + ); - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import { Component } from '@angular/core'; import { CmpA } from './cmp-a'; @@ -104,19 +115,23 @@ runInEachFileSystem(() => { \`, }) export class TestCmp {} - `); + `, + ); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - 'ɵɵdefer(2, 0, TestCmp_Defer_2_DepsFn, 1, null, null, 0, null, i0.ɵɵdeferEnableTimerScheduling)'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain( + 'ɵɵdefer(2, 0, TestCmp_Defer_2_DepsFn, 1, null, null, 0, null, i0.ɵɵdeferEnableTimerScheduling)', + ); + }, + ); describe('imports', () => { it('should retain regular imports when symbol is eagerly referenced', () => { - env.write('cmp-a.ts', ` + env.write( + 'cmp-a.ts', + ` import { Component } from '@angular/core'; @Component({ @@ -125,9 +140,12 @@ runInEachFileSystem(() => { template: 'CmpA!' }) export class CmpA {} - `); + `, + ); - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import { Component } from '@angular/core'; import { CmpA } from './cmp-a'; @@ -148,7 +166,8 @@ runInEachFileSystem(() => { console.log(CmpA); } } - `); + `, + ); env.driveMain(); @@ -163,7 +182,9 @@ runInEachFileSystem(() => { }); it('should retain regular imports when one of the symbols is eagerly referenced', () => { - env.write('cmp-a.ts', ` + env.write( + 'cmp-a.ts', + ` import { Component } from '@angular/core'; @Component({ @@ -179,9 +200,12 @@ runInEachFileSystem(() => { template: 'CmpB!' }) export class CmpB {} - `); + `, + ); - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import { Component } from '@angular/core'; import { CmpA, CmpB } from './cmp-a'; @@ -203,7 +227,8 @@ runInEachFileSystem(() => { console.log(CmpA); } } - `); + `, + ); env.driveMain(); @@ -219,7 +244,9 @@ runInEachFileSystem(() => { }); it('should drop regular imports when none of the symbols are eagerly referenced', () => { - env.write('cmp-a.ts', ` + env.write( + 'cmp-a.ts', + ` import { Component } from '@angular/core'; @Component({ @@ -235,9 +262,12 @@ runInEachFileSystem(() => { template: 'CmpB!' }) export class CmpB {} - `); + `, + ); - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import { Component } from '@angular/core'; import { CmpA, CmpB } from './cmp-a'; @@ -253,7 +283,8 @@ runInEachFileSystem(() => { \`, }) export class TestCmp {} - `); + `, + ); env.driveMain(); @@ -263,16 +294,18 @@ runInEachFileSystem(() => { // Both `CmpA` and `CmpB` were used inside the defer block and were not // referenced elsewhere, so we generate dynamic imports and drop a regular one. - expect(jsContents) - .toContain( - '() => [' + - 'import("./cmp-a").then(m => m.CmpA), ' + - 'import("./cmp-a").then(m => m.CmpB)]'); + expect(jsContents).toContain( + '() => [' + + 'import("./cmp-a").then(m => m.CmpA), ' + + 'import("./cmp-a").then(m => m.CmpB)]', + ); expect(jsContents).not.toContain('import { CmpA, CmpB }'); }); it('should lazy-load dependency referenced with a fowrardRef', () => { - env.write('cmp-a.ts', ` + env.write( + 'cmp-a.ts', + ` import { Component } from '@angular/core'; @Component({ @@ -281,9 +314,12 @@ runInEachFileSystem(() => { template: 'CmpA!' }) export class CmpA {} - `); + `, + ); - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import { Component, forwardRef } from '@angular/core'; import { CmpA } from './cmp-a'; @@ -298,7 +334,8 @@ runInEachFileSystem(() => { \`, }) export class TestCmp {} - `); + `, + ); env.driveMain(); @@ -313,7 +350,9 @@ runInEachFileSystem(() => { }); it('should drop imports when one is deferrable and the rest are type-only imports', () => { - env.write('cmp-a.ts', ` + env.write( + 'cmp-a.ts', + ` import { Component } from '@angular/core'; export class Foo {} @@ -324,9 +363,12 @@ runInEachFileSystem(() => { template: 'CmpA!' }) export class CmpA {} - `); + `, + ); - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import { Component } from '@angular/core'; import { CmpA, type Foo } from './cmp-a'; @@ -343,7 +385,8 @@ runInEachFileSystem(() => { \`, }) export class TestCmp {} - `); + `, + ); env.driveMain(); @@ -354,9 +397,10 @@ runInEachFileSystem(() => { expect(jsContents).not.toContain('import { CmpA }'); }); - it('should drop multiple imports to the same file when one is deferrable and the other has a single type-only element', - () => { - env.write('cmp-a.ts', ` + it('should drop multiple imports to the same file when one is deferrable and the other has a single type-only element', () => { + env.write( + 'cmp-a.ts', + ` import { Component } from '@angular/core'; export class Foo {} @@ -367,9 +411,12 @@ runInEachFileSystem(() => { template: 'CmpA!' }) export class CmpA {} - `); + `, + ); - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import { Component } from '@angular/core'; import { CmpA } from './cmp-a'; import { type Foo } from './cmp-a'; @@ -387,20 +434,22 @@ runInEachFileSystem(() => { \`, }) export class TestCmp {} - `); + `, + ); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); + const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)'); - expect(jsContents).toContain('() => [import("./cmp-a").then(m => m.CmpA)]'); - expect(jsContents).not.toContain('import { CmpA }'); - }); + expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)'); + expect(jsContents).toContain('() => [import("./cmp-a").then(m => m.CmpA)]'); + expect(jsContents).not.toContain('import { CmpA }'); + }); - it('should drop multiple imports to the same file when one is deferrable and the other is type-only at the declaration level', - () => { - env.write('cmp-a.ts', ` + it('should drop multiple imports to the same file when one is deferrable and the other is type-only at the declaration level', () => { + env.write( + 'cmp-a.ts', + ` import { Component } from '@angular/core'; export class Foo {} @@ -411,9 +460,12 @@ runInEachFileSystem(() => { template: 'CmpA!' }) export class CmpA {} - `); + `, + ); - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import { Component } from '@angular/core'; import { CmpA } from './cmp-a'; import type { Foo, CmpA as CmpAlias } from './cmp-a'; @@ -431,20 +483,22 @@ runInEachFileSystem(() => { \`, }) export class TestCmp {} - `); + `, + ); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); + const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)'); - expect(jsContents).toContain('() => [import("./cmp-a").then(m => m.CmpA)]'); - expect(jsContents).not.toContain('import { CmpA }'); - }); + expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)'); + expect(jsContents).toContain('() => [import("./cmp-a").then(m => m.CmpA)]'); + expect(jsContents).not.toContain('import { CmpA }'); + }); - it('should drop multiple imports to the same file when one is deferrable and the other is a type-only import of all symbols', - () => { - env.write('cmp-a.ts', ` + it('should drop multiple imports to the same file when one is deferrable and the other is a type-only import of all symbols', () => { + env.write( + 'cmp-a.ts', + ` import { Component } from '@angular/core'; export class Foo {} @@ -455,9 +509,12 @@ runInEachFileSystem(() => { template: 'CmpA!' }) export class CmpA {} - `); + `, + ); - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import { Component } from '@angular/core'; import { CmpA } from './cmp-a'; import type * as allCmpA from './cmp-a'; @@ -475,19 +532,22 @@ runInEachFileSystem(() => { \`, }) export class TestCmp {} - `); + `, + ); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); + const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)'); - expect(jsContents).toContain('() => [import("./cmp-a").then(m => m.CmpA)]'); - expect(jsContents).not.toContain('import { CmpA }'); - }); + expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)'); + expect(jsContents).toContain('() => [import("./cmp-a").then(m => m.CmpA)]'); + expect(jsContents).not.toContain('import { CmpA }'); + }); it('should drop multiple imports of deferrable symbols from the same file', () => { - env.write('cmps.ts', ` + env.write( + 'cmps.ts', + ` import { Component } from '@angular/core'; @Component({ @@ -503,9 +563,12 @@ runInEachFileSystem(() => { template: 'CmpB!' }) export class CmpB {} - `); + `, + ); - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import { Component } from '@angular/core'; import { CmpA } from './cmps'; import { CmpB } from './cmps'; @@ -522,22 +585,25 @@ runInEachFileSystem(() => { \`, }) export class TestCmp {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)'); - expect(jsContents) - .toContain( - '() => [import("./cmps").then(m => m.CmpA), import("./cmps").then(m => m.CmpB)]'); + expect(jsContents).toContain( + '() => [import("./cmps").then(m => m.CmpA), import("./cmps").then(m => m.CmpB)]', + ); expect(jsContents).not.toContain('import { CmpA }'); expect(jsContents).not.toContain('import { CmpB }'); }); it('should handle deferred dependencies imported through a default import', () => { - env.write('cmp-a.ts', ` + env.write( + 'cmp-a.ts', + ` import { Component } from '@angular/core'; @Component({ standalone: true, @@ -545,8 +611,11 @@ runInEachFileSystem(() => { template: 'CmpA!' }) export default class CmpA {} - `); - env.write('/test.ts', ` + `, + ); + env.write( + '/test.ts', + ` import { Component } from '@angular/core'; import CmpA from './cmp-a'; @Component({ @@ -567,17 +636,18 @@ runInEachFileSystem(() => { \`, }) export class TestCmp {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)'); - expect(jsContents) - .toContain( - 'const TestCmp_Defer_1_DepsFn = () => [import("./cmp-a").then(m => m.default), LocalDep];'); - expect(jsContents) - .toContain( - 'i0.ɵsetClassMetadataAsync(TestCmp, () => [import("./cmp-a").then(m => m.default)]'); + expect(jsContents).toContain( + 'const TestCmp_Defer_1_DepsFn = () => [import("./cmp-a").then(m => m.default), LocalDep];', + ); + expect(jsContents).toContain( + 'i0.ɵsetClassMetadataAsync(TestCmp, () => [import("./cmp-a").then(m => m.default)]', + ); // The `CmpA` symbol wasn't referenced elsewhere, so it can be defer-loaded // via dynamic imports and an original import can be removed. expect(jsContents).not.toContain('import CmpA'); @@ -585,7 +655,9 @@ runInEachFileSystem(() => { }); it('should detect pipe used in the `when` trigger as an eager dependency', () => { - env.write('test-pipe.ts', ` + env.write( + 'test-pipe.ts', + ` import { Pipe } from '@angular/core'; @Pipe({name: 'test', standalone: true}) @@ -594,9 +666,12 @@ runInEachFileSystem(() => { return 1; } } - `); + `, + ); - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import { Component } from '@angular/core'; import { TestPipe } from './test-pipe'; @@ -608,7 +683,8 @@ runInEachFileSystem(() => { }) export class TestCmp { } - `); + `, + ); env.driveMain(); @@ -618,7 +694,9 @@ runInEachFileSystem(() => { }); it('should detect pipe used in the `prefetch when` trigger as an eager dependency', () => { - env.write('test-pipe.ts', ` + env.write( + 'test-pipe.ts', + ` import { Pipe } from '@angular/core'; @Pipe({name: 'test', standalone: true}) @@ -627,9 +705,12 @@ runInEachFileSystem(() => { return 1; } } - `); + `, + ); - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import { Component } from '@angular/core'; import { TestPipe } from './test-pipe'; @@ -641,7 +722,8 @@ runInEachFileSystem(() => { }) export class TestCmp { } - `); + `, + ); env.driveMain(); @@ -651,7 +733,9 @@ runInEachFileSystem(() => { }); it('should detect pipe used both in a trigger and the deferred content as eager', () => { - env.write('test-pipe.ts', ` + env.write( + 'test-pipe.ts', + ` import { Pipe } from '@angular/core'; @Pipe({name: 'test', standalone: true}) @@ -660,9 +744,12 @@ runInEachFileSystem(() => { return 1; } } - `); + `, + ); - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import { Component } from '@angular/core'; import { TestPipe } from './test-pipe'; @@ -674,7 +761,8 @@ runInEachFileSystem(() => { }) export class TestCmp { } - `); + `, + ); env.driveMain(); @@ -689,7 +777,9 @@ runInEachFileSystem(() => { }); it('should handle `@Component.deferredImports` field', () => { - env.write('deferred-a.ts', ` + env.write( + 'deferred-a.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -699,9 +789,12 @@ runInEachFileSystem(() => { }) export class DeferredCmpA { } - `); + `, + ); - env.write('deferred-b.ts', ` + env.write( + 'deferred-b.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -711,9 +804,12 @@ runInEachFileSystem(() => { }) export class DeferredCmpB { } - `); + `, + ); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; import {DeferredCmpA} from './deferred-a'; import {DeferredCmpB} from './deferred-b'; @@ -733,20 +829,21 @@ runInEachFileSystem(() => { }) export class AppCmp { } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); // Expect that all deferrableImports become dynamic imports. - expect(jsContents) - .toContain( - 'const AppCmp_Defer_1_DepsFn = () => [' + - 'import("./deferred-a").then(m => m.DeferredCmpA)];'); - expect(jsContents) - .toContain( - 'const AppCmp_Defer_4_DepsFn = () => [' + - 'import("./deferred-b").then(m => m.DeferredCmpB)];'); + expect(jsContents).toContain( + 'const AppCmp_Defer_1_DepsFn = () => [' + + 'import("./deferred-a").then(m => m.DeferredCmpA)];', + ); + expect(jsContents).toContain( + 'const AppCmp_Defer_4_DepsFn = () => [' + + 'import("./deferred-b").then(m => m.DeferredCmpB)];', + ); // Make sure there are no eager imports present in the output. expect(jsContents).not.toContain(`from './deferred-a'`); @@ -757,17 +854,18 @@ runInEachFileSystem(() => { expect(jsContents).toContain('ɵɵdefer(4, 3, AppCmp_Defer_4_DepsFn);'); // Expect `ɵsetClassMetadataAsync` to contain dynamic imports too. - expect(jsContents) - .toContain( - 'ɵsetClassMetadataAsync(AppCmp, () => [' + - 'import("./deferred-a").then(m => m.DeferredCmpA), ' + - 'import("./deferred-b").then(m => m.DeferredCmpB)], ' + - '(DeferredCmpA, DeferredCmpB) => {'); + expect(jsContents).toContain( + 'ɵsetClassMetadataAsync(AppCmp, () => [' + + 'import("./deferred-a").then(m => m.DeferredCmpA), ' + + 'import("./deferred-b").then(m => m.DeferredCmpB)], ' + + '(DeferredCmpA, DeferredCmpB) => {', + ); }); - it('should handle defer blocks that rely on deps from `deferredImports` and `imports`', - () => { - env.write('eager-a.ts', ` + it('should handle defer blocks that rely on deps from `deferredImports` and `imports`', () => { + env.write( + 'eager-a.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -777,9 +875,12 @@ runInEachFileSystem(() => { }) export class EagerCmpA { } - `); + `, + ); - env.write('deferred-a.ts', ` + env.write( + 'deferred-a.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -789,9 +890,12 @@ runInEachFileSystem(() => { }) export class DeferredCmpA { } - `); + `, + ); - env.write('deferred-b.ts', ` + env.write( + 'deferred-b.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -801,9 +905,12 @@ runInEachFileSystem(() => { }) export class DeferredCmpB { } - `); + `, + ); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; import {DeferredCmpA} from './deferred-a'; import {DeferredCmpB} from './deferred-b'; @@ -827,48 +934,50 @@ runInEachFileSystem(() => { }) export class AppCmp { } - `); - - env.driveMain(); - const jsContents = env.getContents('test.js'); - - // Expect that all deferrableImports to become dynamic imports. - // Other imported symbols remain eager. - expect(jsContents) - .toContain( - 'const AppCmp_Defer_1_DepsFn = () => [' + - 'import("./deferred-a").then(m => m.DeferredCmpA), ' + - 'EagerCmpA];'); - expect(jsContents) - .toContain( - 'const AppCmp_Defer_4_DepsFn = () => [' + - 'import("./deferred-b").then(m => m.DeferredCmpB), ' + - 'EagerCmpA];'); - - // Make sure there are no eager imports present in the output. - expect(jsContents).not.toContain(`from './deferred-a'`); - expect(jsContents).not.toContain(`from './deferred-b'`); - - // Eager dependencies retain their imports. - expect(jsContents).toContain(`from './eager-a';`); - - // Defer blocks would have their own dependency functions in full mode. - expect(jsContents).toContain('ɵɵdefer(1, 0, AppCmp_Defer_1_DepsFn);'); - expect(jsContents).toContain('ɵɵdefer(4, 3, AppCmp_Defer_4_DepsFn);'); - - // Expect `ɵsetClassMetadataAsync` to contain dynamic imports too. - expect(jsContents) - .toContain( - 'ɵsetClassMetadataAsync(AppCmp, () => [' + - 'import("./deferred-a").then(m => m.DeferredCmpA), ' + - 'import("./deferred-b").then(m => m.DeferredCmpB)], ' + - '(DeferredCmpA, DeferredCmpB) => {'); - }); + `, + ); + + env.driveMain(); + const jsContents = env.getContents('test.js'); + + // Expect that all deferrableImports to become dynamic imports. + // Other imported symbols remain eager. + expect(jsContents).toContain( + 'const AppCmp_Defer_1_DepsFn = () => [' + + 'import("./deferred-a").then(m => m.DeferredCmpA), ' + + 'EagerCmpA];', + ); + expect(jsContents).toContain( + 'const AppCmp_Defer_4_DepsFn = () => [' + + 'import("./deferred-b").then(m => m.DeferredCmpB), ' + + 'EagerCmpA];', + ); + + // Make sure there are no eager imports present in the output. + expect(jsContents).not.toContain(`from './deferred-a'`); + expect(jsContents).not.toContain(`from './deferred-b'`); + + // Eager dependencies retain their imports. + expect(jsContents).toContain(`from './eager-a';`); + + // Defer blocks would have their own dependency functions in full mode. + expect(jsContents).toContain('ɵɵdefer(1, 0, AppCmp_Defer_1_DepsFn);'); + expect(jsContents).toContain('ɵɵdefer(4, 3, AppCmp_Defer_4_DepsFn);'); + + // Expect `ɵsetClassMetadataAsync` to contain dynamic imports too. + expect(jsContents).toContain( + 'ɵsetClassMetadataAsync(AppCmp, () => [' + + 'import("./deferred-a").then(m => m.DeferredCmpA), ' + + 'import("./deferred-b").then(m => m.DeferredCmpB)], ' + + '(DeferredCmpA, DeferredCmpB) => {', + ); + }); describe('error handling', () => { - it('should produce an error when unsupported type (@Injectable) is used in `deferredImports`', - () => { - env.write('test.ts', ` + it('should produce an error when unsupported type (@Injectable) is used in `deferredImports`', () => { + env.write( + 'test.ts', + ` import {Component, Injectable} from '@angular/core'; @Injectable() class MyInjectable {} @@ -880,16 +989,18 @@ runInEachFileSystem(() => { }) export class AppCmp { } - `); + `, + ); - const diags = env.driveDiagnostics(); - expect(diags.length).toBe(1); - expect(diags[0].code).toBe(ngErrorCode(ErrorCode.COMPONENT_UNKNOWN_DEFERRED_IMPORT)); - }); + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].code).toBe(ngErrorCode(ErrorCode.COMPONENT_UNKNOWN_DEFERRED_IMPORT)); + }); - it('should produce an error when unsupported type (@NgModule) is used in `deferredImports`', - () => { - env.write('test.ts', ` + it('should produce an error when unsupported type (@NgModule) is used in `deferredImports`', () => { + env.write( + 'test.ts', + ` import {Component, NgModule} from '@angular/core'; @NgModule() class MyModule {} @@ -901,16 +1012,18 @@ runInEachFileSystem(() => { }) export class AppCmp { } - `); + `, + ); - const diags = env.driveDiagnostics(); - expect(diags.length).toBe(1); - expect(diags[0].code).toBe(ngErrorCode(ErrorCode.COMPONENT_UNKNOWN_DEFERRED_IMPORT)); - }); + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].code).toBe(ngErrorCode(ErrorCode.COMPONENT_UNKNOWN_DEFERRED_IMPORT)); + }); - it('should produce an error when components from `deferredImports` are used outside of defer blocks', - () => { - env.write('deferred-a.ts', ` + it('should produce an error when components from `deferredImports` are used outside of defer blocks', () => { + env.write( + 'deferred-a.ts', + ` import {Component} from '@angular/core'; @Component({ standalone: true, @@ -919,9 +1032,12 @@ runInEachFileSystem(() => { }) export class DeferredCmpA { } - `); + `, + ); - env.write('deferred-b.ts', ` + env.write( + 'deferred-b.ts', + ` import {Component} from '@angular/core'; @Component({ standalone: true, @@ -930,9 +1046,12 @@ runInEachFileSystem(() => { }) export class DeferredCmpB { } - `); + `, + ); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; import {DeferredCmpA} from './deferred-a'; import {DeferredCmpB} from './deferred-b'; @@ -949,17 +1068,19 @@ runInEachFileSystem(() => { }) export class AppCmp { } - `); + `, + ); - const diags = env.driveDiagnostics(); + const diags = env.driveDiagnostics(); - expect(diags.length).toBe(1); - expect(diags[0].code).toBe(ngErrorCode(ErrorCode.DEFERRED_DIRECTIVE_USED_EAGERLY)); - }); + expect(diags.length).toBe(1); + expect(diags[0].code).toBe(ngErrorCode(ErrorCode.DEFERRED_DIRECTIVE_USED_EAGERLY)); + }); - it('should produce an error the same component is referenced in both `deferredImports` and `imports`', - () => { - env.write('deferred-a.ts', ` + it('should produce an error the same component is referenced in both `deferredImports` and `imports`', () => { + env.write( + 'deferred-a.ts', + ` import {Component} from '@angular/core'; @Component({ standalone: true, @@ -968,9 +1089,12 @@ runInEachFileSystem(() => { }) export class DeferredCmpA { } - `); + `, + ); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; import {DeferredCmpA} from './deferred-a'; @Component({ @@ -985,17 +1109,18 @@ runInEachFileSystem(() => { \`, }) export class AppCmp {} - `); + `, + ); - const diags = env.driveDiagnostics(); - expect(diags.length).toBe(1); - expect(diags[0].code) - .toBe(ngErrorCode(ErrorCode.DEFERRED_DEPENDENCY_IMPORTED_EAGERLY)); - }); + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].code).toBe(ngErrorCode(ErrorCode.DEFERRED_DEPENDENCY_IMPORTED_EAGERLY)); + }); - it('should produce an error when pipes from `deferredImports` are used outside of defer blocks', - () => { - env.write('deferred-pipe-a.ts', ` + it('should produce an error when pipes from `deferredImports` are used outside of defer blocks', () => { + env.write( + 'deferred-pipe-a.ts', + ` import {Pipe} from '@angular/core'; @Pipe({ standalone: true, @@ -1004,9 +1129,12 @@ runInEachFileSystem(() => { export class DeferredPipeA { transform() {} } - `); + `, + ); - env.write('deferred-pipe-b.ts', ` + env.write( + 'deferred-pipe-b.ts', + ` import {Pipe} from '@angular/core'; @Pipe({ standalone: true, @@ -1015,9 +1143,12 @@ runInEachFileSystem(() => { export class DeferredPipeB { transform() {} } - `); + `, + ); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; import {DeferredPipeA} from './deferred-pipe-a'; import {DeferredPipeB} from './deferred-pipe-b'; @@ -1033,15 +1164,18 @@ runInEachFileSystem(() => { \`, }) export class AppCmp {} - `); + `, + ); - const diags = env.driveDiagnostics(); - expect(diags.length).toBe(1); - expect(diags[0].code).toBe(ngErrorCode(ErrorCode.DEFERRED_PIPE_USED_EAGERLY)); - }); + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].code).toBe(ngErrorCode(ErrorCode.DEFERRED_PIPE_USED_EAGERLY)); + }); it('should not produce an error when a deferred block is wrapped in a conditional', () => { - env.write('deferred-a.ts', ` + env.write( + 'deferred-a.ts', + ` import {Component} from '@angular/core'; @Component({ standalone: true, @@ -1050,9 +1184,12 @@ runInEachFileSystem(() => { }) export class DeferredCmpA { } - `); + `, + ); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; import {DeferredCmpA} from './deferred-a'; @Component({ @@ -1074,15 +1211,17 @@ runInEachFileSystem(() => { export class AppCmp { condition = true; } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags).toEqual([]); }); - it('should not produce an error when a dependency is wrapped in a condition inside of a deferred block', - () => { - env.write('deferred-a.ts', ` + it('should not produce an error when a dependency is wrapped in a condition inside of a deferred block', () => { + env.write( + 'deferred-a.ts', + ` import {Component} from '@angular/core'; @Component({ standalone: true, @@ -1091,9 +1230,12 @@ runInEachFileSystem(() => { }) export class DeferredCmpA { } - `); + `, + ); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; import {DeferredCmpA} from './deferred-a'; @Component({ @@ -1115,17 +1257,20 @@ runInEachFileSystem(() => { export class AppCmp { condition = true; } - `); + `, + ); - const diags = env.driveDiagnostics(); - expect(diags).toEqual([]); - }); + const diags = env.driveDiagnostics(); + expect(diags).toEqual([]); + }); }); }); describe('setClassMetadataAsync', () => { it('should generate setClassMetadataAsync for components with defer blocks', () => { - env.write('cmp-a.ts', ` + env.write( + 'cmp-a.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1134,9 +1279,12 @@ runInEachFileSystem(() => { template: 'CmpA!' }) export class CmpA {} - `); + `, + ); - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import {Component} from '@angular/core'; import {CmpA} from './cmp-a'; @@ -1159,29 +1307,33 @@ runInEachFileSystem(() => { \`, }) export class TestCmp {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)'); - expect(jsContents) - .toContain( - // ngDevMode check is present - '(() => { (typeof ngDevMode === "undefined" || ngDevMode) && ' + - // Main `setClassMetadataAsync` call - 'i0.ɵsetClassMetadataAsync(TestCmp, ' + - // Dependency loading function (note: no local `LocalDep` here) - '() => [import("./cmp-a").then(m => m.CmpA)], ' + - // Callback that invokes `setClassMetadata` at the end - 'CmpA => { i0.ɵsetClassMetadata(TestCmp'); + expect(jsContents).toContain( + // ngDevMode check is present + '(() => { (typeof ngDevMode === "undefined" || ngDevMode) && ' + + // Main `setClassMetadataAsync` call + 'i0.ɵsetClassMetadataAsync(TestCmp, ' + + // Dependency loading function (note: no local `LocalDep` here) + '() => [import("./cmp-a").then(m => m.CmpA)], ' + + // Callback that invokes `setClassMetadata` at the end + 'CmpA => { i0.ɵsetClassMetadata(TestCmp', + ); }); - it('should *not* generate setClassMetadataAsync for components with defer blocks ' + - 'when dependencies are eagerly referenced as well', - () => { - env.write('cmp-a.ts', ` + it( + 'should *not* generate setClassMetadataAsync for components with defer blocks ' + + 'when dependencies are eagerly referenced as well', + () => { + env.write( + 'cmp-a.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1190,9 +1342,12 @@ runInEachFileSystem(() => { template: 'CmpA!' }) export class CmpA {} - `); + `, + ); - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import {Component} from '@angular/core'; import {CmpA} from './cmp-a'; @@ -1212,26 +1367,30 @@ runInEachFileSystem(() => { console.log(CmpA); } } - `); + `, + ); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); + const jsContents = env.getContents('test.js'); - // Dependency function eagerly references `CmpA`. - expect(jsContents).toContain('() => [CmpA]'); + // Dependency function eagerly references `CmpA`. + expect(jsContents).toContain('() => [CmpA]'); - // The `setClassMetadataAsync` wasn't generated, since there are no deferrable - // symbols. - expect(jsContents).not.toContain('setClassMetadataAsync'); + // The `setClassMetadataAsync` wasn't generated, since there are no deferrable + // symbols. + expect(jsContents).not.toContain('setClassMetadataAsync'); - // But the regular `setClassMetadata` is present. - expect(jsContents).toContain('setClassMetadata'); - }); + // But the regular `setClassMetadata` is present. + expect(jsContents).toContain('setClassMetadata'); + }, + ); }); it('should generate setClassMetadataAsync for default imports', () => { - env.write('cmp-a.ts', ` + env.write( + 'cmp-a.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1240,9 +1399,12 @@ runInEachFileSystem(() => { template: 'CmpA!' }) export default class CmpA {} - `); + `, + ); - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import {Component} from '@angular/core'; import CmpA from './cmp-a'; @@ -1265,23 +1427,24 @@ runInEachFileSystem(() => { \`, }) export class TestCmp {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); expect(jsContents).toContain('ɵɵdefer(1, 0, TestCmp_Defer_1_DepsFn)'); - expect(jsContents) - .toContain( - // ngDevMode check is present - '(() => { (typeof ngDevMode === "undefined" || ngDevMode) && ' + - // Main `setClassMetadataAsync` call - 'i0.ɵsetClassMetadataAsync(TestCmp, ' + - // Dependency loading function (note: no local `LocalDep` here) - '() => [import("./cmp-a").then(m => m.default)], ' + - // Callback that invokes `setClassMetadata` at the end - 'CmpA => { i0.ɵsetClassMetadata(TestCmp'); + expect(jsContents).toContain( + // ngDevMode check is present + '(() => { (typeof ngDevMode === "undefined" || ngDevMode) && ' + + // Main `setClassMetadataAsync` call + 'i0.ɵsetClassMetadataAsync(TestCmp, ' + + // Dependency loading function (note: no local `LocalDep` here) + '() => [import("./cmp-a").then(m => m.default)], ' + + // Callback that invokes `setClassMetadata` at the end + 'CmpA => { i0.ɵsetClassMetadata(TestCmp', + ); }); }); }); diff --git a/packages/compiler-cli/test/ngtsc/doc_extraction/class_doc_extraction_spec.ts b/packages/compiler-cli/test/ngtsc/doc_extraction/class_doc_extraction_spec.ts index 80a73fdf6a294..65dff55ef288a 100644 --- a/packages/compiler-cli/test/ngtsc/doc_extraction/class_doc_extraction_spec.ts +++ b/packages/compiler-cli/test/ngtsc/doc_extraction/class_doc_extraction_spec.ts @@ -7,7 +7,14 @@ */ import {DocEntry} from '@angular/compiler-cli/src/ngtsc/docs'; -import {ClassEntry, EntryType, MemberTags, MemberType, MethodEntry, PropertyEntry} from '@angular/compiler-cli/src/ngtsc/docs/src/entities'; +import { + ClassEntry, + EntryType, + MemberTags, + MemberType, + MethodEntry, + PropertyEntry, +} from '@angular/compiler-cli/src/ngtsc/docs/src/entities'; import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; import {loadStandardTestFiles} from '@angular/compiler-cli/src/ngtsc/testing'; @@ -25,11 +32,14 @@ runInEachFileSystem(() => { }); it('should extract classes', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export class UserProfile {} export class CustomSlider {} - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(2); @@ -46,12 +56,15 @@ runInEachFileSystem(() => { }); it('should extract class members', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export class UserProfile { firstName(): string { return 'Morgan'; } age: number = 25; } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); const classEntry = docs[0] as ClassEntry; @@ -69,7 +82,9 @@ runInEachFileSystem(() => { }); it('should extract methods with overloads', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export class UserProfile { ident(value: boolean): boolean ident(value: number): number @@ -77,7 +92,8 @@ runInEachFileSystem(() => { return 0; } } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); const classEntry = docs[0] as ClassEntry; @@ -97,12 +113,15 @@ runInEachFileSystem(() => { }); it('should not extract Angular-internal members', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export class UserProfile { ɵfirstName(): string { return 'Morgan'; } _age: number = 25; } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); const classEntry = docs[0] as ClassEntry; @@ -110,18 +129,21 @@ runInEachFileSystem(() => { }); it('should extract a method with a rest parameter', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export class UserProfile { getNames(prefix: string, ...ids: string[]): string[] { return []; } } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); const classEntry = docs[0] as ClassEntry; const methodEntry = classEntry.members[0] as MethodEntry; - const [prefixParamEntry, idsParamEntry, ] = methodEntry.params; + const [prefixParamEntry, idsParamEntry] = methodEntry.params; expect(prefixParamEntry.name).toBe('prefix'); expect(prefixParamEntry.type).toBe('string'); @@ -133,11 +155,14 @@ runInEachFileSystem(() => { }); it('should extract class method params', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export class UserProfile { setPhone(num: string, intl: number = 1, area?: string): void {} } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); @@ -164,13 +189,16 @@ runInEachFileSystem(() => { }); it('should not extract private class members', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export class UserProfile { private ssn: string; private getSsn(): string { return ''; } private static printSsn(): void { } } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); @@ -180,7 +208,9 @@ runInEachFileSystem(() => { it('should extract member tags', () => { // Test both properties and methods with zero, one, and multiple tags. - env.write('index.ts', ` + env.write( + 'index.ts', + ` export class UserProfile { eyeColor = 'brown'; protected name: string; @@ -195,7 +225,8 @@ runInEachFileSystem(() => { static getCountry(): string { return 'USA'; } protected getBirthday?(): string { return '1/1/2000'; } } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); @@ -236,7 +267,9 @@ runInEachFileSystem(() => { it('should extract member tags', () => { // Test both properties and methods with zero, one, and multiple tags. - env.write('index.ts', ` + env.write( + 'index.ts', + ` export class UserProfile { eyeColor = 'brown'; @@ -252,7 +285,8 @@ runInEachFileSystem(() => { // @internal _doSomethingElse() {} } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); @@ -267,7 +301,9 @@ runInEachFileSystem(() => { it('should extract getters and setters', () => { // Test getter-only, a getter + setter, and setter-only. - env.write('index.ts', ` + env.write( + 'index.ts', + ` export class UserProfile { get userId(): number { return 123; } @@ -276,7 +312,8 @@ runInEachFileSystem(() => { set isAdmin(value: boolean) { } } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); const classEntry = docs[0] as ClassEntry; @@ -284,7 +321,7 @@ runInEachFileSystem(() => { expect(classEntry.members.length).toBe(4); - const [userIdGetter, userNameGetter, userNameSetter, isAdminSetter, ] = classEntry.members; + const [userIdGetter, userNameGetter, userNameSetter, isAdminSetter] = classEntry.members; expect(userIdGetter.name).toBe('userId'); expect(userIdGetter.memberType).toBe(MemberType.Getter); @@ -297,14 +334,17 @@ runInEachFileSystem(() => { }); it('should extract abstract classes', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export abstract class UserProfile { firstName: string; abstract lastName: string; save(): void { } abstract reset(): void; - }`); + }`, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); @@ -329,7 +369,9 @@ runInEachFileSystem(() => { }); it('should extract class generic parameters', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export class UserProfile { constructor(public name: T) { } } @@ -348,7 +390,8 @@ runInEachFileSystem(() => { export class ExecProfile { constructor(public name: W) { } - }`); + }`, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(5); @@ -397,10 +440,13 @@ runInEachFileSystem(() => { }); it('should extract method generic parameters', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export class UserProfile { save(data: T): void { } - }`); + }`, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); @@ -414,7 +460,9 @@ runInEachFileSystem(() => { }); it('should extract inherited members', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` class Ancestor { id: string; value: string|number; @@ -432,7 +480,8 @@ runInEachFileSystem(() => { save(value: number): number; save(value: string|number): string|number { return 0; } - }`); + }`, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); @@ -469,7 +518,9 @@ runInEachFileSystem(() => { }); it('should extract inherited getters/setters', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` class Ancestor { get name(): string { return ''; } set name(v: string) { } @@ -487,7 +538,8 @@ runInEachFileSystem(() => { export class Child extends Parent { get id(): string { return ''; } - }`); + }`, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); @@ -496,7 +548,7 @@ runInEachFileSystem(() => { expect(classEntry.members.length).toBe(4); const [idEntry, nameEntry, ageGetterEntry, ageSetterEntry] = - classEntry.members as PropertyEntry[]; + classEntry.members as PropertyEntry[]; // When the child class overrides an accessor pair with another accessor, it overrides // *both* the getter and the setter, resulting (in this case) in just a getter. diff --git a/packages/compiler-cli/test/ngtsc/doc_extraction/common_doc_extraction_spec.ts b/packages/compiler-cli/test/ngtsc/doc_extraction/common_doc_extraction_spec.ts index bb49159e39c6c..212f03a9b89d3 100644 --- a/packages/compiler-cli/test/ngtsc/doc_extraction/common_doc_extraction_spec.ts +++ b/packages/compiler-cli/test/ngtsc/doc_extraction/common_doc_extraction_spec.ts @@ -24,11 +24,14 @@ runInEachFileSystem(() => { }); it('should not extract unexported statements', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` class UserProfile {} function getUser() { } const name = ''; - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(0); diff --git a/packages/compiler-cli/test/ngtsc/doc_extraction/constant_doc_extraction_spec.ts b/packages/compiler-cli/test/ngtsc/doc_extraction/constant_doc_extraction_spec.ts index 00f403aeeedce..680fb92031e29 100644 --- a/packages/compiler-cli/test/ngtsc/doc_extraction/constant_doc_extraction_spec.ts +++ b/packages/compiler-cli/test/ngtsc/doc_extraction/constant_doc_extraction_spec.ts @@ -7,7 +7,11 @@ */ import {DocEntry} from '@angular/compiler-cli/src/ngtsc/docs'; -import {ConstantEntry, EntryType, EnumEntry} from '@angular/compiler-cli/src/ngtsc/docs/src/entities'; +import { + ConstantEntry, + EntryType, + EnumEntry, +} from '@angular/compiler-cli/src/ngtsc/docs/src/entities'; import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; import {loadStandardTestFiles} from '@angular/compiler-cli/src/ngtsc/testing'; @@ -25,9 +29,12 @@ runInEachFileSystem(() => { }); it('should extract constants', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export const VERSION = '16.0.0'; - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); @@ -39,9 +46,12 @@ runInEachFileSystem(() => { }); it('should extract multiple constant declarations in a single statement', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export const PI = 3.14, VERSION = '16.0.0'; - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(2); @@ -58,11 +68,14 @@ runInEachFileSystem(() => { }); it('should extract non-primitive constants', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` import {InjectionToken} from '@angular/core'; export const SOME_TOKEN = new InjectionToken('something'); export const TYPED_TOKEN = new InjectionToken(); - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(2); @@ -79,7 +92,9 @@ runInEachFileSystem(() => { }); it('should extract an object literal marked as an enum', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` /** * Toppings for your pizza. * @object-literal-as-enum @@ -91,7 +106,8 @@ runInEachFileSystem(() => { /** Or "tomato" if you are British */ Tomato: "tomato", }; - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); @@ -117,7 +133,9 @@ runInEachFileSystem(() => { }); it('should extract an object literal cast to a const and marked as an enum', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` /** * Toppings for your pizza. * @object-literal-as-enum @@ -129,7 +147,8 @@ runInEachFileSystem(() => { /** Or "tomato" if you are British */ Tomato: "tomato", } as const; - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); diff --git a/packages/compiler-cli/test/ngtsc/doc_extraction/decorator_doc_extraction_spec.ts b/packages/compiler-cli/test/ngtsc/doc_extraction/decorator_doc_extraction_spec.ts index 3ef925a511f6b..e1fd4fd5665ab 100644 --- a/packages/compiler-cli/test/ngtsc/doc_extraction/decorator_doc_extraction_spec.ts +++ b/packages/compiler-cli/test/ngtsc/doc_extraction/decorator_doc_extraction_spec.ts @@ -7,7 +7,11 @@ */ import {DocEntry} from '@angular/compiler-cli/src/ngtsc/docs'; -import {DecoratorEntry, DecoratorType, EntryType} from '@angular/compiler-cli/src/ngtsc/docs/src/entities'; +import { + DecoratorEntry, + DecoratorType, + EntryType, +} from '@angular/compiler-cli/src/ngtsc/docs/src/entities'; import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; import {loadStandardTestFiles} from '@angular/compiler-cli/src/ngtsc/testing'; @@ -25,7 +29,9 @@ runInEachFileSystem(() => { }); it('should extract class decorators that define members in an interface', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export interface Component { /** The template. */ template: string; @@ -39,7 +45,8 @@ runInEachFileSystem(() => { function makeDecorator(): ComponentDecorator { return () => {}; } export const Component: ComponentDecorator = makeDecorator(); - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); @@ -57,7 +64,9 @@ runInEachFileSystem(() => { }); it('should extract property decorators', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export interface Input { /** The alias. */ alias: string; @@ -71,7 +80,8 @@ runInEachFileSystem(() => { function makePropDecorator(): InputDecorator { return () => {}); } export const Input: InputDecorator = makePropDecorator(); - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); @@ -89,7 +99,9 @@ runInEachFileSystem(() => { }); it('should extract property decorators with a type alias', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` interface Query { /** The read. */ read: string; @@ -105,7 +117,8 @@ runInEachFileSystem(() => { function makePropDecorator(): ViewChildDecorator { return () => {}); } export const ViewChild: ViewChildDecorator = makePropDecorator(); - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); @@ -123,7 +136,9 @@ runInEachFileSystem(() => { }); it('should extract param decorators', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export interface Inject { /** The token. */ token: string; @@ -137,7 +152,8 @@ runInEachFileSystem(() => { function makePropDecorator(): InjectDecorator { return () => {}; } export const Inject: InjectDecorator = makeParamDecorator(); - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); diff --git a/packages/compiler-cli/test/ngtsc/doc_extraction/directive_doc_extraction_spec.ts b/packages/compiler-cli/test/ngtsc/doc_extraction/directive_doc_extraction_spec.ts index 65ae9e37a443f..85dbeb81b0ff8 100644 --- a/packages/compiler-cli/test/ngtsc/doc_extraction/directive_doc_extraction_spec.ts +++ b/packages/compiler-cli/test/ngtsc/doc_extraction/directive_doc_extraction_spec.ts @@ -7,7 +7,13 @@ */ import {DocEntry} from '@angular/compiler-cli/src/ngtsc/docs'; -import {ClassEntry, DirectiveEntry, EntryType, MemberTags, PropertyEntry} from '@angular/compiler-cli/src/ngtsc/docs/src/entities'; +import { + ClassEntry, + DirectiveEntry, + EntryType, + MemberTags, + PropertyEntry, +} from '@angular/compiler-cli/src/ngtsc/docs/src/entities'; import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; import {loadStandardTestFiles} from '@angular/compiler-cli/src/ngtsc/testing'; @@ -25,7 +31,9 @@ runInEachFileSystem(() => { }); it('should extract standalone directive info', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` import {Directive} from '@angular/core'; @Directive({ standalone: true, @@ -33,7 +41,8 @@ runInEachFileSystem(() => { exportAs: 'userProfile', }) export class UserProfile { } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); @@ -47,7 +56,9 @@ runInEachFileSystem(() => { }); it('should extract standalone component info', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` import {Component} from '@angular/core'; @Component({ standalone: true, @@ -56,7 +67,8 @@ runInEachFileSystem(() => { template: '', }) export class UserProfile { } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); @@ -70,7 +82,9 @@ runInEachFileSystem(() => { }); it('should extract NgModule directive info', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` import {Directive, NgModule} from '@angular/core'; @NgModule({declarations: [UserProfile]}) @@ -82,7 +96,8 @@ runInEachFileSystem(() => { exportAs: 'userProfile', }) export class UserProfile { } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); @@ -96,7 +111,9 @@ runInEachFileSystem(() => { }); it('should extract NgModule component info', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` import {Component, NgModule} from '@angular/core'; @NgModule({declarations: [UserProfile]}) @@ -109,7 +126,8 @@ runInEachFileSystem(() => { template: '', }) export class UserProfile { } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); @@ -123,7 +141,9 @@ runInEachFileSystem(() => { }); it('should extract input and output info for a directive', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` import {Directive, EventEmitter, Input, Output} from '@angular/core'; @Directive({ standalone: true, @@ -137,7 +157,8 @@ runInEachFileSystem(() => { @Output() saved = new EventEmitter(); @Output('onReset') reset = new EventEmitter(); } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); @@ -147,7 +168,8 @@ runInEachFileSystem(() => { const directiveEntry = docs[0] as DirectiveEntry; expect(directiveEntry.members.length).toBe(5); - const [nameEntry, firstNameEntry, middleNameEntry, savedEntry, resetEntry,] = directiveEntry.members as PropertyEntry[]; + const [nameEntry, firstNameEntry, middleNameEntry, savedEntry, resetEntry] = + directiveEntry.members as PropertyEntry[]; expect(nameEntry.memberTags).toEqual([MemberTags.Input]); expect(nameEntry.inputAlias).toBe('name'); @@ -176,7 +198,9 @@ runInEachFileSystem(() => { }); it('should extract input and output info for a component', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` import {Component, EventEmitter, Input, Output} from '@angular/core'; @Component({ standalone: true, @@ -190,7 +214,8 @@ runInEachFileSystem(() => { @Output() saved = new EventEmitter(); @Output('onReset') reset = new EventEmitter(); } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); @@ -200,7 +225,7 @@ runInEachFileSystem(() => { const componentEntry = docs[0] as DirectiveEntry; expect(componentEntry.members.length).toBe(4); - const [nameEntry, firstNameEntry, savedEntry, resetEntry, ] = componentEntry.members; + const [nameEntry, firstNameEntry, savedEntry, resetEntry] = componentEntry.members; expect(nameEntry.memberTags).toEqual([MemberTags.Input]); expect((nameEntry as PropertyEntry).inputAlias).toBe('name'); @@ -221,7 +246,9 @@ runInEachFileSystem(() => { it('should extract getters and setters as inputs', () => { // Test getter-only, a getter + setter, and setter-only. - env.write('index.ts', ` + env.write( + 'index.ts', + ` import {Component, EventEmitter, Input, Output} from '@angular/core'; @Component({ standalone: true, @@ -240,7 +267,8 @@ runInEachFileSystem(() => { @Input() set isAdmin(value: boolean) { } } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); const classEntry = docs[0] as ClassEntry; @@ -248,7 +276,7 @@ runInEachFileSystem(() => { expect(classEntry.members.length).toBe(4); - const [userIdGetter, userNameGetter, userNameSetter, isAdminSetter, ] = classEntry.members; + const [userIdGetter, userNameGetter, userNameSetter, isAdminSetter] = classEntry.members; expect(userIdGetter.name).toBe('userId'); expect(userIdGetter.memberTags).toContain(MemberTags.Input); diff --git a/packages/compiler-cli/test/ngtsc/doc_extraction/doc_extraction_filtering_spec.ts b/packages/compiler-cli/test/ngtsc/doc_extraction/doc_extraction_filtering_spec.ts index cdfb3d8809304..ca77ff73f8335 100644 --- a/packages/compiler-cli/test/ngtsc/doc_extraction/doc_extraction_filtering_spec.ts +++ b/packages/compiler-cli/test/ngtsc/doc_extraction/doc_extraction_filtering_spec.ts @@ -24,7 +24,9 @@ runInEachFileSystem(() => { }); it('should not extract Angular-private symbols', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export class ɵUserProfile {} export class _SliderWidget {} export const ɵPI = 3.14; @@ -37,7 +39,8 @@ runInEachFileSystem(() => { export type _DifferentBoolean = boolean; export enum ɵToppings { Tomato, Onion } export enum _Sauces { Buffalo, Garlic } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(0); diff --git a/packages/compiler-cli/test/ngtsc/doc_extraction/docs_private_spec.ts b/packages/compiler-cli/test/ngtsc/doc_extraction/docs_private_spec.ts index e79596180bd11..99b7a5893a874 100644 --- a/packages/compiler-cli/test/ngtsc/doc_extraction/docs_private_spec.ts +++ b/packages/compiler-cli/test/ngtsc/doc_extraction/docs_private_spec.ts @@ -26,38 +26,48 @@ runInEachFileSystem(() => { } it('should omit constant annotated with `@docsPrivate`', () => { - expect(test(` + expect( + test(` /** @docsPrivate */ export const bla = true; - `)).toEqual([]); + `), + ).toEqual([]); }); it('should omit class annotated with `@docsPrivate`', () => { - expect(test(` + expect( + test(` /** @docsPrivate */ export class Bla {} - `)).toEqual([]); + `), + ).toEqual([]); }); it('should omit function annotated with `@docsPrivate`', () => { - expect(test(` + expect( + test(` /** @docsPrivate */ export function bla() {}; - `)).toEqual([]); + `), + ).toEqual([]); }); it('should omit interface annotated with `@docsPrivate`', () => { - expect(test(` + expect( + test(` /** @docsPrivate */ export interface BlaFunction {} - `)).toEqual([]); + `), + ).toEqual([]); }); it('should error if marked as private without reasoning', () => { - expect(() => test(` + expect(() => + test(` /** @docsPrivate */ export interface BlaFunction {} - `)).toThrowError(/Entry "BlaFunction" is marked as "@docsPrivate" but without reasoning./); + `), + ).toThrowError(/Entry "BlaFunction" is marked as "@docsPrivate" but without reasoning./); }); }); }); diff --git a/packages/compiler-cli/test/ngtsc/doc_extraction/enum_doc_extraction_spec.ts b/packages/compiler-cli/test/ngtsc/doc_extraction/enum_doc_extraction_spec.ts index f00ffc7323985..b4f4745b9fd4c 100644 --- a/packages/compiler-cli/test/ngtsc/doc_extraction/enum_doc_extraction_spec.ts +++ b/packages/compiler-cli/test/ngtsc/doc_extraction/enum_doc_extraction_spec.ts @@ -25,7 +25,9 @@ runInEachFileSystem(() => { }); it('should extract enum info without explicit values', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export enum PizzaTopping { /** It is cheese */ Cheese, @@ -33,7 +35,8 @@ runInEachFileSystem(() => { /** Or "tomato" if you are British */ Tomato, } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); @@ -56,7 +59,9 @@ runInEachFileSystem(() => { }); it('should extract enum info with explicit values', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export enum PizzaTopping { /** It is cheese */ Cheese = 0, @@ -64,7 +69,8 @@ runInEachFileSystem(() => { /** Or "tomato" if you are British */ Tomato = "tomato", } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); diff --git a/packages/compiler-cli/test/ngtsc/doc_extraction/function_doc_extraction_spec.ts b/packages/compiler-cli/test/ngtsc/doc_extraction/function_doc_extraction_spec.ts index 1a9f3fb26f420..8f5a737d4a7c8 100644 --- a/packages/compiler-cli/test/ngtsc/doc_extraction/function_doc_extraction_spec.ts +++ b/packages/compiler-cli/test/ngtsc/doc_extraction/function_doc_extraction_spec.ts @@ -25,9 +25,12 @@ runInEachFileSystem(() => { }); it('should extract functions', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export function getInjector() { } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); @@ -40,11 +43,14 @@ runInEachFileSystem(() => { }); it('should extract function with parameters', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export function go(num: string, intl = 1, area?: string): boolean { return false; } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); @@ -70,11 +76,14 @@ runInEachFileSystem(() => { }); it('should extract a function with a rest parameter', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export function getNames(prefix: string, ...ids: string[]): string[] { return []; } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); const functionEntry = docs[0] as FunctionEntry; @@ -90,13 +99,16 @@ runInEachFileSystem(() => { }); it('should extract overloaded functions', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export function ident(value: boolean): boolean export function ident(value: number): number export function ident(value: number|boolean): number|boolean { return value; } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(2); @@ -115,9 +127,12 @@ runInEachFileSystem(() => { }); it('should extract function generics', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export function save(data: T) { } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); diff --git a/packages/compiler-cli/test/ngtsc/doc_extraction/initializer_api_extraction_spec.ts b/packages/compiler-cli/test/ngtsc/doc_extraction/initializer_api_extraction_spec.ts index beb655e8497ab..22f25eeba7f36 100644 --- a/packages/compiler-cli/test/ngtsc/doc_extraction/initializer_api_extraction_spec.ts +++ b/packages/compiler-cli/test/ngtsc/doc_extraction/initializer_api_extraction_spec.ts @@ -6,7 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {EntryType, FunctionEntry, InitializerApiFunctionEntry, ParameterEntry} from '@angular/compiler-cli/src/ngtsc/docs/src/entities'; +import { + EntryType, + FunctionEntry, + InitializerApiFunctionEntry, + ParameterEntry, +} from '@angular/compiler-cli/src/ngtsc/docs/src/entities'; import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; import {loadStandardTestFiles} from '@angular/compiler-cli/src/ngtsc/testing'; @@ -68,12 +73,13 @@ runInEachFileSystem(() => { env.tsconfig(); }); - function test(input: string): InitializerApiFunctionEntry|undefined { + function test(input: string): InitializerApiFunctionEntry | undefined { env.write('index.ts', input); - return env.driveDocsExtraction('index.ts') - .find( - (f): f is InitializerApiFunctionEntry => - f.entryType === EntryType.InitializerApiFunction); + return env + .driveDocsExtraction('index.ts') + .find( + (f): f is InitializerApiFunctionEntry => f.entryType === EntryType.InitializerApiFunction, + ); } describe('interface-based', () => { @@ -82,13 +88,15 @@ runInEachFileSystem(() => { }); it('should extract container description', () => { - expect(test(inputFixture)?.description.replace(/\n/g, ' ')) - .toBe('This describes the overall initializer API function.'); + expect(test(inputFixture)?.description.replace(/\n/g, ' ')).toBe( + 'This describes the overall initializer API function.', + ); }); it('should extract container tags', () => { - expect(test(inputFixture)?.jsdocTags).toEqual([jasmine.objectContaining( - {name: 'initializerApiFunction'})]); + expect(test(inputFixture)?.jsdocTags).toEqual([ + jasmine.objectContaining({name: 'initializerApiFunction'}), + ]); }); it('should extract top-level call signatures', () => { @@ -110,27 +118,31 @@ runInEachFileSystem(() => { }); it('should extract sub-property call signatures', () => { - expect(test(inputFixture)?.subFunctions).toEqual([{ - name: 'required', - implementation: null, - signatures: [ - jasmine.objectContaining({ - generics: [{name: 'T', constraint: undefined, default: undefined}], - returnType: 'void', - }), - jasmine.objectContaining({ - generics: [ - {name: 'T', constraint: undefined, default: undefined}, - {name: 'TransformT', constraint: undefined, default: undefined}, - ], - params: [ - jasmine.objectContaining( - {name: 'transformFn', type: '(v: TransformT) => T'}), - ], - returnType: 'void', - }), - ], - }]); + expect(test(inputFixture)?.subFunctions).toEqual([ + { + name: 'required', + implementation: null, + signatures: [ + jasmine.objectContaining({ + generics: [{name: 'T', constraint: undefined, default: undefined}], + returnType: 'void', + }), + jasmine.objectContaining({ + generics: [ + {name: 'T', constraint: undefined, default: undefined}, + {name: 'TransformT', constraint: undefined, default: undefined}, + ], + params: [ + jasmine.objectContaining({ + name: 'transformFn', + type: '(v: TransformT) => T', + }), + ], + returnType: 'void', + }), + ], + }, + ]); }); }); @@ -140,13 +152,15 @@ runInEachFileSystem(() => { }); it('should extract container description', () => { - expect(test(contentChildrenFixture)?.description.replace(/\n/g, ' ')) - .toBe('Overall description of "contentChildren" API.'); + expect(test(contentChildrenFixture)?.description.replace(/\n/g, ' ')).toBe( + 'Overall description of "contentChildren" API.', + ); }); it('should extract container tags', () => { - expect(test(contentChildrenFixture)?.jsdocTags).toEqual([jasmine.objectContaining( - {name: 'initializerApiFunction'})]); + expect(test(contentChildrenFixture)?.jsdocTags).toEqual([ + jasmine.objectContaining({name: 'initializerApiFunction'}), + ]); }); it('should extract top-level call signatures', () => { @@ -161,8 +175,11 @@ runInEachFileSystem(() => { generics: [{name: 'LocatorT', constraint: undefined, default: undefined}], params: [ jasmine.objectContaining({name: 'locator', type: 'LocatorT'}), - jasmine.objectContaining( - {name: 'opts', isOptional: true, type: 'Options | undefined'}), + jasmine.objectContaining({ + name: 'opts', + isOptional: true, + type: 'Options | undefined', + }), ], returnType: 'Signal', }), @@ -173,8 +190,11 @@ runInEachFileSystem(() => { ], params: [ jasmine.objectContaining({name: 'locator', type: 'LocatorT'}), - jasmine.objectContaining( - {name: 'opts', isOptional: false, type: 'Options'}), + jasmine.objectContaining({ + name: 'opts', + isOptional: false, + type: 'Options', + }), ], returnType: 'Signal', }), diff --git a/packages/compiler-cli/test/ngtsc/doc_extraction/interface_doc_extraction_spec.ts b/packages/compiler-cli/test/ngtsc/doc_extraction/interface_doc_extraction_spec.ts index 56008cfa8b9fb..a10e459926a6e 100644 --- a/packages/compiler-cli/test/ngtsc/doc_extraction/interface_doc_extraction_spec.ts +++ b/packages/compiler-cli/test/ngtsc/doc_extraction/interface_doc_extraction_spec.ts @@ -7,7 +7,15 @@ */ import {DocEntry} from '@angular/compiler-cli/src/ngtsc/docs'; -import {ClassEntry, EntryType, InterfaceEntry, MemberTags, MemberType, MethodEntry, PropertyEntry} from '@angular/compiler-cli/src/ngtsc/docs/src/entities'; +import { + ClassEntry, + EntryType, + InterfaceEntry, + MemberTags, + MemberType, + MethodEntry, + PropertyEntry, +} from '@angular/compiler-cli/src/ngtsc/docs/src/entities'; import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; import {loadStandardTestFiles} from '@angular/compiler-cli/src/ngtsc/testing'; @@ -25,11 +33,14 @@ runInEachFileSystem(() => { }); it('should extract interfaces', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export interface UserProfile {} export interface CustomSlider {} - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(2); @@ -40,12 +51,15 @@ runInEachFileSystem(() => { }); it('should extract interface members', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export interface UserProfile { firstName(): string; age: number; } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); const interfaceEntry = docs[0] as InterfaceEntry; @@ -63,11 +77,14 @@ runInEachFileSystem(() => { }); it('should extract a method with a rest parameter', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export interface UserProfile { getNames(prefix: string, ...ids: string[]): string[]; } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); const interfaceEntry = docs[0] as InterfaceEntry; @@ -84,11 +101,14 @@ runInEachFileSystem(() => { }); it('should extract interface method params', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export interface UserProfile { setPhone(num: string, area?: string): void; } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); @@ -111,13 +131,16 @@ runInEachFileSystem(() => { }); it('should not extract private interface members', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export interface UserProfile { private ssn: string; private getSsn(): string; private static printSsn(): void; } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); @@ -127,7 +150,9 @@ runInEachFileSystem(() => { it('should extract member tags', () => { // Test both properties and methods with zero, one, and multiple tags. - env.write('index.ts', ` + env.write( + 'index.ts', + ` export interface UserProfile { eyeColor: string; protected name: string; @@ -142,7 +167,8 @@ runInEachFileSystem(() => { static getCountry(): string; protected getBirthday?(): string; } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); @@ -183,7 +209,9 @@ runInEachFileSystem(() => { it('should extract getters and setters', () => { // Test getter-only, a getter + setter, and setter-only. - env.write('index.ts', ` + env.write( + 'index.ts', + ` export interface UserProfile { get userId(): number; @@ -192,7 +220,8 @@ runInEachFileSystem(() => { set isAdmin(value: boolean); } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); const interfaceEntry = docs[0] as InterfaceEntry; @@ -200,7 +229,7 @@ runInEachFileSystem(() => { expect(interfaceEntry.members.length).toBe(4); - const [userIdGetter, userNameGetter, userNameSetter, isAdminSetter, ] = interfaceEntry.members; + const [userIdGetter, userNameGetter, userNameSetter, isAdminSetter] = interfaceEntry.members; expect(userIdGetter.name).toBe('userId'); expect(userIdGetter.memberType).toBe(MemberType.Getter); @@ -213,7 +242,9 @@ runInEachFileSystem(() => { }); it('should extract inherited members', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` interface Ancestor { id: string; value: string|number; @@ -231,7 +262,8 @@ runInEachFileSystem(() => { save(value: number): number; save(value: string|number): string|number; - }`); + }`, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); @@ -240,7 +272,7 @@ runInEachFileSystem(() => { expect(interfaceEntry.members.length).toBe(6); const [ageEntry, valueEntry, numberSaveEntry, unionSaveEntry, nameEntry, idEntry] = - interfaceEntry.members; + interfaceEntry.members; expect(ageEntry.name).toBe('age'); expect(ageEntry.memberType).toBe(MemberType.Property); @@ -274,7 +306,9 @@ runInEachFileSystem(() => { }); it('should extract inherited getters/setters', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` interface Ancestor { get name(): string; set name(v: string); @@ -292,7 +326,8 @@ runInEachFileSystem(() => { export interface Child extends Parent { get id(): string; - }`); + }`, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); @@ -301,7 +336,7 @@ runInEachFileSystem(() => { expect(interfaceEntry.members.length).toBe(4); const [idEntry, nameEntry, ageGetterEntry, ageSetterEntry] = - interfaceEntry.members as PropertyEntry[]; + interfaceEntry.members as PropertyEntry[]; // When the child interface overrides an accessor pair with another accessor, it overrides // *both* the getter and the setter, resulting (in this case) in just a getter. diff --git a/packages/compiler-cli/test/ngtsc/doc_extraction/jsdoc_extraction_spec.ts b/packages/compiler-cli/test/ngtsc/doc_extraction/jsdoc_extraction_spec.ts index 6df4150a0e63a..162cb3ae1edb4 100644 --- a/packages/compiler-cli/test/ngtsc/doc_extraction/jsdoc_extraction_spec.ts +++ b/packages/compiler-cli/test/ngtsc/doc_extraction/jsdoc_extraction_spec.ts @@ -7,7 +7,11 @@ */ import {DocEntry} from '@angular/compiler-cli/src/ngtsc/docs'; -import {ClassEntry, FunctionEntry, MethodEntry} from '@angular/compiler-cli/src/ngtsc/docs/src/entities'; +import { + ClassEntry, + FunctionEntry, + MethodEntry, +} from '@angular/compiler-cli/src/ngtsc/docs/src/entities'; import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; import {loadStandardTestFiles} from '@angular/compiler-cli/src/ngtsc/testing'; @@ -25,7 +29,9 @@ runInEachFileSystem(() => { }); it('should extract jsdoc from all types of top-level statement', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` /** This is a constant. */ export const PI = 3.14; @@ -34,7 +40,8 @@ runInEachFileSystem(() => { /** This is a function. */ export function save() { } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(3); @@ -46,7 +53,9 @@ runInEachFileSystem(() => { }); it('should extract raw comment blocks', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` /** This is a constant. */ export const PI = 3.14; @@ -64,33 +73,41 @@ runInEachFileSystem(() => { * @experimental here is another one */ export function save() { } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(3); const [piEntry, userProfileEntry, saveEntry] = docs; expect(piEntry.rawComment).toBe('/** This is a constant. */'); - expect(userProfileEntry.rawComment).toBe(` + expect(userProfileEntry.rawComment).toBe( + ` /** * Long comment * with multiple lines. - */`.trim()); - expect(saveEntry.rawComment).toBe(` + */`.trim(), + ); + expect(saveEntry.rawComment).toBe( + ` /** * This is a long JsDoc block * that extends multiple lines. * * @deprecated in includes multiple tags. * @experimental here is another one - */`.trim()); + */`.trim(), + ); }); it('should extract a description from a single-line jsdoc', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` /** Framework version. */ export const VERSION = '16'; - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); @@ -100,30 +117,37 @@ runInEachFileSystem(() => { }); it('should extract a description from a multi-line jsdoc', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` /** * This is a really long description that needs * to wrap over multiple lines. */ export const LONG_VERSION = '16.0.0'; - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); - expect(docs[0].description) - .toBe('This is a really long description that needs\nto wrap over multiple lines.'); + expect(docs[0].description).toBe( + 'This is a really long description that needs\nto wrap over multiple lines.', + ); expect(docs[0].jsdocTags.length).toBe(0); }); it('should extract jsdoc with an empty tag', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` /** * Unsupported version. * @deprecated */ export const OLD_VERSION = '1.0.0'; - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); @@ -134,13 +158,16 @@ runInEachFileSystem(() => { }); it('should extract jsdoc with a single-line tag', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` /** * Unsupported version. * @deprecated Use the newer one. */ export const OLD_VERSION = '1.0.0'; - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); @@ -151,7 +178,9 @@ runInEachFileSystem(() => { }); it('should extract jsdoc with a multi-line tags', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` /** * Unsupported version. * @deprecated Use the newer one. @@ -160,7 +189,8 @@ runInEachFileSystem(() => { * long comment that wraps. */ export const OLD_VERSION = '1.0.0'; - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); @@ -180,14 +210,17 @@ runInEachFileSystem(() => { }); it('should extract jsdoc with custom tags', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` /** * Unsupported version. * @ancient Use the newer one. * Or use something else. */ export const OLD_VERSION = '1.0.0'; - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); @@ -203,7 +236,9 @@ runInEachFileSystem(() => { it('should extract a @see jsdoc tag', () => { // "@see" has special behavior with links, so we have tests // specifically for this tag. - env.write('index.ts', ` + env.write( + 'index.ts', + ` import {Component} from '@angular/core'; /** @@ -211,7 +246,8 @@ runInEachFileSystem(() => { * @see {@link Component} */ export const NEW_VERSION = '99.0.0'; - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); @@ -228,7 +264,9 @@ runInEachFileSystem(() => { }); it('should extract function parameter descriptions', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` /** * Save some data. * @param data The data to save. @@ -236,7 +274,8 @@ runInEachFileSystem(() => { * with multiple lines. */ export function save(data: object, timing: number): void { } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); @@ -250,7 +289,9 @@ runInEachFileSystem(() => { }); it('should extract class member descriptions', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export class UserProfile { /** A user identifier. */ userId: number = 0; @@ -268,14 +309,15 @@ runInEachFileSystem(() => { */ save(config: object): boolean { return false; } } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); const classEntry = docs[0] as ClassEntry; expect(classEntry.members.length).toBe(4); - const [userIdEntry, nameGetterEntry, nameSetterEntry, ] = classEntry.members; + const [userIdEntry, nameGetterEntry, nameSetterEntry] = classEntry.members; expect(userIdEntry.description).toBe('A user identifier.'); expect(nameGetterEntry.description).toBe('Name of the user'); @@ -290,14 +332,17 @@ runInEachFileSystem(() => { }); it('should escape decorator names', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` /** * Save some data. * @Component decorators are cool. * @deprecated for some reason */ export type s = string; - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); diff --git a/packages/compiler-cli/test/ngtsc/doc_extraction/ng_module_doc_extraction_spec.ts b/packages/compiler-cli/test/ngtsc/doc_extraction/ng_module_doc_extraction_spec.ts index 52e55fa2f914c..2901428351e8b 100644 --- a/packages/compiler-cli/test/ngtsc/doc_extraction/ng_module_doc_extraction_spec.ts +++ b/packages/compiler-cli/test/ngtsc/doc_extraction/ng_module_doc_extraction_spec.ts @@ -25,7 +25,9 @@ runInEachFileSystem(() => { }); it('should extract NgModule info', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` import {Directive, NgModule} from '@angular/core'; @Directive({selector: 'some-tag'}) @@ -33,7 +35,8 @@ runInEachFileSystem(() => { @NgModule({declarations: [SomeDirective]}) export class SomeNgModule { } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); diff --git a/packages/compiler-cli/test/ngtsc/doc_extraction/pipe_doc_extraction_spec.ts b/packages/compiler-cli/test/ngtsc/doc_extraction/pipe_doc_extraction_spec.ts index 7394ab75bf338..a94a812258487 100644 --- a/packages/compiler-cli/test/ngtsc/doc_extraction/pipe_doc_extraction_spec.ts +++ b/packages/compiler-cli/test/ngtsc/doc_extraction/pipe_doc_extraction_spec.ts @@ -25,7 +25,9 @@ runInEachFileSystem(() => { }); it('should extract standalone pipe info', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` import {Pipe} from '@angular/core'; @Pipe({ standalone: true, @@ -34,7 +36,8 @@ runInEachFileSystem(() => { export class ShortenPipe { transform(value: string): string { return ''; } } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); @@ -48,7 +51,9 @@ runInEachFileSystem(() => { }); it('should extract NgModule pipe info', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` import {Pipe, NgModule} from '@angular/core'; @Pipe({name: 'shorten'}) export class ShortenPipe { @@ -57,7 +62,8 @@ runInEachFileSystem(() => { @NgModule({declarations: [ShortenPipe]}) export class PipeModule { } - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); diff --git a/packages/compiler-cli/test/ngtsc/doc_extraction/reexport_docs_extraction_spec.ts b/packages/compiler-cli/test/ngtsc/doc_extraction/reexport_docs_extraction_spec.ts index 832b8f851a3bc..62cc37a2d4bb9 100644 --- a/packages/compiler-cli/test/ngtsc/doc_extraction/reexport_docs_extraction_spec.ts +++ b/packages/compiler-cli/test/ngtsc/doc_extraction/reexport_docs_extraction_spec.ts @@ -25,14 +25,20 @@ runInEachFileSystem(() => { }); it('should extract info from a named re-export', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export {PI} from './implementation'; - `); + `, + ); - env.write('implementation.ts', ` + env.write( + 'implementation.ts', + ` export const PI = 3.14; export const TAO = 6.28; - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); @@ -42,14 +48,20 @@ runInEachFileSystem(() => { }); it('should extract info from an aggregate re-export', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export * from './implementation'; - `); + `, + ); - env.write('implementation.ts', ` + env.write( + 'implementation.ts', + ` export const PI = 3.14; export const TAO = 6.28; - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); @@ -63,17 +75,26 @@ runInEachFileSystem(() => { }); it('should extract info from a transitive re-export', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export * from './middle'; - `); + `, + ); - env.write('middle.ts', ` + env.write( + 'middle.ts', + ` export * from 'implementation'; - `); + `, + ); - env.write('implementation.ts', ` + env.write( + 'implementation.ts', + ` export const PI = 3.14; - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); @@ -83,15 +104,21 @@ runInEachFileSystem(() => { }); it('should extract info from an aliased re-export', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export * from './implementation'; - `); + `, + ); - env.write('implementation.ts', ` + env.write( + 'implementation.ts', + ` const PI = 3.14; export {PI as PI_CONSTANT}; - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); diff --git a/packages/compiler-cli/test/ngtsc/doc_extraction/type_alias_doc_extraction_spec.ts b/packages/compiler-cli/test/ngtsc/doc_extraction/type_alias_doc_extraction_spec.ts index 30803146cc3a5..35578fa438e99 100644 --- a/packages/compiler-cli/test/ngtsc/doc_extraction/type_alias_doc_extraction_spec.ts +++ b/packages/compiler-cli/test/ngtsc/doc_extraction/type_alias_doc_extraction_spec.ts @@ -25,9 +25,12 @@ runInEachFileSystem(() => { }); it('should extract type aliases based on primitives', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export type SuperNumber = number | string; - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); @@ -39,12 +42,15 @@ runInEachFileSystem(() => { }); it('should extract type aliases for objects', () => { - env.write('index.ts', ` + env.write( + 'index.ts', + ` export type UserProfile = { name: string; age: number; }; - `); + `, + ); const docs: DocEntry[] = env.driveDocsExtraction('index.ts'); expect(docs.length).toBe(1); diff --git a/packages/compiler-cli/test/ngtsc/env.ts b/packages/compiler-cli/test/ngtsc/env.ts index 567e83821de23..d6f6c02ac278f 100644 --- a/packages/compiler-cli/test/ngtsc/env.ts +++ b/packages/compiler-cli/test/ngtsc/env.ts @@ -14,7 +14,13 @@ import ts from 'typescript'; import {createCompilerHost, createProgram} from '../../index'; import {mainXi18n} from '../../src/extract_i18n'; import {main, mainDiagnosticsForTest, readNgcCommandLineAndConfiguration} from '../../src/main'; -import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem, relativeFrom} from '../../src/ngtsc/file_system'; +import { + absoluteFrom, + AbsoluteFsPath, + FileSystem, + getFileSystem, + relativeFrom, +} from '../../src/ngtsc/file_system'; import {Folder, MockFileSystem} from '../../src/ngtsc/file_system/testing'; import {IndexedComponent} from '../../src/ngtsc/indexer'; import {NgtscProgram} from '../../src/ngtsc/program'; @@ -24,7 +30,12 @@ import {TemplateTypeChecker} from '../../src/ngtsc/typecheck/api'; import {setWrapHostForTest} from '../../src/transformers/compiler_host'; type TsConfigOptionsValue = - string|boolean|number|null|TsConfigOptionsValue[]|{[key: string]: TsConfigOptionsValue}; + | string + | boolean + | number + | null + | TsConfigOptionsValue[] + | {[key: string]: TsConfigOptionsValue}; export type TsConfigOptions = { [key: string]: TsConfigOptionsValue; }; @@ -34,13 +45,16 @@ export type TsConfigOptions = { * TypeScript code. */ export class NgtscTestEnvironment { - private multiCompileHostExt: MultiCompileHostExt|null = null; - private oldProgram: Program|null = null; - private changedResources: Set|null = null; + private multiCompileHostExt: MultiCompileHostExt | null = null; + private oldProgram: Program | null = null; + private changedResources: Set | null = null; private commandLineArgs = ['-p', this.basePath]; private constructor( - private fs: FileSystem, readonly outDir: AbsoluteFsPath, readonly basePath: AbsoluteFsPath) {} + private fs: FileSystem, + readonly outDir: AbsoluteFsPath, + readonly basePath: AbsoluteFsPath, + ) {} /** * Set up a new testing environment. @@ -58,7 +72,9 @@ export class NgtscTestEnvironment { const env = new NgtscTestEnvironment(fs, fs.resolve('/built'), workingDir); fs.chdir(workingDir); - env.write(absoluteFrom('/tsconfig-base.json'), `{ + env.write( + absoluteFrom('/tsconfig-base.json'), + `{ "compilerOptions": { "emitDecoratorMetadata": false, "experimentalDecorators": true, @@ -81,7 +97,8 @@ export class NgtscTestEnvironment { "exclude": [ "built" ] - }`); + }`, + ); return env; } @@ -158,7 +175,7 @@ export class NgtscTestEnvironment { throw new Error(`Not tracking written files - call enableMultipleCompilations()`); } const writtenFiles = new Set(); - this.multiCompileHostExt.getFilesWrittenSinceLastFlush().forEach(rawFile => { + this.multiCompileHostExt.getFilesWrittenSinceLastFlush().forEach((rawFile) => { if (rawFile.startsWith(this.outDir)) { writtenFiles.add(rawFile.slice(this.outDir.length)); } @@ -204,8 +221,10 @@ export class NgtscTestEnvironment { } this.write('tsconfig.json', JSON.stringify(tsconfig, null, 2)); - if (extraOpts['_useHostForImportGeneration'] || - extraOpts['_useHostForImportAndAliasGeneration']) { + if ( + extraOpts['_useHostForImportGeneration'] || + extraOpts['_useHostForImportAndAliasGeneration'] + ) { setWrapHostForTest(makeWrapHost(new FileNameToModuleNameHost(this.fs))); } } @@ -215,15 +234,20 @@ export class NgtscTestEnvironment { */ driveMain(customTransformers?: CustomTransformers): void { const errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error); - let reuseProgram: {program: Program|undefined}|undefined = undefined; + let reuseProgram: {program: Program | undefined} | undefined = undefined; if (this.multiCompileHostExt !== null) { reuseProgram = { program: this.oldProgram || undefined, }; } const exitCode = main( - this.commandLineArgs, errorSpy, undefined, customTransformers, reuseProgram, - this.changedResources); + this.commandLineArgs, + errorSpy, + undefined, + customTransformers, + reuseProgram, + this.changedResources, + ); expect(errorSpy).not.toHaveBeenCalled(); expect(exitCode).toBe(0); if (this.multiCompileHostExt !== null) { @@ -236,7 +260,7 @@ export class NgtscTestEnvironment { */ driveDiagnostics(expectedExitCode?: number): ReadonlyArray { // ngtsc only produces ts.Diagnostic messages. - let reuseProgram: {program: Program|undefined}|undefined = undefined; + let reuseProgram: {program: Program | undefined} | undefined = undefined; if (this.multiCompileHostExt !== null) { reuseProgram = { program: this.oldProgram || undefined, @@ -244,12 +268,17 @@ export class NgtscTestEnvironment { } const {exitCode, diagnostics} = mainDiagnosticsForTest( - this.commandLineArgs, undefined, reuseProgram, this.changedResources); + this.commandLineArgs, + undefined, + reuseProgram, + this.changedResources, + ); if (expectedExitCode !== undefined) { expect(exitCode) - .withContext(`Expected program to exit with code ${ - expectedExitCode}, but it actually exited with code ${exitCode}.`) - .toBe(expectedExitCode); + .withContext( + `Expected program to exit with code ${expectedExitCode}, but it actually exited with code ${exitCode}.`, + ) + .toBe(expectedExitCode); } if (this.multiCompileHostExt !== null) { @@ -270,7 +299,7 @@ export class NgtscTestEnvironment { return defaultGatherDiagnostics(program as api.Program) as ts.Diagnostic[]; } - driveTemplateTypeChecker(): {program: ts.Program, checker: TemplateTypeChecker} { + driveTemplateTypeChecker(): {program: ts.Program; checker: TemplateTypeChecker} { const {rootNames, options} = readNgcCommandLineAndConfiguration(this.commandLineArgs); const host = createCompilerHost({options}); const program = createProgram({rootNames, host, options}); @@ -295,13 +324,9 @@ export class NgtscTestEnvironment { return (program as NgtscProgram).getApiDocumentation(entryPoint); } - driveXi18n(format: string, outputFileName: string, locale: string|null = null): void { + driveXi18n(format: string, outputFileName: string, locale: string | null = null): void { const errorSpy = jasmine.createSpy('consoleError').and.callFake(console.error); - const args = [ - ...this.commandLineArgs, - `--i18nFormat=${format}`, - `--outFile=${outputFileName}`, - ]; + const args = [...this.commandLineArgs, `--i18nFormat=${format}`, `--outFile=${outputFileName}`]; if (locale !== null) { args.push(`--locale=${locale}`); } @@ -319,26 +344,34 @@ const ROOT_PREFIX = 'root/'; class FileNameToModuleNameHost extends AugmentedCompilerHost { fileNameToModuleName(importedFilePath: string): string { - const relativeFilePath = - relativeFrom(this.fs.relative(this.fs.pwd(), this.fs.resolve(importedFilePath))); + const relativeFilePath = relativeFrom( + this.fs.relative(this.fs.pwd(), this.fs.resolve(importedFilePath)), + ); const rootedPath = this.fs.join('root', relativeFilePath); return rootedPath.replace(/(\.d)?.ts$/, ''); } resolveModuleNames( - moduleNames: string[], containingFile: string, reusedNames: string[]|undefined, - redirectedReference: ts.ResolvedProjectReference|undefined, - options: ts.CompilerOptions): (ts.ResolvedModule|undefined)[] { - return moduleNames.map(moduleName => { + moduleNames: string[], + containingFile: string, + reusedNames: string[] | undefined, + redirectedReference: ts.ResolvedProjectReference | undefined, + options: ts.CompilerOptions, + ): (ts.ResolvedModule | undefined)[] { + return moduleNames.map((moduleName) => { if (moduleName.startsWith(ROOT_PREFIX)) { // Strip the artificially added root prefix. moduleName = '/' + moduleName.slice(ROOT_PREFIX.length); } - return ts - .resolveModuleName( - moduleName, containingFile, options, this, /* cache */ undefined, redirectedReference) - .resolvedModule; + return ts.resolveModuleName( + moduleName, + containingFile, + options, + this, + /* cache */ undefined, + redirectedReference, + ).resolvedModule; }); } } @@ -348,8 +381,11 @@ class MultiCompileHostExt extends AugmentedCompilerHost implements Partial(); override getSourceFile( - fileName: string, languageVersion: ts.ScriptTarget, onError?: (message: string) => void, - shouldCreateNewSourceFile?: boolean): ts.SourceFile|undefined { + fileName: string, + languageVersion: ts.ScriptTarget, + onError?: (message: string) => void, + shouldCreateNewSourceFile?: boolean, + ): ts.SourceFile | undefined { if (this.cache.has(fileName)) { return this.cache.get(fileName)!; } @@ -365,9 +401,12 @@ class MultiCompileHostExt extends AugmentedCompilerHost implements Partial void)|undefined, - sourceFiles?: ReadonlyArray): void { + fileName: string, + data: string, + writeByteOrderMark: boolean, + onError: ((message: string) => void) | undefined, + sourceFiles?: ReadonlyArray, + ): void { super.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles); this.writtenFiles.add(fileName); } @@ -382,7 +421,7 @@ class MultiCompileHostExt extends AugmentedCompilerHost implements Partial|string { + readResource(fileName: string): Promise | string { const resource = this.readFile(fileName); if (resource === undefined) { throw new Error(`Resource ${fileName} not found`); @@ -400,7 +439,7 @@ function makeWrapHost(wrapped: AugmentedCompilerHost): (host: ts.CompilerHost) = return (wrapped as any)[name]!.bind(wrapped); } return (target as any)[name]; - } + }, }); }; } diff --git a/packages/compiler-cli/test/ngtsc/extended_template_diagnostics_spec.ts b/packages/compiler-cli/test/ngtsc/extended_template_diagnostics_spec.ts index fba84570d2a9c..58f3decfdca59 100644 --- a/packages/compiler-cli/test/ngtsc/extended_template_diagnostics_spec.ts +++ b/packages/compiler-cli/test/ngtsc/extended_template_diagnostics_spec.ts @@ -26,7 +26,9 @@ runInEachFileSystem(() => { }); it('should produce invalid banana in box warning', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ selector: 'test', @@ -35,7 +37,8 @@ runInEachFileSystem(() => { class TestCmp { bar: string = "text"; } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); @@ -45,7 +48,9 @@ runInEachFileSystem(() => { }); it('should produce invalid banana in box warning with external html file', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ selector: 'test', @@ -54,11 +59,15 @@ runInEachFileSystem(() => { class TestCmp { bar: string = "text"; } - `); + `, + ); - env.write('test.html', ` + env.write( + 'test.html', + `
- `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); @@ -68,7 +77,9 @@ runInEachFileSystem(() => { }); it(`should produce nullish coalescing not nullable warning`, () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ selector: 'test', @@ -77,7 +88,8 @@ runInEachFileSystem(() => { export class TestCmp { bar: string = "text"; } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); @@ -108,10 +120,12 @@ runInEachFileSystem(() => { const diagnostics = env.driveDiagnostics(0 /* expectedExitCode */); expect(diagnostics.length).toBe(1); - expect(diagnostics[0]).toEqual(jasmine.objectContaining({ - code: ngErrorCode(ErrorCode.INVALID_BANANA_IN_BOX), - category: ts.DiagnosticCategory.Warning, - })); + expect(diagnostics[0]).toEqual( + jasmine.objectContaining({ + code: ngErrorCode(ErrorCode.INVALID_BANANA_IN_BOX), + category: ts.DiagnosticCategory.Warning, + }), + ); }); it('by disabling extended template diagnostics when `strictTemplates` is disabled', () => { @@ -126,17 +140,19 @@ runInEachFileSystem(() => { it('by emitting unconfigured diagnostics as is', () => { env.tsconfig({ strictTemplates: true, - extendedDiagnostics: {}, // No configured diagnostics. + extendedDiagnostics: {}, // No configured diagnostics. }); env.write('test.ts', warningComponent); const diagnostics = env.driveDiagnostics(0 /* expectedExitCode */); expect(diagnostics.length).toBe(1); - expect(diagnostics[0]).toEqual(jasmine.objectContaining({ - code: ngErrorCode(ErrorCode.INVALID_BANANA_IN_BOX), - category: ts.DiagnosticCategory.Warning, - })); + expect(diagnostics[0]).toEqual( + jasmine.objectContaining({ + code: ngErrorCode(ErrorCode.INVALID_BANANA_IN_BOX), + category: ts.DiagnosticCategory.Warning, + }), + ); }); it('by emitting diagnostics with the default category', () => { @@ -151,10 +167,12 @@ runInEachFileSystem(() => { const diagnostics = env.driveDiagnostics(1 /* expectedExitCode */); expect(diagnostics.length).toBe(1); - expect(diagnostics[0]).toEqual(jasmine.objectContaining({ - code: ngErrorCode(ErrorCode.INVALID_BANANA_IN_BOX), - category: ts.DiagnosticCategory.Error, - })); + expect(diagnostics[0]).toEqual( + jasmine.objectContaining({ + code: ngErrorCode(ErrorCode.INVALID_BANANA_IN_BOX), + category: ts.DiagnosticCategory.Error, + }), + ); }); it('by emitting diagnostics configured as `warning`', () => { @@ -171,10 +189,12 @@ runInEachFileSystem(() => { const diagnostics = env.driveDiagnostics(0 /* expectedExitCode */); expect(diagnostics.length).toBe(1); - expect(diagnostics[0]).toEqual(jasmine.objectContaining({ - code: ngErrorCode(ErrorCode.INVALID_BANANA_IN_BOX), - category: ts.DiagnosticCategory.Warning, - })); + expect(diagnostics[0]).toEqual( + jasmine.objectContaining({ + code: ngErrorCode(ErrorCode.INVALID_BANANA_IN_BOX), + category: ts.DiagnosticCategory.Warning, + }), + ); }); it('by promoting diagnostics configured as `error`', () => { @@ -191,10 +211,12 @@ runInEachFileSystem(() => { const diagnostics = env.driveDiagnostics(1 /* expectedExitCode */); expect(diagnostics.length).toBe(1); - expect(diagnostics[0]).toEqual(jasmine.objectContaining({ - code: ngErrorCode(ErrorCode.INVALID_BANANA_IN_BOX), - category: ts.DiagnosticCategory.Error, - })); + expect(diagnostics[0]).toEqual( + jasmine.objectContaining({ + code: ngErrorCode(ErrorCode.INVALID_BANANA_IN_BOX), + category: ts.DiagnosticCategory.Error, + }), + ); }); it('by suppressing diagnostics configured as `suppress`', () => { @@ -225,10 +247,12 @@ runInEachFileSystem(() => { const diagnostics = env.driveDiagnostics(1 /* expectedExitCode */); expect(diagnostics.length).toBe(1); - expect(diagnostics[0]).toEqual(jasmine.objectContaining({ - code: ngErrorCode(ErrorCode.CONFIG_EXTENDED_DIAGNOSTICS_UNKNOWN_CATEGORY_LABEL), - category: ts.DiagnosticCategory.Error, - })); + expect(diagnostics[0]).toEqual( + jasmine.objectContaining({ + code: ngErrorCode(ErrorCode.CONFIG_EXTENDED_DIAGNOSTICS_UNKNOWN_CATEGORY_LABEL), + category: ts.DiagnosticCategory.Error, + }), + ); }); }); }); diff --git a/packages/compiler-cli/test/ngtsc/host_directives_spec.ts b/packages/compiler-cli/test/ngtsc/host_directives_spec.ts index 221e493199210..b820960b3f95a 100644 --- a/packages/compiler-cli/test/ngtsc/host_directives_spec.ts +++ b/packages/compiler-cli/test/ngtsc/host_directives_spec.ts @@ -29,7 +29,9 @@ runInEachFileSystem(() => { } it('should generate a basic hostDirectives definition', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Component} from '@angular/core'; @Directive({ @@ -50,7 +52,8 @@ runInEachFileSystem(() => { hostDirectives: [DirectiveA, DirectiveB] }) export class MyComp {} - `); + `, + ); env.driveMain(); @@ -59,17 +62,20 @@ runInEachFileSystem(() => { expect(jsContents).toContain('ɵɵdefineDirective({ type: DirectiveA'); expect(jsContents).toContain('ɵɵdefineDirective({ type: DirectiveB'); - expect(jsContents) - .toContain('features: [i0.ɵɵHostDirectivesFeature([DirectiveA, DirectiveB])]'); - expect(dtsContents) - .toContain( - 'ɵɵComponentDeclaration;'); + expect(jsContents).toContain( + 'features: [i0.ɵɵHostDirectivesFeature([DirectiveA, DirectiveB])]', + ); + expect(dtsContents).toContain( + 'ɵɵComponentDeclaration;', + ); }); it('should generate a hostDirectives definition that has inputs and outputs', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Component, Input, Output, EventEmitter} from '@angular/core'; @Directive({ @@ -93,7 +99,8 @@ runInEachFileSystem(() => { }], }) export class MyComp {} - `); + `, + ); env.driveMain(); @@ -101,21 +108,23 @@ runInEachFileSystem(() => { const dtsContents = env.getContents('test.d.ts'); expect(jsContents).toContain('ɵɵdefineDirective({ type: HostDir'); - expect(jsContents) - .toContain( - 'features: [i0.ɵɵHostDirectivesFeature([{ directive: HostDir, ' + - 'inputs: ["value", "value", "color", "colorAlias"], ' + - 'outputs: ["opened", "opened", "closed", "closedAlias"] }])]'); - expect(dtsContents) - .toContain( - 'ɵɵComponentDeclaration;'); + expect(jsContents).toContain( + 'features: [i0.ɵɵHostDirectivesFeature([{ directive: HostDir, ' + + 'inputs: ["value", "value", "color", "colorAlias"], ' + + 'outputs: ["opened", "opened", "closed", "closedAlias"] }])]', + ); + expect(dtsContents).toContain( + 'ɵɵComponentDeclaration;', + ); }); it('should generate a hostDirectives definition that has aliased inputs and outputs', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Component, Input, Output, EventEmitter} from '@angular/core'; @Directive({ @@ -139,7 +148,8 @@ runInEachFileSystem(() => { }], }) export class MyComp {} - `); + `, + ); env.driveMain(); @@ -147,21 +157,23 @@ runInEachFileSystem(() => { const dtsContents = env.getContents('test.d.ts'); expect(jsContents).toContain('ɵɵdefineDirective({ type: HostDir'); - expect(jsContents) - .toContain( - 'features: [i0.ɵɵHostDirectivesFeature([{ directive: HostDir, ' + - 'inputs: ["valueAlias", "valueAlias", "colorAlias", "customColorAlias"], ' + - 'outputs: ["openedAlias", "openedAlias", "closedAlias", "customClosedAlias"] }])]'); - expect(dtsContents) - .toContain( - 'ɵɵComponentDeclaration;'); + expect(jsContents).toContain( + 'features: [i0.ɵɵHostDirectivesFeature([{ directive: HostDir, ' + + 'inputs: ["valueAlias", "valueAlias", "colorAlias", "customColorAlias"], ' + + 'outputs: ["openedAlias", "openedAlias", "closedAlias", "customClosedAlias"] }])]', + ); + expect(dtsContents).toContain( + 'ɵɵComponentDeclaration;', + ); }); it('should generate hostDirectives definitions for a chain of host directives', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Component} from '@angular/core'; @Directive({standalone: true}) @@ -189,7 +201,8 @@ runInEachFileSystem(() => { }) export class MyComp { } - `); + `, + ); env.driveMain(); @@ -197,42 +210,43 @@ runInEachFileSystem(() => { const dtsContents = env.getContents('test.d.ts'); expect(jsContents).toContain('ɵɵdefineDirective({ type: DirectiveA, standalone: true });'); - expect(jsContents) - .toContain( - 'ɵɵdefineDirective({ type: DirectiveB, standalone: true, ' + - 'features: [i0.ɵɵHostDirectivesFeature([DirectiveA])] });'); - expect(jsContents) - .toContain( - 'ɵɵdefineDirective({ type: DirectiveC, standalone: true, ' + - 'features: [i0.ɵɵHostDirectivesFeature([DirectiveB])] });'); - expect(jsContents) - .toContain( - 'ɵɵdefineComponent({ type: MyComp, selectors: [["my-comp"]],' + - ' features: [i0.ɵɵHostDirectivesFeature([DirectiveC])]'); - - expect(dtsContents) - .toContain( - 'ɵɵDirectiveDeclaration;'); - expect(dtsContents) - .toContain( - 'ɵɵDirectiveDeclaration;'); - expect(dtsContents) - .toContain( - 'ɵɵDirectiveDeclaration;'); - expect(dtsContents) - .toContain( - 'ɵɵComponentDeclaration;'); + expect(jsContents).toContain( + 'ɵɵdefineDirective({ type: DirectiveB, standalone: true, ' + + 'features: [i0.ɵɵHostDirectivesFeature([DirectiveA])] });', + ); + expect(jsContents).toContain( + 'ɵɵdefineDirective({ type: DirectiveC, standalone: true, ' + + 'features: [i0.ɵɵHostDirectivesFeature([DirectiveB])] });', + ); + expect(jsContents).toContain( + 'ɵɵdefineComponent({ type: MyComp, selectors: [["my-comp"]],' + + ' features: [i0.ɵɵHostDirectivesFeature([DirectiveC])]', + ); + + expect(dtsContents).toContain( + 'ɵɵDirectiveDeclaration;', + ); + expect(dtsContents).toContain( + 'ɵɵDirectiveDeclaration;', + ); + expect(dtsContents).toContain( + 'ɵɵDirectiveDeclaration;', + ); + expect(dtsContents).toContain( + 'ɵɵComponentDeclaration;', + ); }); it('should generate a hostDirectives definition with forward references', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, forwardRef, Input} from '@angular/core'; @Component({ @@ -254,44 +268,47 @@ runInEachFileSystem(() => { export class DirectiveA { @Input() value: any; } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); const dtsContents = env.getContents('test.d.ts'); - expect(jsContents) - .toContain( - 'features: [i0.ɵɵHostDirectivesFeature(function () { return [DirectiveB]; })]'); - expect(jsContents) - .toContain( - 'features: [i0.ɵɵHostDirectivesFeature(function () { return [{ directive: DirectiveA, inputs: ["value", "value"] }]; })]'); - expect(jsContents) - .toContain( - 'ɵɵdefineDirective({ type: DirectiveA, ' + - 'inputs: { value: "value" }, standalone: true });'); - - expect(dtsContents) - .toContain( - 'ɵɵComponentDeclaration;'); - - expect(dtsContents) - .toContain( - 'ɵɵDirectiveDeclaration;'); - - expect(dtsContents) - .toContain( - 'ɵɵDirectiveDeclaration;'); + expect(jsContents).toContain( + 'features: [i0.ɵɵHostDirectivesFeature(function () { return [DirectiveB]; })]', + ); + expect(jsContents).toContain( + 'features: [i0.ɵɵHostDirectivesFeature(function () { return [{ directive: DirectiveA, inputs: ["value", "value"] }]; })]', + ); + expect(jsContents).toContain( + 'ɵɵdefineDirective({ type: DirectiveA, ' + + 'inputs: { value: "value" }, standalone: true });', + ); + + expect(dtsContents).toContain( + 'ɵɵComponentDeclaration;', + ); + + expect(dtsContents).toContain( + 'ɵɵDirectiveDeclaration;', + ); + + expect(dtsContents).toContain( + 'ɵɵDirectiveDeclaration;', + ); }); it('should generate a definition if the host directives are imported from other files', () => { - env.write('dir-a.ts', ` + env.write( + 'dir-a.ts', + ` import {Directive} from '@angular/core'; @Directive({ @@ -299,9 +316,12 @@ runInEachFileSystem(() => { standalone: true }) export class DirectiveA {} - `); + `, + ); - env.write('dir-b.ts', ` + env.write( + 'dir-b.ts', + ` import {Directive, Input, Output, EventEmitter} from '@angular/core'; @Directive({ @@ -312,9 +332,12 @@ runInEachFileSystem(() => { @Input() input: any; @Output() output = new EventEmitter(); } - `); + `, + ); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, forwardRef} from '@angular/core'; import {DirectiveA} from './dir-a'; import {DirectiveB} from './dir-b'; @@ -332,7 +355,8 @@ runInEachFileSystem(() => { ] }) export class MyComp {} - `); + `, + ); env.driveMain(); @@ -341,33 +365,38 @@ runInEachFileSystem(() => { expect(jsContents).toContain(`import { DirectiveA } from './dir-a'`); expect(jsContents).toContain(`import { DirectiveB } from './dir-b'`); - expect(jsContents) - .toContain( - 'features: [i0.ɵɵHostDirectivesFeature(function () { ' + - 'return [i1.DirectiveA, { directive: i2.DirectiveB, inputs: ["input", "inputAlias"], ' + - 'outputs: ["output", "outputAlias"] }]; })]'); + expect(jsContents).toContain( + 'features: [i0.ɵɵHostDirectivesFeature(function () { ' + + 'return [i1.DirectiveA, { directive: i2.DirectiveB, inputs: ["input", "inputAlias"], ' + + 'outputs: ["output", "outputAlias"] }]; })]', + ); expect(dtsContents).toContain('import * as i1 from "./dir-a";'); expect(dtsContents).toContain('import * as i2 from "./dir-b";'); - expect(dtsContents) - .toContain( - 'ɵɵComponentDeclaration;'); + expect(dtsContents).toContain( + 'ɵɵComponentDeclaration;', + ); }); it('should generate a hostDirectives definition referring to external directives', () => { - env.write('node_modules/external/index.d.ts', ` + env.write( + 'node_modules/external/index.d.ts', + ` import {ɵɵDirectiveDeclaration} from '@angular/core'; export declare class ExternalDir { static ɵdir: ɵɵDirectiveDeclaration; } - `); + `, + ); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, NgModule} from '@angular/core'; import {ExternalDir} from 'external'; @@ -376,39 +405,48 @@ runInEachFileSystem(() => { hostDirectives: [{directive: ExternalDir, inputs: ['input: inputAlias'], outputs: ['output: outputAlias']}] }) export class MyComp {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); const dtsContents = env.getContents('test.d.ts'); expect(jsContents).toContain(`import * as i1 from "external";`); - expect(jsContents) - .toContain( - 'features: [i0.ɵɵHostDirectivesFeature([{ directive: i1.ExternalDir, ' + - 'inputs: ["input", "inputAlias"], outputs: ["output", "outputAlias"] }])]'); + expect(jsContents).toContain( + 'features: [i0.ɵɵHostDirectivesFeature([{ directive: i1.ExternalDir, ' + + 'inputs: ["input", "inputAlias"], outputs: ["output", "outputAlias"] }])]', + ); expect(dtsContents).toContain('import * as i1 from "external";'); - expect(dtsContents) - .toContain( - 'ɵɵComponentDeclaration;'); + expect(dtsContents).toContain( + 'ɵɵComponentDeclaration;', + ); }); it('should reference host directives by their external name', () => { - env.write('node_modules/external/index.d.ts', ` + env.write( + 'node_modules/external/index.d.ts', + ` import {InternalDir} from './internal'; export {InternalDir as ExternalDir} from './internal'; - `); + `, + ); - env.write('node_modules/external/internal.d.ts', ` + env.write( + 'node_modules/external/internal.d.ts', + ` export declare class InternalDir { static ɵdir: ɵɵDirectiveDeclaration; } - `); + `, + ); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; import {ExternalDir} from 'external'; @@ -417,7 +455,8 @@ runInEachFileSystem(() => { hostDirectives: [ExternalDir] }) export class MyComp {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -427,15 +466,16 @@ runInEachFileSystem(() => { expect(jsContents).toContain('features: [i0.ɵɵHostDirectivesFeature([i1.ExternalDir])]'); expect(dtsContents).toContain('import * as i1 from "external";'); - expect(dtsContents) - .toContain( - 'ɵɵComponentDeclaration;'); + expect(dtsContents).toContain( + 'ɵɵComponentDeclaration;', + ); }); - it('should produce a template diagnostic if a required input from a host directive is missing', - () => { - env.write('test.ts', ` + it('should produce a template diagnostic if a required input from a host directive is missing', () => { + env.write( + 'test.ts', + ` import {Directive, Component, Input} from '@angular/core'; @Directive({standalone: true}) @@ -457,17 +497,20 @@ runInEachFileSystem(() => { imports: [Dir] }) class App {} - `); + `, + ); - const messages = env.driveDiagnostics().map(extractMessage); + const messages = env.driveDiagnostics().map(extractMessage); - expect(messages).toEqual( - [`Required input 'customAlias' from directive HostDir must be specified.`]); - }); + expect(messages).toEqual([ + `Required input 'customAlias' from directive HostDir must be specified.`, + ]); + }); - it('should not produce a template diagnostic if a required input from a host directive is bound', - () => { - env.write('test.ts', ` + it('should not produce a template diagnostic if a required input from a host directive is bound', () => { + env.write( + 'test.ts', + ` import {Directive, Component, Input} from '@angular/core'; @Directive({standalone: true}) @@ -491,15 +534,18 @@ runInEachFileSystem(() => { class App { value = 123; } - `); + `, + ); - const messages = env.driveDiagnostics().map(extractMessage); - expect(messages).toEqual([]); - }); + const messages = env.driveDiagnostics().map(extractMessage); + expect(messages).toEqual([]); + }); describe('validations', () => { it('should produce a diagnostic if a host directive is not standalone', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Component, NgModule} from '@angular/core'; @Directive() @@ -509,14 +555,17 @@ runInEachFileSystem(() => { hostDirectives: [HostDir], }) export class Dir {} - `); + `, + ); const messages = env.driveDiagnostics().map(extractMessage); expect(messages).toEqual(['Host directive HostDir must be standalone']); }); it('should produce a diagnostic if a host directive is not a directive', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Pipe, Component, NgModule} from '@angular/core'; @Pipe({name: 'hostDir'}) @@ -526,15 +575,19 @@ runInEachFileSystem(() => { hostDirectives: [HostDir], }) export class Dir {} - `); + `, + ); const messages = env.driveDiagnostics().map(extractMessage); - expect(messages).toEqual( - ['HostDir must be a standalone directive to be used as a host directive']); + expect(messages).toEqual([ + 'HostDir must be a standalone directive to be used as a host directive', + ]); }); it('should produce a diagnostic if a host directive is a component', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Component, NgModule} from '@angular/core'; @Component({ @@ -547,14 +600,17 @@ runInEachFileSystem(() => { hostDirectives: [HostComp], }) export class Dir {} - `); + `, + ); const messages = env.driveDiagnostics().map(extractMessage); expect(messages).toEqual(['Host directive HostComp cannot be a component']); }); it('should produce a diagnostic if hostDirectives is not an array', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -563,14 +619,17 @@ runInEachFileSystem(() => { hostDirectives: {} }) export class MyComp {} - `); + `, + ); const messages = env.driveDiagnostics().map(extractMessage); expect(messages).toContain('hostDirectives must be an array'); }); it('should produce a diagnostic if a host directive is not a reference', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; const hostA = {} as any; @@ -580,14 +639,17 @@ runInEachFileSystem(() => { hostDirectives: [hostA] }) export class MyComp {} - `); + `, + ); const messages = env.driveDiagnostics().map(extractMessage); expect(messages).toEqual(['Host directive must be a reference']); }); it('should produce a diagnostic if a host directive is not a reference to a class', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; function hostA() {} @@ -598,14 +660,17 @@ runInEachFileSystem(() => { hostDirectives: [hostA] }) export class MyComp {} - `); + `, + ); const messages = env.driveDiagnostics().map(extractMessage); expect(messages).toEqual(['Host directive reference must be a class']); }); it('should only produce a diagnostic once in a chain of directives', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Component, NgModule} from '@angular/core'; @Directive({ @@ -626,7 +691,8 @@ runInEachFileSystem(() => { hostDirectives: [HostDirA], }) export class Host {} - `); + `, + ); // What we're checking here is that the diagnostics aren't produced recursively. If that // were the case, the same diagnostic would show up more than once in the diagnostics since @@ -636,7 +702,9 @@ runInEachFileSystem(() => { }); it('should produce a diagnostic if a host directive output does not exist', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Output, EventEmitter} from '@angular/core'; @Directive({standalone: true}) @@ -652,15 +720,19 @@ runInEachFileSystem(() => { }] }) class Dir {} - `); + `, + ); const messages = env.driveDiagnostics().map(extractMessage); - expect(messages).toEqual( - ['Directive HostDir does not have an output with a public name of doesNotExist.']); + expect(messages).toEqual([ + 'Directive HostDir does not have an output with a public name of doesNotExist.', + ]); }); it('should produce a diagnostic if a host directive output alias does not exist', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Output, EventEmitter} from '@angular/core'; @Directive({standalone: true}) @@ -676,15 +748,19 @@ runInEachFileSystem(() => { }] }) class Dir {} - `); + `, + ); const messages = env.driveDiagnostics().map(extractMessage); - expect(messages).toEqual( - ['Directive HostDir does not have an output with a public name of foo.']); + expect(messages).toEqual([ + 'Directive HostDir does not have an output with a public name of foo.', + ]); }); it('should produce a diagnostic if a host directive input does not exist', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({standalone: true}) @@ -700,15 +776,19 @@ runInEachFileSystem(() => { }] }) class Dir {} - `); + `, + ); const messages = env.driveDiagnostics().map(extractMessage); - expect(messages).toEqual( - ['Directive HostDir does not have an input with a public name of doesNotExist.']); + expect(messages).toEqual([ + 'Directive HostDir does not have an input with a public name of doesNotExist.', + ]); }); it('should produce a diagnostic if a host directive input alias does not exist', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({standalone: true}) @@ -721,16 +801,19 @@ runInEachFileSystem(() => { hostDirectives: [{directive: HostDir, inputs: ['foo']}], }) class Dir {} - `); + `, + ); const messages = env.driveDiagnostics().map(extractMessage); - expect(messages).toEqual( - ['Directive HostDir does not have an input with a public name of foo.']); + expect(messages).toEqual([ + 'Directive HostDir does not have an input with a public name of foo.', + ]); }); - it('should produce a diagnostic if a host directive tries to alias to an existing input', - () => { - env.write('test.ts', ` + it('should produce a diagnostic if a host directive tries to alias to an existing input', () => { + env.write( + 'test.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({selector: '[host-dir]', standalone: true}) @@ -744,18 +827,20 @@ runInEachFileSystem(() => { hostDirectives: [{directive: HostDir, inputs: ['colorAlias: buttonColor']}] }) class Dir {} - `); - - const messages = env.driveDiagnostics().map(extractMessage); - expect(messages).toEqual([ - 'Cannot alias input colorAlias of host directive HostDir to buttonColor, because it ' + - 'already has a different input with the same public name.' - ]); - }); - - it('should produce a diagnostic if a host directive tries to alias to an existing input alias', - () => { - env.write('test.ts', ` + `, + ); + + const messages = env.driveDiagnostics().map(extractMessage); + expect(messages).toEqual([ + 'Cannot alias input colorAlias of host directive HostDir to buttonColor, because it ' + + 'already has a different input with the same public name.', + ]); + }); + + it('should produce a diagnostic if a host directive tries to alias to an existing input alias', () => { + env.write( + 'test.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({selector: '[host-dir]', standalone: true}) @@ -769,17 +854,20 @@ runInEachFileSystem(() => { hostDirectives: [{directive: HostDir, inputs: ['colorAlias: buttonColorAlias']}] }) class Dir {} - `); + `, + ); - const messages = env.driveDiagnostics().map(extractMessage); - expect(messages).toEqual( - ['Cannot alias input colorAlias of host directive HostDir to buttonColorAlias, ' + - 'because it already has a different input with the same public name.']); - }); + const messages = env.driveDiagnostics().map(extractMessage); + expect(messages).toEqual([ + 'Cannot alias input colorAlias of host directive HostDir to buttonColorAlias, ' + + 'because it already has a different input with the same public name.', + ]); + }); - it('should not produce a diagnostic if a host directive input aliases to the same name', - () => { - env.write('test.ts', ` + it('should not produce a diagnostic if a host directive input aliases to the same name', () => { + env.write( + 'test.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({selector: '[host-dir]', standalone: true}) @@ -792,15 +880,17 @@ runInEachFileSystem(() => { hostDirectives: [{directive: HostDir, inputs: ['color: buttonColor']}] }) class Dir {} - `); + `, + ); - const messages = env.driveDiagnostics().map(extractMessage); - expect(messages).toEqual([]); - }); + const messages = env.driveDiagnostics().map(extractMessage); + expect(messages).toEqual([]); + }); - it('should produce a diagnostic if a host directive tries to alias to an existing output alias', - () => { - env.write('test.ts', ` + it('should produce a diagnostic if a host directive tries to alias to an existing output alias', () => { + env.write( + 'test.ts', + ` import {Directive, Output, EventEmitter} from '@angular/core'; @Directive({selector: '[host-dir]', standalone: true}) @@ -814,18 +904,20 @@ runInEachFileSystem(() => { hostDirectives: [{directive: HostDir, outputs: ['clickedAlias: tappedAlias']}] }) class Dir {} - `); - - const messages = env.driveDiagnostics().map(extractMessage); - expect(messages).toEqual([ - 'Cannot alias output clickedAlias of host directive HostDir ' + - 'to tappedAlias, because it already has a different output with the same public name.' - ]); - }); - - it('should not produce a diagnostic if a host directive output aliases to the same name', - () => { - env.write('test.ts', ` + `, + ); + + const messages = env.driveDiagnostics().map(extractMessage); + expect(messages).toEqual([ + 'Cannot alias output clickedAlias of host directive HostDir ' + + 'to tappedAlias, because it already has a different output with the same public name.', + ]); + }); + + it('should not produce a diagnostic if a host directive output aliases to the same name', () => { + env.write( + 'test.ts', + ` import {Directive, Output, EventEmitter} from '@angular/core'; @Directive({selector: '[host-dir]', standalone: true}) @@ -838,14 +930,17 @@ runInEachFileSystem(() => { hostDirectives: [{directive: HostDir, outputs: ['clicked: wasClicked']}] }) class Dir {} - `); + `, + ); - const messages = env.driveDiagnostics().map(extractMessage); - expect(messages).toEqual([]); - }); + const messages = env.driveDiagnostics().map(extractMessage); + expect(messages).toEqual([]); + }); it('should produce a diagnostic if a required input is not exposed on the host', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Component, Input} from '@angular/core'; @Directive({ @@ -862,17 +957,20 @@ runInEachFileSystem(() => { hostDirectives: [HostDir] }) export class MyComp {} - `); + `, + ); const messages = env.driveDiagnostics().map(extractMessage); - expect(messages).toEqual( - [`Required input 'input' from host directive HostDir must be exposed.`]); + expect(messages).toEqual([ + `Required input 'input' from host directive HostDir must be exposed.`, + ]); }); - it('should use the public name when producing diagnostics about missing required inputs', - () => { - env.write('test.ts', ` + it('should use the public name when producing diagnostics about missing required inputs', () => { + env.write( + 'test.ts', + ` import {Directive, Component, Input} from '@angular/core'; @Directive({ @@ -889,16 +987,20 @@ runInEachFileSystem(() => { hostDirectives: [HostDir] }) export class MyComp {} - `); + `, + ); - const messages = env.driveDiagnostics().map(extractMessage); + const messages = env.driveDiagnostics().map(extractMessage); - expect(messages).toEqual( - [`Required input 'inputAlias' from host directive HostDir must be exposed.`]); - }); + expect(messages).toEqual([ + `Required input 'inputAlias' from host directive HostDir must be exposed.`, + ]); + }); it('should not produce required input diagnostic when exposed through alias', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Component, Input} from '@angular/core'; @Directive({ @@ -915,16 +1017,18 @@ runInEachFileSystem(() => { hostDirectives: [{directive: HostDir, inputs: ['inputAlias']}] }) export class MyComp {} - `); + `, + ); const messages = env.driveDiagnostics().map(extractMessage); expect(messages).toEqual([]); }); - it('should not produce required input diagnostic when exposed through alias to another alias', - () => { - env.write('test.ts', ` + it('should not produce required input diagnostic when exposed through alias to another alias', () => { + env.write( + 'test.ts', + ` import {Directive, Component, Input} from '@angular/core'; @Directive({ @@ -941,15 +1045,18 @@ runInEachFileSystem(() => { hostDirectives: [{directive: HostDir, inputs: ['inputAlias: customAlias']}] }) export class MyComp {} - `); + `, + ); - const messages = env.driveDiagnostics().map(extractMessage); + const messages = env.driveDiagnostics().map(extractMessage); - expect(messages).toEqual([]); - }); + expect(messages).toEqual([]); + }); it('should not produce a diagnostic when exposing an aliased binding', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, EventEmitter} from '@angular/core'; @Directive({ @@ -967,14 +1074,17 @@ runInEachFileSystem(() => { hostDirectives: [{directive: Trigger, outputs: ['triggerOpened']}] }) export class Host {} - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(0); }); it('should not produce a diagnostic when exposing an inherited aliased binding', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, EventEmitter} from '@angular/core'; @Directive({standalone: true}) @@ -995,7 +1105,8 @@ runInEachFileSystem(() => { hostDirectives: [{directive: Trigger, outputs: ['triggerOpened: hostOpened']}] }) export class Host {} - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(0); diff --git a/packages/compiler-cli/test/ngtsc/imports_spec.ts b/packages/compiler-cli/test/ngtsc/imports_spec.ts index 5d96b71e7621d..ff218ea5b2f22 100644 --- a/packages/compiler-cli/test/ngtsc/imports_spec.ts +++ b/packages/compiler-cli/test/ngtsc/imports_spec.ts @@ -33,7 +33,9 @@ runInEachFileSystem(() => { }); it('should report an error when using a directive outside of rootDirs', () => { - env.write('/app/module.ts', ` + env.write( + '/app/module.ts', + ` import {NgModule} from '@angular/core'; import {ExternalDir} from '../lib/dir'; import {MyComponent} from './comp'; @@ -42,33 +44,42 @@ runInEachFileSystem(() => { declarations: [ExternalDir, MyComponent], }) export class MyModule {} - `); - env.write('/app/comp.ts', ` + `, + ); + env.write( + '/app/comp.ts', + ` import {Component} from '@angular/core'; @Component({ template: '
', }) export class MyComponent {} - `); - env.write('/lib/dir.ts', ` + `, + ); + env.write( + '/lib/dir.ts', + ` import {Directive} from '@angular/core'; @Directive({selector: '[external]'}) export class ExternalDir {} - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')) - .toEqual(`Unable to import class ExternalDir. + .toEqual(`Unable to import class ExternalDir. The file ${absoluteFrom('/lib/dir.ts')} is outside of the configured 'rootDir'.`); expect(diags[0].file!.fileName).toEqual(absoluteFrom('/app/module.ts')); expect(getDiagnosticSourceCode(diags[0])).toEqual('ExternalDir'); }); it('should report an error when a library entry-point does not export the symbol', () => { - env.write('/app/module.ts', ` + env.write( + '/app/module.ts', + ` import {NgModule} from '@angular/core'; import {ExternalModule} from 'lib'; import {MyComponent} from './comp'; @@ -78,35 +89,45 @@ runInEachFileSystem(() => { declarations: [MyComponent], }) export class MyModule {} - `); - env.write('/app/comp.ts', ` + `, + ); + env.write( + '/app/comp.ts', + ` import {Component} from '@angular/core'; @Component({ template: '
', }) export class MyComponent {} - `); - env.write('/node_modules/lib/index.d.ts', ` + `, + ); + env.write( + '/node_modules/lib/index.d.ts', + ` import {ɵɵNgModuleDeclaration} from '@angular/core'; import {ExternalDir} from './dir'; export class ExternalModule { static ɵmod: ɵɵNgModuleDeclaration; } - `); - env.write('/node_modules/lib/dir.d.ts', ` + `, + ); + env.write( + '/node_modules/lib/dir.d.ts', + ` import {ɵɵDirectiveDeclaration} from '@angular/core'; export class ExternalDir { static ɵdir: ɵɵDirectiveDeclaration; } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')) - .toEqual(`Unable to import directive ExternalDir. + .toEqual(`Unable to import directive ExternalDir. The symbol is not exported from ${absoluteFrom('/node_modules/lib/index.d.ts')} (module 'lib').`); expect(diags[0].file!.fileName).toEqual(absoluteFrom('/app/comp.ts')); expect(getDiagnosticSourceCode(diags[0])).toEqual('MyComponent'); diff --git a/packages/compiler-cli/test/ngtsc/incremental_error_spec.ts b/packages/compiler-cli/test/ngtsc/incremental_error_spec.ts index 0fb1bf892524e..0c5a904b410a8 100644 --- a/packages/compiler-cli/test/ngtsc/incremental_error_spec.ts +++ b/packages/compiler-cli/test/ngtsc/incremental_error_spec.ts @@ -25,9 +25,12 @@ runInEachFileSystem(() => { // This file is part of the program, but not referenced by anything else. It can be used by // each test to verify that it isn't re-emitted after incremental builds. - env.write('unrelated.ts', ` + env.write( + 'unrelated.ts', + ` export class Unrelated {} - `); + `, + ); }); function expectToHaveWritten(files: string[]): void { @@ -46,33 +49,45 @@ runInEachFileSystem(() => { } it('should handle an error in an unrelated file', () => { - env.write('cmp.ts', ` + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({selector: 'test-cmp', template: '...'}) export class TestCmp {} - `); - env.write('other.ts', ` + `, + ); + env.write( + 'other.ts', + ` export class Other {} - `); + `, + ); // Start with a clean compilation. env.driveMain(); env.flushWrittenFileTracking(); // Introduce the error. - env.write('other.ts', ` + env.write( + 'other.ts', + ` export class Other // missing braces - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(diags[0].file!.fileName).toBe(_('/other.ts')); expectToHaveWritten([]); // Remove the error. /other.js should now be emitted again. - env.write('other.ts', ` + env.write( + 'other.ts', + ` export class Other {} - `); + `, + ); env.driveMain(); expectToHaveWritten(['/other.js']); @@ -80,15 +95,21 @@ runInEachFileSystem(() => { it('should emit all files after an error on the initial build', () => { // Intentionally start with a broken compilation. - env.write('cmp.ts', ` + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({selector: 'test-cmp', template: '...'}) export class TestCmp {} - `); - env.write('other.ts', ` + `, + ); + env.write( + 'other.ts', + ` export class Other // missing braces - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); @@ -96,35 +117,47 @@ runInEachFileSystem(() => { expectToHaveWritten([]); // Remove the error. All files should be emitted. - env.write('other.ts', ` + env.write( + 'other.ts', + ` export class Other {} - `); + `, + ); env.driveMain(); expectToHaveWritten(['/cmp.js', '/other.js', '/unrelated.js']); }); it('should emit files introduced at the same time as an unrelated error', () => { - env.write('other.ts', ` + env.write( + 'other.ts', + ` // Needed so that the initial program contains @angular/core's .d.ts file. import '@angular/core'; export class Other {} - `); + `, + ); // Clean compile. env.driveMain(); env.flushWrittenFileTracking(); - env.write('cmp.ts', ` + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({selector: 'test-cmp', template: '...'}) export class TestCmp {} - `); - env.write('other.ts', ` + `, + ); + env.write( + 'other.ts', + ` export class Other // missing braces - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); @@ -132,46 +165,64 @@ runInEachFileSystem(() => { expectToHaveWritten([]); // Remove the error. All files should be emitted. - env.write('other.ts', ` + env.write( + 'other.ts', + ` export class Other {} - `); + `, + ); env.driveMain(); expectToHaveWritten(['/cmp.js', '/other.js']); }); it('should emit dependent files even in the face of an error', () => { - env.write('cmp.ts', ` + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; import {SELECTOR} from './selector'; @Component({selector: SELECTOR, template: '...'}) export class TestCmp {} - `); - env.write('selector.ts', ` + `, + ); + env.write( + 'selector.ts', + ` export const SELECTOR = 'test-cmp'; - `); + `, + ); - env.write('other.ts', ` + env.write( + 'other.ts', + ` // Needed so that the initial program contains @angular/core's .d.ts file. import '@angular/core'; export class Other {} - `); + `, + ); // Clean compile. env.driveMain(); env.flushWrittenFileTracking(); - env.write('cmp.ts', ` + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({selector: 'test-cmp', template: '...'}) export class TestCmp {} - `); - env.write('other.ts', ` + `, + ); + env.write( + 'other.ts', + ` export class Other // missing braces - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); @@ -179,44 +230,56 @@ runInEachFileSystem(() => { expectToHaveWritten([]); // Remove the error. All files should be emitted. - env.write('other.ts', ` + env.write( + 'other.ts', + ` export class Other {} - `); + `, + ); env.driveMain(); expectToHaveWritten(['/cmp.js', '/other.js']); }); - it('should recover from an error in a component\'s metadata', () => { - env.write('test.ts', ` + it("should recover from an error in a component's metadata", () => { + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({selector: 'test-cmp', template: '...'}) export class TestCmp {} - `); + `, + ); // Start with a clean compilation. env.driveMain(); env.flushWrittenFileTracking(); // Introduce the error. - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({selector: 'test-cmp', template: ...}) // invalid template export class TestCmp {} - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBeGreaterThan(0); expectToHaveWritten([]); // Clear the error and verify that the compiler now emits test.js again. - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({selector: 'test-cmp', template: '...'}) export class TestCmp {} - `); + `, + ); env.driveMain(); expectToHaveWritten(['/test.js']); @@ -226,19 +289,27 @@ runInEachFileSystem(() => { // In this test, there are two components, TestCmp and TargetCmp, that are part of the same // NgModule. TestCmp is broken in an incremental build and then fixed, and the test verifies // that TargetCmp is re-emitted. - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({selector: 'test-cmp', template: '...'}) export class TestCmp {} - `); - env.write('target.ts', ` + `, + ); + env.write( + 'target.ts', + ` import {Component} from '@angular/core'; @Component({selector: 'target-cmp', template: ''}) export class TargetCmp {} - `); - env.write('module.ts', ` + `, + ); + env.write( + 'module.ts', + ` import {NgModule, NO_ERRORS_SCHEMA} from '@angular/core'; import {TargetCmp} from './target'; import {TestCmp} from './test'; @@ -248,30 +319,37 @@ runInEachFileSystem(() => { schemas: [NO_ERRORS_SCHEMA], }) export class Module {} - `); + `, + ); // Start with a clean compilation. env.driveMain(); env.flushWrittenFileTracking(); // Introduce the syntactic error. - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({selector: ..., template: '...'}) // ... is not valid syntax export class TestCmp {} - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBeGreaterThan(0); expectToHaveWritten([]); // Clear the error and trigger the rebuild. - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({selector: 'test-cmp-fixed', template: '...'}) export class TestCmp {} - `); + `, + ); env.driveMain(); @@ -290,7 +368,9 @@ runInEachFileSystem(() => { }); it('should recover from an error in an external template', () => { - env.write('mod.ts', ` + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; @@ -298,9 +378,12 @@ runInEachFileSystem(() => { declarations: [Cmp], }) export class Mod {} - `); + `, + ); env.write('cmp.html', '{{ error = "true" }} '); - env.write('cmp.ts', ` + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -310,7 +393,8 @@ runInEachFileSystem(() => { export class Cmp { error = 'false'; } - `); + `, + ); // Diagnostics should show for the broken component template. expect(env.driveDiagnostics().length).toBeGreaterThan(0); @@ -327,19 +411,27 @@ runInEachFileSystem(() => { // designed to verify that CmpA and CmpB are re-emitted if somewhere upstream in the NgModule // graph, an error is fixed. To check this, LibModule is broken and then fixed in incremental // build steps. - env.write('a.ts', ` + env.write( + 'a.ts', + ` import {Component} from '@angular/core'; @Component({selector: 'test-cmp', template: '
'}) export class CmpA {} - `); - env.write('b.ts', ` + `, + ); + env.write( + 'b.ts', + ` import {Component} from '@angular/core'; @Component({selector: 'target-cmp', template: '...'}) export class CmpB {} - `); - env.write('module.ts', ` + `, + ); + env.write( + 'module.ts', + ` import {NgModule} from '@angular/core'; import {LibModule} from './lib'; import {CmpA} from './a'; @@ -356,8 +448,11 @@ runInEachFileSystem(() => { imports: [IndirectModule], }) export class Module {} - `); - env.write('lib.ts', ` + `, + ); + env.write( + 'lib.ts', + ` import {Directive, NgModule} from '@angular/core'; @Directive({ @@ -370,14 +465,17 @@ runInEachFileSystem(() => { exports: [LibDir], }) export class LibModule {} - `); + `, + ); // Start with a clean compilation. env.driveMain(); env.flushWrittenFileTracking(); // Introduce the error in LibModule - env.write('lib.ts', ` + env.write( + 'lib.ts', + ` import {Directive, NgModule} from '@angular/core'; @Directive({ @@ -401,14 +499,17 @@ runInEachFileSystem(() => { exports: [LibDir, NewModule], }) export class LibModule // missing braces - `); + `, + ); // env.driveMain(); const diags = env.driveDiagnostics(); expect(diags.length).toBeGreaterThan(0); expectToHaveWritten([]); // Clear the error and recompile. - env.write('lib.ts', ` + env.write( + 'lib.ts', + ` import {Component, NgModule} from '@angular/core'; @Component({ @@ -426,7 +527,8 @@ runInEachFileSystem(() => { exports: [LibCmp, NewModule], }) export class LibModule {} - `); + `, + ); env.driveMain(); @@ -455,12 +557,15 @@ runInEachFileSystem(() => { env.flushWrittenFileTracking(); // Update ACmp - env.write('a.ts', ` + env.write( + 'a.ts', + ` import {Component} from '@angular/core'; @Component({selector: 'a-cmp', template: 'new template'}) export class ACmp {} - `); + `, + ); // Update the file to have an error, simultaneously. writeRandomFile(env, 'other.ts', {error: true}); @@ -547,19 +652,27 @@ runInEachFileSystem(() => { */ export function writeTwoComponentSystem(env: NgtscTestEnvironment): void { env.write('a.html', 'This is the template for CmpA'); - env.write('a.ts', ` + env.write( + 'a.ts', + ` import {Component} from '@angular/core'; @Component({selector: 'a-cmp', templateUrl: './a.html'}) export class ACmp {} - `); - env.write('b.ts', ` + `, + ); + env.write( + 'b.ts', + ` import {Component} from '@angular/core'; @Component({selector: 'b-cmp', template: ''}) export class BCmp {} - `); - env.write('module.ts', ` + `, + ); + env.write( + 'module.ts', + ` import {NgModule} from '@angular/core'; import {ACmp} from './a'; import {BCmp} from './b'; @@ -568,13 +681,20 @@ export function writeTwoComponentSystem(env: NgtscTestEnvironment): void { declarations: [ACmp, BCmp], }) export class Module {} -`); +`, + ); } export function writeRandomFile( - env: NgtscTestEnvironment, name: string, options: {error?: true} = {}): void { - env.write(name, ` + env: NgtscTestEnvironment, + name: string, + options: {error?: true} = {}, +): void { + env.write( + name, + ` // If options.error is set, this class has missing braces. export class Other ${options.error !== true ? '{}' : ''} - `); + `, + ); } diff --git a/packages/compiler-cli/test/ngtsc/incremental_semantic_changes_spec.ts b/packages/compiler-cli/test/ngtsc/incremental_semantic_changes_spec.ts index df55b7ff8888f..fa3b08c223180 100644 --- a/packages/compiler-cli/test/ngtsc/incremental_semantic_changes_spec.ts +++ b/packages/compiler-cli/test/ngtsc/incremental_semantic_changes_spec.ts @@ -46,7 +46,9 @@ runInEachFileSystem(() => { // // ADep is changed during the test without affecting its public API, and the test asserts // that both ACmp and BCmp which consume ADep are not re-emitted. - env.write('a/dep.ts', ` + env.write( + 'a/dep.ts', + ` import {Component, Input, Output, EventEmitter} from '@angular/core'; @Component({ @@ -60,8 +62,11 @@ runInEachFileSystem(() => { @Output() output = new EventEmitter(); } - `); - env.write('a/cmp.ts', ` + `, + ); + env.write( + 'a/cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -69,8 +74,11 @@ runInEachFileSystem(() => { template: '', }) export class ACmp {} - `); - env.write('a/mod.ts', ` + `, + ); + env.write( + 'a/mod.ts', + ` import {NgModule} from '@angular/core'; import {ADep} from './dep'; import {ACmp} from './cmp'; @@ -80,8 +88,11 @@ runInEachFileSystem(() => { exports: [ADep], }) export class AMod {} - `); - env.write('b/cmp.ts', ` + `, + ); + env.write( + 'b/cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -89,8 +100,11 @@ runInEachFileSystem(() => { template: '', }) export class BCmp {} - `); - env.write('b/mod.ts', ` + `, + ); + env.write( + 'b/mod.ts', + ` import {NgModule} from '@angular/core'; import {BCmp} from './cmp'; import {AMod} from '../a/mod'; @@ -100,13 +114,16 @@ runInEachFileSystem(() => { imports: [AMod], }) export class BMod {} - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); // Change ADep without affecting its public API. - env.write('a/dep.ts', ` + env.write( + 'a/dep.ts', + ` import {Component, Input, Output, EventEmitter} from '@angular/core'; @Component({ @@ -120,7 +137,8 @@ runInEachFileSystem(() => { @Output() output = new EventEmitter(); // changed from string to number } - `); + `, + ); env.driveMain(); expectToHaveWritten([ @@ -142,7 +160,9 @@ runInEachFileSystem(() => { // During the test, ADep's public API is changed, and the test verifies that neither ACmp // nor BCmp are re-emitted. - env.write('a/dep.ts', ` + env.write( + 'a/dep.ts', + ` import {Directive, Input, Output, EventEmitter} from '@angular/core'; @Directive({ @@ -155,8 +175,11 @@ runInEachFileSystem(() => { @Output() output = new EventEmitter(); } - `); - env.write('a/cmp.ts', ` + `, + ); + env.write( + 'a/cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -164,8 +187,11 @@ runInEachFileSystem(() => { template: 'Does not use a-dep.', }) export class ACmp {} - `); - env.write('a/mod.ts', ` + `, + ); + env.write( + 'a/mod.ts', + ` import {NgModule} from '@angular/core'; import {ADep} from './dep'; import {ACmp} from './cmp'; @@ -175,8 +201,11 @@ runInEachFileSystem(() => { exports: [ADep], }) export class AMod {} - `); - env.write('b/cmp.ts', ` + `, + ); + env.write( + 'b/cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -184,8 +213,11 @@ runInEachFileSystem(() => { template: 'Does not use a-dep.', }) export class BCmp {} - `); - env.write('b/mod.ts', ` + `, + ); + env.write( + 'b/mod.ts', + ` import {NgModule} from '@angular/core'; import {BCmp} from './cmp'; import {AMod} from '../a/mod'; @@ -195,13 +227,16 @@ runInEachFileSystem(() => { imports: [AMod], }) export class BMod {} - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); // Update ADep and change its public API. - env.write('a/dep.ts', ` + env.write( + 'a/dep.ts', + ` import {Directive, Input, Output, EventEmitter} from '@angular/core'; @Directive({ @@ -214,7 +249,8 @@ runInEachFileSystem(() => { @Output('output-renamed') // public binding name of the @Output is changed. output = new EventEmitter(); } - `); + `, + ); env.driveMain(); expectToHaveWritten([ @@ -234,15 +270,20 @@ runInEachFileSystem(() => { // // During the test, Dep's selector is updated to '[dep]', causing it to begin matching the // template of Cmp. The test verifies that Cmp is re-emitted after this change. - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: '[does-not-match]', }) export class Dep {} - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -250,8 +291,11 @@ runInEachFileSystem(() => { template: '
', }) export class Cmp {} - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dep} from './dep'; @@ -260,19 +304,23 @@ runInEachFileSystem(() => { declarations: [Cmp, Dep], }) export class Mod {} - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: '[dep]', // selector changed to now match inside Cmp's template }) export class Dep {} - `); + `, + ); env.driveMain(); expectToHaveWritten([ @@ -294,15 +342,20 @@ runInEachFileSystem(() => { // During the test, Dep's selector is changed, causing it to no longer match the template of // Cmp. The test verifies that Cmp is re-emitted after this change. - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: '[dep]', }) export class Dep {} - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -310,8 +363,11 @@ runInEachFileSystem(() => { template: '
', }) export class Cmp {} - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dep} from './dep'; @@ -320,19 +376,23 @@ runInEachFileSystem(() => { declarations: [Cmp, Dep], }) export class Mod {} - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: '[does-not-match]', // selector changed to no longer match Cmp's template }) export class Dep {} - `); + `, + ); env.driveMain(); expectToHaveWritten([ @@ -353,15 +413,20 @@ runInEachFileSystem(() => { // // During the test, an input is added to Dep, and the test verifies that Cmp is re-emitted. - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: '[dep]', }) export class Dep {} - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -369,8 +434,11 @@ runInEachFileSystem(() => { template: '
', }) export class Cmp {} - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dep} from './dep'; @@ -379,12 +447,15 @@ runInEachFileSystem(() => { declarations: [Cmp, Dep], }) export class Mod {} - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -393,7 +464,8 @@ runInEachFileSystem(() => { export class Dep { @Input() input!: string; // adding this changes Dep's public API } - `); + `, + ); env.driveMain(); expectToHaveWritten([ @@ -415,7 +487,9 @@ runInEachFileSystem(() => { // During the test, an input of Dep is renamed, and the test verifies that Cmp is // re-emitted. - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -424,8 +498,11 @@ runInEachFileSystem(() => { export class Dep { @Input() input!: string; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -433,8 +510,11 @@ runInEachFileSystem(() => { template: '
', }) export class Cmp {} - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dep} from './dep'; @@ -443,12 +523,15 @@ runInEachFileSystem(() => { declarations: [Cmp, Dep], }) export class Mod {} - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -457,7 +540,8 @@ runInEachFileSystem(() => { export class Dep { @Input('renamed') input!: string; // renaming this changes Dep's public API } - `); + `, + ); env.driveMain(); expectToHaveWritten([ @@ -479,7 +563,9 @@ runInEachFileSystem(() => { // During the test, an input of Dep is removed, and the test verifies that Cmp is // re-emitted. - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -488,8 +574,11 @@ runInEachFileSystem(() => { export class Dep { @Input() input!: string; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -497,8 +586,11 @@ runInEachFileSystem(() => { template: '
', }) export class Cmp {} - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dep} from './dep'; @@ -507,12 +599,15 @@ runInEachFileSystem(() => { declarations: [Cmp, Dep], }) export class Mod {} - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Directive} from '@angular/core'; @Directive({ @@ -521,7 +616,8 @@ runInEachFileSystem(() => { export class Dep { // Dep's input has been removed, which changes its public API } - `); + `, + ); env.driveMain(); expectToHaveWritten([ @@ -542,15 +638,20 @@ runInEachFileSystem(() => { // // During the test, an output of Dep is added, and the test verifies that Cmp is re-emitted. - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: '[dep]', }) export class Dep {} - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -558,8 +659,11 @@ runInEachFileSystem(() => { template: '
', }) export class Cmp {} - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dep} from './dep'; @@ -568,12 +672,15 @@ runInEachFileSystem(() => { declarations: [Cmp, Dep], }) export class Mod {} - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Directive, Output, EventEmitter} from '@angular/core'; @Directive({ @@ -583,7 +690,8 @@ runInEachFileSystem(() => { @Output() output = new EventEmitter(); // added, which changes Dep's public API } - `); + `, + ); env.driveMain(); expectToHaveWritten([ @@ -605,7 +713,9 @@ runInEachFileSystem(() => { // During the test, an output of Dep is renamed, and the test verifies that Cmp is // re-emitted. - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Directive, Output, EventEmitter} from '@angular/core'; @Directive({ @@ -614,8 +724,11 @@ runInEachFileSystem(() => { export class Dep { @Output() output = new EventEmitter(); } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -623,8 +736,11 @@ runInEachFileSystem(() => { template: '
', }) export class Cmp {} - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dep} from './dep'; @@ -633,12 +749,15 @@ runInEachFileSystem(() => { declarations: [Cmp, Dep], }) export class Mod {} - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Directive, Output, EventEmitter} from '@angular/core'; @Directive({ @@ -647,7 +766,8 @@ runInEachFileSystem(() => { export class Dep { @Output('renamed') output = new EventEmitter(); // public API changed } - `); + `, + ); env.driveMain(); expectToHaveWritten([ @@ -669,7 +789,9 @@ runInEachFileSystem(() => { // During the test, an output of Dep is removed, and the test verifies that Cmp is // re-emitted. - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Directive, Output, EventEmitter} from '@angular/core'; @Directive({ @@ -678,8 +800,11 @@ runInEachFileSystem(() => { export class Dep { @Output() output = new EventEmitter(); } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -687,8 +812,11 @@ runInEachFileSystem(() => { template: '
', }) export class Cmp {} - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dep} from './dep'; @@ -697,12 +825,15 @@ runInEachFileSystem(() => { declarations: [Cmp, Dep], }) export class Mod {} - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Directive} from '@angular/core'; @Directive({ @@ -711,7 +842,8 @@ runInEachFileSystem(() => { export class Dep { // Dep's output has been removed, which changes its public API } - `); + `, + ); env.driveMain(); expectToHaveWritten([ @@ -733,7 +865,9 @@ runInEachFileSystem(() => { // During the test, the exportAs clause of Dep is changed, and the test verifies that Cmp is // re-emitted. - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Directive} from '@angular/core'; @Directive({ @@ -741,8 +875,11 @@ runInEachFileSystem(() => { exportAs: 'depExport1', }) export class Dep {} - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -750,8 +887,11 @@ runInEachFileSystem(() => { template: '
', }) export class Cmp {} - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dep} from './dep'; @@ -760,12 +900,15 @@ runInEachFileSystem(() => { declarations: [Cmp, Dep], }) export class Mod {} - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Directive} from '@angular/core'; @Directive({ @@ -773,7 +916,8 @@ runInEachFileSystem(() => { exportAs: 'depExport2', // changing this changes Dep's public API }) export class Dep {} - `); + `, + ); env.driveMain(); expectToHaveWritten([ @@ -796,7 +940,9 @@ runInEachFileSystem(() => { // as the identity of each pipe is now different, the effective public API of those pipe // usages has changed. The test then verifies that Cmp is re-emitted. - env.write('pipes.ts', ` + env.write( + 'pipes.ts', + ` import {Pipe} from '@angular/core'; @Pipe({ @@ -812,8 +958,11 @@ runInEachFileSystem(() => { export class PipeB { transform(value: any): any { return value; } } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -823,8 +972,11 @@ runInEachFileSystem(() => { export class Cmp { value!: string; } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {PipeA, PipeB} from './pipes'; import {Cmp} from './cmp'; @@ -833,12 +985,15 @@ runInEachFileSystem(() => { declarations: [Cmp, PipeA, PipeB], }) export class Mod {} - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); - env.write('pipes.ts', ` + env.write( + 'pipes.ts', + ` import {Pipe} from '@angular/core'; @Pipe({ @@ -854,7 +1009,8 @@ runInEachFileSystem(() => { export class PipeB { transform(value: any): any { return value; } } - `); + `, + ); env.driveMain(); expectToHaveWritten([ @@ -877,7 +1033,9 @@ runInEachFileSystem(() => { // During the test, an input of Dep becomes required, and the test verifies that Cmp is // re-emitted. - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -886,8 +1044,11 @@ runInEachFileSystem(() => { export class Dep { @Input() input!: string; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -895,8 +1056,11 @@ runInEachFileSystem(() => { template: '
', }) export class Cmp {} - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dep} from './dep'; @@ -905,12 +1069,15 @@ runInEachFileSystem(() => { declarations: [Cmp, Dep], }) export class Mod {} - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -919,7 +1086,8 @@ runInEachFileSystem(() => { export class Dep { @Input({required: true}) input!: string; // making this required changes the public API } - `); + `, + ); env.driveMain(); expectToHaveWritten([ @@ -939,14 +1107,15 @@ runInEachFileSystem(() => { }); describe('external declarations', () => { - it('should not recompile components that use external declarations that are not changed', - () => { - // Testing setup: Two components (MyCmpA and MyCmpB) both depend on an external directive - // which matches their templates, via an NgModule import. - // - // During the test, MyCmpA is invalidated, and the test verifies that only MyCmpA and not - // MyCmpB is re-emitted. - env.write('node_modules/external/index.d.ts', ` + it('should not recompile components that use external declarations that are not changed', () => { + // Testing setup: Two components (MyCmpA and MyCmpB) both depend on an external directive + // which matches their templates, via an NgModule import. + // + // During the test, MyCmpA is invalidated, and the test verifies that only MyCmpA and not + // MyCmpB is re-emitted. + env.write( + 'node_modules/external/index.d.ts', + ` import * as ng from '@angular/core'; export declare class ExternalDir { @@ -956,24 +1125,33 @@ runInEachFileSystem(() => { export declare class ExternalMod { static ɵmod: ng.ɵɵNgModuleDefWithMeta; } - `); - env.write('cmp-a.ts', ` + `, + ); + env.write( + 'cmp-a.ts', + ` import {Component} from '@angular/core'; @Component({ template: '
', }) export class MyCmpA {} - `); - env.write('cmp-b.ts', ` + `, + ); + env.write( + 'cmp-b.ts', + ` import {Component} from '@angular/core'; @Component({ template: '
', }) export class MyCmpB {} - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {ExternalMod} from 'external'; import {MyCmpA} from './cmp-a'; @@ -984,24 +1162,25 @@ runInEachFileSystem(() => { imports: [ExternalMod], }) export class MyMod {} - `); - env.driveMain(); - env.flushWrittenFileTracking(); + `, + ); + env.driveMain(); + env.flushWrittenFileTracking(); - // Invalidate MyCmpA, causing it to be re-emitted. - env.invalidateCachedFile('cmp-a.ts'); + // Invalidate MyCmpA, causing it to be re-emitted. + env.invalidateCachedFile('cmp-a.ts'); - env.driveMain(); - expectToHaveWritten([ - // MyMod is written because it has a direct reference to MyCmpA, which was invalidated. - '/mod.js', + env.driveMain(); + expectToHaveWritten([ + // MyMod is written because it has a direct reference to MyCmpA, which was invalidated. + '/mod.js', - // MyCmpA is written because it was invalidated. - '/cmp-a.js', + // MyCmpA is written because it was invalidated. + '/cmp-a.js', - // MyCmpB should not be written because it is unaffected. - ]); - }); + // MyCmpB should not be written because it is unaffected. + ]); + }); it('should recompile components once an external declaration is changed', () => { // Testing setup: Two components (MyCmpA and MyCmpB) both depend on an external directive @@ -1009,7 +1188,9 @@ runInEachFileSystem(() => { // // During the test, the external directive is invalidated, and the test verifies that both // components are re-emitted as a result. - env.write('node_modules/external/index.d.ts', ` + env.write( + 'node_modules/external/index.d.ts', + ` import * as ng from '@angular/core'; export declare class ExternalDir { @@ -1019,24 +1200,33 @@ runInEachFileSystem(() => { export declare class ExternalMod { static ɵmod: ng.ɵɵNgModuleDefWithMeta; } - `); - env.write('cmp-a.ts', ` + `, + ); + env.write( + 'cmp-a.ts', + ` import {Component} from '@angular/core'; @Component({ template: '
', }) export class MyCmpA {} - `); - env.write('cmp-b.ts', ` + `, + ); + env.write( + 'cmp-b.ts', + ` import {Component} from '@angular/core'; @Component({ template: '
', }) export class MyCmpB {} - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {ExternalMod} from 'external'; import {MyCmpA} from './cmp-a'; @@ -1047,7 +1237,8 @@ runInEachFileSystem(() => { imports: [ExternalMod], }) export class MyMod {} - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); @@ -1079,7 +1270,9 @@ runInEachFileSystem(() => { // // During the test, Dep's name is changed while keeping its public API the same. The test // verifies that Cmp is re-emitted. - env.write('cmp.ts', ` + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1087,8 +1280,11 @@ runInEachFileSystem(() => { template: '', }) export class Cmp {} - `); - env.write('dep.ts', ` + `, + ); + env.write( + 'dep.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1096,8 +1292,11 @@ runInEachFileSystem(() => { template: 'Dep', }) export class Dep {} - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dep} from './dep'; @@ -1106,11 +1305,14 @@ runInEachFileSystem(() => { declarations: [Cmp, Dep] }) export class Mod {} - `); + `, + ); env.driveMain(); - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1118,8 +1320,11 @@ runInEachFileSystem(() => { template: 'Dep', }) export class ChangedDep {} // Dep renamed to ChangedDep. - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; @@ -1129,7 +1334,8 @@ runInEachFileSystem(() => { declarations: [Cmp, ChangedDep] }) export class Mod {} - `); + `, + ); env.flushWrittenFileTracking(); env.driveMain(); @@ -1149,7 +1355,9 @@ runInEachFileSystem(() => { // `Cmp` uses `Dir` in its template. This test verifies that the local reference of `Cmp` // that is emitted into `Dir` does not inadvertently cause `cmp.ts` to be emitted even when // nothing changed. - env.write('cmp.ts', ` + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1163,8 +1371,11 @@ runInEachFileSystem(() => { template: '', }) export class Cmp {} - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp, Dep} from './cmp'; @@ -1172,7 +1383,8 @@ runInEachFileSystem(() => { declarations: [Cmp, Dep] }) export class Mod {} - `); + `, + ); env.driveMain(); @@ -1192,7 +1404,9 @@ runInEachFileSystem(() => { // // During the test, Dep's exported name is changed while keeping its declaration name the // same. The test verifies that Cmp is re-emitted. - env.write('cmp.ts', ` + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1200,8 +1414,11 @@ runInEachFileSystem(() => { template: '', }) export class Cmp {} - `); - env.write('dep.ts', ` + `, + ); + env.write( + 'dep.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1209,8 +1426,11 @@ runInEachFileSystem(() => { template: 'Dep', }) export class Dep {} - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dep} from './dep'; @@ -1219,11 +1439,14 @@ runInEachFileSystem(() => { declarations: [Cmp, Dep] }) export class Mod {} - `); + `, + ); env.driveMain(); - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1232,8 +1455,11 @@ runInEachFileSystem(() => { }) class Dep {} export {Dep as ChangedDep}; // the export name of Dep is changed. - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; @@ -1243,7 +1469,8 @@ runInEachFileSystem(() => { declarations: [Cmp, ChangedDep] }) export class Mod {} - `); + `, + ); env.flushWrittenFileTracking(); env.driveMain(); @@ -1265,7 +1492,9 @@ runInEachFileSystem(() => { // During the test, the indirect export name is changed, and the test verifies that CmpUser // is re-emitted. - env.write('cmp-user.ts', ` + env.write( + 'cmp-user.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1273,8 +1502,11 @@ runInEachFileSystem(() => { template: '', }) export class CmpUser {} - `); - env.write('cmp-dep.ts', ` + `, + ); + env.write( + 'cmp-dep.ts', + ` import {Component} from '@angular/core'; export {CmpDep as CmpDepExport}; @@ -1284,8 +1516,11 @@ runInEachFileSystem(() => { template: 'Dep', }) class CmpDep {} - `); - env.write('module.ts', ` + `, + ); + env.write( + 'module.ts', + ` import {NgModule} from '@angular/core'; import {CmpUser} from './cmp-user'; import {CmpDepExport} from './cmp-dep'; @@ -1294,7 +1529,8 @@ runInEachFileSystem(() => { declarations: [CmpUser, CmpDepExport] }) export class Module {} - `); + `, + ); env.driveMain(); @@ -1303,7 +1539,9 @@ runInEachFileSystem(() => { const userCmpJs = env.getContents('cmp-user.js'); expect(userCmpJs).toContain('CmpDepExport'); - env.write('cmp-dep.ts', ` + env.write( + 'cmp-dep.ts', + ` import {Component} from '@angular/core'; export {CmpDep as CmpDepExport2}; @@ -1313,8 +1551,11 @@ runInEachFileSystem(() => { template: 'Dep', }) class CmpDep {} - `); - env.write('module.ts', ` + `, + ); + env.write( + 'module.ts', + ` import {NgModule} from '@angular/core'; import {CmpUser} from './cmp-user'; import {CmpDepExport2} from './cmp-dep'; @@ -1323,7 +1564,8 @@ runInEachFileSystem(() => { declarations: [CmpUser, CmpDepExport2] }) export class Module {} - `); + `, + ); env.flushWrittenFileTracking(); env.driveMain(); @@ -1344,9 +1586,10 @@ runInEachFileSystem(() => { expect(userCmp2Js).toContain('CmpDepExport2'); }); - it('should not recompile components when a directive is changed into a component', () => { - env.write('cmp-user.ts', ` + env.write( + 'cmp-user.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1354,16 +1597,22 @@ runInEachFileSystem(() => { template: '
', }) export class CmpUser {} - `); - env.write('dep.ts', ` + `, + ); + env.write( + 'dep.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: '[dep]', }) export class Dep {} - `); - env.write('module.ts', ` + `, + ); + env.write( + 'module.ts', + ` import {NgModule} from '@angular/core'; import {CmpUser} from './cmp-user'; import {Dep} from './dep'; @@ -1372,10 +1621,13 @@ runInEachFileSystem(() => { declarations: [CmpUser, Dep] }) export class Module {} - `); + `, + ); env.driveMain(); - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1383,7 +1635,8 @@ runInEachFileSystem(() => { template: 'Dep', }) export class Dep {} - `); + `, + ); env.flushWrittenFileTracking(); env.driveMain(); @@ -1407,7 +1660,9 @@ runInEachFileSystem(() => { // SemanticSymbol types for them into different species while ensuring that CmpUser's // template is still valid. The test then verifies that CmpUser is re-emitted. - env.write('cmp-user.ts', ` + env.write( + 'cmp-user.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1415,8 +1670,11 @@ runInEachFileSystem(() => { template: '{{1 | dep}}', }) export class CmpUser {} - `); - env.write('dep.ts', ` + `, + ); + env.write( + 'dep.ts', + ` import {Directive, Pipe} from '@angular/core'; @Directive({ @@ -1430,8 +1688,11 @@ runInEachFileSystem(() => { export class DepB { transform() {} } - `); - env.write('module.ts', ` + `, + ); + env.write( + 'module.ts', + ` import {NgModule} from '@angular/core'; import {CmpUser} from './cmp-user'; import {DepA, DepB} from './dep'; @@ -1440,14 +1701,17 @@ runInEachFileSystem(() => { declarations: [CmpUser, DepA, DepB], }) export class Module {} - `); + `, + ); env.driveMain(); // The annotations on DepA and DepB are swapped. This ensures that when we're comparing the // public API of these symbols to the prior program, the prior symbols are of a different // type (pipe vs directive) than the new symbols, which should lead to a re-emit. - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Directive, Pipe} from '@angular/core'; @Pipe({ @@ -1461,7 +1725,8 @@ runInEachFileSystem(() => { selector: 'dep', }) export class DepB {} - `); + `, + ); env.flushWrittenFileTracking(); env.driveMain(); @@ -1484,7 +1749,9 @@ runInEachFileSystem(() => { // During the test, Dep is changed into a directive, and the test verifies that CmpUser is // not re-emitted (as the public API of a directive and a component are the same). - env.write('cmp-user.ts', ` + env.write( + 'cmp-user.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1492,8 +1759,11 @@ runInEachFileSystem(() => { template: '
', }) export class CmpUser {} - `); - env.write('dep.ts', ` + `, + ); + env.write( + 'dep.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1501,8 +1771,11 @@ runInEachFileSystem(() => { template: 'Dep', }) export class Dep {} - `); - env.write('module.ts', ` + `, + ); + env.write( + 'module.ts', + ` import {NgModule} from '@angular/core'; import {CmpUser} from './cmp-user'; import {Dep} from './dep'; @@ -1511,18 +1784,22 @@ runInEachFileSystem(() => { declarations: [CmpUser, Dep] }) export class Module {} - `); + `, + ); env.driveMain(); - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: '[dep]', }) export class Dep {} - `); + `, + ); env.flushWrittenFileTracking(); env.driveMain(); @@ -1551,7 +1828,9 @@ runInEachFileSystem(() => { // verifies that the NgModule for the components is not re-emitted. env.write('cmp-a-template.html', ``); - env.write('cmp-a.ts', ` + env.write( + 'cmp-a.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1559,9 +1838,12 @@ runInEachFileSystem(() => { templateUrl: './cmp-a-template.html', }) export class MyCmpA {} - `); + `, + ); env.write('cmp-b-template.html', ``); - env.write('cmp-b.ts', ` + env.write( + 'cmp-b.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1569,8 +1851,11 @@ runInEachFileSystem(() => { templateUrl: './cmp-b-template.html', }) export class MyCmpB {} - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {MyCmpA} from './cmp-a'; import {MyCmpB} from './cmp-b'; @@ -1579,7 +1864,8 @@ runInEachFileSystem(() => { declarations: [MyCmpA, MyCmpB], }) export class MyMod {} - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); @@ -1603,7 +1889,9 @@ runInEachFileSystem(() => { // components. The test verifies that the components' NgModule is emitted as a result. env.write('cmp-a-template.html', ``); - env.write('cmp-a.ts', ` + env.write( + 'cmp-a.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1611,9 +1899,12 @@ runInEachFileSystem(() => { templateUrl: './cmp-a-template.html', }) export class MyCmpA {} - `); + `, + ); env.write('cmp-b-template.html', ``); - env.write('cmp-b.ts', ` + env.write( + 'cmp-b.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1621,8 +1912,11 @@ runInEachFileSystem(() => { templateUrl: './cmp-b-template.html', }) export class MyCmpB {} - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {MyCmpA} from './cmp-a'; import {MyCmpB} from './cmp-b'; @@ -1631,7 +1925,8 @@ runInEachFileSystem(() => { declarations: [MyCmpA, MyCmpB], }) export class MyMod {} - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); @@ -1665,7 +1960,9 @@ runInEachFileSystem(() => { // verifies that the components' NgModule is emitted as a result. env.write('cmp-a-template.html', ``); - env.write('cmp-a.ts', ` + env.write( + 'cmp-a.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1673,9 +1970,12 @@ runInEachFileSystem(() => { templateUrl: './cmp-a-template.html', }) export class MyCmpA {} - `); + `, + ); env.write('cmp-b-template.html', ``); - env.write('cmp-b.ts', ` + env.write( + 'cmp-b.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1683,8 +1983,11 @@ runInEachFileSystem(() => { templateUrl: './cmp-b-template.html', }) export class MyCmpB {} - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {MyCmpA} from './cmp-a'; import {MyCmpB} from './cmp-b'; @@ -1693,7 +1996,8 @@ runInEachFileSystem(() => { declarations: [MyCmpA, MyCmpB], }) export class MyMod {} - `); + `, + ); env.driveMain(); // Validate the correctness of the assumption that CmpB will be the remotely scoped @@ -1718,26 +2022,30 @@ runInEachFileSystem(() => { ]); }); - it('should recompile an NgModule when a remotely scoped component\'s scope is changed', - () => { - // Testing setup: MyCmpA and MyCmpB are two components that each consume the other in - // their template, forcing the compiler to utilize remote scoping for MyCmpB (which is - // verified). Dir is a directive which is initially unused by either component. - // - // During the test, MyCmpB is updated to additionally consume Dir in its template. This - // changes the remote scope of MyCmpB, requiring a re-emit of its NgModule which the test - // verifies. - - env.write('dir.ts', ` + it("should recompile an NgModule when a remotely scoped component's scope is changed", () => { + // Testing setup: MyCmpA and MyCmpB are two components that each consume the other in + // their template, forcing the compiler to utilize remote scoping for MyCmpB (which is + // verified). Dir is a directive which is initially unused by either component. + // + // During the test, MyCmpB is updated to additionally consume Dir in its template. This + // changes the remote scope of MyCmpB, requiring a re-emit of its NgModule which the test + // verifies. + + env.write( + 'dir.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: '[dir]', }) export class Dir {} - `); - env.write('cmp-a-template.html', ``); - env.write('cmp-a.ts', ` + `, + ); + env.write('cmp-a-template.html', ``); + env.write( + 'cmp-a.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1745,9 +2053,12 @@ runInEachFileSystem(() => { templateUrl: './cmp-a-template.html', }) export class MyCmpA {} - `); - env.write('cmp-b-template.html', ``); - env.write('cmp-b.ts', ` + `, + ); + env.write('cmp-b-template.html', ``); + env.write( + 'cmp-b.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1755,8 +2066,11 @@ runInEachFileSystem(() => { templateUrl: './cmp-b-template.html', }) export class MyCmpB {} - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {MyCmpA} from './cmp-a'; import {MyCmpB} from './cmp-b'; @@ -1766,32 +2080,32 @@ runInEachFileSystem(() => { declarations: [MyCmpA, MyCmpB, Dir], }) export class MyMod {} - `); - env.driveMain(); - env.flushWrittenFileTracking(); - - // Validate the correctness of the assumption that MyCmpB will be remotely scoped: - const moduleJs = env.getContents('mod.js'); - expect(moduleJs).not.toContain('setComponentScope(MyCmpA,'); - expect(moduleJs).toContain('setComponentScope(MyCmpB,'); + `, + ); + env.driveMain(); + env.flushWrittenFileTracking(); - env.write('cmp-b-template.html', `Update`); + // Validate the correctness of the assumption that MyCmpB will be remotely scoped: + const moduleJs = env.getContents('mod.js'); + expect(moduleJs).not.toContain('setComponentScope(MyCmpA,'); + expect(moduleJs).toContain('setComponentScope(MyCmpB,'); - env.driveMain(); + env.write('cmp-b-template.html', `Update`); - expectToHaveWritten([ - // MyCmpB is written because its template was updated. - '/cmp-b.js', + env.driveMain(); - // MyMod should be written because one of its remotely scoped components has a changed - // scope. - '/mod.js' + expectToHaveWritten([ + // MyCmpB is written because its template was updated. + '/cmp-b.js', - // MyCmpA should not be written because none of its dependencies have changed in their - // public API. - ]); - }); + // MyMod should be written because one of its remotely scoped components has a changed + // scope. + '/mod.js', + // MyCmpA should not be written because none of its dependencies have changed in their + // public API. + ]); + }); it('should recompile an NgModule when its set of remotely scoped components changes', () => { // Testing setup: three components (MyCmpA, MyCmpB, and MyCmpC) are declared. MyCmpA @@ -1804,7 +2118,9 @@ runInEachFileSystem(() => { // component within it now requires remote scoping. env.write('cmp-a-template.html', ` `); - env.write('cmp-a.ts', ` + env.write( + 'cmp-a.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1812,9 +2128,12 @@ runInEachFileSystem(() => { templateUrl: './cmp-a-template.html', }) export class MyCmpA {} - `); + `, + ); env.write('cmp-b-template.html', ``); - env.write('cmp-b.ts', ` + env.write( + 'cmp-b.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1822,10 +2141,13 @@ runInEachFileSystem(() => { templateUrl: './cmp-b-template.html', }) export class MyCmpB {} - `); + `, + ); env.write('cmp-c-template.html', ``); - env.write('cmp-c.ts', ` + env.write( + 'cmp-c.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1833,8 +2155,11 @@ runInEachFileSystem(() => { templateUrl: './cmp-c-template.html', }) export class MyCmpC {} - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {MyCmpA} from './cmp-a'; import {MyCmpB} from './cmp-b'; @@ -1844,7 +2169,8 @@ runInEachFileSystem(() => { declarations: [MyCmpA, MyCmpB, MyCmpC], }) export class MyMod {} - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); @@ -1870,7 +2196,7 @@ runInEachFileSystem(() => { '/cmp-c.js', // MyMod should be written because MyCmpC became remotely scoped - '/mod.js' + '/mod.js', // MyCmpA and MyCmpB should not be written because none of their dependencies have // changed in their public API. @@ -1879,24 +2205,28 @@ runInEachFileSystem(() => { }); describe('NgModule declarations', () => { - it('should recompile components when a matching directive is added in the direct scope', - () => { - // Testing setup: A component Cmp has a template which would match a directive Dir, - // except Dir is not included in Cmp's NgModule. - // - // During the test, Dir is added to the NgModule, causing it to begin matching in Cmp's - // template. The test verifies that Cmp is re-emitted to account for this. - - env.write('dir.ts', ` + it('should recompile components when a matching directive is added in the direct scope', () => { + // Testing setup: A component Cmp has a template which would match a directive Dir, + // except Dir is not included in Cmp's NgModule. + // + // During the test, Dir is added to the NgModule, causing it to begin matching in Cmp's + // template. The test verifies that Cmp is re-emitted to account for this. + + env.write( + 'dir.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: '[dir]', }) export class Dir {} - `); + `, + ); - env.write('cmp.ts', ` + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1904,9 +2234,12 @@ runInEachFileSystem(() => { template: '
', }) export class Cmp {} - `); + `, + ); - env.write('mod.ts', ` + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; @@ -1914,12 +2247,15 @@ runInEachFileSystem(() => { declarations: [Cmp], }) export class Mod {} - `); + `, + ); - env.driveMain(); - env.flushWrittenFileTracking(); + env.driveMain(); + env.flushWrittenFileTracking(); - env.write('mod.ts', ` + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -1928,35 +2264,40 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); - - env.driveMain(); - expectToHaveWritten([ - // Mod is written as it was directly changed. - '/mod.js', - - // Cmp is written as a matching directive was added to Mod's scope. - '/cmp.js', - ]); - }); - - it('should recompile components when a matching directive is removed from the direct scope', - () => { - // Testing setup: Cmp is a component with a template that matches a directive Dir. - // - // During the test, Dir is removed from Cmp's NgModule, which causes it to stop matching - // in Cmp's template. The test verifies that Cmp is re-emitted as a result. - - env.write('dir.ts', ` + `, + ); + + env.driveMain(); + expectToHaveWritten([ + // Mod is written as it was directly changed. + '/mod.js', + + // Cmp is written as a matching directive was added to Mod's scope. + '/cmp.js', + ]); + }); + + it('should recompile components when a matching directive is removed from the direct scope', () => { + // Testing setup: Cmp is a component with a template that matches a directive Dir. + // + // During the test, Dir is removed from Cmp's NgModule, which causes it to stop matching + // in Cmp's template. The test verifies that Cmp is re-emitted as a result. + + env.write( + 'dir.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: '[dir]', }) export class Dir {} - `); + `, + ); - env.write('cmp.ts', ` + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1964,9 +2305,12 @@ runInEachFileSystem(() => { template: '
', }) export class Cmp {} - `); + `, + ); - env.write('mod.ts', ` + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -1975,12 +2319,15 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); + `, + ); - env.driveMain(); - env.flushWrittenFileTracking(); + env.driveMain(); + env.flushWrittenFileTracking(); - env.write('mod.ts', ` + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; @@ -1988,37 +2335,42 @@ runInEachFileSystem(() => { declarations: [Cmp], }) export class Mod {} - `); - - env.driveMain(); - expectToHaveWritten([ - // Mod is written as it was directly changed. - '/mod.js', - - // Cmp is written as a matching directive was removed from Mod's scope. - '/cmp.js', - ]); - }); - - it('should recompile components when a matching directive is added in the transitive scope', - () => { - // Testing setup: A component Cmp has a template which would match a directive Dir, - // except Dir is not included in Cmp's NgModule. - // - // During the test, Dir is added to the NgModule via an import, causing it to begin - // matching in Cmp's template. The test verifies that Cmp is re-emitted to account for - // this. - - env.write('dir.ts', ` + `, + ); + + env.driveMain(); + expectToHaveWritten([ + // Mod is written as it was directly changed. + '/mod.js', + + // Cmp is written as a matching directive was removed from Mod's scope. + '/cmp.js', + ]); + }); + + it('should recompile components when a matching directive is added in the transitive scope', () => { + // Testing setup: A component Cmp has a template which would match a directive Dir, + // except Dir is not included in Cmp's NgModule. + // + // During the test, Dir is added to the NgModule via an import, causing it to begin + // matching in Cmp's template. The test verifies that Cmp is re-emitted to account for + // this. + + env.write( + 'dir.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: '[dir]', }) export class Dir {} - `); + `, + ); - env.write('cmp.ts', ` + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -2026,9 +2378,12 @@ runInEachFileSystem(() => { template: '
', }) export class Cmp {} - `); + `, + ); - env.write('deep.ts', ` + env.write( + 'deep.ts', + ` import {NgModule} from '@angular/core'; @NgModule({ @@ -2036,9 +2391,12 @@ runInEachFileSystem(() => { exports: [], }) export class Deep {} - `); + `, + ); - env.write('mod.ts', ` + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Deep} from './deep'; @@ -2048,13 +2406,15 @@ runInEachFileSystem(() => { imports: [Deep], }) export class Mod {} - `); - - env.driveMain(); - env.flushWrittenFileTracking(); + `, + ); + env.driveMain(); + env.flushWrittenFileTracking(); - env.write('deep.ts', ` + env.write( + 'deep.ts', + ` import {NgModule} from '@angular/core'; import {Dir} from './dir'; @@ -2063,39 +2423,44 @@ runInEachFileSystem(() => { exports: [Dir], }) export class Deep {} - `); - - env.driveMain(); - expectToHaveWritten([ - // Mod is written as it was directly changed. - '/deep.js', - - // Mod is written as its direct dependency (Deep) was changed. - '/mod.js', - - // Cmp is written as a matching directive was added to Mod's transitive scope. - '/cmp.js', - ]); - }); - - it('should recompile components when a matching directive is removed from the transitive scope', - () => { - // Testing setup: Cmp is a component with a template that matches a directive Dir, due to - // Dir's NgModule being imported into Cmp's NgModule. - // - // During the test, this import link is removed, which causes Dir to stop matching in - // Cmp's template. The test verifies that Cmp is re-emitted as a result. - - env.write('dir.ts', ` + `, + ); + + env.driveMain(); + expectToHaveWritten([ + // Mod is written as it was directly changed. + '/deep.js', + + // Mod is written as its direct dependency (Deep) was changed. + '/mod.js', + + // Cmp is written as a matching directive was added to Mod's transitive scope. + '/cmp.js', + ]); + }); + + it('should recompile components when a matching directive is removed from the transitive scope', () => { + // Testing setup: Cmp is a component with a template that matches a directive Dir, due to + // Dir's NgModule being imported into Cmp's NgModule. + // + // During the test, this import link is removed, which causes Dir to stop matching in + // Cmp's template. The test verifies that Cmp is re-emitted as a result. + + env.write( + 'dir.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: '[dir]', }) export class Dir {} - `); + `, + ); - env.write('cmp.ts', ` + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -2103,9 +2468,12 @@ runInEachFileSystem(() => { template: '
', }) export class Cmp {} - `); + `, + ); - env.write('deep.ts', ` + env.write( + 'deep.ts', + ` import {NgModule} from '@angular/core'; import {Dir} from './dir'; @@ -2114,9 +2482,12 @@ runInEachFileSystem(() => { exports: [Dir], }) export class Deep {} - `); + `, + ); - env.write('mod.ts', ` + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Deep} from './deep'; @@ -2126,12 +2497,15 @@ runInEachFileSystem(() => { imports: [Deep], }) export class Mod {} - `); + `, + ); - env.driveMain(); - env.flushWrittenFileTracking(); + env.driveMain(); + env.flushWrittenFileTracking(); - env.write('deep.ts', ` + env.write( + 'deep.ts', + ` import {NgModule} from '@angular/core'; @NgModule({ @@ -2139,20 +2513,21 @@ runInEachFileSystem(() => { exports: [], }) export class Deep {} - `); + `, + ); - env.driveMain(); - expectToHaveWritten([ - // Mod is written as it was directly changed. - '/deep.js', + env.driveMain(); + expectToHaveWritten([ + // Mod is written as it was directly changed. + '/deep.js', - // Mod is written as its direct dependency (Deep) was changed. - '/mod.js', + // Mod is written as its direct dependency (Deep) was changed. + '/mod.js', - // Cmp is written as a matching directive was removed from Mod's transitive scope. - '/cmp.js', - ]); - }); + // Cmp is written as a matching directive was removed from Mod's transitive scope. + '/cmp.js', + ]); + }); it('should not recompile components when a non-matching directive is added in scope', () => { // Testing setup: A component Cmp has a template which does not match a directive Dir, @@ -2162,16 +2537,21 @@ runInEachFileSystem(() => { // However, Dir still does not match the template. The test verifies that Cmp is not // re-emitted. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: '[dir]', }) export class Dir {} - `); + `, + ); - env.write('cmp.ts', ` + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -2179,9 +2559,12 @@ runInEachFileSystem(() => { template: '
', }) export class Cmp {} - `); + `, + ); - env.write('mod.ts', ` + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; @@ -2189,12 +2572,15 @@ runInEachFileSystem(() => { declarations: [Cmp], }) export class Mod {} - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); - env.write('mod.ts', ` + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -2203,7 +2589,8 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); + `, + ); env.driveMain(); expectToHaveWritten([ @@ -2217,20 +2604,21 @@ runInEachFileSystem(() => { }); describe('error recovery', () => { - it('should recompile a component when a matching directive is added that first contains an error', - () => { - // Testing setup: Cmp is a component which would match a directive with the selector - // '[dir]'. - // - // During the test, an initial incremental compilation adds an import to a hypothetical - // directive Dir to the NgModule, and adds Dir as a declaration. However, the import - // points to a non-existent file. - // - // During a second incremental compilation, that missing file is added with a declaration - // for Dir as a directive with the selector '[dir]', causing it to begin matching in - // Cmp's template. The test verifies that Cmp is re-emitted once the program is correct. - - env.write('cmp.ts', ` + it('should recompile a component when a matching directive is added that first contains an error', () => { + // Testing setup: Cmp is a component which would match a directive with the selector + // '[dir]'. + // + // During the test, an initial incremental compilation adds an import to a hypothetical + // directive Dir to the NgModule, and adds Dir as a declaration. However, the import + // points to a non-existent file. + // + // During a second incremental compilation, that missing file is added with a declaration + // for Dir as a directive with the selector '[dir]', causing it to begin matching in + // Cmp's template. The test verifies that Cmp is re-emitted once the program is correct. + + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -2238,9 +2626,12 @@ runInEachFileSystem(() => { template: '
', }) export class Cmp {} - `); + `, + ); - env.write('mod.ts', ` + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; @@ -2248,12 +2639,15 @@ runInEachFileSystem(() => { declarations: [Cmp], }) export class Mod {} - `); + `, + ); - env.driveMain(); - env.flushWrittenFileTracking(); + env.driveMain(); + env.flushWrittenFileTracking(); - env.write('mod.ts', ` + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -2262,35 +2656,39 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); + `, + ); - expect(env.driveDiagnostics().length).not.toBe(0); + expect(env.driveDiagnostics().length).not.toBe(0); - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: '[dir]', }) export class Dir {} - `); + `, + ); - env.flushWrittenFileTracking(); - env.driveMain(); + env.flushWrittenFileTracking(); + env.driveMain(); - expectToHaveWritten([ - // Mod is written as it was changed in the first incremental compilation, but had - // errors and so was not written then. - '/mod.js', + expectToHaveWritten([ + // Mod is written as it was changed in the first incremental compilation, but had + // errors and so was not written then. + '/mod.js', - // Dir is written as it was added in the second incremental compilation. - '/dir.js', + // Dir is written as it was added in the second incremental compilation. + '/dir.js', - // Cmp is written as the cumulative effect of the two changes was to add Dir to its - // scope and thus match in Cmp's template. - '/cmp.js', - ]); - }); + // Cmp is written as the cumulative effect of the two changes was to add Dir to its + // scope and thus match in Cmp's template. + '/cmp.js', + ]); + }); }); it('should correctly emit components when public API changes during a broken program', () => { @@ -2304,10 +2702,15 @@ runInEachFileSystem(() => { // A second incremental compilation then fixes the invalid syntax, and the test verifies that // Cmp is re-emitted due to the earlier public API change to Dir. - env.write('other.ts', ` + env.write( + 'other.ts', + ` export const FOO = true; - `); - env.write('dir.ts', ` + `, + ); + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -2317,8 +2720,11 @@ runInEachFileSystem(() => { @Input() dirIn!: string; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; import './other'; @@ -2327,9 +2733,12 @@ runInEachFileSystem(() => { template: '
', }) export class Cmp {} - `); + `, + ); - env.write('mod.ts', ` + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -2338,12 +2747,15 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -2353,17 +2765,24 @@ runInEachFileSystem(() => { @Input() dirIn_changed!: string; } - `); + `, + ); - env.write('other.ts', ` + env.write( + 'other.ts', + ` export const FOO = ; - `); + `, + ); expect(env.driveDiagnostics().length).not.toBe(0); env.flushWrittenFileTracking(); - env.write('other.ts', ` + env.write( + 'other.ts', + ` export const FOO = false; - `); + `, + ); env.driveMain(); expectToHaveWritten([ diff --git a/packages/compiler-cli/test/ngtsc/incremental_spec.ts b/packages/compiler-cli/test/ngtsc/incremental_spec.ts index 66b58b52b95be..ad5ffeb4019ce 100644 --- a/packages/compiler-cli/test/ngtsc/incremental_spec.ts +++ b/packages/compiler-cli/test/ngtsc/incremental_spec.ts @@ -25,12 +25,15 @@ runInEachFileSystem(() => { }); it('should not crash if CLI does not provide getModifiedResourceFiles()', () => { - env.write('component1.ts', ` + env.write( + 'component1.ts', + ` import {Component} from '@angular/core'; @Component({selector: 'cmp', templateUrl: './component1.template.html'}) export class Cmp1 {} - `); + `, + ); env.write('component1.template.html', 'cmp1'); env.driveMain(); @@ -42,13 +45,18 @@ runInEachFileSystem(() => { }); it('should skip unchanged services', () => { - env.write('service.ts', ` + env.write( + 'service.ts', + ` import {Injectable} from '@angular/core'; @Injectable() export class Service {} - `); - env.write('test.ts', ` + `, + ); + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; import {Service} from './service'; @@ -56,7 +64,8 @@ runInEachFileSystem(() => { export class Cmp { constructor(service: Service) {} } - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); @@ -72,18 +81,24 @@ runInEachFileSystem(() => { it('should rebuild components that have changed', () => { env.tsconfig({strictTemplates: true}); - env.write('component1.ts', ` + env.write( + 'component1.ts', + ` import {Component} from '@angular/core'; @Component({selector: 'cmp', template: 'cmp'}) export class Cmp1 {} - `); - env.write('component2.ts', ` + `, + ); + env.write( + 'component2.ts', + ` import {Component} from '@angular/core'; @Component({selector: 'cmp2', template: 'cmp'}) export class Cmp2 {} - `); + `, + ); env.driveMain(); // Pretend a change was made to Cmp1 @@ -96,19 +111,25 @@ runInEachFileSystem(() => { }); it('should rebuild components whose templates have changed', () => { - env.write('component1.ts', ` + env.write( + 'component1.ts', + ` import {Component} from '@angular/core'; @Component({selector: 'cmp', templateUrl: './component1.template.html'}) export class Cmp1 {} - `); + `, + ); env.write('component1.template.html', 'cmp1'); - env.write('component2.ts', ` + env.write( + 'component2.ts', + ` import {Component} from '@angular/core'; @Component({selector: 'cmp2', templateUrl: './component2.template.html'}) export class Cmp2 {} - `); + `, + ); env.write('component2.template.html', 'cmp2'); env.driveMain(); @@ -123,22 +144,31 @@ runInEachFileSystem(() => { }); it('should rebuild components whose partial-evaluation dependencies have changed', () => { - env.write('component1.ts', ` + env.write( + 'component1.ts', + ` import {Component} from '@angular/core'; @Component({selector: 'cmp', template: 'cmp'}) export class Cmp1 {} - `); - env.write('component2.ts', ` + `, + ); + env.write( + 'component2.ts', + ` import {Component} from '@angular/core'; import {SELECTOR} from './constants'; @Component({selector: SELECTOR, template: 'cmp'}) export class Cmp2 {} - `); - env.write('constants.ts', ` + `, + ); + env.write( + 'constants.ts', + ` export const SELECTOR = 'cmp'; - `); + `, + ); env.driveMain(); // Pretend a change was made to SELECTOR @@ -155,57 +185,77 @@ runInEachFileSystem(() => { setupFooBarProgram(env); // Pretend a change was made to BarDir. - env.write('bar_directive.ts', ` + env.write( + 'bar_directive.ts', + ` import {Directive} from '@angular/core'; @Directive({selector: '[barr]'}) export class BarDir {} - `); + `, + ); env.driveMain(); let written = env.getFilesWrittenSinceLastFlush(); expect(written).toContain('/bar_directive.js'); expect(written).toContain('/bar_component.js'); expect(written).toContain('/bar_module.js'); - expect(written).not.toContain('/foo_component.js'); // BarDir is not exported by BarModule, - // so upstream NgModule is not affected + expect(written).not.toContain('/foo_component.js'); // BarDir is not exported by BarModule, + // so upstream NgModule is not affected expect(written).not.toContain('/foo_pipe.js'); expect(written).not.toContain('/foo_module.js'); }); // https://github.com/angular/angular/issues/32416 it('should rebuild full NgModule scope when a dependency of a declaration has changed', () => { - env.write('component1.ts', ` + env.write( + 'component1.ts', + ` import {Component} from '@angular/core'; import {SELECTOR} from './dep'; @Component({selector: SELECTOR, template: 'cmp'}) export class Cmp1 {} - `); - env.write('component2.ts', ` + `, + ); + env.write( + 'component2.ts', + ` import {Component} from '@angular/core'; @Component({selector: 'cmp2', template: ''}) export class Cmp2 {} - `); - env.write('dep.ts', ` + `, + ); + env.write( + 'dep.ts', + ` export const SELECTOR = 'cmp'; - `); - env.write('directive.ts', ` + `, + ); + env.write( + 'directive.ts', + ` import {Directive} from '@angular/core'; @Directive({selector: 'dir'}) export class Dir {} - `); - env.write('pipe.ts', ` + `, + ); + env.write( + 'pipe.ts', + ` import {Pipe} from '@angular/core'; @Pipe({name: 'myPipe'}) export class MyPipe { transform() {} } - `); - env.write('module.ts', ` + `, + ); + env.write( + 'module.ts', + ` import {NgModule, NO_ERRORS_SCHEMA} from '@angular/core'; import {Cmp1} from './component1'; import {Cmp2} from './component2'; @@ -214,15 +264,19 @@ runInEachFileSystem(() => { @NgModule({declarations: [Cmp1, Cmp2, Dir, MyPipe], schemas: [NO_ERRORS_SCHEMA]}) export class Mod {} - `); + `, + ); env.driveMain(); // Pretend a change was made to 'dep'. Since the selector is updated this affects the NgModule // scope, so all components in the module scope need to be recompiled. env.flushWrittenFileTracking(); - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` export const SELECTOR = 'cmp_updated'; - `); + `, + ); env.driveMain(); const written = env.getFilesWrittenSinceLastFlush(); expect(written).not.toContain('/directive.js'); @@ -237,14 +291,17 @@ runInEachFileSystem(() => { setupFooBarProgram(env); // Rename the pipe so components that use it need to be recompiled. - env.write('foo_pipe.ts', ` + env.write( + 'foo_pipe.ts', + ` import {Pipe} from '@angular/core'; @Pipe({name: 'foo_changed'}) export class FooPipe { transform() {} } - `); + `, + ); env.driveMain(); const written = env.getFilesWrittenSinceLastFlush(); expect(written).not.toContain('/bar_directive.js'); @@ -259,7 +316,9 @@ runInEachFileSystem(() => { setupFooBarProgram(env); // Pretend a change was made to FooModule. - env.write('foo_module.ts', ` + env.write( + 'foo_module.ts', + ` import {NgModule} from '@angular/core'; import {FooCmp} from './foo_component'; import {FooPipe} from './foo_pipe'; @@ -269,7 +328,8 @@ runInEachFileSystem(() => { imports: [BarModule], }) export class FooModule {} - `); + `, + ); env.driveMain(); const written = env.getFilesWrittenSinceLastFlush(); expect(written).not.toContain('/bar_directive.js'); @@ -291,7 +351,9 @@ runInEachFileSystem(() => { // Since the dependency from EntryModule on the contents of MiddleBModule is not "direct" // (meaning MiddleBModule is not discovered during analysis of EntryModule), this test is // verifying that NgModule dependency tracking correctly accounts for this situation. - env.write('entry.ts', ` + env.write( + 'entry.ts', + ` import {Component, NgModule} from '@angular/core'; import {MiddleAModule} from './middle-a'; @@ -306,8 +368,11 @@ runInEachFileSystem(() => { imports: [MiddleAModule], }) export class EntryModule {} - `); - env.write('middle-a.ts', ` + `, + ); + env.write( + 'middle-a.ts', + ` import {NgModule} from '@angular/core'; import {MiddleBModule} from './middle-b'; @@ -315,14 +380,20 @@ runInEachFileSystem(() => { exports: [MiddleBModule], }) export class MiddleAModule {} - `); - env.write('middle-b.ts', ` + `, + ); + env.write( + 'middle-b.ts', + ` import {NgModule} from '@angular/core'; @NgModule({}) export class MiddleBModule {} - `); - env.write('dir_module.ts', ` + `, + ); + env.write( + 'dir_module.ts', + ` import {NgModule} from '@angular/core'; import {Dir} from './dir'; @@ -331,20 +402,26 @@ runInEachFileSystem(() => { exports: [Dir], }) export class DirModule {} - `); - env.write('dir.ts', ` + `, + ); + env.write( + 'dir.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: '[dir]', }) export class Dir {} - `); + `, + ); env.driveMain(); expect(env.getContents('entry.js')).not.toContain('Dir'); - env.write('middle-b.ts', ` + env.write( + 'middle-b.ts', + ` import {NgModule} from '@angular/core'; import {DirModule} from './dir_module'; @@ -352,7 +429,8 @@ runInEachFileSystem(() => { exports: [DirModule], }) export class MiddleBModule {} - `); + `, + ); env.driveMain(); expect(env.getContents('entry.js')).toContain('Dir'); @@ -366,7 +444,9 @@ runInEachFileSystem(() => { // // This is a tricky scenario due to the backwards dependency arrow from a component to its // module. - env.write('dep.ts', ` + env.write( + 'dep.ts', + ` import {Directive, NgModule} from '@angular/core'; @Directive({selector: '[dep]'}) @@ -377,9 +457,12 @@ runInEachFileSystem(() => { exports: [DepDir], }) export class DepModule {} - `); + `, + ); - env.write('cmp.ts', ` + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -387,9 +470,12 @@ runInEachFileSystem(() => { template: '
', }) export class Cmp {} - `); + `, + ); - env.write('module.ts', ` + env.write( + 'module.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {DepModule} from './dep'; @@ -399,13 +485,16 @@ runInEachFileSystem(() => { imports: [DepModule], }) export class Module {} - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); // Remove the component from the module and recompile. - env.write('module.ts', ` + env.write( + 'module.ts', + ` import {NgModule} from '@angular/core'; import {DepModule} from './dep'; @@ -414,7 +503,8 @@ runInEachFileSystem(() => { imports: [DepModule], }) export class Module {} - `); + `, + ); env.driveMain(); @@ -424,25 +514,23 @@ runInEachFileSystem(() => { expect(env.getContents('cmp.js')).not.toContain('DepDir'); }); - it('should rebuild only a Component (but with the correct CompilationScope) if its template has changed', - () => { - setupFooBarProgram(env); - - // Make a change to the template of BarComponent. - env.write('bar_component.html', '
changed
'); - - env.driveMain(); - const written = env.getFilesWrittenSinceLastFlush(); - expect(written).not.toContain('/bar_directive.js'); - expect(written).toContain('/bar_component.js'); - expect(written).not.toContain('/bar_module.js'); - expect(written).not.toContain('/foo_component.js'); - expect(written).not.toContain('/foo_pipe.js'); - expect(written).not.toContain('/foo_module.js'); - // Ensure that the used directives are included in the component's generated template. - expect(env.getContents('/built/bar_component.js')) - .toMatch(/dependencies:\s*\[.+\.BarDir\]/); - }); + it('should rebuild only a Component (but with the correct CompilationScope) if its template has changed', () => { + setupFooBarProgram(env); + + // Make a change to the template of BarComponent. + env.write('bar_component.html', '
changed
'); + + env.driveMain(); + const written = env.getFilesWrittenSinceLastFlush(); + expect(written).not.toContain('/bar_directive.js'); + expect(written).toContain('/bar_component.js'); + expect(written).not.toContain('/bar_module.js'); + expect(written).not.toContain('/foo_component.js'); + expect(written).not.toContain('/foo_pipe.js'); + expect(written).not.toContain('/foo_module.js'); + // Ensure that the used directives are included in the component's generated template. + expect(env.getContents('/built/bar_component.js')).toMatch(/dependencies:\s*\[.+\.BarDir\]/); + }); it('should rebuild everything if a typings file changes', () => { setupFooBarProgram(env); @@ -460,7 +548,9 @@ runInEachFileSystem(() => { }); it('should re-emit an NgModule when the provider status of its imports changes', () => { - env.write('provider-dep.ts', ` + env.write( + 'provider-dep.ts', + ` import {Injectable, NgModule} from '@angular/core'; @Injectable() @@ -470,8 +560,11 @@ runInEachFileSystem(() => { providers: [Service], }) export class DepModule {} - `); - env.write('standalone-cmp.ts', ` + `, + ); + env.write( + 'standalone-cmp.ts', + ` import {Component} from '@angular/core'; import {DepModule} from './provider-dep'; @@ -481,8 +574,11 @@ runInEachFileSystem(() => { imports: [DepModule], }) export class Cmp {} - `); - env.write('module.ts', ` + `, + ); + env.write( + 'module.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './standalone-cmp'; @@ -490,16 +586,20 @@ runInEachFileSystem(() => { imports: [Cmp], }) export class Module {} - `); + `, + ); env.driveMain(); - env.write('provider-dep.ts', ` + env.write( + 'provider-dep.ts', + ` import {Injectable, NgModule} from '@angular/core'; @NgModule({}) export class DepModule {} - `); + `, + ); env.flushWrittenFileTracking(); env.driveMain(); @@ -509,12 +609,15 @@ runInEachFileSystem(() => { it('should compile incrementally with template type-checking turned on', () => { env.tsconfig({fullTemplateTypeCheck: true}); - env.write('main.ts', ` + env.write( + 'main.ts', + ` import {Component} from '@angular/core'; @Component({template: ''}) export class MyComponent {} - `); + `, + ); env.driveMain(); env.invalidateCachedFile('main.ts'); env.driveMain(); @@ -529,14 +632,17 @@ runInEachFileSystem(() => { // in the original program and the incremental program. env.tsconfig({fullTemplateTypeCheck: true}); env.write('node_modules/@types/node/index.d.ts', 'declare var require: any;'); - env.write('main.ts', ` + env.write( + 'main.ts', + ` import {Component} from '@angular/core'; require('path'); @Component({template: ''}) export class MyComponent {} - `); + `, + ); env.driveMain(); env.invalidateCachedFile('main.ts'); const diags = env.driveDiagnostics(); @@ -561,14 +667,17 @@ runInEachFileSystem(() => { env.write('node_modules/b/node_modules/a/package.json', `{"name": "a", "version": "1.0"}`); env.write('node_modules/b/index.js', `export {ServiceA as ServiceB} from 'a';`); env.write('node_modules/b/index.d.ts', `export {ServiceA as ServiceB} from 'a';`); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; import {ServiceA} from 'a'; import {ServiceB} from 'b'; @Component({template: ''}) export class MyComponent {} - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); @@ -595,34 +704,43 @@ runInEachFileSystem(() => { env.write('node_modules/b/node_modules/a/package.json', `{"name": "a", "version": "1.0"}`); env.write('node_modules/b/index.js', `export {ServiceA as ServiceB} from 'a';`); env.write('node_modules/b/index.d.ts', `export {ServiceA as ServiceB} from 'a';`); - env.write('component1.ts', ` + env.write( + 'component1.ts', + ` import {Component} from '@angular/core'; import {ServiceA} from 'a'; import {ServiceB} from 'b'; @Component({selector: 'cmp', template: 'cmp'}) export class Cmp1 {} - `); - env.write('component2.ts', ` + `, + ); + env.write( + 'component2.ts', + ` import {Component} from '@angular/core'; import {ServiceA} from 'a'; import {ServiceB} from 'b'; @Component({selector: 'cmp2', template: 'cmp'}) export class Cmp2 {} - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); // Now update `component1.ts` and change its imports to avoid complete structure reuse, which // forces recreation of source file redirects. - env.write('component1.ts', ` + env.write( + 'component1.ts', + ` import {Component} from '@angular/core'; import {ServiceA} from 'a'; @Component({selector: 'cmp', template: 'cmp'}) export class Cmp1 {} - `); + `, + ); env.driveMain(); const written = env.getFilesWrittenSinceLastFlush(); @@ -641,7 +759,9 @@ runInEachFileSystem(() => { // - a general type error // - an unmatched binding // - a DOM schema error - env.write('component.ts', ` + env.write( + 'component.ts', + ` import {Component} from '@angular/core'; @Component({ selector: 'test-cmp', @@ -652,7 +772,8 @@ runInEachFileSystem(() => { \`, }) export class TestCmp {} - `); + `, + ); let diags = env.driveDiagnostics(); // Should get a diagnostic for each line in the template. @@ -676,12 +797,17 @@ runInEachFileSystem(() => { // a property of a component. The interface is then changed in a subsequent compilation in // a way that introduces a type error in the template. The test verifies the resulting // diagnostic is produced. - env.write('iface.ts', ` + env.write( + 'iface.ts', + ` export interface SomeType { field: string; } - `); - env.write('component.ts', ` + `, + ); + env.write( + 'component.ts', + ` import {Component} from '@angular/core'; import {SomeType} from './iface'; @@ -696,17 +822,21 @@ runInEachFileSystem(() => { return param; } } - `); + `, + ); expect(env.driveDiagnostics().length).toBe(0); env.flushWrittenFileTracking(); // Change the interface. - env.write('iface.ts', ` + env.write( + 'iface.ts', + ` export interface SomeType { field: number; } - `); + `, + ); expect(env.driveDiagnostics().length).toBe(1); }); @@ -717,13 +847,18 @@ runInEachFileSystem(() => { // updates `TestDir` and changes its inputs, thereby triggering re-emit of `TestCmp` without // performing re-analysis of `TestCmp`. The output of the re-emitted file for `TestCmp` // should continue to have retained the default import. - env.write('service.ts', ` + env.write( + 'service.ts', + ` import {Injectable} from '@angular/core'; @Injectable({ providedIn: 'root' }) export default class DefaultService {} - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component, Directive} from '@angular/core'; import DefaultService from './service'; @@ -733,33 +868,43 @@ runInEachFileSystem(() => { export class TestCmp { constructor(service: DefaultService) {} } - `); - env.write('dir.ts', ` + `, + ); + env.write( + 'dir.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: '[dir]' }) export class TestDir {} - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {TestDir} from './dir'; import {TestCmp} from './cmp'; @NgModule({ declarations: [TestDir, TestCmp] }) export class TestMod {} - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); // Update `TestDir` to change its inputs, triggering a re-emit of `TestCmp` that uses // `TestDir`. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: '[dir]', inputs: ['added'] }) export class TestDir {} - `); + `, + ); env.driveMain(); // Verify that `TestCmp` was indeed re-emitted. @@ -780,7 +925,9 @@ runInEachFileSystem(() => { // In the test, during the incremental rebuild Dir is exported from ModuleB. This is a // change to the scope of ModuleA made by changing ModuleB (hence, a "remote change"). The // test verifies that incremental template type-checking. - env.write('cmp.ts', ` + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -788,8 +935,11 @@ runInEachFileSystem(() => { template: '
', }) export class Cmp {} - `); - env.write('module-a.ts', ` + `, + ); + env.write( + 'module-a.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {ModuleB} from './module-b'; @@ -799,10 +949,13 @@ runInEachFileSystem(() => { imports: [ModuleB], }) export class ModuleA {} - `); + `, + ); // Declare ModuleB and a directive Dir, but ModuleB does not yet export Dir. - env.write('module-b.ts', ` + env.write( + 'module-b.ts', + ` import {NgModule} from '@angular/core'; import {Dir} from './dir'; @@ -810,21 +963,24 @@ runInEachFileSystem(() => { declarations: [Dir], }) export class ModuleB {} - `); - env.write('dir.ts', ` + `, + ); + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({selector: '[dir]'}) export class Dir { @Input() someInput!: any; } - `); + `, + ); let diags = env.driveDiagnostics(); // Should get a diagnostic about [dir] not being a valid binding. expect(diags.length).toBe(1); - // As a precautionary check, run the build a second time with no changes, to ensure the // diagnostic is repeated. diags = env.driveDiagnostics(); @@ -832,7 +988,9 @@ runInEachFileSystem(() => { expect(diags.length).toBe(1); // Modify ModuleB to now export Dir. - env.write('module-b.ts', ` + env.write( + 'module-b.ts', + ` import {NgModule} from '@angular/core'; import {Dir} from './dir'; @@ -841,7 +999,8 @@ runInEachFileSystem(() => { exports: [Dir], }) export class ModuleB {} - `); + `, + ); diags = env.driveDiagnostics(); // Diagnostic should be gone, as the error has been fixed. @@ -856,7 +1015,9 @@ runInEachFileSystem(() => { // each build. This test verifies that the above mechanism works properly, by performing // type-checking on an unexported class (not exporting the class forces the inline // checking de-optimization). - env.write('cmp.ts', ` + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -864,7 +1025,8 @@ runInEachFileSystem(() => { template: '{{test}}', }) class Cmp {} - `); + `, + ); // On the first compilation, an error should be produced. let diags = env.driveDiagnostics(); @@ -878,7 +1040,9 @@ runInEachFileSystem(() => { expect(diags.length).toBe(1); // Now, correct the error by adding the 'test' property to the component. - env.write('cmp.ts', ` + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -888,7 +1052,8 @@ runInEachFileSystem(() => { class Cmp { test!: string; } - `); + `, + ); env.driveMain(); @@ -904,7 +1069,9 @@ runInEachFileSystem(() => { // checking cannot be performed incrementally. This test verifies that such cases still // work correctly, by repeatedly performing diagnostics on a component which depends on a // directive with an inlined type constructor. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; export interface Keys { alpha: string; @@ -918,9 +1085,12 @@ runInEachFileSystem(() => { // constructor. @Input() dir: T; } - `); + `, + ); - env.write('cmp.ts', ` + env.write( + 'cmp.ts', + ` import {Component, NgModule} from '@angular/core'; import {Dir} from './dir'; @Component({ @@ -932,18 +1102,21 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Module {} - `); + `, + ); let diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toContain(`Type '"gamma"' is not assignable to type 'keyof Keys'.`); + expect(diags[0].messageText).toContain( + `Type '"gamma"' is not assignable to type 'keyof Keys'.`, + ); // On a rebuild, the same errors should be present. diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toContain(`Type '"gamma"' is not assignable to type 'keyof Keys'.`); + expect(diags[0].messageText).toContain( + `Type '"gamma"' is not assignable to type 'keyof Keys'.`, + ); }); it('should not re-emit files that need inline type constructors', () => { @@ -952,7 +1125,9 @@ runInEachFileSystem(() => { // causes an updated dir.ts to be used in the type-check program, which should not // confuse the incremental engine in undesirably considering dir.ts as affected in // incremental rebuilds. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; interface Keys { alpha: string; @@ -964,9 +1139,12 @@ runInEachFileSystem(() => { export class Dir { @Input() dir: T; } - `); + `, + ); - env.write('cmp.ts', ` + env.write( + 'cmp.ts', + ` import {Component, NgModule} from '@angular/core'; import {Dir} from './dir'; @Component({ @@ -978,7 +1156,8 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Module {} - `); + `, + ); env.driveMain(); // Trigger a recompile by changing cmp.ts. @@ -997,7 +1176,9 @@ runInEachFileSystem(() => { describe('missing resource files', () => { it('should re-analyze a component if a template file becomes available later', () => { - env.write('app.ts', ` + env.write( + 'app.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1005,21 +1186,27 @@ runInEachFileSystem(() => { templateUrl: './some-template.html', }) export class AppComponent {} - `); + `, + ); const firstDiagnostics = env.driveDiagnostics(); expect(firstDiagnostics.length).toBe(1); expect(firstDiagnostics[0].code).toBe(ngErrorCode(ErrorCode.COMPONENT_RESOURCE_NOT_FOUND)); - env.write('some-template.html', ` + env.write( + 'some-template.html', + ` Test - `); + `, + ); env.driveMain(); }); it('should re-analyze if component style file becomes available later', () => { - env.write('app.ts', ` + env.write( + 'app.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1028,7 +1215,8 @@ runInEachFileSystem(() => { styleUrls: ['./some-style.css'], }) export class AppComponent {} - `); + `, + ); const firstDiagnostics = env.driveDiagnostics(); expect(firstDiagnostics.length).toBe(1); @@ -1041,7 +1229,9 @@ runInEachFileSystem(() => { }); function setupFooBarProgram(env: NgtscTestEnvironment) { - env.write('foo_component.ts', ` + env.write( + 'foo_component.ts', + ` import {Component} from '@angular/core'; import {fooSelector} from './foo_selector'; @@ -1050,16 +1240,22 @@ runInEachFileSystem(() => { template: '{{ 1 | foo }}' }) export class FooCmp {} - `); - env.write('foo_pipe.ts', ` + `, + ); + env.write( + 'foo_pipe.ts', + ` import {Pipe} from '@angular/core'; @Pipe({name: 'foo'}) export class FooPipe { transform() {} } - `); - env.write('foo_module.ts', ` + `, + ); + env.write( + 'foo_module.ts', + ` import {NgModule} from '@angular/core'; import {FooCmp} from './foo_component'; import {FooPipe} from './foo_pipe'; @@ -1069,29 +1265,41 @@ runInEachFileSystem(() => { imports: [BarModule], }) export class FooModule {} - `); - env.write('bar_component.ts', ` + `, + ); + env.write( + 'bar_component.ts', + ` import {Component} from '@angular/core'; @Component({selector: 'bar', templateUrl: './bar_component.html'}) export class BarCmp {} - `); + `, + ); env.write('bar_component.html', '
'); - env.write('bar_directive.ts', ` + env.write( + 'bar_directive.ts', + ` import {Directive} from '@angular/core'; @Directive({selector: '[bar]'}) export class BarDir {} - `); - env.write('bar_pipe.ts', ` + `, + ); + env.write( + 'bar_pipe.ts', + ` import {Pipe} from '@angular/core'; @Pipe({name: 'foo'}) export class BarPipe { transform() {} } - `); - env.write('bar_module.ts', ` + `, + ); + env.write( + 'bar_module.ts', + ` import {NgModule} from '@angular/core'; import {BarCmp} from './bar_component'; import {BarDir} from './bar_directive'; @@ -1101,10 +1309,14 @@ runInEachFileSystem(() => { exports: [BarCmp, BarPipe], }) export class BarModule {} - `); - env.write('foo_selector.d.ts', ` + `, + ); + env.write( + 'foo_selector.d.ts', + ` export const fooSelector = 'foo'; - `); + `, + ); env.driveMain(); env.flushWrittenFileTracking(); } diff --git a/packages/compiler-cli/test/ngtsc/incremental_typecheck_spec.ts b/packages/compiler-cli/test/ngtsc/incremental_typecheck_spec.ts index 5512fc5036ca3..e1157df9c49f9 100644 --- a/packages/compiler-cli/test/ngtsc/incremental_typecheck_spec.ts +++ b/packages/compiler-cli/test/ngtsc/incremental_typecheck_spec.ts @@ -30,7 +30,9 @@ runInEachFileSystem(() => { it('should type-check correctly when a backing input field is renamed', () => { // This test verifies that renaming the class field of an input is correctly reflected into // the TCB. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -40,8 +42,11 @@ runInEachFileSystem(() => { @Input('dir') dir!: string; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -51,8 +56,11 @@ runInEachFileSystem(() => { export class Cmp { foo = 'foo'; } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -61,12 +69,15 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); + `, + ); env.driveMain(); // Now rename the backing field of the input; the TCB should be updated such that the `dir` // input binding is still valid. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -76,14 +87,17 @@ runInEachFileSystem(() => { @Input('dir') dirRenamed!: string; } - `); + `, + ); env.driveMain(); }); it('should type-check correctly when a backing signal input field is renamed', () => { // This test verifies that renaming the class field of an input is correctly reflected into // the TCB. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, input} from '@angular/core'; @Directive({ @@ -92,8 +106,11 @@ runInEachFileSystem(() => { export class Dir { dir = input.required(); } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -103,8 +120,11 @@ runInEachFileSystem(() => { export class Cmp { foo = 'foo'; } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -113,12 +133,15 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); + `, + ); env.driveMain(); // Now rename the backing field of the input; the TCB should be updated such that the `dir` // input binding is still valid. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, input} from '@angular/core'; @Directive({ @@ -127,12 +150,15 @@ runInEachFileSystem(() => { export class Dir { dirRenamed = input.required({alias: 'dir'}); } - `); + `, + ); env.driveMain(); }); it('should type-check correctly when an decorator-input is changed to a signal input', () => { - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -141,8 +167,11 @@ runInEachFileSystem(() => { export class Dir { @Input() dir!: string; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -152,8 +181,11 @@ runInEachFileSystem(() => { export class Cmp { foo = 'foo'; } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -162,13 +194,16 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); + `, + ); env.driveMain(); // Now, the input is changed to a signal input. The template should still be valid. // If this `isSignal` change would not be detected, `string` would never be assignable // to `InputSignal` because the TCB would not unwrap it. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, input} from '@angular/core'; @Directive({ @@ -177,12 +212,15 @@ runInEachFileSystem(() => { export class Dir { dir = input(); } - `); + `, + ); env.driveMain(); }); it('should type-check correctly when signal input transform is added', () => { - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, input} from '@angular/core'; @Directive({ @@ -191,8 +229,11 @@ runInEachFileSystem(() => { export class Dir { dir = input.required(); } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -202,8 +243,11 @@ runInEachFileSystem(() => { export class Cmp { foo = 'foo'; } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -212,12 +256,15 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); + `, + ); env.driveMain(); // Transform is added and the input no longer accepts `string`, but only a boolean. // This should result in diagnostics now, assuming the TCB is checked again. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, input} from '@angular/core'; @Directive({ @@ -228,17 +275,21 @@ runInEachFileSystem(() => { transform: v => v.toString(), }); } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(1); - expect(diagnostics[0].messageText) - .toBe(`Type 'string' is not assignable to type 'boolean'.`); + expect(diagnostics[0].messageText).toBe( + `Type 'string' is not assignable to type 'boolean'.`, + ); }); it('should type-check correctly when a backing output field is renamed', () => { // This test verifies that renaming the class field of an output is correctly reflected into // the TCB. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, EventEmitter, Output} from '@angular/core'; @Directive({ @@ -248,8 +299,11 @@ runInEachFileSystem(() => { @Output('dir') dir = new EventEmitter(); } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -259,8 +313,11 @@ runInEachFileSystem(() => { export class Cmp { foo(bar: string) {} } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -269,12 +326,15 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); + `, + ); env.driveMain(); // Now rename the backing field of the output; the TCB should be updated such that the `dir` // input binding is still valid. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, EventEmitter, Output} from '@angular/core'; @Directive({ @@ -284,7 +344,8 @@ runInEachFileSystem(() => { @Output('dir') dirRenamed = new EventEmitter(); } - `); + `, + ); env.driveMain(); }); @@ -293,7 +354,9 @@ runInEachFileSystem(() => { // declared in the TypeScript class, the TCB should not contain a write to the field as it // would be an error. This test verifies that the TCB is regenerated when a backing field // is removed. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive} from '@angular/core'; @Directive({ @@ -303,8 +366,11 @@ runInEachFileSystem(() => { export class Dir { dir!: string; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -314,8 +380,11 @@ runInEachFileSystem(() => { export class Cmp { foo = true; } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -324,15 +393,19 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toContain(`Type 'boolean' is not assignable to type 'string'.`); + expect(diags[0].messageText).toContain( + `Type 'boolean' is not assignable to type 'string'.`, + ); // Now remove the backing field for the `dir` input. The compilation should now succeed // as there are no type-check errors. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive} from '@angular/core'; @Directive({ @@ -340,7 +413,8 @@ runInEachFileSystem(() => { inputs: ['dir'], }) export class Dir {} - `); + `, + ); env.driveMain(); }); @@ -351,7 +425,9 @@ runInEachFileSystem(() => { // become readonly does result in the TCB being updated to use such an indirect write, as // otherwise an error would incorrectly be reported. env.tsconfig({strictTemplates: true, strictInputAccessModifiers: false}); - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive} from '@angular/core'; @Directive({ @@ -361,8 +437,11 @@ runInEachFileSystem(() => { export class Dir { dir!: string; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -372,8 +451,11 @@ runInEachFileSystem(() => { export class Cmp { foo = 'foo'; } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -382,12 +464,15 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); + `, + ); env.driveMain(); // Now change the `dir` input to be readonly. Because `strictInputAccessModifiers` is // disabled this should be allowed. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive} from '@angular/core'; @Directive({ @@ -397,7 +482,8 @@ runInEachFileSystem(() => { export class Dir { readonly dir!: string; } - `); + `, + ); env.driveMain(); }); @@ -405,7 +491,9 @@ runInEachFileSystem(() => { // Declaring a static `ngAcceptInputType` member requires that the TCB is regenerated, as // writes to an input property should then be targeted against this static member instead // of the input field itself. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -415,8 +503,11 @@ runInEachFileSystem(() => { @Input() dir!: string; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -426,8 +517,11 @@ runInEachFileSystem(() => { export class Cmp { foo = true; } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -436,16 +530,20 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toContain(`Type 'boolean' is not assignable to type 'string'.`); + expect(diags[0].messageText).toContain( + `Type 'boolean' is not assignable to type 'string'.`, + ); // Now add an `ngAcceptInputType` static member to the directive such that its `dir` input // also accepts `boolean`, unlike the type of `dir`'s class field. This should therefore // allow the compilation to succeed. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -457,14 +555,17 @@ runInEachFileSystem(() => { static ngAcceptInputType_dir: string | boolean; } - `); + `, + ); env.driveMain(); }); it('should type-check correctly when an ngTemplateContextGuard field is declared', () => { // This test adds an `ngTemplateContextGuard` static member to verify that the TCB is // regenerated for the template context to take effect. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -474,8 +575,11 @@ runInEachFileSystem(() => { @Input() dir!: string; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -485,8 +589,11 @@ runInEachFileSystem(() => { export class Cmp { foo(bar: string) {} } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -495,13 +602,16 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); + `, + ); env.driveMain(); // Now add the template context to declare the `$implicit` variable to be of type `number`. // Doing so should report an error for `Cmp`, as the type of `bar` which binds to // `$implicit` is no longer compatible with the method signature which requires a `string`. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; export interface TemplateContext { @@ -517,18 +627,21 @@ runInEachFileSystem(() => { static ngTemplateContextGuard(dir: Dir, ctx: any): ctx is TemplateContext { return true; } } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toContain( - `Argument of type 'number' is not assignable to parameter of type 'string'.`); + expect(diags[0].messageText).toContain( + `Argument of type 'number' is not assignable to parameter of type 'string'.`, + ); }); it('should type-check correctly when an ngTemplateGuard field is declared', () => { // This test verifies that adding an `ngTemplateGuard` static member has the desired effect // of type-narrowing the bound input expression within the template. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -538,8 +651,11 @@ runInEachFileSystem(() => { @Input() dir!: boolean; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -550,8 +666,11 @@ runInEachFileSystem(() => { foo!: string | null; test(foo: string) {} } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -560,17 +679,20 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')) - .toContain( - `Argument of type 'string | null' is not assignable to parameter of type 'string'.`); + expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')).toContain( + `Argument of type 'string | null' is not assignable to parameter of type 'string'.`, + ); // Now resolve the compilation error by adding the `ngTemplateGuard_dir` static member to // specify that the bound expression for `dir` should be used as template guard. This // should allow the compilation to succeed. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; export interface TemplateContext { @@ -586,7 +708,8 @@ runInEachFileSystem(() => { static ngTemplateGuard_dir: 'binding'; } - `); + `, + ); env.driveMain(); }); @@ -597,7 +720,9 @@ runInEachFileSystem(() => { // context guard is used, but it's ineffective at narrowing an expression that explicitly // compares against null. An incremental step changes the type of the guard to be of type // `binding`. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -609,8 +734,11 @@ runInEachFileSystem(() => { static ngTemplateGuard_dir(dir: Dir, expr: any): expr is NonNullable { return true; }; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -621,8 +749,11 @@ runInEachFileSystem(() => { foo!: string | null; test(foo: string) {} } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -631,16 +762,19 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')) - .toContain( - `Argument of type 'string | null' is not assignable to parameter of type 'string'.`); + expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')).toContain( + `Argument of type 'string | null' is not assignable to parameter of type 'string'.`, + ); // Now change the type of the template guard into "binding" to achieve the desired narrowing // of `foo`, allowing the compilation to succeed. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; export interface TemplateContext { @@ -656,7 +790,8 @@ runInEachFileSystem(() => { static ngTemplateGuard_dir: 'binding'; } - `); + `, + ); env.driveMain(); }); @@ -664,7 +799,9 @@ runInEachFileSystem(() => { // This test verifies that changing the name of the field to which an `ngTemplateGuard` // static member applies correctly removes its narrowing effect on the original input // binding expression. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -676,8 +813,11 @@ runInEachFileSystem(() => { static ngTemplateGuard_dir: 'binding'; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -688,8 +828,11 @@ runInEachFileSystem(() => { foo!: string | null; test(foo: string) {} } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -698,12 +841,15 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); + `, + ); env.driveMain(); // Now change the `ngTemplateGuard` to target a different field. The `dir` binding should // no longer be narrowed, causing the template of `Cmp` to become invalid. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; export interface TemplateContext { @@ -719,12 +865,13 @@ runInEachFileSystem(() => { static ngTemplateGuard_dir_renamed: 'binding'; } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')) - .toContain( - `Argument of type 'string | null' is not assignable to parameter of type 'string'.`); + expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')).toContain( + `Argument of type 'string | null' is not assignable to parameter of type 'string'.`, + ); }); }); @@ -735,7 +882,9 @@ runInEachFileSystem(() => { // of the generic type requires that `Cmp`'s local declaration of `Dir` is also updated, // otherwise the prior declaration without generic type argument would be invalid. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -745,8 +894,11 @@ runInEachFileSystem(() => { @Input() dir!: string; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -756,8 +908,11 @@ runInEachFileSystem(() => { export class Cmp { foo = 'foo'; } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -766,11 +921,14 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); + `, + ); env.driveMain(); // Adding a generic type should still allow the compilation to succeed. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -780,7 +938,8 @@ runInEachFileSystem(() => { @Input() dir!: string; } - `); + `, + ); env.driveMain(); }); @@ -790,7 +949,9 @@ runInEachFileSystem(() => { // type requires that `Cmp`'s local declaration of `Dir` is also updated, otherwise the // prior declaration with fewer generic type argument would be invalid. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -800,8 +961,11 @@ runInEachFileSystem(() => { @Input() dir!: T; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -811,8 +975,11 @@ runInEachFileSystem(() => { export class Cmp { foo = 'foo'; } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -821,11 +988,14 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); + `, + ); env.driveMain(); // Add generic type parameter `U` should continue to allow the compilation to succeed. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -835,7 +1005,8 @@ runInEachFileSystem(() => { @Input() dir!: T; } - `); + `, + ); env.driveMain(); }); @@ -845,7 +1016,9 @@ runInEachFileSystem(() => { // template. The removal of the generic type requires that `Cmp`'s local declaration of // `Dir` is also updated, as otherwise the prior declaration with a generic type argument // would be invalid. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -855,8 +1028,11 @@ runInEachFileSystem(() => { @Input() dir!: string; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -866,8 +1042,11 @@ runInEachFileSystem(() => { export class Cmp { foo = 'foo'; } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -876,11 +1055,14 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); + `, + ); env.driveMain(); // Changing `Dir` to become non-generic should allow the compilation to succeed. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -890,7 +1072,8 @@ runInEachFileSystem(() => { @Input() dir!: string; } - `); + `, + ); env.driveMain(); }); @@ -899,7 +1082,9 @@ runInEachFileSystem(() => { // type-checks component `Cmp` that uses `Dir` in its template. The removal of the generic // type requires that `Cmp`'s local declaration of `Dir` is also updated, as otherwise the // prior declaration with the initial number of generic type arguments would be invalid. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -909,8 +1094,11 @@ runInEachFileSystem(() => { @Input() dir!: T; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -920,8 +1108,11 @@ runInEachFileSystem(() => { export class Cmp { foo = 'foo'; } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -930,11 +1121,14 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); + `, + ); env.driveMain(); // Removing type parameter `U` should allow the compilation to succeed. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -944,7 +1138,8 @@ runInEachFileSystem(() => { @Input() dir!: T; } - `); + `, + ); env.driveMain(); }); @@ -952,12 +1147,17 @@ runInEachFileSystem(() => { // This test verifies that changing an unbound generic type parameter of directive `Dir` // to have a type constraint properly applies the newly added type constraint during // type-checking of `Cmp` that uses `Dir` in its template. - env.write('node_modules/foo/index.ts', ` + env.write( + 'node_modules/foo/index.ts', + ` export interface Foo { a: boolean; } - `); - env.write('dir.ts', ` + `, + ); + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -967,8 +1167,11 @@ runInEachFileSystem(() => { @Input() dir!: T; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -978,8 +1181,11 @@ runInEachFileSystem(() => { export class Cmp { foo: string; } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -988,13 +1194,16 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); + `, + ); env.driveMain(); // Update `Dir` such that its generic type parameter `T` is constrained to type `Foo`. The // template of `Cmp` should now fail to type-check, as its bound value for `T` does not // conform to the `Foo` constraint. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; import {Foo} from 'foo'; @@ -1005,7 +1214,8 @@ runInEachFileSystem(() => { @Input() dir!: T; } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); @@ -1013,7 +1223,9 @@ runInEachFileSystem(() => { // Now update `Dir` again to remove the constraint of `T`, which should allow the template // of `Cmp` to succeed type-checking. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -1023,7 +1235,8 @@ runInEachFileSystem(() => { @Input() dir!: T; } - `); + `, + ); env.driveMain(); }); @@ -1040,17 +1253,25 @@ runInEachFileSystem(() => { // - Perform an incremental compilation where the import of `Foo` is changed into `foo-b`. // The binding in `Cmp` should now report an error, as its value of `Foo` from `foo-a` // no longer conforms to the new type constraint of `Foo` from 'foo-b'. - env.write('node_modules/foo-a/index.ts', ` + env.write( + 'node_modules/foo-a/index.ts', + ` export interface Foo { a: boolean; } - `); - env.write('node_modules/foo-b/index.ts', ` + `, + ); + env.write( + 'node_modules/foo-b/index.ts', + ` export interface Foo { b: boolean; } - `); - env.write('dir.ts', ` + `, + ); + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; import {Foo} from 'foo-a'; @@ -1061,8 +1282,11 @@ runInEachFileSystem(() => { @Input() dir!: T; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; import {Foo} from 'foo-a'; @@ -1073,8 +1297,11 @@ runInEachFileSystem(() => { export class Cmp { foo: Foo = {a: true}; } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -1083,13 +1310,16 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); + `, + ); env.driveMain(); // Now switch the import of `Foo` from `foo-a` to `foo-b`. This should cause a type-check // failure in `Cmp`, as its binding into `Dir` still provides an incompatible `Foo` // from `foo-a`. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; import {Foo} from 'foo-b'; @@ -1100,20 +1330,25 @@ runInEachFileSystem(() => { @Input() dir!: T; } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toContain(`Type 'import("${ - absoluteFrom( - '/node_modules/foo-a/index')}").Foo' is not assignable to type 'import("${ - absoluteFrom('/node_modules/foo-b/index')}").Foo'.`); + expect(diags[0].messageText).toContain( + `Type 'import("${absoluteFrom( + '/node_modules/foo-a/index', + )}").Foo' is not assignable to type 'import("${absoluteFrom( + '/node_modules/foo-b/index', + )}").Foo'.`, + ); // For completeness, update `Cmp` to address the previous template type-check error by // changing the type of the binding into `Dir` to also be the `Foo` from `foo-b`. This // should result in a successful compilation. - env.write('cmp.ts', ` + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; import {Foo} from 'foo-b'; @@ -1124,18 +1359,20 @@ runInEachFileSystem(() => { export class Cmp { foo: Foo = {b: true}; } - `); + `, + ); env.driveMain(); }); }); describe('inheritance', () => { - it('should type-check derived directives when the public API of the parent class is affected', - () => { - // This test verifies that an indirect change to the public API of `Dir` as caused by a - // change to `Dir`'s base class `Parent` causes the type-check result of component `Cmp` - // that uses `Dir` to be updated accordingly. - env.write('parent.ts', ` + it('should type-check derived directives when the public API of the parent class is affected', () => { + // This test verifies that an indirect change to the public API of `Dir` as caused by a + // change to `Dir`'s base class `Parent` causes the type-check result of component `Cmp` + // that uses `Dir` to be updated accordingly. + env.write( + 'parent.ts', + ` import {Directive, Input} from '@angular/core'; @Directive() @@ -1143,8 +1380,11 @@ runInEachFileSystem(() => { @Input() parent!: string; } - `); - env.write('dir.ts', ` + `, + ); + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; import {Parent} from './parent'; @@ -1155,8 +1395,11 @@ runInEachFileSystem(() => { @Input() dir!: string; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1166,8 +1409,11 @@ runInEachFileSystem(() => { export class Cmp { foo = 'foo'; } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -1176,31 +1422,37 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); - env.driveMain(); + `, + ); + env.driveMain(); - // Now remove an input from `Parent`. This invalidates the binding in `Cmp`'s template, - // so an error diagnostic should be reported. - env.write('parent.ts', ` + // Now remove an input from `Parent`. This invalidates the binding in `Cmp`'s template, + // so an error diagnostic should be reported. + env.write( + 'parent.ts', + ` import {Directive, Input} from '@angular/core'; @Directive() export class Parent { } - `); - const diags = env.driveDiagnostics(); - expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toContain(`Can't bind to 'parent' since it isn't a known property of 'div'.`); - }); - - it('should type-check derived directives when the public API of the grandparent class is affected', - () => { - // This test verifies that an indirect change to the public API of `Dir` as caused by a - // change to `Dir`'s transitive base class `Grandparent` causes the type-check result of - // component `Cmp` that uses `Dir` to be updated accordingly. - env.write('grandparent.ts', ` + `, + ); + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].messageText).toContain( + `Can't bind to 'parent' since it isn't a known property of 'div'.`, + ); + }); + + it('should type-check derived directives when the public API of the grandparent class is affected', () => { + // This test verifies that an indirect change to the public API of `Dir` as caused by a + // change to `Dir`'s transitive base class `Grandparent` causes the type-check result of + // component `Cmp` that uses `Dir` to be updated accordingly. + env.write( + 'grandparent.ts', + ` import {Directive, Input} from '@angular/core'; @Directive() @@ -1208,8 +1460,11 @@ runInEachFileSystem(() => { @Input() grandparent!: string; } - `); - env.write('parent.ts', ` + `, + ); + env.write( + 'parent.ts', + ` import {Directive, Input} from '@angular/core'; import {Grandparent} from './grandparent'; @@ -1218,8 +1473,11 @@ runInEachFileSystem(() => { @Input() parent!: string; } - `); - env.write('dir.ts', ` + `, + ); + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; import {Parent} from './parent'; @@ -1230,8 +1488,11 @@ runInEachFileSystem(() => { @Input() dir!: string; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1241,8 +1502,11 @@ runInEachFileSystem(() => { export class Cmp { foo = 'foo'; } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -1251,31 +1515,38 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); - env.driveMain(); + `, + ); + env.driveMain(); - // Now remove an input from `Grandparent`. This invalidates the binding in `Cmp`'s - // template, so an error diagnostic should be reported. - env.write('grandparent.ts', ` + // Now remove an input from `Grandparent`. This invalidates the binding in `Cmp`'s + // template, so an error diagnostic should be reported. + env.write( + 'grandparent.ts', + ` import {Directive, Input} from '@angular/core'; @Directive() export class Grandparent { } - `); - const diags = env.driveDiagnostics(); - expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toContain(`Can't bind to 'grandparent' since it isn't a known property of 'div'.`); - }); + `, + ); + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].messageText).toContain( + `Can't bind to 'grandparent' since it isn't a known property of 'div'.`, + ); + }); it('should type-check derived directives when a base class is added to a grandparent', () => { // This test verifies that an indirect change to the public API of `Dir` as caused by // adding a base class `Grandgrandparent` to `Dir`'s transitive base class `Grandparent` // causes the type-check result of component `Cmp` that uses `Dir` to be // updated accordingly. - env.write('grandgrandparent.ts', ` + env.write( + 'grandgrandparent.ts', + ` import {Directive, Input} from '@angular/core'; @Directive() @@ -1283,8 +1554,11 @@ runInEachFileSystem(() => { @Input() grandgrandparent!: string; } - `); - env.write('grandparent.ts', ` + `, + ); + env.write( + 'grandparent.ts', + ` import {Directive, Input} from '@angular/core'; @Directive() @@ -1292,8 +1566,11 @@ runInEachFileSystem(() => { @Input() grandparent!: string; } - `); - env.write('parent.ts', ` + `, + ); + env.write( + 'parent.ts', + ` import {Directive, Input} from '@angular/core'; import {Grandparent} from './grandparent'; @@ -1302,8 +1579,11 @@ runInEachFileSystem(() => { @Input() parent!: string; } - `); - env.write('dir.ts', ` + `, + ); + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; import {Parent} from './parent'; @@ -1314,8 +1594,11 @@ runInEachFileSystem(() => { @Input() dir!: string; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1325,8 +1608,11 @@ runInEachFileSystem(() => { export class Cmp { foo = 'foo'; } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -1335,19 +1621,22 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); + `, + ); // `Cmp` already binds to the `grandgrandparent` input but it's not available, as // `Granparent` does not yet extend from `Grandgrandparent`. const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toContain( - `Can't bind to 'grandgrandparent' since it isn't a known property of 'div'.`); + expect(diags[0].messageText).toContain( + `Can't bind to 'grandgrandparent' since it isn't a known property of 'div'.`, + ); // Now fix the issue by adding the base class to `Grandparent`; this should allow // type-checking to succeed. - env.write('grandparent.ts', ` + env.write( + 'grandparent.ts', + ` import {Directive, Input} from '@angular/core'; import {Grandgrandparent} from './grandgrandparent'; @@ -1356,17 +1645,19 @@ runInEachFileSystem(() => { @Input() grandparent!: string; } - `); + `, + ); env.driveMain(); }); - it('should type-check derived directives when a base class is removed from a grandparent', - () => { - // This test verifies that an indirect change to the public API of `Dir` as caused by - // removing a base class `Grandgrandparent` from `Dir`'s transitive base class - // `Grandparent` causes the type-check result of component `Cmp` that uses `Dir` to be - // updated accordingly. - env.write('grandgrandparent.ts', ` + it('should type-check derived directives when a base class is removed from a grandparent', () => { + // This test verifies that an indirect change to the public API of `Dir` as caused by + // removing a base class `Grandgrandparent` from `Dir`'s transitive base class + // `Grandparent` causes the type-check result of component `Cmp` that uses `Dir` to be + // updated accordingly. + env.write( + 'grandgrandparent.ts', + ` import {Directive, Input} from '@angular/core'; @Directive() @@ -1374,8 +1665,11 @@ runInEachFileSystem(() => { @Input() grandgrandparent!: string; } - `); - env.write('grandparent.ts', ` + `, + ); + env.write( + 'grandparent.ts', + ` import {Directive, Input} from '@angular/core'; import {Grandgrandparent} from './grandgrandparent'; @@ -1384,8 +1678,11 @@ runInEachFileSystem(() => { @Input() grandparent!: string; } - `); - env.write('parent.ts', ` + `, + ); + env.write( + 'parent.ts', + ` import {Directive, Input} from '@angular/core'; import {Grandparent} from './grandparent'; @@ -1394,8 +1691,11 @@ runInEachFileSystem(() => { @Input() parent!: string; } - `); - env.write('dir.ts', ` + `, + ); + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; import {Parent} from './parent'; @@ -1406,8 +1706,11 @@ runInEachFileSystem(() => { @Input() dir!: string; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1417,8 +1720,11 @@ runInEachFileSystem(() => { export class Cmp { foo = 'foo'; } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -1427,13 +1733,16 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); - env.driveMain(); + `, + ); + env.driveMain(); - // Removing the base class from `Grandparent` should start to report a type-check - // error in `Cmp`'s template, as its binding to the `grandgrandparent` input is no - // longer valid. - env.write('grandparent.ts', ` + // Removing the base class from `Grandparent` should start to report a type-check + // error in `Cmp`'s template, as its binding to the `grandgrandparent` input is no + // longer valid. + env.write( + 'grandparent.ts', + ` import {Directive, Input} from '@angular/core'; @Directive() @@ -1441,20 +1750,22 @@ runInEachFileSystem(() => { @Input() grandparent!: string; } - `); - const diags = env.driveDiagnostics(); - expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toContain( - `Can't bind to 'grandgrandparent' since it isn't a known property of 'div'.`); - }); - - it('should type-check derived directives when the base class of a grandparent changes', - () => { - // This test verifies that an indirect change to the public API of `Dir` as caused by - // changing the base class of `Dir`'s transitive base class `Grandparent` causes the - // type-check result of component `Cmp` that uses `Dir` to be updated accordingly. - env.write('grandgrandparent-a.ts', ` + `, + ); + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].messageText).toContain( + `Can't bind to 'grandgrandparent' since it isn't a known property of 'div'.`, + ); + }); + + it('should type-check derived directives when the base class of a grandparent changes', () => { + // This test verifies that an indirect change to the public API of `Dir` as caused by + // changing the base class of `Dir`'s transitive base class `Grandparent` causes the + // type-check result of component `Cmp` that uses `Dir` to be updated accordingly. + env.write( + 'grandgrandparent-a.ts', + ` import {Directive, Input} from '@angular/core'; @Directive() @@ -1462,8 +1773,11 @@ runInEachFileSystem(() => { @Input() grandgrandparentA!: string; } - `); - env.write('grandgrandparent-b.ts', ` + `, + ); + env.write( + 'grandgrandparent-b.ts', + ` import {Directive, Input} from '@angular/core'; @Directive() @@ -1471,8 +1785,11 @@ runInEachFileSystem(() => { @Input() grandgrandparentB!: string; } - `); - env.write('grandparent.ts', ` + `, + ); + env.write( + 'grandparent.ts', + ` import {Directive, Input} from '@angular/core'; import {GrandgrandparentA} from './grandgrandparent-a'; @@ -1481,8 +1798,11 @@ runInEachFileSystem(() => { @Input() grandparent!: string; } - `); - env.write('parent.ts', ` + `, + ); + env.write( + 'parent.ts', + ` import {Directive, Input} from '@angular/core'; import {Grandparent} from './grandparent'; @@ -1491,8 +1811,11 @@ runInEachFileSystem(() => { @Input() parent!: string; } - `); - env.write('dir.ts', ` + `, + ); + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; import {Parent} from './parent'; @@ -1503,8 +1826,11 @@ runInEachFileSystem(() => { @Input() dir!: string; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1514,8 +1840,11 @@ runInEachFileSystem(() => { export class Cmp { foo = 'foo'; } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -1524,13 +1853,16 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); - env.driveMain(); + `, + ); + env.driveMain(); - // Now switch the base class of `Grandparent` from `GrandgrandparentA` to - // `GrandgrandparentB` causes the input binding to `grandgrandparentA` to be reported as - // an error, as it's no longer available. - env.write('grandparent.ts', ` + // Now switch the base class of `Grandparent` from `GrandgrandparentA` to + // `GrandgrandparentB` causes the input binding to `grandgrandparentA` to be reported as + // an error, as it's no longer available. + env.write( + 'grandparent.ts', + ` import {Directive, Input} from '@angular/core'; import {GrandgrandparentB} from './grandgrandparent-b'; @@ -1539,19 +1871,22 @@ runInEachFileSystem(() => { @Input() grandparent!: string; } - `); - const diags = env.driveDiagnostics(); - expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toContain( - `Can't bind to 'grandgrandparentA' since it isn't a known property of 'div'.`); - }); + `, + ); + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].messageText).toContain( + `Can't bind to 'grandgrandparentA' since it isn't a known property of 'div'.`, + ); + }); }); describe('program re-use', () => { it('should completely re-use structure when the first signal input is introduced', () => { env.tsconfig({strictTemplates: true}); - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -1560,8 +1895,11 @@ runInEachFileSystem(() => { export class Dir { @Input() dir: string = ''; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1571,8 +1909,11 @@ runInEachFileSystem(() => { export class Cmp { foo = 'foo'; } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -1581,11 +1922,14 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); + `, + ); env.driveMain(); // introduce the signal input. - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive, input} from '@angular/core'; @Directive({ @@ -1594,17 +1938,19 @@ runInEachFileSystem(() => { export class Dir { dir = input.required(); } - `); + `, + ); env.driveMain(); expectCompleteReuse(env.getTsProgram()); expectCompleteReuse(env.getReuseTsProgram()); }); - it('should completely re-use structure when an inline constructor generic directive starts using input signals', - () => { - env.tsconfig({strictTemplates: true}); - env.write('dir.ts', ` + it('should completely re-use structure when an inline constructor generic directive starts using input signals', () => { + env.tsconfig({strictTemplates: true}); + env.write( + 'dir.ts', + ` import {Directive, Input} from '@angular/core'; class SomeNonExportedClass {} @@ -1615,8 +1961,11 @@ runInEachFileSystem(() => { export class Dir { @Input() dir: T|undefined; } - `); - env.write('cmp.ts', ` + `, + ); + env.write( + 'cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1626,8 +1975,11 @@ runInEachFileSystem(() => { export class Cmp { foo = 'foo'; } - `); - env.write('mod.ts', ` + `, + ); + env.write( + 'mod.ts', + ` import {NgModule} from '@angular/core'; import {Cmp} from './cmp'; import {Dir} from './dir'; @@ -1636,11 +1988,14 @@ runInEachFileSystem(() => { declarations: [Cmp, Dir], }) export class Mod {} - `); - env.driveMain(); + `, + ); + env.driveMain(); - // turn the input into a signal input- causing a new import. - env.write('dir.ts', ` + // turn the input into a signal input- causing a new import. + env.write( + 'dir.ts', + ` import {Directive, input} from '@angular/core'; class SomeNonExportedClass {} @@ -1651,12 +2006,13 @@ runInEachFileSystem(() => { export class Dir { dir = input.required(); } - `); - env.driveMain(); + `, + ); + env.driveMain(); - expectCompleteReuse(env.getTsProgram()); - expectCompleteReuse(env.getReuseTsProgram()); - }); + expectCompleteReuse(env.getTsProgram()); + expectCompleteReuse(env.getReuseTsProgram()); + }); }); }); }); diff --git a/packages/compiler-cli/test/ngtsc/local_compilation_spec.ts b/packages/compiler-cli/test/ngtsc/local_compilation_spec.ts index 2e333b165b4d1..185bce0951f6a 100644 --- a/packages/compiler-cli/test/ngtsc/local_compilation_spec.ts +++ b/packages/compiler-cli/test/ngtsc/local_compilation_spec.ts @@ -41,9 +41,12 @@ runInEachFileSystem(() => { }); it('should produce no TS semantic diagnostics', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {SubExternalStuff} from './some-where'; - `); + `, + ); env.driveMain(); const diags = env.driveDiagnostics(); @@ -68,7 +71,9 @@ runInEachFileSystem(() => { }); it('should only include NgModule external import as global import', () => { - env.write('a.ts', ` + env.write( + 'a.ts', + ` import {NgModule} from '@angular/core'; import {SomeExternalStuff} from '/some_external_file'; import {SomeExternalStuff2} from '/some_external_file2'; @@ -78,17 +83,24 @@ runInEachFileSystem(() => { @NgModule({imports: [SomeExternalStuff, BModule]}) export class AModule { } - `); - env.write('b.ts', ` + `, + ); + env.write( + 'b.ts', + ` import {NgModule} from '@angular/core'; @NgModule({}) export class BModule { } - `); - env.write('c.ts', ` + `, + ); + env.write( + 'c.ts', + ` // Some code - `); + `, + ); env.driveMain(); const aContents = env.getContents('a.js'); @@ -112,7 +124,9 @@ runInEachFileSystem(() => { }); it('should include NgModule namespace external import as global import', () => { - env.write('a.ts', ` + env.write( + 'a.ts', + ` import {NgModule} from '@angular/core'; import * as n from '/some_external_file'; @@ -121,10 +135,14 @@ runInEachFileSystem(() => { @NgModule({imports: [n.SomeExternalStuff]}) export class AModule { } - `); - env.write('test.ts', ` + `, + ); + env.write( + 'test.ts', + ` // Some code - `); + `, + ); env.driveMain(); @@ -132,9 +150,10 @@ runInEachFileSystem(() => { expect(env.getContents('test.js')).toContain('import "/some_external_file"'); }); - it('should include nested NgModule external import as global import - case of named import', - () => { - env.write('a.ts', ` + it('should include nested NgModule external import as global import - case of named import', () => { + env.write( + 'a.ts', + ` import {NgModule} from '@angular/core'; import {SomeExternalStuff} from '/some_external_file'; @@ -143,20 +162,25 @@ runInEachFileSystem(() => { @NgModule({imports: [[[SomeExternalStuff]]]}) export class AModule { } - `); - env.write('test.ts', ` + `, + ); + env.write( + 'test.ts', + ` // Some code - `); + `, + ); - env.driveMain(); + env.driveMain(); - expect(env.getContents('a.js')).toContain('import "/some_external_file"'); - expect(env.getContents('test.js')).toContain('import "/some_external_file"'); - }); + expect(env.getContents('a.js')).toContain('import "/some_external_file"'); + expect(env.getContents('test.js')).toContain('import "/some_external_file"'); + }); - it('should include nested NgModule external import as global import - case of namespace import', - () => { - env.write('a.ts', ` + it('should include nested NgModule external import as global import - case of namespace import', () => { + env.write( + 'a.ts', + ` import {NgModule} from '@angular/core'; import * as n from '/some_external_file'; @@ -165,20 +189,25 @@ runInEachFileSystem(() => { @NgModule({imports: [[[n.SomeExternalStuff]]]}) export class AModule { } - `); - env.write('test.ts', ` + `, + ); + env.write( + 'test.ts', + ` // Some code - `); + `, + ); - env.driveMain(); + env.driveMain(); - expect(env.getContents('a.js')).toContain('import "/some_external_file"'); - expect(env.getContents('test.js')).toContain('import "/some_external_file"'); - }); + expect(env.getContents('a.js')).toContain('import "/some_external_file"'); + expect(env.getContents('test.js')).toContain('import "/some_external_file"'); + }); - it('should include NgModule external imports as global imports - case of multiple nested imports including named and namespace imports', - () => { - env.write('a.ts', ` + it('should include NgModule external imports as global imports - case of multiple nested imports including named and namespace imports', () => { + env.write( + 'a.ts', + ` import {NgModule} from '@angular/core'; import {SomeExternalStuff} from '/some_external_file'; import * as n from '/some_external_file2'; @@ -188,34 +217,45 @@ runInEachFileSystem(() => { @NgModule({imports: [[SomeExternalStuff], [n.SomeExternalStuff]]}) export class AModule { } - `); - env.write('test.ts', ` + `, + ); + env.write( + 'test.ts', + ` // Some code - `); + `, + ); - env.driveMain(); + env.driveMain(); - expect(env.getContents('test.js')).toContain('import "/some_external_file"'); - expect(env.getContents('test.js')).toContain('import "/some_external_file2"'); - }); + expect(env.getContents('test.js')).toContain('import "/some_external_file"'); + expect(env.getContents('test.js')).toContain('import "/some_external_file2"'); + }); - it('should include extra import for the local component dependencies (component, directive and pipe)', - () => { - env.write('internal_comp.ts', ` + it('should include extra import for the local component dependencies (component, directive and pipe)', () => { + env.write( + 'internal_comp.ts', + ` import {Component} from '@angular/core'; @Component({template: '...', selector: 'internal-comp'}) export class InternalComp { } - `); - env.write('internal_dir.ts', ` + `, + ); + env.write( + 'internal_dir.ts', + ` import {Directive} from '@angular/core'; @Directive({selector: '[internal-dir]'}) export class InternalDir { } - `); - env.write('internal_pipe.ts', ` + `, + ); + env.write( + 'internal_pipe.ts', + ` import {Pipe, PipeTransform} from '@angular/core'; @Pipe({name: 'internalPipe'}) @@ -224,8 +264,11 @@ runInEachFileSystem(() => { return value*2; } } - `); - env.write('internal_module.ts', ` + `, + ); + env.write( + 'internal_module.ts', + ` import {NgModule} from '@angular/core'; import {InternalComp} from 'internal_comp'; @@ -235,15 +278,21 @@ runInEachFileSystem(() => { @NgModule({declarations: [InternalComp, InternalDir, InternalPipe], exports: [InternalComp, InternalDir, InternalPipe]}) export class InternalModule { } - `); - env.write('main_comp.ts', ` + `, + ); + env.write( + 'main_comp.ts', + ` import {Component} from '@angular/core'; @Component({template: ' {{2 | internalPipe}}'}) export class MainComp { } - `); - env.write('main_module.ts', ` + `, + ); + env.write( + 'main_module.ts', + ` import {NgModule} from '@angular/core'; import {MainComp} from 'main_comp'; @@ -252,26 +301,31 @@ runInEachFileSystem(() => { @NgModule({declarations: [MainComp], imports: [InternalModule]}) export class MainModule { } - `); + `, + ); - env.driveMain(); + env.driveMain(); - expect(env.getContents('main_comp.js')).toContain('import "internal_comp"'); - expect(env.getContents('main_comp.js')).toContain('import "internal_dir"'); - expect(env.getContents('main_comp.js')).toContain('import "internal_pipe"'); - }); + expect(env.getContents('main_comp.js')).toContain('import "internal_comp"'); + expect(env.getContents('main_comp.js')).toContain('import "internal_dir"'); + expect(env.getContents('main_comp.js')).toContain('import "internal_pipe"'); + }); - it('should not include extra import and remote scope runtime for the local component dependencies when cycle is produced', - () => { - env.write('internal_comp.ts', ` + it('should not include extra import and remote scope runtime for the local component dependencies when cycle is produced', () => { + env.write( + 'internal_comp.ts', + ` import {Component} from '@angular/core'; import {cycleCreatingDep} from './main_comp'; @Component({template: '...', selector: 'internal-comp'}) export class InternalComp { } - `); - env.write('internal_module.ts', ` + `, + ); + env.write( + 'internal_module.ts', + ` import {NgModule} from '@angular/core'; import {InternalComp} from 'internal_comp'; @@ -279,15 +333,21 @@ runInEachFileSystem(() => { @NgModule({declarations: [InternalComp], exports: [InternalComp]}) export class InternalModule { } - `); - env.write('main_comp.ts', ` + `, + ); + env.write( + 'main_comp.ts', + ` import {Component} from '@angular/core'; @Component({template: ''}) export class MainComp { } - `); - env.write('main_module.ts', ` + `, + ); + env.write( + 'main_module.ts', + ` import {NgModule} from '@angular/core'; import {MainComp} from 'main_comp'; @@ -296,24 +356,28 @@ runInEachFileSystem(() => { @NgModule({declarations: [MainComp], imports: [InternalModule]}) export class MainModule { } - `); + `, + ); - env.driveMain(); + env.driveMain(); - expect(env.getContents('main_comp.js')).not.toContain('import "internal_comp"'); - expect(env.getContents('main_module.js')).not.toContain('ɵɵsetComponentScope'); - }); + expect(env.getContents('main_comp.js')).not.toContain('import "internal_comp"'); + expect(env.getContents('main_module.js')).not.toContain('ɵɵsetComponentScope'); + }); }); describe('ng module injector def', () => { it('should produce empty injector def imports when module has no imports/exports', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {NgModule} from '@angular/core'; @NgModule({}) export class MainModule { } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -321,9 +385,10 @@ runInEachFileSystem(() => { expect(jsContents).toContain('MainModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({})'); }); - it('should include raw module imports array elements (including forward refs) in the injector def imports', - () => { - env.write('test.ts', ` + it('should include raw module imports array elements (including forward refs) in the injector def imports', () => { + env.write( + 'test.ts', + ` import {NgModule, forwardRef} from '@angular/core'; import {SubModule1} from './some-where'; import {SubModule2} from './another-where'; @@ -339,18 +404,21 @@ runInEachFileSystem(() => { @NgModule({}) class LocalModule2 {} - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - 'MainModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ imports: [SubModule1, forwardRef(() => SubModule2), LocalModule1, forwardRef(() => LocalModule2)] })'); - }); + expect(jsContents).toContain( + 'MainModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ imports: [SubModule1, forwardRef(() => SubModule2), LocalModule1, forwardRef(() => LocalModule2)] })', + ); + }); it('should include non-array raw module imports as it is in the injector def imports', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {NgModule, forwardRef} from '@angular/core'; import {SubModule1} from './some-where'; import {SubModule2} from './another-where'; @@ -368,19 +436,21 @@ runInEachFileSystem(() => { @NgModule({}) class LocalModule2 {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - 'MainModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ imports: [NG_IMPORTS] })'); + expect(jsContents).toContain( + 'MainModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ imports: [NG_IMPORTS] })', + ); }); - it('should include raw module exports array elements (including forward refs) in the injector def imports', - () => { - env.write('test.ts', ` + it('should include raw module exports array elements (including forward refs) in the injector def imports', () => { + env.write( + 'test.ts', + ` import {NgModule, forwardRef} from '@angular/core'; import {SubModule1} from './some-where'; import {SubModule2} from './another-where'; @@ -396,19 +466,21 @@ runInEachFileSystem(() => { @NgModule({}) class LocalModule2 {} - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - 'MainModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ imports: [SubModule1, forwardRef(() => SubModule2), LocalModule1, forwardRef(() => LocalModule2)] })'); - }); + expect(jsContents).toContain( + 'MainModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ imports: [SubModule1, forwardRef(() => SubModule2), LocalModule1, forwardRef(() => LocalModule2)] })', + ); + }); - it('should include non-array raw module exports (including forward refs) in the injector def imports', - () => { - env.write('test.ts', ` + it('should include non-array raw module exports (including forward refs) in the injector def imports', () => { + env.write( + 'test.ts', + ` import {NgModule, forwardRef} from '@angular/core'; import {SubModule1} from './some-where'; import {SubModule2} from './another-where'; @@ -426,19 +498,21 @@ runInEachFileSystem(() => { @NgModule({}) class LocalModule2 {} - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - 'MainModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ imports: [NG_EXPORTS] })'); - }); + expect(jsContents).toContain( + 'MainModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ imports: [NG_EXPORTS] })', + ); + }); - it('should concat raw module imports and exports arrays (including forward refs) in the injector def imports', - () => { - env.write('test.ts', ` + it('should concat raw module imports and exports arrays (including forward refs) in the injector def imports', () => { + env.write( + 'test.ts', + ` import {NgModule, forwardRef} from '@angular/core'; import {SubModule1, SubModule2} from './some-where'; import {SubModule3, SubModule4} from './another-where'; @@ -449,19 +523,21 @@ runInEachFileSystem(() => { }) export class MainModule { } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - 'MainModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ imports: [SubModule1, forwardRef(() => SubModule2), SubModule3, forwardRef(() => SubModule4)] })'); - }); + expect(jsContents).toContain( + 'MainModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ imports: [SubModule1, forwardRef(() => SubModule2), SubModule3, forwardRef(() => SubModule4)] })', + ); + }); - it('should combines non-array raw module imports and exports (including forward refs) in the injector def imports', - () => { - env.write('test.ts', ` + it('should combines non-array raw module imports and exports (including forward refs) in the injector def imports', () => { + env.write( + 'test.ts', + ` import {NgModule, forwardRef} from '@angular/core'; import {NG_IMPORTS} from './some-where'; import {NG_EXPORTS} from './another-where'; @@ -472,21 +548,23 @@ runInEachFileSystem(() => { }) export class MainModule { } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - 'MainModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ imports: [NG_IMPORTS, NG_EXPORTS] })'); - }); + expect(jsContents).toContain( + 'MainModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ imports: [NG_IMPORTS, NG_EXPORTS] })', + ); + }); }); describe('component dependencies', () => { - it('should generate ɵɵgetComponentDepsFactory for component def dependencies - for non-standalone component ', - () => { - env.write('test.ts', ` + it('should generate ɵɵgetComponentDepsFactory for component def dependencies - for non-standalone component ', () => { + env.write( + 'test.ts', + ` import {NgModule, Component} from '@angular/core'; @Component({ @@ -501,18 +579,19 @@ runInEachFileSystem(() => { }) export class MainModule { } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain('dependencies: i0.ɵɵgetComponentDepsFactory(MainComponent)'); - }); + expect(jsContents).toContain('dependencies: i0.ɵɵgetComponentDepsFactory(MainComponent)'); + }); - it('should generate ɵɵgetComponentDepsFactory with raw imports as second param for component def dependencies - for standalone component with non-empty imports', - () => { - env.write('test.ts', ` + it('should generate ɵɵgetComponentDepsFactory with raw imports as second param for component def dependencies - for standalone component with non-empty imports', () => { + env.write( + 'test.ts', + ` import {Component, forwardRef} from '@angular/core'; import {SomeThing} from 'some-where'; import {SomeThing2} from 'some-where2'; @@ -525,19 +604,21 @@ runInEachFileSystem(() => { }) export class MainComponent { } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - 'dependencies: i0.ɵɵgetComponentDepsFactory(MainComponent, [SomeThing, forwardRef(() => SomeThing2)])'); - }); + expect(jsContents).toContain( + 'dependencies: i0.ɵɵgetComponentDepsFactory(MainComponent, [SomeThing, forwardRef(() => SomeThing2)])', + ); + }); - it('should generate ɵɵgetComponentDepsFactory with raw non-array imports as second param for component def dependencies - for standalone component with non-empty imports', - () => { - env.write('test.ts', ` + it('should generate ɵɵgetComponentDepsFactory with raw non-array imports as second param for component def dependencies - for standalone component with non-empty imports', () => { + env.write( + 'test.ts', + ` import {Component, forwardRef} from '@angular/core'; import {SomeThing} from 'some-where'; import {SomeThing2} from 'some-where2'; @@ -552,18 +633,21 @@ runInEachFileSystem(() => { }) export class MainComponent { } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain('dependencies: i0.ɵɵgetComponentDepsFactory(MainComponent, NG_IMPORTS)'); - }); + expect(jsContents).toContain( + 'dependencies: i0.ɵɵgetComponentDepsFactory(MainComponent, NG_IMPORTS)', + ); + }); - it('should generate ɵɵgetComponentDepsFactory with empty array as secon d arg for standalone component with empty imports', - () => { - env.write('test.ts', ` + it('should generate ɵɵgetComponentDepsFactory with empty array as secon d arg for standalone component with empty imports', () => { + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -574,18 +658,21 @@ runInEachFileSystem(() => { }) export class MainComponent { } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain('dependencies: i0.ɵɵgetComponentDepsFactory(MainComponent, [])'); - }); + expect(jsContents).toContain( + 'dependencies: i0.ɵɵgetComponentDepsFactory(MainComponent, [])', + ); + }); - it('should not generate ɵɵgetComponentDepsFactory for standalone component with no imports', - () => { - env.write('test.ts', ` + it('should not generate ɵɵgetComponentDepsFactory for standalone component with no imports', () => { + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -595,18 +682,21 @@ runInEachFileSystem(() => { }) export class MainComponent { } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents).not.toContain('i0.ɵɵgetComponentDepsFactory'); - }); + expect(jsContents).not.toContain('i0.ɵɵgetComponentDepsFactory'); + }); }); describe('component fields', () => { it('should place the changeDetection as it is into the component def', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; import {SomeWeirdThing} from 'some-where'; @@ -616,7 +706,8 @@ runInEachFileSystem(() => { }) export class MainComponent { } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -624,9 +715,10 @@ runInEachFileSystem(() => { expect(jsContents).toContain('changeDetection: SomeWeirdThing'); }); - it('should place the correct value of encapsulation into the component def - case of ViewEncapsulation.Emulated with no styles', - () => { - env.write('test.ts', ` + it('should place the correct value of encapsulation into the component def - case of ViewEncapsulation.Emulated with no styles', () => { + env.write( + 'test.ts', + ` import {Component, ViewEncapsulation} from '@angular/core'; @Component({ @@ -635,19 +727,21 @@ runInEachFileSystem(() => { }) export class MainComponent { } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - // If there is no style, don't generate css selectors on elements by setting - // encapsulation to none (=2) - expect(jsContents).toContain('encapsulation: 2'); - }); + // If there is no style, don't generate css selectors on elements by setting + // encapsulation to none (=2) + expect(jsContents).toContain('encapsulation: 2'); + }); - it('should place the correct value of encapsulation into the component def - case of ViewEncapsulation.Emulated with styles', - () => { - env.write('test.ts', ` + it('should place the correct value of encapsulation into the component def - case of ViewEncapsulation.Emulated with styles', () => { + env.write( + 'test.ts', + ` import {Component, ViewEncapsulation} from '@angular/core'; @Component({ @@ -657,19 +751,21 @@ runInEachFileSystem(() => { }) export class MainComponent { } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - // encapsulation is set only for non-default value - expect(jsContents).not.toContain('encapsulation: 0'); - expect(jsContents).toContain('styles: ["color: blue"]'); - }); + // encapsulation is set only for non-default value + expect(jsContents).not.toContain('encapsulation: 0'); + expect(jsContents).toContain('styles: ["color: blue"]'); + }); - it('should place the correct value of encapsulation into the component def - case of ViewEncapsulation.ShadowDom', - () => { - env.write('test.ts', ` + it('should place the correct value of encapsulation into the component def - case of ViewEncapsulation.ShadowDom', () => { + env.write( + 'test.ts', + ` import {Component, ViewEncapsulation} from '@angular/core'; @Component({ @@ -678,17 +774,19 @@ runInEachFileSystem(() => { }) export class MainComponent { } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('encapsulation: 3'); - }); + expect(jsContents).toContain('encapsulation: 3'); + }); - it('should place the correct value of encapsulation into the component def - case of ViewEncapsulation.None', - () => { - env.write('test.ts', ` + it('should place the correct value of encapsulation into the component def - case of ViewEncapsulation.None', () => { + env.write( + 'test.ts', + ` import {Component, ViewEncapsulation} from '@angular/core'; @Component({ @@ -697,16 +795,19 @@ runInEachFileSystem(() => { }) export class MainComponent { } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('encapsulation: 2'); - }); + expect(jsContents).toContain('encapsulation: 2'); + }); it('should default encapsulation to Emulated', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, ViewEncapsulation} from '@angular/core'; @Component({ @@ -714,7 +815,8 @@ runInEachFileSystem(() => { }) export class MainComponent { } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -726,9 +828,10 @@ runInEachFileSystem(() => { }); describe('constructor injection', () => { - it('should include injector types with all possible import/injection styles into component factory', - () => { - env.write('test.ts', ` + it('should include injector types with all possible import/injection styles into component factory', () => { + env.write( + 'test.ts', + ` import {Component, NgModule, Attribute, Inject} from '@angular/core'; import {SomeClass} from './some-where' import {SomeService1} from './some-where1' @@ -756,19 +859,21 @@ runInEachFileSystem(() => { }) export class MainModule { } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - `MainComponent.ɵfac = function MainComponent_Factory(t) { return new (t || MainComponent)(i0.ɵɵdirectiveInject(i1.SomeService1), i0.ɵɵdirectiveInject(SomeService2), i0.ɵɵdirectiveInject(i2.SomeService3), i0.ɵɵdirectiveInject(i3.nested.SomeService4), i0.ɵɵinjectAttribute('title'), i0.ɵɵdirectiveInject(MESSAGE_TOKEN)); };`); - }); + expect(jsContents).toContain( + `MainComponent.ɵfac = function MainComponent_Factory(t) { return new (t || MainComponent)(i0.ɵɵdirectiveInject(i1.SomeService1), i0.ɵɵdirectiveInject(SomeService2), i0.ɵɵdirectiveInject(i2.SomeService3), i0.ɵɵdirectiveInject(i3.nested.SomeService4), i0.ɵɵinjectAttribute('title'), i0.ɵɵdirectiveInject(MESSAGE_TOKEN)); };`, + ); + }); - it('should include injector types with all possible import/injection styles into standalone component factory', - () => { - env.write('test.ts', ` + it('should include injector types with all possible import/injection styles into standalone component factory', () => { + env.write( + 'test.ts', + ` import {Component, NgModule, Attribute, Inject} from '@angular/core'; import {SomeClass} from './some-where' import {SomeService1} from './some-where1' @@ -791,19 +896,21 @@ runInEachFileSystem(() => { @Inject(MESSAGE_TOKEN) tokenMessage: SomeClass, ) {} } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - `MainComponent.ɵfac = function MainComponent_Factory(t) { return new (t || MainComponent)(i0.ɵɵdirectiveInject(i1.SomeService1), i0.ɵɵdirectiveInject(SomeService2), i0.ɵɵdirectiveInject(i2.SomeService3), i0.ɵɵdirectiveInject(i3.nested.SomeService4), i0.ɵɵinjectAttribute('title'), i0.ɵɵdirectiveInject(MESSAGE_TOKEN)); };`); - }); + expect(jsContents).toContain( + `MainComponent.ɵfac = function MainComponent_Factory(t) { return new (t || MainComponent)(i0.ɵɵdirectiveInject(i1.SomeService1), i0.ɵɵdirectiveInject(SomeService2), i0.ɵɵdirectiveInject(i2.SomeService3), i0.ɵɵdirectiveInject(i3.nested.SomeService4), i0.ɵɵinjectAttribute('title'), i0.ɵɵdirectiveInject(MESSAGE_TOKEN)); };`, + ); + }); - it('should include injector types with all possible import/injection styles into directive factory', - () => { - env.write('test.ts', ` + it('should include injector types with all possible import/injection styles into directive factory', () => { + env.write( + 'test.ts', + ` import {Directive, NgModule, Attribute, Inject} from '@angular/core'; import {SomeClass} from './some-where' import {SomeService1} from './some-where1' @@ -829,19 +936,21 @@ runInEachFileSystem(() => { }) export class MainModule { } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - `MainDirective.ɵfac = function MainDirective_Factory(t) { return new (t || MainDirective)(i0.ɵɵdirectiveInject(i1.SomeService1), i0.ɵɵdirectiveInject(SomeService2), i0.ɵɵdirectiveInject(i2.SomeService3), i0.ɵɵdirectiveInject(i3.nested.SomeService4), i0.ɵɵinjectAttribute('title'), i0.ɵɵdirectiveInject(MESSAGE_TOKEN)); };`); - }); + expect(jsContents).toContain( + `MainDirective.ɵfac = function MainDirective_Factory(t) { return new (t || MainDirective)(i0.ɵɵdirectiveInject(i1.SomeService1), i0.ɵɵdirectiveInject(SomeService2), i0.ɵɵdirectiveInject(i2.SomeService3), i0.ɵɵdirectiveInject(i3.nested.SomeService4), i0.ɵɵinjectAttribute('title'), i0.ɵɵdirectiveInject(MESSAGE_TOKEN)); };`, + ); + }); - it('should include injector types with all possible import/injection styles into standalone directive factory', - () => { - env.write('test.ts', ` + it('should include injector types with all possible import/injection styles into standalone directive factory', () => { + env.write( + 'test.ts', + ` import {Directive, Attribute, Inject} from '@angular/core'; import {SomeClass} from './some-where' import {SomeService1} from './some-where1' @@ -862,19 +971,21 @@ runInEachFileSystem(() => { @Inject(MESSAGE_TOKEN) tokenMessage: SomeClass, ) {} } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - `MainDirective.ɵfac = function MainDirective_Factory(t) { return new (t || MainDirective)(i0.ɵɵdirectiveInject(i1.SomeService1), i0.ɵɵdirectiveInject(SomeService2), i0.ɵɵdirectiveInject(i2.SomeService3), i0.ɵɵdirectiveInject(i3.nested.SomeService4), i0.ɵɵinjectAttribute('title'), i0.ɵɵdirectiveInject(MESSAGE_TOKEN)); };`); - }); + expect(jsContents).toContain( + `MainDirective.ɵfac = function MainDirective_Factory(t) { return new (t || MainDirective)(i0.ɵɵdirectiveInject(i1.SomeService1), i0.ɵɵdirectiveInject(SomeService2), i0.ɵɵdirectiveInject(i2.SomeService3), i0.ɵɵdirectiveInject(i3.nested.SomeService4), i0.ɵɵinjectAttribute('title'), i0.ɵɵdirectiveInject(MESSAGE_TOKEN)); };`, + ); + }); - it('should include injector types with all possible import/injection styles into pipe factory', - () => { - env.write('test.ts', ` + it('should include injector types with all possible import/injection styles into pipe factory', () => { + env.write( + 'test.ts', + ` import {Pipe, NgModule, Attribute, Inject} from '@angular/core'; import {SomeClass} from './some-where' import {SomeService1} from './some-where1' @@ -899,19 +1010,21 @@ runInEachFileSystem(() => { }) export class MainModule { } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - `MainPipe.ɵfac = function MainPipe_Factory(t) { return new (t || MainPipe)(i0.ɵɵdirectiveInject(i1.SomeService1, 16), i0.ɵɵdirectiveInject(SomeService2, 16), i0.ɵɵdirectiveInject(i2.SomeService3, 16), i0.ɵɵdirectiveInject(i3.nested.SomeService4, 16), i0.ɵɵinjectAttribute('title'), i0.ɵɵdirectiveInject(MESSAGE_TOKEN, 16)); };`); - }); + expect(jsContents).toContain( + `MainPipe.ɵfac = function MainPipe_Factory(t) { return new (t || MainPipe)(i0.ɵɵdirectiveInject(i1.SomeService1, 16), i0.ɵɵdirectiveInject(SomeService2, 16), i0.ɵɵdirectiveInject(i2.SomeService3, 16), i0.ɵɵdirectiveInject(i3.nested.SomeService4, 16), i0.ɵɵinjectAttribute('title'), i0.ɵɵdirectiveInject(MESSAGE_TOKEN, 16)); };`, + ); + }); - it('should include injector types with all possible import/injection styles into standalone pipe factory', - () => { - env.write('test.ts', ` + it('should include injector types with all possible import/injection styles into standalone pipe factory', () => { + env.write( + 'test.ts', + ` import {Pipe, Attribute, Inject} from '@angular/core'; import {SomeClass} from './some-where' import {SomeService1} from './some-where1' @@ -933,19 +1046,21 @@ runInEachFileSystem(() => { @Inject(MESSAGE_TOKEN) tokenMessage: SomeClass, ) {} } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - `MainPipe.ɵfac = function MainPipe_Factory(t) { return new (t || MainPipe)(i0.ɵɵdirectiveInject(i1.SomeService1, 16), i0.ɵɵdirectiveInject(SomeService2, 16), i0.ɵɵdirectiveInject(i2.SomeService3, 16), i0.ɵɵdirectiveInject(i3.nested.SomeService4, 16), i0.ɵɵinjectAttribute('title'), i0.ɵɵdirectiveInject(MESSAGE_TOKEN, 16)); };`); - }); + expect(jsContents).toContain( + `MainPipe.ɵfac = function MainPipe_Factory(t) { return new (t || MainPipe)(i0.ɵɵdirectiveInject(i1.SomeService1, 16), i0.ɵɵdirectiveInject(SomeService2, 16), i0.ɵɵdirectiveInject(i2.SomeService3, 16), i0.ɵɵdirectiveInject(i3.nested.SomeService4, 16), i0.ɵɵinjectAttribute('title'), i0.ɵɵdirectiveInject(MESSAGE_TOKEN, 16)); };`, + ); + }); - it('should include injector types with all possible import/injection styles into injectable factory', - () => { - env.write('test.ts', ` + it('should include injector types with all possible import/injection styles into injectable factory', () => { + env.write( + 'test.ts', + ` import {Injectable, Attribute, Inject} from '@angular/core'; import {SomeClass} from './some-where' import {SomeService1} from './some-where1' @@ -966,19 +1081,21 @@ runInEachFileSystem(() => { @Inject(MESSAGE_TOKEN) tokenMessage: SomeClass, ) {} } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - `MainService.ɵfac = function MainService_Factory(t) { return new (t || MainService)(i0.ɵɵinject(i1.SomeService1), i0.ɵɵinject(SomeService2), i0.ɵɵinject(i2.SomeService3), i0.ɵɵinject(i3.nested.SomeService4), i0.ɵɵinjectAttribute('title'), i0.ɵɵinject(MESSAGE_TOKEN)); };`); - }); + expect(jsContents).toContain( + `MainService.ɵfac = function MainService_Factory(t) { return new (t || MainService)(i0.ɵɵinject(i1.SomeService1), i0.ɵɵinject(SomeService2), i0.ɵɵinject(i2.SomeService3), i0.ɵɵinject(i3.nested.SomeService4), i0.ɵɵinjectAttribute('title'), i0.ɵɵinject(MESSAGE_TOKEN)); };`, + ); + }); - it('should include injector types with all possible import/injection styles into ng module factory', - () => { - env.write('test.ts', ` + it('should include injector types with all possible import/injection styles into ng module factory', () => { + env.write( + 'test.ts', + ` import {Component, NgModule, Attribute, Inject} from '@angular/core'; import {SomeClass} from './some-where' import {SomeService1} from './some-where1' @@ -998,19 +1115,21 @@ runInEachFileSystem(() => { @Inject(MESSAGE_TOKEN) tokenMessage: SomeClass, ) {} } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - `MainModule.ɵfac = function MainModule_Factory(t) { return new (t || MainModule)(i0.ɵɵinject(i1.SomeService1), i0.ɵɵinject(SomeService2), i0.ɵɵinject(i2.SomeService3), i0.ɵɵinject(i3.nested.SomeService4), i0.ɵɵinjectAttribute('title'), i0.ɵɵinject(MESSAGE_TOKEN)); };`); - }); + expect(jsContents).toContain( + `MainModule.ɵfac = function MainModule_Factory(t) { return new (t || MainModule)(i0.ɵɵinject(i1.SomeService1), i0.ɵɵinject(SomeService2), i0.ɵɵinject(i2.SomeService3), i0.ɵɵinject(i3.nested.SomeService4), i0.ɵɵinjectAttribute('title'), i0.ɵɵinject(MESSAGE_TOKEN)); };`, + ); + }); - it('should generate invalid factory for injectable when type parameter types are used as token', - () => { - env.write('test.ts', ` + it('should generate invalid factory for injectable when type parameter types are used as token', () => { + env.write( + 'test.ts', + ` import {Injectable} from '@angular/core'; @Injectable() @@ -1019,19 +1138,21 @@ runInEachFileSystem(() => { private someService1: S, ) {} } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - `MainService.ɵfac = function MainService_Factory(t) { i0.ɵɵinvalidFactory(); };`); - }); + expect(jsContents).toContain( + `MainService.ɵfac = function MainService_Factory(t) { i0.ɵɵinvalidFactory(); };`, + ); + }); - it('should generate invalid factory for injectable when when token is imported as type', - () => { - env.write('test.ts', ` + it('should generate invalid factory for injectable when when token is imported as type', () => { + env.write( + 'test.ts', + ` import {Injectable} from '@angular/core'; import {type MyService} from './somewhere'; @@ -1041,18 +1162,21 @@ runInEachFileSystem(() => { private myService: MyService, ) {} } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - `MainService.ɵfac = function MainService_Factory(t) { i0.ɵɵinvalidFactory(); };`); - }); + expect(jsContents).toContain( + `MainService.ɵfac = function MainService_Factory(t) { i0.ɵɵinvalidFactory(); };`, + ); + }); it('should generate invalid factory for injectable when when token is a type', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Injectable} from '@angular/core'; type MyService = {a:string}; @@ -1063,18 +1187,21 @@ runInEachFileSystem(() => { private myService: MyService, ) {} } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - `MainService.ɵfac = function MainService_Factory(t) { i0.ɵɵinvalidFactory(); };`); + expect(jsContents).toContain( + `MainService.ɵfac = function MainService_Factory(t) { i0.ɵɵinvalidFactory(); };`, + ); }); it('should generate invalid factory for injectable when when token is an interface', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Injectable} from '@angular/core'; interface MyService {a:string} @@ -1085,19 +1212,21 @@ runInEachFileSystem(() => { private myService: MyService, ) {} } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - `MainService.ɵfac = function MainService_Factory(t) { i0.ɵɵinvalidFactory(); };`); + expect(jsContents).toContain( + `MainService.ɵfac = function MainService_Factory(t) { i0.ɵɵinvalidFactory(); };`, + ); }); - it('should generate invalid factory for directive without selector type parameter types are used as token', - () => { - env.write('test.ts', ` + it('should generate invalid factory for directive without selector type parameter types are used as token', () => { + env.write( + 'test.ts', + ` import {Directive} from '@angular/core'; @Directive() @@ -1106,19 +1235,21 @@ runInEachFileSystem(() => { private myService: S, ) {} } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - `MyDirective.ɵfac = function MyDirective_Factory(t) { i0.ɵɵinvalidFactory(); };`); - }); + expect(jsContents).toContain( + `MyDirective.ɵfac = function MyDirective_Factory(t) { i0.ɵɵinvalidFactory(); };`, + ); + }); - it('should generate invalid factory for directive without selector when token is imported as type', - () => { - env.write('test.ts', ` + it('should generate invalid factory for directive without selector when token is imported as type', () => { + env.write( + 'test.ts', + ` import {Directive} from '@angular/core'; import {type MyService} from './somewhere'; @@ -1128,19 +1259,21 @@ runInEachFileSystem(() => { private myService: MyService, ) {} } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - `MyDirective.ɵfac = function MyDirective_Factory(t) { i0.ɵɵinvalidFactory(); };`); - }); + expect(jsContents).toContain( + `MyDirective.ɵfac = function MyDirective_Factory(t) { i0.ɵɵinvalidFactory(); };`, + ); + }); - it('should generate invalid factory for directive without selector when token is a type', - () => { - env.write('test.ts', ` + it('should generate invalid factory for directive without selector when token is a type', () => { + env.write( + 'test.ts', + ` import {Directive} from '@angular/core'; type MyService = {a:string}; @@ -1151,19 +1284,21 @@ runInEachFileSystem(() => { private myService: MyService, ) {} } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - `MyDirective.ɵfac = function MyDirective_Factory(t) { i0.ɵɵinvalidFactory(); };`); - }); + expect(jsContents).toContain( + `MyDirective.ɵfac = function MyDirective_Factory(t) { i0.ɵɵinvalidFactory(); };`, + ); + }); - it('should generate invalid factory for directive without selector when token is an interface', - () => { - env.write('test.ts', ` + it('should generate invalid factory for directive without selector when token is an interface', () => { + env.write( + 'test.ts', + ` import {Directive} from '@angular/core'; interface MyService {a:string} @@ -1174,21 +1309,23 @@ runInEachFileSystem(() => { private myService: MyService, ) {} } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - `MyDirective.ɵfac = function MyDirective_Factory(t) { i0.ɵɵinvalidFactory(); };`); - }); + expect(jsContents).toContain( + `MyDirective.ɵfac = function MyDirective_Factory(t) { i0.ɵɵinvalidFactory(); };`, + ); + }); }); describe('LOCAL_COMPILATION_UNRESOLVED_CONST errors', () => { - it('should show correct error message when using an external symbol for component template', - () => { - env.write('test.ts', ` + it('should show correct error message when using an external symbol for component template', () => { + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; import {ExternalString} from './some-where'; @@ -1197,27 +1334,30 @@ runInEachFileSystem(() => { }) export class Main { } - `); + `, + ); - const errors = env.driveDiagnostics(); + const errors = env.driveDiagnostics(); - expect(errors.length).toBe(1); + expect(errors.length).toBe(1); - const {code, messageText, length, relatedInformation} = errors[0]; + const {code, messageText, length, relatedInformation} = errors[0]; - expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST)); - expect(length).toBe(14); - expect(relatedInformation).toBeUndefined(); + expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST)); + expect(length).toBe(14); + expect(relatedInformation).toBeUndefined(); - const text = ts.flattenDiagnosticMessageText(messageText, '\n'); + const text = ts.flattenDiagnosticMessageText(messageText, '\n'); - expect(text).toEqual( - 'Unresolved identifier found for @Component.template field! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declaration into a file within the compilation unit, 2) Inline the template, 3) Move the template into a separate .html file and include it using @Component.templateUrl'); - }); + expect(text).toEqual( + 'Unresolved identifier found for @Component.template field! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declaration into a file within the compilation unit, 2) Inline the template, 3) Move the template into a separate .html file and include it using @Component.templateUrl', + ); + }); - it('should show correct error message when using an external symbol for component styles array', - () => { - env.write('test.ts', ` + it('should show correct error message when using an external symbol for component styles array', () => { + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; import {ExternalString} from './some-where'; @@ -1230,25 +1370,29 @@ runInEachFileSystem(() => { }) export class Main { } - `); + `, + ); - const errors = env.driveDiagnostics(); + const errors = env.driveDiagnostics(); - expect(errors.length).toBe(1); + expect(errors.length).toBe(1); - const {code, messageText, relatedInformation, length} = errors[0]; + const {code, messageText, relatedInformation, length} = errors[0]; - expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST)); - expect(length).toBe(14), expect(relatedInformation).toBeUndefined(); + expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST)); + expect(length).toBe(14), expect(relatedInformation).toBeUndefined(); - const text = ts.flattenDiagnosticMessageText(messageText, '\n'); + const text = ts.flattenDiagnosticMessageText(messageText, '\n'); - expect(text).toEqual( - 'Unresolved identifier found for @Component.styles field! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declarations into a file within the compilation unit, 2) Inline the styles, 3) Move the styles into separate files and include it using @Component.styleUrls'); - }); + expect(text).toEqual( + 'Unresolved identifier found for @Component.styles field! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declarations into a file within the compilation unit, 2) Inline the styles, 3) Move the styles into separate files and include it using @Component.styleUrls', + ); + }); it('should show correct error message when using an external symbol for component styles', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; import {ExternalString} from './some-where'; @@ -1259,7 +1403,8 @@ runInEachFileSystem(() => { }) export class Main { } - `); + `, + ); const errors = env.driveDiagnostics(); @@ -1273,12 +1418,14 @@ runInEachFileSystem(() => { const text = ts.flattenDiagnosticMessageText(messageText, '\n'); expect(text).toEqual( - 'Unresolved identifier found for @Component.styles field! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declarations into a file within the compilation unit, 2) Inline the styles, 3) Move the styles into separate files and include it using @Component.styleUrls'); + 'Unresolved identifier found for @Component.styles field! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declarations into a file within the compilation unit, 2) Inline the styles, 3) Move the styles into separate files and include it using @Component.styleUrls', + ); }); - it('should show correct error message when using an external symbol for component selector', - () => { - env.write('test.ts', ` + it('should show correct error message when using an external symbol for component selector', () => { + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; import {ExternalString} from './some-where'; @@ -1289,26 +1436,29 @@ runInEachFileSystem(() => { }) export class Main { } - `); + `, + ); - const errors = env.driveDiagnostics(); + const errors = env.driveDiagnostics(); - expect(errors.length).toBe(1); + expect(errors.length).toBe(1); - const {code, messageText, relatedInformation, length} = errors[0]; + const {code, messageText, relatedInformation, length} = errors[0]; - expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST)); - expect(length).toBe(14), expect(relatedInformation).toBeUndefined(); + expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST)); + expect(length).toBe(14), expect(relatedInformation).toBeUndefined(); - const text = ts.flattenDiagnosticMessageText(messageText, '\n'); + const text = ts.flattenDiagnosticMessageText(messageText, '\n'); - expect(text).toEqual( - 'Unresolved identifier found for @Component.selector field! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declarations into a file within the compilation unit, 2) Inline the selector'); - }); + expect(text).toEqual( + 'Unresolved identifier found for @Component.selector field! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declarations into a file within the compilation unit, 2) Inline the selector', + ); + }); - it('should show correct error message when using an external symbol for component @HostListener\'s event name argument', - () => { - env.write('test.ts', ` + it("should show correct error message when using an external symbol for component @HostListener's event name argument", () => { + env.write( + 'test.ts', + ` import {Component, HostListener} from '@angular/core'; import {ExternalString} from './some-where'; @@ -1319,26 +1469,29 @@ runInEachFileSystem(() => { @HostListener(ExternalString, ['$event']) handle() {} } - `); + `, + ); - const errors = env.driveDiagnostics(); + const errors = env.driveDiagnostics(); - expect(errors.length).toBe(1); + expect(errors.length).toBe(1); - const {code, messageText, relatedInformation, length} = errors[0]; + const {code, messageText, relatedInformation, length} = errors[0]; - expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST)); - expect(length).toBe(14), expect(relatedInformation).toBeUndefined(); + expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST)); + expect(length).toBe(14), expect(relatedInformation).toBeUndefined(); - const text = ts.flattenDiagnosticMessageText(messageText, '\n'); + const text = ts.flattenDiagnosticMessageText(messageText, '\n'); - expect(text).toEqual( - `Unresolved identifier found for @HostListener's event name argument! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declaration into a file within the compilation unit, 2) Inline the argument`); - }); + expect(text).toEqual( + `Unresolved identifier found for @HostListener's event name argument! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declaration into a file within the compilation unit, 2) Inline the argument`, + ); + }); - it('should show correct error message when using an external symbol for directive @HostListener\'s event name argument', - () => { - env.write('test.ts', ` + it("should show correct error message when using an external symbol for directive @HostListener's event name argument", () => { + env.write( + 'test.ts', + ` import {Directive, HostListener} from '@angular/core'; import {ExternalString} from './some-where'; @@ -1347,26 +1500,29 @@ runInEachFileSystem(() => { @HostListener(ExternalString, ['$event']) handle() {} } - `); + `, + ); - const errors = env.driveDiagnostics(); + const errors = env.driveDiagnostics(); - expect(errors.length).toBe(1); + expect(errors.length).toBe(1); - const {code, messageText, relatedInformation, length} = errors[0]; + const {code, messageText, relatedInformation, length} = errors[0]; - expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST)); - expect(length).toBe(14), expect(relatedInformation).toBeUndefined(); + expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST)); + expect(length).toBe(14), expect(relatedInformation).toBeUndefined(); - const text = ts.flattenDiagnosticMessageText(messageText, '\n'); + const text = ts.flattenDiagnosticMessageText(messageText, '\n'); - expect(text).toEqual( - `Unresolved identifier found for @HostListener's event name argument! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declaration into a file within the compilation unit, 2) Inline the argument`); - }); + expect(text).toEqual( + `Unresolved identifier found for @HostListener's event name argument! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declaration into a file within the compilation unit, 2) Inline the argument`, + ); + }); - it('should show correct error message when using an external symbol for component @HostBinding\'s argument', - () => { - env.write('test.ts', ` + it("should show correct error message when using an external symbol for component @HostBinding's argument", () => { + env.write( + 'test.ts', + ` import {Component, HostBinding} from '@angular/core'; import {ExternalString} from './some-where'; @@ -1374,26 +1530,29 @@ runInEachFileSystem(() => { export class Main { @HostBinding(ExternalString) id = 'some thing'; } - `); + `, + ); - const errors = env.driveDiagnostics(); + const errors = env.driveDiagnostics(); - expect(errors.length).toBe(1); + expect(errors.length).toBe(1); - const {code, messageText, relatedInformation, length} = errors[0]; + const {code, messageText, relatedInformation, length} = errors[0]; - expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST)); - expect(length).toBe(14), expect(relatedInformation).toBeUndefined(); + expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST)); + expect(length).toBe(14), expect(relatedInformation).toBeUndefined(); - const text = ts.flattenDiagnosticMessageText(messageText, '\n'); + const text = ts.flattenDiagnosticMessageText(messageText, '\n'); - expect(text).toEqual( - `Unresolved identifier found for @HostBinding's argument! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declaration into a file within the compilation unit, 2) Inline the argument`); - }); + expect(text).toEqual( + `Unresolved identifier found for @HostBinding's argument! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declaration into a file within the compilation unit, 2) Inline the argument`, + ); + }); - it('should show correct error message when using an external symbol for directive @HostBinding\'s argument', - () => { - env.write('test.ts', ` + it("should show correct error message when using an external symbol for directive @HostBinding's argument", () => { + env.write( + 'test.ts', + ` import {Directive, HostBinding} from '@angular/core'; import {ExternalString} from './some-where'; @@ -1401,54 +1560,60 @@ runInEachFileSystem(() => { export class Main { @HostBinding(ExternalString) id = 'some thing'; } - `); + `, + ); - const errors = env.driveDiagnostics(); + const errors = env.driveDiagnostics(); - expect(errors.length).toBe(1); + expect(errors.length).toBe(1); - const {code, messageText, relatedInformation, length} = errors[0]; + const {code, messageText, relatedInformation, length} = errors[0]; - expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST)); - expect(length).toBe(14), expect(relatedInformation).toBeUndefined(); + expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST)); + expect(length).toBe(14), expect(relatedInformation).toBeUndefined(); - const text = ts.flattenDiagnosticMessageText(messageText, '\n'); + const text = ts.flattenDiagnosticMessageText(messageText, '\n'); - expect(text).toEqual( - `Unresolved identifier found for @HostBinding's argument! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declaration into a file within the compilation unit, 2) Inline the argument`); - }); + expect(text).toEqual( + `Unresolved identifier found for @HostBinding's argument! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declaration into a file within the compilation unit, 2) Inline the argument`, + ); + }); - it('should show correct error message when using an external symbol for @Directive.exportAs argument', - () => { - env.write('test.ts', ` + it('should show correct error message when using an external symbol for @Directive.exportAs argument', () => { + env.write( + 'test.ts', + ` import {Directive} from '@angular/core'; import {ExternalString} from './some-where'; @Directive({selector: '[test]', exportAs: ExternalString}) export class Main { } - `); + `, + ); - const errors = env.driveDiagnostics(); + const errors = env.driveDiagnostics(); - expect(errors.length).toBe(1); + expect(errors.length).toBe(1); - const {code, messageText, relatedInformation, length} = errors[0]; + const {code, messageText, relatedInformation, length} = errors[0]; - expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST)); - expect(length).toBe(14), expect(relatedInformation).toBeUndefined(); + expect(code).toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST)); + expect(length).toBe(14), expect(relatedInformation).toBeUndefined(); - const text = ts.flattenDiagnosticMessageText(messageText, '\n'); + const text = ts.flattenDiagnosticMessageText(messageText, '\n'); - expect(text).toEqual( - `Unresolved identifier found for exportAs field! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declarations into a file within the compilation unit, 2) Inline the selector`); - }); + expect(text).toEqual( + `Unresolved identifier found for exportAs field! Did you import this identifier from a file outside of the compilation unit? This is not allowed when Angular compiler runs in local mode. Possible solutions: 1) Move the declarations into a file within the compilation unit, 2) Inline the selector`, + ); + }); }); describe('ng module bootstrap def', () => { - it('should include the bootstrap definition in ɵɵsetNgModuleScope instead of ɵɵdefineNgModule', - () => { - env.write('test.ts', ` + it('should include the bootstrap definition in ɵɵsetNgModuleScope instead of ɵɵdefineNgModule', () => { + env.write( + 'test.ts', + ` import {NgModule} from '@angular/core'; import {App} from './some-where'; @@ -1458,22 +1623,24 @@ runInEachFileSystem(() => { }) export class AppModule { } - `); - - env.driveMain(); - const jsContents = env.getContents('test.js'); - - expect(jsContents) - .toContain( - 'AppModule.ɵmod = /*@__PURE__*/ i0.ɵɵdefineNgModule({ type: AppModule });'); - expect(jsContents) - .toContain( - 'ɵɵsetNgModuleScope(AppModule, { declarations: [App], bootstrap: [App] }); })();'); - }); - - it('should include no bootstrap definition in ɵɵsetNgModuleScope if the NgModule has no bootstrap field', - () => { - env.write('test.ts', ` + `, + ); + + env.driveMain(); + const jsContents = env.getContents('test.js'); + + expect(jsContents).toContain( + 'AppModule.ɵmod = /*@__PURE__*/ i0.ɵɵdefineNgModule({ type: AppModule });', + ); + expect(jsContents).toContain( + 'ɵɵsetNgModuleScope(AppModule, { declarations: [App], bootstrap: [App] }); })();', + ); + }); + + it('should include no bootstrap definition in ɵɵsetNgModuleScope if the NgModule has no bootstrap field', () => { + env.write( + 'test.ts', + ` import {NgModule} from '@angular/core'; import {App} from './some-where'; @@ -1482,22 +1649,26 @@ runInEachFileSystem(() => { }) export class AppModule { } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - 'AppModule.ɵmod = /*@__PURE__*/ i0.ɵɵdefineNgModule({ type: AppModule });'); - expect(jsContents) - .toContain('ɵɵsetNgModuleScope(AppModule, { declarations: [App] }); })();'); - }); + expect(jsContents).toContain( + 'AppModule.ɵmod = /*@__PURE__*/ i0.ɵɵdefineNgModule({ type: AppModule });', + ); + expect(jsContents).toContain( + 'ɵɵsetNgModuleScope(AppModule, { declarations: [App] }); })();', + ); + }); }); describe('host directives', () => { it('should generate component hostDirectives definition without input and output', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Component} from '@angular/core'; import {ExternalDirective} from 'some_where'; import * as n from 'some_where2'; @@ -1512,19 +1683,21 @@ runInEachFileSystem(() => { hostDirectives: [ExternalDirective, n.ExternalDirective, LocalDirective] }) export class MyComp {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - 'features: [i0.ɵɵHostDirectivesFeature([ExternalDirective, n.ExternalDirective, LocalDirective])]'); + expect(jsContents).toContain( + 'features: [i0.ɵɵHostDirectivesFeature([ExternalDirective, n.ExternalDirective, LocalDirective])]', + ); }); - it('should generate component hostDirectives definition for externally imported directives with input and output', - () => { - env.write('test.ts', ` + it('should generate component hostDirectives definition for externally imported directives with input and output', () => { + env.write( + 'test.ts', + ` import {Directive, Component} from '@angular/core'; import {HostDir} from 'some_where'; @@ -1538,21 +1711,23 @@ runInEachFileSystem(() => { }], }) export class MyComp {} - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - 'features: [i0.ɵɵHostDirectivesFeature([{ directive: HostDir, ' + - 'inputs: ["value", "value", "color", "colorAlias"], ' + - 'outputs: ["opened", "opened", "closed", "closedAlias"] }])]'); - }); + expect(jsContents).toContain( + 'features: [i0.ɵɵHostDirectivesFeature([{ directive: HostDir, ' + + 'inputs: ["value", "value", "color", "colorAlias"], ' + + 'outputs: ["opened", "opened", "closed", "closedAlias"] }])]', + ); + }); - it('should generate component hostDirectives definition for local directives with input and output', - () => { - env.write('test.ts', ` + it('should generate component hostDirectives definition for local directives with input and output', () => { + env.write( + 'test.ts', + ` import {Directive, Component} from '@angular/core'; @Directive({standalone: true}) @@ -1569,20 +1744,23 @@ runInEachFileSystem(() => { }], }) export class MyComp {} - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - 'features: [i0.ɵɵHostDirectivesFeature([{ directive: LocalDirective, ' + - 'inputs: ["value", "value", "color", "colorAlias"], ' + - 'outputs: ["opened", "opened", "closed", "closedAlias"] }])]'); - }); + expect(jsContents).toContain( + 'features: [i0.ɵɵHostDirectivesFeature([{ directive: LocalDirective, ' + + 'inputs: ["value", "value", "color", "colorAlias"], ' + + 'outputs: ["opened", "opened", "closed", "closedAlias"] }])]', + ); + }); it('should generate directive hostDirectives definitions', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Component} from '@angular/core'; import {ExternalDirective} from 'some_where'; @@ -1599,23 +1777,26 @@ runInEachFileSystem(() => { }) export class LocalDirective2 { } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - 'ɵɵdefineDirective({ type: LocalDirective, standalone: true, ' + - 'features: [i0.ɵɵHostDirectivesFeature([ExternalDirective])] });'); - expect(jsContents) - .toContain( - 'ɵɵdefineDirective({ type: LocalDirective2, standalone: true, ' + - 'features: [i0.ɵɵHostDirectivesFeature([LocalDirective])] });'); + expect(jsContents).toContain( + 'ɵɵdefineDirective({ type: LocalDirective, standalone: true, ' + + 'features: [i0.ɵɵHostDirectivesFeature([ExternalDirective])] });', + ); + expect(jsContents).toContain( + 'ɵɵdefineDirective({ type: LocalDirective2, standalone: true, ' + + 'features: [i0.ɵɵHostDirectivesFeature([LocalDirective])] });', + ); }); it('should generate hostDirectives definition with forward references of local directives', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, forwardRef, Input} from '@angular/core'; @Component({ @@ -1637,22 +1818,24 @@ runInEachFileSystem(() => { export class DirectiveA { @Input() value: any; } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - 'features: [i0.ɵɵHostDirectivesFeature(function () { return [DirectiveB]; })]'); - expect(jsContents) - .toContain( - 'features: [i0.ɵɵHostDirectivesFeature(function () { return [{ directive: DirectiveA, inputs: ["value", "value"] }]; })]'); + expect(jsContents).toContain( + 'features: [i0.ɵɵHostDirectivesFeature(function () { return [DirectiveB]; })]', + ); + expect(jsContents).toContain( + 'features: [i0.ɵɵHostDirectivesFeature(function () { return [{ directive: DirectiveA, inputs: ["value", "value"] }]; })]', + ); }); - it('should produce fatal diagnostics for host directives with forward references of externally imported directive', - () => { - env.write('test.ts', ` + it('should produce fatal diagnostics for host directives with forward references of externally imported directive', () => { + env.write( + 'test.ts', + ` import {Component, Directive, forwardRef} from '@angular/core'; import {ExternalDirective} from 'some_where'; @@ -1664,22 +1847,25 @@ runInEachFileSystem(() => { }) export class MyComponent { } - `); + `, + ); - const messages = env.driveDiagnostics(); + const messages = env.driveDiagnostics(); - expect(messages[0].code) - .toBe(ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION)); - expect(messages[0].messageText) - .toEqual( - 'In local compilation mode, host directive cannot be an expression. Use an identifier instead'); - }); + expect(messages[0].code).toBe( + ngErrorCode(ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION), + ); + expect(messages[0].messageText).toEqual( + 'In local compilation mode, host directive cannot be an expression. Use an identifier instead', + ); + }); }); - describe('input transform', () => { it('should generate input info for transform function imported externally using name', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, NgModule, Input} from '@angular/core'; import {externalFunc} from './some_where'; @@ -1690,7 +1876,8 @@ runInEachFileSystem(() => { @Input({transform: externalFunc}) x: string = ''; } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -1698,9 +1885,10 @@ runInEachFileSystem(() => { expect(jsContents).toContain('inputs: { x: [2, "x", "x", externalFunc] }'); }); - it('should generate input info for transform function imported externally using namespace', - () => { - env.write('test.ts', ` + it('should generate input info for transform function imported externally using namespace', () => { + env.write( + 'test.ts', + ` import {Component, NgModule, Input} from '@angular/core'; import * as n from './some_where'; @@ -1711,16 +1899,19 @@ runInEachFileSystem(() => { @Input({transform: n.externalFunc}) x: string = ''; } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); + env.driveMain(); + const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('inputs: { x: [2, "x", "x", n.externalFunc] }'); - }); + expect(jsContents).toContain('inputs: { x: [2, "x", "x", n.externalFunc] }'); + }); it('should generate input info for transform function defined locally', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, NgModule, Input} from '@angular/core'; @Component({ @@ -1734,7 +1925,8 @@ runInEachFileSystem(() => { function localFunc(value: string) { return value; } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -1743,7 +1935,9 @@ runInEachFileSystem(() => { }); it('should generate input info for inline transform function', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, NgModule, Input} from '@angular/core'; @Component({ @@ -1753,7 +1947,8 @@ runInEachFileSystem(() => { @Input({transform: (v: string) => v + 'TRANSFORMED!'}) x: string = ''; } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -1762,7 +1957,9 @@ runInEachFileSystem(() => { }); it('should not check inline function param type', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, NgModule, Input} from '@angular/core'; @Component({ @@ -1772,7 +1969,8 @@ runInEachFileSystem(() => { @Input({transform: v => v + 'TRANSFORMED!'}) x: string = ''; } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -1787,7 +1985,9 @@ runInEachFileSystem(() => { }); it('should handle `@Component.deferredImports` field', () => { - env.write('deferred-a.ts', ` + env.write( + 'deferred-a.ts', + ` import {Component} from '@angular/core'; @Component({ standalone: true, @@ -1796,9 +1996,12 @@ runInEachFileSystem(() => { }) export class DeferredCmpA { } - `); + `, + ); - env.write('deferred-b.ts', ` + env.write( + 'deferred-b.ts', + ` import {Component} from '@angular/core'; @Component({ standalone: true, @@ -1807,9 +2010,12 @@ runInEachFileSystem(() => { }) export class DeferredCmpB { } - `); + `, + ); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; import {DeferredCmpA} from './deferred-a'; import {DeferredCmpB} from './deferred-b'; @@ -1827,7 +2033,8 @@ runInEachFileSystem(() => { }) export class AppCmp { } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -1835,11 +2042,11 @@ runInEachFileSystem(() => { // Expect that all deferrableImports in local compilation mode // are located in a single function (since we can't detect in // the local mode which components belong to which block). - expect(jsContents) - .toContain( - 'const AppCmp_DeferFn = () => [' + - 'import("./deferred-a").then(m => m.DeferredCmpA), ' + - 'import("./deferred-b").then(m => m.DeferredCmpB)];'); + expect(jsContents).toContain( + 'const AppCmp_DeferFn = () => [' + + 'import("./deferred-a").then(m => m.DeferredCmpA), ' + + 'import("./deferred-b").then(m => m.DeferredCmpB)];', + ); // Make sure there are no eager imports present in the output. expect(jsContents).not.toContain(`from './deferred-a'`); @@ -1850,16 +2057,18 @@ runInEachFileSystem(() => { expect(jsContents).toContain('ɵɵdefer(4, 3, AppCmp_DeferFn);'); // Expect `ɵsetClassMetadataAsync` to contain dynamic imports too. - expect(jsContents) - .toContain( - 'ɵsetClassMetadataAsync(AppCmp, () => [' + - 'import("./deferred-a").then(m => m.DeferredCmpA), ' + - 'import("./deferred-b").then(m => m.DeferredCmpB)], ' + - '(DeferredCmpA, DeferredCmpB) => {'); + expect(jsContents).toContain( + 'ɵsetClassMetadataAsync(AppCmp, () => [' + + 'import("./deferred-a").then(m => m.DeferredCmpA), ' + + 'import("./deferred-b").then(m => m.DeferredCmpB)], ' + + '(DeferredCmpA, DeferredCmpB) => {', + ); }); it('should handle `@Component.imports` field', () => { - env.write('deferred-a.ts', ` + env.write( + 'deferred-a.ts', + ` import {Component} from '@angular/core'; @Component({ standalone: true, @@ -1868,9 +2077,12 @@ runInEachFileSystem(() => { }) export class DeferredCmpA { } - `); + `, + ); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; import {DeferredCmpA} from './deferred-a'; @Component({ @@ -1884,7 +2096,8 @@ runInEachFileSystem(() => { }) export class AppCmp { } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -1905,9 +2118,10 @@ runInEachFileSystem(() => { expect(jsContents).toContain('ɵsetClassMetadata(AppCmp,'); }); - it('should handle defer blocks that rely on deps from `deferredImports` and `imports`', - () => { - env.write('eager-a.ts', ` + it('should handle defer blocks that rely on deps from `deferredImports` and `imports`', () => { + env.write( + 'eager-a.ts', + ` import {Component} from '@angular/core'; @Component({ standalone: true, @@ -1916,9 +2130,12 @@ runInEachFileSystem(() => { }) export class EagerCmpA { } - `); + `, + ); - env.write('deferred-a.ts', ` + env.write( + 'deferred-a.ts', + ` import {Component} from '@angular/core'; @Component({ standalone: true, @@ -1927,9 +2144,12 @@ runInEachFileSystem(() => { }) export class DeferredCmpA { } - `); + `, + ); - env.write('deferred-b.ts', ` + env.write( + 'deferred-b.ts', + ` import {Component} from '@angular/core'; @Component({ standalone: true, @@ -1938,9 +2158,12 @@ runInEachFileSystem(() => { }) export class DeferredCmpB { } - `); + `, + ); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; import {DeferredCmpA} from './deferred-a'; import {DeferredCmpB} from './deferred-b'; @@ -1962,45 +2185,49 @@ runInEachFileSystem(() => { }) export class AppCmp { } - `); - - env.driveMain(); - const jsContents = env.getContents('test.js'); - - // Expect that all deferrableImports in local compilation mode - // are located in a single function (since we can't detect in - // the local mode which components belong to which block). - // Eager dependencies are **not* included here. - expect(jsContents) - .toContain( - 'const AppCmp_DeferFn = () => [' + - 'import("./deferred-a").then(m => m.DeferredCmpA), ' + - 'import("./deferred-b").then(m => m.DeferredCmpB)];'); - - // Make sure there are no eager imports present in the output. - expect(jsContents).not.toContain(`from './deferred-a'`); - expect(jsContents).not.toContain(`from './deferred-b'`); - - // Eager dependencies retain their imports. - expect(jsContents).toContain(`from './eager-a';`); - - // All defer instructions use the same dependency function. - expect(jsContents).toContain('ɵɵdefer(1, 0, AppCmp_DeferFn);'); - expect(jsContents).toContain('ɵɵdefer(4, 3, AppCmp_DeferFn);'); - - // Expect `ɵsetClassMetadataAsync` to contain dynamic imports too. - expect(jsContents) - .toContain( - 'ɵsetClassMetadataAsync(AppCmp, () => [' + - 'import("./deferred-a").then(m => m.DeferredCmpA), ' + - 'import("./deferred-b").then(m => m.DeferredCmpB)], ' + - '(DeferredCmpA, DeferredCmpB) => {'); - }); - - it('should support importing multiple deferrable deps from a single file ' + - 'and use them within `@Component.deferrableImports` field', - () => { - env.write('deferred-deps.ts', ` + `, + ); + + env.driveMain(); + const jsContents = env.getContents('test.js'); + + // Expect that all deferrableImports in local compilation mode + // are located in a single function (since we can't detect in + // the local mode which components belong to which block). + // Eager dependencies are **not* included here. + expect(jsContents).toContain( + 'const AppCmp_DeferFn = () => [' + + 'import("./deferred-a").then(m => m.DeferredCmpA), ' + + 'import("./deferred-b").then(m => m.DeferredCmpB)];', + ); + + // Make sure there are no eager imports present in the output. + expect(jsContents).not.toContain(`from './deferred-a'`); + expect(jsContents).not.toContain(`from './deferred-b'`); + + // Eager dependencies retain their imports. + expect(jsContents).toContain(`from './eager-a';`); + + // All defer instructions use the same dependency function. + expect(jsContents).toContain('ɵɵdefer(1, 0, AppCmp_DeferFn);'); + expect(jsContents).toContain('ɵɵdefer(4, 3, AppCmp_DeferFn);'); + + // Expect `ɵsetClassMetadataAsync` to contain dynamic imports too. + expect(jsContents).toContain( + 'ɵsetClassMetadataAsync(AppCmp, () => [' + + 'import("./deferred-a").then(m => m.DeferredCmpA), ' + + 'import("./deferred-b").then(m => m.DeferredCmpB)], ' + + '(DeferredCmpA, DeferredCmpB) => {', + ); + }); + + it( + 'should support importing multiple deferrable deps from a single file ' + + 'and use them within `@Component.deferrableImports` field', + () => { + env.write( + 'deferred-deps.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -2018,9 +2245,12 @@ runInEachFileSystem(() => { }) export class DeferredCmpB { } - `); + `, + ); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; // This import brings multiple symbols, but all of them are @@ -2049,44 +2279,49 @@ runInEachFileSystem(() => { \`, }) export class AppCmpB {} - `); - - env.driveMain(); - const jsContents = env.getContents('test.js'); - - // Expect that we generate 2 different defer functions - // (one for each component). - expect(jsContents) - .toContain( - 'const AppCmpA_DeferFn = () => [' + - 'import("./deferred-deps").then(m => m.DeferredCmpA)]'); - expect(jsContents) - .toContain( - 'const AppCmpB_DeferFn = () => [' + - 'import("./deferred-deps").then(m => m.DeferredCmpB)]'); - - // Make sure there are no eager imports present in the output. - expect(jsContents).not.toContain(`from './deferred-deps'`); - - // Defer instructions use per-component dependency function. - expect(jsContents).toContain('ɵɵdefer(1, 0, AppCmpA_DeferFn)'); - expect(jsContents).toContain('ɵɵdefer(1, 0, AppCmpB_DeferFn)'); - - // Expect `ɵsetClassMetadataAsync` to contain dynamic imports too. - expect(jsContents) - .toContain( - 'ɵsetClassMetadataAsync(AppCmpA, () => [' + - 'import("./deferred-deps").then(m => m.DeferredCmpA)]'); - expect(jsContents) - .toContain( - 'ɵsetClassMetadataAsync(AppCmpB, () => [' + - 'import("./deferred-deps").then(m => m.DeferredCmpB)]'); - }); - - it('should produce a diagnostic in case imports with symbols used ' + - 'in `deferredImports` can not be removed', - () => { - env.write('deferred-deps.ts', ` + `, + ); + + env.driveMain(); + const jsContents = env.getContents('test.js'); + + // Expect that we generate 2 different defer functions + // (one for each component). + expect(jsContents).toContain( + 'const AppCmpA_DeferFn = () => [' + + 'import("./deferred-deps").then(m => m.DeferredCmpA)]', + ); + expect(jsContents).toContain( + 'const AppCmpB_DeferFn = () => [' + + 'import("./deferred-deps").then(m => m.DeferredCmpB)]', + ); + + // Make sure there are no eager imports present in the output. + expect(jsContents).not.toContain(`from './deferred-deps'`); + + // Defer instructions use per-component dependency function. + expect(jsContents).toContain('ɵɵdefer(1, 0, AppCmpA_DeferFn)'); + expect(jsContents).toContain('ɵɵdefer(1, 0, AppCmpB_DeferFn)'); + + // Expect `ɵsetClassMetadataAsync` to contain dynamic imports too. + expect(jsContents).toContain( + 'ɵsetClassMetadataAsync(AppCmpA, () => [' + + 'import("./deferred-deps").then(m => m.DeferredCmpA)]', + ); + expect(jsContents).toContain( + 'ɵsetClassMetadataAsync(AppCmpB, () => [' + + 'import("./deferred-deps").then(m => m.DeferredCmpB)]', + ); + }, + ); + + it( + 'should produce a diagnostic in case imports with symbols used ' + + 'in `deferredImports` can not be removed', + () => { + env.write( + 'deferred-deps.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -2106,9 +2341,12 @@ runInEachFileSystem(() => { } export function utilityFn() {} - `); + `, + ); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; // This import can not be removed, since it'd contain @@ -2149,29 +2387,33 @@ runInEachFileSystem(() => { template: 'Component without any dependencies' }) export class ComponentWithoutDeps {} - `); - - const diags = env.driveDiagnostics(); - - // Expect 2 diagnostics: one for each component `AppCmpA` and `AppCmpB`, - // since both of them refer to symbols from an import declaration that - // can not be removed. - expect(diags.length).toBe(2); - - for (let i = 0; i < 2; i++) { - const {code, messageText} = diags[i]; - expect(code).toBe(ngErrorCode(ErrorCode.DEFERRED_DEPENDENCY_IMPORTED_EAGERLY)); - expect(messageText) - .toContain( - 'This import contains symbols that are used both inside and outside ' + - 'of the `@Component.deferredImports` fields in the file.'); - } - }); + `, + ); + + const diags = env.driveDiagnostics(); + + // Expect 2 diagnostics: one for each component `AppCmpA` and `AppCmpB`, + // since both of them refer to symbols from an import declaration that + // can not be removed. + expect(diags.length).toBe(2); + + for (let i = 0; i < 2; i++) { + const {code, messageText} = diags[i]; + expect(code).toBe(ngErrorCode(ErrorCode.DEFERRED_DEPENDENCY_IMPORTED_EAGERLY)); + expect(messageText).toContain( + 'This import contains symbols that are used both inside and outside ' + + 'of the `@Component.deferredImports` fields in the file.', + ); + } + }, + ); }); describe('custom decorator', () => { it('should produce diagnostic for each custom decorator', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; import {customDecorator1, customDecorator2} from './some-where'; @@ -2182,7 +2424,8 @@ runInEachFileSystem(() => { @customDecorator2 export class Main { } - `); + `, + ); const errors = env.driveDiagnostics(); @@ -2194,9 +2437,11 @@ runInEachFileSystem(() => { expect(errors[0].code).toBe(ngErrorCode(ErrorCode.DECORATOR_UNEXPECTED)); expect(errors[1].code).toBe(ngErrorCode(ErrorCode.DECORATOR_UNEXPECTED)); expect(text1).toContain( - 'In local compilation mode, Angular does not support custom decorators'); + 'In local compilation mode, Angular does not support custom decorators', + ); expect(text2).toContain( - 'In local compilation mode, Angular does not support custom decorators'); + 'In local compilation mode, Angular does not support custom decorators', + ); }); }); }); diff --git a/packages/compiler-cli/test/ngtsc/ls_typecheck_helpers_spec.ts b/packages/compiler-cli/test/ngtsc/ls_typecheck_helpers_spec.ts index bdfbe36dce12c..4550fef8fa722 100644 --- a/packages/compiler-cli/test/ngtsc/ls_typecheck_helpers_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ls_typecheck_helpers_spec.ts @@ -12,7 +12,11 @@ import {DiagnosticCategoryLabel} from '../../src/ngtsc/core/api'; import {ErrorCode, ngErrorCode} from '../../src/ngtsc/diagnostics'; import {absoluteFrom as _, getSourceFileOrError} from '../../src/ngtsc/file_system'; import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing'; -import {expectCompleteReuse, getSourceCodeForDiagnostic, loadStandardTestFiles} from '../../src/ngtsc/testing'; +import { + expectCompleteReuse, + getSourceCodeForDiagnostic, + loadStandardTestFiles, +} from '../../src/ngtsc/testing'; import {factory as invalidBananaInBoxFactory} from '../../src/ngtsc/typecheck/extended/checks/invalid_banana_in_box'; import {NgtscTestEnvironment} from './env'; @@ -20,8 +24,6 @@ import {getClass} from './util'; const testFiles = loadStandardTestFiles({fakeCommon: true}); - - runInEachFileSystem(() => { describe('full type checking', () => { let env!: NgtscTestEnvironment; @@ -33,7 +35,9 @@ runInEachFileSystem(() => { describe('supports `getPrimaryAngularDecorator()` ', () => { it('for components', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -42,7 +46,8 @@ runInEachFileSystem(() => { template: '
', }) export class TestCmp {} - `); + `, + ); const {program, checker} = env.driveTemplateTypeChecker(); const sf = program.getSourceFile(_('/test.ts')); expect(sf).not.toBeNull(); @@ -51,7 +56,9 @@ runInEachFileSystem(() => { }); it('for pipes', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Pipe, PipeTransform} from '@angular/core'; @Pipe({name: 'expPipe'}) @@ -60,7 +67,8 @@ runInEachFileSystem(() => { return Math.pow(value, exponent); } } - `); + `, + ); const {program, checker} = env.driveTemplateTypeChecker(); const sf = program.getSourceFile(_('/test.ts')); expect(sf).not.toBeNull(); @@ -69,7 +77,9 @@ runInEachFileSystem(() => { }); it('for NgModules', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {NgModule} from '@angular/core'; @NgModule({ @@ -79,7 +89,8 @@ runInEachFileSystem(() => { bootstrap: [] }) export class AppModule {} - `); + `, + ); const {program, checker} = env.driveTemplateTypeChecker(); const sf = program.getSourceFile(_('/test.ts')); expect(sf).not.toBeNull(); @@ -90,7 +101,9 @@ runInEachFileSystem(() => { describe('supports `getOwningNgModule()` ', () => { it('for components', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, NgModule} from '@angular/core'; @NgModule({ @@ -106,7 +119,8 @@ runInEachFileSystem(() => { template: '
', }) export class AppCmp {} - `); + `, + ); const {program, checker} = env.driveTemplateTypeChecker(); const sf = program.getSourceFile(_('/test.ts')); expect(sf).not.toBeNull(); @@ -117,7 +131,9 @@ runInEachFileSystem(() => { }); it('for standalone components (which should be null)', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, NgModule} from '@angular/core'; @NgModule({ @@ -134,7 +150,8 @@ runInEachFileSystem(() => { standalone: true, }) export class AppCmp {} - `); + `, + ); const {program, checker} = env.driveTemplateTypeChecker(); const sf = program.getSourceFile(_('/test.ts')); expect(sf).not.toBeNull(); @@ -145,7 +162,9 @@ runInEachFileSystem(() => { }); it('for pipes', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, NgModule, Pipe, PipeTransform} from '@angular/core'; @NgModule({ @@ -161,7 +180,8 @@ runInEachFileSystem(() => { return Math.pow(value, exponent); } } - `); + `, + ); const {program, checker} = env.driveTemplateTypeChecker(); const sf = program.getSourceFile(_('/test.ts')); expect(sf).not.toBeNull(); @@ -174,7 +194,9 @@ runInEachFileSystem(() => { describe('can retrieve candidate directives` ', () => { it('which are out of scope', () => { - env.write('one.ts', ` + env.write( + 'one.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -183,9 +205,12 @@ runInEachFileSystem(() => { template: '
', }) export class OneCmp {} - `); + `, + ); - env.write('two.ts', ` + env.write( + 'two.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -194,18 +219,21 @@ runInEachFileSystem(() => { template: '
', }) export class TwoCmp {} - `); + `, + ); const {program, checker} = env.driveTemplateTypeChecker(); const sf = program.getSourceFile(_('/one.ts')); expect(sf).not.toBeNull(); const directives = checker.getPotentialTemplateDirectives(getClass(sf!, 'OneCmp')); - expect(directives.map(d => d.selector)).toContain('two-cmp'); + expect(directives.map((d) => d.selector)).toContain('two-cmp'); }); }); describe('can retrieve candidate pipes` ', () => { it('which are out of scope', () => { - env.write('one.ts', ` + env.write( + 'one.ts', + ` import {Pipe} from '@angular/core'; @Pipe({ @@ -214,9 +242,12 @@ runInEachFileSystem(() => { }) export class OnePipe { } - `); + `, + ); - env.write('two.ts', ` + env.write( + 'two.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -225,18 +256,21 @@ runInEachFileSystem(() => { template: '
', }) export class TwoCmp {} - `); + `, + ); const {program, checker} = env.driveTemplateTypeChecker(); const sf = program.getSourceFile(_('/one.ts')); expect(sf).not.toBeNull(); const pipes = checker.getPotentialPipes(getClass(sf!, 'OnePipe')); - expect(pipes.map(p => p.name)).toContain('foo-pipe'); + expect(pipes.map((p) => p.name)).toContain('foo-pipe'); }); }); describe('can generate imports` ', () => { it('for out of scope standalone components', () => { - env.write('one.ts', ` + env.write( + 'one.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -245,9 +279,12 @@ runInEachFileSystem(() => { template: '
', }) export class OneCmp {} - `); + `, + ); - env.write('two.ts', ` + env.write( + 'two.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -256,16 +293,21 @@ runInEachFileSystem(() => { template: '
', }) export class TwoCmp {} - `); + `, + ); const {program, checker} = env.driveTemplateTypeChecker(); const sfOne = program.getSourceFile(_('/one.ts')); expect(sfOne).not.toBeNull(); const OneCmpClass = getClass(sfOne!, 'OneCmp'); - const TwoCmpDir = checker.getPotentialTemplateDirectives(OneCmpClass) - .filter(d => d.selector === 'two-cmp')[0]; - const imports = - checker.getPotentialImportsFor(TwoCmpDir.ref, OneCmpClass, PotentialImportMode.Normal); + const TwoCmpDir = checker + .getPotentialTemplateDirectives(OneCmpClass) + .filter((d) => d.selector === 'two-cmp')[0]; + const imports = checker.getPotentialImportsFor( + TwoCmpDir.ref, + OneCmpClass, + PotentialImportMode.Normal, + ); expect(imports.length).toBe(1); expect(imports[0].moduleSpecifier).toBe('./two'); @@ -274,7 +316,9 @@ runInEachFileSystem(() => { }); it('for out of scope ngModules', () => { - env.write('one.ts', ` + env.write( + 'one.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -283,9 +327,12 @@ runInEachFileSystem(() => { template: '
', }) export class OneCmp {} - `); + `, + ); - env.write('two.ts', ` + env.write( + 'two.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -293,9 +340,12 @@ runInEachFileSystem(() => { template: '
', }) export class TwoCmp {} - `); + `, + ); - env.write('twomod.ts', ` + env.write( + 'twomod.ts', + ` import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { TwoCmp } from './two'; @@ -312,17 +362,22 @@ runInEachFileSystem(() => { ] }) export class TwoModule { } - `); + `, + ); const {program, checker} = env.driveTemplateTypeChecker(); const sfOne = program.getSourceFile(_('/one.ts')); expect(sfOne).not.toBeNull(); const OneCmpClass = getClass(sfOne!, 'OneCmp'); - const TwoNgMod = checker.getPotentialTemplateDirectives(OneCmpClass) - .filter(d => d.selector === 'two-cmp')[0]; - const imports = - checker.getPotentialImportsFor(TwoNgMod.ref, OneCmpClass, PotentialImportMode.Normal); + const TwoNgMod = checker + .getPotentialTemplateDirectives(OneCmpClass) + .filter((d) => d.selector === 'two-cmp')[0]; + const imports = checker.getPotentialImportsFor( + TwoNgMod.ref, + OneCmpClass, + PotentialImportMode.Normal, + ); expect(imports.length).toBe(1); expect(imports[0].moduleSpecifier).toBe('./twomod'); @@ -331,7 +386,9 @@ runInEachFileSystem(() => { }); it('for forward references in the same file', () => { - env.write('decls.ts', ` + env.write( + 'decls.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -347,16 +404,21 @@ runInEachFileSystem(() => { template: '
', }) export class TwoCmp {} - `); + `, + ); const {program, checker} = env.driveTemplateTypeChecker(); const sfOne = program.getSourceFile(_('/decls.ts')); expect(sfOne).not.toBeNull(); const OneCmpClass = getClass(sfOne!, 'OneCmp'); - const TwoCmpDir = checker.getPotentialTemplateDirectives(OneCmpClass) - .filter(d => d.selector === 'two-cmp')[0]; - const imports = - checker.getPotentialImportsFor(TwoCmpDir.ref, OneCmpClass, PotentialImportMode.Normal); + const TwoCmpDir = checker + .getPotentialTemplateDirectives(OneCmpClass) + .filter((d) => d.selector === 'two-cmp')[0]; + const imports = checker.getPotentialImportsFor( + TwoCmpDir.ref, + OneCmpClass, + PotentialImportMode.Normal, + ); expect(imports.length).toBe(1); expect(imports[0].moduleSpecifier).toBeUndefined(); diff --git a/packages/compiler-cli/test/ngtsc/monorepo_spec.ts b/packages/compiler-cli/test/ngtsc/monorepo_spec.ts index 34bfa892d98b6..9fc72e24ef2dc 100644 --- a/packages/compiler-cli/test/ngtsc/monorepo_spec.ts +++ b/packages/compiler-cli/test/ngtsc/monorepo_spec.ts @@ -28,7 +28,9 @@ runInEachFileSystem(() => { }); it('should compile a project with a reference above the current dir', () => { - env.write('/app/index.ts', ` + env.write( + '/app/index.ts', + ` import {Component, NgModule} from '@angular/core'; import {LibModule} from '../lib/module'; @@ -43,8 +45,11 @@ runInEachFileSystem(() => { imports: [LibModule], }) export class AppModule {} - `); - env.write('/lib/module.ts', ` + `, + ); + env.write( + '/lib/module.ts', + ` import {NgModule} from '@angular/core'; import {LibCmp} from './cmp'; @@ -53,8 +58,11 @@ runInEachFileSystem(() => { exports: [LibCmp], }) export class LibModule {} - `); - env.write('/lib/cmp.ts', ` + `, + ); + env.write( + '/lib/cmp.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -62,7 +70,8 @@ runInEachFileSystem(() => { template: '...', }) export class LibCmp {} - `); + `, + ); env.driveMain(); @@ -71,7 +80,9 @@ runInEachFileSystem(() => { }); it('should compile a project with a reference into the same dir', () => { - env.write('/app/index.ts', ` + env.write( + '/app/index.ts', + ` import {Component, NgModule} from '@angular/core'; import {TargetModule} from './target'; @@ -86,9 +97,12 @@ runInEachFileSystem(() => { imports: [TargetModule], }) export class AppModule {} - `); + `, + ); - env.write('/app/target.ts', ` + env.write( + '/app/target.ts', + ` import {Component, NgModule} from '@angular/core'; @Component({ @@ -102,7 +116,8 @@ runInEachFileSystem(() => { exports: [TargetCmp], }) export class TargetModule {} - `); + `, + ); env.driveMain(); diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 9060f04c93345..4f2c69358c0a3 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -16,7 +16,10 @@ import {ErrorCode, ngErrorCode} from '../../src/ngtsc/diagnostics'; import {absoluteFrom, NgtscCompilerHost} from '../../src/ngtsc/file_system'; import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing'; import {loadStandardTestFiles} from '../../src/ngtsc/testing'; -import {restoreTypeScriptVersionForTesting, setTypeScriptVersionForTesting} from '../../src/typescript_support'; +import { + restoreTypeScriptVersionForTesting, + setTypeScriptVersionForTesting, +} from '../../src/typescript_support'; import {NgtscTestEnvironment} from './env'; @@ -35,7 +38,7 @@ const contentQueryRegExp = (predicate: string, flags: number, ref?: string): Reg }; const setClassMetadataRegExp = (expectedType: string): RegExp => - new RegExp(`setClassMetadata(.*?${expectedType}.*?)`); + new RegExp(`setClassMetadata(.*?${expectedType}.*?)`); const testFiles = loadStandardTestFiles(); @@ -59,7 +62,9 @@ function allTests(os: string) { it('should accept relative file paths as command line argument', () => { env.addCommandLineArgs('--rootDir', './rootDir'); env.write('rootDir/test.html', '

Hello World

'); - env.write('rootDir/test.ts', ` + env.write( + 'rootDir/test.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -67,7 +72,8 @@ function allTests(os: string) { templateUrl: 'test.html', }) export class TestCmp {} - `); + `, + ); env.driveMain(); @@ -76,7 +82,9 @@ function allTests(os: string) { }); it('should compile Injectables without errors', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Injectable} from '@angular/core'; @Injectable() @@ -86,10 +94,10 @@ function allTests(os: string) { export class Service { constructor(dep: Dep) {} } - `); + `, + ); env.driveMain(); - const jsContents = env.getContents('test.js'); expect(jsContents).toContain('Dep.ɵprov ='); expect(jsContents).toContain('Service.ɵprov ='); @@ -102,16 +110,18 @@ function allTests(os: string) { }); it('should compile Injectables with a generic service', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Injectable} from '@angular/core'; @Injectable() export class Store {} - `); + `, + ); env.driveMain(); - const jsContents = env.getContents('test.js'); expect(jsContents).toContain('Store.ɵprov ='); const dtsContents = env.getContents('test.d.ts'); @@ -120,7 +130,9 @@ function allTests(os: string) { }); it('should compile Injectables with providedIn without errors', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Injectable} from '@angular/core'; @Injectable() @@ -130,18 +142,18 @@ function allTests(os: string) { export class Service { constructor(dep: Dep) {} } - `); + `, + ); env.driveMain(); - const jsContents = env.getContents('test.js'); expect(jsContents).toContain('Dep.ɵprov ='); expect(jsContents).toContain('Service.ɵprov ='); - expect(jsContents) - .toContain( - 'Service.ɵfac = function Service_Factory(t) { return new (t || Service)(i0.ɵɵinject(Dep)); };'); - expect(jsContents).toContain('providedIn: \'root\' })'); + expect(jsContents).toContain( + 'Service.ɵfac = function Service_Factory(t) { return new (t || Service)(i0.ɵɵinject(Dep)); };', + ); + expect(jsContents).toContain("providedIn: 'root' })"); expect(jsContents).not.toContain('__decorate'); const dtsContents = env.getContents('test.d.ts'); expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDeclaration;'); @@ -151,23 +163,25 @@ function allTests(os: string) { }); it('should compile Injectables with providedIn and factory without errors', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Injectable} from '@angular/core'; @Injectable({ providedIn: 'root', useFactory: () => new Service() }) export class Service { constructor() {} } - `); + `, + ); env.driveMain(); - const jsContents = env.getContents('test.js'); expect(jsContents).toContain('Service.ɵprov ='); expect(jsContents).toContain('factory: () => (() => new Service())()'); expect(jsContents).toContain('Service_Factory(t) { return new (t || Service)(); }'); - expect(jsContents).toContain(', providedIn: \'root\' });'); + expect(jsContents).toContain(", providedIn: 'root' });"); expect(jsContents).not.toContain('__decorate'); const dtsContents = env.getContents('test.d.ts'); expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDeclaration;'); @@ -175,7 +189,9 @@ function allTests(os: string) { }); it('should compile Injectables with providedIn and factory with deps without errors', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Injectable} from '@angular/core'; @Injectable() @@ -185,26 +201,27 @@ function allTests(os: string) { export class Service { constructor(dep: Dep) {} } - `); + `, + ); env.driveMain(); - const jsContents = env.getContents('test.js'); expect(jsContents).toContain('Service.ɵprov ='); expect(jsContents).toContain('factory: function Service_Factory(t) { let r = null; if (t) {'); expect(jsContents).toContain('return new (t || Service)(i0.ɵɵinject(Dep));'); expect(jsContents).toContain('r = ((dep) => new Service(dep))(i0.ɵɵinject(Dep));'); - expect(jsContents).toContain('return r; }, providedIn: \'root\' });'); + expect(jsContents).toContain("return r; }, providedIn: 'root' });"); expect(jsContents).not.toContain('__decorate'); const dtsContents = env.getContents('test.d.ts'); expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDeclaration;'); expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDeclaration;'); }); - it('should compile Injectables with providedIn and factory with deps with array literal tokens', - () => { - env.write('test.ts', ` + it('should compile Injectables with providedIn and factory with deps with array literal tokens', () => { + env.write( + 'test.ts', + ` import {Injectable, Optional, Self} from '@angular/core'; @Injectable() @@ -218,25 +235,27 @@ function allTests(os: string) { export class Service { constructor(dep: Dep) {} } - `); - - env.driveMain(); - - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('Service.ɵprov ='); - expect(jsContents) - .toContain('factory: function Service_Factory(t) { let r = null; if (t) {'); - expect(jsContents).toContain('return new (t || Service)(i0.ɵɵinject(Dep));'); - expect(jsContents).toContain('r = ((dep) => new Service(dep))(i0.ɵɵinject(Dep, 10));'); - expect(jsContents).toContain(`return r; }, providedIn: 'root' });`); - expect(jsContents).not.toContain('__decorate'); - const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDeclaration;'); - expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDeclaration;'); - }); + `, + ); + + env.driveMain(); + + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('Service.ɵprov ='); + expect(jsContents).toContain('factory: function Service_Factory(t) { let r = null; if (t) {'); + expect(jsContents).toContain('return new (t || Service)(i0.ɵɵinject(Dep));'); + expect(jsContents).toContain('r = ((dep) => new Service(dep))(i0.ɵɵinject(Dep, 10));'); + expect(jsContents).toContain(`return r; }, providedIn: 'root' });`); + expect(jsContents).not.toContain('__decorate'); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents).toContain('static ɵprov: i0.ɵɵInjectableDeclaration;'); + expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDeclaration;'); + }); it('should compile Injectables with providedIn using forwardRef without errors', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Injectable, NgModule, forwardRef} from '@angular/core'; @Injectable() @@ -249,7 +268,8 @@ function allTests(os: string) { @NgModule() export class Mod {} - `); + `, + ); env.driveMain(); @@ -257,9 +277,9 @@ function allTests(os: string) { expect(jsContents).toContain('Dep.ɵprov ='); expect(jsContents).toContain('Service.ɵprov ='); expect(jsContents).toContain('Mod.ɵmod ='); - expect(jsContents) - .toContain( - 'Service.ɵfac = function Service_Factory(t) { return new (t || Service)(i0.ɵɵinject(Dep)); };'); + expect(jsContents).toContain( + 'Service.ɵfac = function Service_Factory(t) { return new (t || Service)(i0.ɵɵinject(Dep)); };', + ); expect(jsContents).toContain('providedIn: i0.forwardRef(() => Mod) })'); expect(jsContents).not.toContain('__decorate'); const dtsContents = env.getContents('test.d.ts'); @@ -271,7 +291,9 @@ function allTests(os: string) { }); it('should compile @Injectable with an @Optional dependency', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Injectable, Optional as Opt} from '@angular/core'; @Injectable() @@ -281,14 +303,17 @@ function allTests(os: string) { class Service { constructor(@Opt() dep: Dep) {} } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); expect(jsContents).toContain('inject(Dep, 8)'); }); it('should compile @Injectable with constructor overloads', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Injectable, Optional} from '@angular/core'; @Injectable() @@ -303,23 +328,27 @@ function allTests(os: string) { constructor(dep: Dep, @Optional() optionalDep?: OptionalDep) {} } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - `Service.ɵfac = function Service_Factory(t) { ` + - `return new (t || Service)(i0.ɵɵinject(Dep), i0.ɵɵinject(OptionalDep, 8)); };`); + expect(jsContents).toContain( + `Service.ɵfac = function Service_Factory(t) { ` + + `return new (t || Service)(i0.ɵɵinject(Dep), i0.ɵɵinject(OptionalDep, 8)); };`, + ); }); it('should compile Directives without errors', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive} from '@angular/core'; @Directive({selector: '[dir]'}) export class TestDir {} - `); + `, + ); env.driveMain(); @@ -330,18 +359,21 @@ function allTests(os: string) { const dtsContents = env.getContents('test.d.ts'); const expectedDirectiveDeclaration = - 'static ɵdir: i0.ɵɵDirectiveDeclaration'; + 'static ɵdir: i0.ɵɵDirectiveDeclaration'; expect(dtsContents).toContain(expectedDirectiveDeclaration); expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDeclaration'); }); it('should compile abstract Directives without errors', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive} from '@angular/core'; @Directive() export class TestDir {} - `); + `, + ); env.driveMain(); @@ -351,14 +383,16 @@ function allTests(os: string) { expect(jsContents).not.toContain('__decorate'); const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents) - .toContain( - 'static ɵdir: i0.ɵɵDirectiveDeclaration'); + expect(dtsContents).toContain( + 'static ɵdir: i0.ɵɵDirectiveDeclaration', + ); expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDeclaration'); }); it('should compile Components (inline template) without errors', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -366,7 +400,8 @@ function allTests(os: string) { template: 'this is a test', }) export class TestCmp {} - `); + `, + ); env.driveMain(); @@ -377,13 +412,15 @@ function allTests(os: string) { const dtsContents = env.getContents('test.d.ts'); const expectedComponentDeclaration = - 'static ɵcmp: i0.ɵɵComponentDeclaration'; + 'static ɵcmp: i0.ɵɵComponentDeclaration'; expect(dtsContents).toContain(expectedComponentDeclaration); expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDeclaration'); }); it('should compile Components (dynamic inline template) without errors', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -391,7 +428,8 @@ function allTests(os: string) { template: 'this is ' + 'a test', }) export class TestCmp {} - `); + `, + ); env.driveMain(); @@ -402,15 +440,17 @@ function allTests(os: string) { const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents) - .toContain( - 'static ɵcmp: i0.ɵɵComponentDeclaration' + - ''); + expect(dtsContents).toContain( + 'static ɵcmp: i0.ɵɵComponentDeclaration' + + '', + ); expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDeclaration'); }); it('should compile Components (function call inline template) without errors', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; function getTemplate() { @@ -421,7 +461,8 @@ function allTests(os: string) { template: getTemplate(), }) export class TestCmp {} - `); + `, + ); env.driveMain(); @@ -432,13 +473,15 @@ function allTests(os: string) { const dtsContents = env.getContents('test.d.ts'); const expectedComponentDeclaration = - 'static ɵcmp: i0.ɵɵComponentDeclaration'; + 'static ɵcmp: i0.ɵɵComponentDeclaration'; expect(dtsContents).toContain(expectedComponentDeclaration); expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDeclaration'); }); it('should compile Components (external template) without errors', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -446,7 +489,8 @@ function allTests(os: string) { templateUrl: './dir/test.html', }) export class TestCmp {} - `); + `, + ); env.write('dir/test.html', '

Hello World

'); env.driveMain(); @@ -456,7 +500,9 @@ function allTests(os: string) { }); it('should not report that broken components in modules are not components', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, NgModule} from '@angular/core'; @Component({ @@ -471,10 +517,11 @@ function allTests(os: string) { export class Module { broken = "false"; } - `); + `, + ); const diags = env.driveDiagnostics(); - if (diags.some(diag => diag.code === ngErrorCode(ErrorCode.NGMODULE_INVALID_DECLARATION))) { + if (diags.some((diag) => diag.code === ngErrorCode(ErrorCode.NGMODULE_INVALID_DECLARATION))) { fail('Should not produce a diagnostic that BrokenCmp is not a component'); } }); @@ -483,7 +530,9 @@ function allTests(os: string) { env.tsconfig({ onlyPublishPublicTypingsForNgModules: true, }); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, NgModule} from '@angular/core'; @Directive({selector: 'internal'}) @@ -497,21 +546,24 @@ function allTests(os: string) { exports: [ExternalDir], }) export class Module {} - `); + `, + ); env.driveMain(); const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents) - .toContain( - 'static ɵmod: i0.ɵɵNgModuleDeclaration'); + expect(dtsContents).toContain( + 'static ɵmod: i0.ɵɵNgModuleDeclaration', + ); }); it('should not list imports in NgModule .d.ts when requested', () => { env.tsconfig({ onlyPublishPublicTypingsForNgModules: true, }); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, NgModule} from '@angular/core'; @Directive({selector: 'internal'}) @@ -530,14 +582,14 @@ function allTests(os: string) { imports: [DepModule], }) export class Module {} - `); + `, + ); env.driveMain(); const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents) - .toContain( - 'static ɵmod: i0.ɵɵNgModuleDeclaration'); + expect(dtsContents).toContain( + 'static ɵmod: i0.ɵɵNgModuleDeclaration', + ); }); it('should error when supportJitMode is false and forbidOrphanComponents is true', () => { @@ -549,10 +601,13 @@ function allTests(os: string) { const diagnostics = env.driveDiagnostics(); - expect(diagnostics).toEqual([jasmine.objectContaining({ - messageText: jasmine.stringMatching( - /JIT mode support \("supportJitMode" option\) cannot be disabled when forbidOrphanComponents is set to true/), - })]); + expect(diagnostics).toEqual([ + jasmine.objectContaining({ + messageText: jasmine.stringMatching( + /JIT mode support \("supportJitMode" option\) cannot be disabled when forbidOrphanComponents is set to true/, + ), + }), + ]); }); // This test triggers the Tsickle compiler which asserts that the file-paths @@ -564,7 +619,9 @@ function allTests(os: string) { env.tsconfig({ 'annotateForClosureCompiler': true, }); - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Directive} from '@angular/core'; @Directive({ @@ -577,22 +634,25 @@ function allTests(os: string) { }) class Dir extends Base { } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); expect(jsContents).toContain('Dir.ɵfac = /** @pureOrBreakMyCode */ (() => {'); - expect(jsContents) - .toContain( - '(ɵDir_BaseFactory || (ɵDir_BaseFactory = i0.ɵɵgetInheritedFactory(Dir)))(t || Dir);'); + expect(jsContents).toContain( + '(ɵDir_BaseFactory || (ɵDir_BaseFactory = i0.ɵɵgetInheritedFactory(Dir)))(t || Dir);', + ); }); it('should add @nocollapse to static fields', () => { env.tsconfig({ 'annotateForClosureCompiler': true, }); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -600,7 +660,8 @@ function allTests(os: string) { templateUrl: './dir/test.html', }) export class TestCmp {} - `); + `, + ); env.write('dir/test.html', '

Hello World

'); env.driveMain(); @@ -614,7 +675,9 @@ function allTests(os: string) { 'fullTemplateTypeCheck': false, 'annotateForClosureCompiler': true, }); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, NgModule} from '@angular/core'; @Component({ @@ -637,13 +700,15 @@ function allTests(os: string) { declarations: [TestCmp, TestDir], }) export class TestModule {} - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); expect(diags[0].code).toBe(ngErrorCode(ErrorCode.SCHEMA_INVALID_ELEMENT)); - expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')) - .toContain('not-a-cmp'); + expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')).toContain( + 'not-a-cmp', + ); }); /** * The following set of tests verify that after Tsickle run we do not have cases @@ -681,7 +746,8 @@ function allTests(os: string) { // insertion by checking that there are no function return statements // not wrapped in parentheses expect(trim(jsContents)).not.toMatch(/return\s+function/); - expect(trim(jsContents)).toContain(trim(` + expect(trim(jsContents)).toContain( + trim(` [{ provide: 'token-a', useFactory: ((service) => { @@ -693,7 +759,8 @@ function allTests(os: string) { return (function () { return service.id; }); }) }] - `)); + `), + ); }; it('should wrap functions in "providers" list in NgModule', () => { @@ -701,7 +768,9 @@ function allTests(os: string) { 'annotateForClosureCompiler': true, }); env.write('service.ts', service); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {NgModule} from '@angular/core'; import {Service} from './service'; @@ -709,7 +778,8 @@ function allTests(os: string) { providers: ${providers} }) export class SomeModule {} - `); + `, + ); env.driveMain(); verifyOutput(env.getContents('test.js')); @@ -720,7 +790,9 @@ function allTests(os: string) { 'annotateForClosureCompiler': true, }); env.write('service.ts', service); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; import {Service} from './service'; @@ -729,7 +801,8 @@ function allTests(os: string) { providers: ${providers} }) export class SomeComponent {} - `); + `, + ); env.driveMain(); verifyOutput(env.getContents('test.js')); @@ -740,7 +813,9 @@ function allTests(os: string) { 'annotateForClosureCompiler': true, }); env.write('service.ts', service); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; import {Service} from './service'; @@ -749,7 +824,8 @@ function allTests(os: string) { viewProviders: ${providers} }) export class SomeComponent {} - `); + `, + ); env.driveMain(); verifyOutput(env.getContents('test.js')); @@ -760,7 +836,9 @@ function allTests(os: string) { 'annotateForClosureCompiler': true, }); env.write('service.ts', service); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive} from '@angular/core'; import {Service} from './service'; @@ -768,7 +846,8 @@ function allTests(os: string) { providers: ${providers} }) export class SomeDirective {} - `); + `, + ); env.driveMain(); verifyOutput(env.getContents('test.js')); @@ -778,7 +857,9 @@ function allTests(os: string) { } it('should recognize aliased decorators', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import { Component as AngularComponent, Directive as AngularDirective, @@ -830,7 +911,8 @@ function allTests(os: string) { ] }) class MyModule {} - `); + `, + ); env.driveMain(); @@ -848,7 +930,9 @@ function allTests(os: string) { it('should pick a Pipe defined in `declarations` over imported Pipes', () => { env.tsconfig({}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Pipe, NgModule} from '@angular/core'; // ModuleA classes @@ -884,7 +968,8 @@ function allTests(os: string) { declarations: [PipeB, App], }) class ModuleB {} - `); + `, + ); env.driveMain(); @@ -892,10 +977,11 @@ function allTests(os: string) { expect(jsContents).toContain('dependencies: [PipeB]'); }); - it('should respect imported module order when selecting Pipe (last imported Pipe is used)', - () => { - env.tsconfig({}); - env.write('test.ts', ` + it('should respect imported module order when selecting Pipe (last imported Pipe is used)', () => { + env.tsconfig({}); + env.write( + 'test.ts', + ` import {Component, Pipe, NgModule} from '@angular/core'; // ModuleA classes @@ -939,17 +1025,20 @@ function allTests(os: string) { declarations: [App], }) class ModuleC {} - `); + `, + ); - env.driveMain(); + env.driveMain(); - const jsContents = trim(env.getContents('test.js')); - expect(jsContents).toContain('dependencies: [PipeB]'); - }); + const jsContents = trim(env.getContents('test.js')); + expect(jsContents).toContain('dependencies: [PipeB]'); + }); it('should add Directives and Components from `declarations` at the end of the list', () => { env.tsconfig({}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, NgModule} from '@angular/core'; // ModuleA classes @@ -994,18 +1083,22 @@ function allTests(os: string) { declarations: [DirectiveB, ComponentB, App], }) class ModuleB {} - `); + `, + ); env.driveMain(); const jsContents = trim(env.getContents('test.js')); - expect(jsContents) - .toContain('dependencies: [DirectiveA, ComponentA, DirectiveB, ComponentB]'); + expect(jsContents).toContain( + 'dependencies: [DirectiveA, ComponentA, DirectiveB, ComponentB]', + ); }); it('should respect imported module order while processing Directives and Components', () => { env.tsconfig({}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, NgModule} from '@angular/core'; // ModuleA classes @@ -1058,19 +1151,23 @@ function allTests(os: string) { declarations: [App], }) class ModuleC {} - `); + `, + ); env.driveMain(); const jsContents = trim(env.getContents('test.js')); - expect(jsContents) - .toContain('dependencies: [DirectiveA, ComponentA, DirectiveB, ComponentB]'); + expect(jsContents).toContain( + 'dependencies: [DirectiveA, ComponentA, DirectiveB, ComponentB]', + ); }); it('should compile Components with a templateUrl in a different rootDir', () => { env.tsconfig({}, ['./extraRootDir']); env.write('extraRootDir/test.html', '

Hello World

'); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1078,7 +1175,8 @@ function allTests(os: string) { templateUrl: 'test.html', }) export class TestCmp {} - `); + `, + ); env.driveMain(); @@ -1089,7 +1187,9 @@ function allTests(os: string) { it('should compile Components with an absolute templateUrl in a different rootDir', () => { env.tsconfig({}, ['./extraRootDir']); env.write('extraRootDir/test.html', '

Hello World

'); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1097,7 +1197,8 @@ function allTests(os: string) { templateUrl: '/test.html', }) export class TestCmp {} - `); + `, + ); env.driveMain(); @@ -1106,7 +1207,9 @@ function allTests(os: string) { }); it('should compile components with styleUrls', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1115,7 +1218,8 @@ function allTests(os: string) { template: '', }) export class TestCmp {} - `); + `, + ); env.write('dir/style.css', ':host { background-color: blue; }'); env.driveMain(); @@ -1125,7 +1229,9 @@ function allTests(os: string) { }); it('should compile components with styleUrls with fallback to .css extension', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1134,7 +1240,8 @@ function allTests(os: string) { template: '', }) export class TestCmp {} - `); + `, + ); env.write('dir/style.css', ':host { background-color: blue; }'); env.driveMain(); @@ -1144,80 +1251,88 @@ function allTests(os: string) { }); it('should include generic type in directive definition', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Input, NgModule} from '@angular/core'; @Directive() export class TestBase { @Input() input: any; } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain('i0.ɵɵdefineDirective({ type: TestBase, inputs: { input: "input" } });'); + expect(jsContents).toContain( + 'i0.ɵɵdefineDirective({ type: TestBase, inputs: { input: "input" } });', + ); const dtsContents = env.getContents('test.d.ts'); - const expectedDirectiveDeclaration = - `static ɵdir: i0.ɵɵDirectiveDeclaration;`; + const expectedDirectiveDeclaration = `static ɵdir: i0.ɵɵDirectiveDeclaration;`; expect(dtsContents).toContain(expectedDirectiveDeclaration); }); describe('undecorated classes using Angular features', () => { - it('should error if @Input has been discovered', - () => assertErrorUndecoratedClassWithField('Input')); - it('should error if @Output has been discovered', - () => assertErrorUndecoratedClassWithField('Output')); - it('should error if @ViewChild has been discovered', - () => assertErrorUndecoratedClassWithField('ViewChild')); - it('should error if @ViewChildren has been discovered', - () => assertErrorUndecoratedClassWithField('ViewChildren')); - it('should error if @ContentChild has been discovered', - () => assertErrorUndecoratedClassWithField('ContentChildren')); - it('should error if @HostBinding has been discovered', - () => assertErrorUndecoratedClassWithField('HostBinding')); - it('should error if @HostListener has been discovered', - () => assertErrorUndecoratedClassWithField('HostListener')); - - it(`should error if ngOnChanges lifecycle hook has been discovered`, - () => assertErrorUndecoratedClassWithLifecycleHook('ngOnChanges')); - it(`should error if ngOnInit lifecycle hook has been discovered`, - () => assertErrorUndecoratedClassWithLifecycleHook('ngOnInit')); - it(`should error if ngOnDestroy lifecycle hook has been discovered`, - () => assertErrorUndecoratedClassWithLifecycleHook('ngOnDestroy')); - it(`should error if ngDoCheck lifecycle hook has been discovered`, - () => assertErrorUndecoratedClassWithLifecycleHook('ngDoCheck')); - it(`should error if ngAfterViewInit lifecycle hook has been discovered`, - () => assertErrorUndecoratedClassWithLifecycleHook('ngAfterViewInit')); - it(`should error if ngAfterViewChecked lifecycle hook has been discovered`, - () => assertErrorUndecoratedClassWithLifecycleHook('ngAfterViewChecked')); - it(`should error if ngAfterContentInit lifecycle hook has been discovered`, - () => assertErrorUndecoratedClassWithLifecycleHook('ngAfterContentInit')); - it(`should error if ngAfterContentChecked lifecycle hook has been discovered`, - () => assertErrorUndecoratedClassWithLifecycleHook('ngAfterContentChecked')); + it('should error if @Input has been discovered', () => + assertErrorUndecoratedClassWithField('Input')); + it('should error if @Output has been discovered', () => + assertErrorUndecoratedClassWithField('Output')); + it('should error if @ViewChild has been discovered', () => + assertErrorUndecoratedClassWithField('ViewChild')); + it('should error if @ViewChildren has been discovered', () => + assertErrorUndecoratedClassWithField('ViewChildren')); + it('should error if @ContentChild has been discovered', () => + assertErrorUndecoratedClassWithField('ContentChildren')); + it('should error if @HostBinding has been discovered', () => + assertErrorUndecoratedClassWithField('HostBinding')); + it('should error if @HostListener has been discovered', () => + assertErrorUndecoratedClassWithField('HostListener')); + + it(`should error if ngOnChanges lifecycle hook has been discovered`, () => + assertErrorUndecoratedClassWithLifecycleHook('ngOnChanges')); + it(`should error if ngOnInit lifecycle hook has been discovered`, () => + assertErrorUndecoratedClassWithLifecycleHook('ngOnInit')); + it(`should error if ngOnDestroy lifecycle hook has been discovered`, () => + assertErrorUndecoratedClassWithLifecycleHook('ngOnDestroy')); + it(`should error if ngDoCheck lifecycle hook has been discovered`, () => + assertErrorUndecoratedClassWithLifecycleHook('ngDoCheck')); + it(`should error if ngAfterViewInit lifecycle hook has been discovered`, () => + assertErrorUndecoratedClassWithLifecycleHook('ngAfterViewInit')); + it(`should error if ngAfterViewChecked lifecycle hook has been discovered`, () => + assertErrorUndecoratedClassWithLifecycleHook('ngAfterViewChecked')); + it(`should error if ngAfterContentInit lifecycle hook has been discovered`, () => + assertErrorUndecoratedClassWithLifecycleHook('ngAfterContentInit')); + it(`should error if ngAfterContentChecked lifecycle hook has been discovered`, () => + assertErrorUndecoratedClassWithLifecycleHook('ngAfterContentChecked')); function assertErrorUndecoratedClassWithField(fieldDecoratorName: string) { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, ${fieldDecoratorName}, NgModule} from '@angular/core'; export class SomeBaseClass { // @ts-ignore — Because no arguments are specified. @${fieldDecoratorName}() someMember: any; } - `); + `, + ); const errors = env.driveDiagnostics(); expect(errors.length).toBe(1); - expect(trim(errors[0].messageText as string)) - .toContain( - 'Class is using Angular features but is not decorated. Please add an explicit ' + - 'Angular decorator.'); + expect(trim(errors[0].messageText as string)).toContain( + 'Class is using Angular features but is not decorated. Please add an explicit ' + + 'Angular decorator.', + ); } function assertErrorUndecoratedClassWithLifecycleHook(lifecycleName: string) { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, NgModule} from '@angular/core'; export class SomeBaseClass { @@ -1225,19 +1340,22 @@ function allTests(os: string) { // empty } } - `); + `, + ); const errors = env.driveDiagnostics(); expect(errors.length).toBe(1); - expect(trim(errors[0].messageText as string)) - .toContain( - 'Class is using Angular features but is not decorated. Please add an explicit ' + - 'Angular decorator.'); + expect(trim(errors[0].messageText as string)).toContain( + 'Class is using Angular features but is not decorated. Please add an explicit ' + + 'Angular decorator.', + ); } }); it('should compile NgModules without errors', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, NgModule} from '@angular/core'; @Component({ @@ -1251,38 +1369,43 @@ function allTests(os: string) { bootstrap: [TestCmp], }) export class TestModule {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain('i0.ɵɵdefineNgModule({ type: TestModule, bootstrap: [TestCmp] });'); - expect(jsContents) - .toContain( - 'function () { (typeof ngJitMode === "undefined" || ngJitMode) && i0.ɵɵsetNgModuleScope(TestModule, { declarations: [TestCmp] }); })();'); - expect(jsContents) - .toContain( - 'TestModule.ɵfac = function TestModule_Factory(t) { return new (t || TestModule)(); }'); + expect(jsContents).toContain( + 'i0.ɵɵdefineNgModule({ type: TestModule, bootstrap: [TestCmp] });', + ); + expect(jsContents).toContain( + 'function () { (typeof ngJitMode === "undefined" || ngJitMode) && i0.ɵɵsetNgModuleScope(TestModule, { declarations: [TestCmp] }); })();', + ); + expect(jsContents).toContain( + 'TestModule.ɵfac = function TestModule_Factory(t) { return new (t || TestModule)(); }', + ); expect(jsContents).toContain('i0.ɵɵdefineInjector({});'); const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents) - .toContain( - 'static ɵcmp: i0.ɵɵComponentDeclaration'); - expect(dtsContents) - .toContain( - 'static ɵmod: i0.ɵɵNgModuleDeclaration'); + expect(dtsContents).toContain( + 'static ɵcmp: i0.ɵɵComponentDeclaration', + ); + expect(dtsContents).toContain( + 'static ɵmod: i0.ɵɵNgModuleDeclaration', + ); expect(dtsContents).not.toContain('__decorate'); }); it('should not emit a ɵɵsetNgModuleScope call when no scope metadata is present', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {NgModule} from '@angular/core'; @NgModule({}) export class TestModule {} - `); + `, + ); env.driveMain(); @@ -1291,13 +1414,16 @@ function allTests(os: string) { expect(jsContents).not.toContain('ɵɵsetNgModuleScope(TestModule,'); }); - it('should emit the id when the module\'s id is a string', () => { - env.write('test.ts', ` + it("should emit the id when the module's id is a string", () => { + env.write( + 'test.ts', + ` import {NgModule} from '@angular/core'; @NgModule({id: 'test'}) export class TestModule {} - `); + `, + ); env.driveMain(); @@ -1306,15 +1432,21 @@ function allTests(os: string) { }); it('should warn when an NgModule id is defined as module.id, and not emit it', () => { - env.write('index.d.ts', ` + env.write( + 'index.d.ts', + ` declare const module = {id: string}; - `); - env.write('test.ts', ` + `, + ); + env.write( + 'test.ts', + ` import {NgModule} from '@angular/core'; @NgModule({id: module.id}) export class TestModule {} - `); + `, + ); const diags = env.driveDiagnostics(); @@ -1329,12 +1461,15 @@ function allTests(os: string) { }); it('should emit a side-effectful registration call when an @NgModule has an id', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {NgModule} from '@angular/core'; @NgModule({id: 'test'}) export class TestModule {} - `); + `, + ); env.driveMain(); @@ -1343,7 +1478,9 @@ function allTests(os: string) { }); it('should filter out directives and pipes from module exports in the injector def', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {NgModule} from '@angular/core'; import {RouterComp, RouterModule} from '@angular/router'; import {Dir, OtherDir, MyPipe, Comp} from './decls'; @@ -1362,8 +1499,11 @@ function allTests(os: string) { exports: [EXPORTS], }) export class TestModule {} - `); - env.write(`decls.ts`, ` + `, + ); + env.write( + `decls.ts`, + ` import {Component, Directive, Pipe} from '@angular/core'; @Directive({selector: '[dir]'}) @@ -1377,8 +1517,11 @@ function allTests(os: string) { @Component({selector: 'test', template: ''}) export class Comp {} - `); - env.write('node_modules/@angular/router/index.d.ts', ` + `, + ); + env.write( + 'node_modules/@angular/router/index.d.ts', + ` import {ɵɵComponentDeclaration, ModuleWithProviders, ɵɵNgModuleDeclaration} from '@angular/core'; export declare class RouterComp { @@ -1389,22 +1532,25 @@ function allTests(os: string) { static forRoot(): ModuleWithProviders; static ɵmod: ɵɵNgModuleDeclaration; } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - 'TestModule.ɵfac = function TestModule_Factory(t) { return new (t || TestModule)(); }'); - expect(jsContents) - .toContain( - 'i0.ɵɵdefineInjector({ imports: [OtherModule, RouterModule.forRoot(),' + - ' OtherModule, RouterModule] });'); + expect(jsContents).toContain( + 'TestModule.ɵfac = function TestModule_Factory(t) { return new (t || TestModule)(); }', + ); + expect(jsContents).toContain( + 'i0.ɵɵdefineInjector({ imports: [OtherModule, RouterModule.forRoot(),' + + ' OtherModule, RouterModule] });', + ); }); it('should compile NgModules with services without errors', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, NgModule} from '@angular/core'; export class Token {} @@ -1424,30 +1570,33 @@ function allTests(os: string) { imports: [OtherModule], }) export class TestModule {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - 'TestModule.ɵfac = function TestModule_Factory(t) { return new (t || TestModule)(); }'); + expect(jsContents).toContain( + 'TestModule.ɵfac = function TestModule_Factory(t) { return new (t || TestModule)(); }', + ); expect(jsContents).toContain('i0.ɵɵdefineNgModule({ type: TestModule });'); - expect(jsContents) - .toContain( - `TestModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ ` + - `providers: [{ provide: Token, useValue: 'test' }], ` + - `imports: [OtherModule] });`); + expect(jsContents).toContain( + `TestModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ ` + + `providers: [{ provide: Token, useValue: 'test' }], ` + + `imports: [OtherModule] });`, + ); const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents) - .toContain( - 'static ɵmod: i0.ɵɵNgModuleDeclaration'); + expect(dtsContents).toContain( + 'static ɵmod: i0.ɵɵNgModuleDeclaration', + ); expect(dtsContents).toContain('static ɵinj: i0.ɵɵInjectorDeclaration'); }); it('should compile NgModules with factory providers without errors', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, NgModule} from '@angular/core'; export class Token {} @@ -1467,30 +1616,33 @@ function allTests(os: string) { imports: [OtherModule], }) export class TestModule {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - 'TestModule.ɵfac = function TestModule_Factory(t) { return new (t || TestModule)(); }'); + expect(jsContents).toContain( + 'TestModule.ɵfac = function TestModule_Factory(t) { return new (t || TestModule)(); }', + ); expect(jsContents).toContain('i0.ɵɵdefineNgModule({ type: TestModule });'); - expect(jsContents) - .toContain( - `TestModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ ` + - `providers: [{ provide: Token, useFactory: () => new Token() }], ` + - `imports: [OtherModule] });`); + expect(jsContents).toContain( + `TestModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ ` + + `providers: [{ provide: Token, useFactory: () => new Token() }], ` + + `imports: [OtherModule] });`, + ); const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents) - .toContain( - 'static ɵmod: i0.ɵɵNgModuleDeclaration'); + expect(dtsContents).toContain( + 'static ɵmod: i0.ɵɵNgModuleDeclaration', + ); expect(dtsContents).toContain('static ɵinj: i0.ɵɵInjectorDeclaration'); }); it('should compile NgModules with factory providers and deps without errors', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, NgModule} from '@angular/core'; export class Dep {} @@ -1514,30 +1666,33 @@ function allTests(os: string) { imports: [OtherModule], }) export class TestModule {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - 'TestModule.ɵfac = function TestModule_Factory(t) { return new (t || TestModule)(); }'); + expect(jsContents).toContain( + 'TestModule.ɵfac = function TestModule_Factory(t) { return new (t || TestModule)(); }', + ); expect(jsContents).toContain('i0.ɵɵdefineNgModule({ type: TestModule });'); - expect(jsContents) - .toContain( - `TestModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ ` + - `providers: [{ provide: Token, useFactory: (dep) => new Token(dep), deps: [Dep] }], ` + - `imports: [OtherModule] });`); + expect(jsContents).toContain( + `TestModule.ɵinj = /*@__PURE__*/ i0.ɵɵdefineInjector({ ` + + `providers: [{ provide: Token, useFactory: (dep) => new Token(dep), deps: [Dep] }], ` + + `imports: [OtherModule] });`, + ); const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents) - .toContain( - 'static ɵmod: i0.ɵɵNgModuleDeclaration'); + expect(dtsContents).toContain( + 'static ɵmod: i0.ɵɵNgModuleDeclaration', + ); expect(dtsContents).toContain('static ɵinj: i0.ɵɵInjectorDeclaration'); }); it('should compile NgModules with references to local components', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {NgModule} from '@angular/core'; import {Foo} from './foo'; @@ -1545,34 +1700,43 @@ function allTests(os: string) { declarations: [Foo], }) export class FooModule {} - `); - env.write('foo.ts', ` + `, + ); + env.write( + 'foo.ts', + ` import {Component} from '@angular/core'; @Component({selector: 'foo', template: ''}) export class Foo {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); const dtsContents = env.getContents('test.d.ts'); - expect(jsContents).toContain('import { Foo } from \'./foo\';'); + expect(jsContents).toContain("import { Foo } from './foo';"); expect(jsContents).not.toMatch(/as i[0-9] from ".\/foo"/); expect(dtsContents).toContain('as i1 from "./foo";'); }); it('should compile NgModules with references to absolute components', () => { - env.write('tsconfig.json', JSON.stringify({ - extends: './tsconfig-base.json', - compilerOptions: { - baseUrl: '.', - paths: { - '*': ['*', 'shared/*'], + env.write( + 'tsconfig.json', + JSON.stringify({ + extends: './tsconfig-base.json', + compilerOptions: { + baseUrl: '.', + paths: { + '*': ['*', 'shared/*'], + }, }, - }, - })); - env.write('test.ts', ` + }), + ); + env.write( + 'test.ts', + ` import {NgModule} from '@angular/core'; import {Foo} from 'foo'; @@ -1580,8 +1744,11 @@ function allTests(os: string) { declarations: [Foo], }) export class FooModule {} - `); - env.write('shared/foo/index.ts', ` + `, + ); + env.write( + 'shared/foo/index.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1590,20 +1757,23 @@ function allTests(os: string) { }) export class Foo { } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); const dtsContents = env.getContents('test.d.ts'); - expect(jsContents).toContain('import { Foo } from \'foo\';'); + expect(jsContents).toContain("import { Foo } from 'foo';"); expect(jsContents).not.toMatch(/as i[0-9] from "foo"/); expect(dtsContents).toContain('as i1 from "foo";'); }); it('should compile NgModules with references to forward declared bootstrap components', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, forwardRef, NgModule} from '@angular/core'; @NgModule({ @@ -1613,7 +1783,8 @@ function allTests(os: string) { @Component({selector: 'foo', template: 'foo'}) export class Foo {} - `); + `, + ); env.driveMain(); @@ -1622,7 +1793,9 @@ function allTests(os: string) { }); it('should compile NgModules with references to forward declared directives', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, forwardRef, NgModule} from '@angular/core'; @NgModule({ @@ -1632,7 +1805,8 @@ function allTests(os: string) { @Directive({selector: 'foo'}) export class Foo {} - `); + `, + ); env.driveMain(); @@ -1641,7 +1815,9 @@ function allTests(os: string) { }); it('should compile NgModules with references to forward declared imports', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {forwardRef, NgModule} from '@angular/core'; @NgModule({ @@ -1651,7 +1827,8 @@ function allTests(os: string) { @NgModule({}) export class BarModule {} - `); + `, + ); env.driveMain(); @@ -1660,7 +1837,9 @@ function allTests(os: string) { }); it('should compile NgModules with references to forward declared exports', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {forwardRef, NgModule} from '@angular/core'; @NgModule({ @@ -1670,7 +1849,8 @@ function allTests(os: string) { @NgModule({}) export class BarModule {} - `); + `, + ); env.driveMain(); @@ -1678,17 +1858,21 @@ function allTests(os: string) { expect(jsContents).toContain('exports: () => [BarModule]'); }); - it('should use relative import for forward references that were resolved from a relative file', - () => { - env.write('dir.ts', ` + it('should use relative import for forward references that were resolved from a relative file', () => { + env.write( + 'dir.ts', + ` import {Directive, forwardRef} from '@angular/core'; export const useFoo = forwardRef(() => Foo); @Directive({selector: 'foo'}) export class Foo {} - `); - env.write('test.ts', ` + `, + ); + env.write( + 'test.ts', + ` import {NgModule} from '@angular/core'; import {useFoo} from './dir'; @@ -1696,26 +1880,31 @@ function allTests(os: string) { declarations: [useFoo], }) export class FooModule {} - `); + `, + ); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('import * as i1 from "./dir";'); - expect(jsContents).toContain('declarations: [i1.Foo]'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('import * as i1 from "./dir";'); + expect(jsContents).toContain('declarations: [i1.Foo]'); + }); - it('should use absolute import for forward references that were resolved from an absolute file', - () => { - env.write('dir.ts', ` + it('should use absolute import for forward references that were resolved from an absolute file', () => { + env.write( + 'dir.ts', + ` import {Directive, forwardRef} from '@angular/core'; export const useFoo = forwardRef(() => Foo); @Directive({selector: 'foo'}) export class Foo {} - `); - env.write('test.ts', ` + `, + ); + env.write( + 'test.ts', + ` import {forwardRef, NgModule} from '@angular/core'; import {useFoo} from 'dir'; @@ -1723,17 +1912,20 @@ function allTests(os: string) { declarations: [useFoo], }) export class FooModule {} - `); + `, + ); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('import * as i1 from "dir";'); - expect(jsContents).toContain('declarations: [i1.Foo]'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('import * as i1 from "dir";'); + expect(jsContents).toContain('declarations: [i1.Foo]'); + }); it('should compile Pipes without errors', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Pipe} from '@angular/core'; @Pipe({ @@ -1741,52 +1933,60 @@ function allTests(os: string) { pure: false, }) export class TestPipe {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); const dtsContents = env.getContents('test.d.ts'); - expect(jsContents) - .toContain( - 'TestPipe.ɵpipe = /*@__PURE__*/ i0.ɵɵdefinePipe({ name: "test-pipe", type: TestPipe, pure: false })'); - expect(jsContents) - .toContain( - 'TestPipe.ɵfac = function TestPipe_Factory(t) { return new (t || TestPipe)(); }'); - expect(dtsContents) - .toContain('static ɵpipe: i0.ɵɵPipeDeclaration;'); + expect(jsContents).toContain( + 'TestPipe.ɵpipe = /*@__PURE__*/ i0.ɵɵdefinePipe({ name: "test-pipe", type: TestPipe, pure: false })', + ); + expect(jsContents).toContain( + 'TestPipe.ɵfac = function TestPipe_Factory(t) { return new (t || TestPipe)(); }', + ); + expect(dtsContents).toContain( + 'static ɵpipe: i0.ɵɵPipeDeclaration;', + ); expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDeclaration;'); }); it('should compile pure Pipes without errors', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Pipe} from '@angular/core'; @Pipe({ name: 'test-pipe', }) export class TestPipe {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); const dtsContents = env.getContents('test.d.ts'); - expect(jsContents) - .toContain( - 'TestPipe.ɵpipe = /*@__PURE__*/ i0.ɵɵdefinePipe({ name: "test-pipe", type: TestPipe, pure: true })'); - expect(jsContents) - .toContain( - 'TestPipe.ɵfac = function TestPipe_Factory(t) { return new (t || TestPipe)(); }'); - expect(dtsContents) - .toContain('static ɵpipe: i0.ɵɵPipeDeclaration;'); + expect(jsContents).toContain( + 'TestPipe.ɵpipe = /*@__PURE__*/ i0.ɵɵdefinePipe({ name: "test-pipe", type: TestPipe, pure: true })', + ); + expect(jsContents).toContain( + 'TestPipe.ɵfac = function TestPipe_Factory(t) { return new (t || TestPipe)(); }', + ); + expect(dtsContents).toContain( + 'static ɵpipe: i0.ɵɵPipeDeclaration;', + ); expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDeclaration;'); }); it('should compile Pipes with dependencies', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Pipe} from '@angular/core'; export class Dep {} @@ -1798,7 +1998,8 @@ function allTests(os: string) { export class TestPipe { constructor(dep: Dep) {} } - `); + `, + ); env.driveMain(); @@ -1807,27 +2008,33 @@ function allTests(os: string) { }); it('should compile Pipes with generic types', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Pipe} from '@angular/core'; @Pipe({ name: 'test-pipe', }) export class TestPipe {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); expect(jsContents).toContain('TestPipe.ɵpipe ='); const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents) - .toContain('static ɵpipe: i0.ɵɵPipeDeclaration, "test-pipe", false>;'); + expect(dtsContents).toContain( + 'static ɵpipe: i0.ɵɵPipeDeclaration, "test-pipe", false>;', + ); expect(dtsContents).toContain('static ɵfac: i0.ɵɵFactoryDeclaration, never>;'); }); it('should include @Pipes in @NgModule scopes', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, NgModule, Pipe} from '@angular/core'; @Pipe({name: 'test'}) @@ -1842,7 +2049,8 @@ function allTests(os: string) { @NgModule({declarations: [TestPipe, TestCmp]}) export class TestModule {} - `); + `, + ); env.driveMain(); @@ -1850,21 +2058,24 @@ function allTests(os: string) { expect(jsContents).toContain('dependencies: [TestPipe]'); const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents) - .toContain( - 'i0.ɵɵNgModuleDeclaration'); + expect(dtsContents).toContain( + 'i0.ɵɵNgModuleDeclaration', + ); }); describe('empty and missing selectors', () => { it('should use default selector for Components when no selector present', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ template: '...', }) export class TestCmp {} - `); + `, + ); env.driveMain(); @@ -1873,7 +2084,9 @@ function allTests(os: string) { }); it('should use default selector for Components with empty string selector', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -1881,7 +2094,8 @@ function allTests(os: string) { template: '...', }) export class TestCmp {} - `); + `, + ); env.driveMain(); @@ -1890,7 +2104,9 @@ function allTests(os: string) { }); it('should allow directives with no selector that are not in NgModules', () => { - env.write('main.ts', ` + env.write( + 'main.ts', + ` import {Directive} from '@angular/core'; @Directive({}) @@ -1906,27 +2122,36 @@ function allTests(os: string) { inputs: ['a', 'b'] }) export class TestDirWithInputs {} - `); + `, + ); const errors = env.driveDiagnostics(); expect(errors.length).toBe(0); }); it('should be able to use abstract directive in other compilation units', () => { - env.write('tsconfig.json', JSON.stringify({ - extends: './tsconfig-base.json', - compilerOptions: {rootDir: '.', outDir: '../node_modules/lib1_built'}, - })); - env.write('index.ts', ` + env.write( + 'tsconfig.json', + JSON.stringify({ + extends: './tsconfig-base.json', + compilerOptions: {rootDir: '.', outDir: '../node_modules/lib1_built'}, + }), + ); + env.write( + 'index.ts', + ` import {Directive} from '@angular/core'; @Directive() export class BaseClass {} - `); + `, + ); expect(env.driveDiagnostics().length).toBe(0); env.tsconfig(); - env.write('index.ts', ` + env.write( + 'index.ts', + ` import {NgModule, Directive} from '@angular/core'; import {BaseClass} from 'lib1_built'; @@ -1935,13 +2160,16 @@ function allTests(os: string) { @NgModule({declarations: [MyDirective]}) export class AppModule {} - `); + `, + ); expect(env.driveDiagnostics().length).toBe(0); }); it('should not allow directives with no selector that are in NgModules', () => { - env.write('main.ts', ` + env.write( + 'main.ts', + ` import {Directive, NgModule} from '@angular/core'; @Directive({}) @@ -1951,25 +2179,31 @@ function allTests(os: string) { declarations: [BaseDir], }) export class MyModule {} - `); + `, + ); const errors = env.driveDiagnostics(); - expect(trim(errors[0].messageText as string)) - .toContain('Directive BaseDir has no selector, please add it!'); + expect(trim(errors[0].messageText as string)).toContain( + 'Directive BaseDir has no selector, please add it!', + ); }); it('should throw if Directive selector is an empty string', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: '' }) export class TestDir {} - `); + `, + ); const errors = env.driveDiagnostics(); - expect(trim(errors[0].messageText as string)) - .toContain('Directive TestDir has no selector, please add it!'); + expect(trim(errors[0].messageText as string)).toContain( + 'Directive TestDir has no selector, please add it!', + ); }); }); @@ -1985,20 +2219,27 @@ function allTests(os: string) { it('should throw if invalid arguments are provided in @NgModule', () => { env.tsconfig({}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {NgModule} from '@angular/core'; // @ts-ignore @NgModule('invalidNgModuleArgumentType') export class MyModule {} - `); + `, + ); verifyThrownError( - ErrorCode.DECORATOR_ARG_NOT_LITERAL, '@NgModule argument must be an object literal'); + ErrorCode.DECORATOR_ARG_NOT_LITERAL, + '@NgModule argument must be an object literal', + ); }); it('should throw if multiple query decorators are used on the same field', () => { env.tsconfig({}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, ContentChild} from '@angular/core'; @Component({ @@ -2010,16 +2251,20 @@ function allTests(os: string) { @ContentChild('foo') foo: any; } - `); + `, + ); verifyThrownError( - ErrorCode.DECORATOR_COLLISION, 'Cannot combine multiple query decorators.'); + ErrorCode.DECORATOR_COLLISION, + 'Cannot combine multiple query decorators.', + ); }); - ['ViewChild', 'ViewChildren', 'ContentChild', 'ContentChildren'].forEach(decorator => { - it(`should throw if @Input and @${decorator} decorators are applied to the same property`, - () => { - env.tsconfig({}); - env.write('test.ts', ` + ['ViewChild', 'ViewChildren', 'ContentChild', 'ContentChildren'].forEach((decorator) => { + it(`should throw if @Input and @${decorator} decorators are applied to the same property`, () => { + env.tsconfig({}); + env.write( + 'test.ts', + ` import {Component, ${decorator}, Input} from '@angular/core'; @Component({ @@ -2029,15 +2274,19 @@ function allTests(os: string) { export class TestCmp { @Input() @${decorator}('foo') foo: any; } - `); - verifyThrownError( - ErrorCode.DECORATOR_COLLISION, - 'Cannot combine @Input decorators with query decorators'); - }); + `, + ); + verifyThrownError( + ErrorCode.DECORATOR_COLLISION, + 'Cannot combine @Input decorators with query decorators', + ); + }); it(`should throw if invalid options are provided in ${decorator}`, () => { env.tsconfig({}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, ${decorator}, Input} from '@angular/core'; @Component({ @@ -2048,15 +2297,19 @@ function allTests(os: string) { // @ts-ignore @${decorator}('foo', 'invalidOptionsArgumentType') foo: any; } - `); + `, + ); verifyThrownError( - ErrorCode.DECORATOR_ARG_NOT_LITERAL, - `@${decorator} options must be an object literal`); + ErrorCode.DECORATOR_ARG_NOT_LITERAL, + `@${decorator} options must be an object literal`, + ); }); it(`should throw if @${decorator} is used on non property-type member`, () => { env.tsconfig({}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, ${decorator}} from '@angular/core'; @Component({ @@ -2067,14 +2320,19 @@ function allTests(os: string) { @${decorator}('foo') private someFn() {} } - `); + `, + ); verifyThrownError( - ErrorCode.DECORATOR_UNEXPECTED, 'Query decorator must go on a property-type member'); + ErrorCode.DECORATOR_UNEXPECTED, + 'Query decorator must go on a property-type member', + ); }); it(`should throw error if @${decorator} has too many arguments`, () => { env.tsconfig({}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, ${decorator}} from '@angular/core'; @Component({ @@ -2085,14 +2343,19 @@ function allTests(os: string) { // @ts-ignore @${decorator}('foo', {}, 'invalid-extra-arg') foo: any; } - `); + `, + ); verifyThrownError( - ErrorCode.DECORATOR_ARITY_WRONG, `@${decorator} has too many arguments`); + ErrorCode.DECORATOR_ARITY_WRONG, + `@${decorator} has too many arguments`, + ); }); it(`should throw error if @${decorator} predicate argument has wrong type`, () => { env.tsconfig({}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, ${decorator}} from '@angular/core'; @Component({ @@ -2103,14 +2366,19 @@ function allTests(os: string) { // @ts-ignore @${decorator}({'invalid-predicate-type': true}) foo: any; } - `); + `, + ); verifyThrownError( - ErrorCode.VALUE_HAS_WRONG_TYPE, `@${decorator} predicate cannot be interpreted`); + ErrorCode.VALUE_HAS_WRONG_TYPE, + `@${decorator} predicate cannot be interpreted`, + ); }); it(`should throw error if one of @${decorator}'s predicate has wrong type`, () => { env.tsconfig({}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, ${decorator}} from '@angular/core'; @Component({ @@ -2121,16 +2389,20 @@ function allTests(os: string) { // @ts-ignore @${decorator}(['predicate-a', {'invalid-predicate-type': true}]) foo: any; } - `); + `, + ); verifyThrownError( - ErrorCode.VALUE_HAS_WRONG_TYPE, - `Failed to resolve @${decorator} predicate at position 1 to a string`); + ErrorCode.VALUE_HAS_WRONG_TYPE, + `Failed to resolve @${decorator} predicate at position 1 to a string`, + ); }); }); it(`should throw error if @Directive.inputs has wrong type`, () => { env.tsconfig({}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive} from '@angular/core'; @Directive({ @@ -2139,21 +2411,26 @@ function allTests(os: string) { inputs: 'invalid-field-type', }) export class TestDir {} - `); + `, + ); verifyThrownError( - ErrorCode.VALUE_HAS_WRONG_TYPE, - `Failed to resolve @Directive.inputs to an array Value is of type 'string'.`); + ErrorCode.VALUE_HAS_WRONG_TYPE, + `Failed to resolve @Directive.inputs to an array Value is of type 'string'.`, + ); }); it('should throw if decorator input is declared on static member', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Input} from '@angular/core'; @Directive() export class TestDir { @Input() static someInput: string = ''; } - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics).toEqual([ jasmine.objectContaining({ @@ -2164,7 +2441,9 @@ function allTests(os: string) { it(`should throw error if @Directive.outputs has wrong type`, () => { env.tsconfig({}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive} from '@angular/core'; @Directive({ @@ -2173,18 +2452,20 @@ function allTests(os: string) { outputs: 'invalid-field-type', }) export class TestDir {} - `); + `, + ); verifyThrownError( - ErrorCode.VALUE_HAS_WRONG_TYPE, - `Failed to resolve @Directive.outputs to a string array`); + ErrorCode.VALUE_HAS_WRONG_TYPE, + `Failed to resolve @Directive.outputs to a string array`, + ); }); - ['ContentChild', 'ContentChildren'].forEach(decorator => { - it(`should throw if \`descendants\` field of @${ - decorator}'s options argument has wrong type`, - () => { - env.tsconfig({}); - env.write('test.ts', ` + ['ContentChild', 'ContentChildren'].forEach((decorator) => { + it(`should throw if \`descendants\` field of @${decorator}'s options argument has wrong type`, () => { + env.tsconfig({}); + env.write( + 'test.ts', + ` import {Component, ContentChild} from '@angular/core'; @Component({ @@ -2195,17 +2476,21 @@ function allTests(os: string) { // @ts-ignore @ContentChild('foo', {descendants: 'invalid'}) foo: any; } - `); - verifyThrownError( - ErrorCode.VALUE_HAS_WRONG_TYPE, - '@ContentChild options.descendants must be a boolean'); - }); + `, + ); + verifyThrownError( + ErrorCode.VALUE_HAS_WRONG_TYPE, + '@ContentChild options.descendants must be a boolean', + ); + }); }); - ['Input', 'Output'].forEach(decorator => { + ['Input', 'Output'].forEach((decorator) => { it(`should throw error if @${decorator} decorator argument has unsupported type`, () => { env.tsconfig({}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, ${decorator}} from '@angular/core'; @Component({ @@ -2216,15 +2501,19 @@ function allTests(os: string) { // @ts-ignore @${decorator}(['invalid-arg-type']) foo: any; } - `); + `, + ); verifyThrownError( - ErrorCode.VALUE_HAS_WRONG_TYPE, - `@${decorator} decorator argument must resolve to a string`); + ErrorCode.VALUE_HAS_WRONG_TYPE, + `@${decorator} decorator argument must resolve to a string`, + ); }); it(`should throw error if @${decorator} decorator has too many arguments`, () => { env.tsconfig({}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, ${decorator}} from '@angular/core'; @Component({ @@ -2235,16 +2524,20 @@ function allTests(os: string) { // @ts-ignore @${decorator}('name', 'invalid-extra-arg') foo: any; } - `); + `, + ); verifyThrownError( - ErrorCode.DECORATOR_ARITY_WRONG, - `@${decorator} can have at most one argument, got 2 argument(s)`); + ErrorCode.DECORATOR_ARITY_WRONG, + `@${decorator} can have at most one argument, got 2 argument(s)`, + ); }); }); it('should throw error if @HostBinding decorator argument has unsupported type', () => { env.tsconfig({}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, HostBinding} from '@angular/core'; @Component({ @@ -2255,14 +2548,19 @@ function allTests(os: string) { // @ts-ignore @HostBinding(['invalid-arg-type']) foo: any; } - `); + `, + ); verifyThrownError( - ErrorCode.VALUE_HAS_WRONG_TYPE, `@HostBinding's argument must be a string`); + ErrorCode.VALUE_HAS_WRONG_TYPE, + `@HostBinding's argument must be a string`, + ); }); it('should throw error if @HostBinding decorator has too many arguments', () => { env.tsconfig({}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, HostBinding} from '@angular/core'; @Component({ @@ -2273,14 +2571,19 @@ function allTests(os: string) { // @ts-ignore @HostBinding('name', 'invalid-extra-arg') foo: any; } - `); + `, + ); verifyThrownError( - ErrorCode.DECORATOR_ARITY_WRONG, '@HostBinding can have at most one argument'); + ErrorCode.DECORATOR_ARITY_WRONG, + '@HostBinding can have at most one argument', + ); }); it('should throw error if @Directive.host field has wrong type', () => { env.tsconfig({}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive} from '@angular/core'; @Directive({ @@ -2289,15 +2592,19 @@ function allTests(os: string) { host: 'invalid-host-type' }) export class TestDir {} - `); + `, + ); verifyThrownError( - ErrorCode.VALUE_HAS_WRONG_TYPE, 'Decorator host metadata must be an object'); + ErrorCode.VALUE_HAS_WRONG_TYPE, + 'Decorator host metadata must be an object', + ); }); - it('should throw error if @Directive.host field is an object with values that have wrong types', - () => { - env.tsconfig({}); - env.write('test.ts', ` + it('should throw error if @Directive.host field is an object with values that have wrong types', () => { + env.tsconfig({}); + env.write( + 'test.ts', + ` import {Directive} from '@angular/core'; @Directive({ @@ -2306,15 +2613,19 @@ function allTests(os: string) { host: {'key': ['invalid-host-value']} }) export class TestDir {} - `); - verifyThrownError( - ErrorCode.VALUE_HAS_WRONG_TYPE, - 'Decorator host metadata must be a string -> string object, but found unparseable value'); - }); + `, + ); + verifyThrownError( + ErrorCode.VALUE_HAS_WRONG_TYPE, + 'Decorator host metadata must be a string -> string object, but found unparseable value', + ); + }); it('should throw error if @Directive.queries field has wrong type', () => { env.tsconfig({}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive} from '@angular/core'; @Directive({ @@ -2323,14 +2634,19 @@ function allTests(os: string) { queries: 'invalid-queries-type' }) export class TestDir {} - `); + `, + ); verifyThrownError( - ErrorCode.VALUE_HAS_WRONG_TYPE, 'Decorator queries metadata must be an object'); + ErrorCode.VALUE_HAS_WRONG_TYPE, + 'Decorator queries metadata must be an object', + ); }); it('should throw error if @Directive.queries object has incorrect values', () => { env.tsconfig({}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive} from '@angular/core'; @Directive({ @@ -2340,16 +2656,19 @@ function allTests(os: string) { } }) export class TestDir {} - `); + `, + ); verifyThrownError( - ErrorCode.VALUE_HAS_WRONG_TYPE, - 'Decorator query metadata must be an instance of a query type'); + ErrorCode.VALUE_HAS_WRONG_TYPE, + 'Decorator query metadata must be an instance of a query type', + ); }); - it('should throw error if @Directive.queries object has incorrect values (refs to other decorators)', - () => { - env.tsconfig({}); - env.write('test.ts', ` + it('should throw error if @Directive.queries object has incorrect values (refs to other decorators)', () => { + env.tsconfig({}); + env.write( + 'test.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -2359,27 +2678,36 @@ function allTests(os: string) { } }) export class TestDir {} - `); - verifyThrownError( - ErrorCode.VALUE_HAS_WRONG_TYPE, - 'Decorator query metadata must be an instance of a query type'); - }); + `, + ); + verifyThrownError( + ErrorCode.VALUE_HAS_WRONG_TYPE, + 'Decorator query metadata must be an instance of a query type', + ); + }); it('should throw error if @Injectable has incorrect argument', () => { env.tsconfig({}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Injectable} from '@angular/core'; // @ts-ignore @Injectable('invalid') export class TestProvider {} - `); + `, + ); verifyThrownError( - ErrorCode.DECORATOR_ARG_NOT_LITERAL, '@Injectable argument must be an object literal'); + ErrorCode.DECORATOR_ARG_NOT_LITERAL, + '@Injectable argument must be an object literal', + ); }); it('should produce a diagnostic if the transform value is not a function', () => { - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import {Directive, Input} from '@angular/core'; const NOT_A_FUNCTION: any = null!; @@ -2388,14 +2716,16 @@ function allTests(os: string) { export class Dir { @Input({transform: NOT_A_FUNCTION}) value!: number; } - `); + `, + ); verifyThrownError(ErrorCode.VALUE_HAS_WRONG_TYPE, `Input transform must be a function`); }); - it('should produce a diagnostic if the transform value in the inputs array is not a function', - () => { - env.write('/test.ts', ` + it('should produce a diagnostic if the transform value in the inputs array is not a function', () => { + env.write( + '/test.ts', + ` import {Directive, Input} from '@angular/core'; const NOT_A_FUNCTION: any = null!; @@ -2411,46 +2741,58 @@ function allTests(os: string) { export class Dir { value!: number; } - `); + `, + ); - verifyThrownError( - ErrorCode.VALUE_HAS_WRONG_TYPE, - `Transform of value at position 0 of @Directive.inputs array must be a function Value is of type 'null'.`); - }); + verifyThrownError( + ErrorCode.VALUE_HAS_WRONG_TYPE, + `Transform of value at position 0 of @Directive.inputs array must be a function Value is of type 'null'.`, + ); + }); - it('should produce a diangostic if the transform function first parameter has no arguments', - () => { - env.tsconfig({noImplicitAny: false}); - env.write('/test.ts', ` + it('should produce a diangostic if the transform function first parameter has no arguments', () => { + env.tsconfig({noImplicitAny: false}); + env.write( + '/test.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({selector: '[dir]', standalone: true}) export class Dir { @Input({transform: (val) => 1}) value!: number; } - `); + `, + ); - verifyThrownError( - ErrorCode.VALUE_HAS_WRONG_TYPE, - `Input transform function first parameter must have a type`); - }); + verifyThrownError( + ErrorCode.VALUE_HAS_WRONG_TYPE, + `Input transform function first parameter must have a type`, + ); + }); it('should produce a diangostic if the transform function is generic', () => { - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({selector: '[dir]', standalone: true}) export class Dir { @Input({transform: (val: T) => 1}) value!: number; } - `); + `, + ); verifyThrownError( - ErrorCode.VALUE_HAS_WRONG_TYPE, `Input transform function cannot be generic`); + ErrorCode.VALUE_HAS_WRONG_TYPE, + `Input transform function cannot be generic`, + ); }); it('should produce a diangostic if there is a conflicting coercion member', () => { - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({selector: '[dir]', standalone: true}) @@ -2459,24 +2801,30 @@ function allTests(os: string) { static ngAcceptInputType_value: boolean; } - `); + `, + ); verifyThrownError( - ErrorCode.CONFLICTING_INPUT_TRANSFORM, - `Class cannot have both a transform function on Input value and a static member called ngAcceptInputType_value`); + ErrorCode.CONFLICTING_INPUT_TRANSFORM, + `Class cannot have both a transform function on Input value and a static member called ngAcceptInputType_value`, + ); }); - it('should produce a diangostic if the transform function type cannot be referenced from the source file', - () => { - env.write('/util.ts', ` + it('should produce a diangostic if the transform function type cannot be referenced from the source file', () => { + env.write( + '/util.ts', + ` interface InternalType { foo: boolean; } export function toNumber(val: InternalType) { return 1; } - `); + `, + ); - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import {Directive, Input} from '@angular/core'; import {toNumber} from './util'; @@ -2484,23 +2832,30 @@ function allTests(os: string) { export class Dir { @Input({transform: toNumber}) value!: number; } - `); + `, + ); - verifyThrownError( - ErrorCode.IMPORT_GENERATION_FAILURE, 'Unable to import type InternalType.'); - }); + verifyThrownError( + ErrorCode.IMPORT_GENERATION_FAILURE, + 'Unable to import type InternalType.', + ); + }); - it('should produce a diangostic if a sub-type of the transform function cannot be referenced from the source file', - () => { - env.write('/util.ts', ` + it('should produce a diangostic if a sub-type of the transform function cannot be referenced from the source file', () => { + env.write( + '/util.ts', + ` interface InternalType { foo: boolean; } export function toNumber(val: {value: InternalType}) { return 1; } - `); + `, + ); - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import {Directive, Input} from '@angular/core'; import {toNumber} from './util'; @@ -2508,15 +2863,19 @@ function allTests(os: string) { export class Dir { @Input({transform: toNumber}) value!: number; } - `); + `, + ); - verifyThrownError( - ErrorCode.IMPORT_GENERATION_FAILURE, 'Unable to import type InternalType.'); - }); + verifyThrownError( + ErrorCode.IMPORT_GENERATION_FAILURE, + 'Unable to import type InternalType.', + ); + }); - it('should produce a diangostic if a generic parameter of the transform function cannot be referenced from the source file', - () => { - env.write('/util.ts', ` + it('should produce a diangostic if a generic parameter of the transform function cannot be referenced from the source file', () => { + env.write( + '/util.ts', + ` export interface GenericWrapper { value: T; } @@ -2526,9 +2885,12 @@ function allTests(os: string) { } export function toNumber(val: GenericWrapper) { return 1; } - `); + `, + ); - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import {Directive, Input} from '@angular/core'; import {toNumber} from './util'; @@ -2536,14 +2898,19 @@ function allTests(os: string) { export class Dir { @Input({transform: toNumber}) value!: number; } - `); + `, + ); - verifyThrownError( - ErrorCode.IMPORT_GENERATION_FAILURE, 'Unable to import type InternalType.'); - }); + verifyThrownError( + ErrorCode.IMPORT_GENERATION_FAILURE, + 'Unable to import type InternalType.', + ); + }); it('should produce a diangostic if transform type is not exported', () => { - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import {Directive, Input} from '@angular/core'; interface InternalType { @@ -2554,15 +2921,19 @@ function allTests(os: string) { export class Dir { @Input({transform: (val: InternalType) => 1}) val!: number; } - `); + `, + ); verifyThrownError( - ErrorCode.SYMBOL_NOT_EXPORTED, - 'Symbol must be exported in order to be used as the type of an Input transform function'); + ErrorCode.SYMBOL_NOT_EXPORTED, + 'Symbol must be exported in order to be used as the type of an Input transform function', + ); }); it('should produce a diangostic if the transform value is not a function', () => { - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import {Directive, Input} from '@angular/core'; function createTransform(outerValue: number) { @@ -2573,13 +2944,16 @@ function allTests(os: string) { export class Dir { @Input({transform: createTransform(1)}) value!: number; } - `); + `, + ); verifyThrownError(ErrorCode.VALUE_HAS_WRONG_TYPE, `Input transform must be a function`); }); it('should produce a diangostic if the first parameter of a transform is a spread', () => { - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import {Directive, Input} from '@angular/core'; function toNumber(...value: (string | boolean)[]) { return 1; } @@ -2588,15 +2962,19 @@ function allTests(os: string) { export class Dir { @Input({transform: toNumber}) value!: number; } - `); + `, + ); verifyThrownError( - ErrorCode.VALUE_HAS_WRONG_TYPE, - `Input transform function first parameter cannot be a spread parameter`); + ErrorCode.VALUE_HAS_WRONG_TYPE, + `Input transform function first parameter cannot be a spread parameter`, + ); }); it('should produce a diangostic if a transform function has multiple signatures', () => { - env.write('/test.ts', ` + env.write( + '/test.ts', + ` import {Directive, Input} from '@angular/core'; function toNumber(value: boolean): number; @@ -2607,17 +2985,21 @@ function allTests(os: string) { export class Dir { @Input({transform: toNumber}) value!: number; } - `); + `, + ); verifyThrownError( - ErrorCode.VALUE_HAS_WRONG_TYPE, - `Input transform function cannot have multiple signatures`); + ErrorCode.VALUE_HAS_WRONG_TYPE, + `Input transform function cannot have multiple signatures`, + ); }); }); describe('multiple decorators on classes', () => { it('should compile @Injectable on Components, Directives, Pipes, and Modules', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, Injectable, NgModule, Pipe} from '@angular/core'; @Component({selector: 'test', template: 'test'}) @@ -2635,7 +3017,8 @@ function allTests(os: string) { @NgModule({declarations: [TestCmp, TestDir, TestPipe]}) @Injectable() export class TestNgModule {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -2668,23 +3051,26 @@ function allTests(os: string) { }); it('should not compile a component and a directive annotation on the same class', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive} from '@angular/core'; @Component({selector: 'test', template: 'test'}) @Directive({selector: 'test'}) class ShouldNotCompile {} - `); + `, + ); const errors = env.driveDiagnostics(); expect(errors.length).toBe(1); expect(errors[0].messageText).toContain('Two incompatible decorators on class'); }); - - it('should leave decorators present on jit: true directives', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Inject} from '@angular/core'; @Directive({ @@ -2694,7 +3080,8 @@ function allTests(os: string) { export class Test { constructor(@Inject('foo') foo: string) {} } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); expect(jsContents).toContain('Directive({'); @@ -2704,59 +3091,69 @@ function allTests(os: string) { describe('compiling invalid @Injectables', () => { describe('with strictInjectionParameters = true', () => { - it('should give a compile-time error if an invalid @Injectable is used with no arguments', - () => { - env.tsconfig({strictInjectionParameters: true}); - env.write('test.ts', ` + it('should give a compile-time error if an invalid @Injectable is used with no arguments', () => { + env.tsconfig({strictInjectionParameters: true}); + env.write( + 'test.ts', + ` import {Injectable} from '@angular/core'; @Injectable() export class Test { constructor(private notInjectable: string) {} } - `); - - const errors = env.driveDiagnostics(); - expect(errors.length).toBe(1); - expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n')) - .toBe( - `No suitable injection token for parameter 'notInjectable' of class 'Test'.\n` + - ` Consider using the @Inject decorator to specify an injection token.`); - expect(errors[0].relatedInformation!.length).toBe(1); - expect(errors[0].relatedInformation![0].messageText) - .toBe('This type is not supported as injection token.'); - }); - - it('should give a compile-time error if an invalid @Injectable is used with an argument', - () => { - env.tsconfig({strictInjectionParameters: true}); - env.write('test.ts', ` + `, + ); + + const errors = env.driveDiagnostics(); + expect(errors.length).toBe(1); + expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n')).toBe( + `No suitable injection token for parameter 'notInjectable' of class 'Test'.\n` + + ` Consider using the @Inject decorator to specify an injection token.`, + ); + expect(errors[0].relatedInformation!.length).toBe(1); + expect(errors[0].relatedInformation![0].messageText).toBe( + 'This type is not supported as injection token.', + ); + }); + + it('should give a compile-time error if an invalid @Injectable is used with an argument', () => { + env.tsconfig({strictInjectionParameters: true}); + env.write( + 'test.ts', + ` import {Injectable} from '@angular/core'; @Injectable({providedIn: 'root'}) export class Test { constructor(private notInjectable: string) {} } - `); - - const errors = env.driveDiagnostics(); - expect(errors.length).toBe(1); - expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n')) - .toBe( - `No suitable injection token for parameter 'notInjectable' of class 'Test'.\n` + - ` Consider using the @Inject decorator to specify an injection token.`); - expect(errors[0].relatedInformation!.length).toBe(1); - expect(errors[0].relatedInformation![0].messageText) - .toBe('This type is not supported as injection token.'); - }); - - it('should report an error when using a symbol from a type-only import clause as injection token', - () => { - env.tsconfig({strictInjectionParameters: true}); - env.write(`types.ts`, ` + `, + ); + + const errors = env.driveDiagnostics(); + expect(errors.length).toBe(1); + expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n')).toBe( + `No suitable injection token for parameter 'notInjectable' of class 'Test'.\n` + + ` Consider using the @Inject decorator to specify an injection token.`, + ); + expect(errors[0].relatedInformation!.length).toBe(1); + expect(errors[0].relatedInformation![0].messageText).toBe( + 'This type is not supported as injection token.', + ); + }); + + it('should report an error when using a symbol from a type-only import clause as injection token', () => { + env.tsconfig({strictInjectionParameters: true}); + env.write( + `types.ts`, + ` export class TypeOnly {} - `); - env.write(`test.ts`, ` + `, + ); + env.write( + `test.ts`, + ` import {Injectable} from '@angular/core'; import type {TypeOnly} from './types'; @@ -2764,31 +3161,37 @@ function allTests(os: string) { export class MyService { constructor(param: TypeOnly) {} } - `); - - const diags = env.driveDiagnostics(); - expect(diags.length).toBe(1); - expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')) - .toBe( - `No suitable injection token for parameter 'param' of class 'MyService'.\n` + - ` Consider changing the type-only import to a regular import, ` + - `or use the @Inject decorator to specify an injection token.`); - expect(diags[0].relatedInformation!.length).toBe(2); - expect(diags[0].relatedInformation![0].messageText) - .toBe( - 'This type is imported using a type-only import, ' + - 'which prevents it from being usable as an injection token.'); - expect(diags[0].relatedInformation![1].messageText) - .toBe('The type-only import occurs here.'); - }); - - it('should report an error when using a symbol from a type-only import specifier as injection token', - () => { - env.tsconfig({strictInjectionParameters: true}); - env.write(`types.ts`, ` + `, + ); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')).toBe( + `No suitable injection token for parameter 'param' of class 'MyService'.\n` + + ` Consider changing the type-only import to a regular import, ` + + `or use the @Inject decorator to specify an injection token.`, + ); + expect(diags[0].relatedInformation!.length).toBe(2); + expect(diags[0].relatedInformation![0].messageText).toBe( + 'This type is imported using a type-only import, ' + + 'which prevents it from being usable as an injection token.', + ); + expect(diags[0].relatedInformation![1].messageText).toBe( + 'The type-only import occurs here.', + ); + }); + + it('should report an error when using a symbol from a type-only import specifier as injection token', () => { + env.tsconfig({strictInjectionParameters: true}); + env.write( + `types.ts`, + ` export class TypeOnly {} - `); - env.write(`test.ts`, ` + `, + ); + env.write( + `test.ts`, + ` import {Injectable} from '@angular/core'; import {type TypeOnly} from './types'; @@ -2796,49 +3199,57 @@ function allTests(os: string) { export class MyService { constructor(param: TypeOnly) {} } - `); - - const diags = env.driveDiagnostics(); - expect(diags.length).toBe(1); - expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')) - .toBe( - `No suitable injection token for parameter 'param' of class 'MyService'.\n` + - ` Consider changing the type-only import to a regular import, ` + - `or use the @Inject decorator to specify an injection token.`); - expect(diags[0].relatedInformation!.length).toBe(2); - expect(diags[0].relatedInformation![0].messageText) - .toBe( - 'This type is imported using a type-only import, ' + - 'which prevents it from being usable as an injection token.'); - expect(diags[0].relatedInformation![1].messageText) - .toBe('The type-only import occurs here.'); - }); + `, + ); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')).toBe( + `No suitable injection token for parameter 'param' of class 'MyService'.\n` + + ` Consider changing the type-only import to a regular import, ` + + `or use the @Inject decorator to specify an injection token.`, + ); + expect(diags[0].relatedInformation!.length).toBe(2); + expect(diags[0].relatedInformation![0].messageText).toBe( + 'This type is imported using a type-only import, ' + + 'which prevents it from being usable as an injection token.', + ); + expect(diags[0].relatedInformation![1].messageText).toBe( + 'The type-only import occurs here.', + ); + }); it('should report an error when using a primitive type as injection token', () => { env.tsconfig({strictInjectionParameters: true}); - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Injectable} from '@angular/core'; @Injectable() export class MyService { constructor(param: string) {} } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')) - .toBe( - `No suitable injection token for parameter 'param' of class 'MyService'.\n` + - ` Consider using the @Inject decorator to specify an injection token.`); + expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')).toBe( + `No suitable injection token for parameter 'param' of class 'MyService'.\n` + + ` Consider using the @Inject decorator to specify an injection token.`, + ); expect(diags[0].relatedInformation!.length).toBe(1); - expect(diags[0].relatedInformation![0].messageText) - .toBe('This type is not supported as injection token.'); + expect(diags[0].relatedInformation![0].messageText).toBe( + 'This type is not supported as injection token.', + ); }); it('should report an error when using a union type as injection token', () => { env.tsconfig({strictInjectionParameters: true}); - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Injectable} from '@angular/core'; export class ClassA {} @@ -2848,22 +3259,26 @@ function allTests(os: string) { export class MyService { constructor(param: ClassA|ClassB) {} } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')) - .toBe( - `No suitable injection token for parameter 'param' of class 'MyService'.\n` + - ` Consider using the @Inject decorator to specify an injection token.`); + expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')).toBe( + `No suitable injection token for parameter 'param' of class 'MyService'.\n` + + ` Consider using the @Inject decorator to specify an injection token.`, + ); expect(diags[0].relatedInformation!.length).toBe(1); - expect(diags[0].relatedInformation![0].messageText) - .toBe('This type is not supported as injection token.'); + expect(diags[0].relatedInformation![0].messageText).toBe( + 'This type is not supported as injection token.', + ); }); it('should report an error when using an interface as injection token', () => { env.tsconfig({strictInjectionParameters: true}); - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Injectable} from '@angular/core'; export interface Interface {} @@ -2872,17 +3287,19 @@ function allTests(os: string) { export class MyService { constructor(param: Interface) {} } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')) - .toBe( - `No suitable injection token for parameter 'param' of class 'MyService'.\n` + - ` Consider using the @Inject decorator to specify an injection token.`); + expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')).toBe( + `No suitable injection token for parameter 'param' of class 'MyService'.\n` + + ` Consider using the @Inject decorator to specify an injection token.`, + ); expect(diags[0].relatedInformation!.length).toBe(2); - expect(diags[0].relatedInformation![0].messageText) - .toBe('This type does not have a value, so it cannot be used as injection token.'); + expect(diags[0].relatedInformation![0].messageText).toBe( + 'This type does not have a value, so it cannot be used as injection token.', + ); expect(diags[0].relatedInformation![1].messageText).toBe('The type is declared here.'); }); @@ -2892,7 +3309,9 @@ function allTests(os: string) { // would result in a semantic TypeScript diagnostic which we ignore in this // test to verify that ngtsc's analysis is able to operate in this situation. env.tsconfig({strictInjectionParameters: true}); - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Injectable} from '@angular/core'; // @ts-expect-error import {Interface} from 'missing'; @@ -2901,45 +3320,51 @@ function allTests(os: string) { export class MyService { constructor(param: Interface) {} } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')) - .toBe( - `No suitable injection token for parameter 'param' of ` + - `class 'MyService'.\n` + - ` Consider using the @Inject decorator to specify an injection token.`); + expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')).toBe( + `No suitable injection token for parameter 'param' of ` + + `class 'MyService'.\n` + + ` Consider using the @Inject decorator to specify an injection token.`, + ); expect(diags[0].relatedInformation!.length).toBe(1); - expect(diags[0].relatedInformation![0].messageText) - .toBe('This type does not have a value, so it cannot be used as injection token.'); + expect(diags[0].relatedInformation![0].messageText).toBe( + 'This type does not have a value, so it cannot be used as injection token.', + ); }); it('should report an error when no type is present', () => { env.tsconfig({strictInjectionParameters: true, noImplicitAny: false}); - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Injectable} from '@angular/core'; @Injectable() export class MyService { constructor(param) {} } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')) - .toBe( - `No suitable injection token for parameter 'param' of class 'MyService'.\n` + - ` Consider adding a type to the parameter or ` + - `use the @Inject decorator to specify an injection token.`); + expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')).toBe( + `No suitable injection token for parameter 'param' of class 'MyService'.\n` + + ` Consider adding a type to the parameter or ` + + `use the @Inject decorator to specify an injection token.`, + ); expect(diags[0].relatedInformation).toBeUndefined(); }); - it('should not give a compile-time error if an invalid @Injectable is used with useValue', - () => { - env.tsconfig({strictInjectionParameters: true}); - env.write('test.ts', ` + it('should not give a compile-time error if an invalid @Injectable is used with useValue', () => { + env.tsconfig({strictInjectionParameters: true}); + env.write( + 'test.ts', + ` import {Injectable} from '@angular/core'; @Injectable({ @@ -2949,18 +3374,19 @@ function allTests(os: string) { export class Test { constructor(private notInjectable: string) {} } - `); - - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents) - .toMatch(/function Test_Factory\(t\) { i0\.ɵɵinvalidFactory\(\)/ms); - }); - - it('should not give a compile-time error if an invalid @Injectable is used with useFactory', - () => { - env.tsconfig({strictInjectionParameters: true}); - env.write('test.ts', ` + `, + ); + + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toMatch(/function Test_Factory\(t\) { i0\.ɵɵinvalidFactory\(\)/ms); + }); + + it('should not give a compile-time error if an invalid @Injectable is used with useFactory', () => { + env.tsconfig({strictInjectionParameters: true}); + env.write( + 'test.ts', + ` import {Injectable} from '@angular/core'; @Injectable({ @@ -2970,18 +3396,19 @@ function allTests(os: string) { export class Test { constructor(private notInjectable: string) {} } - `); - - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents) - .toMatch(/function Test_Factory\(t\) { i0\.ɵɵinvalidFactory\(\)/ms); - }); - - it('should not give a compile-time error if an invalid @Injectable is used with useExisting', - () => { - env.tsconfig({strictInjectionParameters: true}); - env.write('test.ts', ` + `, + ); + + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toMatch(/function Test_Factory\(t\) { i0\.ɵɵinvalidFactory\(\)/ms); + }); + + it('should not give a compile-time error if an invalid @Injectable is used with useExisting', () => { + env.tsconfig({strictInjectionParameters: true}); + env.write( + 'test.ts', + ` import {Injectable} from '@angular/core'; export class MyService {} @@ -2993,18 +3420,19 @@ function allTests(os: string) { export class Test { constructor(private notInjectable: string) {} } - `); - - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents) - .toMatch(/function Test_Factory\(t\) { i0\.ɵɵinvalidFactory\(\)/ms); - }); - - it('should not give a compile-time error if an invalid @Injectable is used with useClass', - () => { - env.tsconfig({strictInjectionParameters: true}); - env.write('test.ts', ` + `, + ); + + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toMatch(/function Test_Factory\(t\) { i0\.ɵɵinvalidFactory\(\)/ms); + }); + + it('should not give a compile-time error if an invalid @Injectable is used with useClass', () => { + env.tsconfig({strictInjectionParameters: true}); + env.write( + 'test.ts', + ` import {Injectable} from '@angular/core'; export class MyService {} @@ -3016,36 +3444,38 @@ function allTests(os: string) { export class Test { constructor(private notInjectable: string) {} } - `); - - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents) - .toMatch(/function Test_Factory\(t\) { i0\.ɵɵinvalidFactory\(\)/ms); - }); - - it('should not give a compile-time error if an invalid @Injectable without providedIn is an abstract class', - () => { - env.tsconfig({strictInjectionParameters: true}); - env.write('test.ts', ` + `, + ); + + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toMatch(/function Test_Factory\(t\) { i0\.ɵɵinvalidFactory\(\)/ms); + }); + + it('should not give a compile-time error if an invalid @Injectable without providedIn is an abstract class', () => { + env.tsconfig({strictInjectionParameters: true}); + env.write( + 'test.ts', + ` import {Injectable} from '@angular/core'; @Injectable() export abstract class Test { constructor(private notInjectable: string) {} } - `); - - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents) - .toMatch(/function Test_Factory\(t\) { i0\.ɵɵinvalidFactory\(\)/ms); - }); - - it('should not give a compile-time error if an invalid @Injectable with providedIn is an abstract class', - () => { - env.tsconfig({strictInjectionParameters: true}); - env.write('test.ts', ` + `, + ); + + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toMatch(/function Test_Factory\(t\) { i0\.ɵɵinvalidFactory\(\)/ms); + }); + + it('should not give a compile-time error if an invalid @Injectable with providedIn is an abstract class', () => { + env.tsconfig({strictInjectionParameters: true}); + env.write( + 'test.ts', + ` import {Injectable} from '@angular/core'; @Injectable({ @@ -3054,17 +3484,19 @@ function allTests(os: string) { export abstract class Test { constructor(private notInjectable: string) {} } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents) - .toMatch(/function Test_Factory\(t\) { i0\.ɵɵinvalidFactory\(\)/ms); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toMatch(/function Test_Factory\(t\) { i0\.ɵɵinvalidFactory\(\)/ms); + }); it('should give a compile-time error when a derived Directive inherits an invalid constructor', () => { env.tsconfig({strictInjectionParameters: true}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive} from '@angular/core'; @Directive() @@ -3090,34 +3522,39 @@ function allTests(os: string) { @Directive() export class ConcreteDerivedDirWithoutCtor extends ConcreteDirWithCtor {} - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(2); - expect(diags[0].code) - .toBe(ngErrorCode(ErrorCode.INJECTABLE_INHERITS_INVALID_CONSTRUCTOR)); - expect(diags[0].messageText) - .toEqual( - `The directive ConcreteMiddleDir inherits its constructor from ParentDir, but the latter has ` + - `a constructor parameter that is not compatible with dependency injection. Either add an explicit ` + - `constructor to ConcreteMiddleDir or change ParentDir's constructor to use parameters that are ` + - `valid for DI.`); + expect(diags[0].code).toBe( + ngErrorCode(ErrorCode.INJECTABLE_INHERITS_INVALID_CONSTRUCTOR), + ); + expect(diags[0].messageText).toEqual( + `The directive ConcreteMiddleDir inherits its constructor from ParentDir, but the latter has ` + + `a constructor parameter that is not compatible with dependency injection. Either add an explicit ` + + `constructor to ConcreteMiddleDir or change ParentDir's constructor to use parameters that are ` + + `valid for DI.`, + ); expect(getDiagnosticSourceCode(diags[0])).toBe('ConcreteMiddleDir'); - expect(diags[1].code) - .toBe(ngErrorCode(ErrorCode.INJECTABLE_INHERITS_INVALID_CONSTRUCTOR)); - expect(diags[1].messageText) - .toEqual( - `The directive ConcreteDirWithoutCtor inherits its constructor from ParentDir, but the latter ` + - `has a constructor parameter that is not compatible with dependency injection. Either add an ` + - `explicit constructor to ConcreteDirWithoutCtor or change ParentDir's constructor to use ` + - `parameters that are valid for DI.`); + expect(diags[1].code).toBe( + ngErrorCode(ErrorCode.INJECTABLE_INHERITS_INVALID_CONSTRUCTOR), + ); + expect(diags[1].messageText).toEqual( + `The directive ConcreteDirWithoutCtor inherits its constructor from ParentDir, but the latter ` + + `has a constructor parameter that is not compatible with dependency injection. Either add an ` + + `explicit constructor to ConcreteDirWithoutCtor or change ParentDir's constructor to use ` + + `parameters that are valid for DI.`, + ); expect(getDiagnosticSourceCode(diags[1])).toBe('ConcreteDirWithoutCtor'); }); it('should give a compile-time error when a derived Injectable inherits an invalid constructor', () => { env.tsconfig({strictInjectionParameters: true}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Injectable} from '@angular/core'; @Injectable() @@ -3158,44 +3595,50 @@ function allTests(os: string) { @Injectable({ providedIn: 'root', useExisting: ConcreteServiceWithCtor }) export class UseExistingService extends ParentService {} - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(3); - expect(diags[0].code) - .toBe(ngErrorCode(ErrorCode.INJECTABLE_INHERITS_INVALID_CONSTRUCTOR)); - expect(diags[0].messageText) - .toEqual( - `The injectable ConcreteMiddleService inherits its constructor from ParentService, but the ` + - `latter has a constructor parameter that is not compatible with dependency injection. Either add ` + - `an explicit constructor to ConcreteMiddleService or change ParentService's constructor to use ` + - `parameters that are valid for DI.`); + expect(diags[0].code).toBe( + ngErrorCode(ErrorCode.INJECTABLE_INHERITS_INVALID_CONSTRUCTOR), + ); + expect(diags[0].messageText).toEqual( + `The injectable ConcreteMiddleService inherits its constructor from ParentService, but the ` + + `latter has a constructor parameter that is not compatible with dependency injection. Either add ` + + `an explicit constructor to ConcreteMiddleService or change ParentService's constructor to use ` + + `parameters that are valid for DI.`, + ); expect(getDiagnosticSourceCode(diags[0])).toBe('ConcreteMiddleService'); - expect(diags[1].code) - .toBe(ngErrorCode(ErrorCode.INJECTABLE_INHERITS_INVALID_CONSTRUCTOR)); - expect(diags[1].messageText) - .toEqual( - `The injectable ConcreteServiceWithoutCtor inherits its constructor from ParentService, but the ` + - `latter has a constructor parameter that is not compatible with dependency injection. Either add ` + - `an explicit constructor to ConcreteServiceWithoutCtor or change ParentService's constructor to ` + - `use parameters that are valid for DI.`); + expect(diags[1].code).toBe( + ngErrorCode(ErrorCode.INJECTABLE_INHERITS_INVALID_CONSTRUCTOR), + ); + expect(diags[1].messageText).toEqual( + `The injectable ConcreteServiceWithoutCtor inherits its constructor from ParentService, but the ` + + `latter has a constructor parameter that is not compatible with dependency injection. Either add ` + + `an explicit constructor to ConcreteServiceWithoutCtor or change ParentService's constructor to ` + + `use parameters that are valid for DI.`, + ); expect(getDiagnosticSourceCode(diags[1])).toBe('ConcreteServiceWithoutCtor'); - expect(diags[2].code) - .toBe(ngErrorCode(ErrorCode.INJECTABLE_INHERITS_INVALID_CONSTRUCTOR)); - expect(diags[2].messageText) - .toEqual( - `The injectable ProvidedInRootService inherits its constructor from ParentService, but the ` + - `latter has a constructor parameter that is not compatible with dependency injection. Either add ` + - `an explicit constructor to ProvidedInRootService or change ParentService's constructor to use ` + - `parameters that are valid for DI.`); + expect(diags[2].code).toBe( + ngErrorCode(ErrorCode.INJECTABLE_INHERITS_INVALID_CONSTRUCTOR), + ); + expect(diags[2].messageText).toEqual( + `The injectable ProvidedInRootService inherits its constructor from ParentService, but the ` + + `latter has a constructor parameter that is not compatible with dependency injection. Either add ` + + `an explicit constructor to ProvidedInRootService or change ParentService's constructor to use ` + + `parameters that are valid for DI.`, + ); expect(getDiagnosticSourceCode(diags[2])).toBe('ProvidedInRootService'); }); it('should give a compile-time error when a derived Directive inherits from a non-decorated class', () => { env.tsconfig({strictInjectionParameters: true}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive} from '@angular/core'; export abstract class ParentClass { @@ -3220,43 +3663,45 @@ function allTests(os: string) { @Directive() export class ConcreteDerivedDirWithoutCtor extends ConcreteDirWithCtor {} - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(3); expect(diags[0].code).toBe(ngErrorCode(ErrorCode.DIRECTIVE_INHERITS_UNDECORATED_CTOR)); - expect(diags[0].messageText) - .toEqual( - `The directive AbstractMiddleDir inherits its constructor from ParentClass, but the latter ` + - `does not have an Angular decorator of its own. Dependency injection will not be able to resolve ` + - `the parameters of ParentClass's constructor. Either add a @Directive decorator to ParentClass, ` + - `or add an explicit constructor to AbstractMiddleDir.`); + expect(diags[0].messageText).toEqual( + `The directive AbstractMiddleDir inherits its constructor from ParentClass, but the latter ` + + `does not have an Angular decorator of its own. Dependency injection will not be able to resolve ` + + `the parameters of ParentClass's constructor. Either add a @Directive decorator to ParentClass, ` + + `or add an explicit constructor to AbstractMiddleDir.`, + ); expect(getDiagnosticSourceCode(diags[0])).toBe('AbstractMiddleDir'); expect(diags[1].code).toBe(ngErrorCode(ErrorCode.DIRECTIVE_INHERITS_UNDECORATED_CTOR)); - expect(diags[1].messageText) - .toEqual( - `The directive ConcreteMiddleDir inherits its constructor from ParentClass, but the latter ` + - `does not have an Angular decorator of its own. Dependency injection will not be able to resolve ` + - `the parameters of ParentClass's constructor. Either add a @Directive decorator to ParentClass, or ` + - `add an explicit constructor to ConcreteMiddleDir.`); + expect(diags[1].messageText).toEqual( + `The directive ConcreteMiddleDir inherits its constructor from ParentClass, but the latter ` + + `does not have an Angular decorator of its own. Dependency injection will not be able to resolve ` + + `the parameters of ParentClass's constructor. Either add a @Directive decorator to ParentClass, or ` + + `add an explicit constructor to ConcreteMiddleDir.`, + ); expect(getDiagnosticSourceCode(diags[1])).toBe('ConcreteMiddleDir'); expect(diags[2].code).toBe(ngErrorCode(ErrorCode.DIRECTIVE_INHERITS_UNDECORATED_CTOR)); - expect(diags[2].messageText) - .toEqual( - `The directive ConcreteDirWithoutCtor inherits its constructor from ParentClass, but the latter ` + - `does not have an Angular decorator of its own. Dependency injection will not be able to resolve ` + - `the parameters of ParentClass's constructor. Either add a @Directive decorator to ParentClass, ` + - `or add an explicit constructor to ConcreteDirWithoutCtor.`); + expect(diags[2].messageText).toEqual( + `The directive ConcreteDirWithoutCtor inherits its constructor from ParentClass, but the latter ` + + `does not have an Angular decorator of its own. Dependency injection will not be able to resolve ` + + `the parameters of ParentClass's constructor. Either add a @Directive decorator to ParentClass, ` + + `or add an explicit constructor to ConcreteDirWithoutCtor.`, + ); expect(getDiagnosticSourceCode(diags[2])).toBe('ConcreteDirWithoutCtor'); }); // https://github.com/angular/angular/issues/48152 - it('should not give a compile-time error when a class inherits from foreign compilation unit', - () => { - env.tsconfig({strictInjectionParameters: true}); - env.write('node_modules/external/index.d.ts', ` + it('should not give a compile-time error when a class inherits from foreign compilation unit', () => { + env.tsconfig({strictInjectionParameters: true}); + env.write( + 'node_modules/external/index.d.ts', + ` import * as i0 from '@angular/core'; export abstract class ExternalClass { @@ -3265,8 +3710,11 @@ function allTests(os: string) { constructor(invalid: string) {} } - `); - env.write('test.ts', ` + `, + ); + env.write( + 'test.ts', + ` import {Directive} from '@angular/core'; import {ExternalClass} from 'external'; @@ -3278,50 +3726,60 @@ function allTests(os: string) { @Directive() export class ConcreteDirWithoutCtor extends ConcreteMiddleDir {} - `); + `, + ); - env.driveMain(); - }); + env.driveMain(); + }); }); describe('with strictInjectionParameters = false', () => { it('should compile an @Injectable on a class with a non-injectable constructor', () => { env.tsconfig({strictInjectionParameters: false}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Injectable} from '@angular/core'; @Injectable() export class Test { constructor(private notInjectable: string) {} } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain('Test.ɵfac = function Test_Factory(t) { i0.ɵɵinvalidFactory()'); + expect(jsContents).toContain( + 'Test.ɵfac = function Test_Factory(t) { i0.ɵɵinvalidFactory()', + ); }); - it('should compile an @Injectable provided in the root on a class with a non-injectable constructor', - () => { - env.tsconfig({strictInjectionParameters: false}); - env.write('test.ts', ` + it('should compile an @Injectable provided in the root on a class with a non-injectable constructor', () => { + env.tsconfig({strictInjectionParameters: false}); + env.write( + 'test.ts', + ` import {Injectable} from '@angular/core'; @Injectable({providedIn: 'root'}) export class Test { constructor(private notInjectable: string) {} } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain('Test.ɵfac = function Test_Factory(t) { i0.ɵɵinvalidFactory()'); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain( + 'Test.ɵfac = function Test_Factory(t) { i0.ɵɵinvalidFactory()', + ); + }); it('should compile when a derived Directive inherits an invalid constructor', () => { env.tsconfig({strictInjectionParameters: false}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive} from '@angular/core'; @Directive() @@ -3347,14 +3805,17 @@ function allTests(os: string) { @Directive() export class ConcreteDerivedDirWithoutCtor extends ConcreteDirWithCtor {} - `); + `, + ); env.driveMain(); }); it('should compile when a derived Injectable inherits an invalid constructor', () => { env.tsconfig({strictInjectionParameters: false}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Injectable} from '@angular/core'; @Injectable() @@ -3395,7 +3856,8 @@ function allTests(os: string) { @Injectable({ providedIn: 'root', useExisting: ConcreteServiceWithCtor }) export class UseExistingService extends ParentService {} - `); + `, + ); env.driveMain(); }); @@ -3404,7 +3866,9 @@ function allTests(os: string) { // Errors for undecorated base classes should always be reported, even under // `strictInjectionParameters`. env.tsconfig({strictInjectionParameters: false}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive} from '@angular/core'; export abstract class ParentClass { @@ -3429,35 +3893,36 @@ function allTests(os: string) { @Directive() export class ConcreteDerivedDirWithoutCtor extends ConcreteDirWithCtor {} - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(3); expect(diags[0].code).toBe(ngErrorCode(ErrorCode.DIRECTIVE_INHERITS_UNDECORATED_CTOR)); - expect(diags[0].messageText) - .toEqual( - `The directive AbstractMiddleDir inherits its constructor from ParentClass, but the latter ` + - `does not have an Angular decorator of its own. Dependency injection will not be able to resolve ` + - `the parameters of ParentClass's constructor. Either add a @Directive decorator to ParentClass, ` + - `or add an explicit constructor to AbstractMiddleDir.`); + expect(diags[0].messageText).toEqual( + `The directive AbstractMiddleDir inherits its constructor from ParentClass, but the latter ` + + `does not have an Angular decorator of its own. Dependency injection will not be able to resolve ` + + `the parameters of ParentClass's constructor. Either add a @Directive decorator to ParentClass, ` + + `or add an explicit constructor to AbstractMiddleDir.`, + ); expect(getDiagnosticSourceCode(diags[0])).toBe('AbstractMiddleDir'); expect(diags[1].code).toBe(ngErrorCode(ErrorCode.DIRECTIVE_INHERITS_UNDECORATED_CTOR)); - expect(diags[1].messageText) - .toEqual( - `The directive ConcreteMiddleDir inherits its constructor from ParentClass, but the latter ` + - `does not have an Angular decorator of its own. Dependency injection will not be able to resolve ` + - `the parameters of ParentClass's constructor. Either add a @Directive decorator to ParentClass, ` + - `or add an explicit constructor to ConcreteMiddleDir.`); + expect(diags[1].messageText).toEqual( + `The directive ConcreteMiddleDir inherits its constructor from ParentClass, but the latter ` + + `does not have an Angular decorator of its own. Dependency injection will not be able to resolve ` + + `the parameters of ParentClass's constructor. Either add a @Directive decorator to ParentClass, ` + + `or add an explicit constructor to ConcreteMiddleDir.`, + ); expect(getDiagnosticSourceCode(diags[1])).toBe('ConcreteMiddleDir'); expect(diags[2].code).toBe(ngErrorCode(ErrorCode.DIRECTIVE_INHERITS_UNDECORATED_CTOR)); - expect(diags[2].messageText) - .toEqual( - `The directive ConcreteDirWithoutCtor inherits its constructor from ParentClass, but the latter ` + - `does not have an Angular decorator of its own. Dependency injection will not be able to resolve ` + - `the parameters of ParentClass's constructor. Either add a @Directive decorator to ParentClass, ` + - `or add an explicit constructor to ConcreteDirWithoutCtor.`); + expect(diags[2].messageText).toEqual( + `The directive ConcreteDirWithoutCtor inherits its constructor from ParentClass, but the latter ` + + `does not have an Angular decorator of its own. Dependency injection will not be able to resolve ` + + `the parameters of ParentClass's constructor. Either add a @Directive decorator to ParentClass, ` + + `or add an explicit constructor to ConcreteDirWithoutCtor.`, + ); expect(getDiagnosticSourceCode(diags[2])).toBe('ConcreteDirWithoutCtor'); }); }); @@ -3467,88 +3932,101 @@ function allTests(os: string) { describe('directives with a selector', () => { it('should give a compile-time error if an invalid constructor is used', () => { env.tsconfig({strictInjectionParameters: true}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive} from '@angular/core'; @Directive({selector: 'app-test'}) export class Test { constructor(private notInjectable: string) {} } - `); + `, + ); const errors = env.driveDiagnostics(); expect(errors.length).toBe(1); - expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n')) - .toContain('No suitable injection token for parameter'); + expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n')).toContain( + 'No suitable injection token for parameter', + ); }); }); describe('abstract directives', () => { it('should generate a factory function that throws', () => { env.tsconfig({strictInjectionParameters: false}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive} from '@angular/core'; @Directive() export class Test { constructor(private notInjectable: string) {} } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain('Test.ɵfac = function Test_Factory(t) { i0.ɵɵinvalidFactory()'); + expect(jsContents).toContain( + 'Test.ɵfac = function Test_Factory(t) { i0.ɵɵinvalidFactory()', + ); }); }); - it('should generate a factory function that throws, even under strictInjectionParameters', - () => { - env.tsconfig({strictInjectionParameters: true}); - env.write('test.ts', ` + it('should generate a factory function that throws, even under strictInjectionParameters', () => { + env.tsconfig({strictInjectionParameters: true}); + env.write( + 'test.ts', + ` import {Directive} from '@angular/core'; @Directive() export class Test { constructor(private notInjectable: string) {} } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain('Test.ɵfac = function Test_Factory(t) { i0.ɵɵinvalidFactory()'); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain( + 'Test.ɵfac = function Test_Factory(t) { i0.ɵɵinvalidFactory()', + ); + }); }); describe('templateUrl and styleUrls processing', () => { const testsForResource = (resource: string) => [ - // [component location, resource location, resource reference] + // [component location, resource location, resource reference] - // component and resource are in the same folder - [`a/app.ts`, `a/${resource}`, `./${resource}`], // - [`a/app.ts`, `a/${resource}`, resource], // - [`a/app.ts`, `a/${resource}`, `/a/${resource}`], + // component and resource are in the same folder + [`a/app.ts`, `a/${resource}`, `./${resource}`], // + [`a/app.ts`, `a/${resource}`, resource], // + [`a/app.ts`, `a/${resource}`, `/a/${resource}`], - // resource is one level up - [`a/app.ts`, resource, `../${resource}`], // - [`a/app.ts`, resource, `/${resource}`], + // resource is one level up + [`a/app.ts`, resource, `../${resource}`], // + [`a/app.ts`, resource, `/${resource}`], - // component and resource are in different folders - [`a/app.ts`, `b/${resource}`, `../b/${resource}`], // - [`a/app.ts`, `b/${resource}`, `/b/${resource}`], + // component and resource are in different folders + [`a/app.ts`, `b/${resource}`, `../b/${resource}`], // + [`a/app.ts`, `b/${resource}`, `/b/${resource}`], - // resource is in subfolder of component directory - [`a/app.ts`, `a/b/c/${resource}`, `./b/c/${resource}`], // - [`a/app.ts`, `a/b/c/${resource}`, `b/c/${resource}`], // - [`a/app.ts`, `a/b/c/${resource}`, `/a/b/c/${resource}`], + // resource is in subfolder of component directory + [`a/app.ts`, `a/b/c/${resource}`, `./b/c/${resource}`], // + [`a/app.ts`, `a/b/c/${resource}`, `b/c/${resource}`], // + [`a/app.ts`, `a/b/c/${resource}`, `/a/b/c/${resource}`], ]; testsForResource('style.css').forEach((test) => { const [compLoc, styleLoc, styleRef] = test; it(`should handle ${styleRef}`, () => { env.write(styleLoc, ':host { background-color: blue; }'); - env.write(compLoc, ` + env.write( + compLoc, + ` import {Component} from '@angular/core'; @Component({ @@ -3557,7 +4035,8 @@ function allTests(os: string) { template: '...', }) export class TestCmp {} - `); + `, + ); env.driveMain(); @@ -3570,7 +4049,9 @@ function allTests(os: string) { const [compLoc, templateLoc, templateRef] = test; it(`should handle ${templateRef}`, () => { env.write(templateLoc, 'Template Content'); - env.write(compLoc, ` + env.write( + compLoc, + ` import {Component} from '@angular/core'; @Component({ @@ -3578,7 +4059,8 @@ function allTests(os: string) { templateUrl: '${templateRef}' }) export class TestCmp {} - `); + `, + ); env.driveMain(); @@ -3590,7 +4072,9 @@ function allTests(os: string) { describe('former View Engine AST transform bugs', () => { it('should compile array literals behind conditionals', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -3601,14 +4085,17 @@ function allTests(os: string) { value = true; no = 'no'; } - `); + `, + ); env.driveMain(); expect(env.getContents('test.js')).toContain('i0.ɵɵpureFunction1'); }); it('should compile array literals inside function arguments', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -3622,7 +4109,8 @@ function allTests(os: string) { test = 'test'; } - `); + `, + ); env.driveMain(); expect(env.getContents('test.js')).toContain('i0.ɵɵpureFunction1'); @@ -3630,9 +4118,10 @@ function allTests(os: string) { }); describe('unwrapping ModuleWithProviders functions', () => { - it('should use a local ModuleWithProviders-annotated return type if a function is not statically analyzable', - () => { - env.write(`module.ts`, ` + it('should use a local ModuleWithProviders-annotated return type if a function is not statically analyzable', () => { + env.write( + `module.ts`, + ` import {NgModule, ModuleWithProviders} from '@angular/core'; export function notStaticallyAnalyzable(): ModuleWithProviders { @@ -3645,9 +4134,12 @@ function allTests(os: string) { @NgModule() export class SomeModule {} - `); + `, + ); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {NgModule} from '@angular/core'; import {notStaticallyAnalyzable} from './module'; @@ -3655,37 +4147,44 @@ function allTests(os: string) { imports: [notStaticallyAnalyzable()] }) export class TestModule {} - `); + `, + ); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('imports: [notStaticallyAnalyzable()]'); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('imports: [notStaticallyAnalyzable()]'); - const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents).toContain(`import * as i1 from "./module";`); - expect(dtsContents) - .toContain( - 'i0.ɵɵNgModuleDeclaration'); - }); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents).toContain(`import * as i1 from "./module";`); + expect(dtsContents).toContain( + 'i0.ɵɵNgModuleDeclaration', + ); + }); - it('should extract the generic type and include it in the module\'s declaration', () => { - env.write(`test.ts`, ` + it("should extract the generic type and include it in the module's declaration", () => { + env.write( + `test.ts`, + ` import {NgModule} from '@angular/core'; import {RouterModule} from 'router'; @NgModule({imports: [RouterModule.forRoot()]}) export class TestModule {} - `); + `, + ); - env.write('node_modules/router/index.d.ts', ` + env.write( + 'node_modules/router/index.d.ts', + ` import {ModuleWithProviders, ɵɵNgModuleDeclaration} from '@angular/core'; declare class RouterModule { static forRoot(): ModuleWithProviders; static ɵmod: ɵɵNgModuleDeclaration; } - `); + `, + ); env.driveMain(); @@ -3694,46 +4193,57 @@ function allTests(os: string) { const dtsContents = env.getContents('test.d.ts'); expect(dtsContents).toContain(`import * as i1 from "router";`); - expect(dtsContents) - .toContain( - 'i0.ɵɵNgModuleDeclaration'); + expect(dtsContents).toContain( + 'i0.ɵɵNgModuleDeclaration', + ); }); it('should throw if ModuleWithProviders is missing its generic type argument', () => { - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {NgModule} from '@angular/core'; import {RouterModule} from 'router'; @NgModule({imports: [RouterModule.forRoot()]}) export class TestModule {} - `); + `, + ); - env.write('node_modules/router/index.d.ts', ` + env.write( + 'node_modules/router/index.d.ts', + ` import {ModuleWithProviders, ɵɵNgModuleDeclaration} from '@angular/core'; declare class RouterModule { static forRoot(): ModuleWithProviders; static ɵmod: ɵɵNgModuleDeclaration; } - `); + `, + ); const errors = env.driveDiagnostics(); - expect(trim(errors[0].messageText as string)) - .toContain( - `RouterModule.forRoot returns a ModuleWithProviders type without a generic type argument. ` + - `Please add a generic type argument to the ModuleWithProviders type. If this ` + - `occurrence is in library code you don't control, please contact the library authors.`); + expect(trim(errors[0].messageText as string)).toContain( + `RouterModule.forRoot returns a ModuleWithProviders type without a generic type argument. ` + + `Please add a generic type argument to the ModuleWithProviders type. If this ` + + `occurrence is in library code you don't control, please contact the library authors.`, + ); }); it('should extract the generic type if it is provided as qualified type name', () => { - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {NgModule} from '@angular/core'; import {RouterModule} from 'router'; @NgModule({imports: [RouterModule.forRoot()]}) export class TestModule {} - `); + `, + ); - env.write('node_modules/router/index.d.ts', ` + env.write( + 'node_modules/router/index.d.ts', + ` import {ModuleWithProviders} from '@angular/core'; import * as internal from './internal'; export {InternalRouterModule} from './internal'; @@ -3742,14 +4252,18 @@ function allTests(os: string) { static forRoot(): ModuleWithProviders; } - `); + `, + ); - env.write('node_modules/router/internal.d.ts', ` + env.write( + 'node_modules/router/internal.d.ts', + ` import {ɵɵNgModuleDeclaration} from '@angular/core'; export declare class InternalRouterModule { static ɵmod: ɵɵNgModuleDeclaration; } - `); + `, + ); env.driveMain(); @@ -3758,57 +4272,69 @@ function allTests(os: string) { const dtsContents = env.getContents('test.d.ts'); expect(dtsContents).toContain(`import * as i1 from "router";`); - expect(dtsContents) - .toContain( - 'i0.ɵɵNgModuleDeclaration'); + expect(dtsContents).toContain( + 'i0.ɵɵNgModuleDeclaration', + ); }); - it('should extract the generic type if it is provided as qualified type name from another package', - () => { - env.write(`test.ts`, ` + it('should extract the generic type if it is provided as qualified type name from another package', () => { + env.write( + `test.ts`, + ` import {NgModule} from '@angular/core'; import {RouterModule} from 'router'; @NgModule({imports: [RouterModule.forRoot()]}) - export class TestModule {}`); + export class TestModule {}`, + ); - env.write('node_modules/router/index.d.ts', ` + env.write( + 'node_modules/router/index.d.ts', + ` import {ModuleWithProviders} from '@angular/core'; import * as router2 from 'router2'; declare export class RouterModule { static forRoot(): ModuleWithProviders; - }`); + }`, + ); - env.write('node_modules/router2/index.d.ts', ` + env.write( + 'node_modules/router2/index.d.ts', + ` import {ɵɵNgModuleDeclaration} from '@angular/core'; export declare class Router2Module { static ɵmod: ɵɵNgModuleDeclaration; - }`); + }`, + ); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('imports: [RouterModule.forRoot()]'); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('imports: [RouterModule.forRoot()]'); - const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents).toContain(`import * as i1 from "router2";`); - expect(dtsContents) - .toContain( - 'i0.ɵɵNgModuleDeclaration'); - }); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents).toContain(`import * as i1 from "router2";`); + expect(dtsContents).toContain( + 'i0.ɵɵNgModuleDeclaration', + ); + }); - it('should not reference a constant with a ModuleWithProviders value in module def imports', - () => { - env.write('dep.d.ts', ` + it('should not reference a constant with a ModuleWithProviders value in module def imports', () => { + env.write( + 'dep.d.ts', + ` import {ModuleWithProviders, ɵɵNgModuleDeclaration as ɵɵNgModuleDeclaration} from '@angular/core'; export declare class DepModule { static forRoot(arg1: any, arg2: any): ModuleWithProviders; static ɵmod: ɵɵNgModuleDeclaration; } - `); - env.write('test.ts', ` + `, + ); + env.write( + 'test.ts', + ` import {NgModule, ModuleWithProviders} from '@angular/core'; import {DepModule} from './dep'; @@ -3821,24 +4347,29 @@ function allTests(os: string) { imports: [mwp], }) export class Module {} - `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('imports: [i1.DepModule]'); - }); + `, + ); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('imports: [i1.DepModule]'); + }); }); - it('should unwrap a ModuleWithProviders-like function if a matching literal type is provided for it', - () => { - env.write(`test.ts`, ` + it('should unwrap a ModuleWithProviders-like function if a matching literal type is provided for it', () => { + env.write( + `test.ts`, + ` import {NgModule} from '@angular/core'; import {RouterModule} from 'router'; @NgModule({imports: [RouterModule.forRoot()]}) export class TestModule {} - `); + `, + ); - env.write('node_modules/router/index.d.ts', ` + env.write( + 'node_modules/router/index.d.ts', + ` import {ModuleWithProviders, ɵɵNgModuleDeclaration} from '@angular/core'; export interface MyType extends ModuleWithProviders {} @@ -3847,31 +4378,36 @@ function allTests(os: string) { static forRoot(): (MyType)&{ngModule: typeof RouterModule}; static ɵmod: ɵɵNgModuleDeclaration; } - `); + `, + ); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('imports: [RouterModule.forRoot()]'); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('imports: [RouterModule.forRoot()]'); - const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents).toContain(`import * as i1 from "router";`); - expect(dtsContents) - .toContain( - 'i0.ɵɵNgModuleDeclaration'); - }); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents).toContain(`import * as i1 from "router";`); + expect(dtsContents).toContain( + 'i0.ɵɵNgModuleDeclaration', + ); + }); - it('should unwrap a namespace imported ModuleWithProviders function if a generic type is provided for it', - () => { - env.write(`test.ts`, ` + it('should unwrap a namespace imported ModuleWithProviders function if a generic type is provided for it', () => { + env.write( + `test.ts`, + ` import {NgModule} from '@angular/core'; import {RouterModule} from 'router'; @NgModule({imports: [RouterModule.forRoot()]}) export class TestModule {} - `); + `, + ); - env.write('node_modules/router/index.d.ts', ` + env.write( + 'node_modules/router/index.d.ts', + ` import * as core from '@angular/core'; import {RouterModule} from 'router'; @@ -3879,22 +4415,25 @@ function allTests(os: string) { static forRoot(): core.ModuleWithProviders; static ɵmod: ɵɵNgModuleDeclaration; } - `); + `, + ); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('imports: [RouterModule.forRoot()]'); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('imports: [RouterModule.forRoot()]'); - const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents).toContain(`import * as i1 from "router";`); - expect(dtsContents) - .toContain( - 'i0.ɵɵNgModuleDeclaration'); - }); + const dtsContents = env.getContents('test.d.ts'); + expect(dtsContents).toContain(`import * as i1 from "router";`); + expect(dtsContents).toContain( + 'i0.ɵɵNgModuleDeclaration', + ); + }); it('should inject special types according to the metadata', () => { - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import { Attribute, ChangeDetectorRef, @@ -3921,17 +4460,20 @@ function allTests(os: string) { vcr: ViewContainerRef, ) {} } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - `FooCmp.ɵfac = function FooCmp_Factory(t) { return new (t || FooCmp)(i0.ɵɵinjectAttribute("test"), i0.ɵɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵɵdirectiveInject(i0.ElementRef), i0.ɵɵdirectiveInject(i0.Injector), i0.ɵɵdirectiveInject(i0.Renderer2), i0.ɵɵdirectiveInject(i0.TemplateRef), i0.ɵɵdirectiveInject(i0.ViewContainerRef)); }`); + expect(jsContents).toContain( + `FooCmp.ɵfac = function FooCmp_Factory(t) { return new (t || FooCmp)(i0.ɵɵinjectAttribute("test"), i0.ɵɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵɵdirectiveInject(i0.ElementRef), i0.ɵɵdirectiveInject(i0.Injector), i0.ɵɵdirectiveInject(i0.Renderer2), i0.ɵɵdirectiveInject(i0.TemplateRef), i0.ɵɵdirectiveInject(i0.ViewContainerRef)); }`, + ); }); it('should include constructor dependency metadata for directives/components/pipes', () => { - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Attribute, Component, Directive, Pipe, Self, SkipSelf, Host, Optional} from '@angular/core'; export class MyService {} @@ -3972,28 +4514,34 @@ function allTests(os: string) { export class MyPipe { constructor(@Host() withHost: MyService) {} } - `); + `, + ); env.driveMain(); const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents) - .toContain( - 'static ɵfac: i0.ɵɵFactoryDeclaration'); + expect(dtsContents).toContain( + 'static ɵfac: i0.ɵɵFactoryDeclaration', + ); expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDeclaration`); expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDeclaration`); - expect(dtsContents) - .toContain(`static ɵfac: i0.ɵɵFactoryDeclaration`); - expect(dtsContents) - .toContain(`static ɵfac: i0.ɵɵFactoryDeclaration`); - expect(dtsContents) - .toContain(`static ɵfac: i0.ɵɵFactoryDeclaration`); + expect(dtsContents).toContain( + `static ɵfac: i0.ɵɵFactoryDeclaration`, + ); + expect(dtsContents).toContain( + `static ɵfac: i0.ɵɵFactoryDeclaration`, + ); + expect(dtsContents).toContain( + `static ɵfac: i0.ɵɵFactoryDeclaration`, + ); }); it('should include constructor dependency metadata for @Injectable', () => { - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Injectable, Self, Host} from '@angular/core'; export class MyService {} @@ -4032,29 +4580,36 @@ function allTests(os: string) { export class InjUseValue { constructor(@Self() service: MyService) {} } - `); + `, + ); env.driveMain(); const dtsContents = env.getContents('test.d.ts'); expect(dtsContents).toContain(`static ɵfac: i0.ɵɵFactoryDeclaration`); - expect(dtsContents) - .toContain(`static ɵfac: i0.ɵɵFactoryDeclaration`); - expect(dtsContents) - .toContain(`static ɵfac: i0.ɵɵFactoryDeclaration`); - expect(dtsContents) - .toContain( - `static ɵfac: i0.ɵɵFactoryDeclaration`); - expect(dtsContents) - .toContain(`static ɵfac: i0.ɵɵFactoryDeclaration`); - expect(dtsContents) - .toContain( - `static ɵfac: i0.ɵɵFactoryDeclaration`); - expect(dtsContents) - .toContain(`static ɵfac: i0.ɵɵFactoryDeclaration`); + expect(dtsContents).toContain( + `static ɵfac: i0.ɵɵFactoryDeclaration`, + ); + expect(dtsContents).toContain( + `static ɵfac: i0.ɵɵFactoryDeclaration`, + ); + expect(dtsContents).toContain( + `static ɵfac: i0.ɵɵFactoryDeclaration`, + ); + expect(dtsContents).toContain( + `static ɵfac: i0.ɵɵFactoryDeclaration`, + ); + expect(dtsContents).toContain( + `static ɵfac: i0.ɵɵFactoryDeclaration`, + ); + expect(dtsContents).toContain( + `static ɵfac: i0.ɵɵFactoryDeclaration`, + ); }); it('should include ng-content selectors in the metadata', () => { - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Component} from '@angular/core'; @Component({ @@ -4063,17 +4618,20 @@ function allTests(os: string) { }) export class TestCmp { } - `); + `, + ); env.driveMain(); const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents) - .toContain( - 'static ɵcmp: i0.ɵɵComponentDeclaration'); + expect(dtsContents).toContain( + 'static ɵcmp: i0.ɵɵComponentDeclaration', + ); }); it('should generate queries for components', () => { - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Component, ContentChild, ContentChildren, TemplateRef, ViewChild} from '@angular/core'; @Component({ @@ -4090,7 +4648,8 @@ function allTests(os: string) { get aview(): any { return null; } @ViewChild('accessor') set aview(value: any) {} } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -4105,7 +4664,9 @@ function allTests(os: string) { }); it('should generate queries for directives', () => { - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Directive, ContentChild, ContentChildren, TemplateRef, ViewChild} from '@angular/core'; import * as core from '@angular/core'; @@ -4122,7 +4683,8 @@ function allTests(os: string) { get aview(): any { return null; } @ViewChild('accessor') set aview(value: any) {} } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -4141,7 +4703,9 @@ function allTests(os: string) { }); it('should handle queries that use forwardRef', () => { - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Component, ContentChild, TemplateRef, ViewContainerRef, forwardRef} from '@angular/core'; @Component({ @@ -4155,7 +4719,8 @@ function allTests(os: string) { @ContentChild((forwardRef((function() { return 'parens'; }) as any))) childInParens: any; } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -4169,7 +4734,9 @@ function allTests(os: string) { }); it('should handle queries that use an InjectionToken', () => { - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Component, ContentChild, InjectionToken, ViewChild} from '@angular/core'; const TOKEN = new InjectionToken('token'); @@ -4182,7 +4749,8 @@ function allTests(os: string) { @ViewChild(TOKEN) viewChild: any; @ContentChild(TOKEN) contentChild: any; } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -4193,7 +4761,9 @@ function allTests(os: string) { }); it('should compile expressions that write keys', () => { - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Component, ContentChild, TemplateRef, ViewContainerRef, forwardRef} from '@angular/core'; @Component({ @@ -4204,14 +4774,17 @@ function allTests(os: string) { test: any; key: string; } - `); + `, + ); env.driveMain(); expect(env.getContents('test.js')).toContain('test[key] = $event'); }); it('should generate host listeners for components', () => { - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Component, HostListener} from '@angular/core'; @Component({ @@ -4228,7 +4801,8 @@ function allTests(os: string) { @HostListener('window:scroll') onWindowScroll(event: any): void {} } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -4243,7 +4817,9 @@ function allTests(os: string) { }); it('should throw in case unknown global target is provided', () => { - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Component, HostListener} from '@angular/core'; @Component({ @@ -4254,15 +4830,18 @@ function allTests(os: string) { @HostListener('UnknownTarget:click') onClick(event: any): void {} } - `); + `, + ); const errors = env.driveDiagnostics(); - expect(trim(errors[0].messageText as string)) - .toContain( - `Unexpected global target 'UnknownTarget' defined for 'click' event. Supported list of global targets: window,document,body.`); + expect(trim(errors[0].messageText as string)).toContain( + `Unexpected global target 'UnknownTarget' defined for 'click' event. Supported list of global targets: window,document,body.`, + ); }); it('should provide error location for invalid host properties', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -4273,7 +4852,8 @@ function allTests(os: string) { } }) class FooCmp {} - `); + `, + ); const errors = env.driveDiagnostics(); expect(getDiagnosticSourceCode(errors[0])).toBe(`{ @@ -4283,7 +4863,9 @@ function allTests(os: string) { }); it('should throw in case pipes are used in host listeners', () => { - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Component} from '@angular/core'; @Component({ @@ -4294,14 +4876,18 @@ function allTests(os: string) { } }) class FooCmp {} - `); + `, + ); const errors = env.driveDiagnostics(); - expect(trim(errors[0].messageText as string)) - .toContain('Cannot have a pipe in an action expression'); + expect(trim(errors[0].messageText as string)).toContain( + 'Cannot have a pipe in an action expression', + ); }); it('should throw in case pipes are used in host bindings (defined as `value | pipe`)', () => { - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Component} from '@angular/core'; @Component({ @@ -4312,14 +4898,18 @@ function allTests(os: string) { } }) class FooCmp {} - `); + `, + ); const errors = env.driveDiagnostics(); - expect(trim(errors[0].messageText as string)) - .toContain('Host binding expression cannot contain pipes'); + expect(trim(errors[0].messageText as string)).toContain( + 'Host binding expression cannot contain pipes', + ); }); it('should generate host bindings for directives', () => { - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Component, HostBinding, HostListener, TemplateRef} from '@angular/core'; @Component({ @@ -4341,7 +4931,8 @@ function allTests(os: string) { @HostListener('change', ['arg1', 'arg2', 'arg3']) onChange(event: any, arg: any): void {} } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -4363,7 +4954,9 @@ function allTests(os: string) { // https://github.com/angular/angular/issues/46936 it('should support bindings with Object builtin names', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -4371,16 +4964,20 @@ function allTests(os: string) { template: '
', }) export class TestCmp {} - `); + `, + ); const errors = env.driveDiagnostics(); expect(errors.length).toBe(1); - expect(errors[0].messageText) - .toContain(`Can't bind to 'valueOf' since it isn't a known property of 'div'.`); + expect(errors[0].messageText).toContain( + `Can't bind to 'valueOf' since it isn't a known property of 'div'.`, + ); }); it('should handle $any used inside a listener', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -4388,19 +4985,25 @@ function allTests(os: string) { template: '
', }) export class TestCmp {} - `); + `, + ); env.driveMain(); - expect(env.getContents('test.js')) - .toContain( - `ɵɵlistener("click", function TestCmp_Template_div_click_0_listener() { return 123; });`); + expect(env.getContents('test.js')).toContain( + `ɵɵlistener("click", function TestCmp_Template_div_click_0_listener() { return 123; });`, + ); }); it('should accept dynamic host attribute bindings', () => { - env.write('other.d.ts', ` + env.write( + 'other.d.ts', + ` export declare const foo: any; - `); - env.write('test.ts', ` + `, + ); + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; import {foo} from './other'; @@ -4414,14 +5017,17 @@ function allTests(os: string) { }, }) export class TestCmp {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); expect(jsContents).toContain('hostAttrs: ["test", test]'); }); it('should accept enum values as host bindings', () => { - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Component, HostBinding, HostListener, TemplateRef} from '@angular/core'; enum HostBindings { @@ -4438,14 +5044,17 @@ function allTests(os: string) { class FooCmp { foo = 'test'; } - `); + `, + ); env.driveMain(); expect(env.getContents('test.js')).toContain('i0.ɵɵattribute("hello", ctx.foo)'); }); it('should generate host listeners for directives within hostBindings section', () => { - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Directive, HostListener} from '@angular/core'; @Directive({ @@ -4455,7 +5064,8 @@ function allTests(os: string) { @HostListener('change', ['$event', 'arg']) onChange(event: any, arg: any): void {} } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -4470,8 +5080,10 @@ function allTests(os: string) { }); it('should use proper default value for preserveWhitespaces config param', () => { - env.tsconfig(); // default is `false` - env.write(`test.ts`, ` + env.tsconfig(); // default is `false` + env.write( + `test.ts`, + ` import {Component} from '@angular/core'; @Component({ selector: 'test', @@ -4483,7 +5095,8 @@ function allTests(os: string) { \` }) class FooCmp {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); expect(jsContents).toContain('text(1, " Template with whitespaces ");'); @@ -4491,7 +5104,9 @@ function allTests(os: string) { it('should take preserveWhitespaces config option into account', () => { env.tsconfig({preserveWhitespaces: true}); - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Component} from '@angular/core'; @Component({ selector: 'test', @@ -4502,16 +5117,20 @@ function allTests(os: string) { \` }) class FooCmp {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain('text(2, "\\n Template with whitespaces\\n ");'); + expect(jsContents).toContain( + 'text(2, "\\n Template with whitespaces\\n ");', + ); }); - it('@Component\'s preserveWhitespaces should override the one defined in config', () => { + it("@Component's preserveWhitespaces should override the one defined in config", () => { env.tsconfig({preserveWhitespaces: true}); - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Component} from '@angular/core'; @Component({ selector: 'test', @@ -4523,22 +5142,26 @@ function allTests(os: string) { \` }) class FooCmp {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); expect(jsContents).toContain('text(1, " Template with whitespaces ");'); }); it('should use proper default value for i18nUseExternalIds config param', () => { - env.tsconfig(); // default is `true` - env.write(`test.ts`, ` + env.tsconfig(); // default is `true` + env.write( + `test.ts`, + ` import {Component} from '@angular/core'; @Component({ selector: 'test', template: '
Some text
' }) class FooCmp {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); expect(jsContents).toContain('MSG_EXTERNAL_8321000940098097247$$TEST_TS_0'); @@ -4546,14 +5169,17 @@ function allTests(os: string) { it('should take i18nUseExternalIds config option into account', () => { env.tsconfig({i18nUseExternalIds: false}); - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Component} from '@angular/core'; @Component({ selector: 'test', template: '
Some text
' }) class FooCmp {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); expect(jsContents).not.toContain('MSG_EXTERNAL_'); @@ -4561,57 +5187,66 @@ function allTests(os: string) { it('should render legacy ids when `enableI18nLegacyMessageIdFormat` is not false', () => { env.tsconfig({}); - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Component} from '@angular/core'; @Component({ selector: 'test', template: '
Some text
' }) - class FooCmp {}`); + class FooCmp {}`, + ); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - '`:\u241F5dbba0a3da8dff890e20cf76eb075d58900fbcd3\u241F8321000940098097247:Some text`'); + expect(jsContents).toContain( + '`:\u241F5dbba0a3da8dff890e20cf76eb075d58900fbcd3\u241F8321000940098097247:Some text`', + ); }); - it('should render custom id and legacy ids if `enableI18nLegacyMessageIdFormat` is not false', - () => { - env.tsconfig({i18nFormatIn: 'xlf'}); - env.write(`test.ts`, ` + it('should render custom id and legacy ids if `enableI18nLegacyMessageIdFormat` is not false', () => { + env.tsconfig({i18nFormatIn: 'xlf'}); + env.write( + `test.ts`, + ` import {Component} from '@angular/core'; @Component({ selector: 'test', template: '
Some text
' }) - class FooCmp {}`); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - ':@@custom\u241F5dbba0a3da8dff890e20cf76eb075d58900fbcd3\u241F8321000940098097247:Some text'); - }); - - it('should not render legacy ids when `enableI18nLegacyMessageIdFormat` is set to false', - () => { - env.tsconfig({enableI18nLegacyMessageIdFormat: false, i18nInFormat: 'xmb'}); - env.write(`test.ts`, ` + class FooCmp {}`, + ); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain( + ':@@custom\u241F5dbba0a3da8dff890e20cf76eb075d58900fbcd3\u241F8321000940098097247:Some text', + ); + }); + + it('should not render legacy ids when `enableI18nLegacyMessageIdFormat` is set to false', () => { + env.tsconfig({enableI18nLegacyMessageIdFormat: false, i18nInFormat: 'xmb'}); + env.write( + `test.ts`, + ` import {Component} from '@angular/core'; @Component({ selector: 'test', template: '
Some text
' }) - class FooCmp {}`); - env.driveMain(); - const jsContents = env.getContents('test.js'); - // Note that the colon would only be there if there is an id attached to the - // string. - expect(jsContents).not.toContain(':Some text'); - }); + class FooCmp {}`, + ); + env.driveMain(); + const jsContents = env.getContents('test.js'); + // Note that the colon would only be there if there is an id attached to the + // string. + expect(jsContents).not.toContain(':Some text'); + }); it('should also render legacy ids for ICUs when normal messages are using legacy ids', () => { env.tsconfig({i18nInFormat: 'xliff'}); - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Component} from '@angular/core'; @Component({ selector: 'test', @@ -4619,19 +5254,22 @@ function allTests(os: string) { }) class FooCmp { age = 1; - }`); + }`, + ); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - ':\u241F720ba589d043a0497ac721ff972f41db0c919efb\u241F3221232817843005870:{VAR_PLURAL, plural, 10 {ten} other {other}}'); - expect(jsContents) - .toContain( - ':@@custom\u241Fdcb6170595f5d548a3d00937e87d11858f51ad04\u241F7419139165339437596:Some text'); + expect(jsContents).toContain( + ':\u241F720ba589d043a0497ac721ff972f41db0c919efb\u241F3221232817843005870:{VAR_PLURAL, plural, 10 {ten} other {other}}', + ); + expect(jsContents).toContain( + ':@@custom\u241Fdcb6170595f5d548a3d00937e87d11858f51ad04\u241F7419139165339437596:Some text', + ); }); - it('@Component\'s `interpolation` should override default interpolation config', () => { - env.write(`test.ts`, ` + it("@Component's `interpolation` should override default interpolation config", () => { + env.write( + `test.ts`, + ` import {Component} from '@angular/core'; @Component({ selector: 'cmp-with-custom-interpolation-a', @@ -4641,7 +5279,8 @@ function allTests(os: string) { class ComponentWithCustomInterpolationA { text = 'Custom Interpolation A'; } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -4649,7 +5288,9 @@ function allTests(os: string) { }); it('should handle `encapsulation` field', () => { - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Component, ViewEncapsulation} from '@angular/core'; @Component({ selector: 'comp-a', @@ -4657,7 +5298,8 @@ function allTests(os: string) { encapsulation: ViewEncapsulation.None }) class CompA {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -4665,7 +5307,9 @@ function allTests(os: string) { }); it('should throw if `encapsulation` contains invalid value', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ selector: 'comp-a', @@ -4674,17 +5318,21 @@ function allTests(os: string) { encapsulation: 'invalid-value' }) class CompA {} - `); + `, + ); const errors = env.driveDiagnostics(); expect(errors.length).toBe(1); const messageText = ts.flattenDiagnosticMessageText(errors[0].messageText, '\n'); - expect(messageText) - .toContain('encapsulation must be a member of ViewEncapsulation enum from @angular/core'); - expect(messageText).toContain('Value is of type \'string\'.'); + expect(messageText).toContain( + 'encapsulation must be a member of ViewEncapsulation enum from @angular/core', + ); + expect(messageText).toContain("Value is of type 'string'."); }); it('should handle `changeDetection` field', () => { - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Component, ChangeDetectionStrategy} from '@angular/core'; @Component({ selector: 'comp-a', @@ -4692,7 +5340,8 @@ function allTests(os: string) { changeDetection: ChangeDetectionStrategy.OnPush }) class CompA {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -4700,7 +5349,9 @@ function allTests(os: string) { }); it('should throw if `changeDetection` contains invalid value', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ selector: 'comp-a', @@ -4709,32 +5360,38 @@ function allTests(os: string) { changeDetection: 'invalid-value' }) class CompA {} - `); + `, + ); const errors = env.driveDiagnostics(); expect(errors.length).toBe(1); const messageText = ts.flattenDiagnosticMessageText(errors[0].messageText, '\n'); - expect(messageText) - .toContain( - 'changeDetection must be a member of ChangeDetectionStrategy enum from @angular/core'); - expect(messageText).toContain('Value is of type \'string\'.'); + expect(messageText).toContain( + 'changeDetection must be a member of ChangeDetectionStrategy enum from @angular/core', + ); + expect(messageText).toContain("Value is of type 'string'."); }); it('should ignore empty bindings', () => { - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Component} from '@angular/core'; @Component({ selector: 'test', template: '
' }) class FooCmp {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); expect(jsContents).not.toContain('i0.ɵɵproperty'); }); it('should correctly recognize local symbols', () => { - env.write('module.ts', ` + env.write( + 'module.ts', + ` import {NgModule} from '@angular/core'; import {Dir, Comp} from './test'; @@ -4743,8 +5400,11 @@ function allTests(os: string) { exports: [Dir, Comp], }) class Module {} - `); - env.write(`test.ts`, ` + `, + ); + env.write( + `test.ts`, + ` import {Component, Directive} from '@angular/core'; @Directive({ @@ -4757,7 +5417,8 @@ function allTests(os: string) { template: '
Test
', }) export class Comp {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -4765,7 +5426,9 @@ function allTests(os: string) { }); it('should generate exportAs declarations', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive} from '@angular/core'; @Directive({ @@ -4773,7 +5436,8 @@ function allTests(os: string) { exportAs: 'foo', }) class Dir {} - `); + `, + ); env.driveMain(); @@ -4782,7 +5446,9 @@ function allTests(os: string) { }); it('should generate multiple exportAs declarations', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive} from '@angular/core'; @Directive({ @@ -4790,7 +5456,8 @@ function allTests(os: string) { exportAs: 'foo, bar', }) class Dir {} - `); + `, + ); env.driveMain(); @@ -4799,7 +5466,9 @@ function allTests(os: string) { }); it('should compile a banana-in-a-box inside of a template', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -4807,13 +5476,16 @@ function allTests(os: string) { selector: 'test' }) class TestCmp {} - `); + `, + ); env.driveMain(); }); it('generates inherited factory definitions', () => { - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Injectable} from '@angular/core'; class Dep {} @@ -4832,23 +5504,27 @@ function allTests(os: string) { super(null!); } } - `); - + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain('function Base_Factory(t) { return new (t || Base)(i0.ɵɵinject(Dep)); }'); - expect(jsContents) - .toContain( - '(() => { let ɵChild_BaseFactory; return function Child_Factory(t) { return (ɵChild_BaseFactory || (ɵChild_BaseFactory = i0.ɵɵgetInheritedFactory(Child)))(t || Child); }; })();'); - expect(jsContents) - .toContain('function GrandChild_Factory(t) { return new (t || GrandChild)(); }'); + expect(jsContents).toContain( + 'function Base_Factory(t) { return new (t || Base)(i0.ɵɵinject(Dep)); }', + ); + expect(jsContents).toContain( + '(() => { let ɵChild_BaseFactory; return function Child_Factory(t) { return (ɵChild_BaseFactory || (ɵChild_BaseFactory = i0.ɵɵgetInheritedFactory(Child)))(t || Child); }; })();', + ); + expect(jsContents).toContain( + 'function GrandChild_Factory(t) { return new (t || GrandChild)(); }', + ); }); it('generates base factories for directives', () => { - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Directive} from '@angular/core'; @Directive({ @@ -4861,19 +5537,20 @@ function allTests(os: string) { }) class Dir extends Base { } - `); - + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents) - .toContain( - '/*@__PURE__*/ (() => { let ɵDir_BaseFactory; return function Dir_Factory(t) { return (ɵDir_BaseFactory || (ɵDir_BaseFactory = i0.ɵɵgetInheritedFactory(Dir)))(t || Dir); }; })();'); + expect(jsContents).toContain( + '/*@__PURE__*/ (() => { let ɵDir_BaseFactory; return function Dir_Factory(t) { return (ɵDir_BaseFactory || (ɵDir_BaseFactory = i0.ɵɵgetInheritedFactory(Dir)))(t || Dir); }; })();', + ); }); - it('should wrap "directives" in component metadata in a closure when forward references are present', - () => { - env.write('test.ts', ` + it('should wrap "directives" in component metadata in a closure when forward references are present', () => { + env.write( + 'test.ts', + ` import {Component, NgModule} from '@angular/core'; @Component({ @@ -4892,32 +5569,38 @@ function allTests(os: string) { declarations: [CmpA, CmpB], }) class Module {} - `); + `, + ); - env.driveMain(); + env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('dependencies: () => [CmpB]'); - }); + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('dependencies: () => [CmpB]'); + }); it('should wrap setClassMetadata in an iife with ngDevMode guard', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Injectable} from '@angular/core'; @Injectable({providedIn: 'root'}) export class Service {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js').replace(/\s+/g, ' '); - expect(jsContents) - .toContain( - `(() => { (typeof ngDevMode === "undefined" || ngDevMode) && ` + - `i0.ɵsetClassMetadata(Service, [{ type: Injectable, args: [{ providedIn: 'root' }] }], null, null); })();`); + expect(jsContents).toContain( + `(() => { (typeof ngDevMode === "undefined" || ngDevMode) && ` + + `i0.ɵsetClassMetadata(Service, [{ type: Injectable, args: [{ providedIn: 'root' }] }], null, null); })();`, + ); }); it('should not include `schemas` in component and module defs', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, NgModule, NO_ERRORS_SCHEMA} from '@angular/core'; @Component({ selector: 'comp', @@ -4929,11 +5612,13 @@ function allTests(os: string) { schemas: [NO_ERRORS_SCHEMA], }) class MyModule {} - `); + `, + ); env.driveMain(); const jsContents = trim(env.getContents('test.js')); - expect(jsContents).toContain(trim(` + expect(jsContents).toContain( + trim(` MyComp.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MyComp, selectors: [["comp"]], @@ -4946,14 +5631,17 @@ function allTests(os: string) { }, encapsulation: 2 }); - `)); - expect(jsContents) - .toContain( - trim('MyModule.ɵmod = /*@__PURE__*/ i0.ɵɵdefineNgModule({ type: MyModule });')); + `), + ); + expect(jsContents).toContain( + trim('MyModule.ɵmod = /*@__PURE__*/ i0.ɵɵdefineNgModule({ type: MyModule });'), + ); }); it('should emit setNgModuleScope calls for NgModules by default', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, Injectable, NgModule, Pipe} from '@angular/core'; @Component({selector: 'cmp', template: 'I am a component!'}) class TestComponent {} @@ -4961,7 +5649,8 @@ function allTests(os: string) { @Injectable() class TestInjectable {} @NgModule({declarations: [TestComponent, TestDirective]}) class TestNgModule {} @Pipe({name: 'pipe'}) class TestPipe {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -4972,7 +5661,9 @@ function allTests(os: string) { env.tsconfig({ 'supportJitMode': true, }); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, Injectable, NgModule, Pipe} from '@angular/core'; @Component({selector: 'cmp', template: 'I am a component!'}) class TestComponent {} @@ -4980,7 +5671,8 @@ function allTests(os: string) { @Injectable() class TestInjectable {} @NgModule({declarations: [TestComponent, TestDirective]}) class TestNgModule {} @Pipe({name: 'pipe'}) class TestPipe {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -4991,7 +5683,9 @@ function allTests(os: string) { env.tsconfig({ 'supportJitMode': false, }); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, Injectable, NgModule, Pipe} from '@angular/core'; @Component({selector: 'cmp', template: 'I am a component!'}) class TestComponent {} @@ -4999,7 +5693,8 @@ function allTests(os: string) { @Injectable() class TestInjectable {} @NgModule({declarations: [TestComponent, TestDirective]}) class TestNgModule {} @Pipe({name: 'pipe'}) class TestPipe {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -5007,7 +5702,9 @@ function allTests(os: string) { }); it('should emit setClassMetadata calls for all types by default', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, Injectable, NgModule, Pipe} from '@angular/core'; @Component({selector: 'cmp', template: 'I am a component!'}) class TestComponent {} @@ -5015,7 +5712,8 @@ function allTests(os: string) { @Injectable() class TestInjectable {} @NgModule({declarations: [TestComponent, TestDirective]}) class TestNgModule {} @Pipe({name: 'pipe'}) class TestPipe {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -5030,7 +5728,9 @@ function allTests(os: string) { env.tsconfig({ 'supportTestBed': true, }); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, Injectable, NgModule, Pipe} from '@angular/core'; @Component({selector: 'cmp', template: 'I am a component!'}) class TestComponent {} @@ -5038,7 +5738,8 @@ function allTests(os: string) { @Injectable() class TestInjectable {} @NgModule({declarations: [TestComponent, TestDirective]}) class TestNgModule {} @Pipe({name: 'pipe'}) class TestPipe {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -5053,7 +5754,9 @@ function allTests(os: string) { env.tsconfig({ 'supportTestBed': false, }); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, Injectable, NgModule, Pipe} from '@angular/core'; @Component({selector: 'cmp', template: 'I am a component!'}) class TestComponent {} @@ -5061,7 +5764,8 @@ function allTests(os: string) { @Injectable() class TestInjectable {} @NgModule({declarations: [TestComponent, TestDirective]}) class TestNgModule {} @Pipe({name: 'pipe'}) class TestPipe {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -5069,11 +5773,16 @@ function allTests(os: string) { }); it('should use imported types in setClassMetadata if they can be represented as values', () => { - env.write(`types.ts`, ` + env.write( + `types.ts`, + ` export class MyTypeA {} export class MyTypeB {} - `); - env.write(`test.ts`, ` + `, + ); + env.write( + `test.ts`, + ` import {Component, Inject, Injectable} from '@angular/core'; import {MyTypeA, MyTypeB} from './types'; @@ -5089,7 +5798,8 @@ function allTests(os: string) { export class SomeComp { constructor(@Inject('arg-token') arg: MyTypeB) {} } - `); + `, + ); env.driveMain(); const jsContents = trim(env.getContents('test.js')); @@ -5098,13 +5808,17 @@ function allTests(os: string) { expect(jsContents).toMatch(setClassMetadataRegExp('type: i1\\.MyTypeB')); }); - it('should use imported types in setClassMetadata if they can be represented as values and imported as `* as foo`', - () => { - env.write(`types.ts`, ` + it('should use imported types in setClassMetadata if they can be represented as values and imported as `* as foo`', () => { + env.write( + `types.ts`, + ` export class MyTypeA {} export class MyTypeB {} - `); - env.write(`test.ts`, ` + `, + ); + env.write( + `test.ts`, + ` import {Component, Inject, Injectable} from '@angular/core'; import * as types from './types'; @@ -5120,21 +5834,27 @@ function allTests(os: string) { export class SomeComp { constructor(@Inject('arg-token') arg: types.MyTypeB) {} } - `); + `, + ); - env.driveMain(); - const jsContents = trim(env.getContents('test.js')); - expect(jsContents).toContain(`import * as i1 from "./types";`); - expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.MyTypeA')); - expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.MyTypeB')); - }); + env.driveMain(); + const jsContents = trim(env.getContents('test.js')); + expect(jsContents).toContain(`import * as i1 from "./types";`); + expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.MyTypeA')); + expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.MyTypeB')); + }); it('should use default-imported types if they can be represented as values', () => { - env.write(`types.ts`, ` + env.write( + `types.ts`, + ` export default class Default {} export class Other {} - `); - env.write(`test.ts`, ` + `, + ); + env.write( + `test.ts`, + ` import {Component} from '@angular/core'; import {Other} from './types'; import Default from './types'; @@ -5143,7 +5863,8 @@ function allTests(os: string) { export class SomeCmp { constructor(arg: Default, other: Other) {} } - `); + `, + ); env.driveMain(); const jsContents = trim(env.getContents('test.js')); @@ -5156,7 +5877,9 @@ function allTests(os: string) { }); it('should not throw when using an SVG-specific `title` tag', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, NgModule} from '@angular/core'; @Component({ template: \` @@ -5172,16 +5895,18 @@ function allTests(os: string) { declarations: [SvgCmp], }) export class SvgModule {} - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(0); }); describe('namespace support', () => { - it('should generate correct imports for type references to namespaced symbols using a namespace import', - () => { - env.write(`/node_modules/ns/index.d.ts`, ` + it('should generate correct imports for type references to namespaced symbols using a namespace import', () => { + env.write( + `/node_modules/ns/index.d.ts`, + ` export declare class Zero {} export declare namespace one { export declare class One {} @@ -5189,8 +5914,11 @@ function allTests(os: string) { export declare namespace one.two { export declare class Two {} } - `); - env.write(`test.ts`, ` + `, + ); + env.write( + `test.ts`, + ` import {Inject, Injectable, InjectionToken} from '@angular/core'; import * as ns from 'ns'; @@ -5202,22 +5930,24 @@ function allTests(os: string) { two: ns.one.two.Two, ) {} } - `); - - env.driveMain(); - const jsContents = trim(env.getContents('test.js')); - expect(jsContents).toContain(`import * as i1 from "ns";`); - expect(jsContents).toContain('i0.ɵɵinject(i1.Zero)'); - expect(jsContents).toContain('i0.ɵɵinject(i1.one.One)'); - expect(jsContents).toContain('i0.ɵɵinject(i1.one.two.Two)'); - expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.Zero')); - expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.one.One')); - expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.one.two.Two')); - }); - - it('should generate correct imports for type references to namespaced symbols using named imports', - () => { - env.write(`/node_modules/ns/index.d.ts`, ` + `, + ); + + env.driveMain(); + const jsContents = trim(env.getContents('test.js')); + expect(jsContents).toContain(`import * as i1 from "ns";`); + expect(jsContents).toContain('i0.ɵɵinject(i1.Zero)'); + expect(jsContents).toContain('i0.ɵɵinject(i1.one.One)'); + expect(jsContents).toContain('i0.ɵɵinject(i1.one.two.Two)'); + expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.Zero')); + expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.one.One')); + expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.one.two.Two')); + }); + + it('should generate correct imports for type references to namespaced symbols using named imports', () => { + env.write( + `/node_modules/ns/index.d.ts`, + ` export namespace ns { export declare class Zero {} export declare namespace one { @@ -5227,8 +5957,11 @@ function allTests(os: string) { export declare class Two {} } } - `); - env.write(`test.ts`, ` + `, + ); + env.write( + `test.ts`, + ` import {Inject, Injectable, InjectionToken} from '@angular/core'; import {ns} from 'ns'; import {ns as alias} from 'ns'; @@ -5244,32 +5977,38 @@ function allTests(os: string) { aliasedTwo: alias.one.two.Two, ) {} } - `); - - env.driveMain(); - const jsContents = trim(env.getContents('test.js')); - expect(jsContents).toContain(`import * as i1 from "ns";`); - expect(jsContents) - .toContain( - 'i0.ɵɵinject(i1.ns.Zero), ' + - 'i0.ɵɵinject(i1.ns.one.One), ' + - 'i0.ɵɵinject(i1.ns.one.two.Two), ' + - 'i0.ɵɵinject(i1.ns.Zero), ' + - 'i0.ɵɵinject(i1.ns.one.One), ' + - 'i0.ɵɵinject(i1.ns.one.two.Two)'); - expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.ns.Zero')); - expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.ns.one.One')); - expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.ns.one.two.Two')); - }); + `, + ); + + env.driveMain(); + const jsContents = trim(env.getContents('test.js')); + expect(jsContents).toContain(`import * as i1 from "ns";`); + expect(jsContents).toContain( + 'i0.ɵɵinject(i1.ns.Zero), ' + + 'i0.ɵɵinject(i1.ns.one.One), ' + + 'i0.ɵɵinject(i1.ns.one.two.Two), ' + + 'i0.ɵɵinject(i1.ns.Zero), ' + + 'i0.ɵɵinject(i1.ns.one.One), ' + + 'i0.ɵɵinject(i1.ns.one.two.Two)', + ); + expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.ns.Zero')); + expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.ns.one.One')); + expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.ns.one.two.Two')); + }); it('should not error for a namespace import as parameter type when @Inject is used', () => { env.tsconfig({'strictInjectionParameters': true}); - env.write(`/node_modules/foo/index.d.ts`, ` + env.write( + `/node_modules/foo/index.d.ts`, + ` export = Foo; declare class Foo {} declare namespace Foo {} - `); - env.write(`test.ts`, ` + `, + ); + env.write( + `test.ts`, + ` import {Inject, Injectable, InjectionToken} from '@angular/core'; import * as Foo from 'foo'; @@ -5279,7 +6018,8 @@ function allTests(os: string) { export class MyService { constructor(@Inject(TOKEN) foo: Foo) {} } - `); + `, + ); env.driveMain(); const jsContents = trim(env.getContents('test.js')); @@ -5289,12 +6029,17 @@ function allTests(os: string) { it('should error for a namespace import as parameter type used for DI', () => { env.tsconfig({'strictInjectionParameters': true}); - env.write(`/node_modules/foo/index.d.ts`, ` + env.write( + `/node_modules/foo/index.d.ts`, + ` export = Foo; declare class Foo {} declare namespace Foo {} - `); - env.write(`test.ts`, ` + `, + ); + env.write( + `test.ts`, + ` import {Injectable} from '@angular/core'; import * as Foo from 'foo'; @@ -5302,29 +6047,35 @@ function allTests(os: string) { export class MyService { constructor(foo: Foo) {} } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')) - .toBe( - `No suitable injection token for parameter 'foo' of class 'MyService'.\n` + - ` Consider using the @Inject decorator to specify an injection token.`); + expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n')).toBe( + `No suitable injection token for parameter 'foo' of class 'MyService'.\n` + + ` Consider using the @Inject decorator to specify an injection token.`, + ); expect(diags[0].relatedInformation!.length).toBe(2); - expect(diags[0].relatedInformation![0].messageText) - .toBe( - 'This type corresponds with a namespace, which cannot be used as injection token.'); - expect(diags[0].relatedInformation![1].messageText) - .toBe('The namespace import occurs here.'); + expect(diags[0].relatedInformation![0].messageText).toBe( + 'This type corresponds with a namespace, which cannot be used as injection token.', + ); + expect(diags[0].relatedInformation![1].messageText).toBe( + 'The namespace import occurs here.', + ); }); }); - it('should use `undefined` in setClassMetadata if types can\'t be represented as values', - () => { - env.write(`types.ts`, ` + it("should use `undefined` in setClassMetadata if types can't be represented as values", () => { + env.write( + `types.ts`, + ` export type MyType = Map; - `); - env.write(`test.ts`, ` + `, + ); + env.write( + `test.ts`, + ` import {Component, Inject, Injectable} from '@angular/core'; import {MyType} from './types'; @@ -5335,20 +6086,26 @@ function allTests(os: string) { export class SomeComp { constructor(@Inject('arg-token') arg: MyType) {} } - `); + `, + ); - env.driveMain(); - const jsContents = trim(env.getContents('test.js')); - expect(jsContents).not.toContain(`import { MyType } from './types';`); - // Note: `type: undefined` below, since MyType can't be represented as a value - expect(jsContents).toMatch(setClassMetadataRegExp('type: undefined')); - }); + env.driveMain(); + const jsContents = trim(env.getContents('test.js')); + expect(jsContents).not.toContain(`import { MyType } from './types';`); + // Note: `type: undefined` below, since MyType can't be represented as a value + expect(jsContents).toMatch(setClassMetadataRegExp('type: undefined')); + }); it('should use `undefined` in setClassMetadata for const enums', () => { - env.write(`keycodes.ts`, ` + env.write( + `keycodes.ts`, + ` export const enum KeyCodes {A, B}; - `); - env.write(`test.ts`, ` + `, + ); + env.write( + `test.ts`, + ` import {Component, Inject} from '@angular/core'; import {KeyCodes} from './keycodes'; @@ -5359,7 +6116,8 @@ function allTests(os: string) { export class SomeComp { constructor(@Inject('arg-token') arg: KeyCodes) {} } - `); + `, + ); env.driveMain(); const jsContents = trim(env.getContents('test.js')); @@ -5369,10 +6127,15 @@ function allTests(os: string) { }); it('should preserve the types of non-const enums in setClassMetadata', () => { - env.write(`keycodes.ts`, ` + env.write( + `keycodes.ts`, + ` export enum KeyCodes {A, B}; - `); - env.write(`test.ts`, ` + `, + ); + env.write( + `test.ts`, + ` import {Component, Inject} from '@angular/core'; import {KeyCodes} from './keycodes'; @@ -5383,7 +6146,8 @@ function allTests(os: string) { export class SomeComp { constructor(@Inject('arg-token') arg: KeyCodes) {} } - `); + `, + ); env.driveMain(); const jsContents = trim(env.getContents('test.js')); @@ -5391,14 +6155,18 @@ function allTests(os: string) { expect(jsContents).toMatch(setClassMetadataRegExp('type: i1.KeyCodes')); }); - it('should use `undefined` in setClassMetadata if types originate from type-only imports', - () => { - env.write(`types.ts`, ` + it('should use `undefined` in setClassMetadata if types originate from type-only imports', () => { + env.write( + `types.ts`, + ` export default class {} export class TypeOnlyOne {} export class TypeOnlyTwo {} - `); - env.write(`test.ts`, ` + `, + ); + env.write( + `test.ts`, + ` import {Component, Inject, Injectable} from '@angular/core'; import type DefaultImport from './types'; import type {TypeOnlyOne} from './types'; @@ -5417,25 +6185,27 @@ function allTests(os: string) { @Inject('token') typeOnlySpecifier: TypeOnlyTwo, ) {} } - `); - - env.driveMain(); - const jsContents = trim(env.getContents('test.js')); - // Module specifier for type-only import should not be emitted - expect(jsContents).not.toContain('./types'); - // Default type-only import should not be emitted - expect(jsContents).not.toContain('DefaultImport'); - // Named type-only import should not be emitted - expect(jsContents).not.toContain('TypeOnlyOne'); - // Symbols from type-only specifiers should not be emitted - expect(jsContents).not.toContain('TypeOnlyTwo'); - // The parameter type in class metadata should be undefined - expect(jsContents).toMatch(setClassMetadataRegExp('type: undefined')); - }); - - it('should not throw in case whitespaces and HTML comments are present inside ', - () => { - env.write('test.ts', ` + `, + ); + + env.driveMain(); + const jsContents = trim(env.getContents('test.js')); + // Module specifier for type-only import should not be emitted + expect(jsContents).not.toContain('./types'); + // Default type-only import should not be emitted + expect(jsContents).not.toContain('DefaultImport'); + // Named type-only import should not be emitted + expect(jsContents).not.toContain('TypeOnlyOne'); + // Symbols from type-only specifiers should not be emitted + expect(jsContents).not.toContain('TypeOnlyTwo'); + // The parameter type in class metadata should be undefined + expect(jsContents).toMatch(setClassMetadataRegExp('type: undefined')); + }); + + it('should not throw in case whitespaces and HTML comments are present inside ', () => { + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -5447,13 +6217,16 @@ function allTests(os: string) { \`, }) class CmpA {} - `); - const errors = env.driveDiagnostics(); - expect(errors.length).toBe(0); - }); + `, + ); + const errors = env.driveDiagnostics(); + expect(errors.length).toBe(0); + }); it('should compile a template using multiple directives with the same selector', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, Directive, NgModule} from '@angular/core'; @Directive({selector: '[test]'}) @@ -5471,7 +6244,8 @@ function allTests(os: string) { declarations: [Cmp, DirA, DirB], }) class Module {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -5480,7 +6254,9 @@ function allTests(os: string) { describe('cycle detection', () => { it('should detect a simple cycle and use remote component scoping', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, NgModule} from '@angular/core'; import {NormalComponent} from './cyclic'; @@ -5494,9 +6270,12 @@ function allTests(os: string) { declarations: [NormalComponent, CyclicComponent], }) export class Module {} - `); + `, + ); - env.write('cyclic.ts', ` + env.write( + 'cyclic.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -5504,26 +6283,32 @@ function allTests(os: string) { template: '', }) export class NormalComponent {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents) - .toMatch( - /i\d\.ɵɵsetComponentScope\(NormalComponent,\s+\[CyclicComponent\],\s+\[\]\)/); + expect(jsContents).toMatch( + /i\d\.ɵɵsetComponentScope\(NormalComponent,\s+\[CyclicComponent\],\s+\[\]\)/, + ); expect(jsContents).not.toContain('/*__PURE__*/ i0.ɵɵsetComponentScope'); }); it('should detect a cycle added entirely during compilation', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {NgModule} from '@angular/core'; import {ACmp} from './a'; import {BCmp} from './b'; @NgModule({declarations: [ACmp, BCmp]}) export class Module {} - `); - env.write('a.ts', ` + `, + ); + env.write( + 'a.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -5531,8 +6316,11 @@ function allTests(os: string) { template: '', }) export class ACmp {} - `); - env.write('b.ts', ` + `, + ); + env.write( + 'b.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -5540,7 +6328,8 @@ function allTests(os: string) { template: '', }) export class BCmp {} - `); + `, + ); env.driveMain(); const aJsContents = env.getContents('a.js'); const bJsContents = env.getContents('b.js'); @@ -5548,16 +6337,21 @@ function allTests(os: string) { expect(bJsContents).not.toMatch(/import \* as i\d? from ".\/a"/); }); - it('should not detect a potential cycle if it doesn\'t actually happen', () => { - env.write('test.ts', ` + it("should not detect a potential cycle if it doesn't actually happen", () => { + env.write( + 'test.ts', + ` import {NgModule} from '@angular/core'; import {ACmp} from './a'; import {BCmp} from './b'; @NgModule({declarations: [ACmp, BCmp]}) export class Module {} - `); - env.write('a.ts', ` + `, + ); + env.write( + 'a.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -5565,8 +6359,11 @@ function allTests(os: string) { template: '', }) export class ACmp {} - `); - env.write('b.ts', ` + `, + ); + env.write( + 'b.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -5574,22 +6371,28 @@ function allTests(os: string) { template: 'does not use a-cmp', }) export class BCmp {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); expect(jsContents).not.toContain('setComponentScope'); }); it('should not consider type-only import clauses during cycle detection', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {NgModule} from '@angular/core'; import {ACmp} from './a'; import {BCmp} from './b'; @NgModule({declarations: [ACmp, BCmp]}) export class Module {} - `); - env.write('a.ts', ` + `, + ); + env.write( + 'a.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -5597,8 +6400,11 @@ function allTests(os: string) { template: '', }) export class ACmp {} - `); - env.write('b.ts', ` + `, + ); + env.write( + 'b.ts', + ` import {Component} from '@angular/core'; import type {ACmp} from './a'; @@ -5609,23 +6415,28 @@ function allTests(os: string) { export class BCmp { a: ACmp; } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); expect(jsContents).not.toContain('setComponentScope'); }); - it('should not consider import clauses where all the specifiers are type-only during cycle detection', - () => { - env.write('test.ts', ` + it('should not consider import clauses where all the specifiers are type-only during cycle detection', () => { + env.write( + 'test.ts', + ` import {NgModule} from '@angular/core'; import {SharedOne, SharedTwo} from './shared'; import {CyclicCmp} from './cyclic'; @NgModule({declarations: [SharedOne, SharedTwo, CyclicCmp]}) export class Module {} - `); - env.write('shared.ts', ` + `, + ); + env.write( + 'shared.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -5639,8 +6450,11 @@ function allTests(os: string) { template: '', }) export class SharedTwo {} - `); - env.write('cyclic.ts', ` + `, + ); + env.write( + 'cyclic.ts', + ` import {Component} from '@angular/core'; import {type SharedOne, type SharedTwo} from './shared'; @@ -5652,15 +6466,18 @@ function allTests(os: string) { one: SharedOne; two: SharedTwo; } - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); - expect(jsContents).not.toContain('setComponentScope'); - }); + env.driveMain(); + const jsContents = env.getContents('test.js'); + expect(jsContents).not.toContain('setComponentScope'); + }); it('should only pass components actually used to setComponentScope', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, NgModule} from '@angular/core'; import {NormalComponent} from './cyclic'; import {OtherComponent} from './other'; @@ -5675,9 +6492,12 @@ function allTests(os: string) { declarations: [NormalComponent, CyclicComponent, OtherComponent], }) export class Module {} - `); + `, + ); - env.write('cyclic.ts', ` + env.write( + 'cyclic.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -5685,9 +6505,12 @@ function allTests(os: string) { template: '', }) export class NormalComponent {} - `); + `, + ); - env.write('other.ts', ` + env.write( + 'other.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -5695,7 +6518,8 @@ function allTests(os: string) { template: 'An unused other component', }) export class OtherComponent {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -5707,7 +6531,9 @@ function allTests(os: string) { compilationMode: 'partial', }); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component, NgModule} from '@angular/core'; import {NormalComponent} from './cyclic'; @@ -5721,9 +6547,12 @@ function allTests(os: string) { declarations: [NormalComponent, CyclicComponent], }) export class Module {} - `); + `, + ); - env.write('cyclic.ts', ` + env.write( + 'cyclic.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -5731,41 +6560,43 @@ function allTests(os: string) { template: '', }) export class NormalComponent {} - `); + `, + ); const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toEqual(1); const error = diagnostics[0]; expect(error.code).toBe(ngErrorCode(ErrorCode.IMPORT_CYCLE_DETECTED)); - expect(error.messageText) - .toEqual( - 'One or more import cycles would need to be created to compile this component, ' + - 'which is not supported by the current compiler configuration.'); + expect(error.messageText).toEqual( + 'One or more import cycles would need to be created to compile this component, ' + + 'which is not supported by the current compiler configuration.', + ); const _abs = absoluteFrom; - expect(error.relatedInformation?.map(diag => diag.messageText)).toEqual([ + expect(error.relatedInformation?.map((diag) => diag.messageText)).toEqual([ `The component 'CyclicComponent' is used in the template ` + - `but importing it would create a cycle: ` + - `${_abs('/cyclic.ts')} -> ${_abs('/test.ts')} -> ${_abs('/cyclic.ts')}`, + `but importing it would create a cycle: ` + + `${_abs('/cyclic.ts')} -> ${_abs('/test.ts')} -> ${_abs('/cyclic.ts')}`, ]); }); }); describe('local refs', () => { - it('should not generate an error when a local ref is unresolved (outside of template type-checking)', - () => { - env.write('test.ts', ` + it('should not generate an error when a local ref is unresolved (outside of template type-checking)', () => { + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ template: '
', }) export class TestCmp {} - `); - const diags = env.driveDiagnostics(); - expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toEqual(`No directive found with exportAs 'unknownTarget'.`); - }); + `, + ); + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].messageText).toEqual(`No directive found with exportAs 'unknownTarget'.`); + }); }); describe('multiple local refs', () => { @@ -5803,10 +6634,10 @@ function allTests(os: string) { `
- ` + `, ]; - cases.forEach(template => { + cases.forEach((template) => { it('should not throw', () => { env.write('test.ts', getComponentScript(template)); const errors = env.driveDiagnostics(); @@ -5816,7 +6647,9 @@ function allTests(os: string) { }); it('should wrap "inputs" and "outputs" keys if they contain unsafe characters', () => { - env.write(`test.ts`, ` + env.write( + `test.ts`, + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -5828,7 +6661,8 @@ function allTests(os: string) { @Input('track-type') trackType: string; @Input('track-name') trackName: string; } - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -5852,7 +6686,9 @@ function allTests(os: string) { }); it('should generate the correct declaration for class members decorated with @Input', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({selector: '[dir]'}) @@ -5862,23 +6698,26 @@ function allTests(os: string) { @Input({required: true}) requiredNoAlias: any; @Input({alias: 'aliasedRequiredWithAlias', required: true}) requiredWithAlias: any; } - `); + `, + ); env.driveMain(); const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents) - .toContain( - 'static ɵdir: i0.ɵɵDirectiveDeclaration;'); + expect(dtsContents).toContain( + 'static ɵdir: i0.ɵɵDirectiveDeclaration;', + ); }); it('should generate the correct declaration for directives using the `inputs` array', () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Input} from '@angular/core'; @Directive({ @@ -5898,21 +6737,22 @@ function allTests(os: string) { requiredLiteral: any; requiredAlisedLiteral: any; } - `); + `, + ); env.driveMain(); const dtsContents = env.getContents('test.d.ts'); - expect(dtsContents) - .toContain( - 'static ɵdir: i0.ɵɵDirectiveDeclaration;'); + expect(dtsContents).toContain( + 'static ɵdir: i0.ɵɵDirectiveDeclaration;', + ); }); describe('NgModule invalid import/export errors', () => { @@ -5921,17 +6761,22 @@ function allTests(os: string) { expect(errors.length).toBe(1); let {code, messageText} = errors[0]; if (errors[0].relatedInformation !== undefined) { - messageText += errors[0].relatedInformation.map(info => info.messageText).join('\n'); + messageText += errors[0].relatedInformation.map((info) => info.messageText).join('\n'); } expect(code).toBe(ngErrorCode(errorCode)); expect(trim(messageText as string)).toContain(errorMessage); } it('should provide a hint when importing an invalid NgModule from node_modules', () => { - env.write('node_modules/external/index.d.ts', ` + env.write( + 'node_modules/external/index.d.ts', + ` export declare class NotAModule {} - `); - env.write('test.ts', ` + `, + ); + env.write( + 'test.ts', + ` import {NgModule} from '@angular/core'; import {NotAModule} from 'external'; @@ -5939,20 +6784,27 @@ function allTests(os: string) { imports: [NotAModule], }) export class Module {} - `); + `, + ); verifyThrownError( - ErrorCode.NGMODULE_INVALID_IMPORT, - 'This likely means that the library (external) which declares NotAModule is not ' + - 'compatible with Angular Ivy.'); + ErrorCode.NGMODULE_INVALID_IMPORT, + 'This likely means that the library (external) which declares NotAModule is not ' + + 'compatible with Angular Ivy.', + ); }); it('should provide a hint when importing an invalid NgModule from a local library', () => { - env.write('libs/external/index.d.ts', ` + env.write( + 'libs/external/index.d.ts', + ` export declare class NotAModule {} - `); + `, + ); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {NgModule} from '@angular/core'; import {NotAModule} from './libs/external'; @@ -5960,20 +6812,27 @@ function allTests(os: string) { imports: [NotAModule], }) export class Module {} - `); + `, + ); verifyThrownError( - ErrorCode.NGMODULE_INVALID_IMPORT, - 'This likely means that the dependency which declares NotAModule is not ' + - 'compatible with Angular Ivy.'); + ErrorCode.NGMODULE_INVALID_IMPORT, + 'This likely means that the dependency which declares NotAModule is not ' + + 'compatible with Angular Ivy.', + ); }); it('should provide a hint when importing an invalid NgModule in the current program', () => { - env.write('invalid.ts', ` + env.write( + 'invalid.ts', + ` export class NotAModule {} - `); + `, + ); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {NgModule} from '@angular/core'; import {NotAModule} from './invalid'; @@ -5981,16 +6840,21 @@ function allTests(os: string) { imports: [NotAModule], }) export class Module {} - `); + `, + ); verifyThrownError( - ErrorCode.NGMODULE_INVALID_IMPORT, 'Is it missing an @NgModule annotation?'); + ErrorCode.NGMODULE_INVALID_IMPORT, + 'Is it missing an @NgModule annotation?', + ); }); }); describe('when processing external directives', () => { it('should not emit multiple references to the same directive', () => { - env.write('node_modules/external/index.d.ts', ` + env.write( + 'node_modules/external/index.d.ts', + ` import {ɵɵDirectiveDeclaration, ɵɵNgModuleDeclaration} from '@angular/core'; export declare class ExternalDir { @@ -6000,8 +6864,11 @@ function allTests(os: string) { export declare class ExternalModule { static ɵmod: ɵɵNgModuleDeclaration; } - `); - env.write('test.ts', ` + `, + ); + env.write( + 'test.ts', + ` import {Component, Directive, NgModule} from '@angular/core'; import {ExternalModule} from 'external'; @@ -6017,7 +6884,8 @@ function allTests(os: string) { imports: [ExternalModule, ExternalModule], }) class Module {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -6025,7 +6893,9 @@ function allTests(os: string) { }); it('should import directives by their external name', () => { - env.write('node_modules/external/index.d.ts', ` + env.write( + 'node_modules/external/index.d.ts', + ` import {ɵɵDirectiveDeclaration, ɵɵNgModuleDeclaration} from '@angular/core'; import {InternalDir} from './internal'; @@ -6034,14 +6904,20 @@ function allTests(os: string) { export declare class ExternalModule { static ɵmod: ɵɵNgModuleDeclaration; } - `); - env.write('node_modules/external/internal.d.ts', ` + `, + ); + env.write( + 'node_modules/external/internal.d.ts', + ` export declare class InternalDir { static ɵdir: ɵɵDirectiveDeclaration; } - `); - env.write('test.ts', ` + `, + ); + env.write( + 'test.ts', + ` import {Component, Directive, NgModule} from '@angular/core'; import {ExternalModule} from 'external'; @@ -6055,7 +6931,8 @@ function allTests(os: string) { imports: [ExternalModule], }) class Module {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -6066,7 +6943,7 @@ function allTests(os: string) { // Run checks that are present in preanalysis phase in both sync and async mode, to // make sure the error messages are consistently thrown from `analyzeSync` and // `analyzeAsync` functions. - ['sync', 'async'].forEach(mode => { + ['sync', 'async'].forEach((mode) => { describe(`preanalysis phase checks [${mode}]`, () => { let driveDiagnostics: () => Promise>; beforeEach(() => { @@ -6079,14 +6956,17 @@ function allTests(os: string) { }); it('should throw if @Component is missing a template', async () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ selector: 'test', }) export class TestCmp {} - `); + `, + ); const diags = await driveDiagnostics(); expect(diags[0].messageText).toBe('component is missing a template'); @@ -6094,7 +6974,9 @@ function allTests(os: string) { }); it('should throw if `styleUrls` is defined incorrectly in @Component', async () => { - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -6104,13 +6986,14 @@ function allTests(os: string) { styleUrls: '...' }) export class TestCmp {} - `); + `, + ); const diags = await driveDiagnostics(); expect(diags.length).toBe(1); const messageText = ts.flattenDiagnosticMessageText(diags[0].messageText, '\n'); expect(messageText).toContain('styleUrls must be an array of strings'); - expect(messageText).toContain('Value is of type \'string\'.'); + expect(messageText).toContain("Value is of type 'string'."); expect(diags[0].file!.fileName).toBe(absoluteFrom('/test.ts')); }); }); @@ -6125,7 +7008,7 @@ function allTests(os: string) { env.driveMain(); const jsContents = env.getContents('flat.js'); - expect(jsContents).toContain('export * from \'./test\';'); + expect(jsContents).toContain("export * from './test';"); }); it('should determine the flat module entry-point within multiple root files', () => { @@ -6137,10 +7020,10 @@ function allTests(os: string) { env.driveMain(); const jsContents = env.getContents('flat.js'); - expect(jsContents) - .toContain( - 'export * from \'./index\';', - 'Should detect the "index.ts" file as flat module entry-point.'); + expect(jsContents).toContain( + "export * from './index';", + 'Should detect the "index.ts" file as flat module entry-point.', + ); }); it('should generate a flat module with an id', () => { @@ -6177,40 +7060,43 @@ function allTests(os: string) { env.driveMain(); }); - it('should not throw or produce flat module index if "flatModuleOutFile" is set to ' + - 'empty string', - () => { - env.tsconfig({ - 'flatModuleOutFile': '', - }); - - env.write('test.ts', `export const SOME_EXPORT = 'some-export'`); - // The "driveMain" method automatically ensures that there is no - // exception and that the build succeeded. - env.driveMain(); - // Previously ngtsc incorrectly tried generating a flat module index - // file if the "flatModuleOutFile" was set to an empty string. ngtsc - // just wrote the bundle file with an empty filename (just extension). - env.assertDoesNotExist('.js'); - env.assertDoesNotExist('.d.ts'); - }); - - it('should report an error when a flat module index is requested but no entrypoint can be determined', - () => { - env.tsconfig({'flatModuleOutFile': 'flat.js'}); - env.write('test.ts', 'export class Foo {}'); - env.write('test2.ts', 'export class Bar {}'); - - const errors = env.driveDiagnostics(); - expect(errors.length).toBe(1); - expect(errors[0].messageText) - .toBe( - 'Angular compiler option "flatModuleOutFile" requires one and only one .ts file in the "files" field.'); - }); + it( + 'should not throw or produce flat module index if "flatModuleOutFile" is set to ' + + 'empty string', + () => { + env.tsconfig({ + 'flatModuleOutFile': '', + }); + + env.write('test.ts', `export const SOME_EXPORT = 'some-export'`); + // The "driveMain" method automatically ensures that there is no + // exception and that the build succeeded. + env.driveMain(); + // Previously ngtsc incorrectly tried generating a flat module index + // file if the "flatModuleOutFile" was set to an empty string. ngtsc + // just wrote the bundle file with an empty filename (just extension). + env.assertDoesNotExist('.js'); + env.assertDoesNotExist('.d.ts'); + }, + ); + + it('should report an error when a flat module index is requested but no entrypoint can be determined', () => { + env.tsconfig({'flatModuleOutFile': 'flat.js'}); + env.write('test.ts', 'export class Foo {}'); + env.write('test2.ts', 'export class Bar {}'); + + const errors = env.driveDiagnostics(); + expect(errors.length).toBe(1); + expect(errors[0].messageText).toBe( + 'Angular compiler option "flatModuleOutFile" requires one and only one .ts file in the "files" field.', + ); + }); it('should report an error when a visible directive is not exported', () => { env.tsconfig({'flatModuleOutFile': 'flat.js'}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, NgModule} from '@angular/core'; // The directive is not exported. @@ -6220,15 +7106,16 @@ function allTests(os: string) { // The module is, which makes the directive visible. @NgModule({declarations: [Dir], exports: [Dir]}) export class Module {} - `); + `, + ); const errors = env.driveDiagnostics(); expect(errors.length).toBe(1); - expect(errors[0].messageText) - .toBe( - 'Unsupported private class Dir. This class is visible ' + - 'to consumers via Module -> Dir, but is not exported from the top-level library ' + - 'entrypoint.'); + expect(errors[0].messageText).toBe( + 'Unsupported private class Dir. This class is visible ' + + 'to consumers via Module -> Dir, but is not exported from the top-level library ' + + 'entrypoint.', + ); // Verify that the error is for the correct class. const error = errors[0] as ts.Diagnostic; @@ -6239,7 +7126,9 @@ function allTests(os: string) { it('should report an error when a visible host directive is not exported', () => { env.tsconfig({'flatModuleOutFile': 'flat.js'}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, NgModule} from '@angular/core'; @Directive({ @@ -6250,7 +7139,8 @@ function allTests(os: string) { // The directive is not exported. @Directive({selector: 'test', hostDirectives: [HostDir]}) export class Dir {} - `); + `, + ); const errors = env.driveDiagnostics(); expect(errors.length).toBe(1); @@ -6259,7 +7149,9 @@ function allTests(os: string) { it('should report an error when a deeply visible directive is not exported', () => { env.tsconfig({'flatModuleOutFile': 'flat.js'}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, NgModule} from '@angular/core'; // The directive is not exported. @@ -6273,25 +7165,28 @@ function allTests(os: string) { // The module is, which makes the directive visible. @NgModule({exports: [DirModule]}) export class Module {} - `); + `, + ); const errors = env.driveDiagnostics(); expect(errors.length).toBe(2); - expect(errors[0].messageText) - .toBe( - 'Unsupported private class DirModule. This class is ' + - 'visible to consumers via Module -> DirModule, but is not exported from the top-level ' + - 'library entrypoint.'); - expect(errors[1].messageText) - .toBe( - 'Unsupported private class Dir. This class is visible ' + - 'to consumers via Module -> DirModule -> Dir, but is not exported from the top-level ' + - 'library entrypoint.'); + expect(errors[0].messageText).toBe( + 'Unsupported private class DirModule. This class is ' + + 'visible to consumers via Module -> DirModule, but is not exported from the top-level ' + + 'library entrypoint.', + ); + expect(errors[1].messageText).toBe( + 'Unsupported private class Dir. This class is visible ' + + 'to consumers via Module -> DirModule -> Dir, but is not exported from the top-level ' + + 'library entrypoint.', + ); }); it('should report an error when a deeply visible module is not exported', () => { env.tsconfig({'flatModuleOutFile': 'flat.js'}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, NgModule} from '@angular/core'; // The directive is exported. @@ -6305,21 +7200,23 @@ function allTests(os: string) { // The module is, which makes the module and directive visible. @NgModule({exports: [DirModule]}) export class Module {} - `); + `, + ); const errors = env.driveDiagnostics(); expect(errors.length).toBe(1); - expect(errors[0].messageText) - .toBe( - 'Unsupported private class DirModule. This class is ' + - 'visible to consumers via Module -> DirModule, but is not exported from the top-level ' + - 'library entrypoint.'); + expect(errors[0].messageText).toBe( + 'Unsupported private class DirModule. This class is ' + + 'visible to consumers via Module -> DirModule, but is not exported from the top-level ' + + 'library entrypoint.', + ); }); - it('should not report an error when a non-exported module is imported by a visible one', - () => { - env.tsconfig({'flatModuleOutFile': 'flat.js'}); - env.write('test.ts', ` + it('should not report an error when a non-exported module is imported by a visible one', () => { + env.tsconfig({'flatModuleOutFile': 'flat.js'}); + env.write( + 'test.ts', + ` import {Directive, NgModule} from '@angular/core'; // The directive is not exported. @@ -6334,23 +7231,29 @@ function allTests(os: string) { // directive visible. @NgModule({imports: [DirModule]}) export class Module {} - `); + `, + ); - const errors = env.driveDiagnostics(); - expect(errors.length).toBe(0); - }); + const errors = env.driveDiagnostics(); + expect(errors.length).toBe(0); + }); it('should not report an error when re-exporting an external symbol', () => { env.tsconfig({'flatModuleOutFile': 'flat.js'}); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, NgModule} from '@angular/core'; import {ExternalModule} from 'external'; // This module makes ExternalModule and ExternalDir visible. @NgModule({exports: [ExternalModule]}) export class Module {} - `); - env.write('node_modules/external/index.d.ts', ` + `, + ); + env.write( + 'node_modules/external/index.d.ts', + ` import {ɵɵDirectiveDeclaration, ɵɵNgModuleDeclaration} from '@angular/core'; export declare class ExternalDir { @@ -6360,7 +7263,8 @@ function allTests(os: string) { export declare class ExternalModule { static ɵmod: ɵɵNgModuleDeclaration; } - `); + `, + ); const errors = env.driveDiagnostics(); expect(errors.length).toBe(0); @@ -6375,15 +7279,20 @@ function allTests(os: string) { }); it('should re-export a directive from a different file under a private symbol name', () => { - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: 'dir', }) export class Dir {} - `); - env.write('module.ts', ` + `, + ); + env.write( + 'module.ts', + ` import {Directive, NgModule} from '@angular/core'; import {Dir} from './dir'; @@ -6395,7 +7304,8 @@ function allTests(os: string) { exports: [Dir, InlineDir], }) export class Module {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('module.js'); @@ -6407,9 +7317,10 @@ function allTests(os: string) { expect(dtsContents).not.toContain('ɵngExportɵModuleɵInlineDir'); }); - it('should re-export a directive from an exported NgModule under a private symbol name', - () => { - env.write('dir.ts', ` + it('should re-export a directive from an exported NgModule under a private symbol name', () => { + env.write( + 'dir.ts', + ` import {Directive, NgModule} from '@angular/core'; @Directive({ @@ -6422,8 +7333,11 @@ function allTests(os: string) { exports: [Dir], }) export class DirModule {} - `); - env.write('module.ts', ` + `, + ); + env.write( + 'module.ts', + ` import {NgModule} from '@angular/core'; import {DirModule} from './dir'; @@ -6431,26 +7345,32 @@ function allTests(os: string) { exports: [DirModule], }) export class Module {} - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('module.js'); - const dtsContents = env.getContents('module.d.ts'); + env.driveMain(); + const jsContents = env.getContents('module.js'); + const dtsContents = env.getContents('module.d.ts'); - expect(jsContents).toContain('export { Dir as ɵngExportɵModuleɵDir } from "./dir";'); - expect(dtsContents).toContain('export { Dir as ɵngExportɵModuleɵDir } from "./dir";'); - }); + expect(jsContents).toContain('export { Dir as ɵngExportɵModuleɵDir } from "./dir";'); + expect(dtsContents).toContain('export { Dir as ɵngExportɵModuleɵDir } from "./dir";'); + }); - it('should not re-export a directive that\'s not exported from the NgModule', () => { - env.write('dir.ts', ` + it("should not re-export a directive that's not exported from the NgModule", () => { + env.write( + 'dir.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: 'dir', }) export class Dir {} - `); - env.write('module.ts', ` + `, + ); + env.write( + 'module.ts', + ` import {NgModule} from '@angular/core'; import {Dir} from './dir'; @@ -6459,7 +7379,8 @@ function allTests(os: string) { exports: [], }) export class Module {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('module.js'); @@ -6469,16 +7390,21 @@ function allTests(os: string) { expect(dtsContents).not.toContain('ɵngExportɵModuleɵDir'); }); - it('should not re-export a directive that\'s already exported', () => { - env.write('dir.ts', ` + it("should not re-export a directive that's already exported", () => { + env.write( + 'dir.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: 'dir', }) export class Dir {} - `); - env.write('module.ts', ` + `, + ); + env.write( + 'module.ts', + ` import {NgModule} from '@angular/core'; import {Dir} from './dir'; @@ -6489,7 +7415,8 @@ function allTests(os: string) { export class Module {} export {Dir}; - `); + `, + ); env.driveMain(); const jsContents = env.getContents('module.js'); @@ -6500,7 +7427,9 @@ function allTests(os: string) { }); it('should not re-export a directive from an exported, external NgModule', () => { - env.write(`node_modules/external/index.d.ts`, ` + env.write( + `node_modules/external/index.d.ts`, + ` import {ɵɵDirectiveDeclaration, ɵɵNgModuleDeclaration} from '@angular/core'; export declare class ExternalDir { @@ -6510,8 +7439,11 @@ function allTests(os: string) { export declare class ExternalModule { static ɵmod: ɵɵNgModuleDeclaration; } - `); - env.write('module.ts', ` + `, + ); + env.write( + 'module.ts', + ` import {NgModule} from '@angular/core'; import {ExternalModule} from 'external'; @@ -6519,7 +7451,8 @@ function allTests(os: string) { exports: [ExternalModule], }) export class Module {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('module.js'); @@ -6527,25 +7460,32 @@ function allTests(os: string) { expect(jsContents).not.toContain('ɵngExportɵExternalModuleɵExternalDir'); }); - it('should error when two directives with the same declared name are exported from the same NgModule', - () => { - env.write('dir.ts', ` + it('should error when two directives with the same declared name are exported from the same NgModule', () => { + env.write( + 'dir.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: 'dir', }) export class Dir {} - `); - env.write('dir2.ts', ` + `, + ); + env.write( + 'dir2.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: 'dir', }) export class Dir {} - `); - env.write('module.ts', ` + `, + ); + env.write( + 'module.ts', + ` import {NgModule} from '@angular/core'; import {Dir} from './dir'; import {Dir as Dir2} from './dir2'; @@ -6555,32 +7495,40 @@ function allTests(os: string) { exports: [Dir, Dir2], }) export class Module {} - `); + `, + ); - const diag = env.driveDiagnostics(); - expect(diag.length).toBe(1); - expect(diag[0]!.code).toEqual(ngErrorCode(ErrorCode.NGMODULE_REEXPORT_NAME_COLLISION)); - }); + const diag = env.driveDiagnostics(); + expect(diag.length).toBe(1); + expect(diag[0]!.code).toEqual(ngErrorCode(ErrorCode.NGMODULE_REEXPORT_NAME_COLLISION)); + }); - it('should not error when two directives with the same declared name are exported from the same NgModule, but one is exported from the file directly', - () => { - env.write('dir.ts', ` + it('should not error when two directives with the same declared name are exported from the same NgModule, but one is exported from the file directly', () => { + env.write( + 'dir.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: 'dir', }) export class Dir {} - `); - env.write('dir2.ts', ` + `, + ); + env.write( + 'dir2.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: 'dir', }) export class Dir {} - `); - env.write('module.ts', ` + `, + ); + env.write( + 'module.ts', + ` import {NgModule} from '@angular/core'; import {Dir} from './dir'; import {Dir as Dir2} from './dir2'; @@ -6592,22 +7540,28 @@ function allTests(os: string) { export class Module {} export {Dir} from './dir2'; - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('module.js'); - expect(jsContents).toContain('export { Dir as ɵngExportɵModuleɵDir } from "./dir";'); - }); + env.driveMain(); + const jsContents = env.getContents('module.js'); + expect(jsContents).toContain('export { Dir as ɵngExportɵModuleɵDir } from "./dir";'); + }); it('should choose a re-exported symbol if one is present', () => { - env.write(`node_modules/external/dir.d.ts`, ` + env.write( + `node_modules/external/dir.d.ts`, + ` import {ɵɵDirectiveDeclaration} from '@angular/core'; export declare class ExternalDir { static ɵdir: ɵɵDirectiveDeclaration; } - `); - env.write('node_modules/external/module.d.ts', ` + `, + ); + env.write( + 'node_modules/external/module.d.ts', + ` import {ɵɵNgModuleDeclaration} from '@angular/core'; import {ExternalDir} from './dir'; @@ -6616,8 +7570,11 @@ function allTests(os: string) { } export {ExternalDir as ɵngExportɵExternalModuleɵExternalDir}; - `); - env.write('test.ts', ` + `, + ); + env.write( + 'test.ts', + ` import {Component, Directive, NgModule} from '@angular/core'; import {ExternalModule} from 'external/module'; @@ -6632,7 +7589,8 @@ function allTests(os: string) { imports: [ExternalModule], }) class Module {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('test.js'); @@ -6644,15 +7602,20 @@ function allTests(os: string) { // Return to the default configuration, which has re-exports disabled. env.tsconfig(); - env.write('dir.ts', ` + env.write( + 'dir.ts', + ` import {Directive} from '@angular/core'; @Directive({ selector: 'dir', }) export class Dir {} - `); - env.write('module.ts', ` + `, + ); + env.write( + 'module.ts', + ` import {NgModule} from '@angular/core'; import {Dir} from './dir'; @@ -6661,7 +7624,8 @@ function allTests(os: string) { exports: [Dir], }) export class Module {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('module.js'); @@ -6676,22 +7640,29 @@ function allTests(os: string) { let beforeCount = 0; let afterCount = 0; - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {NgModule} from '@angular/core'; @NgModule({}) class Module {} - `); + `, + ); env.driveMain({ - beforeTs: [() => (sourceFile: ts.SourceFile) => { - beforeCount++; - return sourceFile; - }], - afterTs: [() => (sourceFile: ts.SourceFile) => { - afterCount++; - return sourceFile; - }], + beforeTs: [ + () => (sourceFile: ts.SourceFile) => { + beforeCount++; + return sourceFile; + }, + ], + afterTs: [ + () => (sourceFile: ts.SourceFile) => { + afterCount++; + return sourceFile; + }, + ], }); expect(beforeCount).toBe(1); @@ -6699,9 +7670,10 @@ function allTests(os: string) { }); describe('sanitization', () => { - it('should generate sanitizers for unsafe attributes in hostBindings fn in Directives', - () => { - env.write(`test.ts`, ` + it('should generate sanitizers for unsafe attributes in hostBindings fn in Directives', () => { + env.write( + `test.ts`, + ` import {Component, Directive, HostBinding, NgModule, Input} from '@angular/core'; @Directive({ @@ -6739,11 +7711,12 @@ function allTests(os: string) { @NgModule({declarations: [FooCmp, UnsafeAttrsDirective]}) export class Module {} - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); - const hostBindingsFn = ` + env.driveMain(); + const jsContents = env.getContents('test.js'); + const hostBindingsFn = ` hostVars: 6, hostBindings: function UnsafeAttrsDirective_HostBindings(rf, ctx) { if (rf & 2) { @@ -6751,12 +7724,13 @@ function allTests(os: string) { } } `; - expect(trim(jsContents)).toContain(trim(hostBindingsFn)); - }); + expect(trim(jsContents)).toContain(trim(hostBindingsFn)); + }); - it('should generate sanitizers for unsafe properties in hostBindings fn in Directives', - () => { - env.write(`test.ts`, ` + it('should generate sanitizers for unsafe properties in hostBindings fn in Directives', () => { + env.write( + `test.ts`, + ` import {Component, Directive, HostBinding, Input, NgModule} from '@angular/core'; @Directive({ @@ -6794,11 +7768,12 @@ function allTests(os: string) { @NgModule({declarations: [FooCmp, UnsafePropsDirective]}) class MyModule {} - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); - const hostBindingsFn = ` + env.driveMain(); + const jsContents = env.getContents('test.js'); + const hostBindingsFn = ` hostVars: 6, hostBindings: function UnsafePropsDirective_HostBindings(rf, ctx) { if (rf & 2) { @@ -6806,12 +7781,13 @@ function allTests(os: string) { } } `; - expect(trim(jsContents)).toContain(trim(hostBindingsFn)); - }); + expect(trim(jsContents)).toContain(trim(hostBindingsFn)); + }); - it('should not generate sanitizers for URL properties in hostBindings fn in Component', - () => { - env.write(`test.ts`, ` + it('should not generate sanitizers for URL properties in hostBindings fn in Component', () => { + env.write( + `test.ts`, + ` import {Component} from '@angular/core'; @Component({ @@ -6827,11 +7803,12 @@ function allTests(os: string) { } }) class FooCmp {} - `); + `, + ); - env.driveMain(); - const jsContents = env.getContents('test.js'); - const hostBindingsFn = ` + env.driveMain(); + const jsContents = env.getContents('test.js'); + const hostBindingsFn = ` hostVars: 6, hostBindings: function FooCmp_HostBindings(rf, ctx) { if (rf & 2) { @@ -6840,17 +7817,18 @@ function allTests(os: string) { } } `; - expect(trim(jsContents)).toContain(trim(hostBindingsFn)); - }); + expect(trim(jsContents)).toContain(trim(hostBindingsFn)); + }); }); describe('NgModule export aliasing', () => { - it('should use an alias to import a directive from a deep dependency when _useHostForImportAndAliasGeneration is set', - () => { - env.tsconfig({'_useHostForImportAndAliasGeneration': true}); + it('should use an alias to import a directive from a deep dependency when _useHostForImportAndAliasGeneration is set', () => { + env.tsconfig({'_useHostForImportAndAliasGeneration': true}); - // 'alpha' declares the directive which will ultimately be imported. - env.write('alpha.d.ts', ` + // 'alpha' declares the directive which will ultimately be imported. + env.write( + 'alpha.d.ts', + ` import {ɵɵDirectiveDeclaration, ɵɵNgModuleDeclaration} from '@angular/core'; export declare class ExternalDir { @@ -6860,21 +7838,27 @@ function allTests(os: string) { export declare class AlphaModule { static ɵmod: ɵɵNgModuleDeclaration; } - `); + `, + ); - // 'beta' re-exports AlphaModule from alpha. - env.write('beta.d.ts', ` + // 'beta' re-exports AlphaModule from alpha. + env.write( + 'beta.d.ts', + ` import {ɵɵNgModuleDeclaration} from '@angular/core'; import {AlphaModule} from './alpha'; export declare class BetaModule { static ɵmod: ɵɵNgModuleDeclaration; } - `); + `, + ); - // The application imports BetaModule from beta, gaining visibility of - // ExternalDir from alpha. - env.write('test.ts', ` + // The application imports BetaModule from beta, gaining visibility of + // ExternalDir from alpha. + env.write( + 'test.ts', + ` import {Component, NgModule} from '@angular/core'; import {BetaModule} from './beta'; @@ -6889,21 +7873,23 @@ function allTests(os: string) { imports: [BetaModule], }) export class Module {} - `); - env.driveMain(); - const jsContents = env.getContents('test.js'); + `, + ); + env.driveMain(); + const jsContents = env.getContents('test.js'); - // Expect that ExternalDir from alpha is imported via the re-export from beta. - expect(jsContents).toContain('import * as i1 from "root/beta";'); - expect(jsContents).toContain('dependencies: [i1.\u0275ng$root$alpha$$ExternalDir]'); - }); + // Expect that ExternalDir from alpha is imported via the re-export from beta. + expect(jsContents).toContain('import * as i1 from "root/beta";'); + expect(jsContents).toContain('dependencies: [i1.\u0275ng$root$alpha$$ExternalDir]'); + }); - it('should directly import a directive from a deep dependency when _useHostForImportGeneration is set', - () => { - env.tsconfig({'_useHostForImportGeneration': true}); + it('should directly import a directive from a deep dependency when _useHostForImportGeneration is set', () => { + env.tsconfig({'_useHostForImportGeneration': true}); - // 'alpha' declares the directive which will ultimately be imported. - env.write('alpha.d.ts', ` + // 'alpha' declares the directive which will ultimately be imported. + env.write( + 'alpha.d.ts', + ` import {ɵɵDirectiveDeclaration, ɵɵNgModuleDeclaration} from '@angular/core'; export declare class ExternalDir { @@ -6913,21 +7899,27 @@ function allTests(os: string) { export declare class AlphaModule { static ɵmod: ɵɵNgModuleDeclaration; } - `); + `, + ); - // 'beta' re-exports AlphaModule from alpha. - env.write('beta.d.ts', ` + // 'beta' re-exports AlphaModule from alpha. + env.write( + 'beta.d.ts', + ` import {ɵɵNgModuleDeclaration} from '@angular/core'; import {AlphaModule} from './alpha'; export declare class BetaModule { static ɵmod: ɵɵNgModuleDeclaration; } - `); + `, + ); - // The application imports BetaModule from beta, gaining visibility of - // ExternalDir from alpha. - env.write('test.ts', ` + // The application imports BetaModule from beta, gaining visibility of + // ExternalDir from alpha. + env.write( + 'test.ts', + ` import {Component, NgModule} from '@angular/core'; import {BetaModule} from './beta'; @@ -6942,19 +7934,21 @@ function allTests(os: string) { imports: [BetaModule], }) export class Module {} - `); - env.driveMain(); - const jsContents = env.getContents('test.js'); - - // Expect that ExternalDir from alpha is imported via the re-export from beta. - expect(jsContents).toContain('import * as i1 from "root/alpha";'); - expect(jsContents).toContain('dependencies: [i1.ExternalDir]'); - }); - - it('should write alias ES2015 exports for NgModule exported directives when _useHostForImportAndAliasGeneration is set', - () => { - env.tsconfig({'_useHostForImportAndAliasGeneration': true}); - env.write('external.d.ts', ` + `, + ); + env.driveMain(); + const jsContents = env.getContents('test.js'); + + // Expect that ExternalDir from alpha is imported via the re-export from beta. + expect(jsContents).toContain('import * as i1 from "root/alpha";'); + expect(jsContents).toContain('dependencies: [i1.ExternalDir]'); + }); + + it('should write alias ES2015 exports for NgModule exported directives when _useHostForImportAndAliasGeneration is set', () => { + env.tsconfig({'_useHostForImportAndAliasGeneration': true}); + env.write( + 'external.d.ts', + ` import {ɵɵDirectiveDeclaration, ɵɵNgModuleDeclaration} from '@angular/core'; import {LibModule} from './lib'; @@ -6965,8 +7959,11 @@ function allTests(os: string) { export declare class ExternalModule { static ɵmod: ɵɵNgModuleDeclaration; } - `); - env.write('lib.d.ts', ` + `, + ); + env.write( + 'lib.d.ts', + ` import {ɵɵDirectiveDeclaration, ɵɵNgModuleDeclaration} from '@angular/core'; export declare class LibDir { @@ -6976,8 +7973,11 @@ function allTests(os: string) { export declare class LibModule { static ɵmod: ɵɵNgModuleDeclaration; } - `); - env.write('foo.ts', ` + `, + ); + env.write( + 'foo.ts', + ` import {Directive, NgModule} from '@angular/core'; import {ExternalModule} from './external'; @@ -6989,8 +7989,11 @@ function allTests(os: string) { exports: [FooDir, ExternalModule] }) export class FooModule {} - `); - env.write('index.ts', ` + `, + ); + env.write( + 'index.ts', + ` import {Component, NgModule} from '@angular/core'; import {FooModule} from './foo'; @@ -7005,17 +8008,20 @@ function allTests(os: string) { exports: [FooModule], }) export class IndexModule {} - `); - env.driveMain(); - const jsContents = env.getContents('index.js'); - expect(jsContents) - .toContain('export { FooDir as \u0275ng$root$foo$$FooDir } from "root/foo";'); - }); - - it('should not write alias ES2015 exports for NgModule exported directives when _useHostForImportGeneration is set', - () => { - env.tsconfig({'_useHostForImportGeneration': true}); - env.write('external.d.ts', ` + `, + ); + env.driveMain(); + const jsContents = env.getContents('index.js'); + expect(jsContents).toContain( + 'export { FooDir as \u0275ng$root$foo$$FooDir } from "root/foo";', + ); + }); + + it('should not write alias ES2015 exports for NgModule exported directives when _useHostForImportGeneration is set', () => { + env.tsconfig({'_useHostForImportGeneration': true}); + env.write( + 'external.d.ts', + ` import {ɵɵDirectiveDeclaration, ɵɵNgModuleDeclaration} from '@angular/core'; import {LibModule} from './lib'; @@ -7026,8 +8032,11 @@ function allTests(os: string) { export declare class ExternalModule { static ɵmod: ɵɵNgModuleDeclaration; } - `); - env.write('lib.d.ts', ` + `, + ); + env.write( + 'lib.d.ts', + ` import {ɵɵDirectiveDeclaration, ɵɵNgModuleDeclaration} from '@angular/core'; export declare class LibDir { @@ -7037,8 +8046,11 @@ function allTests(os: string) { export declare class LibModule { static ɵmod: ɵɵNgModuleDeclaration; } - `); - env.write('foo.ts', ` + `, + ); + env.write( + 'foo.ts', + ` import {Directive, NgModule} from '@angular/core'; import {ExternalModule} from './external'; @@ -7050,8 +8062,11 @@ function allTests(os: string) { exports: [FooDir, ExternalModule] }) export class FooModule {} - `); - env.write('index.ts', ` + `, + ); + env.write( + 'index.ts', + ` import {Component, NgModule} from '@angular/core'; import {FooModule} from './foo'; @@ -7066,17 +8081,20 @@ function allTests(os: string) { exports: [FooModule], }) export class IndexModule {} - `); - env.driveMain(); - const jsContents = env.getContents('index.js'); - expect(jsContents) - .not.toMatch( - /export\s+\{\s*FooDir\s+as\s+ \u0275ng$root$foo$$FooDir\s*\}\s+from\s+"root\/foo";/); - }); + `, + ); + env.driveMain(); + const jsContents = env.getContents('index.js'); + expect(jsContents).not.toMatch( + /export\s+\{\s*FooDir\s+as\s+ \u0275ng$root$foo$$FooDir\s*\}\s+from\s+"root\/foo";/, + ); + }); it('should escape unusual characters in aliased filenames', () => { env.tsconfig({'_useHostForImportAndAliasGeneration': true}); - env.write('other._$test.ts', ` + env.write( + 'other._$test.ts', + ` import {Directive, NgModule} from '@angular/core'; @Directive({selector: 'test'}) @@ -7087,8 +8105,11 @@ function allTests(os: string) { exports: [TestDir], }) export class OtherModule {} - `); - env.write('index.ts', ` + `, + ); + env.write( + 'index.ts', + ` import {NgModule} from '@angular/core'; import {OtherModule} from './other._$test'; @@ -7096,12 +8117,13 @@ function allTests(os: string) { exports: [OtherModule], }) export class IndexModule {} - `); + `, + ); env.driveMain(); const jsContents = env.getContents('index.js'); - expect(jsContents) - .toContain( - 'export { TestDir as \u0275ng$root$other___test$$TestDir } from "root/other._$test";'); + expect(jsContents).toContain( + 'export { TestDir as \u0275ng$root$other___test$$TestDir } from "root/other._$test";', + ); }); }); @@ -7149,7 +8171,9 @@ function allTests(os: string) { describe('inherited directives', () => { beforeEach(() => { - env.write('local.ts', ` + env.write( + 'local.ts', + ` import {Component, Directive, ElementRef} from '@angular/core'; export class BasePlain {} @@ -7172,9 +8196,12 @@ function allTests(os: string) { selector: '[base]', }) export class BaseDir {} - `); + `, + ); - env.write('lib.d.ts', ` + env.write( + 'lib.d.ts', + ` import {ɵɵComponentDeclaration, ɵɵDirectiveDeclaration, ElementRef} from '@angular/core'; export declare class BasePlain {} @@ -7194,12 +8221,15 @@ function allTests(os: string) { export declare class BaseDir { static ɵdir: ɵɵDirectiveDeclaration; } - `); + `, + ); }); it('should not error when inheriting a constructor from a decorated directive class', () => { env.tsconfig(); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Component} from '@angular/core'; import {BaseDir, BaseCmp} from './local'; @@ -7213,14 +8243,17 @@ function allTests(os: string) { template: 'TestCmp', }) export class Cmp extends BaseCmp {} - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(0); }); it('should not error when inheriting a constructor without parameters', () => { env.tsconfig(); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Component} from '@angular/core'; import {BasePlainWithBlankConstructor} from './local'; @@ -7234,14 +8267,17 @@ function allTests(os: string) { template: 'TestCmp', }) export class Cmp extends BasePlainWithBlankConstructor {} - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(0); }); it('should not error when inheriting from a class without a constructor', () => { env.tsconfig(); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Component} from '@angular/core'; import {BasePlain} from './local'; @@ -7255,14 +8291,17 @@ function allTests(os: string) { template: 'TestCmp', }) export class Cmp extends BasePlain {} - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(0); }); it('should error when inheriting a constructor from an undecorated class', () => { env.tsconfig(); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Component} from '@angular/core'; import {BasePlainWithConstructorParameters} from './local'; @@ -7276,7 +8315,8 @@ function allTests(os: string) { template: 'TestCmp', }) export class Cmp extends BasePlainWithConstructorParameters {} - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(2); expect(diags[0].messageText).toContain('Dir'); @@ -7287,7 +8327,9 @@ function allTests(os: string) { it('should error when inheriting a constructor from undecorated grand super class', () => { env.tsconfig(); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Component} from '@angular/core'; import {BasePlainWithConstructorParameters} from './local'; @@ -7303,7 +8345,8 @@ function allTests(os: string) { template: 'TestCmp', }) export class Cmp extends Parent {} - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(2); @@ -7313,10 +8356,11 @@ function allTests(os: string) { expect(diags[1].messageText).toContain('BasePlainWithConstructorParameters'); }); - it('should error when inheriting a constructor from undecorated grand grand super class', - () => { - env.tsconfig(); - env.write('test.ts', ` + it('should error when inheriting a constructor from undecorated grand grand super class', () => { + env.tsconfig(); + env.write( + 'test.ts', + ` import {Directive, Component} from '@angular/core'; import {BasePlainWithConstructorParameters} from './local'; @@ -7334,20 +8378,22 @@ function allTests(os: string) { template: 'TestCmp', }) export class Cmp extends Parent {} - `); - - const diags = env.driveDiagnostics(); - expect(diags.length).toBe(2); - expect(diags[0].messageText).toContain('Dir'); - expect(diags[0].messageText).toContain('BasePlainWithConstructorParameters'); - expect(diags[1].messageText).toContain('Cmp'); - expect(diags[1].messageText).toContain('BasePlainWithConstructorParameters'); - }); - - it('should not error when inheriting a constructor from decorated directive or component classes in a .d.ts file', - () => { - env.tsconfig(); - env.write('test.ts', ` + `, + ); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(2); + expect(diags[0].messageText).toContain('Dir'); + expect(diags[0].messageText).toContain('BasePlainWithConstructorParameters'); + expect(diags[1].messageText).toContain('Cmp'); + expect(diags[1].messageText).toContain('BasePlainWithConstructorParameters'); + }); + + it('should not error when inheriting a constructor from decorated directive or component classes in a .d.ts file', () => { + env.tsconfig(); + env.write( + 'test.ts', + ` import {Component, Directive} from '@angular/core'; import {BaseDir, BaseCmp} from './lib'; @@ -7361,15 +8407,17 @@ function allTests(os: string) { template: 'TestCmp', }) export class Cmp extends BaseCmp {} - `); - const diags = env.driveDiagnostics(); - expect(diags.length).toBe(0); - }); - - it('should error when inheriting a constructor from an undecorated class in a .d.ts file', - () => { - env.tsconfig(); - env.write('test.ts', ` + `, + ); + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(0); + }); + + it('should error when inheriting a constructor from an undecorated class in a .d.ts file', () => { + env.tsconfig(); + env.write( + 'test.ts', + ` import {Directive} from '@angular/core'; import {BasePlainWithConstructorParameters} from './lib'; @@ -7378,16 +8426,19 @@ function allTests(os: string) { selector: '[dir]', }) export class Dir extends BasePlainWithConstructorParameters {} - `); - const diags = env.driveDiagnostics(); - expect(diags.length).toBe(1); - expect(diags[0].messageText).toContain('Dir'); - expect(diags[0].messageText).toContain('Base'); - }); + `, + ); + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].messageText).toContain('Dir'); + expect(diags[0].messageText).toContain('Base'); + }); it('should produce a diagnostic if an inherited required input is not bound', () => { env.tsconfig(); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Component, Input} from '@angular/core'; @Directive() @@ -7408,16 +8459,20 @@ function allTests(os: string) { imports: [Dir] }) export class Cmp {} - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(1); - expect(diags[0].messageText) - .toBe(`Required input 'input' from directive Dir must be specified.`); + expect(diags[0].messageText).toBe( + `Required input 'input' from directive Dir must be specified.`, + ); }); it('should not produce a diagnostic if an inherited required input is bound', () => { env.tsconfig(); - env.write('test.ts', ` + env.write( + 'test.ts', + ` import {Directive, Component, Input} from '@angular/core'; @Directive() @@ -7440,7 +8495,8 @@ function allTests(os: string) { export class Cmp { value = 123; } - `); + `, + ); const diags = env.driveDiagnostics(); expect(diags.length).toBe(0); }); @@ -7448,7 +8504,9 @@ function allTests(os: string) { describe('inline resources', () => { it('should process inline