From 20b03bad11b0774503b0d33a9a628869668a623a Mon Sep 17 00:00:00 2001 From: Chuck Jazdzewski Date: Thu, 28 Jul 2016 17:32:29 -0700 Subject: [PATCH] feat(compiler): Added support for conditional expressions. (#10366) Expression evaluated by the static reflector can now supports conditional expressions. Closes: #10365 --- .../compiler-cli/src/static_reflector.ts | 43 ++++++++------ .../test/static_reflector_spec.ts | 18 ++++++ tools/@angular/tsc-wrapped/src/evaluator.ts | 9 +++ tools/@angular/tsc-wrapped/src/schema.ts | 13 ++++- .../tsc-wrapped/test/collector.spec.ts | 56 +++++++++++++++++-- 5 files changed, 116 insertions(+), 23 deletions(-) diff --git a/modules/@angular/compiler-cli/src/static_reflector.ts b/modules/@angular/compiler-cli/src/static_reflector.ts index 771039347d..59979f0f55 100644 --- a/modules/@angular/compiler-cli/src/static_reflector.ts +++ b/modules/@angular/compiler-cli/src/static_reflector.ts @@ -307,26 +307,29 @@ export class StaticReflector implements ReflectorReader { throw new Error('Recursion not supported'); } calling.set(functionSymbol, true); - let value = targetFunction['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']; - let functionScope = BindingScope.build(); - for (let i = 0; i < parameters.length; i++) { - functionScope.define(parameters[i], args[i]); + try { + let value = targetFunction['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']; + let functionScope = BindingScope.build(); + for (let i = 0; i < parameters.length; i++) { + functionScope.define(parameters[i], args[i]); + } + let oldScope = scope; + let result: any; + try { + scope = functionScope.done(); + result = simplifyInContext(functionSymbol, value, depth + 1); + } finally { + scope = oldScope; + } + return result; } - let oldScope = scope; - let result: any; - try { - scope = functionScope.done(); - result = simplifyInContext(functionSymbol, value, depth + 1); - } finally { - scope = oldScope; - } - return result; + } finally { + calling.delete(functionSymbol); } - calling.delete(functionSymbol); } } @@ -417,6 +420,10 @@ export class StaticReflector implements ReflectorReader { return left % right; } return null; + case 'if': + let condition = simplify(expression['condition']); + return condition ? simplify(expression['thenExpression']) : + simplify(expression['elseExpression']); case 'pre': let operand = simplify(expression['operand']); if (shouldIgnore(operand)) return operand; diff --git a/modules/@angular/compiler-cli/test/static_reflector_spec.ts b/modules/@angular/compiler-cli/test/static_reflector_spec.ts index cfa79c2196..54620120cd 100644 --- a/modules/@angular/compiler-cli/test/static_reflector_spec.ts +++ b/modules/@angular/compiler-cli/test/static_reflector_spec.ts @@ -411,6 +411,16 @@ describe('StaticReflector', () => { expect(annotations.length).toBe(1); expect(annotations[0].providers).toEqual([{provider: 'a', useValue: 'Some string'}]); }); + + it('should be able to get the metadata for a class calling a method with a conditional expression', + () => { + const annotations = reflector.annotations( + host.getStaticSymbol('/tmp/src/static-method-call.ts', 'MyCondComponent')); + expect(annotations.length).toBe(1); + expect(annotations[0].providers).toEqual([ + [{provider: 'a', useValue: '1'}], [{provider: 'a', useValue: '2'}] + ]); + }); }); class MockReflectorHost implements StaticReflectorHost { @@ -960,6 +970,9 @@ class MockReflectorHost implements StaticReflectorHost { static with(data: any) { return { provider: 'a', useValue: data } } + static condMethod(cond: boolean) { + return [{ provider: 'a', useValue: cond ? '1' : '2'}]; + } } `, '/tmp/src/static-method-call.ts': ` @@ -970,6 +983,11 @@ class MockReflectorHost implements StaticReflectorHost { providers: MyModule.with(100) }) export class MyComponent { } + + @Component({ + providers: [MyModule.condMethod(true), MyModule.condMethod(false)] + }) + export class MyCondComponent { } `, '/tmp/src/static-field.ts': ` import {Injectable} from 'angular2/core'; diff --git a/tools/@angular/tsc-wrapped/src/evaluator.ts b/tools/@angular/tsc-wrapped/src/evaluator.ts index dc0d8662ed..4ed3480cb4 100644 --- a/tools/@angular/tsc-wrapped/src/evaluator.ts +++ b/tools/@angular/tsc-wrapped/src/evaluator.ts @@ -511,6 +511,15 @@ export class Evaluator { }; } break; + case ts.SyntaxKind.ConditionalExpression: + const conditionalExpression = node; + const condition = this.evaluateNode(conditionalExpression.condition); + const thenExpression = this.evaluateNode(conditionalExpression.whenTrue); + const elseExpression = this.evaluateNode(conditionalExpression.whenFalse); + if (isPrimitive(condition)) { + return condition ? thenExpression : elseExpression; + } + return {__symbolic: 'if', condition, thenExpression, elseExpression}; case ts.SyntaxKind.FunctionExpression: case ts.SyntaxKind.ArrowFunction: return errorSymbol('Function call not supported', node); diff --git a/tools/@angular/tsc-wrapped/src/schema.ts b/tools/@angular/tsc-wrapped/src/schema.ts index 8b95ab748a..5dd176bff3 100644 --- a/tools/@angular/tsc-wrapped/src/schema.ts +++ b/tools/@angular/tsc-wrapped/src/schema.ts @@ -79,7 +79,7 @@ export interface MetadataObject { [name: string]: MetadataValue; } export interface MetadataArray { [name: number]: MetadataValue; } export interface MetadataSymbolicExpression { - __symbolic: 'binary'|'call'|'index'|'new'|'pre'|'reference'|'select'|'spread' + __symbolic: 'binary'|'call'|'index'|'new'|'pre'|'reference'|'select'|'spread'|'if' } export function isMetadataSymbolicExpression(value: any): value is MetadataSymbolicExpression { if (value) { @@ -92,6 +92,7 @@ export function isMetadataSymbolicExpression(value: any): value is MetadataSymbo case 'reference': case 'select': case 'spread': + case 'if': return true; } } @@ -140,6 +141,16 @@ export function isMetadataSymbolicPrefixExpression(value: any): return value && value.__symbolic === 'pre'; } +export interface MetadataSymbolicIfExpression extends MetadataSymbolicExpression { + __symbolic: 'if'; + condition: MetadataValue; + thenExpression: MetadataValue; + elseExpression: MetadataValue; +} +export function isMetadataSymbolicIfExpression(value: any): value is MetadataSymbolicIfExpression { + return value && value.__symbolic === 'if'; +} + export interface MetadataGlobalReferenceExpression extends MetadataSymbolicExpression { __symbolic: 'reference'; name: string; diff --git a/tools/@angular/tsc-wrapped/test/collector.spec.ts b/tools/@angular/tsc-wrapped/test/collector.spec.ts index 15854b4eb3..01a51b768a 100644 --- a/tools/@angular/tsc-wrapped/test/collector.spec.ts +++ b/tools/@angular/tsc-wrapped/test/collector.spec.ts @@ -14,10 +14,20 @@ describe('Collector', () => { beforeEach(() => { 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', 'static-method.ts', 'static-method-call.ts', - 'static-field-reference.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', + 'exported-enum.ts', + 'exported-consts.ts', + 'static-field-reference.ts', + 'static-method.ts', + 'static-method-call.ts', + 'static-method-with-if.ts', ]); service = ts.createLanguageService(host, documentRegistry); program = service.getProgram(); @@ -410,6 +420,31 @@ describe('Collector', () => { }] }]); }); + + it('should be able to collect a method with a conditional expression', () => { + let source = program.getSourceFile('/static-method-with-if.ts'); + let metadata = collector.getMetadata(source); + expect(metadata).toBeDefined(); + let classData = metadata.metadata['MyModule']; + expect(classData).toBeDefined(); + expect(classData.statics).toEqual({ + with: { + __symbolic: 'function', + parameters: ['cond'], + value: [ + {__symbolic: 'reference', name: 'MyModule'}, { + provider: 'a', + useValue: { + __symbolic: 'if', + condition: {__symbolic: 'reference', name: 'cond'}, + thenExpression: '1', + elseExpression: '2' + } + } + ] + } + }); + }); }); // TODO: Do not use \` in a template literal as it confuses clang-format @@ -691,6 +726,19 @@ const FILES: Directory = { }) export class Foo { } `, + 'static-method-with-if.ts': ` + import {Injectable} from 'angular2/core'; + + @Injectable() + export class MyModule { + static with(cond: boolean): any[] { + return [ + MyModule, + { provider: 'a', useValue: cond ? '1' : '2' } + ]; + } + } + `, 'node_modules': { 'angular2': { 'core.d.ts': `