fix(compiler): Generate temporary variables for guarded expressions (#10657)
Fixes: #10639
This commit is contained in:
parent
5d59c6e80f
commit
2d520ae7e7
@ -59,7 +59,8 @@ export class CompileEventListener {
|
|||||||
this._method.resetDebugInfo(this.compileElement.nodeIndex, hostEvent);
|
this._method.resetDebugInfo(this.compileElement.nodeIndex, hostEvent);
|
||||||
var context = isPresent(directiveInstance) ? directiveInstance :
|
var context = isPresent(directiveInstance) ? directiveInstance :
|
||||||
this.compileElement.view.componentContext;
|
this.compileElement.view.componentContext;
|
||||||
var actionStmts = convertCdStatementToIr(this.compileElement.view, context, hostEvent.handler);
|
var actionStmts = convertCdStatementToIr(
|
||||||
|
this.compileElement.view, context, hostEvent.handler, this.compileElement.nodeIndex);
|
||||||
var lastIndex = actionStmts.length - 1;
|
var lastIndex = actionStmts.length - 1;
|
||||||
if (lastIndex >= 0) {
|
if (lastIndex >= 0) {
|
||||||
var lastStatement = actionStmts[lastIndex];
|
var lastStatement = actionStmts[lastIndex];
|
||||||
|
@ -21,25 +21,45 @@ export interface NameResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ExpressionWithWrappedValueInfo {
|
export class ExpressionWithWrappedValueInfo {
|
||||||
constructor(public expression: o.Expression, public needsValueUnwrapper: boolean) {}
|
constructor(
|
||||||
|
public expression: o.Expression, public needsValueUnwrapper: boolean,
|
||||||
|
public temporaryCount: number) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertCdExpressionToIr(
|
export function convertCdExpressionToIr(
|
||||||
nameResolver: NameResolver, implicitReceiver: o.Expression, expression: cdAst.AST,
|
nameResolver: NameResolver, implicitReceiver: o.Expression, expression: cdAst.AST,
|
||||||
valueUnwrapper: o.ReadVarExpr): ExpressionWithWrappedValueInfo {
|
valueUnwrapper: o.ReadVarExpr, bindingIndex: number): ExpressionWithWrappedValueInfo {
|
||||||
const visitor = new _AstToIrVisitor(nameResolver, implicitReceiver, valueUnwrapper);
|
const visitor = new _AstToIrVisitor(nameResolver, implicitReceiver, valueUnwrapper, bindingIndex);
|
||||||
const irAst: o.Expression = expression.visit(visitor, _Mode.Expression);
|
const irAst: o.Expression = expression.visit(visitor, _Mode.Expression);
|
||||||
return new ExpressionWithWrappedValueInfo(irAst, visitor.needsValueUnwrapper);
|
return new ExpressionWithWrappedValueInfo(
|
||||||
|
irAst, visitor.needsValueUnwrapper, visitor.temporaryCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertCdStatementToIr(
|
export function convertCdStatementToIr(
|
||||||
nameResolver: NameResolver, implicitReceiver: o.Expression, stmt: cdAst.AST): o.Statement[] {
|
nameResolver: NameResolver, implicitReceiver: o.Expression, stmt: cdAst.AST,
|
||||||
const visitor = new _AstToIrVisitor(nameResolver, implicitReceiver, null);
|
bindingIndex: number): o.Statement[] {
|
||||||
|
const visitor = new _AstToIrVisitor(nameResolver, implicitReceiver, null, bindingIndex);
|
||||||
let statements: o.Statement[] = [];
|
let statements: o.Statement[] = [];
|
||||||
flattenStatements(stmt.visit(visitor, _Mode.Statement), statements);
|
flattenStatements(stmt.visit(visitor, _Mode.Statement), statements);
|
||||||
|
prependTemporaryDecls(visitor.temporaryCount, bindingIndex, statements);
|
||||||
return statements;
|
return statements;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function temporaryName(bindingIndex: number, temporaryNumber: number): string {
|
||||||
|
return `tmp_${bindingIndex}_${temporaryNumber}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function temporaryDeclaration(bindingIndex: number, temporaryNumber: number): o.Statement {
|
||||||
|
return new o.DeclareVarStmt(temporaryName(bindingIndex, temporaryNumber), o.NULL_EXPR);
|
||||||
|
}
|
||||||
|
|
||||||
|
function prependTemporaryDecls(
|
||||||
|
temporaryCount: number, bindingIndex: number, statements: o.Statement[]) {
|
||||||
|
for (let i = temporaryCount - 1; i >= 0; i--) {
|
||||||
|
statements.unshift(temporaryDeclaration(bindingIndex, i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum _Mode {
|
enum _Mode {
|
||||||
Statement,
|
Statement,
|
||||||
Expression
|
Expression
|
||||||
@ -66,13 +86,15 @@ function convertToStatementIfNeeded(mode: _Mode, expr: o.Expression): o.Expressi
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AstToIrVisitor implements cdAst.AstVisitor {
|
class _AstToIrVisitor implements cdAst.AstVisitor {
|
||||||
private _map = new Map<cdAst.AST, cdAst.AST>();
|
private _nodeMap = new Map<cdAst.AST, cdAst.AST>();
|
||||||
|
private _resultMap = new Map<cdAst.AST, o.Expression>();
|
||||||
|
private _currentTemporary: number = 0;
|
||||||
public needsValueUnwrapper: boolean = false;
|
public needsValueUnwrapper: boolean = false;
|
||||||
|
public temporaryCount: number = 0;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _nameResolver: NameResolver, private _implicitReceiver: o.Expression,
|
private _nameResolver: NameResolver, private _implicitReceiver: o.Expression,
|
||||||
private _valueUnwrapper: o.ReadVarExpr) {}
|
private _valueUnwrapper: o.ReadVarExpr, private bindingIndex: number) {}
|
||||||
|
|
||||||
visitBinary(ast: cdAst.Binary, mode: _Mode): any {
|
visitBinary(ast: cdAst.Binary, mode: _Mode): any {
|
||||||
var op: o.BinaryOperator;
|
var op: o.BinaryOperator;
|
||||||
@ -273,7 +295,9 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private visit(ast: cdAst.AST, mode: _Mode): any {
|
private visit(ast: cdAst.AST, mode: _Mode): any {
|
||||||
return (this._map.get(ast) || ast).visit(this, mode);
|
const result = this._resultMap.get(ast);
|
||||||
|
if (result) return result;
|
||||||
|
return (this._nodeMap.get(ast) || ast).visit(this, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private convertSafeAccess(
|
private convertSafeAccess(
|
||||||
@ -317,17 +341,30 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
|
|||||||
// Notice that the first guard condition is the left hand of the left most safe access node
|
// Notice that the first guard condition is the left hand of the left most safe access node
|
||||||
// which comes in as leftMostSafe to this routine.
|
// which comes in as leftMostSafe to this routine.
|
||||||
|
|
||||||
const condition = this.visit(leftMostSafe.receiver, mode).isBlank();
|
let guardedExpression = this.visit(leftMostSafe.receiver, mode);
|
||||||
|
let temporary: o.ReadVarExpr;
|
||||||
|
if (this.needsTemporary(leftMostSafe.receiver)) {
|
||||||
|
// If the expression has method calls or pipes then we need to save the result into a
|
||||||
|
// temporary variable to avoid calling stateful or impure code more than once.
|
||||||
|
temporary = this.allocateTemporary();
|
||||||
|
|
||||||
|
// Preserve the result in the temporary variable
|
||||||
|
guardedExpression = temporary.set(guardedExpression);
|
||||||
|
|
||||||
|
// Ensure all further references to the guarded expression refer to the temporary instead.
|
||||||
|
this._resultMap.set(leftMostSafe.receiver, temporary);
|
||||||
|
}
|
||||||
|
const condition = guardedExpression.isBlank();
|
||||||
|
|
||||||
// Convert the ast to an unguarded access to the receiver's member. The map will substitute
|
// Convert the ast to an unguarded access to the receiver's member. The map will substitute
|
||||||
// leftMostNode with its unguarded version in the call to `this.visit()`.
|
// leftMostNode with its unguarded version in the call to `this.visit()`.
|
||||||
if (leftMostSafe instanceof cdAst.SafeMethodCall) {
|
if (leftMostSafe instanceof cdAst.SafeMethodCall) {
|
||||||
this._map.set(
|
this._nodeMap.set(
|
||||||
leftMostSafe,
|
leftMostSafe,
|
||||||
new cdAst.MethodCall(
|
new cdAst.MethodCall(
|
||||||
leftMostSafe.span, leftMostSafe.receiver, leftMostSafe.name, leftMostSafe.args));
|
leftMostSafe.span, leftMostSafe.receiver, leftMostSafe.name, leftMostSafe.args));
|
||||||
} else {
|
} else {
|
||||||
this._map.set(
|
this._nodeMap.set(
|
||||||
leftMostSafe,
|
leftMostSafe,
|
||||||
new cdAst.PropertyRead(leftMostSafe.span, leftMostSafe.receiver, leftMostSafe.name));
|
new cdAst.PropertyRead(leftMostSafe.span, leftMostSafe.receiver, leftMostSafe.name));
|
||||||
}
|
}
|
||||||
@ -337,7 +374,12 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
|
|||||||
|
|
||||||
// Remove the mapping. This is not strictly required as the converter only traverses each node
|
// Remove the mapping. This is not strictly required as the converter only traverses each node
|
||||||
// once but is safer if the conversion is changed to traverse the nodes more than once.
|
// once but is safer if the conversion is changed to traverse the nodes more than once.
|
||||||
this._map.delete(leftMostSafe);
|
this._nodeMap.delete(leftMostSafe);
|
||||||
|
|
||||||
|
// If we allcoated a temporary, release it.
|
||||||
|
if (temporary) {
|
||||||
|
this.releaseTemporary(temporary);
|
||||||
|
}
|
||||||
|
|
||||||
// Produce the conditional
|
// Produce the conditional
|
||||||
return condition.conditional(o.literal(null), access);
|
return condition.conditional(o.literal(null), access);
|
||||||
@ -351,8 +393,8 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
|
|||||||
// then to:
|
// then to:
|
||||||
// a == null ? null : a.b.c == null ? null : a.b.c.d.e
|
// a == null ? null : a.b.c == null ? null : a.b.c.d.e
|
||||||
private leftMostSafeNode(ast: cdAst.AST): cdAst.SafePropertyRead|cdAst.SafeMethodCall {
|
private leftMostSafeNode(ast: cdAst.AST): cdAst.SafePropertyRead|cdAst.SafeMethodCall {
|
||||||
let visit = (visitor: cdAst.AstVisitor, ast: cdAst.AST): any => {
|
const visit = (visitor: cdAst.AstVisitor, ast: cdAst.AST): any => {
|
||||||
return (this._map.get(ast) || ast).visit(visitor);
|
return (this._nodeMap.get(ast) || ast).visit(visitor);
|
||||||
};
|
};
|
||||||
return ast.visit({
|
return ast.visit({
|
||||||
visitBinary(ast: cdAst.Binary) { return null; },
|
visitBinary(ast: cdAst.Binary) { return null; },
|
||||||
@ -378,6 +420,55 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns true of the AST includes a method or a pipe indicating that, if the
|
||||||
|
// expression is used as the target of a safe property or method access then
|
||||||
|
// the expression should be stored into a temporary variable.
|
||||||
|
private needsTemporary(ast: cdAst.AST): boolean {
|
||||||
|
const visit = (visitor: cdAst.AstVisitor, ast: cdAst.AST): boolean => {
|
||||||
|
return ast && (this._nodeMap.get(ast) || ast).visit(visitor);
|
||||||
|
};
|
||||||
|
const visitSome = (visitor: cdAst.AstVisitor, ast: cdAst.AST[]): boolean => {
|
||||||
|
return ast.some(ast => visit(visitor, ast));
|
||||||
|
};
|
||||||
|
return ast.visit({
|
||||||
|
visitBinary(ast: cdAst.Binary):
|
||||||
|
boolean{return visit(this, ast.left) || visit(this, ast.right);},
|
||||||
|
visitChain(ast: cdAst.Chain) { return false; },
|
||||||
|
visitConditional(ast: cdAst.Conditional):
|
||||||
|
boolean{return visit(this, ast.condition) || visit(this, ast.trueExp) ||
|
||||||
|
visit(this, ast.falseExp);},
|
||||||
|
visitFunctionCall(ast: cdAst.FunctionCall) { return true; },
|
||||||
|
visitImplicitReceiver(ast: cdAst.ImplicitReceiver) { return false; },
|
||||||
|
visitInterpolation(ast: cdAst.Interpolation) { return visitSome(this, ast.expressions); },
|
||||||
|
visitKeyedRead(ast: cdAst.KeyedRead) { return false; },
|
||||||
|
visitKeyedWrite(ast: cdAst.KeyedWrite) { return false; },
|
||||||
|
visitLiteralArray(ast: cdAst.LiteralArray) { return true; },
|
||||||
|
visitLiteralMap(ast: cdAst.LiteralMap) { return true; },
|
||||||
|
visitLiteralPrimitive(ast: cdAst.LiteralPrimitive) { return false; },
|
||||||
|
visitMethodCall(ast: cdAst.MethodCall) { return true; },
|
||||||
|
visitPipe(ast: cdAst.BindingPipe) { return true; },
|
||||||
|
visitPrefixNot(ast: cdAst.PrefixNot) { return visit(this, ast.expression); },
|
||||||
|
visitPropertyRead(ast: cdAst.PropertyRead) { return false; },
|
||||||
|
visitPropertyWrite(ast: cdAst.PropertyWrite) { return false; },
|
||||||
|
visitQuote(ast: cdAst.Quote) { return false; },
|
||||||
|
visitSafeMethodCall(ast: cdAst.SafeMethodCall) { return true; },
|
||||||
|
visitSafePropertyRead(ast: cdAst.SafePropertyRead) { return false; }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private allocateTemporary(): o.ReadVarExpr {
|
||||||
|
const tempNumber = this._currentTemporary++;
|
||||||
|
this.temporaryCount = Math.max(this._currentTemporary, this.temporaryCount);
|
||||||
|
return new o.ReadVarExpr(temporaryName(this.bindingIndex, tempNumber));
|
||||||
|
}
|
||||||
|
|
||||||
|
private releaseTemporary(temporary: o.ReadVarExpr) {
|
||||||
|
this._currentTemporary--;
|
||||||
|
if (temporary.name != temporaryName(this.bindingIndex, this._currentTemporary)) {
|
||||||
|
throw new BaseException(`Temporary ${temporary.name} released out of order`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function flattenStatements(arg: any, output: o.Statement[]) {
|
function flattenStatements(arg: any, output: o.Statement[]) {
|
||||||
|
@ -21,7 +21,7 @@ import {CompileElement, CompileNode} from './compile_element';
|
|||||||
import {CompileMethod} from './compile_method';
|
import {CompileMethod} from './compile_method';
|
||||||
import {CompileView} from './compile_view';
|
import {CompileView} from './compile_view';
|
||||||
import {DetectChangesVars, ViewProperties} from './constants';
|
import {DetectChangesVars, ViewProperties} from './constants';
|
||||||
import {convertCdExpressionToIr} from './expression_converter';
|
import {convertCdExpressionToIr, temporaryDeclaration} from './expression_converter';
|
||||||
|
|
||||||
function createBindFieldExpr(exprIndex: number): o.ReadPropExpr {
|
function createBindFieldExpr(exprIndex: number): o.ReadPropExpr {
|
||||||
return o.THIS_EXPR.prop(`_expr_${exprIndex}`);
|
return o.THIS_EXPR.prop(`_expr_${exprIndex}`);
|
||||||
@ -36,14 +36,20 @@ const _animationViewCheckedFlagMap = new Map<CompileView, boolean>();
|
|||||||
function bind(
|
function bind(
|
||||||
view: CompileView, currValExpr: o.ReadVarExpr, fieldExpr: o.ReadPropExpr,
|
view: CompileView, currValExpr: o.ReadVarExpr, fieldExpr: o.ReadPropExpr,
|
||||||
parsedExpression: cdAst.AST, context: o.Expression, actions: o.Statement[],
|
parsedExpression: cdAst.AST, context: o.Expression, actions: o.Statement[],
|
||||||
method: CompileMethod) {
|
method: CompileMethod, bindingIndex: number) {
|
||||||
var checkExpression =
|
var checkExpression = convertCdExpressionToIr(
|
||||||
convertCdExpressionToIr(view, context, parsedExpression, DetectChangesVars.valUnwrapper);
|
view, context, parsedExpression, DetectChangesVars.valUnwrapper, bindingIndex);
|
||||||
if (isBlank(checkExpression.expression)) {
|
if (isBlank(checkExpression.expression)) {
|
||||||
// e.g. an empty expression was given
|
// e.g. an empty expression was given
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (checkExpression.temporaryCount) {
|
||||||
|
for (let i = 0; i < checkExpression.temporaryCount; i++) {
|
||||||
|
method.addStmt(temporaryDeclaration(bindingIndex, i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// private is fine here as no child view will reference the cached value...
|
// private is fine here as no child view will reference the cached value...
|
||||||
view.fields.push(new o.ClassField(fieldExpr.name, null, [o.StmtModifier.Private]));
|
view.fields.push(new o.ClassField(fieldExpr.name, null, [o.StmtModifier.Private]));
|
||||||
view.createMethod.addStmt(
|
view.createMethod.addStmt(
|
||||||
@ -80,7 +86,7 @@ export function bindRenderText(
|
|||||||
[o.THIS_EXPR.prop('renderer')
|
[o.THIS_EXPR.prop('renderer')
|
||||||
.callMethod('setText', [compileNode.renderNode, currValExpr])
|
.callMethod('setText', [compileNode.renderNode, currValExpr])
|
||||||
.toStmt()],
|
.toStmt()],
|
||||||
view.detectChangesRenderPropertiesMethod);
|
view.detectChangesRenderPropertiesMethod, bindingIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindAndWriteToRenderer(
|
function bindAndWriteToRenderer(
|
||||||
@ -183,7 +189,7 @@ function bindAndWriteToRenderer(
|
|||||||
|
|
||||||
bind(
|
bind(
|
||||||
view, currValExpr, fieldExpr, boundProp.value, context, updateStmts,
|
view, currValExpr, fieldExpr, boundProp.value, context, updateStmts,
|
||||||
view.detectChangesRenderPropertiesMethod);
|
view.detectChangesRenderPropertiesMethod, view.bindings.length);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,7 +280,7 @@ export function bindDirectiveInputs(
|
|||||||
}
|
}
|
||||||
bind(
|
bind(
|
||||||
view, currValExpr, fieldExpr, input.value, view.componentContext, statements,
|
view, currValExpr, fieldExpr, input.value, view.componentContext, statements,
|
||||||
detectChangesInInputsMethod);
|
detectChangesInInputsMethod, bindingIndex);
|
||||||
});
|
});
|
||||||
if (isOnPushComp) {
|
if (isOnPushComp) {
|
||||||
detectChangesInInputsMethod.addStmt(new o.IfStmt(DetectChangesVars.changed, [
|
detectChangesInInputsMethod.addStmt(new o.IfStmt(DetectChangesVars.changed, [
|
||||||
|
@ -72,6 +72,39 @@ function declareTests({useJit}: {useJit: boolean}) {
|
|||||||
async.done();
|
async.done();
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should only evaluate stateful pipes once - #10639',
|
||||||
|
inject(
|
||||||
|
[TestComponentBuilder, AsyncTestCompleter],
|
||||||
|
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
|
||||||
|
tcb.overrideView(
|
||||||
|
MyComp1,
|
||||||
|
new ViewMetadata(
|
||||||
|
{template: '{{(null|countingPipe)?.value}}', pipes: [CountingPipe]}))
|
||||||
|
.createAsync(MyComp1)
|
||||||
|
.then(fixture => {
|
||||||
|
CountingPipe.reset();
|
||||||
|
fixture.detectChanges(/* checkNoChanges */ false);
|
||||||
|
expect(fixture.nativeElement).toHaveText('counting pipe value');
|
||||||
|
expect(CountingPipe.calls).toBe(1);
|
||||||
|
async.done();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should only evaluate methods once - #10639',
|
||||||
|
inject(
|
||||||
|
[TestComponentBuilder, AsyncTestCompleter],
|
||||||
|
(tcb: TestComponentBuilder, async: AsyncTestCompleter) => {
|
||||||
|
tcb.overrideView(MyCountingComp, new ViewMetadata({template: '{{method()?.value}}'}))
|
||||||
|
.createAsync(MyCountingComp)
|
||||||
|
.then(fixture => {
|
||||||
|
MyCountingComp.reset();
|
||||||
|
fixture.detectChanges(/* checkNoChanges */ false);
|
||||||
|
expect(fixture.nativeElement).toHaveText('counting method value');
|
||||||
|
expect(MyCountingComp.calls).toBe(1);
|
||||||
|
async.done();
|
||||||
|
});
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('providers', () => {
|
describe('providers', () => {
|
||||||
@ -204,6 +237,27 @@ class CustomPipe implements PipeTransform {
|
|||||||
class CmpWithNgContent {
|
class CmpWithNgContent {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Component({selector: 'counting-cmp', template: ''})
|
||||||
|
class MyCountingComp {
|
||||||
|
method(): {value: string}|undefined {
|
||||||
|
MyCountingComp.calls++;
|
||||||
|
return {value: 'counting method value'};
|
||||||
|
}
|
||||||
|
|
||||||
|
static reset() { MyCountingComp.calls = 0; }
|
||||||
|
static calls = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Pipe({name: 'countingPipe'})
|
||||||
|
class CountingPipe implements PipeTransform {
|
||||||
|
transform(value: any): any {
|
||||||
|
CountingPipe.calls++;
|
||||||
|
return {value: 'counting pipe value'};
|
||||||
|
}
|
||||||
|
static reset() { CountingPipe.calls = 0; }
|
||||||
|
static calls = 0;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'left',
|
selector: 'left',
|
||||||
template: `L<right *ngIf="false"></right>`,
|
template: `L<right *ngIf="false"></right>`,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user