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:
@ -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 {
|
||||
<input #a>{{a.value}}
|
||||
<div *ngIf="true">{{a.value}}</div>
|
||||
`,
|
||||
directives: [common.NgIf]
|
||||
directives: [wrapInArray(common.NgIf)]
|
||||
})
|
||||
export class CompWithReferences {
|
||||
}
|
||||
|
@ -0,0 +1,3 @@
|
||||
export function wrapInArray(value: any): any[] {
|
||||
return [value];
|
||||
}
|
@ -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'
|
||||
};
|
||||
|
@ -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<StaticSymbol, any[]>();
|
||||
private metadataCache = new Map<string, {[key: string]: any}>();
|
||||
private conversionMap = new Map<StaticSymbol, (context: StaticSymbol, args: any[]) => 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<StaticSymbol, boolean>();
|
||||
|
||||
function simplify(expression: any): any {
|
||||
if (isPrimitive(expression)) {
|
||||
return expression;
|
||||
}
|
||||
if (expression instanceof Array) {
|
||||
let result: any[] = [];
|
||||
for (let item of (<any>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 (<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 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<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);
|
||||
}
|
||||
|
@ -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];
|
||||
|
Reference in New Issue
Block a user