Skip to content

Commit 080a8bf

Browse files
JoostKAndrewKushnir
authored andcommitted
fix(ngcc): do not attempt compilation when analysis fails (#34889)
In #34288, ngtsc was refactored to separate the result of the analysis and resolve phase for more granular incremental rebuilds. In this model, any errors in one phase transition the trait into an error state, which prevents it from being ran through subsequent phases. The ngcc compiler on the other hand did not adopt this strict error model, which would cause incomplete metadata—due to errors in earlier phases—to be offered for compilation that could result in a hard crash. This commit updates ngcc to take advantage of ngtsc's `TraitCompiler`, that internally manages all Ivy classes that are part of the compilation. This effectively replaces ngcc's own `AnalyzedFile` and `AnalyzedClass` types, together with all of the logic to drive the `DecoratorHandler`s. All of this is now handled in the `TraitCompiler`, benefiting from its explicit state transitions of `Trait`s so that the ngcc crash is a thing of the past. Fixes #34500 Resolves FW-1788 PR Close #34889
1 parent 8f5c920 commit 080a8bf

File tree

20 files changed

+778
-598
lines changed

20 files changed

+778
-598
lines changed

packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts

Lines changed: 69 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,19 @@ import {FileSystem, LogicalFileSystem, absoluteFrom, dirname, resolve} from '../
1515
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, PrivateExportAliasingHost, Reexport, ReferenceEmitter} from '../../../src/ngtsc/imports';
1616
import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry} from '../../../src/ngtsc/metadata';
1717
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
18-
import {ClassDeclaration} from '../../../src/ngtsc/reflection';
1918
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../../src/ngtsc/scope';
20-
import {CompileResult, DecoratorHandler} from '../../../src/ngtsc/transform';
21-
import {NgccClassSymbol, NgccReflectionHost} from '../host/ngcc_host';
19+
import {DecoratorHandler} from '../../../src/ngtsc/transform';
20+
import {NgccReflectionHost} from '../host/ngcc_host';
2221
import {Migration} from '../migrations/migration';
2322
import {MissingInjectableMigration} from '../migrations/missing_injectable_migration';
2423
import {UndecoratedChildMigration} from '../migrations/undecorated_child_migration';
2524
import {UndecoratedParentMigration} from '../migrations/undecorated_parent_migration';
2625
import {EntryPointBundle} from '../packages/entry_point_bundle';
27-
import {isDefined} from '../utils';
2826

2927
import {DefaultMigrationHost} from './migration_host';
30-
import {AnalyzedClass, AnalyzedFile, CompiledClass, CompiledFile, DecorationAnalyses} from './types';
31-
import {NOOP_DEPENDENCY_TRACKER, analyzeDecorators, isWithinPackage} from './util';
28+
import {NgccTraitCompiler} from './ngcc_trait_compiler';
29+
import {CompiledClass, CompiledFile, DecorationAnalyses} from './types';
30+
import {NOOP_DEPENDENCY_TRACKER, isWithinPackage} from './util';
3231

3332

3433

