feat(ivy): support array and object literals in binding expressions (#22336)

PR Close #22336
This commit is contained in:
Chuck Jazdzewski 2018-02-14 17:12:05 -08:00 committed by Alex Eagle
parent ec445b5c73
commit 49f074f61d
6 changed files with 503 additions and 18 deletions

View File

@ -97,7 +97,7 @@ export enum BindingForm {
General,
// Try to generate a simple binding (no temporaries or statements)
// otherise generate a general binding
// otherwise generate a general binding
TrySimple,
}
@ -341,7 +341,7 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
}
visitLiteralPrimitive(ast: cdAst.LiteralPrimitive, mode: _Mode): any {
// For literal values of null, undefined, true, or false allow type inteference
// For literal values of null, undefined, true, or false allow type interference
// to infer the type.
const type =
ast.value === null || ast.value === undefined || ast.value === true || ast.value === true ?
@ -648,7 +648,7 @@ function convertStmtIntoExpression(stmt: o.Statement): o.Expression|null {
return null;
}
class BuiltinFunctionCall extends cdAst.FunctionCall {
export class BuiltinFunctionCall extends cdAst.FunctionCall {
constructor(span: cdAst.ParseSpan, public args: cdAst.AST[], public converter: BuiltinConverter) {
super(span, null, args);
}

View File

@ -13,6 +13,14 @@ const CONSTANT_PREFIX = '_c';
export const enum DefinitionKind {Injector, Directive, Component, Pipe}
/**
* Context to use when producing a key.
*
* This ensures we see the constant not the reference variable when producing
* a key.
*/
const KEY_CONTEXT = {};
/**
* A node that is a place-holder that allows the node to be replaced when the actual
* node is known.
@ -22,18 +30,31 @@ export const enum DefinitionKind {Injector, Directive, Component, Pipe}
* change the referenced expression.
*/
class FixupExpression extends o.Expression {
constructor(public resolved: o.Expression) { super(resolved.type); }
private original: o.Expression;
shared: boolean;
constructor(public resolved: o.Expression) {
super(resolved.type);
this.original = resolved;
}
visitExpression(visitor: o.ExpressionVisitor, context: any): any {
if (context === KEY_CONTEXT) {
// When producing a key we want to traverse the constant not the
// variable used to refer to it.
return this.original.visitExpression(visitor, context);
} else {
return this.resolved.visitExpression(visitor, context);
}
}
isEquivalent(e: o.Expression): boolean {
return e instanceof FixupExpression && this.resolved.isEquivalent(e.resolved);
}
isConstant() { return true; }
fixup(expression: o.Expression) {
this.resolved = expression;
this.shared = true;
@ -48,6 +69,7 @@ class FixupExpression extends o.Expression {
export class ConstantPool {
statements: o.Statement[] = [];
private literals = new Map<string, FixupExpression>();
private literalFactories = new Map<string, o.Expression>();
private injectorDefinitions = new Map<any, FixupExpression>();
private directiveDefinitions = new Map<any, FixupExpression>();
private componentDefinitions = new Map<any, FixupExpression>();
@ -56,6 +78,11 @@ export class ConstantPool {
private nextNameIndex = 0;
getConstLiteral(literal: o.Expression, forceShared?: boolean): o.Expression {
if (literal instanceof o.LiteralExpr || literal instanceof FixupExpression) {
// Do no put simple literals into the constant pool or try to produce a constant for a
// reference to a constant.
return literal;
}
const key = this.keyOf(literal);
let fixup = this.literals.get(key);
let newValue = false;
@ -97,6 +124,54 @@ export class ConstantPool {
return fixup;
}
getLiteralFactory(literal: o.LiteralArrayExpr|o.LiteralMapExpr):
{literalFactory: o.Expression, literalFactoryArguments: o.Expression[]} {
// Create a pure function that builds an array of a mix of constant and variable expressions
if (literal instanceof o.LiteralArrayExpr) {
const argumentsForKey = literal.entries.map(e => e.isConstant() ? e : o.literal(null));
const key = this.keyOf(o.literalArr(argumentsForKey));
return this._getLiteralFactory(key, literal.entries, entries => o.literalArr(entries));
} else {
const expressionForKey = o.literalMap(
literal.entries.map(e => ({
key: e.key,
value: e.value.isConstant() ? e.value : o.literal(null),
quoted: e.quoted
})));
const key = this.keyOf(expressionForKey);
return this._getLiteralFactory(
key, literal.entries.map(e => e.value),
entries => o.literalMap(entries.map((value, index) => ({
key: literal.entries[index].key,
value,
quoted: literal.entries[index].quoted
}))));
}
}
private _getLiteralFactory(
key: string, values: o.Expression[], resultMap: (parameters: o.Expression[]) => o.Expression):
{literalFactory: o.Expression, literalFactoryArguments: o.Expression[]} {
let literalFactory = this.literalFactories.get(key);
const literalFactoryArguments = values.filter((e => !e.isConstant()));
if (!literalFactory) {
const resultExpressions = values.map(
(e, index) => e.isConstant() ? this.getConstLiteral(e, true) : o.variable(`a${index}`));
const parameters =
resultExpressions.filter(isVariable).map(e => new o.FnParam(e.name !, o.DYNAMIC_TYPE));
const pureFunctionDeclaration =
o.fn(parameters, [new o.ReturnStatement(resultMap(resultExpressions))], o.INFERRED_TYPE);
const name = this.freshName();
this.statements.push(
o.variable(name).set(pureFunctionDeclaration).toDeclStmt(o.INFERRED_TYPE, [
o.StmtModifier.Final
]));
literalFactory = o.variable(name);
this.literalFactories.set(key, literalFactory);
}
return {literalFactory, literalFactoryArguments};
}
/**
* Produce a unique name.
*
@ -139,7 +214,7 @@ export class ConstantPool {
private freshName(): string { return this.uniqueName(CONSTANT_PREFIX); }
private keyOf(expression: o.Expression) {
return expression.visitExpression(new KeyVisitor(), null);
return expression.visitExpression(new KeyVisitor(), KEY_CONTEXT);
}
}
@ -147,13 +222,13 @@ class KeyVisitor implements o.ExpressionVisitor {
visitLiteralExpr(ast: o.LiteralExpr): string {
return `${typeof ast.value === 'string' ? '"' + ast.value + '"' : ast.value}`;
}
visitLiteralArrayExpr(ast: o.LiteralArrayExpr): string {
return `[${ast.entries.map(entry => entry.visitExpression(this, null)).join(',')}]`;
visitLiteralArrayExpr(ast: o.LiteralArrayExpr, context: object): string {
return `[${ast.entries.map(entry => entry.visitExpression(this, context)).join(',')}]`;
}
visitLiteralMapExpr(ast: o.LiteralMapExpr): string {
visitLiteralMapExpr(ast: o.LiteralMapExpr, context: object): string {
const mapEntry = (entry: o.LiteralMapEntry) =>
`${entry.key}:${entry.value.visitExpression(this, null)}`;
`${entry.key}:${entry.value.visitExpression(this, context)}`;
return `{${ast.entries.map(mapEntry).join(',')}`;
}
@ -184,3 +259,7 @@ function invalid<T>(arg: o.Expression | o.Statement): never {
throw new Error(
`Invalid state: Visitor ${this.constructor.name} doesn't handle ${o.constructor.name}`);
}
function isVariable(e: o.Expression): e is o.ReadVarExpr {
return e instanceof o.ReadVarExpr;
}

View File

@ -144,6 +144,11 @@ export abstract class Expression {
*/
abstract isEquivalent(e: Expression): boolean;
/**
* Return true if the expression is constant.
*/
abstract isConstant(): boolean;
prop(name: string, sourceSpan?: ParseSourceSpan|null): ReadPropExpr {
return new ReadPropExpr(this, name, null, sourceSpan);
}
@ -250,10 +255,13 @@ export class ReadVarExpr extends Expression {
this.builtin = <BuiltinVar>name;
}
}
isEquivalent(e: Expression): boolean {
return e instanceof ReadVarExpr && this.name === e.name && this.builtin === e.builtin;
}
isConstant() { return false; }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitReadVarExpr(this, context);
}
@ -274,10 +282,13 @@ export class WriteVarExpr extends Expression {
super(type || value.type, sourceSpan);
this.value = value;
}
isEquivalent(e: Expression): boolean {
return e instanceof WriteVarExpr && this.name === e.name && this.value.isEquivalent(e.value);
}
isConstant() { return false; }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitWriteVarExpr(this, context);
}
@ -296,10 +307,14 @@ export class WriteKeyExpr extends Expression {
super(type || value.type, sourceSpan);
this.value = value;
}
isEquivalent(e: Expression): boolean {
return e instanceof WriteKeyExpr && this.receiver.isEquivalent(e.receiver) &&
this.index.isEquivalent(e.index) && this.value.isEquivalent(e.value);
}
isConstant() { return false; }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitWriteKeyExpr(this, context);
}
@ -314,10 +329,14 @@ export class WritePropExpr extends Expression {
super(type || value.type, sourceSpan);
this.value = value;
}
isEquivalent(e: Expression): boolean {
return e instanceof WritePropExpr && this.receiver.isEquivalent(e.receiver) &&
this.name === e.name && this.value.isEquivalent(e.value);
}
isConstant() { return false; }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitWritePropExpr(this, context);
}
@ -344,10 +363,14 @@ export class InvokeMethodExpr extends Expression {
this.builtin = <BuiltinMethod>method;
}
}
isEquivalent(e: Expression): boolean {
return e instanceof InvokeMethodExpr && this.receiver.isEquivalent(e.receiver) &&
this.name === e.name && this.builtin === e.builtin && areAllEquivalent(this.args, e.args);
}
isConstant() { return false; }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitInvokeMethodExpr(this, context);
}
@ -360,10 +383,14 @@ export class InvokeFunctionExpr extends Expression {
sourceSpan?: ParseSourceSpan|null) {
super(type, sourceSpan);
}
isEquivalent(e: Expression): boolean {
return e instanceof InvokeFunctionExpr && this.fn.isEquivalent(e.fn) &&
areAllEquivalent(this.args, e.args);
}
isConstant() { return false; }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitInvokeFunctionExpr(this, context);
}
@ -376,10 +403,14 @@ export class InstantiateExpr extends Expression {
sourceSpan?: ParseSourceSpan|null) {
super(type, sourceSpan);
}
isEquivalent(e: Expression): boolean {
return e instanceof InstantiateExpr && this.classExpr.isEquivalent(e.classExpr) &&
areAllEquivalent(this.args, e.args);
}
isConstant() { return false; }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitInstantiateExpr(this, context);
}
@ -392,9 +423,13 @@ export class LiteralExpr extends Expression {
sourceSpan?: ParseSourceSpan|null) {
super(type, sourceSpan);
}
isEquivalent(e: Expression): boolean {
return e instanceof LiteralExpr && this.value === e.value;
}
isConstant() { return true; }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitLiteralExpr(this, context);
}
@ -407,10 +442,14 @@ export class ExternalExpr extends Expression {
sourceSpan?: ParseSourceSpan|null) {
super(type, sourceSpan);
}
isEquivalent(e: Expression): boolean {
return e instanceof ExternalExpr && this.value.name === e.value.name &&
this.value.moduleName === e.value.moduleName && this.value.runtime === e.value.runtime;
}
isConstant() { return false; }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitExternalExpr(this, context);
}
@ -424,16 +463,21 @@ export class ExternalReference {
export class ConditionalExpr extends Expression {
public trueCase: Expression;
constructor(
public condition: Expression, trueCase: Expression, public falseCase: Expression|null = null,
type?: Type|null, sourceSpan?: ParseSourceSpan|null) {
super(type || trueCase.type, sourceSpan);
this.trueCase = trueCase;
}
isEquivalent(e: Expression): boolean {
return e instanceof ConditionalExpr && this.condition.isEquivalent(e.condition) &&
this.trueCase.isEquivalent(e.trueCase) && nullSafeIsEquivalent(this.falseCase, e.falseCase);
}
isConstant() { return false; }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitConditionalExpr(this, context);
}
@ -444,9 +488,13 @@ export class NotExpr extends Expression {
constructor(public condition: Expression, sourceSpan?: ParseSourceSpan|null) {
super(BOOL_TYPE, sourceSpan);
}
isEquivalent(e: Expression): boolean {
return e instanceof NotExpr && this.condition.isEquivalent(e.condition);
}
isConstant() { return false; }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitNotExpr(this, context);
}
@ -456,9 +504,13 @@ export class AssertNotNull extends Expression {
constructor(public condition: Expression, sourceSpan?: ParseSourceSpan|null) {
super(condition.type, sourceSpan);
}
isEquivalent(e: Expression): boolean {
return e instanceof AssertNotNull && this.condition.isEquivalent(e.condition);
}
isConstant() { return false; }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitAssertNotNullExpr(this, context);
}
@ -468,9 +520,13 @@ export class CastExpr extends Expression {
constructor(public value: Expression, type?: Type|null, sourceSpan?: ParseSourceSpan|null) {
super(type, sourceSpan);
}
isEquivalent(e: Expression): boolean {
return e instanceof CastExpr && this.value.isEquivalent(e.value);
}
isConstant() { return false; }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitCastExpr(this, context);
}
@ -490,10 +546,14 @@ export class FunctionExpr extends Expression {
sourceSpan?: ParseSourceSpan|null, public name?: string|null) {
super(type, sourceSpan);
}
isEquivalent(e: Expression): boolean {
return e instanceof FunctionExpr && areAllEquivalent(this.params, e.params) &&
areAllEquivalent(this.statements, e.statements);
}
isConstant() { return false; }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitFunctionExpr(this, context);
}
@ -513,10 +573,14 @@ export class BinaryOperatorExpr extends Expression {
super(type || lhs.type, sourceSpan);
this.lhs = lhs;
}
isEquivalent(e: Expression): boolean {
return e instanceof BinaryOperatorExpr && this.operator === e.operator &&
this.lhs.isEquivalent(e.lhs) && this.rhs.isEquivalent(e.rhs);
}
isConstant() { return false; }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitBinaryOperatorExpr(this, context);
}
@ -529,13 +593,18 @@ export class ReadPropExpr extends Expression {
sourceSpan?: ParseSourceSpan|null) {
super(type, sourceSpan);
}
isEquivalent(e: Expression): boolean {
return e instanceof ReadPropExpr && this.receiver.isEquivalent(e.receiver) &&
this.name === e.name;
}
isConstant() { return false; }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitReadPropExpr(this, context);
}
set(value: Expression): WritePropExpr {
return new WritePropExpr(this.receiver, this.name, value, null, this.sourceSpan);
}
@ -548,13 +617,18 @@ export class ReadKeyExpr extends Expression {
sourceSpan?: ParseSourceSpan|null) {
super(type, sourceSpan);
}
isEquivalent(e: Expression): boolean {
return e instanceof ReadKeyExpr && this.receiver.isEquivalent(e.receiver) &&
this.index.isEquivalent(e.index);
}
isConstant() { return false; }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitReadKeyExpr(this, context);
}
set(value: Expression): WriteKeyExpr {
return new WriteKeyExpr(this.receiver, this.index, value, null, this.sourceSpan);
}
@ -567,6 +641,9 @@ export class LiteralArrayExpr extends Expression {
super(type, sourceSpan);
this.entries = entries;
}
isConstant() { return this.entries.every(e => e.isConstant()); }
isEquivalent(e: Expression): boolean {
return e instanceof LiteralArrayExpr && areAllEquivalent(this.entries, e.entries);
}
@ -591,9 +668,13 @@ export class LiteralMapExpr extends Expression {
this.valueType = type.valueType;
}
}
isEquivalent(e: Expression): boolean {
return e instanceof LiteralMapExpr && areAllEquivalent(this.entries, e.entries);
}
isConstant() { return this.entries.every(e => e.value.isConstant()); }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitLiteralMapExpr(this, context);
}
@ -603,9 +684,13 @@ export class CommaExpr extends Expression {
constructor(public parts: Expression[], sourceSpan?: ParseSourceSpan|null) {
super(parts[parts.length - 1].type, sourceSpan);
}
isEquivalent(e: Expression): boolean {
return e instanceof CommaExpr && areAllEquivalent(this.parts, e.parts);
}
isConstant() { return false; }
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitCommaExpr(this, context);
}

