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.
This commit is contained in:
Chuck Jazdzewski 2016-06-13 15:56:51 -07:00 committed by GitHub
parent 5c0cfdee48
commit 5504ca1e38
10 changed files with 642 additions and 142 deletions

View File

@ -1,6 +1,8 @@
import * as common from '@angular/common'; import * as common from '@angular/common';
import {Component, Inject, OpaqueToken} from '@angular/core'; import {Component, Inject, OpaqueToken} from '@angular/core';
import {wrapInArray} from './funcs';
export const SOME_OPAQUE_TOKEN = new OpaqueToken('opaqueToken'); export const SOME_OPAQUE_TOKEN = new OpaqueToken('opaqueToken');
@Component({ @Component({
@ -23,7 +25,7 @@ export class CompWithProviders {
<input #a>{{a.value}} <input #a>{{a.value}}
<div *ngIf="true">{{a.value}}</div> <div *ngIf="true">{{a.value}}</div>
`, `,
directives: [common.NgIf] directives: [wrapInArray(common.NgIf)]
}) })
export class CompWithReferences { export class CompWithReferences {
} }

View File

@ -0,0 +1,3 @@
export function wrapInArray(value: any): any[] {
return [value];
}

View File

@ -29,6 +29,7 @@ export class ReflectorHost implements StaticReflectorHost, ImportGenerator {
coreDecorators: '@angular/core/src/metadata', coreDecorators: '@angular/core/src/metadata',
diDecorators: '@angular/core/src/di/decorators', diDecorators: '@angular/core/src/di/decorators',
diMetadata: '@angular/core/src/di/metadata', diMetadata: '@angular/core/src/di/metadata',
diOpaqueToken: '@angular/core/src/di/opaque_token',
animationMetadata: '@angular/core/src/animation/metadata', animationMetadata: '@angular/core/src/animation/metadata',
provider: '@angular/core/src/di/provider' provider: '@angular/core/src/di/provider'
}; };

View File

