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

@ -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 = <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:
const variableStatement = <ts.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<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 {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 = <ts.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 = <ts.PropertyAssignment>child;
const assignment = <ts.PropertyAssignment|ts.ShorthandPropertyAssignment>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 = <ts.SpreadElementExpression>node;
let spreadExpression = this.evaluateNode(spread.expression);
return {__symbolic: 'spread', expression: spreadExpression};
case ts.SyntaxKind.CallExpression:
const callExpression = <ts.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 (<any>expression)[<string>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;
}

View File

@ -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';

View File

@ -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 = <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
@ -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': `

View File

@ -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;
`,