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:
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
Reference in New Issue
Block a user