@ -32,6 +32,7 @@ export interface StaticReflectorHost {
coreDecorators: string, coreDecorators: string,
diDecorators: string, diDecorators: string,
diMetadata: string, diMetadata: string,
diOpaqueToken: string,
animationMetadata: string, animationMetadata: string,
provider: string provider: string
}; };
@ -56,6 +57,7 @@ export class StaticReflector implements ReflectorReader {
private parameterCache = new Map<StaticSymbol, any[]>(); private parameterCache = new Map<StaticSymbol, any[]>();
private metadataCache = new Map<string, {[key: string]: any}>(); private metadataCache = new Map<string, {[key: string]: any}>();
private conversionMap = new Map<StaticSymbol, (context: StaticSymbol, args: any[]) => any>(); private conversionMap = new Map<StaticSymbol, (context: StaticSymbol, args: any[]) => any>();
private opaqueToken: StaticSymbol;
constructor(private host: StaticReflectorHost) { this.initializeConversionMap(); } constructor(private host: StaticReflectorHost) { this.initializeConversionMap(); }
@ -177,8 +179,9 @@ export class StaticReflector implements ReflectorReader {
} }
private initializeConversionMap(): void { private initializeConversionMap(): void {
const {coreDecorators, diDecorators, diMetadata, animationMetadata, provider} = const {coreDecorators, diDecorators, diMetadata, diOpaqueToken, animationMetadata, provider} =
this.host.angularImportLocations(); this.host.angularImportLocations();
this.opaqueToken = this.host.findDeclaration(diOpaqueToken, 'OpaqueToken');
this.registerDecoratorOrConstructor(this.host.findDeclaration(provider, 'Provider'), Provider); this.registerDecoratorOrConstructor(this.host.findDeclaration(provider, 'Provider'), Provider);
this.registerDecoratorOrConstructor( this.registerDecoratorOrConstructor(
@ -245,147 +248,231 @@ export class StaticReflector implements ReflectorReader {
/** @internal */ /** @internal */
public simplify(context: StaticSymbol, value: any): any { public simplify(context: StaticSymbol, value: any): any {
let _this = this; let _this = this;
let scope = BindingScope.empty;
let calling = new Map<StaticSymbol, boolean>();
function simplify(expression: any): any { function simplifyInContext(context: StaticSymbol, value: any): any {
if (isPrimitive(expression)) { function resolveReference(expression: any): StaticSymbol {
return expression; let staticSymbol: StaticSymbol;
} if (expression['module']) {
if (expression instanceof Array) { staticSymbol = _this.host.findDeclaration(
let result: any[] = []; expression['module'], expression['name'], context.filePath);
for (let item of (<any>expression)) { } else {
result.push(simplify(item)); staticSymbol = _this.host.getStaticSymbol(context.filePath, expression['name']);
} }
return result; return staticSymbol;
} }
if (expression) {
if (expression['__symbolic']) { function isOpaqueToken(value: any): boolean {
let staticSymbol: StaticSymbol; if (value && value.__symbolic === 'new' && value.expression) {
switch (expression['__symbolic']) { let target = value.expression;
case 'binop': if (target.__symbolic == 'reference') {
let left = simplify(expression['left']); return sameSymbol(resolveReference(target), _this.opaqueToken);
let right = simplify(expression['right']); }
switch (expression['operator']) { }
case '&&': return false;
return left && right; }
case '||':
return left || right; function simplifyCall(expression: any) {
case '|': if (expression['__symbolic'] == 'call') {
return left | right; let target = expression['expression'];
case '^': let targetFunction = simplify(target);
return left ^ right; if (targetFunction['__symbolic'] == 'function') {
case '&': if (calling.get(targetFunction)) {
return left & right; throw new Error('Recursion not supported');
case '==': }
return left == right; calling.set(targetFunction, true);
case '!=': let value = targetFunction['value'];
return left != right; if (value) {
case '===': // Determine the arguments
return left === right; let args = (expression['arguments'] || []).map((arg: any) => simplify(arg));
case '!==': let parameters: string[] = targetFunction['parameters'];
return left !== right; let functionScope = BindingScope.build();
case '<': for (let i = 0; i < parameters.length; i++) {
return left < right; functionScope.define(parameters[i], args[i]);
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; let oldScope = scope;
case 'pre': let result: any;
let operand = simplify(expression['operand']); try {
switch (expression['operator']) { scope = functionScope.done();
case '+': result = simplify(value);
return operand; } finally {
case '-': scope = oldScope;
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);
} }
return result; return result;
case 'class': }
return context; calling.delete(targetFunction);
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);
} }
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 (<any>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 simplifyInContext(context, value);
return simplify(value);
} catch (e) {
throw new Error(`${e.message}, resolving symbol ${context.name} in ${context.filePath}`);
}
} }
/** /**
@ -460,3 +547,40 @@ function mapStringMap(input: {[key: string]: any}, transform: (value: any, key:
function isPrimitive(o: any): boolean { function isPrimitive(o: any): boolean {
return o === null || (typeof o !== 'function' && typeof o !== 'object'); 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<string, any>();
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<string, any>) { 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);
}

View File

@ -293,6 +293,41 @@ describe('StaticReflector', () => {
({__symbolic: 'reference', module: './extern', name: 'nonExisting'}))) ({__symbolic: 'reference', module: './extern', name: 'nonExisting'})))
.toEqual(host.getStaticSymbol('/src/extern.d.ts', '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 { class MockReflectorHost implements StaticReflectorHost {
@ -303,6 +338,7 @@ class MockReflectorHost implements StaticReflectorHost {
coreDecorators: 'angular2/src/core/metadata', coreDecorators: 'angular2/src/core/metadata',
diDecorators: 'angular2/src/core/di/decorators', diDecorators: 'angular2/src/core/di/decorators',
diMetadata: 'angular2/src/core/di/metadata', diMetadata: 'angular2/src/core/di/metadata',
diOpaqueToken: 'angular2/src/core/di/opaque_token',
animationMetadata: 'angular2/src/core/animation/metadata', animationMetadata: 'angular2/src/core/animation/metadata',
provider: 'angular2/src/core/di/provider' provider: 'angular2/src/core/di/provider'
}; };
@ -624,6 +660,138 @@ class MockReflectorHost implements StaticReflectorHost {
character: 33 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]; return data[moduleId];

View File

@ -152,6 +152,30 @@ export class MetadataCollector {
} }
// Otherwise don't record metadata for the class. // Otherwise don't record metadata for the class.
break; 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 = <ts.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 = <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.
break;
case ts.SyntaxKind.VariableStatement: case ts.SyntaxKind.VariableStatement:
const variableStatement = <ts.VariableStatement>node; const variableStatement = <ts.VariableStatement>node;
for (let variableDeclaration of variableStatement.declarationList.declarations) { for (let variableDeclaration of variableStatement.declarationList.declarations) {
@ -209,3 +233,26 @@ export class MetadataCollector {
return metadata && {__symbolic: 'module', version: VERSION, metadata}; return metadata && {__symbolic: 'module', version: VERSION, metadata};
} }
} }
// Collect parameter names from a function.
function namesOf(parameters: ts.NodeArray<ts.ParameterDeclaration>): string[] {
let result: string[] = [];
function addNamesOf(name: ts.Identifier | ts.BindingPattern) {
if (name.kind == ts.SyntaxKind.Identifier) {
const identifier = <ts.Identifier>name;
result.push(identifier.text);
} else {
const bindingPattern = <ts.BindingPattern>name;
for (let element of bindingPattern.elements) {
addNamesOf(element.name);
}
}
}
for (let parameter of parameters) {
addNamesOf(parameter.name);
}
return result;
}

View File

@ -1,6 +1,6 @@
import * as ts from 'typescript'; 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'; import {Symbols} from './symbols';
function isMethodCallOf(callExpression: ts.CallExpression, memberName: string): boolean { function isMethodCallOf(callExpression: ts.CallExpression, memberName: string): boolean {
@ -187,7 +187,7 @@ export class Evaluator {
case ts.SyntaxKind.Identifier: case ts.SyntaxKind.Identifier:
let identifier = <ts.Identifier>node; let identifier = <ts.Identifier>node;
let reference = this.symbols.resolve(identifier.text); let reference = this.symbols.resolve(identifier.text);
if (isPrimitive(reference)) { if (reference !== undefined && isPrimitive(reference)) {
return true; return true;
} }
break; break;
@ -207,14 +207,17 @@ export class Evaluator {
let obj: {[name: string]: any} = {}; let obj: {[name: string]: any} = {};
ts.forEachChild(node, child => { ts.forEachChild(node, child => {
switch (child.kind) { switch (child.kind) {
case ts.SyntaxKind.ShorthandPropertyAssignment:
case ts.SyntaxKind.PropertyAssignment: case ts.SyntaxKind.PropertyAssignment:
const assignment = <ts.PropertyAssignment>child; const assignment = <ts.PropertyAssignment|ts.ShorthandPropertyAssignment>child;
const propertyName = this.nameOf(assignment.name); const propertyName = this.nameOf(assignment.name);
if (isMetadataError(propertyName)) { if (isMetadataError(propertyName)) {
error = propertyName; error = propertyName;
return true; return true;
} }
const propertyValue = this.evaluateNode(assignment.initializer); const propertyValue = isPropertyAssignment(assignment) ?
this.evaluateNode(assignment.initializer) :
{__symbolic: 'reference', name: propertyName};
if (isMetadataError(propertyValue)) { if (isMetadataError(propertyValue)) {
error = propertyValue; error = propertyValue;
return true; // Stop the forEachChild. return true; // Stop the forEachChild.
@ -229,14 +232,31 @@ export class Evaluator {
let arr: MetadataValue[] = []; let arr: MetadataValue[] = [];
ts.forEachChild(node, child => { ts.forEachChild(node, child => {
const value = this.evaluateNode(child); const value = this.evaluateNode(child);
// Check for error
if (isMetadataError(value)) { if (isMetadataError(value)) {
error = value; error = value;
return true; // Stop the forEachChild. 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); arr.push(value);
}); });
if (error) return error; if (error) return error;
return arr; return arr;
case ts.SyntaxKind.SpreadElementExpression:
let spread = <ts.SpreadElementExpression>node;
let spreadExpression = this.evaluateNode(spread.expression);
return {__symbolic: 'spread', expression: spreadExpression};
case ts.SyntaxKind.CallExpression: case ts.SyntaxKind.CallExpression:
const callExpression = <ts.CallExpression>node; const callExpression = <ts.CallExpression>node;
if (isCallOf(callExpression, 'forwardRef') && callExpression.arguments.length === 1) { if (isCallOf(callExpression, 'forwardRef') && callExpression.arguments.length === 1) {
@ -296,7 +316,7 @@ export class Evaluator {
if (isMetadataError(member)) { if (isMetadataError(member)) {
return member; return member;
} }
if (this.isFoldable(propertyAccessExpression.expression)) if (expression && this.isFoldable(propertyAccessExpression.expression))
return (<any>expression)[<string>member]; return (<any>expression)[<string>member];
if (isMetadataModuleReferenceExpression(expression)) { if (isMetadataModuleReferenceExpression(expression)) {
// A select into a module refrence and be converted into a reference to the symbol // 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); return errorSymbol('Expression form not supported', node);
} }
} }
function isPropertyAssignment(node: ts.Node): node is ts.PropertyAssignment {
return node.kind == ts.SyntaxKind.PropertyAssignment;
}

View File

@ -61,6 +61,15 @@ export function isConstructorMetadata(value: any): value is ConstructorMetadata
return value && value.__symbolic === 'constructor'; 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 | export type MetadataValue = string | number | boolean | MetadataObject | MetadataArray |
MetadataSymbolicExpression | MetadataError; MetadataSymbolicExpression | MetadataError;
@ -69,7 +78,7 @@ export interface MetadataObject { [name: string]: MetadataValue; }
export interface MetadataArray { [name: number]: MetadataValue; } export interface MetadataArray { [name: number]: MetadataValue; }
export interface MetadataSymbolicExpression { 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 { export function isMetadataSymbolicExpression(value: any): value is MetadataSymbolicExpression {
if (value) { if (value) {
@ -81,6 +90,7 @@ export function isMetadataSymbolicExpression(value: any): value is MetadataSymbo
case 'pre': case 'pre':
case 'reference': case 'reference':
case 'select': case 'select':
case 'spread':
return true; return true;
} }
} }
@ -190,6 +200,15 @@ export function isMetadataSymbolicSelectExpression(value: any):
return value && value.__symbolic === 'select'; 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 { export interface MetadataError {
__symbolic: 'error'; __symbolic: 'error';

View File

@ -6,6 +6,7 @@ import {ClassMetadata, ConstructorMetadata, ModuleMetadata} from '../src/schema'
import {Directory, Host, expectValidSources} from './typescript.mocks'; import {Directory, Host, expectValidSources} from './typescript.mocks';
describe('Collector', () => { describe('Collector', () => {
let documentRegistry = ts.createDocumentRegistry();
let host: ts.LanguageServiceHost; let host: ts.LanguageServiceHost;
let service: ts.LanguageService; let service: ts.LanguageService;
let program: ts.Program; let program: ts.Program;
@ -14,9 +15,9 @@ describe('Collector', () => {
beforeEach(() => { beforeEach(() => {
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' '/unsupported-1.ts', '/unsupported-2.ts', 'import-star.ts', 'exported-functions.ts'
]); ]);
service = ts.createLanguageService(host); service = ts.createLanguageService(host, documentRegistry);
program = service.getProgram(); program = service.getProgram();
collector = new MetadataCollector(); collector = new MetadataCollector();
}); });
@ -246,6 +247,75 @@ describe('Collector', () => {
{__symbolic: 'reference', module: 'angular2/common', name: 'NgFor'} {__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 = <ClassMetadata>metadata.metadata['SomeClass'];
let ctor = <ConstructorMetadata>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 // TODO: Do not use \` in a template literal as it confuses clang-format
@ -447,9 +517,9 @@ const FILES: Directory = {
`, `,
'unsupported-2.ts': ` 'unsupported-2.ts': `
import {Injectable} from 'angular2/core'; import {Injectable} from 'angular2/core';
class Foo {} class Foo {}
@Injectable() @Injectable()
export class Bar { export class Bar {
constructor(private f: Foo) {} constructor(private f: Foo) {}
@ -464,6 +534,20 @@ const FILES: Directory = {
constructor(private f: common.NgFor) {} 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': { 'node_modules': {
'angular2': { 'angular2': {
'core.d.ts': ` 'core.d.ts': `

View File

@ -48,8 +48,14 @@ describe('Evaluator', () => {
it('should be able to fold expressions with foldable references', () => { it('should be able to fold expressions with foldable references', () => {
var expressions = program.getSourceFile('expressions.ts'); 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, 'three').initializer)).toBeTruthy();
expect(evaluator.isFoldable(findVar(expressions, 'four').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, 'obj').initializer)).toBeTruthy();
expect(evaluator.isFoldable(findVar(expressions, 'arr').initializer)).toBeTruthy(); expect(evaluator.isFoldable(findVar(expressions, 'arr').initializer)).toBeTruthy();
}); });
@ -183,6 +189,21 @@ describe('Evaluator', () => {
character: 11 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 = { const FILES: Directory = {
@ -201,8 +222,11 @@ const FILES: Directory = {
export var someBool = true; export var someBool = true;
export var one = 1; export var one = 1;
export var two = 2; export var two = 2;
export var arrImport = [1, 2, 3, 4];
`, `,
'expressions.ts': ` 'expressions.ts': `
import {arrImport} from './consts';
export var someName = 'some-name'; export var someName = 'some-name';
export var someBool = true; export var someBool = true;
export var one = 1; export var one = 1;
@ -236,6 +260,10 @@ const FILES: Directory = {
export var bShiftRight = -1 >> 2; // -1 export var bShiftRight = -1 >> 2; // -1
export var bShiftRightU = -1 >>> 2; // 0x3fffffff export var bShiftRightU = -1 >>> 2; // 0x3fffffff
export var arrSpread = [0, ...arr, 5];
export var arrSpreadRef = [0, ...arrImport, 5];
export var recursiveA = recursiveB; export var recursiveA = recursiveB;
export var recursiveB = recursiveA; export var recursiveB = recursiveA;
`, `,