fix(ivy): properly parenthesize ternary expressions when emitted (#34221)

Previously, ternary expressions were emitted as:

condExpr ? trueCase : falseCase

However, this causes problems when ternary operations are nested. In
particular, a template expression of the form:

a?.b ? c : d

would have compiled to:

a == null ? null : a.b ? c : d

The ternary operator is right-associative, so that expression is interpreted
as:

a == null ? null : (a.b ? c : d)

when in reality left-associativity is desired in this particular instance:

(a == null ? null : a.b) ? c : d

This commit adds a check in the expression translator to detect such
left-associative usages of ternaries and to enforce such associativity with
parentheses when necessary.

A test is also added for the template type-checking expression translator,
to ensure it correctly produces right-associative expressions for ternaries
in the user's template.

Fixes #34087

PR Close #34221
This commit is contained in:
Alex Rickabaugh 2019-12-03 17:46:45 -08:00 committed by Andrew Kushnir
parent f72de51a6f
commit 718d7fe5fe
3 changed files with 59 additions and 1 deletions

View File

@ -292,8 +292,36 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
}
visitConditionalExpr(ast: ConditionalExpr, context: Context): ts.ConditionalExpression {
let cond: ts.Expression = ast.condition.visitExpression(this, context);
// Ordinarily the ternary operator is right-associative. The following are equivalent:
// `a ? b : c ? d : e` => `a ? b : (c ? d : e)`
//
// However, occasionally Angular needs to produce a left-associative conditional, such as in
// the case of a null-safe navigation production: `{{a?.b ? c : d}}`. This template produces
// a ternary of the form:
// `a == null ? null : rest of expression`
// If the rest of the expression is also a ternary though, this would produce the form:
// `a == null ? null : a.b ? c : d`
// which, if left as right-associative, would be incorrectly associated as:
// `a == null ? null : (a.b ? c : d)`
//
// In such cases, the left-associativity needs to be enforced with parentheses:
// `(a == null ? null : a.b) ? c : d`
//
// Such parentheses could always be included in the condition (guaranteeing correct behavior) in
// all cases, but this has a code size cost. Instead, parentheses are added only when a
// conditional expression is directly used as the condition of another.
//
// TODO(alxhub): investigate better logic for precendence of conditional operators
if (ast.condition instanceof ConditionalExpr) {
// The condition of this ternary needs to be wrapped in parentheses to maintain
// left-associativity.
cond = ts.createParen(cond);
}
return ts.createConditional(
ast.condition.visitExpression(this, context), ast.trueCase.visitExpression(this, context),
cond, ast.trueCase.visitExpression(this, context),
ast.falseCase !.visitExpression(this, context));
}

View File

@ -35,6 +35,11 @@ describe('type check blocks', () => {
expect(tcb(TEMPLATE)).toContain('((ctx).a)[(ctx).b];');
});
it('should handle nested ternary expressions', () => {
const TEMPLATE = `{{a ? b : c ? d : e}}`;
expect(tcb(TEMPLATE)).toContain('((ctx).a ? (ctx).b : ((ctx).c ? (ctx).d : (ctx).e))');
});
it('should handle attribute values for directive inputs', () => {
const TEMPLATE = `<div dir inputA="value"></div>`;
const DIRECTIVES: TestDeclaration[] = [{

View File

@ -755,4 +755,29 @@ describe('compiler compliance: template', () => {
expectEmit(result.source, template, 'Incorrect template');
});
it('should safely nest ternary operations', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`
{{a?.b ? 1 : 2 }}\`
})
export class MyComponent {}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};
const template = `i0.ɵɵtextInterpolate1(" ", (ctx.a == null ? null : ctx.a.b) ? 1 : 2, "")`;
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
});