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

@ -34,11 +34,11 @@ describe('parser', () => {
checkAction('undefined');
});
it('should parse unary - expressions', () => {
checkAction('-1', '0 - 1');
checkAction('+1', '1 - 0');
checkAction(`-'1'`, `0 - "1"`);
checkAction(`+'1'`, `"1" - 0`);
it('should parse unary - and + expressions', () => {
checkAction('-1', '-1');
checkAction('+1', '+1');
checkAction(`-'1'`, `-"1"`);
checkAction(`+'1'`, `+"1"`);
});
it('should parse unary ! expressions', () => {

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AbsoluteSourceSpan, AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead} from '../../../src/expression_parser/ast';
import {AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead, Unary} from '../../../src/expression_parser/ast';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../../src/ml_parser/interpolation_config';
class Unparser implements AstVisitor {
@ -35,6 +35,11 @@ class Unparser implements AstVisitor {
this._visit(ast.value);
}
visitUnary(ast: Unary, context: any) {
this._expression += ast.operator;
this._visit(ast.expr);
}
visitBinary(ast: Binary, context: any) {
this._visit(ast.left);
this._expression += ` ${ast.operation} `;

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AST, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead} from '../../../src/expression_parser/ast';
import {AST, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead, Unary} from '../../../src/expression_parser/ast';
import {unparse} from './unparser';
@ -34,6 +34,10 @@ class ASTValidator extends RecursiveAstVisitor {
this.parentSpan = oldParent;
}
visitUnary(ast: Unary, context: any): any {
this.validate(ast, () => super.visitUnary(ast, context));
}
visitBinary(ast: Binary, context: any): any {
this.validate(ast, () => super.visitBinary(ast, context));
}