angular-cn/packages/compiler-cli/test/metadata/collector_spec.ts

1664 lines
53 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 * as ts from 'typescript';
import {MetadataCollector} from '../../src/metadata/collector';
import {ClassMetadata, ConstructorMetadata, METADATA_VERSION, MetadataEntry, ModuleMetadata, isClassMetadata, isMetadataGlobalReferenceExpression} from '../../src/metadata/schema';
import {Directory, Host, expectValidSources} from './typescript.mocks';
describe('Collector', () => {
const documentRegistry = ts.createDocumentRegistry();
let host: Host;
let service: ts.LanguageService;
let program: ts.Program;
let collector: MetadataCollector;
beforeEach(() => {
host = new Host(FILES, [
'/app/app.component.ts', '/app/cases-data.ts',
'/app/error-cases.ts', '/promise.ts',
'/unsupported-1.ts', '/unsupported-2.ts',
'/unsupported-3.ts', 'class-arity.ts',
'declarations.d.ts', 'import-star.ts',
'exported-classes.ts', 'exported-functions.ts',
'exported-enum.ts', 'exported-type.ts',
'exported-consts.ts', 'local-symbol-ref.ts',
'local-function-ref.ts', 'local-symbol-ref-func.ts',
'private-enum.ts', 're-exports.ts',
're-exports-2.ts', 'export-as.d.ts',
'named-module.d.ts', 'static-field-reference.ts',
'static-method.ts', 'static-method-call.ts',
'static-method-with-if.ts', 'static-method-with-default.ts',
'class-inheritance.ts', 'class-inheritance-parent.ts',
'interface-reference.ts'
]);
service = ts.createLanguageService(host, documentRegistry);
program = service.getProgram();
collector = new MetadataCollector({quotedNames: true});
});
it('should not have errors in test data', () => { expectValidSources(service, program); });
it('should return undefined for modules that have no metadata', () => {
const sourceFile = program.getSourceFile('app/empty.ts') !;
const metadata = collector.getMetadata(sourceFile);
expect(metadata).toBeUndefined();
});
it('should treat all symbols of .d.ts files as exported', () => {
const sourceFile = program.getSourceFile('declarations.d.ts') !;
const metadata = collector.getMetadata(sourceFile);
expect(metadata).toEqual({
__symbolic: 'module',
version: METADATA_VERSION,
metadata: {
DeclaredClass: {__symbolic: 'class'},
declaredFn: {__symbolic: 'function'},
}
});
});
it('should return an interface reference for types', () => {
const sourceFile = program.getSourceFile('/exported-type.ts') !;
const metadata = collector.getMetadata(sourceFile);
expect(metadata).toEqual({
__symbolic: 'module',
version: METADATA_VERSION,
metadata: {SomeType: {__symbolic: 'interface'}}
});
});
it('should return an interface reference for interfaces', () => {
const sourceFile = program.getSourceFile('app/hero.ts') !;
const metadata = collector.getMetadata(sourceFile);
expect(metadata).toEqual({
__symbolic: 'module',
version: METADATA_VERSION,
metadata: {Hero: {__symbolic: 'interface'}}
});
});
it('should preserve module names from TypeScript sources', () => {
const sourceFile = program.getSourceFile('named-module.d.ts') !;
const metadata = collector.getMetadata(sourceFile);
expect(metadata !['importAs']).toEqual('some-named-module');
});
it('should be able to collect a simple component\'s metadata', () => {
const sourceFile = program.getSourceFile('app/hero-detail.component.ts') !;
const metadata = collector.getMetadata(sourceFile);
expect(metadata).toEqual({
__symbolic: 'module',
version: METADATA_VERSION,
metadata: {
HeroDetailComponent: {
__symbolic: 'class',
decorators: [{
__symbolic: 'call',
expression: {
__symbolic: 'reference',
module: 'angular2/core',
name: 'Component',
line: 4,
character: 7
},
arguments: [{
selector: 'my-hero-detail',
template: `
<div *ngIf="hero">
<h2>{{hero.name}} details!</h2>
<div><label>id: </label>{{hero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="hero.name" placeholder="name"/>
</div>
</div>
`
}]
}],
members: {
hero: [{
__symbolic: 'property',
decorators: [{
__symbolic: 'call',
expression: {
__symbolic: 'reference',
module: 'angular2/core',
name: 'Input',
line: 18,
character: 9
}
}]
}]
}
}
}
});
});
it('should be able to get a more complicated component\'s metadata', () => {
const sourceFile = program.getSourceFile('/app/app.component.ts') !;
const metadata = collector.getMetadata(sourceFile);
expect(metadata).toEqual({
__symbolic: 'module',
version: METADATA_VERSION,
metadata: {
AppComponent: {
__symbolic: 'class',
decorators: [{
__symbolic: 'call',
expression: {
__symbolic: 'reference',
module: 'angular2/core',
name: 'Component',
line: 9,
character: 7
},
arguments: [{
selector: 'my-app',
template: `
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="#hero of heroes"
(click)="onSelect(hero)"
[class.selected]="hero === selectedHero">
<span class="badge">{{hero.id | lowercase}}</span> {{hero.name | uppercase}}
</li>
</ul>
<my-hero-detail [hero]="selectedHero"></my-hero-detail>
`,
directives: [
{
__symbolic: 'reference',
module: './hero-detail.component',
name: 'HeroDetailComponent',
line: 22,
character: 21
},
{
__symbolic: 'reference',
module: 'angular2/common',
name: 'NgFor',
line: 22,
character: 42
}
],
providers: [{
__symbolic: 'reference',
module: './hero.service',
default: true,
line: 23,
character: 20
}],
pipes: [
{
__symbolic: 'reference',
module: 'angular2/common',
name: 'LowerCasePipe',
line: 24,
character: 16
},
{
__symbolic: 'reference',
module: 'angular2/common',
name: 'UpperCasePipe',
line: 24,
character: 38
}
]
}]
}],
members: {
__ctor__: [{
__symbolic: 'constructor',
parameters: [{
__symbolic: 'reference',
module: './hero.service',
default: true,
line: 31,
character: 42
}]
}],
onSelect: [{__symbolic: 'method'}],
ngOnInit: [{__symbolic: 'method'}],
getHeroes: [{__symbolic: 'method'}]
}
}
}
});
});
it('should return the values of exported variables', () => {
const sourceFile = program.getSourceFile('/app/mock-heroes.ts') !;
const metadata = collector.getMetadata(sourceFile);
expect(metadata).toEqual({
__symbolic: 'module',
version: METADATA_VERSION,
metadata: {
HEROES: [
{'id': 11, 'name': 'Mr. Nice', '$quoted$': ['id', 'name']},
{'id': 12, 'name': 'Narco', '$quoted$': ['id', 'name']},
{'id': 13, 'name': 'Bombasto', '$quoted$': ['id', 'name']},
{'id': 14, 'name': 'Celeritas', '$quoted$': ['id', 'name']},
{'id': 15, 'name': 'Magneta', '$quoted$': ['id', 'name']},
{'id': 16, 'name': 'RubberMan', '$quoted$': ['id', 'name']},
{'id': 17, 'name': 'Dynama', '$quoted$': ['id', 'name']},
{'id': 18, 'name': 'Dr IQ', '$quoted$': ['id', 'name']},
{'id': 19, 'name': 'Magma', '$quoted$': ['id', 'name']},
{'id': 20, 'name': 'Tornado', '$quoted$': ['id', 'name']}
]
}
});
});
let casesFile: ts.SourceFile;
let casesMetadata: ModuleMetadata;
beforeEach(() => {
casesFile = program.getSourceFile('/app/cases-data.ts') !;
casesMetadata = collector.getMetadata(casesFile) !;
});
it('should provide any reference for an any ctor parameter type', () => {
const casesAny = <ClassMetadata>casesMetadata.metadata['CaseAny'];
expect(casesAny).toBeTruthy();
const ctorData = casesAny.members !['__ctor__'];
expect(ctorData).toEqual(
[{__symbolic: 'constructor', parameters: [{__symbolic: 'reference', name: 'any'}]}]);
});
it('should record annotations on set and get declarations', () => {
const propertyData = (line: number) => ({
name: [{
__symbolic: 'property',
decorators: [{
__symbolic: 'call',
expression:
{__symbolic: 'reference', module: 'angular2/core', name: 'Input', line, character: 9},
arguments: ['firstName']
}]
}]
});
const caseGetProp = <ClassMetadata>casesMetadata.metadata['GetProp'];
expect(caseGetProp.members).toEqual(propertyData(11));
const caseSetProp = <ClassMetadata>casesMetadata.metadata['SetProp'];
expect(caseSetProp.members).toEqual(propertyData(19));
const caseFullProp = <ClassMetadata>casesMetadata.metadata['FullProp'];
expect(caseFullProp.members).toEqual(propertyData(27));
});
it('should record references to parameterized types', () => {
const casesForIn = <ClassMetadata>casesMetadata.metadata['NgFor'];
expect(casesForIn).toEqual({
__symbolic: 'class',
decorators: [{
__symbolic: 'call',
expression: {
__symbolic: 'reference',
module: 'angular2/core',
name: 'Injectable',
line: 40,
character: 7
}
}],
members: {
__ctor__: [{
__symbolic: 'constructor',
parameters: [{
__symbolic: 'reference',
name: 'ClassReference',
arguments: [{__symbolic: 'reference', name: 'NgForRow'}]
}]
}]
}
});
});
it('should report errors for destructured imports', () => {
const unsupported1 = program.getSourceFile('/unsupported-1.ts') !;
const metadata = collector.getMetadata(unsupported1);
expect(metadata).toEqual({
__symbolic: 'module',
version: METADATA_VERSION,
metadata: {
a: {__symbolic: 'error', message: 'Destructuring not supported', line: 1, character: 16},
b: {__symbolic: 'error', message: 'Destructuring not supported', line: 1, character: 19},
c: {__symbolic: 'error', message: 'Destructuring not supported', line: 2, character: 16},
d: {__symbolic: 'error', message: 'Destructuring not supported', line: 2, character: 19},
e: {__symbolic: 'error', message: 'Variable not initialized', line: 3, character: 15}
}
});
});
it('should report an error for references to unexpected types', () => {
const unsupported1 = program.getSourceFile('/unsupported-2.ts') !;
const metadata = collector.getMetadata(unsupported1) !;
const barClass = <ClassMetadata>metadata.metadata['Bar'];
const ctor = <ConstructorMetadata>barClass.members !['__ctor__'][0];
const parameter = ctor.parameters ![0];
expect(parameter).toEqual({
__symbolic: 'error',
message: 'Reference to non-exported class',
line: 3,
character: 4,
context: {className: 'Foo'}
});
});
it('should be able to handle import star type references', () => {
const importStar = program.getSourceFile('/import-star.ts') !;
const metadata = collector.getMetadata(importStar) !;
const someClass = <ClassMetadata>metadata.metadata['SomeClass'];
const ctor = <ConstructorMetadata>someClass.members !['__ctor__'][0];
const parameters = ctor.parameters;
expect(parameters).toEqual([
{__symbolic: 'reference', module: 'angular2/common', name: 'NgFor', line: 6, character: 29}
]);
});
it('should record all exported classes', () => {
const sourceFile = program.getSourceFile('/exported-classes.ts') !;
const metadata = collector.getMetadata(sourceFile);
expect(metadata).toEqual({
__symbolic: 'module',
version: METADATA_VERSION,
metadata: {
SimpleClass: {__symbolic: 'class'},
AbstractClass: {__symbolic: 'class'},
DeclaredClass: {__symbolic: 'class'}
}
});
});
it('should be able to record functions', () => {
const exportedFunctions = program.getSourceFile('/exported-functions.ts') !;
const metadata = collector.getMetadata(exportedFunctions);
expect(metadata).toEqual({
__symbolic: 'module',
version: METADATA_VERSION,
metadata: {
one: {
__symbolic: 'function',
parameters: ['a', 'b', 'c'],
value: {
a: {__symbolic: 'reference', name: 'a'},
b: {__symbolic: 'reference', name: 'b'},
c: {__symbolic: 'reference', name: 'c'}
}
},
two: {
__symbolic: 'function',
parameters: ['a', 'b', 'c'],
value: {
a: {__symbolic: 'reference', name: 'a'},
b: {__symbolic: 'reference', name: 'b'},
c: {__symbolic: 'reference', name: 'c'}
}
},
three: {
__symbolic: 'function',
parameters: ['a', 'b', 'c'],
value: [
{__symbolic: 'reference', name: 'a'}, {__symbolic: 'reference', name: 'b'},
{__symbolic: 'reference', name: 'c'}
]
},
supportsState: {
__symbolic: 'function',
parameters: [],
value: {
__symbolic: 'pre',
operator: '!',
operand: {
__symbolic: 'pre',
operator: '!',
operand: {
__symbolic: 'select',
expression: {
__symbolic: 'select',
expression: {__symbolic: 'reference', name: 'window'},
member: 'history'
},
member: 'pushState'
}
}
}
},
complexFn: {__symbolic: 'function'},
declaredFn: {__symbolic: 'function'}
}
});
});
it('should be able to handle import star type references', () => {
const importStar = program.getSourceFile('/import-star.ts') !;
const metadata = collector.getMetadata(importStar) !;
const someClass = <ClassMetadata>metadata.metadata['SomeClass'];
const ctor = <ConstructorMetadata>someClass.members !['__ctor__'][0];
const parameters = ctor.parameters;
expect(parameters).toEqual([
{__symbolic: 'reference', module: 'angular2/common', name: 'NgFor', line: 6, character: 29}
]);
});
it('should be able to collect the value of an enum', () => {
const enumSource = program.getSourceFile('/exported-enum.ts') !;
const metadata = collector.getMetadata(enumSource) !;
const someEnum: any = metadata.metadata['SomeEnum'];
expect(someEnum).toEqual({A: 0, B: 1, C: 100, D: 101});
});
it('should ignore a non-export enum', () => {
const enumSource = program.getSourceFile('/private-enum.ts') !;
const metadata = collector.getMetadata(enumSource) !;
const publicEnum: any = metadata.metadata['PublicEnum'];
const privateEnum: any = metadata.metadata['PrivateEnum'];
expect(publicEnum).toEqual({a: 0, b: 1, c: 2});
expect(privateEnum).toBeUndefined();
});
it('should be able to collect enums initialized from consts', () => {
const enumSource = program.getSourceFile('/exported-enum.ts') !;
const metadata = collector.getMetadata(enumSource) !;
const complexEnum: any = metadata.metadata['ComplexEnum'];
expect(complexEnum).toEqual({
A: 0,
B: 1,
C: 30,
D: 40,
E: {
__symbolic: 'reference',
module: './exported-consts',
name: 'constValue',
line: 5,
character: 75
}
});
});
it('should be able to collect a simple static method', () => {
const staticSource = program.getSourceFile('/static-method.ts') !;
const metadata = collector.getMetadata(staticSource) !;
expect(metadata).toBeDefined();
const classData = <ClassMetadata>metadata.metadata['MyModule'];
expect(classData).toBeDefined();
expect(classData.statics).toEqual({
with: {
__symbolic: 'function',
parameters: ['comp'],
value: [
{__symbolic: 'reference', name: 'MyModule'},
{provider: 'a', useValue: {__symbolic: 'reference', name: 'comp'}}
]
}
});
});
it('should be able to collect a call to a static method', () => {
const staticSource = program.getSourceFile('/static-method-call.ts') !;
const metadata = collector.getMetadata(staticSource) !;
expect(metadata).toBeDefined();
const classData = <ClassMetadata>metadata.metadata['Foo'];
expect(classData).toBeDefined();
expect(classData.decorators).toEqual([{
__symbolic: 'call',
expression: {
__symbolic: 'reference',
module: 'angular2/core',
name: 'Component',
line: 4,
character: 5
},
arguments: [{
providers: {
__symbolic: 'call',
expression: {
__symbolic: 'select',
expression: {
__symbolic: 'reference',
module: './static-method',
name: 'MyModule',
line: 5,
character: 17
},
member: 'with'
},
arguments: ['a']
}
}]
}]);
});
it('should be able to collect a static field', () => {
const staticSource = program.getSourceFile('/static-field.ts') !;
const metadata = collector.getMetadata(staticSource) !;
expect(metadata).toBeDefined();
const classData = <ClassMetadata>metadata.metadata['MyModule'];
expect(classData).toBeDefined();
expect(classData.statics).toEqual({VALUE: 'Some string'});
});
it('should be able to collect a reference to a static field', () => {
const staticSource = program.getSourceFile('/static-field-reference.ts') !;
const metadata = collector.getMetadata(staticSource) !;
expect(metadata).toBeDefined();
const classData = <ClassMetadata>metadata.metadata['Foo'];
expect(classData).toBeDefined();
expect(classData.decorators).toEqual([{
__symbolic: 'call',
expression: {
__symbolic: 'reference',
module: 'angular2/core',
name: 'Component',
line: 4,
character: 5
},
arguments: [{
providers: [{
provide: 'a',
useValue: {
__symbolic: 'select',
expression: {
__symbolic: 'reference',
module: './static-field',
name: 'MyModule',
line: 5,
character: 45
},
member: 'VALUE'
}
}]
}]
}]);
});
it('should be able to collect a method with a conditional expression', () => {
const source = program.getSourceFile('/static-method-with-if.ts') !;
const metadata = collector.getMetadata(source) !;
expect(metadata).toBeDefined();
const classData = <ClassMetadata>metadata.metadata['MyModule'];
expect(classData).toBeDefined();
expect(classData.statics).toEqual({
with: {
__symbolic: 'function',
parameters: ['cond'],
value: [
{__symbolic: 'reference', name: 'MyModule'}, {
provider: 'a',
useValue: {
__symbolic: 'if',
condition: {__symbolic: 'reference', name: 'cond'},
thenExpression: '1',
elseExpression: '2'
}
}
]
}
});
});
it('should be able to collect a method with a default parameter', () => {
const source = program.getSourceFile('/static-method-with-default.ts') !;
const metadata = collector.getMetadata(source) !;
expect(metadata).toBeDefined();
const classData = <ClassMetadata>metadata.metadata['MyModule'];
expect(classData).toBeDefined();
expect(classData.statics).toEqual({
with: {
__symbolic: 'function',
parameters: ['comp', 'foo', 'bar'],
defaults: [undefined, true, false],
value: [
{__symbolic: 'reference', name: 'MyModule'}, {
__symbolic: 'if',
condition: {__symbolic: 'reference', name: 'foo'},
thenExpression: {provider: 'a', useValue: {__symbolic: 'reference', name: 'comp'}},
elseExpression: {provider: 'b', useValue: {__symbolic: 'reference', name: 'comp'}}
},
{
__symbolic: 'if',
condition: {__symbolic: 'reference', name: 'bar'},
thenExpression: {provider: 'c', useValue: {__symbolic: 'reference', name: 'comp'}},
elseExpression: {provider: 'd', useValue: {__symbolic: 'reference', name: 'comp'}}
}
]
}
});
});
it('should be able to collect re-exported symbols', () => {
const source = program.getSourceFile('/re-exports.ts') !;
const metadata = collector.getMetadata(source) !;
expect(metadata.exports).toEqual([
{from: './static-field', export: ['MyModule']},
{from: './static-field-reference', export: [{name: 'Foo', as: 'OtherModule'}]},
{from: 'angular2/core'}
]);
});
it('should be able to collect a export as symbol', () => {
const source = program.getSourceFile('export-as.d.ts') !;
const metadata = collector.getMetadata(source) !;
expect(metadata.metadata).toEqual({SomeFunction: {__symbolic: 'function'}});
});
it('should be able to collect exports with no module specifier', () => {
const source = program.getSourceFile('/re-exports-2.ts') !;
const metadata = collector.getMetadata(source) !;
expect(metadata.metadata).toEqual({
MyClass: Object({__symbolic: 'class'}),
OtherModule: {
__symbolic: 'reference',
module: './static-field-reference',
name: 'Foo',
line: 4,
character: 12
},
MyOtherModule: {
__symbolic: 'reference',
module: './static-field',
name: 'MyModule',
line: 4,
character: 25
}
});
});
it('should collect an error symbol if collecting a reference to a non-exported symbol', () => {
const source = program.getSourceFile('/local-symbol-ref.ts') !;
const metadata = collector.getMetadata(source) !;
expect(metadata.metadata).toEqual({
REQUIRED_VALIDATOR: {
__symbolic: 'error',
message: 'Reference to a local symbol',
line: 3,
character: 8,
context: {name: 'REQUIRED'}
},
SomeComponent: {
__symbolic: 'class',
decorators: [{
__symbolic: 'call',
expression: {
__symbolic: 'reference',
module: 'angular2/core',
name: 'Component',
line: 11,
character: 5
},
arguments: [{providers: [{__symbolic: 'reference', name: 'REQUIRED_VALIDATOR'}]}]
}]
}
});
});
it('should collect an error symbol if collecting a reference to a non-exported function', () => {
const source = program.getSourceFile('/local-function-ref.ts') !;
const metadata = collector.getMetadata(source) !;
expect(metadata.metadata).toEqual({
REQUIRED_VALIDATOR: {
__symbolic: 'error',
message: 'Reference to a non-exported function',
line: 3,
character: 13,
context: {name: 'required'}
},
SomeComponent: {
__symbolic: 'class',
decorators: [{
__symbolic: 'call',
expression: {
__symbolic: 'reference',
module: 'angular2/core',
name: 'Component',
line: 11,
character: 5
},
arguments: [{providers: [{__symbolic: 'reference', name: 'REQUIRED_VALIDATOR'}]}]
}]
}
});
});
it('should collect an error for a simple function that references a local variable', () => {
const source = program.getSourceFile('/local-symbol-ref-func.ts') !;
const metadata = collector.getMetadata(source) !;
expect(metadata.metadata).toEqual({
foo: {
__symbolic: 'function',
parameters: ['index'],
value: {
__symbolic: 'error',
message: 'Reference to a local symbol',
line: 1,
character: 8,
context: {name: 'localSymbol'}
}
}
});
});
it('should collect any for interface parameter reference', () => {
const source = program.getSourceFile('/interface-reference.ts') !;
const metadata = collector.getMetadata(source) !;
expect((metadata.metadata['SomeClass'] as ClassMetadata).members).toEqual({
__ctor__: [{
__symbolic: 'constructor',
parameterDecorators: [[{
__symbolic: 'call',
expression: {
__symbolic: 'reference',
module: 'angular2/core',
name: 'Inject',
line: 6,
character: 19
},
arguments: ['a']
}]],
parameters: [{__symbolic: 'reference', name: 'any'}]
}]
});
});
describe('with interpolations', () => {
function e(expr: string, prefix?: string) {
const metadata = collectSource(`${prefix || ''} export let value = ${expr};`);
return expect(metadata.metadata['value']);
}
it('should be able to collect a raw interpolated string',
() => { e('`simple value`').toBe('simple value'); });
it('should be able to interpolate a single value',
() => { e('`${foo}`', 'const foo = "foo value"').toBe('foo value'); });
it('should be able to interpolate multiple values', () => {
e('`foo:${foo}, bar:${bar}, end`', 'const foo = "foo"; const bar = "bar";')
.toBe('foo:foo, bar:bar, end');
});
it('should be able to interpolate with an imported reference', () => {
e('`external:${external}`', 'import {external} from "./external";').toEqual({
__symbolic: 'binop',
operator: '+',
left: 'external:',
right: {
__symbolic: 'reference',
module: './external',
name: 'external',
line: 0,
character: 68,
}
});
});
it('should simplify a redundant template', () => {
e('`${external}`', 'import {external} from "./external";').toEqual({
__symbolic: 'reference',
module: './external',
name: 'external',
line: 0,
character: 59
});
});
it('should be able to collect complex template with imported references', () => {
e('`foo:${foo}, bar:${bar}, end`', 'import {foo, bar} from "./external";').toEqual({
__symbolic: 'binop',
operator: '+',
left: {
__symbolic: 'binop',
operator: '+',
left: {
__symbolic: 'binop',
operator: '+',
left: {
__symbolic: 'binop',
operator: '+',
left: 'foo:',
right: {
__symbolic: 'reference',
module: './external',
name: 'foo',
line: 0,
character: 63
}
},
right: ', bar:'
},
right:
{__symbolic: 'reference', module: './external', name: 'bar', line: 0, character: 75}
},
right: ', end'
});
});
it('should reject a tagged literal', () => {
e('tag`some value`').toEqual({
__symbolic: 'error',
message: 'Tagged template expressions are not supported in metadata',
line: 0,
character: 20
});
});
});
it('should ignore |null or |undefined in type expressions', () => {
const metadata = collectSource(`
import {Foo} from './foo';
export class SomeClass {
constructor (a: Foo, b: Foo | null, c: Foo | undefined, d: Foo | undefined | null, e: Foo | undefined | null | Foo) {}
}
`);
expect((metadata.metadata['SomeClass'] as ClassMetadata).members).toEqual({
__ctor__: [{
__symbolic: 'constructor',
parameters: [
{__symbolic: 'reference', module: './foo', name: 'Foo', line: 3, character: 24},
{__symbolic: 'reference', module: './foo', name: 'Foo', line: 3, character: 24},
{__symbolic: 'reference', module: './foo', name: 'Foo', line: 3, character: 24},
{__symbolic: 'reference', module: './foo', name: 'Foo', line: 3, character: 24},
{__symbolic: 'reference', module: './foo', name: 'Foo', line: 3, character: 24}
]
}]
});
});
it('should treat exported class expressions as a class', () => {
const source = ts.createSourceFile(
'', `
export const InjectionToken: {new<T>(desc: string): InjectionToken<T>;} = class {
constructor(protected _desc: string) {}
toString(): string { return \`InjectionToken \${this._desc}\`; }
} as any;`,
ts.ScriptTarget.Latest, true);
const metadata = collector.getMetadata(source) !;
expect(metadata.metadata).toEqual({InjectionToken: {__symbolic: 'class'}});
});
describe('in strict mode', () => {
it('should throw if an error symbol is collecting a reference to a non-exported symbol', () => {
const source = program.getSourceFile('/local-symbol-ref.ts') !;
expect(() => collector.getMetadata(source, true)).toThrowError(/Reference to a local symbol/);
});
it('should throw if an error if collecting a reference to a non-exported function', () => {
const source = program.getSourceFile('/local-function-ref.ts') !;
expect(() => collector.getMetadata(source, true))
.toThrowError(/Reference to a non-exported function/);
});
it('should throw for references to unexpected types', () => {
const unsupported2 = program.getSourceFile('/unsupported-2.ts') !;
expect(() => collector.getMetadata(unsupported2, true))
.toThrowError(/Reference to non-exported class/);
});
it('should throw for errors in a static method', () => {
const unsupported3 = program.getSourceFile('/unsupported-3.ts') !;
expect(() => collector.getMetadata(unsupported3, true))
.toThrowError(/Reference to a non-exported class/);
});
});
describe('with invalid input', () => {
it('should not throw with a class with no name', () => {
const fileName = '/invalid-class.ts';
override(fileName, 'export class');
const invalidClass = program.getSourceFile(fileName) !;
expect(() => collector.getMetadata(invalidClass)).not.toThrow();
});
it('should not throw with a function with no name', () => {
const fileName = '/invalid-function.ts';
override(fileName, 'export function');
const invalidFunction = program.getSourceFile(fileName) !;
expect(() => collector.getMetadata(invalidFunction)).not.toThrow();
});
});
describe('inheritance', () => {
it('should record `extends` clauses for declared classes', () => {
const metadata = collector.getMetadata(program.getSourceFile('/class-inheritance.ts') !) !;
expect(metadata.metadata['DeclaredChildClass'])
.toEqual({__symbolic: 'class', extends: {__symbolic: 'reference', name: 'ParentClass'}});
});
it('should record `extends` clauses for classes in the same file', () => {
const metadata = collector.getMetadata(program.getSourceFile('/class-inheritance.ts') !) !;
expect(metadata.metadata['ChildClassSameFile'])
.toEqual({__symbolic: 'class', extends: {__symbolic: 'reference', name: 'ParentClass'}});
});
it('should record `extends` clauses for classes in a different file', () => {
const metadata = collector.getMetadata(program.getSourceFile('/class-inheritance.ts') !) !;
expect(metadata.metadata['ChildClassOtherFile']).toEqual({
__symbolic: 'class',
extends: {
__symbolic: 'reference',
module: './class-inheritance-parent',
name: 'ParentClassFromOtherFile',
line: 9,
character: 45,
}
});
});
function expectClass(entry: MetadataEntry): entry is ClassMetadata {
const result = isClassMetadata(entry);
expect(result).toBeTruthy();
return result;
}
it('should collect the correct arity for a class', () => {
const metadata = collector.getMetadata(program.getSourceFile('/class-arity.ts') !) !;
const zero = metadata.metadata['Zero'];
if (expectClass(zero)) expect(zero.arity).toBeUndefined();
const one = metadata.metadata['One'];
if (expectClass(one)) expect(one.arity).toBe(1);
const two = metadata.metadata['Two'];
if (expectClass(two)) expect(two.arity).toBe(2);
const three = metadata.metadata['Three'];
if (expectClass(three)) expect(three.arity).toBe(3);
const nine = metadata.metadata['Nine'];
if (expectClass(nine)) expect(nine.arity).toBe(9);
});
});
describe('regerssion', () => {
it('should be able to collect a short-hand property value', () => {
const metadata = collectSource(`
const children = { f1: 1 };
export const r = [
{path: ':locale', children}
];
`);
expect(metadata.metadata).toEqual({r: [{path: ':locale', children: {f1: 1}}]});
});
// #17518
it('should skip a default function', () => {
const metadata = collectSource(`
export default function () {
const mainRoutes = [
{name: 'a', abstract: true, component: 'main'},
{name: 'a.welcome', url: '/welcome', component: 'welcome'}
];
return mainRoutes;
}`);
expect(metadata).toBeUndefined();
});
it('should skip a named default export', () => {
const metadata = collectSource(`
function mainRoutes() {
const mainRoutes = [
{name: 'a', abstract: true, component: 'main'},
{name: 'a.welcome', url: '/welcome', component: 'welcome'}
];
return mainRoutes;
}
exports = foo;
`);
expect(metadata).toBeUndefined();
});
it('should collect type guards', () => {
const metadata = collectSource(`
import {Directive, Input, TemplateRef} from '@angular/core';
@Directive({selector: '[myIf]'})
export class MyIf {
constructor(private templateRef: TemplateRef) {}
@Input() myIf: any;
static typeGuard: <T>(v: T | null | undefined): v is T;
}
`);
expect((metadata.metadata.MyIf as any).statics.typeGuard)
.not.toBeUndefined('typeGuard was not collected');
});
it('should be able to collect an invalid access expression', () => {
const source = createSource(`
import {Component} from '@angular/core';
const value = [];
@Component({
provider: [{provide: 'some token', useValue: value[]}]
})
export class MyComponent {}
`);
const metadata = collector.getMetadata(source) !;
expect(metadata.metadata.MyComponent).toEqual({
__symbolic: 'class',
decorators: [{
__symbolic: 'call',
expression: {
__symbolic: 'reference',
module: '@angular/core',
name: 'Component',
line: 4,
character: 9
},
arguments: [{
__symbolic: 'error',
message: 'Expression form not supported',
line: 5,
character: 55
}]
}]
});
});
});
describe('references', () => {
beforeEach(() => { collector = new MetadataCollector({quotedNames: true}); });
it('should record a reference to an exported field of a useValue', () => {
const metadata = collectSource(`
export var someValue = 1;
export const v = {
useValue: someValue
};
`);
expect(metadata.metadata['someValue']).toEqual(1);
expect(metadata.metadata['v']).toEqual({
useValue: {__symbolic: 'reference', name: 'someValue'}
});
});
it('should leave external references in place in an object literal', () => {
const metadata = collectSource(`
export const myLambda = () => [1, 2, 3];
const indirect = [{a: 1, b: 3: c: myLambda}];
export const v = {
v: {i: indirect}
}
`);
expect(metadata.metadata['v']).toEqual({
v: {i: [{a: 1, b: 3, c: {__symbolic: 'reference', name: 'myLambda'}}]}
});
});
it('should leave an external reference in place in an array literal', () => {
const metadata = collectSource(`
export const myLambda = () => [1, 2, 3];
const indirect = [1, 3, myLambda}];
export const v = {
v: {i: indirect}
}
`);
expect(metadata.metadata['v']).toEqual({
v: {i: [1, 3, {__symbolic: 'reference', name: 'myLambda'}]}
});
});
});
describe('substitutions', () => {
const lambdaTemp = 'lambdaTemp';
it('should be able to substitute a lambda', () => {
const source = createSource(`
const b = 1;
export const a = () => b;
`);
const metadata = collector.getMetadata(source, /* strict */ false, (value, node) => {
if (node.kind === ts.SyntaxKind.ArrowFunction) {
return {__symbolic: 'reference', name: lambdaTemp};
}
return value;
});
expect(metadata !.metadata['a']).toEqual({__symbolic: 'reference', name: lambdaTemp});
});
it('should compose substitution functions', () => {
const collector = new MetadataCollector({
substituteExpression: (value, node) => isMetadataGlobalReferenceExpression(value) &&
value.name == lambdaTemp ?
{__symbolic: 'reference', name: value.name + '2'} :
value
});
const source = createSource(`
const b = 1;
export const a = () => b;
`);
const metadata = collector.getMetadata(source, /* strict */ false, (value, node) => {
if (node.kind === ts.SyntaxKind.ArrowFunction) {
return {__symbolic: 'reference', name: lambdaTemp};
}
return value;
});
expect(metadata !.metadata['a']).toEqual({__symbolic: 'reference', name: lambdaTemp + '2'});
});
});
function override(fileName: string, content: string) {
host.overrideFile(fileName, content);
host.addFile(fileName);
program = service.getProgram();
}
function collectSource(content: string): ModuleMetadata {
const sourceFile = createSource(content);
return collector.getMetadata(sourceFile) !;
}
});
// TODO: Do not use \` in a template literal as it confuses clang-format
const FILES: Directory = {
'app': {
'app.component.ts': `
import {Component as MyComponent, OnInit} from 'angular2/core';
import * as common from 'angular2/common';
import {Hero} from './hero';
import {HeroDetailComponent} from './hero-detail.component';
import HeroService from './hero.service';
// thrown away
import 'angular2/core';
@MyComponent({
selector: 'my-app',
template:` +
'`' +
`
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="#hero of heroes"
(click)="onSelect(hero)"
[class.selected]="hero === selectedHero">
<span class="badge">{{hero.id | lowercase}}</span> {{hero.name | uppercase}}
</li>
</ul>
<my-hero-detail [hero]="selectedHero"></my-hero-detail>
` +
'`' +
`,
directives: [HeroDetailComponent, common.NgFor],
providers: [HeroService],
pipes: [common.LowerCasePipe, common.UpperCasePipe]
})
export class AppComponent implements OnInit {
public title = 'Tour of Heroes';
public heroes: Hero[];
public selectedHero: Hero;
constructor(private _heroService: HeroService) { }
onSelect(hero: Hero) { this.selectedHero = hero; }
ngOnInit() {
this.getHeroes()
}
getHeroes() {
this._heroService.getHeroesSlowly().then(heroes => this.heroes = heroes);
}
}`,
'hero.ts': `
export interface Hero {
id: number;
name: string;
}`,
'empty.ts': ``,
'hero-detail.component.ts': `
import {Component, Input} from 'angular2/core';
import {Hero} from './hero';
@Component({
selector: 'my-hero-detail',
template: ` +
'`' +
`
<div *ngIf="hero">
<h2>{{hero.name}} details!</h2>
<div><label>id: </label>{{hero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="hero.name" placeholder="name"/>
</div>
</div>
` +
'`' +
`,
})
export class HeroDetailComponent {
@Input() public hero: Hero;
}`,
'mock-heroes.ts': `
import {Hero as Hero} from './hero';
export const HEROES: Hero[] = [
{"id": 11, "name": "Mr. Nice"},
{"id": 12, "name": "Narco"},
{"id": 13, "name": "Bombasto"},
{"id": 14, "name": "Celeritas"},
{"id": 15, "name": "Magneta"},
{"id": 16, "name": "RubberMan"},
{"id": 17, "name": "Dynama"},
{"id": 18, "name": "Dr IQ"},
{"id": 19, "name": "Magma"},
{"id": 20, "name": "Tornado"}
];`,
'default-exporter.ts': `
let a: string;
export default a;
`,
'hero.service.ts': `
import {Injectable} from 'angular2/core';
import {HEROES} from './mock-heroes';
import {Hero} from './hero';
@Injectable()
class HeroService {
getHeros() {
return Promise.resolve(HEROES);
}
getHeroesSlowly() {
return new Promise<Hero[]>(resolve =>
setTimeout(()=>resolve(HEROES), 2000)); // 2 seconds
}
}
export default HeroService;`,
'cases-data.ts': `
import {Injectable, Input} from 'angular2/core';
@Injectable()
export class CaseAny {
constructor(param: any) {}
}
@Injectable()
export class GetProp {
private _name: string;
@Input('firstName') get name(): string {
return this._name;
}
}
@Injectable()
export class SetProp {
private _name: string;
@Input('firstName') set name(value: string) {
this._name = value;
}
}
@Injectable()
export class FullProp {
private _name: string;
@Input('firstName') get name(): string {
return this._name;
}
set name(value: string) {
this._name = value;
}
}
export class ClassReference<T> { }
export class NgForRow {
}
@Injectable()
export class NgFor {
constructor (public ref: ClassReference<NgForRow>) {}
}
`,
'error-cases.ts': `
import HeroService from './hero.service';
export class CaseCtor {
constructor(private _heroService: HeroService) { }
}
`
},
'promise.ts': `
interface PromiseLike<T> {
then<TResult>(onfulfilled?: (value: T) => TResult | PromiseLike<TResult>, onrejected?: (reason: any) => TResult | PromiseLike<TResult>): PromiseLike<TResult>;
then<TResult>(onfulfilled?: (value: T) => TResult | PromiseLike<TResult>, onrejected?: (reason: any) => void): PromiseLike<TResult>;
}
interface Promise<T> {
then<TResult>(onfulfilled?: (value: T) => TResult | PromiseLike<TResult>, onrejected?: (reason: any) => TResult | PromiseLike<TResult>): Promise<TResult>;
then<TResult>(onfulfilled?: (value: T) => TResult | PromiseLike<TResult>, onrejected?: (reason: any) => void): Promise<TResult>;
catch(onrejected?: (reason: any) => T | PromiseLike<T>): Promise<T>;
catch(onrejected?: (reason: any) => void): Promise<T>;
}
interface PromiseConstructor {
prototype: Promise<any>;
new <T>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void): Promise<T>;
reject(reason: any): Promise<void>;
reject<T>(reason: any): Promise<T>;
resolve<T>(value: T | PromiseLike<T>): Promise<T>;
resolve(): Promise<void>;
}
declare var Promise: PromiseConstructor;
`,
'class-arity.ts': `
export class Zero {}
export class One<T> {}
export class Two<T, V> {}
export class Three<T1, T2, T3> {}
export class Nine<T1, T2, T3, T4, T5, T6, T7, T8, T9> {}
`,
'unsupported-1.ts': `
export let {a, b} = {a: 1, b: 2};
export let [c, d] = [1, 2];
export let e;
`,
'unsupported-2.ts': `
import {Injectable} from 'angular2/core';
class Foo {}
@Injectable()
export class Bar {
constructor(private f: Foo) {}
}
`,
'unsupported-3.ts': `
class Foo {}
export class SomeClass {
static someStatic() {
return Foo;
}
}
`,
'interface-reference.ts': `
import {Injectable, Inject} from 'angular2/core';
export interface Test {}
@Injectable()
export class SomeClass {
constructor(@Inject("a") test: Test) {}
}
`,
'import-star.ts': `
import {Injectable} from 'angular2/core';
import * as common from 'angular2/common';
@Injectable()
export class SomeClass {
constructor(private f: common.NgFor) {}
}
`,
'declarations.d.ts': `
declare class DeclaredClass {}
declare function declaredFn();
`,
'exported-classes.ts': `
export class SimpleClass {}
export abstract class AbstractClass {}
export declare class DeclaredClass {}
`,
'class-inheritance-parent.ts': `
export class ParentClassFromOtherFile {}
`,
'class-inheritance.ts': `
import {ParentClassFromOtherFile} from './class-inheritance-parent';
export class ParentClass {}
export declare class DeclaredChildClass extends ParentClass {}
export class ChildClassSameFile extends ParentClass {}
export class ChildClassOtherFile extends ParentClassFromOtherFile {}
`,
'exported-functions.ts': `
export function one(a: string, b: string, c: string) {
return {a: a, b: b, c: c};
}
export function two(a: string, b: string, c: string) {
return {a, b, c};
}
export function three({a, b, c}: {a: string, b: string, c: string}) {
return [a, b, c];
}
export function supportsState(): boolean {
return !!window.history.pushState;
}
export function complexFn(x: any): boolean {
if (x) {
return true;
} else {
return false;
}
}
export declare function declaredFn();
`,
'exported-type.ts': `
export type SomeType = 'a' | 'b';
`,
'exported-enum.ts': `
import {constValue} from './exported-consts';
export const someValue = 30;
export enum SomeEnum { A, B, C = 100, D };
export enum ComplexEnum { A, B, C = someValue, D = someValue + 10, E = constValue };
`,
'exported-consts.ts': `
export const constValue = 100;
`,
'static-method.ts': `
export class MyModule {
static with(comp: any): any[] {
return [
MyModule,
{ provider: 'a', useValue: comp }
];
}
}
`,
'static-method-with-default.ts': `
export class MyModule {
static with(comp: any, foo: boolean = true, bar: boolean = false): any[] {
return [
MyModule,
foo ? { provider: 'a', useValue: comp } : {provider: 'b', useValue: comp},
bar ? { provider: 'c', useValue: comp } : {provider: 'd', useValue: comp}
];
}
}
`,
'static-method-call.ts': `
import {Component} from 'angular2/core';
import {MyModule} from './static-method';
@Component({
providers: MyModule.with('a')
})
export class Foo { }
`,
'static-field.ts': `
export class MyModule {
static VALUE = 'Some string';
}
`,
'static-field-reference.ts': `
import {Component} from 'angular2/core';
import {MyModule} from './static-field';
@Component({
providers: [ { provide: 'a', useValue: MyModule.VALUE } ]
})
export class Foo { }
`,
'static-method-with-if.ts': `
export class MyModule {
static with(cond: boolean): any[] {
return [
MyModule,
{ provider: 'a', useValue: cond ? '1' : '2' }
];
}
}
`,
're-exports.ts': `
export {MyModule} from './static-field';
export {Foo as OtherModule} from './static-field-reference';
export * from 'angular2/core';
`,
're-exports-2.ts': `
import {MyModule} from './static-field';
import {Foo as OtherModule} from './static-field-reference';
class MyClass {}
export {OtherModule, MyModule as MyOtherModule, MyClass};
`,
'export-as.d.ts': `
declare function someFunction(): void;
export { someFunction as SomeFunction };
`,
'named-module.d.ts': `
/// <amd-module name="some-named-module" />
export type SomeType = 'a';
`,
'local-symbol-ref.ts': `
import {Component, Validators} from 'angular2/core';
var REQUIRED;
export const REQUIRED_VALIDATOR: any = {
provide: 'SomeToken',
useValue: REQUIRED,
multi: true
};
@Component({
providers: [REQUIRED_VALIDATOR]
})
export class SomeComponent {}
`,
'private-enum.ts': `
export enum PublicEnum { a, b, c }
enum PrivateEnum { e, f, g }
`,
'local-function-ref.ts': `
import {Component, Validators} from 'angular2/core';
function required() {}
export const REQUIRED_VALIDATOR: any = {
provide: 'SomeToken',
useValue: required,
multi: true
};
@Component({
providers: [REQUIRED_VALIDATOR]
})
export class SomeComponent {}
`,
'local-symbol-ref-func.ts': `
var localSymbol: any[];
export function foo(index: number): string {
return localSymbol[index];
}
`,
'node_modules': {
'angular2': {
'core.d.ts': `
export interface Type extends Function { }
export interface TypeDecorator {
<T extends Type>(type: T): T;
(target: Object, propertyKey?: string | symbol, parameterIndex?: number): void;
annotations: any[];
}
export interface ComponentDecorator extends TypeDecorator { }
export interface ComponentFactory {
(obj: {
selector?: string;
inputs?: string[];
outputs?: string[];
properties?: string[];
events?: string[];
host?: {
[key: string]: string;
};
bindings?: any[];
providers?: any[];
exportAs?: string;
moduleId?: string;
queries?: {
[key: string]: any;
};
viewBindings?: any[];
viewProviders?: any[];
templateUrl?: string;
template?: string;
styleUrls?: string[];
styles?: string[];
directives?: Array<Type | any[]>;
pipes?: Array<Type | any[]>;
}): ComponentDecorator;
}
export declare var Component: ComponentFactory;
export interface InputFactory {
(bindingPropertyName?: string): any;
new (bindingPropertyName?: string): any;
}
export declare var Input: InputFactory;
export interface InjectableFactory {
(): any;
}
export declare var Injectable: InjectableFactory;
export interface InjectFactory {
(binding?: any): any;
new (binding?: any): any;
}
export declare var Inject: InjectFactory;
export interface OnInit {
ngOnInit(): any;
}
export class Validators {
static required(): void;
}
`,
'common.d.ts': `
export declare class NgFor {
ngForOf: any;
ngForTemplate: any;
ngDoCheck(): void;
}
export declare class LowerCasePipe {
transform(value: string, args?: any[]): string;
}
export declare class UpperCasePipe {
transform(value: string, args?: any[]): string;
}
`
}
}
};
function createSource(text: string): ts.SourceFile {
return ts.createSourceFile('', text, ts.ScriptTarget.Latest, true);
}