Skip to content

Commit 178cc51

Browse files
alxhubmhevery
authored andcommitted
fix(compiler-cli): track poisoned scopes with a flag (#39967)
To avoid overwhelming a user with secondary diagnostics that derive from a "root cause" error, the compiler has the notion of a "poisoned" NgModule. An NgModule becomes poisoned when its declaration contains semantic errors: declarations which are not components or pipes, imports which are not other NgModules, etc. An NgModule also becomes poisoned if it imports or exports another poisoned NgModule. Previously, the compiler tracked this poisoned status as an alternate state for each scope. Either a correct scope could be produced, or the entire scope would be set to a sentinel error value. This meant that the compiler would not track any information about a scope that was determined to be in error. This method presents several issues: 1. The compiler is unable to support the language service and return results when a component or its module scope is poisoned. This is fine for compilation, since diagnostics will be produced showing the error(s), but the language service needs to still work for incorrect code. 2. `getComponentScopes()` does not return components with a poisoned scope, which interferes with resource tracking of incremental builds. If the component isn't included in that list, then the NgModule for it will not have its dependencies properly tracked, and this can cause future incremental build steps to produce incorrect results. This commit changes the tracking of poisoned module scopes to use a flag on the scope itself, rather than a sentinel value that replaces the scope. This means that the scope itself will still be tracked, even if it contains semantic errors. PR Close #39967
1 parent 0aa35ec commit 178cc51

File tree

21 files changed

+156
-92
lines changed

21 files changed

+156
-92
lines changed

Diff for: packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts

+1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export class DecorationAnalyzer {
9797
this.scopeRegistry, this.scopeRegistry, new TemplateMapping(), this.isCore,
9898
this.resourceManager, this.rootDirs, !!this.compilerOptions.preserveWhitespaces,
9999
/* i18nUseExternalIds */ true, this.bundle.enableI18nLegacyMessageIdFormat,
100+
/* usePoisonedData */ false,
100101
/* i18nNormalizeLineEndingsInICUs */ false, this.moduleResolver, this.cycleAnalyzer,
101102
this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER, NOOP_DEPENDENCY_TRACKER,
102103
this.injectableRegistry, !!this.compilerOptions.annotateForClosureCompiler),

Diff for: packages/compiler-cli/src/ngtsc/annotations/src/component.ts

+25-9
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ export interface ComponentAnalysisData {
7070
* require an Angular factory definition at runtime.
7171
*/
7272
viewProvidersRequiringFactory: Set<Reference<ClassDeclaration>>|null;
73+
74+
isPoisoned: boolean;
7375
}
7476

7577
export type ComponentResolutionData = Pick<R3ComponentMetadata, ComponentMetadataResolvedFields>;
@@ -86,7 +88,7 @@ export class ComponentDecoratorHandler implements
8688
private templateMapping: TemplateMapping, private isCore: boolean,
8789
private resourceLoader: ResourceLoader, private rootDirs: ReadonlyArray<string>,
8890
private defaultPreserveWhitespaces: boolean, private i18nUseExternalIds: boolean,
89-
private enableI18nLegacyMessageIdFormat: boolean,
91+
private enableI18nLegacyMessageIdFormat: boolean, private usePoisonedData: boolean,
9092
private i18nNormalizeLineEndingsInICUs: boolean|undefined,
9193
private moduleResolver: ModuleResolver, private cycleAnalyzer: CycleAnalyzer,
9294
private refEmitter: ReferenceEmitter, private defaultImportRecorder: DefaultImportRecorder,
@@ -359,6 +361,7 @@ export class ComponentDecoratorHandler implements
359361
template,
360362
providersRequiringFactory,
361363
viewProvidersRequiringFactory,
364+
isPoisoned: diagnostics !== undefined && diagnostics.length > 0,
362365
},
363366
diagnostics,
364367
};
@@ -383,6 +386,7 @@ export class ComponentDecoratorHandler implements
383386
isComponent: true,
384387
baseClass: analysis.baseClass,
385388
...analysis.typeCheckMeta,
389+
isPoisoned: analysis.isPoisoned,
386390
});
387391

