From 5504ca1e389f7bfd74604b8573ddaab3e9f07b0d Mon Sep 17 00:00:00 2001 From: Chuck Jazdzewski Date: Mon, 13 Jun 2016 15:56:51 -0700 Subject: [PATCH] feat(compiler): Added support for limited function calls in metadata. (#9125) The collector now collects the body of functions that return an expression as a symbolic 'function'. The static reflector supports expanding these functions statically to allow provider macros. Also added support for the array spread operator in both the collector and the static reflector. --- .../integrationtest/src/features.ts | 4 +- .../compiler-cli/integrationtest/src/funcs.ts | 3 + .../compiler-cli/src/reflector_host.ts | 1 + .../compiler-cli/src/static_reflector.ts | 386 ++++++++++++------ .../test/static_reflector_spec.ts | 168 ++++++++ tools/@angular/tsc-wrapped/src/collector.ts | 47 +++ tools/@angular/tsc-wrapped/src/evaluator.ts | 34 +- tools/@angular/tsc-wrapped/src/schema.ts | 21 +- .../tsc-wrapped/test/collector.spec.ts | 92 ++++- .../tsc-wrapped/test/evaluator.spec.ts | 28 ++ 10 files changed, 642 insertions(+), 142 deletions(-) create mode 100644 modules/@angular/compiler-cli/integrationtest/src/funcs.ts diff --git a/modules/@angular/compiler-cli/integrationtest/src/features.ts b/modules/@angular/compiler-cli/integrationtest/src/features.ts index 497887047e..1a4e72a39a 100644 --- a/modules/@angular/compiler-cli/integrationtest/src/features.ts +++ b/modules/@angular/compiler-cli/integrationtest/src/features.ts @@ -1,6 +1,8 @@ import * as common from '@angular/common'; import {Component, Inject, OpaqueToken} from '@angular/core'; +import {wrapInArray} from './funcs'; + export const SOME_OPAQUE_TOKEN = new OpaqueToken('opaqueToken'); @Component({ @@ -23,7 +25,7 @@ export class CompWithProviders { {{a.value}}
{{a.value}}
`, - directives: [common.NgIf] + directives: [wrapInArray(common.NgIf)] }) export class CompWithReferences { } diff --git a/modules/@angular/compiler-cli/integrationtest/src/funcs.ts b/modules/@angular/compiler-cli/integrationtest/src/funcs.ts new file mode 100644 index 0000000000..fb2e47d600 --- /dev/null +++ b/modules/@angular/compiler-cli/integrationtest/src/funcs.ts @@ -0,0 +1,3 @@ +export function wrapInArray(value: any): any[] { + return [value]; +} \ No newline at end of file diff --git a/modules/@angular/compiler-cli/src/reflector_host.ts b/modules/@angular/compiler-cli/src/reflector_host.ts index f19d0cbb0e..c112be8523 100644 --- a/modules/@angular/compiler-cli/src/reflector_host.ts +++ b/modules/@angular/compiler-cli/src/reflector_host.ts @@ -29,6 +29,7 @@ export class ReflectorHost implements StaticReflectorHost, ImportGenerator { coreDecorators: '@angular/core/src/metadata', diDecorators: '@angular/core/src/di/decorators', diMetadata: '@angular/core/src/di/metadata', + diOpaqueToken: '@angular/core/src/di/opaque_token', animationMetadata: '@angular/core/src/animation/metadata', provider: '@angular/core/src/di/provider' }; diff --git a/modules/@angular/compiler-cli/src/static_reflector.ts b/modules/@angular/compiler-cli/src/static_reflector.ts index ab2ac7cbd6..f8e0fb55c5 100644 --- a/modules/@angular/compiler-cli/src/static_reflector.ts +++ b/modules/@angular/compiler-cli/src/static_reflector.ts @@ -32,6 +32,7 @@ export interface StaticReflectorHost { coreDecorators: string, diDecorators: string, diMetadata: string, + diOpaqueToken: string, animationMetadata: string, provider: string }; @@ -56,6 +57,7 @@ export class StaticReflector implements ReflectorReader { private parameterCache = new Map(); private metadataCache = new Map(); private conversionMap = new Map any>(); + private opaqueToken: StaticSymbol; constructor(private host: StaticReflectorHost) { this.initializeConversionMap(); } @@ -177,8 +179,9 @@ export class StaticReflector implements ReflectorReader { } private initializeConversionMap(): void { - const {coreDecorators, diDecorators, diMetadata, animationMetadata, provider} = + const {coreDecorators, diDecorators, diMetadata, diOpaqueToken, animationMetadata, provider} = this.host.angularImportLocations(); + this.opaqueToken = this.host.findDeclaration(diOpaqueToken, 'OpaqueToken'); this.registerDecoratorOrConstructor(this.host.findDeclaration(provider, 'Provider'), Provider); this.registerDecoratorOrConstructor( @@ -245,147 +248,231 @@ export class StaticReflector implements ReflectorReader { /** @internal */ public simplify(context: StaticSymbol, value: any): any { let _this = this; + let scope = BindingScope.empty; + let calling = new Map(); - function simplify(expression: any): any { - if (isPrimitive(expression)) { - return expression; - } - if (expression instanceof Array) { - let result: any[] = []; - for (let item of (expression)) { - result.push(simplify(item)); + function simplifyInContext(context: StaticSymbol, value: any): any { + function resolveReference(expression: any): StaticSymbol { + let staticSymbol: StaticSymbol; + if (expression['module']) { + staticSymbol = _this.host.findDeclaration( + expression['module'], expression['name'], context.filePath); + } else { + staticSymbol = _this.host.getStaticSymbol(context.filePath, expression['name']); } - return result; + return staticSymbol; } - if (expression) { - if (expression['__symbolic']) { - let staticSymbol: StaticSymbol; - switch (expression['__symbolic']) { - case 'binop': - let left = simplify(expression['left']); - let right = simplify(expression['right']); - switch (expression['operator']) { - case '&&': - return left && right; - case '||': - return left || right; - case '|': - return left | right; - case '^': - return left ^ right; - case '&': - return left & right; - case '==': - return left == right; - case '!=': - return left != right; - case '===': - return left === right; - case '!==': - return left !== right; - case '<': - return left < right; - case '>': - return left > right; - case '<=': - return left <= right; - case '>=': - return left >= right; - case '<<': - return left << right; - case '>>': - return left >> right; - case '+': - return left + right; - case '-': - return left - right; - case '*': - return left * right; - case '/': - return left / right; - case '%': - return left % right; + + function isOpaqueToken(value: any): boolean { + if (value && value.__symbolic === 'new' && value.expression) { + let target = value.expression; + if (target.__symbolic == 'reference') { + return sameSymbol(resolveReference(target), _this.opaqueToken); + } + } + return false; + } + + function simplifyCall(expression: any) { + if (expression['__symbolic'] == 'call') { + let target = expression['expression']; + let targetFunction = simplify(target); + if (targetFunction['__symbolic'] == 'function') { + if (calling.get(targetFunction)) { + throw new Error('Recursion not supported'); + } + calling.set(targetFunction, true); + let value = targetFunction['value']; + if (value) { + // 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]); } - return null; - case 'pre': - let operand = simplify(expression['operand']); - switch (expression['operator']) { - case '+': - return operand; - case '-': - return -operand; - case '!': - return !operand; - case '~': - return ~operand; - } - return null; - case 'index': - let indexTarget = simplify(expression['expression']); - let index = simplify(expression['index']); - if (indexTarget && isPrimitive(index)) return indexTarget[index]; - return null; - case 'select': - let selectTarget = simplify(expression['expression']); - let member = simplify(expression['member']); - if (selectTarget && isPrimitive(member)) return selectTarget[member]; - return null; - case 'reference': - if (expression['module']) { - staticSymbol = _this.host.findDeclaration( - expression['module'], expression['name'], context.filePath); - } else { - staticSymbol = _this.host.getStaticSymbol(context.filePath, expression['name']); - } - let result = staticSymbol; - let moduleMetadata = _this.getModuleMetadata(staticSymbol.filePath); - let declarationValue = - moduleMetadata ? moduleMetadata['metadata'][staticSymbol.name] : null; - if (declarationValue) { - result = _this.simplify(staticSymbol, declarationValue); + let oldScope = scope; + let result: any; + try { + scope = functionScope.done(); + result = simplify(value); + } finally { + scope = oldScope; } return result; - case 'class': - return context; - case 'new': - case 'call': - let target = expression['expression']; - if (target['module']) { - staticSymbol = - _this.host.findDeclaration(target['module'], target['name'], context.filePath); - } else { - staticSymbol = _this.host.getStaticSymbol(context.filePath, target['name']); - } - let converter = _this.conversionMap.get(staticSymbol); - if (converter) { - let args = expression['arguments']; - if (!args) { - args = []; - } - return converter(context, args); - } else { - return context; - } - case 'error': - let message = produceErrorMessage(expression); - if (expression['line']) { - message = - `${message} (position ${expression['line']}:${expression['character']} in the original .ts file)`; - } - throw new Error(message); + } + calling.delete(targetFunction); } - return null; } - return mapStringMap(expression, (value, name) => simplify(value)); + + return simplify({__symbolic: 'error', message: 'Function call not supported'}); + } + + function simplify(expression: any): any { + if (isPrimitive(expression)) { + return expression; + } + if (expression instanceof Array) { + let result: any[] = []; + for (let item of (expression)) { + // Check for a spread expression + if (item && item.__symbolic === 'spread') { + let spreadArray = simplify(item.expression); + if (Array.isArray(spreadArray)) { + for (let spreadItem of spreadArray) { + result.push(spreadItem); + } + continue; + } + } + result.push(simplify(item)); + } + return result; + } + if (expression) { + if (expression['__symbolic']) { + let staticSymbol: StaticSymbol; + switch (expression['__symbolic']) { + case 'binop': + let left = simplify(expression['left']); + let right = simplify(expression['right']); + switch (expression['operator']) { + case '&&': + return left && right; + case '||': + return left || right; + case '|': + return left | right; + case '^': + return left ^ right; + case '&': + return left & right; + case '==': + return left == right; + case '!=': + return left != right; + case '===': + return left === right; + case '!==': + return left !== right; + case '<': + return left < right; + case '>': + return left > right; + case '<=': + return left <= right; + case '>=': + return left >= right; + case '<<': + return left << right; + case '>>': + return left >> right; + case '+': + return left + right; + case '-': + return left - right; + case '*': + return left * right; + case '/': + return left / right; + case '%': + return left % right; + } + return null; + case 'pre': + let operand = simplify(expression['operand']); + switch (expression['operator']) { + case '+': + return operand; + case '-': + return -operand; + case '!': + return !operand; + case '~': + return ~operand; + } + return null; + case 'index': + let indexTarget = simplify(expression['expression']); + let index = simplify(expression['index']); + if (indexTarget && isPrimitive(index)) return indexTarget[index]; + return null; + case 'select': + let selectTarget = simplify(expression['expression']); + let member = simplify(expression['member']); + if (selectTarget && isPrimitive(member)) return selectTarget[member]; + return null; + case 'reference': + if (!expression.module) { + let name: string = expression['name']; + let localValue = scope.resolve(name); + if (localValue != BindingScope.missing) { + return localValue; + } + } + staticSymbol = resolveReference(expression); + let result: any = staticSymbol; + let moduleMetadata = _this.getModuleMetadata(staticSymbol.filePath); + let declarationValue = + moduleMetadata ? moduleMetadata['metadata'][staticSymbol.name] : null; + if (declarationValue) { + if (isOpaqueToken(declarationValue)) { + // If the referenced symbol is initalized by a new OpaqueToken we can keep the + // reference to the symbol. + return staticSymbol; + } + result = simplifyInContext(staticSymbol, declarationValue); + } + return result; + case 'class': + return context; + case 'function': + return expression; + case 'new': + case 'call': + // Determine if the function is a built-in conversion + let target = expression['expression']; + if (target['module']) { + staticSymbol = _this.host.findDeclaration( + target['module'], target['name'], context.filePath); + } else { + staticSymbol = _this.host.getStaticSymbol(context.filePath, target['name']); + } + let converter = _this.conversionMap.get(staticSymbol); + if (converter) { + let args = expression['arguments']; + if (!args) { + args = []; + } + return converter(context, args); + } + + // Determine if the function is one we can simplify. + return simplifyCall(expression); + + case 'error': + let message = produceErrorMessage(expression); + if (expression['line']) { + message = + `${message} (position ${expression['line']}:${expression['character']} in the original .ts file)`; + } + throw new Error(message); + } + return null; + } + return mapStringMap(expression, (value, name) => simplify(value)); + } + return null; + } + + try { + return simplify(value); + } catch (e) { + throw new Error(`${e.message}, resolving symbol ${context.name} in ${context.filePath}`); } - return null; } - try { - return simplify(value); - } catch (e) { - throw new Error(`${e.message}, resolving symbol ${context.name} in ${context.filePath}`); - } + return simplifyInContext(context, value); } /** @@ -460,3 +547,40 @@ function mapStringMap(input: {[key: string]: any}, transform: (value: any, key: function isPrimitive(o: any): boolean { return o === null || (typeof o !== 'function' && typeof o !== 'object'); } + +interface BindingScopeBuilder { + define(name: string, value: any): BindingScopeBuilder; + done(): BindingScope; +} + +abstract class BindingScope { + abstract resolve(name: string): any; + public static missing = {}; + public static empty: BindingScope = {resolve: name => BindingScope.missing}; + + public static build(): BindingScopeBuilder { + let current = new Map(); + let parent: BindingScope = undefined; + return { + define: function(name, value) { + current.set(name, value); + return this; + }, + done: function() { + return current.size > 0 ? new PopulatedScope(current) : BindingScope.empty; + } + }; + } +} + +class PopulatedScope extends BindingScope { + constructor(private bindings: Map) { super(); } + + resolve(name: string): any { + return this.bindings.has(name) ? this.bindings.get(name) : BindingScope.missing; + } +} + +function sameSymbol(a: StaticSymbol, b: StaticSymbol): boolean { + return a === b || (a.name == b.name && a.filePath == b.filePath); +} diff --git a/modules/@angular/compiler-cli/test/static_reflector_spec.ts b/modules/@angular/compiler-cli/test/static_reflector_spec.ts index 464cc1927a..f791a1f2ee 100644 --- a/modules/@angular/compiler-cli/test/static_reflector_spec.ts +++ b/modules/@angular/compiler-cli/test/static_reflector_spec.ts @@ -293,6 +293,41 @@ describe('StaticReflector', () => { ({__symbolic: 'reference', module: './extern', name: 'nonExisting'}))) .toEqual(host.getStaticSymbol('/src/extern.d.ts', 'nonExisting')); }); + + it('should simplify values initialized with a function call', () => { + expect(simplify(new StaticSymbol('/tmp/src/function-reference.ts', ''), { + __symbolic: 'reference', + name: 'one' + })).toEqual(['some-value']); + expect(simplify(new StaticSymbol('/tmp/src/function-reference.ts', ''), { + __symbolic: 'reference', + name: 'two' + })).toEqual(2); + }); + + it('should error on direct recursive calls', () => { + expect( + () => simplify( + new StaticSymbol('/tmp/src/function-reference.ts', ''), + {__symbolic: 'reference', name: 'recursion'})) + .toThrow(new Error( + 'Recursion not supported, resolving symbol recursion in /tmp/src/function-reference.ts, resolving symbol in /tmp/src/function-reference.ts')); + }); + + it('should error on indirect recursive calls', () => { + expect( + () => simplify( + new StaticSymbol('/tmp/src/function-reference.ts', ''), + {__symbolic: 'reference', name: 'indirectRecursion'})) + .toThrow(new Error( + 'Recursion not supported, resolving symbol indirectRecursion in /tmp/src/function-reference.ts, resolving symbol in /tmp/src/function-reference.ts')); + }); + it('should simplify a spread expression', () => { + expect(simplify(new StaticSymbol('/tmp/src/spread.ts', ''), { + __symbolic: 'reference', + name: 'spread' + })).toEqual([0, 1, 2, 3, 4, 5]); + }); }); class MockReflectorHost implements StaticReflectorHost { @@ -303,6 +338,7 @@ class MockReflectorHost implements StaticReflectorHost { coreDecorators: 'angular2/src/core/metadata', diDecorators: 'angular2/src/core/di/decorators', diMetadata: 'angular2/src/core/di/metadata', + diOpaqueToken: 'angular2/src/core/di/opaque_token', animationMetadata: 'angular2/src/core/animation/metadata', provider: 'angular2/src/core/di/provider' }; @@ -624,6 +660,138 @@ class MockReflectorHost implements StaticReflectorHost { character: 33 } } + }, + '/tmp/src/function-declaration.d.ts': { + __symbolic: 'module', + version: 1, + metadata: { + one: { + __symbolic: 'function', + parameters: ['a'], + value: [ + {__symbolic: 'reference', name: 'a'} + ] + }, + add: { + __symbolic: 'function', + parameters: ['a','b'], + value: { + __symbolic: 'binop', + operator: '+', + left: {__symbolic: 'reference', name: 'a'}, + right: {__symbolic: 'reference', name: 'b'} + } + } + } + }, + '/tmp/src/function-reference.ts': { + __symbolic: 'module', + version: 1, + metadata: { + one: { + __symbolic: 'call', + expression: { + __symbolic: 'reference', + module: './function-declaration', + name: 'one' + }, + arguments: ['some-value'] + }, + two: { + __symbolic: 'call', + expression: { + __symbolic: 'reference', + module: './function-declaration', + name: 'add' + }, + arguments: [1, 1] + }, + recursion: { + __symbolic: 'call', + expression: { + __symbolic: 'reference', + module: './function-recursive', + name: 'recursive' + }, + arguments: [1] + }, + indirectRecursion: { + __symbolic: 'call', + expression: { + __symbolic: 'reference', + module: './function-recursive', + name: 'indirectRecursion1' + }, + arguments: [1] + } + } + }, + '/tmp/src/function-recursive.d.ts': { + __symbolic: 'modules', + version: 1, + metadata: { + recursive: { + __symbolic: 'function', + parameters: ['a'], + value: { + __symbolic: 'call', + expression: { + __symbolic: 'reference', + module: './function-recursive', + name: 'recursive', + }, + arguments: [ + { + __symbolic: 'reference', + name: 'a' + } + ] + } + }, + indirectRecursion1: { + __symbolic: 'function', + parameters: ['a'], + value: { + __symbolic: 'call', + expression: { + __symbolic: 'reference', + module: './function-recursive', + name: 'indirectRecursion2', + }, + arguments: [ + { + __symbolic: 'reference', + name: 'a' + } + ] + } + }, + indirectRecursion2: { + __symbolic: 'function', + parameters: ['a'], + value: { + __symbolic: 'call', + expression: { + __symbolic: 'reference', + module: './function-recursive', + name: 'indirectRecursion1', + }, + arguments: [ + { + __symbolic: 'reference', + name: 'a' + } + ] + } + } + }, + }, + '/tmp/src/spread.ts': { + __symbolic: 'module', + version: 1, + metadata: { + spread: [0, {__symbolic: 'spread', expression: [1, 2, 3, 4]}, 5] + } } }; return data[moduleId]; diff --git a/tools/@angular/tsc-wrapped/src/collector.ts b/tools/@angular/tsc-wrapped/src/collector.ts index 9f83117bec..0506d68e58 100644 --- a/tools/@angular/tsc-wrapped/src/collector.ts +++ b/tools/@angular/tsc-wrapped/src/collector.ts @@ -152,6 +152,30 @@ export class MetadataCollector { } // Otherwise don't record metadata for the class. break; + case ts.SyntaxKind.FunctionDeclaration: + // Record functions that return a single value. Record the parameter + // 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) + }; + } + } + } + } + // Otherwise don't record the function. + break; case ts.SyntaxKind.VariableStatement: const variableStatement = node; for (let variableDeclaration of variableStatement.declarationList.declarations) { @@ -209,3 +233,26 @@ export class MetadataCollector { return metadata && {__symbolic: 'module', version: VERSION, metadata}; } } + +// Collect parameter names from a function. +function namesOf(parameters: ts.NodeArray): string[] { + let result: string[] = []; + + function addNamesOf(name: ts.Identifier | ts.BindingPattern) { + if (name.kind == ts.SyntaxKind.Identifier) { + const identifier = name; + result.push(identifier.text); + } else { + const bindingPattern = name; + for (let element of bindingPattern.elements) { + addNamesOf(element.name); + } + } + } + + for (let parameter of parameters) { + addNamesOf(parameter.name); + } + + return result; +} \ No newline at end of file diff --git a/tools/@angular/tsc-wrapped/src/evaluator.ts b/tools/@angular/tsc-wrapped/src/evaluator.ts index e6cdf29276..e5aabb6de5 100644 --- a/tools/@angular/tsc-wrapped/src/evaluator.ts +++ b/tools/@angular/tsc-wrapped/src/evaluator.ts @@ -1,6 +1,6 @@ import * as ts from 'typescript'; -import {MetadataError, MetadataGlobalReferenceExpression, MetadataImportedSymbolReferenceExpression, MetadataSymbolicCallExpression, MetadataSymbolicReferenceExpression, MetadataValue, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataImportedSymbolReferenceExpression, isMetadataModuleReferenceExpression, isMetadataSymbolicReferenceExpression} from './schema'; +import {MetadataError, MetadataGlobalReferenceExpression, MetadataImportedSymbolReferenceExpression, MetadataSymbolicCallExpression, MetadataSymbolicReferenceExpression, MetadataValue, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataImportedSymbolReferenceExpression, isMetadataModuleReferenceExpression, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSpreadExpression} from './schema'; import {Symbols} from './symbols'; function isMethodCallOf(callExpression: ts.CallExpression, memberName: string): boolean { @@ -187,7 +187,7 @@ export class Evaluator { case ts.SyntaxKind.Identifier: let identifier = node; let reference = this.symbols.resolve(identifier.text); - if (isPrimitive(reference)) { + if (reference !== undefined && isPrimitive(reference)) { return true; } break; @@ -207,14 +207,17 @@ export class Evaluator { let obj: {[name: string]: any} = {}; ts.forEachChild(node, child => { switch (child.kind) { + case ts.SyntaxKind.ShorthandPropertyAssignment: case ts.SyntaxKind.PropertyAssignment: - const assignment = child; + const assignment = child; const propertyName = this.nameOf(assignment.name); if (isMetadataError(propertyName)) { error = propertyName; return true; } - const propertyValue = this.evaluateNode(assignment.initializer); + const propertyValue = isPropertyAssignment(assignment) ? + this.evaluateNode(assignment.initializer) : + {__symbolic: 'reference', name: propertyName}; if (isMetadataError(propertyValue)) { error = propertyValue; return true; // Stop the forEachChild. @@ -229,14 +232,31 @@ export class Evaluator { let arr: MetadataValue[] = []; ts.forEachChild(node, child => { const value = this.evaluateNode(child); + + // Check for error if (isMetadataError(value)) { error = value; return true; // Stop the forEachChild. } + + // Handle spread expressions + if (isMetadataSymbolicSpreadExpression(value)) { + if (Array.isArray(value.expression)) { + for (let spreadValue of value.expression) { + arr.push(spreadValue); + } + return; + } + } + arr.push(value); }); if (error) return error; return arr; + case ts.SyntaxKind.SpreadElementExpression: + let spread = node; + let spreadExpression = this.evaluateNode(spread.expression); + return {__symbolic: 'spread', expression: spreadExpression}; case ts.SyntaxKind.CallExpression: const callExpression = node; if (isCallOf(callExpression, 'forwardRef') && callExpression.arguments.length === 1) { @@ -296,7 +316,7 @@ export class Evaluator { if (isMetadataError(member)) { return member; } - if (this.isFoldable(propertyAccessExpression.expression)) + if (expression && this.isFoldable(propertyAccessExpression.expression)) return (expression)[member]; if (isMetadataModuleReferenceExpression(expression)) { // A select into a module refrence and be converted into a reference to the symbol @@ -495,3 +515,7 @@ export class Evaluator { return errorSymbol('Expression form not supported', node); } } + +function isPropertyAssignment(node: ts.Node): node is ts.PropertyAssignment { + return node.kind == ts.SyntaxKind.PropertyAssignment; +} \ No newline at end of file diff --git a/tools/@angular/tsc-wrapped/src/schema.ts b/tools/@angular/tsc-wrapped/src/schema.ts index 4bb94790e8..431727e677 100644 --- a/tools/@angular/tsc-wrapped/src/schema.ts +++ b/tools/@angular/tsc-wrapped/src/schema.ts @@ -61,6 +61,15 @@ export function isConstructorMetadata(value: any): value is ConstructorMetadata return value && value.__symbolic === 'constructor'; } +export interface FunctionMetadata { + __symbolic: 'function'; + parameters: string[]; + result: MetadataValue; +} +export function isFunctionMetadata(value: any): value is FunctionMetadata { + return value && value.__symbolic === 'function'; +} + export type MetadataValue = string | number | boolean | MetadataObject | MetadataArray | MetadataSymbolicExpression | MetadataError; @@ -69,7 +78,7 @@ export interface MetadataObject { [name: string]: MetadataValue; } export interface MetadataArray { [name: number]: MetadataValue; } export interface MetadataSymbolicExpression { - __symbolic: 'binary'|'call'|'index'|'new'|'pre'|'reference'|'select' + __symbolic: 'binary'|'call'|'index'|'new'|'pre'|'reference'|'select'|'spread' } export function isMetadataSymbolicExpression(value: any): value is MetadataSymbolicExpression { if (value) { @@ -81,6 +90,7 @@ export function isMetadataSymbolicExpression(value: any): value is MetadataSymbo case 'pre': case 'reference': case 'select': + case 'spread': return true; } } @@ -190,6 +200,15 @@ export function isMetadataSymbolicSelectExpression(value: any): return value && value.__symbolic === 'select'; } +export interface MetadataSymbolicSpreadExpression extends MetadataSymbolicExpression { + __symbolic: 'spread'; + expression: MetadataValue; +} +export function isMetadataSymbolicSpreadExpression(value: any): + value is MetadataSymbolicSpreadExpression { + return value && value.__symbolic === 'spread'; +} + export interface MetadataError { __symbolic: 'error'; diff --git a/tools/@angular/tsc-wrapped/test/collector.spec.ts b/tools/@angular/tsc-wrapped/test/collector.spec.ts index 5a32785922..4e76fd0113 100644 --- a/tools/@angular/tsc-wrapped/test/collector.spec.ts +++ b/tools/@angular/tsc-wrapped/test/collector.spec.ts @@ -6,6 +6,7 @@ import {ClassMetadata, ConstructorMetadata, ModuleMetadata} from '../src/schema' import {Directory, Host, expectValidSources} from './typescript.mocks'; describe('Collector', () => { + let documentRegistry = ts.createDocumentRegistry(); let host: ts.LanguageServiceHost; let service: ts.LanguageService; let program: ts.Program; @@ -14,9 +15,9 @@ 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' + '/unsupported-1.ts', '/unsupported-2.ts', 'import-star.ts', 'exported-functions.ts' ]); - service = ts.createLanguageService(host); + service = ts.createLanguageService(host, documentRegistry); program = service.getProgram(); collector = new MetadataCollector(); }); @@ -246,6 +247,75 @@ describe('Collector', () => { {__symbolic: 'reference', module: 'angular2/common', name: 'NgFor'} ]); }); + + it('should be able to record functions', () => { + let exportedFunctions = program.getSourceFile('/exported-functions.ts'); + let metadata = collector.getMetadata(exportedFunctions); + expect(metadata).toEqual({ + __symbolic: 'module', + version: 1, + metadata: { + one: { + __symbolic: 'function', + parameters: ['a', 'b', 'c'], + value: { + a: {__symbolic: 'reference', name: 'a'}, + b: {__symbolic: 'reference', name: 'b'}, + c: {__symbolic: 'reference', name: 'c'} + } + }, + two: { + __symbolic: 'function', + parameters: ['a', 'b', 'c'], + value: { + a: {__symbolic: 'reference', name: 'a'}, + b: {__symbolic: 'reference', name: 'b'}, + c: {__symbolic: 'reference', name: 'c'} + } + }, + three: { + __symbolic: 'function', + parameters: ['a', 'b', 'c'], + value: [ + {__symbolic: 'reference', name: 'a'}, {__symbolic: 'reference', name: 'b'}, + {__symbolic: 'reference', name: 'c'} + ] + }, + supportsState: { + __symbolic: 'function', + parameters: [], + value: { + __symbolic: 'pre', + operator: '!', + operand: { + __symbolic: 'pre', + operator: '!', + operand: { + __symbolic: 'select', + expression: { + __symbolic: 'select', + expression: {__symbolic: 'reference', name: 'window'}, + member: 'history' + }, + member: 'pushState' + } + } + } + } + } + }); + }); + + it('should be able to handle import star type references', () => { + let importStar = program.getSourceFile('/import-star.ts'); + let metadata = collector.getMetadata(importStar); + let someClass = metadata.metadata['SomeClass']; + let ctor = someClass.members['__ctor__'][0]; + let parameters = ctor.parameters; + expect(parameters).toEqual([ + {__symbolic: 'reference', module: 'angular2/common', name: 'NgFor'} + ]); + }); }); // TODO: Do not use \` in a template literal as it confuses clang-format @@ -447,9 +517,9 @@ const FILES: Directory = { `, 'unsupported-2.ts': ` import {Injectable} from 'angular2/core'; - + class Foo {} - + @Injectable() export class Bar { constructor(private f: Foo) {} @@ -464,6 +534,20 @@ const FILES: Directory = { constructor(private f: common.NgFor) {} } `, + 'exported-functions.ts': ` + export function one(a: string, b: string, c: string) { + return {a: a, b: b, c: c}; + } + export function two(a: string, b: string, c: string) { + return {a, b, c}; + } + export function three({a, b, c}: {a: string, b: string, c: string}) { + return [a, b, c]; + } + export function supportsState(): boolean { + return !!window.history.pushState; + } + `, 'node_modules': { 'angular2': { 'core.d.ts': ` diff --git a/tools/@angular/tsc-wrapped/test/evaluator.spec.ts b/tools/@angular/tsc-wrapped/test/evaluator.spec.ts index f47841bf1f..88b48c6127 100644 --- a/tools/@angular/tsc-wrapped/test/evaluator.spec.ts +++ b/tools/@angular/tsc-wrapped/test/evaluator.spec.ts @@ -48,8 +48,14 @@ describe('Evaluator', () => { it('should be able to fold expressions with foldable references', () => { var expressions = program.getSourceFile('expressions.ts'); + symbols.define('someName', 'some-name'); + symbols.define('someBool', true); + symbols.define('one', 1); + symbols.define('two', 2); expect(evaluator.isFoldable(findVar(expressions, 'three').initializer)).toBeTruthy(); expect(evaluator.isFoldable(findVar(expressions, 'four').initializer)).toBeTruthy(); + symbols.define('three', 3); + symbols.define('four', 4); expect(evaluator.isFoldable(findVar(expressions, 'obj').initializer)).toBeTruthy(); expect(evaluator.isFoldable(findVar(expressions, 'arr').initializer)).toBeTruthy(); }); @@ -183,6 +189,21 @@ describe('Evaluator', () => { character: 11 }); }); + + it('should be able to fold an array spread', () => { + let expressions = program.getSourceFile('expressions.ts'); + symbols.define('arr', [1, 2, 3, 4]); + let arrSpread = findVar(expressions, 'arrSpread'); + expect(evaluator.evaluateNode(arrSpread.initializer)).toEqual([0, 1, 2, 3, 4, 5]); + }); + + it('should be able to produce a spread expression', () => { + let expressions = program.getSourceFile('expressions.ts'); + let arrSpreadRef = findVar(expressions, 'arrSpreadRef'); + expect(evaluator.evaluateNode(arrSpreadRef.initializer)).toEqual([ + 0, {__symbolic: 'spread', expression: {__symbolic: 'reference', name: 'arrImport'}}, 5 + ]); + }); }); const FILES: Directory = { @@ -201,8 +222,11 @@ const FILES: Directory = { export var someBool = true; export var one = 1; export var two = 2; + export var arrImport = [1, 2, 3, 4]; `, 'expressions.ts': ` + import {arrImport} from './consts'; + export var someName = 'some-name'; export var someBool = true; export var one = 1; @@ -236,6 +260,10 @@ const FILES: Directory = { export var bShiftRight = -1 >> 2; // -1 export var bShiftRightU = -1 >>> 2; // 0x3fffffff + export var arrSpread = [0, ...arr, 5]; + + export var arrSpreadRef = [0, ...arrImport, 5]; + export var recursiveA = recursiveB; export var recursiveB = recursiveA; `,