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 target = expression['expression'];
|
||||||
let functionSymbol: StaticSymbol;
|
let functionSymbol: StaticSymbol;
|
||||||
let targetFunction: any;
|
let targetFunction: any;
|
||||||
if (target && target.__symbolic === 'reference') {
|
if (target) {
|
||||||
callContext = {name: target.name};
|
switch (target.__symbolic) {
|
||||||
functionSymbol = resolveReference(context, target);
|
case 'reference':
|
||||||
targetFunction = resolveReferenceValue(functionSymbol);
|
// 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 (targetFunction && targetFunction['__symbolic'] == 'function') {
|
||||||
if (calling.get(functionSymbol)) {
|
if (calling.get(functionSymbol)) {
|
||||||
@ -292,7 +307,7 @@ export class StaticReflector implements ReflectorReader {
|
|||||||
}
|
}
|
||||||
calling.set(functionSymbol, true);
|
calling.set(functionSymbol, true);
|
||||||
let value = targetFunction['value'];
|
let value = targetFunction['value'];
|
||||||
if (value) {
|
if (value && (depth != 0 || value.__symbolic != 'error')) {
|
||||||
// Determine the arguments
|
// Determine the arguments
|
||||||
let args = (expression['arguments'] || []).map((arg: any) => simplify(arg));
|
let args = (expression['arguments'] || []).map((arg: any) => simplify(arg));
|
||||||
let parameters: string[] = targetFunction['parameters'];
|
let parameters: string[] = targetFunction['parameters'];
|
||||||
|
@ -395,6 +395,13 @@ describe('StaticReflector', () => {
|
|||||||
.toThrow(new Error(
|
.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`));
|
`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 {
|
class MockReflectorHost implements StaticReflectorHost {
|
||||||
@ -456,7 +463,12 @@ class MockReflectorHost implements StaticReflectorHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (modulePath.indexOf('.') === 0) {
|
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);
|
return this.getStaticSymbol('/tmp/' + modulePath + '.d.ts', symbolName);
|
||||||
}
|
}
|
||||||
@ -907,6 +919,27 @@ class MockReflectorHost implements StaticReflectorHost {
|
|||||||
directives: [NgIf]
|
directives: [NgIf]
|
||||||
})
|
})
|
||||||
export class MyOtherComponent { }
|
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 * as ts from 'typescript';
|
||||||
|
|
||||||
import {Evaluator, errorSymbol, isPrimitive} from './evaluator';
|
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';
|
import {Symbols} from './symbols';
|
||||||
|
|
||||||
|
|
||||||
@ -30,6 +30,31 @@ export class MetadataCollector {
|
|||||||
return errorSymbol(message, node, context, sourceFile);
|
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 {
|
function classMetadataOf(classDeclaration: ts.ClassDeclaration): ClassMetadata {
|
||||||
let result: ClassMetadata = {__symbolic: 'class'};
|
let result: ClassMetadata = {__symbolic: 'class'};
|
||||||
|
|
||||||
@ -63,6 +88,14 @@ export class MetadataCollector {
|
|||||||
data.push(metadata);
|
data.push(metadata);
|
||||||
members[name] = data;
|
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) {
|
for (const member of classDeclaration.members) {
|
||||||
let isConstructor = false;
|
let isConstructor = false;
|
||||||
switch (member.kind) {
|
switch (member.kind) {
|
||||||
@ -70,6 +103,13 @@ export class MetadataCollector {
|
|||||||
case ts.SyntaxKind.MethodDeclaration:
|
case ts.SyntaxKind.MethodDeclaration:
|
||||||
isConstructor = member.kind === ts.SyntaxKind.Constructor;
|
isConstructor = member.kind === ts.SyntaxKind.Constructor;
|
||||||
const method = <ts.MethodDeclaration|ts.ConstructorDeclaration>member;
|
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 methodDecorators = getDecorators(method.decorators);
|
||||||
const parameters = method.parameters;
|
const parameters = method.parameters;
|
||||||
const parameterDecoratorData: (MetadataSymbolicExpression | MetadataError)[][] = [];
|
const parameterDecoratorData: (MetadataSymbolicExpression | MetadataError)[][] = [];
|
||||||
@ -123,8 +163,11 @@ export class MetadataCollector {
|
|||||||
if (members) {
|
if (members) {
|
||||||
result.members = members;
|
result.members = members;
|
||||||
}
|
}
|
||||||
|
if (statics) {
|
||||||
|
result.statics = statics;
|
||||||
|
}
|
||||||
|
|
||||||
return result.decorators || members ? result : undefined;
|
return result.decorators || members || statics ? result : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Predeclare classes
|
// Predeclare classes
|
||||||
@ -160,21 +203,10 @@ export class MetadataCollector {
|
|||||||
// names substitution will be performed by the StaticReflector.
|
// names substitution will be performed by the StaticReflector.
|
||||||
if (node.flags & ts.NodeFlags.Export) {
|
if (node.flags & ts.NodeFlags.Export) {
|
||||||
const functionDeclaration = <ts.FunctionDeclaration>node;
|
const functionDeclaration = <ts.FunctionDeclaration>node;
|
||||||
const functionName = functionDeclaration.name.text;
|
const maybeFunc = maybeGetSimpleFunction(functionDeclaration);
|
||||||
const functionBody = functionDeclaration.body;
|
if (maybeFunc) {
|
||||||
if (functionBody && functionBody.statements.length == 1) {
|
if (!metadata) metadata = {};
|
||||||
const statement = functionBody.statements[0];
|
metadata[maybeFunc.name] = maybeFunc.func;
|
||||||
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)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Otherwise don't record the function.
|
// Otherwise don't record the function.
|
||||||
|
@ -22,6 +22,7 @@ export interface ClassMetadata {
|
|||||||
__symbolic: 'class';
|
__symbolic: 'class';
|
||||||
decorators?: (MetadataSymbolicExpression|MetadataError)[];
|
decorators?: (MetadataSymbolicExpression|MetadataError)[];
|
||||||
members?: MetadataMap;
|
members?: MetadataMap;
|
||||||
|
statics?: MetadataObject;
|
||||||
}
|
}
|
||||||
export function isClassMetadata(value: any): value is ClassMetadata {
|
export function isClassMetadata(value: any): value is ClassMetadata {
|
||||||
return value && value.__symbolic === 'class';
|
return value && value.__symbolic === 'class';
|
||||||
|
@ -16,7 +16,7 @@ describe('Collector', () => {
|
|||||||
host = new Host(FILES, [
|
host = new Host(FILES, [
|
||||||
'/app/app.component.ts', '/app/cases-data.ts', '/app/error-cases.ts', '/promise.ts',
|
'/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',
|
'/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);
|
service = ts.createLanguageService(host, documentRegistry);
|
||||||
program = service.getProgram();
|
program = service.getProgram();
|
||||||
@ -337,6 +337,47 @@ describe('Collector', () => {
|
|||||||
E: {__symbolic: 'reference', module: './exported-consts', name: 'constValue'}
|
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
|
// TODO: Do not use \` in a template literal as it confuses clang-format
|
||||||
@ -579,6 +620,28 @@ const FILES: Directory = {
|
|||||||
'exported-consts.ts': `
|
'exported-consts.ts': `
|
||||||
export const constValue = 100;
|
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': {
|
'node_modules': {
|
||||||
'angular2': {
|
'angular2': {
|
||||||
'core.d.ts': `
|
'core.d.ts': `
|
||||||
|
Loading…
x
Reference in New Issue
Block a user