647d7bdd88
Fixes all TypeScript failures caused by enabling the `--strict` flag for test source files. We also want to enable the strict options for tests as the strictness enforcement improves the overall codehealth, unveiled common issues and additionally it allows us to enable `strict` in the `tsconfig.json` that is picked up by IDE's. PR Close #30993
1705 lines
60 KiB
TypeScript
1705 lines
60 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google Inc. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
|
|
import {StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, StaticSymbolResolverHost, core as compilerCore} from '@angular/compiler';
|
|
import {CollectorOptions, METADATA_VERSION} from '@angular/compiler-cli';
|
|
|
|
import {MockStaticSymbolResolverHost, MockSummaryResolver} from './static_symbol_resolver_spec';
|
|
|
|
describe('StaticReflector', () => {
|
|
let noContext: StaticSymbol;
|
|
let host: StaticSymbolResolverHost;
|
|
let symbolResolver: StaticSymbolResolver;
|
|
let reflector: StaticReflector;
|
|
|
|
function init(
|
|
testData: {[key: string]: any} = DEFAULT_TEST_DATA,
|
|
decorators: {name: string, filePath: string, ctor: any}[] = [],
|
|
errorRecorder?: (error: any, fileName?: string) => void,
|
|
collectorOptions?: CollectorOptions) {
|
|
const symbolCache = new StaticSymbolCache();
|
|
host = new MockStaticSymbolResolverHost(testData, collectorOptions);
|
|
const summaryResolver = new MockSummaryResolver([]);
|
|
spyOn(summaryResolver, 'isLibraryFile').and.returnValue(false);
|
|
symbolResolver = new StaticSymbolResolver(host, symbolCache, summaryResolver, errorRecorder);
|
|
reflector = new StaticReflector(summaryResolver, symbolResolver, decorators, [], errorRecorder);
|
|
noContext = reflector.getStaticSymbol('', '');
|
|
}
|
|
|
|
beforeEach(() => init());
|
|
|
|
function simplify(context: StaticSymbol, value: any) {
|
|
return (reflector as any).simplify(context, value);
|
|
}
|
|
|
|
it('should get annotations for NgFor', () => {
|
|
const NgFor = reflector.findDeclaration('@angular/common/src/directives/ng_for', 'NgFor');
|
|
const annotations = reflector.annotations(NgFor);
|
|
expect(annotations.length).toEqual(1);
|
|
const annotation = annotations[0];
|
|
expect(annotation.selector).toEqual('[ngFor][ngForOf]');
|
|
expect(annotation.inputs).toEqual(['ngForTrackBy', 'ngForOf', 'ngForTemplate']);
|
|
});
|
|
|
|
it('should get constructor for NgFor', () => {
|
|
const NgFor = reflector.findDeclaration('@angular/common/src/directives/ng_for', 'NgFor');
|
|
const ViewContainerRef = reflector.findDeclaration('@angular/core', 'ViewContainerRef');
|
|
const TemplateRef = reflector.findDeclaration('@angular/core', 'TemplateRef');
|
|
const IterableDiffers = reflector.findDeclaration('@angular/core', 'IterableDiffers');
|
|
const ChangeDetectorRef = reflector.findDeclaration('@angular/core', 'ChangeDetectorRef');
|
|
|
|
const parameters = reflector.parameters(NgFor);
|
|
expect(parameters).toEqual([
|
|
[ViewContainerRef], [TemplateRef], [IterableDiffers], [ChangeDetectorRef]
|
|
]);
|
|
});
|
|
|
|
it('should get annotations for HeroDetailComponent', () => {
|
|
const HeroDetailComponent =
|
|
reflector.findDeclaration('src/app/hero-detail.component', 'HeroDetailComponent');
|
|
const annotations = reflector.annotations(HeroDetailComponent);
|
|
expect(annotations.length).toEqual(1);
|
|
const annotation = annotations[0];
|
|
expect(annotation.selector).toEqual('my-hero-detail');
|
|
});
|
|
|
|
it('should get and empty annotation list for an unknown class', () => {
|
|
const UnknownClass = reflector.findDeclaration('src/app/app.component', 'UnknownClass');
|
|
const annotations = reflector.annotations(UnknownClass);
|
|
expect(annotations).toEqual([]);
|
|
});
|
|
|
|
it('should get and empty annotation list for a symbol with null value', () => {
|
|
init({
|
|
'/tmp/test.ts': `
|
|
export var x = null;
|
|
`
|
|
});
|
|
const annotations = reflector.annotations(reflector.getStaticSymbol('/tmp/test.ts', 'x'));
|
|
expect(annotations).toEqual([]);
|
|
});
|
|
|
|
it('should get propMetadata for HeroDetailComponent', () => {
|
|
const HeroDetailComponent =
|
|
reflector.findDeclaration('src/app/hero-detail.component', 'HeroDetailComponent');
|
|
const props = reflector.propMetadata(HeroDetailComponent);
|
|
expect(props['hero']).toBeTruthy();
|
|
expect(props['onMouseOver']).toEqual([compilerCore.createHostListener(
|
|
'mouseover', ['$event'])]);
|
|
});
|
|
|
|
it('should get an empty object from propMetadata for an unknown class', () => {
|
|
const UnknownClass = reflector.findDeclaration('src/app/app.component', 'UnknownClass');
|
|
const properties = reflector.propMetadata(UnknownClass);
|
|
expect(properties).toEqual({});
|
|
});
|
|
|
|
it('should get empty parameters list for an unknown class ', () => {
|
|
const UnknownClass = reflector.findDeclaration('src/app/app.component', 'UnknownClass');
|
|
const parameters = reflector.parameters(UnknownClass);
|
|
expect(parameters).toEqual([]);
|
|
});
|
|
|
|
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 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', () => {
|
|
expect(simplify(noContext, 1)).toBe(1);
|
|
expect(simplify(noContext, true)).toBe(true);
|
|
expect(simplify(noContext, 'some value')).toBe('some value');
|
|
});
|
|
|
|
it('should simplify a static symbol into itself', () => {
|
|
const staticSymbol = reflector.getStaticSymbol('', '');
|
|
expect(simplify(noContext, staticSymbol)).toBe(staticSymbol);
|
|
});
|
|
|
|
it('should simplify an array into a copy of the array', () => {
|
|
expect(simplify(noContext, [1, 2, 3])).toEqual([1, 2, 3]);
|
|
});
|
|
|
|
it('should simplify an object to a copy of the object', () => {
|
|
const expr = {a: 1, b: 2, c: 3};
|
|
expect(simplify(noContext, expr)).toEqual(expr);
|
|
});
|
|
|
|
it('should simplify &&', () => {
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '&&', left: true, right: true})))
|
|
.toBe(true);
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '&&', left: true, right: false})))
|
|
.toBe(false);
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '&&', left: false, right: true})))
|
|
.toBe(false);
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '&&', left: false, right: false})))
|
|
.toBe(false);
|
|
});
|
|
|
|
it('should simplify ||', () => {
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '||', left: true, right: true})))
|
|
.toBe(true);
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '||', left: true, right: false})))
|
|
.toBe(true);
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '||', left: false, right: true})))
|
|
.toBe(true);
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '||', left: false, right: false})))
|
|
.toBe(false);
|
|
});
|
|
|
|
it('should simplify &', () => {
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '&', left: 0x22, right: 0x0F})))
|
|
.toBe(0x22 & 0x0F);
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '&', left: 0x22, right: 0xF0})))
|
|
.toBe(0x22 & 0xF0);
|
|
});
|
|
|
|
it('should simplify |', () => {
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '|', left: 0x22, right: 0x0F})))
|
|
.toBe(0x22 | 0x0F);
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '|', left: 0x22, right: 0xF0})))
|
|
.toBe(0x22 | 0xF0);
|
|
});
|
|
|
|
it('should simplify ^', () => {
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '|', left: 0x22, right: 0x0F})))
|
|
.toBe(0x22 | 0x0F);
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '|', left: 0x22, right: 0xF0})))
|
|
.toBe(0x22 | 0xF0);
|
|
});
|
|
|
|
it('should simplify ==', () => {
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '==', left: 0x22, right: 0x22})))
|
|
.toBe(0x22 == 0x22);
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '==', left: 0x22, right: 0xF0})))
|
|
.toBe(0x22 as any == 0xF0);
|
|
});
|
|
|
|
it('should simplify !=', () => {
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '!=', left: 0x22, right: 0x22})))
|
|
.toBe(0x22 != 0x22);
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '!=', left: 0x22, right: 0xF0})))
|
|
.toBe(0x22 as any != 0xF0);
|
|
});
|
|
|
|
it('should simplify ===', () => {
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '===', left: 0x22, right: 0x22})))
|
|
.toBe(0x22 === 0x22);
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '===', left: 0x22, right: 0xF0})))
|
|
.toBe(0x22 as any === 0xF0);
|
|
});
|
|
|
|
it('should simplify !==', () => {
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '!==', left: 0x22, right: 0x22})))
|
|
.toBe(0x22 !== 0x22);
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '!==', left: 0x22, right: 0xF0})))
|
|
.toBe(0x22 as any !== 0xF0);
|
|
});
|
|
|
|
it('should simplify >', () => {
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '>', left: 1, right: 1})))
|
|
.toBe(1 > 1);
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '>', left: 1, right: 0})))
|
|
.toBe(1 > 0);
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '>', left: 0, right: 1})))
|
|
.toBe(0 > 1);
|
|
});
|
|
|
|
it('should simplify >=', () => {
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '>=', left: 1, right: 1})))
|
|
.toBe(1 >= 1);
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '>=', left: 1, right: 0})))
|
|
.toBe(1 >= 0);
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '>=', left: 0, right: 1})))
|
|
.toBe(0 >= 1);
|
|
});
|
|
|
|
it('should simplify <=', () => {
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '<=', left: 1, right: 1})))
|
|
.toBe(1 <= 1);
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '<=', left: 1, right: 0})))
|
|
.toBe(1 <= 0);
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '<=', left: 0, right: 1})))
|
|
.toBe(0 <= 1);
|
|
});
|
|
|
|
it('should simplify <', () => {
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '<', left: 1, right: 1})))
|
|
.toBe(1 < 1);
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '<', left: 1, right: 0})))
|
|
.toBe(1 < 0);
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '<', left: 0, right: 1})))
|
|
.toBe(0 < 1);
|
|
});
|
|
|
|
it('should simplify <<', () => {
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '<<', left: 0x55, right: 2})))
|
|
.toBe(0x55 << 2);
|
|
});
|
|
|
|
it('should simplify >>', () => {
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '>>', left: 0x55, right: 2})))
|
|
.toBe(0x55 >> 2);
|
|
});
|
|
|
|
it('should simplify +', () => {
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '+', left: 0x55, right: 2})))
|
|
.toBe(0x55 + 2);
|
|
});
|
|
|
|
it('should simplify -', () => {
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '-', left: 0x55, right: 2})))
|
|
.toBe(0x55 - 2);
|
|
});
|
|
|
|
it('should simplify *', () => {
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '*', left: 0x55, right: 2})))
|
|
.toBe(0x55 * 2);
|
|
});
|
|
|
|
it('should simplify /', () => {
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '/', left: 0x55, right: 2})))
|
|
.toBe(0x55 / 2);
|
|
});
|
|
|
|
it('should simplify %', () => {
|
|
expect(simplify(noContext, ({__symbolic: 'binop', operator: '%', left: 0x55, right: 2})))
|
|
.toBe(0x55 % 2);
|
|
});
|
|
|
|
it('should simplify prefix -', () => {
|
|
expect(simplify(noContext, ({__symbolic: 'pre', operator: '-', operand: 2}))).toBe(-2);
|
|
});
|
|
|
|
it('should simplify prefix ~', () => {
|
|
expect(simplify(noContext, ({__symbolic: 'pre', operator: '~', operand: 2}))).toBe(~2);
|
|
});
|
|
|
|
it('should simplify prefix !', () => {
|
|
expect(simplify(noContext, ({__symbolic: 'pre', operator: '!', operand: true}))).toBe(!true);
|
|
expect(simplify(noContext, ({__symbolic: 'pre', operator: '!', operand: false}))).toBe(!false);
|
|
});
|
|
|
|
it('should simplify an array index', () => {
|
|
expect(simplify(noContext, ({__symbolic: 'index', expression: [1, 2, 3], index: 2}))).toBe(3);
|
|
});
|
|
|
|
it('should simplify an object index', () => {
|
|
const expr = {__symbolic: 'select', expression: {a: 1, b: 2, c: 3}, member: 'b'};
|
|
expect(simplify(noContext, expr)).toBe(2);
|
|
});
|
|
|
|
it('should simplify a file reference', () => {
|
|
expect(simplify(
|
|
reflector.getStaticSymbol('/src/cases', ''),
|
|
reflector.getStaticSymbol('/src/extern.d.ts', 's')))
|
|
.toEqual('s');
|
|
});
|
|
|
|
it('should simplify a non existing reference as a static symbol', () => {
|
|
expect(simplify(
|
|
reflector.getStaticSymbol('/src/cases', ''),
|
|
reflector.getStaticSymbol('/src/extern.d.ts', 'nonExisting')))
|
|
.toEqual(reflector.getStaticSymbol('/src/extern.d.ts', 'nonExisting'));
|
|
});
|
|
|
|
it('should simplify a function reference as a static symbol', () => {
|
|
expect(simplify(
|
|
reflector.getStaticSymbol('/src/cases', 'myFunction'),
|
|
({__symbolic: 'function', parameters: ['a'], value: []})))
|
|
.toEqual(reflector.getStaticSymbol('/src/cases', 'myFunction'));
|
|
});
|
|
|
|
it('should simplify values initialized with a function call', () => {
|
|
expect(simplify(
|
|
reflector.getStaticSymbol('/tmp/src/function-reference.ts', ''),
|
|
reflector.getStaticSymbol('/tmp/src/function-reference.ts', 'one')))
|
|
.toEqual(['some-value']);
|
|
expect(simplify(
|
|
reflector.getStaticSymbol('/tmp/src/function-reference.ts', ''),
|
|
reflector.getStaticSymbol('/tmp/src/function-reference.ts', 'three')))
|
|
.toEqual(3);
|
|
});
|
|
|
|
it('should error on direct recursive calls', () => {
|
|
expect(
|
|
() => simplify(
|
|
reflector.getStaticSymbol('/tmp/src/function-reference.ts', 'MyComp'),
|
|
reflector.getStaticSymbol('/tmp/src/function-reference.ts', 'recursion')))
|
|
.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',
|
|
() => {
|
|
expect(
|
|
() => simplify(
|
|
reflector.getStaticSymbol('/tmp/src/function-reference.ts', 'AppModule'), ({
|
|
__symbolic: 'error',
|
|
message:
|
|
'Could not resolve ./does-not-exist.component relative to /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', () => {
|
|
let threw = false;
|
|
try {
|
|
const metadata = host.getMetadataFor('/tmp/src/invalid-metadata.ts') !;
|
|
expect(metadata).toBeDefined();
|
|
const moduleMetadata: any = metadata[0]['metadata'];
|
|
expect(moduleMetadata).toBeDefined();
|
|
const classData: any = moduleMetadata['InvalidMetadata'];
|
|
expect(classData).toBeDefined();
|
|
simplify(
|
|
reflector.getStaticSymbol('/tmp/src/invalid-metadata.ts', ''),
|
|
classData.decorators[0].arguments);
|
|
} catch (e) {
|
|
expect(e.position).toBeDefined();
|
|
threw = true;
|
|
}
|
|
expect(threw).toBe(true);
|
|
});
|
|
|
|
it('should error on indirect recursive calls', () => {
|
|
expect(
|
|
() => simplify(
|
|
reflector.getStaticSymbol('/tmp/src/function-reference.ts', 'MyComp'),
|
|
reflector.getStaticSymbol('/tmp/src/function-reference.ts', 'indirectRecursion')))
|
|
.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', () => {
|
|
expect(simplify(
|
|
reflector.getStaticSymbol('/tmp/src/spread.ts', ''),
|
|
reflector.getStaticSymbol('/tmp/src/spread.ts', 'spread')))
|
|
.toEqual([0, 1, 2, 3, 4, 5]);
|
|
});
|
|
|
|
it('should be able to get metadata for a class containing a custom decorator', () => {
|
|
const props = reflector.propMetadata(
|
|
reflector.getStaticSymbol('/tmp/src/custom-decorator-reference.ts', 'Foo'));
|
|
expect(props).toEqual({foo: []});
|
|
});
|
|
|
|
it('should read ctor parameters with forwardRef', () => {
|
|
const src = '/tmp/src/forward-ref.ts';
|
|
const dep = reflector.getStaticSymbol(src, 'Dep');
|
|
const props = reflector.parameters(reflector.getStaticSymbol(src, 'Forward'));
|
|
expect(props).toEqual([[dep, compilerCore.createInject(dep)]]);
|
|
});
|
|
|
|
it('should report an error for invalid function calls', () => {
|
|
expect(
|
|
() => reflector.annotations(
|
|
reflector.getStaticSymbol('/tmp/src/invalid-calls.ts', 'MyComponent')))
|
|
.toThrow(new Error(
|
|
`/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', () => {
|
|
const annotations = reflector.annotations(
|
|
reflector.getStaticSymbol('/tmp/src/static-method-call.ts', 'MyComponent'));
|
|
expect(annotations.length).toBe(1);
|
|
expect(annotations[0].providers).toEqual({provider: 'a', useValue: 100});
|
|
});
|
|
|
|
it('should be able to get metadata for a class containing a static field reference', () => {
|
|
const annotations = reflector.annotations(
|
|
reflector.getStaticSymbol('/tmp/src/static-field-reference.ts', 'Foo'));
|
|
expect(annotations.length).toBe(1);
|
|
expect(annotations[0].providers).toEqual([{provider: 'a', useValue: 'Some string'}]);
|
|
});
|
|
|
|
it('should be able to get the metadata for a class calling a method with a conditional expression',
|
|
() => {
|
|
const annotations = reflector.annotations(
|
|
reflector.getStaticSymbol('/tmp/src/static-method-call.ts', 'MyCondComponent'));
|
|
expect(annotations.length).toBe(1);
|
|
expect(annotations[0].providers).toEqual([
|
|
[{provider: 'a', useValue: '1'}], [{provider: 'a', useValue: '2'}]
|
|
]);
|
|
});
|
|
|
|
it('should be able to get metadata for a class with nested method calls', () => {
|
|
const annotations = reflector.annotations(
|
|
reflector.getStaticSymbol('/tmp/src/static-method-call.ts', 'MyFactoryComponent'));
|
|
expect(annotations.length).toBe(1);
|
|
expect(annotations[0].providers).toEqual({
|
|
provide: 'c',
|
|
useFactory:
|
|
reflector.getStaticSymbol('/tmp/src/static-method.ts', 'AnotherModule', ['someFactory'])
|
|
});
|
|
});
|
|
|
|
it('should be able to get the metadata for a class calling a method with default parameters',
|
|
() => {
|
|
const annotations = reflector.annotations(
|
|
reflector.getStaticSymbol('/tmp/src/static-method-call.ts', 'MyDefaultsComponent'));
|
|
expect(annotations.length).toBe(1);
|
|
expect(annotations[0].providers).toEqual([['a', true, false]]);
|
|
});
|
|
|
|
it('should be able to get metadata with a reference to a static method', () => {
|
|
const annotations = reflector.annotations(
|
|
reflector.getStaticSymbol('/tmp/src/static-method-ref.ts', 'MethodReference'));
|
|
expect(annotations.length).toBe(1);
|
|
expect(annotations[0].providers[0].useValue.members[0]).toEqual('staticMethod');
|
|
});
|
|
|
|
it('should be able to get metadata for a class calling a macro function', () => {
|
|
const annotations = reflector.annotations(
|
|
reflector.getStaticSymbol('/tmp/src/call-macro-function.ts', 'MyComponent'));
|
|
expect(annotations.length).toBe(1);
|
|
expect(annotations[0].providers.useValue).toBe(100);
|
|
});
|
|
|
|
it('should be able to get metadata for a class calling a nested macro function', () => {
|
|
const annotations = reflector.annotations(
|
|
reflector.getStaticSymbol('/tmp/src/call-macro-function.ts', 'MyComponentNested'));
|
|
expect(annotations.length).toBe(1);
|
|
expect(annotations[0].providers.useValue.useValue).toBe(100);
|
|
});
|
|
|
|
// #13605
|
|
it('should not throw on unknown decorators', () => {
|
|
const data = Object.create(DEFAULT_TEST_DATA);
|
|
const file = '/tmp/src/app.component.ts';
|
|
data[file] = `
|
|
import { Component } from '@angular/core';
|
|
|
|
export const enum TypeEnum {
|
|
type
|
|
}
|
|
|
|
export function MyValidationDecorator(p1: any, p2: any): any {
|
|
return null;
|
|
}
|
|
|
|
export function ValidationFunction(a1: any): any {
|
|
return null;
|
|
}
|
|
|
|
@Component({
|
|
selector: 'my-app',
|
|
template: "<h1>Hello {{name}}</h1>",
|
|
})
|
|
export class AppComponent {
|
|
name = 'Angular';
|
|
|
|
@MyValidationDecorator( TypeEnum.type, ValidationFunction({option: 'value'}))
|
|
myClassProp: number;
|
|
}`;
|
|
init(data);
|
|
const appComponent = reflector.getStaticSymbol(file, 'AppComponent');
|
|
expect(() => reflector.propMetadata(appComponent)).not.toThrow();
|
|
});
|
|
|
|
it('should not throw with an invalid extends', () => {
|
|
const data = Object.create(DEFAULT_TEST_DATA);
|
|
const file = '/tmp/src/invalid-component.ts';
|
|
data[file] = `
|
|
import {Component} from '@angular/core';
|
|
|
|
function InvalidParent() {
|
|
return InvalidParent;
|
|
}
|
|
|
|
@Component({
|
|
selector: 'tmp',
|
|
template: '',
|
|
})
|
|
export class BadComponent extends InvalidParent() {
|
|
|
|
}
|
|
`;
|
|
init(data);
|
|
const badComponent = reflector.getStaticSymbol(file, 'BadComponent');
|
|
expect(reflector.propMetadata(badComponent)).toEqual({});
|
|
expect(reflector.parameters(badComponent)).toEqual([]);
|
|
expect(reflector.hasLifecycleHook(badComponent, 'onDestroy')).toEqual(false);
|
|
});
|
|
|
|
it('should produce a annotation even if it contains errors', () => {
|
|
const data = Object.create(DEFAULT_TEST_DATA);
|
|
const file = '/tmp/src/invalid-component.ts';
|
|
data[file] = `
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'tmp',
|
|
template: () => {},
|
|
providers: [1, 2, (() => {}), 3, !(() => {}), 4, 5, (() => {}) + (() => {}), 6, 7]
|
|
})
|
|
export class BadComponent {
|
|
|
|
}
|
|
`;
|
|
init(data, [], () => {}, {verboseInvalidExpression: true});
|
|
|
|
const badComponent = reflector.getStaticSymbol(file, 'BadComponent');
|
|
const annotations = reflector.annotations(badComponent);
|
|
const annotation = annotations[0];
|
|
expect(annotation.selector).toEqual('tmp');
|
|
expect(annotation.template).toBeUndefined();
|
|
expect(annotation.providers).toEqual([1, 2, 3, 4, 5, 6, 7]);
|
|
});
|
|
|
|
it('should ignore unresolved calls', () => {
|
|
const data = Object.create(DEFAULT_TEST_DATA);
|
|
const file = '/tmp/src/invalid-component.ts';
|
|
data[file] = `
|
|
import {Component} from '@angular/core';
|
|
import {unknown} from 'unresolved';
|
|
|
|
@Component({
|
|
selector: 'tmp',
|
|
template: () => {},
|
|
providers: [triggers()]
|
|
})
|
|
export class BadComponent {
|
|
|
|
}
|
|
`;
|
|
init(data, [], () => {}, {verboseInvalidExpression: true});
|
|
|
|
const badComponent = reflector.getStaticSymbol(file, 'BadComponent');
|
|
const annotations = reflector.annotations(badComponent);
|
|
const annotation = annotations[0];
|
|
expect(annotation.providers).toEqual([]);
|
|
});
|
|
|
|
// #15424
|
|
it('should be able to inject a ctor parameter with a @Inject and a type expression', () => {
|
|
const data = Object.create(DEFAULT_TEST_DATA);
|
|
const file = '/tmp/src/invalid-component.ts';
|
|
data[file] = `
|
|
import {Injectable, Inject} from '@angular/core';
|
|
|
|
@Injectable()
|
|
export class SomeClass {
|
|
constructor (@Inject('some-token') a: {a: string, b: string}) {}
|
|
}
|
|
`;
|
|
init(data);
|
|
|
|
const someClass = reflector.getStaticSymbol(file, 'SomeClass');
|
|
const parameters = reflector.parameters(someClass);
|
|
expect(compilerCore.createInject.isTypeOf(parameters[0][0])).toBe(true);
|
|
});
|
|
|
|
it('should reject a ctor parameter without a @Inject and a type exprssion', () => {
|
|
const data = Object.create(DEFAULT_TEST_DATA);
|
|
const file = '/tmp/src/invalid-component.ts';
|
|
data[file] = `
|
|
import {Injectable} from '@angular/core';
|
|
|
|
@Injectable()
|
|
export class SomeClass {
|
|
constructor (a: {a: string, b: string}) {}
|
|
}
|
|
`;
|
|
|
|
let error: any = undefined;
|
|
init(data, [], (err: any, filePath?: string) => {
|
|
expect(error).toBeUndefined();
|
|
error = err;
|
|
});
|
|
|
|
const someClass = reflector.getStaticSymbol(file, 'SomeClass');
|
|
expect(reflector.parameters(someClass)).toEqual([[]]);
|
|
expect(error).toBeUndefined();
|
|
});
|
|
|
|
describe('inheritance', () => {
|
|
class ClassDecorator {
|
|
constructor(public value: any) {}
|
|
}
|
|
|
|
class ParamDecorator {
|
|
constructor(public value: any) {}
|
|
}
|
|
|
|
class PropDecorator {
|
|
constructor(public value: any) {}
|
|
}
|
|
|
|
function initWithDecorator(testData: {[key: string]: any}) {
|
|
testData['/tmp/src/decorator.ts'] = `
|
|
export function ClassDecorator(): any {}
|
|
export function ParamDecorator(): any {}
|
|
export function PropDecorator(): any {}
|
|
`;
|
|
init(testData, [
|
|
{filePath: '/tmp/src/decorator.ts', name: 'ClassDecorator', ctor: ClassDecorator},
|
|
{filePath: '/tmp/src/decorator.ts', name: 'ParamDecorator', ctor: ParamDecorator},
|
|
{filePath: '/tmp/src/decorator.ts', name: 'PropDecorator', ctor: PropDecorator}
|
|
]);
|
|
}
|
|
|
|
it('should inherit annotations', () => {
|
|
initWithDecorator({
|
|
'/tmp/src/main.ts': `
|
|
import {ClassDecorator} from './decorator';
|
|
|
|
@ClassDecorator('parent')
|
|
export class Parent {}
|
|
|
|
@ClassDecorator('child')
|
|
export class Child extends Parent {}
|
|
|
|
export class ChildNoDecorators extends Parent {}
|
|
|
|
export class ChildInvalidParent extends a.InvalidParent {}
|
|
`
|
|
});
|
|
|
|
// Check that metadata for Parent was not changed!
|
|
expect(reflector.annotations(reflector.getStaticSymbol('/tmp/src/main.ts', 'Parent')))
|
|
.toEqual([new ClassDecorator('parent')]);
|
|
|
|
expect(reflector.annotations(reflector.getStaticSymbol('/tmp/src/main.ts', 'Child')))
|
|
.toEqual([new ClassDecorator('parent'), new ClassDecorator('child')]);
|
|
|
|
expect(
|
|
reflector.annotations(reflector.getStaticSymbol('/tmp/src/main.ts', 'ChildNoDecorators')))
|
|
.toEqual([new ClassDecorator('parent')]);
|
|
|
|
expect(reflector.annotations(
|
|
reflector.getStaticSymbol('/tmp/src/main.ts', 'ChildInvalidParent')))
|
|
.toEqual([]);
|
|
});
|
|
|
|
it('should inherit parameters', () => {
|
|
initWithDecorator({
|
|
'/tmp/src/main.ts': `
|
|
import {ParamDecorator} from './decorator';
|
|
|
|
export class A {}
|
|
export class B {}
|
|
export class C {}
|
|
|
|
export class Parent {
|
|
constructor(@ParamDecorator('a') a: A, @ParamDecorator('b') b: B) {}
|
|
}
|
|
|
|
export class Child extends Parent {}
|
|
|
|
export class ChildWithCtor extends Parent {
|
|
constructor(@ParamDecorator('c') c: C) {}
|
|
}
|
|
|
|
export class ChildInvalidParent extends a.InvalidParent {}
|
|
`
|
|
});
|
|
|
|
// Check that metadata for Parent was not changed!
|
|
expect(reflector.parameters(reflector.getStaticSymbol('/tmp/src/main.ts', 'Parent')))
|
|
.toEqual([
|
|
[reflector.getStaticSymbol('/tmp/src/main.ts', 'A'), new ParamDecorator('a')],
|
|
[reflector.getStaticSymbol('/tmp/src/main.ts', 'B'), new ParamDecorator('b')]
|
|
]);
|
|
|
|
expect(reflector.parameters(reflector.getStaticSymbol('/tmp/src/main.ts', 'Child'))).toEqual([
|
|
[reflector.getStaticSymbol('/tmp/src/main.ts', 'A'), new ParamDecorator('a')],
|
|
[reflector.getStaticSymbol('/tmp/src/main.ts', 'B'), new ParamDecorator('b')]
|
|
]);
|
|
|
|
expect(reflector.parameters(reflector.getStaticSymbol('/tmp/src/main.ts', 'ChildWithCtor')))
|
|
.toEqual([[reflector.getStaticSymbol('/tmp/src/main.ts', 'C'), new ParamDecorator('c')]]);
|
|
|
|
expect(
|
|
reflector.parameters(reflector.getStaticSymbol('/tmp/src/main.ts', 'ChildInvalidParent')))
|
|
.toEqual([]);
|
|
});
|
|
|
|
it('should inherit property metadata', () => {
|
|
initWithDecorator({
|
|
'/tmp/src/main.ts': `
|
|
import {PropDecorator} from './decorator';
|
|
|
|
export class A {}
|
|
export class B {}
|
|
export class C {}
|
|
|
|
export class Parent {
|
|
@PropDecorator('a')
|
|
a: A;
|
|
@PropDecorator('b1')
|
|
b: B;
|
|
}
|
|
|
|
export class Child extends Parent {
|
|
@PropDecorator('b2')
|
|
b: B;
|
|
@PropDecorator('c')
|
|
c: C;
|
|
}
|
|
|
|
export class ChildInvalidParent extends a.InvalidParent {}
|
|
`
|
|
});
|
|
|
|
// Check that metadata for Parent was not changed!
|
|
expect(reflector.propMetadata(reflector.getStaticSymbol('/tmp/src/main.ts', 'Parent')))
|
|
.toEqual({
|
|
'a': [new PropDecorator('a')],
|
|
'b': [new PropDecorator('b1')],
|
|
});
|
|
|
|
expect(reflector.propMetadata(reflector.getStaticSymbol('/tmp/src/main.ts', 'Child')))
|
|
.toEqual({
|
|
'a': [new PropDecorator('a')],
|
|
'b': [new PropDecorator('b1'), new PropDecorator('b2')],
|
|
'c': [new PropDecorator('c')]
|
|
});
|
|
|
|
expect(reflector.propMetadata(
|
|
reflector.getStaticSymbol('/tmp/src/main.ts', 'ChildInvalidParent')))
|
|
.toEqual({});
|
|
});
|
|
|
|
it('should inherit lifecycle hooks', () => {
|
|
initWithDecorator({
|
|
'/tmp/src/main.ts': `
|
|
export class Parent {
|
|
hook1() {}
|
|
hook2() {}
|
|
}
|
|
|
|
export class Child extends Parent {
|
|
hook2() {}
|
|
hook3() {}
|
|
}
|
|
|
|
export class ChildInvalidParent extends a.InvalidParent {}
|
|
`
|
|
});
|
|
|
|
function hooks(symbol: StaticSymbol, names: string[]): boolean[] {
|
|
return names.map(name => reflector.hasLifecycleHook(symbol, name));
|
|
}
|
|
|
|
// Check that metadata for Parent was not changed!
|
|
expect(hooks(reflector.getStaticSymbol('/tmp/src/main.ts', 'Parent'), [
|
|
'hook1', 'hook2', 'hook3'
|
|
])).toEqual([true, true, false]);
|
|
|
|
expect(hooks(reflector.getStaticSymbol('/tmp/src/main.ts', 'Child'), [
|
|
'hook1', 'hook2', 'hook3'
|
|
])).toEqual([true, true, true]);
|
|
|
|
expect(hooks(reflector.getStaticSymbol('/tmp/src/main.ts', 'ChildInvalidParent'), [
|
|
'hook1', 'hook2', 'hook3'
|
|
])).toEqual([false, false, false]);
|
|
});
|
|
|
|
it('should allow inheritance from expressions', () => {
|
|
initWithDecorator({
|
|
'/tmp/src/main.ts': `
|
|
export function metaClass() { return null; };
|
|
export class Child extends metaClass() {}
|
|
`
|
|
});
|
|
|
|
expect(reflector.annotations(reflector.getStaticSymbol('/tmp/src/main.ts', 'Child')))
|
|
.toEqual([]);
|
|
});
|
|
|
|
it('should allow inheritance from functions', () => {
|
|
initWithDecorator({
|
|
'/tmp/src/main.ts': `
|
|
export let ctor: {new(): T} = function() { return null; }
|
|
export class Child extends ctor {}
|
|
`
|
|
});
|
|
|
|
expect(reflector.annotations(reflector.getStaticSymbol('/tmp/src/main.ts', 'Child')))
|
|
.toEqual([]);
|
|
});
|
|
|
|
it('should support constructor parameters with @Inject and an interface type', () => {
|
|
const data = Object.create(DEFAULT_TEST_DATA);
|
|
const file = '/tmp/src/inject_interface.ts';
|
|
data[file] = `
|
|
import {Injectable, Inject} from '@angular/core';
|
|
import {F} from './f';
|
|
|
|
export interface InjectedInterface {
|
|
|
|
}
|
|
|
|
export class Token {}
|
|
|
|
@Injectable()
|
|
export class SomeClass {
|
|
constructor (@Inject(Token) injected: InjectedInterface, t: Token, @Inject(Token) f: F) {}
|
|
}
|
|
`;
|
|
|
|
init(data);
|
|
|
|
expect(reflector.parameters(reflector.getStaticSymbol(file, 'SomeClass'))[0].length)
|
|
.toEqual(1);
|
|
});
|
|
});
|
|
|
|
describe('expression lowering', () => {
|
|
it('should be able to accept a lambda in a reference location', () => {
|
|
const data = Object.create(DEFAULT_TEST_DATA);
|
|
const file = '/tmp/src/my_component.ts';
|
|
data[file] = `
|
|
import {Component, InjectionToken} from '@angular/core';
|
|
|
|
export const myLambda = () => [1, 2, 3];
|
|
export const NUMBERS = new InjectionToken<number[]>();
|
|
|
|
@Component({
|
|
template: '<div>{{name}}</div>',
|
|
providers: [{provide: NUMBERS, useFactory: myLambda}]
|
|
})
|
|
export class MyComponent {
|
|
name = 'Some name';
|
|
}
|
|
`;
|
|
init(data);
|
|
|
|
expect(reflector.annotations(reflector.getStaticSymbol(file, 'MyComponent'))[0]
|
|
.providers[0]
|
|
.useFactory)
|
|
.toBe(reflector.getStaticSymbol(file, 'myLambda'));
|
|
});
|
|
});
|
|
|
|
// Regression #18170
|
|
it('should continue to aggresively evaluate enum member accessors', () => {
|
|
const data = Object.create(DEFAULT_TEST_DATA);
|
|
const file = '/tmp/src/my_component.ts';
|
|
data[file] = `
|
|
import {Component} from '@angular/core';
|
|
import {intermediate} from './index';
|
|
|
|
@Component({
|
|
template: '<div></div>',
|
|
providers: [{provide: 'foo', useValue: [...intermediate]}]
|
|
})
|
|
export class MyComponent { }
|
|
`;
|
|
data['/tmp/src/intermediate.ts'] = `
|
|
import {MyEnum} from './indirect';
|
|
export const intermediate = [{
|
|
data: {
|
|
c: [MyEnum.Value]
|
|
}
|
|
}];`;
|
|
data['/tmp/src/index.ts'] = `export * from './intermediate';`;
|
|
data['/tmp/src/indirect.ts'] = `export * from './consts';`;
|
|
data['/tmp/src/consts.ts'] = `
|
|
export enum MyEnum {
|
|
Value = 3
|
|
}
|
|
`;
|
|
init(data);
|
|
|
|
expect(reflector.annotations(reflector.getStaticSymbol(file, 'MyComponent'))[0]
|
|
.providers[0]
|
|
.useValue)
|
|
.toEqual([{data: {c: [3]}}]);
|
|
});
|
|
|
|
// Regression #18170
|
|
it('should evaluate enums and statics that are 0', () => {
|
|
const data = Object.create(DEFAULT_TEST_DATA);
|
|
const file = '/tmp/src/my_component.ts';
|
|
data[file] = `
|
|
import {Component} from '@angular/core';
|
|
import {provideRoutes} from './macro';
|
|
import {MyEnum, MyClass} from './consts';
|
|
|
|
@Component({
|
|
template: '<div></div>',
|
|
providers: [provideRoutes({
|
|
path: 'foo',
|
|
data: {
|
|
e: MyEnum.Value
|
|
}
|
|
})]
|
|
})
|
|
export class MyComponent { }
|
|
`;
|
|
data['/tmp/src/macro.ts'] = `
|
|
import {ANALYZE_FOR_ENTRY_COMPONENTS, ROUTES} from '@angular/core';
|
|
|
|
export interface Route {
|
|
path?: string;
|
|
data?: any;
|
|
}
|
|
export type Routes = Route[];
|
|
export function provideRoutes(routes: Routes): any {
|
|
return [
|
|
{provide: ANALYZE_FOR_ENTRY_COMPONENTS, multi: true, useValue: routes},
|
|
{provide: ROUTES, multi: true, useValue: routes},
|
|
];
|
|
}
|
|
`;
|
|
data['/tmp/src/consts.ts'] = `
|
|
export enum MyEnum {
|
|
Value = 0,
|
|
}
|
|
`;
|
|
init(data);
|
|
expect(reflector.annotations(reflector.getStaticSymbol(file, 'MyComponent'))[0]
|
|
.providers[0][0]
|
|
.useValue)
|
|
.toEqual({path: 'foo', data: {e: 0}});
|
|
});
|
|
|
|
// Regression #18170
|
|
it('should eagerly evaluate enums selects', () => {
|
|
const data = Object.create(DEFAULT_TEST_DATA);
|
|
const file = '/tmp/src/my_component.ts';
|
|
data[file] = `
|
|
import {Component} from '@angular/core';
|
|
import {provideRoutes} from './macro';
|
|
import {E} from './indirect';
|
|
|
|
@Component({
|
|
template: '<div></div>',
|
|
providers: [provideRoutes({
|
|
path: 'foo',
|
|
data: {
|
|
e: E.Value,
|
|
}
|
|
})]
|
|
})
|
|
export class MyComponent { }
|
|
`;
|
|
data['/tmp/src/macro.ts'] = `
|
|
import {ANALYZE_FOR_ENTRY_COMPONENTS, ROUTES} from '@angular/core';
|
|
|
|
export interface Route {
|
|
path?: string;
|
|
data?: any;
|
|
}
|
|
export type Routes = Route[];
|
|
export function provideRoutes(routes: Routes): any {
|
|
return [
|
|
{provide: ANALYZE_FOR_ENTRY_COMPONENTS, multi: true, useValue: routes},
|
|
{provide: ROUTES, multi: true, useValue: routes},
|
|
];
|
|
}
|
|
`;
|
|
data['/tmp/src/indirect.ts'] = `
|
|
import {MyEnum} from './consts';
|
|
|
|
export const E = MyEnum;
|
|
`,
|
|
data['/tmp/src/consts.ts'] = `
|
|
export enum MyEnum {
|
|
Value = 1,
|
|
}
|
|
`;
|
|
init(data);
|
|
expect(reflector.annotations(reflector.getStaticSymbol(file, 'MyComponent'))[0]
|
|
.providers[0][0]
|
|
.useValue)
|
|
.toEqual({path: 'foo', data: {e: 1}});
|
|
});
|
|
|
|
// Regression #18170
|
|
it('should aggressively evaluate array indexes', () => {
|
|
const data = Object.create(DEFAULT_TEST_DATA);
|
|
const file = '/tmp/src/my_component.ts';
|
|
data[file] = `
|
|
import {Component} from '@angular/core';
|
|
import {provideRoutes} from './macro';
|
|
import {E} from './indirect';
|
|
|
|
@Component({
|
|
template: '<div></div>',
|
|
providers: [provideRoutes({
|
|
path: 'foo',
|
|
data: {
|
|
e: E[E[E[1]]],
|
|
}
|
|
})]
|
|
})
|
|
export class MyComponent { }
|
|
`;
|
|
data['/tmp/src/macro.ts'] = `
|
|
import {ANALYZE_FOR_ENTRY_COMPONENTS, ROUTES} from '@angular/core';
|
|
|
|
export interface Route {
|
|
path?: string;
|
|
data?: any;
|
|
}
|
|
export type Routes = Route[];
|
|
export function provideRoutes(routes: Routes): any {
|
|
return [
|
|
{provide: ANALYZE_FOR_ENTRY_COMPONENTS, multi: true, useValue: routes},
|
|
{provide: ROUTES, multi: true, useValue: routes},
|
|
];
|
|
}
|
|
`;
|
|
data['/tmp/src/indirect.ts'] = `
|
|
import {A} from './consts';
|
|
|
|
export const E = A;
|
|
`,
|
|
data['/tmp/src/consts.ts'] = `
|
|
export const A = [0, 1];
|
|
`;
|
|
init(data);
|
|
expect(reflector.annotations(reflector.getStaticSymbol(file, 'MyComponent'))[0]
|
|
.providers[0][0]
|
|
.useValue)
|
|
.toEqual({path: 'foo', data: {e: 1}});
|
|
});
|
|
|
|
describe('resolveExternalReference', () => {
|
|
it('should register modules names in the StaticSymbolResolver if no containingFile is given',
|
|
() => {
|
|
init({
|
|
'/tmp/root.ts': ``,
|
|
'/tmp/a.ts': `export const x = 1;`,
|
|
});
|
|
let symbol = reflector.resolveExternalReference(
|
|
{moduleName: './a', name: 'x', runtime: null}, '/tmp/root.ts');
|
|
expect(symbolResolver.getKnownModuleName(symbol.filePath)).toBeFalsy();
|
|
|
|
symbol = reflector.resolveExternalReference({moduleName: 'a', name: 'x', runtime: null});
|
|
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 reference 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 reference 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} = {
|
|
'/tmp/@angular/common/src/forms-deprecated/directives.d.ts': [{
|
|
'__symbolic': 'module',
|
|
'version': METADATA_VERSION,
|
|
'metadata': {
|
|
'FORM_DIRECTIVES': [{
|
|
'__symbolic': 'reference',
|
|
'name': 'NgFor',
|
|
'module': '@angular/common/src/directives/ng_for'
|
|
}]
|
|
}
|
|
}],
|
|
'/tmp/@angular/common/src/directives/ng_for.d.ts': {
|
|
'__symbolic': 'module',
|
|
'version': METADATA_VERSION,
|
|
'metadata': {
|
|
'NgFor': {
|
|
'__symbolic': 'class',
|
|
'decorators': [{
|
|
'__symbolic': 'call',
|
|
'expression': {'__symbolic': 'reference', 'name': 'Directive', 'module': '@angular/core'},
|
|
'arguments': [{
|
|
'selector': '[ngFor][ngForOf]',
|
|
'inputs': ['ngForTrackBy', 'ngForOf', 'ngForTemplate']
|
|
}]
|
|
}],
|
|
'members': {
|
|
'__ctor__': [{
|
|
'__symbolic': 'constructor',
|
|
'parameters': [
|
|
{'__symbolic': 'reference', 'module': '@angular/core', 'name': 'ViewContainerRef'},
|
|
{'__symbolic': 'reference', 'module': '@angular/core', 'name': 'TemplateRef'},
|
|
{'__symbolic': 'reference', 'module': '@angular/core', 'name': 'IterableDiffers'}, {
|
|
'__symbolic': 'reference',
|
|
'module': '@angular/core',
|
|
'name': 'ChangeDetectorRef'
|
|
}
|
|
]
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
'/tmp/@angular/core/src/linker/view_container_ref.d.ts':
|
|
{version: METADATA_VERSION, 'metadata': {'ViewContainerRef': {'__symbolic': 'class'}}},
|
|
'/tmp/@angular/core/src/linker/template_ref.d.ts': {
|
|
version: METADATA_VERSION,
|
|
'module': './template_ref',
|
|
'metadata': {'TemplateRef': {'__symbolic': 'class'}}
|
|
},
|
|
'/tmp/@angular/core/src/change_detection/differs/iterable_differs.d.ts':
|
|
{version: METADATA_VERSION, 'metadata': {'IterableDiffers': {'__symbolic': 'class'}}},
|
|
'/tmp/@angular/core/src/change_detection/change_detector_ref.d.ts':
|
|
{version: METADATA_VERSION, 'metadata': {'ChangeDetectorRef': {'__symbolic': 'class'}}},
|
|
'/tmp/src/app/hero-detail.component.d.ts': {
|
|
'__symbolic': 'module',
|
|
'version': METADATA_VERSION,
|
|
'metadata': {
|
|
'HeroDetailComponent': {
|
|
'__symbolic': 'class',
|
|
'decorators': [{
|
|
'__symbolic': 'call',
|
|
'expression': {'__symbolic': 'reference', 'name': 'Component', 'module': '@angular/core'},
|
|
'arguments': [{
|
|
'selector': 'my-hero-detail',
|
|
'template':
|
|
'\n <div *ngIf="hero">\n <h2>{{hero.name}} details!</h2>\n <div><label>id: </label>{{hero.id}}</div>\n <div>\n <label>name: </label>\n <input [(ngModel)]="hero.name" placeholder="name"/>\n </div>\n </div>\n',
|
|
}]
|
|
}],
|
|
'members': {
|
|
'hero': [{
|
|
'__symbolic': 'property',
|
|
'decorators': [{
|
|
'__symbolic': 'call',
|
|
'expression':
|
|
{'__symbolic': 'reference', 'name': 'Input', 'module': '@angular/core'}
|
|
}]
|
|
}],
|
|
'onMouseOver': [{
|
|
'__symbolic': 'method',
|
|
'decorators': [{
|
|
'__symbolic': 'call',
|
|
'expression':
|
|
{'__symbolic': 'reference', 'module': '@angular/core', 'name': 'HostListener'},
|
|
'arguments': ['mouseover', ['$event']]
|
|
}]
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
},
|
|
'/src/extern.d.ts': {'__symbolic': 'module', 'version': METADATA_VERSION, metadata: {s: 's'}},
|
|
'/tmp/src/error-reporting.d.ts': {
|
|
__symbolic: 'module',
|
|
version: METADATA_VERSION,
|
|
metadata: {
|
|
SomeClass: {
|
|
__symbolic: 'class',
|
|
decorators: [{
|
|
__symbolic: 'call',
|
|
expression: {__symbolic: 'reference', name: 'Component', module: '@angular/core'},
|
|
arguments: [{
|
|
entryComponents: [{
|
|
__symbolic: 'reference',
|
|
module: 'src/error-references',
|
|
name: 'Link1',
|
|
}]
|
|
}]
|
|
}],
|
|
}
|
|
}
|
|
},
|
|
'/tmp/src/error-references.d.ts': {
|
|
__symbolic: 'module',
|
|
version: METADATA_VERSION,
|
|
metadata: {
|
|
Link1: {__symbolic: 'reference', module: 'src/error-references', name: 'Link2'},
|
|
Link2: {__symbolic: 'reference', module: 'src/error-references', name: 'ErrorSym'},
|
|
ErrorSym:
|
|
{__symbolic: 'error', message: 'A reasonable error message', line: 12, character: 33}
|
|
}
|
|
},
|
|
'/tmp/src/function-declaration.d.ts': {
|
|
__symbolic: 'module',
|
|
version: METADATA_VERSION,
|
|
metadata: {
|
|
one: {
|
|
__symbolic: 'function',
|
|
parameters: ['a'],
|
|
value: [{__symbolic: 'reference', name: 'a'}]
|
|
},
|
|
add: {
|
|
__symbolic: 'function',
|
|
parameters: ['a', 'b'],
|
|
value: {
|
|
__symbolic: 'binop',
|
|
operator: '+',
|
|
left: {__symbolic: 'reference', name: 'a'},
|
|
right: {
|
|
__symbolic: 'binop',
|
|
operator: '+',
|
|
left: {__symbolic: 'reference', name: 'b'},
|
|
right: {__symbolic: 'reference', name: 'oneLiteral'}
|
|
}
|
|
}
|
|
},
|
|
oneLiteral: 1
|
|
}
|
|
},
|
|
'/tmp/src/function-reference.ts': {
|
|
__symbolic: 'module',
|
|
version: METADATA_VERSION,
|
|
metadata: {
|
|
one: {
|
|
__symbolic: 'call',
|
|
expression: {__symbolic: 'reference', module: './function-declaration', name: 'one'},
|
|
arguments: ['some-value']
|
|
},
|
|
three: {
|
|
__symbolic: 'call',
|
|
expression: {__symbolic: 'reference', module: './function-declaration', name: 'add'},
|
|
arguments: [1, 1]
|
|
},
|
|
recursion: {
|
|
__symbolic: 'call',
|
|
expression: {__symbolic: 'reference', module: './function-recursive', name: 'recursive'},
|
|
arguments: [1]
|
|
},
|
|
indirectRecursion: {
|
|
__symbolic: 'call',
|
|
expression:
|
|
{__symbolic: 'reference', module: './function-recursive', name: 'indirectRecursion1'},
|
|
arguments: [1]
|
|
}
|
|
}
|
|
},
|
|
'/tmp/src/function-recursive.d.ts': {
|
|
__symbolic: 'modules',
|
|
version: METADATA_VERSION,
|
|
metadata: {
|
|
recursive: {
|
|
__symbolic: 'function',
|
|
parameters: ['a'],
|
|
value: {
|
|
__symbolic: 'call',
|
|
expression: {
|
|
__symbolic: 'reference',
|
|
module: './function-recursive',
|
|
name: 'recursive',
|
|
},
|
|
arguments: [{__symbolic: 'reference', name: 'a'}]
|
|
}
|
|
},
|
|
indirectRecursion1: {
|
|
__symbolic: 'function',
|
|
parameters: ['a'],
|
|
value: {
|
|
__symbolic: 'call',
|
|
expression: {
|
|
__symbolic: 'reference',
|
|
module: './function-recursive',
|
|
name: 'indirectRecursion2',
|
|
},
|
|
arguments: [{__symbolic: 'reference', name: 'a'}]
|
|
}
|
|
},
|
|
indirectRecursion2: {
|
|
__symbolic: 'function',
|
|
parameters: ['a'],
|
|
value: {
|
|
__symbolic: 'call',
|
|
expression: {
|
|
__symbolic: 'reference',
|
|
module: './function-recursive',
|
|
name: 'indirectRecursion1',
|
|
},
|
|
arguments: [{__symbolic: 'reference', name: 'a'}]
|
|
}
|
|
}
|
|
},
|
|
},
|
|
'/tmp/src/spread.ts': {
|
|
__symbolic: 'module',
|
|
version: METADATA_VERSION,
|
|
metadata: {spread: [0, {__symbolic: 'spread', expression: [1, 2, 3, 4]}, 5]}
|
|
},
|
|
'/tmp/src/custom-decorator.ts': `
|
|
export function CustomDecorator(): any {
|
|
return () => {};
|
|
}
|
|
`,
|
|
'/tmp/src/custom-decorator-reference.ts': `
|
|
import {CustomDecorator} from './custom-decorator';
|
|
|
|
@CustomDecorator()
|
|
export class Foo {
|
|
@CustomDecorator() get foo(): string { return ''; }
|
|
}
|
|
`,
|
|
'/tmp/src/invalid-call-definitions.ts': `
|
|
export function someFunction(a: any) {
|
|
if (Array.isArray(a)) {
|
|
return a;
|
|
}
|
|
return undefined;
|
|
}
|
|
`,
|
|
'/tmp/src/invalid-calls.ts': `
|
|
import {someFunction} from './nvalid-call-definitions.ts';
|
|
import {Component} from '@angular/core';
|
|
import {NgIf} from '@angular/common';
|
|
|
|
@Component({
|
|
selector: 'my-component',
|
|
entryComponents: [someFunction([NgIf])]
|
|
})
|
|
export class MyComponent {}
|
|
|
|
@someFunction()
|
|
@Component({
|
|
selector: 'my-component',
|
|
entryComponents: [NgIf]
|
|
})
|
|
export class MyOtherComponent { }
|
|
`,
|
|
'/tmp/src/static-method.ts': `
|
|
import {Component} from '@angular/core/src/metadata';
|
|
|
|
@Component({
|
|
selector: 'stub'
|
|
})
|
|
export class MyModule {
|
|
static with(data: any) {
|
|
return { provider: 'a', useValue: data }
|
|
}
|
|
static condMethod(cond: boolean) {
|
|
return [{ provider: 'a', useValue: cond ? '1' : '2'}];
|
|
}
|
|
static defaultsMethod(a, b = true, c = false) {
|
|
return [a, b, c];
|
|
}
|
|
static withFactory() {
|
|
return { provide: 'c', useFactory: AnotherModule.someFactory };
|
|
}
|
|
}
|
|
|
|
export class AnotherModule {
|
|
static someFactory() {
|
|
return 'e';
|
|
}
|
|
}
|
|
`,
|
|
'/tmp/src/static-method-call.ts': `
|
|
import {Component} from '@angular/core';
|
|
import {MyModule} from './static-method';
|
|
|
|
@Component({
|
|
providers: MyModule.with(100)
|
|
})
|
|
export class MyComponent { }
|
|
|
|
@Component({
|
|
providers: [MyModule.condMethod(true), MyModule.condMethod(false)]
|
|
})
|
|
export class MyCondComponent { }
|
|
|
|
@Component({
|
|
providers: [MyModule.defaultsMethod('a')]
|
|
})
|
|
export class MyDefaultsComponent { }
|
|
|
|
@Component({
|
|
providers: MyModule.withFactory()
|
|
})
|
|
export class MyFactoryComponent { }
|
|
`,
|
|
'/tmp/src/static-field.ts': `
|
|
import {Injectable} from '@angular/core';
|
|
|
|
@Injectable()
|
|
export class MyModule {
|
|
static VALUE = 'Some string';
|
|
}
|
|
`,
|
|
'/tmp/src/macro-function.ts': `
|
|
export function v(value: any) {
|
|
return { provide: 'a', useValue: value };
|
|
}
|
|
`,
|
|
'/tmp/src/call-macro-function.ts': `
|
|
import {Component} from '@angular/core';
|
|
import {v} from './macro-function';
|
|
|
|
@Component({
|
|
providers: v(100)
|
|
})
|
|
export class MyComponent { }
|
|
|
|
@Component({
|
|
providers: v(v(100))
|
|
})
|
|
export class MyComponentNested { }
|
|
`,
|
|
'/tmp/src/static-field-reference.ts': `
|
|
import {Component} from '@angular/core';
|
|
import {MyModule} from './static-field';
|
|
|
|
@Component({
|
|
providers: [ { provider: 'a', useValue: MyModule.VALUE } ]
|
|
})
|
|
export class Foo { }
|
|
`,
|
|
'/tmp/src/static-method-def.ts': `
|
|
export class ClassWithStatics {
|
|
static staticMethod() {}
|
|
}
|
|
`,
|
|
'/tmp/src/static-method-ref.ts': `
|
|
import {Component} from '@angular/core';
|
|
import {ClassWithStatics} from './static-method-def';
|
|
|
|
@Component({
|
|
providers: [ { provider: 'a', useValue: ClassWithStatics.staticMethod}]
|
|
})
|
|
export class MethodReference {
|
|
|
|
}
|
|
`,
|
|
'/tmp/src/invalid-metadata.ts': `
|
|
import {Component} from '@angular/core';
|
|
|
|
@Component({
|
|
providers: [ { provider: 'a', useValue: (() => 1)() }]
|
|
})
|
|
export class InvalidMetadata {}
|
|
`,
|
|
'/tmp/src/forward-ref.ts': `
|
|
import {forwardRef} from '@angular/core';
|
|
import {Component} from '@angular/core';
|
|
import {Inject} from '@angular/core';
|
|
@Component({})
|
|
export class Forward {
|
|
constructor(@Inject(forwardRef(() => Dep)) d: Dep) {}
|
|
}
|
|
export class Dep {
|
|
@Input f: Forward;
|
|
}
|
|
`,
|
|
};
|