perf(compiler-cli): detect semantic changes and their effect on an incremental rebuild (#40947)

In Angular programs, changing a file may require other files to be
emitted as well due to implicit NgModule dependencies. For example, if
the selector of a directive is changed then all components that have
that directive in their compilation scope need to be recompiled, as the
change of selector may affect the directive matching results.

Until now, the compiler solved this problem using a single dependency
graph. The implicit NgModule dependencies were represented in this
graph, such that a changed file would correctly also cause other files
to be re-emitted. This approach is limited in a few ways:

1. The file dependency graph is used to determine whether it is safe to
   reuse the analysis data of an Angular decorated class. This analysis
   data is invariant to unrelated changes to the NgModule scope, but
   because the single dependency graph also tracked the implicit
   NgModule dependencies the compiler had to consider analysis data as
   stale far more often than necessary.
2. It is typical for a change to e.g. a directive to not affect its
   public API—its selector, inputs, outputs, or exportAs clause—in which
   case there is no need to re-emit all declarations in scope, as their
   compilation output wouldn't have changed.

This commit implements a mechanism by which the compiler is able to
determine the impact of a change by comparing it to the prior
compilation. To achieve this, a new graph is maintained that tracks all
public API information of all Angular decorated symbols. During an
incremental compilation this information is compared to the information
that was captured in the most recently succeeded compilation. This
determines the exact impact of the changes to the public API, which
is then used to determine which files need to be re-emitted.

Note that the file dependency graph remains, as it is still used to
track the dependencies of analysis data. This graph does no longer track
the implicit NgModule dependencies, which allows for better reuse of
analysis data.

These changes also fix a bug where template type-checking would fail to
incorporate changes made to a transitive base class of a
directive/component. This used to be a problem because transitive base
classes were not recorded as a transitive dependency in the file
dependency graph, such that prior type-check blocks would erroneously
be reused.

This commit also fixes an incorrectness where a change to a declaration
in NgModule `A` would not cause the declarations in NgModules that
import from NgModule `A` to be re-emitted. This was intentionally
incorrect as otherwise the performance of incremental rebuilds would
have been far worse. This is no longer a concern, as the compiler is now
able to only re-emit when actually necessary.

Fixes #34867
Fixes #40635
Closes #40728

PR Close #40947
This commit is contained in:
JoostK
2021-03-08 08:41:19 -08:00
committed by Andrew Kushnir
parent 8c062493a0
commit fed6a7ce7d
43 changed files with 5253 additions and 378 deletions
+1
View File
@@ -19,6 +19,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/incremental:api",
"//packages/compiler-cli/src/ngtsc/incremental/semantic_graph",
"//packages/compiler-cli/src/ngtsc/logging",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
@@ -14,6 +14,7 @@ import {CycleAnalyzer, CycleHandlingStrategy, ImportGraph} from '../../../src/ng
import {isFatalDiagnosticError} from '../../../src/ngtsc/diagnostics';
import {absoluteFromSourceFile, LogicalFileSystem, ReadonlyFileSystem} from '../../../src/ngtsc/file_system';
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, PrivateExportAliasingHost, Reexport, ReferenceEmitter} from '../../../src/ngtsc/imports';
import {SemanticSymbol} from '../../../src/ngtsc/incremental/semantic_graph';
import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, ResourceRegistry} from '../../../src/ngtsc/metadata';
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver, TypeCheckScopeRegistry} from '../../../src/ngtsc/scope';
@@ -92,7 +93,7 @@ export class DecorationAnalyzer {
cycleAnalyzer = new CycleAnalyzer(this.importGraph);
injectableRegistry = new InjectableClassRegistry(this.reflectionHost);
typeCheckScopeRegistry = new TypeCheckScopeRegistry(this.scopeRegistry, this.fullMetaReader);
handlers: DecoratorHandler<unknown, unknown, unknown>[] = [
handlers: DecoratorHandler<unknown, unknown, SemanticSymbol|null, unknown>[] = [
new ComponentDecoratorHandler(
this.reflectionHost, this.evaluator, this.fullRegistry, this.fullMetaReader,
this.scopeRegistry, this.scopeRegistry, this.typeCheckScopeRegistry, new ResourceRegistry(),
@@ -103,19 +104,21 @@ export class DecorationAnalyzer {
/* i18nNormalizeLineEndingsInICUs */ false, this.moduleResolver, this.cycleAnalyzer,
CycleHandlingStrategy.UseRemoteScoping, this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER,
NOOP_DEPENDENCY_TRACKER, this.injectableRegistry,
!!this.compilerOptions.annotateForClosureCompiler),
/* semanticDepGraphUpdater */ null, !!this.compilerOptions.annotateForClosureCompiler),
// See the note in ngtsc about why this cast is needed.
// clang-format off
new DirectiveDecoratorHandler(
this.reflectionHost, this.evaluator, this.fullRegistry, this.scopeRegistry,
this.fullMetaReader, NOOP_DEFAULT_IMPORT_RECORDER, this.injectableRegistry, this.isCore,
/* semanticDepGraphUpdater */ null,
!!this.compilerOptions.annotateForClosureCompiler,
// In ngcc we want to compile undecorated classes with Angular features. As of
// version 10, undecorated classes that use Angular features are no longer handled
// in ngtsc, but we want to ensure compatibility in ngcc for outdated libraries that
// have not migrated to explicit decorators. See: https://hackmd.io/@alx/ryfYYuvzH.
/* compileUndecoratedClassesWithAngularFeatures */ true
) as DecoratorHandler<unknown, unknown, unknown>,
) as DecoratorHandler<unknown, unknown, SemanticSymbol|null,unknown>,
// 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)
@@ -8,6 +8,7 @@
import * as ts from 'typescript';
import {IncrementalBuild} from '../../../src/ngtsc/incremental/api';
import {SemanticSymbol} from '../../../src/ngtsc/incremental/semantic_graph';
import {NOOP_PERF_RECORDER} from '../../../src/ngtsc/perf';
import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection';
import {CompilationMode, DecoratorHandler, DtsTransformRegistry, HandlerFlags, Trait, TraitCompiler} from '../../../src/ngtsc/transform';
@@ -22,11 +23,12 @@ import {isDefined} from '../utils';
*/
export class NgccTraitCompiler extends TraitCompiler {
constructor(
handlers: DecoratorHandler<unknown, unknown, unknown>[],
handlers: DecoratorHandler<unknown, unknown, SemanticSymbol|null, unknown>[],
private ngccReflector: NgccReflectionHost) {
super(
handlers, ngccReflector, NOOP_PERF_RECORDER, new NoIncrementalBuild(),
/* compileNonExportedClasses */ true, CompilationMode.FULL, new DtsTransformRegistry());
/* compileNonExportedClasses */ true, CompilationMode.FULL, new DtsTransformRegistry(),
/* semanticDepGraphUpdater */ null);
}
get analyzedFiles(): ts.SourceFile[] {
@@ -54,7 +56,7 @@ export class NgccTraitCompiler extends TraitCompiler {
* @param flags optional bitwise flag to influence the compilation of the decorator.
*/
injectSyntheticDecorator(clazz: ClassDeclaration, decorator: Decorator, flags?: HandlerFlags):
Trait<unknown, unknown, unknown>[] {
Trait<unknown, unknown, SemanticSymbol|null, unknown>[] {
const migratedTraits = this.detectTraits(clazz, [decorator]);
if (migratedTraits === null) {
return [];
@@ -16,8 +16,6 @@ export function isWithinPackage(packagePath: AbsoluteFsPath, filePath: AbsoluteF
class NoopDependencyTracker implements DependencyTracker {
addDependency(): void {}
addResourceDependency(): void {}
addTransitiveDependency(): void {}
addTransitiveResources(): void {}
recordDependencyAnalysisFailure(): void {}
}
@@ -19,6 +19,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/incremental/semantic_graph",
"//packages/compiler-cli/src/ngtsc/logging/testing",
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/reflection",
@@ -10,6 +10,7 @@ import * as ts from 'typescript';
import {FatalDiagnosticError, makeDiagnostic} from '../../../src/ngtsc/diagnostics';
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/testing';
import {SemanticSymbol} from '../../../src/ngtsc/incremental/semantic_graph';
import {MockLogger} from '../../../src/ngtsc/logging/testing';
import {ClassDeclaration, DeclarationNode, Decorator} from '../../../src/ngtsc/reflection';
import {loadFakeCore, loadTestFiles} from '../../../src/ngtsc/testing';
@@ -21,8 +22,9 @@ import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {Migration, MigrationHost} from '../../src/migrations/migration';
import {getRootFiles, makeTestEntryPointBundle} from '../helpers/utils';
type DecoratorHandlerWithResolve = DecoratorHandler<unknown, unknown, unknown>&{
resolve: NonNullable<DecoratorHandler<unknown, unknown, unknown>['resolve']>;
type DecoratorHandlerWithResolve =
DecoratorHandler<unknown, unknown, SemanticSymbol|null, unknown>&{
resolve: NonNullable<DecoratorHandler<unknown, unknown, SemanticSymbol|null, unknown>['resolve']>;
};
runInEachFileSystem(() => {
@@ -46,6 +48,7 @@ runInEachFileSystem(() => {
const handler = jasmine.createSpyObj<DecoratorHandlerWithResolve>('TestDecoratorHandler', [
'detect',
'analyze',
'symbol',
'register',
'resolve',
'compileFull',
@@ -442,7 +445,7 @@ runInEachFileSystem(() => {
describe('declaration files', () => {
it('should not run decorator handlers against declaration files', () => {
class FakeDecoratorHandler implements DecoratorHandler<{}|null, unknown, unknown> {
class FakeDecoratorHandler implements DecoratorHandler<{}|null, unknown, null, unknown> {
name = 'FakeDecoratorHandler';
precedence = HandlerPrecedence.PRIMARY;
@@ -452,6 +455,9 @@ runInEachFileSystem(() => {
analyze(): AnalysisOutput<unknown> {
throw new Error('analyze should not have been called');
}
symbol(): null {
throw new Error('symbol should not have been called');
}
compileFull(): CompileResult {
throw new Error('compile should not have been called');
}
@@ -11,6 +11,7 @@ import * as ts from 'typescript';
import {makeDiagnostic} from '../../../src/ngtsc/diagnostics';
import {absoluteFrom} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {SemanticSymbol} from '../../../src/ngtsc/incremental/semantic_graph';
import {MockLogger} from '../../../src/ngtsc/logging/testing';
import {ClassDeclaration, Decorator, isNamedClassDeclaration} from '../../../src/ngtsc/reflection';
import {getDeclaration, loadTestFiles} from '../../../src/ngtsc/testing';
@@ -44,7 +45,8 @@ runInEachFileSystem(() => {
});
function createMigrationHost({entryPoint, handlers}: {
entryPoint: EntryPointBundle; handlers: DecoratorHandler<unknown, unknown, unknown>[]
entryPoint: EntryPointBundle;
handlers: DecoratorHandler<unknown, unknown, SemanticSymbol|null, unknown>[]
}) {
const reflectionHost = new Esm2015ReflectionHost(new MockLogger(), false, entryPoint.src);
const compiler = new NgccTraitCompiler(handlers, reflectionHost);
@@ -190,7 +192,7 @@ runInEachFileSystem(() => {
});
});
class DetectDecoratorHandler implements DecoratorHandler<unknown, unknown, unknown> {
class DetectDecoratorHandler implements DecoratorHandler<unknown, unknown, null, unknown> {
readonly name = DetectDecoratorHandler.name;
constructor(private decorator: string, readonly precedence: HandlerPrecedence) {}
@@ -210,12 +212,16 @@ class DetectDecoratorHandler implements DecoratorHandler<unknown, unknown, unkno
return {};
}
symbol(node: ClassDeclaration, analysis: Readonly<unknown>): null {
return null;
}
compileFull(node: ClassDeclaration): CompileResult|CompileResult[] {
return [];
}
}
class DiagnosticProducingHandler implements DecoratorHandler<unknown, unknown, unknown> {
class DiagnosticProducingHandler implements DecoratorHandler<unknown, unknown, null, unknown> {
readonly name = DiagnosticProducingHandler.name;
readonly precedence = HandlerPrecedence.PRIMARY;
@@ -228,6 +234,10 @@ class DiagnosticProducingHandler implements DecoratorHandler<unknown, unknown, u
return {diagnostics: [makeDiagnostic(9999, node, 'test diagnostic')]};
}
symbol(node: ClassDeclaration, analysis: Readonly<unknown>): null {
return null;
}
compileFull(node: ClassDeclaration): CompileResult|CompileResult[] {
return [];
}
@@ -9,6 +9,7 @@
import {ErrorCode, makeDiagnostic, ngErrorCode} from '../../../src/ngtsc/diagnostics';
import {absoluteFrom} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {SemanticSymbol} from '../../../src/ngtsc/incremental/semantic_graph';
import {MockLogger} from '../../../src/ngtsc/logging/testing';
import {ClassDeclaration, Decorator, isNamedClassDeclaration} from '../../../src/ngtsc/reflection';
import {getDeclaration, loadTestFiles} from '../../../src/ngtsc/testing';
@@ -39,7 +40,8 @@ runInEachFileSystem(() => {
});
function createCompiler({entryPoint, handlers}: {
entryPoint: EntryPointBundle; handlers: DecoratorHandler<unknown, unknown, unknown>[]
entryPoint: EntryPointBundle;
handlers: DecoratorHandler<unknown, unknown, SemanticSymbol|null, unknown>[]
}) {
const reflectionHost = new Esm2015ReflectionHost(new MockLogger(), false, entryPoint.src);
return new NgccTraitCompiler(handlers, reflectionHost);
@@ -295,7 +297,7 @@ runInEachFileSystem(() => {
});
});
class TestHandler implements DecoratorHandler<unknown, unknown, unknown> {
class TestHandler implements DecoratorHandler<unknown, unknown, null, unknown> {
constructor(readonly name: string, protected log: string[]) {}
precedence = HandlerPrecedence.PRIMARY;
@@ -310,6 +312,10 @@ class TestHandler implements DecoratorHandler<unknown, unknown, unknown> {
return {};
}
symbol(node: ClassDeclaration, analysis: Readonly<unknown>): null {
return null;
}
compileFull(node: ClassDeclaration): CompileResult|CompileResult[] {
this.log.push(this.name + ':compile:' + node.name.text);
return [];
+4 -1
View File
@@ -7,6 +7,8 @@
*/
import {Trait, TraitState} from '@angular/compiler-cli/src/ngtsc/transform';
import * as ts from 'typescript';
import {SemanticSymbol} from '../../../src/ngtsc/incremental/semantic_graph';
import {CtorParameter, TypeValueReferenceKind} from '../../../src/ngtsc/reflection';
/**
@@ -50,7 +52,8 @@ export function expectTypeValueReferencesForParameters(
});
}
export function getTraitDiagnostics(trait: Trait<unknown, unknown, unknown>): ts.Diagnostic[]|null {
export function getTraitDiagnostics(trait: Trait<unknown, unknown, SemanticSymbol|null, unknown>):
ts.Diagnostic[]|null {
if (trait.state === TraitState.Analyzed) {
return trait.analysisDiagnostics;
} else if (trait.state === TraitState.Resolved) {
@@ -14,6 +14,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/incremental:api",
"//packages/compiler-cli/src/ngtsc/incremental/semantic_graph",
"//packages/compiler-cli/src/ngtsc/indexer",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
@@ -14,6 +14,7 @@ import {ErrorCode, FatalDiagnosticError, makeDiagnostic, makeRelatedInformation}
import {absoluteFrom, relative} from '../../file_system';
import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from '../../imports';
import {DependencyTracker} from '../../incremental/api';
import {extractSemanticTypeParameters, isArrayEqual, isReferenceEqual, SemanticDepGraphUpdater, SemanticReference, SemanticSymbol} from '../../incremental/semantic_graph';
import {IndexingContext} from '../../indexer';
import {ClassPropertyMapping, ComponentResources, DirectiveMeta, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry, Resource, ResourceRegistry} from '../../metadata';
import {EnumValue, PartialEvaluator, ResolvedValue} from '../../partial_evaluator';
@@ -26,9 +27,10 @@ import {SubsetOfKeys} from '../../util/src/typescript';
import {ResourceLoader} from './api';
import {createValueHasWrongTypeError, getDirectiveDiagnostics, getProviderDiagnostics} from './diagnostics';
import {extractDirectiveMetadata, parseFieldArrayValue} from './directive';
import {DirectiveSymbol, extractDirectiveMetadata, parseFieldArrayValue} from './directive';
import {compileNgFactoryDefField} from './factory';
import {generateSetClassMetadataCall} from './metadata';
import {NgModuleSymbol} from './ng_module';
import {findAngularDecorator, isAngularCoreReference, isExpressionForwardReference, readBaseClass, resolveProvidersRequiringFactory, unwrapExpression, wrapFunctionExpressionsInParens} from './util';
const EMPTY_MAP = new Map<string, Expression>();
@@ -111,11 +113,85 @@ export const enum ResourceTypeForDiagnostics {
StylesheetFromDecorator,
}
/**
* Represents an Angular component.
*/
export class ComponentSymbol extends DirectiveSymbol {
usedDirectives: SemanticReference[] = [];
usedPipes: SemanticReference[] = [];
isRemotelyScoped = false;
isEmitAffected(previousSymbol: SemanticSymbol, publicApiAffected: Set<SemanticSymbol>): boolean {
if (!(previousSymbol instanceof ComponentSymbol)) {
return true;
}
// Create an equality function that considers symbols equal if they represent the same
// declaration, but only if the symbol in the current compilation does not have its public API
// affected.
const isSymbolUnaffected = (current: SemanticReference, previous: SemanticReference) =>
isReferenceEqual(current, previous) && !publicApiAffected.has(current.symbol);
// The emit of a component is affected if either of the following is true:
// 1. The component used to be remotely scoped but no longer is, or vice versa.
// 2. The list of used directives has changed or any of those directives have had their public
// API changed. If the used directives have been reordered but not otherwise affected then
// the component must still be re-emitted, as this may affect directive instantiation order.
// 3. The list of used pipes has changed, or any of those pipes have had their public API
// changed.
return this.isRemotelyScoped !== previousSymbol.isRemotelyScoped ||
!isArrayEqual(this.usedDirectives, previousSymbol.usedDirectives, isSymbolUnaffected) ||
!isArrayEqual(this.usedPipes, previousSymbol.usedPipes, isSymbolUnaffected);
}
isTypeCheckBlockAffected(
previousSymbol: SemanticSymbol, typeCheckApiAffected: Set<SemanticSymbol>): boolean {
if (!(previousSymbol instanceof ComponentSymbol)) {
return true;
}
// To verify that a used directive is not affected we need to verify that its full inheritance
// chain is not present in `typeCheckApiAffected`.
const isInheritanceChainAffected = (symbol: SemanticSymbol): boolean => {
let currentSymbol: SemanticSymbol|null = symbol;
while (currentSymbol instanceof DirectiveSymbol) {
if (typeCheckApiAffected.has(currentSymbol)) {
return true;
}
currentSymbol = currentSymbol.baseClass;
}
return false;
};
// Create an equality function that considers directives equal if they represent the same
// declaration and if the symbol and all symbols it inherits from in the current compilation
// do not have their type-check API affected.
const isDirectiveUnaffected = (current: SemanticReference, previous: SemanticReference) =>
isReferenceEqual(current, previous) && !isInheritanceChainAffected(current.symbol);
// Create an equality function that considers pipes equal if they represent the same
// declaration and if the symbol in the current compilation does not have its type-check
// API affected.
const isPipeUnaffected = (current: SemanticReference, previous: SemanticReference) =>
isReferenceEqual(current, previous) && !typeCheckApiAffected.has(current.symbol);
// The emit of a type-check block of a component is affected if either of the following is true:
// 1. The list of used directives has changed or any of those directives have had their
// type-check API changed.
// 2. The list of used pipes has changed, or any of those pipes have had their type-check API
// changed.
return !isArrayEqual(
this.usedDirectives, previousSymbol.usedDirectives, isDirectiveUnaffected) ||
!isArrayEqual(this.usedPipes, previousSymbol.usedPipes, isPipeUnaffected);
}
}
/**
* `DecoratorHandler` which handles the `@Component` annotation.
*/
export class ComponentDecoratorHandler implements
DecoratorHandler<Decorator, ComponentAnalysisData, ComponentResolutionData> {
DecoratorHandler<Decorator, ComponentAnalysisData, ComponentSymbol, ComponentResolutionData> {
constructor(
private reflector: ReflectionHost, private evaluator: PartialEvaluator,
private metaRegistry: MetadataRegistry, private metaReader: MetadataReader,
@@ -131,6 +207,7 @@ export class ComponentDecoratorHandler implements
private defaultImportRecorder: DefaultImportRecorder,
private depTracker: DependencyTracker|null,
private injectableRegistry: InjectableClassRegistry,
private semanticDepGraphUpdater: SemanticDepGraphUpdater|null,
private annotateForClosureCompiler: boolean) {}
private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>();
@@ -397,6 +474,14 @@ export class ComponentDecoratorHandler implements
return output;
}
symbol(node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>): ComponentSymbol {
const typeParameters = extractSemanticTypeParameters(node);
return new ComponentSymbol(
node, analysis.meta.selector, analysis.inputs, analysis.outputs, analysis.meta.exportAs,
analysis.typeCheckMeta, typeParameters);
}
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.
@@ -476,8 +561,13 @@ export class ComponentDecoratorHandler implements
meta.template.sourceMapping, meta.template.file, meta.template.errors);
}
resolve(node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>):
ResolveResult<ComponentResolutionData> {
resolve(
node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>,
symbol: ComponentSymbol): ResolveResult<ComponentResolutionData> {
if (this.semanticDepGraphUpdater !== null && analysis.baseClass instanceof Reference) {
symbol.baseClass = this.semanticDepGraphUpdater.getSymbol(analysis.baseClass.node);
}
if (analysis.isPoisoned && !this.usePoisonedData) {
return {};
}
@@ -538,7 +628,7 @@ export class ComponentDecoratorHandler implements
const bound = binder.bind({template: metadata.template.nodes});
// The BoundTarget knows which directives and pipes matched the template.
type UsedDirective = R3UsedDirectiveMetadata&{ref: Reference};
type UsedDirective = R3UsedDirectiveMetadata&{ref: Reference<ClassDeclaration>};
const usedDirectives: UsedDirective[] = bound.getUsedDirectives().map(directive => {
return {
ref: directive.ref,
@@ -550,8 +640,7 @@ export class ComponentDecoratorHandler implements
isComponent: directive.isComponent,
};
});
type UsedPipe = {ref: Reference, pipeName: string, expression: Expression};
type UsedPipe = {ref: Reference<ClassDeclaration>, pipeName: string, expression: Expression};
const usedPipes: UsedPipe[] = [];
for (const pipeName of bound.getUsedPipes()) {
if (!pipes.has(pipeName)) {
@@ -564,6 +653,13 @@ export class ComponentDecoratorHandler implements
expression: this.refEmitter.emit(pipe, context),
});
}
if (this.semanticDepGraphUpdater !== null) {
symbol.usedDirectives = usedDirectives.map(
dir => this.semanticDepGraphUpdater!.getSemanticReference(dir.ref.node, dir.type));
symbol.usedPipes = usedPipes.map(
pipe =>
this.semanticDepGraphUpdater!.getSemanticReference(pipe.ref.node, pipe.expression));
}
// Scan through the directives/pipes actually used in the template and check whether any
// import which needs to be generated would create a cycle.
@@ -582,7 +678,8 @@ export class ComponentDecoratorHandler implements
}
}
if (cyclesFromDirectives.size === 0 && cyclesFromPipes.size === 0) {
const cycleDetected = cyclesFromDirectives.size !== 0 || cyclesFromPipes.size !== 0;
if (!cycleDetected) {
// No cycle was detected. Record the imports that need to be created in the cycle detector
// so that future cyclic import checks consider their production.
for (const {type} of usedDirectives) {
@@ -613,6 +710,21 @@ export class ComponentDecoratorHandler implements
// NgModule file will take care of setting the directives for the component.
this.scopeRegistry.setComponentRemoteScope(
node, usedDirectives.map(dir => dir.ref), usedPipes.map(pipe => pipe.ref));
symbol.isRemotelyScoped = true;
// If a semantic graph is being tracked, record the fact that this component is remotely
// scoped with the declaring NgModule symbol as the NgModule's emit becomes dependent on
// the directive/pipe usages of this component.
if (this.semanticDepGraphUpdater !== null) {
const moduleSymbol = this.semanticDepGraphUpdater.getSymbol(scope.ngModule);
if (!(moduleSymbol instanceof NgModuleSymbol)) {
throw new Error(
`AssertionError: Expected ${scope.ngModule.name} to be an NgModuleSymbol.`);
}
moduleSymbol.addRemotelyScopedComponent(
symbol, symbol.usedDirectives, symbol.usedPipes);
}
} else {
// We are not able to handle this cycle so throw an error.
const relatedMessages: ts.DiagnosticRelatedInformation[] = [];
@@ -11,8 +11,10 @@ import {emitDistinctChangesOnlyDefaultValue} from '@angular/compiler/src/core';
import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {absoluteFromSourceFile} from '../../file_system';
import {DefaultImportRecorder, Reference} from '../../imports';
import {ClassPropertyMapping, DirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata';
import {areTypeParametersEqual, extractSemanticTypeParameters, isArrayEqual, isSetEqual, isSymbolEqual, SemanticDepGraphUpdater, SemanticSymbol, SemanticTypeParameter} from '../../incremental/semantic_graph';
import {BindingPropertyName, ClassPropertyMapping, ClassPropertyName, DirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry, TemplateGuardMeta} from '../../metadata';
import {extractDirectiveTypeCheckMeta} from '../../metadata/src/util';
import {DynamicValue, EnumValue, PartialEvaluator} from '../../partial_evaluator';
import {ClassDeclaration, ClassMember, ClassMemberKind, Decorator, filterToMembersWithDecorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
@@ -46,13 +48,138 @@ export interface DirectiveHandlerData {
isStructural: boolean;
}
/**
* Represents an Angular directive. Components are represented by `ComponentSymbol`, which inherits
* from this symbol.
*/
export class DirectiveSymbol extends SemanticSymbol {
baseClass: SemanticSymbol|null = null;
constructor(
decl: ClassDeclaration, public readonly selector: string|null,
public readonly inputs: ClassPropertyMapping, public readonly outputs: ClassPropertyMapping,
public readonly exportAs: string[]|null,
public readonly typeCheckMeta: DirectiveTypeCheckMeta,
public readonly typeParameters: SemanticTypeParameter[]|null) {
super(decl);
}
isPublicApiAffected(previousSymbol: SemanticSymbol): boolean {
// Note: since components and directives have exactly the same items contributing to their
// public API, it is okay for a directive to change into a component and vice versa without
// the API being affected.
if (!(previousSymbol instanceof DirectiveSymbol)) {
return true;
}
// Directives and components have a public API of:
// 1. Their selector.
// 2. The binding names of their inputs and outputs; a change in ordering is also considered
// to be a change in public API.
// 3. The list of exportAs names and its ordering.
return this.selector !== previousSymbol.selector ||
!isArrayEqual(this.inputs.propertyNames, previousSymbol.inputs.propertyNames) ||
!isArrayEqual(this.outputs.propertyNames, previousSymbol.outputs.propertyNames) ||
!isArrayEqual(this.exportAs, previousSymbol.exportAs);
}
isTypeCheckApiAffected(previousSymbol: SemanticSymbol): boolean {
// If the public API of the directive has changed, then so has its type-check API.
if (this.isPublicApiAffected(previousSymbol)) {
return true;
}
if (!(previousSymbol instanceof DirectiveSymbol)) {
return true;
}
// The type-check block also depends on the class property names, as writes property bindings
// directly into the backing fields.
if (!isArrayEqual(
Array.from(this.inputs), Array.from(previousSymbol.inputs), isInputMappingEqual) ||
!isArrayEqual(
Array.from(this.outputs), Array.from(previousSymbol.outputs), isInputMappingEqual)) {
return true;
}
// The type parameters of a directive are emitted into the type constructors in the type-check
// block of a component, so if the type parameters are not considered equal then consider the
// type-check API of this directive to be affected.
if (!areTypeParametersEqual(this.typeParameters, previousSymbol.typeParameters)) {
return true;
}
// The type-check metadata is used during TCB code generation, so any changes should invalidate
// prior type-check files.
if (!isTypeCheckMetaEqual(this.typeCheckMeta, previousSymbol.typeCheckMeta)) {
return true;
}
// Changing the base class of a directive means that its inputs/outputs etc may have changed,
// so the type-check block of components that use this directive needs to be regenerated.
if (!isBaseClassEqual(this.baseClass, previousSymbol.baseClass)) {
return true;
}
return false;
}
}
function isInputMappingEqual(
current: [ClassPropertyName, BindingPropertyName],
previous: [ClassPropertyName, BindingPropertyName]): boolean {
return current[0] === previous[0] && current[1] === previous[1];
}
function isTypeCheckMetaEqual(
current: DirectiveTypeCheckMeta, previous: DirectiveTypeCheckMeta): boolean {
if (current.hasNgTemplateContextGuard !== previous.hasNgTemplateContextGuard) {
return false;
}
if (current.isGeneric !== previous.isGeneric) {
// Note: changes in the number of type parameters is also considered in `areTypeParametersEqual`
// so this check is technically not needed; it is done anyway for completeness in terms of
// whether the `DirectiveTypeCheckMeta` struct itself compares equal or not.
return false;
}
if (!isArrayEqual(current.ngTemplateGuards, previous.ngTemplateGuards, isTemplateGuardEqual)) {
return false;
}
if (!isSetEqual(current.coercedInputFields, previous.coercedInputFields)) {
return false;
}
if (!isSetEqual(current.restrictedInputFields, previous.restrictedInputFields)) {
return false;
}
if (!isSetEqual(current.stringLiteralInputFields, previous.stringLiteralInputFields)) {
return false;
}
if (!isSetEqual(current.undeclaredInputFields, previous.undeclaredInputFields)) {
return false;
}
return true;
}
function isTemplateGuardEqual(current: TemplateGuardMeta, previous: TemplateGuardMeta): boolean {
return current.inputName === previous.inputName && current.type === previous.type;
}
function isBaseClassEqual(current: SemanticSymbol|null, previous: SemanticSymbol|null): boolean {
if (current === null || previous === null) {
return current === previous;
}
return isSymbolEqual(current, previous);
}
export class DirectiveDecoratorHandler implements
DecoratorHandler<Decorator|null, DirectiveHandlerData, unknown> {
DecoratorHandler<Decorator|null, DirectiveHandlerData, DirectiveSymbol, unknown> {
constructor(
private reflector: ReflectionHost, private evaluator: PartialEvaluator,
private metaRegistry: MetadataRegistry, private scopeRegistry: LocalModuleScopeRegistry,
private metaReader: MetadataReader, private defaultImportRecorder: DefaultImportRecorder,
private injectableRegistry: InjectableClassRegistry, private isCore: boolean,
private semanticDepGraphUpdater: SemanticDepGraphUpdater|null,
private annotateForClosureCompiler: boolean,
private compileUndecoratedClassesWithAngularFeatures: boolean) {}
@@ -116,6 +243,14 @@ export class DirectiveDecoratorHandler implements
};
}
symbol(node: ClassDeclaration, analysis: Readonly<DirectiveHandlerData>): DirectiveSymbol {
const typeParameters = extractSemanticTypeParameters(node);
return new DirectiveSymbol(
node, analysis.meta.selector, analysis.inputs, analysis.outputs, analysis.meta.exportAs,
analysis.typeCheckMeta, typeParameters);
}
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.
@@ -138,9 +273,13 @@ export class DirectiveDecoratorHandler implements
this.injectableRegistry.registerInjectable(node);
}
resolve(node: ClassDeclaration, analysis: DirectiveHandlerData): ResolveResult<unknown> {
const diagnostics: ts.Diagnostic[] = [];
resolve(node: ClassDeclaration, analysis: DirectiveHandlerData, symbol: DirectiveSymbol):
ResolveResult<unknown> {
if (this.semanticDepGraphUpdater !== null && analysis.baseClass instanceof Reference) {
symbol.baseClass = this.semanticDepGraphUpdater.getSymbol(analysis.baseClass.node);
}
const diagnostics: ts.Diagnostic[] = [];
if (analysis.providersRequiringFactory !== null &&
analysis.meta.providers instanceof WrappedNodeExpr) {
const providerDiagnostics = getProviderDiagnostics(
@@ -30,7 +30,7 @@ export interface InjectableHandlerData {
* Adapts the `compileIvyInjectable` compiler for `@Injectable` decorators to the Ivy compiler.
*/
export class InjectableDecoratorHandler implements
DecoratorHandler<Decorator, InjectableHandlerData, unknown> {
DecoratorHandler<Decorator, InjectableHandlerData, null, unknown> {
constructor(
private reflector: ReflectionHost, private defaultImportRecorder: DefaultImportRecorder,
private isCore: boolean, private strictCtorDeps: boolean,
@@ -83,6 +83,10 @@ export class InjectableDecoratorHandler implements
};
}
symbol(): null {
return null;
}
register(node: ClassDeclaration): void {
this.injectableRegistry.registerInjectable(node);
}
@@ -11,9 +11,10 @@ import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError, makeDiagnostic, makeRelatedInformation} from '../../diagnostics';
import {DefaultImportRecorder, Reference, ReferenceEmitter} from '../../imports';
import {isArrayEqual, isReferenceEqual, isSymbolEqual, SemanticReference, SemanticSymbol} from '../../incremental/semantic_graph';
import {InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata';
import {PartialEvaluator, ResolvedValue} from '../../partial_evaluator';
import {ClassDeclaration, DeclarationNode, Decorator, isNamedClassDeclaration, ReflectionHost, reflectObjectLiteral, typeNodeToValueExpr} from '../../reflection';
import {ClassDeclaration, Decorator, isNamedClassDeclaration, ReflectionHost, reflectObjectLiteral, typeNodeToValueExpr} from '../../reflection';
import {NgModuleRouteAnalyzer} from '../../routing';
import {LocalModuleScopeRegistry, ScopeData} from '../../scope';
import {FactoryTracker} from '../../shims/api';
@@ -44,13 +45,84 @@ export interface NgModuleResolution {
injectorImports: Expression[];
}
/**
* Represents an Angular NgModule.
*/
export class NgModuleSymbol extends SemanticSymbol {
private remotelyScopedComponents: {
component: SemanticSymbol,
usedDirectives: SemanticReference[],
usedPipes: SemanticReference[]
}[] = [];
isPublicApiAffected(previousSymbol: SemanticSymbol): boolean {
if (!(previousSymbol instanceof NgModuleSymbol)) {
return true;
}
// NgModules don't have a public API that could affect emit of Angular decorated classes.
return false;
}
isEmitAffected(previousSymbol: SemanticSymbol): boolean {
if (!(previousSymbol instanceof NgModuleSymbol)) {
return true;
}
// compare our remotelyScopedComponents to the previous symbol
if (previousSymbol.remotelyScopedComponents.length !== this.remotelyScopedComponents.length) {
return true;
}
for (const currEntry of this.remotelyScopedComponents) {
const prevEntry = previousSymbol.remotelyScopedComponents.find(prevEntry => {
return isSymbolEqual(prevEntry.component, currEntry.component);
});
if (prevEntry === undefined) {
// No previous entry was found, which means that this component became remotely scoped and
// hence this NgModule needs to be re-emitted.
return true;
}
if (!isArrayEqual(currEntry.usedDirectives, prevEntry.usedDirectives, isReferenceEqual)) {
// The list of used directives or their order has changed. Since this NgModule emits
// references to the list of used directives, it should be re-emitted to update this list.
// Note: the NgModule does not have to be re-emitted when any of the directives has had
// their public API changed, as the NgModule only emits a reference to the symbol by its
// name. Therefore, testing for symbol equality is sufficient.
return true;
}
if (!isArrayEqual(currEntry.usedPipes, prevEntry.usedPipes, isReferenceEqual)) {
return true;
}
}
return false;
}
isTypeCheckApiAffected(previousSymbol: SemanticSymbol): boolean {
if (!(previousSymbol instanceof NgModuleSymbol)) {
return true;
}
return false;
}
addRemotelyScopedComponent(
component: SemanticSymbol, usedDirectives: SemanticReference[],
usedPipes: SemanticReference[]): void {
this.remotelyScopedComponents.push({component, usedDirectives, usedPipes});
}
}
/**
* Compiles @NgModule annotations to ngModuleDef fields.
*
* TODO(alxhub): handle injector side of things as well.
*/
export class NgModuleDecoratorHandler implements
DecoratorHandler<Decorator, NgModuleAnalysis, NgModuleResolution> {
DecoratorHandler<Decorator, NgModuleAnalysis, NgModuleSymbol, NgModuleResolution> {
constructor(
private reflector: ReflectionHost, private evaluator: PartialEvaluator,
private metaReader: MetadataReader, private metaRegistry: MetadataRegistry,
@@ -293,6 +365,10 @@ export class NgModuleDecoratorHandler implements
};
}
symbol(node: ClassDeclaration): NgModuleSymbol {
return new NgModuleSymbol(node);
}
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
@@ -11,13 +11,14 @@ import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {DefaultImportRecorder, Reference} from '../../imports';
import {SemanticSymbol} from '../../incremental/semantic_graph';
import {InjectableClassRegistry, MetadataRegistry} from '../../metadata';
import {PartialEvaluator} from '../../partial_evaluator';
import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
import {LocalModuleScopeRegistry} from '../../scope';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../transform';
import {createValueHasWrongTypeError} from './diagnostics';
import {createValueHasWrongTypeError} from './diagnostics';
import {compileNgFactoryDefField} from './factory';
import {generateSetClassMetadataCall} from './metadata';
import {findAngularDecorator, getValidConstructorDependencies, makeDuplicateDeclarationError, unwrapExpression, wrapTypeReference} from './util';
@@ -27,7 +28,29 @@ export interface PipeHandlerData {
metadataStmt: Statement|null;
}
export class PipeDecoratorHandler implements DecoratorHandler<Decorator, PipeHandlerData, unknown> {
/**
* Represents an Angular pipe.
*/
export class PipeSymbol extends SemanticSymbol {
constructor(decl: ClassDeclaration, public readonly name: string) {
super(decl);
}
isPublicApiAffected(previousSymbol: SemanticSymbol): boolean {
if (!(previousSymbol instanceof PipeSymbol)) {
return true;
}
return this.name !== previousSymbol.name;
}
isTypeCheckApiAffected(previousSymbol: SemanticSymbol): boolean {
return this.isPublicApiAffected(previousSymbol);
}
}
export class PipeDecoratorHandler implements
DecoratorHandler<Decorator, PipeHandlerData, PipeSymbol, unknown> {
constructor(
private reflector: ReflectionHost, private evaluator: PartialEvaluator,
private metaRegistry: MetadataRegistry, private scopeRegistry: LocalModuleScopeRegistry,
@@ -114,6 +137,10 @@ export class PipeDecoratorHandler implements DecoratorHandler<Decorator, PipeHan
};
}
symbol(node: ClassDeclaration, analysis: Readonly<PipeHandlerData>): PipeSymbol {
return new PipeSymbol(node, analysis.meta.name);
}
register(node: ClassDeclaration, analysis: Readonly<PipeHandlerData>): void {
const ref = new Reference(node);
this.metaRegistry.registerPipeMetadata({ref, name: analysis.meta.pipeName});
@@ -17,6 +17,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/incremental:api",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/reflection",
@@ -78,6 +78,7 @@ function setup(program: ts.Program, options: ts.CompilerOptions, host: ts.Compil
NOOP_DEFAULT_IMPORT_RECORDER,
/* depTracker */ null,
injectableRegistry,
/* semanticDepGraphUpdater */ null,
/* annotateForClosureCompiler */ false,
);
return {reflectionHost, handler};
@@ -247,7 +248,8 @@ runInEachFileSystem(() => {
return fail('Failed to recognize @Component');
}
const {analysis} = handler.analyze(TestCmp, detected.metadata);
const resolution = handler.resolve(TestCmp, analysis!);
const symbol = handler.symbol(TestCmp, analysis!);
const resolution = handler.resolve(TestCmp, analysis!, symbol);
const compileResult =
handler.compileFull(TestCmp, analysis!, resolution.data!, new ConstantPool());
@@ -169,6 +169,7 @@ runInEachFileSystem(() => {
const handler = new DirectiveDecoratorHandler(
reflectionHost, evaluator, scopeRegistry, scopeRegistry, metaReader,
NOOP_DEFAULT_IMPORT_RECORDER, injectableRegistry, /*isCore*/ false,
/*semanticDepGraphUpdater*/ null,
/*annotateForClosureCompiler*/ false,
/*detectUndecoratedClassesWithAngularFeatures*/ false);
@@ -19,6 +19,8 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/incremental",
"//packages/compiler-cli/src/ngtsc/incremental:api",
"//packages/compiler-cli/src/ngtsc/incremental/semantic_graph",
"//packages/compiler-cli/src/ngtsc/indexer",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/modulewithproviders",
@@ -16,6 +16,7 @@ import {checkForPrivateExports, ReferenceGraph} from '../../entry_point';
import {LogicalFileSystem, resolve} from '../../file_system';
import {AbsoluteModuleStrategy, AliasingHost, AliasStrategy, DefaultImportTracker, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, PrivateExportAliasingHost, R3SymbolsImportRewriter, Reference, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy, UnifiedModulesAliasingHost, UnifiedModulesStrategy} from '../../imports';
import {IncrementalBuildStrategy, IncrementalDriver} from '../../incremental';
import {SemanticSymbol} from '../../incremental/semantic_graph';
import {generateAnalysis, IndexedComponent, IndexingContext} from '../../indexer';
import {ComponentResources, CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, MetadataReader, ResourceRegistry} from '../../metadata';
import {ModuleWithProvidersScanner} from '../../modulewithproviders';
@@ -636,8 +637,6 @@ export class NgCompiler {
private resolveCompilation(traitCompiler: TraitCompiler): void {
traitCompiler.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(traitCompiler);
@@ -810,74 +809,6 @@ export class NgCompiler {
return this.nonTemplateDiagnostics;
}
/**
* 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');
const depGraph = this.incrementalDriver.depGraph;
for (const scope of this.compilation!.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.
depGraph.addTransitiveDependency(ngModuleFile, file);
// A change to the NgModule file should cause the declaration itself to be invalidated.
depGraph.addDependency(file, ngModuleFile);
const meta =
this.compilation!.metaReader.getDirectiveMetadata(new Reference(scope.declaration));
if (meta !== null && meta.isComponent) {
// If a component's template changes, it might have affected the import graph, and thus the
// remote scoping feature which is activated in the event of potential import cycles. Thus,
// the module depends not only on the transitive dependencies of the component, but on its
// resources as well.
depGraph.addTransitiveResources(ngModuleFile, file);
// A change to any directive/pipe in the compilation scope should cause the component to be
// invalidated.
for (const directive of scope.directives) {
// When a directive in scope is updated, the component needs to be recompiled as e.g. a
// selector may have changed.
depGraph.addTransitiveDependency(file, directive.ref.node.getSourceFile());
}
for (const pipe of scope.pipes) {
// When a pipe in scope is updated, the component needs to be recompiled as e.g. the
// pipe's name may have changed.
depGraph.addTransitiveDependency(file, pipe.ref.node.getSourceFile());
}
// Components depend on the entire export scope. In addition to transitive dependencies on
// all directives/pipes in the export scope, they also depend on every NgModule in the
// scope, as changes to a module may add new directives/pipes to the scope.
for (const depModule of scope.ngModules) {
// There is a correctness issue here. To be correct, this should be a transitive
// dependency on the depModule file, since the depModule's exports might change via one of
// its dependencies, even if depModule's file itself doesn't change. However, doing this
// would also trigger recompilation if a non-exported component or directive changed,
// which causes performance issues for rebuilds.
//
// Given the rebuild issue is an edge case, currently we err on the side of performance
// instead of correctness. A correct and performant design would distinguish between
// changes to the depModule which affect its export scope and changes which do not, and
// only add a dependency for the former. This concept is currently in development.
//
// TODO(alxhub): fix correctness issue by understanding the semantics of the dependency.
depGraph.addDependency(file, depModule.getSourceFile());
}
} else {
// Directives (not components) and pipes only depend on the NgModule which directly declares
// them.
depGraph.addDependency(file, ngModuleFile);
}
}
this.perfRecorder.stop(recordSpan);
}
private scanForMwp(sf: ts.SourceFile): void {
this.compilation!.mwpScanner.scan(sf, {
addTypeReplacement: (node: ts.Declaration, type: Type): void => {
@@ -957,6 +888,7 @@ export class NgCompiler {
const scopeRegistry =
new LocalModuleScopeRegistry(localMetaReader, depScopeReader, refEmitter, aliasingHost);
const scopeReader: ComponentScopeReader = scopeRegistry;
const semanticDepGraphUpdater = this.incrementalDriver.getSemanticDepGraphUpdater();
const metaRegistry = new CompoundMetadataRegistry([localMetaRegistry, scopeRegistry]);
const injectableRegistry = new InjectableClassRegistry(reflector);
@@ -998,7 +930,7 @@ export class NgCompiler {
CycleHandlingStrategy.Error;
// Set up the IvyCompilation, which manages state for the Ivy transformer.
const handlers: DecoratorHandler<unknown, unknown, unknown>[] = [
const handlers: DecoratorHandler<unknown, unknown, SemanticSymbol|null, unknown>[] = [
new ComponentDecoratorHandler(
reflector, evaluator, metaRegistry, metaReader, scopeReader, scopeRegistry,
typeCheckScopeRegistry, resourceRegistry, isCore, this.resourceManager,
@@ -1007,15 +939,16 @@ export class NgCompiler {
this.options.enableI18nLegacyMessageIdFormat !== false, this.usePoisonedData,
this.options.i18nNormalizeLineEndingsInICUs, this.moduleResolver, this.cycleAnalyzer,
cycleHandlingStrategy, refEmitter, defaultImportTracker, this.incrementalDriver.depGraph,
injectableRegistry, this.closureCompilerEnabled),
injectableRegistry, semanticDepGraphUpdater, this.closureCompilerEnabled),
// TODO(alxhub): understand why the cast here is necessary (something to do with `null`
// not being assignable to `unknown` when wrapped in `Readonly`).
// clang-format off
new DirectiveDecoratorHandler(
reflector, evaluator, metaRegistry, scopeRegistry, metaReader,
defaultImportTracker, injectableRegistry, isCore, this.closureCompilerEnabled,
compileUndecoratedClassesWithAngularFeatures,
) as Readonly<DecoratorHandler<unknown, unknown, unknown>>,
defaultImportTracker, injectableRegistry, isCore, semanticDepGraphUpdater,
this.closureCompilerEnabled, compileUndecoratedClassesWithAngularFeatures,
) as Readonly<DecoratorHandler<unknown, unknown, SemanticSymbol | null,unknown>>,
// 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)
@@ -1033,7 +966,8 @@ export class NgCompiler {
const traitCompiler = new TraitCompiler(
handlers, reflector, this.perfRecorder, this.incrementalDriver,
this.options.compileNonExportedClasses !== false, compilationMode, dtsTransforms);
this.options.compileNonExportedClasses !== false, compilationMode, dtsTransforms,
semanticDepGraphUpdater);
const templateTypeChecker = new TemplateTypeCheckerImpl(
this.tsProgram, this.typeCheckingProgramStrategy, traitCompiler,
@@ -11,6 +11,7 @@ ts_library(
":api",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/incremental/semantic_graph",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/reflection",
@@ -27,6 +28,7 @@ ts_library(
srcs = ["api.ts"],
deps = [
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/reflection",
"@npm//typescript",
],
)
File diff suppressed because it is too large Load Diff
@@ -49,22 +49,6 @@ export interface DependencyTracker<T extends {fileName: string} = ts.SourceFile>
*/
addResourceDependency(from: T, on: AbsoluteFsPath): void;
/**
* Record that the file `from` depends on the file `on` as well as `on`'s direct dependencies.
*
* This operation is reified immediately, so if future dependencies are added to `on` they will
* not automatically be added to `from`.
*/
addTransitiveDependency(from: T, on: T): void;
/**
* Record that the file `from` depends on the resource dependencies of `resourcesOf`.
*
* This operation is reified immediately, so if future resource dependencies are added to
* `resourcesOf` they will not automatically be added to `from`.
*/
addTransitiveResources(from: T, resourcesOf: T): void;
/**
* Record that the given file contains unresolvable dependencies.
*
@@ -0,0 +1,16 @@
load("//tools:defaults.bzl", "ts_library")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "semantic_graph",
srcs = ["index.ts"] + glob([
"src/**/*.ts",
]),
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/reflection",
"@npm//typescript",
],
)
@@ -0,0 +1,12 @@
/**
* @license
* Copyright Google LLC 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
*/
export {SemanticReference, SemanticSymbol} from './src/api';
export {SemanticDepGraph, SemanticDepGraphUpdater} from './src/graph';
export {areTypeParametersEqual, extractSemanticTypeParameters, SemanticTypeParameter} from './src/type_parameters';
export {isArrayEqual, isReferenceEqual, isSetEqual, isSymbolEqual} from './src/util';
@@ -0,0 +1,127 @@
/**
* @license
* Copyright Google LLC 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 {absoluteFromSourceFile, AbsoluteFsPath} from '../../../file_system';
import {ClassDeclaration} from '../../../reflection';
/**
* Represents a symbol that is recognizable across incremental rebuilds, which enables the captured
* metadata to be compared to the prior compilation. This allows for semantic understanding of
* the changes that have been made in a rebuild, which potentially enables more reuse of work
* from the prior compilation.
*/
export abstract class SemanticSymbol {
/**
* The path of the file that declares this symbol.
*/
public readonly path: AbsoluteFsPath;
/**
* The identifier of this symbol, or null if no identifier could be determined. It should
* uniquely identify the symbol relative to `file`. This is typically just the name of a
* top-level class declaration, as that uniquely identifies the class within the file.
*
* If the identifier is null, then this symbol cannot be recognized across rebuilds. In that
* case, the symbol is always assumed to have semantically changed to guarantee a proper
* rebuild.
*/
public readonly identifier: string|null;
constructor(
/**
* The declaration for this symbol.
*/
public readonly decl: ClassDeclaration,
) {
this.path = absoluteFromSourceFile(decl.getSourceFile());
this.identifier = getSymbolIdentifier(decl);
}
/**
* Allows the symbol to be compared to the equivalent symbol in the previous compilation. The
* return value indicates whether the symbol has been changed in a way such that its public API
* is affected.
*
* This method determines whether a change to _this_ symbol require the symbols that
* use to this symbol to be re-emitted.
*
* Note: `previousSymbol` is obtained from the most recently succeeded compilation. Symbols of
* failed compilations are never provided.
*
* @param previousSymbol The symbol from a prior compilation.
*/
abstract isPublicApiAffected(previousSymbol: SemanticSymbol): boolean;
/**
* Allows the symbol to determine whether its emit is affected. The equivalent symbol from a prior
* build is given, in addition to the set of symbols of which the public API has changed.
*
* This method determines whether a change to _other_ symbols, i.e. those present in
* `publicApiAffected`, should cause _this_ symbol to be re-emitted.
*
* @param previousSymbol The equivalent symbol from a prior compilation. Note that it may be a
* different type of symbol, if e.g. a Component was changed into a Directive with the same name.
* @param publicApiAffected The set of symbols of which the public API has changed.
*/
isEmitAffected?(previousSymbol: SemanticSymbol, publicApiAffected: Set<SemanticSymbol>): boolean;
/**
* Similar to `isPublicApiAffected`, but here equivalent symbol from a prior compilation needs
* to be compared to see if the type-check block of components that use this symbol is affected.
*
* This method determines whether a change to _this_ symbol require the symbols that
* use to this symbol to have their type-check block regenerated.
*
* Note: `previousSymbol` is obtained from the most recently succeeded compilation. Symbols of
* failed compilations are never provided.
*
* @param previousSymbol The symbol from a prior compilation.
*/
abstract isTypeCheckApiAffected(previousSymbol: SemanticSymbol): boolean;
/**
* Similar to `isEmitAffected`, but focused on the type-check block of this symbol. This method
* determines whether a change to _other_ symbols, i.e. those present in `typeCheckApiAffected`,
* should cause _this_ symbol's type-check block to be regenerated.
*
* @param previousSymbol The equivalent symbol from a prior compilation. Note that it may be a
* different type of symbol, if e.g. a Component was changed into a Directive with the same name.
* @param typeCheckApiAffected The set of symbols of which the type-check API has changed.
*/
isTypeCheckBlockAffected?
(previousSymbol: SemanticSymbol, typeCheckApiAffected: Set<SemanticSymbol>): boolean;
}
/**
* Represents a reference to a semantic symbol that has been emitted into a source file. The
* reference may refer to the symbol using a different name than the semantic symbol's declared
* name, e.g. in case a re-export under a different name was chosen by a reference emitter.
* Consequently, to know that an emitted reference is still valid not only requires that the
* semantic symbol is still valid, but also that the path by which the symbol is imported has not
* changed.
*/
export interface SemanticReference {
symbol: SemanticSymbol;
/**
* The path by which the symbol has been referenced.
*/
importPath: string|null;
}
function getSymbolIdentifier(decl: ClassDeclaration): string|null {
if (!ts.isSourceFile(decl.parent)) {
return null;
}
// If this is a top-level class declaration, the class name is used as unique identifier.
// Other scenarios are currently not supported and causes the symbol not to be identified
// across rebuilds, unless the declaration node has not changed.
return decl.name.text;
}
@@ -0,0 +1,281 @@
/**
* @license
* Copyright Google LLC 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 {Expression, ExternalExpr} from '@angular/compiler';
import {AbsoluteFsPath} from '../../../file_system';
import {ClassDeclaration} from '../../../reflection';
import {SemanticReference, SemanticSymbol} from './api';
export interface SemanticDependencyResult {
/**
* The files that need to be re-emitted.
*/
needsEmit: Set<AbsoluteFsPath>;
/**
* The files for which the type-check block should be regenerated.
*/
needsTypeCheckEmit: Set<AbsoluteFsPath>;
/**
* The newly built graph that represents the current compilation.
*/
newGraph: SemanticDepGraph;
}
/**
* Represents a declaration for which no semantic symbol has been registered. For example,
* declarations from external dependencies have not been explicitly registered and are represented
* by this symbol. This allows the unresolved symbol to still be compared to a symbol from a prior
* compilation.
*/
class OpaqueSymbol extends SemanticSymbol {
isPublicApiAffected(): false {
return false;
}
isTypeCheckApiAffected(): false {
return false;
}
}
/**
* The semantic dependency graph of a single compilation.
*/
export class SemanticDepGraph {
readonly files = new Map<AbsoluteFsPath, Map<string, SemanticSymbol>>();
readonly symbolByDecl = new Map<ClassDeclaration, SemanticSymbol>();
/**
* Registers a symbol in the graph. The symbol is given a unique identifier if possible, such that
* its equivalent symbol can be obtained from a prior graph even if its declaration node has
* changed across rebuilds. Symbols without an identifier are only able to find themselves in a
* prior graph if their declaration node is identical.
*/
registerSymbol(symbol: SemanticSymbol): void {
this.symbolByDecl.set(symbol.decl, symbol);
if (symbol.identifier !== null) {
// If the symbol has a unique identifier, record it in the file that declares it. This enables
// the symbol to be requested by its unique name.
if (!this.files.has(symbol.path)) {
this.files.set(symbol.path, new Map<string, SemanticSymbol>());
}
this.files.get(symbol.path)!.set(symbol.identifier, symbol);
}
}
/**
* Attempts to resolve a symbol in this graph that represents the given symbol from another graph.
* If no matching symbol could be found, null is returned.
*
* @param symbol The symbol from another graph for which its equivalent in this graph should be
* found.
*/
getEquivalentSymbol(symbol: SemanticSymbol): SemanticSymbol|null {
// First lookup the symbol by its declaration. It is typical for the declaration to not have
// changed across rebuilds, so this is likely to find the symbol. Using the declaration also
// allows to diff symbols for which no unique identifier could be determined.
let previousSymbol = this.getSymbolByDecl(symbol.decl);
if (previousSymbol === null && symbol.identifier !== null) {
// The declaration could not be resolved to a symbol in a prior compilation, which may
// happen because the file containing the declaration has changed. In that case we want to
// lookup the symbol based on its unique identifier, as that allows us to still compare the
// changed declaration to the prior compilation.
previousSymbol = this.getSymbolByName(symbol.path, symbol.identifier);
}
return previousSymbol;
}
/**
* Attempts to find the symbol by its identifier.
*/
private getSymbolByName(path: AbsoluteFsPath, identifier: string): SemanticSymbol|null {
if (!this.files.has(path)) {
return null;
}
const file = this.files.get(path)!;
if (!file.has(identifier)) {
return null;
}
return file.get(identifier)!;
}
/**
* Attempts to resolve the declaration to its semantic symbol.
*/
getSymbolByDecl(decl: ClassDeclaration): SemanticSymbol|null {
if (!this.symbolByDecl.has(decl)) {
return null;
}
return this.symbolByDecl.get(decl)!;
}
}
/**
* Implements the logic to go from a previous dependency graph to a new one, along with information
* on which files have been affected.
*/
export class SemanticDepGraphUpdater {
private readonly newGraph = new SemanticDepGraph();
/**
* Contains opaque symbols that were created for declarations for which there was no symbol
* registered, which happens for e.g. external declarations.
*/
private readonly opaqueSymbols = new Map<ClassDeclaration, OpaqueSymbol>();
constructor(
/**
* The semantic dependency graph of the most recently succeeded compilation, or null if this
* is the initial build.
*/
private priorGraph: SemanticDepGraph|null) {}
/**
* Registers the symbol in the new graph that is being created.
*/
registerSymbol(symbol: SemanticSymbol): void {
this.newGraph.registerSymbol(symbol);
}
/**
* Takes all facts that have been gathered to create a new semantic dependency graph. In this
* process, the semantic impact of the changes is determined which results in a set of files that
* need to be emitted and/or type-checked.
*/
finalize(): SemanticDependencyResult {
if (this.priorGraph === null) {
// If no prior dependency graph is available then this was the initial build, in which case
// we don't need to determine the semantic impact as everything is already considered
// logically changed.
return {
needsEmit: new Set<AbsoluteFsPath>(),
needsTypeCheckEmit: new Set<AbsoluteFsPath>(),
newGraph: this.newGraph,
};
}
const needsEmit = this.determineInvalidatedFiles(this.priorGraph);
const needsTypeCheckEmit = this.determineInvalidatedTypeCheckFiles(this.priorGraph);
return {
needsEmit,
needsTypeCheckEmit,
newGraph: this.newGraph,
};
}
private determineInvalidatedFiles(priorGraph: SemanticDepGraph): Set<AbsoluteFsPath> {
const isPublicApiAffected = new Set<SemanticSymbol>();
// The first phase is to collect all symbols which have their public API affected. Any symbols
// that cannot be matched up with a symbol from the prior graph are considered affected.
for (const symbol of this.newGraph.symbolByDecl.values()) {
const previousSymbol = priorGraph.getEquivalentSymbol(symbol);
if (previousSymbol === null || symbol.isPublicApiAffected(previousSymbol)) {
isPublicApiAffected.add(symbol);
}
}
// The second phase is to find all symbols for which the emit result is affected, either because
// their used declarations have changed or any of those used declarations has had its public API
// affected as determined in the first phase.
const needsEmit = new Set<AbsoluteFsPath>();
for (const symbol of this.newGraph.symbolByDecl.values()) {
if (symbol.isEmitAffected === undefined) {
continue;
}
const previousSymbol = priorGraph.getEquivalentSymbol(symbol);
if (previousSymbol === null || symbol.isEmitAffected(previousSymbol, isPublicApiAffected)) {
needsEmit.add(symbol.path);
}
}
return needsEmit;
}
private determineInvalidatedTypeCheckFiles(priorGraph: SemanticDepGraph): Set<AbsoluteFsPath> {
const isTypeCheckApiAffected = new Set<SemanticSymbol>();
// The first phase is to collect all symbols which have their public API affected. Any symbols
// that cannot be matched up with a symbol from the prior graph are considered affected.
for (const symbol of this.newGraph.symbolByDecl.values()) {
const previousSymbol = priorGraph.getEquivalentSymbol(symbol);
if (previousSymbol === null || symbol.isTypeCheckApiAffected(previousSymbol)) {
isTypeCheckApiAffected.add(symbol);
}
}
// The second phase is to find all symbols for which the emit result is affected, either because
// their used declarations have changed or any of those used declarations has had its public API
// affected as determined in the first phase.
const needsTypeCheckEmit = new Set<AbsoluteFsPath>();
for (const symbol of this.newGraph.symbolByDecl.values()) {
if (symbol.isTypeCheckBlockAffected === undefined) {
continue;
}
const previousSymbol = priorGraph.getEquivalentSymbol(symbol);
if (previousSymbol === null ||
symbol.isTypeCheckBlockAffected(previousSymbol, isTypeCheckApiAffected)) {
needsTypeCheckEmit.add(symbol.path);
}
}
return needsTypeCheckEmit;
}
/**
* Creates a `SemanticReference` for the reference to `decl` using the expression `expr`. See
* the documentation of `SemanticReference` for details.
*/
getSemanticReference(decl: ClassDeclaration, expr: Expression): SemanticReference {
return {
symbol: this.getSymbol(decl),
importPath: getImportPath(expr),
};
}
/**
* Gets the `SemanticSymbol` that was registered for `decl` during the current compilation, or
* returns an opaque symbol that represents `decl`.
*/
getSymbol(decl: ClassDeclaration): SemanticSymbol {
const symbol = this.newGraph.getSymbolByDecl(decl);
if (symbol === null) {
// No symbol has been recorded for the provided declaration, which would be the case if the
// declaration is external. Return an opaque symbol in that case, to allow the external
// declaration to be compared to a prior compilation.
return this.getOpaqueSymbol(decl);
}
return symbol;
}
/**
* Gets or creates an `OpaqueSymbol` for the provided class declaration.
*/
private getOpaqueSymbol(decl: ClassDeclaration): OpaqueSymbol {
if (this.opaqueSymbols.has(decl)) {
return this.opaqueSymbols.get(decl)!;
}
const symbol = new OpaqueSymbol(decl);
this.opaqueSymbols.set(decl, symbol);
return symbol;
}
}
function getImportPath(expr: Expression): string|null {
if (expr instanceof ExternalExpr) {
return `${expr.value.moduleName}\$${expr.value.name}`;
} else {
return null;
}
}
@@ -0,0 +1,70 @@
/**
* @license
* Copyright Google LLC 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 {ClassDeclaration} from '../../../reflection';
import {isArrayEqual} from './util';
/**
* Describes a generic type parameter of a semantic symbol. A class declaration with type parameters
* needs special consideration in certain contexts. For example, template type-check blocks may
* contain type constructors of used directives which include the type parameters of the directive.
* As a consequence, if a change is made that affects the type parameters of said directive, any
* template type-check blocks that use the directive need to be regenerated.
*
* This type represents a single generic type parameter. It currently only tracks whether the
* type parameter has a constraint, i.e. has an `extends` clause. When a constraint is present, we
* currently assume that the type parameter is affected in each incremental rebuild; proving that
* a type parameter with constraint is not affected is non-trivial as it requires full semantic
* understanding of the type constraint.
*/
export interface SemanticTypeParameter {
/**
* Whether a type constraint, i.e. an `extends` clause is present on the type parameter.
*/
hasGenericTypeBound: boolean;
}
/**
* Converts the type parameters of the given class into their semantic representation. If the class
* does not have any type parameters, then `null` is returned.
*/
export function extractSemanticTypeParameters(node: ClassDeclaration): SemanticTypeParameter[]|
null {
if (!ts.isClassDeclaration(node) || node.typeParameters === undefined) {
return null;
}
return node.typeParameters.map(
typeParam => ({hasGenericTypeBound: typeParam.constraint !== undefined}));
}
/**
* Compares the list of type parameters to determine if they can be considered equal.
*/
export function areTypeParametersEqual(
current: SemanticTypeParameter[]|null, previous: SemanticTypeParameter[]|null): boolean {
// First compare all type parameters one-to-one; any differences mean that the list of type
// parameters has changed.
if (!isArrayEqual(current, previous, isTypeParameterEqual)) {
return false;
}
// If there is a current list of type parameters and if any of them has a generic type constraint,
// then the meaning of that type parameter may have changed without us being aware; as such we
// have to assume that the type parameters have in fact changed.
if (current !== null && current.some(typeParam => typeParam.hasGenericTypeBound)) {
return false;
}
return true;
}
function isTypeParameterEqual(a: SemanticTypeParameter, b: SemanticTypeParameter): boolean {
return a.hasGenericTypeBound === b.hasGenericTypeBound;
}
@@ -0,0 +1,93 @@
/**
* @license
* Copyright Google LLC 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 {SemanticReference, SemanticSymbol} from './api';
/**
* Determines whether the provided symbols represent the same declaration.
*/
export function isSymbolEqual(a: SemanticSymbol, b: SemanticSymbol): boolean {
if (a.decl === b.decl) {
// If the declaration is identical then it must represent the same symbol.
return true;
}
if (a.identifier === null || b.identifier === null) {
// Unidentifiable symbols are assumed to be different.
return false;
}
return a.path === b.path && a.identifier === b.identifier;
}
/**
* Determines whether the provided references to a semantic symbol are still equal, i.e. represent
* the same symbol and are imported by the same path.
*/
export function isReferenceEqual(a: SemanticReference, b: SemanticReference): boolean {
if (!isSymbolEqual(a.symbol, b.symbol)) {
// If the reference's target symbols are different, the reference itself is different.
return false;
}
// The reference still corresponds with the same symbol, now check that the path by which it is
// imported has not changed.
return a.importPath === b.importPath;
}
export function referenceEquality<T>(a: T, b: T): boolean {
return a === b;
}
/**
* Determines if the provided arrays are equal to each other, using the provided equality tester
* that is called for all entries in the array.
*/
export function isArrayEqual<T>(
a: readonly T[]|null, b: readonly T[]|null,
equalityTester: (a: T, b: T) => boolean = referenceEquality): boolean {
if (a === null || b === null) {
return a === b;
}
if (a.length !== b.length) {
return false;
}
return !a.some((item, index) => !equalityTester(item, b[index]));
}
/**
* Determines if the provided sets are equal to each other, using the provided equality tester.
* Sets that only differ in ordering are considered equal.
*/
export function isSetEqual<T>(
a: ReadonlySet<T>|null, b: ReadonlySet<T>|null,
equalityTester: (a: T, b: T) => boolean = referenceEquality): boolean {
if (a === null || b === null) {
return a === b;
}
if (a.size !== b.size) {
return false;
}
for (const itemA of a) {
let found = false;
for (const itemB of b) {
if (equalityTester(itemA, itemB)) {
found = true;
break;
}
}
if (!found) {
return false;
}
}
return true;
}
@@ -35,24 +35,6 @@ export class FileDependencyGraph<T extends {fileName: string} = ts.SourceFile> i
this.nodeFor(from).usesResources.add(resource);
}
addTransitiveDependency(from: T, on: T): void {
const nodeFrom = this.nodeFor(from);
nodeFrom.dependsOn.add(on.fileName);
const nodeOn = this.nodeFor(on);
for (const dep of nodeOn.dependsOn) {
nodeFrom.dependsOn.add(dep);
}
}
addTransitiveResources(from: T, resourcesOf: T): void {
const nodeFrom = this.nodeFor(from);
const nodeOn = this.nodeFor(resourcesOf);
for (const dep of nodeOn.usesResources) {
nodeFrom.usesResources.add(dep);
}
}
recordDependencyAnalysisFailure(file: T): void {
this.nodeFor(file).failedAnalysis = true;
}
@@ -63,10 +45,6 @@ export class FileDependencyGraph<T extends {fileName: string} = ts.SourceFile> i
return node ? [...node.usesResources] : [];
}
isStale(sf: T, changedTsPaths: Set<string>, changedResources: Set<AbsoluteFsPath>): boolean {
return isLogicallyChanged(sf, this.nodeFor(sf), changedTsPaths, EMPTY_SET, changedResources);
}
/**
* Update the current dependency graph from a previous one, incorporating a set of physical
* changes.
@@ -160,5 +138,3 @@ interface FileNode {
usesResources: Set<AbsoluteFsPath>;
failedAnalysis: boolean;
}
const EMPTY_SET: ReadonlySet<any> = new Set<any>();

Some files were not shown because too many files have changed in this diff Show More