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:

committed by
Kara Erickson

parent
252e3e9487
commit
50cdc0ac1b
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user