feat(compiler): support unary operators for more accurate type checking (#37918)

Prior to this change, the unary + and - operators would be parsed as `x - 0`
and `0 - x` respectively. The runtime semantics of these expressions are
equivalent, however they may introduce inaccurate template type checking
errors as the literal type is lost, for example:

```ts
@Component({
  template: `<button [disabled]="isAdjacent(-1)"></button>`
})
export class Example {
  isAdjacent(direction: -1 | 1): boolean { return false; }
}
```

would incorrectly report a type-check error:

> error TS2345: Argument of type 'number' is not assignable to parameter
  of type '-1 | 1'.

Additionally, the translated expression for the unary + operator would be
considered as arithmetic expression with an incompatible left-hand side:

> error TS2362: The left-hand side of an arithmetic operation must be of
  type 'any', 'number', 'bigint' or an enum type.

To resolve this issues, the implicit transformation should be avoided.
This commit adds a new unary AST node to represent these expressions,
allowing for more accurate type-checking.

Fixes #20845
Fixes #36178

PR Close #37918
This commit is contained in:
JoostK
2020-07-04 01:52:40 +02:00
committed by Misko Hevery
parent e7da4040d6
commit 874792dc43
23 changed files with 313 additions and 23 deletions

View File

@ -420,6 +420,25 @@ export abstract class AbstractEmitterVisitor implements o.StatementVisitor, o.Ex
abstract visitFunctionExpr(ast: o.FunctionExpr, ctx: EmitterVisitorContext): any;
abstract visitDeclareFunctionStmt(stmt: o.DeclareFunctionStmt, context: any): any;
visitUnaryOperatorExpr(ast: o.UnaryOperatorExpr, ctx: EmitterVisitorContext): any {
let opStr: string;
switch (ast.operator) {
case o.UnaryOperator.Plus:
opStr = '+';
break;
case o.UnaryOperator.Minus:
opStr = '-';
break;
default:
throw new Error(`Unknown operator ${ast.operator}`);
}
if (ast.parens) ctx.print(ast, `(`);
ctx.print(ast, opStr);
ast.expr.visitExpression(this, ctx);
if (ast.parens) ctx.print(ast, `)`);
return null;
}
visitBinaryOperatorExpr(ast: o.BinaryOperatorExpr, ctx: EmitterVisitorContext): any {
let opStr: string;
switch (ast.operator) {

View File

@ -100,6 +100,11 @@ export interface TypeVisitor {
///// Expressions
export enum UnaryOperator {
Minus,
Plus,
}
export enum BinaryOperator {
Equals,
NotEquals,
@ -753,6 +758,28 @@ export class FunctionExpr extends Expression {
}
export class UnaryOperatorExpr extends Expression {
constructor(
public operator: UnaryOperator, public expr: Expression, type?: Type|null,
sourceSpan?: ParseSourceSpan|null, public parens: boolean = true) {
super(type || NUMBER_TYPE, sourceSpan);
}
isEquivalent(e: Expression): boolean {
return e instanceof UnaryOperatorExpr && this.operator === e.operator &&
this.expr.isEquivalent(e.expr);
}
isConstant() {
return false;
}
visitExpression(visitor: ExpressionVisitor, context: any): any {
return visitor.visitUnaryOperatorExpr(this, context);
}
}
export class BinaryOperatorExpr extends Expression {
public lhs: Expression;
constructor(
@ -912,6 +939,7 @@ export interface ExpressionVisitor {
visitAssertNotNullExpr(ast: AssertNotNull, context: any): any;
visitCastExpr(ast: CastExpr, context: any): any;
visitFunctionExpr(ast: FunctionExpr, context: any): any;
visitUnaryOperatorExpr(ast: UnaryOperatorExpr, context: any): any;
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any): any;
visitReadPropExpr(ast: ReadPropExpr, context: any): any;
visitReadKeyExpr(ast: ReadKeyExpr, context: any): any;
@ -1292,6 +1320,13 @@ export class AstTransformer implements StatementVisitor, ExpressionVisitor {
context);
}
visitUnaryOperatorExpr(ast: UnaryOperatorExpr, context: any): any {
return this.transformExpr(
new UnaryOperatorExpr(
ast.operator, ast.expr.visitExpression(this, context), ast.type, ast.sourceSpan),
context);
}
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any): any {
return this.transformExpr(
new BinaryOperatorExpr(
@ -1517,6 +1552,10 @@ export class RecursiveAstVisitor implements StatementVisitor, ExpressionVisitor
this.visitAllStatements(ast.statements, context);
return this.visitExpression(ast, context);
}
visitUnaryOperatorExpr(ast: UnaryOperatorExpr, context: any): any {
ast.expr.visitExpression(this, context);
return this.visitExpression(ast, context);
}
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: any): any {
ast.lhs.visitExpression(this, context);
ast.rhs.visitExpression(this, context);
@ -1730,6 +1769,12 @@ export function literalMap(
values.map(e => new LiteralMapEntry(e.key, e.value, e.quoted)), type, null);
}
export function unary(
operator: UnaryOperator, expr: Expression, type?: Type,
sourceSpan?: ParseSourceSpan|null): UnaryOperatorExpr {
return new UnaryOperatorExpr(operator, expr, type, sourceSpan);
}
export function not(expr: Expression, sourceSpan?: ParseSourceSpan|null): NotExpr {
return new NotExpr(expr, sourceSpan);
}

View File

@ -282,6 +282,18 @@ class StatementInterpreter implements o.StatementVisitor, o.ExpressionVisitor {
}
return null;
}
visitUnaryOperatorExpr(ast: o.UnaryOperatorExpr, ctx: _ExecutionContext): any {
const rhs = () => ast.expr.visitExpression(this, ctx);
switch (ast.operator) {
case o.UnaryOperator.Plus:
return +rhs();
case o.UnaryOperator.Minus:
return -rhs();
default:
throw new Error(`Unknown operator ${ast.operator}`);
}
}
visitBinaryOperatorExpr(ast: o.BinaryOperatorExpr, ctx: _ExecutionContext): any {
const lhs = () => ast.lhs.visitExpression(this, ctx);
const rhs = () => ast.rhs.visitExpression(this, ctx);