fix(compiler): recover from structural errors in watch mode (#19953)
This also changes the compiler so that we throw less often on structural changes and produce a meaningful state in the `ng.Program` in case of errors. Related to #19951 PR Close #19953
This commit is contained in:
parent
18e9d86a3b
commit
957be960d2
|
@ -191,6 +191,10 @@ export function performWatchCompilation(host: PerformWatchHost):
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
ingoreFilesForWatch.clear();
|
ingoreFilesForWatch.clear();
|
||||||
|
const oldProgram = cachedProgram;
|
||||||
|
// We clear out the `cachedProgram` here as a
|
||||||
|
// program can only be used as `oldProgram` 1x
|
||||||
|
cachedProgram = undefined;
|
||||||
const compileResult = performCompilation({
|
const compileResult = performCompilation({
|
||||||
rootNames: cachedOptions.rootNames,
|
rootNames: cachedOptions.rootNames,
|
||||||
options: cachedOptions.options,
|
options: cachedOptions.options,
|
||||||
|
|
|
@ -175,15 +175,17 @@ class AngularCompilerProgram implements Program {
|
||||||
if (this._analyzedModules) {
|
if (this._analyzedModules) {
|
||||||
throw new Error('Angular structure already loaded');
|
throw new Error('Angular structure already loaded');
|
||||||
}
|
}
|
||||||
const {tmpProgram, sourceFiles, rootNames} = this._createProgramWithBasicStubs();
|
return Promise.resolve()
|
||||||
return this.compiler.loadFilesAsync(sourceFiles)
|
.then(() => {
|
||||||
.catch(this.catchAnalysisError.bind(this))
|
const {tmpProgram, sourceFiles, rootNames} = this._createProgramWithBasicStubs();
|
||||||
.then(analyzedModules => {
|
return this.compiler.loadFilesAsync(sourceFiles).then(analyzedModules => {
|
||||||
if (this._analyzedModules) {
|
if (this._analyzedModules) {
|
||||||
throw new Error('Angular structure loaded both synchronously and asynchronsly');
|
throw new Error('Angular structure loaded both synchronously and asynchronsly');
|
||||||
}
|
}
|
||||||
this._updateProgramWithTypeCheckStubs(tmpProgram, analyzedModules, rootNames);
|
this._updateProgramWithTypeCheckStubs(tmpProgram, analyzedModules, rootNames);
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
.catch(e => this._createProgramOnError(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
listLazyRoutes(route?: string): LazyRoute[] {
|
listLazyRoutes(route?: string): LazyRoute[] {
|
||||||
|
@ -402,14 +404,13 @@ class AngularCompilerProgram implements Program {
|
||||||
if (this._analyzedModules) {
|
if (this._analyzedModules) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const {tmpProgram, sourceFiles, rootNames} = this._createProgramWithBasicStubs();
|
|
||||||
let analyzedModules: NgAnalyzedModules|null;
|
|
||||||
try {
|
try {
|
||||||
analyzedModules = this.compiler.loadFilesSync(sourceFiles);
|
const {tmpProgram, sourceFiles, rootNames} = this._createProgramWithBasicStubs();
|
||||||
|
const analyzedModules = this.compiler.loadFilesSync(sourceFiles);
|
||||||
|
this._updateProgramWithTypeCheckStubs(tmpProgram, analyzedModules, rootNames);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
analyzedModules = this.catchAnalysisError(e);
|
this._createProgramOnError(e);
|
||||||
}
|
}
|
||||||
this._updateProgramWithTypeCheckStubs(tmpProgram, analyzedModules, rootNames);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createCompiler() {
|
private _createCompiler() {
|
||||||
|
@ -457,7 +458,7 @@ class AngularCompilerProgram implements Program {
|
||||||
|
|
||||||
let rootNames = this.rootNames;
|
let rootNames = this.rootNames;
|
||||||
if (this.options.generateCodeForLibraries !== false) {
|
if (this.options.generateCodeForLibraries !== false) {
|
||||||
// if we should generateCodeForLibraries, enver include
|
// if we should generateCodeForLibraries, never include
|
||||||
// generated files in the program as otherwise we will
|
// generated files in the program as otherwise we will
|
||||||
// ovewrite them and typescript will report the error
|
// ovewrite them and typescript will report the error
|
||||||
// TS5055: Cannot write file ... because it would overwrite input file.
|
// TS5055: Cannot write file ... because it would overwrite input file.
|
||||||
|
@ -482,23 +483,21 @@ class AngularCompilerProgram implements Program {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _updateProgramWithTypeCheckStubs(
|
private _updateProgramWithTypeCheckStubs(
|
||||||
tmpProgram: ts.Program, analyzedModules: NgAnalyzedModules|null, rootNames: string[]) {
|
tmpProgram: ts.Program, analyzedModules: NgAnalyzedModules, rootNames: string[]) {
|
||||||
this._analyzedModules = analyzedModules || emptyModules;
|
this._analyzedModules = analyzedModules;
|
||||||
if (analyzedModules) {
|
tmpProgram.getSourceFiles().forEach(sf => {
|
||||||
tmpProgram.getSourceFiles().forEach(sf => {
|
if (sf.fileName.endsWith('.ngfactory.ts')) {
|
||||||
if (sf.fileName.endsWith('.ngfactory.ts')) {
|
const {generate, baseFileName} = this.hostAdapter.shouldGenerateFile(sf.fileName);
|
||||||
const {generate, baseFileName} = this.hostAdapter.shouldGenerateFile(sf.fileName);
|
if (generate) {
|
||||||
if (generate) {
|
// Note: ! is ok as hostAdapter.shouldGenerateFile will always return a basefileName
|
||||||
// Note: ! is ok as hostAdapter.shouldGenerateFile will always return a basefileName
|
// for .ngfactory.ts files.
|
||||||
// for .ngfactory.ts files.
|
const genFile = this.compiler.emitTypeCheckStub(sf.fileName, baseFileName !);
|
||||||
const genFile = this.compiler.emitTypeCheckStub(sf.fileName, baseFileName !);
|
if (genFile) {
|
||||||
if (genFile) {
|
this.hostAdapter.updateGeneratedFile(genFile);
|
||||||
this.hostAdapter.updateGeneratedFile(genFile);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
this._tsProgram = ts.createProgram(rootNames, this.options, this.hostAdapter, tmpProgram);
|
this._tsProgram = ts.createProgram(rootNames, this.options, this.hostAdapter, tmpProgram);
|
||||||
// Note: the new ts program should be completely reusable by TypeScript as:
|
// Note: the new ts program should be completely reusable by TypeScript as:
|
||||||
// - we cache all the files in the hostAdapter
|
// - we cache all the files in the hostAdapter
|
||||||
|
@ -509,7 +508,13 @@ class AngularCompilerProgram implements Program {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private catchAnalysisError(e: any): NgAnalyzedModules|null {
|
private _createProgramOnError(e: any) {
|
||||||
|
// Still fill the analyzedModules and the tsProgram
|
||||||
|
// so that we don't cause other errors for users who e.g. want to emit the ngProgram.
|
||||||
|
this._analyzedModules = emptyModules;
|
||||||
|
this.oldTsProgram = undefined;
|
||||||
|
this._hostAdapter.isSourceFile = () => false;
|
||||||
|
this._tsProgram = ts.createProgram(this.rootNames, this.options, this.hostAdapter);
|
||||||
if (isSyntaxError(e)) {
|
if (isSyntaxError(e)) {
|
||||||
const parserErrors = getParseErrors(e);
|
const parserErrors = getParseErrors(e);
|
||||||
if (parserErrors && parserErrors.length) {
|
if (parserErrors && parserErrors.length) {
|
||||||
|
@ -533,7 +538,7 @@ class AngularCompilerProgram implements Program {
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return null;
|
return;
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1464,7 +1464,7 @@ describe('ngc transformer command-line', () => {
|
||||||
const messages: string[] = [];
|
const messages: string[] = [];
|
||||||
const exitCode =
|
const exitCode =
|
||||||
main(['-p', path.join(basePath, 'src/tsconfig.json')], message => messages.push(message));
|
main(['-p', path.join(basePath, 'src/tsconfig.json')], message => messages.push(message));
|
||||||
expect(exitCode).toBe(2, 'Compile was expected to fail');
|
expect(exitCode).toBe(1, 'Compile was expected to fail');
|
||||||
expect(messages[0]).toContain(['Tagged template expressions are not supported in metadata']);
|
expect(messages[0]).toContain(['Tagged template expressions are not supported in metadata']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -105,6 +105,47 @@ describe('perform watch', () => {
|
||||||
expect(getSourceFileSpy !).toHaveBeenCalledWith(mainTsPath, ts.ScriptTarget.ES5);
|
expect(getSourceFileSpy !).toHaveBeenCalledWith(mainTsPath, ts.ScriptTarget.ES5);
|
||||||
expect(getSourceFileSpy !).toHaveBeenCalledWith(utilTsPath, ts.ScriptTarget.ES5);
|
expect(getSourceFileSpy !).toHaveBeenCalledWith(utilTsPath, ts.ScriptTarget.ES5);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should recover from static analysis errors', () => {
|
||||||
|
const config = createConfig();
|
||||||
|
const host = new MockWatchHost(config);
|
||||||
|
|
||||||
|
const okFileContent = `
|
||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
@NgModule()
|
||||||
|
export class MyModule {}
|
||||||
|
`;
|
||||||
|
const errorFileContent = `
|
||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
@NgModule(() => (1===1 ? null as any : null as any))
|
||||||
|
export class MyModule {}
|
||||||
|
`;
|
||||||
|
const indexTsPath = path.resolve(testSupport.basePath, 'src', 'index.ts');
|
||||||
|
|
||||||
|
testSupport.write(indexTsPath, okFileContent);
|
||||||
|
|
||||||
|
performWatchCompilation(host);
|
||||||
|
expectNoDiagnostics(config.options, host.diagnostics);
|
||||||
|
|
||||||
|
// Do it multiple times as the watch mode switches internal modes.
|
||||||
|
// E.g. from regular compile to using summaries, ...
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
host.diagnostics = [];
|
||||||
|
testSupport.write(indexTsPath, okFileContent);
|
||||||
|
host.triggerFileChange(FileChangeEvent.Change, indexTsPath);
|
||||||
|
expectNoDiagnostics(config.options, host.diagnostics);
|
||||||
|
|
||||||
|
host.diagnostics = [];
|
||||||
|
testSupport.write(indexTsPath, errorFileContent);
|
||||||
|
host.triggerFileChange(FileChangeEvent.Change, indexTsPath);
|
||||||
|
|
||||||
|
const errDiags = host.diagnostics.filter(d => d.category === ts.DiagnosticCategory.Error);
|
||||||
|
expect(errDiags.length).toBe(1);
|
||||||
|
expect(errDiags[0].messageText).toContain('Function calls are not supported.');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function createModuleAndCompSource(prefix: string, template: string = prefix + 'template') {
|
function createModuleAndCompSource(prefix: string, template: string = prefix + 'template') {
|
||||||
|
@ -122,7 +163,8 @@ function createModuleAndCompSource(prefix: string, template: string = prefix + '
|
||||||
}
|
}
|
||||||
|
|
||||||
class MockWatchHost {
|
class MockWatchHost {
|
||||||
timeoutListeners: Array<(() => void)|null> = [];
|
nextTimeoutListenerId = 1;
|
||||||
|
timeoutListeners: {[id: string]: (() => void)} = {};
|
||||||
fileChangeListeners: Array<((event: FileChangeEvent, fileName: string) => void)|null> = [];
|
fileChangeListeners: Array<((event: FileChangeEvent, fileName: string) => void)|null> = [];
|
||||||
diagnostics: ng.Diagnostics = [];
|
diagnostics: ng.Diagnostics = [];
|
||||||
constructor(public config: ng.ParsedConfiguration) {}
|
constructor(public config: ng.ParsedConfiguration) {}
|
||||||
|
@ -141,16 +183,16 @@ class MockWatchHost {
|
||||||
close: () => this.fileChangeListeners[id] = null,
|
close: () => this.fileChangeListeners[id] = null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
setTimeout(callback: () => void, ms: number): any {
|
setTimeout(callback: () => void): any {
|
||||||
const id = this.timeoutListeners.length;
|
const id = this.nextTimeoutListenerId++;
|
||||||
this.timeoutListeners.push(callback);
|
this.timeoutListeners[id] = callback;
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
clearTimeout(timeoutId: any): void { this.timeoutListeners[timeoutId] = null; }
|
clearTimeout(timeoutId: any): void { delete this.timeoutListeners[timeoutId]; }
|
||||||
flushTimeouts() {
|
flushTimeouts() {
|
||||||
this.timeoutListeners.forEach(cb => {
|
const listeners = this.timeoutListeners;
|
||||||
if (cb) cb();
|
this.timeoutListeners = {};
|
||||||
});
|
Object.keys(listeners).forEach(id => listeners[id]());
|
||||||
}
|
}
|
||||||
triggerFileChange(event: FileChangeEvent, fileName: string) {
|
triggerFileChange(event: FileChangeEvent, fileName: string) {
|
||||||
this.fileChangeListeners.forEach(listener => {
|
this.fileChangeListeners.forEach(listener => {
|
||||||
|
|
|
@ -64,10 +64,10 @@ export function setup(): TestSupport {
|
||||||
function write(fileName: string, content: string) {
|
function write(fileName: string, content: string) {
|
||||||
const dir = path.dirname(fileName);
|
const dir = path.dirname(fileName);
|
||||||
if (dir != '.') {
|
if (dir != '.') {
|
||||||
const newDir = path.join(basePath, dir);
|
const newDir = path.resolve(basePath, dir);
|
||||||
if (!fs.existsSync(newDir)) fs.mkdirSync(newDir);
|
if (!fs.existsSync(newDir)) fs.mkdirSync(newDir);
|
||||||
}
|
}
|
||||||
fs.writeFileSync(path.join(basePath, fileName), content, {encoding: 'utf-8'});
|
fs.writeFileSync(path.resolve(basePath, fileName), content, {encoding: 'utf-8'});
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeFiles(...mockDirs: {[fileName: string]: string}[]) {
|
function writeFiles(...mockDirs: {[fileName: string]: string}[]) {
|
||||||
|
|
|
@ -889,4 +889,66 @@ describe('ng program', () => {
|
||||||
.toContain(
|
.toContain(
|
||||||
`src/main.html(1,1): error TS100: Property 'nonExistent' does not exist on type 'MyComp'.`);
|
`src/main.html(1,1): error TS100: Property 'nonExistent' does not exist on type 'MyComp'.`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('errors', () => {
|
||||||
|
const fileWithStructuralError = `
|
||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
@NgModule(() => (1===1 ? null as any : null as any))
|
||||||
|
export class MyModule {}
|
||||||
|
`;
|
||||||
|
const fileWithGoodContent = `
|
||||||
|
import {NgModule} from '@angular/core';
|
||||||
|
|
||||||
|
@NgModule()
|
||||||
|
export class MyModule {}
|
||||||
|
`;
|
||||||
|
|
||||||
|
it('should not throw on structural errors but collect them', () => {
|
||||||
|
testSupport.write('src/index.ts', fileWithStructuralError);
|
||||||
|
|
||||||
|
const options = testSupport.createCompilerOptions();
|
||||||
|
const host = ng.createCompilerHost({options});
|
||||||
|
const program = ng.createProgram(
|
||||||
|
{rootNames: [path.resolve(testSupport.basePath, 'src/index.ts')], options, host});
|
||||||
|
|
||||||
|
const structuralErrors = program.getNgStructuralDiagnostics();
|
||||||
|
expect(structuralErrors.length).toBe(1);
|
||||||
|
expect(structuralErrors[0].messageText).toContain('Function calls are not supported.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw on structural errors but collect them (loadNgStructureAsync)', (done) => {
|
||||||
|
testSupport.write('src/index.ts', fileWithStructuralError);
|
||||||
|
|
||||||
|
const options = testSupport.createCompilerOptions();
|
||||||
|
const host = ng.createCompilerHost({options});
|
||||||
|
const program = ng.createProgram(
|
||||||
|
{rootNames: [path.resolve(testSupport.basePath, 'src/index.ts')], options, host});
|
||||||
|
program.loadNgStructureAsync().then(() => {
|
||||||
|
const structuralErrors = program.getNgStructuralDiagnostics();
|
||||||
|
expect(structuralErrors.length).toBe(1);
|
||||||
|
expect(structuralErrors[0].messageText).toContain('Function calls are not supported.');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to use a program with structural errors as oldProgram', () => {
|
||||||
|
testSupport.write('src/index.ts', fileWithStructuralError);
|
||||||
|
|
||||||
|
const options = testSupport.createCompilerOptions();
|
||||||
|
const host = ng.createCompilerHost({options});
|
||||||
|
const program1 = ng.createProgram(
|
||||||
|
{rootNames: [path.resolve(testSupport.basePath, 'src/index.ts')], options, host});
|
||||||
|
expect(program1.getNgStructuralDiagnostics().length).toBe(1);
|
||||||
|
|
||||||
|
testSupport.write('src/index.ts', fileWithGoodContent);
|
||||||
|
const program2 = ng.createProgram({
|
||||||
|
rootNames: [path.resolve(testSupport.basePath, 'src/index.ts')],
|
||||||
|
options,
|
||||||
|
host,
|
||||||
|
oldProgram: program1
|
||||||
|
});
|
||||||
|
expectNoDiagnosticsInProgram(options, program2);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -752,7 +752,7 @@ class PopulatedScope extends BindingScope {
|
||||||
}
|
}
|
||||||
|
|
||||||
function positionalError(message: string, fileName: string, line: number, column: number): Error {
|
function positionalError(message: string, fileName: string, line: number, column: number): Error {
|
||||||
const result = new Error(message);
|
const result = syntaxError(message);
|
||||||
(result as any).fileName = fileName;
|
(result as any).fileName = fileName;
|
||||||
(result as any).line = line;
|
(result as any).line = line;
|
||||||
(result as any).column = column;
|
(result as any).column = column;
|
||||||
|
|
Loading…
Reference in New Issue