Skip to content

Commit 30c82cd

Browse files
JoostKatscott
authored andcommitted
fix(compiler-cli): inline type checking instructions no longer prevent incremental reuse (#42759)
Source files that contain directives or components that need an inline type constructor or inline template type-check block would always be considered as affected in incremental rebuilds. The inline operations cause the source file to be updated in the TypeScript program that is created for template type-checking, which becomes the reuse program in a subsequent incremental rebuild. In an incremental rebuild, the source files from the new user program are compared to those from the reuse program. The updated source files are not the same as the original source file from the user program, so the incremental engine would mark the file which needed inline operations as affected. This prevents incremental reuse for these files, causing sub-optimal rebuild performance. This commit attaches the original source file for source files that have been updated with inline operations, such that the incremental engine is able to compare source files using the original source file. Fixes #42543 PR Close #42759
1 parent 95ba5b4 commit 30c82cd

File tree

9 files changed

+140
-20
lines changed

9 files changed

+140
-20
lines changed

packages/compiler-cli/src/ngtsc/core/src/compiler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {generateAnalysis, IndexedComponent, IndexingContext} from '../../indexer
2020
import {ComponentResources, CompoundMetadataReader, CompoundMetadataRegistry, DirectiveMeta, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, MetadataReader, PipeMeta, ResourceRegistry} from '../../metadata';
2121
import {PartialEvaluator} from '../../partial_evaluator';
2222
import {ActivePerfRecorder, DelegatingPerfRecorder, PerfCheckpoint, PerfEvent, PerfPhase} from '../../perf';
23-
import {ProgramDriver, UpdateMode} from '../../program_driver';
23+
import {FileUpdate, ProgramDriver, UpdateMode} from '../../program_driver';
2424
import {DeclarationNode, isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
2525
import {AdapterResourceLoader} from '../../resource';
2626
import {entryPointKeyFor, NgModuleRouteAnalyzer} from '../../routing';
@@ -1180,7 +1180,7 @@ class NotifyingProgramDriverWrapper implements ProgramDriver {
11801180
return this.delegate.getProgram();
11811181
}
11821182

1183-
updateFiles(contents: Map<AbsoluteFsPath, string>, updateMode: UpdateMode): void {
1183+
updateFiles(contents: Map<AbsoluteFsPath, FileUpdate>, updateMode: UpdateMode): void {
11841184
this.delegate.updateFiles(contents, updateMode);
11851185
this.notifyNewProgram(this.delegate.getProgram());
11861186
}

packages/compiler-cli/src/ngtsc/incremental/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ ts_library(
1515
"//packages/compiler-cli/src/ngtsc/metadata",
1616
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
1717
"//packages/compiler-cli/src/ngtsc/perf",
18+
"//packages/compiler-cli/src/ngtsc/program_driver",
1819
"//packages/compiler-cli/src/ngtsc/reflection",
1920
"//packages/compiler-cli/src/ngtsc/scope",
2021
"//packages/compiler-cli/src/ngtsc/transform",

packages/compiler-cli/src/ngtsc/incremental/src/incremental.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ import * as ts from 'typescript';
1010

1111
import {absoluteFromSourceFile, AbsoluteFsPath, resolve} from '../../file_system';
1212
import {PerfPhase, PerfRecorder} from '../../perf';
13+
import {MaybeSourceFileWithOriginalFile, NgOriginalFile} from '../../program_driver';
1314
import {ClassRecord, TraitCompiler} from '../../transform';
1415
import {FileTypeCheckingData} from '../../typecheck';
1516
import {toUnredirectedSourceFile} from '../../util/src/typescript';
1617
import {IncrementalBuild} from '../api';
1718
import {SemanticDepGraphUpdater} from '../semantic_graph';
19+
1820
import {FileDependencyGraph} from './dependency_tracking';
1921
import {AnalyzedIncrementalState, DeltaIncrementalState, IncrementalState, IncrementalStateKind} from './state';
2022

@@ -134,12 +136,12 @@ export class IncrementalCompilation implements IncrementalBuild<ClassRecord, Fil
134136

135137
const oldVersions = priorAnalysis.versions;
136138

137-
const oldFilesArray = oldProgram.getSourceFiles().map(sf => toUnredirectedSourceFile(sf));
139+
const oldFilesArray = oldProgram.getSourceFiles().map(toOriginalSourceFile);
138140
const oldFiles = new Set(oldFilesArray);
139141
const deletedTsFiles = new Set(oldFilesArray.map(sf => absoluteFromSourceFile(sf)));
140142

141143
for (const possiblyRedirectedNewFile of program.getSourceFiles()) {
142-
const sf = toUnredirectedSourceFile(possiblyRedirectedNewFile);
144+
const sf = toOriginalSourceFile(possiblyRedirectedNewFile);
143145
const sfPath = absoluteFromSourceFile(sf);
144146
// Since we're seeing a file in the incoming program with this name, it can't have been
145147
// deleted.
@@ -373,3 +375,28 @@ export class IncrementalCompilation implements IncrementalBuild<ClassRecord, Fil
373375
return this.step.priorState.emitted.has(sfPath);
374376
}
375377
}
378+
379+
/**
380+
* To accurately detect whether a source file was affected during an incremental rebuild, the
381+
* "original" source file needs to be consistently used.
382+
*
383+
* First, TypeScript may have created source file redirects when declaration files of the same
384+
* version of a library are included multiple times. The non-redirected source file should be used
385+
* to detect changes, as otherwise the redirected source files cause a mismatch when compared to
386+
* a prior program.
387+
*
388+
* Second, the program that is used for template type checking may contain mutated source files, if
389+
* inline type constructors or inline template type-check blocks had to be used. Such source files
390+
* store their original, non-mutated source file from the original program in a symbol. For
391+
* computing the affected files in an incremental build this original source file should be used, as
392+
* the mutated source file would always be considered affected.
393+
*/
394+
function toOriginalSourceFile(sf: ts.SourceFile): ts.SourceFile {
395+
const unredirectedSf = toUnredirectedSourceFile(sf);
396+
const originalFile = (unredirectedSf as MaybeSourceFileWithOriginalFile)[NgOriginalFile];
397+
if (originalFile !== undefined) {
398+
return originalFile;
399+
} else {
400+
return unredirectedSf;
401+
}
402+
}

packages/compiler-cli/src/ngtsc/program_driver/src/api.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,29 @@
99
import * as ts from 'typescript';
1010
import {AbsoluteFsPath} from '../../file_system';
1111

12+
export interface FileUpdate {
13+
/**
14+
* The source file text.
15+
*/
16+
newText: string;
17+
18+
/**
19+
* Represents the source file from the original program that is being updated. If the file update
20+
* targets a shim file then this is null, as shim files do not have an associated original file.
21+
*/
22+
originalFile: ts.SourceFile|null;
23+
}
24+
25+
export const NgOriginalFile = Symbol('NgOriginalFile');
26+
27+
/**
28+
* If an updated file has an associated original source file, then the original source file
29+
* is attached to the updated file using the `NgOriginalFile` symbol.
30+
*/
31+
export interface MaybeSourceFileWithOriginalFile extends ts.SourceFile {
32+
[NgOriginalFile]?: ts.SourceFile;
33+
}
34+
1235
export interface ProgramDriver {
1336
/**
1437
* Whether this strategy supports modifying user files (inline modifications) in addition to
@@ -25,7 +48,7 @@ export interface ProgramDriver {
2548
* Incorporate a set of changes to either augment or completely replace the type-checking code
2649
* included in the type-checking program.
2750
*/
28-
updateFiles(contents: Map<AbsoluteFsPath, string>, updateMode: UpdateMode): void;
51+
updateFiles(contents: Map<AbsoluteFsPath, FileUpdate>, updateMode: UpdateMode): void;
2952

3053
/**
3154
* Retrieve a string version for a given `ts.SourceFile`, which much change when the contents of

packages/compiler-cli/src/ngtsc/program_driver/src/ts_create_program_driver.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {AbsoluteFsPath} from '../../file_system';
1212
import {copyFileShimData, retagAllTsFiles, ShimReferenceTagger, untagAllTsFiles} from '../../shims';
1313
import {RequiredDelegations, toUnredirectedSourceFile} from '../../util/src/typescript';
1414

15-
import {ProgramDriver, UpdateMode} from './api';
15+
import {FileUpdate, MaybeSourceFileWithOriginalFile, NgOriginalFile, ProgramDriver, UpdateMode} from './api';
1616

1717
/**
1818
* Delegates all methods of `ts.CompilerHost` to a delegate, with the exception of
@@ -153,7 +153,7 @@ export class TsCreateProgramDriver implements ProgramDriver {
153153
return this.program;
154154
}
155155

156-
updateFiles(contents: Map<AbsoluteFsPath, string>, updateMode: UpdateMode): void {
156+
updateFiles(contents: Map<AbsoluteFsPath, FileUpdate>, updateMode: UpdateMode): void {
157157
if (contents.size === 0) {
158158
// No changes have been requested. Is it safe to skip updating entirely?
159159
// If UpdateMode is Incremental, then yes. If UpdateMode is Complete, then it's safe to skip
@@ -169,8 +169,12 @@ export class TsCreateProgramDriver implements ProgramDriver {
169169
this.sfMap.clear();
170170
}
171171

172-
for (const [filePath, text] of contents.entries()) {
173-
this.sfMap.set(filePath, ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true));
172+
for (const [filePath, {newText, originalFile}] of contents.entries()) {
173+
const sf = ts.createSourceFile(filePath, newText, ts.ScriptTarget.Latest, true);
174+
if (originalFile !== null) {
175+
(sf as MaybeSourceFileWithOriginalFile)[NgOriginalFile] = originalFile;
176+
}
177+
this.sfMap.set(filePath, sf);
174178
}
175179

176180
const host = new UpdatedProgramHost(

packages/compiler-cli/src/ngtsc/typecheck/src/context.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import * as ts from 'typescript';
1313
import {absoluteFromSourceFile, AbsoluteFsPath} from '../../file_system';
1414
import {NoopImportRewriter, Reference, ReferenceEmitter} from '../../imports';
1515
import {PerfEvent, PerfRecorder} from '../../perf';
16+
import {FileUpdate} from '../../program_driver';
1617
import {ClassDeclaration, ReflectionHost} from '../../reflection';
1718
import {ImportManager} from '../../translator';
1819
import {TemplateId, TemplateSourceMapping, TypeCheckableDirectiveMeta, TypeCheckBlockMetadata, TypeCheckContext, TypeCheckingConfig, TypeCtorMetadata} from '../api';
@@ -376,13 +377,16 @@ export class TypeCheckContextImpl implements TypeCheckContext {
376377
return code;
377378
}
378379

379-
finalize(): Map<AbsoluteFsPath, string> {
380+
finalize(): Map<AbsoluteFsPath, FileUpdate> {
380381
// First, build the map of updates to source files.
381-
const updates = new Map<AbsoluteFsPath, string>();
382+
const updates = new Map<AbsoluteFsPath, FileUpdate>();
382383
for (const originalSf of this.opMap.keys()) {
383384
const newText = this.transform(originalSf);
384385
if (newText !== null) {
385-
updates.set(absoluteFromSourceFile(originalSf), newText);
386+
updates.set(absoluteFromSourceFile(originalSf), {
387+
newText,
388+
originalFile: originalSf,
389+
});
386390
}
387391
}
388392

@@ -399,8 +403,13 @@ export class TypeCheckContextImpl implements TypeCheckContext {
399403
path: pendingShimData.file.fileName,
400404
templates: pendingShimData.templates,
401405
});
402-
updates.set(
403-
pendingShimData.file.fileName, pendingShimData.file.render(false /* removeComments */));
406+
const sfText = pendingShimData.file.render(false /* removeComments */);
407+
updates.set(pendingShimData.file.fileName, {
408+
newText: sfText,
409+
410+
// Shim files do not have an associated original file.
411+
originalFile: null,
412+
});
404413
}
405414
}
406415

