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:

committed by
Miško Hevery

parent
1366762d12
commit
8ecda94899
@ -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.`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
`
|
||||
`,
|
||||
};
|
||||
|
@ -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'}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user