388392
if (!analysis.template.isInline) {
@@ -394,15 +398,19 @@ export class ComponentDecoratorHandler implements
394398

395399
index(
396400
context: IndexingContext, node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>) {
401+
if (analysis.isPoisoned && !this.usePoisonedData) {
402+
return null;
403+
}
397404
const scope = this.scopeReader.getScopeForComponent(node);
398405
const selector = analysis.meta.selector;
399406
const matcher = new SelectorMatcher<DirectiveMeta>();
400-
if (scope === 'error') {
401-
// Don't bother indexing components which had erroneous scopes.
402-
return null;
403-
}
404-
405407
if (scope !== null) {
408+
if ((scope.compilation.isPoisoned || scope.exported.isPoisoned) && !this.usePoisonedData) {
409+
// Don't bother indexing components which had erroneous scopes, unless specifically
410+
// requested.
411+
return null;
412+
}
413+
406414
for (const directive of scope.compilation.directives) {
407415
if (directive.selector !== null) {
408416
matcher.addSelectables(CssSelector.parse(directive.selector), directive);
@@ -429,9 +437,13 @@ export class ComponentDecoratorHandler implements
429437
return;
430438
}
431439

440+
if (meta.isPoisoned && !this.usePoisonedData) {
441+
return;
442+
}
443+
432444
const scope = this.typeCheckScopes.getTypeCheckScope(node);
433-
if (scope === 'error') {
434-
// Don't type-check components that had errors in their scopes.
445+
if (scope.isPoisoned && !this.usePoisonedData) {
446+
// Don't type-check components that had errors in their scopes, unless requested.
435447
return;
436448
}
437449

@@ -443,6 +455,10 @@ export class ComponentDecoratorHandler implements
443455

444456
resolve(node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>):
445457
ResolveResult<ComponentResolutionData> {
458+
if (analysis.isPoisoned && !this.usePoisonedData) {
459+
return {};
460+
}
461+
446462
const context = node.getSourceFile();
447463
// Check whether this component was registered with an NgModule. If so, it should be compiled
448464
// under that module's compilation scope.
@@ -455,7 +471,7 @@ export class ComponentDecoratorHandler implements
455471
wrapDirectivesAndPipesInClosure: false,
456472
};
457473

458-
if (scope !== null && scope !== 'error') {
474+
if (scope !== null && (!scope.compilation.isPoisoned || this.usePoisonedData)) {
459475
// Replace the empty components and directives from the analyze() step with a fully expanded
460476
// scope. This is possible now because during resolve() the whole compilation unit has been
461477
// fully analyzed.

Diff for: packages/compiler-cli/src/ngtsc/annotations/src/directive.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export interface DirectiveHandlerData {
4141
providersRequiringFactory: Set<Reference<ClassDeclaration>>|null;
4242
inputs: ClassPropertyMapping;
4343
outputs: ClassPropertyMapping;
44+
isPoisoned: boolean;
4445
}
4546

4647
export class DirectiveDecoratorHandler implements
@@ -106,7 +107,8 @@ export class DirectiveDecoratorHandler implements
106107
this.annotateForClosureCompiler),
107108
baseClass: readBaseClass(node, this.reflector, this.evaluator),
108109
typeCheckMeta: extractDirectiveTypeCheckMeta(node, directiveResult.inputs, this.reflector),
109-
providersRequiringFactory
110+
providersRequiringFactory,
111+
isPoisoned: false,
110112
}
111113
};
112114
}
@@ -126,6 +128,7 @@ export class DirectiveDecoratorHandler implements
126128
isComponent: false,
127129
baseClass: analysis.baseClass,
128130
...analysis.typeCheckMeta,
131+
isPoisoned: analysis.isPoisoned,
129132
});
130133

131134
this.injectableRegistry.registerInjectable(node);

Diff for: packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ export class NgModuleDecoratorHandler implements
336336
injectorImports: [],
337337
};
338338

