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:
parent
e7da4040d6
commit
874792dc43
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import {ArrayType, AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinType, BuiltinTypeName, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StmtModifier, ThrowStmt, TryCatchStmt, Type, TypeofExpr, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
|
||||
import {LocalizedString} from '@angular/compiler/src/output/output_ast';
|
||||
import {LocalizedString, UnaryOperator, UnaryOperatorExpr} from '@angular/compiler/src/output/output_ast';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {DefaultImportRecorder, ImportRewriter, NOOP_DEFAULT_IMPORT_RECORDER, NoopImportRewriter} from '../../imports';
|
||||
|
@ -24,6 +24,11 @@ export class Context {
|
|||
}
|
||||
}
|
||||
|
||||
const UNARY_OPERATORS = new Map<UnaryOperator, ts.PrefixUnaryOperator>([
|
||||
[UnaryOperator.Minus, ts.SyntaxKind.MinusToken],
|
||||
[UnaryOperator.Plus, ts.SyntaxKind.PlusToken],
|
||||
]);
|
||||
|
||||
const BINARY_OPERATORS = new Map<BinaryOperator, ts.BinaryOperator>([
|
||||
[BinaryOperator.And, ts.SyntaxKind.AmpersandAmpersandToken],
|
||||
[BinaryOperator.Bigger, ts.SyntaxKind.GreaterThanToken],
|
||||
|
@ -361,6 +366,14 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
|
|||
undefined, ts.createBlock(ast.statements.map(stmt => stmt.visitStatement(this, context))));
|
||||
}
|
||||
|
||||
visitUnaryOperatorExpr(ast: UnaryOperatorExpr, context: Context): ts.Expression {
|
||||
if (!UNARY_OPERATORS.has(ast.operator)) {
|
||||
throw new Error(`Unknown unary operator: ${UnaryOperator[ast.operator]}`);
|
||||
}
|
||||
return ts.createPrefix(
|
||||
UNARY_OPERATORS.get(ast.operator)!, ast.expr.visitExpression(this, context));
|
||||
}
|
||||
|
||||
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: Context): ts.Expression {
|
||||
if (!BINARY_OPERATORS.has(ast.operator)) {
|
||||
throw new Error(`Unknown binary operator: ${BinaryOperator[ast.operator]}`);
|
||||
|
@ -567,6 +580,10 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
|
|||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitUnaryOperatorExpr(ast: UnaryOperatorExpr, context: Context) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
visitBinaryOperatorExpr(ast: BinaryOperatorExpr, context: Context) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, EmptyExpr, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead} from '@angular/compiler';
|
||||
import {AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, EmptyExpr, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead, Unary} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
import {TypeCheckingConfig} from '../api';
|
||||
|
||||
|
@ -17,7 +17,12 @@ export const NULL_AS_ANY =
|
|||
ts.createAsExpression(ts.createNull(), ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
|
||||
const UNDEFINED = ts.createIdentifier('undefined');
|
||||
|
||||
const BINARY_OPS = new Map<string, ts.SyntaxKind>([
|
||||
const UNARY_OPS = new Map<string, ts.PrefixUnaryOperator>([
|
||||
['+', ts.SyntaxKind.PlusToken],
|
||||
['-', ts.SyntaxKind.MinusToken],
|
||||
]);
|
||||
|
||||
const BINARY_OPS = new Map<string, ts.BinaryOperator>([
|
||||
['+', ts.SyntaxKind.PlusToken],
|
||||
['-', ts.SyntaxKind.MinusToken],
|
||||
['<', ts.SyntaxKind.LessThanToken],
|
||||
|
@ -74,6 +79,17 @@ class AstTranslator implements AstVisitor {
|
|||
return ast.visit(this);
|
||||
}
|
||||
|
||||
visitUnary(ast: Unary): ts.Expression {
|
||||
const expr = this.translate(ast.expr);
|
||||
const op = UNARY_OPS.get(ast.operator);
|
||||
if (op === undefined) {
|
||||
throw new Error(`Unsupported Unary.operator: ${ast.operator}`);
|
||||
}
|
||||
const node = wrapForDiagnostics(ts.createPrefix(op, expr));
|
||||
addParseSpanInfo(node, ast.sourceSpan);
|
||||
return node;
|
||||
}
|
||||
|
||||
visitBinary(ast: Binary): ts.Expression {
|
||||
const lhs = wrapForDiagnostics(this.translate(ast.left));
|
||||
const rhs = wrapForDiagnostics(this.translate(ast.right));
|
||||
|
@ -81,7 +97,7 @@ class AstTranslator implements AstVisitor {
|
|||
if (op === undefined) {
|
||||
throw new Error(`Unsupported Binary.operation: ${ast.operation}`);
|
||||
}
|
||||
const node = ts.createBinary(lhs, op as any, rhs);
|
||||
const node = ts.createBinary(lhs, op, rhs);
|
||||
addParseSpanInfo(node, ast.sourceSpan);
|
||||
return node;
|
||||
}
|
||||
|
@ -314,6 +330,9 @@ class VeSafeLhsInferenceBugDetector implements AstVisitor {
|
|||
return ast.receiver.visit(VeSafeLhsInferenceBugDetector.SINGLETON);
|
||||
}
|
||||
|
||||
visitUnary(ast: Unary): boolean {
|
||||
return ast.expr.visit(this);
|
||||
}
|
||||
visitBinary(ast: Binary): boolean {
|
||||
return ast.left.visit(this) || ast.right.visit(this);
|
||||
}
|
||||
|
|
|
@ -248,6 +248,17 @@ runInEachFileSystem(() => {
|
|||
expect(messages).toEqual([]);
|
||||
});
|
||||
|
||||
it('should treat unary operators as literal types', () => {
|
||||
const messages = diagnose(`{{ test(-1) + test(+1) + test(-2) }}`, `
|
||||
class TestComponent {
|
||||
test(value: -1 | 1): number { return value; }
|
||||
}`);
|
||||
|
||||
expect(messages).toEqual([
|
||||
`TestComponent.html(1, 31): Argument of type '-2' is not assignable to parameter of type '1 | -1'.`,
|
||||
]);
|
||||
});
|
||||
|
||||
describe('outputs', () => {
|
||||
it('should produce a diagnostic for directive outputs', () => {
|
||||
const messages = diagnose(
|
||||
|
|
|
@ -10,6 +10,10 @@ import {tcb, TestDeclaration} from './test_utils';
|
|||
|
||||
describe('type check blocks diagnostics', () => {
|
||||
describe('parse spans', () => {
|
||||
it('should annotate unary ops', () => {
|
||||
expect(tcbWithSpans('{{ -a }}')).toContain('(-((ctx).a /*4,5*/) /*4,5*/) /*3,5*/');
|
||||
});
|
||||
|
||||
it('should annotate binary ops', () => {
|
||||
expect(tcbWithSpans('{{ a + b }}'))
|
||||
.toContain('(((ctx).a /*3,4*/) /*3,4*/) + (((ctx).b /*7,8*/) /*7,8*/) /*3,8*/');
|
||||
|
|
|
@ -31,6 +31,11 @@ describe('type check blocks', () => {
|
|||
expect(tcb(TEMPLATE)).toContain('((((ctx).a))!);');
|
||||
});
|
||||
|
||||
it('should handle unary - operator', () => {
|
||||
const TEMPLATE = `{{-1}}`;
|
||||
expect(tcb(TEMPLATE)).toContain('(-1);');
|
||||
});
|
||||
|
||||
it('should handle keyed property access', () => {
|
||||
const TEMPLATE = `{{a[b]}}`;
|
||||
expect(tcb(TEMPLATE)).toContain('(((ctx).a))[((ctx).b)];');
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import {AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinMethod, BuiltinVar, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, ExpressionStatement, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, NotExpr, ParseSourceFile, ParseSourceSpan, PartialModule, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, StmtModifier, ThrowStmt, TryCatchStmt, TypeofExpr, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
|
||||
import {LocalizedString} from '@angular/compiler/src/output/output_ast';
|
||||
import {LocalizedString, UnaryOperator, UnaryOperatorExpr} from '@angular/compiler/src/output/output_ast';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {error} from './util';
|
||||
|
@ -622,6 +622,23 @@ export class NodeEmitterVisitor implements StatementVisitor, ExpressionVisitor {
|
|||
/* type */ undefined, this._visitStatements(expr.statements)));
|
||||
}
|
||||
|
||||
visitUnaryOperatorExpr(expr: UnaryOperatorExpr):
|
||||
RecordedNode<ts.UnaryExpression|ts.ParenthesizedExpression> {
|
||||
let unaryOperator: ts.BinaryOperator;
|
||||
switch (expr.operator) {
|
||||
case UnaryOperator.Minus:
|
||||
unaryOperator = ts.SyntaxKind.MinusToken;
|
||||
break;
|
||||
case UnaryOperator.Plus:
|
||||
unaryOperator = ts.SyntaxKind.PlusToken;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown operator: ${expr.operator}`);
|
||||
}
|
||||
const binary = ts.createPrefix(unaryOperator, expr.expr.visitExpression(this, null));
|
||||
return this.record(expr, expr.parens ? ts.createParen(binary) : binary);
|
||||
}
|
||||
|
||||
visitBinaryOperatorExpr(expr: BinaryOperatorExpr):
|
||||
RecordedNode<ts.BinaryExpression|ts.ParenthesizedExpression> {
|
||||
let binaryOperator: ts.BinaryOperator;
|
||||
|
|
|
@ -330,6 +330,26 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
|
|||
private bindingId: string, private interpolationFunction: InterpolationFunction|undefined,
|
||||
private baseSourceSpan?: ParseSourceSpan, private implicitReceiverAccesses?: Set<string>) {}
|
||||
|
||||
visitUnary(ast: cdAst.Unary, mode: _Mode): any {
|
||||
let op: o.UnaryOperator;
|
||||
switch (ast.operator) {
|
||||
case '+':
|
||||
op = o.UnaryOperator.Plus;
|
||||
break;
|
||||
case '-':
|
||||
op = o.UnaryOperator.Minus;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported operator ${ast.operator}`);
|
||||
}
|
||||
|
||||
return convertToStatementIfNeeded(
|
||||
mode,
|
||||
new o.UnaryOperatorExpr(
|
||||
op, this._visit(ast.expr, _Mode.Expression), undefined,
|
||||
this.convertSourceSpan(ast.span)));
|
||||
}
|
||||
|
||||
visitBinary(ast: cdAst.Binary, mode: _Mode): any {
|
||||
let op: o.BinaryOperator;
|
||||
switch (ast.operation) {
|
||||
|
@ -710,6 +730,9 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
|
|||
return (this._nodeMap.get(ast) || ast).visit(visitor);
|
||||
};
|
||||
return ast.visit({
|
||||
visitUnary(ast: cdAst.Unary) {
|
||||
return null;
|
||||
},
|
||||
visitBinary(ast: cdAst.Binary) {
|
||||
return null;
|
||||
},
|
||||
|
@ -784,6 +807,9 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
|
|||
return ast.some(ast => visit(visitor, ast));
|
||||
};
|
||||
return ast.visit({
|
||||
visitUnary(ast: cdAst.Unary): boolean {
|
||||
return visit(this, ast.expr);
|
||||
},
|
||||
visitBinary(ast: cdAst.Binary): boolean {
|
||||
return visit(this, ast.left) || visit(this, ast.right);
|
||||
},
|
||||
|
|
|
@ -330,6 +330,7 @@ class KeyVisitor implements o.ExpressionVisitor {
|
|||
visitAssertNotNullExpr = invalid;
|
||||
visitCastExpr = invalid;
|
||||
visitFunctionExpr = invalid;
|
||||
visitUnaryOperatorExpr = invalid;
|
||||
visitBinaryOperatorExpr = invalid;
|
||||
visitReadPropExpr = invalid;
|
||||
visitReadKeyExpr = invalid;
|
||||
|
|
|
@ -227,6 +227,52 @@ export class Binary extends AST {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For backwards compatibility reasons, `Unary` inherits from `Binary` and mimics the binary AST
|
||||
* node that was originally used. This inheritance relation can be deleted in some future major,
|
||||
* after consumers have been given a chance to fully support Unary.
|
||||
*/
|
||||
export class Unary extends Binary {
|
||||
// Redeclare the properties that are inherited from `Binary` as `never`, as consumers should not
|
||||
// depend on these fields when operating on `Unary`.
|
||||
left: never;
|
||||
right: never;
|
||||
operation: never;
|
||||
|
||||
/**
|
||||
* Creates a unary minus expression "-x", represented as `Binary` using "0 - x".
|
||||
*/
|
||||
static createMinus(span: ParseSpan, sourceSpan: AbsoluteSourceSpan, expr: AST): Unary {
|
||||
return new Unary(
|
||||
span, sourceSpan, '-', expr, '-', new LiteralPrimitive(span, sourceSpan, 0), expr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a unary plus expression "+x", represented as `Binary` using "x - 0".
|
||||
*/
|
||||
static createPlus(span: ParseSpan, sourceSpan: AbsoluteSourceSpan, expr: AST): Unary {
|
||||
return new Unary(
|
||||
span, sourceSpan, '+', expr, '-', expr, new LiteralPrimitive(span, sourceSpan, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* During the deprecation period this constructor is private, to avoid consumers from creating
|
||||
* a `Unary` with the fallback properties for `Binary`.
|
||||
*/
|
||||
private constructor(
|
||||
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public operator: string, public expr: AST,
|
||||
binaryOp: string, binaryLeft: AST, binaryRight: AST) {
|
||||
super(span, sourceSpan, binaryOp, binaryLeft, binaryRight);
|
||||
}
|
||||
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
if (visitor.visitUnary !== undefined) {
|
||||
return visitor.visitUnary(this, context);
|
||||
}
|
||||
return visitor.visitBinary(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class PrefixNot extends AST {
|
||||
constructor(span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public expression: AST) {
|
||||
super(span, sourceSpan);
|
||||
|
@ -361,6 +407,11 @@ export interface TemplateBindingIdentifier {
|
|||
}
|
||||
|
||||
export interface AstVisitor {
|
||||
/**
|
||||
* The `visitUnary` method is declared as optional for backwards compatibility. In an upcoming
|
||||
* major release, this method will be made required.
|
||||
*/
|
||||
visitUnary?(ast: Unary, context: any): any;
|
||||
visitBinary(ast: Binary, context: any): any;
|
||||
visitChain(ast: Chain, context: any): any;
|
||||
visitConditional(ast: Conditional, context: any): any;
|
||||
|
@ -398,6 +449,9 @@ export class RecursiveAstVisitor implements AstVisitor {
|
|||
// to selectively visit the specified node.
|
||||
ast.visit(this, context);
|
||||
}
|
||||
visitUnary(ast: Unary, context: any): any {
|
||||
this.visit(ast.expr, context);
|
||||
}
|
||||
visitBinary(ast: Binary, context: any): any {
|
||||
this.visit(ast.left, context);
|
||||
this.visit(ast.right, context);
|
||||
|
@ -527,6 +581,17 @@ export class AstTransformer implements AstVisitor {
|
|||
return new LiteralMap(ast.span, ast.sourceSpan, ast.keys, this.visitAll(ast.values));
|
||||
}
|
||||
|
||||
visitUnary(ast: Unary, context: any): AST {
|
||||
switch (ast.operator) {
|
||||
case '+':
|
||||
return Unary.createPlus(ast.span, ast.sourceSpan, ast.expr.visit(this));
|
||||
case '-':
|
||||
return Unary.createMinus(ast.span, ast.sourceSpan, ast.expr.visit(this));
|
||||
default:
|
||||
throw new Error(`Unknown unary operator ${ast.operator}`);
|
||||
}
|
||||
}
|
||||
|
||||
visitBinary(ast: Binary, context: any): AST {
|
||||
return new Binary(
|
||||
ast.span, ast.sourceSpan, ast.operation, ast.left.visit(this), ast.right.visit(this));
|
||||
|
@ -665,6 +730,21 @@ export class AstMemoryEfficientTransformer implements AstVisitor {
|
|||
return ast;
|
||||
}
|
||||
|
||||
visitUnary(ast: Unary, context: any): AST {
|
||||
const expr = ast.expr.visit(this);
|
||||
if (expr !== ast.expr) {
|
||||
switch (ast.operator) {
|
||||
case '+':
|
||||
return Unary.createPlus(ast.span, ast.sourceSpan, expr);
|
||||
case '-':
|
||||
return Unary.createMinus(ast.span, ast.sourceSpan, expr);
|
||||
default:
|
||||
throw new Error(`Unknown unary operator ${ast.operator}`);
|
||||
}
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
||||
visitBinary(ast: Binary, context: any): AST {
|
||||
const left = ast.left.visit(this);
|
||||
const right = ast.right.visit(this);
|
||||
|
|
|
@ -10,7 +10,7 @@ import * as chars from '../chars';
|
|||
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config';
|
||||
import {escapeRegExp} from '../util';
|
||||
|
||||
import {AbsoluteSourceSpan, AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, EmptyExpr, ExpressionBinding, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralMapKey, LiteralPrimitive, MethodCall, NonNullAssert, ParserError, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead, TemplateBinding, TemplateBindingIdentifier, VariableBinding} from './ast';
|
||||
import {AbsoluteSourceSpan, AST, AstVisitor, ASTWithSource, Binary, BindingPipe, Chain, Conditional, EmptyExpr, ExpressionBinding, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralMapKey, LiteralPrimitive, MethodCall, NonNullAssert, ParserError, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead, TemplateBinding, TemplateBindingIdentifier, Unary, VariableBinding} from './ast';
|
||||
import {EOF, isIdentifier, isQuote, Lexer, Token, TokenType} from './lexer';
|
||||
|
||||
export class SplitInterpolation {
|
||||
|
@ -591,22 +591,16 @@ export class _ParseAST {
|
|||
if (this.next.type == TokenType.Operator) {
|
||||
const start = this.inputIndex;
|
||||
const operator = this.next.strValue;
|
||||
const literalSpan = new ParseSpan(start, start);
|
||||
const literalSourceSpan = literalSpan.toAbsolute(this.absoluteOffset);
|
||||
let result: AST;
|
||||
switch (operator) {
|
||||
case '+':
|
||||
this.advance();
|
||||
result = this.parsePrefix();
|
||||
return new Binary(
|
||||
this.span(start), this.sourceSpan(start), '-', result,
|
||||
new LiteralPrimitive(literalSpan, literalSourceSpan, 0));
|
||||
return Unary.createPlus(this.span(start), this.sourceSpan(start), result);
|
||||
case '-':
|
||||
this.advance();
|
||||
result = this.parsePrefix();
|
||||
return new Binary(
|
||||
this.span(start), this.sourceSpan(start), operator,
|
||||
new LiteralPrimitive(literalSpan, literalSourceSpan, 0), result);
|
||||
return Unary.createMinus(this.span(start), this.sourceSpan(start), result);
|
||||
case '!':
|
||||
this.advance();
|
||||
result = this.parsePrefix();
|
||||
|
@ -1059,6 +1053,8 @@ class SimpleExpressionChecker implements AstVisitor {
|
|||
this.visitAll(ast.values, context);
|
||||
}
|
||||
|
||||
visitUnary(ast: Unary, context: any) {}
|
||||
|
||||
visitBinary(ast: Binary, context: any) {}
|
||||
|
||||
visitPrefixNot(ast: PrefixNot, context: any) {}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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} `;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -138,6 +138,8 @@ const externalModuleIdentifier = new o.ExternalReference(anotherModuleUrl, 'some
|
|||
const lhs = o.variable('lhs');
|
||||
const rhs = o.variable('rhs');
|
||||
expect(emitStmt(o.not(someVar).toStmt())).toEqual('!someVar;');
|
||||
expect(emitStmt(o.unary(o.UnaryOperator.Minus, someVar).toStmt())).toEqual('(-someVar);');
|
||||
expect(emitStmt(o.unary(o.UnaryOperator.Plus, someVar).toStmt())).toEqual('(+someVar);');
|
||||
expect(emitStmt(o.assertNotNull(someVar).toStmt())).toEqual('someVar;');
|
||||
expect(
|
||||
emitStmt(someVar.conditional(o.variable('trueCase'), o.variable('falseCase')).toStmt()))
|
||||
|
|
|
@ -191,6 +191,8 @@ const externalModuleIdentifier = new o.ExternalReference(anotherModuleUrl, 'some
|
|||
const rhs = o.variable('rhs');
|
||||
expect(emitStmt(someVar.cast(o.INT_TYPE).toStmt())).toEqual('(<number>someVar);');
|
||||
expect(emitStmt(o.not(someVar).toStmt())).toEqual('!someVar;');
|
||||
expect(emitStmt(o.unary(o.UnaryOperator.Minus, someVar).toStmt())).toEqual('(-someVar);');
|
||||
expect(emitStmt(o.unary(o.UnaryOperator.Plus, someVar).toStmt())).toEqual('(+someVar);');
|
||||
expect(emitStmt(o.assertNotNull(someVar).toStmt())).toEqual('someVar!;');
|
||||
expect(
|
||||
emitStmt(someVar.conditional(o.variable('trueCase'), o.variable('falseCase')).toStmt()))
|
||||
|
|
|
@ -1656,7 +1656,7 @@ Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>
|
|||
expect(humanizeTplAst(parse('<div *ngIf="-1">', [ngIf]))).toEqual([
|
||||
[EmbeddedTemplateAst],
|
||||
[DirectiveAst, ngIf],
|
||||
[BoundDirectivePropertyAst, 'ngIf', '0 - 1'],
|
||||
[BoundDirectivePropertyAst, 'ngIf', '-1'],
|
||||
[ElementAst, 'div'],
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -30,6 +30,10 @@ class ExpressionSourceHumanizer extends e.RecursiveAstVisitor implements t.Templ
|
|||
this.recordAst(ast);
|
||||
this.visitAll([ast.ast], null);
|
||||
}
|
||||
visitUnary(ast: e.Unary) {
|
||||
this.recordAst(ast);
|
||||
super.visitUnary(ast, null);
|
||||
}
|
||||
visitBinary(ast: e.Binary) {
|
||||
this.recordAst(ast);
|
||||
super.visitBinary(ast, null);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AST, AstVisitor, ASTWithName, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead} from '@angular/compiler';
|
||||
import {AST, AstVisitor, ASTWithName, Binary, BindingPipe, Chain, Conditional, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, NonNullAssert, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead, Unary} from '@angular/compiler';
|
||||
|
||||
import {createDiagnostic, Diagnostic} from './diagnostic_messages';
|
||||
import {BuiltinType, Signature, Symbol, SymbolQuery, SymbolTable} from './symbols';
|
||||
|
@ -38,6 +38,23 @@ export class AstType implements AstVisitor {
|
|||
return this.diagnostics;
|
||||
}
|
||||
|
||||
visitUnary(ast: Unary): Symbol {
|
||||
// Visit the child to produce diagnostics.
|
||||
ast.expr.visit(this);
|
||||
|
||||
// The unary plus and minus operator are always of type number.
|
||||
// https://github.com/Microsoft/TypeScript/blob/v1.8.10/doc/spec.md#4.18
|
||||
switch (ast.operator) {
|
||||
case '-':
|
||||
case '+':
|
||||
return this.query.getBuiltinType(BuiltinType.Number);
|
||||
}
|
||||
|
||||
this.diagnostics.push(
|
||||
createDiagnostic(refinedSpan(ast), Diagnostic.unrecognized_operator, ast.operator));
|
||||
return this.anyType;
|
||||
}
|
||||
|
||||
visitBinary(ast: Binary): Symbol {
|
||||
const getType = (ast: AST, operation: string): Symbol => {
|
||||
const type = this.getType(ast);
|
||||
|
|
|
@ -63,6 +63,7 @@ export function getExpressionCompletions(
|
|||
// (that is the scope of the implicit receiver) is the right scope as the user is typing the
|
||||
// beginning of an expression.
|
||||
tail.visit({
|
||||
visitUnary(_ast) {},
|
||||
visitBinary(_ast) {},
|
||||
visitChain(_ast) {},
|
||||
visitConditional(_ast) {},
|
||||
|
@ -157,6 +158,7 @@ export function getExpressionSymbol(
|
|||
// (that is the scope of the implicit receiver) is the right scope as the user is typing the
|
||||
// beginning of an expression.
|
||||
tail.visit({
|
||||
visitUnary(_ast) {},
|
||||
visitBinary(_ast) {},
|
||||
visitChain(_ast) {},
|
||||
visitConditional(_ast) {},
|
||||
|
|
|
@ -102,6 +102,8 @@ describe('expression diagnostics', () => {
|
|||
it('should reject *ngIf of misspelled identifier in PrefixNot node',
|
||||
() =>
|
||||
reject('<div *ngIf="people && !persson"></div>', 'Identifier \'persson\' is not defined'));
|
||||
it('should reject misspelled field in unary operator expression',
|
||||
() => reject('{{ +persson }}', `Identifier 'persson' is not defined`));
|
||||
it('should accept an *ngFor', () => accept(`
|
||||
<div *ngFor="let p of people">
|
||||
{{p.name.first}} {{p.name.last}}
|
||||
|
|
Loading…
Reference in New Issue