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 2020-11-20 21:18:46 +01:00 committed by Andrew Kushnir
parent 8c062493a0
commit fed6a7ce7d
43 changed files with 5253 additions and 378 deletions

View File

@ -19,6 +19,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/incremental:api", "//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/logging",
"//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/partial_evaluator", "//packages/compiler-cli/src/ngtsc/partial_evaluator",

View File

@ -14,6 +14,7 @@ import {CycleAnalyzer, CycleHandlingStrategy, ImportGraph} from '../../../src/ng
import {isFatalDiagnosticError} from '../../../src/ngtsc/diagnostics'; import {isFatalDiagnosticError} from '../../../src/ngtsc/diagnostics';
import {absoluteFromSourceFile, LogicalFileSystem, ReadonlyFileSystem} from '../../../src/ngtsc/file_system'; 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 {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 {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, ResourceRegistry} from '../../../src/ngtsc/metadata';
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator'; import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver, TypeCheckScopeRegistry} from '../../../src/ngtsc/scope'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver, TypeCheckScopeRegistry} from '../../../src/ngtsc/scope';
@ -92,7 +93,7 @@ export class DecorationAnalyzer {
cycleAnalyzer = new CycleAnalyzer(this.importGraph); cycleAnalyzer = new CycleAnalyzer(this.importGraph);
injectableRegistry = new InjectableClassRegistry(this.reflectionHost); injectableRegistry = new InjectableClassRegistry(this.reflectionHost);
typeCheckScopeRegistry = new TypeCheckScopeRegistry(this.scopeRegistry, this.fullMetaReader); typeCheckScopeRegistry = new TypeCheckScopeRegistry(this.scopeRegistry, this.fullMetaReader);
handlers: DecoratorHandler<unknown, unknown, unknown>[] = [ handlers: DecoratorHandler<unknown, unknown, SemanticSymbol|null, unknown>[] = [
new ComponentDecoratorHandler( new ComponentDecoratorHandler(
this.reflectionHost, this.evaluator, this.fullRegistry, this.fullMetaReader, this.reflectionHost, this.evaluator, this.fullRegistry, this.fullMetaReader,
this.scopeRegistry, this.scopeRegistry, this.typeCheckScopeRegistry, new ResourceRegistry(), this.scopeRegistry, this.scopeRegistry, this.typeCheckScopeRegistry, new ResourceRegistry(),
@ -103,19 +104,21 @@ export class DecorationAnalyzer {
/* i18nNormalizeLineEndingsInICUs */ false, this.moduleResolver, this.cycleAnalyzer, /* i18nNormalizeLineEndingsInICUs */ false, this.moduleResolver, this.cycleAnalyzer,
CycleHandlingStrategy.UseRemoteScoping, this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER, CycleHandlingStrategy.UseRemoteScoping, this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER,
NOOP_DEPENDENCY_TRACKER, this.injectableRegistry, NOOP_DEPENDENCY_TRACKER, this.injectableRegistry,
!!this.compilerOptions.annotateForClosureCompiler), /* semanticDepGraphUpdater */ null, !!this.compilerOptions.annotateForClosureCompiler),
// See the note in ngtsc about why this cast is needed. // See the note in ngtsc about why this cast is needed.
// clang-format off // clang-format off
new DirectiveDecoratorHandler( new DirectiveDecoratorHandler(
this.reflectionHost, this.evaluator, this.fullRegistry, this.scopeRegistry, this.reflectionHost, this.evaluator, this.fullRegistry, this.scopeRegistry,
this.fullMetaReader, NOOP_DEFAULT_IMPORT_RECORDER, this.injectableRegistry, this.isCore, this.fullMetaReader, NOOP_DEFAULT_IMPORT_RECORDER, this.injectableRegistry, this.isCore,
/* semanticDepGraphUpdater */ null,
!!this.compilerOptions.annotateForClosureCompiler, !!this.compilerOptions.annotateForClosureCompiler,
// In ngcc we want to compile undecorated classes with Angular features. As of // 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 // 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 // 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. // have not migrated to explicit decorators. See: https://hackmd.io/@alx/ryfYYuvzH.
/* compileUndecoratedClassesWithAngularFeatures */ true /* compileUndecoratedClassesWithAngularFeatures */ true
) as DecoratorHandler<unknown, unknown, unknown>, ) as DecoratorHandler<unknown, unknown, SemanticSymbol|null,unknown>,
// clang-format on // clang-format on
// Pipe handler must be before injectable handler in list so pipe factories are printed // Pipe handler must be before injectable handler in list so pipe factories are printed
// before injectable factories (so injectable factories can delegate to them) // before injectable factories (so injectable factories can delegate to them)

View File

@ -8,6 +8,7 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {IncrementalBuild} from '../../../src/ngtsc/incremental/api'; import {IncrementalBuild} from '../../../src/ngtsc/incremental/api';
import {SemanticSymbol} from '../../../src/ngtsc/incremental/semantic_graph';
import {NOOP_PERF_RECORDER} from '../../../src/ngtsc/perf'; import {NOOP_PERF_RECORDER} from '../../../src/ngtsc/perf';
import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection'; import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection';
import {CompilationMode, DecoratorHandler, DtsTransformRegistry, HandlerFlags, Trait, TraitCompiler} from '../../../src/ngtsc/transform'; import {CompilationMode, DecoratorHandler, DtsTransformRegistry, HandlerFlags, Trait, TraitCompiler} from '../../../src/ngtsc/transform';
@ -22,11 +23,12 @@ import {isDefined} from '../utils';
*/ */
export class NgccTraitCompiler extends TraitCompiler { export class NgccTraitCompiler extends TraitCompiler {
constructor( constructor(
handlers: DecoratorHandler<unknown, unknown, unknown>[], handlers: DecoratorHandler<unknown, unknown, SemanticSymbol|null, unknown>[],
private ngccReflector: NgccReflectionHost) { private ngccReflector: NgccReflectionHost) {
super( super(
handlers, ngccReflector, NOOP_PERF_RECORDER, new NoIncrementalBuild(), 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[] { 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. * @param flags optional bitwise flag to influence the compilation of the decorator.
*/ */
injectSyntheticDecorator(clazz: ClassDeclaration, decorator: Decorator, flags?: HandlerFlags): injectSyntheticDecorator(clazz: ClassDeclaration, decorator: Decorator, flags?: HandlerFlags):
Trait<unknown, unknown, unknown>[] { Trait<unknown, unknown, SemanticSymbol|null, unknown>[] {
const migratedTraits = this.detectTraits(clazz, [decorator]); const migratedTraits = this.detectTraits(clazz, [decorator]);
if (migratedTraits === null) { if (migratedTraits === null) {
return []; return [];

View File

@ -16,8 +16,6 @@ export function isWithinPackage(packagePath: AbsoluteFsPath, filePath: AbsoluteF
class NoopDependencyTracker implements DependencyTracker { class NoopDependencyTracker implements DependencyTracker {
addDependency(): void {} addDependency(): void {}
addResourceDependency(): void {} addResourceDependency(): void {}
addTransitiveDependency(): void {}
addTransitiveResources(): void {}
recordDependencyAnalysisFailure(): void {} recordDependencyAnalysisFailure(): void {}
} }

View File

@ -19,6 +19,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing", "//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/imports", "//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/logging/testing",
"//packages/compiler-cli/src/ngtsc/partial_evaluator", "//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/reflection",

View File

@ -10,6 +10,7 @@ import * as ts from 'typescript';
import {FatalDiagnosticError, makeDiagnostic} from '../../../src/ngtsc/diagnostics'; import {FatalDiagnosticError, makeDiagnostic} from '../../../src/ngtsc/diagnostics';
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system'; import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/testing'; 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 {MockLogger} from '../../../src/ngtsc/logging/testing';
import {ClassDeclaration, DeclarationNode, Decorator} from '../../../src/ngtsc/reflection'; import {ClassDeclaration, DeclarationNode, Decorator} from '../../../src/ngtsc/reflection';
import {loadFakeCore, loadTestFiles} from '../../../src/ngtsc/testing'; 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 {Migration, MigrationHost} from '../../src/migrations/migration';
import {getRootFiles, makeTestEntryPointBundle} from '../helpers/utils'; import {getRootFiles, makeTestEntryPointBundle} from '../helpers/utils';
type DecoratorHandlerWithResolve = DecoratorHandler<unknown, unknown, unknown>&{ type DecoratorHandlerWithResolve =
resolve: NonNullable<DecoratorHandler<unknown, unknown, unknown>['resolve']>; DecoratorHandler<unknown, unknown, SemanticSymbol|null, unknown>&{
resolve: NonNullable<DecoratorHandler<unknown, unknown, SemanticSymbol|null, unknown>['resolve']>;
}; };
runInEachFileSystem(() => { runInEachFileSystem(() => {
@ -46,6 +48,7 @@ runInEachFileSystem(() => {
const handler = jasmine.createSpyObj<DecoratorHandlerWithResolve>('TestDecoratorHandler', [ const handler = jasmine.createSpyObj<DecoratorHandlerWithResolve>('TestDecoratorHandler', [
'detect', 'detect',
'analyze', 'analyze',
'symbol',
'register', 'register',
'resolve', 'resolve',
'compileFull', 'compileFull',
@ -442,7 +445,7 @@ runInEachFileSystem(() => {
describe('declaration files', () => { describe('declaration files', () => {
it('should not run decorator handlers against 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'; name = 'FakeDecoratorHandler';
precedence = HandlerPrecedence.PRIMARY; precedence = HandlerPrecedence.PRIMARY;
@ -452,6 +455,9 @@ runInEachFileSystem(() => {
analyze(): AnalysisOutput<unknown> { analyze(): AnalysisOutput<unknown> {
throw new Error('analyze should not have been called'); throw new Error('analyze should not have been called');
} }
symbol(): null {
throw new Error('symbol should not have been called');
}
compileFull(): CompileResult { compileFull(): CompileResult {
throw new Error('compile should not have been called'); throw new Error('compile should not have been called');
} }

View File

@ -11,6 +11,7 @@ import * as ts from 'typescript';
import {makeDiagnostic} from '../../../src/ngtsc/diagnostics'; import {makeDiagnostic} from '../../../src/ngtsc/diagnostics';
import {absoluteFrom} from '../../../src/ngtsc/file_system'; import {absoluteFrom} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {SemanticSymbol} from '../../../src/ngtsc/incremental/semantic_graph';
import {MockLogger} from '../../../src/ngtsc/logging/testing'; import {MockLogger} from '../../../src/ngtsc/logging/testing';
import {ClassDeclaration, Decorator, isNamedClassDeclaration} from '../../../src/ngtsc/reflection'; import {ClassDeclaration, Decorator, isNamedClassDeclaration} from '../../../src/ngtsc/reflection';
import {getDeclaration, loadTestFiles} from '../../../src/ngtsc/testing'; import {getDeclaration, loadTestFiles} from '../../../src/ngtsc/testing';
@ -44,7 +45,8 @@ runInEachFileSystem(() => {
}); });
function createMigrationHost({entryPoint, handlers}: { 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 reflectionHost = new Esm2015ReflectionHost(new MockLogger(), false, entryPoint.src);
const compiler = new NgccTraitCompiler(handlers, reflectionHost); 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; readonly name = DetectDecoratorHandler.name;
constructor(private decorator: string, readonly precedence: HandlerPrecedence) {} constructor(private decorator: string, readonly precedence: HandlerPrecedence) {}
@ -210,12 +212,16 @@ class DetectDecoratorHandler implements DecoratorHandler<unknown, unknown, unkno
return {}; return {};
} }
symbol(node: ClassDeclaration, analysis: Readonly<unknown>): null {
return null;
}
compileFull(node: ClassDeclaration): CompileResult|CompileResult[] { compileFull(node: ClassDeclaration): CompileResult|CompileResult[] {
return []; return [];
} }
} }
class DiagnosticProducingHandler implements DecoratorHandler<unknown, unknown, unknown> { class DiagnosticProducingHandler implements DecoratorHandler<unknown, unknown, null, unknown> {
readonly name = DiagnosticProducingHandler.name; readonly name = DiagnosticProducingHandler.name;
readonly precedence = HandlerPrecedence.PRIMARY; readonly precedence = HandlerPrecedence.PRIMARY;
@ -228,6 +234,10 @@ class DiagnosticProducingHandler implements DecoratorHandler<unknown, unknown, u
return {diagnostics: [makeDiagnostic(9999, node, 'test diagnostic')]}; return {diagnostics: [makeDiagnostic(9999, node, 'test diagnostic')]};
} }
symbol(node: ClassDeclaration, analysis: Readonly<unknown>): null {
return null;
}
compileFull(node: ClassDeclaration): CompileResult|CompileResult[] { compileFull(node: ClassDeclaration): CompileResult|CompileResult[] {
return []; return [];
} }

View File

@ -9,6 +9,7 @@
import {ErrorCode, makeDiagnostic, ngErrorCode} from '../../../src/ngtsc/diagnostics'; import {ErrorCode, makeDiagnostic, ngErrorCode} from '../../../src/ngtsc/diagnostics';
import {absoluteFrom} from '../../../src/ngtsc/file_system'; import {absoluteFrom} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {SemanticSymbol} from '../../../src/ngtsc/incremental/semantic_graph';
import {MockLogger} from '../../../src/ngtsc/logging/testing'; import {MockLogger} from '../../../src/ngtsc/logging/testing';
import {ClassDeclaration, Decorator, isNamedClassDeclaration} from '../../../src/ngtsc/reflection'; import {ClassDeclaration, Decorator, isNamedClassDeclaration} from '../../../src/ngtsc/reflection';
import {getDeclaration, loadTestFiles} from '../../../src/ngtsc/testing'; import {getDeclaration, loadTestFiles} from '../../../src/ngtsc/testing';
@ -39,7 +40,8 @@ runInEachFileSystem(() => {
}); });
function createCompiler({entryPoint, handlers}: { 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); const reflectionHost = new Esm2015ReflectionHost(new MockLogger(), false, entryPoint.src);
return new NgccTraitCompiler(handlers, reflectionHost); 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[]) {} constructor(readonly name: string, protected log: string[]) {}
precedence = HandlerPrecedence.PRIMARY; precedence = HandlerPrecedence.PRIMARY;
@ -310,6 +312,10 @@ class TestHandler implements DecoratorHandler<unknown, unknown, unknown> {
return {}; return {};
} }
symbol(node: ClassDeclaration, analysis: Readonly<unknown>): null {
return null;
}
compileFull(node: ClassDeclaration): CompileResult|CompileResult[] { compileFull(node: ClassDeclaration): CompileResult|CompileResult[] {
this.log.push(this.name + ':compile:' + node.name.text); this.log.push(this.name + ':compile:' + node.name.text);
return []; return [];

View File

@ -7,6 +7,8 @@
*/ */
import {Trait, TraitState} from '@angular/compiler-cli/src/ngtsc/transform'; import {Trait, TraitState} from '@angular/compiler-cli/src/ngtsc/transform';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {SemanticSymbol} from '../../../src/ngtsc/incremental/semantic_graph';
import {CtorParameter, TypeValueReferenceKind} from '../../../src/ngtsc/reflection'; 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) { if (trait.state === TraitState.Analyzed) {
return trait.analysisDiagnostics; return trait.analysisDiagnostics;
} else if (trait.state === TraitState.Resolved) { } else if (trait.state === TraitState.Resolved) {

View File

@ -14,6 +14,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/incremental:api", "//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/indexer",
"//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/partial_evaluator", "//packages/compiler-cli/src/ngtsc/partial_evaluator",

View File

@ -14,6 +14,7 @@ import {ErrorCode, FatalDiagnosticError, makeDiagnostic, makeRelatedInformation}
import {absoluteFrom, relative} from '../../file_system'; import {absoluteFrom, relative} from '../../file_system';
import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from '../../imports'; import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from '../../imports';
import {DependencyTracker} from '../../incremental/api'; import {DependencyTracker} from '../../incremental/api';
import {extractSemanticTypeParameters, isArrayEqual, isReferenceEqual, SemanticDepGraphUpdater, SemanticReference, SemanticSymbol} from '../../incremental/semantic_graph';
import {IndexingContext} from '../../indexer'; import {IndexingContext} from '../../indexer';
import {ClassPropertyMapping, ComponentResources, DirectiveMeta, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry, Resource, ResourceRegistry} from '../../metadata'; import {ClassPropertyMapping, ComponentResources, DirectiveMeta, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry, Resource, ResourceRegistry} from '../../metadata';
import {EnumValue, PartialEvaluator, ResolvedValue} from '../../partial_evaluator'; import {EnumValue, PartialEvaluator, ResolvedValue} from '../../partial_evaluator';
@ -26,9 +27,10 @@ import {SubsetOfKeys} from '../../util/src/typescript';
import {ResourceLoader} from './api'; import {ResourceLoader} from './api';
import {createValueHasWrongTypeError, getDirectiveDiagnostics, getProviderDiagnostics} from './diagnostics'; import {createValueHasWrongTypeError, getDirectiveDiagnostics, getProviderDiagnostics} from './diagnostics';
import {extractDirectiveMetadata, parseFieldArrayValue} from './directive'; import {DirectiveSymbol, extractDirectiveMetadata, parseFieldArrayValue} from './directive';
import {compileNgFactoryDefField} from './factory'; import {compileNgFactoryDefField} from './factory';
import {generateSetClassMetadataCall} from './metadata'; import {generateSetClassMetadataCall} from './metadata';
import {NgModuleSymbol} from './ng_module';
import {findAngularDecorator, isAngularCoreReference, isExpressionForwardReference, readBaseClass, resolveProvidersRequiringFactory, unwrapExpression, wrapFunctionExpressionsInParens} from './util'; import {findAngularDecorator, isAngularCoreReference, isExpressionForwardReference, readBaseClass, resolveProvidersRequiringFactory, unwrapExpression, wrapFunctionExpressionsInParens} from './util';
const EMPTY_MAP = new Map<string, Expression>(); const EMPTY_MAP = new Map<string, Expression>();
@ -111,11 +113,85 @@ export const enum ResourceTypeForDiagnostics {
StylesheetFromDecorator, 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. * `DecoratorHandler` which handles the `@Component` annotation.
*/ */
export class ComponentDecoratorHandler implements export class ComponentDecoratorHandler implements
DecoratorHandler<Decorator, ComponentAnalysisData, ComponentResolutionData> { DecoratorHandler<Decorator, ComponentAnalysisData, ComponentSymbol, ComponentResolutionData> {
constructor( constructor(
private reflector: ReflectionHost, private evaluator: PartialEvaluator, private reflector: ReflectionHost, private evaluator: PartialEvaluator,
private metaRegistry: MetadataRegistry, private metaReader: MetadataReader, private metaRegistry: MetadataRegistry, private metaReader: MetadataReader,
@ -131,6 +207,7 @@ export class ComponentDecoratorHandler implements
private defaultImportRecorder: DefaultImportRecorder, private defaultImportRecorder: DefaultImportRecorder,
private depTracker: DependencyTracker|null, private depTracker: DependencyTracker|null,
private injectableRegistry: InjectableClassRegistry, private injectableRegistry: InjectableClassRegistry,
private semanticDepGraphUpdater: SemanticDepGraphUpdater|null,
private annotateForClosureCompiler: boolean) {} private annotateForClosureCompiler: boolean) {}
private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>(); private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>();
@ -397,6 +474,14 @@ export class ComponentDecoratorHandler implements
return output; 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(node: ClassDeclaration, analysis: ComponentAnalysisData): void {
// Register this component's information with the `MetadataRegistry`. This ensures that // Register this component's information with the `MetadataRegistry`. This ensures that
// the information about the component is available during the compile() phase. // 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); meta.template.sourceMapping, meta.template.file, meta.template.errors);
} }
resolve(node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>): resolve(
ResolveResult<ComponentResolutionData> { 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) { if (analysis.isPoisoned && !this.usePoisonedData) {
return {}; return {};
} }
@ -538,7 +628,7 @@ export class ComponentDecoratorHandler implements
const bound = binder.bind({template: metadata.template.nodes}); const bound = binder.bind({template: metadata.template.nodes});
// The BoundTarget knows which directives and pipes matched the template. // 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 => { const usedDirectives: UsedDirective[] = bound.getUsedDirectives().map(directive => {
return { return {
ref: directive.ref, ref: directive.ref,
@ -550,8 +640,7 @@ export class ComponentDecoratorHandler implements
isComponent: directive.isComponent, isComponent: directive.isComponent,
}; };
}); });
type UsedPipe = {ref: Reference<ClassDeclaration>, pipeName: string, expression: Expression};
type UsedPipe = {ref: Reference, pipeName: string, expression: Expression};
const usedPipes: UsedPipe[] = []; const usedPipes: UsedPipe[] = [];
for (const pipeName of bound.getUsedPipes()) { for (const pipeName of bound.getUsedPipes()) {
if (!pipes.has(pipeName)) { if (!pipes.has(pipeName)) {
@ -564,6 +653,13 @@ export class ComponentDecoratorHandler implements
expression: this.refEmitter.emit(pipe, context), 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 // Scan through the directives/pipes actually used in the template and check whether any
// import which needs to be generated would create a cycle. // 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 // 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. // so that future cyclic import checks consider their production.
for (const {type} of usedDirectives) { 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. // NgModule file will take care of setting the directives for the component.
this.scopeRegistry.setComponentRemoteScope( this.scopeRegistry.setComponentRemoteScope(
node, usedDirectives.map(dir => dir.ref), usedPipes.map(pipe => pipe.ref)); 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 { } else {
// We are not able to handle this cycle so throw an error. // We are not able to handle this cycle so throw an error.
const relatedMessages: ts.DiagnosticRelatedInformation[] = []; const relatedMessages: ts.DiagnosticRelatedInformation[] = [];

View File

@ -11,8 +11,10 @@ import {emitDistinctChangesOnlyDefaultValue} from '@angular/compiler/src/core';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {absoluteFromSourceFile} from '../../file_system';
import {DefaultImportRecorder, Reference} from '../../imports'; 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 {extractDirectiveTypeCheckMeta} from '../../metadata/src/util';
import {DynamicValue, EnumValue, PartialEvaluator} from '../../partial_evaluator'; import {DynamicValue, EnumValue, PartialEvaluator} from '../../partial_evaluator';
import {ClassDeclaration, ClassMember, ClassMemberKind, Decorator, filterToMembersWithDecorator, ReflectionHost, reflectObjectLiteral} from '../../reflection'; import {ClassDeclaration, ClassMember, ClassMemberKind, Decorator, filterToMembersWithDecorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
@ -46,13 +48,138 @@ export interface DirectiveHandlerData {
isStructural: boolean; 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 export class DirectiveDecoratorHandler implements
DecoratorHandler<Decorator|null, DirectiveHandlerData, unknown> { DecoratorHandler<Decorator|null, DirectiveHandlerData, DirectiveSymbol, unknown> {
constructor( constructor(
private reflector: ReflectionHost, private evaluator: PartialEvaluator, private reflector: ReflectionHost, private evaluator: PartialEvaluator,
private metaRegistry: MetadataRegistry, private scopeRegistry: LocalModuleScopeRegistry, private metaRegistry: MetadataRegistry, private scopeRegistry: LocalModuleScopeRegistry,
private metaReader: MetadataReader, private defaultImportRecorder: DefaultImportRecorder, private metaReader: MetadataReader, private defaultImportRecorder: DefaultImportRecorder,
private injectableRegistry: InjectableClassRegistry, private isCore: boolean, private injectableRegistry: InjectableClassRegistry, private isCore: boolean,
private semanticDepGraphUpdater: SemanticDepGraphUpdater|null,
private annotateForClosureCompiler: boolean, private annotateForClosureCompiler: boolean,
private compileUndecoratedClassesWithAngularFeatures: 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(node: ClassDeclaration, analysis: Readonly<DirectiveHandlerData>): void {
// Register this directive's information with the `MetadataRegistry`. This ensures that // Register this directive's information with the `MetadataRegistry`. This ensures that
// the information about the directive is available during the compile() phase. // the information about the directive is available during the compile() phase.
@ -138,9 +273,13 @@ export class DirectiveDecoratorHandler implements
this.injectableRegistry.registerInjectable(node); this.injectableRegistry.registerInjectable(node);
} }
resolve(node: ClassDeclaration, analysis: DirectiveHandlerData): ResolveResult<unknown> { resolve(node: ClassDeclaration, analysis: DirectiveHandlerData, symbol: DirectiveSymbol):
const diagnostics: ts.Diagnostic[] = []; 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 && if (analysis.providersRequiringFactory !== null &&
analysis.meta.providers instanceof WrappedNodeExpr) { analysis.meta.providers instanceof WrappedNodeExpr) {
const providerDiagnostics = getProviderDiagnostics( const providerDiagnostics = getProviderDiagnostics(

View File

@ -30,7 +30,7 @@ export interface InjectableHandlerData {
* Adapts the `compileIvyInjectable` compiler for `@Injectable` decorators to the Ivy compiler. * Adapts the `compileIvyInjectable` compiler for `@Injectable` decorators to the Ivy compiler.
*/ */
export class InjectableDecoratorHandler implements export class InjectableDecoratorHandler implements
DecoratorHandler<Decorator, InjectableHandlerData, unknown> { DecoratorHandler<Decorator, InjectableHandlerData, null, unknown> {
constructor( constructor(
private reflector: ReflectionHost, private defaultImportRecorder: DefaultImportRecorder, private reflector: ReflectionHost, private defaultImportRecorder: DefaultImportRecorder,
private isCore: boolean, private strictCtorDeps: boolean, private isCore: boolean, private strictCtorDeps: boolean,
@ -83,6 +83,10 @@ export class InjectableDecoratorHandler implements
}; };
} }
symbol(): null {
return null;
}
register(node: ClassDeclaration): void { register(node: ClassDeclaration): void {
this.injectableRegistry.registerInjectable(node); this.injectableRegistry.registerInjectable(node);
} }

View File

@ -11,9 +11,10 @@ import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError, makeDiagnostic, makeRelatedInformation} from '../../diagnostics'; import {ErrorCode, FatalDiagnosticError, makeDiagnostic, makeRelatedInformation} from '../../diagnostics';
import {DefaultImportRecorder, Reference, ReferenceEmitter} from '../../imports'; import {DefaultImportRecorder, Reference, ReferenceEmitter} from '../../imports';
import {isArrayEqual, isReferenceEqual, isSymbolEqual, SemanticReference, SemanticSymbol} from '../../incremental/semantic_graph';
import {InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata'; import {InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata';
import {PartialEvaluator, ResolvedValue} from '../../partial_evaluator'; 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 {NgModuleRouteAnalyzer} from '../../routing';
import {LocalModuleScopeRegistry, ScopeData} from '../../scope'; import {LocalModuleScopeRegistry, ScopeData} from '../../scope';
import {FactoryTracker} from '../../shims/api'; import {FactoryTracker} from '../../shims/api';
@ -44,13 +45,84 @@ export interface NgModuleResolution {
injectorImports: Expression[]; 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. * Compiles @NgModule annotations to ngModuleDef fields.
* *
* TODO(alxhub): handle injector side of things as well. * TODO(alxhub): handle injector side of things as well.
*/ */
export class NgModuleDecoratorHandler implements export class NgModuleDecoratorHandler implements
DecoratorHandler<Decorator, NgModuleAnalysis, NgModuleResolution> { DecoratorHandler<Decorator, NgModuleAnalysis, NgModuleSymbol, NgModuleResolution> {
constructor( constructor(
private reflector: ReflectionHost, private evaluator: PartialEvaluator, private reflector: ReflectionHost, private evaluator: PartialEvaluator,
private metaReader: MetadataReader, private metaRegistry: MetadataRegistry, 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(node: ClassDeclaration, analysis: NgModuleAnalysis): void {
// Register this module's information with the LocalModuleScopeRegistry. This ensures that // Register this module's information with the LocalModuleScopeRegistry. This ensures that
// during the compile() phase, the module's metadata is available for selector scope // during the compile() phase, the module's metadata is available for selector scope

View File

@ -11,13 +11,14 @@ import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {DefaultImportRecorder, Reference} from '../../imports'; import {DefaultImportRecorder, Reference} from '../../imports';
import {SemanticSymbol} from '../../incremental/semantic_graph';
import {InjectableClassRegistry, MetadataRegistry} from '../../metadata'; import {InjectableClassRegistry, MetadataRegistry} from '../../metadata';
import {PartialEvaluator} from '../../partial_evaluator'; import {PartialEvaluator} from '../../partial_evaluator';
import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection'; import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
import {LocalModuleScopeRegistry} from '../../scope'; import {LocalModuleScopeRegistry} from '../../scope';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../transform'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../transform';
import {createValueHasWrongTypeError} from './diagnostics';
import {createValueHasWrongTypeError} from './diagnostics';
import {compileNgFactoryDefField} from './factory'; import {compileNgFactoryDefField} from './factory';
import {generateSetClassMetadataCall} from './metadata'; import {generateSetClassMetadataCall} from './metadata';
import {findAngularDecorator, getValidConstructorDependencies, makeDuplicateDeclarationError, unwrapExpression, wrapTypeReference} from './util'; import {findAngularDecorator, getValidConstructorDependencies, makeDuplicateDeclarationError, unwrapExpression, wrapTypeReference} from './util';
@ -27,7 +28,29 @@ export interface PipeHandlerData {
metadataStmt: Statement|null; 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( constructor(
private reflector: ReflectionHost, private evaluator: PartialEvaluator, private reflector: ReflectionHost, private evaluator: PartialEvaluator,
private metaRegistry: MetadataRegistry, private scopeRegistry: LocalModuleScopeRegistry, 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 { register(node: ClassDeclaration, analysis: Readonly<PipeHandlerData>): void {
const ref = new Reference(node); const ref = new Reference(node);
this.metaRegistry.registerPipeMetadata({ref, name: analysis.meta.pipeName}); this.metaRegistry.registerPipeMetadata({ref, name: analysis.meta.pipeName});

View File

@ -17,6 +17,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing", "//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/incremental:api",
"//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/partial_evaluator", "//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/reflection",

View File

@ -78,6 +78,7 @@ function setup(program: ts.Program, options: ts.CompilerOptions, host: ts.Compil
NOOP_DEFAULT_IMPORT_RECORDER, NOOP_DEFAULT_IMPORT_RECORDER,
/* depTracker */ null, /* depTracker */ null,
injectableRegistry, injectableRegistry,
/* semanticDepGraphUpdater */ null,
/* annotateForClosureCompiler */ false, /* annotateForClosureCompiler */ false,
); );
return {reflectionHost, handler}; return {reflectionHost, handler};
@ -247,7 +248,8 @@ runInEachFileSystem(() => {
return fail('Failed to recognize @Component'); return fail('Failed to recognize @Component');
} }
const {analysis} = handler.analyze(TestCmp, detected.metadata); 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 = const compileResult =
handler.compileFull(TestCmp, analysis!, resolution.data!, new ConstantPool()); handler.compileFull(TestCmp, analysis!, resolution.data!, new ConstantPool());

View File

@ -169,6 +169,7 @@ runInEachFileSystem(() => {
const handler = new DirectiveDecoratorHandler( const handler = new DirectiveDecoratorHandler(
reflectionHost, evaluator, scopeRegistry, scopeRegistry, metaReader, reflectionHost, evaluator, scopeRegistry, scopeRegistry, metaReader,
NOOP_DEFAULT_IMPORT_RECORDER, injectableRegistry, /*isCore*/ false, NOOP_DEFAULT_IMPORT_RECORDER, injectableRegistry, /*isCore*/ false,
/*semanticDepGraphUpdater*/ null,
/*annotateForClosureCompiler*/ false, /*annotateForClosureCompiler*/ false,
/*detectUndecoratedClassesWithAngularFeatures*/ false); /*detectUndecoratedClassesWithAngularFeatures*/ false);

View File

@ -19,6 +19,8 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/incremental", "//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/indexer",
"//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/modulewithproviders", "//packages/compiler-cli/src/ngtsc/modulewithproviders",

View File

@ -16,6 +16,7 @@ import {checkForPrivateExports, ReferenceGraph} from '../../entry_point';
import {LogicalFileSystem, resolve} from '../../file_system'; 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 {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 {IncrementalBuildStrategy, IncrementalDriver} from '../../incremental';
import {SemanticSymbol} from '../../incremental/semantic_graph';
import {generateAnalysis, IndexedComponent, IndexingContext} from '../../indexer'; import {generateAnalysis, IndexedComponent, IndexingContext} from '../../indexer';
import {ComponentResources, CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, MetadataReader, ResourceRegistry} from '../../metadata'; import {ComponentResources, CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, MetadataReader, ResourceRegistry} from '../../metadata';
import {ModuleWithProvidersScanner} from '../../modulewithproviders'; import {ModuleWithProvidersScanner} from '../../modulewithproviders';
@ -636,8 +637,6 @@ export class NgCompiler {
private resolveCompilation(traitCompiler: TraitCompiler): void { private resolveCompilation(traitCompiler: TraitCompiler): void {
traitCompiler.resolve(); traitCompiler.resolve();
this.recordNgModuleScopeDependencies();
// At this point, analysis is complete and the compiler can now calculate which files need to // At this point, analysis is complete and the compiler can now calculate which files need to
// be emitted, so do that. // be emitted, so do that.
this.incrementalDriver.recordSuccessfulAnalysis(traitCompiler); this.incrementalDriver.recordSuccessfulAnalysis(traitCompiler);
@ -810,74 +809,6 @@ export class NgCompiler {
return this.nonTemplateDiagnostics; 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 { private scanForMwp(sf: ts.SourceFile): void {
this.compilation!.mwpScanner.scan(sf, { this.compilation!.mwpScanner.scan(sf, {
addTypeReplacement: (node: ts.Declaration, type: Type): void => { addTypeReplacement: (node: ts.Declaration, type: Type): void => {
@ -957,6 +888,7 @@ export class NgCompiler {
const scopeRegistry = const scopeRegistry =
new LocalModuleScopeRegistry(localMetaReader, depScopeReader, refEmitter, aliasingHost); new LocalModuleScopeRegistry(localMetaReader, depScopeReader, refEmitter, aliasingHost);
const scopeReader: ComponentScopeReader = scopeRegistry; const scopeReader: ComponentScopeReader = scopeRegistry;
const semanticDepGraphUpdater = this.incrementalDriver.getSemanticDepGraphUpdater();
const metaRegistry = new CompoundMetadataRegistry([localMetaRegistry, scopeRegistry]); const metaRegistry = new CompoundMetadataRegistry([localMetaRegistry, scopeRegistry]);
const injectableRegistry = new InjectableClassRegistry(reflector); const injectableRegistry = new InjectableClassRegistry(reflector);
@ -998,7 +930,7 @@ export class NgCompiler {
CycleHandlingStrategy.Error; CycleHandlingStrategy.Error;
// Set up the IvyCompilation, which manages state for the Ivy transformer. // 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( new ComponentDecoratorHandler(
reflector, evaluator, metaRegistry, metaReader, scopeReader, scopeRegistry, reflector, evaluator, metaRegistry, metaReader, scopeReader, scopeRegistry,
typeCheckScopeRegistry, resourceRegistry, isCore, this.resourceManager, typeCheckScopeRegistry, resourceRegistry, isCore, this.resourceManager,
@ -1007,15 +939,16 @@ export class NgCompiler {
this.options.enableI18nLegacyMessageIdFormat !== false, this.usePoisonedData, this.options.enableI18nLegacyMessageIdFormat !== false, this.usePoisonedData,
this.options.i18nNormalizeLineEndingsInICUs, this.moduleResolver, this.cycleAnalyzer, this.options.i18nNormalizeLineEndingsInICUs, this.moduleResolver, this.cycleAnalyzer,
cycleHandlingStrategy, refEmitter, defaultImportTracker, this.incrementalDriver.depGraph, 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` // TODO(alxhub): understand why the cast here is necessary (something to do with `null`
// not being assignable to `unknown` when wrapped in `Readonly`). // not being assignable to `unknown` when wrapped in `Readonly`).
// clang-format off // clang-format off
new DirectiveDecoratorHandler( new DirectiveDecoratorHandler(
reflector, evaluator, metaRegistry, scopeRegistry, metaReader, reflector, evaluator, metaRegistry, scopeRegistry, metaReader,
defaultImportTracker, injectableRegistry, isCore, this.closureCompilerEnabled, defaultImportTracker, injectableRegistry, isCore, semanticDepGraphUpdater,
compileUndecoratedClassesWithAngularFeatures, this.closureCompilerEnabled, compileUndecoratedClassesWithAngularFeatures,
) as Readonly<DecoratorHandler<unknown, unknown, unknown>>, ) as Readonly<DecoratorHandler<unknown, unknown, SemanticSymbol | null,unknown>>,
// clang-format on // clang-format on
// Pipe handler must be before injectable handler in list so pipe factories are printed // Pipe handler must be before injectable handler in list so pipe factories are printed
// before injectable factories (so injectable factories can delegate to them) // before injectable factories (so injectable factories can delegate to them)
@ -1033,7 +966,8 @@ export class NgCompiler {
const traitCompiler = new TraitCompiler( const traitCompiler = new TraitCompiler(
handlers, reflector, this.perfRecorder, this.incrementalDriver, handlers, reflector, this.perfRecorder, this.incrementalDriver,
this.options.compileNonExportedClasses !== false, compilationMode, dtsTransforms); this.options.compileNonExportedClasses !== false, compilationMode, dtsTransforms,
semanticDepGraphUpdater);
const templateTypeChecker = new TemplateTypeCheckerImpl( const templateTypeChecker = new TemplateTypeCheckerImpl(
this.tsProgram, this.typeCheckingProgramStrategy, traitCompiler, this.tsProgram, this.typeCheckingProgramStrategy, traitCompiler,

View File

@ -11,6 +11,7 @@ ts_library(
":api", ":api",
"//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/imports", "//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/metadata",
"//packages/compiler-cli/src/ngtsc/partial_evaluator", "//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/reflection",
@ -27,6 +28,7 @@ ts_library(
srcs = ["api.ts"], srcs = ["api.ts"],
deps = [ deps = [
"//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/reflection",
"@npm//typescript", "@npm//typescript",
], ],
) )

View File

@ -1,154 +1,291 @@
# What is the `incremental` package? # Incremental Compilation
This package contains logic related to incremental compilation in ngtsc. The `incremental` package contains logic related to incremental compilation in ngtsc. Its goal is to
ensure that the compiler's incremental performance is largely O(number of files changed in that
iteration) instead of O(size of the program as a whole), by allowing the compiler to optimize away
as much work as possible without sacrificing the correctness of its output.
In particular, it tracks dependencies between `ts.SourceFile`s, so the compiler can make intelligent decisions about when it's safe to skip certain operations. An incremental compilation receives information about the prior compilation, including
its `ts.Program` and the result of ngtsc's analyses of each class in that program. Depending on the
nature of any changes made to files in the program between its prior and current versions, and on
the semantic effect of those changes, ngtsc may perform 3 different optimizations as it processes
the new build:
* It can reuse analysis work performed in the previous program
# What optimizations are made? ngtsc receives the analyses of all decorated classes performed as part of the previous compilation,
and can reuse that work for a class if it can prove that the results are not stale.
ngtsc currently makes two optimizations: reuse of prior analysis work, and the skipping of file emits. * It can skip emitting a file
## Reuse of analyses Emitting a file is a very expensive operation in TypeScript, involving the execution of many
internal TS transforms (downleveling, module system, etc) as well as the synthesis of a large text
buffer for the final JS output. Skipping emit of a file is the most effective optimizations ngtsc
can do. It's also one of the most challenging. Even if ngtsc's _analysis_ of a specific file is not
stale, that file may still need to be re-emitted if other changes in the program impact its
semantics. For example, a change to a component selector affects other components which use that
selector in their templates, even though no direct dependency exists between them.
If a build has succeeded previously, ngtsc has available the analyses of all Angular classes in the prior program, as well as the dependency graph which outlines inter-file dependencies. This is known as the "last good compilation". * It can reuse template type-checking code
When the next build begins, ngtsc follows a simple algorithm which reuses prior work where possible: Template type-checking code is generated using semantic information extracted from the user's
program. This generation can be expensive, and ngtsc attempts to reuse previous results as much as
possible. This optimization can be thought of as a special case of the above re-emit optimization,
since template type-checking code is a particular flavor of "emit" for a component.
1) For each input file, ngtsc makes a determination as to whether the file is "logically changed". Due to the way that template type-checking works (creation of a second `ts.Program`
with `.ngtypecheck` files containing template type-checking blocks, or TCBs), reuse of template
type-checking code is critical for good performance. Not only is generation of these TCBs expensive,
but forcing TypeScript to re-parse and re-analyze every `.ngtypecheck` file on each incremental
change would be costly as well.
"Logically changed" means that either: The `incremental` package is dedicated to allowing ngtsc to make these important optimizations
safely.
During an incremental compilation, the compiler begins with a process called "reconciliation",
focused on understanding the differences between the incoming, new `ts.Program` and the
last `ts.Program`. In TypeScript, an unchanged file will have its `ts.SourceFile` AST completely
reused. Reconciliation therefore examines the `ts.SourceFile`s of both the old and new programs, and
identifies files which have been added, removed, or changed. This information feeds in to the rest
of the incremental compilation process.
## Reuse of analysis results
Angular's process of understanding an individual component, directive, or other decorated class is
known as "analysis". Analysis is always performed on a class-by-class basis, so the analysis of a
component only takes into consideration information present in the `@Component` decorator, and not
for example in the `@NgModule` which declares the component.
However, analysis _can_ depend on information outside of the decorated class's file. This can happen
in two ways:
* External resources, such as templates or stylesheets, are covered by analysis.
* The partial evaluation of expressions within a class's metadata may descend into symbols imported
from other files.
For example, a directive's selector may be determined via an imported constant:
* The file itself has physically changed on disk, or ```typescript=
* One of the file's dependencies has physically changed on disk. import {Directive} from '@angular/core';
import {DIR_SELECTOR} from './selectors';
@Directive({
selector: DIR_SELECTOR,
})
export class Dir {}
```
The analysis of this directive _depends on_ the value of `DIR_SELECTOR` from `selectors.ts`.
Consequently, if `selectors.ts` changes, `Dir` needs to be re-analyzed, even if `dir.ts` has not
changed.
The `incremental` system provides a mechanism which tracks such dependencies at the file level. The
partial evaluation system records dependencies for any given evaluation operation when an import
boundary is crossed, building up a file-to-file dependency graph. This graph is then transmitted to
the next incremental compilation, where it can be used to determine, based on the set of files
physically changed on disk, which files have _logically_ changed and need to be re-analyzed.
Either of these conditions invalidates the previous analysis of the file. ## Reuse of emit results
In plain TypeScript programs, the compiled JavaScript code for any given input file (e.g. `foo.ts`)
depends only on the code within that input file. That is, only the contents of `foo.ts` can affect
the generated contents written to `foo.js`. The TypeScript compiler can therefore perform a very
simple optimization, and avoid generating and emitting code for any input files which do not change.
This is important for good incremental build performance, as emitting a file is a very expensive
operation.
2) ngtsc begins constructing a new dependency graph. (in practice, the TypeScript feature of `const enum` declarations breaks this overly simple model)
For each logically unchanged file, its dependencies are copied wholesale into the new graph. In Angular applications, however, this optimization is not nearly so simple. The emit of a `.js`
file in Angular is affected in four main ways:
3) ngtsc begins analyzing each file in the program. * Just as in plain TS, it depends on the contents of the input `.ts` file.
* It can be affected by expressions that were statically evaluated during analysis of any decorated
classes in the input, and these expressions can depend on other files.
If the file is logically unchanged, ngtsc will reuse the previous analysis and only call the 'register' phase of compilation, to apply any necessary side effects. For example, the directive with its selector specified via the imported `DIR_SELECTOR` constant
above has compilation output which depends on the value of `DIR_SELECTOR`. Therefore, the `dir.js`
file needs to be emitted whenever the value of the selector constant in `selectors.ts` changes, even
if `dir.ts` itself is unchanged. The compiler therefore will re-emit `dir.js` if the `dir.ts` file
is determined to have _logically_ changed, using the same dependency graph that powers analysis
reuse.
If the file is logically changed, ngtsc will re-analyze it. * Components can have external templates and CSS stylesheets which influence their compilation.
## Reuse of template type-checking code These are incorporated into a component's analysis dependencies.
Generally speaking, the generation of a template type-checking "shim" for an input component file is a time-consuming operation. Such generation produces several outputs: * Components (and NgModules) are influenced by the NgModule graph, which controls which directives
and pipes are "in scope" for each component's template.
1) The text of the template type-checking shim file, which can later be fed to TypeScript for the production of raw diagnostics. This last relationship is the most difficult, as there is no import relationship between a component
2) Metadata regarding source mappings within the template type-checking shim, which can be used to convert the raw diagnostics into mapped template diagnostics. and the directives and pipes it uses in its template. That means that a component file can be
3) "Construction" diagnostics, which are diagnostics produced as a side effect of generation of the shim itself. logically unchanged, but still require re-emit if one of its dependencies has been updated in a way
that influences the compilation of the component.
When a component file is logically unchanged, ngtsc attempts to reuse this generation work. As part of creating both the new emit program and template type-checking program, the `ts.SourceFile` of the shim for the component file is included directly and not re-generated. ### Example
At the same time, the metadata and construction diagnostics are passed via the incremental build system. When TS gets diagnostics for the shim file, this metadata is used to convert them into mapped template diagnostics for delivery to the user. For example, the output of a compiled component includes an array called `directiveDefs`, listing
all of the directives and components actually used within the component's template. This array is
built by combining the template (from analysis) with the "scope" of the component - the set of
directives and pipes which are available for use in its template. This scope is synthesized from the
analysis of not just the component's NgModule, but other NgModules which might be imported, and the
components/directives that those NgModules export, and their analysis data as well.
### Limitations on template type-checking reuse These dependencies of a component on the directives/pipes it consumes, and the NgModule structures
that made them visible, are not captured in the file-level dependency graph. This is due to the
peculiar nature of NgModule and component relationships: NgModules import components, so there is
never a reference from a component to its NgModule, or any of its directive or pipe dependencies.
In certain cases the template type-checking system is unable to use the existing shim code. If the component is logically changed, the shim is regenerated in case its contents may have changed. If generating the shim itself required the use of any "inline" code (type-checking code which needs to be inserted into the component file instead for some reason), it also becomes ineligible for reuse. In code, this looks like:
## Skipping emit ```typescript=
// dir.ts
@Directive({selector: '[dir]'})
export class Dir {}
ngtsc makes a decision to skip the emit of a file if it can prove that the contents of the file will not have changed since the last good compilation. To prove this, two conditions must be true. // cmp.ts
@Component({
selector: 'cmp',
template: '<div dir></div>', // Matches the `[dir]` selector
})
export class Cmp {}
* The input file itself must not have changed since the previous compilation. // mod.ts
import {Dir} from './dir';
import {Cmp} from './cmp';
* None of the files on which the input file is dependent have changed since the previous compilation. @NgModule({declarations: [Dir, Cmp]})
export class Mod {}
```
The second condition is challenging to prove, as Angular allows statically evaluated expressions in lots of contexts that could result in changes from file to file. For example, the `name` of an `@Pipe` could be a reference to a constant in a different file. As part of analyzing the program, the compiler keeps track of such dependencies in order to answer this question. Here, `Cmp` never directly imports or refers to `Dir`, but it _does_ consume the directive in its
template. During emit, `Cmp` would receive a `directiveDefs` array:
The emit of a file is the most expensive part of TypeScript/Angular compilation, so skipping emits when they are not necessary is one of the most valuable things the compiler can do to improve incremental build performance. ```typescript=
// cmp.js
import * as i1 from './dir';
## The two dependency graphs export class Cmp {
static cmp = defineComponent({
...
directiveDefs: [i1.Dir],
});
}
```
For both of the above optimizations, ngtsc makes use of dependency information extracted from the program. But these usages are subtly different. If `Dir`'s selector were to change to `[other]` in an incremental step, it might no longer
match `Cmp`'s template, in which case `cmp.js` would need to be re-emitted.
To reuse previous analyses, ngtsc uses the _prior_ compilation's dependency graph, plus the information about which files have changed, to determine whether it's safe to reuse the prior compilation's work. ### SemanticSymbols
To skip emit, ngtsc uses the _current_ compilation's dependency graph, coupled with the information about which files have changed since the last successful build, to determine the set of outputs that need to be re-emitted. For each decorated class being processed, the compiler creates a `SemanticSymbol` representing the
data regarding that class that's involved in these "indirect" relationships. During the
compiler's `resolve` phase, these `SemanticSymbol`s are connected together to form a "semantic
dependency graph". Two classes of data are recorded:
# How does incremental compilation work? * Information about the public shape API of the class.
The initial compilation is no different from a standalone compilation; the compiler is unaware that incremental compilation will be utilized. For example, directives have a public API which includes their selector, any inputs or outputs, and
their `exportAs` name if any.
When an `NgtscProgram` is created for a _subsequent_ compilation, it is initialized with the `NgtscProgram` from the previous compilation. It is therefore able to take advantage of any information present in the previous compilation to optimize the next one. * Information about the emit shape of the class, including any dependencies on
other `SemanticSymbol`s.
This information is leveraged in two major ways: This information allows the compiler to determine which classes have been semantically affected by
other changes in the program (and therefore need to be re-emitted) according to a simple algorithm:
1) The previous `ts.Program` itself is used to create the next `ts.Program`, allowing TypeScript internally to leverage information from the previous compile in much the same way. 1. Determine the set of `SemanticSymbol`s which have had their public API changed.
2. For each `SemanticSymbol`, determine if its emit shape was affected by any of the public API
changes (that is, if it depends on a symbol with public API changes).
2) An `IncrementalDriver` instance is constructed from the old and new `ts.Program`s, and the previous program's `IncrementalDriver`. ### Determination of public API changes
The compiler then proceeds normally, using the `IncrementalDriver` to manage the reuse of any pertinent information while processing the new program. As a part of this process, the compiler (again) maps out all of the dependencies between files. The first step of this algorithm is to determine, for each `SemanticSymbol`, if its public API has
been affected. Doing this requires knowing which `SemanticSymbol` in the previous program
corresponds to the current version of the symbol. There are two ways that symbols can be "matched":
## Determination of files to emit * The old and new symbols share the same `ts.ClassDeclaration`.
The principle question the incremental build system must answer is "which TS files need to be emitted for a given compilation?" This is true whenever the `ts.SourceFile` declaring the class has not changed between the old and
new programs. The public API of the symbol may still have changed (such as when a directive's
selector is determined by a constant imported from another file, like in one of the examples above).
But if the declaration file itself has not changed, then the previous symbol can be directly found
this way.
To determine whether an individual TS file needs to be emitted, the compiler must determine 3 things about the file: * By its unique path and name.
1. Have its contents changed since the last time it was emitted? If the file _has_ changed, then symbols can be located by their declaration path plus their name, if
2. Has any resource file that the TS file depends on (like an HTML template) changed since the last time it was emitted? they have a name that's guaranteed to be unique. Currently, this means that the classes are declared
3. Have any of the dependencies of the TS file changed since the last time it was emitted? at the top level of the source file, so their names are in the module's scope. If this is the case,
then a symbol can be matched to its ancestor even if the declaration itself has changed in the
meantime. Note that there is no guarantee the symbol will be of the same type - an incremental step
may change a directive into a component, or even into a pipe or injectable.
If the answer to any of these questions is yes, then the TS file needs to be re-emitted. Once a previous symbol is located, its public API can be compared against the current version of the
symbol. Symbols without a valid ancestor are assumed to have changed in their public API.
## Tracking of changes The compiler processes all `SemanticSymbol`s and determines the `Set` of them which have experienced
public API changes. In the example above, this `Set` would include the `DirectiveSymbol` for `Dir`,
since its selector would have changed.
On every invocation, the compiler receives (or can easily determine) several pieces of information: ### Determination of emit requirements
* The set of `ts.SourceFile`s that have changed since the last invocation. For each potential output file, the compiler then looks at all declared `SemanticSymbol`s and uses
* The set of resources (`.html` files) that have changed since the last invocation. their ancestor symbol (if present) as well as the `Set` of public API changes to make a
determination if that file needs be emitted.
With this information, the compiler can perform rebuild optimizations: In the case of a `ComponentSymbol`, for example, the symbol tracks the dependencies of the component
which will go into the `directiveDefs` array. If that array is different, the component needs to be
re-emitted. Even if the same directives are referenced, if one of those directives has changed in
its public API, the emitted output (especially when generating prelink library code) may be
affected, and the component needs to be re-emitted.
1. The compiler uses the last good compilation's dependency graph to determine which parts of its analysis work can be reused, and an initial set of files which need to be re-emitted. ### `SemanticReference`s
2. The compiler analyzes the rest of the program and generates an updated dependency graph, which describes the relationships between files in the program as they are currently.
3. Based on this graph, the compiler can make a final determination for each TS file whether it needs to be re-emitted or can safely be skipped. This produces a set called `pendingEmit` of every file which requires a re-emit.
4. The compiler cycles through the files and emits those which are necessary, removing them from `pendingEmit`.
Theoretically, after this process `pendingEmit` should be empty. As a precaution against errors which might happen in the future, `pendingEmit` is also passed into future compilations, so any files which previously were determined to need an emit (but have not been successfully produced yet) will be retried on subsequent compilations. This is mostly relevant if a client of `ngtsc` attempts to implement emit-on-error functionality. `ComponentSymbol`s track their dependencies via an intermediate type, a `SemanticReference`. Such
references track not only the `SemanticSymbol` of the dependency, but also the name by which it was
imported previously. Even if a dependency's identity and public API remain the same, changes in how
it was exported can affect the import which needs to be emitted within the component consuming it,
and thus would require a re-emit.
However, normally the execution of these steps requires a correct input program. In the presence of TypeScript errors, the compiler cannot perform this process. It might take many invocations for the user to fix all their TypeScript errors and reach a compilation that can be analyzed. ## Reuse of template type-checking results
As a result, the compiler must accumulate the set of these changes (to source files and resource files) from build to build until analysis can succeed. Since type-checking block (TCB) generation for template type-checking is a form of
emit, `SemanticSymbol`s also track the type-checking shape of decorated classes. This includes any
data which is not public API, but upon which the TCB generation for components might depend. Such
data includes:
This accumulation happens via a type called `BuildState`. This type is a union of two possible states. * Type-checking API shape from any base classes, since TCB generation uses information from the full
inheritance chain of a directive/pipe.
* The generic signature shape of the class.
* Private field names for `@Input`s and `@Output`s.
### `PendingBuildState` Using a similar algorithm to the `emit` optimization, the compiler can determine which files need
their type-checking code regenerated, and which can continue to use TCB code from the previous
program, even if some dependencies have unrelated changes.
This is the initial state of any build, and the final state of any unsuccessful build. This state tracks both `pendingEmit` files from the previous program as well as any source or resource files which have changed since the last successful analysis. ## Unsuccessful compilation attempts
If a new build starts and inherits from a failed build, it will merge the failed build's `PendingBuildState` into its own, including the sets of changed files. Often, incremental compilations will fail. The user's input program may contain incomplete changes,
typos, semantic errors, or other problems which prevent the compiler from fully analyzing or
emitting it. Such errors create problems for incremental build correctness, as the compiler relies
on information extracted from the previous program to correctly optimize the next compilation. If
the previous compilation failed, such information may be unreliable.
### `AnalyzedBuildState` In theory, the compiler could simply not perform incremental compilation on top of a broken build,
and assume that it must redo all analysis and re-emit all files, but this would result in
devestatingly poor performance for common developer workflows that rely on automatically running
builds and/or tests on every change. The compiler must deal with such scenarios more gracefully.
After analysis is successfully performed, the compiler uses its dependency graph to evaluate the impact of any accumulated changes from the `PendingBuildState`, and updates `pendingEmit` with all of the pending files. At this point, the compiler transitions from a `PendingBuildState` to an `AnalyzedBuildState`, which only tracks `pendingEmit`. In `AnalyzedBuildState` this set is complete, and the raw changes can be forgotten. ngtsc solves this problem by always performing its incremental steps from a "last known good"
compilation. Thus, if compilation A succeeds, and a subsequent compilation B fails, compilation C
will begin using the state of compilation A as a starting point. This requires tracking of two
important pieces of state:
If a new build is started after a successful build, only `pendingEmit` from the `AnalyzedBuildState` needs to be merged into the new build's `PendingBuildState`. * Reusable information, such as analysis results, from the last known good compilation.
* The accumulated set of files which have physically changed since the last known good compilation.
## Component to NgModule dependencies Using this information, ngtsc is able to "forget" about the intermediate failed attempts and begin
each new compilation as if it were a single step from the last successful build. It can then ensure
The dependency of a component on its NgModule is slightly problematic, because its arrow is in the opposite direction of the source dependency (which is from NgModule to the component, via `declarations`). This creates a scenario where, if the NgModule is changed to no longer include the component, the component still needs to be re-emitted because the module has changed. complete correctness of its reuse optimization, since it has reliable data extracted from the "
previous" successful build.
This is one of very few cases where `pendingEmit` must be populated with the logical changes from the previous program (those files determined to be changed in step 1 under "Tracking of changes" above), and cannot simply be created from the current dependency graph.
# What optimizations are possible in the future?
There is plenty of room for improvement here, with diminishing returns for the work involved.
## Semantic dependency tracking
Currently the compiler tracks dependencies only at the file level, and will re-emit dependent files if they _may_ have been affected by a change. Often times a change though does _not_ require updates to dependent files.
For example, today a component's `NgModule` and all of the other components which consume that module's export scope are considered to depend on the component file itself. If the component's template changes, this triggers a re-emit of not only the component's file, but the entire chain of its NgModule and that module's export scope. This happens even though the template of a component _does not have any impact_ on any components which consume it - these other emits are deeply unnecessary.
In contrast, if the component's _selector_ changes, then all those dependent files do need to be updated since their `directiveDefs` might have changed.
Currently the compiler does not distinguish these two cases, and conservatively always re-emits the entire NgModule chain. It would be possible to break the dependency graph down into finer-grained nodes and distinguish between updates that affect the component, vs updates that affect its dependents. This would be a huge win, but is exceedingly complex.
## Skipping template type-checking
Under ideal conditions, after an initial template type-checking program is created, it may be possible to reuse it for emit _and_ type-checking in subsequent builds. This would be a pretty advanced optimization but would save creation of a second `ts.Program` on each valid rebuild.

View File

@ -49,22 +49,6 @@ export interface DependencyTracker<T extends {fileName: string} = ts.SourceFile>
*/ */
addResourceDependency(from: T, on: AbsoluteFsPath): void; 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. * Record that the given file contains unresolvable dependencies.
* *

View File

@ -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",
],
)

View File

@ -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';

View File

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

View File

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

View File

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

View File

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

View File

@ -35,24 +35,6 @@ export class FileDependencyGraph<T extends {fileName: string} = ts.SourceFile> i
this.nodeFor(from).usesResources.add(resource); 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 { recordDependencyAnalysisFailure(file: T): void {
this.nodeFor(file).failedAnalysis = true; this.nodeFor(file).failedAnalysis = true;
} }
@ -63,10 +45,6 @@ export class FileDependencyGraph<T extends {fileName: string} = ts.SourceFile> i
return node ? [...node.usesResources] : []; 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 * Update the current dependency graph from a previous one, incorporating a set of physical
* changes. * changes.
@ -160,5 +138,3 @@ interface FileNode {
usesResources: Set<AbsoluteFsPath>; usesResources: Set<AbsoluteFsPath>;
failedAnalysis: boolean; failedAnalysis: boolean;
} }
const EMPTY_SET: ReadonlySet<any> = new Set<any>();

View File

@ -9,9 +9,11 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath} from '../../file_system'; import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath} from '../../file_system';
import {ClassDeclaration} from '../../reflection';
import {ClassRecord, TraitCompiler} from '../../transform'; import {ClassRecord, TraitCompiler} from '../../transform';
import {FileTypeCheckingData} from '../../typecheck/src/checker'; import {FileTypeCheckingData} from '../../typecheck/src/checker';
import {IncrementalBuild} from '../api'; import {IncrementalBuild} from '../api';
import {SemanticDepGraph, SemanticDepGraphUpdater} from '../semantic_graph';
import {FileDependencyGraph} from './dependency_tracking'; import {FileDependencyGraph} from './dependency_tracking';
@ -27,8 +29,8 @@ export class IncrementalDriver implements IncrementalBuild<ClassRecord, FileType
private state: BuildState; private state: BuildState;
private constructor( private constructor(
state: PendingBuildState, private allTsFiles: Set<ts.SourceFile>, state: PendingBuildState, readonly depGraph: FileDependencyGraph,
readonly depGraph: FileDependencyGraph, private logicalChanges: Set<string>|null) { private logicalChanges: Set<string>|null) {
this.state = state; this.state = state;
} }
@ -49,14 +51,21 @@ export class IncrementalDriver implements IncrementalBuild<ClassRecord, FileType
// this build. // this build.
state = oldDriver.state; state = oldDriver.state;
} else { } else {
let priorGraph: SemanticDepGraph|null = null;
if (oldDriver.state.lastGood !== null) {
priorGraph = oldDriver.state.lastGood.semanticDepGraph;
}
// The previous build was successfully analyzed. `pendingEmit` is the only state carried // The previous build was successfully analyzed. `pendingEmit` is the only state carried
// forward into this build. // forward into this build.
state = { state = {
kind: BuildStateKind.Pending, kind: BuildStateKind.Pending,
pendingEmit: oldDriver.state.pendingEmit, pendingEmit: oldDriver.state.pendingEmit,
pendingTypeCheckEmit: oldDriver.state.pendingTypeCheckEmit,
changedResourcePaths: new Set<AbsoluteFsPath>(), changedResourcePaths: new Set<AbsoluteFsPath>(),
changedTsPaths: new Set<string>(), changedTsPaths: new Set<string>(),
lastGood: oldDriver.state.lastGood, lastGood: oldDriver.state.lastGood,
semanticDepGraphUpdater: new SemanticDepGraphUpdater(priorGraph),
}; };
} }
@ -106,6 +115,7 @@ export class IncrementalDriver implements IncrementalBuild<ClassRecord, FileType
// The next step is to remove any deleted files from the state. // The next step is to remove any deleted files from the state.
for (const filePath of deletedTsPaths) { for (const filePath of deletedTsPaths) {
state.pendingEmit.delete(filePath); state.pendingEmit.delete(filePath);
state.pendingTypeCheckEmit.delete(filePath);
// Even if the file doesn't exist in the current compilation, it still might have been changed // Even if the file doesn't exist in the current compilation, it still might have been changed
// in a previous one, so delete it from the set of changed TS files, just in case. // in a previous one, so delete it from the set of changed TS files, just in case.
@ -138,13 +148,13 @@ export class IncrementalDriver implements IncrementalBuild<ClassRecord, FileType
// re-emitted. // re-emitted.
for (const change of logicalChanges) { for (const change of logicalChanges) {
state.pendingEmit.add(change); state.pendingEmit.add(change);
state.pendingTypeCheckEmit.add(change);
} }
} }
// `state` now reflects the initial pending state of the current compilation. // `state` now reflects the initial pending state of the current compilation.
return new IncrementalDriver( return new IncrementalDriver(state, depGraph, logicalChanges);
state, new Set<ts.SourceFile>(tsOnlyFiles(newProgram)), depGraph, logicalChanges);
} }
static fresh(program: ts.Program): IncrementalDriver { static fresh(program: ts.Program): IncrementalDriver {
@ -155,13 +165,21 @@ export class IncrementalDriver implements IncrementalBuild<ClassRecord, FileType
const state: PendingBuildState = { const state: PendingBuildState = {
kind: BuildStateKind.Pending, kind: BuildStateKind.Pending,
pendingEmit: new Set<string>(tsFiles.map(sf => sf.fileName)), pendingEmit: new Set<string>(tsFiles.map(sf => sf.fileName)),
pendingTypeCheckEmit: new Set<string>(tsFiles.map(sf => sf.fileName)),
changedResourcePaths: new Set<AbsoluteFsPath>(), changedResourcePaths: new Set<AbsoluteFsPath>(),
changedTsPaths: new Set<string>(), changedTsPaths: new Set<string>(),
lastGood: null, lastGood: null,
semanticDepGraphUpdater: new SemanticDepGraphUpdater(/* priorGraph */ null),
}; };
return new IncrementalDriver( return new IncrementalDriver(state, new FileDependencyGraph(), /* logicalChanges */ null);
state, new Set(tsFiles), new FileDependencyGraph(), /* logicalChanges */ null); }
getSemanticDepGraphUpdater(): SemanticDepGraphUpdater {
if (this.state.kind !== BuildStateKind.Pending) {
throw new Error('Semantic dependency updater is only available when pending analysis');
}
return this.state.semanticDepGraphUpdater;
} }
recordSuccessfulAnalysis(traitCompiler: TraitCompiler): void { recordSuccessfulAnalysis(traitCompiler: TraitCompiler): void {
@ -170,26 +188,29 @@ export class IncrementalDriver implements IncrementalBuild<ClassRecord, FileType
return; return;
} }
const {needsEmit, needsTypeCheckEmit, newGraph} = this.state.semanticDepGraphUpdater.finalize();
const pendingEmit = this.state.pendingEmit; const pendingEmit = this.state.pendingEmit;
for (const path of needsEmit) {
pendingEmit.add(path);
}
const state: PendingBuildState = this.state; const pendingTypeCheckEmit = this.state.pendingTypeCheckEmit;
for (const path of needsTypeCheckEmit) {
for (const sf of this.allTsFiles) { pendingTypeCheckEmit.add(path);
if (this.depGraph.isStale(sf, state.changedTsPaths, state.changedResourcePaths)) {
// Something has changed which requires this file be re-emitted.
pendingEmit.add(sf.fileName);
}
} }
// Update the state to an `AnalyzedBuildState`. // Update the state to an `AnalyzedBuildState`.
this.state = { this.state = {
kind: BuildStateKind.Analyzed, kind: BuildStateKind.Analyzed,
pendingEmit, pendingEmit,
pendingTypeCheckEmit,
// Since this compilation was successfully analyzed, update the "last good" artifacts to the // Since this compilation was successfully analyzed, update the "last good" artifacts to the
// ones from the current compilation. // ones from the current compilation.
lastGood: { lastGood: {
depGraph: this.depGraph, depGraph: this.depGraph,
semanticDepGraph: newGraph,
traitCompiler: traitCompiler, traitCompiler: traitCompiler,
typeCheckingResults: null, typeCheckingResults: null,
}, },
@ -204,6 +225,12 @@ export class IncrementalDriver implements IncrementalBuild<ClassRecord, FileType
return; return;
} }
this.state.lastGood.typeCheckingResults = results; this.state.lastGood.typeCheckingResults = results;
// Delete the files for which type-check code was generated from the set of pending type-check
// files.
for (const fileName of results.keys()) {
this.state.pendingTypeCheckEmit.delete(fileName);
}
} }
recordSuccessfulEmit(sf: ts.SourceFile): void { recordSuccessfulEmit(sf: ts.SourceFile): void {
@ -233,7 +260,7 @@ export class IncrementalDriver implements IncrementalBuild<ClassRecord, FileType
return null; return null;
} }
if (this.logicalChanges.has(sf.fileName)) { if (this.logicalChanges.has(sf.fileName) || this.state.pendingTypeCheckEmit.has(sf.fileName)) {
return null; return null;
} }
@ -284,6 +311,13 @@ interface BaseBuildState {
*/ */
pendingEmit: Set<string>; pendingEmit: Set<string>;
/**
* Similar to `pendingEmit`, but then for representing the set of files for which the type-check
* file should be regenerated. It behaves identically with respect to errored compilations as
* `pendingEmit`.
*/
pendingTypeCheckEmit: Set<string>;
/** /**
* Specific aspects of the last compilation which successfully completed analysis, if any. * Specific aspects of the last compilation which successfully completed analysis, if any.
@ -296,6 +330,14 @@ interface BaseBuildState {
*/ */
depGraph: FileDependencyGraph; depGraph: FileDependencyGraph;
/**
* The semantic dependency graph from the last successfully analyzed build.
*
* This is used to perform in-depth comparison of Angular decorated classes, to determine
* which files have to be re-emitted and/or re-type-checked.
*/
semanticDepGraph: SemanticDepGraph;
/** /**
* The `TraitCompiler` from the last successfully analyzed build. * The `TraitCompiler` from the last successfully analyzed build.
* *
@ -333,6 +375,12 @@ interface PendingBuildState extends BaseBuildState {
* Set of resource file paths which have changed since the last successfully analyzed build. * Set of resource file paths which have changed since the last successfully analyzed build.
*/ */
changedResourcePaths: Set<AbsoluteFsPath>; changedResourcePaths: Set<AbsoluteFsPath>;
/**
* In a pending state, the semantic dependency graph is available to the compilation to register
* the incremental symbols into.
*/
semanticDepGraphUpdater: SemanticDepGraphUpdater;
} }
interface AnalyzedBuildState extends BaseBuildState { interface AnalyzedBuildState extends BaseBuildState {

View File

@ -989,7 +989,5 @@ runInEachFileSystem(() => {
const fakeDepTracker: DependencyTracker = { const fakeDepTracker: DependencyTracker = {
addDependency: () => undefined, addDependency: () => undefined,
addResourceDependency: () => undefined, addResourceDependency: () => undefined,
addTransitiveDependency: () => undefined,
addTransitiveResources: () => undefined,
recordDependencyAnalysisFailure: () => undefined, recordDependencyAnalysisFailure: () => undefined,
}; };

View File

@ -32,16 +32,6 @@ export interface LocalModuleScope extends ExportScope {
schemas: SchemaMetadata[]; schemas: SchemaMetadata[];
} }
/**
* Information about the compilation scope of a registered declaration.
*/
export interface CompilationScope extends ScopeData {
/** The declaration whose compilation scope is described here. */
declaration: ClassDeclaration;
/** The declaration of the NgModule that declares this `declaration`. */
ngModule: ClassDeclaration;
}
/** /**
* A registry which collects information about NgModules, Directives, Components, and Pipes which * A registry which collects information about NgModules, Directives, Components, and Pipes which
* are local (declared in the ts.Program being compiled), and can produce `LocalModuleScope`s * are local (declared in the ts.Program being compiled), and can produce `LocalModuleScope`s
@ -187,20 +177,6 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
} }
} }
/**
* Returns a collection of the compilation scope for each registered declaration.
*/
getCompilationScopes(): CompilationScope[] {
const scopes: CompilationScope[] = [];
this.declarationToModule.forEach((declData, declaration) => {
const scope = this.getScopeOfModule(declData.ngModule);
if (scope !== null) {
scopes.push({declaration, ngModule: declData.ngModule, ...scope.compilation});
}
});
return scopes;
}
private registerDeclarationOfModule( private registerDeclarationOfModule(
ngModule: ClassDeclaration, decl: Reference<ClassDeclaration>, ngModule: ClassDeclaration, decl: Reference<ClassDeclaration>,
rawDeclarations: ts.Expression|null): void { rawDeclarations: ts.Expression|null): void {

View File

@ -13,6 +13,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/diagnostics", "//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/incremental:api", "//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/indexer",
"//packages/compiler-cli/src/ngtsc/modulewithproviders", "//packages/compiler-cli/src/ngtsc/modulewithproviders",
"//packages/compiler-cli/src/ngtsc/perf", "//packages/compiler-cli/src/ngtsc/perf",

View File

@ -10,6 +10,7 @@ import {ConstantPool, Expression, Statement, Type} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {Reexport} from '../../imports'; import {Reexport} from '../../imports';
import {SemanticSymbol} from '../../incremental/semantic_graph';
import {IndexingContext} from '../../indexer'; import {IndexingContext} from '../../indexer';
import {ClassDeclaration, Decorator} from '../../reflection'; import {ClassDeclaration, Decorator} from '../../reflection';
import {ImportManager} from '../../translator'; import {ImportManager} from '../../translator';
@ -87,7 +88,7 @@ export enum HandlerFlags {
* @param `A` The type of analysis metadata produced by `analyze`. * @param `A` The type of analysis metadata produced by `analyze`.
* @param `R` The type of resolution metadata produced by `resolve`. * @param `R` The type of resolution metadata produced by `resolve`.
*/ */
export interface DecoratorHandler<D, A, R> { export interface DecoratorHandler<D, A, S extends SemanticSymbol|null, R> {
readonly name: string; readonly name: string;
/** /**
@ -134,6 +135,20 @@ export interface DecoratorHandler<D, A, R> {
*/ */
updateResources?(node: ClassDeclaration, analysis: A, resolution: R): void; updateResources?(node: ClassDeclaration, analysis: A, resolution: R): void;
/**
* Produces a `SemanticSymbol` that represents the class, which is registered into the semantic
* dependency graph. The symbol is used in incremental compilations to let the compiler determine
* how a change to the class affects prior emit results. See the `incremental` target's README for
* details on how this works.
*
* The symbol is passed in to `resolve`, where it can be extended with references into other parts
* of the compilation as needed.
*
* Only primary handlers are allowed to have symbols; handlers with `precedence` other than
* `HandlerPrecedence.PRIMARY` must return a `null` symbol.
*/
symbol(node: ClassDeclaration, analysis: Readonly<A>): S;
/** /**
* Post-process the analysis of a decorator/class combination and record any necessary information * Post-process the analysis of a decorator/class combination and record any necessary information
* in the larger compilation. * in the larger compilation.
@ -159,7 +174,7 @@ export interface DecoratorHandler<D, A, R> {
* `DecoratorHandler` a chance to leverage information from the whole compilation unit to enhance * `DecoratorHandler` a chance to leverage information from the whole compilation unit to enhance
* the `analysis` before the emit phase. * the `analysis` before the emit phase.
*/ */
resolve?(node: ClassDeclaration, analysis: Readonly<A>): ResolveResult<R>; resolve?(node: ClassDeclaration, analysis: Readonly<A>, symbol: S): ResolveResult<R>;
typeCheck? typeCheck?
(ctx: TypeCheckContext, node: ClassDeclaration, analysis: Readonly<A>, (ctx: TypeCheckContext, node: ClassDeclaration, analysis: Readonly<A>,

View File

@ -11,6 +11,7 @@ import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {IncrementalBuild} from '../../incremental/api'; import {IncrementalBuild} from '../../incremental/api';
import {SemanticDepGraphUpdater, SemanticSymbol} from '../../incremental/semantic_graph';
import {IndexingContext} from '../../indexer'; import {IndexingContext} from '../../indexer';
import {PerfRecorder} from '../../perf'; import {PerfRecorder} from '../../perf';
import {ClassDeclaration, DeclarationNode, Decorator, ReflectionHost} from '../../reflection'; import {ClassDeclaration, DeclarationNode, Decorator, ReflectionHost} from '../../reflection';
@ -34,7 +35,7 @@ export interface ClassRecord {
/** /**
* All traits which matched on the class. * All traits which matched on the class.
*/ */
traits: Trait<unknown, unknown, unknown>[]; traits: Trait<unknown, unknown, SemanticSymbol|null, unknown>[];
/** /**
* Meta-diagnostics about the class, which are usually related to whether certain combinations of * Meta-diagnostics about the class, which are usually related to whether certain combinations of
@ -82,14 +83,16 @@ export class TraitCompiler implements ProgramTypeCheckAdapter {
private reexportMap = new Map<string, Map<string, [string, string]>>(); private reexportMap = new Map<string, Map<string, [string, string]>>();
private handlersByName = new Map<string, DecoratorHandler<unknown, unknown, unknown>>(); private handlersByName =
new Map<string, DecoratorHandler<unknown, unknown, SemanticSymbol|null, unknown>>();
constructor( constructor(
private handlers: DecoratorHandler<unknown, unknown, unknown>[], private handlers: DecoratorHandler<unknown, unknown, SemanticSymbol|null, unknown>[],
private reflector: ReflectionHost, private perf: PerfRecorder, private reflector: ReflectionHost, private perf: PerfRecorder,
private incrementalBuild: IncrementalBuild<ClassRecord, unknown>, private incrementalBuild: IncrementalBuild<ClassRecord, unknown>,
private compileNonExportedClasses: boolean, private compilationMode: CompilationMode, private compileNonExportedClasses: boolean, private compilationMode: CompilationMode,
private dtsTransforms: DtsTransformRegistry) { private dtsTransforms: DtsTransformRegistry,
private semanticDepGraphUpdater: SemanticDepGraphUpdater|null) {
for (const handler of handlers) { for (const handler of handlers) {
this.handlersByName.set(handler.name, handler); this.handlersByName.set(handler.name, handler);
} }
@ -179,10 +182,12 @@ export class TraitCompiler implements ProgramTypeCheckAdapter {
for (const priorTrait of priorRecord.traits) { for (const priorTrait of priorRecord.traits) {
const handler = this.handlersByName.get(priorTrait.handler.name)!; const handler = this.handlersByName.get(priorTrait.handler.name)!;
let trait: Trait<unknown, unknown, unknown> = Trait.pending(handler, priorTrait.detected); let trait: Trait<unknown, unknown, SemanticSymbol|null, unknown> =
Trait.pending(handler, priorTrait.detected);
if (priorTrait.state === TraitState.Analyzed || priorTrait.state === TraitState.Resolved) { if (priorTrait.state === TraitState.Analyzed || priorTrait.state === TraitState.Resolved) {
trait = trait.toAnalyzed(priorTrait.analysis, priorTrait.analysisDiagnostics); const symbol = this.makeSymbolForTrait(handler, record.node, priorTrait.analysis);
trait = trait.toAnalyzed(priorTrait.analysis, priorTrait.analysisDiagnostics, symbol);
if (trait.analysis !== null && trait.handler.register !== undefined) { if (trait.analysis !== null && trait.handler.register !== undefined) {
trait.handler.register(record.node, trait.analysis); trait.handler.register(record.node, trait.analysis);
} }
@ -202,7 +207,7 @@ export class TraitCompiler implements ProgramTypeCheckAdapter {
} }
private scanClassForTraits(clazz: ClassDeclaration): private scanClassForTraits(clazz: ClassDeclaration):
PendingTrait<unknown, unknown, unknown>[]|null { PendingTrait<unknown, unknown, SemanticSymbol|null, unknown>[]|null {
if (!this.compileNonExportedClasses && !isExported(clazz)) { if (!this.compileNonExportedClasses && !isExported(clazz)) {
return null; return null;
} }
@ -213,9 +218,9 @@ export class TraitCompiler implements ProgramTypeCheckAdapter {
} }
protected detectTraits(clazz: ClassDeclaration, decorators: Decorator[]|null): protected detectTraits(clazz: ClassDeclaration, decorators: Decorator[]|null):
PendingTrait<unknown, unknown, unknown>[]|null { PendingTrait<unknown, unknown, SemanticSymbol|null, unknown>[]|null {
let record: ClassRecord|null = this.recordFor(clazz); let record: ClassRecord|null = this.recordFor(clazz);
let foundTraits: PendingTrait<unknown, unknown, unknown>[] = []; let foundTraits: PendingTrait<unknown, unknown, SemanticSymbol|null, unknown>[] = [];
for (const handler of this.handlers) { for (const handler of this.handlers) {
const result = handler.detect(clazz, decorators); const result = handler.detect(clazz, decorators);
@ -293,6 +298,25 @@ export class TraitCompiler implements ProgramTypeCheckAdapter {
return foundTraits.length > 0 ? foundTraits : null; return foundTraits.length > 0 ? foundTraits : null;
} }
private makeSymbolForTrait(
handler: DecoratorHandler<unknown, unknown, SemanticSymbol|null, unknown>,
decl: ClassDeclaration, analysis: Readonly<unknown>|null): SemanticSymbol|null {
if (analysis === null) {
return null;
}
const symbol = handler.symbol(decl, analysis);
if (symbol !== null && this.semanticDepGraphUpdater !== null) {
const isPrimary = handler.precedence === HandlerPrecedence.PRIMARY;
if (!isPrimary) {
throw new Error(
`AssertionError: ${handler.name} returned a symbol but is not a primary handler.`);
}
this.semanticDepGraphUpdater.registerSymbol(symbol);
}
return symbol;
}
protected analyzeClass(clazz: ClassDeclaration, preanalyzeQueue: Promise<void>[]|null): void { protected analyzeClass(clazz: ClassDeclaration, preanalyzeQueue: Promise<void>[]|null): void {
const traits = this.scanClassForTraits(clazz); const traits = this.scanClassForTraits(clazz);
@ -312,7 +336,7 @@ export class TraitCompiler implements ProgramTypeCheckAdapter {
preanalysis = trait.handler.preanalyze(clazz, trait.detected.metadata) || null; preanalysis = trait.handler.preanalyze(clazz, trait.detected.metadata) || null;
} catch (err) { } catch (err) {
if (err instanceof FatalDiagnosticError) { if (err instanceof FatalDiagnosticError) {
trait.toAnalyzed(null, [err.toDiagnostic()]); trait.toAnalyzed(null, [err.toDiagnostic()], null);
return; return;
} else { } else {
throw err; throw err;
@ -328,7 +352,7 @@ export class TraitCompiler implements ProgramTypeCheckAdapter {
} }
protected analyzeTrait( protected analyzeTrait(
clazz: ClassDeclaration, trait: Trait<unknown, unknown, unknown>, clazz: ClassDeclaration, trait: Trait<unknown, unknown, SemanticSymbol|null, unknown>,
flags?: HandlerFlags): void { flags?: HandlerFlags): void {
if (trait.state !== TraitState.Pending) { if (trait.state !== TraitState.Pending) {
throw new Error(`Attempt to analyze trait of ${clazz.name.text} in state ${ throw new Error(`Attempt to analyze trait of ${clazz.name.text} in state ${
@ -341,18 +365,18 @@ export class TraitCompiler implements ProgramTypeCheckAdapter {
result = trait.handler.analyze(clazz, trait.detected.metadata, flags); result = trait.handler.analyze(clazz, trait.detected.metadata, flags);
} catch (err) { } catch (err) {
if (err instanceof FatalDiagnosticError) { if (err instanceof FatalDiagnosticError) {
trait.toAnalyzed(null, [err.toDiagnostic()]); trait.toAnalyzed(null, [err.toDiagnostic()], null);
return; return;
} else { } else {
throw err; throw err;
} }
} }
const symbol = this.makeSymbolForTrait(trait.handler, clazz, result.analysis ?? null);
if (result.analysis !== undefined && trait.handler.register !== undefined) { if (result.analysis !== undefined && trait.handler.register !== undefined) {
trait.handler.register(clazz, result.analysis); trait.handler.register(clazz, result.analysis);
} }
trait = trait.toAnalyzed(result.analysis ?? null, result.diagnostics ?? null, symbol);
trait = trait.toAnalyzed(result.analysis ?? null, result.diagnostics ?? null);
} }
resolve(): void { resolve(): void {
@ -384,7 +408,7 @@ export class TraitCompiler implements ProgramTypeCheckAdapter {
let result: ResolveResult<unknown>; let result: ResolveResult<unknown>;
try { try {
result = handler.resolve(clazz, trait.analysis as Readonly<unknown>); result = handler.resolve(clazz, trait.analysis as Readonly<unknown>, trait.symbol);
} catch (err) { } catch (err) {
if (err instanceof FatalDiagnosticError) { if (err instanceof FatalDiagnosticError) {
trait = trait.toResolved(null, [err.toDiagnostic()]); trait = trait.toResolved(null, [err.toDiagnostic()]);

View File

@ -7,6 +7,7 @@
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {SemanticSymbol} from '../../incremental/semantic_graph';
import {DecoratorHandler, DetectResult} from './api'; import {DecoratorHandler, DetectResult} from './api';
export enum TraitState { export enum TraitState {
@ -44,22 +45,23 @@ export enum TraitState {
* This not only simplifies the implementation, but ensures traits are monomorphic objects as * This not only simplifies the implementation, but ensures traits are monomorphic objects as
* they're all just "views" in the type system of the same object (which never changes shape). * they're all just "views" in the type system of the same object (which never changes shape).
*/ */
export type Trait<D, A, R> = export type Trait<D, A, S extends SemanticSymbol|null, R> = PendingTrait<D, A, S, R>|
PendingTrait<D, A, R>|SkippedTrait<D, A, R>|AnalyzedTrait<D, A, R>|ResolvedTrait<D, A, R>; SkippedTrait<D, A, S, R>|AnalyzedTrait<D, A, S, R>|ResolvedTrait<D, A, S, R>;
/** /**
* The value side of `Trait` exposes a helper to create a `Trait` in a pending state (by delegating * The value side of `Trait` exposes a helper to create a `Trait` in a pending state (by delegating
* to `TraitImpl`). * to `TraitImpl`).
*/ */
export const Trait = { export const Trait = {
pending: <D, A, R>(handler: DecoratorHandler<D, A, R>, detected: DetectResult<D>): pending: <D, A, S extends SemanticSymbol|null, R>(
PendingTrait<D, A, R> => TraitImpl.pending(handler, detected), handler: DecoratorHandler<D, A, S, R>, detected: DetectResult<D>): PendingTrait<D, A, S, R> =>
TraitImpl.pending(handler, detected),
}; };
/** /**
* The part of the `Trait` interface that's common to all trait states. * The part of the `Trait` interface that's common to all trait states.
*/ */
export interface TraitBase<D, A, R> { export interface TraitBase<D, A, S extends SemanticSymbol|null, R> {
/** /**
* Current state of the trait. * Current state of the trait.
* *
@ -70,7 +72,7 @@ export interface TraitBase<D, A, R> {
/** /**
* The `DecoratorHandler` which matched on the class to create this trait. * The `DecoratorHandler` which matched on the class to create this trait.
*/ */
handler: DecoratorHandler<D, A, R>; handler: DecoratorHandler<D, A, S, R>;
/** /**
* The detection result (of `handler.detect`) which indicated that this trait applied to the * The detection result (of `handler.detect`) which indicated that this trait applied to the
@ -86,20 +88,22 @@ export interface TraitBase<D, A, R> {
* *
* Pending traits have yet to be analyzed in any way. * Pending traits have yet to be analyzed in any way.
*/ */
export interface PendingTrait<D, A, R> extends TraitBase<D, A, R> { export interface PendingTrait<D, A, S extends SemanticSymbol|null, R> extends
TraitBase<D, A, S, R> {
state: TraitState.Pending; state: TraitState.Pending;
/** /**
* This pending trait has been successfully analyzed, and should transition to the "analyzed" * This pending trait has been successfully analyzed, and should transition to the "analyzed"
* state. * state.
*/ */
toAnalyzed(analysis: A|null, diagnostics: ts.Diagnostic[]|null): AnalyzedTrait<D, A, R>; toAnalyzed(analysis: A|null, diagnostics: ts.Diagnostic[]|null, symbol: S):
AnalyzedTrait<D, A, S, R>;
/** /**
* During analysis it was determined that this trait is not eligible for compilation after all, * During analysis it was determined that this trait is not eligible for compilation after all,
* and should be transitioned to the "skipped" state. * and should be transitioned to the "skipped" state.
*/ */
toSkipped(): SkippedTrait<D, A, R>; toSkipped(): SkippedTrait<D, A, S, R>;
} }
/** /**
@ -109,7 +113,8 @@ export interface PendingTrait<D, A, R> extends TraitBase<D, A, R> {
* *
* This is a terminal state. * This is a terminal state.
*/ */
export interface SkippedTrait<D, A, R> extends TraitBase<D, A, R> { export interface SkippedTrait<D, A, S extends SemanticSymbol|null, R> extends
TraitBase<D, A, S, R> {
state: TraitState.Skipped; state: TraitState.Skipped;
} }
@ -118,8 +123,10 @@ export interface SkippedTrait<D, A, R> extends TraitBase<D, A, R> {
* *
* Analyzed traits have analysis results available, and are eligible for resolution. * Analyzed traits have analysis results available, and are eligible for resolution.
*/ */
export interface AnalyzedTrait<D, A, R> extends TraitBase<D, A, R> { export interface AnalyzedTrait<D, A, S extends SemanticSymbol|null, R> extends
TraitBase<D, A, S, R> {
state: TraitState.Analyzed; state: TraitState.Analyzed;
symbol: S;
/** /**
* Analysis results of the given trait (if able to be produced), or `null` if analysis failed * Analysis results of the given trait (if able to be produced), or `null` if analysis failed
@ -136,7 +143,7 @@ export interface AnalyzedTrait<D, A, R> extends TraitBase<D, A, R> {
* This analyzed trait has been successfully resolved, and should be transitioned to the * This analyzed trait has been successfully resolved, and should be transitioned to the
* "resolved" state. * "resolved" state.
*/ */
toResolved(resolution: R|null, diagnostics: ts.Diagnostic[]|null): ResolvedTrait<D, A, R>; toResolved(resolution: R|null, diagnostics: ts.Diagnostic[]|null): ResolvedTrait<D, A, S, R>;
} }
/** /**
@ -147,8 +154,10 @@ export interface AnalyzedTrait<D, A, R> extends TraitBase<D, A, R> {
* *
* This is a terminal state. * This is a terminal state.
*/ */
export interface ResolvedTrait<D, A, R> extends TraitBase<D, A, R> { export interface ResolvedTrait<D, A, S extends SemanticSymbol|null, R> extends
TraitBase<D, A, S, R> {
state: TraitState.Resolved; state: TraitState.Resolved;
symbol: S;
/** /**
* Resolved traits must have produced valid analysis results. * Resolved traits must have produced valid analysis results.
@ -176,30 +185,33 @@ export interface ResolvedTrait<D, A, R> extends TraitBase<D, A, R> {
* An implementation of the `Trait` type which transitions safely between the various * An implementation of the `Trait` type which transitions safely between the various
* `TraitState`s. * `TraitState`s.
*/ */
class TraitImpl<D, A, R> { class TraitImpl<D, A, S extends SemanticSymbol|null, R> {
state: TraitState = TraitState.Pending; state: TraitState = TraitState.Pending;
handler: DecoratorHandler<D, A, R>; handler: DecoratorHandler<D, A, S, R>;
detected: DetectResult<D>; detected: DetectResult<D>;
analysis: Readonly<A>|null = null; analysis: Readonly<A>|null = null;
symbol: S|null = null;
resolution: Readonly<R>|null = null; resolution: Readonly<R>|null = null;
analysisDiagnostics: ts.Diagnostic[]|null = null; analysisDiagnostics: ts.Diagnostic[]|null = null;
resolveDiagnostics: ts.Diagnostic[]|null = null; resolveDiagnostics: ts.Diagnostic[]|null = null;
constructor(handler: DecoratorHandler<D, A, R>, detected: DetectResult<D>) { constructor(handler: DecoratorHandler<D, A, S, R>, detected: DetectResult<D>) {
this.handler = handler; this.handler = handler;
this.detected = detected; this.detected = detected;
} }
toAnalyzed(analysis: A|null, diagnostics: ts.Diagnostic[]|null): AnalyzedTrait<D, A, R> { toAnalyzed(analysis: A|null, diagnostics: ts.Diagnostic[]|null, symbol: S):
AnalyzedTrait<D, A, S, R> {
// Only pending traits can be analyzed. // Only pending traits can be analyzed.
this.assertTransitionLegal(TraitState.Pending, TraitState.Analyzed); this.assertTransitionLegal(TraitState.Pending, TraitState.Analyzed);
this.analysis = analysis; this.analysis = analysis;
this.analysisDiagnostics = diagnostics; this.analysisDiagnostics = diagnostics;
this.symbol = symbol;
this.state = TraitState.Analyzed; this.state = TraitState.Analyzed;
return this as AnalyzedTrait<D, A, R>; return this as AnalyzedTrait<D, A, S, R>;
} }
toResolved(resolution: R|null, diagnostics: ts.Diagnostic[]|null): ResolvedTrait<D, A, R> { toResolved(resolution: R|null, diagnostics: ts.Diagnostic[]|null): ResolvedTrait<D, A, S, R> {
// Only analyzed traits can be resolved. // Only analyzed traits can be resolved.
this.assertTransitionLegal(TraitState.Analyzed, TraitState.Resolved); this.assertTransitionLegal(TraitState.Analyzed, TraitState.Resolved);
if (this.analysis === null) { if (this.analysis === null) {
@ -208,14 +220,14 @@ class TraitImpl<D, A, R> {
this.resolution = resolution; this.resolution = resolution;
this.state = TraitState.Resolved; this.state = TraitState.Resolved;
this.resolveDiagnostics = diagnostics; this.resolveDiagnostics = diagnostics;
return this as ResolvedTrait<D, A, R>; return this as ResolvedTrait<D, A, S, R>;
} }
toSkipped(): SkippedTrait<D, A, R> { toSkipped(): SkippedTrait<D, A, S, R> {
// Only pending traits can be skipped. // Only pending traits can be skipped.
this.assertTransitionLegal(TraitState.Pending, TraitState.Skipped); this.assertTransitionLegal(TraitState.Pending, TraitState.Skipped);
this.state = TraitState.Skipped; this.state = TraitState.Skipped;
return this as SkippedTrait<D, A, R>; return this as SkippedTrait<D, A, S, R>;
} }
/** /**
@ -236,8 +248,8 @@ class TraitImpl<D, A, R> {
/** /**
* Construct a new `TraitImpl` in the pending state. * Construct a new `TraitImpl` in the pending state.
*/ */
static pending<D, A, R>(handler: DecoratorHandler<D, A, R>, detected: DetectResult<D>): static pending<D, A, S extends SemanticSymbol|null, R>(
PendingTrait<D, A, R> { handler: DecoratorHandler<D, A, S, R>, detected: DetectResult<D>): PendingTrait<D, A, S, R> {
return new TraitImpl(handler, detected) as PendingTrait<D, A, R>; return new TraitImpl(handler, detected) as PendingTrait<D, A, S, R>;
} }
} }

View File

@ -23,7 +23,7 @@ runInEachFileSystem(() => {
beforeEach(() => _ = absoluteFrom); beforeEach(() => _ = absoluteFrom);
it('should not run decoration handlers against declaration files', () => { it('should not run decoration handlers against declaration files', () => {
class FakeDecoratorHandler implements DecoratorHandler<{}|null, unknown, unknown> { class FakeDecoratorHandler implements DecoratorHandler<{}|null, unknown, null, unknown> {
name = 'FakeDecoratorHandler'; name = 'FakeDecoratorHandler';
precedence = HandlerPrecedence.PRIMARY; precedence = HandlerPrecedence.PRIMARY;
@ -33,6 +33,9 @@ runInEachFileSystem(() => {
analyze(): AnalysisOutput<unknown> { analyze(): AnalysisOutput<unknown> {
throw new Error('analyze should not have been called'); throw new Error('analyze should not have been called');
} }
symbol(): null {
throw new Error('symbol should not have been called');
}
compileFull(): CompileResult { compileFull(): CompileResult {
throw new Error('compile should not have been called'); throw new Error('compile should not have been called');
} }
@ -46,7 +49,7 @@ runInEachFileSystem(() => {
const reflectionHost = new TypeScriptReflectionHost(checker); const reflectionHost = new TypeScriptReflectionHost(checker);
const compiler = new TraitCompiler( const compiler = new TraitCompiler(
[new FakeDecoratorHandler()], reflectionHost, NOOP_PERF_RECORDER, NOOP_INCREMENTAL_BUILD, [new FakeDecoratorHandler()], reflectionHost, NOOP_PERF_RECORDER, NOOP_INCREMENTAL_BUILD,
true, CompilationMode.FULL, new DtsTransformRegistry()); true, CompilationMode.FULL, new DtsTransformRegistry(), null);
const sourceFile = program.getSourceFile('lib.d.ts')!; const sourceFile = program.getSourceFile('lib.d.ts')!;
const analysis = compiler.analyzeSync(sourceFile); const analysis = compiler.analyzeSync(sourceFile);
@ -55,7 +58,7 @@ runInEachFileSystem(() => {
}); });
describe('compilation mode', () => { describe('compilation mode', () => {
class PartialDecoratorHandler implements DecoratorHandler<{}, {}, unknown> { class PartialDecoratorHandler implements DecoratorHandler<{}, {}, null, unknown> {
name = 'PartialDecoratorHandler'; name = 'PartialDecoratorHandler';
precedence = HandlerPrecedence.PRIMARY; precedence = HandlerPrecedence.PRIMARY;
@ -70,6 +73,10 @@ runInEachFileSystem(() => {
return {analysis: {}}; return {analysis: {}};
} }
symbol(): null {
return null;
}
compileFull(): CompileResult { compileFull(): CompileResult {
return { return {
name: 'compileFull', name: 'compileFull',
@ -89,7 +96,7 @@ runInEachFileSystem(() => {
} }
} }
class FullDecoratorHandler implements DecoratorHandler<{}, {}, unknown> { class FullDecoratorHandler implements DecoratorHandler<{}, {}, null, unknown> {
name = 'FullDecoratorHandler'; name = 'FullDecoratorHandler';
precedence = HandlerPrecedence.PRIMARY; precedence = HandlerPrecedence.PRIMARY;
@ -104,6 +111,10 @@ runInEachFileSystem(() => {
return {analysis: {}}; return {analysis: {}};
} }
symbol(): null {
return null;
}
compileFull(): CompileResult { compileFull(): CompileResult {
return { return {
name: 'compileFull', name: 'compileFull',
@ -127,7 +138,7 @@ runInEachFileSystem(() => {
const compiler = new TraitCompiler( const compiler = new TraitCompiler(
[new PartialDecoratorHandler(), new FullDecoratorHandler()], reflectionHost, [new PartialDecoratorHandler(), new FullDecoratorHandler()], reflectionHost,
NOOP_PERF_RECORDER, NOOP_INCREMENTAL_BUILD, true, CompilationMode.PARTIAL, NOOP_PERF_RECORDER, NOOP_INCREMENTAL_BUILD, true, CompilationMode.PARTIAL,
new DtsTransformRegistry()); new DtsTransformRegistry(), null);
const sourceFile = program.getSourceFile('test.ts')!; const sourceFile = program.getSourceFile('test.ts')!;
compiler.analyzeSync(sourceFile); compiler.analyzeSync(sourceFile);
compiler.resolve(); compiler.resolve();
@ -157,7 +168,7 @@ runInEachFileSystem(() => {
const compiler = new TraitCompiler( const compiler = new TraitCompiler(
[new PartialDecoratorHandler(), new FullDecoratorHandler()], reflectionHost, [new PartialDecoratorHandler(), new FullDecoratorHandler()], reflectionHost,
NOOP_PERF_RECORDER, NOOP_INCREMENTAL_BUILD, true, CompilationMode.FULL, NOOP_PERF_RECORDER, NOOP_INCREMENTAL_BUILD, true, CompilationMode.FULL,
new DtsTransformRegistry()); new DtsTransformRegistry(), null);
const sourceFile = program.getSourceFile('test.ts')!; const sourceFile = program.getSourceFile('test.ts')!;
compiler.analyzeSync(sourceFile); compiler.analyzeSync(sourceFile);
compiler.resolve(); compiler.resolve();

View File

@ -161,7 +161,9 @@ export class NgtscTestEnvironment {
const absFilePath = this.fs.resolve(this.basePath, fileName); const absFilePath = this.fs.resolve(this.basePath, fileName);
if (this.multiCompileHostExt !== null) { if (this.multiCompileHostExt !== null) {
this.multiCompileHostExt.invalidate(absFilePath); this.multiCompileHostExt.invalidate(absFilePath);
this.changedResources!.add(absFilePath); if (!fileName.endsWith('.ts')) {
this.changedResources!.add(absFilePath);
}
} }
this.fs.ensureDir(this.fs.dirname(absFilePath)); this.fs.ensureDir(this.fs.dirname(absFilePath));
this.fs.writeFile(absFilePath, content); this.fs.writeFile(absFilePath, content);
@ -173,6 +175,9 @@ export class NgtscTestEnvironment {
throw new Error(`Not caching files - call enableMultipleCompilations()`); throw new Error(`Not caching files - call enableMultipleCompilations()`);
} }
this.multiCompileHostExt.invalidate(absFilePath); this.multiCompileHostExt.invalidate(absFilePath);
if (!fileName.endsWith('.ts')) {
this.changedResources!.add(absFilePath);
}
} }
tsconfig( tsconfig(

View File

@ -239,12 +239,13 @@ runInEachFileSystem(() => {
export class TargetCmp {} export class TargetCmp {}
`); `);
env.write('module.ts', ` env.write('module.ts', `
import {NgModule} from '@angular/core'; import {NgModule, NO_ERRORS_SCHEMA} from '@angular/core';
import {TargetCmp} from './target'; import {TargetCmp} from './target';
import {TestCmp} from './test'; import {TestCmp} from './test';
@NgModule({ @NgModule({
declarations: [TestCmp, TargetCmp], declarations: [TestCmp, TargetCmp],
schemas: [NO_ERRORS_SCHEMA],
}) })
export class Module {} export class Module {}
`); `);
@ -268,7 +269,7 @@ runInEachFileSystem(() => {
env.write('test.ts', ` env.write('test.ts', `
import {Component} from '@angular/core'; import {Component} from '@angular/core';
@Component({selector: 'test-cmp', template: '...'}) @Component({selector: 'test-cmp-fixed', template: '...'})
export class TestCmp {} export class TestCmp {}
`); `);
@ -283,7 +284,7 @@ runInEachFileSystem(() => {
'/module.js', '/module.js',
// Because TargetCmp also belongs to the same module, it should be re-emitted since // Because TargetCmp also belongs to the same module, it should be re-emitted since
// TestCmp's elector may have changed. // TestCmp's selector was changed.
'/target.js', '/target.js',
]); ]);
}); });
@ -329,7 +330,7 @@ runInEachFileSystem(() => {
env.write('a.ts', ` env.write('a.ts', `
import {Component} from '@angular/core'; import {Component} from '@angular/core';
@Component({selector: 'test-cmp', template: '...'}) @Component({selector: 'test-cmp', template: '<div dir></div>'})
export class CmpA {} export class CmpA {}
`); `);
env.write('b.ts', ` env.write('b.ts', `
@ -357,17 +358,16 @@ runInEachFileSystem(() => {
export class Module {} export class Module {}
`); `);
env.write('lib.ts', ` env.write('lib.ts', `
import {Component, NgModule} from '@angular/core'; import {Directive, NgModule} from '@angular/core';
@Component({ @Directive({
selector: 'lib-cmp', selector: '[dir]',
template: '...',
}) })
export class LibCmp {} export class LibDir {}
@NgModule({ @NgModule({
declarations: [LibCmp], declarations: [LibDir],
exports: [LibCmp], exports: [LibDir],
}) })
export class LibModule {} export class LibModule {}
`); `);
@ -378,17 +378,27 @@ runInEachFileSystem(() => {
// Introduce the error in LibModule // Introduce the error in LibModule
env.write('lib.ts', ` env.write('lib.ts', `
import {Component, NgModule} from '@angular/core'; import {Directive, NgModule} from '@angular/core';
@Component({ @Directive({
selector: 'lib-cmp', selector: '[dir]',
template: '...',
}) })
export class LibCmp {} export class LibDir {}
@Directive({
selector: '[dir]',
})
export class NewDir {}
@NgModule({ @NgModule({
declarations: [LibCmp], declarations: [NewDir],
exports: [LibCmp], })
export class NewModule {}
@NgModule({
declarations: [LibDir],
imports: [NewModule],
exports: [LibDir, NewModule],
}) })
export class LibModule // missing braces export class LibModule // missing braces
`); `);
@ -407,9 +417,13 @@ runInEachFileSystem(() => {
}) })
export class LibCmp {} export class LibCmp {}
@NgModule({})
export class NewModule {}
@NgModule({ @NgModule({
declarations: [LibCmp], declarations: [LibCmp],
exports: [LibCmp], imports: [NewModule],
exports: [LibCmp, NewModule],
}) })
export class LibModule {} export class LibModule {}
`); `);
@ -417,9 +431,10 @@ runInEachFileSystem(() => {
env.driveMain(); env.driveMain();
expectToHaveWritten([ expectToHaveWritten([
// Both CmpA and CmpB should be re-emitted. // CmpA should be re-emitted as `NewModule` was added since the successful emit, which added
// `NewDir` as a matching directive to CmpA. Alternatively, CmpB should not be re-emitted
// as it does not use the newly added directive.
'/a.js', '/a.js',
'/b.js',
// So should the module itself. // So should the module itself.
'/module.js', '/module.js',
@ -468,8 +483,7 @@ runInEachFileSystem(() => {
'/other.js', '/other.js',
'/a.js', '/a.js',
// Bcause they depend on a.ts // Because they depend on a.ts
'/b.js',
'/module.js', '/module.js',
]); ]);
}); });
@ -512,7 +526,10 @@ runInEachFileSystem(() => {
'/other.js', '/other.js',
// Because a.html changed // Because a.html changed
'/a.js', '/module.js', '/a.js',
// module.js should not be re-emitted, as it is not affected by the change and its remote
// scope is unaffected.
// b.js and module.js should not be re-emitted, because specifically when tracking // b.js and module.js should not be re-emitted, because specifically when tracking
// resource dependencies, the compiler knows that a change to a resource file only affects // resource dependencies, the compiler knows that a change to a resource file only affects

File diff suppressed because it is too large Load Diff

View File

@ -154,14 +154,20 @@ runInEachFileSystem(() => {
setupFooBarProgram(env); setupFooBarProgram(env);
// Pretend a change was made to BarDir. // Pretend a change was made to BarDir.
env.invalidateCachedFile('bar_directive.ts'); env.write('bar_directive.ts', `
import {Directive} from '@angular/core';
@Directive({selector: '[barr]'})
export class BarDir {}
`);
env.driveMain(); env.driveMain();
let written = env.getFilesWrittenSinceLastFlush(); let written = env.getFilesWrittenSinceLastFlush();
expect(written).toContain('/bar_directive.js'); expect(written).toContain('/bar_directive.js');
expect(written).toContain('/bar_component.js'); expect(written).toContain('/bar_component.js');
expect(written).toContain('/bar_module.js'); expect(written).toContain('/bar_module.js');
expect(written).toContain('/foo_component.js'); expect(written).not.toContain('/foo_component.js'); // BarDir is not exported by BarModule,
// so upstream NgModule is not affected
expect(written).not.toContain('/foo_pipe.js'); expect(written).not.toContain('/foo_pipe.js');
expect(written).not.toContain('/foo_module.js'); expect(written).not.toContain('/foo_module.js');
}); });
@ -178,7 +184,7 @@ runInEachFileSystem(() => {
env.write('component2.ts', ` env.write('component2.ts', `
import {Component} from '@angular/core'; import {Component} from '@angular/core';
@Component({selector: 'cmp2', template: 'cmp2'}) @Component({selector: 'cmp2', template: '<cmp></cmp>'})
export class Cmp2 {} export class Cmp2 {}
`); `);
env.write('dep.ts', ` env.write('dep.ts', `
@ -197,36 +203,43 @@ runInEachFileSystem(() => {
export class MyPipe {} export class MyPipe {}
`); `);
env.write('module.ts', ` env.write('module.ts', `
import {NgModule} from '@angular/core'; import {NgModule, NO_ERRORS_SCHEMA} from '@angular/core';
import {Cmp1} from './component1'; import {Cmp1} from './component1';
import {Cmp2} from './component2'; import {Cmp2} from './component2';
import {Dir} from './directive'; import {Dir} from './directive';
import {MyPipe} from './pipe'; import {MyPipe} from './pipe';
@NgModule({declarations: [Cmp1, Cmp2, Dir, MyPipe]}) @NgModule({declarations: [Cmp1, Cmp2, Dir, MyPipe], schemas: [NO_ERRORS_SCHEMA]})
export class Mod {} export class Mod {}
`); `);
env.driveMain(); env.driveMain();
// Pretend a change was made to 'dep'. Since this may affect the NgModule scope, like it does // Pretend a change was made to 'dep'. Since the selector is updated this affects the NgModule
// here if the selector is updated, all components in the module scope need to be recompiled. // scope, so all components in the module scope need to be recompiled.
env.flushWrittenFileTracking(); env.flushWrittenFileTracking();
env.invalidateCachedFile('dep.ts'); env.write('dep.ts', `
export const SELECTOR = 'cmp_updated';
`);
env.driveMain(); env.driveMain();
const written = env.getFilesWrittenSinceLastFlush(); const written = env.getFilesWrittenSinceLastFlush();
expect(written).not.toContain('/directive.js'); expect(written).not.toContain('/directive.js');
expect(written).not.toContain('/pipe.js'); expect(written).not.toContain('/pipe.js');
expect(written).not.toContain('/module.js');
expect(written).toContain('/component1.js'); expect(written).toContain('/component1.js');
expect(written).toContain('/component2.js'); expect(written).toContain('/component2.js');
expect(written).toContain('/dep.js'); expect(written).toContain('/dep.js');
expect(written).toContain('/module.js');
}); });
it('should rebuild components where their NgModule declared dependencies have changed', () => { it('should rebuild components where their NgModule declared dependencies have changed', () => {
setupFooBarProgram(env); setupFooBarProgram(env);
// Pretend a change was made to FooPipe. // Rename the pipe so components that use it need to be recompiled.
env.invalidateCachedFile('foo_pipe.ts'); env.write('foo_pipe.ts', `
import {Pipe} from '@angular/core';
@Pipe({name: 'foo_changed'})
export class FooPipe {}
`);
env.driveMain(); env.driveMain();
const written = env.getFilesWrittenSinceLastFlush(); const written = env.getFilesWrittenSinceLastFlush();
expect(written).not.toContain('/bar_directive.js'); expect(written).not.toContain('/bar_directive.js');
@ -240,15 +253,25 @@ runInEachFileSystem(() => {
it('should rebuild components where their NgModule has changed', () => { it('should rebuild components where their NgModule has changed', () => {
setupFooBarProgram(env); setupFooBarProgram(env);
// Pretend a change was made to FooPipe. // Pretend a change was made to FooModule.
env.invalidateCachedFile('foo_module.ts'); env.write('foo_module.ts', `
import {NgModule} from '@angular/core';
import {FooCmp} from './foo_component';
import {FooPipe} from './foo_pipe';
import {BarModule} from './bar_module';
@NgModule({
declarations: [FooCmp], // removed FooPipe
imports: [BarModule],
})
export class FooModule {}
`);
env.driveMain(); env.driveMain();
const written = env.getFilesWrittenSinceLastFlush(); const written = env.getFilesWrittenSinceLastFlush();
expect(written).not.toContain('/bar_directive.js'); expect(written).not.toContain('/bar_directive.js');
expect(written).not.toContain('/bar_component.js'); expect(written).not.toContain('/bar_component.js');
expect(written).not.toContain('/bar_module.js'); expect(written).not.toContain('/bar_module.js');
expect(written).not.toContain('/foo_pipe.js');
expect(written).toContain('/foo_component.js'); expect(written).toContain('/foo_component.js');
expect(written).toContain('/foo_pipe.js');
expect(written).toContain('/foo_module.js'); expect(written).toContain('/foo_module.js');
}); });
@ -396,7 +419,7 @@ runInEachFileSystem(() => {
expect(env.getContents('cmp.js')).not.toContain('DepDir'); expect(env.getContents('cmp.js')).not.toContain('DepDir');
}); });
it('should rebuild only a Component (but with the correct CompilationScope) and its module if its template has changed', it('should rebuild only a Component (but with the correct CompilationScope) if its template has changed',
() => { () => {
setupFooBarProgram(env); setupFooBarProgram(env);
@ -407,9 +430,7 @@ runInEachFileSystem(() => {
const written = env.getFilesWrittenSinceLastFlush(); const written = env.getFilesWrittenSinceLastFlush();
expect(written).not.toContain('/bar_directive.js'); expect(written).not.toContain('/bar_directive.js');
expect(written).toContain('/bar_component.js'); expect(written).toContain('/bar_component.js');
// /bar_module.js should also be re-emitted, because remote scoping of BarComponent might expect(written).not.toContain('/bar_module.js');
// have been affected.
expect(written).toContain('/bar_module.js');
expect(written).not.toContain('/foo_component.js'); expect(written).not.toContain('/foo_component.js');
expect(written).not.toContain('/foo_pipe.js'); expect(written).not.toContain('/foo_pipe.js');
expect(written).not.toContain('/foo_module.js'); expect(written).not.toContain('/foo_module.js');
@ -764,7 +785,10 @@ runInEachFileSystem(() => {
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {fooSelector} from './foo_selector'; import {fooSelector} from './foo_selector';
@Component({selector: fooSelector, template: 'foo'}) @Component({
selector: fooSelector,
template: '{{ 1 | foo }}'
})
export class FooCmp {} export class FooCmp {}
`); `);
env.write('foo_pipe.ts', ` env.write('foo_pipe.ts', `
@ -796,14 +820,21 @@ runInEachFileSystem(() => {
@Directive({selector: '[bar]'}) @Directive({selector: '[bar]'})
export class BarDir {} export class BarDir {}
`);
env.write('bar_pipe.ts', `
import {Pipe} from '@angular/core';
@Pipe({name: 'foo'})
export class BarPipe {}
`); `);
env.write('bar_module.ts', ` env.write('bar_module.ts', `
import {NgModule} from '@angular/core'; import {NgModule} from '@angular/core';
import {BarCmp} from './bar_component'; import {BarCmp} from './bar_component';
import {BarDir} from './bar_directive'; import {BarDir} from './bar_directive';
import {BarPipe} from './bar_pipe';
@NgModule({ @NgModule({
declarations: [BarCmp, BarDir], declarations: [BarCmp, BarDir, BarPipe],
exports: [BarCmp], exports: [BarCmp, BarPipe],
}) })
export class BarModule {} export class BarModule {}
`); `);

File diff suppressed because it is too large Load Diff