@@ -57,10 +56,6 @@ export class DecorationAnalyzer {
5756
private packagePath = this.bundle.entryPoint.package;
5857
private isCore = this.bundle.isCore;
5958

60-
/**
61-
* Map of NgModule declarations to the re-exports for that NgModule.
62-
*/
63-
private reexportMap = new Map<ts.Declaration, Map<string, [string, string]>>();
6459
moduleResolver =
6560
new ModuleResolver(this.program, this.options, this.host, /* moduleResolutionCache */ null);
6661
resourceManager = new NgccResourceLoader(this.fs);
@@ -118,6 +113,7 @@ export class DecorationAnalyzer {
118113
/* factoryTracker */ null, NOOP_DEFAULT_IMPORT_RECORDER,
119114
/* annotateForClosureCompiler */ false, this.injectableRegistry),
120115
];
116+
compiler = new NgccTraitCompiler(this.handlers, this.reflectionHost);
121117
migrations: Migration[] = [
122118
new UndecoratedParentMigration(),
123119
new UndecoratedChildMigration(),
@@ -135,56 +131,54 @@ export class DecorationAnalyzer {
135131
* @returns a map of the source files to the analysis for those files.
136132
*/
137133
analyzeProgram(): DecorationAnalyses {
138-
const decorationAnalyses = new DecorationAnalyses();
139-
const analyzedFiles = this.program.getSourceFiles()
140-
.filter(sourceFile => !sourceFile.isDeclarationFile)
141-
.filter(sourceFile => isWithinPackage(this.packagePath, sourceFile))
142-
.map(sourceFile => this.analyzeFile(sourceFile))
143-
.filter(isDefined);
134+
for (const sourceFile of this.program.getSourceFiles()) {
135+
if (!sourceFile.isDeclarationFile && isWithinPackage(this.packagePath, sourceFile)) {
136+
this.compiler.analyzeFile(sourceFile);
137+
}
138+
}
144139

145-
this.applyMigrations(analyzedFiles);
140+
this.applyMigrations();
146141

147-
analyzedFiles.forEach(analyzedFile => this.resolveFile(analyzedFile));
148-
const compiledFiles = analyzedFiles.map(analyzedFile => this.compileFile(analyzedFile));
149-
compiledFiles.forEach(
150-
compiledFile => decorationAnalyses.set(compiledFile.sourceFile, compiledFile));
151-
return decorationAnalyses;
152-
}
142+
this.compiler.resolve();
153143

154-
protected analyzeFile(sourceFile: ts.SourceFile): AnalyzedFile|undefined {
155-
const analyzedClasses = this.reflectionHost.findClassSymbols(sourceFile)
156-
.map(symbol => this.analyzeClass(symbol))
157-
.filter(isDefined);
158-
return analyzedClasses.length ? {sourceFile, analyzedClasses} : undefined;
159-
}
144+
this.reportDiagnostics();
160145

161-
protected analyzeClass(symbol: NgccClassSymbol): AnalyzedClass|null {
162-
const decorators = this.reflectionHost.getDecoratorsOfSymbol(symbol);
163-
const analyzedClass = analyzeDecorators(symbol, decorators, this.handlers);
164-
if (analyzedClass !== null && analyzedClass.diagnostics !== undefined) {
165-
for (const diagnostic of analyzedClass.diagnostics) {
166-
this.diagnosticHandler(diagnostic);
167-
}
146+
const decorationAnalyses = new DecorationAnalyses();
147+
for (const analyzedFile of this.compiler.analyzedFiles) {
148+
const compiledFile = this.compileFile(analyzedFile);
149+
decorationAnalyses.set(compiledFile.sourceFile, compiledFile);
168150
}
169-
return analyzedClass;
151+
return decorationAnalyses;
170152
}
171153

172-
protected applyMigrations(analyzedFiles: AnalyzedFile[]): void {
154+
protected applyMigrations(): void {
173155
const migrationHost = new DefaultMigrationHost(
174-
this.reflectionHost, this.fullMetaReader, this.evaluator, this.handlers,
175-
this.bundle.entryPoint.path, analyzedFiles, this.diagnosticHandler);
156+
this.reflectionHost, this.fullMetaReader, this.evaluator, this.compiler,
157+
this.bundle.entryPoint.path);
176158

177159
this.migrations.forEach(migration => {
178-
analyzedFiles.forEach(analyzedFile => {
179-
analyzedFile.analyzedClasses.forEach(({declaration}) => {
160+
this.compiler.analyzedFiles.forEach(analyzedFile => {
161+
const records = this.compiler.recordsFor(analyzedFile);
162+
if (records === null) {
163+
throw new Error('Assertion error: file to migrate must have records.');
164+
}
165+
166+
records.forEach(record => {
167+
const addDiagnostic = (diagnostic: ts.Diagnostic) => {
168+
if (record.metaDiagnostics === null) {
169+
record.metaDiagnostics = [];
170+
}
171+
record.metaDiagnostics.push(diagnostic);
172+
};
173+
180174
try {
181-
const result = migration.apply(declaration, migrationHost);
175+
const result = migration.apply(record.node, migrationHost);
182176
if (result !== null) {
183-
this.diagnosticHandler(result);
177+
addDiagnostic(result);
184178
}
185179
} catch (e) {
186180
if (isFatalDiagnosticError(e)) {
187-
this.diagnosticHandler(e.toDiagnostic());
181+
addDiagnostic(e.toDiagnostic());
188182
} else {
189183
throw e;
190184
}
@@ -194,67 +188,45 @@ export class DecorationAnalyzer {
194188
});
195189
}
196190

197-
protected compileFile(analyzedFile: AnalyzedFile): CompiledFile {
198-
const constantPool = new ConstantPool();
199-
const compiledClasses: CompiledClass[] = analyzedFile.analyzedClasses.map(analyzedClass => {
200-
const compilation = this.compileClass(analyzedClass, constantPool);
201-
const declaration = analyzedClass.declaration;
202-
const reexports: Reexport[] = this.getReexportsForClass(declaration);
203-
return {...analyzedClass, compilation, reexports};
204-
});
205-
return {constantPool, sourceFile: analyzedFile.sourceFile, compiledClasses};
206-
}
191+
protected reportDiagnostics() { this.compiler.diagnostics.forEach(this.diagnosticHandler); }
207192

208-
protected compileClass(clazz: AnalyzedClass, constantPool: ConstantPool): CompileResult[] {
209-
const compilations: CompileResult[] = [];
210-
for (const {handler, analysis, resolution} of clazz.matches) {
211-
const result = handler.compile(clazz.declaration, analysis, resolution, constantPool);
212-
if (Array.isArray(result)) {
213-
result.forEach(current => {
214-
if (!compilations.some(compilation => compilation.name === current.name)) {
215-
compilations.push(current);
216-
}
217-
});
218-
} else if (!compilations.some(compilation => compilation.name === result.name)) {
219-
compilations.push(result);
220-
}
193+
protected compileFile(sourceFile: ts.SourceFile): CompiledFile {
194+
const constantPool = new ConstantPool();
195+
const records = this.compiler.recordsFor(sourceFile);
196+
if (records === null) {
197+
throw new Error('Assertion error: file to compile must have records.');
221198
}
222-
return compilations;
223-
}
224199

225-
protected resolveFile(analyzedFile: AnalyzedFile): void {
226-
for (const {declaration, matches} of analyzedFile.analyzedClasses) {
227-
for (const match of matches) {
228-
const {handler, analysis} = match;
229-
if ((handler.resolve !== undefined) && analysis) {
230-
const {reexports, diagnostics, data} = handler.resolve(declaration, analysis);
231-
if (reexports !== undefined) {
232-
this.addReexports(reexports, declaration);
233-
}
234-
if (diagnostics !== undefined) {
235-
diagnostics.forEach(error => this.diagnosticHandler(error));
236-
}
237-
match.resolution = data as Readonly<unknown>;
238-
}
200+
const compiledClasses: CompiledClass[] = [];
201+
202+
for (const record of records) {
203+
const compilation = this.compiler.compile(record.node, constantPool);
204+
if (compilation === null) {
205+
continue;
239206
}
240-
}
241-
}
242207

243-
private getReexportsForClass(declaration: ClassDeclaration<ts.Declaration>) {
244-
const reexports: Reexport[] = [];
245-
if (this.reexportMap.has(declaration)) {
246-
this.reexportMap.get(declaration) !.forEach(([fromModule, symbolName], asAlias) => {
247-
reexports.push({asAlias, fromModule, symbolName});
208+
compiledClasses.push({
209+
name: record.node.name.text,
210+
decorators: this.compiler.getAllDecorators(record.node),
211+
declaration: record.node, compilation
248212
});
249213
}
250-
return reexports;
214+
215+
const reexports = this.getReexportsForSourceFile(sourceFile);
216+
return {constantPool, sourceFile: sourceFile, compiledClasses, reexports};
251217
}
252218

253-
private addReexports(reexports: Reexport[], declaration: ClassDeclaration<ts.Declaration>) {
254-
const map = new Map<string, [string, string]>();
255-
for (const reexport of reexports) {
256-
map.set(reexport.asAlias, [reexport.fromModule, reexport.symbolName]);
219+
private getReexportsForSourceFile(sf: ts.SourceFile): Reexport[] {
220+
const exportStatements = this.compiler.exportStatements;
221+
if (!exportStatements.has(sf.fileName)) {
222+
return [];
257223
}
258-
this.reexportMap.set(declaration, map);
224+
const exports = exportStatements.get(sf.fileName) !;
225+
226+
const reexports: Reexport[] = [];
227+
exports.forEach(([fromModule, symbolName], asAlias) => {
228+
reexports.push({asAlias, fromModule, symbolName});
229+
});
230+
return reexports;
259231
}
260232
}

packages/compiler-cli/ngcc/src/analysis/migration_host.ts

Lines changed: 12 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -7,110 +7,47 @@
77
*/
88
import * as ts from 'typescript';
99

10-
import {ErrorCode, FatalDiagnosticError} from '../../../src/ngtsc/diagnostics';
1110
import {AbsoluteFsPath} from '../../../src/ngtsc/file_system';
1211
import {MetadataReader} from '../../../src/ngtsc/metadata';
1312
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
1413
import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection';
15-
import {DecoratorHandler, HandlerFlags} from '../../../src/ngtsc/transform';
14+
import {HandlerFlags, TraitState} from '../../../src/ngtsc/transform';
1615
import {NgccReflectionHost} from '../host/ngcc_host';
1716
import {MigrationHost} from '../migrations/migration';
1817

19-
import {AnalyzedClass, AnalyzedFile} from './types';
20-
import {analyzeDecorators, isWithinPackage} from './util';
18+
import {NgccTraitCompiler} from './ngcc_trait_compiler';
19+
import {isWithinPackage} from './util';
2120

2221
/**
23-
* The standard implementation of `MigrationHost`, which is created by the
24-
* `DecorationAnalyzer`.
22+
* The standard implementation of `MigrationHost`, which is created by the `DecorationAnalyzer`.
2523
*/
2624
export class DefaultMigrationHost implements MigrationHost {
2725
constructor(
2826
readonly reflectionHost: NgccReflectionHost, readonly metadata: MetadataReader,
29-
readonly evaluator: PartialEvaluator,
30-
private handlers: DecoratorHandler<unknown, unknown, unknown>[],
31-
private entryPointPath: AbsoluteFsPath, private analyzedFiles: AnalyzedFile[],
32-
private diagnosticHandler: (error: ts.Diagnostic) => void) {}
27+
readonly evaluator: PartialEvaluator, private compiler: NgccTraitCompiler,
28+
private entryPointPath: AbsoluteFsPath) {}
3329

3430
injectSyntheticDecorator(clazz: ClassDeclaration, decorator: Decorator, flags?: HandlerFlags):
3531
void {
36-
const classSymbol = this.reflectionHost.getClassSymbol(clazz) !;
37-
const newAnalyzedClass = analyzeDecorators(classSymbol, [decorator], this.handlers, flags);
38-
if (newAnalyzedClass === null) {
39-
return;
40-
}
32+
const migratedTraits = this.compiler.injectSyntheticDecorator(clazz, decorator, flags);
4133

42-
if (newAnalyzedClass.diagnostics !== undefined) {
43-
for (const diagnostic of newAnalyzedClass.diagnostics) {
44-
this.diagnosticHandler(createMigrationDiagnostic(diagnostic, clazz, decorator));
34+
for (const trait of migratedTraits) {
35+
if (trait.state === TraitState.ERRORED) {
36+
trait.diagnostics =
37+
trait.diagnostics.map(diag => createMigrationDiagnostic(diag, clazz, decorator));
4538
}
4639
}
47-
48-
const analyzedFile = getOrCreateAnalyzedFile(this.analyzedFiles, clazz.getSourceFile());
49-
const oldAnalyzedClass = analyzedFile.analyzedClasses.find(c => c.declaration === clazz);
50-
if (oldAnalyzedClass === undefined) {
51-
analyzedFile.analyzedClasses.push(newAnalyzedClass);
52-
} else {
53-
mergeAnalyzedClasses(oldAnalyzedClass, newAnalyzedClass);
54-
}
5540
}
5641

5742
getAllDecorators(clazz: ClassDeclaration): Decorator[]|null {
58-
const sourceFile = clazz.getSourceFile();
59-
const analyzedFile = this.analyzedFiles.find(file => file.sourceFile === sourceFile);
60-
if (analyzedFile === undefined) {
61-
return null;
62-
}
63-
64-
const analyzedClass = analyzedFile.analyzedClasses.find(c => c.declaration === clazz);
65-
if (analyzedClass === undefined) {
66-
return null;
67-
}
68-
69-
return analyzedClass.decorators;
43+
return this.compiler.getAllDecorators(clazz);
7044
}
7145

7246
isInScope(clazz: ClassDeclaration): boolean {
7347
return isWithinPackage(this.entryPointPath, clazz.getSourceFile());
7448
}
7549
}
7650

77-
function getOrCreateAnalyzedFile(
78-
analyzedFiles: AnalyzedFile[], sourceFile: ts.SourceFile): AnalyzedFile {
79-
const analyzedFile = analyzedFiles.find(file => file.sourceFile === sourceFile);
80-
if (analyzedFile !== undefined) {
81-
return analyzedFile;
82-
} else {
83-
const newAnalyzedFile: AnalyzedFile = {sourceFile, analyzedClasses: []};
84-
analyzedFiles.push(newAnalyzedFile);
85-
return newAnalyzedFile;
86-
}
87-
}
88-
89-
function mergeAnalyzedClasses(oldClass: AnalyzedClass, newClass: AnalyzedClass) {
90-
if (newClass.decorators !== null) {
91-
if (oldClass.decorators === null) {
92-
oldClass.decorators = newClass.decorators;
93-
} else {
94-
for (const newDecorator of newClass.decorators) {
95-
if (oldClass.decorators.some(d => d.name === newDecorator.name)) {
96-
throw new FatalDiagnosticError(
97-
ErrorCode.NGCC_MIGRATION_DECORATOR_INJECTION_ERROR, newClass.declaration,
98-
`Attempted to inject "${newDecorator.name}" decorator over a pre-existing decorator with the same name on the "${newClass.name}" class.`);
99-
}
100-
}
101-
oldClass.decorators.push(...newClass.decorators);
102-
}
103-
}
104-
105-
if (newClass.diagnostics !== undefined) {
106-
if (oldClass.diagnostics === undefined) {
107-
oldClass.diagnostics = newClass.diagnostics;
108-
} else {
109-
oldClass.diagnostics.push(...newClass.diagnostics);
110-
}
111-
}
112-
}
113-
11451
/**
11552
* Creates a diagnostic from another one, containing additional information about the synthetic
11653
* decorator.

0 commit comments

Comments
 (0)