feat(compiler): Allow calls to simple static methods (#10289)
Closes: #10266
This commit is contained in:
parent
0aba42ae5b
commit
b449467940
|
@ -281,10 +281,25 @@ export class StaticReflector implements ReflectorReader {
|
|||
let target = expression['expression'];
|
||||
let functionSymbol: StaticSymbol;
|
||||
let targetFunction: any;
|
||||
if (target && target.__symbolic === 'reference') {
|
||||
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'];
|
||||
|
|
|
@ -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 { }
|
||||
`
|
||||
};
|
||||
|
||||
|
|
|
@ -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) {
|
||||
const maybeFunc = maybeGetSimpleFunction(functionDeclaration);
|
||||
if (maybeFunc) {
|
||||
if (!metadata) metadata = {};
|
||||
metadata[functionName] = {
|
||||
__symbolic: 'function',
|
||||
parameters: namesOf(functionDeclaration.parameters),
|
||||
value: evaluator.evaluateNode(returnStatement.expression)
|
||||
};
|
||||
}
|
||||
}
|
||||
metadata[maybeFunc.name] = maybeFunc.func;
|
||||
}
|
||||
}
|
||||
// Otherwise don't record the function.
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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': `
|
||||
|
|
Loading…
Reference in New Issue