feat(compiler-cli): improve error messages produced during structural errors (#20459)

The errors produced when error were encountered while interpreting the
content of a directive was often incomprehencible. With this change
these kind of error messages should be easier to understand and diagnose.

PR Close #20459
This commit is contained in:
Chuck Jazdzewski
2017-11-14 17:49:47 -08:00
committed by Miško Hevery
parent 1366762d12
commit 8ecda94899
25 changed files with 1247 additions and 308 deletions

View File

@ -768,9 +768,9 @@ describe('compiler (unbundled Angular)', () => {
childClassDecorator: '',
childModuleDecorator: '@NgModule({providers: [Extends]})',
}))
.toThrowError(
'Class Extends in /app/main.ts extends from a Injectable in another compilation unit without duplicating the decorator. ' +
'Please add a Injectable or Pipe or Directive or Component or NgModule decorator to the class.');
.toThrowError(`Error during template compile of 'Extends'
Class Extends in /app/main.ts extends from a Injectable in another compilation unit without duplicating the decorator
Please add a Injectable or Pipe or Directive or Component or NgModule decorator to the class.`);
});
});
@ -792,9 +792,9 @@ describe('compiler (unbundled Angular)', () => {
childClassDecorator: '',
childModuleDecorator: '@NgModule({declarations: [Extends]})',
}))
.toThrowError(
'Class Extends in /app/main.ts extends from a Directive in another compilation unit without duplicating the decorator. ' +
'Please add a Directive or Component decorator to the class.');
.toThrowError(`Error during template compile of 'Extends'
Class Extends in /app/main.ts extends from a Directive in another compilation unit without duplicating the decorator
Please add a Directive or Component decorator to the class.`);
});
});
@ -816,9 +816,9 @@ describe('compiler (unbundled Angular)', () => {
childClassDecorator: '',
childModuleDecorator: '@NgModule({declarations: [Extends]})',
}))
.toThrowError(
'Class Extends in /app/main.ts extends from a Directive in another compilation unit without duplicating the decorator. ' +
'Please add a Directive or Component decorator to the class.');
.toThrowError(`Error during template compile of 'Extends'
Class Extends in /app/main.ts extends from a Directive in another compilation unit without duplicating the decorator
Please add a Directive or Component decorator to the class.`);
});
});
@ -840,9 +840,9 @@ describe('compiler (unbundled Angular)', () => {
childClassDecorator: '',
childModuleDecorator: '@NgModule({declarations: [Extends]})',
}))
.toThrowError(
'Class Extends in /app/main.ts extends from a Pipe in another compilation unit without duplicating the decorator. ' +
'Please add a Pipe decorator to the class.');
.toThrowError(`Error during template compile of 'Extends'
Class Extends in /app/main.ts extends from a Pipe in another compilation unit without duplicating the decorator
Please add a Pipe decorator to the class.`);
});
});
@ -864,9 +864,9 @@ describe('compiler (unbundled Angular)', () => {
childClassDecorator: '',
childModuleDecorator: '',
}))
.toThrowError(
'Class Extends in /app/main.ts extends from a NgModule in another compilation unit without duplicating the decorator. ' +
'Please add a NgModule decorator to the class.');
.toThrowError(`Error during template compile of 'Extends'
Class Extends in /app/main.ts extends from a NgModule in another compilation unit without duplicating the decorator
Please add a NgModule decorator to the class.`);
});
});
}

View File

