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

@ -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);
}
}