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

@ -281,10 +281,25 @@ export class StaticReflector implements ReflectorReader {
let target = expression['expression'];
let functionSymbol: StaticSymbol;
let targetFunction: any;
if (target && target.__symbolic === 'reference') {
callContext = {name: target.name};
functionSymbol = resolveReference(context, target);
targetFunction = resolveReferenceValue(functionSymbol);
if (target) {
switch (target.__symbolic) {
case 'reference':
// Find the function to call.
callContext = {name: target.name};
functionSymbol = resolveReference(context, target);
targetFunction = resolveReferenceValue(functionSymbol);
break;
case 'select':
// Find the static method to call
if (target.expression.__symbolic == 'reference') {
functionSymbol = resolveReference(context, target.expression);
const classData = resolveReferenceValue(functionSymbol);
if (classData && classData.statics) {
targetFunction = classData.statics[target.member];
}
}
break;
}
}
if (targetFunction && targetFunction['__symbolic'] == 'function') {
if (calling.get(functionSymbol)) {
@ -292,7 +307,7 @@ export class StaticReflector implements ReflectorReader {
}
calling.set(functionSymbol, true);
let value = targetFunction['value'];
if (value) {
if (value && (depth != 0 || value.__symbolic != 'error')) {
// Determine the arguments
let args = (expression['arguments'] || []).map((arg: any) => simplify(arg));
let parameters: string[] = targetFunction['parameters'];

View File

@ -395,6 +395,13 @@ describe('StaticReflector', () => {
.toThrow(new Error(
`Error encountered resolving symbol values statically. Calling function 'someFunction', function calls are not supported. Consider replacing the function or lambda with a reference to an exported function, resolving symbol MyComponent in /tmp/src/invalid-calls.ts, resolving symbol MyComponent in /tmp/src/invalid-calls.ts`));
});
it('should be able to get metadata for a class containing a static method call', () => {
const annotations = reflector.annotations(
host.getStaticSymbol('/tmp/src/static-method-call.ts', 'MyComponent'));
expect(annotations.length).toBe(1);
expect(annotations[0].providers).toEqual({provider: 'a', useValue: 100});
});
});
class MockReflectorHost implements StaticReflectorHost {
@ -456,7 +463,12 @@ class MockReflectorHost implements StaticReflectorHost {
}
if (modulePath.indexOf('.') === 0) {
return this.getStaticSymbol(pathTo(containingFile, modulePath) + '.d.ts', symbolName);
const baseName = pathTo(containingFile, modulePath);
const tsName = baseName + '.ts';
if (this.getMetadataFor(tsName)) {
return this.getStaticSymbol(tsName, symbolName);
}
return this.getStaticSymbol(baseName + '.d.ts', symbolName);
}
return this.getStaticSymbol('/tmp/' + modulePath + '.d.ts', symbolName);
}
@ -907,6 +919,27 @@ class MockReflectorHost implements StaticReflectorHost {
directives: [NgIf]
})
export class MyOtherComponent { }
`,
'/tmp/src/static-method.ts': `
import {Component} from 'angular2/src/core/metadata';
@Component({
selector: 'stub'
})
export class MyModule {
static with(data: any) {
return { provider: 'a', useValue: data }
}
}
`,
'/tmp/src/static-method-call.ts': `
import {Component} from 'angular2/src/core/metadata';
import {MyModule} from './static-method';
@Component({
providers: MyModule.with(100)
})
export class MyComponent { }
`
};

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': `