fix(compiler): evaluate safe navigation expressions in correct binding order (#37911)
When using the safe navigation operator in a binding expression, a temporary variable may be used for storing the result of a side-effectful call. For example, the following template uses a pipe and a safe property access: ```html <app-person-view [enabled]="enabled" [firstName]="(person$ | async)?.name"></app-person-view> ``` The result of the pipe evaluation is stored in a temporary to be able to check whether it is present. The temporary variable needs to be declared in a separate statement and this would also cause the full expression itself to be pulled out into a separate statement. This would compile into the following pseudo-code instructions: ```js var temp = null; var firstName = (temp = pipe('async', ctx.person$)) == null ? null : temp.name; property('enabled', ctx.enabled)('firstName', firstName); ``` Notice that the pipe evaluation happens before evaluating the `enabled` binding, such that the runtime's internal binding index would correspond with `enabled`, not `firstName`. This introduces a problem when the pipe uses `WrappedValue` to force a change to be detected, as the runtime would then mark the binding slot corresponding with `enabled` as dirty, instead of `firstName`. This results in the `enabled` binding to be updated, triggering setters and affecting how `OnChanges` is called. In the pseudo-code above, the intermediate `firstName` variable is not strictly necessary---it only improved readability a bit---and emitting it inline with the binding itself avoids the out-of-order execution of the pipe: ```js var temp = null; property('enabled', ctx.enabled) ('firstName', (temp = pipe('async', ctx.person$)) == null ? null : temp.name); ``` This commit introduces a new `BindingForm` that results in the above code to be generated and adds compiler and acceptance tests to verify the proper behavior. Fixes #37194 PR Close #37911
This commit is contained in:
@ -154,6 +154,11 @@ export enum BindingForm {
|
||||
// Try to generate a simple binding (no temporaries or statements)
|
||||
// otherwise generate a general binding
|
||||
TrySimple,
|
||||
|
||||
// Inlines assignment of temporaries into the generated expression. The result may still
|
||||
// have statements attached for declarations of temporary variables.
|
||||
// This is the only relevant form for Ivy, the other forms are only used in ViewEngine.
|
||||
Expression,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -168,7 +173,6 @@ export function convertPropertyBinding(
|
||||
if (!localResolver) {
|
||||
localResolver = new DefaultLocalResolver();
|
||||
}
|
||||
const currValExpr = createCurrValueExpr(bindingId);
|
||||
const visitor =
|
||||
new _AstToIrVisitor(localResolver, implicitReceiver, bindingId, interpolationFunction);
|
||||
const outputExpr: o.Expression = expressionWithoutBuiltins.visit(visitor, _Mode.Expression);
|
||||
@ -180,8 +184,11 @@ export function convertPropertyBinding(
|
||||
|
||||
if (visitor.temporaryCount === 0 && form == BindingForm.TrySimple) {
|
||||
return new ConvertPropertyBindingResult([], outputExpr);
|
||||
} else if (form === BindingForm.Expression) {
|
||||
return new ConvertPropertyBindingResult(stmts, outputExpr);
|
||||
}
|
||||
|
||||
const currValExpr = createCurrValueExpr(bindingId);
|
||||
stmts.push(currValExpr.set(outputExpr).toDeclStmt(o.DYNAMIC_TYPE, [o.StmtModifier.Final]));
|
||||
return new ConvertPropertyBindingResult(stmts, currValExpr);
|
||||
}
|
||||
|
@ -714,7 +714,7 @@ function createHostBindingsFunction(
|
||||
|
||||
function bindingFn(implicit: any, value: AST) {
|
||||
return convertPropertyBinding(
|
||||
null, implicit, value, 'b', BindingForm.TrySimple, () => error('Unexpected interpolation'));
|
||||
null, implicit, value, 'b', BindingForm.Expression, () => error('Unexpected interpolation'));
|
||||
}
|
||||
|
||||
function convertStylingCall(
|
||||
|
@ -1213,7 +1213,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
|
||||
private convertPropertyBinding(value: AST): o.Expression {
|
||||
const convertedPropertyBinding = convertPropertyBinding(
|
||||
this, this.getImplicitReceiverExpr(), value, this.bindingContext(), BindingForm.TrySimple,
|
||||
this, this.getImplicitReceiverExpr(), value, this.bindingContext(), BindingForm.Expression,
|
||||
() => error('Unexpected interpolation'));
|
||||
const valExpr = convertedPropertyBinding.currValExpr;
|
||||
this._tempVariables.push(...convertedPropertyBinding.stmts);
|
||||
|
Reference in New Issue
Block a user