refactor: move angular source to /packages rather than modules/@angular
This commit is contained in:
392
packages/compiler/src/expression_parser/ast.ts
Normal file
392
packages/compiler/src/expression_parser/ast.ts
Normal file
@ -0,0 +1,392 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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
|
||||
*/
|
||||
|
||||
|
||||
|
||||
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) {}
|
||||
}
|
||||
|
||||
export class AST {
|
||||
constructor(public span: ParseSpan) {}
|
||||
visit(visitor: AstVisitor, context: any = null): any { return null; }
|
||||
toString(): string { return 'AST'; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, public prefix: string, public uninterpretedExpression: string,
|
||||
public location: any) {
|
||||
super(span);
|
||||
}
|
||||
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, public expressions: any[]) { super(span); }
|
||||
visit(visitor: AstVisitor, context: any = null): any { return visitor.visitChain(this, context); }
|
||||
}
|
||||
|
||||
export class Conditional extends AST {
|
||||
constructor(span: ParseSpan, public condition: AST, public trueExp: AST, public falseExp: AST) {
|
||||
super(span);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitConditional(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class PropertyRead extends AST {
|
||||
constructor(span: ParseSpan, public receiver: AST, public name: string) { super(span); }
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitPropertyRead(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class PropertyWrite extends AST {
|
||||
constructor(span: ParseSpan, public receiver: AST, public name: string, public value: AST) {
|
||||
super(span);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitPropertyWrite(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class SafePropertyRead extends AST {
|
||||
constructor(span: ParseSpan, public receiver: AST, public name: string) { super(span); }
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitSafePropertyRead(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class KeyedRead extends AST {
|
||||
constructor(span: ParseSpan, public obj: AST, public key: AST) { super(span); }
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitKeyedRead(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class KeyedWrite extends AST {
|
||||
constructor(span: ParseSpan, public obj: AST, public key: AST, public value: AST) { super(span); }
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitKeyedWrite(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class BindingPipe extends AST {
|
||||
constructor(span: ParseSpan, public exp: AST, public name: string, public args: any[]) {
|
||||
super(span);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any { return visitor.visitPipe(this, context); }
|
||||
}
|
||||
|
||||
export class LiteralPrimitive extends AST {
|
||||
constructor(span: ParseSpan, public value: any) { super(span); }
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitLiteralPrimitive(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class LiteralArray extends AST {
|
||||
constructor(span: ParseSpan, public expressions: any[]) { super(span); }
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitLiteralArray(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class LiteralMap extends AST {
|
||||
constructor(span: ParseSpan, public keys: any[], public values: any[]) { super(span); }
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitLiteralMap(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class Interpolation extends AST {
|
||||
constructor(span: ParseSpan, public strings: any[], public expressions: any[]) { super(span); }
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitInterpolation(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class Binary extends AST {
|
||||
constructor(span: ParseSpan, public operation: string, public left: AST, public right: AST) {
|
||||
super(span);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitBinary(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class PrefixNot extends AST {
|
||||
constructor(span: ParseSpan, public expression: AST) { super(span); }
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitPrefixNot(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class MethodCall extends AST {
|
||||
constructor(span: ParseSpan, public receiver: AST, public name: string, public args: any[]) {
|
||||
super(span);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitMethodCall(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class SafeMethodCall extends AST {
|
||||
constructor(span: ParseSpan, public receiver: AST, public name: string, public args: any[]) {
|
||||
super(span);
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitSafeMethodCall(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class FunctionCall extends AST {
|
||||
constructor(span: ParseSpan, public target: AST, public args: any[]) { super(span); }
|
||||
visit(visitor: AstVisitor, context: any = null): any {
|
||||
return visitor.visitFunctionCall(this, context);
|
||||
}
|
||||
}
|
||||
|
||||
export class ASTWithSource extends AST {
|
||||
constructor(
|
||||
public ast: AST, public source: string, public location: string,
|
||||
public errors: ParserError[]) {
|
||||
super(new ParseSpan(0, source == null ? 0 : source.length));
|
||||
}
|
||||
visit(visitor: AstVisitor, context: any = null): any { return this.ast.visit(visitor, context); }
|
||||
toString(): string { return `${this.source} in ${this.location}`; }
|
||||
}
|
||||
|
||||
export class TemplateBinding {
|
||||
constructor(
|
||||
public span: ParseSpan, public key: string, public keyIsVar: boolean, public name: string,
|
||||
public expression: ASTWithSource) {}
|
||||
}
|
||||
|
||||
export interface AstVisitor {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
export class RecursiveAstVisitor implements AstVisitor {
|
||||
visitBinary(ast: Binary, context: any): any {
|
||||
ast.left.visit(this);
|
||||
ast.right.visit(this);
|
||||
return null;
|
||||
}
|
||||
visitChain(ast: Chain, context: any): any { return this.visitAll(ast.expressions, context); }
|
||||
visitConditional(ast: Conditional, context: any): any {
|
||||
ast.condition.visit(this);
|
||||
ast.trueExp.visit(this);
|
||||
ast.falseExp.visit(this);
|
||||
return null;
|
||||
}
|
||||
visitPipe(ast: BindingPipe, context: any): any {
|
||||
ast.exp.visit(this);
|
||||
this.visitAll(ast.args, context);
|
||||
return null;
|
||||
}
|
||||
visitFunctionCall(ast: FunctionCall, context: any): any {
|
||||
ast.target.visit(this);
|
||||
this.visitAll(ast.args, context);
|
||||
return null;
|
||||
}
|
||||
visitImplicitReceiver(ast: ImplicitReceiver, context: any): any { return null; }
|
||||
visitInterpolation(ast: Interpolation, context: any): any {
|
||||
return this.visitAll(ast.expressions, context);
|
||||
}
|
||||
visitKeyedRead(ast: KeyedRead, context: any): any {
|
||||
ast.obj.visit(this);
|
||||
ast.key.visit(this);
|
||||
return null;
|
||||
}
|
||||
visitKeyedWrite(ast: KeyedWrite, context: any): any {
|
||||
ast.obj.visit(this);
|
||||
ast.key.visit(this);
|
||||
ast.value.visit(this);
|
||||
return null;
|
||||
}
|
||||
visitLiteralArray(ast: LiteralArray, context: any): any {
|
||||
return this.visitAll(ast.expressions, context);
|
||||
}
|
||||
visitLiteralMap(ast: LiteralMap, context: any): any { return this.visitAll(ast.values, context); }
|
||||
visitLiteralPrimitive(ast: LiteralPrimitive, context: any): any { return null; }
|
||||
visitMethodCall(ast: MethodCall, context: any): any {
|
||||
ast.receiver.visit(this);
|
||||
return this.visitAll(ast.args, context);
|
||||
}
|
||||
visitPrefixNot(ast: PrefixNot, context: any): any {
|
||||
ast.expression.visit(this);
|
||||
return null;
|
||||
}
|
||||
visitPropertyRead(ast: PropertyRead, context: any): any {
|
||||
ast.receiver.visit(this);
|
||||
return null;
|
||||
}
|
||||
visitPropertyWrite(ast: PropertyWrite, context: any): any {
|
||||
ast.receiver.visit(this);
|
||||
ast.value.visit(this);
|
||||
return null;
|
||||
}
|
||||
visitSafePropertyRead(ast: SafePropertyRead, context: any): any {
|
||||
ast.receiver.visit(this);
|
||||
return null;
|
||||
}
|
||||
visitSafeMethodCall(ast: SafeMethodCall, context: any): any {
|
||||
ast.receiver.visit(this);
|
||||
return this.visitAll(ast.args, context);
|
||||
}
|
||||
visitAll(asts: AST[], context: any): any {
|
||||
asts.forEach(ast => ast.visit(this, context));
|
||||
return null;
|
||||
}
|
||||
visitQuote(ast: Quote, context: any): any { return null; }
|
||||
}
|
||||
|
||||
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.strings, this.visitAll(ast.expressions));
|
||||
}
|
||||
|
||||
visitLiteralPrimitive(ast: LiteralPrimitive, context: any): AST {
|
||||
return new LiteralPrimitive(ast.span, ast.value);
|
||||
}
|
||||
|
||||
visitPropertyRead(ast: PropertyRead, context: any): AST {
|
||||
return new PropertyRead(ast.span, ast.receiver.visit(this), ast.name);
|
||||
}
|
||||
|
||||
visitPropertyWrite(ast: PropertyWrite, context: any): AST {
|
||||
return new PropertyWrite(ast.span, ast.receiver.visit(this), ast.name, ast.value.visit(this));
|
||||
}
|
||||
|
||||
visitSafePropertyRead(ast: SafePropertyRead, context: any): AST {
|
||||
return new SafePropertyRead(ast.span, ast.receiver.visit(this), ast.name);
|
||||
}
|
||||
|
||||
visitMethodCall(ast: MethodCall, context: any): AST {
|
||||
return new MethodCall(ast.span, ast.receiver.visit(this), ast.name, this.visitAll(ast.args));
|
||||
}
|
||||
|
||||
visitSafeMethodCall(ast: SafeMethodCall, context: any): AST {
|
||||
return new SafeMethodCall(
|
||||
ast.span, ast.receiver.visit(this), ast.name, this.visitAll(ast.args));
|
||||
}
|
||||
|
||||
visitFunctionCall(ast: FunctionCall, context: any): AST {
|
||||
return new FunctionCall(ast.span, ast.target.visit(this), this.visitAll(ast.args));
|
||||
}
|
||||
|
||||
visitLiteralArray(ast: LiteralArray, context: any): AST {
|
||||
return new LiteralArray(ast.span, this.visitAll(ast.expressions));
|
||||
}
|
||||
|
||||
visitLiteralMap(ast: LiteralMap, context: any): AST {
|
||||
return new LiteralMap(ast.span, ast.keys, this.visitAll(ast.values));
|
||||
}
|
||||
|
||||
visitBinary(ast: Binary, context: any): AST {
|
||||
return new Binary(ast.span, ast.operation, ast.left.visit(this), ast.right.visit(this));
|
||||
}
|
||||
|
||||
visitPrefixNot(ast: PrefixNot, context: any): AST {
|
||||
return new PrefixNot(ast.span, ast.expression.visit(this));
|
||||
}
|
||||
|
||||
visitConditional(ast: Conditional, context: any): AST {
|
||||
return new Conditional(
|
||||
ast.span, ast.condition.visit(this), ast.trueExp.visit(this), ast.falseExp.visit(this));
|
||||
}
|
||||
|
||||
visitPipe(ast: BindingPipe, context: any): AST {
|
||||
return new BindingPipe(ast.span, ast.exp.visit(this), ast.name, this.visitAll(ast.args));
|
||||
}
|
||||
|
||||
visitKeyedRead(ast: KeyedRead, context: any): AST {
|
||||
return new KeyedRead(ast.span, ast.obj.visit(this), ast.key.visit(this));
|
||||
}
|
||||
|
||||
visitKeyedWrite(ast: KeyedWrite, context: any): AST {
|
||||
return new KeyedWrite(
|
||||
ast.span, ast.obj.visit(this), ast.key.visit(this), ast.value.visit(this));
|
||||
}
|
||||
|
||||
visitAll(asts: any[]): any[] {
|
||||
const res = new Array(asts.length);
|
||||
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, this.visitAll(ast.expressions));
|
||||
}
|
||||
|
||||
visitQuote(ast: Quote, context: any): AST {
|
||||
return new Quote(ast.span, ast.prefix, ast.uninterpretedExpression, ast.location);
|
||||
}
|
||||
}
|
392
packages/compiler/src/expression_parser/lexer.ts
Normal file
392
packages/compiler/src/expression_parser/lexer.ts
Normal file
@ -0,0 +1,392 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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 * as chars from '../chars';
|
||||
import {CompilerInjectable} from '../injectable';
|
||||
|
||||
export enum TokenType {
|
||||
Character,
|
||||
Identifier,
|
||||
Keyword,
|
||||
String,
|
||||
Operator,
|
||||
Number,
|
||||
Error
|
||||
}
|
||||
|
||||
const KEYWORDS = ['var', 'let', 'null', 'undefined', 'true', 'false', 'if', 'else', 'this'];
|
||||
|
||||
@CompilerInjectable()
|
||||
export class Lexer {
|
||||
tokenize(text: string): Token[] {
|
||||
const scanner = new _Scanner(text);
|
||||
const tokens: Token[] = [];
|
||||
let token = scanner.scanToken();
|
||||
while (token != null) {
|
||||
tokens.push(token);
|
||||
token = scanner.scanToken();
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
}
|
||||
|
||||
export class Token {
|
||||
constructor(
|
||||
public index: number, public type: TokenType, public numValue: number,
|
||||
public strValue: string) {}
|
||||
|
||||
isCharacter(code: number): boolean {
|
||||
return this.type == TokenType.Character && this.numValue == code;
|
||||
}
|
||||
|
||||
isNumber(): boolean { return this.type == TokenType.Number; }
|
||||
|
||||
isString(): boolean { return this.type == TokenType.String; }
|
||||
|
||||
isOperator(operater: string): boolean {
|
||||
return this.type == TokenType.Operator && this.strValue == operater;
|
||||
}
|
||||
|
||||
isIdentifier(): boolean { return this.type == TokenType.Identifier; }
|
||||
|
||||
isKeyword(): boolean { return this.type == TokenType.Keyword; }
|
||||
|
||||
isKeywordLet(): boolean { return this.type == TokenType.Keyword && this.strValue == 'let'; }
|
||||
|
||||
isKeywordNull(): boolean { return this.type == TokenType.Keyword && this.strValue == 'null'; }
|
||||
|
||||
isKeywordUndefined(): boolean {
|
||||
return this.type == TokenType.Keyword && this.strValue == 'undefined';
|
||||
}
|
||||
|
||||
isKeywordTrue(): boolean { return this.type == TokenType.Keyword && this.strValue == 'true'; }
|
||||
|
||||
isKeywordFalse(): boolean { return this.type == TokenType.Keyword && this.strValue == 'false'; }
|
||||
|
||||
isKeywordThis(): boolean { return this.type == TokenType.Keyword && this.strValue == 'this'; }
|
||||
|
||||
isError(): boolean { return this.type == TokenType.Error; }
|
||||
|
||||
toNumber(): number { return this.type == TokenType.Number ? this.numValue : -1; }
|
||||
|
||||
toString(): string {
|
||||
switch (this.type) {
|
||||
case TokenType.Character:
|
||||
case TokenType.Identifier:
|
||||
case TokenType.Keyword:
|
||||
case TokenType.Operator:
|
||||
case TokenType.String:
|
||||
case TokenType.Error:
|
||||
return this.strValue;
|
||||
case TokenType.Number:
|
||||
return this.numValue.toString();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function newCharacterToken(index: number, code: number): Token {
|
||||
return new Token(index, TokenType.Character, code, String.fromCharCode(code));
|
||||
}
|
||||
|
||||
function newIdentifierToken(index: number, text: string): Token {
|
||||
return new Token(index, TokenType.Identifier, 0, text);
|
||||
}
|
||||
|
||||
function newKeywordToken(index: number, text: string): Token {
|
||||
return new Token(index, TokenType.Keyword, 0, text);
|
||||
}
|
||||
|
||||
function newOperatorToken(index: number, text: string): Token {
|
||||
return new Token(index, TokenType.Operator, 0, text);
|
||||
}
|
||||
|
||||
function newStringToken(index: number, text: string): Token {
|
||||
return new Token(index, TokenType.String, 0, text);
|
||||
}
|
||||
|
||||
function newNumberToken(index: number, n: number): Token {
|
||||
return new Token(index, TokenType.Number, n, '');
|
||||
}
|
||||
|
||||
function newErrorToken(index: number, message: string): Token {
|
||||
return new Token(index, TokenType.Error, 0, message);
|
||||
}
|
||||
|
||||
export const EOF: Token = new Token(-1, TokenType.Character, 0, '');
|
||||
|
||||
class _Scanner {
|
||||
length: number;
|
||||
peek: number = 0;
|
||||
index: number = -1;
|
||||
|
||||
constructor(public input: string) {
|
||||
this.length = input.length;
|
||||
this.advance();
|
||||
}
|
||||
|
||||
advance() {
|
||||
this.peek = ++this.index >= this.length ? chars.$EOF : this.input.charCodeAt(this.index);
|
||||
}
|
||||
|
||||
scanToken(): Token {
|
||||
const input = this.input, length = this.length;
|
||||
let peek = this.peek, index = this.index;
|
||||
|
||||
// Skip whitespace.
|
||||
while (peek <= chars.$SPACE) {
|
||||
if (++index >= length) {
|
||||
peek = chars.$EOF;
|
||||
break;
|
||||
} else {
|
||||
peek = input.charCodeAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
this.peek = peek;
|
||||
this.index = index;
|
||||
|
||||
if (index >= length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle identifiers and numbers.
|
||||
if (isIdentifierStart(peek)) return this.scanIdentifier();
|
||||
if (chars.isDigit(peek)) return this.scanNumber(index);
|
||||
|
||||
const start: number = index;
|
||||
switch (peek) {
|
||||
case chars.$PERIOD:
|
||||
this.advance();
|
||||
return chars.isDigit(this.peek) ? this.scanNumber(start) :
|
||||
newCharacterToken(start, chars.$PERIOD);
|
||||
case chars.$LPAREN:
|
||||
case chars.$RPAREN:
|
||||
case chars.$LBRACE:
|
||||
case chars.$RBRACE:
|
||||
case chars.$LBRACKET:
|
||||
case chars.$RBRACKET:
|
||||
case chars.$COMMA:
|
||||
case chars.$COLON:
|
||||
case chars.$SEMICOLON:
|
||||
return this.scanCharacter(start, peek);
|
||||
case chars.$SQ:
|
||||
case chars.$DQ:
|
||||
return this.scanString();
|
||||
case chars.$HASH:
|
||||
case chars.$PLUS:
|
||||
case chars.$MINUS:
|
||||
case chars.$STAR:
|
||||
case chars.$SLASH:
|
||||
case chars.$PERCENT:
|
||||
case chars.$CARET:
|
||||
return this.scanOperator(start, String.fromCharCode(peek));
|
||||
case chars.$QUESTION:
|
||||
return this.scanComplexOperator(start, '?', chars.$PERIOD, '.');
|
||||
case chars.$LT:
|
||||
case chars.$GT:
|
||||
return this.scanComplexOperator(start, String.fromCharCode(peek), chars.$EQ, '=');
|
||||
case chars.$BANG:
|
||||
case chars.$EQ:
|
||||
return this.scanComplexOperator(
|
||||
start, String.fromCharCode(peek), chars.$EQ, '=', chars.$EQ, '=');
|
||||
case chars.$AMPERSAND:
|
||||
return this.scanComplexOperator(start, '&', chars.$AMPERSAND, '&');
|
||||
case chars.$BAR:
|
||||
return this.scanComplexOperator(start, '|', chars.$BAR, '|');
|
||||
case chars.$NBSP:
|
||||
while (chars.isWhitespace(this.peek)) this.advance();
|
||||
return this.scanToken();
|
||||
}
|
||||
|
||||
this.advance();
|
||||
return this.error(`Unexpected character [${String.fromCharCode(peek)}]`, 0);
|
||||
}
|
||||
|
||||
scanCharacter(start: number, code: number): Token {
|
||||
this.advance();
|
||||
return newCharacterToken(start, code);
|
||||
}
|
||||
|
||||
|
||||
scanOperator(start: number, str: string): Token {
|
||||
this.advance();
|
||||
return newOperatorToken(start, str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tokenize a 2/3 char long operator
|
||||
*
|
||||
* @param start start index in the expression
|
||||
* @param one first symbol (always part of the operator)
|
||||
* @param twoCode code point for the second symbol
|
||||
* @param two second symbol (part of the operator when the second code point matches)
|
||||
* @param threeCode code point for the third symbol
|
||||
* @param three third symbol (part of the operator when provided and matches source expression)
|
||||
* @returns {Token}
|
||||
*/
|
||||
scanComplexOperator(
|
||||
start: number, one: string, twoCode: number, two: string, threeCode?: number,
|
||||
three?: string): Token {
|
||||
this.advance();
|
||||
let str: string = one;
|
||||
if (this.peek == twoCode) {
|
||||
this.advance();
|
||||
str += two;
|
||||
}
|
||||
if (threeCode != null && this.peek == threeCode) {
|
||||
this.advance();
|
||||
str += three;
|
||||
}
|
||||
return newOperatorToken(start, str);
|
||||
}
|
||||
|
||||
scanIdentifier(): Token {
|
||||
const start: number = this.index;
|
||||
this.advance();
|
||||
while (isIdentifierPart(this.peek)) this.advance();
|
||||
const str: string = this.input.substring(start, this.index);
|
||||
return KEYWORDS.indexOf(str) > -1 ? newKeywordToken(start, str) :
|
||||
newIdentifierToken(start, str);
|
||||
}
|
||||
|
||||
scanNumber(start: number): Token {
|
||||
let simple: boolean = (this.index === start);
|
||||
this.advance(); // Skip initial digit.
|
||||
while (true) {
|
||||
if (chars.isDigit(this.peek)) {
|
||||
// Do nothing.
|
||||
} else if (this.peek == chars.$PERIOD) {
|
||||
simple = false;
|
||||
} else if (isExponentStart(this.peek)) {
|
||||
this.advance();
|
||||
if (isExponentSign(this.peek)) this.advance();
|
||||
if (!chars.isDigit(this.peek)) return this.error('Invalid exponent', -1);
|
||||
simple = false;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
this.advance();
|
||||
}
|
||||
const str: string = this.input.substring(start, this.index);
|
||||
const value: number = simple ? parseIntAutoRadix(str) : parseFloat(str);
|
||||
return newNumberToken(start, value);
|
||||
}
|
||||
|
||||
scanString(): Token {
|
||||
const start: number = this.index;
|
||||
const quote: number = this.peek;
|
||||
this.advance(); // Skip initial quote.
|
||||
|
||||
let buffer: string = '';
|
||||
let marker: number = this.index;
|
||||
const input: string = this.input;
|
||||
|
||||
while (this.peek != quote) {
|
||||
if (this.peek == chars.$BACKSLASH) {
|
||||
buffer += input.substring(marker, this.index);
|
||||
this.advance();
|
||||
let unescapedCode: number;
|
||||
// Workaround for TS2.1-introduced type strictness
|
||||
this.peek = this.peek;
|
||||
if (this.peek == chars.$u) {
|
||||
// 4 character hex code for unicode character.
|
||||
const hex: string = input.substring(this.index + 1, this.index + 5);
|
||||
if (/^[0-9a-f]+$/i.test(hex)) {
|
||||
unescapedCode = parseInt(hex, 16);
|
||||
} else {
|
||||
return this.error(`Invalid unicode escape [\\u${hex}]`, 0);
|
||||
}
|
||||
for (let i: number = 0; i < 5; i++) {
|
||||
this.advance();
|
||||
}
|
||||
} else {
|
||||
unescapedCode = unescape(this.peek);
|
||||
this.advance();
|
||||
}
|
||||
buffer += String.fromCharCode(unescapedCode);
|
||||
marker = this.index;
|
||||
} else if (this.peek == chars.$EOF) {
|
||||
return this.error('Unterminated quote', 0);
|
||||
} else {
|
||||
this.advance();
|
||||
}
|
||||
}
|
||||
|
||||
const last: string = input.substring(marker, this.index);
|
||||
this.advance(); // Skip terminating quote.
|
||||
|
||||
return newStringToken(start, buffer + last);
|
||||
}
|
||||
|
||||
error(message: string, offset: number): Token {
|
||||
const position: number = this.index + offset;
|
||||
return newErrorToken(
|
||||
position, `Lexer Error: ${message} at column ${position} in expression [${this.input}]`);
|
||||
}
|
||||
}
|
||||
|
||||
function isIdentifierStart(code: number): boolean {
|
||||
return (chars.$a <= code && code <= chars.$z) || (chars.$A <= code && code <= chars.$Z) ||
|
||||
(code == chars.$_) || (code == chars.$$);
|
||||
}
|
||||
|
||||
export function isIdentifier(input: string): boolean {
|
||||
if (input.length == 0) return false;
|
||||
const scanner = new _Scanner(input);
|
||||
if (!isIdentifierStart(scanner.peek)) return false;
|
||||
scanner.advance();
|
||||
while (scanner.peek !== chars.$EOF) {
|
||||
if (!isIdentifierPart(scanner.peek)) return false;
|
||||
scanner.advance();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isIdentifierPart(code: number): boolean {
|
||||
return chars.isAsciiLetter(code) || chars.isDigit(code) || (code == chars.$_) ||
|
||||
(code == chars.$$);
|
||||
}
|
||||
|
||||
function isExponentStart(code: number): boolean {
|
||||
return code == chars.$e || code == chars.$E;
|
||||
}
|
||||
|
||||
function isExponentSign(code: number): boolean {
|
||||
return code == chars.$MINUS || code == chars.$PLUS;
|
||||
}
|
||||
|
||||
export function isQuote(code: number): boolean {
|
||||
return code === chars.$SQ || code === chars.$DQ || code === chars.$BT;
|
||||
}
|
||||
|
||||
function unescape(code: number): number {
|
||||
switch (code) {
|
||||
case chars.$n:
|
||||
return chars.$LF;
|
||||
case chars.$f:
|
||||
return chars.$FF;
|
||||
case chars.$r:
|
||||
return chars.$CR;
|
||||
case chars.$t:
|
||||
return chars.$TAB;
|
||||
case chars.$v:
|
||||
return chars.$VTAB;
|
||||
default:
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
function parseIntAutoRadix(text: string): number {
|
||||
const result: number = parseInt(text);
|
||||
if (isNaN(result)) {
|
||||
throw new Error('Invalid integer literal when parsing ' + text);
|
||||
}
|
||||
return result;
|
||||
}
|
812
packages/compiler/src/expression_parser/parser.ts
Normal file
812
packages/compiler/src/expression_parser/parser.ts
Normal file
@ -0,0 +1,812 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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 * as chars from '../chars';
|
||||
import {CompilerInjectable} from '../injectable';
|
||||
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config';
|
||||
import {escapeRegExp} from '../util';
|
||||
import {AST, ASTWithSource, AstVisitor, Binary, BindingPipe, Chain, Conditional, EmptyExpr, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, ParseSpan, ParserError, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead, TemplateBinding} from './ast';
|
||||
import {EOF, Lexer, Token, TokenType, isIdentifier, isQuote} from './lexer';
|
||||
|
||||
|
||||
export class SplitInterpolation {
|
||||
constructor(public strings: string[], public expressions: string[], public offsets: number[]) {}
|
||||
}
|
||||
|
||||
export class TemplateBindingParseResult {
|
||||
constructor(
|
||||
public templateBindings: TemplateBinding[], public warnings: string[],
|
||||
public errors: ParserError[]) {}
|
||||
}
|
||||
|
||||
function _createInterpolateRegExp(config: InterpolationConfig): RegExp {
|
||||
const pattern = escapeRegExp(config.start) + '([\\s\\S]*?)' + escapeRegExp(config.end);
|
||||
return new RegExp(pattern, 'g');
|
||||
}
|
||||
|
||||
@CompilerInjectable()
|
||||
export class Parser {
|
||||
private errors: ParserError[] = [];
|
||||
|
||||
constructor(private _lexer: Lexer) {}
|
||||
|
||||
parseAction(
|
||||
input: string, location: any,
|
||||
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
|
||||
this._checkNoInterpolation(input, location, interpolationConfig);
|
||||
const sourceToLex = this._stripComments(input);
|
||||
const tokens = this._lexer.tokenize(this._stripComments(input));
|
||||
const ast = new _ParseAST(
|
||||
input, location, tokens, sourceToLex.length, true, this.errors,
|
||||
input.length - sourceToLex.length)
|
||||
.parseChain();
|
||||
return new ASTWithSource(ast, input, location, this.errors);
|
||||
}
|
||||
|
||||
parseBinding(
|
||||
input: string, location: any,
|
||||
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
|
||||
const ast = this._parseBindingAst(input, location, interpolationConfig);
|
||||
return new ASTWithSource(ast, input, location, this.errors);
|
||||
}
|
||||
|
||||
parseSimpleBinding(
|
||||
input: string, location: string,
|
||||
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
|
||||
const ast = this._parseBindingAst(input, location, interpolationConfig);
|
||||
const errors = SimpleExpressionChecker.check(ast);
|
||||
if (errors.length > 0) {
|
||||
this._reportError(
|
||||
`Host binding expression cannot contain ${errors.join(' ')}`, input, location);
|
||||
}
|
||||
return new ASTWithSource(ast, input, location, this.errors);
|
||||
}
|
||||
|
||||
private _reportError(message: string, input: string, errLocation: string, ctxLocation?: any) {
|
||||
this.errors.push(new ParserError(message, input, errLocation, ctxLocation));
|
||||
}
|
||||
|
||||
private _parseBindingAst(
|
||||
input: string, location: string, interpolationConfig: InterpolationConfig): AST {
|
||||
// Quotes expressions use 3rd-party expression language. We don't want to use
|
||||
// our lexer or parser for that, so we check for that ahead of time.
|
||||
const quote = this._parseQuote(input, location);
|
||||
|
||||
if (quote != null) {
|
||||
return quote;
|
||||
}
|
||||
|
||||
this._checkNoInterpolation(input, location, interpolationConfig);
|
||||
const sourceToLex = this._stripComments(input);
|
||||
const tokens = this._lexer.tokenize(sourceToLex);
|
||||
return new _ParseAST(
|
||||
input, location, tokens, sourceToLex.length, false, this.errors,
|
||||
input.length - sourceToLex.length)
|
||||
.parseChain();
|
||||
}
|
||||
|
||||
private _parseQuote(input: string, location: any): AST {
|
||||
if (input == null) return null;
|
||||
const prefixSeparatorIndex = input.indexOf(':');
|
||||
if (prefixSeparatorIndex == -1) return null;
|
||||
const prefix = input.substring(0, prefixSeparatorIndex).trim();
|
||||
if (!isIdentifier(prefix)) return null;
|
||||
const uninterpretedExpression = input.substring(prefixSeparatorIndex + 1);
|
||||
return new Quote(new ParseSpan(0, input.length), prefix, uninterpretedExpression, location);
|
||||
}
|
||||
|
||||
parseTemplateBindings(prefixToken: string, input: string, location: any):
|
||||
TemplateBindingParseResult {
|
||||
const tokens = this._lexer.tokenize(input);
|
||||
if (prefixToken) {
|
||||
// Prefix the tokens with the tokens from prefixToken but have them take no space (0 index).
|
||||
const prefixTokens = this._lexer.tokenize(prefixToken).map(t => {
|
||||
t.index = 0;
|
||||
return t;
|
||||
});
|
||||
tokens.unshift(...prefixTokens);
|
||||
}
|
||||
return new _ParseAST(input, location, tokens, input.length, false, this.errors, 0)
|
||||
.parseTemplateBindings();
|
||||
}
|
||||
|
||||
parseInterpolation(
|
||||
input: string, location: any,
|
||||
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
|
||||
const split = this.splitInterpolation(input, location, interpolationConfig);
|
||||
if (split == null) return null;
|
||||
|
||||
const expressions: AST[] = [];
|
||||
|
||||
for (let i = 0; i < split.expressions.length; ++i) {
|
||||
const expressionText = split.expressions[i];
|
||||
const sourceToLex = this._stripComments(expressionText);
|
||||
const tokens = this._lexer.tokenize(this._stripComments(split.expressions[i]));
|
||||
const ast = new _ParseAST(
|
||||
input, location, tokens, sourceToLex.length, false, this.errors,
|
||||
split.offsets[i] + (expressionText.length - sourceToLex.length))
|
||||
.parseChain();
|
||||
expressions.push(ast);
|
||||
}
|
||||
|
||||
return new ASTWithSource(
|
||||
new Interpolation(
|
||||
new ParseSpan(0, input == null ? 0 : input.length), split.strings, expressions),
|
||||
input, location, this.errors);
|
||||
}
|
||||
|
||||
splitInterpolation(
|
||||
input: string, location: string,
|
||||
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): SplitInterpolation {
|
||||
const regexp = _createInterpolateRegExp(interpolationConfig);
|
||||
const parts = input.split(regexp);
|
||||
if (parts.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
const strings: string[] = [];
|
||||
const expressions: string[] = [];
|
||||
const offsets: number[] = [];
|
||||
let offset = 0;
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part: string = parts[i];
|
||||
if (i % 2 === 0) {
|
||||
// fixed string
|
||||
strings.push(part);
|
||||
offset += part.length;
|
||||
} else if (part.trim().length > 0) {
|
||||
offset += interpolationConfig.start.length;
|
||||
expressions.push(part);
|
||||
offsets.push(offset);
|
||||
offset += part.length + interpolationConfig.end.length;
|
||||
} else {
|
||||
this._reportError(
|
||||
'Blank expressions are not allowed in interpolated strings', input,
|
||||
`at column ${this._findInterpolationErrorColumn(parts, i, interpolationConfig)} in`,
|
||||
location);
|
||||
expressions.push('$implict');
|
||||
offsets.push(offset);
|
||||
}
|
||||
}
|
||||
return new SplitInterpolation(strings, expressions, offsets);
|
||||
}
|
||||
|
||||
wrapLiteralPrimitive(input: string, location: any): ASTWithSource {
|
||||
return new ASTWithSource(
|
||||
new LiteralPrimitive(new ParseSpan(0, input == null ? 0 : input.length), input), input,
|
||||
location, this.errors);
|
||||
}
|
||||
|
||||
private _stripComments(input: string): string {
|
||||
const i = this._commentStart(input);
|
||||
return i != null ? input.substring(0, i).trim() : input;
|
||||
}
|
||||
|
||||
private _commentStart(input: string): number {
|
||||
let outerQuote: number = null;
|
||||
for (let i = 0; i < input.length - 1; i++) {
|
||||
const char = input.charCodeAt(i);
|
||||
const nextChar = input.charCodeAt(i + 1);
|
||||
|
||||
if (char === chars.$SLASH && nextChar == chars.$SLASH && outerQuote == null) return i;
|
||||
|
||||
if (outerQuote === char) {
|
||||
outerQuote = null;
|
||||
} else if (outerQuote == null && isQuote(char)) {
|
||||
outerQuote = char;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private _checkNoInterpolation(
|
||||
input: string, location: any, interpolationConfig: InterpolationConfig): void {
|
||||
const regexp = _createInterpolateRegExp(interpolationConfig);
|
||||
const parts = input.split(regexp);
|
||||
if (parts.length > 1) {
|
||||
this._reportError(
|
||||
`Got interpolation (${interpolationConfig.start}${interpolationConfig.end}) where expression was expected`,
|
||||
input,
|
||||
`at column ${this._findInterpolationErrorColumn(parts, 1, interpolationConfig)} in`,
|
||||
location);
|
||||
}
|
||||
}
|
||||
|
||||
private _findInterpolationErrorColumn(
|
||||
parts: string[], partInErrIdx: number, interpolationConfig: InterpolationConfig): number {
|
||||
let errLocation = '';
|
||||
for (let j = 0; j < partInErrIdx; j++) {
|
||||
errLocation += j % 2 === 0 ?
|
||||
parts[j] :
|
||||
`${interpolationConfig.start}${parts[j]}${interpolationConfig.end}`;
|
||||
}
|
||||
|
||||
return errLocation.length;
|
||||
}
|
||||
}
|
||||
|
||||
export class _ParseAST {
|
||||
private rparensExpected = 0;
|
||||
private rbracketsExpected = 0;
|
||||
private rbracesExpected = 0;
|
||||
|
||||
index: number = 0;
|
||||
|
||||
constructor(
|
||||
public input: string, public location: any, public tokens: Token[],
|
||||
public inputLength: number, public parseAction: boolean, private errors: ParserError[],
|
||||
private offset: number) {}
|
||||
|
||||
peek(offset: number): Token {
|
||||
const i = this.index + offset;
|
||||
return i < this.tokens.length ? this.tokens[i] : EOF;
|
||||
}
|
||||
|
||||
get next(): Token { return this.peek(0); }
|
||||
|
||||
get inputIndex(): number {
|
||||
return (this.index < this.tokens.length) ? this.next.index + this.offset :
|
||||
this.inputLength + this.offset;
|
||||
}
|
||||
|
||||
span(start: number) { return new ParseSpan(start, this.inputIndex); }
|
||||
|
||||
advance() { this.index++; }
|
||||
|
||||
optionalCharacter(code: number): boolean {
|
||||
if (this.next.isCharacter(code)) {
|
||||
this.advance();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
peekKeywordLet(): boolean { return this.next.isKeywordLet(); }
|
||||
|
||||
expectCharacter(code: number) {
|
||||
if (this.optionalCharacter(code)) return;
|
||||
this.error(`Missing expected ${String.fromCharCode(code)}`);
|
||||
}
|
||||
|
||||
optionalOperator(op: string): boolean {
|
||||
if (this.next.isOperator(op)) {
|
||||
this.advance();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
expectOperator(operator: string) {
|
||||
if (this.optionalOperator(operator)) return;
|
||||
this.error(`Missing expected operator ${operator}`);
|
||||
}
|
||||
|
||||
expectIdentifierOrKeyword(): string {
|
||||
const n = this.next;
|
||||
if (!n.isIdentifier() && !n.isKeyword()) {
|
||||
this.error(`Unexpected token ${n}, expected identifier or keyword`);
|
||||
return '';
|
||||
}
|
||||
this.advance();
|
||||
return n.toString();
|
||||
}
|
||||
|
||||
expectIdentifierOrKeywordOrString(): string {
|
||||
const n = this.next;
|
||||
if (!n.isIdentifier() && !n.isKeyword() && !n.isString()) {
|
||||
this.error(`Unexpected token ${n}, expected identifier, keyword, or string`);
|
||||
return '';
|
||||
}
|
||||
this.advance();
|
||||
return n.toString();
|
||||
}
|
||||
|
||||
parseChain(): AST {
|
||||
const exprs: AST[] = [];
|
||||
const start = this.inputIndex;
|
||||
while (this.index < this.tokens.length) {
|
||||
const expr = this.parsePipe();
|
||||
exprs.push(expr);
|
||||
|
||||
if (this.optionalCharacter(chars.$SEMICOLON)) {
|
||||
if (!this.parseAction) {
|
||||
this.error('Binding expression cannot contain chained expression');
|
||||
}
|
||||
while (this.optionalCharacter(chars.$SEMICOLON)) {
|
||||
} // read all semicolons
|
||||
} else if (this.index < this.tokens.length) {
|
||||
this.error(`Unexpected token '${this.next}'`);
|
||||
}
|
||||
}
|
||||
if (exprs.length == 0) return new EmptyExpr(this.span(start));
|
||||
if (exprs.length == 1) return exprs[0];
|
||||
return new Chain(this.span(start), exprs);
|
||||
}
|
||||
|
||||
parsePipe(): AST {
|
||||
let result = this.parseExpression();
|
||||
if (this.optionalOperator('|')) {
|
||||
if (this.parseAction) {
|
||||
this.error('Cannot have a pipe in an action expression');
|
||||
}
|
||||
|
||||
do {
|
||||
const name = this.expectIdentifierOrKeyword();
|
||||
const args: AST[] = [];
|
||||
while (this.optionalCharacter(chars.$COLON)) {
|
||||
args.push(this.parseExpression());
|
||||
}
|
||||
result = new BindingPipe(this.span(result.span.start), result, name, args);
|
||||
} while (this.optionalOperator('|'));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
parseExpression(): AST { return this.parseConditional(); }
|
||||
|
||||
parseConditional(): AST {
|
||||
const start = this.inputIndex;
|
||||
const result = this.parseLogicalOr();
|
||||
|
||||
if (this.optionalOperator('?')) {
|
||||
const yes = this.parsePipe();
|
||||
let no: AST;
|
||||
if (!this.optionalCharacter(chars.$COLON)) {
|
||||
const end = this.inputIndex;
|
||||
const expression = this.input.substring(start, end);
|
||||
this.error(`Conditional expression ${expression} requires all 3 expressions`);
|
||||
no = new EmptyExpr(this.span(start));
|
||||
} else {
|
||||
no = this.parsePipe();
|
||||
}
|
||||
return new Conditional(this.span(start), result, yes, no);
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
parseLogicalOr(): AST {
|
||||
// '||'
|
||||
let result = this.parseLogicalAnd();
|
||||
while (this.optionalOperator('||')) {
|
||||
const right = this.parseLogicalAnd();
|
||||
result = new Binary(this.span(result.span.start), '||', result, right);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
parseLogicalAnd(): AST {
|
||||
// '&&'
|
||||
let result = this.parseEquality();
|
||||
while (this.optionalOperator('&&')) {
|
||||
const right = this.parseEquality();
|
||||
result = new Binary(this.span(result.span.start), '&&', result, right);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
parseEquality(): AST {
|
||||
// '==','!=','===','!=='
|
||||
let result = this.parseRelational();
|
||||
while (this.next.type == TokenType.Operator) {
|
||||
const operator = this.next.strValue;
|
||||
switch (operator) {
|
||||
case '==':
|
||||
case '===':
|
||||
case '!=':
|
||||
case '!==':
|
||||
this.advance();
|
||||
const right = this.parseRelational();
|
||||
result = new Binary(this.span(result.span.start), operator, result, right);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
parseRelational(): AST {
|
||||
// '<', '>', '<=', '>='
|
||||
let result = this.parseAdditive();
|
||||
while (this.next.type == TokenType.Operator) {
|
||||
const operator = this.next.strValue;
|
||||
switch (operator) {
|
||||
case '<':
|
||||
case '>':
|
||||
case '<=':
|
||||
case '>=':
|
||||
this.advance();
|
||||
const right = this.parseAdditive();
|
||||
result = new Binary(this.span(result.span.start), operator, result, right);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
parseAdditive(): AST {
|
||||
// '+', '-'
|
||||
let result = this.parseMultiplicative();
|
||||
while (this.next.type == TokenType.Operator) {
|
||||
const operator = this.next.strValue;
|
||||
switch (operator) {
|
||||
case '+':
|
||||
case '-':
|
||||
this.advance();
|
||||
let right = this.parseMultiplicative();
|
||||
result = new Binary(this.span(result.span.start), operator, result, right);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
parseMultiplicative(): AST {
|
||||
// '*', '%', '/'
|
||||
let result = this.parsePrefix();
|
||||
while (this.next.type == TokenType.Operator) {
|
||||
const operator = this.next.strValue;
|
||||
switch (operator) {
|
||||
case '*':
|
||||
case '%':
|
||||
case '/':
|
||||
this.advance();
|
||||
let right = this.parsePrefix();
|
||||
result = new Binary(this.span(result.span.start), operator, result, right);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
parsePrefix(): AST {
|
||||
if (this.next.type == TokenType.Operator) {
|
||||
const start = this.inputIndex;
|
||||
const operator = this.next.strValue;
|
||||
let result: AST;
|
||||
switch (operator) {
|
||||
case '+':
|
||||
this.advance();
|
||||
return this.parsePrefix();
|
||||
case '-':
|
||||
this.advance();
|
||||
result = this.parsePrefix();
|
||||
return new Binary(
|
||||
this.span(start), operator, new LiteralPrimitive(new ParseSpan(start, start), 0),
|
||||
result);
|
||||
case '!':
|
||||
this.advance();
|
||||
result = this.parsePrefix();
|
||||
return new PrefixNot(this.span(start), result);
|
||||
}
|
||||
}
|
||||
return this.parseCallChain();
|
||||
}
|
||||
|
||||
parseCallChain(): AST {
|
||||
let result = this.parsePrimary();
|
||||
while (true) {
|
||||
if (this.optionalCharacter(chars.$PERIOD)) {
|
||||
result = this.parseAccessMemberOrMethodCall(result, false);
|
||||
|
||||
} else if (this.optionalOperator('?.')) {
|
||||
result = this.parseAccessMemberOrMethodCall(result, true);
|
||||
|
||||
} else if (this.optionalCharacter(chars.$LBRACKET)) {
|
||||
this.rbracketsExpected++;
|
||||
const key = this.parsePipe();
|
||||
this.rbracketsExpected--;
|
||||
this.expectCharacter(chars.$RBRACKET);
|
||||
if (this.optionalOperator('=')) {
|
||||
const value = this.parseConditional();
|
||||
result = new KeyedWrite(this.span(result.span.start), result, key, value);
|
||||
} else {
|
||||
result = new KeyedRead(this.span(result.span.start), result, key);
|
||||
}
|
||||
|
||||
} else if (this.optionalCharacter(chars.$LPAREN)) {
|
||||
this.rparensExpected++;
|
||||
const args = this.parseCallArguments();
|
||||
this.rparensExpected--;
|
||||
this.expectCharacter(chars.$RPAREN);
|
||||
result = new FunctionCall(this.span(result.span.start), result, args);
|
||||
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parsePrimary(): AST {
|
||||
const start = this.inputIndex;
|
||||
if (this.optionalCharacter(chars.$LPAREN)) {
|
||||
this.rparensExpected++;
|
||||
const result = this.parsePipe();
|
||||
this.rparensExpected--;
|
||||
this.expectCharacter(chars.$RPAREN);
|
||||
return result;
|
||||
|
||||
} else if (this.next.isKeywordNull()) {
|
||||
this.advance();
|
||||
return new LiteralPrimitive(this.span(start), null);
|
||||
|
||||
} else if (this.next.isKeywordUndefined()) {
|
||||
this.advance();
|
||||
return new LiteralPrimitive(this.span(start), void 0);
|
||||
|
||||
} else if (this.next.isKeywordTrue()) {
|
||||
this.advance();
|
||||
return new LiteralPrimitive(this.span(start), true);
|
||||
|
||||
} else if (this.next.isKeywordFalse()) {
|
||||
this.advance();
|
||||
return new LiteralPrimitive(this.span(start), false);
|
||||
|
||||
} else if (this.next.isKeywordThis()) {
|
||||
this.advance();
|
||||
return new ImplicitReceiver(this.span(start));
|
||||
|
||||
} else if (this.optionalCharacter(chars.$LBRACKET)) {
|
||||
this.rbracketsExpected++;
|
||||
const elements = this.parseExpressionList(chars.$RBRACKET);
|
||||
this.rbracketsExpected--;
|
||||
this.expectCharacter(chars.$RBRACKET);
|
||||
return new LiteralArray(this.span(start), elements);
|
||||
|
||||
} else if (this.next.isCharacter(chars.$LBRACE)) {
|
||||
return this.parseLiteralMap();
|
||||
|
||||
} else if (this.next.isIdentifier()) {
|
||||
return this.parseAccessMemberOrMethodCall(new ImplicitReceiver(this.span(start)), false);
|
||||
|
||||
} else if (this.next.isNumber()) {
|
||||
const value = this.next.toNumber();
|
||||
this.advance();
|
||||
return new LiteralPrimitive(this.span(start), value);
|
||||
|
||||
} else if (this.next.isString()) {
|
||||
const literalValue = this.next.toString();
|
||||
this.advance();
|
||||
return new LiteralPrimitive(this.span(start), literalValue);
|
||||
|
||||
} else if (this.index >= this.tokens.length) {
|
||||
this.error(`Unexpected end of expression: ${this.input}`);
|
||||
return new EmptyExpr(this.span(start));
|
||||
} else {
|
||||
this.error(`Unexpected token ${this.next}`);
|
||||
return new EmptyExpr(this.span(start));
|
||||
}
|
||||
}
|
||||
|
||||
parseExpressionList(terminator: number): AST[] {
|
||||
const result: AST[] = [];
|
||||
if (!this.next.isCharacter(terminator)) {
|
||||
do {
|
||||
result.push(this.parsePipe());
|
||||
} while (this.optionalCharacter(chars.$COMMA));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
parseLiteralMap(): LiteralMap {
|
||||
const keys: string[] = [];
|
||||
const values: AST[] = [];
|
||||
const start = this.inputIndex;
|
||||
this.expectCharacter(chars.$LBRACE);
|
||||
if (!this.optionalCharacter(chars.$RBRACE)) {
|
||||
this.rbracesExpected++;
|
||||
do {
|
||||
const key = this.expectIdentifierOrKeywordOrString();
|
||||
keys.push(key);
|
||||
this.expectCharacter(chars.$COLON);
|
||||
values.push(this.parsePipe());
|
||||
} while (this.optionalCharacter(chars.$COMMA));
|
||||
this.rbracesExpected--;
|
||||
this.expectCharacter(chars.$RBRACE);
|
||||
}
|
||||
return new LiteralMap(this.span(start), keys, values);
|
||||
}
|
||||
|
||||
parseAccessMemberOrMethodCall(receiver: AST, isSafe: boolean = false): AST {
|
||||
const start = receiver.span.start;
|
||||
const id = this.expectIdentifierOrKeyword();
|
||||
|
||||
if (this.optionalCharacter(chars.$LPAREN)) {
|
||||
this.rparensExpected++;
|
||||
const args = this.parseCallArguments();
|
||||
this.expectCharacter(chars.$RPAREN);
|
||||
this.rparensExpected--;
|
||||
const span = this.span(start);
|
||||
return isSafe ? new SafeMethodCall(span, receiver, id, args) :
|
||||
new MethodCall(span, receiver, id, args);
|
||||
|
||||
} else {
|
||||
if (isSafe) {
|
||||
if (this.optionalOperator('=')) {
|
||||
this.error('The \'?.\' operator cannot be used in the assignment');
|
||||
return new EmptyExpr(this.span(start));
|
||||
} else {
|
||||
return new SafePropertyRead(this.span(start), receiver, id);
|
||||
}
|
||||
} else {
|
||||
if (this.optionalOperator('=')) {
|
||||
if (!this.parseAction) {
|
||||
this.error('Bindings cannot contain assignments');
|
||||
return new EmptyExpr(this.span(start));
|
||||
}
|
||||
|
||||
const value = this.parseConditional();
|
||||
return new PropertyWrite(this.span(start), receiver, id, value);
|
||||
} else {
|
||||
return new PropertyRead(this.span(start), receiver, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parseCallArguments(): BindingPipe[] {
|
||||
if (this.next.isCharacter(chars.$RPAREN)) return [];
|
||||
const positionals: AST[] = [];
|
||||
do {
|
||||
positionals.push(this.parsePipe());
|
||||
} while (this.optionalCharacter(chars.$COMMA));
|
||||
return positionals as BindingPipe[];
|
||||
}
|
||||
|
||||
/**
|
||||
* An identifier, a keyword, a string with an optional `-` inbetween.
|
||||
*/
|
||||
expectTemplateBindingKey(): string {
|
||||
let result = '';
|
||||
let operatorFound = false;
|
||||
do {
|
||||
result += this.expectIdentifierOrKeywordOrString();
|
||||
operatorFound = this.optionalOperator('-');
|
||||
if (operatorFound) {
|
||||
result += '-';
|
||||
}
|
||||
} while (operatorFound);
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
parseTemplateBindings(): TemplateBindingParseResult {
|
||||
const bindings: TemplateBinding[] = [];
|
||||
let prefix: string = null;
|
||||
const warnings: string[] = [];
|
||||
while (this.index < this.tokens.length) {
|
||||
const start = this.inputIndex;
|
||||
const keyIsVar: boolean = this.peekKeywordLet();
|
||||
if (keyIsVar) {
|
||||
this.advance();
|
||||
}
|
||||
let key = this.expectTemplateBindingKey();
|
||||
if (!keyIsVar) {
|
||||
if (prefix == null) {
|
||||
prefix = key;
|
||||
} else {
|
||||
key = prefix + key[0].toUpperCase() + key.substring(1);
|
||||
}
|
||||
}
|
||||
this.optionalCharacter(chars.$COLON);
|
||||
let name: string = null;
|
||||
let expression: ASTWithSource = null;
|
||||
if (keyIsVar) {
|
||||
if (this.optionalOperator('=')) {
|
||||
name = this.expectTemplateBindingKey();
|
||||
} else {
|
||||
name = '\$implicit';
|
||||
}
|
||||
} else if (this.next !== EOF && !this.peekKeywordLet()) {
|
||||
const start = this.inputIndex;
|
||||
const ast = this.parsePipe();
|
||||
const source = this.input.substring(start - this.offset, this.inputIndex - this.offset);
|
||||
expression = new ASTWithSource(ast, source, this.location, this.errors);
|
||||
}
|
||||
bindings.push(new TemplateBinding(this.span(start), key, keyIsVar, name, expression));
|
||||
if (!this.optionalCharacter(chars.$SEMICOLON)) {
|
||||
this.optionalCharacter(chars.$COMMA);
|
||||
}
|
||||
}
|
||||
return new TemplateBindingParseResult(bindings, warnings, this.errors);
|
||||
}
|
||||
|
||||
error(message: string, index: number = null) {
|
||||
this.errors.push(new ParserError(message, this.input, this.locationText(index), this.location));
|
||||
this.skip();
|
||||
}
|
||||
|
||||
private locationText(index: number = null) {
|
||||
if (index == null) index = this.index;
|
||||
return (index < this.tokens.length) ? `at column ${this.tokens[index].index + 1} in` :
|
||||
`at the end of the expression`;
|
||||
}
|
||||
|
||||
// Error recovery should skip tokens until it encounters a recovery point. skip() treats
|
||||
// the end of input and a ';' as unconditionally a recovery point. It also treats ')',
|
||||
// '}' and ']' as conditional recovery points if one of calling productions is expecting
|
||||
// one of these symbols. This allows skip() to recover from errors such as '(a.) + 1' allowing
|
||||
// more of the AST to be retained (it doesn't skip any tokens as the ')' is retained because
|
||||
// of the '(' begins an '(' <expr> ')' production). The recovery points of grouping symbols
|
||||
// must be conditional as they must be skipped if none of the calling productions are not
|
||||
// expecting the closing token else we will never make progress in the case of an
|
||||
// extraneous group closing symbol (such as a stray ')'). This is not the case for ';' because
|
||||
// parseChain() is always the root production and it expects a ';'.
|
||||
|
||||
// If a production expects one of these token it increments the corresponding nesting count,
|
||||
// and then decrements it just prior to checking if the token is in the input.
|
||||
private skip() {
|
||||
let n = this.next;
|
||||
while (this.index < this.tokens.length && !n.isCharacter(chars.$SEMICOLON) &&
|
||||
(this.rparensExpected <= 0 || !n.isCharacter(chars.$RPAREN)) &&
|
||||
(this.rbracesExpected <= 0 || !n.isCharacter(chars.$RBRACE)) &&
|
||||
(this.rbracketsExpected <= 0 || !n.isCharacter(chars.$RBRACKET))) {
|
||||
if (this.next.isError()) {
|
||||
this.errors.push(
|
||||
new ParserError(this.next.toString(), this.input, this.locationText(), this.location));
|
||||
}
|
||||
this.advance();
|
||||
n = this.next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleExpressionChecker implements AstVisitor {
|
||||
static check(ast: AST): string[] {
|
||||
const s = new SimpleExpressionChecker();
|
||||
ast.visit(s);
|
||||
return s.errors;
|
||||
}
|
||||
|
||||
errors: string[] = [];
|
||||
|
||||
visitImplicitReceiver(ast: ImplicitReceiver, context: any) {}
|
||||
|
||||
visitInterpolation(ast: Interpolation, context: any) {}
|
||||
|
||||
visitLiteralPrimitive(ast: LiteralPrimitive, context: any) {}
|
||||
|
||||
visitPropertyRead(ast: PropertyRead, context: any) {}
|
||||
|
||||
visitPropertyWrite(ast: PropertyWrite, context: any) {}
|
||||
|
||||
visitSafePropertyRead(ast: SafePropertyRead, context: any) {}
|
||||
|
||||
visitMethodCall(ast: MethodCall, context: any) {}
|
||||
|
||||
visitSafeMethodCall(ast: SafeMethodCall, context: any) {}
|
||||
|
||||
visitFunctionCall(ast: FunctionCall, context: any) {}
|
||||
|
||||
visitLiteralArray(ast: LiteralArray, context: any) { this.visitAll(ast.expressions); }
|
||||
|
||||
visitLiteralMap(ast: LiteralMap, context: any) { this.visitAll(ast.values); }
|
||||
|
||||
visitBinary(ast: Binary, context: any) {}
|
||||
|
||||
visitPrefixNot(ast: PrefixNot, context: any) {}
|
||||
|
||||
visitConditional(ast: Conditional, context: any) {}
|
||||
|
||||
visitPipe(ast: BindingPipe, context: any) { this.errors.push('pipes'); }
|
||||
|
||||
visitKeyedRead(ast: KeyedRead, context: any) {}
|
||||
|
||||
visitKeyedWrite(ast: KeyedWrite, context: any) {}
|
||||
|
||||
visitAll(asts: any[]): any[] { return asts.map(node => node.visit(this)); }
|
||||
|
||||
visitChain(ast: Chain, context: any) {}
|
||||
|
||||
visitQuote(ast: Quote, context: any) {}
|
||||
}
|
Reference in New Issue
Block a user