diff --git a/modules/@angular/compiler-cli/src/static_reflector.ts b/modules/@angular/compiler-cli/src/static_reflector.ts index 4dfb7cd75e..3f6163e06d 100644 --- a/modules/@angular/compiler-cli/src/static_reflector.ts +++ b/modules/@angular/compiler-cli/src/static_reflector.ts @@ -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']; diff --git a/modules/@angular/compiler-cli/test/static_reflector_spec.ts b/modules/@angular/compiler-cli/test/static_reflector_spec.ts index 585adb0a6b..7423647405 100644 --- a/modules/@angular/compiler-cli/test/static_reflector_spec.ts +++ b/modules/@angular/compiler-cli/test/static_reflector_spec.ts @@ -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 { } ` }; diff --git a/tools/@angular/tsc-wrapped/src/collector.ts b/tools/@angular/tsc-wrapped/src/collector.ts index c80bdf5a32..3433b0c352 100644 --- a/tools/@angular/tsc-wrapped/src/collector.ts +++ b/tools/@angular/tsc-wrapped/src/collector.ts @@ -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 = 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 = 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 = member; + if (method.flags & ts.NodeFlags.Static) { + const maybeFunc = maybeGetSimpleFunction(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 = 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 = 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. diff --git a/tools/@angular/tsc-wrapped/src/schema.ts b/tools/@angular/tsc-wrapped/src/schema.ts index 431727e677..8b95ab748a 100644 --- a/tools/@angular/tsc-wrapped/src/schema.ts +++ b/tools/@angular/tsc-wrapped/src/schema.ts @@ -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'; diff --git a/tools/@angular/tsc-wrapped/test/collector.spec.ts b/tools/@angular/tsc-wrapped/test/collector.spec.ts index 3079154eb4..c5d7df0588 100644 --- a/tools/@angular/tsc-wrapped/test/collector.spec.ts +++ b/tools/@angular/tsc-wrapped/test/collector.spec.ts @@ -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 = 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 = 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': `