View File

@ -64,6 +64,17 @@ export class Identifiers {
static interpolation8: o.ExternalReference = {name: 'ɵi8', moduleName: CORE};
static interpolationV: o.ExternalReference = {name: 'ɵiV', moduleName: CORE};
static pureFunction0: o.ExternalReference = {name: 'ɵf0', moduleName: CORE};
static pureFunction1: o.ExternalReference = {name: 'ɵf1', moduleName: CORE};
static pureFunction2: o.ExternalReference = {name: 'ɵf2', moduleName: CORE};
static pureFunction3: o.ExternalReference = {name: 'ɵf3', moduleName: CORE};
static pureFunction4: o.ExternalReference = {name: 'ɵf4', moduleName: CORE};
static pureFunction5: o.ExternalReference = {name: 'ɵf5', moduleName: CORE};
static pureFunction6: o.ExternalReference = {name: 'ɵf6', moduleName: CORE};
static pureFunction7: o.ExternalReference = {name: 'ɵf7', moduleName: CORE};
static pureFunction8: o.ExternalReference = {name: 'ɵf8', moduleName: CORE};
static pureFunctionV: o.ExternalReference = {name: 'ɵfV', moduleName: CORE};
static pipeBind1: o.ExternalReference = {name: 'ɵpb1', moduleName: CORE};
static pipeBind2: o.ExternalReference = {name: 'ɵpb2', moduleName: CORE};
static pipeBind3: o.ExternalReference = {name: 'ɵpb3', moduleName: CORE};

View File

@ -8,9 +8,9 @@
import {CompileDirectiveMetadata, CompilePipeSummary, CompileTokenMetadata, CompileTypeMetadata, flatten, identifierName, rendererTypeName, tokenReference, viewClassName} from '../compile_metadata';
import {CompileReflector} from '../compile_reflector';
import {BindingForm, BuiltinConverter, ConvertPropertyBindingResult, EventHandlerVars, LocalResolver, convertActionBinding, convertPropertyBinding, convertPropertyBindingBuiltins} from '../compiler_util/expression_converter';
import {BindingForm, BuiltinConverter, BuiltinFunctionCall, ConvertPropertyBindingResult, EventHandlerVars, LocalResolver, convertActionBinding, convertPropertyBinding, convertPropertyBindingBuiltins} from '../compiler_util/expression_converter';
import {ConstantPool, DefinitionKind} from '../constant_pool';
import {AST, AstMemoryEfficientTransformer, AstTransformer, BindingPipe, FunctionCall, ImplicitReceiver, LiteralPrimitive, MethodCall, ParseSpan, PropertyRead} from '../expression_parser/ast';
import {AST, AstMemoryEfficientTransformer, AstTransformer, BindingPipe, FunctionCall, ImplicitReceiver, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, ParseSpan, PropertyRead} from '../expression_parser/ast';
import {Identifiers} from '../identifiers';
import {LifecycleHooks} from '../lifecycle_reflector';
import * as o from '../output/output_ast';
@ -22,6 +22,7 @@ import {OutputContext, error} from '../util';
import {Identifiers as R3} from './r3_identifiers';
/** Name of the context parameter passed into a template function */
const CONTEXT_NAME = 'ctx';
@ -217,6 +218,23 @@ function pipeBinding(args: o.Expression[]): o.ExternalReference {
}
}
const pureFunctionIdentifiers = [
R3.pureFunction0, R3.pureFunction1, R3.pureFunction2, R3.pureFunction3, R3.pureFunction4,
R3.pureFunction5, R3.pureFunction6, R3.pureFunction7, R3.pureFunction8
];
function getLiteralFactory(
outputContext: OutputContext, literal: o.LiteralArrayExpr | o.LiteralMapExpr): o.Expression {
const {literalFactory, literalFactoryArguments} =
outputContext.constantPool.getLiteralFactory(literal);
literalFactoryArguments.length > 0 || error(`Expected arguments to a literal factory function`);
let pureFunctionIdent =
pureFunctionIdentifiers[literalFactoryArguments.length] || R3.pureFunctionV;
// Literal factories are pure functions that only need to be re-invoked when the parameters
// change.
return o.importExpr(pureFunctionIdent).callFn([literalFactory, ...literalFactoryArguments]);
}
class BindingScope {
private map = new Map<string, o.Expression>();
private referenceNameIndex = 0;
@ -269,7 +287,7 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
private _postfix: o.Statement[] = [];
private _contentProjections: Map<NgContentAst, NgContentInfo>;
private _projectionDefinitionIndex = 0;
private _pipeConverter: PipeConverter;
private _valueConverter: ValueConverter;
private unsupported = unsupported;
private invalid = invalid;
@ -279,8 +297,8 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
private bindingScope: BindingScope, private level = 0, private ngContentSelectors: string[],
private contextName: string|null, private templateName: string|null,
private pipes: Map<string, CompilePipeSummary>) {
this._pipeConverter =
new PipeConverter(() => this.allocateDataSlot(), (name, localName, slot, value) => {
this._valueConverter = new ValueConverter(
outputCtx, () => this.allocateDataSlot(), (name, localName, slot, value) => {
bindingScope.set(localName, value);
const pipe = pipes.get(name) !;
pipe || error(`Could not find pipe ${name}`);
@ -634,7 +652,7 @@ class TemplateDefinitionBuilder implements TemplateAstVisitor, LocalResolver {
}
private convertPropertyBinding(implicit: o.Expression, value: AST): o.Expression {
const pipesConvertedValue = value.visit(this._pipeConverter);
const pipesConvertedValue = value.visit(this._valueConverter);
const convertedPropertyBinding = convertPropertyBinding(
this, implicit, pipesConvertedValue, this.bindingContext(), BindingForm.TrySimple,
interpolate);
@ -688,10 +706,10 @@ export function createFactory(
o.INFERRED_TYPE, null, type.reference.name ? `${type.reference.name}_Factory` : null);
}
class PipeConverter extends AstMemoryEfficientTransformer {
class ValueConverter extends AstMemoryEfficientTransformer {
private pipeSlots = new Map<string, number>();
constructor(
private allocateSlot: () => number,
private outputCtx: OutputContext, private allocateSlot: () => number,
private definePipe:
(name: string, localName: string, slot: number, value: o.Expression) => void) {
super();
@ -715,6 +733,31 @@ class PipeConverter extends AstMemoryEfficientTransformer {
return new FunctionCall(
ast.span, target, [new LiteralPrimitive(ast.span, slot), value, ...args]);
}
visitLiteralArray(ast: LiteralArray, context: any): AST {
return new BuiltinFunctionCall(ast.span, this.visitAll(ast.expressions), values => {
// If the literal has calculated (non-literal) elements transform it into
// calls to literal factories that compose the literal and will cache intermediate
// values. Otherwise, just return an literal array that contains the values.
const literal = o.literalArr(values);
return values.every(a => a.isConstant()) ?
this.outputCtx.constantPool.getConstLiteral(literal, true) :
getLiteralFactory(this.outputCtx, literal);
});
}
visitLiteralMap(ast: LiteralMap, context: any): AST {
return new BuiltinFunctionCall(ast.span, this.visitAll(ast.values), values => {
// If the literal has calculated (non-literal) elements transform it into
// calls to literal factories that compose the literal and will cache intermediate
// values. Otherwise, just return an literal array that contains the values.
const literal = o.literalMap(values.map(
(value, index) => ({key: ast.keys[index].key, value, quoted: ast.keys[index].quoted})));
return values.every(a => a.isConstant()) ?
this.outputCtx.constantPool.getConstLiteral(literal, true) :
getLiteralFactory(this.outputCtx, literal);
});
}
}
function invalid<T>(arg: o.Expression | o.Statement | TemplateAst): never {

View File

@ -209,6 +209,273 @@ describe('compiler compliance', () => {
expectEmit(source, MyComponentDefinition, 'Incorrect MyComponent.ngComponentDef');
});
describe('value composition', () => {
it('should support array literals', () => {
const files = {
app: {
'spec.ts': `
import {Component, Input, NgModule} from '@angular/core';
@Component({
selector: 'my-comp',
template: \`
<p>{{ names[0] }}</p>
<p>{{ names[1] }}</p>
\`
})
export class MyComp {
@Input() names: string[];
}
@Component({
selector: 'my-app',
template: \`
<my-comp [names]="['Nancy', customName]"></my-comp>
\`
})
export class MyApp {
customName = 'Bess';
}
@NgModule({declarations: [MyComp, MyApp]})
export class MyModule { }
`
}
};
const MyAppDeclaration = `
const $e0_ff$ = ($v$: any) => { return ['Nancy', $v$]; };
static ngComponentDef = $r3$.ɵdefineComponent({
type: MyApp,
tag: 'my-app',
factory: function MyApp_Factory() { return new MyApp(); },
template: function MyApp_Template(ctx: $MyApp$, cm: $boolean$) {
if (cm) {
$r3$.ɵE(0, MyComp);
$r3$.ɵe();
}
$r3$.ɵp(0, 'names', $r3$.ɵb($r3$.ɵf1($e0_ff$, ctx.customName)));
MyComp.ngComponentDef.h(1, 0);
$r3$.ɵr(1, 0);
}
});
`;
const result = compile(files, angularFiles);
const source = result.source;
expectEmit(source, MyAppDeclaration, 'Invalid array emit');
});
it('should support 9+ bindings in array literals', () => {
const files = {
app: {
'spec.ts': `
import {Component, Input, NgModule} from '@angular/core';
@Component({
selector: 'my-comp',
template: \`
{{ names[0] }}
{{ names[1] }}
{{ names[3] }}
{{ names[4] }}
{{ names[5] }}
{{ names[6] }}
{{ names[7] }}
{{ names[8] }}
{{ names[9] }}
{{ names[10] }}
{{ names[11] }}
\`
})
export class MyComp {
@Input() names: string[];
}
@Component({
selector: 'my-app',
template: \`
<my-comp [names]="['start-', n0, n1, n2, n3, n4, '-middle-', n5, n6, n7, n8, '-end']">
</my-comp>
\`
})
export class MyApp {
n0 = 'a';
n1 = 'b';
n2 = 'c';
n3 = 'd';
n4 = 'e';
n5 = 'f';
n6 = 'g';
n7 = 'h';
n8 = 'i';
}
@NgModule({declarations: [MyComp, MyApp]})
export class MyModule {}
`
}
};
const MyAppDefinition = `
const $e0_ff$ = ($v0$: $any$, $v1$: $any$, $v2$: $any$, $v3$: $any$, $v4$: $any$, $v5$: $any$, $v6$: $any$, $v7$: $any$, $v8$: $any$) => {
return ['start-', $v0$, $v1$, $v2$, $v3$, $v4$, '-middle-', $v5$, $v6$, $v7$, $v8$, '-end'];
}
static ngComponentDef = $r3$.ɵdefineComponent({
type: MyApp,
tag: 'my-app',
factory: function MyApp_Factory() { return new MyApp(); },
template: function MyApp_Template(ctx: $MyApp$, cm: $boolean$) {
if (cm) {
$r3$.ɵE(0, MyComp);
$r3$.ɵe();
}
$r3$.ɵp(
0, 'names',
$r3$.ɵb($r3$.ɵfV($e0_ff$, ctx.n0, ctx.n1, ctx.n2, ctx.n3, ctx.n4, ctx.n5, ctx.n6, ctx.n7, ctx.n8)));
MyComp.ngComponentDef.h(1, 0);
$r3$.ɵr(1, 0);
}
});
`;
const result = compile(files, angularFiles);
const source = result.source;
expectEmit(source, MyAppDefinition, 'Invalid array binding');
});
it('should support object literals', () => {
const files = {
app: {
'spec.ts': `
import {Component, Input, NgModule} from '@angular/core';
@Component({
selector: 'object-comp',
template: \`
<p> {{ config['duration'] }} </p>
<p> {{ config.animation }} </p>
\`
})
export class ObjectComp {
@Input() config: {[key: string]: any};
}
@Component({
selector: 'my-app',
template: \`
<object-comp [config]="{'duration': 500, animation: name}"></object-comp>
\`
})
export class MyApp {
name = 'slide';
}
@NgModule({declarations: [ObjectComp, MyApp]})
export class MyModule {}
`
}
};
const MyAppDefinition = `
const $e0_ff$ = ($v$: any) => { return {'duration': 500, animation: $v$}; };
static ngComponentDef = $r3$.ɵdefineComponent({
type: MyApp,
tag: 'my-app',
factory: function MyApp_Factory() { return new MyApp(); },
template: function MyApp_Template(ctx: $MyApp$, cm: $boolean$) {
if (cm) {
$r3$.ɵE(0, ObjectComp);
$r3$.ɵe();
}
$r3$.ɵp(0, 'config', $r3$.ɵb($r3$.ɵf1($e0_ff$, ctx.name)));
ObjectComp.ngComponentDef.h(1, 0);
$r3$.ɵr(1, 0);
}
});
`;
const result = compile(files, angularFiles);
const source = result.source;
expectEmit(source, MyAppDefinition, 'Invalid object literal binding');
});
it('should support expressions nested deeply in object/array literals', () => {
const files = {
app: {
'spec.ts': `
import {Component, Input, NgModule} from '@angular/core';
@Component({
selector: 'nested-comp',
template: \`
<p> {{ config.animation }} </p>
<p> {{config.actions[0].opacity }} </p>
<p> {{config.actions[1].duration }} </p>
\`
})
export class NestedComp {
@Input() config: {[key: string]: any};
}
@Component({
selector: 'my-app',
template: \`
<nested-comp [config]="{animation: name, actions: [{ opacity: 0, duration: 0}, {opacity: 1, duration: duration }]}">
</nested-comp>
\`
})
export class MyApp {
name = 'slide';
duration = 100;
}
@NgModule({declarations: [NestedComp, MyApp]})
export class MyModule {}
`
}
};
const MyAppDefinition = `
const $c0$ = {opacity: 0, duration: 0};
const $e0_ff$ = ($v$: any) => { return {opacity: 1, duration: $v$}; };
const $e0_ff_1$ = ($v$: any) => { return [$c0$, $v$]; };
const $e0_ff_2$ = ($v1$: any, $v2$: any) => { return {animation: $v1$, actions: $v2$}; };
static ngComponentDef = $r3$.ɵdefineComponent({
type: MyApp,
tag: 'my-app',
factory: function MyApp_Factory() { return new MyApp(); },
template: function MyApp_Template(ctx: $MyApp$, cm: $boolean$) {
if (cm) {
$r3$.ɵE(0, NestedComp);
$r3$.ɵe();
}
$r3$.ɵp(
0, 'config',
$r3$.ɵb($r3$.ɵf2(
$e0_ff_2$, ctx.name, $r3$.ɵf1($e0_ff_1$, $r3$.ɵf1($e0_ff$, ctx.duration)))));
NestedComp.ngComponentDef.h(1, 0);
$r3$.ɵr(1, 0);
}
});
`;
const result = compile(files, angularFiles);
const source = result.source;
expectEmit(source, MyAppDefinition, 'Invalid array/object literal binding');
});
});
it('should support content projection', () => {
const files = {
app: {