feat(compiler): Allow calls to simple static methods (#10289)

Closes: #10266
This commit is contained in:
Chuck Jazdzewski
2016-07-26 10:18:35 -07:00
committed by GitHub
parent 0aba42ae5b
commit b449467940
5 changed files with 168 additions and 24 deletions

View File

@ -1,7 +1,7 @@
import * as ts from 'typescript';
import {Evaluator, errorSymbol, isPrimitive} from './evaluator';
import {ClassMetadata, ConstructorMetadata, MemberMetadata, MetadataError, MetadataMap, MetadataSymbolicExpression, MetadataSymbolicReferenceExpression, MetadataSymbolicSelectExpression, MetadataValue, MethodMetadata, ModuleMetadata, VERSION, isMetadataError, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSelectExpression} from './schema';
import {ClassMetadata, ConstructorMetadata, MemberMetadata, MetadataError, MetadataMap, MetadataObject, MetadataSymbolicExpression, MetadataSymbolicReferenceExpression, MetadataSymbolicSelectExpression, MetadataValue, MethodMetadata, ModuleMetadata, VERSION, isMetadataError, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSelectExpression} from './schema';
import {Symbols} from './symbols';
@ -30,6 +30,31 @@ export class MetadataCollector {
return errorSymbol(message, node, context, sourceFile);
}
function maybeGetSimpleFunction(
functionDeclaration: ts.FunctionDeclaration |
ts.MethodDeclaration): {func: MetadataValue, name: string}|undefined {
if (functionDeclaration.name.kind == ts.SyntaxKind.Identifier) {
const nameNode = <ts.Identifier>functionDeclaration.name;
const functionName = nameNode.text;
const functionBody = functionDeclaration.body;
if (functionBody && functionBody.statements.length == 1) {
const statement = functionBody.statements[0];
if (statement.kind === ts.SyntaxKind.ReturnStatement) {
const returnStatement = <ts.ReturnStatement>statement;
if (returnStatement.expression) {
return {
name: functionName, func: {
__symbolic: 'function',
parameters: namesOf(functionDeclaration.parameters),
value: evaluator.evaluateNode(returnStatement.expression)
}
}
}
}
}
}
}
function classMetadataOf(classDeclaration: ts.ClassDeclaration): ClassMetadata {
let result: ClassMetadata = {__symbolic: 'class'};
@ -63,6 +88,14 @@ export class MetadataCollector {
data.push(metadata);
members[name] = data;
}
// static member
let statics: MetadataObject = null;
function recordStaticMember(name: string, value: MetadataValue) {
if (!statics) statics = {};
statics[name] = value;
}
for (const member of classDeclaration.members) {
let isConstructor = false;
switch (member.kind) {
@ -70,6 +103,13 @@ export class MetadataCollector {
case ts.SyntaxKind.MethodDeclaration:
isConstructor = member.kind === ts.SyntaxKind.Constructor;
const method = <ts.MethodDeclaration|ts.ConstructorDeclaration>member;
if (method.flags & ts.NodeFlags.Static) {
const maybeFunc = maybeGetSimpleFunction(<ts.MethodDeclaration>method);
if (maybeFunc) {
recordStaticMember(maybeFunc.name, maybeFunc.func);
}
continue;
}
const methodDecorators = getDecorators(method.decorators);
const parameters = method.parameters;
const parameterDecoratorData: (MetadataSymbolicExpression | MetadataError)[][] = [];
@ -123,8 +163,11 @@ export class MetadataCollector {
if (members) {
result.members = members;
}
if (statics) {
result.statics = statics;
}
return result.decorators || members ? result : undefined;
return result.decorators || members || statics ? result : undefined;
}
// Predeclare classes
@ -160,21 +203,10 @@ export class MetadataCollector {
// names substitution will be performed by the StaticReflector.
if (node.flags & ts.NodeFlags.Export) {
const functionDeclaration = <ts.FunctionDeclaration>node;
const functionName = functionDeclaration.name.text;
const functionBody = functionDeclaration.body;
if (functionBody && functionBody.statements.length == 1) {
const statement = functionBody.statements[0];
if (statement.kind === ts.SyntaxKind.ReturnStatement) {
const returnStatement = <ts.ReturnStatement>statement;
if (returnStatement.expression) {
if (!metadata) metadata = {};
metadata[functionName] = {
__symbolic: 'function',
parameters: namesOf(functionDeclaration.parameters),
value: evaluator.evaluateNode(returnStatement.expression)
};
}
}
const maybeFunc = maybeGetSimpleFunction(functionDeclaration);
if (maybeFunc) {
if (!metadata) metadata = {};
metadata[maybeFunc.name] = maybeFunc.func;
}
}
// Otherwise don't record the function.

View File

@ -22,6 +22,7 @@ export interface ClassMetadata {
__symbolic: 'class';
decorators?: (MetadataSymbolicExpression|MetadataError)[];
members?: MetadataMap;
statics?: MetadataObject;
}
export function isClassMetadata(value: any): value is ClassMetadata {
return value && value.__symbolic === 'class';

View File

@ -16,7 +16,7 @@ describe('Collector', () => {
host = new Host(FILES, [
'/app/app.component.ts', '/app/cases-data.ts', '/app/error-cases.ts', '/promise.ts',
'/unsupported-1.ts', '/unsupported-2.ts', 'import-star.ts', 'exported-functions.ts',
'exported-enum.ts', 'exported-consts.ts'
'exported-enum.ts', 'exported-consts.ts', 'static-method.ts', 'static-method-call.ts'
]);
service = ts.createLanguageService(host, documentRegistry);
program = service.getProgram();
@ -337,6 +337,47 @@ describe('Collector', () => {
E: {__symbolic: 'reference', module: './exported-consts', name: 'constValue'}
});
});
it('should be able to collect a simple static method', () => {
let staticSource = program.getSourceFile('/static-method.ts');
let metadata = collector.getMetadata(staticSource);
expect(metadata).toBeDefined();
let 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', () => {
let staticSource = program.getSourceFile('/static-method-call.ts');
let metadata = collector.getMetadata(staticSource);
expect(metadata).toBeDefined();
let classData = <ClassMetadata>metadata.metadata['Foo'];
expect(classData).toBeDefined();
expect(classData.decorators).toEqual([{
__symbolic: 'call',
expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'},
arguments: [{
providers: {
__symbolic: 'call',
expression: {
__symbolic: 'select',
expression: {__symbolic: 'reference', module: './static-method.ts', name: 'MyModule'},
member: 'with'
},
arguments: ['a']
}
}]
}]);
});
});
// TODO: Do not use \` in a template literal as it confuses clang-format
@ -579,6 +620,28 @@ const FILES: Directory = {
'exported-consts.ts': `
export const constValue = 100;
`,
'static-method.ts': `
import {Injectable} from 'angular2/core';
@Injectable()
export class MyModule {
static with(comp: any): any[] {
return [
MyModule,
{ provider: 'a', useValue: comp }
];
}
}
`,
'static-method-call.ts': `
import {Component} from 'angular2/core';
import {MyModule} from './static-method.ts';
@Component({
providers: MyModule.with('a')
})
export class Foo { }
`,
'node_modules': {
'angular2': {
'core.d.ts': `