Skip to content

Commit d6b990d

Browse files
alxhubprofanis
authored andcommitted
refactor(compiler-cli): efficient single-file type checking diagnostics (angular#38105)
Previously, the `TemplateTypeChecker` abstraction allowed fetching diagnostics for a single file, but under the hood would generate type checking code for the entire program to satisfy the request. With this commit, an `OptimizeFor` hint is passed to `getDiagnosticsForFile` which indicates whether the user intends to request diagnostics for the whole program or is truly interested in just the single file. If the latter, the `TemplateTypeChecker` can perform only the work needed to produce diagnostics for just that file, thus returning answers more efficiently. PR Close angular#38105
1 parent 63f5167 commit d6b990d

File tree

6 files changed

+241
-18
lines changed

6 files changed

+241
-18
lines changed

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {generatedFactoryTransform} from '../../shims';
2929
import {ivySwitchTransform} from '../../switch';
3030
import {aliasTransformFactory, declarationTransformFactory, DecoratorHandler, DtsTransformRegistry, ivyTransformFactory, TraitCompiler} from '../../transform';
3131
import {isTemplateDiagnostic, TemplateTypeCheckerImpl} from '../../typecheck';
32-
import {TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy} from '../../typecheck/api';
32+
import {OptimizeFor, TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy} from '../../typecheck/api';
3333
import {getSourceFileOrNull, isDtsPath, resolveModuleName} from '../../util/src/typescript';
3434
import {LazyRoute, NgCompilerAdapter, NgCompilerOptions} from '../api';
3535

@@ -507,7 +507,8 @@ export class NgCompiler {
507507
continue;
508508
}
509509

510-
diagnostics.push(...compilation.templateTypeChecker.getDiagnosticsForFile(sf));
510+
diagnostics.push(
511+
...compilation.templateTypeChecker.getDiagnosticsForFile(sf, OptimizeFor.WholeProgram));
511512
}
512513

513514
const program = this.typeCheckingProgramStrategy.getProgram();

packages/compiler-cli/src/ngtsc/typecheck/api/checker.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,16 @@ export interface TemplateTypeChecker {
2929
*
3030
* This method will fail (throw) if there are components within the `ts.SourceFile` that do not
3131
* have TCBs available.
32+
*
33+
* Generating a template type-checking program is expensive, and in some workflows (e.g. checking
34+
* an entire program before emit), it should ideally only be done once. The `optimizeFor` flag
35+
* allows the caller to hint to `getDiagnosticsForFile` (which internally will create a template
36+
* type-checking program if needed) whether the caller is interested in just the results of the
37+
* single file, or whether they plan to query about other files in the program. Based on this
38+
* flag, `getDiagnosticsForFile` will determine how much of the user's program to prepare for
39+
* checking as part of the template type-checking program it creates.
3240
*/
33-
getDiagnosticsForFile(sf: ts.SourceFile): ts.Diagnostic[];
41+
getDiagnosticsForFile(sf: ts.SourceFile, optimizeFor: OptimizeFor): ts.Diagnostic[];
3442

3543
/**
3644
* Retrieve the top-level node representing the TCB for the given component.
@@ -41,3 +49,27 @@ export interface TemplateTypeChecker {
4149
*/
4250
getTypeCheckBlock(component: ts.ClassDeclaration): ts.Node|null;
4351
}
52+
53+
/**
54+
* Describes the scope of the caller's interest in template type-checking results.
55+
*/
56+
export enum OptimizeFor {
57+
/**
58+
* Indicates that a consumer of a `TemplateTypeChecker` is only interested in results for a given
59+
* file, and wants them as fast as possible.
60+
*
61+
* Calling `TemplateTypeChecker` methods successively for multiple files while specifying
62+
* `OptimizeFor.SingleFile` can result in significant unnecessary overhead overall.
63+
*/
64+
SingleFile,
65+
66+
/**
67+
* Indicates that a consumer of a `TemplateTypeChecker` intends to query for results pertaining to
68+
* the entire user program, and so the type-checker should internally optimize for this case.
69+
*
70+
* Initial calls to retrieve type-checking information may take longer, but repeated calls to
71+
* gather information for the whole user program will be significantly faster with this mode of
72+
* optimization.
73+
*/
74+
WholeProgram,
75+
}

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

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {IncrementalBuild} from '../../incremental/api';
1414
import {ReflectionHost} from '../../reflection';
1515
import {isShim} from '../../shims';
1616
import {getSourceFileOrNull} from '../../util/src/typescript';
17-
import {ProgramTypeCheckAdapter, TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy, UpdateMode} from '../api';
17+
import {OptimizeFor, ProgramTypeCheckAdapter, TemplateTypeChecker, TypeCheckingConfig, TypeCheckingProgramStrategy, UpdateMode} from '../api';
1818