packages/compiler-cli/src/ngtsc/typecheck/test/program_spec.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import * as ts from 'typescript';
1010

1111
import {absoluteFrom, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
1212
import {runInEachFileSystem} from '../../file_system/testing';
13-
import {TsCreateProgramDriver, UpdateMode} from '../../program_driver';
13+
import {FileUpdate, TsCreateProgramDriver, UpdateMode} from '../../program_driver';
1414
import {sfExtensionData, ShimReferenceTagger} from '../../shims';
1515
import {expectCompleteReuse, makeProgram} from '../../testing';
1616
import {OptimizeFor} from '../api';
@@ -42,7 +42,8 @@ runInEachFileSystem(() => {
4242
// Update /main.ngtypecheck.ts without changing its shape. Verify that the old program was
4343
// reused completely.
4444
programStrategy.updateFiles(
45-
new Map([[typecheckPath, 'export const VERSION = 2;']]), UpdateMode.Complete);
45+
new Map([[typecheckPath, createUpdate('export const VERSION = 2;')]]),
46+
UpdateMode.Complete);
4647

4748
expectCompleteReuse(programStrategy.getProgram());
4849
});
@@ -54,13 +55,21 @@ runInEachFileSystem(() => {
5455
// Update /main.ts without changing its shape. Verify that the old program was reused
5556
// completely.
5657
programStrategy.updateFiles(
57-
new Map([[mainPath, 'export const STILL_NOT_A_COMPONENT = true;']]), UpdateMode.Complete);
58+
new Map([[mainPath, createUpdate('export const STILL_NOT_A_COMPONENT = true;')]]),
59+
UpdateMode.Complete);
5860

5961
expectCompleteReuse(programStrategy.getProgram());
6062
});
6163
});
6264
});
6365