@ -107,8 +107,11 @@ describe('StaticReflector', () => {
it('should provide context for errors reported by the collector', () => {
const SomeClass = reflector.findDeclaration('src/error-reporting', 'SomeClass');
expect(() => reflector.annotations(SomeClass))
.toThrow(new Error(
'Error encountered resolving symbol values statically. A reasonable error message (position 13:34 in the original .ts file), resolving symbol ErrorSym in /tmp/src/error-references.d.ts, resolving symbol Link2 in /tmp/src/error-references.d.ts, resolving symbol Link1 in /tmp/src/error-references.d.ts, resolving symbol SomeClass in /tmp/src/error-reporting.d.ts, resolving symbol SomeClass in /tmp/src/error-reporting.d.ts'));
.toThrow(new Error(`Error during template compile of 'SomeClass'
A reasonable error message in 'Link1'
'Link1' references 'Link2'
'Link2' references 'ErrorSym'
'ErrorSym' contains the error at /tmp/src/error-references.ts(13,34).`));
});
it('should simplify primitive into itself', () => {
@ -330,10 +333,12 @@ describe('StaticReflector', () => {
it('should error on direct recursive calls', () => {
expect(
() => simplify(
reflector.getStaticSymbol('/tmp/src/function-reference.ts', ''),
reflector.getStaticSymbol('/tmp/src/function-reference.ts', 'MyComp'),
reflector.getStaticSymbol('/tmp/src/function-reference.ts', 'recursion')))
.toThrow(new Error(
'Recursion not supported, resolving symbol recursive in /tmp/src/function-recursive.d.ts, resolving symbol recursion in /tmp/src/function-reference.ts, resolving symbol in /tmp/src/function-reference.ts'));
.toThrow(new Error(`Error during template compile of 'MyComp'
Recursion is not supported in 'recursion'
'recursion' references 'recursive'
'recursive' called 'recursive' recursively.`));
});
it('should throw a SyntaxError without stack trace when the required resource cannot be resolved',
@ -345,8 +350,8 @@ describe('StaticReflector', () => {
message:
'Could not resolve ./does-not-exist.component relative to /tmp/src/function-reference.ts'
})))
.toThrowError(
'Error encountered resolving symbol values statically. Could not resolve ./does-not-exist.component relative to /tmp/src/function-reference.ts, resolving symbol AppModule in /tmp/src/function-reference.ts');
.toThrowError(`Error during template compile of 'AppModule'
Could not resolve ./does-not-exist.component relative to /tmp/src/function-reference.ts.`);
});
it('should record data about the error in the exception', () => {
@ -361,7 +366,7 @@ describe('StaticReflector', () => {
simplify(
reflector.getStaticSymbol('/tmp/src/invalid-metadata.ts', ''), classData.decorators[0]);
} catch (e) {
expect(e.fileName).toBe('/tmp/src/invalid-metadata.ts');
expect(e.position).toBeDefined();
threw = true;
}
expect(threw).toBe(true);
@ -370,10 +375,13 @@ describe('StaticReflector', () => {
it('should error on indirect recursive calls', () => {
expect(
() => simplify(
reflector.getStaticSymbol('/tmp/src/function-reference.ts', ''),
reflector.getStaticSymbol('/tmp/src/function-reference.ts', 'MyComp'),
reflector.getStaticSymbol('/tmp/src/function-reference.ts', 'indirectRecursion')))
.toThrow(new Error(
'Recursion not supported, resolving symbol indirectRecursion2 in /tmp/src/function-recursive.d.ts, resolving symbol indirectRecursion1 in /tmp/src/function-recursive.d.ts, resolving symbol indirectRecursion in /tmp/src/function-reference.ts, resolving symbol in /tmp/src/function-reference.ts'));
.toThrow(new Error(`Error during template compile of 'MyComp'
Recursion is not supported in 'indirectRecursion'
'indirectRecursion' references 'indirectRecursion1'
'indirectRecursion1' references 'indirectRecursion2'
'indirectRecursion2' called 'indirectRecursion1' recursively.`));
});
it('should simplify a spread expression', () => {
@ -401,7 +409,8 @@ describe('StaticReflector', () => {
() => reflector.annotations(
reflector.getStaticSymbol('/tmp/src/invalid-calls.ts', 'MyComponent')))
.toThrow(new Error(
`Error encountered resolving symbol values statically. Calling function 'someFunction', function calls are not supported. Consider replacing the function or lambda with a reference to an exported function, resolving symbol MyComponent in /tmp/src/invalid-calls.ts, resolving symbol MyComponent in /tmp/src/invalid-calls.ts`));
`/tmp/src/invalid-calls.ts(8,29): Error during template compile of 'MyComponent'
Function calls are not supported in decorators but 'someFunction' was called.`));
});
it('should be able to get metadata for a class containing a static method call', () => {
@ -962,7 +971,7 @@ describe('StaticReflector', () => {
});
// Regression #18170
it('should agressively evaluate enums selects', () => {
it('should eagerly evaluate enums selects', () => {
const data = Object.create(DEFAULT_TEST_DATA);
const file = '/tmp/src/my_component.ts';
data[file] = `
@ -1078,6 +1087,228 @@ describe('StaticReflector', () => {
expect(symbolResolver.getKnownModuleName(symbol.filePath)).toBe('a');
});
});
describe('formatted error reporting', () => {
describe('function calls', () => {
const fileName = '/tmp/src/invalid/components.ts';
beforeEach(() => {
const localData = {
'/tmp/src/invalid/function-call.ts': `
import {functionToCall} from 'some-module';
export const CALL_FUNCTION = functionToCall();
`,
'/tmp/src/invalid/indirect.ts': `
import {CALL_FUNCTION} from './function-call';
export const INDIRECT_CALL_FUNCTION = CALL_FUNCTION + 1;
`,
'/tmp/src/invalid/two-levels-indirect.ts': `
import {INDIRECT_CALL_FUNCTION} from './indirect';
export const TWO_LEVELS_INDIRECT_CALL_FUNCTION = INDIRECT_CALL_FUNCTION + 1;
`,
'/tmp/src/invalid/components.ts': `
import {functionToCall} from 'some-module';
import {Component} from '@angular/core';
import {CALL_FUNCTION} from './function-call';
import {INDIRECT_CALL_FUNCTION} from './indirect';
import {TWO_LEVELS_INDIRECT_CALL_FUNCTION} from './two-levels-indirect';
@Component({
value: functionToCall()
})
export class CallImportedFunction {}
@Component({
value: CALL_FUNCTION
})
export class ReferenceCalledFunction {}
@Component({
value: INDIRECT_CALL_FUNCTION
})
export class IndirectReferenceCalledFunction {}
@Component({
value: TWO_LEVELS_INDIRECT_CALL_FUNCTION
})
export class TwoLevelsIndirectReferenceCalledFunction {}
`
};
init({...DEFAULT_TEST_DATA, ...localData});
});
it('should report a formatted error for a direct function call', () => {
expect(() => {
return reflector.annotations(reflector.getStaticSymbol(fileName, 'CallImportedFunction'));
})
.toThrowError(
`/tmp/src/invalid/components.ts(9,18): Error during template compile of 'CallImportedFunction'
Function calls are not supported in decorators but 'functionToCall' was called.`);
});
it('should report a formatted error for a refernce to a function call', () => {
expect(() => {
return reflector.annotations(
reflector.getStaticSymbol(fileName, 'ReferenceCalledFunction'));
})
.toThrowError(
`/tmp/src/invalid/components.ts(14,18): Error during template compile of 'ReferenceCalledFunction'
Function calls are not supported in decorators but 'functionToCall' was called in 'CALL_FUNCTION'
'CALL_FUNCTION' calls 'functionToCall' at /tmp/src/invalid/function-call.ts(3,38).`);
});
it('should report a formatted error for an indirect reference to a function call', () => {
expect(() => {
return reflector.annotations(
reflector.getStaticSymbol(fileName, 'IndirectReferenceCalledFunction'));
})
.toThrowError(
`/tmp/src/invalid/components.ts(19,18): Error during template compile of 'IndirectReferenceCalledFunction'
Function calls are not supported in decorators but 'functionToCall' was called in 'INDIRECT_CALL_FUNCTION'
'INDIRECT_CALL_FUNCTION' references 'CALL_FUNCTION' at /tmp/src/invalid/indirect.ts(4,47)
'CALL_FUNCTION' calls 'functionToCall' at /tmp/src/invalid/function-call.ts(3,38).`);
});
it('should report a formatted error for a double-indirect refernce to a function call', () => {
expect(() => {
return reflector.annotations(
reflector.getStaticSymbol(fileName, 'TwoLevelsIndirectReferenceCalledFunction'));
})
.toThrowError(
`/tmp/src/invalid/components.ts(24,18): Error during template compile of 'TwoLevelsIndirectReferenceCalledFunction'
Function calls are not supported in decorators but 'functionToCall' was called in 'TWO_LEVELS_INDIRECT_CALL_FUNCTION'
'TWO_LEVELS_INDIRECT_CALL_FUNCTION' references 'INDIRECT_CALL_FUNCTION' at /tmp/src/invalid/two-levels-indirect.ts(4,58)
'INDIRECT_CALL_FUNCTION' references 'CALL_FUNCTION' at /tmp/src/invalid/indirect.ts(4,47)
'CALL_FUNCTION' calls 'functionToCall' at /tmp/src/invalid/function-call.ts(3,38).`);
});
});
describe('macro functions', () => {
const fileName = '/tmp/src/invalid/components.ts';
beforeEach(() => {
const localData = {
'/tmp/src/invalid/function-call.ts': `
import {functionToCall} from 'some-module';
export const CALL_FUNCTION = functionToCall();
`,
'/tmp/src/invalid/indirect.ts': `
import {CALL_FUNCTION} from './function-call';
export const INDIRECT_CALL_FUNCTION = CALL_FUNCTION + 1;
`,
'/tmp/src/invalid/macros.ts': `
export function someMacro(value: any) {
return [ { provide: 'key', value: value } ];
}
`,
'/tmp/src/invalid/components.ts': `
import {Component} from '@angular/core';
import {functionToCall} from 'some-module';
import {someMacro} from './macros';
import {CALL_FUNCTION} from './function-call';
import {INDIRECT_CALL_FUNCTION} from './indirect';
@Component({
template: someMacro(functionToCall())
})
export class DirectCall {}
@Component({
template: someMacro(CALL_FUNCTION)
})
export class IndirectCall {}
@Component({
template: someMacro(INDIRECT_CALL_FUNCTION)
})
export class DoubleIndirectCall {}
`
};
init({...DEFAULT_TEST_DATA, ...localData});
});
it('should report a formatted error for a direct function call', () => {
expect(() => {
return reflector.annotations(reflector.getStaticSymbol(fileName, 'DirectCall'));
})
.toThrowError(
`/tmp/src/invalid/components.ts(9,31): Error during template compile of 'DirectCall'
Function calls are not supported in decorators but 'functionToCall' was called.`);
});
it('should report a formatted error for a reference to a function call', () => {
expect(() => {
return reflector.annotations(reflector.getStaticSymbol(fileName, 'IndirectCall'));
})
.toThrowError(
`/tmp/src/invalid/components.ts(14,31): Error during template compile of 'IndirectCall'
Function calls are not supported in decorators but 'functionToCall' was called in 'CALL_FUNCTION'
'CALL_FUNCTION' calls 'functionToCall' at /tmp/src/invalid/function-call.ts(3,38).`);
});
it('should report a formatted error for an indirect refernece to a function call', () => {
expect(() => {
return reflector.annotations(reflector.getStaticSymbol(fileName, 'DoubleIndirectCall'));
})
.toThrowError(
`/tmp/src/invalid/components.ts(19,31): Error during template compile of 'DoubleIndirectCall'
Function calls are not supported in decorators but 'functionToCall' was called in 'INDIRECT_CALL_FUNCTION'
'INDIRECT_CALL_FUNCTION' references 'CALL_FUNCTION' at /tmp/src/invalid/indirect.ts(4,47)
'CALL_FUNCTION' calls 'functionToCall' at /tmp/src/invalid/function-call.ts(3,38).`);
});
});
describe('and give advice', () => {
// If in a reference expression, advice the user to replace with a reference.
const fileName = '/tmp/src/invalid/components.ts';
function collectError(symbol: string): string {
try {
reflector.annotations(reflector.getStaticSymbol(fileName, symbol));
} catch (e) {
return e.message;
}
fail('Expected an exception to be thrown');
return '';
}
function initWith(content: string) {
init({
...DEFAULT_TEST_DATA,
[fileName]: `import {Component} from '@angular/core';\n${content}`
});
}
it('should advise exorting a local', () => {
initWith(`const f: string; @Component({value: f}) export class MyComp {}`);
expect(collectError('MyComp')).toContain(`Consider exporting 'f'`);
});
it('should advise export a class', () => {
initWith('class Foo {} @Component({value: Foo}) export class MyComp {}');
expect(collectError('MyComp')).toContain(`Consider exporting 'Foo'`);
});
it('should advise avoiding destructuring', () => {
initWith(
'export const {foo, bar} = {foo: 1, bar: 2}; @Component({value: foo}) export class MyComp {}');
expect(collectError('MyComp')).toContain(`Consider simplifying to avoid destructuring`);
});
it('should advise converting an arrow function into an exported function', () => {
initWith('@Component({value: () => true}) export class MyComp {}');
expect(collectError('MyComp'))
.toContain(`Consider changing the function expression into an exported function`);
});
it('should advise converting a function expression into an exported function', () => {
initWith('@Component({value: function () { return true; }}) export class MyComp {}');
expect(collectError('MyComp'))
.toContain(`Consider changing the function expression into an exported function`);
});
});
});
});
const DEFAULT_TEST_DATA: {[key: string]: any} = {
@ -1467,5 +1698,5 @@ const DEFAULT_TEST_DATA: {[key: string]: any} = {
export class Dep {
@Input f: Forward;
}
`
`,
};

View File

@ -234,15 +234,25 @@ describe('StaticSymbolResolver', () => {
});
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'a')).metadata)
.toEqual(symbolCache.get('/test2.ts', 'b'));
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'x')).metadata).toEqual([
symbolCache.get('/test2.ts', 'y')
]);
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'x')).metadata).toEqual([{
__symbolic: 'resolved',
symbol: symbolCache.get('/test2.ts', 'y'),
line: 3,
character: 24,
fileName: '/test.ts'
}]);
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'simpleFn')).metadata).toEqual({
__symbolic: 'function',
parameters: ['fnArg'],
value: [
symbolCache.get('/test.ts', 'a'), symbolCache.get('/test2.ts', 'y'),
Object({__symbolic: 'reference', name: 'fnArg'})
symbolCache.get('/test.ts', 'a'), {
__symbolic: 'resolved',
symbol: symbolCache.get('/test2.ts', 'y'),
line: 6,
character: 21,
fileName: '/test.ts'
},
{__symbolic: 'reference', name: 'fnArg'}
]
});
});