1919
import {InliningMode, ShimTypeCheckingData, TypeCheckContextImpl, TypeCheckingHost} from './context';
2020
import {findTypeCheckBlock, shouldReportDiagnostic, TemplateSourceResolver, translateDiagnostic} from './diagnostics';
@@ -41,8 +41,15 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
4141
* Retrieve type-checking diagnostics from the given `ts.SourceFile` using the most recent
4242
* type-checking program.
4343
*/
44-
getDiagnosticsForFile(sf: ts.SourceFile): ts.Diagnostic[] {
45-
this.ensureAllShimsForAllFiles();
44+
getDiagnosticsForFile(sf: ts.SourceFile, optimizeFor: OptimizeFor): ts.Diagnostic[] {
45+
switch (optimizeFor) {
46+
case OptimizeFor.WholeProgram:
47+
this.ensureAllShimsForAllFiles();
48+
break;
49+
case OptimizeFor.SingleFile:
50+
this.ensureAllShimsForOneFile(sf);
51+
break;
52+
}
4653

4754
const sfPath = absoluteFromSourceFile(sf);
4855
const fileRecord = this.state.get(sfPath)!;
@@ -68,7 +75,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
6875
}
6976

7077
getTypeCheckBlock(component: ts.ClassDeclaration): ts.Node|null {
71-
this.ensureAllShimsForAllFiles();
78+
this.ensureAllShimsForOneFile(component.getSourceFile());
7279

7380
const program = this.typeCheckingStrategy.getProgram();
7481
const filePath = absoluteFromSourceFile(component.getSourceFile());
@@ -81,7 +88,7 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
8188
const id = fileRecord.sourceManager.getTemplateId(component);
8289

8390
const shimSf = getSourceFileOrNull(program, shimPath);
84-
if (shimSf === null) {
91+
if (shimSf === null || !fileRecord.shimData.has(shimPath)) {
8592
throw new Error(`Error: no shim file in program: ${shimPath}`);
8693
}
8794

@@ -144,6 +151,27 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
144151
this.isComplete = true;
145152
}
146153

154+
private ensureAllShimsForOneFile(sf: ts.SourceFile): void {
155+
this.maybeAdoptPriorResultsForFile(sf);
156+
157+
const sfPath = absoluteFromSourceFile(sf);
158+
159+
const fileData = this.getFileData(sfPath);
160+
if (fileData.isComplete) {
161+
// All data for this file is present and accounted for already.
162+
return;
163+
}
164+
165+
const host = new SingleFileTypeCheckingHost(sfPath, fileData, this.typeCheckingStrategy, this);
166+
const ctx = this.newContext(host);
167+
168+
this.typeCheckAdapter.typeCheck(sf, ctx);
169+
170+
fileData.isComplete = true;
171+
172+
this.updateFromContext(ctx);
173+
}
174+
147175
private newContext(host: TypeCheckingHost): TypeCheckContextImpl {
148176
const inlining = this.typeCheckingStrategy.supportsInlineOperations ? InliningMode.InlineOps :
149177
InliningMode.Error;
@@ -152,6 +180,30 @@ export class TemplateTypeCheckerImpl implements TemplateTypeChecker {
152180
host, inlining);
153181
}
154182

183+
/**
184+
* Remove any shim data that depends on inline operations applied to the type-checking program.
185+
*
186+
* This can be useful if new inlines need to be applied, and it's not possible to guarantee that
187+
* they won't overwrite or corrupt existing inlines that are used by such shims.
188+
*/
189+
clearAllShimDataUsingInlines(): void {
190+
for (const fileData of this.state.values()) {
191+
if (!fileData.hasInlines) {
192+
continue;
193+
}
194+
195+
for (const [shimFile, shimData] of fileData.shimData.entries()) {
196+
if (shimData.hasInlines) {
197+
fileData.shimData.delete(shimFile);
198+
}
199+
}
200+
201+
fileData.hasInlines = false;
202+
fileData.isComplete = false;
203+
this.isComplete = false;
204+
}
205+
}
206+
155207
private updateFromContext(ctx: TypeCheckContextImpl): void {
156208
const updates = ctx.finalize();
157209
this.typeCheckingStrategy.updateFiles(updates, UpdateMode.Incremental);
@@ -240,3 +292,61 @@ class WholeProgramTypeCheckingHost implements TypeCheckingHost {
240292
this.impl.getFileData(sfPath).isComplete = true;
241293
}
242294
}
295+
296+
/**
297+
* Drives a `TypeCheckContext` to generate type-checking code efficiently for a single input file.
298+
*/
299+
class SingleFileTypeCheckingHost implements TypeCheckingHost {
300+
private seenInlines = false;
301+
302+
constructor(
303+
private sfPath: AbsoluteFsPath, private fileData: FileTypeCheckingData,
304+
private strategy: TypeCheckingProgramStrategy, private impl: TemplateTypeCheckerImpl) {}
305+
306+
private assertPath(sfPath: AbsoluteFsPath): void {
307+
if (this.sfPath !== sfPath) {
308+
throw new Error(`AssertionError: querying TypeCheckingHost outside of assigned file`);
309+
}
310+
}
311+
312+
getSourceManager(sfPath: AbsoluteFsPath): TemplateSourceManager {
313+
this.assertPath(sfPath);
314+
return this.fileData.sourceManager;
315+
}
316+
317+
shouldCheckComponent(node: ts.ClassDeclaration): boolean {
318+
if (this.sfPath !== absoluteFromSourceFile(node.getSourceFile())) {
319+
return false;
320+
}
321+
const shimPath = this.strategy.shimPathForComponent(node);
322+
323+
// Only need to generate a TCB for the class if no shim exists for it currently.
324+
return !this.fileData.shimData.has(shimPath);
325+
}
326+
327+
recordShimData(sfPath: AbsoluteFsPath, data: ShimTypeCheckingData): void {
328+
this.assertPath(sfPath);
329+
330+
// Previous type-checking state may have required the use of inlines (assuming they were
331+
// supported). If the current operation also requires inlines, this presents a problem:
332+
// generating new inlines may invalidate any old inlines that old state depends on.
333+
//
334+
// Rather than resolve this issue by tracking specific dependencies on inlines, if the new state
335+
// relies on inlines, any old state that relied on them is simply cleared. This happens when the
336+
// first new state that uses inlines is encountered.
337+
if (data.hasInlines && !this.seenInlines) {
338+
this.impl.clearAllShimDataUsingInlines();
339+
this.seenInlines = true;
340+
}
341+
342+
this.fileData.shimData.set(data.path, data);
343+
if (data.hasInlines) {
344+
this.fileData.hasInlines = true;
345+
}
346+
}
347+
348+
recordComplete(sfPath: AbsoluteFsPath): void {
349+
this.assertPath(sfPath);
350+
this.fileData.isComplete = true;
351+
}
352+
}

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

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

1111
import {absoluteFrom, getSourceFileOrError} from '../../file_system';
1212
import {runInEachFileSystem, TestFile} from '../../file_system/testing';
13-
import {TypeCheckingConfig} from '../api';
13+
import {OptimizeFor, TypeCheckingConfig} from '../api';
1414

1515
import {ngForDeclaration, ngForDts, setup, TestDeclaration} from './test_utils';
1616

@@ -472,7 +472,7 @@ function diagnose(
472472
],
473473
{config, options});
474474
const sf = getSourceFileOrError(program, sfPath);
475-
const diagnostics = templateTypeChecker.getDiagnosticsForFile(sf);
475+
const diagnostics = templateTypeChecker.getDiagnosticsForFile(sf, OptimizeFor.WholeProgram);
476476
return diagnostics.map(diag => {
477477
const text =
478478
typeof diag.messageText === 'string' ? diag.messageText : diag.messageText.messageText;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {absoluteFrom, AbsoluteFsPath, getSourceFileOrError} from '../../file_sys
1212
import {runInEachFileSystem} from '../../file_system/testing';
1313
import {sfExtensionData, ShimReferenceTagger} from '../../shims';
1414
import {expectCompleteReuse, makeProgram} from '../../testing';
15-
import {UpdateMode} from '../api';
15+
import {OptimizeFor, UpdateMode} from '../api';
1616
import {ReusedProgramStrategy} from '../src/augmented_program';
1717

1818
import {setup} from './test_utils';
@@ -28,7 +28,7 @@ runInEachFileSystem(() => {
2828
}]);
2929
const sf = getSourceFileOrError(program, fileName);
3030

31-
templateTypeChecker.getDiagnosticsForFile(sf);
31+
templateTypeChecker.getDiagnosticsForFile(sf, OptimizeFor.WholeProgram);
3232
// expect() here would create a really long error message, so this is checked manually.
3333
if (programStrategy.getProgram() !== program) {
3434
fail('Template type-checking created a new ts.Program even though it had no changes.');

0 commit comments

Comments
 (0)