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:

committed by
Matias Niemelä

parent
18e9d86a3b
commit
957be960d2
@ -1464,7 +1464,7 @@ describe('ngc transformer command-line', () => {
|
||||
const messages: string[] = [];
|
||||
const exitCode =
|
||||
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']);
|
||||
});
|
||||
|
||||
|
@ -105,6 +105,47 @@ describe('perform watch', () => {
|
||||
expect(getSourceFileSpy !).toHaveBeenCalledWith(mainTsPath, 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') {
|
||||
@ -122,7 +163,8 @@ function createModuleAndCompSource(prefix: string, template: string = prefix + '
|
||||
}
|
||||
|
||||
class MockWatchHost {
|
||||
timeoutListeners: Array<(() => void)|null> = [];
|
||||
nextTimeoutListenerId = 1;
|
||||
timeoutListeners: {[id: string]: (() => void)} = {};
|
||||
fileChangeListeners: Array<((event: FileChangeEvent, fileName: string) => void)|null> = [];
|
||||
diagnostics: ng.Diagnostics = [];
|
||||
constructor(public config: ng.ParsedConfiguration) {}
|
||||
@ -141,16 +183,16 @@ class MockWatchHost {
|
||||
close: () => this.fileChangeListeners[id] = null,
|
||||
};
|
||||
}
|
||||
setTimeout(callback: () => void, ms: number): any {
|
||||
const id = this.timeoutListeners.length;
|
||||
this.timeoutListeners.push(callback);
|
||||
setTimeout(callback: () => void): any {
|
||||
const id = this.nextTimeoutListenerId++;
|
||||
this.timeoutListeners[id] = callback;
|
||||
return id;
|
||||
}
|
||||
clearTimeout(timeoutId: any): void { this.timeoutListeners[timeoutId] = null; }
|
||||
clearTimeout(timeoutId: any): void { delete this.timeoutListeners[timeoutId]; }
|
||||
flushTimeouts() {
|
||||
this.timeoutListeners.forEach(cb => {
|
||||
if (cb) cb();
|
||||
});
|
||||
const listeners = this.timeoutListeners;
|
||||
this.timeoutListeners = {};
|
||||
Object.keys(listeners).forEach(id => listeners[id]());
|
||||
}
|
||||
triggerFileChange(event: FileChangeEvent, fileName: string) {
|
||||
this.fileChangeListeners.forEach(listener => {
|
||||
|
@ -64,10 +64,10 @@ export function setup(): TestSupport {
|
||||
function write(fileName: string, content: string) {
|
||||
const dir = path.dirname(fileName);
|
||||
if (dir != '.') {
|
||||
const newDir = path.join(basePath, dir);
|
||||
const newDir = path.resolve(basePath, dir);
|
||||
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}[]) {
|
||||
|
@ -889,4 +889,66 @@ describe('ng program', () => {
|
||||
.toContain(
|
||||
`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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user