feat(compiler): Expression span information and error correction (#9772)
Added error correction so the parser always returns an AST Added span information to the expression parser Refactored the test to account for the difference in error reporting Added tests for error corretion Modified tests to validate the span information
This commit is contained in:
parent
ae62f082fd
commit
9a04fcd061
|
@ -7,8 +7,22 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ListWrapper} from '../facade/collection';
|
import {ListWrapper} from '../facade/collection';
|
||||||
|
import {isBlank} from '../facade/lang';
|
||||||
|
|
||||||
|
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 {
|
export class AST {
|
||||||
|
constructor(public span: ParseSpan) {}
|
||||||
visit(visitor: AstVisitor, context: any = null): any { return null; }
|
visit(visitor: AstVisitor, context: any = null): any { return null; }
|
||||||
toString(): string { return 'AST'; }
|
toString(): string { return 'AST'; }
|
||||||
}
|
}
|
||||||
|
@ -27,8 +41,10 @@ export class AST {
|
||||||
* therefore not interpreted by the Angular's own expression parser.
|
* therefore not interpreted by the Angular's own expression parser.
|
||||||
*/
|
*/
|
||||||
export class Quote extends AST {
|
export class Quote extends AST {
|
||||||
constructor(public prefix: string, public uninterpretedExpression: string, public location: any) {
|
constructor(
|
||||||
super();
|
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); }
|
visit(visitor: AstVisitor, context: any = null): any { return visitor.visitQuote(this, context); }
|
||||||
toString(): string { return 'Quote'; }
|
toString(): string { return 'Quote'; }
|
||||||
|
@ -50,122 +66,138 @@ export class ImplicitReceiver extends AST {
|
||||||
* Multiple expressions separated by a semicolon.
|
* Multiple expressions separated by a semicolon.
|
||||||
*/
|
*/
|
||||||
export class Chain extends AST {
|
export class Chain extends AST {
|
||||||
constructor(public expressions: any[]) { super(); }
|
constructor(span: ParseSpan, public expressions: any[]) { super(span); }
|
||||||
visit(visitor: AstVisitor, context: any = null): any { return visitor.visitChain(this, context); }
|
visit(visitor: AstVisitor, context: any = null): any { return visitor.visitChain(this, context); }
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Conditional extends AST {
|
export class Conditional extends AST {
|
||||||
constructor(public condition: AST, public trueExp: AST, public falseExp: AST) { super(); }
|
constructor(span: ParseSpan, public condition: AST, public trueExp: AST, public falseExp: AST) {
|
||||||
|
super(span);
|
||||||
|
}
|
||||||
visit(visitor: AstVisitor, context: any = null): any {
|
visit(visitor: AstVisitor, context: any = null): any {
|
||||||
return visitor.visitConditional(this, context);
|
return visitor.visitConditional(this, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PropertyRead extends AST {
|
export class PropertyRead extends AST {
|
||||||
constructor(public receiver: AST, public name: string) { super(); }
|
constructor(span: ParseSpan, public receiver: AST, public name: string) { super(span); }
|
||||||
visit(visitor: AstVisitor, context: any = null): any {
|
visit(visitor: AstVisitor, context: any = null): any {
|
||||||
return visitor.visitPropertyRead(this, context);
|
return visitor.visitPropertyRead(this, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PropertyWrite extends AST {
|
export class PropertyWrite extends AST {
|
||||||
constructor(public receiver: AST, public name: string, public value: AST) { super(); }
|
constructor(span: ParseSpan, public receiver: AST, public name: string, public value: AST) {
|
||||||
|
super(span);
|
||||||
|
}
|
||||||
visit(visitor: AstVisitor, context: any = null): any {
|
visit(visitor: AstVisitor, context: any = null): any {
|
||||||
return visitor.visitPropertyWrite(this, context);
|
return visitor.visitPropertyWrite(this, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SafePropertyRead extends AST {
|
export class SafePropertyRead extends AST {
|
||||||
constructor(public receiver: AST, public name: string) { super(); }
|
constructor(span: ParseSpan, public receiver: AST, public name: string) { super(span); }
|
||||||
visit(visitor: AstVisitor, context: any = null): any {
|
visit(visitor: AstVisitor, context: any = null): any {
|
||||||
return visitor.visitSafePropertyRead(this, context);
|
return visitor.visitSafePropertyRead(this, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class KeyedRead extends AST {
|
export class KeyedRead extends AST {
|
||||||
constructor(public obj: AST, public key: AST) { super(); }
|
constructor(span: ParseSpan, public obj: AST, public key: AST) { super(span); }
|
||||||
visit(visitor: AstVisitor, context: any = null): any {
|
visit(visitor: AstVisitor, context: any = null): any {
|
||||||
return visitor.visitKeyedRead(this, context);
|
return visitor.visitKeyedRead(this, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class KeyedWrite extends AST {
|
export class KeyedWrite extends AST {
|
||||||
constructor(public obj: AST, public key: AST, public value: AST) { super(); }
|
constructor(span: ParseSpan, public obj: AST, public key: AST, public value: AST) { super(span); }
|
||||||
visit(visitor: AstVisitor, context: any = null): any {
|
visit(visitor: AstVisitor, context: any = null): any {
|
||||||
return visitor.visitKeyedWrite(this, context);
|
return visitor.visitKeyedWrite(this, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BindingPipe extends AST {
|
export class BindingPipe extends AST {
|
||||||
constructor(public exp: AST, public name: string, public args: any[]) { super(); }
|
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); }
|
visit(visitor: AstVisitor, context: any = null): any { return visitor.visitPipe(this, context); }
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LiteralPrimitive extends AST {
|
export class LiteralPrimitive extends AST {
|
||||||
constructor(public value: any) { super(); }
|
constructor(span: ParseSpan, public value: any) { super(span); }
|
||||||
visit(visitor: AstVisitor, context: any = null): any {
|
visit(visitor: AstVisitor, context: any = null): any {
|
||||||
return visitor.visitLiteralPrimitive(this, context);
|
return visitor.visitLiteralPrimitive(this, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LiteralArray extends AST {
|
export class LiteralArray extends AST {
|
||||||
constructor(public expressions: any[]) { super(); }
|
constructor(span: ParseSpan, public expressions: any[]) { super(span); }
|
||||||
visit(visitor: AstVisitor, context: any = null): any {
|
visit(visitor: AstVisitor, context: any = null): any {
|
||||||
return visitor.visitLiteralArray(this, context);
|
return visitor.visitLiteralArray(this, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LiteralMap extends AST {
|
export class LiteralMap extends AST {
|
||||||
constructor(public keys: any[], public values: any[]) { super(); }
|
constructor(span: ParseSpan, public keys: any[], public values: any[]) { super(span); }
|
||||||
visit(visitor: AstVisitor, context: any = null): any {
|
visit(visitor: AstVisitor, context: any = null): any {
|
||||||
return visitor.visitLiteralMap(this, context);
|
return visitor.visitLiteralMap(this, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Interpolation extends AST {
|
export class Interpolation extends AST {
|
||||||
constructor(public strings: any[], public expressions: any[]) { super(); }
|
constructor(span: ParseSpan, public strings: any[], public expressions: any[]) { super(span); }
|
||||||
visit(visitor: AstVisitor, context: any = null): any {
|
visit(visitor: AstVisitor, context: any = null): any {
|
||||||
return visitor.visitInterpolation(this, context);
|
return visitor.visitInterpolation(this, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Binary extends AST {
|
export class Binary extends AST {
|
||||||
constructor(public operation: string, public left: AST, public right: AST) { super(); }
|
constructor(span: ParseSpan, public operation: string, public left: AST, public right: AST) {
|
||||||
|
super(span);
|
||||||
|
}
|
||||||
visit(visitor: AstVisitor, context: any = null): any {
|
visit(visitor: AstVisitor, context: any = null): any {
|
||||||
return visitor.visitBinary(this, context);
|
return visitor.visitBinary(this, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PrefixNot extends AST {
|
export class PrefixNot extends AST {
|
||||||
constructor(public expression: AST) { super(); }
|
constructor(span: ParseSpan, public expression: AST) { super(span); }
|
||||||
visit(visitor: AstVisitor, context: any = null): any {
|
visit(visitor: AstVisitor, context: any = null): any {
|
||||||
return visitor.visitPrefixNot(this, context);
|
return visitor.visitPrefixNot(this, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MethodCall extends AST {
|
export class MethodCall extends AST {
|
||||||
constructor(public receiver: AST, public name: string, public args: any[]) { super(); }
|
constructor(span: ParseSpan, public receiver: AST, public name: string, public args: any[]) {
|
||||||
|
super(span);
|
||||||
|
}
|
||||||
visit(visitor: AstVisitor, context: any = null): any {
|
visit(visitor: AstVisitor, context: any = null): any {
|
||||||
return visitor.visitMethodCall(this, context);
|
return visitor.visitMethodCall(this, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SafeMethodCall extends AST {
|
export class SafeMethodCall extends AST {
|
||||||
constructor(public receiver: AST, public name: string, public args: any[]) { super(); }
|
constructor(span: ParseSpan, public receiver: AST, public name: string, public args: any[]) {
|
||||||
|
super(span);
|
||||||
|
}
|
||||||
visit(visitor: AstVisitor, context: any = null): any {
|
visit(visitor: AstVisitor, context: any = null): any {
|
||||||
return visitor.visitSafeMethodCall(this, context);
|
return visitor.visitSafeMethodCall(this, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FunctionCall extends AST {
|
export class FunctionCall extends AST {
|
||||||
constructor(public target: AST, public args: any[]) { super(); }
|
constructor(span: ParseSpan, public target: AST, public args: any[]) { super(span); }
|
||||||
visit(visitor: AstVisitor, context: any = null): any {
|
visit(visitor: AstVisitor, context: any = null): any {
|
||||||
return visitor.visitFunctionCall(this, context);
|
return visitor.visitFunctionCall(this, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ASTWithSource extends AST {
|
export class ASTWithSource extends AST {
|
||||||
constructor(public ast: AST, public source: string, public location: string) { super(); }
|
constructor(
|
||||||
|
public ast: AST, public source: string, public location: string,
|
||||||
|
public errors: ParserError[]) {
|
||||||
|
super(new ParseSpan(0, isBlank(source) ? 0 : source.length));
|
||||||
|
}
|
||||||
visit(visitor: AstVisitor, context: any = null): any { return this.ast.visit(visitor, context); }
|
visit(visitor: AstVisitor, context: any = null): any { return this.ast.visit(visitor, context); }
|
||||||
toString(): string { return `${this.source} in ${this.location}`; }
|
toString(): string { return `${this.source} in ${this.location}`; }
|
||||||
}
|
}
|
||||||
|
@ -277,68 +309,70 @@ export class AstTransformer implements AstVisitor {
|
||||||
visitImplicitReceiver(ast: ImplicitReceiver, context: any): AST { return ast; }
|
visitImplicitReceiver(ast: ImplicitReceiver, context: any): AST { return ast; }
|
||||||
|
|
||||||
visitInterpolation(ast: Interpolation, context: any): AST {
|
visitInterpolation(ast: Interpolation, context: any): AST {
|
||||||
return new Interpolation(ast.strings, this.visitAll(ast.expressions));
|
return new Interpolation(ast.span, ast.strings, this.visitAll(ast.expressions));
|
||||||
}
|
}
|
||||||
|
|
||||||
visitLiteralPrimitive(ast: LiteralPrimitive, context: any): AST {
|
visitLiteralPrimitive(ast: LiteralPrimitive, context: any): AST {
|
||||||
return new LiteralPrimitive(ast.value);
|
return new LiteralPrimitive(ast.span, ast.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
visitPropertyRead(ast: PropertyRead, context: any): AST {
|
visitPropertyRead(ast: PropertyRead, context: any): AST {
|
||||||
return new PropertyRead(ast.receiver.visit(this), ast.name);
|
return new PropertyRead(ast.span, ast.receiver.visit(this), ast.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
visitPropertyWrite(ast: PropertyWrite, context: any): AST {
|
visitPropertyWrite(ast: PropertyWrite, context: any): AST {
|
||||||
return new PropertyWrite(ast.receiver.visit(this), ast.name, ast.value);
|
return new PropertyWrite(ast.span, ast.receiver.visit(this), ast.name, ast.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
visitSafePropertyRead(ast: SafePropertyRead, context: any): AST {
|
visitSafePropertyRead(ast: SafePropertyRead, context: any): AST {
|
||||||
return new SafePropertyRead(ast.receiver.visit(this), ast.name);
|
return new SafePropertyRead(ast.span, ast.receiver.visit(this), ast.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
visitMethodCall(ast: MethodCall, context: any): AST {
|
visitMethodCall(ast: MethodCall, context: any): AST {
|
||||||
return new MethodCall(ast.receiver.visit(this), ast.name, this.visitAll(ast.args));
|
return new MethodCall(ast.span, ast.receiver.visit(this), ast.name, this.visitAll(ast.args));
|
||||||
}
|
}
|
||||||
|
|
||||||
visitSafeMethodCall(ast: SafeMethodCall, context: any): AST {
|
visitSafeMethodCall(ast: SafeMethodCall, context: any): AST {
|
||||||
return new SafeMethodCall(ast.receiver.visit(this), ast.name, this.visitAll(ast.args));
|
return new SafeMethodCall(
|
||||||
|
ast.span, ast.receiver.visit(this), ast.name, this.visitAll(ast.args));
|
||||||
}
|
}
|
||||||
|
|
||||||
visitFunctionCall(ast: FunctionCall, context: any): AST {
|
visitFunctionCall(ast: FunctionCall, context: any): AST {
|
||||||
return new FunctionCall(ast.target.visit(this), this.visitAll(ast.args));
|
return new FunctionCall(ast.span, ast.target.visit(this), this.visitAll(ast.args));
|
||||||
}
|
}
|
||||||
|
|
||||||
visitLiteralArray(ast: LiteralArray, context: any): AST {
|
visitLiteralArray(ast: LiteralArray, context: any): AST {
|
||||||
return new LiteralArray(this.visitAll(ast.expressions));
|
return new LiteralArray(ast.span, this.visitAll(ast.expressions));
|
||||||
}
|
}
|
||||||
|
|
||||||
visitLiteralMap(ast: LiteralMap, context: any): AST {
|
visitLiteralMap(ast: LiteralMap, context: any): AST {
|
||||||
return new LiteralMap(ast.keys, this.visitAll(ast.values));
|
return new LiteralMap(ast.span, ast.keys, this.visitAll(ast.values));
|
||||||
}
|
}
|
||||||
|
|
||||||
visitBinary(ast: Binary, context: any): AST {
|
visitBinary(ast: Binary, context: any): AST {
|
||||||
return new Binary(ast.operation, ast.left.visit(this), ast.right.visit(this));
|
return new Binary(ast.span, ast.operation, ast.left.visit(this), ast.right.visit(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
visitPrefixNot(ast: PrefixNot, context: any): AST {
|
visitPrefixNot(ast: PrefixNot, context: any): AST {
|
||||||
return new PrefixNot(ast.expression.visit(this));
|
return new PrefixNot(ast.span, ast.expression.visit(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
visitConditional(ast: Conditional, context: any): AST {
|
visitConditional(ast: Conditional, context: any): AST {
|
||||||
return new Conditional(
|
return new Conditional(
|
||||||
ast.condition.visit(this), ast.trueExp.visit(this), ast.falseExp.visit(this));
|
ast.span, ast.condition.visit(this), ast.trueExp.visit(this), ast.falseExp.visit(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
visitPipe(ast: BindingPipe, context: any): AST {
|
visitPipe(ast: BindingPipe, context: any): AST {
|
||||||
return new BindingPipe(ast.exp.visit(this), ast.name, this.visitAll(ast.args));
|
return new BindingPipe(ast.span, ast.exp.visit(this), ast.name, this.visitAll(ast.args));
|
||||||
}
|
}
|
||||||
|
|
||||||
visitKeyedRead(ast: KeyedRead, context: any): AST {
|
visitKeyedRead(ast: KeyedRead, context: any): AST {
|
||||||
return new KeyedRead(ast.obj.visit(this), ast.key.visit(this));
|
return new KeyedRead(ast.span, ast.obj.visit(this), ast.key.visit(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
visitKeyedWrite(ast: KeyedWrite, context: any): AST {
|
visitKeyedWrite(ast: KeyedWrite, context: any): AST {
|
||||||
return new KeyedWrite(ast.obj.visit(this), ast.key.visit(this), ast.value.visit(this));
|
return new KeyedWrite(
|
||||||
|
ast.span, ast.obj.visit(this), ast.key.visit(this), ast.value.visit(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
visitAll(asts: any[]): any[] {
|
visitAll(asts: any[]): any[] {
|
||||||
|
@ -349,9 +383,11 @@ export class AstTransformer implements AstVisitor {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
visitChain(ast: Chain, context: any): AST { return new Chain(this.visitAll(ast.expressions)); }
|
visitChain(ast: Chain, context: any): AST {
|
||||||
|
return new Chain(ast.span, this.visitAll(ast.expressions));
|
||||||
|
}
|
||||||
|
|
||||||
visitQuote(ast: Quote, context: any): AST {
|
visitQuote(ast: Quote, context: any): AST {
|
||||||
return new Quote(ast.prefix, ast.uninterpretedExpression, ast.location);
|
return new Quote(ast.span, ast.prefix, ast.uninterpretedExpression, ast.location);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,8 @@ export enum TokenType {
|
||||||
Keyword,
|
Keyword,
|
||||||
String,
|
String,
|
||||||
Operator,
|
Operator,
|
||||||
Number
|
Number,
|
||||||
|
Error
|
||||||
}
|
}
|
||||||
|
|
||||||
const KEYWORDS = ['var', 'let', 'null', 'undefined', 'true', 'false', 'if', 'else'];
|
const KEYWORDS = ['var', 'let', 'null', 'undefined', 'true', 'false', 'if', 'else'];
|
||||||
|
@ -74,6 +75,8 @@ export class Token {
|
||||||
|
|
||||||
isKeywordFalse(): boolean { return (this.type == TokenType.Keyword && this.strValue == 'false'); }
|
isKeywordFalse(): boolean { return (this.type == TokenType.Keyword && this.strValue == 'false'); }
|
||||||
|
|
||||||
|
isError(): boolean { return this.type == TokenType.Error; }
|
||||||
|
|
||||||
toNumber(): number {
|
toNumber(): number {
|
||||||
// -1 instead of NULL ok?
|
// -1 instead of NULL ok?
|
||||||
return (this.type == TokenType.Number) ? this.numValue : -1;
|
return (this.type == TokenType.Number) ? this.numValue : -1;
|
||||||
|
@ -86,6 +89,7 @@ export class Token {
|
||||||
case TokenType.Keyword:
|
case TokenType.Keyword:
|
||||||
case TokenType.Operator:
|
case TokenType.Operator:
|
||||||
case TokenType.String:
|
case TokenType.String:
|
||||||
|
case TokenType.Error:
|
||||||
return this.strValue;
|
return this.strValue;
|
||||||
case TokenType.Number:
|
case TokenType.Number:
|
||||||
return this.numValue.toString();
|
return this.numValue.toString();
|
||||||
|
@ -119,14 +123,12 @@ function newNumberToken(index: number, n: number): Token {
|
||||||
return new Token(index, TokenType.Number, n, '');
|
return new Token(index, TokenType.Number, n, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
export var EOF: Token = new Token(-1, TokenType.Character, 0, '');
|
function newErrorToken(index: number, message: string): Token {
|
||||||
|
return new Token(index, TokenType.Error, 0, message);
|
||||||
export class ScannerError extends BaseException {
|
|
||||||
constructor(public message: string) { super(); }
|
|
||||||
|
|
||||||
toString(): string { return this.message; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export var EOF: Token = new Token(-1, TokenType.Character, 0, '');
|
||||||
|
|
||||||
class _Scanner {
|
class _Scanner {
|
||||||
length: number;
|
length: number;
|
||||||
peek: number = 0;
|
peek: number = 0;
|
||||||
|
@ -211,8 +213,8 @@ class _Scanner {
|
||||||
return this.scanToken();
|
return this.scanToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.error(`Unexpected character [${StringWrapper.fromCharCode(peek)}]`, 0);
|
this.advance();
|
||||||
return null;
|
return this.error(`Unexpected character [${StringWrapper.fromCharCode(peek)}]`, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
scanCharacter(start: number, code: number): Token {
|
scanCharacter(start: number, code: number): Token {
|
||||||
|
@ -273,7 +275,7 @@ class _Scanner {
|
||||||
} else if (isExponentStart(this.peek)) {
|
} else if (isExponentStart(this.peek)) {
|
||||||
this.advance();
|
this.advance();
|
||||||
if (isExponentSign(this.peek)) this.advance();
|
if (isExponentSign(this.peek)) this.advance();
|
||||||
if (!chars.isDigit(this.peek)) this.error('Invalid exponent', -1);
|
if (!chars.isDigit(this.peek)) return this.error('Invalid exponent', -1);
|
||||||
simple = false;
|
simple = false;
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
|
@ -307,7 +309,7 @@ class _Scanner {
|
||||||
try {
|
try {
|
||||||
unescapedCode = NumberWrapper.parseInt(hex, 16);
|
unescapedCode = NumberWrapper.parseInt(hex, 16);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.error(`Invalid unicode escape [\\u${hex}]`, 0);
|
return this.error(`Invalid unicode escape [\\u${hex}]`, 0);
|
||||||
}
|
}
|
||||||
for (var i: number = 0; i < 5; i++) {
|
for (var i: number = 0; i < 5; i++) {
|
||||||
this.advance();
|
this.advance();
|
||||||
|
@ -319,7 +321,7 @@ class _Scanner {
|
||||||
buffer.add(StringWrapper.fromCharCode(unescapedCode));
|
buffer.add(StringWrapper.fromCharCode(unescapedCode));
|
||||||
marker = this.index;
|
marker = this.index;
|
||||||
} else if (this.peek == chars.$EOF) {
|
} else if (this.peek == chars.$EOF) {
|
||||||
this.error('Unterminated quote', 0);
|
return this.error('Unterminated quote', 0);
|
||||||
} else {
|
} else {
|
||||||
this.advance();
|
this.advance();
|
||||||
}
|
}
|
||||||
|
@ -337,10 +339,10 @@ class _Scanner {
|
||||||
return newStringToken(start, unescaped);
|
return newStringToken(start, unescaped);
|
||||||
}
|
}
|
||||||
|
|
||||||
error(message: string, offset: number) {
|
error(message: string, offset: number): Token {
|
||||||
var position: number = this.index + offset;
|
const position: number = this.index + offset;
|
||||||
throw new ScannerError(
|
return newErrorToken(
|
||||||
`Lexer Error: ${message} at column ${position} in expression [${this.input}]`);
|
position, `Lexer Error: ${message} at column ${position} in expression [${this.input}]`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,24 +14,18 @@ import {BaseException} from '../facade/exceptions';
|
||||||
import {RegExpWrapper, StringWrapper, escapeRegExp, isBlank, isPresent} from '../facade/lang';
|
import {RegExpWrapper, StringWrapper, escapeRegExp, isBlank, isPresent} from '../facade/lang';
|
||||||
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../interpolation_config';
|
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../interpolation_config';
|
||||||
|
|
||||||
import {AST, ASTWithSource, AstVisitor, Binary, BindingPipe, Chain, Conditional, EmptyExpr, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead, TemplateBinding} from './ast';
|
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, isIdentifier, isQuote} from './lexer';
|
import {EOF, Lexer, Token, TokenType, isIdentifier, isQuote} from './lexer';
|
||||||
|
|
||||||
|
|
||||||
var _implicitReceiver = new ImplicitReceiver();
|
|
||||||
|
|
||||||
class ParseException extends BaseException {
|
|
||||||
constructor(message: string, input: string, errLocation: string, ctxLocation?: any) {
|
|
||||||
super(`Parser Error: ${message} ${errLocation} [${input}] in ${ctxLocation}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SplitInterpolation {
|
export class SplitInterpolation {
|
||||||
constructor(public strings: string[], public expressions: string[]) {}
|
constructor(public strings: string[], public expressions: string[]) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TemplateBindingParseResult {
|
export class TemplateBindingParseResult {
|
||||||
constructor(public templateBindings: TemplateBinding[], public warnings: string[]) {}
|
constructor(
|
||||||
|
public templateBindings: TemplateBinding[], public warnings: string[],
|
||||||
|
public errors: ParserError[]) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _createInterpolateRegExp(config: InterpolationConfig): RegExp {
|
function _createInterpolateRegExp(config: InterpolationConfig): RegExp {
|
||||||
|
@ -41,6 +35,8 @@ function _createInterpolateRegExp(config: InterpolationConfig): RegExp {
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class Parser {
|
export class Parser {
|
||||||
|
private errors: ParserError[] = [];
|
||||||
|
|
||||||
constructor(/** @internal */
|
constructor(/** @internal */
|
||||||
public _lexer: Lexer) {}
|
public _lexer: Lexer) {}
|
||||||
|
|
||||||
|
@ -49,15 +45,15 @@ export class Parser {
|
||||||
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
|
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
|
||||||
this._checkNoInterpolation(input, location, interpolationConfig);
|
this._checkNoInterpolation(input, location, interpolationConfig);
|
||||||
var tokens = this._lexer.tokenize(this._stripComments(input));
|
var tokens = this._lexer.tokenize(this._stripComments(input));
|
||||||
var ast = new _ParseAST(input, location, tokens, true).parseChain();
|
var ast = new _ParseAST(input, location, tokens, true, this.errors).parseChain();
|
||||||
return new ASTWithSource(ast, input, location);
|
return new ASTWithSource(ast, input, location, this.errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
parseBinding(
|
parseBinding(
|
||||||
input: string, location: any,
|
input: string, location: any,
|
||||||
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
|
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
|
||||||
var ast = this._parseBindingAst(input, location, interpolationConfig);
|
var ast = this._parseBindingAst(input, location, interpolationConfig);
|
||||||
return new ASTWithSource(ast, input, location);
|
return new ASTWithSource(ast, input, location, this.errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
parseSimpleBinding(
|
parseSimpleBinding(
|
||||||
|
@ -65,10 +61,14 @@ export class Parser {
|
||||||
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
|
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
|
||||||
var ast = this._parseBindingAst(input, location, interpolationConfig);
|
var ast = this._parseBindingAst(input, location, interpolationConfig);
|
||||||
if (!SimpleExpressionChecker.check(ast)) {
|
if (!SimpleExpressionChecker.check(ast)) {
|
||||||
throw new ParseException(
|
this._reportError(
|
||||||
'Host binding expression can only contain field access and constants', input, location);
|
'Host binding expression can only contain field access and constants', input, location);
|
||||||
}
|
}
|
||||||
return new ASTWithSource(ast, 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(
|
private _parseBindingAst(
|
||||||
|
@ -83,7 +83,7 @@ export class Parser {
|
||||||
|
|
||||||
this._checkNoInterpolation(input, location, interpolationConfig);
|
this._checkNoInterpolation(input, location, interpolationConfig);
|
||||||
var tokens = this._lexer.tokenize(this._stripComments(input));
|
var tokens = this._lexer.tokenize(this._stripComments(input));
|
||||||
return new _ParseAST(input, location, tokens, false).parseChain();
|
return new _ParseAST(input, location, tokens, false, this.errors).parseChain();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _parseQuote(input: string, location: any): AST {
|
private _parseQuote(input: string, location: any): AST {
|
||||||
|
@ -93,12 +93,12 @@ export class Parser {
|
||||||
var prefix = input.substring(0, prefixSeparatorIndex).trim();
|
var prefix = input.substring(0, prefixSeparatorIndex).trim();
|
||||||
if (!isIdentifier(prefix)) return null;
|
if (!isIdentifier(prefix)) return null;
|
||||||
var uninterpretedExpression = input.substring(prefixSeparatorIndex + 1);
|
var uninterpretedExpression = input.substring(prefixSeparatorIndex + 1);
|
||||||
return new Quote(prefix, uninterpretedExpression, location);
|
return new Quote(new ParseSpan(0, input.length), prefix, uninterpretedExpression, location);
|
||||||
}
|
}
|
||||||
|
|
||||||
parseTemplateBindings(input: string, location: any): TemplateBindingParseResult {
|
parseTemplateBindings(input: string, location: any): TemplateBindingParseResult {
|
||||||
var tokens = this._lexer.tokenize(input);
|
var tokens = this._lexer.tokenize(input);
|
||||||
return new _ParseAST(input, location, tokens, false).parseTemplateBindings();
|
return new _ParseAST(input, location, tokens, false, this.errors).parseTemplateBindings();
|
||||||
}
|
}
|
||||||
|
|
||||||
parseInterpolation(
|
parseInterpolation(
|
||||||
|
@ -111,11 +111,14 @@ export class Parser {
|
||||||
|
|
||||||
for (let i = 0; i < split.expressions.length; ++i) {
|
for (let i = 0; i < split.expressions.length; ++i) {
|
||||||
var tokens = this._lexer.tokenize(this._stripComments(split.expressions[i]));
|
var tokens = this._lexer.tokenize(this._stripComments(split.expressions[i]));
|
||||||
var ast = new _ParseAST(input, location, tokens, false).parseChain();
|
var ast = new _ParseAST(input, location, tokens, false, this.errors).parseChain();
|
||||||
expressions.push(ast);
|
expressions.push(ast);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ASTWithSource(new Interpolation(split.strings, expressions), input, location);
|
return new ASTWithSource(
|
||||||
|
new Interpolation(
|
||||||
|
new ParseSpan(0, isBlank(input) ? 0 : input.length), split.strings, expressions),
|
||||||
|
input, location, this.errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
splitInterpolation(
|
splitInterpolation(
|
||||||
|
@ -137,7 +140,7 @@ export class Parser {
|
||||||
} else if (part.trim().length > 0) {
|
} else if (part.trim().length > 0) {
|
||||||
expressions.push(part);
|
expressions.push(part);
|
||||||
} else {
|
} else {
|
||||||
throw new ParseException(
|
this._reportError(
|
||||||
'Blank expressions are not allowed in interpolated strings', input,
|
'Blank expressions are not allowed in interpolated strings', input,
|
||||||
`at column ${this._findInterpolationErrorColumn(parts, i, interpolationConfig)} in`,
|
`at column ${this._findInterpolationErrorColumn(parts, i, interpolationConfig)} in`,
|
||||||
location);
|
location);
|
||||||
|
@ -147,7 +150,9 @@ export class Parser {
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapLiteralPrimitive(input: string, location: any): ASTWithSource {
|
wrapLiteralPrimitive(input: string, location: any): ASTWithSource {
|
||||||
return new ASTWithSource(new LiteralPrimitive(input), input, location);
|
return new ASTWithSource(
|
||||||
|
new LiteralPrimitive(new ParseSpan(0, isBlank(input) ? 0 : input.length), input), input,
|
||||||
|
location, this.errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _stripComments(input: string): string {
|
private _stripComments(input: string): string {
|
||||||
|
@ -177,7 +182,7 @@ export class Parser {
|
||||||
var regexp = _createInterpolateRegExp(interpolationConfig);
|
var regexp = _createInterpolateRegExp(interpolationConfig);
|
||||||
var parts = StringWrapper.split(input, regexp);
|
var parts = StringWrapper.split(input, regexp);
|
||||||
if (parts.length > 1) {
|
if (parts.length > 1) {
|
||||||
throw new ParseException(
|
this._reportError(
|
||||||
`Got interpolation (${interpolationConfig.start}${interpolationConfig.end}) where expression was expected`,
|
`Got interpolation (${interpolationConfig.start}${interpolationConfig.end}) where expression was expected`,
|
||||||
input,
|
input,
|
||||||
`at column ${this._findInterpolationErrorColumn(parts, 1, interpolationConfig)} in`,
|
`at column ${this._findInterpolationErrorColumn(parts, 1, interpolationConfig)} in`,
|
||||||
|
@ -199,10 +204,15 @@ export class Parser {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class _ParseAST {
|
export class _ParseAST {
|
||||||
|
private rparensExpected = 0;
|
||||||
|
private rbracketsExpected = 0;
|
||||||
|
private rbracesExpected = 0;
|
||||||
|
|
||||||
index: number = 0;
|
index: number = 0;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public input: string, public location: any, public tokens: any[],
|
public input: string, public location: any, public tokens: any[], public parseAction: boolean,
|
||||||
public parseAction: boolean) {}
|
private errors: ParserError[]) {}
|
||||||
|
|
||||||
peek(offset: number): Token {
|
peek(offset: number): Token {
|
||||||
var i = this.index + offset;
|
var i = this.index + offset;
|
||||||
|
@ -215,6 +225,8 @@ export class _ParseAST {
|
||||||
return (this.index < this.tokens.length) ? this.next.index : this.input.length;
|
return (this.index < this.tokens.length) ? this.next.index : this.input.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span(start: number) { return new ParseSpan(start, this.inputIndex); }
|
||||||
|
|
||||||
advance() { this.index++; }
|
advance() { this.index++; }
|
||||||
|
|
||||||
optionalCharacter(code: number): boolean {
|
optionalCharacter(code: number): boolean {
|
||||||
|
@ -256,6 +268,7 @@ export class _ParseAST {
|
||||||
var n = this.next;
|
var n = this.next;
|
||||||
if (!n.isIdentifier() && !n.isKeyword()) {
|
if (!n.isIdentifier() && !n.isKeyword()) {
|
||||||
this.error(`Unexpected token ${n}, expected identifier or keyword`);
|
this.error(`Unexpected token ${n}, expected identifier or keyword`);
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
this.advance();
|
this.advance();
|
||||||
return n.toString();
|
return n.toString();
|
||||||
|
@ -265,6 +278,7 @@ export class _ParseAST {
|
||||||
var n = this.next;
|
var n = this.next;
|
||||||
if (!n.isIdentifier() && !n.isKeyword() && !n.isString()) {
|
if (!n.isIdentifier() && !n.isKeyword() && !n.isString()) {
|
||||||
this.error(`Unexpected token ${n}, expected identifier, keyword, or string`);
|
this.error(`Unexpected token ${n}, expected identifier, keyword, or string`);
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
this.advance();
|
this.advance();
|
||||||
return n.toString();
|
return n.toString();
|
||||||
|
@ -272,6 +286,7 @@ export class _ParseAST {
|
||||||
|
|
||||||
parseChain(): AST {
|
parseChain(): AST {
|
||||||
var exprs: AST[] = [];
|
var exprs: AST[] = [];
|
||||||
|
const start = this.inputIndex;
|
||||||
while (this.index < this.tokens.length) {
|
while (this.index < this.tokens.length) {
|
||||||
var expr = this.parsePipe();
|
var expr = this.parsePipe();
|
||||||
exprs.push(expr);
|
exprs.push(expr);
|
||||||
|
@ -286,9 +301,9 @@ export class _ParseAST {
|
||||||
this.error(`Unexpected token '${this.next}'`);
|
this.error(`Unexpected token '${this.next}'`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (exprs.length == 0) return new EmptyExpr();
|
if (exprs.length == 0) return new EmptyExpr(this.span(start));
|
||||||
if (exprs.length == 1) return exprs[0];
|
if (exprs.length == 1) return exprs[0];
|
||||||
return new Chain(exprs);
|
return new Chain(this.span(start), exprs);
|
||||||
}
|
}
|
||||||
|
|
||||||
parsePipe(): AST {
|
parsePipe(): AST {
|
||||||
|
@ -304,7 +319,7 @@ export class _ParseAST {
|
||||||
while (this.optionalCharacter(chars.$COLON)) {
|
while (this.optionalCharacter(chars.$COLON)) {
|
||||||
args.push(this.parseExpression());
|
args.push(this.parseExpression());
|
||||||
}
|
}
|
||||||
result = new BindingPipe(result, name, args);
|
result = new BindingPipe(this.span(result.span.start), result, name, args);
|
||||||
} while (this.optionalOperator('|'));
|
} while (this.optionalOperator('|'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -314,18 +329,21 @@ export class _ParseAST {
|
||||||
parseExpression(): AST { return this.parseConditional(); }
|
parseExpression(): AST { return this.parseConditional(); }
|
||||||
|
|
||||||
parseConditional(): AST {
|
parseConditional(): AST {
|
||||||
var start = this.inputIndex;
|
const start = this.inputIndex;
|
||||||
var result = this.parseLogicalOr();
|
const result = this.parseLogicalOr();
|
||||||
|
|
||||||
if (this.optionalOperator('?')) {
|
if (this.optionalOperator('?')) {
|
||||||
var yes = this.parsePipe();
|
const yes = this.parsePipe();
|
||||||
|
let no: AST;
|
||||||
if (!this.optionalCharacter(chars.$COLON)) {
|
if (!this.optionalCharacter(chars.$COLON)) {
|
||||||
var end = this.inputIndex;
|
var end = this.inputIndex;
|
||||||
var expression = this.input.substring(start, end);
|
var expression = this.input.substring(start, end);
|
||||||
this.error(`Conditional expression ${expression} requires all 3 expressions`);
|
this.error(`Conditional expression ${expression} requires all 3 expressions`);
|
||||||
|
no = new EmptyExpr(this.span(start));
|
||||||
|
} else {
|
||||||
|
no = this.parsePipe();
|
||||||
}
|
}
|
||||||
var no = this.parsePipe();
|
return new Conditional(this.span(start), result, yes, no);
|
||||||
return new Conditional(result, yes, no);
|
|
||||||
} else {
|
} else {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -333,102 +351,126 @@ export class _ParseAST {
|
||||||
|
|
||||||
parseLogicalOr(): AST {
|
parseLogicalOr(): AST {
|
||||||
// '||'
|
// '||'
|
||||||
var result = this.parseLogicalAnd();
|
let result = this.parseLogicalAnd();
|
||||||
while (this.optionalOperator('||')) {
|
while (this.optionalOperator('||')) {
|
||||||
result = new Binary('||', result, this.parseLogicalAnd());
|
const right = this.parseLogicalAnd();
|
||||||
|
result = new Binary(this.span(result.span.start), '||', result, right);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
parseLogicalAnd(): AST {
|
parseLogicalAnd(): AST {
|
||||||
// '&&'
|
// '&&'
|
||||||
var result = this.parseEquality();
|
let result = this.parseEquality();
|
||||||
while (this.optionalOperator('&&')) {
|
while (this.optionalOperator('&&')) {
|
||||||
result = new Binary('&&', result, this.parseEquality());
|
const right = this.parseEquality();
|
||||||
|
result = new Binary(this.span(result.span.start), '&&', result, right);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
parseEquality(): AST {
|
parseEquality(): AST {
|
||||||
// '==','!=','===','!=='
|
// '==','!=','===','!=='
|
||||||
var result = this.parseRelational();
|
let result = this.parseRelational();
|
||||||
while (true) {
|
while (this.next.type == TokenType.Operator) {
|
||||||
if (this.optionalOperator('==')) {
|
let operator = this.next.strValue;
|
||||||
result = new Binary('==', result, this.parseRelational());
|
switch (operator) {
|
||||||
} else if (this.optionalOperator('===')) {
|
case '==':
|
||||||
result = new Binary('===', result, this.parseRelational());
|
case '===':
|
||||||
} else if (this.optionalOperator('!=')) {
|
case '!=':
|
||||||
result = new Binary('!=', result, this.parseRelational());
|
case '!==':
|
||||||
} else if (this.optionalOperator('!==')) {
|
this.advance();
|
||||||
result = new Binary('!==', result, this.parseRelational());
|
const right = this.parseRelational();
|
||||||
} else {
|
result = new Binary(this.span(result.span.start), operator, result, right);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parseRelational(): AST {
|
parseRelational(): AST {
|
||||||
// '<', '>', '<=', '>='
|
// '<', '>', '<=', '>='
|
||||||
var result = this.parseAdditive();
|
let result = this.parseAdditive();
|
||||||
while (true) {
|
while (this.next.type == TokenType.Operator) {
|
||||||
if (this.optionalOperator('<')) {
|
let operator = this.next.strValue;
|
||||||
result = new Binary('<', result, this.parseAdditive());
|
switch (operator) {
|
||||||
} else if (this.optionalOperator('>')) {
|
case '<':
|
||||||
result = new Binary('>', result, this.parseAdditive());
|
case '>':
|
||||||
} else if (this.optionalOperator('<=')) {
|
case '<=':
|
||||||
result = new Binary('<=', result, this.parseAdditive());
|
case '>=':
|
||||||
} else if (this.optionalOperator('>=')) {
|
this.advance();
|
||||||
result = new Binary('>=', result, this.parseAdditive());
|
const right = this.parseAdditive();
|
||||||
} else {
|
result = new Binary(this.span(result.span.start), operator, result, right);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parseAdditive(): AST {
|
parseAdditive(): AST {
|
||||||
// '+', '-'
|
// '+', '-'
|
||||||
var result = this.parseMultiplicative();
|
let result = this.parseMultiplicative();
|
||||||
while (true) {
|
while (this.next.type == TokenType.Operator) {
|
||||||
if (this.optionalOperator('+')) {
|
const operator = this.next.strValue;
|
||||||
result = new Binary('+', result, this.parseMultiplicative());
|
switch (operator) {
|
||||||
} else if (this.optionalOperator('-')) {
|
case '+':
|
||||||
result = new Binary('-', result, this.parseMultiplicative());
|
case '-':
|
||||||
} else {
|
this.advance();
|
||||||
|
let right = this.parseMultiplicative();
|
||||||
|
result = new Binary(this.span(result.span.start), operator, result, right);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parseMultiplicative(): AST {
|
parseMultiplicative(): AST {
|
||||||
// '*', '%', '/'
|
// '*', '%', '/'
|
||||||
var result = this.parsePrefix();
|
let result = this.parsePrefix();
|
||||||
while (true) {
|
while (this.next.type == TokenType.Operator) {
|
||||||
if (this.optionalOperator('*')) {
|
const operator = this.next.strValue;
|
||||||
result = new Binary('*', result, this.parsePrefix());
|
switch (operator) {
|
||||||
} else if (this.optionalOperator('%')) {
|
case '*':
|
||||||
result = new Binary('%', result, this.parsePrefix());
|
case '%':
|
||||||
} else if (this.optionalOperator('/')) {
|
case '/':
|
||||||
result = new Binary('/', result, this.parsePrefix());
|
this.advance();
|
||||||
} else {
|
let right = this.parsePrefix();
|
||||||
|
result = new Binary(this.span(result.span.start), operator, result, right);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parsePrefix(): AST {
|
parsePrefix(): AST {
|
||||||
if (this.optionalOperator('+')) {
|
if (this.next.type == TokenType.Operator) {
|
||||||
|
const start = this.inputIndex;
|
||||||
|
const operator = this.next.strValue;
|
||||||
|
switch (operator) {
|
||||||
|
case '+':
|
||||||
|
this.advance();
|
||||||
return this.parsePrefix();
|
return this.parsePrefix();
|
||||||
} else if (this.optionalOperator('-')) {
|
case '-':
|
||||||
return new Binary('-', new LiteralPrimitive(0), this.parsePrefix());
|
this.advance();
|
||||||
} else if (this.optionalOperator('!')) {
|
var result = this.parsePrefix();
|
||||||
return new PrefixNot(this.parsePrefix());
|
return new Binary(
|
||||||
} else {
|
this.span(start), operator, new LiteralPrimitive(new ParseSpan(start, start), 0),
|
||||||
return this.parseCallChain();
|
result);
|
||||||
|
case '!':
|
||||||
|
this.advance();
|
||||||
|
var result = this.parsePrefix();
|
||||||
|
return new PrefixNot(this.span(start), result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return this.parseCallChain();
|
||||||
|
}
|
||||||
|
|
||||||
parseCallChain(): AST {
|
parseCallChain(): AST {
|
||||||
var result = this.parsePrimary();
|
let result = this.parsePrimary();
|
||||||
while (true) {
|
while (true) {
|
||||||
if (this.optionalCharacter(chars.$PERIOD)) {
|
if (this.optionalCharacter(chars.$PERIOD)) {
|
||||||
result = this.parseAccessMemberOrMethodCall(result, false);
|
result = this.parseAccessMemberOrMethodCall(result, false);
|
||||||
|
@ -437,19 +479,23 @@ export class _ParseAST {
|
||||||
result = this.parseAccessMemberOrMethodCall(result, true);
|
result = this.parseAccessMemberOrMethodCall(result, true);
|
||||||
|
|
||||||
} else if (this.optionalCharacter(chars.$LBRACKET)) {
|
} else if (this.optionalCharacter(chars.$LBRACKET)) {
|
||||||
var key = this.parsePipe();
|
this.rbracketsExpected++;
|
||||||
|
const key = this.parsePipe();
|
||||||
|
this.rbracketsExpected--;
|
||||||
this.expectCharacter(chars.$RBRACKET);
|
this.expectCharacter(chars.$RBRACKET);
|
||||||
if (this.optionalOperator('=')) {
|
if (this.optionalOperator('=')) {
|
||||||
var value = this.parseConditional();
|
const value = this.parseConditional();
|
||||||
result = new KeyedWrite(result, key, value);
|
result = new KeyedWrite(this.span(result.span.start), result, key, value);
|
||||||
} else {
|
} else {
|
||||||
result = new KeyedRead(result, key);
|
result = new KeyedRead(this.span(result.span.start), result, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (this.optionalCharacter(chars.$LPAREN)) {
|
} else if (this.optionalCharacter(chars.$LPAREN)) {
|
||||||
var args = this.parseCallArguments();
|
this.rparensExpected++;
|
||||||
|
const args = this.parseCallArguments();
|
||||||
|
this.rparensExpected--;
|
||||||
this.expectCharacter(chars.$RPAREN);
|
this.expectCharacter(chars.$RPAREN);
|
||||||
result = new FunctionCall(result, args);
|
result = new FunctionCall(this.span(result.span.start), result, args);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return result;
|
return result;
|
||||||
|
@ -458,55 +504,59 @@ export class _ParseAST {
|
||||||
}
|
}
|
||||||
|
|
||||||
parsePrimary(): AST {
|
parsePrimary(): AST {
|
||||||
|
const start = this.inputIndex;
|
||||||
if (this.optionalCharacter(chars.$LPAREN)) {
|
if (this.optionalCharacter(chars.$LPAREN)) {
|
||||||
let result = this.parsePipe();
|
this.rparensExpected++;
|
||||||
|
const result = this.parsePipe();
|
||||||
|
this.rparensExpected--;
|
||||||
this.expectCharacter(chars.$RPAREN);
|
this.expectCharacter(chars.$RPAREN);
|
||||||
return result;
|
return result;
|
||||||
} else if (this.next.isKeywordNull() || this.next.isKeywordUndefined()) {
|
} else if (this.next.isKeywordNull() || this.next.isKeywordUndefined()) {
|
||||||
this.advance();
|
this.advance();
|
||||||
return new LiteralPrimitive(null);
|
return new LiteralPrimitive(this.span(start), null);
|
||||||
|
|
||||||
} else if (this.next.isKeywordTrue()) {
|
} else if (this.next.isKeywordTrue()) {
|
||||||
this.advance();
|
this.advance();
|
||||||
return new LiteralPrimitive(true);
|
return new LiteralPrimitive(this.span(start), true);
|
||||||
|
|
||||||
} else if (this.next.isKeywordFalse()) {
|
} else if (this.next.isKeywordFalse()) {
|
||||||
this.advance();
|
this.advance();
|
||||||
return new LiteralPrimitive(false);
|
return new LiteralPrimitive(this.span(start), false);
|
||||||
|
|
||||||
} else if (this.optionalCharacter(chars.$LBRACKET)) {
|
} else if (this.optionalCharacter(chars.$LBRACKET)) {
|
||||||
var elements = this.parseExpressionList(chars.$RBRACKET);
|
this.rbracketsExpected++;
|
||||||
|
const elements = this.parseExpressionList(chars.$RBRACKET);
|
||||||
|
this.rbracketsExpected--;
|
||||||
this.expectCharacter(chars.$RBRACKET);
|
this.expectCharacter(chars.$RBRACKET);
|
||||||
return new LiteralArray(elements);
|
return new LiteralArray(this.span(start), elements);
|
||||||
|
|
||||||
} else if (this.next.isCharacter(chars.$LBRACE)) {
|
} else if (this.next.isCharacter(chars.$LBRACE)) {
|
||||||
return this.parseLiteralMap();
|
return this.parseLiteralMap();
|
||||||
|
|
||||||
} else if (this.next.isIdentifier()) {
|
} else if (this.next.isIdentifier()) {
|
||||||
return this.parseAccessMemberOrMethodCall(_implicitReceiver, false);
|
return this.parseAccessMemberOrMethodCall(new ImplicitReceiver(this.span(start)), false);
|
||||||
|
|
||||||
} else if (this.next.isNumber()) {
|
} else if (this.next.isNumber()) {
|
||||||
var value = this.next.toNumber();
|
const value = this.next.toNumber();
|
||||||
this.advance();
|
this.advance();
|
||||||
return new LiteralPrimitive(value);
|
return new LiteralPrimitive(this.span(start), value);
|
||||||
|
|
||||||
} else if (this.next.isString()) {
|
} else if (this.next.isString()) {
|
||||||
var literalValue = this.next.toString();
|
const literalValue = this.next.toString();
|
||||||
this.advance();
|
this.advance();
|
||||||
return new LiteralPrimitive(literalValue);
|
return new LiteralPrimitive(this.span(start), literalValue);
|
||||||
|
|
||||||
} else if (this.index >= this.tokens.length) {
|
} else if (this.index >= this.tokens.length) {
|
||||||
this.error(`Unexpected end of expression: ${this.input}`);
|
this.error(`Unexpected end of expression: ${this.input}`);
|
||||||
|
return new EmptyExpr(this.span(start));
|
||||||
} else {
|
} else {
|
||||||
this.error(`Unexpected token ${this.next}`);
|
this.error(`Unexpected token ${this.next}`);
|
||||||
|
return new EmptyExpr(this.span(start));
|
||||||
}
|
}
|
||||||
// error() throws, so we don't reach here.
|
|
||||||
throw new BaseException('Fell through all cases in parsePrimary');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
parseExpressionList(terminator: number): AST[] {
|
parseExpressionList(terminator: number): AST[] {
|
||||||
var result: AST[] = [];
|
let result: AST[] = [];
|
||||||
if (!this.next.isCharacter(terminator)) {
|
if (!this.next.isCharacter(terminator)) {
|
||||||
do {
|
do {
|
||||||
result.push(this.parsePipe());
|
result.push(this.parsePipe());
|
||||||
|
@ -516,51 +566,59 @@ export class _ParseAST {
|
||||||
}
|
}
|
||||||
|
|
||||||
parseLiteralMap(): LiteralMap {
|
parseLiteralMap(): LiteralMap {
|
||||||
var keys: string[] = [];
|
let keys: string[] = [];
|
||||||
var values: AST[] = [];
|
let values: AST[] = [];
|
||||||
|
const start = this.inputIndex;
|
||||||
this.expectCharacter(chars.$LBRACE);
|
this.expectCharacter(chars.$LBRACE);
|
||||||
if (!this.optionalCharacter(chars.$RBRACE)) {
|
if (!this.optionalCharacter(chars.$RBRACE)) {
|
||||||
|
this.rbracesExpected++;
|
||||||
do {
|
do {
|
||||||
var key = this.expectIdentifierOrKeywordOrString();
|
var key = this.expectIdentifierOrKeywordOrString();
|
||||||
keys.push(key);
|
keys.push(key);
|
||||||
this.expectCharacter(chars.$COLON);
|
this.expectCharacter(chars.$COLON);
|
||||||
values.push(this.parsePipe());
|
values.push(this.parsePipe());
|
||||||
} while (this.optionalCharacter(chars.$COMMA));
|
} while (this.optionalCharacter(chars.$COMMA));
|
||||||
|
this.rbracesExpected--;
|
||||||
this.expectCharacter(chars.$RBRACE);
|
this.expectCharacter(chars.$RBRACE);
|
||||||
}
|
}
|
||||||
return new LiteralMap(keys, values);
|
return new LiteralMap(this.span(start), keys, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
parseAccessMemberOrMethodCall(receiver: AST, isSafe: boolean = false): AST {
|
parseAccessMemberOrMethodCall(receiver: AST, isSafe: boolean = false): AST {
|
||||||
let id = this.expectIdentifierOrKeyword();
|
const start = receiver.span.start;
|
||||||
|
const id = this.expectIdentifierOrKeyword();
|
||||||
|
|
||||||
if (this.optionalCharacter(chars.$LPAREN)) {
|
if (this.optionalCharacter(chars.$LPAREN)) {
|
||||||
let args = this.parseCallArguments();
|
this.rparensExpected++;
|
||||||
|
const args = this.parseCallArguments();
|
||||||
this.expectCharacter(chars.$RPAREN);
|
this.expectCharacter(chars.$RPAREN);
|
||||||
return isSafe ? new SafeMethodCall(receiver, id, args) : new MethodCall(receiver, id, args);
|
this.rparensExpected--;
|
||||||
|
let span = this.span(start);
|
||||||
|
return isSafe ? new SafeMethodCall(span, receiver, id, args) :
|
||||||
|
new MethodCall(span, receiver, id, args);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (isSafe) {
|
if (isSafe) {
|
||||||
if (this.optionalOperator('=')) {
|
if (this.optionalOperator('=')) {
|
||||||
this.error('The \'?.\' operator cannot be used in the assignment');
|
this.error('The \'?.\' operator cannot be used in the assignment');
|
||||||
|
return new EmptyExpr(this.span(start));
|
||||||
} else {
|
} else {
|
||||||
return new SafePropertyRead(receiver, id);
|
return new SafePropertyRead(this.span(start), receiver, id);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this.optionalOperator('=')) {
|
if (this.optionalOperator('=')) {
|
||||||
if (!this.parseAction) {
|
if (!this.parseAction) {
|
||||||
this.error('Bindings cannot contain assignments');
|
this.error('Bindings cannot contain assignments');
|
||||||
|
return new EmptyExpr(this.span(start));
|
||||||
}
|
}
|
||||||
|
|
||||||
let value = this.parseConditional();
|
let value = this.parseConditional();
|
||||||
return new PropertyWrite(receiver, id, value);
|
return new PropertyWrite(this.span(start), receiver, id, value);
|
||||||
} else {
|
} else {
|
||||||
return new PropertyRead(receiver, id);
|
return new PropertyRead(this.span(start), receiver, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
parseCallArguments(): BindingPipe[] {
|
parseCallArguments(): BindingPipe[] {
|
||||||
|
@ -576,8 +634,8 @@ export class _ParseAST {
|
||||||
* An identifier, a keyword, a string with an optional `-` inbetween.
|
* An identifier, a keyword, a string with an optional `-` inbetween.
|
||||||
*/
|
*/
|
||||||
expectTemplateBindingKey(): string {
|
expectTemplateBindingKey(): string {
|
||||||
var result = '';
|
let result = '';
|
||||||
var operatorFound = false;
|
let operatorFound = false;
|
||||||
do {
|
do {
|
||||||
result += this.expectIdentifierOrKeywordOrString();
|
result += this.expectIdentifierOrKeywordOrString();
|
||||||
operatorFound = this.optionalOperator('-');
|
operatorFound = this.optionalOperator('-');
|
||||||
|
@ -590,9 +648,9 @@ export class _ParseAST {
|
||||||
}
|
}
|
||||||
|
|
||||||
parseTemplateBindings(): TemplateBindingParseResult {
|
parseTemplateBindings(): TemplateBindingParseResult {
|
||||||
var bindings: TemplateBinding[] = [];
|
let bindings: TemplateBinding[] = [];
|
||||||
var prefix: string = null;
|
let prefix: string = null;
|
||||||
var warnings: string[] = [];
|
let warnings: string[] = [];
|
||||||
while (this.index < this.tokens.length) {
|
while (this.index < this.tokens.length) {
|
||||||
var keyIsVar: boolean = this.peekKeywordLet();
|
var keyIsVar: boolean = this.peekKeywordLet();
|
||||||
if (!keyIsVar && this.peekDeprecatedKeywordVar()) {
|
if (!keyIsVar && this.peekDeprecatedKeywordVar()) {
|
||||||
|
@ -626,26 +684,56 @@ export class _ParseAST {
|
||||||
} else if (
|
} else if (
|
||||||
this.next !== EOF && !this.peekKeywordLet() && !this.peekDeprecatedKeywordVar() &&
|
this.next !== EOF && !this.peekKeywordLet() && !this.peekDeprecatedKeywordVar() &&
|
||||||
!this.peekDeprecatedOperatorHash()) {
|
!this.peekDeprecatedOperatorHash()) {
|
||||||
var start = this.inputIndex;
|
const start = this.inputIndex;
|
||||||
var ast = this.parsePipe();
|
var ast = this.parsePipe();
|
||||||
var source = this.input.substring(start, this.inputIndex);
|
var source = this.input.substring(start, this.inputIndex);
|
||||||
expression = new ASTWithSource(ast, source, this.location);
|
expression = new ASTWithSource(ast, source, this.location, this.errors);
|
||||||
}
|
}
|
||||||
bindings.push(new TemplateBinding(key, keyIsVar, name, expression));
|
bindings.push(new TemplateBinding(key, keyIsVar, name, expression));
|
||||||
if (!this.optionalCharacter(chars.$SEMICOLON)) {
|
if (!this.optionalCharacter(chars.$SEMICOLON)) {
|
||||||
this.optionalCharacter(chars.$COMMA);
|
this.optionalCharacter(chars.$COMMA);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new TemplateBindingParseResult(bindings, warnings);
|
return new TemplateBindingParseResult(bindings, warnings, this.errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
error(message: string, index: number = null) {
|
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 (isBlank(index)) index = this.index;
|
if (isBlank(index)) index = this.index;
|
||||||
|
return (index < this.tokens.length) ? `at column ${this.tokens[index].index + 1} in` :
|
||||||
var location = (index < this.tokens.length) ? `at column ${this.tokens[index].index + 1} in` :
|
|
||||||
`at the end of the expression`;
|
`at the end of the expression`;
|
||||||
|
}
|
||||||
|
|
||||||
throw new ParseException(message, this.input, location, this.location);
|
// 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
|
||||||
|
// extrainious 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {Console, MAX_INTERPOLATION_VALUES} from '../core_private';
|
||||||
import {ListWrapper, StringMapWrapper, SetWrapper,} from '../src/facade/collection';
|
import {ListWrapper, StringMapWrapper, SetWrapper,} from '../src/facade/collection';
|
||||||
import {RegExpWrapper, isPresent, StringWrapper, isBlank, isArray} from '../src/facade/lang';
|
import {RegExpWrapper, isPresent, StringWrapper, isBlank, isArray} from '../src/facade/lang';
|
||||||
import {BaseException} from '../src/facade/exceptions';
|
import {BaseException} from '../src/facade/exceptions';
|
||||||
import {AST, Interpolation, ASTWithSource, TemplateBinding, RecursiveAstVisitor, BindingPipe} from './expression_parser/ast';
|
import {AST, Interpolation, ASTWithSource, TemplateBinding, RecursiveAstVisitor, BindingPipe, ParserError} from './expression_parser/ast';
|
||||||
import {Parser} from './expression_parser/parser';
|
import {Parser} from './expression_parser/parser';
|
||||||
import {CompileDirectiveMetadata, CompilePipeMetadata, CompileMetadataWithType,} from './compile_metadata';
|
import {CompileDirectiveMetadata, CompilePipeMetadata, CompileMetadataWithType,} from './compile_metadata';
|
||||||
import {HtmlParser} from './html_parser';
|
import {HtmlParser} from './html_parser';
|
||||||
|
@ -190,10 +190,17 @@ class TemplateParseVisitor implements HtmlAstVisitor {
|
||||||
this.errors.push(new TemplateParseError(message, sourceSpan, level));
|
this.errors.push(new TemplateParseError(message, sourceSpan, level));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _reportParserErors(errors: ParserError[], sourceSpan: ParseSourceSpan) {
|
||||||
|
for (let error of errors) {
|
||||||
|
this._reportError(error.message, sourceSpan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _parseInterpolation(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
|
private _parseInterpolation(value: string, sourceSpan: ParseSourceSpan): ASTWithSource {
|
||||||
var sourceInfo = sourceSpan.start.toString();
|
var sourceInfo = sourceSpan.start.toString();
|
||||||
try {
|
try {
|
||||||
var ast = this._exprParser.parseInterpolation(value, sourceInfo, this._interpolationConfig);
|
var ast = this._exprParser.parseInterpolation(value, sourceInfo, this._interpolationConfig);
|
||||||
|
if (ast) this._reportParserErors(ast.errors, sourceSpan);
|
||||||
this._checkPipes(ast, sourceSpan);
|
this._checkPipes(ast, sourceSpan);
|
||||||
if (isPresent(ast) &&
|
if (isPresent(ast) &&
|
||||||
(<Interpolation>ast.ast).expressions.length > MAX_INTERPOLATION_VALUES) {
|
(<Interpolation>ast.ast).expressions.length > MAX_INTERPOLATION_VALUES) {
|
||||||
|
@ -211,6 +218,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
|
||||||
var sourceInfo = sourceSpan.start.toString();
|
var sourceInfo = sourceSpan.start.toString();
|
||||||
try {
|
try {
|
||||||
var ast = this._exprParser.parseAction(value, sourceInfo, this._interpolationConfig);
|
var ast = this._exprParser.parseAction(value, sourceInfo, this._interpolationConfig);
|
||||||
|
if (ast) this._reportParserErors(ast.errors, sourceSpan);
|
||||||
this._checkPipes(ast, sourceSpan);
|
this._checkPipes(ast, sourceSpan);
|
||||||
return ast;
|
return ast;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -223,6 +231,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
|
||||||
var sourceInfo = sourceSpan.start.toString();
|
var sourceInfo = sourceSpan.start.toString();
|
||||||
try {
|
try {
|
||||||
var ast = this._exprParser.parseBinding(value, sourceInfo, this._interpolationConfig);
|
var ast = this._exprParser.parseBinding(value, sourceInfo, this._interpolationConfig);
|
||||||
|
if (ast) this._reportParserErors(ast.errors, sourceSpan);
|
||||||
this._checkPipes(ast, sourceSpan);
|
this._checkPipes(ast, sourceSpan);
|
||||||
return ast;
|
return ast;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -235,6 +244,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
|
||||||
var sourceInfo = sourceSpan.start.toString();
|
var sourceInfo = sourceSpan.start.toString();
|
||||||
try {
|
try {
|
||||||
var bindingsResult = this._exprParser.parseTemplateBindings(value, sourceInfo);
|
var bindingsResult = this._exprParser.parseTemplateBindings(value, sourceInfo);
|
||||||
|
this._reportParserErors(bindingsResult.errors, sourceSpan);
|
||||||
bindingsResult.templateBindings.forEach((binding) => {
|
bindingsResult.templateBindings.forEach((binding) => {
|
||||||
if (isPresent(binding.expression)) {
|
if (isPresent(binding.expression)) {
|
||||||
this._checkPipes(binding.expression, sourceSpan);
|
this._checkPipes(binding.expression, sourceSpan);
|
||||||
|
|
|
@ -15,52 +15,52 @@ function lex(text: string): any[] {
|
||||||
return new Lexer().tokenize(text);
|
return new Lexer().tokenize(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
function expectToken(token: any /** TODO #9100 */, index: any /** TODO #9100 */) {
|
function expectToken(token: any, index: any) {
|
||||||
expect(token instanceof Token).toBe(true);
|
expect(token instanceof Token).toBe(true);
|
||||||
expect(token.index).toEqual(index);
|
expect(token.index).toEqual(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
function expectCharacterToken(
|
function expectCharacterToken(token: any, index: any, character: any) {
|
||||||
token: any /** TODO #9100 */, index: any /** TODO #9100 */, character: any /** TODO #9100 */) {
|
|
||||||
expect(character.length).toBe(1);
|
expect(character.length).toBe(1);
|
||||||
expectToken(token, index);
|
expectToken(token, index);
|
||||||
expect(token.isCharacter(StringWrapper.charCodeAt(character, 0))).toBe(true);
|
expect(token.isCharacter(StringWrapper.charCodeAt(character, 0))).toBe(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function expectOperatorToken(
|
function expectOperatorToken(token: any, index: any, operator: any) {
|
||||||
token: any /** TODO #9100 */, index: any /** TODO #9100 */, operator: any /** TODO #9100 */) {
|
|
||||||
expectToken(token, index);
|
expectToken(token, index);
|
||||||
expect(token.isOperator(operator)).toBe(true);
|
expect(token.isOperator(operator)).toBe(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function expectNumberToken(
|
function expectNumberToken(token: any, index: any, n: any) {
|
||||||
token: any /** TODO #9100 */, index: any /** TODO #9100 */, n: any /** TODO #9100 */) {
|
|
||||||
expectToken(token, index);
|
expectToken(token, index);
|
||||||
expect(token.isNumber()).toBe(true);
|
expect(token.isNumber()).toBe(true);
|
||||||
expect(token.toNumber()).toEqual(n);
|
expect(token.toNumber()).toEqual(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
function expectStringToken(
|
function expectStringToken(token: any, index: any, str: any) {
|
||||||
token: any /** TODO #9100 */, index: any /** TODO #9100 */, str: any /** TODO #9100 */) {
|
|
||||||
expectToken(token, index);
|
expectToken(token, index);
|
||||||
expect(token.isString()).toBe(true);
|
expect(token.isString()).toBe(true);
|
||||||
expect(token.toString()).toEqual(str);
|
expect(token.toString()).toEqual(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
function expectIdentifierToken(
|
function expectIdentifierToken(token: any, index: any, identifier: any) {
|
||||||
token: any /** TODO #9100 */, index: any /** TODO #9100 */, identifier: any /** TODO #9100 */) {
|
|
||||||
expectToken(token, index);
|
expectToken(token, index);
|
||||||
expect(token.isIdentifier()).toBe(true);
|
expect(token.isIdentifier()).toBe(true);
|
||||||
expect(token.toString()).toEqual(identifier);
|
expect(token.toString()).toEqual(identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
function expectKeywordToken(
|
function expectKeywordToken(token: any, index: any, keyword: any) {
|
||||||
token: any /** TODO #9100 */, index: any /** TODO #9100 */, keyword: any /** TODO #9100 */) {
|
|
||||||
expectToken(token, index);
|
expectToken(token, index);
|
||||||
expect(token.isKeyword()).toBe(true);
|
expect(token.isKeyword()).toBe(true);
|
||||||
expect(token.toString()).toEqual(keyword);
|
expect(token.toString()).toEqual(keyword);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function expectErrorToken(token: Token, index: any, message: string) {
|
||||||
|
expectToken(token, index);
|
||||||
|
expect(token.isError()).toBe(true);
|
||||||
|
expect(token.toString()).toEqual(message);
|
||||||
|
}
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
describe('lexer', function() {
|
describe('lexer', function() {
|
||||||
describe('token', function() {
|
describe('token', function() {
|
||||||
|
@ -228,14 +228,13 @@ export function main() {
|
||||||
expectNumberToken(tokens[0], 0, 0.5E+10);
|
expectNumberToken(tokens[0], 0, 0.5E+10);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throws exception for invalid exponent', function() {
|
it('should return exception for invalid exponent', function() {
|
||||||
expect(() => {
|
expectErrorToken(
|
||||||
lex('0.5E-');
|
lex('0.5E-')[0], 4, 'Lexer Error: Invalid exponent at column 4 in expression [0.5E-]');
|
||||||
}).toThrowError('Lexer Error: Invalid exponent at column 4 in expression [0.5E-]');
|
|
||||||
|
|
||||||
expect(() => {
|
expectErrorToken(
|
||||||
lex('0.5E-A');
|
lex('0.5E-A')[0], 4,
|
||||||
}).toThrowError('Lexer Error: Invalid exponent at column 4 in expression [0.5E-A]');
|
'Lexer Error: Invalid exponent at column 4 in expression [0.5E-A]');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should tokenize number starting with a dot', function() {
|
it('should tokenize number starting with a dot', function() {
|
||||||
|
@ -244,8 +243,8 @@ export function main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error on invalid unicode', function() {
|
it('should throw error on invalid unicode', function() {
|
||||||
expect(() => { lex('\'\\u1\'\'bla\''); })
|
expectErrorToken(
|
||||||
.toThrowError(
|
lex('\'\\u1\'\'bla\'')[0], 2,
|
||||||
'Lexer Error: Invalid unicode escape [\\u1\'\'b] at column 2 in expression [\'\\u1\'\'bla\']');
|
'Lexer Error: Invalid unicode escape [\\u1\'\'b] at column 2 in expression [\'\\u1\'\'bla\']');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6,64 +6,79 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {AST, BindingPipe} from '@angular/compiler/src/expression_parser/ast';
|
import {AST, ASTWithSource, BindingPipe, Interpolation, LiteralPrimitive, ParserError, TemplateBinding} from '@angular/compiler/src/expression_parser/ast';
|
||||||
import {Lexer} from '@angular/compiler/src/expression_parser/lexer';
|
import {Lexer} from '@angular/compiler/src/expression_parser/lexer';
|
||||||
import {Parser} from '@angular/compiler/src/expression_parser/parser';
|
import {Parser, TemplateBindingParseResult} from '@angular/compiler/src/expression_parser/parser';
|
||||||
import {beforeEach, ddescribe, describe, expect, iit, it, xit} from '@angular/core/testing';
|
import {beforeEach, ddescribe, describe, expect, iit, it, xit} from '@angular/core/testing';
|
||||||
|
|
||||||
import {isBlank, isPresent} from '../../src/facade/lang';
|
import {isBlank, isPresent} from '../../src/facade/lang';
|
||||||
|
|
||||||
import {Unparser} from './unparser';
|
import {unparse} from './unparser';
|
||||||
|
import {validate} from './validator';
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
function createParser() { return new Parser(new Lexer()); }
|
function createParser() { return new Parser(new Lexer()); }
|
||||||
|
|
||||||
function parseAction(text: string, location: any = null): any {
|
function parseAction(text: string, location: any = null): ASTWithSource {
|
||||||
return createParser().parseAction(text, location);
|
return createParser().parseAction(text, location);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseBinding(text: string, location: any = null): any {
|
function parseBinding(text: string, location: any = null): ASTWithSource {
|
||||||
return createParser().parseBinding(text, location);
|
return createParser().parseBinding(text, location);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTemplateBindings(text: string, location: any = null): any {
|
function parseTemplateBindingsResult(
|
||||||
return createParser().parseTemplateBindings(text, location).templateBindings;
|
text: string, location: any = null): TemplateBindingParseResult {
|
||||||
|
return createParser().parseTemplateBindings(text, location);
|
||||||
|
}
|
||||||
|
function parseTemplateBindings(text: string, location: any = null): TemplateBinding[] {
|
||||||
|
return parseTemplateBindingsResult(text, location).templateBindings;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseInterpolation(text: string, location: any = null): any {
|
function parseInterpolation(text: string, location: any = null): ASTWithSource {
|
||||||
return createParser().parseInterpolation(text, location);
|
return createParser().parseInterpolation(text, location);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseSimpleBinding(text: string, location: any = null): any {
|
function parseSimpleBinding(text: string, location: any = null): ASTWithSource {
|
||||||
return createParser().parseSimpleBinding(text, location);
|
return createParser().parseSimpleBinding(text, location);
|
||||||
}
|
}
|
||||||
|
|
||||||
function unparse(ast: AST): string { return new Unparser().unparse(ast); }
|
|
||||||
|
|
||||||
function checkInterpolation(exp: string, expected?: string) {
|
function checkInterpolation(exp: string, expected?: string) {
|
||||||
var ast = parseInterpolation(exp);
|
var ast = parseInterpolation(exp);
|
||||||
if (isBlank(expected)) expected = exp;
|
if (isBlank(expected)) expected = exp;
|
||||||
expect(unparse(ast)).toEqual(expected);
|
expect(unparse(ast)).toEqual(expected);
|
||||||
|
validate(ast);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkBinding(exp: string, expected?: string) {
|
function checkBinding(exp: string, expected?: string) {
|
||||||
var ast = parseBinding(exp);
|
var ast = parseBinding(exp);
|
||||||
if (isBlank(expected)) expected = exp;
|
if (isBlank(expected)) expected = exp;
|
||||||
expect(unparse(ast)).toEqual(expected);
|
expect(unparse(ast)).toEqual(expected);
|
||||||
|
validate(ast);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkAction(exp: string, expected?: string) {
|
function checkAction(exp: string, expected?: string) {
|
||||||
var ast = parseAction(exp);
|
var ast = parseAction(exp);
|
||||||
if (isBlank(expected)) expected = exp;
|
if (isBlank(expected)) expected = exp;
|
||||||
expect(unparse(ast)).toEqual(expected);
|
expect(unparse(ast)).toEqual(expected);
|
||||||
|
validate(ast);
|
||||||
}
|
}
|
||||||
|
|
||||||
function expectActionError(text: any /** TODO #9100 */) {
|
function expectError(ast: {errors: ParserError[]}, message: string) {
|
||||||
return expect(() => parseAction(text));
|
for (var error of ast.errors) {
|
||||||
|
if (error.message.indexOf(message) >= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw Error(`Expected an error containing "${message}" to be reported`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function expectBindingError(text: any /** TODO #9100 */) {
|
function expectActionError(text: string, message: string) {
|
||||||
return expect(() => parseBinding(text));
|
expectError(validate(parseAction(text)), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectBindingError(text: string, message: string) {
|
||||||
|
expectError(validate(parseBinding(text)), message);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('parser', () => {
|
describe('parser', () => {
|
||||||
|
@ -140,8 +155,8 @@ export function main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should only allow identifier, string, or keyword as map key', () => {
|
it('should only allow identifier, string, or keyword as map key', () => {
|
||||||
expectActionError('{(:0}').toThrowError(/expected identifier, keyword, or string/);
|
expectActionError('{(:0}', 'expected identifier, keyword, or string');
|
||||||
expectActionError('{1234:0}').toThrowError(/expected identifier, keyword, or string/);
|
expectActionError('{1234:0}', 'expected identifier, keyword, or string');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -152,9 +167,9 @@ export function main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should only allow identifier or keyword as member names', () => {
|
it('should only allow identifier or keyword as member names', () => {
|
||||||
expectActionError('x.(').toThrowError(/identifier or keyword/);
|
expectActionError('x.(', 'identifier or keyword');
|
||||||
expectActionError('x. 1234').toThrowError(/identifier or keyword/);
|
expectActionError('x. 1234', 'identifier or keyword');
|
||||||
expectActionError('x."foo"').toThrowError(/identifier or keyword/);
|
expectActionError('x."foo"', 'identifier or keyword');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse safe field access', () => {
|
it('should parse safe field access', () => {
|
||||||
|
@ -182,9 +197,8 @@ export function main() {
|
||||||
checkAction('false ? 10 : 20');
|
checkAction('false ? 10 : 20');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw on incorrect ternary operator syntax', () => {
|
it('should report incorrect ternary operator syntax', () => {
|
||||||
expectActionError('true?1').toThrowError(
|
expectActionError('true?1', 'Conditional expression true?1 requires all 3 expressions');
|
||||||
/Parser Error: Conditional expression true\?1 requires all 3 expressions/);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -195,39 +209,35 @@ export function main() {
|
||||||
checkAction('a = 123; b = 234;');
|
checkAction('a = 123; b = 234;');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw on safe field assignments', () => {
|
it('should report on safe field assignments',
|
||||||
expectActionError('a?.a = 123').toThrowError(/cannot be used in the assignment/);
|
() => { expectActionError('a?.a = 123', 'cannot be used in the assignment'); });
|
||||||
});
|
|
||||||
|
|
||||||
it('should support array updates', () => { checkAction('a[0] = 200'); });
|
it('should support array updates', () => { checkAction('a[0] = 200'); });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should error when using pipes',
|
it('should error when using pipes',
|
||||||
() => { expectActionError('x|blah').toThrowError(/Cannot have a pipe/); });
|
() => { expectActionError('x|blah', 'Cannot have a pipe'); });
|
||||||
|
|
||||||
it('should store the source in the result',
|
it('should store the source in the result',
|
||||||
() => { expect(parseAction('someExpr').source).toBe('someExpr'); });
|
() => { expect(parseAction('someExpr', 'someExpr')); });
|
||||||
|
|
||||||
it('should store the passed-in location',
|
it('should store the passed-in location',
|
||||||
() => { expect(parseAction('someExpr', 'location').location).toBe('location'); });
|
() => { expect(parseAction('someExpr', 'location').location).toBe('location'); });
|
||||||
|
|
||||||
it('should throw when encountering interpolation', () => {
|
it('should report when encountering interpolation', () => {
|
||||||
expectActionError('{{a()}}').toThrowError(
|
expectActionError('{{a()}}', 'Got interpolation ({{}}) where expression was expected');
|
||||||
/Got interpolation \(\{\{\}\}\) where expression was expected/);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('general error handling', () => {
|
describe('general error handling', () => {
|
||||||
it('should throw on an unexpected token',
|
it('should report an unexpected token',
|
||||||
() => { expectActionError('[1,2] trac').toThrowError(/Unexpected token \'trac\'/); });
|
() => { expectActionError('[1,2] trac', 'Unexpected token \'trac\''); });
|
||||||
|
|
||||||
it('should throw a reasonable error for unconsumed tokens', () => {
|
it('should report reasonable error for unconsumed tokens',
|
||||||
expectActionError(')').toThrowError(/Unexpected token \) at column 1 in \[\)\]/);
|
() => { expectActionError(')', 'Unexpected token ) at column 1 in [)]'); });
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw on missing expected token', () => {
|
it('should report a missing expected token', () => {
|
||||||
expectActionError('a(b').toThrowError(
|
expectActionError('a(b', 'Missing expected ) at the end of the expression [a(b]');
|
||||||
/Missing expected \) at the end of the expression \[a\(b\]/);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -246,9 +256,9 @@ export function main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should only allow identifier or keyword as formatter names', () => {
|
it('should only allow identifier or keyword as formatter names', () => {
|
||||||
expectBindingError('"Foo"|(').toThrowError(/identifier or keyword/);
|
expectBindingError('"Foo"|(', 'identifier or keyword');
|
||||||
expectBindingError('"Foo"|1234').toThrowError(/identifier or keyword/);
|
expectBindingError('"Foo"|1234', 'identifier or keyword');
|
||||||
expectBindingError('"Foo"|"uppercase"').toThrowError(/identifier or keyword/);
|
expectBindingError('"Foo"|"uppercase"', 'identifier or keyword');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse quoted expressions', () => { checkBinding('a:b', 'a:b'); });
|
it('should parse quoted expressions', () => { checkBinding('a:b', 'a:b'); });
|
||||||
|
@ -259,8 +269,8 @@ export function main() {
|
||||||
it('should ignore whitespace around quote prefix', () => { checkBinding(' a :b', 'a:b'); });
|
it('should ignore whitespace around quote prefix', () => { checkBinding(' a :b', 'a:b'); });
|
||||||
|
|
||||||
it('should refuse prefixes that are not single identifiers', () => {
|
it('should refuse prefixes that are not single identifiers', () => {
|
||||||
expectBindingError('a + b:c').toThrowError();
|
expectBindingError('a + b:c', '');
|
||||||
expectBindingError('1:c').toThrowError();
|
expectBindingError('1:c', '');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -270,15 +280,14 @@ export function main() {
|
||||||
it('should store the passed-in location',
|
it('should store the passed-in location',
|
||||||
() => { expect(parseBinding('someExpr', 'location').location).toBe('location'); });
|
() => { expect(parseBinding('someExpr', 'location').location).toBe('location'); });
|
||||||
|
|
||||||
it('should throw on chain expressions',
|
it('should report chain expressions',
|
||||||
() => { expect(() => parseBinding('1;2')).toThrowError(/contain chained expression/); });
|
() => { expectError(parseBinding('1;2'), 'contain chained expression'); });
|
||||||
|
|
||||||
it('should throw on assignment',
|
it('should report assignment',
|
||||||
() => { expect(() => parseBinding('a=2')).toThrowError(/contain assignments/); });
|
() => { expectError(parseBinding('a=2'), 'contain assignments'); });
|
||||||
|
|
||||||
it('should throw when encountering interpolation', () => {
|
it('should report when encountering interpolation', () => {
|
||||||
expectBindingError('{{a.b}}').toThrowError(
|
expectBindingError('{{a.b}}', 'Got interpolation ({{}}) where expression was expected');
|
||||||
/Got interpolation \(\{\{\}\}\) where expression was expected/);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse conditional expression', () => { checkBinding('a < b ? a : b'); });
|
it('should parse conditional expression', () => { checkBinding('a < b ? a : b'); });
|
||||||
|
@ -331,13 +340,10 @@ export function main() {
|
||||||
bindings = parseTemplateBindings('a-b:\'c\'');
|
bindings = parseTemplateBindings('a-b:\'c\'');
|
||||||
expect(keys(bindings)).toEqual(['a-b']);
|
expect(keys(bindings)).toEqual(['a-b']);
|
||||||
|
|
||||||
expect(() => {
|
expectError(parseTemplateBindingsResult('(:0'), 'expected identifier, keyword, or string');
|
||||||
parseTemplateBindings('(:0');
|
|
||||||
}).toThrowError(/expected identifier, keyword, or string/);
|
|
||||||
|
|
||||||
expect(() => {
|
expectError(
|
||||||
parseTemplateBindings('1234:0');
|
parseTemplateBindingsResult('1234:0'), 'expected identifier, keyword, or string');
|
||||||
}).toThrowError(/expected identifier, keyword, or string/);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should detect expressions as value', () => {
|
it('should detect expressions as value', () => {
|
||||||
|
@ -436,7 +442,7 @@ export function main() {
|
||||||
() => { expect(parseInterpolation('nothing')).toBe(null); });
|
() => { expect(parseInterpolation('nothing')).toBe(null); });
|
||||||
|
|
||||||
it('should parse no prefix/suffix interpolation', () => {
|
it('should parse no prefix/suffix interpolation', () => {
|
||||||
var ast = parseInterpolation('{{a}}').ast;
|
var ast = parseInterpolation('{{a}}').ast as Interpolation;
|
||||||
expect(ast.strings).toEqual(['', '']);
|
expect(ast.strings).toEqual(['', '']);
|
||||||
expect(ast.expressions.length).toEqual(1);
|
expect(ast.expressions.length).toEqual(1);
|
||||||
expect(ast.expressions[0].name).toEqual('a');
|
expect(ast.expressions[0].name).toEqual('a');
|
||||||
|
@ -445,17 +451,18 @@ export function main() {
|
||||||
it('should parse prefix/suffix with multiple interpolation', () => {
|
it('should parse prefix/suffix with multiple interpolation', () => {
|
||||||
var originalExp = 'before {{ a }} middle {{ b }} after';
|
var originalExp = 'before {{ a }} middle {{ b }} after';
|
||||||
var ast = parseInterpolation(originalExp).ast;
|
var ast = parseInterpolation(originalExp).ast;
|
||||||
expect(new Unparser().unparse(ast)).toEqual(originalExp);
|
expect(unparse(ast)).toEqual(originalExp);
|
||||||
|
validate(ast);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw on empty interpolation expressions', () => {
|
it('should report empty interpolation expressions', () => {
|
||||||
expect(() => parseInterpolation('{{}}'))
|
expectError(
|
||||||
.toThrowError(
|
parseInterpolation('{{}}'),
|
||||||
/Parser Error: Blank expressions are not allowed in interpolated strings/);
|
'Blank expressions are not allowed in interpolated strings');
|
||||||
|
|
||||||
expect(() => parseInterpolation('foo {{ }}'))
|
expectError(
|
||||||
.toThrowError(
|
parseInterpolation('foo {{ }}'),
|
||||||
/Parser Error: Blank expressions are not allowed in interpolated strings/);
|
'Parser Error: Blank expressions are not allowed in interpolated strings');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse conditional expression',
|
it('should parse conditional expression',
|
||||||
|
@ -503,28 +510,47 @@ export function main() {
|
||||||
it('should parse a field access', () => {
|
it('should parse a field access', () => {
|
||||||
var p = parseSimpleBinding('name');
|
var p = parseSimpleBinding('name');
|
||||||
expect(unparse(p)).toEqual('name');
|
expect(unparse(p)).toEqual('name');
|
||||||
|
validate(p);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse a constant', () => {
|
it('should parse a constant', () => {
|
||||||
var p = parseSimpleBinding('[1, 2]');
|
var p = parseSimpleBinding('[1, 2]');
|
||||||
expect(unparse(p)).toEqual('[1, 2]');
|
expect(unparse(p)).toEqual('[1, 2]');
|
||||||
|
validate(p);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw when the given expression is not just a field name', () => {
|
it('should report when the given expression is not just a field name', () => {
|
||||||
expect(() => parseSimpleBinding('name + 1'))
|
expectError(
|
||||||
.toThrowError(/Host binding expression can only contain field access and constants/);
|
validate(parseSimpleBinding('name + 1')),
|
||||||
|
'Host binding expression can only contain field access and constants');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw when encountering interpolation', () => {
|
it('should report when encountering interpolation', () => {
|
||||||
expect(() => parseSimpleBinding('{{exp}}'))
|
expectError(
|
||||||
.toThrowError(/Got interpolation \(\{\{\}\}\) where expression was expected/);
|
validate(parseSimpleBinding('{{exp}}')),
|
||||||
|
'Got interpolation ({{}}) where expression was expected');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('wrapLiteralPrimitive', () => {
|
describe('wrapLiteralPrimitive', () => {
|
||||||
it('should wrap a literal primitive', () => {
|
it('should wrap a literal primitive', () => {
|
||||||
expect(unparse(createParser().wrapLiteralPrimitive('foo', null))).toEqual('"foo"');
|
expect(unparse(validate(createParser().wrapLiteralPrimitive('foo', null))))
|
||||||
|
.toEqual('"foo"');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('error recovery', () => {
|
||||||
|
function recover(text: string, expected?: string) {
|
||||||
|
let expr = validate(parseAction(text));
|
||||||
|
expect(unparse(expr)).toEqual(expected || text);
|
||||||
|
}
|
||||||
|
it('should be able to recover from an extra paren', () => recover('((a)))', 'a'));
|
||||||
|
it('should be able to recover from an extra bracket', () => recover('[[a]]]', '[[a]]'));
|
||||||
|
it('should be able to recover from a missing )', () => recover('(a;b', 'a; b;'));
|
||||||
|
it('should be able to recover from a missing ]', () => recover('[a,b', '[a, b]'));
|
||||||
|
it('should be able to recover from a missing selector', () => recover('a.'));
|
||||||
|
it('should be able to recover from a missing selector in a array literal',
|
||||||
|
() => recover('[[a.], b, c]'))
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,12 +10,12 @@ import {AST, AstVisitor, Binary, BindingPipe, Chain, Conditional, EmptyExpr, Fun
|
||||||
import {StringWrapper, isPresent, isString} from '../../src/facade/lang';
|
import {StringWrapper, isPresent, isString} from '../../src/facade/lang';
|
||||||
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../src/interpolation_config';
|
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../src/interpolation_config';
|
||||||
|
|
||||||
export class Unparser implements AstVisitor {
|
class Unparser implements AstVisitor {
|
||||||
private static _quoteRegExp = /"/g;
|
private static _quoteRegExp = /"/g;
|
||||||
private _expression: string;
|
private _expression: string;
|
||||||
private _interpolationConfig: InterpolationConfig;
|
private _interpolationConfig: InterpolationConfig;
|
||||||
|
|
||||||
unparse(ast: AST, interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG) {
|
unparse(ast: AST, interpolationConfig: InterpolationConfig) {
|
||||||
this._expression = '';
|
this._expression = '';
|
||||||
this._interpolationConfig = interpolationConfig;
|
this._interpolationConfig = interpolationConfig;
|
||||||
this._visit(ast);
|
this._visit(ast);
|
||||||
|
@ -180,3 +180,10 @@ export class Unparser implements AstVisitor {
|
||||||
|
|
||||||
private _visit(ast: AST) { ast.visit(this); }
|
private _visit(ast: AST) { ast.visit(this); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sharedUnparser = new Unparser();
|
||||||
|
|
||||||
|
export function unparse(
|
||||||
|
ast: AST, interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): string {
|
||||||
|
return sharedUnparser.unparse(ast, interpolationConfig);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
import {AST, Binary, BindingPipe, Chain, Conditional, EmptyExpr, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, ParseSpan, PrefixNot, PropertyRead, PropertyWrite, Quote, RecursiveAstVisitor, SafeMethodCall, SafePropertyRead} from '../../src/expression_parser/ast';
|
||||||
|
|
||||||
|
import {unparse} from './unparser';
|
||||||
|
|
||||||
|
class ASTValidator extends RecursiveAstVisitor {
|
||||||
|
private parentSpan: ParseSpan|undefined;
|
||||||
|
|
||||||
|
visit(ast: AST) {
|
||||||
|
this.parentSpan = undefined;
|
||||||
|
ast.visit(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
validate(ast: AST, cb: () => void): void {
|
||||||
|
if (!inSpan(ast.span, this.parentSpan)) {
|
||||||
|
throw Error(
|
||||||
|
`Invalid AST span [expected (${ast.span.start}, ${ast.span.end}) to be in (${this.parentSpan.start}, ${this.parentSpan.end}) for ${unparse(ast)}`);
|
||||||
|
}
|
||||||
|
const oldParent = this.parentSpan;
|
||||||
|
this.parentSpan = ast.span;
|
||||||
|
cb();
|
||||||
|
this.parentSpan = oldParent;
|
||||||
|
}
|
||||||
|
|
||||||
|
visitBinary(ast: Binary, context: any): any {
|
||||||
|
this.validate(ast, () => super.visitBinary(ast, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
visitChain(ast: Chain, context: any): any {
|
||||||
|
this.validate(ast, () => super.visitChain(ast, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
visitConditional(ast: Conditional, context: any): any {
|
||||||
|
this.validate(ast, () => super.visitConditional(ast, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
visitFunctionCall(ast: FunctionCall, context: any): any {
|
||||||
|
this.validate(ast, () => super.visitFunctionCall(ast, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
visitImplicitReceiver(ast: ImplicitReceiver, context: any): any {
|
||||||
|
this.validate(ast, () => super.visitImplicitReceiver(ast, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
visitInterpolation(ast: Interpolation, context: any): any {
|
||||||
|
this.validate(ast, () => super.visitInterpolation(ast, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
visitKeyedRead(ast: KeyedRead, context: any): any {
|
||||||
|
this.validate(ast, () => super.visitKeyedRead(ast, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
visitKeyedWrite(ast: KeyedWrite, context: any): any {
|
||||||
|
this.validate(ast, () => super.visitKeyedWrite(ast, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
visitLiteralArray(ast: LiteralArray, context: any): any {
|
||||||
|
this.validate(ast, () => super.visitLiteralArray(ast, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
visitLiteralMap(ast: LiteralMap, context: any): any {
|
||||||
|
this.validate(ast, () => super.visitLiteralMap(ast, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
visitLiteralPrimitive(ast: LiteralPrimitive, context: any): any {
|
||||||
|
this.validate(ast, () => super.visitLiteralPrimitive(ast, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
visitMethodCall(ast: MethodCall, context: any): any {
|
||||||
|
this.validate(ast, () => super.visitMethodCall(ast, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
visitPipe(ast: BindingPipe, context: any): any {
|
||||||
|
this.validate(ast, () => super.visitPipe(ast, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
visitPrefixNot(ast: PrefixNot, context: any): any {
|
||||||
|
this.validate(ast, () => super.visitPrefixNot(ast, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
visitPropertyRead(ast: PropertyRead, context: any): any {
|
||||||
|
this.validate(ast, () => super.visitPropertyRead(ast, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
visitPropertyWrite(ast: PropertyWrite, context: any): any {
|
||||||
|
this.validate(ast, () => super.visitPropertyWrite(ast, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
visitQuote(ast: Quote, context: any): any {
|
||||||
|
this.validate(ast, () => super.visitQuote(ast, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
visitSafeMethodCall(ast: SafeMethodCall, context: any): any {
|
||||||
|
this.validate(ast, () => super.visitSafeMethodCall(ast, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
visitSafePropertyRead(ast: SafePropertyRead, context: any): any {
|
||||||
|
this.validate(ast, () => super.visitSafePropertyRead(ast, context));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function inSpan(span: ParseSpan, parentSpan: ParseSpan | undefined): parentSpan is ParseSpan {
|
||||||
|
return !parentSpan || (span.start >= parentSpan.start && span.end <= parentSpan.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sharedValidator = new ASTValidator();
|
||||||
|
|
||||||
|
export function validate<T extends AST>(ast: T): T {
|
||||||
|
sharedValidator.visit(ast);
|
||||||
|
return ast;
|
||||||
|
}
|
|
@ -19,11 +19,9 @@ import {afterEach, beforeEach, beforeEachProviders, ddescribe, describe, expect,
|
||||||
import {Identifiers, identifierToken} from '../src/identifiers';
|
import {Identifiers, identifierToken} from '../src/identifiers';
|
||||||
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../src/interpolation_config';
|
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../src/interpolation_config';
|
||||||
|
|
||||||
import {Unparser} from './expression_parser/unparser';
|
import {unparse} from './expression_parser/unparser';
|
||||||
import {TEST_PROVIDERS} from './test_bindings';
|
import {TEST_PROVIDERS} from './test_bindings';
|
||||||
|
|
||||||
var expressionUnparser = new Unparser();
|
|
||||||
|
|
||||||
var someModuleUrl = 'package:someModule';
|
var someModuleUrl = 'package:someModule';
|
||||||
|
|
||||||
var MOCK_SCHEMA_REGISTRY = [{
|
var MOCK_SCHEMA_REGISTRY = [{
|
||||||
|
@ -809,7 +807,7 @@ There is no directive with "exportAs" set to "dirA" ("<div [ERROR ->]#a="dirA"><
|
||||||
expect(() => parse('<div #a></div><div #a></div>', []))
.toThrowError(
|
expect(() => parse('<div #a></div><div #a></div>', []))
.toThrowError(
|
||||||
`Template parse errors:
|
`Template parse errors:
|
||||||
Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>"): TestComp@0:19`);
|
Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>"): TestComp@0:19`);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it(
|
it(
|
||||||
|
@ -817,7 +815,7 @@ Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>
|
||||||
() => {
|
() => {
|
||||||
expect(() => parse('<div #a><template #a><span>OK</span></template></div>', []))
|
expect(() => parse('<div #a><template #a><span>OK</span></template></div>', []))
|
||||||
.not.toThrowError();
|
.not.toThrowError();
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should assign references with empty value to components', () => {
|
it('should assign references with empty value to components', () => {
|
||||||
|
@ -1504,17 +1502,14 @@ class TemplateHumanizer implements TemplateAstVisitor {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
visitEvent(ast: BoundEventAst, context: any): any {
|
visitEvent(ast: BoundEventAst, context: any): any {
|
||||||
var res = [
|
var res = [BoundEventAst, ast.name, ast.target, unparse(ast.handler, this.interpolationConfig)];
|
||||||
BoundEventAst, ast.name, ast.target,
|
|
||||||
expressionUnparser.unparse(ast.handler, this.interpolationConfig)
|
|
||||||
];
|
|
||||||
this.result.push(this._appendContext(ast, res));
|
this.result.push(this._appendContext(ast, res));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
visitElementProperty(ast: BoundElementPropertyAst, context: any): any {
|
visitElementProperty(ast: BoundElementPropertyAst, context: any): any {
|
||||||
var res = [
|
var res = [
|
||||||
BoundElementPropertyAst, ast.type, ast.name,
|
BoundElementPropertyAst, ast.type, ast.name, unparse(ast.value, this.interpolationConfig),
|
||||||
expressionUnparser.unparse(ast.value, this.interpolationConfig), ast.unit
|
ast.unit
|
||||||
];
|
];
|
||||||
this.result.push(this._appendContext(ast, res));
|
this.result.push(this._appendContext(ast, res));
|
||||||
return null;
|
return null;
|
||||||
|
@ -1525,7 +1520,7 @@ class TemplateHumanizer implements TemplateAstVisitor {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
visitBoundText(ast: BoundTextAst, context: any): any {
|
visitBoundText(ast: BoundTextAst, context: any): any {
|
||||||
var res = [BoundTextAst, expressionUnparser.unparse(ast.value, this.interpolationConfig)];
|
var res = [BoundTextAst, unparse(ast.value, this.interpolationConfig)];
|
||||||
this.result.push(this._appendContext(ast, res));
|
this.result.push(this._appendContext(ast, res));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1544,8 +1539,7 @@ class TemplateHumanizer implements TemplateAstVisitor {
|
||||||
}
|
}
|
||||||
visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any {
|
visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any {
|
||||||
var res = [
|
var res = [
|
||||||
BoundDirectivePropertyAst, ast.directiveName,
|
BoundDirectivePropertyAst, ast.directiveName, unparse(ast.value, this.interpolationConfig)
|
||||||
expressionUnparser.unparse(ast.value, this.interpolationConfig)
|
|
||||||
];
|
];
|
||||||
this.result.push(this._appendContext(ast, res));
|
this.result.push(this._appendContext(ast, res));
|
||||||
return null;
|
return null;
|
||||||
|
@ -1590,7 +1584,7 @@ class TemplateContentProjectionHumanizer implements TemplateAstVisitor {
|
||||||
visitElementProperty(ast: BoundElementPropertyAst, context: any): any { return null; }
|
visitElementProperty(ast: BoundElementPropertyAst, context: any): any { return null; }
|
||||||
visitAttr(ast: AttrAst, context: any): any { return null; }
|
visitAttr(ast: AttrAst, context: any): any { return null; }
|
||||||
visitBoundText(ast: BoundTextAst, context: any): any {
|
visitBoundText(ast: BoundTextAst, context: any): any {
|
||||||
this.result.push([`#text(${expressionUnparser.unparse(ast.value)})`, ast.ngContentIndex]);
|
this.result.push([`#text(${unparse(ast.value)})`, ast.ngContentIndex]);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
visitText(ast: TextAst, context: any): any {
|
visitText(ast: TextAst, context: any): any {
|
||||||
|
|
Loading…
Reference in New Issue