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