66+
function createUpdate(text: string): FileUpdate {
67+
return {
68+
newText: text,
69+
originalFile: null,
70+
};
71+
}
72+
6473
function makeSingleFileProgramWithTypecheckShim(): {
6574
program: ts.Program,
6675
host: ts.CompilerHost,

packages/compiler-cli/test/ngtsc/incremental_spec.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,53 @@ runInEachFileSystem(() => {
895895
expect(diags[0].messageText)
896896
.toContain(`Type '"gamma"' is not assignable to type 'keyof Keys'.`);
897897
});
898+
899+
it('should not re-emit files that need inline type constructors', () => {
900+
// Setup a directive that requires an inline type constructor, as it has a generic type
901+
// parameter that refer an interface that has not been exported. The inline operation
902+
// causes an updated dir.ts to be used in the type-check program, which should not
903+
// confuse the incremental engine in undesirably considering dir.ts as affected in
904+
// incremental rebuilds.
905+
env.write('dir.ts', `
906+
import {Directive, Input} from '@angular/core';
907+
interface Keys {
908+
alpha: string;
909+
beta: string;
910+
}
911+
@Directive({
912+
selector: '[dir]'
913+
})
914+
export class Dir<T extends keyof Keys> {
915+
@Input() dir: T;
916+
}
917+
`);
918+
919+
env.write('cmp.ts', `
920+
import {Component, NgModule} from '@angular/core';
921+
import {Dir} from './dir';
922+
@Component({
923+
selector: 'test-cmp',
924+
template: '<div dir="alpha"></div>',
925+
})
926+
export class Cmp {}
927+
@NgModule({
928+
declarations: [Cmp, Dir],
929+
})
930+
export class Module {}
931+
`);
932+
env.driveMain();
933+
934+
// Trigger a recompile by changing cmp.ts.
935+
env.invalidateCachedFile('cmp.ts');
936+
937+
env.flushWrittenFileTracking();
938+
env.driveMain();
939+
940+
// Verify that only cmp.ts was emitted, but not dir.ts as it was not affected.
941+
const written = env.getFilesWrittenSinceLastFlush();
942+
expect(written).toContain('/cmp.js');
943+
expect(written).not.toContain('/dir.js');
944+
});
898945
});
899946
});
900947
});

