Skip to content

Commit b449467

Browse files
authored
feat(compiler): Allow calls to simple static methods (angular#10289)
Closes: angular#10266
1 parent 0aba42a commit b449467

File tree

5 files changed

+168
-24
lines changed

5 files changed

+168
-24
lines changed

modules/@angular/compiler-cli/src/static_reflector.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -281,18 +281,33 @@ export class StaticReflector implements ReflectorReader {
281281
let target = expression['expression'];
282282
let functionSymbol: StaticSymbol;
283283
let targetFunction: any;
284-
if (target && target.__symbolic === 'reference') {
285-
callContext = {name: target.name};
286-
functionSymbol = resolveReference(context, target);
287-
targetFunction = resolveReferenceValue(functionSymbol);
284+
if (target) {
285+
switch (target.__symbolic) {
286+
case 'reference':
287+
// Find the function to call.
288+
callContext = {name: target.name};
289+
functionSymbol = resolveReference(context, target);
290+
targetFunction = resolveReferenceValue(functionSymbol);
291+
break;
292+
case 'select':
293+
// Find the static method to call
294+
if (target.expression.__symbolic == 'reference') {
295+
functionSymbol = resolveReference(context, target.expression);
296+
const classData = resolveReferenceValue(functionSymbol);
297+
if (classData && classData.statics) {
298+
targetFunction = classData.statics[target.member];
299+
}
300+
}
301+
break;
302+
}
288303
}
289304
if (targetFunction && targetFunction['__symbolic'] == 'function') {
290305
if (calling.get(functionSymbol)) {
291306
throw new Error('Recursion not supported');
292307
}
293308
calling.set(functionSymbol, true);
294309
let value = targetFunction['value'];
295-
if (value) {
310+
if (value && (depth != 0 || value.__symbolic != 'error')) {
296311
// Determine the arguments
297312
let args = (expression['arguments'] || []).map((arg: any) => simplify(arg));
298313
let parameters: string[] = targetFunction['parameters'];

modules/@angular/compiler-cli/test/static_reflector_spec.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,13 @@ describe('StaticReflector', () => {
395395
.toThrow(new Error(
396396
`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`));
397397
});
398+
399+
it('should be able to get metadata for a class containing a static method call', () => {
400+
const annotations = reflector.annotations(
401+
host.getStaticSymbol('/tmp/src/static-method-call.ts', 'MyComponent'));
402+
expect(annotations.length).toBe(1);
403+
expect(annotations[0].providers).toEqual({provider: 'a', useValue: 100});
404+
});
398405
});
399406

400407
class MockReflectorHost implements StaticReflectorHost {
@@ -456,7 +463,12 @@ class MockReflectorHost implements StaticReflectorHost {
456463
}
457464

458465
if (modulePath.indexOf('.') === 0) {
459-
return this.getStaticSymbol(pathTo(containingFile, modulePath) + '.d.ts', symbolName);
466+
const baseName = pathTo(containingFile, modulePath);
467+
const tsName = baseName + '.ts';
468+
if (this.getMetadataFor(tsName)) {
469+
return this.getStaticSymbol(tsName, symbolName);
470+
}
471+
return this.getStaticSymbol(baseName + '.d.ts', symbolName);
460472
}
461473
return this.getStaticSymbol('/tmp/' + modulePath + '.d.ts', symbolName);
462474
}
@@ -907,6 +919,27 @@ class MockReflectorHost implements StaticReflectorHost {
907919
directives: [NgIf]
908920
})
909921
export class MyOtherComponent { }
922+
`,
923+
'/tmp/src/static-method.ts': `
924+
import {Component} from 'angular2/src/core/metadata';
925+
926+
@Component({
927+
selector: 'stub'
928+
})
929+
export class MyModule {
930+
static with(data: any) {
931+
return { provider: 'a', useValue: data }
932+
}
933+
}
934+
`,
935+
'/tmp/src/static-method-call.ts': `
936+
import {Component} from 'angular2/src/core/metadata';
937+
import {MyModule} from './static-method';
938+
939+
@Component({
940+
providers: MyModule.with(100)
941+
})
942+
export class MyComponent { }
910943
`
911944
};
912945

tools/@angular/tsc-wrapped/src/collector.ts

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as ts from 'typescript';
22

33
import {Evaluator, errorSymbol, isPrimitive} from './evaluator';
4-
import {ClassMetadata, ConstructorMetadata, MemberMetadata, MetadataError, MetadataMap, MetadataSymbolicExpression, MetadataSymbolicReferenceExpression, MetadataSymbolicSelectExpression, MetadataValue, MethodMetadata, ModuleMetadata, VERSION, isMetadataError, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSelectExpression} from './schema';
4+
import {ClassMetadata, ConstructorMetadata, MemberMetadata, MetadataError, MetadataMap, MetadataObject, MetadataSymbolicExpression, MetadataSymbolicReferenceExpression, MetadataSymbolicSelectExpression, MetadataValue, MethodMetadata, ModuleMetadata, VERSION, isMetadataError, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSelectExpression} from './schema';
55
import {Symbols} from './symbols';
66

77

@@ -30,6 +30,31 @@ export class MetadataCollector {
3030
return errorSymbol(message, node, context, sourceFile);
3131
}
3232

33+
function maybeGetSimpleFunction(
34+
functionDeclaration: ts.FunctionDeclaration |
35+
ts.MethodDeclaration): {func: MetadataValue, name: string}|undefined {
36+
if (functionDeclaration.name.kind == ts.SyntaxKind.Identifier) {
37+
const nameNode = <ts.Identifier>functionDeclaration.name;
38+
const functionName = nameNode.text;
39+
const functionBody = functionDeclaration.body;
40+
if (functionBody && functionBody.statements.length == 1) {
41+
const statement = functionBody.statements[0];
42+
if (statement.kind === ts.SyntaxKind.ReturnStatement) {
43+
const returnStatement = <ts.ReturnStatement>statement;
44+
if (returnStatement.expression) {
45+
return {
46+
name: functionName, func: {
47+
__symbolic: 'function',
48+
parameters: namesOf(functionDeclaration.parameters),
49+
value: evaluator.evaluateNode(returnStatement.expression)
50+
}
51+
}
52+
}
53+
}
54+
}
55+
}
56+
}
57+
3358
function classMetadataOf(classDeclaration: ts.ClassDeclaration): ClassMetadata {
3459
let result: ClassMetadata = {__symbolic: 'class'};
3560

@@ -63,13 +88,28 @@ export class MetadataCollector {
6388
data.push(metadata);
6489
members[name] = data;
6590
}
91+
92+
// static member
93+
let statics: MetadataObject = null;
94+
function recordStaticMember(name: string, value: MetadataValue) {
95+
if (!statics) statics = {};
96+
statics[name] = value;
97+
}
98+
6699
for (const member of classDeclaration.members) {
67100
let isConstructor = false;
68101
switch (member.kind) {
69102
case ts.SyntaxKind.Constructor:
70103
case ts.SyntaxKind.MethodDeclaration:
71104
isConstructor = member.kind === ts.SyntaxKind.Constructor;
72105
const method = <ts.MethodDeclaration|ts.ConstructorDeclaration>member;
106+
if (method.flags & ts.NodeFlags.Static) {
107+
const maybeFunc = maybeGetSimpleFunction(<ts.MethodDeclaration>method);
108+
if (maybeFunc) {
109+
recordStaticMember(maybeFunc.name, maybeFunc.func);
110+
}
111+
continue;
112+
}
73113
const methodDecorators = getDecorators(method.decorators);
74114
const parameters = method.parameters;
75115
const parameterDecoratorData: (MetadataSymbolicExpression | MetadataError)[][] = [];
@@ -123,8 +163,11 @@ export class MetadataCollector {
123163
if (members) {
124164
result.members = members;
125165
}
166+
if (statics) {
167+
result.statics = statics;
168+
}
126169

127-
return result.decorators || members ? result : undefined;
170+
return result.decorators || members || statics ? result : undefined;
128171
}
129172

130173
// Predeclare classes
@@ -160,21 +203,10 @@ export class MetadataCollector {
160203
// names substitution will be performed by the StaticReflector.
161204
if (node.flags & ts.NodeFlags.Export) {
162205
const functionDeclaration = <ts.FunctionDeclaration>node;
163-
const functionName = functionDeclaration.name.text;
164-
const functionBody = functionDeclaration.body;
165-
if (functionBody && functionBody.statements.length == 1) {
166-
const statement = functionBody.statements[0];
167-
if (statement.kind === ts.SyntaxKind.ReturnStatement) {
168-
const returnStatement = <ts.ReturnStatement>statement;
169-
if (returnStatement.expression) {
170-
if (!metadata) metadata = {};
171-
metadata[functionName] = {
172-
__symbolic: 'function',
173-
parameters: namesOf(functionDeclaration.parameters),
174-
value: evaluator.evaluateNode(returnStatement.expression)
175-
};
176-
}
177-
}
206+
const maybeFunc = maybeGetSimpleFunction(functionDeclaration);
207+
if (maybeFunc) {
208+
if (!metadata) metadata = {};
209+
metadata[maybeFunc.name] = maybeFunc.func;
178210
}
179211
}
180212
// Otherwise don't record the function.

tools/@angular/tsc-wrapped/src/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface ClassMetadata {
2222
__symbolic: 'class';
2323
decorators?: (MetadataSymbolicExpression|MetadataError)[];
2424
members?: MetadataMap;
25+
statics?: MetadataObject;
2526
}
2627
export function isClassMetadata(value: any): value is ClassMetadata {
2728
return value && value.__symbolic === 'class';

tools/@angular/tsc-wrapped/test/collector.spec.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ describe('Collector', () => {
1616
host = new Host(FILES, [
1717
'/app/app.component.ts', '/app/cases-data.ts', '/app/error-cases.ts', '/promise.ts',
1818
'/unsupported-1.ts', '/unsupported-2.ts', 'import-star.ts', 'exported-functions.ts',
19-
'exported-enum.ts', 'exported-consts.ts'
19+
'exported-enum.ts', 'exported-consts.ts', 'static-method.ts', 'static-method-call.ts'
2020
]);
2121
service = ts.createLanguageService(host, documentRegistry);
2222
program = service.getProgram();
@@ -337,6 +337,47 @@ describe('Collector', () => {
337337
E: {__symbolic: 'reference', module: './exported-consts', name: 'constValue'}
338338
});
339339
});
340+
341+
it('should be able to collect a simple static method', () => {
342+
let staticSource = program.getSourceFile('/static-method.ts');
343+
let metadata = collector.getMetadata(staticSource);
344+
expect(metadata).toBeDefined();
345+
let classData = <ClassMetadata>metadata.metadata['MyModule'];
346+
expect(classData).toBeDefined();
347+
expect(classData.statics).toEqual({
348+
with: {
349+
__symbolic: 'function',
350+
parameters: ['comp'],
351+
value: [
352+
{__symbolic: 'reference', name: 'MyModule'},
353+
{provider: 'a', useValue: {__symbolic: 'reference', name: 'comp'}}
354+
]
355+
}
356+
});
357+
});
358+
359+
it('should be able to collect a call to a static method', () => {
360+
let staticSource = program.getSourceFile('/static-method-call.ts');
361+
let metadata = collector.getMetadata(staticSource);
362+
expect(metadata).toBeDefined();
363+
let classData = <ClassMetadata>metadata.metadata['Foo'];
364+
expect(classData).toBeDefined();
365+
expect(classData.decorators).toEqual([{
366+
__symbolic: 'call',
367+
expression: {__symbolic: 'reference', module: 'angular2/core', name: 'Component'},
368+
arguments: [{
369+
providers: {
370+
__symbolic: 'call',
371+
expression: {
372+
__symbolic: 'select',
373+
expression: {__symbolic: 'reference', module: './static-method.ts', name: 'MyModule'},
374+
member: 'with'
375+
},
376+
arguments: ['a']
377+
}
378+
}]
379+
}]);
380+
});
340381
});
341382

342383
// TODO: Do not use \` in a template literal as it confuses clang-format
@@ -579,6 +620,28 @@ const FILES: Directory = {
579620
'exported-consts.ts': `
580621
export const constValue = 100;
581622
`,
623+
'static-method.ts': `
624+
import {Injectable} from 'angular2/core';
625+
626+
@Injectable()
627+
export class MyModule {
628+
static with(comp: any): any[] {
629+
return [
630+
MyModule,
631+
{ provider: 'a', useValue: comp }
632+
];
633+
}
634+
}
635+
`,
636+
'static-method-call.ts': `
637+
import {Component} from 'angular2/core';
638+
import {MyModule} from './static-method.ts';
639+
640+
@Component({
641+
providers: MyModule.with('a')
642+
})
643+
export class Foo { }
644+
`,
582645
'node_modules': {
583646
'angular2': {
584647
'core.d.ts': `

0 commit comments

Comments
 (0)