fix(ivy): wrap 'as any' casts in parentheses when needed (#34649)

Previously, when generating template type-checking code, casts to 'any' were
produced as `expr as any`, regardless of the expression. However, for
certain expression types, this led to precedence issues with the cast. For
example, `a !== b` is a `ts.BinaryExpression`, and wrapping it directly in
the cast yields `a !== b as any`, which is semantically equivalent to
`a !== (b as any)`. This is obviously not what is intended.

Instead, this commit adds a list of expression types for which a "bare"
wrapping is permitted. For other expressions, parentheses are added to
ensure correct precedence: `(a !== b) as any`

PR Close #34649
This commit is contained in:
Alex Rickabaugh 2019-12-19 15:05:56 -08:00 committed by Andrew Kushnir
parent cfe5dccdd2
commit af015982f5
2 changed files with 51 additions and 1 deletions

View File

@ -9,7 +9,46 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ClassDeclaration} from '../../reflection'; import {ClassDeclaration} from '../../reflection';
/**
* A `Set` of `ts.SyntaxKind`s of `ts.Expression` which are safe to wrap in a `ts.AsExpression`
* without needing to be wrapped in parentheses.
*
* For example, `foo.bar()` is a `ts.CallExpression`, and can be safely cast to `any` with
* `foo.bar() as any`. however, `foo !== bar` is a `ts.BinaryExpression`, and attempting to cast
* without the parentheses yields the expression `foo !== bar as any`. This is semantically
* equivalent to `foo !== (bar as any)`, which is not what was intended. Thus,
* `ts.BinaryExpression`s need to be wrapped in parentheses before casting.
*/
//
const SAFE_TO_CAST_WITHOUT_PARENS: Set<ts.SyntaxKind> = new Set([
// Expressions which are already parenthesized can be cast without further wrapping.
ts.SyntaxKind.ParenthesizedExpression,
// Expressions which form a single lexical unit leave no room for precedence issues with the cast.
ts.SyntaxKind.Identifier,
ts.SyntaxKind.CallExpression,
ts.SyntaxKind.NonNullExpression,
ts.SyntaxKind.ElementAccessExpression,
ts.SyntaxKind.PropertyAccessExpression,
ts.SyntaxKind.ArrayLiteralExpression,
ts.SyntaxKind.ObjectLiteralExpression,
// The same goes for various literals.
ts.SyntaxKind.StringLiteral,
ts.SyntaxKind.NumericLiteral,
ts.SyntaxKind.TrueKeyword,
ts.SyntaxKind.FalseKeyword,
ts.SyntaxKind.NullKeyword,
ts.SyntaxKind.UndefinedKeyword,
]);
export function tsCastToAny(expr: ts.Expression): ts.Expression { export function tsCastToAny(expr: ts.Expression): ts.Expression {
// Wrap `expr` in parentheses if needed (see `SAFE_TO_CAST_WITHOUT_PARENS` above).
if (!SAFE_TO_CAST_WITHOUT_PARENS.has(expr.kind)) {
expr = ts.createParen(expr);
}
// The outer expression is always wrapped in parentheses.
return ts.createParen( return ts.createParen(
ts.createAsExpression(expr, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword))); ts.createAsExpression(expr, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)));
} }

View File

@ -376,20 +376,31 @@ describe('type check blocks', () => {
}); });
describe('config.checkTypeOfBindings', () => { describe('config.checkTypeOfBindings', () => {
const TEMPLATE = `<div dir [dirInput]="a" [nonDirInput]="b"></div>`;
it('should check types of bindings when enabled', () => { it('should check types of bindings when enabled', () => {
const TEMPLATE = `<div dir [dirInput]="a" [nonDirInput]="b"></div>`;
const block = tcb(TEMPLATE, DIRECTIVES); const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('Dir.ngTypeCtor({ "dirInput": ((ctx).a) })'); expect(block).toContain('Dir.ngTypeCtor({ "dirInput": ((ctx).a) })');
expect(block).toContain('(ctx).b;'); expect(block).toContain('(ctx).b;');
}); });
it('should not check types of bindings when disabled', () => { it('should not check types of bindings when disabled', () => {
const TEMPLATE = `<div dir [dirInput]="a" [nonDirInput]="b"></div>`;
const DISABLED_CONFIG: const DISABLED_CONFIG:
TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfInputBindings: false}; TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfInputBindings: false};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG); const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).toContain('Dir.ngTypeCtor({ "dirInput": (((ctx).a as any)) })'); expect(block).toContain('Dir.ngTypeCtor({ "dirInput": (((ctx).a as any)) })');
expect(block).toContain('((ctx).b as any);'); expect(block).toContain('((ctx).b as any);');
}); });
it('should wrap the cast to any in parentheses when required', () => {
const TEMPLATE = `<div dir [dirInput]="a === b"></div>`;
const DISABLED_CONFIG:
TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfInputBindings: false};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).toContain(
'Dir.ngTypeCtor({ "dirInput": (((((ctx).a) === ((ctx).b)) as any)) })');
});
}); });
describe('config.checkTypeOfOutputEvents', () => { describe('config.checkTypeOfOutputEvents', () => {