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;