339-
if (scope !== null && scope !== 'error') {
339+
if (scope !== null && !scope.compilation.isPoisoned) {
340340
// Using the scope information, extend the injector's imports using the modules that are
341341
// specified as module exports.
342342
const context = getSourceFile(node);
@@ -361,7 +361,8 @@ export class NgModuleDecoratorHandler implements
361361
return {diagnostics};
362362
}
363363

364-
if (scope === null || scope === 'error' || scope.reexports === null) {
364+
if (scope === null || scope.compilation.isPoisoned || scope.exported.isPoisoned ||
365+
scope.reexports === null) {
365366
return {data};
366367
} else {
367368
return {

Diff for: packages/compiler-cli/src/ngtsc/annotations/src/typecheck_scopes.ts

+19-5
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ export interface TypeCheckScope {
3333
* The schemas that are used in this scope.
3434
*/
3535
schemas: SchemaMetadata[];
36+
37+
/**
38+
* Whether the original compilation scope which produced this `TypeCheckScope` was itself poisoned
39+
* (contained semantic errors during its production).
40+
*/
41+
isPoisoned: boolean;
3642
}
3743

3844
/**
@@ -57,15 +63,18 @@ export class TypeCheckScopes {
5763
* contains an error, then 'error' is returned. If the component is not declared in any NgModule,
5864
* an empty type-check scope is returned.
5965
*/
60-
getTypeCheckScope(node: ClassDeclaration): TypeCheckScope|'error' {
66+
getTypeCheckScope(node: ClassDeclaration): TypeCheckScope {
6167
const matcher = new SelectorMatcher<DirectiveMeta>();
6268
const pipes = new Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>();
6369

6470
const scope = this.scopeReader.getScopeForComponent(node);
6571
if (scope === null) {
66-
return {matcher, pipes, schemas: []};
67-
} else if (scope === 'error') {
68-
return scope;
72+
return {
73+
matcher,
74+
pipes,
75+
schemas: [],
76+
isPoisoned: false,
77+
};
6978
}
7079

7180
if (this.scopeCache.has(scope.ngModule)) {
@@ -87,7 +96,12 @@ export class TypeCheckScopes {
8796
pipes.set(name, ref as Reference<ClassDeclaration<ts.ClassDeclaration>>);
8897
}
8998

90-
const typeCheckScope: TypeCheckScope = {matcher, pipes, schemas: scope.schemas};
99+
const typeCheckScope: TypeCheckScope = {
100+
matcher,
101+
pipes,
102+
schemas: scope.schemas,
103+
isPoisoned: scope.compilation.isPoisoned || scope.exported.isPoisoned,
104+
};
91105
this.scopeCache.set(scope.ngModule, typeCheckScope);
92106
return typeCheckScope;
93107
}

Diff for: packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ runInEachFileSystem(() => {
7373
/* isCore */ false, new NoopResourceLoader(), /* rootDirs */[''],
7474
/* defaultPreserveWhitespaces */ false, /* i18nUseExternalIds */ true,
7575
/* enableI18nLegacyMessageIdFormat */ false,
76+
/* usePoisonedData */ false,
7677
/* i18nNormalizeLineEndingsInICUs */ undefined, moduleResolver, cycleAnalyzer, refEmitter,
7778
NOOP_DEFAULT_IMPORT_RECORDER, /* depTracker */ null, injectableRegistry,
7879
/* annotateForClosureCompiler */ false);

Diff for: packages/compiler-cli/src/ngtsc/core/src/compiler.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export class NgCompiler {
106106
private typeCheckingProgramStrategy: TypeCheckingProgramStrategy,
107107
private incrementalStrategy: IncrementalBuildStrategy,
108108
private enableTemplateTypeChecker: boolean,
109+
private usePoisonedData: boolean,
109110
oldProgram: ts.Program|null = null,
110111
private perfRecorder: PerfRecorder = NOOP_PERF_RECORDER,
111112
) {
@@ -742,7 +743,7 @@ export class NgCompiler {
742743
reflector, evaluator, metaRegistry, metaReader, scopeReader, scopeRegistry,
743744
templateMapping, isCore, this.resourceManager, this.adapter.rootDirs,
744745
this.options.preserveWhitespaces || false, this.options.i18nUseExternalIds !== false,
745-
this.options.enableI18nLegacyMessageIdFormat !== false,
746+
this.options.enableI18nLegacyMessageIdFormat !== false, this.usePoisonedData,
746747
this.options.i18nNormalizeLineEndingsInICUs, this.moduleResolver, this.cycleAnalyzer,
747748
refEmitter, defaultImportTracker, this.incrementalDriver.depGraph, injectableRegistry,
748749
this.closureCompilerEnabled),

Diff for: packages/compiler-cli/src/ngtsc/core/test/compiler_test.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ runInEachFileSystem(() => {
5151
const program = ts.createProgram({host, options, rootNames: host.inputFiles});
5252
const compiler = new NgCompiler(
5353
host, options, program, new ReusedProgramStrategy(program, host, options, []),
54-
new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false);
54+
new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false,
55+
/* usePoisonedData */ false);
5556

5657
const diags = compiler.getDiagnostics(getSourceFileOrError(program, COMPONENT));
5758
expect(diags.length).toBe(1);
@@ -100,7 +101,8 @@ runInEachFileSystem(() => {
100101
const CmpC = getClass(getSourceFileOrError(program, cmpCFile), 'CmpC');
101102
const compiler = new NgCompiler(
102103
host, options, program, new ReusedProgramStrategy(program, host, options, []),
103-
new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false);
104+
new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false,
105+
/* usePoisonedData */ false);
104106
const components = compiler.getComponentsWithTemplateFile(templateFile);
105107
expect(components).toEqual(new Set([CmpA, CmpC]));
106108
});
@@ -129,7 +131,8 @@ runInEachFileSystem(() => {
129131
const program = ts.createProgram({host, options, rootNames: host.inputFiles});
130132
const compiler = new NgCompiler(
131133
host, options, program, new ReusedProgramStrategy(program, host, options, []),
132-
new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false);
134+
new NoopIncrementalBuildStrategy(), /** enableTemplateTypeChecker */ false,
135+
/* usePoisonedData */ false);
133136

134137
const deps = compiler.getResourceDependencies(getSourceFileOrError(program, COMPONENT));
135138
expect(deps.length).toBe(2);

Diff for: packages/compiler-cli/src/ngtsc/metadata/src/api.ts

+6
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ export interface DirectiveMeta extends T2DirectiveMeta, DirectiveTypeCheckMeta {
108108
* another type, it could not statically determine the base class.
109109
*/
110110
baseClass: Reference<ClassDeclaration>|'dynamic'|null;
111+
112+
/**
113+
* Whether the directive had some issue with its declaration that means it might not have complete
114+
* and reliable metadata.
115+
*/
116+
isPoisoned: boolean;
111117
}
112118

113119
/**

Diff for: packages/compiler-cli/src/ngtsc/metadata/src/dts.ts

+1
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export class DtsMetadataReader implements MetadataReader {
9292
queries: readStringArrayType(def.type.typeArguments[5]),
9393
...extractDirectiveTypeCheckMeta(clazz, inputs, this.reflector),
9494
baseClass: readBaseClass(clazz, this.checker, this.reflector),
95+
isPoisoned: false,
9596
};
9697
}
9798

Diff for: packages/compiler-cli/src/ngtsc/program.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ export class NgtscProgram implements api.Program {
100100
// Create the NgCompiler which will drive the rest of the compilation.
101101
this.compiler = new NgCompiler(
102102
this.host, options, this.tsProgram, reusedProgramStrategy, this.incrementalStrategy,
103-
/** enableTemplateTypeChecker */ false, reuseProgram, this.perfRecorder);
103+
/** enableTemplateTypeChecker */ false, /* usePoisonedData */ false, reuseProgram,
104+
this.perfRecorder);
104105
}
105106

106107
getTsProgram(): ts.Program {

Diff for: packages/compiler-cli/src/ngtsc/scope/src/api.ts

+6
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ export interface ScopeData {
2929
* NgModules which contributed to the scope of the module.
3030
*/
3131
ngModules: ClassDeclaration[];
32+
33+
/**
34+
* Whether some module or component in this scope contains errors and is thus semantically
35+
* unreliable.
36+
*/
37+
isPoisoned: boolean;
3238
}
3339

3440
/**

Diff for: packages/compiler-cli/src/ngtsc/scope/src/component_scope.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {LocalModuleScope} from './local';
1313
* Read information about the compilation scope of components.
1414
*/
1515
export interface ComponentScopeReader {
16-
getScopeForComponent(clazz: ClassDeclaration): LocalModuleScope|null|'error';
16+
getScopeForComponent(clazz: ClassDeclaration): LocalModuleScope|null;
1717

1818
/**
1919
* Get the `RemoteScope` required for this component, if any.
@@ -34,7 +34,7 @@ export interface ComponentScopeReader {
3434
export class CompoundComponentScopeReader implements ComponentScopeReader {
3535
constructor(private readers: ComponentScopeReader[]) {}
3636

37-
getScopeForComponent(clazz: ClassDeclaration): LocalModuleScope|null|'error' {
37+
getScopeForComponent(clazz: ClassDeclaration): LocalModuleScope|null {
3838
for (const reader of this.readers) {
3939
const meta = reader.getScopeForComponent(clazz);
4040
if (meta !== null) {

Diff for: packages/compiler-cli/src/ngtsc/scope/src/dependency.ts

+1
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ export class MetadataDtsModuleScopeResolver implements DtsModuleScopeResolver {
132132
directives,
133133
pipes,
134134
ngModules: Array.from(ngModules),
135+
isPoisoned: false,
135136
},
136137
};
137138
this.cache.set(clazz, exportScope);

0 commit comments

Comments
 (0)