
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
901 lines
30 KiB
TypeScript
901 lines
30 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
|
|
import {SecurityContext} from '../core';
|
|
import {ParseSourceSpan} from '../parse_util';
|
|
|
|
export class ParserError {
|
|
public message: string;
|
|
constructor(
|
|
message: string, public input: string, public errLocation: string, public ctxLocation?: any) {
|
|
this.message = `Parser Error: ${message} ${errLocation} [${input}] in ${ctxLocation}`;
|
|
}
|
|
}
|
|
|
|
export class ParseSpan {
|
|
constructor(public start: number, public end: number) {}
|
|
toAbsolute(absoluteOffset: number): AbsoluteSourceSpan {
|
|
return new AbsoluteSourceSpan(absoluteOffset + this.start, absoluteOffset + this.end);
|
|
}
|
|
}
|
|
|
|
export class AST {
|
|
constructor(
|
|
public span: ParseSpan,
|
|
/**
|
|
* Absolute location of the expression AST in a source code file.
|
|
*/
|
|
public sourceSpan: AbsoluteSourceSpan) {}
|
|
visit(visitor: AstVisitor, context: any = null): any {
|
|
return null;
|
|
}
|
|
toString(): string {
|
|
return 'AST';
|
|
}
|
|
}
|
|
|
|
export abstract class ASTWithName extends AST {
|
|
constructor(
|
|
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public nameSpan: AbsoluteSourceSpan) {
|
|
super(span, sourceSpan);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents a quoted expression of the form:
|
|
*
|
|
* quote = prefix `:` uninterpretedExpression
|
|
* prefix = identifier
|
|
* uninterpretedExpression = arbitrary string
|
|
*
|
|
* A quoted expression is meant to be pre-processed by an AST transformer that
|
|
* converts it into another AST that no longer contains quoted expressions.
|
|
* It is meant to allow third-party developers to extend Angular template
|
|
* expression language. The `uninterpretedExpression` part of the quote is
|
|
* therefore not interpreted by the Angular's own expression parser.
|
|
*/
|
|
export class Quote extends AST {
|
|
constructor(
|
|
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public prefix: string,
|
|
public uninterpretedExpression: string, public location: any) {
|
|
super(span, sourceSpan);
|
|
}
|
|
visit(visitor: AstVisitor, context: any = null): any {
|
|
return visitor.visitQuote(this, context);
|
|
}
|
|
toString(): string {
|
|
return 'Quote';
|
|
}
|
|
}
|
|
|
|
export class EmptyExpr extends AST {
|
|
visit(visitor: AstVisitor, context: any = null) {
|
|
// do nothing
|
|
}
|
|
}
|
|
|
|
export class ImplicitReceiver extends AST {
|
|
visit(visitor: AstVisitor, context: any = null): any {
|
|
return visitor.visitImplicitReceiver(this, context);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Multiple expressions separated by a semicolon.
|
|
*/
|
|
export class Chain extends AST {
|
|
constructor(span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public expressions: any[]) {
|
|
super(span, sourceSpan);
|
|
}
|
|
visit(visitor: AstVisitor, context: any = null): any {
|
|
return visitor.visitChain(this, context);
|
|
}
|
|
}
|
|
|
|
export class Conditional extends AST {
|
|
constructor(
|
|
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public condition: AST, public trueExp: AST,
|
|
public falseExp: AST) {
|
|
super(span, sourceSpan);
|
|
}
|
|
visit(visitor: AstVisitor, context: any = null): any {
|
|
return visitor.visitConditional(this, context);
|
|
}
|
|
}
|
|
|
|
export class PropertyRead extends ASTWithName {
|
|
constructor(
|
|
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, nameSpan: AbsoluteSourceSpan,
|
|
public receiver: AST, public name: string) {
|
|
super(span, sourceSpan, nameSpan);
|
|
}
|
|
visit(visitor: AstVisitor, context: any = null): any {
|
|
return visitor.visitPropertyRead(this, context);
|
|
}
|
|
}
|
|
|
|
export class PropertyWrite extends ASTWithName {
|
|
constructor(
|
|
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, nameSpan: AbsoluteSourceSpan,
|
|
public receiver: AST, public name: string, public value: AST) {
|
|
super(span, sourceSpan, nameSpan);
|
|
}
|
|
visit(visitor: AstVisitor, context: any = null): any {
|
|
return visitor.visitPropertyWrite(this, context);
|
|
}
|
|
}
|
|
|
|
export class SafePropertyRead extends ASTWithName {
|
|
constructor(
|
|
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, nameSpan: AbsoluteSourceSpan,
|
|
public receiver: AST, public name: string) {
|
|
super(span, sourceSpan, nameSpan);
|
|
}
|
|
visit(visitor: AstVisitor, context: any = null): any {
|
|
return visitor.visitSafePropertyRead(this, context);
|
|
}
|
|
}
|
|
|
|
export class KeyedRead extends AST {
|
|
constructor(span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public obj: AST, public key: AST) {
|
|
super(span, sourceSpan);
|
|
}
|
|
visit(visitor: AstVisitor, context: any = null): any {
|
|
return visitor.visitKeyedRead(this, context);
|
|
}
|
|
}
|
|
|
|
export class KeyedWrite extends AST {
|
|
constructor(
|
|
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public obj: AST, public key: AST,
|
|
public value: AST) {
|
|
super(span, sourceSpan);
|
|
}
|
|
visit(visitor: AstVisitor, context: any = null): any {
|
|
return visitor.visitKeyedWrite(this, context);
|
|
}
|
|
}
|
|
|
|
export class BindingPipe extends ASTWithName {
|
|
constructor(
|
|
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public exp: AST, public name: string,
|
|
public args: any[], nameSpan: AbsoluteSourceSpan) {
|
|
super(span, sourceSpan, nameSpan);
|
|
}
|
|
visit(visitor: AstVisitor, context: any = null): any {
|
|
return visitor.visitPipe(this, context);
|
|
}
|
|
}
|
|
|
|
export class LiteralPrimitive extends AST {
|
|
constructor(span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public value: any) {
|
|
super(span, sourceSpan);
|
|
}
|
|
visit(visitor: AstVisitor, context: any = null): any {
|
|
return visitor.visitLiteralPrimitive(this, context);
|
|
}
|
|
}
|
|
|
|
export class LiteralArray extends AST {
|
|
constructor(span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public expressions: any[]) {
|
|
super(span, sourceSpan);
|
|
}
|
|
visit(visitor: AstVisitor, context: any = null): any {
|
|
return visitor.visitLiteralArray(this, context);
|
|
}
|
|
}
|
|
|
|
export type LiteralMapKey = {
|
|
key: string; quoted: boolean;
|
|
};
|
|
|
|
export class LiteralMap extends AST {
|
|
constructor(
|
|
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public keys: LiteralMapKey[],
|
|
public values: any[]) {
|
|
super(span, sourceSpan);
|
|
}
|
|
visit(visitor: AstVisitor, context: any = null): any {
|
|
return visitor.visitLiteralMap(this, context);
|
|
}
|
|
}
|
|
|
|
export class Interpolation extends AST {
|
|
constructor(
|
|
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public strings: any[],
|
|
public expressions: any[]) {
|
|
super(span, sourceSpan);
|
|
}
|
|
visit(visitor: AstVisitor, context: any = null): any {
|
|
return visitor.visitInterpolation(this, context);
|
|
}
|
|
}
|
|
|
|
export class Binary extends AST {
|
|
constructor(
|
|
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public operation: string, public left: AST,
|
|
public right: AST) {
|
|
super(span, sourceSpan);
|
|
}
|
|
visit(visitor: AstVisitor, context: any = null): any {
|
|
return visitor.visitBinary(this, context);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
visit(visitor: AstVisitor, context: any = null): any {
|
|
return visitor.visitPrefixNot(this, context);
|
|
}
|
|
}
|
|
|
|
export class NonNullAssert extends AST {
|
|
constructor(span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public expression: AST) {
|
|
super(span, sourceSpan);
|
|
}
|
|
visit(visitor: AstVisitor, context: any = null): any {
|
|
return visitor.visitNonNullAssert(this, context);
|
|
}
|
|
}
|
|
|
|
export class MethodCall extends ASTWithName {
|
|
constructor(
|
|
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, nameSpan: AbsoluteSourceSpan,
|
|
public receiver: AST, public name: string, public args: any[]) {
|
|
super(span, sourceSpan, nameSpan);
|
|
}
|
|
visit(visitor: AstVisitor, context: any = null): any {
|
|
return visitor.visitMethodCall(this, context);
|
|
}
|
|
}
|
|
|
|
export class SafeMethodCall extends ASTWithName {
|
|
constructor(
|
|
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, nameSpan: AbsoluteSourceSpan,
|
|
public receiver: AST, public name: string, public args: any[]) {
|
|
super(span, sourceSpan, nameSpan);
|
|
}
|
|
visit(visitor: AstVisitor, context: any = null): any {
|
|
return visitor.visitSafeMethodCall(this, context);
|
|
}
|
|
}
|
|
|
|
export class FunctionCall extends AST {
|
|
constructor(
|
|
span: ParseSpan, sourceSpan: AbsoluteSourceSpan, public target: AST|null,
|
|
public args: any[]) {
|
|
super(span, sourceSpan);
|
|
}
|
|
visit(visitor: AstVisitor, context: any = null): any {
|
|
return visitor.visitFunctionCall(this, context);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Records the absolute position of a text span in a source file, where `start` and `end` are the
|
|
* starting and ending byte offsets, respectively, of the text span in a source file.
|
|
*/
|
|
export class AbsoluteSourceSpan {
|
|
constructor(public readonly start: number, public readonly end: number) {}
|
|
}
|
|
|
|
export class ASTWithSource extends AST {
|
|
constructor(
|
|
public ast: AST, public source: string|null, public location: string, absoluteOffset: number,
|
|
public errors: ParserError[]) {
|
|
super(
|
|
new ParseSpan(0, source === null ? 0 : source.length),
|
|
new AbsoluteSourceSpan(
|
|
absoluteOffset, source === null ? absoluteOffset : absoluteOffset + source.length));
|
|
}
|
|
visit(visitor: AstVisitor, context: any = null): any {
|
|
if (visitor.visitASTWithSource) {
|
|
return visitor.visitASTWithSource(this, context);
|
|
}
|
|
return this.ast.visit(visitor, context);
|
|
}
|
|
toString(): string {
|
|
return `${this.source} in ${this.location}`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* TemplateBinding refers to a particular key-value pair in a microsyntax
|
|
* expression. A few examples are:
|
|
*
|
|
* |---------------------|--------------|---------|--------------|
|
|
* | expression | key | value | binding type |
|
|
* |---------------------|--------------|---------|--------------|
|
|
* | 1. let item | item | null | variable |
|
|
* | 2. of items | ngForOf | items | expression |
|
|
* | 3. let x = y | x | y | variable |
|
|
* | 4. index as i | i | index | variable |
|
|
* | 5. trackBy: func | ngForTrackBy | func | expression |
|
|
* | 6. *ngIf="cond" | ngIf | cond | expression |
|
|
* |---------------------|--------------|---------|--------------|
|
|
*
|
|
* (6) is a notable exception because it is a binding from the template key in
|
|
* the LHS of a HTML attribute to the expression in the RHS. All other bindings
|
|
* in the example above are derived solely from the RHS.
|
|
*/
|
|
export type TemplateBinding = VariableBinding|ExpressionBinding;
|
|
|
|
export class VariableBinding {
|
|
/**
|
|
* @param sourceSpan entire span of the binding.
|
|
* @param key name of the LHS along with its span.
|
|
* @param value optional value for the RHS along with its span.
|
|
*/
|
|
constructor(
|
|
public readonly sourceSpan: AbsoluteSourceSpan,
|
|
public readonly key: TemplateBindingIdentifier,
|
|
public readonly value: TemplateBindingIdentifier|null) {}
|
|
}
|
|
|
|
export class ExpressionBinding {
|
|
/**
|
|
* @param sourceSpan entire span of the binding.
|
|
* @param key binding name, like ngForOf, ngForTrackBy, ngIf, along with its
|
|
* span. Note that the length of the span may not be the same as
|
|
* `key.source.length`. For example,
|
|
* 1. key.source = ngFor, key.span is for "ngFor"
|
|
* 2. key.source = ngForOf, key.span is for "of"
|
|
* 3. key.source = ngForTrackBy, key.span is for "trackBy"
|
|
* @param value optional expression for the RHS.
|
|
*/
|
|
constructor(
|
|
public readonly sourceSpan: AbsoluteSourceSpan,
|
|
public readonly key: TemplateBindingIdentifier, public readonly value: ASTWithSource|null) {}
|
|
}
|
|
|
|
export interface TemplateBindingIdentifier {
|
|
source: string;
|
|
span: AbsoluteSourceSpan;
|
|
}
|
|
|
|
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;
|
|
visitFunctionCall(ast: FunctionCall, context: any): any;
|
|
visitImplicitReceiver(ast: ImplicitReceiver, context: any): any;
|
|
visitInterpolation(ast: Interpolation, context: any): any;
|
|
visitKeyedRead(ast: KeyedRead, context: any): any;
|
|
visitKeyedWrite(ast: KeyedWrite, context: any): any;
|
|
visitLiteralArray(ast: LiteralArray, context: any): any;
|
|
visitLiteralMap(ast: LiteralMap, context: any): any;
|
|
visitLiteralPrimitive(ast: LiteralPrimitive, context: any): any;
|
|
visitMethodCall(ast: MethodCall, context: any): any;
|
|
visitPipe(ast: BindingPipe, context: any): any;
|
|
visitPrefixNot(ast: PrefixNot, context: any): any;
|
|
visitNonNullAssert(ast: NonNullAssert, context: any): any;
|
|
visitPropertyRead(ast: PropertyRead, context: any): any;
|
|
visitPropertyWrite(ast: PropertyWrite, context: any): any;
|
|
visitQuote(ast: Quote, context: any): any;
|
|
visitSafeMethodCall(ast: SafeMethodCall, context: any): any;
|
|
visitSafePropertyRead(ast: SafePropertyRead, context: any): any;
|
|
visitASTWithSource?(ast: ASTWithSource, context: any): any;
|
|
/**
|
|
* This function is optionally defined to allow classes that implement this
|
|
* interface to selectively decide if the specified `ast` should be visited.
|
|
* @param ast node to visit
|
|
* @param context context that gets passed to the node and all its children
|
|
*/
|
|
visit?(ast: AST, context?: any): any;
|
|
}
|
|
|
|
export class RecursiveAstVisitor implements AstVisitor {
|
|
visit(ast: AST, context?: any): any {
|
|
// The default implementation just visits every node.
|
|
// Classes that extend RecursiveAstVisitor should override this function
|
|
// 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);
|
|
}
|
|
visitChain(ast: Chain, context: any): any {
|
|
this.visitAll(ast.expressions, context);
|
|
}
|
|
visitConditional(ast: Conditional, context: any): any {
|
|
this.visit(ast.condition, context);
|
|
this.visit(ast.trueExp, context);
|
|
this.visit(ast.falseExp, context);
|
|
}
|
|
visitPipe(ast: BindingPipe, context: any): any {
|
|
this.visit(ast.exp, context);
|
|
this.visitAll(ast.args, context);
|
|
}
|
|
visitFunctionCall(ast: FunctionCall, context: any): any {
|
|
if (ast.target) {
|
|
this.visit(ast.target, context);
|
|
}
|
|
this.visitAll(ast.args, context);
|
|
}
|
|
visitImplicitReceiver(ast: ImplicitReceiver, context: any): any {}
|
|
visitInterpolation(ast: Interpolation, context: any): any {
|
|
this.visitAll(ast.expressions, context);
|
|
}
|
|
visitKeyedRead(ast: KeyedRead, context: any): any {
|
|
this.visit(ast.obj, context);
|
|
this.visit(ast.key, context);
|
|
}
|
|
visitKeyedWrite(ast: KeyedWrite, context: any): any {
|
|
this.visit(ast.obj, context);
|
|
this.visit(ast.key, context);
|
|
this.visit(ast.value, context);
|
|
}
|
|
visitLiteralArray(ast: LiteralArray, context: any): any {
|
|
this.visitAll(ast.expressions, context);
|
|
}
|
|
visitLiteralMap(ast: LiteralMap, context: any): any {
|
|
this.visitAll(ast.values, context);
|
|
}
|
|
visitLiteralPrimitive(ast: LiteralPrimitive, context: any): any {}
|
|
visitMethodCall(ast: MethodCall, context: any): any {
|
|
this.visit(ast.receiver, context);
|
|
this.visitAll(ast.args, context);
|
|
}
|
|
visitPrefixNot(ast: PrefixNot, context: any): any {
|
|
this.visit(ast.expression, context);
|
|
}
|
|
visitNonNullAssert(ast: NonNullAssert, context: any): any {
|
|
this.visit(ast.expression, context);
|
|
}
|
|
visitPropertyRead(ast: PropertyRead, context: any): any {
|
|
this.visit(ast.receiver, context);
|
|
}
|
|
visitPropertyWrite(ast: PropertyWrite, context: any): any {
|
|
this.visit(ast.receiver, context);
|
|
this.visit(ast.value, context);
|
|
}
|
|
visitSafePropertyRead(ast: SafePropertyRead, context: any): any {
|
|
this.visit(ast.receiver, context);
|
|
}
|
|
visitSafeMethodCall(ast: SafeMethodCall, context: any): any {
|
|
this.visit(ast.receiver, context);
|
|
this.visitAll(ast.args, context);
|
|
}
|
|
visitQuote(ast: Quote, context: any): any {}
|
|
// This is not part of the AstVisitor interface, just a helper method
|
|
visitAll(asts: AST[], context: any): any {
|
|
for (const ast of asts) {
|
|
this.visit(ast, context);
|
|
}
|
|
}
|
|
}
|
|
|
|
export class AstTransformer implements AstVisitor {
|
|
visitImplicitReceiver(ast: ImplicitReceiver, context: any): AST {
|
|
return ast;
|
|
}
|
|
|
|
visitInterpolation(ast: Interpolation, context: any): AST {
|
|
return new Interpolation(ast.span, ast.sourceSpan, ast.strings, this.visitAll(ast.expressions));
|
|
}
|
|
|
|
visitLiteralPrimitive(ast: LiteralPrimitive, context: any): AST {
|
|
return new LiteralPrimitive(ast.span, ast.sourceSpan, ast.value);
|
|
}
|
|
|
|
visitPropertyRead(ast: PropertyRead, context: any): AST {
|
|
return new PropertyRead(
|
|
ast.span, ast.sourceSpan, ast.nameSpan, ast.receiver.visit(this), ast.name);
|
|
}
|
|
|
|
visitPropertyWrite(ast: PropertyWrite, context: any): AST {
|
|
return new PropertyWrite(
|
|
ast.span, ast.sourceSpan, ast.nameSpan, ast.receiver.visit(this), ast.name,
|
|
ast.value.visit(this));
|
|
}
|
|
|
|
visitSafePropertyRead(ast: SafePropertyRead, context: any): AST {
|
|
return new SafePropertyRead(
|
|
ast.span, ast.sourceSpan, ast.nameSpan, ast.receiver.visit(this), ast.name);
|
|
}
|
|
|
|
visitMethodCall(ast: MethodCall, context: any): AST {
|
|
return new MethodCall(
|
|
ast.span, ast.sourceSpan, ast.nameSpan, ast.receiver.visit(this), ast.name,
|
|
this.visitAll(ast.args));
|
|
}
|
|
|
|
visitSafeMethodCall(ast: SafeMethodCall, context: any): AST {
|
|
return new SafeMethodCall(
|
|
ast.span, ast.sourceSpan, ast.nameSpan, ast.receiver.visit(this), ast.name,
|
|
this.visitAll(ast.args));
|
|
}
|
|
|
|
visitFunctionCall(ast: FunctionCall, context: any): AST {
|
|
return new FunctionCall(
|
|
ast.span, ast.sourceSpan, ast.target!.visit(this), this.visitAll(ast.args));
|
|
}
|
|
|
|
visitLiteralArray(ast: LiteralArray, context: any): AST {
|
|
return new LiteralArray(ast.span, ast.sourceSpan, this.visitAll(ast.expressions));
|
|
}
|
|
|
|
visitLiteralMap(ast: LiteralMap, context: any): AST {
|
|
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));
|
|
}
|
|
|
|
visitPrefixNot(ast: PrefixNot, context: any): AST {
|
|
return new PrefixNot(ast.span, ast.sourceSpan, ast.expression.visit(this));
|
|
}
|
|
|
|
visitNonNullAssert(ast: NonNullAssert, context: any): AST {
|
|
return new NonNullAssert(ast.span, ast.sourceSpan, ast.expression.visit(this));
|
|
}
|
|
|
|
visitConditional(ast: Conditional, context: any): AST {
|
|
return new Conditional(
|
|
ast.span, ast.sourceSpan, ast.condition.visit(this), ast.trueExp.visit(this),
|
|
ast.falseExp.visit(this));
|
|
}
|
|
|
|
visitPipe(ast: BindingPipe, context: any): AST {
|
|
return new BindingPipe(
|
|
ast.span, ast.sourceSpan, ast.exp.visit(this), ast.name, this.visitAll(ast.args),
|
|
ast.nameSpan);
|
|
}
|
|
|
|
visitKeyedRead(ast: KeyedRead, context: any): AST {
|
|
return new KeyedRead(ast.span, ast.sourceSpan, ast.obj.visit(this), ast.key.visit(this));
|
|
}
|
|
|
|
visitKeyedWrite(ast: KeyedWrite, context: any): AST {
|
|
return new KeyedWrite(
|
|
ast.span, ast.sourceSpan, ast.obj.visit(this), ast.key.visit(this), ast.value.visit(this));
|
|
}
|
|
|
|
visitAll(asts: any[]): any[] {
|
|
const res = [];
|
|
for (let i = 0; i < asts.length; ++i) {
|
|
res[i] = asts[i].visit(this);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
visitChain(ast: Chain, context: any): AST {
|
|
return new Chain(ast.span, ast.sourceSpan, this.visitAll(ast.expressions));
|
|
}
|
|
|
|
visitQuote(ast: Quote, context: any): AST {
|
|
return new Quote(
|
|
ast.span, ast.sourceSpan, ast.prefix, ast.uninterpretedExpression, ast.location);
|
|
}
|
|
}
|
|
|
|
// A transformer that only creates new nodes if the transformer makes a change or
|
|
// a change is made a child node.
|
|
export class AstMemoryEfficientTransformer implements AstVisitor {
|
|
visitImplicitReceiver(ast: ImplicitReceiver, context: any): AST {
|
|
return ast;
|
|
}
|
|
|
|
visitInterpolation(ast: Interpolation, context: any): Interpolation {
|
|
const expressions = this.visitAll(ast.expressions);
|
|
if (expressions !== ast.expressions)
|
|
return new Interpolation(ast.span, ast.sourceSpan, ast.strings, expressions);
|
|
return ast;
|
|
}
|
|
|
|
visitLiteralPrimitive(ast: LiteralPrimitive, context: any): AST {
|
|
return ast;
|
|
}
|
|
|
|
visitPropertyRead(ast: PropertyRead, context: any): AST {
|
|
const receiver = ast.receiver.visit(this);
|
|
if (receiver !== ast.receiver) {
|
|
return new PropertyRead(ast.span, ast.sourceSpan, ast.nameSpan, receiver, ast.name);
|
|
}
|
|
return ast;
|
|
}
|
|
|
|
visitPropertyWrite(ast: PropertyWrite, context: any): AST {
|
|
const receiver = ast.receiver.visit(this);
|
|
const value = ast.value.visit(this);
|
|
if (receiver !== ast.receiver || value !== ast.value) {
|
|
return new PropertyWrite(ast.span, ast.sourceSpan, ast.nameSpan, receiver, ast.name, value);
|
|
}
|
|
return ast;
|
|
}
|
|
|
|
visitSafePropertyRead(ast: SafePropertyRead, context: any): AST {
|
|
const receiver = ast.receiver.visit(this);
|
|
if (receiver !== ast.receiver) {
|
|
return new SafePropertyRead(ast.span, ast.sourceSpan, ast.nameSpan, receiver, ast.name);
|
|
}
|
|
return ast;
|
|
}
|
|
|
|
visitMethodCall(ast: MethodCall, context: any): AST {
|
|
const receiver = ast.receiver.visit(this);
|
|
const args = this.visitAll(ast.args);
|
|
if (receiver !== ast.receiver || args !== ast.args) {
|
|
return new MethodCall(ast.span, ast.sourceSpan, ast.nameSpan, receiver, ast.name, args);
|
|
}
|
|
return ast;
|
|
}
|
|
|
|
visitSafeMethodCall(ast: SafeMethodCall, context: any): AST {
|
|
const receiver = ast.receiver.visit(this);
|
|
const args = this.visitAll(ast.args);
|
|
if (receiver !== ast.receiver || args !== ast.args) {
|
|
return new SafeMethodCall(ast.span, ast.sourceSpan, ast.nameSpan, receiver, ast.name, args);
|
|
}
|
|
return ast;
|
|
}
|
|
|
|
visitFunctionCall(ast: FunctionCall, context: any): AST {
|
|
const target = ast.target && ast.target.visit(this);
|
|
const args = this.visitAll(ast.args);
|
|
if (target !== ast.target || args !== ast.args) {
|
|
return new FunctionCall(ast.span, ast.sourceSpan, target, args);
|
|
}
|
|
return ast;
|
|
}
|
|
|
|
visitLiteralArray(ast: LiteralArray, context: any): AST {
|
|
const expressions = this.visitAll(ast.expressions);
|
|
if (expressions !== ast.expressions) {
|
|
return new LiteralArray(ast.span, ast.sourceSpan, expressions);
|
|
}
|
|
return ast;
|
|
}
|
|
|
|
visitLiteralMap(ast: LiteralMap, context: any): AST {
|
|
const values = this.visitAll(ast.values);
|
|
if (values !== ast.values) {
|
|
return new LiteralMap(ast.span, ast.sourceSpan, ast.keys, values);
|
|
}
|
|
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);
|
|
if (left !== ast.left || right !== ast.right) {
|
|
return new Binary(ast.span, ast.sourceSpan, ast.operation, left, right);
|
|
}
|
|
return ast;
|
|
}
|
|
|
|
visitPrefixNot(ast: PrefixNot, context: any): AST {
|
|
const expression = ast.expression.visit(this);
|
|
if (expression !== ast.expression) {
|
|
return new PrefixNot(ast.span, ast.sourceSpan, expression);
|
|
}
|
|
return ast;
|
|
}
|
|
|
|
visitNonNullAssert(ast: NonNullAssert, context: any): AST {
|
|
const expression = ast.expression.visit(this);
|
|
if (expression !== ast.expression) {
|
|
return new NonNullAssert(ast.span, ast.sourceSpan, expression);
|
|
}
|
|
return ast;
|
|
}
|
|
|
|
visitConditional(ast: Conditional, context: any): AST {
|
|
const condition = ast.condition.visit(this);
|
|
const trueExp = ast.trueExp.visit(this);
|
|
const falseExp = ast.falseExp.visit(this);
|
|
if (condition !== ast.condition || trueExp !== ast.trueExp || falseExp !== ast.falseExp) {
|
|
return new Conditional(ast.span, ast.sourceSpan, condition, trueExp, falseExp);
|
|
}
|
|
return ast;
|
|
}
|
|
|
|
visitPipe(ast: BindingPipe, context: any): AST {
|
|
const exp = ast.exp.visit(this);
|
|
const args = this.visitAll(ast.args);
|
|
if (exp !== ast.exp || args !== ast.args) {
|
|
return new BindingPipe(ast.span, ast.sourceSpan, exp, ast.name, args, ast.nameSpan);
|
|
}
|
|
return ast;
|
|
}
|
|
|
|
visitKeyedRead(ast: KeyedRead, context: any): AST {
|
|
const obj = ast.obj.visit(this);
|
|
const key = ast.key.visit(this);
|
|
if (obj !== ast.obj || key !== ast.key) {
|
|
return new KeyedRead(ast.span, ast.sourceSpan, obj, key);
|
|
}
|
|
return ast;
|
|
}
|
|
|
|
visitKeyedWrite(ast: KeyedWrite, context: any): AST {
|
|
const obj = ast.obj.visit(this);
|
|
const key = ast.key.visit(this);
|
|
const value = ast.value.visit(this);
|
|
if (obj !== ast.obj || key !== ast.key || value !== ast.value) {
|
|
return new KeyedWrite(ast.span, ast.sourceSpan, obj, key, value);
|
|
}
|
|
return ast;
|
|
}
|
|
|
|
visitAll(asts: any[]): any[] {
|
|
const res = [];
|
|
let modified = false;
|
|
for (let i = 0; i < asts.length; ++i) {
|
|
const original = asts[i];
|
|
const value = original.visit(this);
|
|
res[i] = value;
|
|
modified = modified || value !== original;
|
|
}
|
|
return modified ? res : asts;
|
|
}
|
|
|
|
visitChain(ast: Chain, context: any): AST {
|
|
const expressions = this.visitAll(ast.expressions);
|
|
if (expressions !== ast.expressions) {
|
|
return new Chain(ast.span, ast.sourceSpan, expressions);
|
|
}
|
|
return ast;
|
|
}
|
|
|
|
visitQuote(ast: Quote, context: any): AST {
|
|
return ast;
|
|
}
|
|
}
|
|
|
|
// Bindings
|
|
|
|
export class ParsedProperty {
|
|
public readonly isLiteral: boolean;
|
|
public readonly isAnimation: boolean;
|
|
|
|
constructor(
|
|
public name: string, public expression: ASTWithSource, public type: ParsedPropertyType,
|
|
public sourceSpan: ParseSourceSpan, public valueSpan?: ParseSourceSpan) {
|
|
this.isLiteral = this.type === ParsedPropertyType.LITERAL_ATTR;
|
|
this.isAnimation = this.type === ParsedPropertyType.ANIMATION;
|
|
}
|
|
}
|
|
|
|
export enum ParsedPropertyType {
|
|
DEFAULT,
|
|
LITERAL_ATTR,
|
|
ANIMATION
|
|
}
|
|
|
|
export const enum ParsedEventType {
|
|
// DOM or Directive event
|
|
Regular,
|
|
// Animation specific event
|
|
Animation,
|
|
}
|
|
|
|
export class ParsedEvent {
|
|
// Regular events have a target
|
|
// Animation events have a phase
|
|
constructor(
|
|
public name: string, public targetOrPhase: string, public type: ParsedEventType,
|
|
public handler: ASTWithSource, public sourceSpan: ParseSourceSpan,
|
|
public handlerSpan: ParseSourceSpan) {}
|
|
}
|
|
|
|
/**
|
|
* ParsedVariable represents a variable declaration in a microsyntax expression.
|
|
*/
|
|
export class ParsedVariable {
|
|
constructor(
|
|
public readonly name: string, public readonly value: string,
|
|
public readonly sourceSpan: ParseSourceSpan, public readonly keySpan: ParseSourceSpan,
|
|
public readonly valueSpan?: ParseSourceSpan) {}
|
|
}
|
|
|
|
export const enum BindingType {
|
|
// A regular binding to a property (e.g. `[property]="expression"`).
|
|
Property,
|
|
// A binding to an element attribute (e.g. `[attr.name]="expression"`).
|
|
Attribute,
|
|
// A binding to a CSS class (e.g. `[class.name]="condition"`).
|
|
Class,
|
|
// A binding to a style rule (e.g. `[style.rule]="expression"`).
|
|
Style,
|
|
// A binding to an animation reference (e.g. `[animate.key]="expression"`).
|
|
Animation,
|
|
}
|
|
|
|
export class BoundElementProperty {
|
|
constructor(
|
|
public name: string, public type: BindingType, public securityContext: SecurityContext,
|
|
public value: ASTWithSource, public unit: string|null, public sourceSpan: ParseSourceSpan,
|
|
public valueSpan?: ParseSourceSpan) {}
|
|
}
|