packages/language-service/ivy/language_service.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
1212
import {ErrorCode, ngErrorCode} from '@angular/compiler-cli/src/ngtsc/diagnostics';
1313
import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
1414
import {PerfPhase} from '@angular/compiler-cli/src/ngtsc/perf';
15-
import {ProgramDriver} from '@angular/compiler-cli/src/ngtsc/program_driver';
15+
import {FileUpdate, ProgramDriver} from '@angular/compiler-cli/src/ngtsc/program_driver';
1616
import {isNamedClassDeclaration} from '@angular/compiler-cli/src/ngtsc/reflection';
1717
import {TypeCheckShimGenerator} from '@angular/compiler-cli/src/ngtsc/typecheck';
1818
import {OptimizeFor} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
@@ -483,8 +483,8 @@ function createProgramDriver(project: ts.server.Project): ProgramDriver {
483483
}
484484
return program;
485485
},
486-
updateFiles(contents: Map<AbsoluteFsPath, string>) {
487-
for (const [fileName, newText] of contents) {
486+
updateFiles(contents: Map<AbsoluteFsPath, FileUpdate>) {
487+
for (const [fileName, {newText}] of contents) {
488488
const scriptInfo = getOrCreateTypeCheckScriptInfo(project, fileName);
489489
const snapshot = scriptInfo.getSnapshot();
490490
const length = snapshot.getLength();

0 commit comments

Comments
 (0)