From 252e3e948781cdd2efb7af46ec2988add341b3d3 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Mon, 9 Dec 2019 15:22:59 -0800 Subject: [PATCH] refactor(ivy): formalize the compilation process for matched handlers (#34288) Prior to this commit, the `IvyCompilation` tracked the state of each matched `DecoratorHandler` on each class in the `ts.Program`, and how they progressed through the compilation process. This tracking was originally simple, but had grown more complicated as the compiler evolved. The state of each specific "target" of compilation was determined by the nullability of a number of fields on the object which tracked it. This commit formalizes the process of compilation of each matched handler into a new "trait" concept. A trait is some aspect of a class which gets created when a `DecoratorHandler` matches the class. It represents an Ivy aspect that needs to go through the compilation process. Traits begin in a "pending" state and undergo transitions as various steps of compilation take place. The `IvyCompilation` class is renamed to the `TraitCompiler`, which manages the state of all of the traits in the active program. Making the trait concept explicit will support future work to incrementalize the expensive analysis process of compilation. PR Close #34288 --- .../ngcc/src/analysis/decoration_analyzer.ts | 23 +- .../ngcc/src/analysis/migration_host.ts | 3 +- .../compiler-cli/ngcc/src/analysis/types.ts | 12 +- .../compiler-cli/ngcc/src/analysis/util.ts | 48 +- .../test/analysis/decoration_analyzer_spec.ts | 6 +- .../ngcc/test/analysis/migration_host_spec.ts | 8 +- .../src/ngtsc/annotations/src/component.ts | 71 +- .../src/ngtsc/annotations/src/directive.ts | 11 +- .../src/ngtsc/annotations/src/injectable.ts | 7 +- .../src/ngtsc/annotations/src/ng_module.ts | 10 +- .../src/ngtsc/annotations/src/pipe.ts | 7 +- packages/compiler-cli/src/ngtsc/program.ts | 60 +- .../compiler-cli/src/ngtsc/transform/index.ts | 2 +- .../src/ngtsc/transform/src/api.ts | 37 +- .../src/ngtsc/transform/src/compilation.ts | 663 ++++++++++-------- .../src/ngtsc/transform/src/trait.ts | 266 +++++++ .../src/ngtsc/transform/src/transform.ts | 14 +- .../src/ngtsc/util/src/typescript.ts | 5 + 18 files changed, 825 insertions(+), 428 deletions(-) create mode 100644 packages/compiler-cli/src/ngtsc/transform/src/trait.ts diff --git a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts index 39a5d1cdd7..26df1db3f7 100644 --- a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts +++ b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts @@ -83,7 +83,7 @@ export class DecorationAnalyzer { moduleResolver = new ModuleResolver(this.program, this.options, this.host); importGraph = new ImportGraph(this.moduleResolver); cycleAnalyzer = new CycleAnalyzer(this.importGraph); - handlers: DecoratorHandler[] = [ + handlers: DecoratorHandler[] = [ new ComponentDecoratorHandler( this.reflectionHost, this.evaluator, this.fullRegistry, this.fullMetaReader, this.scopeRegistry, this.scopeRegistry, this.isCore, this.resourceManager, this.rootDirs, @@ -91,9 +91,12 @@ export class DecorationAnalyzer { /* i18nUseExternalIds */ true, this.bundle.enableI18nLegacyMessageIdFormat, this.moduleResolver, this.cycleAnalyzer, this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER, /* annotateForClosureCompiler */ false), + // clang-format off + // See the note in ngtsc about why this cast is needed. new DirectiveDecoratorHandler( this.reflectionHost, this.evaluator, this.fullRegistry, NOOP_DEFAULT_IMPORT_RECORDER, - this.isCore, /* annotateForClosureCompiler */ false), + this.isCore, /* annotateForClosureCompiler */ false) as DecoratorHandler, + // 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( @@ -195,8 +198,8 @@ export class DecorationAnalyzer { protected compileClass(clazz: AnalyzedClass, constantPool: ConstantPool): CompileResult[] { const compilations: CompileResult[] = []; - for (const {handler, analysis} of clazz.matches) { - const result = handler.compile(clazz.declaration, analysis, constantPool); + for (const {handler, analysis, resolution} of clazz.matches) { + const result = handler.compile(clazz.declaration, analysis, resolution, constantPool); if (Array.isArray(result)) { result.forEach(current => { if (!compilations.some(compilation => compilation.name === current.name)) { @@ -211,19 +214,21 @@ export class DecorationAnalyzer { } protected resolveFile(analyzedFile: AnalyzedFile): void { - analyzedFile.analyzedClasses.forEach(({declaration, matches}) => { - matches.forEach(({handler, analysis}) => { + for (const {declaration, matches} of analyzedFile.analyzedClasses) { + for (const match of matches) { + const {handler, analysis} = match; if ((handler.resolve !== undefined) && analysis) { - const {reexports, diagnostics} = handler.resolve(declaration, analysis); + const {reexports, diagnostics, data} = handler.resolve(declaration, analysis); if (reexports !== undefined) { this.addReexports(reexports, declaration); } if (diagnostics !== undefined) { diagnostics.forEach(error => this.diagnosticHandler(error)); } + match.resolution = data as Readonly; } - }); - }); + } + } } private getReexportsForClass(declaration: ClassDeclaration) { diff --git a/packages/compiler-cli/ngcc/src/analysis/migration_host.ts b/packages/compiler-cli/ngcc/src/analysis/migration_host.ts index 7d12989afc..9ed169ece2 100644 --- a/packages/compiler-cli/ngcc/src/analysis/migration_host.ts +++ b/packages/compiler-cli/ngcc/src/analysis/migration_host.ts @@ -26,7 +26,8 @@ import {analyzeDecorators, isWithinPackage} from './util'; export class DefaultMigrationHost implements MigrationHost { constructor( readonly reflectionHost: NgccReflectionHost, readonly metadata: MetadataReader, - readonly evaluator: PartialEvaluator, private handlers: DecoratorHandler[], + readonly evaluator: PartialEvaluator, + private handlers: DecoratorHandler[], private entryPointPath: AbsoluteFsPath, private analyzedFiles: AnalyzedFile[], private diagnosticHandler: (error: ts.Diagnostic) => void) {} diff --git a/packages/compiler-cli/ngcc/src/analysis/types.ts b/packages/compiler-cli/ngcc/src/analysis/types.ts index 84d3faae4e..fd4859ea3b 100644 --- a/packages/compiler-cli/ngcc/src/analysis/types.ts +++ b/packages/compiler-cli/ngcc/src/analysis/types.ts @@ -9,7 +9,7 @@ import {ConstantPool} from '@angular/compiler'; import * as ts from 'typescript'; import {Reexport} from '../../../src/ngtsc/imports'; import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection'; -import {CompileResult, DecoratorHandler} from '../../../src/ngtsc/transform'; +import {CompileResult, DecoratorHandler, DetectResult} from '../../../src/ngtsc/transform'; export interface AnalyzedFile { sourceFile: ts.SourceFile; @@ -21,7 +21,7 @@ export interface AnalyzedClass { decorators: Decorator[]|null; declaration: ClassDeclaration; diagnostics?: ts.Diagnostic[]; - matches: {handler: DecoratorHandler; analysis: any;}[]; + matches: MatchingHandler[]; } export interface CompiledClass extends AnalyzedClass { @@ -42,7 +42,9 @@ export interface CompiledFile { export type DecorationAnalyses = Map; export const DecorationAnalyses = Map; -export interface MatchingHandler { - handler: DecoratorHandler; - detected: M; +export interface MatchingHandler { + handler: DecoratorHandler; + detected: DetectResult; + analysis: Readonly; + resolution: Readonly; } diff --git a/packages/compiler-cli/ngcc/src/analysis/util.ts b/packages/compiler-cli/ngcc/src/analysis/util.ts index 66530ed4af..7e04b25373 100644 --- a/packages/compiler-cli/ngcc/src/analysis/util.ts +++ b/packages/compiler-cli/ngcc/src/analysis/util.ts @@ -19,26 +19,37 @@ export function isWithinPackage(packagePath: AbsoluteFsPath, sourceFile: ts.Sour return !relative(packagePath, absoluteFromSourceFile(sourceFile)).startsWith('..'); } +const NOT_YET_KNOWN: Readonly = null as unknown as Readonly; + export function analyzeDecorators( classSymbol: NgccClassSymbol, decorators: Decorator[] | null, - handlers: DecoratorHandler[], flags?: HandlerFlags): AnalyzedClass|null { + handlers: DecoratorHandler[], flags?: HandlerFlags): AnalyzedClass| + null { const declaration = classSymbol.declaration.valueDeclaration; - const matchingHandlers = handlers - .map(handler => { - const detected = handler.detect(declaration, decorators); - return {handler, detected}; - }) - .filter(isMatchingHandler); + const matchingHandlers: MatchingHandler[] = []; + for (const handler of handlers) { + const detected = handler.detect(declaration, decorators); + if (detected !== undefined) { + matchingHandlers.push({ + handler, + detected, + analysis: NOT_YET_KNOWN, + resolution: NOT_YET_KNOWN, + }); + } + } if (matchingHandlers.length === 0) { return null; } - const detections: {handler: DecoratorHandler, detected: DetectResult}[] = []; + + const detections: MatchingHandler[] = []; let hasWeakHandler: boolean = false; let hasNonWeakHandler: boolean = false; let hasPrimaryHandler: boolean = false; - for (const {handler, detected} of matchingHandlers) { + for (const match of matchingHandlers) { + const {handler} = match; if (hasNonWeakHandler && handler.precedence === HandlerPrecedence.WEAK) { continue; } else if (hasWeakHandler && handler.precedence !== HandlerPrecedence.WEAK) { @@ -49,7 +60,7 @@ export function analyzeDecorators( throw new Error(`TODO.Diagnostic: Class has multiple incompatible Angular decorators.`); } - detections.push({handler, detected}); + detections.push(match); if (handler.precedence === HandlerPrecedence.WEAK) { hasWeakHandler = true; } else if (handler.precedence === HandlerPrecedence.SHARED) { @@ -60,15 +71,17 @@ export function analyzeDecorators( } } - const matches: {handler: DecoratorHandler, analysis: any}[] = []; + const matches: MatchingHandler[] = []; const allDiagnostics: ts.Diagnostic[] = []; - for (const {handler, detected} of detections) { + for (const match of detections) { try { - const {analysis, diagnostics} = handler.analyze(declaration, detected.metadata, flags); + const {analysis, diagnostics} = + match.handler.analyze(declaration, match.detected.metadata, flags); if (diagnostics !== undefined) { allDiagnostics.push(...diagnostics); } - matches.push({handler, analysis}); + match.analysis = analysis !; + matches.push(match); } catch (e) { if (isFatalDiagnosticError(e)) { allDiagnostics.push(e.toDiagnostic()); @@ -82,11 +95,6 @@ export function analyzeDecorators( declaration, decorators, matches, - diagnostics: allDiagnostics.length > 0 ? allDiagnostics : undefined + diagnostics: allDiagnostics.length > 0 ? allDiagnostics : undefined, }; } - -function isMatchingHandler(handler: Partial>): - handler is MatchingHandler { - return !!handler.detected; -} diff --git a/packages/compiler-cli/ngcc/test/analysis/decoration_analyzer_spec.ts b/packages/compiler-cli/ngcc/test/analysis/decoration_analyzer_spec.ts index 968e5fdd4e..572f26e10c 100644 --- a/packages/compiler-cli/ngcc/test/analysis/decoration_analyzer_spec.ts +++ b/packages/compiler-cli/ngcc/test/analysis/decoration_analyzer_spec.ts @@ -21,8 +21,8 @@ import {Migration, MigrationHost} from '../../src/migrations/migration'; import {MockLogger} from '../helpers/mock_logger'; import {getRootFiles, makeTestEntryPointBundle} from '../helpers/utils'; -type DecoratorHandlerWithResolve = DecoratorHandler& { - resolve: NonNullable['resolve']>; +type DecoratorHandlerWithResolve = DecoratorHandler& { + resolve: NonNullable['resolve']>; }; runInEachFileSystem(() => { @@ -49,7 +49,7 @@ runInEachFileSystem(() => { ]); // Only detect the Component and Directive decorators handler.detect.and.callFake( - (node: ts.Declaration, decorators: Decorator[] | null): DetectResult| + (node: ts.Declaration, decorators: Decorator[] | null): DetectResult| undefined => { const className = (node as any).name.text; if (decorators === null) { diff --git a/packages/compiler-cli/ngcc/test/analysis/migration_host_spec.ts b/packages/compiler-cli/ngcc/test/analysis/migration_host_spec.ts index f01eea01df..0629e7f7b4 100644 --- a/packages/compiler-cli/ngcc/test/analysis/migration_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/analysis/migration_host_spec.ts @@ -297,15 +297,15 @@ runInEachFileSystem(() => { }); }); -class TestHandler implements DecoratorHandler { +class TestHandler implements DecoratorHandler { constructor(protected name: string, protected log: string[]) {} precedence = HandlerPrecedence.PRIMARY; - detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult|undefined { + detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult|undefined { this.log.push(`${this.name}:detect:${node.name.text}:${decorators !.map(d => d.name)}`); return undefined; } - analyze(node: ClassDeclaration): AnalysisOutput { + analyze(node: ClassDeclaration): AnalysisOutput { this.log.push(this.name + ':analyze:' + node.name.text); return {}; } @@ -316,7 +316,7 @@ class TestHandler implements DecoratorHandler { } class AlwaysDetectHandler extends TestHandler { - detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult|undefined { + detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult|undefined { super.detect(node, decorators); return {trigger: node, metadata: {}}; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index ead2eb8ab0..7637299f36 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -23,6 +23,7 @@ import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerFl import {TemplateSourceMapping, TypeCheckContext} from '../../typecheck'; import {NoopResourceDependencyRecorder, ResourceDependencyRecorder} from '../../util/src/resource_recorder'; import {tsSourceMapBug29300Fixed} from '../../util/src/ts_source_map_bug_29300'; +import {SubsetOfKeys} from '../../util/src/typescript'; import {ResourceLoader} from './api'; import {extractDirectiveMetadata, parseFieldArrayValue} from './directive'; @@ -33,19 +34,34 @@ import {findAngularDecorator, isAngularCoreReference, isExpressionForwardReferen const EMPTY_MAP = new Map(); const EMPTY_ARRAY: any[] = []; -export interface ComponentHandlerData { - meta: R3ComponentMetadata; +/** + * These fields of `R3ComponentMetadata` are updated in the `resolve` phase. + * + * The `keyof R3ComponentMetadata &` condition ensures that only fields of `R3ComponentMetadata` can + * be included here. + */ +export type ComponentMetadataResolvedFields = + SubsetOfKeys; + +export interface ComponentAnalysisData { + /** + * `meta` includes those fields of `R3ComponentMetadata` which are calculated at `analyze` time + * (not during resolve). + */ + meta: Omit; parsedTemplate: ParsedTemplate; templateSourceMapping: TemplateSourceMapping; metadataStmt: Statement|null; parseTemplate: (options?: ParseTemplateOptions) => ParsedTemplate; } +export type ComponentResolutionData = Pick; + /** * `DecoratorHandler` which handles the `@Component` annotation. */ export class ComponentDecoratorHandler implements - DecoratorHandler { + DecoratorHandler { constructor( private reflector: ReflectionHost, private evaluator: PartialEvaluator, private metaRegistry: MetadataRegistry, private metaReader: MetadataReader, @@ -86,7 +102,7 @@ export class ComponentDecoratorHandler implements } } - preanalyze(node: ClassDeclaration, decorator: Decorator): 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: @@ -138,8 +154,9 @@ export class ComponentDecoratorHandler implements } } - analyze(node: ClassDeclaration, decorator: Decorator, flags: HandlerFlags = HandlerFlags.NONE): - AnalysisOutput { + analyze( + node: ClassDeclaration, decorator: Readonly, + flags: HandlerFlags = HandlerFlags.NONE): AnalysisOutput { const containingFile = node.getSourceFile().fileName; this.literalCache.delete(decorator); @@ -283,7 +300,7 @@ export class ComponentDecoratorHandler implements animations = new WrappedNodeExpr(component.get('animations') !); } - const output = { + const output: AnalysisOutput = { analysis: { meta: { ...metadata, @@ -294,12 +311,9 @@ export class ComponentDecoratorHandler implements // These will be replaced during the compilation step, after all `NgModule`s have been // analyzed and the full compilation scope for the component can be realized. - pipes: EMPTY_MAP, - directives: EMPTY_ARRAY, - wrapDirectivesAndPipesInClosure: false, // animations, viewProviders, - i18nUseExternalIds: this.i18nUseExternalIds, relativeContextFilePath + i18nUseExternalIds: this.i18nUseExternalIds, relativeContextFilePath, }, metadataStmt: generateSetClassMetadataCall( node, this.reflector, this.defaultImportRecorder, this.isCore, @@ -309,12 +323,13 @@ export class ComponentDecoratorHandler implements typeCheck: true, }; if (changeDetection !== null) { - (output.analysis.meta as R3ComponentMetadata).changeDetection = changeDetection; + output.analysis !.meta.changeDetection = changeDetection; } return output; } - index(context: IndexingContext, node: ClassDeclaration, analysis: ComponentHandlerData) { + index( + context: IndexingContext, node: ClassDeclaration, analysis: Readonly) { // The component template may have been previously parsed without preserving whitespace or with // `leadingTriviaChar`s, both of which may manipulate the AST into a form not representative of // the source code, making it unsuitable for indexing. The template is reparsed with preserving @@ -347,7 +362,8 @@ export class ComponentDecoratorHandler implements }); } - typeCheck(ctx: TypeCheckContext, node: ClassDeclaration, meta: ComponentHandlerData): void { + typeCheck(ctx: TypeCheckContext, node: ClassDeclaration, meta: Readonly): + void { if (!ts.isClassDeclaration(node)) { return; } @@ -393,12 +409,20 @@ export class ComponentDecoratorHandler implements new Reference(node), bound, pipes, schemas, meta.templateSourceMapping, template.file); } - resolve(node: ClassDeclaration, analysis: ComponentHandlerData): ResolveResult { + resolve(node: ClassDeclaration, analysis: Readonly): + ResolveResult { const context = node.getSourceFile(); // Check whether this component was registered with an NgModule. If so, it should be compiled // under that module's compilation scope. const scope = this.scopeReader.getScopeForComponent(node); - let metadata = analysis.meta; + let metadata = analysis.meta as Readonly; + + const data: ComponentResolutionData = { + directives: EMPTY_ARRAY, + pipes: EMPTY_MAP, + wrapDirectivesAndPipesInClosure: false, + }; + if (scope !== null) { // Replace the empty components and directives from the analyze() step with a fully expanded // scope. This is possible now because during resolve() the whole compilation unit has been @@ -479,9 +503,9 @@ export class ComponentDecoratorHandler implements // actually used (though the two should agree perfectly). // // TODO(alxhub): switch TemplateDefinitionBuilder over to using R3TargetBinder directly. - metadata.directives = directives; - metadata.pipes = pipes; - metadata.wrapDirectivesAndPipesInClosure = wrapDirectivesAndPipesInClosure; + data.directives = directives; + data.pipes = pipes; + data.wrapDirectivesAndPipesInClosure = wrapDirectivesAndPipesInClosure; } else { // Declaring the directiveDefs/pipeDefs arrays directly would require imports that would // create a cycle. Instead, mark this component as requiring remote scoping, so that the @@ -489,12 +513,13 @@ export class ComponentDecoratorHandler implements this.scopeRegistry.setComponentAsRequiringRemoteScoping(node); } } - return {}; + return {data}; } - compile(node: ClassDeclaration, analysis: ComponentHandlerData, pool: ConstantPool): - CompileResult[] { - const meta = analysis.meta; + compile( + node: ClassDeclaration, analysis: Readonly, + resolution: Readonly, pool: ConstantPool): CompileResult[] { + const meta: R3ComponentMetadata = {...analysis.meta, ...resolution}; const res = compileComponentFromMetadata(meta, pool, makeBindingParser()); const factoryRes = compileNgFactoryDefField( {...meta, injectFn: Identifiers.directiveInject, target: R3FactoryTarget.Component}); diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index 38688f5ba8..66a26c5f69 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -36,7 +36,7 @@ export interface DirectiveHandlerData { metadataStmt: Statement|null; } export class DirectiveDecoratorHandler implements - DecoratorHandler { + DecoratorHandler { constructor( private reflector: ReflectionHost, private evaluator: PartialEvaluator, private metaRegistry: MetadataRegistry, private defaultImportRecorder: DefaultImportRecorder, @@ -72,7 +72,7 @@ export class DirectiveDecoratorHandler implements } } - analyze(node: ClassDeclaration, decorator: Decorator|null, flags = HandlerFlags.NONE): + analyze(node: ClassDeclaration, decorator: Readonly, flags = HandlerFlags.NONE): AnalysisOutput { const directiveResult = extractDirectiveMetadata( node, decorator, this.reflector, this.evaluator, this.defaultImportRecorder, this.isCore, @@ -108,8 +108,9 @@ export class DirectiveDecoratorHandler implements }; } - compile(node: ClassDeclaration, analysis: DirectiveHandlerData, pool: ConstantPool): - CompileResult[] { + compile( + node: ClassDeclaration, analysis: Readonly, + resolution: Readonly, pool: ConstantPool): CompileResult[] { const meta = analysis.meta; const res = compileDirectiveFromMetadata(meta, pool, makeBindingParser()); const factoryRes = compileNgFactoryDefField( @@ -135,7 +136,7 @@ export class DirectiveDecoratorHandler implements * the module. */ export function extractDirectiveMetadata( - clazz: ClassDeclaration, decorator: Decorator | null, reflector: ReflectionHost, + clazz: ClassDeclaration, decorator: Readonly, reflector: ReflectionHost, evaluator: PartialEvaluator, defaultImportRecorder: DefaultImportRecorder, isCore: boolean, flags: HandlerFlags, annotateForClosureCompiler: boolean, defaultSelector: string | null = null): { diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts index a55662ddd6..6a7ac9ecc1 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts @@ -29,7 +29,7 @@ export interface InjectableHandlerData { * Adapts the `compileIvyInjectable` compiler for `@Injectable` decorators to the Ivy compiler. */ export class InjectableDecoratorHandler implements - DecoratorHandler { + DecoratorHandler { constructor( private reflector: ReflectionHost, private defaultImportRecorder: DefaultImportRecorder, private isCore: boolean, private strictCtorDeps: boolean, @@ -58,7 +58,8 @@ export class InjectableDecoratorHandler implements } } - analyze(node: ClassDeclaration, decorator: Decorator): AnalysisOutput { + analyze(node: ClassDeclaration, decorator: Readonly): + AnalysisOutput { const meta = extractInjectableMetadata(node, decorator, this.reflector); const decorators = this.reflector.getDecoratorsOfDeclaration(node); @@ -78,7 +79,7 @@ export class InjectableDecoratorHandler implements }; } - compile(node: ClassDeclaration, analysis: InjectableHandlerData): CompileResult[] { + compile(node: ClassDeclaration, analysis: Readonly): CompileResult[] { const res = compileIvyInjectable(analysis.meta); const statements = res.statements; const results: CompileResult[] = []; diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts index 3268872c30..8ad262d966 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts @@ -37,7 +37,8 @@ export interface NgModuleAnalysis { * * TODO(alxhub): handle injector side of things as well. */ -export class NgModuleDecoratorHandler implements DecoratorHandler { +export class NgModuleDecoratorHandler implements + DecoratorHandler { constructor( private reflector: ReflectionHost, private evaluator: PartialEvaluator, private metaReader: MetadataReader, private metaRegistry: MetadataRegistry, @@ -64,7 +65,8 @@ export class NgModuleDecoratorHandler implements DecoratorHandler { + analyze(node: ClassDeclaration, decorator: Readonly): + AnalysisOutput { const name = node.name.text; if (decorator.args === null || decorator.args.length > 1) { throw new FatalDiagnosticError( @@ -256,7 +258,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler): ResolveResult { const scope = this.scopeRegistry.getScopeOfModule(node); const diagnostics = this.scopeRegistry.getDiagnosticsOfModule(node) || undefined; @@ -291,7 +293,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler): CompileResult[] { const ngInjectorDef = compileInjector(analysis.inj); const ngModuleDef = compileNgModule(analysis.mod); const ngModuleStatements = ngModuleDef.additionalStatements; diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts b/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts index 85c9289a01..1cd5b78d71 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts @@ -25,7 +25,7 @@ export interface PipeHandlerData { metadataStmt: Statement|null; } -export class PipeDecoratorHandler implements DecoratorHandler { +export class PipeDecoratorHandler implements DecoratorHandler { constructor( private reflector: ReflectionHost, private evaluator: PartialEvaluator, private metaRegistry: MetadataRegistry, private defaultImportRecorder: DefaultImportRecorder, @@ -48,7 +48,8 @@ export class PipeDecoratorHandler implements DecoratorHandler { + analyze(clazz: ClassDeclaration, decorator: Readonly): + AnalysisOutput { const name = clazz.name.text; const type = new WrappedNodeExpr(clazz.name); const internalType = new WrappedNodeExpr(this.reflector.getInternalNameOfClass(clazz)); @@ -110,7 +111,7 @@ export class PipeDecoratorHandler implements DecoratorHandler): CompileResult[] { const meta = analysis.meta; const res = compilePipeFromMetadata(meta); const factoryRes = compileNgFactoryDefField({ diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index de99bc56c0..858b44485a 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -32,7 +32,7 @@ import {NgModuleRouteAnalyzer, entryPointKeyFor} from './routing'; import {ComponentScopeReader, CompoundComponentScopeReader, LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from './scope'; import {FactoryGenerator, FactoryInfo, GeneratedShimsHostWrapper, ShimGenerator, SummaryGenerator, TypeCheckShimGenerator, generatedFactoryTransform} from './shims'; import {ivySwitchTransform} from './switch'; -import {DtsTransformRegistry, IvyCompilation, declarationTransformFactory, ivyTransformFactory} from './transform'; +import {DecoratorHandler, DtsTransformRegistry, TraitCompiler, declarationTransformFactory, ivyTransformFactory} from './transform'; import {aliasTransformFactory} from './transform/src/alias'; import {TypeCheckContext, TypeCheckingConfig, typeCheckFilePath} from './typecheck'; import {normalizeSeparators} from './util/src/path'; @@ -42,7 +42,7 @@ export class NgtscProgram implements api.Program { private tsProgram: ts.Program; private reuseTsProgram: ts.Program; private resourceManager: HostResourceLoader; - private compilation: IvyCompilation|undefined = undefined; + private compilation: TraitCompiler|undefined = undefined; private factoryToSourceInfo: Map|null = null; private sourceToFactorySymbols: Map>|null = null; private _coreImportsFrom: ts.SourceFile|null|undefined = undefined; @@ -239,21 +239,26 @@ export class NgtscProgram implements api.Program { this.compilation = this.makeCompilation(); } const analyzeSpan = this.perfRecorder.start('analyze'); - await Promise.all(this.tsProgram.getSourceFiles() - .filter(file => !file.fileName.endsWith('.d.ts')) - .map(file => { + const promises: Promise[] = []; + for (const sf of this.tsProgram.getSourceFiles()) { + if (sf.isDeclarationFile) { + continue; + } + + const analyzeFileSpan = this.perfRecorder.start('analyzeFile', sf); + let analysisPromise = this.compilation !.analyzeAsync(sf); + if (analysisPromise === undefined) { + this.perfRecorder.stop(analyzeFileSpan); + } else if (this.perfRecorder.enabled) { + analysisPromise = analysisPromise.then(() => this.perfRecorder.stop(analyzeFileSpan)); + } + if (analysisPromise !== undefined) { + promises.push(analysisPromise); + } + } + + await Promise.all(promises); - const analyzeFileSpan = this.perfRecorder.start('analyzeFile', file); - let analysisPromise = this.compilation !.analyzeAsync(file); - if (analysisPromise === undefined) { - this.perfRecorder.stop(analyzeFileSpan); - } else if (this.perfRecorder.enabled) { - analysisPromise = analysisPromise.then( - () => this.perfRecorder.stop(analyzeFileSpan)); - } - return analysisPromise; - }) - .filter((result): result is Promise => result !== undefined)); this.perfRecorder.stop(analyzeSpan); this.compilation.resolve(); @@ -311,15 +316,18 @@ export class NgtscProgram implements api.Program { throw new Error('Method not implemented.'); } - private ensureAnalyzed(): IvyCompilation { + private ensureAnalyzed(): TraitCompiler { if (this.compilation === undefined) { const analyzeSpan = this.perfRecorder.start('analyze'); this.compilation = this.makeCompilation(); - this.tsProgram.getSourceFiles().filter(file => !file.isDeclarationFile).forEach(file => { - const analyzeFileSpan = this.perfRecorder.start('analyzeFile', file); - this.compilation !.analyzeSync(file); + for (const sf of this.tsProgram.getSourceFiles()) { + if (sf.isDeclarationFile) { + continue; + } + const analyzeFileSpan = this.perfRecorder.start('analyzeFile', sf); + this.compilation !.analyzeSync(sf); this.perfRecorder.stop(analyzeFileSpan); - }); + } this.perfRecorder.stop(analyzeSpan); this.compilation.resolve(); @@ -538,7 +546,7 @@ export class NgtscProgram implements api.Program { return generateAnalysis(context); } - private makeCompilation(): IvyCompilation { + private makeCompilation(): TraitCompiler { const checker = this.tsProgram.getTypeChecker(); // Construct the ReferenceEmitter. @@ -627,7 +635,7 @@ export class NgtscProgram implements api.Program { this.mwpScanner = new ModuleWithProvidersScanner(this.reflector, evaluator, this.refEmitter); // Set up the IvyCompilation, which manages state for the Ivy transformer. - const handlers = [ + const handlers: DecoratorHandler[] = [ new ComponentDecoratorHandler( this.reflector, evaluator, metaRegistry, this.metaReader !, scopeReader, scopeRegistry, this.isCore, this.resourceManager, this.rootDirs, @@ -635,9 +643,11 @@ export class NgtscProgram implements api.Program { this.options.enableI18nLegacyMessageIdFormat !== false, this.moduleResolver, this.cycleAnalyzer, this.refEmitter, this.defaultImportTracker, this.closureCompilerEnabled, this.incrementalDriver), + // TODO(alxhub): understand why the cast here is necessary (something to do with `null` not + // being assignable to `unknown` when wrapped in `Readonly`). new DirectiveDecoratorHandler( this.reflector, evaluator, metaRegistry, this.defaultImportTracker, this.isCore, - this.closureCompilerEnabled), + this.closureCompilerEnabled) as Readonly>, // 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( @@ -651,7 +661,7 @@ export class NgtscProgram implements api.Program { this.defaultImportTracker, this.closureCompilerEnabled, this.options.i18nInLocale), ]; - return new IvyCompilation( + return new TraitCompiler( handlers, this.reflector, this.importRewriter, this.incrementalDriver, this.perfRecorder, this.sourceToFactorySymbols, scopeRegistry, this.options.compileNonExportedClasses !== false, this.dtsTransforms, this.mwpScanner); diff --git a/packages/compiler-cli/src/ngtsc/transform/index.ts b/packages/compiler-cli/src/ngtsc/transform/index.ts index d02cef8ea9..94e032c31f 100644 --- a/packages/compiler-cli/src/ngtsc/transform/index.ts +++ b/packages/compiler-cli/src/ngtsc/transform/index.ts @@ -7,6 +7,6 @@ */ export * from './src/api'; -export {IvyCompilation} from './src/compilation'; +export {TraitCompiler} from './src/compilation'; export {declarationTransformFactory, DtsTransformRegistry, IvyDeclarationDtsTransform, ReturnTypeTransform} from './src/declaration'; export {ivyTransformFactory} from './src/transform'; diff --git a/packages/compiler-cli/src/ngtsc/transform/src/api.ts b/packages/compiler-cli/src/ngtsc/transform/src/api.ts index 1aeb07b3a8..4dc1745fe9 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/api.ts @@ -67,8 +67,12 @@ export enum HandlerFlags { * The decorator compilers in @angular/compiler do not depend on Typescript. The handler is * responsible for extracting the information required to perform compilation from the decorators * and Typescript source, invoking the decorator compiler, and returning the result. + * + * @param `D` The type of decorator metadata produced by `detect`. + * @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 { /** * The precedence of a handler controls how it interacts with other handlers that match the same * class. @@ -81,30 +85,33 @@ 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. * - * `preAnalyze` is optional and is not guaranteed to be called through all compilation flows. It + * `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: M): Promise|undefined; + preanalyze?(node: ClassDeclaration, metadata: Readonly): Promise|undefined; /** * Perform analysis on the decorator/class combination, producing instructions for compilation * if successful, or an array of diagnostic messages if the analysis fails or the decorator * isn't valid. */ - analyze(node: ClassDeclaration, metadata: M, handlerFlags?: HandlerFlags): AnalysisOutput; + analyze(node: ClassDeclaration, metadata: Readonly, handlerFlags?: HandlerFlags): + AnalysisOutput; /** * Registers information about the decorator for the indexing phase in a * `IndexingContext`, which stores information about components discovered in the * program. */ - index?(context: IndexingContext, node: ClassDeclaration, metadata: A): void; + index? + (context: IndexingContext, node: ClassDeclaration, analysis: Readonly, + resolution: Readonly): void; /** * Perform resolution on the given decorator along with the result of analysis. @@ -113,21 +120,24 @@ export interface DecoratorHandler { * `DecoratorHandler` a chance to leverage information from the whole compilation unit to enhance * the `analysis` before the emit phase. */ - resolve?(node: ClassDeclaration, analysis: A): ResolveResult; + resolve?(node: ClassDeclaration, analysis: Readonly): ResolveResult; - typeCheck?(ctx: TypeCheckContext, node: ClassDeclaration, metadata: A): void; + typeCheck? + (ctx: TypeCheckContext, node: ClassDeclaration, analysis: Readonly, + resolution: Readonly): void; /** * Generate a description of the field which should be added to the class, including any * initialization code to be generated. */ - compile(node: ClassDeclaration, analysis: A, constantPool: ConstantPool): CompileResult - |CompileResult[]; + compile( + node: ClassDeclaration, analysis: Readonly, resolution: Readonly, + constantPool: ConstantPool): CompileResult|CompileResult[]; } export interface DetectResult { trigger: ts.Node|null; - metadata: M; + metadata: Readonly; } /** @@ -136,7 +146,7 @@ export interface DetectResult { * analysis. */ export interface AnalysisOutput { - analysis?: A; + analysis?: Readonly; diagnostics?: ts.Diagnostic[]; factorySymbolName?: string; typeCheck?: boolean; @@ -153,9 +163,10 @@ export interface CompileResult { type: Type; } -export interface ResolveResult { +export interface ResolveResult { reexports?: Reexport[]; diagnostics?: ts.Diagnostic[]; + data?: Readonly; } export interface DtsTransform { diff --git a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts index fc243d50eb..be9728bce6 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -20,43 +20,65 @@ import {LocalModuleScopeRegistry} from '../../scope'; import {TypeCheckContext} from '../../typecheck'; import {getSourceFile, isExported} from '../../util/src/typescript'; -import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from './api'; +import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from './api'; import {DtsTransformRegistry} from './declaration'; +import {Trait, TraitState} from './trait'; const EMPTY_ARRAY: any = []; /** - * Record of an adapter which decided to emit a static field, and the analysis it performed to - * prepare for that operation. + * Records information about a specific class that has matched traits. */ -interface MatchedHandler { - handler: DecoratorHandler; - analyzed: AnalysisOutput|null; - detected: DetectResult; -} +interface ClassRecord { + /** + * The `ClassDeclaration` of the class which has Angular traits applied. + */ + node: ClassDeclaration; -interface IvyClass { - matchedHandlers: MatchedHandler[]; + /** + * All traits which matched on the class. + */ + 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; + + // Subsequent fields are "internal" and used during the matching of `DecoratorHandler`s. This is + // mutable state during the `detect`/`analyze` phases of compilation. + + /** + * Whether `traits` contains traits matched from `DecoratorHandler`s marked as `WEAK`. + */ hasWeakHandlers: boolean; + + /** + * Whether `traits` contains a trait from a `DecoratorHandler` matched as `PRIMARY`. + */ hasPrimaryHandler: boolean; } /** - * Manages a compilation of Ivy decorators into static fields across an entire ts.Program. + * The heart of Angular compilation. * - * The compilation is stateful - source files are analyzed and records of the operations that need - * to be performed during the transform/emit process are maintained internally. + * The `TraitCompiler` is responsible for processing all classes in the program and */ -export class IvyCompilation { +export class TraitCompiler { /** - * Tracks classes which have been analyzed and found to have an Ivy decorator, and the - * information recorded about them for later compilation. + * Maps class declarations to their `ClassRecord`, which tracks the Ivy traits being applied to + * those classes. */ - private ivyClasses = new Map(); + private classes = new Map(); + + /** + * Maps source files to any class declaration(s) within them which have been discovered to contain + * Ivy traits. + */ + private fileToClasses = new Map>(); private reexportMap = new Map>(); - private _diagnostics: ts.Diagnostic[] = []; /** * @param handlers array of `DecoratorHandler`s which will be executed against each class in the @@ -68,169 +90,26 @@ export class IvyCompilation { * `null` in most cases. */ constructor( - private handlers: DecoratorHandler[], private reflector: ReflectionHost, - private importRewriter: ImportRewriter, private incrementalDriver: IncrementalDriver, - private perf: PerfRecorder, private sourceToFactorySymbols: Map>|null, + private handlers: DecoratorHandler[], + private reflector: ReflectionHost, private importRewriter: ImportRewriter, + private incrementalDriver: IncrementalDriver, private perf: PerfRecorder, + private sourceToFactorySymbols: Map>|null, private scopeRegistry: LocalModuleScopeRegistry, private compileNonExportedClasses: boolean, private dtsTransforms: DtsTransformRegistry, private mwpScanner: ModuleWithProvidersScanner) { } - get exportStatements(): Map> { return this.reexportMap; } + analyzeSync(sf: ts.SourceFile): void { this.analyze(sf, false); } - analyzeSync(sf: ts.SourceFile): void { return this.analyze(sf, false); } + analyzeAsync(sf: ts.SourceFile): Promise|void { return this.analyze(sf, true); } - analyzeAsync(sf: ts.SourceFile): Promise|undefined { return this.analyze(sf, true); } - - private detectHandlersForClass(node: ClassDeclaration): IvyClass|null { - if (!this.compileNonExportedClasses && !isExported(node)) { - return null; - } - - // The first step is to reflect the decorators. - const classDecorators = this.reflector.getDecoratorsOfDeclaration(node); - let ivyClass: IvyClass|null = null; - - // Look through the DecoratorHandlers to see if any are relevant. - for (const handler of this.handlers) { - // An adapter is relevant if it matches one of the decorators on the class. - const detected = handler.detect(node, classDecorators); - if (detected === undefined) { - // This handler didn't match. - continue; - } - - const isPrimaryHandler = handler.precedence === HandlerPrecedence.PRIMARY; - const isWeakHandler = handler.precedence === HandlerPrecedence.WEAK; - const match = { - handler, - analyzed: null, detected, - }; - - if (ivyClass === null) { - // This is the first handler to match this class. This path is a fast path through which - // most classes will flow. - ivyClass = { - matchedHandlers: [match], - hasPrimaryHandler: isPrimaryHandler, - hasWeakHandlers: isWeakHandler, - }; - this.ivyClasses.set(node, ivyClass); - } else { - // This is at least the second handler to match this class. This is a slower path that some - // classes will go through, which validates that the set of decorators applied to the class - // is valid. - - // Validate according to rules as follows: - // - // * WEAK handlers are removed if a non-WEAK handler matches. - // * Only one PRIMARY handler can match at a time. Any other PRIMARY handler matching a - // class with an existing PRIMARY handler is an error. - - if (!isWeakHandler && ivyClass.hasWeakHandlers) { - // The current handler is not a WEAK handler, but the class has other WEAK handlers. - // Remove them. - ivyClass.matchedHandlers = ivyClass.matchedHandlers.filter( - field => field.handler.precedence !== HandlerPrecedence.WEAK); - ivyClass.hasWeakHandlers = false; - } else if (isWeakHandler && !ivyClass.hasWeakHandlers) { - // The current handler is a WEAK handler, but the class has non-WEAK handlers already. - // Drop the current one. - continue; - } - - if (isPrimaryHandler && ivyClass.hasPrimaryHandler) { - // The class already has a PRIMARY handler, and another one just matched. - this._diagnostics.push({ - category: ts.DiagnosticCategory.Error, - code: Number('-99' + ErrorCode.DECORATOR_COLLISION), - file: getSourceFile(node), - start: node.getStart(undefined, false), - length: node.getWidth(), - messageText: 'Two incompatible decorators on class', - }); - this.ivyClasses.delete(node); - return null; - } - - // Otherwise, it's safe to accept the multiple decorators here. Update some of the metadata - // regarding this class. - ivyClass.matchedHandlers.push(match); - ivyClass.hasPrimaryHandler = ivyClass.hasPrimaryHandler || isPrimaryHandler; - } - } - - return ivyClass; - } - - /** - * Analyze a source file and produce diagnostics for it (if any). - */ - private analyze(sf: ts.SourceFile, preanalyze: false): undefined; - private analyze(sf: ts.SourceFile, preanalyze: true): Promise|undefined; - private analyze(sf: ts.SourceFile, preanalyze: boolean): Promise|undefined { + private analyze(sf: ts.SourceFile, preanalyze: false): void; + private analyze(sf: ts.SourceFile, preanalyze: true): Promise|void; + private analyze(sf: ts.SourceFile, preanalyze: boolean): Promise|void { const promises: Promise[] = []; - const analyzeClass = (node: ClassDeclaration): void => { - const ivyClass = this.detectHandlersForClass(node); - - // If the class has no Ivy behavior (or had errors), skip it. - if (ivyClass === null) { - return; - } - - // Loop through each matched handler that needs to be analyzed and analyze it, either - // synchronously or asynchronously. - for (const match of ivyClass.matchedHandlers) { - // The analyze() function will run the analysis phase of the handler. - const analyze = () => { - const analyzeClassSpan = this.perf.start('analyzeClass', node); - try { - match.analyzed = match.handler.analyze(node, match.detected.metadata); - - if (match.analyzed.diagnostics !== undefined) { - this._diagnostics.push(...match.analyzed.diagnostics); - } - - if (match.analyzed.factorySymbolName !== undefined && - this.sourceToFactorySymbols !== null && - this.sourceToFactorySymbols.has(sf.fileName)) { - this.sourceToFactorySymbols.get(sf.fileName) !.add(match.analyzed.factorySymbolName); - } - } catch (err) { - if (err instanceof FatalDiagnosticError) { - this._diagnostics.push(err.toDiagnostic()); - } else { - throw err; - } - } finally { - this.perf.stop(analyzeClassSpan); - } - }; - - // If preanalysis was requested and a preanalysis step exists, then run preanalysis. - // Otherwise, skip directly to analysis. - if (preanalyze && match.handler.preanalyze !== undefined) { - // Preanalysis might return a Promise, indicating an async operation was necessary. Or it - // might return undefined, indicating no async step was needed and analysis can proceed - // immediately. - const preanalysis = match.handler.preanalyze(node, match.detected.metadata); - if (preanalysis !== undefined) { - // Await the results of preanalysis before running analysis. - promises.push(preanalysis.then(analyze)); - } else { - // No async preanalysis needed, skip directly to analysis. - analyze(); - } - } else { - // Not in preanalysis mode or not needed for this handler, skip directly to analysis. - analyze(); - } - } - }; const visit = (node: ts.Node): void => { - // Process nodes recursively, and look for class declarations with decorators. if (isNamedClassDeclaration(node)) { - analyzeClass(node); + this.analyzeClass(node, preanalyze ? promises : null); } ts.forEachChild(node, visit); }; @@ -246,67 +125,328 @@ export class IvyCompilation { }); if (preanalyze && promises.length > 0) { - return Promise.all(promises).then(() => undefined); + return Promise.all(promises).then(() => undefined as void); } else { - return undefined; + return; } } - /** - * Feeds components discovered in the compilation to a context for indexing. - */ - index(context: IndexingContext) { - this.ivyClasses.forEach((ivyClass, declaration) => { - for (const match of ivyClass.matchedHandlers) { - if (match.handler.index !== undefined && match.analyzed !== null && - match.analyzed.analysis !== undefined) { - match.handler.index(context, declaration, match.analyzed.analysis); - } + private scanClassForTraits(clazz: ClassDeclaration): ClassRecord|null { + if (!this.compileNonExportedClasses && !isExported(clazz)) { + return null; + } + + const decorators = this.reflector.getDecoratorsOfDeclaration(clazz); + + let record: ClassRecord|null = null; + + for (const handler of this.handlers) { + const result = handler.detect(clazz, decorators); + if (result === undefined) { + continue; } - }); + + + const isPrimaryHandler = handler.precedence === HandlerPrecedence.PRIMARY; + const isWeakHandler = handler.precedence === HandlerPrecedence.WEAK; + const trait = Trait.pending(handler, result); + + if (record === null) { + // This is the first handler to match this class. This path is a fast path through which + // most classes will flow. + record = { + node: clazz, + traits: [trait], + metaDiagnostics: null, + hasPrimaryHandler: isPrimaryHandler, + hasWeakHandlers: isWeakHandler, + }; + + this.classes.set(clazz, record); + const sf = clazz.getSourceFile(); + if (!this.fileToClasses.has(sf)) { + this.fileToClasses.set(sf, new Set()); + } + this.fileToClasses.get(sf) !.add(clazz); + } else { + // This is at least the second handler to match this class. This is a slower path that some + // classes will go through, which validates that the set of decorators applied to the class + // is valid. + + // Validate according to rules as follows: + // + // * WEAK handlers are removed if a non-WEAK handler matches. + // * Only one PRIMARY handler can match at a time. Any other PRIMARY handler matching a + // class with an existing PRIMARY handler is an error. + + 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.hasWeakHandlers = false; + } else if (isWeakHandler && !record.hasWeakHandlers) { + // The current handler is a WEAK handler, but the class has non-WEAK handlers already. + // Drop the current one. + continue; + } + + 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.traits = []; + return record; + } + + // Otherwise, it's safe to accept the multiple decorators here. Update some of the metadata + // regarding this class. + record.traits.push(trait); + record.hasPrimaryHandler = record.hasPrimaryHandler || isPrimaryHandler; + } + } + + return record; + } + + private analyzeClass(clazz: ClassDeclaration, preanalyzeQueue: Promise[]|null): void { + const record = this.scanClassForTraits(clazz); + + if (record === null) { + // There are no Ivy traits on the class, so it can safely be skipped. + return; + } + + for (const trait of record.traits) { + const analyze = () => this.analyzeTrait(clazz, trait); + + let preanalysis: Promise|null = null; + if (preanalyzeQueue !== null && trait.handler.preanalyze !== undefined) { + preanalysis = trait.handler.preanalyze(clazz, trait.detected.metadata) || null; + } + if (preanalysis !== null) { + preanalyzeQueue !.push(preanalysis.then(analyze)); + } else { + analyze(); + } + } + } + + private analyzeTrait(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)`); + } + + // Attempt analysis. This could fail with a `FatalDiagnosticError`; catch it if it does. + let result: AnalysisOutput; + try { + result = trait.handler.analyze(clazz, trait.detected.metadata); + } catch (err) { + if (err instanceof FatalDiagnosticError) { + trait = trait.toErrored([err.toDiagnostic()]); + return; + } else { + throw err; + } + } + + if (result.diagnostics !== undefined) { + trait = trait.toErrored(result.diagnostics); + } else if (result.analysis !== undefined) { + trait = trait.toAnalyzed(result.analysis); + + const sf = clazz.getSourceFile(); + if (result.factorySymbolName !== undefined && this.sourceToFactorySymbols !== null && + this.sourceToFactorySymbols.has(sf.fileName)) { + this.sourceToFactorySymbols.get(sf.fileName) !.add(result.factorySymbolName); + } + } else { + trait = trait.toSkipped(); + } } resolve(): void { - const resolveSpan = this.perf.start('resolve'); - this.ivyClasses.forEach((ivyClass, node) => { - for (const match of ivyClass.matchedHandlers) { - if (match.handler.resolve !== undefined && match.analyzed !== null && - match.analyzed.analysis !== undefined) { - const resolveClassSpan = this.perf.start('resolveClass', node); - try { - const res = match.handler.resolve(node, match.analyzed.analysis); - if (res.reexports !== undefined) { - const fileName = node.getSourceFile().fileName; - if (!this.reexportMap.has(fileName)) { - this.reexportMap.set(fileName, new Map()); - } - const fileReexports = this.reexportMap.get(fileName) !; - for (const reexport of res.reexports) { - fileReexports.set(reexport.asAlias, [reexport.fromModule, reexport.symbolName]); - } - } - if (res.diagnostics !== undefined) { - this._diagnostics.push(...res.diagnostics); - } - } catch (err) { - if (err instanceof FatalDiagnosticError) { - this._diagnostics.push(err.toDiagnostic()); - } else { - throw err; - } - } finally { - this.perf.stop(resolveClassSpan); + const classes = Array.from(this.classes.keys()); + for (const clazz of classes) { + const record = this.classes.get(clazz) !; + for (let trait of record.traits) { + const handler = trait.handler; + switch (trait.state) { + case TraitState.SKIPPED: + case TraitState.ERRORED: + continue; + case TraitState.PENDING: + throw new Error( + `Resolving a trait that hasn't been analyzed: ${clazz.name.text} / ${Object.getPrototypeOf(trait.handler).constructor.name}`); + case TraitState.RESOLVED: + throw new Error(`Resolving an already resolved trait`); + } + + if (handler.resolve === undefined) { + // No resolution of this trait needed - it's considered successful by default. + trait = trait.toResolved(null); + continue; + } + + let result: ResolveResult; + try { + result = handler.resolve(clazz, trait.analysis as Readonly); + } catch (err) { + if (err instanceof FatalDiagnosticError) { + trait = trait.toErrored([err.toDiagnostic()]); + continue; + } else { + throw err; + } + } + + if (result.diagnostics !== undefined) { + trait = trait.toErrored(result.diagnostics); + } else { + if (result.data !== undefined) { + trait = trait.toResolved(result.data); + } else { + trait = trait.toResolved(null); + } + } + + if (result.reexports !== undefined) { + const fileName = clazz.getSourceFile().fileName; + if (!this.reexportMap.has(fileName)) { + this.reexportMap.set(fileName, new Map()); + } + const fileReexports = this.reexportMap.get(fileName) !; + for (const reexport of result.reexports) { + fileReexports.set(reexport.asAlias, [reexport.fromModule, reexport.symbolName]); } } } - }); - this.perf.stop(resolveSpan); + } + this.recordNgModuleScopeDependencies(); } + typeCheck(ctx: TypeCheckContext): void { + for (const clazz of this.classes.keys()) { + const record = this.classes.get(clazz) !; + for (const trait of record.traits) { + if (trait.state !== TraitState.RESOLVED) { + continue; + } else if (trait.handler.typeCheck === undefined) { + continue; + } + trait.handler.typeCheck(ctx, clazz, trait.analysis, trait.resolution); + } + } + } + + index(ctx: IndexingContext): void { + for (const clazz of this.classes.keys()) { + const record = this.classes.get(clazz) !; + for (const trait of record.traits) { + if (trait.state !== TraitState.RESOLVED) { + // Skip traits that haven't been resolved successfully. + continue; + } else if (trait.handler.index === undefined) { + // Skip traits that don't affect indexing. + continue; + } + + trait.handler.index(ctx, clazz, trait.analysis, trait.resolution); + } + } + } + + compile(clazz: ts.Declaration, constantPool: ConstantPool): CompileResult[]|null { + const original = ts.getOriginalNode(clazz) as typeof clazz; + if (!isNamedClassDeclaration(clazz) || !isNamedClassDeclaration(original) || + !this.classes.has(original)) { + return null; + } + + const record = this.classes.get(original) !; + + let res: CompileResult[] = []; + + for (const trait of record.traits) { + if (trait.state !== TraitState.RESOLVED) { + continue; + } + + const compileSpan = this.perf.start('compileClass', original); + const compileMatchRes = + trait.handler.compile(clazz, trait.analysis, trait.resolution, constantPool); + this.perf.stop(compileSpan); + if (Array.isArray(compileMatchRes)) { + for (const result of compileMatchRes) { + if (!res.some(r => r.name === result.name)) { + res.push(result); + } + } + } 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); + + // Return the instruction to the transformer so the fields will be added. + return res.length > 0 ? res : null; + } + + decoratorsFor(node: ts.Declaration): ts.Decorator[] { + const original = ts.getOriginalNode(node) as typeof node; + if (!isNamedClassDeclaration(original) || !this.classes.has(original)) { + return []; + } + + const record = this.classes.get(original) !; + const decorators: ts.Decorator[] = []; + + for (const trait of record.traits) { + if (trait.state !== TraitState.RESOLVED) { + continue; + } + + if (trait.detected.trigger !== null && ts.isDecorator(trait.detected.trigger)) { + decorators.push(trait.detected.trigger); + } + } + + return decorators; + } + + get diagnostics(): ReadonlyArray { + const diagnostics: ts.Diagnostic[] = []; + for (const clazz of this.classes.keys()) { + const record = this.classes.get(clazz) !; + if (record.metaDiagnostics !== null) { + diagnostics.push(...record.metaDiagnostics); + } + for (const trait of record.traits) { + if (trait.state === TraitState.ERRORED) { + diagnostics.push(...trait.diagnostics); + } + } + } + return diagnostics; + } + + get exportStatements(): Map> { return this.reexportMap; } + private recordNgModuleScopeDependencies() { const recordSpan = this.perf.start('recordDependencies'); - this.scopeRegistry !.getCompilationScopes().forEach(scope => { + for (const scope of this.scopeRegistry.getCompilationScopes()) { const file = scope.declaration.getSourceFile(); const ngModuleFile = scope.ngModule.getSourceFile(); @@ -320,7 +460,7 @@ export class IvyCompilation { // A change to any directive/pipe in the compilation scope should cause the declaration to be // invalidated. - scope.directives.forEach(directive => { + for (const directive of scope.directives) { const dirSf = directive.ref.node.getSourceFile(); // When a directive in scope is updated, the declaration needs to be recompiled as e.g. @@ -333,94 +473,13 @@ export class IvyCompilation { if (directive.isComponent) { this.incrementalDriver.trackFileDependencies(deps, dirSf); } - }); - scope.pipes.forEach(pipe => { + } + for (const pipe of scope.pipes) { // When a pipe in scope is updated, the declaration needs to be recompiled as e.g. // the pipe's name may have changed. this.incrementalDriver.trackFileDependency(pipe.ref.node.getSourceFile(), file); - }); - }); + } + } this.perf.stop(recordSpan); } - - typeCheck(context: TypeCheckContext): void { - this.ivyClasses.forEach((ivyClass, node) => { - for (const match of ivyClass.matchedHandlers) { - if (match.handler.typeCheck !== undefined && match.analyzed !== null && - match.analyzed.analysis !== undefined) { - match.handler.typeCheck(context, node, match.analyzed.analysis); - } - } - }); - } - - /** - * Perform a compilation operation on the given class declaration and return instructions to an - * AST transformer if any are available. - */ - compileIvyFieldFor(node: ts.Declaration, constantPool: ConstantPool): CompileResult[]|undefined { - // Look to see whether the original node was analyzed. If not, there's nothing to do. - const original = ts.getOriginalNode(node) as typeof node; - if (!isNamedClassDeclaration(original) || !this.ivyClasses.has(original)) { - return undefined; - } - - const ivyClass = this.ivyClasses.get(original) !; - - let res: CompileResult[] = []; - - for (const match of ivyClass.matchedHandlers) { - if (match.analyzed === null || match.analyzed.analysis === undefined) { - continue; - } - - const compileSpan = this.perf.start('compileClass', original); - const compileMatchRes = - match.handler.compile(node as ClassDeclaration, match.analyzed.analysis, constantPool); - this.perf.stop(compileSpan); - if (Array.isArray(compileMatchRes)) { - compileMatchRes.forEach(result => { - if (!res.some(r => r.name === result.name)) { - res.push(result); - } - }); - } 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); - - // Return the instruction to the transformer so the fields will be added. - return res.length > 0 ? res : undefined; - } - - /** - * Lookup the `ts.Decorator` which triggered transformation of a particular class declaration. - */ - ivyDecoratorsFor(node: ts.Declaration): ts.Decorator[] { - const original = ts.getOriginalNode(node) as typeof node; - - if (!isNamedClassDeclaration(original) || !this.ivyClasses.has(original)) { - return EMPTY_ARRAY; - } - const ivyClass = this.ivyClasses.get(original) !; - const decorators: ts.Decorator[] = []; - - for (const match of ivyClass.matchedHandlers) { - if (match.analyzed === null || match.analyzed.analysis === undefined) { - continue; - } - if (match.detected.trigger !== null && ts.isDecorator(match.detected.trigger)) { - decorators.push(match.detected.trigger); - } - } - - return decorators; - } - - get diagnostics(): ReadonlyArray { return this._diagnostics; } } diff --git a/packages/compiler-cli/src/ngtsc/transform/src/trait.ts b/packages/compiler-cli/src/ngtsc/transform/src/trait.ts new file mode 100644 index 0000000000..1bdd897354 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/transform/src/trait.ts @@ -0,0 +1,266 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * 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 * as ts from 'typescript'; +import {DecoratorHandler, DetectResult} from './api'; + +export enum TraitState { + /** + * Pending traits are freshly created and have never been analyzed. + */ + PENDING = 0x01, + + /** + * Analyzed traits have successfully been analyzed, but are pending resolution. + */ + ANALYZED = 0x02, + + /** + * Resolved traits have successfully been analyzed and resolved and are ready for compilation. + */ + RESOLVED = 0x04, + + /** + * Errored traits have failed either analysis or resolution and as a result contain diagnostics + * describing the failure(s). + */ + ERRORED = 0x08, + + /** + * Skipped traits are no longer considered for compilation. + */ + SKIPPED = 0x10, +} + +/** + * An Ivy aspect added to a class (for example, the compilation of a component definition). + * + * Traits are created when a `DecoratorHandler` matches a class. Each trait begins in a pending + * state and undergoes transitions as compilation proceeds through the various steps. + * + * In practice, traits are instances of the private class `TraitImpl` declared below. Through the + * various interfaces included in this union type, the legal API of a trait in any given state is + * represented in the type system. This includes any possible transitions from one type to the next. + * + * 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| ErroredTrait; + +/** + * 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), +}; + +/** + * The part of the `Trait` interface that's common to all trait states. + */ +export interface TraitBase { + /** + * Current state of the trait. + * + * This will be narrowed in the interfaces for each specific state. + */ + state: TraitState; + + /** + * The `DecoratorHandler` which matched on the class to create this trait. + */ + handler: DecoratorHandler; + + /** + * The detection result (of `handler.detect`) which indicated that this trait applied to the + * class. + * + * This is mainly used to cache the detection between pre-analysis and analysis. + */ + detected: DetectResult; +} + +/** + * A trait in the pending state. + * + * Pending traits have yet to be analyzed in any way. + */ +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): AnalyzedTrait; + + /** + * This trait failed analysis, and should transition to the "errored" state with the resulting + * diagnostics. + */ + toErrored(errors: ts.Diagnostic[]): ErroredTrait; + + /** + * During analysis it was determined that this trait is not eligible for compilation after all, + * and should be transitioned to the "skipped" state. + */ + toSkipped(): SkippedTrait; +} + +/** + * A trait in the "errored" state. + * + * Errored traits contain `ts.Diagnostic`s indicating any problem(s) with the class. + * + * This is a terminal state. + */ +export interface ErroredTrait extends TraitBase { + state: TraitState.ERRORED; + + /** + * Diagnostics which were produced while attempting to analyze the trait. + */ + diagnostics: ts.Diagnostic[]; +} + +/** + * A trait in the "skipped" state. + * + * Skipped traits aren't considered for compilation. + * + * This is a terminal state. + */ +export interface SkippedTrait extends TraitBase { state: TraitState.SKIPPED; } + +/** + * The part of the `Trait` interface for any trait which has been successfully analyzed. + * + * Mainly, this is used to share the comment on the `analysis` field. + */ +export interface TraitWithAnalysis { + /** + * The results returned by a successful analysis of the given class/`DecoratorHandler` + * combination. + */ + analysis: Readonly; +} + +/** + * A trait in the "analyzed" state. + * + * Analyzed traits have analysis results available, and are eligible for resolution. + */ +export interface AnalyzedTrait extends TraitBase, TraitWithAnalysis { + state: TraitState.ANALYZED; + + /** + * This analyzed trait has been successfully resolved, and should be transitioned to the + * "resolved" state. + */ + toResolved(resolution: R): ResolvedTrait; + + /** + * This trait failed resolution, and should transition to the "errored" state with the resulting + * diagnostics. + */ + toErrored(errors: ts.Diagnostic[]): ErroredTrait; +} + +/** + * A trait in the "resolved" state. + * + * Resolved traits have been successfully analyzed and resolved, contain no errors, and are ready + * for the compilation phase. + * + * This is a terminal state. + */ +export interface ResolvedTrait extends TraitBase, TraitWithAnalysis { + state: TraitState.RESOLVED; + + /** + * The results returned by a successful resolution of the given class/`DecoratorHandler` + * combination. + */ + resolution: Readonly; +} + +/** + * An implementation of the `Trait` type which transitions safely between the various + * `TraitState`s. + */ +class TraitImpl { + state: TraitState = TraitState.PENDING; + handler: DecoratorHandler; + detected: DetectResult; + analysis: Readonly|null = null; + resolution: Readonly|null = null; + diagnostics: ts.Diagnostic[]|null = null; + + constructor(handler: DecoratorHandler, detected: DetectResult) { + this.handler = handler; + this.detected = detected; + } + + toAnalyzed(analysis: A): AnalyzedTrait { + // Only pending traits can be analyzed. + this.assertTransitionLegal(TraitState.PENDING, TraitState.ANALYZED); + this.analysis = analysis; + this.state = TraitState.ANALYZED; + return this as AnalyzedTrait; + } + + toErrored(diagnostics: ts.Diagnostic[]): ErroredTrait { + // Pending traits (during analysis) or analyzed traits (during resolution) can produce + // diagnostics and enter an errored state. + this.assertTransitionLegal(TraitState.PENDING | TraitState.ANALYZED, TraitState.RESOLVED); + this.diagnostics = diagnostics; + this.analysis = null; + this.state = TraitState.ERRORED; + return this as ErroredTrait; + } + + toResolved(resolution: R): ResolvedTrait { + // Only analyzed traits can be resolved. + this.assertTransitionLegal(TraitState.ANALYZED, TraitState.RESOLVED); + this.resolution = resolution; + this.state = TraitState.RESOLVED; + return this as ResolvedTrait; + } + + toSkipped(): SkippedTrait { + // Only pending traits can be skipped. + this.assertTransitionLegal(TraitState.PENDING, TraitState.SKIPPED); + this.state = TraitState.SKIPPED; + return this as SkippedTrait; + } + + /** + * Verifies that the trait is currently in one of the `allowedState`s. + * + * If correctly used, the `Trait` type and transition methods prevent illegal transitions from + * occurring. However, if a reference to the `TraitImpl` instance typed with the previous + * interface is retained after calling one of its transition methods, it will allow for illegal + * transitions to take place. Hence, this assertion provides a little extra runtime protection. + */ + 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]}.`); + } + } + + /** + * Construct a new `TraitImpl` in the pending state. + */ + 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 06233f111f..23c7198e97 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts @@ -14,7 +14,7 @@ import {Decorator, ReflectionHost} from '../../reflection'; import {ImportManager, translateExpression, translateStatement} from '../../translator'; import {VisitListEntryResult, Visitor, visit} from '../../util/src/visitor'; -import {IvyCompilation} from './compilation'; +import {TraitCompiler} from './compilation'; import {addImports} from './utils'; const NO_DECORATORS = new Set(); @@ -31,7 +31,7 @@ interface FileOverviewMeta { } export function ivyTransformFactory( - compilation: IvyCompilation, reflector: ReflectionHost, importRewriter: ImportRewriter, + compilation: TraitCompiler, reflector: ReflectionHost, importRewriter: ImportRewriter, defaultImportRecorder: DefaultImportRecorder, isCore: boolean, isClosureCompilerEnabled: boolean): ts.TransformerFactory { return (context: ts.TransformationContext): ts.Transformer => { @@ -45,7 +45,7 @@ export function ivyTransformFactory( class IvyVisitor extends Visitor { constructor( - private compilation: IvyCompilation, private reflector: ReflectionHost, + private compilation: TraitCompiler, private reflector: ReflectionHost, private importManager: ImportManager, private defaultImportRecorder: DefaultImportRecorder, private isCore: boolean, private constantPool: ConstantPool) { super(); @@ -55,9 +55,9 @@ class IvyVisitor extends Visitor { VisitListEntryResult { // Determine if this class has an Ivy field that needs to be added, and compile the field // to an expression if so. - const res = this.compilation.compileIvyFieldFor(node, this.constantPool); + const res = this.compilation.compile(node, this.constantPool); - if (res !== undefined) { + if (res !== null) { // There is at least one field to add. const statements: ts.Statement[] = []; const members = [...node.members]; @@ -86,7 +86,7 @@ class IvyVisitor extends Visitor { node = ts.updateClassDeclaration( node, // Remove the decorator which triggered this compilation, leaving the others alone. - maybeFilterDecorator(node.decorators, this.compilation.ivyDecoratorsFor(node)), + maybeFilterDecorator(node.decorators, this.compilation.decoratorsFor(node)), node.modifiers, node.name, node.typeParameters, node.heritageClauses || [], // Map over the class members and remove any Angular decorators from them. members.map(member => this._stripAngularDecorators(member))); @@ -206,7 +206,7 @@ class IvyVisitor extends Visitor { * A transformer which operates on ts.SourceFiles and applies changes from an `IvyCompilation`. */ function transformIvySourceFile( - compilation: IvyCompilation, context: ts.TransformationContext, reflector: ReflectionHost, + compilation: TraitCompiler, context: ts.TransformationContext, reflector: ReflectionHost, importRewriter: ImportRewriter, file: ts.SourceFile, isCore: boolean, isClosureCompilerEnabled: boolean, defaultImportRecorder: DefaultImportRecorder): ts.SourceFile { diff --git a/packages/compiler-cli/src/ngtsc/util/src/typescript.ts b/packages/compiler-cli/src/ngtsc/util/src/typescript.ts index 95adf80722..67708ee461 100644 --- a/packages/compiler-cli/src/ngtsc/util/src/typescript.ts +++ b/packages/compiler-cli/src/ngtsc/util/src/typescript.ts @@ -122,3 +122,8 @@ export function resolveModuleName( .resolvedModule; } } + +/** + * Asserts that the keys `K` form a subset of the keys of `T`. + */ +export type SubsetOfKeys = K;