refactor(ivy): move analysis side effects into a register phase (#34288)

Previously 'analyze' in the various `DecoratorHandler`s not only extracts
information from the decorators on the classes being analyzed, but also has
several side effects within the compiler:

* it can register metadata about the types involved in global metadata
  trackers.
* it can register information about which .ngfactory symbols are actually
  needed.

In this commit, these side-effects are moved into a new 'register' phase,
which runs after the 'analyze' step. Currently this is a no-op refactoring
as 'register' is always called directly after 'analyze'. In the future this
opens the door for re-use of prior analysis work (with only 'register' being
called, to apply the above side effects).

Also as part of this refactoring, the reification of NgModule scope
information into the incremental dependency graph is moved to the
`NgtscProgram` instead of the `TraitCompiler` (which now only manages trait
compilation and does not have other side effects).

PR Close #34288
This commit is contained in:
Alex Rickabaugh 2019-12-09 16:24:14 -08:00 committed by Kara Erickson
parent 252e3e9487
commit 50cdc0ac1b
14 changed files with 233 additions and 149 deletions

View File

@ -108,7 +108,9 @@ export class DecorationAnalyzer {
new NgModuleDecoratorHandler(
this.reflectionHost, this.evaluator, this.fullMetaReader, this.fullRegistry,
this.scopeRegistry, this.referencesRegistry, this.isCore, /* routeAnalyzer */ null,
this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER, /* annotateForClosureCompiler */ false),
this.refEmitter,
/* factoryTracker */ null, NOOP_DEFAULT_IMPORT_RECORDER,
/* annotateForClosureCompiler */ false),
];
migrations: Migration[] = [
new UndecoratedParentMigration(),

View File

@ -80,7 +80,12 @@ export function analyzeDecorators(
if (diagnostics !== undefined) {
allDiagnostics.push(...diagnostics);
}
match.analysis = analysis !;
if (analysis !== undefined) {
match.analysis = analysis;
if (match.handler.register !== undefined) {
match.handler.register(declaration, analysis);
}
}
matches.push(match);
} catch (e) {
if (isFatalDiagnosticError(e)) {

View File

@ -19,6 +19,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/routing",
"//packages/compiler-cli/src/ngtsc/scope",
"//packages/compiler-cli/src/ngtsc/shims",
"//packages/compiler-cli/src/ngtsc/transform",
"//packages/compiler-cli/src/ngtsc/typecheck",
"//packages/compiler-cli/src/ngtsc/util",

View File

@ -49,6 +49,8 @@ export interface ComponentAnalysisData {
* (not during resolve).
*/
meta: Omit<R3ComponentMetadata, ComponentMetadataResolvedFields>;
baseClass: Reference<ClassDeclaration>|'dynamic'|null;
guards: ReturnType<typeof extractDirectiveGuards>;
parsedTemplate: ParsedTemplate;
templateSourceMapping: TemplateSourceMapping;
metadataStmt: Statement|null;
@ -239,21 +241,6 @@ export class ComponentDecoratorHandler implements
`Errors parsing template: ${template.errors.map(e => e.toString()).join(', ')}`);
}
// Register this component's information with the `MetadataRegistry`. This ensures that
// the information about the component is available during the compile() phase.
const ref = new Reference(node);
this.metaRegistry.registerDirectiveMetadata({
ref,
name: node.name.text,
selector: metadata.selector,
exportAs: metadata.exportAs,
inputs: metadata.inputs,
outputs: metadata.outputs,
queries: metadata.queries.map(query => query.propertyName),
isComponent: true, ...extractDirectiveGuards(node, this.reflector),
baseClass: readBaseClass(node, this.reflector, this.evaluator),
});
// Figure out the set of styles. The ordering here is important: external resources (styleUrls)
// precede inline styles, and styles defined in the template override styles defined in the
// component.
@ -302,6 +289,7 @@ export class ComponentDecoratorHandler implements
const output: AnalysisOutput<ComponentAnalysisData> = {
analysis: {
baseClass: readBaseClass(node, this.reflector, this.evaluator),
meta: {
...metadata,
template,
@ -315,12 +303,12 @@ export class ComponentDecoratorHandler implements
viewProviders,
i18nUseExternalIds: this.i18nUseExternalIds, relativeContextFilePath,
},
guards: extractDirectiveGuards(node, this.reflector),
metadataStmt: generateSetClassMetadataCall(
node, this.reflector, this.defaultImportRecorder, this.isCore,
this.annotateForClosureCompiler),
parsedTemplate: template, parseTemplate, templateSourceMapping,
},
typeCheck: true,
};
if (changeDetection !== null) {
output.analysis !.meta.changeDetection = changeDetection;
@ -328,6 +316,23 @@ export class ComponentDecoratorHandler implements
return output;
}
register(node: ClassDeclaration, analysis: ComponentAnalysisData): void {
// Register this component's information with the `MetadataRegistry`. This ensures that
// the information about the component is available during the compile() phase.
const ref = new Reference(node);
this.metaRegistry.registerDirectiveMetadata({
ref,
name: node.name.text,
selector: analysis.meta.selector,
exportAs: analysis.meta.exportAs,
inputs: analysis.meta.inputs,
outputs: analysis.meta.outputs,
queries: analysis.meta.queries.map(query => query.propertyName),
isComponent: true,
baseClass: analysis.baseClass, ...analysis.guards,
});
}
index(
context: IndexingContext, node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>) {
// The component template may have been previously parsed without preserving whitespace or with

View File

@ -32,6 +32,8 @@ const LIFECYCLE_HOOKS = new Set([
]);
export interface DirectiveHandlerData {
baseClass: Reference<ClassDeclaration>|'dynamic'|null;
guards: ReturnType<typeof extractDirectiveGuards>;
meta: R3DirectiveMetadata;
metadataStmt: Statement|null;
}
@ -83,31 +85,35 @@ export class DirectiveDecoratorHandler implements
return {};
}
// Register this directive's information with the `MetadataRegistry`. This ensures that
// the information about the directive is available during the compile() phase.
const ref = new Reference(node);
this.metaRegistry.registerDirectiveMetadata({
ref,
name: node.name.text,
selector: analysis.selector,
exportAs: analysis.exportAs,
inputs: analysis.inputs,
outputs: analysis.outputs,
queries: analysis.queries.map(query => query.propertyName),
isComponent: false, ...extractDirectiveGuards(node, this.reflector),
baseClass: readBaseClass(node, this.reflector, this.evaluator),
});
return {
analysis: {
meta: analysis,
metadataStmt: generateSetClassMetadataCall(
node, this.reflector, this.defaultImportRecorder, this.isCore,
this.annotateForClosureCompiler),
baseClass: readBaseClass(node, this.reflector, this.evaluator),
guards: extractDirectiveGuards(node, this.reflector),
}
};
}
register(node: ClassDeclaration, analysis: Readonly<DirectiveHandlerData>): void {
// Register this directive's information with the `MetadataRegistry`. This ensures that
// the information about the directive is available during the compile() phase.
const ref = new Reference(node);
this.metaRegistry.registerDirectiveMetadata({
ref,
name: node.name.text,
selector: analysis.meta.selector,
exportAs: analysis.meta.exportAs,
inputs: analysis.meta.inputs,
outputs: analysis.meta.outputs,
queries: analysis.meta.queries.map(query => query.propertyName),
isComponent: false,
baseClass: analysis.baseClass, ...analysis.guards,
});
}
compile(
node: ClassDeclaration, analysis: Readonly<DirectiveHandlerData>,
resolution: Readonly<unknown>, pool: ConstantPool): CompileResult[] {

View File

@ -16,6 +16,7 @@ import {PartialEvaluator, ResolvedValue} from '../../partial_evaluator';
import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral, typeNodeToValueExpr} from '../../reflection';
import {NgModuleRouteAnalyzer} from '../../routing';
import {LocalModuleScopeRegistry, ScopeData} from '../../scope';
import {FactoryTracker} from '../../shims';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../transform';
import {getSourceFile} from '../../util/src/typescript';
@ -28,8 +29,11 @@ export interface NgModuleAnalysis {
inj: R3InjectorMetadata;
metadataStmt: Statement|null;
declarations: Reference<ClassDeclaration>[];
schemas: SchemaMetadata[];
imports: Reference<ClassDeclaration>[];
exports: Reference<ClassDeclaration>[];
id: Expression|null;
factorySymbolName: string;
}
/**
@ -45,6 +49,7 @@ export class NgModuleDecoratorHandler implements
private scopeRegistry: LocalModuleScopeRegistry,
private referencesRegistry: ReferencesRegistry, private isCore: boolean,
private routeAnalyzer: NgModuleRouteAnalyzer|null, private refEmitter: ReferenceEmitter,
private factoryTracker: FactoryTracker|null,
private defaultImportRecorder: DefaultImportRecorder,
private annotateForClosureCompiler: boolean, private localeId?: string) {}
@ -166,18 +171,6 @@ export class NgModuleDecoratorHandler implements
const id: Expression|null =
ngModule.has('id') ? new WrappedNodeExpr(ngModule.get('id') !) : null;
// Register this module's information with the LocalModuleScopeRegistry. This ensures that
// during the compile() phase, the module's metadata is available for selector scope
// computation.
this.metaRegistry.registerNgModuleMetadata({
ref: new Reference(node),
schemas,
declarations: declarationRefs,
imports: importRefs,
exports: exportRefs,
});
const valueContext = node.getSourceFile();
let typeContext = valueContext;
@ -246,18 +239,37 @@ export class NgModuleDecoratorHandler implements
return {
analysis: {
id,
schemas: schemas,
mod: ngModuleDef,
inj: ngInjectorDef,
declarations: declarationRefs,
imports: importRefs,
exports: exportRefs,
metadataStmt: generateSetClassMetadataCall(
node, this.reflector, this.defaultImportRecorder, this.isCore,
this.annotateForClosureCompiler),
factorySymbolName: node.name.text,
},
factorySymbolName: node.name.text,
};
}
register(node: ClassDeclaration, analysis: NgModuleAnalysis): void {
// Register this module's information with the LocalModuleScopeRegistry. This ensures that
// during the compile() phase, the module's metadata is available for selector scope
// computation.
this.metaRegistry.registerNgModuleMetadata({
ref: new Reference(node),
schemas: analysis.schemas,
declarations: analysis.declarations,
imports: analysis.imports,
exports: analysis.exports,
});
if (this.factoryTracker !== null) {
this.factoryTracker.track(node.getSourceFile(), analysis.factorySymbolName);
}
}
resolve(node: ClassDeclaration, analysis: Readonly<NgModuleAnalysis>): ResolveResult<unknown> {
const scope = this.scopeRegistry.getScopeOfModule(node);
const diagnostics = this.scopeRegistry.getDiagnosticsOfModule(node) || undefined;

View File

@ -80,8 +80,6 @@ export class PipeDecoratorHandler implements DecoratorHandler<Decorator, PipeHan
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, pipeNameExpr, `@Pipe.name must be a string`);
}
const ref = new Reference(clazz);
this.metaRegistry.registerPipeMetadata({ref, name: pipeName});
let pure = true;
if (pipe.has('pure')) {
@ -111,6 +109,11 @@ export class PipeDecoratorHandler implements DecoratorHandler<Decorator, PipeHan
};
}
register(node: ClassDeclaration, analysis: Readonly<PipeHandlerData>): void {
const ref = new Reference(node);
this.metaRegistry.registerPipeMetadata({ref, name: analysis.meta.pipeName});
}
compile(node: ClassDeclaration, analysis: Readonly<PipeHandlerData>): CompileResult[] {
const meta = analysis.meta;
const res = compilePipeFromMetadata(meta);

View File

@ -69,8 +69,8 @@ runInEachFileSystem(() => {
const handler = new NgModuleDecoratorHandler(
reflectionHost, evaluator, metaReader, metaRegistry, scopeRegistry, referencesRegistry,
false, null, refEmitter, NOOP_DEFAULT_IMPORT_RECORDER,
/* annotateForClosureCompiler */ false);
/* isCore */ false, /* routeAnalyzer */ null, refEmitter, /* factoryTracker */ null,
NOOP_DEFAULT_IMPORT_RECORDER, /* annotateForClosureCompiler */ false);
const TestModule =
getDeclaration(program, _('/entry.ts'), 'TestModule', isNamedClassDeclaration);
const detected =

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {GeneratedFile} from '@angular/compiler';
import {GeneratedFile, Type} from '@angular/compiler';
import * as ts from 'typescript';
import * as api from '../transformers/api';
@ -30,7 +30,7 @@ import {TypeScriptReflectionHost} from './reflection';
import {HostResourceLoader} from './resource_loader';
import {NgModuleRouteAnalyzer, entryPointKeyFor} from './routing';
import {ComponentScopeReader, CompoundComponentScopeReader, LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from './scope';
import {FactoryGenerator, FactoryInfo, GeneratedShimsHostWrapper, ShimGenerator, SummaryGenerator, TypeCheckShimGenerator, generatedFactoryTransform} from './shims';
import {FactoryGenerator, FactoryTracker, GeneratedShimsHostWrapper, ShimGenerator, SummaryGenerator, TypeCheckShimGenerator, generatedFactoryTransform} from './shims';
import {ivySwitchTransform} from './switch';
import {DecoratorHandler, DtsTransformRegistry, TraitCompiler, declarationTransformFactory, ivyTransformFactory} from './transform';
import {aliasTransformFactory} from './transform/src/alias';
@ -43,8 +43,6 @@ export class NgtscProgram implements api.Program {
private reuseTsProgram: ts.Program;
private resourceManager: HostResourceLoader;
private compilation: TraitCompiler|undefined = undefined;
private factoryToSourceInfo: Map<string, FactoryInfo>|null = null;
private sourceToFactorySymbols: Map<string, Set<string>>|null = null;
private _coreImportsFrom: ts.SourceFile|null|undefined = undefined;
private _importRewriter: ImportRewriter|undefined = undefined;
private _reflector: TypeScriptReflectionHost|undefined = undefined;
@ -55,6 +53,7 @@ export class NgtscProgram implements api.Program {
private exportReferenceGraph: ReferenceGraph|null = null;
private flatIndexGenerator: FlatIndexGenerator|null = null;
private routeAnalyzer: NgModuleRouteAnalyzer|null = null;
private scopeRegistry: LocalModuleScopeRegistry|null = null;
private constructionDiagnostics: ts.Diagnostic[] = [];
private moduleResolver: ModuleResolver;
@ -69,6 +68,8 @@ export class NgtscProgram implements api.Program {
private perfTracker: PerfTracker|null = null;
private incrementalDriver: IncrementalDriver;
private typeCheckFilePath: AbsoluteFsPath;
private factoryTracker: FactoryTracker|null = null;
private modifiedResourceFiles: Set<string>|null;
private dtsTransforms: DtsTransformRegistry|null = null;
private mwpScanner: ModuleWithProvidersScanner|null = null;
@ -117,17 +118,12 @@ export class NgtscProgram implements api.Program {
// Factory generation.
const factoryGenerator = FactoryGenerator.forRootFiles(normalizedRootNames);
const factoryFileMap = factoryGenerator.factoryFileMap;
this.factoryToSourceInfo = new Map<string, FactoryInfo>();
this.sourceToFactorySymbols = new Map<string, Set<string>>();
factoryFileMap.forEach((sourceFilePath, factoryPath) => {
const moduleSymbolNames = new Set<string>();
this.sourceToFactorySymbols !.set(sourceFilePath, moduleSymbolNames);
this.factoryToSourceInfo !.set(factoryPath, {sourceFilePath, moduleSymbolNames});
});
const factoryFileNames = Array.from(factoryFileMap.keys());
rootFiles.push(...factoryFileNames);
generators.push(factoryGenerator);
this.factoryTracker = new FactoryTracker(factoryGenerator);
}
// Done separately to preserve the order of factory files before summary files in rootFiles.
@ -247,6 +243,7 @@ export class NgtscProgram implements api.Program {
const analyzeFileSpan = this.perfRecorder.start('analyzeFile', sf);
let analysisPromise = this.compilation !.analyzeAsync(sf);
this.scanForMwp(sf);
if (analysisPromise === undefined) {
this.perfRecorder.stop(analyzeFileSpan);
} else if (this.perfRecorder.enabled) {
@ -262,6 +259,8 @@ export class NgtscProgram implements api.Program {
this.perfRecorder.stop(analyzeSpan);
this.compilation.resolve();
this.recordNgModuleScopeDependencies();
// At this point, analysis is complete and the compiler can now calculate which files need to be
// emitted, so do that.
this.incrementalDriver.recordSuccessfulAnalysis();
@ -316,6 +315,16 @@ export class NgtscProgram implements api.Program {
throw new Error('Method not implemented.');
}
private scanForMwp(sf: ts.SourceFile): void {
this.mwpScanner !.scan(sf, {
addTypeReplacement: (node: ts.Declaration, type: Type): void => {
// Only obtain the return type transform for the source file once there's a type to replace,
// so that no transform is allocated when there's nothing to do.
this.dtsTransforms !.getReturnTypeTransform(sf).addTypeReplacement(node, type);
}
});
}
private ensureAnalyzed(): TraitCompiler {
if (this.compilation === undefined) {
const analyzeSpan = this.perfRecorder.start('analyze');
@ -326,11 +335,14 @@ export class NgtscProgram implements api.Program {
}
const analyzeFileSpan = this.perfRecorder.start('analyzeFile', sf);
this.compilation !.analyzeSync(sf);
this.scanForMwp(sf);
this.perfRecorder.stop(analyzeFileSpan);
}
this.perfRecorder.stop(analyzeSpan);
this.compilation.resolve();
this.recordNgModuleScopeDependencies();
// At this point, analysis is complete and the compiler can now calculate which files need to
// be emitted, so do that.
this.incrementalDriver.recordSuccessfulAnalysis();
@ -391,9 +403,9 @@ export class NgtscProgram implements api.Program {
afterDeclarationsTransforms.push(aliasTransformFactory(compilation.exportStatements));
}
if (this.factoryToSourceInfo !== null) {
if (this.factoryTracker !== null) {
beforeTransforms.push(
generatedFactoryTransform(this.factoryToSourceInfo, this.importRewriter));
generatedFactoryTransform(this.factoryTracker.sourceInfo, this.importRewriter));
}
beforeTransforms.push(ivySwitchTransform);
if (customTransforms && customTransforms.beforeTs) {
@ -609,10 +621,10 @@ export class NgtscProgram implements api.Program {
const localMetaRegistry = new LocalMetadataRegistry();
const localMetaReader: MetadataReader = localMetaRegistry;
const depScopeReader = new MetadataDtsModuleScopeResolver(dtsReader, this.aliasingHost);
const scopeRegistry = new LocalModuleScopeRegistry(
this.scopeRegistry = new LocalModuleScopeRegistry(
localMetaReader, depScopeReader, this.refEmitter, this.aliasingHost);
const scopeReader: ComponentScopeReader = scopeRegistry;
const metaRegistry = new CompoundMetadataRegistry([localMetaRegistry, scopeRegistry]);
const scopeReader: ComponentScopeReader = this.scopeRegistry;
const metaRegistry = new CompoundMetadataRegistry([localMetaRegistry, this.scopeRegistry]);
this.metaReader = new CompoundMetadataReader([localMetaReader, dtsReader]);
@ -637,8 +649,8 @@ export class NgtscProgram implements api.Program {
// Set up the IvyCompilation, which manages state for the Ivy transformer.
const handlers: DecoratorHandler<unknown, unknown, unknown>[] = [
new ComponentDecoratorHandler(
this.reflector, evaluator, metaRegistry, this.metaReader !, scopeReader, scopeRegistry,
this.isCore, this.resourceManager, this.rootDirs,
this.reflector, evaluator, metaRegistry, this.metaReader !, scopeReader,
this.scopeRegistry, this.isCore, this.resourceManager, this.rootDirs,
this.options.preserveWhitespaces || false, this.options.i18nUseExternalIds !== false,
this.options.enableI18nLegacyMessageIdFormat !== false, this.moduleResolver,
this.cycleAnalyzer, this.refEmitter, this.defaultImportTracker,
@ -656,15 +668,57 @@ export class NgtscProgram implements api.Program {
this.reflector, this.defaultImportTracker, this.isCore,
this.options.strictInjectionParameters || false),
new NgModuleDecoratorHandler(
this.reflector, evaluator, this.metaReader, metaRegistry, scopeRegistry,
referencesRegistry, this.isCore, this.routeAnalyzer, this.refEmitter,
this.reflector, evaluator, this.metaReader, metaRegistry, this.scopeRegistry,
referencesRegistry, this.isCore, this.routeAnalyzer, this.refEmitter, this.factoryTracker,
this.defaultImportTracker, this.closureCompilerEnabled, this.options.i18nInLocale),
];
return new TraitCompiler(
handlers, this.reflector, this.importRewriter, this.incrementalDriver, this.perfRecorder,
this.sourceToFactorySymbols, scopeRegistry,
this.options.compileNonExportedClasses !== false, this.dtsTransforms, this.mwpScanner);
handlers, this.reflector, this.perfRecorder,
this.options.compileNonExportedClasses !== false, this.dtsTransforms);
}
/**
* Reifies the inter-dependencies of NgModules and the components within their compilation scopes
* into the `IncrementalDriver`'s dependency graph.
*/
private recordNgModuleScopeDependencies() {
const recordSpan = this.perfRecorder.start('recordDependencies');
for (const scope of this.scopeRegistry !.getCompilationScopes()) {
const file = scope.declaration.getSourceFile();
const ngModuleFile = scope.ngModule.getSourceFile();
// A change to any dependency of the declaration causes the declaration to be invalidated,
// which requires the NgModule to be invalidated as well.
const deps = this.incrementalDriver.getFileDependencies(file);
this.incrementalDriver.trackFileDependencies(deps, ngModuleFile);
// A change to the NgModule file should cause the declaration itself to be invalidated.
this.incrementalDriver.trackFileDependency(ngModuleFile, file);
// A change to any directive/pipe in the compilation scope should cause the declaration to be
// invalidated.
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.
// a selector may have changed.
this.incrementalDriver.trackFileDependency(dirSf, file);
// When any of the dependencies of the declaration changes, the NgModule scope may be
// affected so a component within scope must be recompiled. Only components need to be
// recompiled, as directives are not dependent upon the compilation scope.
if (directive.isComponent) {
this.incrementalDriver.trackFileDependencies(deps, dirSf);
}
}
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.perfRecorder.stop(recordSpan);
}
private get reflector(): TypeScriptReflectionHost {

View File

@ -9,6 +9,7 @@
/// <reference types="node" />
export {FactoryGenerator, FactoryInfo, generatedFactoryTransform} from './src/factory_generator';
export {FactoryTracker} from './src/factory_tracker';
export {GeneratedShimsHostWrapper, ShimGenerator} from './src/host';
export {SummaryGenerator} from './src/summary_generator';
export {TypeCheckShimGenerator} from './src/typecheck_shim';

View File

@ -0,0 +1,38 @@
/**
* @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 {FactoryGenerator, FactoryInfo} from './factory_generator';
/**
* Maintains a mapping of which symbols in a .ngfactory file have been used.
*
* .ngfactory files are generated with one symbol per defined class in the source file, regardless
* of whether the classes in the source files are NgModules (because that isn't known at the time
* the factory files are generated). The `FactoryTracker` exists to support removing factory symbols
* which didn't end up being NgModules, by tracking the ones which are.
*/
export class FactoryTracker {
readonly sourceInfo = new Map<string, FactoryInfo>();
private sourceToFactorySymbols = new Map<string, Set<string>>();
constructor(generator: FactoryGenerator) {
generator.factoryFileMap.forEach((sourceFilePath, factoryPath) => {
const moduleSymbolNames = new Set<string>();
this.sourceToFactorySymbols.set(sourceFilePath, moduleSymbolNames);
this.sourceInfo.set(factoryPath, {sourceFilePath, moduleSymbolNames});
});
}
track(sf: ts.SourceFile, factorySymbolName: string): void {
if (this.sourceToFactorySymbols.has(sf.fileName)) {
this.sourceToFactorySymbols.get(sf.fileName) !.add(factorySymbolName);
}
}
}

View File

@ -11,12 +11,10 @@ ts_library(
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/incremental",
"//packages/compiler-cli/src/ngtsc/indexer",
"//packages/compiler-cli/src/ngtsc/modulewithproviders",
"//packages/compiler-cli/src/ngtsc/perf",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/scope",
"//packages/compiler-cli/src/ngtsc/translator",
"//packages/compiler-cli/src/ngtsc/typecheck",
"//packages/compiler-cli/src/ngtsc/util",

View File

@ -97,13 +97,29 @@ export interface DecoratorHandler<D, A, R> {
preanalyze?(node: ClassDeclaration, metadata: Readonly<D>): Promise<void>|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.
* Perform analysis on the decorator/class combination, extracting information from the class
* required for compilation.
*
* Returns analyzed metadata if successful, or an array of diagnostic messages if the analysis
* fails or the decorator isn't valid.
*
* Analysis should always be a "pure" operation, with no side effects. This is because the
* detect/analysis steps might be skipped for files which have not changed during incremental
* builds. Any side effects required for compilation (e.g. registration of metadata) should happen
* in the `register` phase, which is guaranteed to run even for incremental builds.
*/
analyze(node: ClassDeclaration, metadata: Readonly<D>, handlerFlags?: HandlerFlags):
AnalysisOutput<A>;
/**
* Post-process the analysis of a decorator/class combination and record any necessary information
* in the larger compilation.
*
* Registration always occurs for a given decorator/class, regardless of whether analysis was
* performed directly or whether the analysis results were reused from the previous program.
*/
register?(node: ClassDeclaration, analysis: A): void;
/**
* Registers information about the decorator for the indexing phase in a
* `IndexingContext`, which stores information about components discovered in the
@ -148,8 +164,6 @@ export interface DetectResult<M> {
export interface AnalysisOutput<A> {
analysis?: Readonly<A>;
diagnostics?: ts.Diagnostic[];
factorySymbolName?: string;
typeCheck?: boolean;
}
/**

View File

@ -11,12 +11,10 @@ import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {ImportRewriter} from '../../imports';
import {IncrementalDriver} from '../../incremental';
import {IndexingContext} from '../../indexer';
import {ModuleWithProvidersScanner} from '../../modulewithproviders';
import {PerfRecorder} from '../../perf';
import {ClassDeclaration, ReflectionHost, isNamedClassDeclaration} from '../../reflection';
import {LocalModuleScopeRegistry} from '../../scope';
import {TypeCheckContext} from '../../typecheck';
import {getSourceFile, isExported} from '../../util/src/typescript';
@ -24,8 +22,6 @@ import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPr
import {DtsTransformRegistry} from './declaration';
import {Trait, TraitState} from './trait';
const EMPTY_ARRAY: any = [];
/**
* Records information about a specific class that has matched traits.
*/
@ -91,20 +87,18 @@ export class TraitCompiler {
*/
constructor(
private handlers: DecoratorHandler<unknown, unknown, unknown>[],
private reflector: ReflectionHost, private importRewriter: ImportRewriter,
private incrementalDriver: IncrementalDriver, private perf: PerfRecorder,
private sourceToFactorySymbols: Map<string, Set<string>>|null,
private scopeRegistry: LocalModuleScopeRegistry, private compileNonExportedClasses: boolean,
private dtsTransforms: DtsTransformRegistry, private mwpScanner: ModuleWithProvidersScanner) {
}
private reflector: ReflectionHost, private perf: PerfRecorder,
private compileNonExportedClasses: boolean, private dtsTransforms: DtsTransformRegistry) {}
analyzeSync(sf: ts.SourceFile): void { this.analyze(sf, false); }
analyzeAsync(sf: ts.SourceFile): Promise<void>|void { return this.analyze(sf, true); }
analyzeAsync(sf: ts.SourceFile): Promise<void>|undefined { return this.analyze(sf, true); }
private analyze(sf: ts.SourceFile, preanalyze: false): void;
private analyze(sf: ts.SourceFile, preanalyze: true): Promise<void>|void;
private analyze(sf: ts.SourceFile, preanalyze: boolean): Promise<void>|void {
private analyze(sf: ts.SourceFile, preanalyze: true): Promise<void>|undefined;
private analyze(sf: ts.SourceFile, preanalyze: boolean): Promise<void>|undefined {
// analyze() really wants to return `Promise<void>|void`, but TypeScript cannot narrow a return
// type of 'void', so `undefined` is used instead.
const promises: Promise<void>[] = [];
const visit = (node: ts.Node): void => {
@ -116,18 +110,10 @@ export class TraitCompiler {
visit(sf);
this.mwpScanner.scan(sf, {
addTypeReplacement: (node: ts.Declaration, type: Type): void => {
// Only obtain the return type transform for the source file once there's a type to replace,
// so that no transform is allocated when there's nothing to do.
this.dtsTransforms.getReturnTypeTransform(sf).addTypeReplacement(node, type);
}
});
if (preanalyze && promises.length > 0) {
return Promise.all(promises).then(() => undefined as void);
} else {
return;
return undefined;
}
}
@ -260,13 +246,13 @@ export class TraitCompiler {
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);
// Analysis was successful. Trigger registration.
if (trait.handler.register !== undefined) {
trait.handler.register(clazz, result.analysis);
}
// Successfully analyzed and registered.
trait = trait.toAnalyzed(result.analysis);
} else {
trait = trait.toSkipped();
}
@ -329,8 +315,6 @@ export class TraitCompiler {
}
}
}
this.recordNgModuleScopeDependencies();
}
typeCheck(ctx: TypeCheckContext): void {
@ -443,43 +427,4 @@ export class TraitCompiler {
}
get exportStatements(): Map<string, Map<string, [string, string]>> { return this.reexportMap; }
private recordNgModuleScopeDependencies() {
const recordSpan = this.perf.start('recordDependencies');
for (const scope of this.scopeRegistry.getCompilationScopes()) {
const file = scope.declaration.getSourceFile();
const ngModuleFile = scope.ngModule.getSourceFile();
// A change to any dependency of the declaration causes the declaration to be invalidated,
// which requires the NgModule to be invalidated as well.
const deps = this.incrementalDriver.getFileDependencies(file);
this.incrementalDriver.trackFileDependencies(deps, ngModuleFile);
// A change to the NgModule file should cause the declaration itself to be invalidated.
this.incrementalDriver.trackFileDependency(ngModuleFile, file);
// A change to any directive/pipe in the compilation scope should cause the declaration to be
// invalidated.
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.
// a selector may have changed.
this.incrementalDriver.trackFileDependency(dirSf, file);
// When any of the dependencies of the declaration changes, the NgModule scope may be
// affected so a component within scope must be recompiled. Only components need to be
// recompiled, as directives are not dependent upon the compilation scope.
if (directive.isComponent) {
this.incrementalDriver.trackFileDependencies(deps, dirSf);
}
}
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);
}
}