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:
Chuck Jazdzewski 2016-07-06 14:06:47 -07:00 committed by GitHub
parent ae62f082fd
commit 9a04fcd061
9 changed files with 572 additions and 300 deletions

View File

@ -7,8 +7,22 @@
*/
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 {
constructor(public span: ParseSpan) {}
visit(visitor: AstVisitor, context: any = null): any { return null; }
toString(): string { return 'AST'; }
}
@ -27,8 +41,10 @@ export class AST {
* therefore not interpreted by the Angular's own expression parser.
*/
export class Quote extends AST {
constructor(public prefix: string, public uninterpretedExpression: string, public location: any) {
super();
constructor(
span: ParseSpan, public prefix: string, public uninterpretedExpression: string,
public location: any) {
super(span);
}
visit(visitor: AstVisitor, context: any = null): any { return visitor.visitQuote(this, context); }
toString(): string { return 'Quote'; }
@ -50,122 +66,138 @@ export class ImplicitReceiver extends AST {
* Multiple expressions separated by a semicolon.
*/
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); }
}
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 {
return visitor.visitConditional(this, context);
}
}
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 {
return visitor.visitPropertyRead(this, context);
}
}
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 {
return visitor.visitPropertyWrite(this, context);
}
}
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 {
return visitor.visitSafePropertyRead(this, context);
}
}
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 {
return visitor.visitKeyedRead(this, context);
}
}
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 {
return visitor.visitKeyedWrite(this, context);
}
}
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); }
}
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 {
return visitor.visitLiteralPrimitive(this, context);
}
}
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 {
return visitor.visitLiteralArray(this, context);
}
}
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 {
return visitor.visitLiteralMap(this, context);
}
}
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 {
return visitor.visitInterpolation(this, context);
}
}
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 {
return visitor.visitBinary(this, context);
}
}
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 {
return visitor.visitPrefixNot(this, context);
}
}
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 {
return visitor.visitMethodCall(this, context);
}
}
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 {
return visitor.visitSafeMethodCall(this, context);
}
}
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 {
return visitor.visitFunctionCall(this, context);
}
}
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); }
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; }
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 {
return new LiteralPrimitive(ast.value);
return new LiteralPrimitive(ast.span, ast.value);
}
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 {
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 {
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 {
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 {
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 {
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 {
return new LiteralArray(this.visitAll(ast.expressions));
return new LiteralArray(ast.span, this.visitAll(ast.expressions));
}
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 {
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 {
return new PrefixNot(ast.expression.visit(this));
return new PrefixNot(ast.span, ast.expression.visit(this));
}
visitConditional(ast: Conditional, context: any): AST {
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 {
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 {
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 {
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[] {
@ -349,9 +383,11 @@ export class AstTransformer implements AstVisitor {
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 {
return new Quote(ast.prefix, ast.uninterpretedExpression, ast.location);
return new Quote(ast.span, ast.prefix, ast.uninterpretedExpression, ast.location);
}
}

View File

@ -18,7 +18,8 @@ export enum TokenType {
Keyword,
String,
Operator,
Number
Number,
Error
}
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'); }
isError(): boolean { return this.type == TokenType.Error; }
toNumber(): number {
// -1 instead of NULL ok?
return (this.type == TokenType.Number) ? this.numValue : -1;
@ -86,6 +89,7 @@ export class Token {
case TokenType.Keyword:
case TokenType.Operator:
case TokenType.String:
case TokenType.Error:
return this.strValue;
case TokenType.Number:
return this.numValue.toString();
@ -119,14 +123,12 @@ function newNumberToken(index: number, n: number): Token {
return new Token(index, TokenType.Number, n, '');
}
export var EOF: Token = new Token(-1, TokenType.Character, 0, '');
export class ScannerError extends BaseException {
constructor(public message: string) { super(); }
toString(): string { return this.message; }
function newErrorToken(index: number, message: string): Token {
return new Token(index, TokenType.Error, 0, message);
}
export var EOF: Token = new Token(-1, TokenType.Character, 0, '');
class _Scanner {
length: number;
peek: number = 0;
@ -211,8 +213,8 @@ class _Scanner {
return this.scanToken();
}
this.error(`Unexpected character [${StringWrapper.fromCharCode(peek)}]`, 0);
return null;
this.advance();
return this.error(`Unexpected character [${StringWrapper.fromCharCode(peek)}]`, 0);
}
scanCharacter(start: number, code: number): Token {
@ -273,7 +275,7 @@ class _Scanner {
} else if (isExponentStart(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;
} else {
break;
@ -307,7 +309,7 @@ class _Scanner {
try {
unescapedCode = NumberWrapper.parseInt(hex, 16);
} 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++) {
this.advance();
@ -319,7 +321,7 @@ class _Scanner {
buffer.add(StringWrapper.fromCharCode(unescapedCode));
marker = this.index;
} else if (this.peek == chars.$EOF) {
this.error('Unterminated quote', 0);
return this.error('Unterminated quote', 0);
} else {
this.advance();
}
@ -337,10 +339,10 @@ class _Scanner {
return newStringToken(start, unescaped);
}
error(message: string, offset: number) {
var position: number = this.index + offset;
throw new ScannerError(
`Lexer Error: ${message} at column ${position} in expression [${this.input}]`);
error(message: string, offset: number): Token {
const position: number = this.index + offset;
return newErrorToken(
position, `Lexer Error: ${message} at column ${position} in expression [${this.input}]`);
}
}

View File

@ -14,24 +14,18 @@ import {BaseException} from '../facade/exceptions';
import {RegExpWrapper, StringWrapper, escapeRegExp, isBlank, isPresent} from '../facade/lang';
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 {EOF, Lexer, Token, isIdentifier, isQuote} from './lexer';
import {AST, ASTWithSource, AstVisitor, Binary, BindingPipe, Chain, Conditional, EmptyExpr, FunctionCall, ImplicitReceiver, Interpolation, KeyedRead, KeyedWrite, LiteralArray, LiteralMap, LiteralPrimitive, MethodCall, ParseSpan, ParserError, PrefixNot, PropertyRead, PropertyWrite, Quote, SafeMethodCall, SafePropertyRead, TemplateBinding} from './ast';
import {EOF, Lexer, Token, TokenType, isIdentifier, isQuote} from './lexer';
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 {
constructor(public strings: string[], public expressions: string[]) {}
}
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 {
@ -41,6 +35,8 @@ function _createInterpolateRegExp(config: InterpolationConfig): RegExp {
@Injectable()
export class Parser {
private errors: ParserError[] = [];
constructor(/** @internal */
public _lexer: Lexer) {}
@ -49,15 +45,15 @@ export class Parser {
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
this._checkNoInterpolation(input, location, interpolationConfig);
var tokens = this._lexer.tokenize(this._stripComments(input));
var ast = new _ParseAST(input, location, tokens, true).parseChain();
return new ASTWithSource(ast, input, location);
var ast = new _ParseAST(input, location, tokens, true, this.errors).parseChain();
return new ASTWithSource(ast, input, location, this.errors);
}
parseBinding(
input: string, location: any,
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
var ast = this._parseBindingAst(input, location, interpolationConfig);
return new ASTWithSource(ast, input, location);
return new ASTWithSource(ast, input, location, this.errors);
}
parseSimpleBinding(
@ -65,10 +61,14 @@ export class Parser {
interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG): ASTWithSource {
var ast = this._parseBindingAst(input, location, interpolationConfig);
if (!SimpleExpressionChecker.check(ast)) {
throw new ParseException(
this._reportError(
'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(
@ -83,7 +83,7 @@ export class Parser {
this._checkNoInterpolation(input, location, interpolationConfig);
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 {
@ -93,12 +93,12 @@ export class Parser {
var prefix = input.substring(0, prefixSeparatorIndex).trim();
if (!isIdentifier(prefix)) return null;
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 {
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(
@ -111,11 +111,14 @@ export class Parser {
for (let i = 0; i < split.expressions.length; ++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);
}
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(
@ -137,7 +140,7 @@ export class Parser {
} else if (part.trim().length > 0) {
expressions.push(part);
} else {
throw new ParseException(
this._reportError(
'Blank expressions are not allowed in interpolated strings', input,
`at column ${this._findInterpolationErrorColumn(parts, i, interpolationConfig)} in`,
location);
@ -147,7 +150,9 @@ export class Parser {
}
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 {
@ -177,7 +182,7 @@ export class Parser {
var regexp = _createInterpolateRegExp(interpolationConfig);
var parts = StringWrapper.split(input, regexp);
if (parts.length > 1) {
throw new ParseException(
this._reportError(
`Got interpolation (${interpolationConfig.start}${interpolationConfig.end}) where expression was expected`,
input,
`at column ${this._findInterpolationErrorColumn(parts, 1, interpolationConfig)} in`,
@ -199,10 +204,15 @@ export class Parser {
}
export class _ParseAST {
private rparensExpected = 0;
private rbracketsExpected = 0;
private rbracesExpected = 0;
index: number = 0;
constructor(
public input: string, public location: any, public tokens: any[],
public parseAction: boolean) {}
public input: string, public location: any, public tokens: any[], public parseAction: boolean,
private errors: ParserError[]) {}
peek(offset: number): Token {
var i = this.index + offset;
@ -215,6 +225,8 @@ export class _ParseAST {
return (this.index < this.tokens.length) ? this.next.index : this.input.length;
}
span(start: number) { return new ParseSpan(start, this.inputIndex); }
advance() { this.index++; }
optionalCharacter(code: number): boolean {
@ -256,6 +268,7 @@ export class _ParseAST {
var n = this.next;
if (!n.isIdentifier() && !n.isKeyword()) {
this.error(`Unexpected token ${n}, expected identifier or keyword`);
return '';
}
this.advance();
return n.toString();
@ -265,6 +278,7 @@ export class _ParseAST {
var n = this.next;
if (!n.isIdentifier() && !n.isKeyword() && !n.isString()) {
this.error(`Unexpected token ${n}, expected identifier, keyword, or string`);
return '';
}
this.advance();
return n.toString();
@ -272,6 +286,7 @@ export class _ParseAST {
parseChain(): AST {
var exprs: AST[] = [];
const start = this.inputIndex;
while (this.index < this.tokens.length) {
var expr = this.parsePipe();
exprs.push(expr);
@ -286,9 +301,9 @@ export class _ParseAST {
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];
return new Chain(exprs);
return new Chain(this.span(start), exprs);
}
parsePipe(): AST {
@ -304,7 +319,7 @@ export class _ParseAST {
while (this.optionalCharacter(chars.$COLON)) {
args.push(this.parseExpression());
}
result = new BindingPipe(result, name, args);
result = new BindingPipe(this.span(result.span.start), result, name, args);
} while (this.optionalOperator('|'));
}
@ -314,18 +329,21 @@ export class _ParseAST {
parseExpression(): AST { return this.parseConditional(); }
parseConditional(): AST {
var start = this.inputIndex;
var result = this.parseLogicalOr();
const start = this.inputIndex;
const result = this.parseLogicalOr();
if (this.optionalOperator('?')) {
var yes = this.parsePipe();
const yes = this.parsePipe();
let no: AST;
if (!this.optionalCharacter(chars.$COLON)) {
var end = this.inputIndex;
var expression = this.input.substring(start, end);
this.error(`Conditional expression ${expression} requires all 3 expressions`);
no = new EmptyExpr(this.span(start));
} else {
no = this.parsePipe();
}
var no = this.parsePipe();
return new Conditional(result, yes, no);
return new Conditional(this.span(start), result, yes, no);
} else {
return result;
}
@ -333,102 +351,126 @@ export class _ParseAST {
parseLogicalOr(): AST {
// '||'
var result = this.parseLogicalAnd();
let result = this.parseLogicalAnd();
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;
}
parseLogicalAnd(): AST {
// '&&'
var result = this.parseEquality();
let result = this.parseEquality();
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;
}
parseEquality(): AST {
// '==','!=','===','!=='
var result = this.parseRelational();
while (true) {
if (this.optionalOperator('==')) {
result = new Binary('==', result, this.parseRelational());
} else if (this.optionalOperator('===')) {
result = new Binary('===', result, this.parseRelational());
} else if (this.optionalOperator('!=')) {
result = new Binary('!=', result, this.parseRelational());
} else if (this.optionalOperator('!==')) {
result = new Binary('!==', result, this.parseRelational());
} else {
let result = this.parseRelational();
while (this.next.type == TokenType.Operator) {
let operator = this.next.strValue;
switch (operator) {
case '==':
case '===':
case '!=':
case '!==':
this.advance();
const right = this.parseRelational();
result = new Binary(this.span(result.span.start), operator, result, right);
continue;
}
break;
}
return result;
}
}
}
parseRelational(): AST {
// '<', '>', '<=', '>='
var result = this.parseAdditive();
while (true) {
if (this.optionalOperator('<')) {
result = new Binary('<', result, this.parseAdditive());
} else if (this.optionalOperator('>')) {
result = new Binary('>', result, this.parseAdditive());
} else if (this.optionalOperator('<=')) {
result = new Binary('<=', result, this.parseAdditive());
} else if (this.optionalOperator('>=')) {
result = new Binary('>=', result, this.parseAdditive());
} else {
let result = this.parseAdditive();
while (this.next.type == TokenType.Operator) {
let operator = this.next.strValue;
switch (operator) {
case '<':
case '>':
case '<=':
case '>=':
this.advance();
const right = this.parseAdditive();
result = new Binary(this.span(result.span.start), operator, result, right);
continue;
}
break;
}
return result;
}
}
}
parseAdditive(): AST {
// '+', '-'
var result = this.parseMultiplicative();
while (true) {
if (this.optionalOperator('+')) {
result = new Binary('+', result, this.parseMultiplicative());
} else if (this.optionalOperator('-')) {
result = new Binary('-', result, this.parseMultiplicative());
} else {
let result = this.parseMultiplicative();
while (this.next.type == TokenType.Operator) {
const operator = this.next.strValue;
switch (operator) {
case '+':
case '-':
this.advance();
let right = this.parseMultiplicative();
result = new Binary(this.span(result.span.start), operator, result, right);
continue;
}
break;
}
return result;
}
}
}
parseMultiplicative(): AST {
// '*', '%', '/'
var result = this.parsePrefix();
while (true) {
if (this.optionalOperator('*')) {
result = new Binary('*', result, this.parsePrefix());
} else if (this.optionalOperator('%')) {
result = new Binary('%', result, this.parsePrefix());
} else if (this.optionalOperator('/')) {
result = new Binary('/', result, this.parsePrefix());
} else {
let result = this.parsePrefix();
while (this.next.type == TokenType.Operator) {
const operator = this.next.strValue;
switch (operator) {
case '*':
case '%':
case '/':
this.advance();
let right = this.parsePrefix();
result = new Binary(this.span(result.span.start), operator, result, right);
continue;
}
break;
}
return result;
}
}
}
parsePrefix(): AST {
if (this.optionalOperator('+')) {
if (this.next.type == TokenType.Operator) {
const start = this.inputIndex;
const operator = this.next.strValue;
switch (operator) {
case '+':
this.advance();
return this.parsePrefix();
} else if (this.optionalOperator('-')) {
return new Binary('-', new LiteralPrimitive(0), this.parsePrefix());
} else if (this.optionalOperator('!')) {
return new PrefixNot(this.parsePrefix());
} else {
return this.parseCallChain();
case '-':
this.advance();
var result = this.parsePrefix();
return new Binary(
this.span(start), operator, new LiteralPrimitive(new ParseSpan(start, start), 0),
result);
case '!':
this.advance();
var result = this.parsePrefix();
return new PrefixNot(this.span(start), result);
}
}
return this.parseCallChain();
}
parseCallChain(): AST {
var result = this.parsePrimary();
let result = this.parsePrimary();
while (true) {
if (this.optionalCharacter(chars.$PERIOD)) {
result = this.parseAccessMemberOrMethodCall(result, false);
@ -437,19 +479,23 @@ export class _ParseAST {
result = this.parseAccessMemberOrMethodCall(result, true);
} else if (this.optionalCharacter(chars.$LBRACKET)) {
var key = this.parsePipe();
this.rbracketsExpected++;
const key = this.parsePipe();
this.rbracketsExpected--;
this.expectCharacter(chars.$RBRACKET);
if (this.optionalOperator('=')) {
var value = this.parseConditional();
result = new KeyedWrite(result, key, value);
const value = this.parseConditional();
result = new KeyedWrite(this.span(result.span.start), result, key, value);
} else {
result = new KeyedRead(result, key);
result = new KeyedRead(this.span(result.span.start), result, key);
}
} else if (this.optionalCharacter(chars.$LPAREN)) {
var args = this.parseCallArguments();
this.rparensExpected++;
const args = this.parseCallArguments();
this.rparensExpected--;
this.expectCharacter(chars.$RPAREN);
result = new FunctionCall(result, args);
result = new FunctionCall(this.span(result.span.start), result, args);
} else {
return result;
@ -458,55 +504,59 @@ export class _ParseAST {
}
parsePrimary(): AST {
const start = this.inputIndex;
if (this.optionalCharacter(chars.$LPAREN)) {
let result = this.parsePipe();
this.rparensExpected++;
const result = this.parsePipe();
this.rparensExpected--;
this.expectCharacter(chars.$RPAREN);
return result;
} else if (this.next.isKeywordNull() || this.next.isKeywordUndefined()) {
this.advance();
return new LiteralPrimitive(null);
return new LiteralPrimitive(this.span(start), null);
} else if (this.next.isKeywordTrue()) {
this.advance();
return new LiteralPrimitive(true);
return new LiteralPrimitive(this.span(start), true);
} else if (this.next.isKeywordFalse()) {
this.advance();
return new LiteralPrimitive(false);
return new LiteralPrimitive(this.span(start), false);
} 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);
return new LiteralArray(elements);
return new LiteralArray(this.span(start), elements);
} else if (this.next.isCharacter(chars.$LBRACE)) {
return this.parseLiteralMap();
} else if (this.next.isIdentifier()) {
return this.parseAccessMemberOrMethodCall(_implicitReceiver, false);
return this.parseAccessMemberOrMethodCall(new ImplicitReceiver(this.span(start)), false);
} else if (this.next.isNumber()) {
var value = this.next.toNumber();
const value = this.next.toNumber();
this.advance();
return new LiteralPrimitive(value);
return new LiteralPrimitive(this.span(start), value);
} else if (this.next.isString()) {
var literalValue = this.next.toString();
const literalValue = this.next.toString();
this.advance();
return new LiteralPrimitive(literalValue);
return new LiteralPrimitive(this.span(start), literalValue);
} else if (this.index >= this.tokens.length) {
this.error(`Unexpected end of expression: ${this.input}`);
return new EmptyExpr(this.span(start));
} else {
this.error(`Unexpected token ${this.next}`);
return new EmptyExpr(this.span(start));
}
// error() throws, so we don't reach here.
throw new BaseException('Fell through all cases in parsePrimary');
}
parseExpressionList(terminator: number): AST[] {
var result: AST[] = [];
let result: AST[] = [];
if (!this.next.isCharacter(terminator)) {
do {
result.push(this.parsePipe());
@ -516,51 +566,59 @@ export class _ParseAST {
}
parseLiteralMap(): LiteralMap {
var keys: string[] = [];
var values: AST[] = [];
let keys: string[] = [];
let values: AST[] = [];
const start = this.inputIndex;
this.expectCharacter(chars.$LBRACE);
if (!this.optionalCharacter(chars.$RBRACE)) {
this.rbracesExpected++;
do {
var key = this.expectIdentifierOrKeywordOrString();
keys.push(key);
this.expectCharacter(chars.$COLON);
values.push(this.parsePipe());
} while (this.optionalCharacter(chars.$COMMA));
this.rbracesExpected--;
this.expectCharacter(chars.$RBRACE);
}
return new LiteralMap(keys, values);
return new LiteralMap(this.span(start), keys, values);
}
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)) {
let args = this.parseCallArguments();
this.rparensExpected++;
const args = this.parseCallArguments();
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 {
if (isSafe) {
if (this.optionalOperator('=')) {
this.error('The \'?.\' operator cannot be used in the assignment');
return new EmptyExpr(this.span(start));
} else {
return new SafePropertyRead(receiver, id);
return new SafePropertyRead(this.span(start), receiver, id);
}
} else {
if (this.optionalOperator('=')) {
if (!this.parseAction) {
this.error('Bindings cannot contain assignments');
return new EmptyExpr(this.span(start));
}
let value = this.parseConditional();
return new PropertyWrite(receiver, id, value);
return new PropertyWrite(this.span(start), receiver, id, value);
} else {
return new PropertyRead(receiver, id);
return new PropertyRead(this.span(start), receiver, id);
}
}
}
return null;
}
parseCallArguments(): BindingPipe[] {
@ -576,8 +634,8 @@ export class _ParseAST {
* An identifier, a keyword, a string with an optional `-` inbetween.
*/
expectTemplateBindingKey(): string {
var result = '';
var operatorFound = false;
let result = '';
let operatorFound = false;
do {
result += this.expectIdentifierOrKeywordOrString();
operatorFound = this.optionalOperator('-');
@ -590,9 +648,9 @@ export class _ParseAST {
}
parseTemplateBindings(): TemplateBindingParseResult {
var bindings: TemplateBinding[] = [];
var prefix: string = null;
var warnings: string[] = [];
let bindings: TemplateBinding[] = [];
let prefix: string = null;
let warnings: string[] = [];
while (this.index < this.tokens.length) {
var keyIsVar: boolean = this.peekKeywordLet();
if (!keyIsVar && this.peekDeprecatedKeywordVar()) {
@ -626,26 +684,56 @@ export class _ParseAST {
} else if (
this.next !== EOF && !this.peekKeywordLet() && !this.peekDeprecatedKeywordVar() &&
!this.peekDeprecatedOperatorHash()) {
var start = this.inputIndex;
const start = this.inputIndex;
var ast = this.parsePipe();
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));
if (!this.optionalCharacter(chars.$SEMICOLON)) {
this.optionalCharacter(chars.$COMMA);
}
}
return new TemplateBindingParseResult(bindings, warnings);
return new TemplateBindingParseResult(bindings, warnings, this.errors);
}
error(message: string, index: number = null) {
this.errors.push(new ParserError(message, this.input, this.locationText(index), this.location));
this.skip();
}
private locationText(index: number = null) {
if (isBlank(index)) index = this.index;
var location = (index < this.tokens.length) ? `at column ${this.tokens[index].index + 1} in` :
return (index < this.tokens.length) ? `at column ${this.tokens[index].index + 1} in` :
`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;
}
}
}

View File

@ -13,7 +13,7 @@ import {Console, MAX_INTERPOLATION_VALUES} from '../core_private';
import {ListWrapper, StringMapWrapper, SetWrapper,} from '../src/facade/collection';
import {RegExpWrapper, isPresent, StringWrapper, isBlank, isArray} from '../src/facade/lang';
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 {CompileDirectiveMetadata, CompilePipeMetadata, CompileMetadataWithType,} from './compile_metadata';
import {HtmlParser} from './html_parser';
@ -190,10 +190,17 @@ class TemplateParseVisitor implements HtmlAstVisitor {
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 {
var sourceInfo = sourceSpan.start.toString();
try {
var ast = this._exprParser.parseInterpolation(value, sourceInfo, this._interpolationConfig);
if (ast) this._reportParserErors(ast.errors, sourceSpan);
this._checkPipes(ast, sourceSpan);
if (isPresent(ast) &&
(<Interpolation>ast.ast).expressions.length > MAX_INTERPOLATION_VALUES) {
@ -211,6 +218,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
var sourceInfo = sourceSpan.start.toString();
try {
var ast = this._exprParser.parseAction(value, sourceInfo, this._interpolationConfig);
if (ast) this._reportParserErors(ast.errors, sourceSpan);
this._checkPipes(ast, sourceSpan);
return ast;
} catch (e) {
@ -223,6 +231,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
var sourceInfo = sourceSpan.start.toString();
try {
var ast = this._exprParser.parseBinding(value, sourceInfo, this._interpolationConfig);
if (ast) this._reportParserErors(ast.errors, sourceSpan);
this._checkPipes(ast, sourceSpan);
return ast;
} catch (e) {
@ -235,6 +244,7 @@ class TemplateParseVisitor implements HtmlAstVisitor {
var sourceInfo = sourceSpan.start.toString();
try {
var bindingsResult = this._exprParser.parseTemplateBindings(value, sourceInfo);
this._reportParserErors(bindingsResult.errors, sourceSpan);
bindingsResult.templateBindings.forEach((binding) => {
if (isPresent(binding.expression)) {
this._checkPipes(binding.expression, sourceSpan);

View File

@ -15,52 +15,52 @@ function lex(text: string): any[] {
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.index).toEqual(index);
}
function expectCharacterToken(
token: any /** TODO #9100 */, index: any /** TODO #9100 */, character: any /** TODO #9100 */) {
function expectCharacterToken(token: any, index: any, character: any) {
expect(character.length).toBe(1);
expectToken(token, index);
expect(token.isCharacter(StringWrapper.charCodeAt(character, 0))).toBe(true);
}
function expectOperatorToken(
token: any /** TODO #9100 */, index: any /** TODO #9100 */, operator: any /** TODO #9100 */) {
function expectOperatorToken(token: any, index: any, operator: any) {
expectToken(token, index);
expect(token.isOperator(operator)).toBe(true);
}
function expectNumberToken(
token: any /** TODO #9100 */, index: any /** TODO #9100 */, n: any /** TODO #9100 */) {
function expectNumberToken(token: any, index: any, n: any) {
expectToken(token, index);
expect(token.isNumber()).toBe(true);
expect(token.toNumber()).toEqual(n);
}
function expectStringToken(
token: any /** TODO #9100 */, index: any /** TODO #9100 */, str: any /** TODO #9100 */) {
function expectStringToken(token: any, index: any, str: any) {
expectToken(token, index);
expect(token.isString()).toBe(true);
expect(token.toString()).toEqual(str);
}
function expectIdentifierToken(
token: any /** TODO #9100 */, index: any /** TODO #9100 */, identifier: any /** TODO #9100 */) {
function expectIdentifierToken(token: any, index: any, identifier: any) {
expectToken(token, index);
expect(token.isIdentifier()).toBe(true);
expect(token.toString()).toEqual(identifier);
}
function expectKeywordToken(
token: any /** TODO #9100 */, index: any /** TODO #9100 */, keyword: any /** TODO #9100 */) {
function expectKeywordToken(token: any, index: any, keyword: any) {
expectToken(token, index);
expect(token.isKeyword()).toBe(true);
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() {
describe('lexer', function() {
describe('token', function() {
@ -228,14 +228,13 @@ export function main() {
expectNumberToken(tokens[0], 0, 0.5E+10);
});
it('should throws exception for invalid exponent', function() {
expect(() => {
lex('0.5E-');
}).toThrowError('Lexer Error: Invalid exponent at column 4 in expression [0.5E-]');
it('should return exception for invalid exponent', function() {
expectErrorToken(
lex('0.5E-')[0], 4, 'Lexer Error: Invalid exponent at column 4 in expression [0.5E-]');
expect(() => {
lex('0.5E-A');
}).toThrowError('Lexer Error: Invalid exponent at column 4 in expression [0.5E-A]');
expectErrorToken(
lex('0.5E-A')[0], 4,
'Lexer Error: Invalid exponent at column 4 in expression [0.5E-A]');
});
it('should tokenize number starting with a dot', function() {
@ -244,8 +243,8 @@ export function main() {
});
it('should throw error on invalid unicode', function() {
expect(() => { lex('\'\\u1\'\'bla\''); })
.toThrowError(
expectErrorToken(
lex('\'\\u1\'\'bla\'')[0], 2,
'Lexer Error: Invalid unicode escape [\\u1\'\'b] at column 2 in expression [\'\\u1\'\'bla\']');
});

View File

@ -6,64 +6,79 @@
* 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 {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 {isBlank, isPresent} from '../../src/facade/lang';
import {Unparser} from './unparser';
import {unparse} from './unparser';
import {validate} from './validator';
export function main() {
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);
}
function parseBinding(text: string, location: any = null): any {
function parseBinding(text: string, location: any = null): ASTWithSource {
return createParser().parseBinding(text, location);
}
function parseTemplateBindings(text: string, location: any = null): any {
return createParser().parseTemplateBindings(text, location).templateBindings;
function parseTemplateBindingsResult(
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);
}
function parseSimpleBinding(text: string, location: any = null): any {
function parseSimpleBinding(text: string, location: any = null): ASTWithSource {
return createParser().parseSimpleBinding(text, location);
}
function unparse(ast: AST): string { return new Unparser().unparse(ast); }
function checkInterpolation(exp: string, expected?: string) {
var ast = parseInterpolation(exp);
if (isBlank(expected)) expected = exp;
expect(unparse(ast)).toEqual(expected);
validate(ast);
}
function checkBinding(exp: string, expected?: string) {
var ast = parseBinding(exp);
if (isBlank(expected)) expected = exp;
expect(unparse(ast)).toEqual(expected);
validate(ast);
}
function checkAction(exp: string, expected?: string) {
var ast = parseAction(exp);
if (isBlank(expected)) expected = exp;
expect(unparse(ast)).toEqual(expected);
validate(ast);
}
function expectActionError(text: any /** TODO #9100 */) {
return expect(() => parseAction(text));
function expectError(ast: {errors: ParserError[]}, message: string) {
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 */) {
return expect(() => parseBinding(text));
function expectActionError(text: string, message: string) {
expectError(validate(parseAction(text)), message);
}
function expectBindingError(text: string, message: string) {
expectError(validate(parseBinding(text)), message);
}
describe('parser', () => {
@ -140,8 +155,8 @@ export function main() {
});
it('should only allow identifier, string, or keyword as map key', () => {
expectActionError('{(:0}').toThrowError(/expected identifier, keyword, or string/);
expectActionError('{1234:0}').toThrowError(/expected identifier, keyword, or string/);
expectActionError('{(:0}', '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', () => {
expectActionError('x.(').toThrowError(/identifier or keyword/);
expectActionError('x. 1234').toThrowError(/identifier or keyword/);
expectActionError('x."foo"').toThrowError(/identifier or keyword/);
expectActionError('x.(', 'identifier or keyword');
expectActionError('x. 1234', 'identifier or keyword');
expectActionError('x."foo"', 'identifier or keyword');
});
it('should parse safe field access', () => {
@ -182,9 +197,8 @@ export function main() {
checkAction('false ? 10 : 20');
});
it('should throw on incorrect ternary operator syntax', () => {
expectActionError('true?1').toThrowError(
/Parser Error: Conditional expression true\?1 requires all 3 expressions/);
it('should report incorrect ternary operator syntax', () => {
expectActionError('true?1', 'Conditional expression true?1 requires all 3 expressions');
});
});
@ -195,39 +209,35 @@ export function main() {
checkAction('a = 123; b = 234;');
});
it('should throw on safe field assignments', () => {
expectActionError('a?.a = 123').toThrowError(/cannot be used in the assignment/);
});
it('should report on safe field assignments',
() => { expectActionError('a?.a = 123', 'cannot be used in the assignment'); });
it('should support array updates', () => { checkAction('a[0] = 200'); });
});
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',
() => { expect(parseAction('someExpr').source).toBe('someExpr'); });
() => { expect(parseAction('someExpr', 'someExpr')); });
it('should store the passed-in location',
() => { expect(parseAction('someExpr', 'location').location).toBe('location'); });
it('should throw when encountering interpolation', () => {
expectActionError('{{a()}}').toThrowError(
/Got interpolation \(\{\{\}\}\) where expression was expected/);
it('should report when encountering interpolation', () => {
expectActionError('{{a()}}', 'Got interpolation ({{}}) where expression was expected');
});
});
describe('general error handling', () => {
it('should throw on an unexpected token',
() => { expectActionError('[1,2] trac').toThrowError(/Unexpected token \'trac\'/); });
it('should report an unexpected token',
() => { expectActionError('[1,2] trac', 'Unexpected token \'trac\''); });
it('should throw a reasonable error for unconsumed tokens', () => {
expectActionError(')').toThrowError(/Unexpected token \) at column 1 in \[\)\]/);
});
it('should report reasonable error for unconsumed tokens',
() => { expectActionError(')', 'Unexpected token ) at column 1 in [)]'); });
it('should throw on missing expected token', () => {
expectActionError('a(b').toThrowError(
/Missing expected \) at the end of the expression \[a\(b\]/);
it('should report a missing expected token', () => {
expectActionError('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', () => {
expectBindingError('"Foo"|(').toThrowError(/identifier or keyword/);
expectBindingError('"Foo"|1234').toThrowError(/identifier or keyword/);
expectBindingError('"Foo"|"uppercase"').toThrowError(/identifier or keyword/);
expectBindingError('"Foo"|(', 'identifier or keyword');
expectBindingError('"Foo"|1234', 'identifier or keyword');
expectBindingError('"Foo"|"uppercase"', 'identifier or keyword');
});
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 refuse prefixes that are not single identifiers', () => {
expectBindingError('a + b:c').toThrowError();
expectBindingError('1:c').toThrowError();
expectBindingError('a + b:c', '');
expectBindingError('1:c', '');
});
});
@ -270,15 +280,14 @@ export function main() {
it('should store the passed-in location',
() => { expect(parseBinding('someExpr', 'location').location).toBe('location'); });
it('should throw on chain expressions',
() => { expect(() => parseBinding('1;2')).toThrowError(/contain chained expression/); });
it('should report chain expressions',
() => { expectError(parseBinding('1;2'), 'contain chained expression'); });
it('should throw on assignment',
() => { expect(() => parseBinding('a=2')).toThrowError(/contain assignments/); });
it('should report assignment',
() => { expectError(parseBinding('a=2'), 'contain assignments'); });
it('should throw when encountering interpolation', () => {
expectBindingError('{{a.b}}').toThrowError(
/Got interpolation \(\{\{\}\}\) where expression was expected/);
it('should report when encountering interpolation', () => {
expectBindingError('{{a.b}}', 'Got interpolation ({{}}) where expression was expected');
});
it('should parse conditional expression', () => { checkBinding('a < b ? a : b'); });
@ -331,13 +340,10 @@ export function main() {
bindings = parseTemplateBindings('a-b:\'c\'');
expect(keys(bindings)).toEqual(['a-b']);
expect(() => {
parseTemplateBindings('(:0');
}).toThrowError(/expected identifier, keyword, or string/);
expectError(parseTemplateBindingsResult('(:0'), 'expected identifier, keyword, or string');
expect(() => {
parseTemplateBindings('1234:0');
}).toThrowError(/expected identifier, keyword, or string/);
expectError(
parseTemplateBindingsResult('1234:0'), 'expected identifier, keyword, or string');
});
it('should detect expressions as value', () => {
@ -436,7 +442,7 @@ export function main() {
() => { expect(parseInterpolation('nothing')).toBe(null); });
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.expressions.length).toEqual(1);
expect(ast.expressions[0].name).toEqual('a');
@ -445,17 +451,18 @@ export function main() {
it('should parse prefix/suffix with multiple interpolation', () => {
var originalExp = 'before {{ a }} middle {{ b }} after';
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', () => {
expect(() => parseInterpolation('{{}}'))
.toThrowError(
/Parser Error: Blank expressions are not allowed in interpolated strings/);
it('should report empty interpolation expressions', () => {
expectError(
parseInterpolation('{{}}'),
'Blank expressions are not allowed in interpolated strings');
expect(() => parseInterpolation('foo {{ }}'))
.toThrowError(
/Parser Error: Blank expressions are not allowed in interpolated strings/);
expectError(
parseInterpolation('foo {{ }}'),
'Parser Error: Blank expressions are not allowed in interpolated strings');
});
it('should parse conditional expression',
@ -503,28 +510,47 @@ export function main() {
it('should parse a field access', () => {
var p = parseSimpleBinding('name');
expect(unparse(p)).toEqual('name');
validate(p);
});
it('should parse a constant', () => {
var p = parseSimpleBinding('[1, 2]');
expect(unparse(p)).toEqual('[1, 2]');
validate(p);
});
it('should throw when the given expression is not just a field name', () => {
expect(() => parseSimpleBinding('name + 1'))
.toThrowError(/Host binding expression can only contain field access and constants/);
it('should report when the given expression is not just a field name', () => {
expectError(
validate(parseSimpleBinding('name + 1')),
'Host binding expression can only contain field access and constants');
});
it('should throw when encountering interpolation', () => {
expect(() => parseSimpleBinding('{{exp}}'))
.toThrowError(/Got interpolation \(\{\{\}\}\) where expression was expected/);
it('should report when encountering interpolation', () => {
expectError(
validate(parseSimpleBinding('{{exp}}')),
'Got interpolation ({{}}) where expression was expected');
});
});
describe('wrapLiteralPrimitive', () => {
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]'))
});
});
}

View File

@ -10,12 +10,12 @@ import {AST, AstVisitor, Binary, BindingPipe, Chain, Conditional, EmptyExpr, Fun
import {StringWrapper, isPresent, isString} from '../../src/facade/lang';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../../src/interpolation_config';
export class Unparser implements AstVisitor {
class Unparser implements AstVisitor {
private static _quoteRegExp = /"/g;
private _expression: string;
private _interpolationConfig: InterpolationConfig;
unparse(ast: AST, interpolationConfig: InterpolationConfig = DEFAULT_INTERPOLATION_CONFIG) {
unparse(ast: AST, interpolationConfig: InterpolationConfig) {
this._expression = '';
this._interpolationConfig = interpolationConfig;
this._visit(ast);
@ -180,3 +180,10 @@ export class Unparser implements AstVisitor {
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);
}

View File

@ -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;
}

View File

@ -19,11 +19,9 @@ import {afterEach, beforeEach, beforeEachProviders, ddescribe, describe, expect,
import {Identifiers, identifierToken} from '../src/identifiers';
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';
var expressionUnparser = new Unparser();
var someModuleUrl = 'package:someModule';
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(
`Template parse errors:
Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>"): TestComp@0:19`);
});
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>', []))
.not.toThrowError();
});
it('should assign references with empty value to components', () => {
@ -1504,17 +1502,14 @@ class TemplateHumanizer implements TemplateAstVisitor {
return null;
}
visitEvent(ast: BoundEventAst, context: any): any {
var res = [
BoundEventAst, ast.name, ast.target,
expressionUnparser.unparse(ast.handler, this.interpolationConfig)
];
var res = [BoundEventAst, ast.name, ast.target, unparse(ast.handler, this.interpolationConfig)];
this.result.push(this._appendContext(ast, res));
return null;
}
visitElementProperty(ast: BoundElementPropertyAst, context: any): any {
var res = [
BoundElementPropertyAst, ast.type, ast.name,
expressionUnparser.unparse(ast.value, this.interpolationConfig), ast.unit
BoundElementPropertyAst, ast.type, ast.name, unparse(ast.value, this.interpolationConfig),
ast.unit
];
this.result.push(this._appendContext(ast, res));
return null;
@ -1525,7 +1520,7 @@ class TemplateHumanizer implements TemplateAstVisitor {
return null;
}
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));
return null;
}
@ -1544,8 +1539,7 @@ class TemplateHumanizer implements TemplateAstVisitor {
}
visitDirectiveProperty(ast: BoundDirectivePropertyAst, context: any): any {
var res = [
BoundDirectivePropertyAst, ast.directiveName,
expressionUnparser.unparse(ast.value, this.interpolationConfig)
BoundDirectivePropertyAst, ast.directiveName, unparse(ast.value, this.interpolationConfig)
];
this.result.push(this._appendContext(ast, res));
return null;
@ -1590,7 +1584,7 @@ class TemplateContentProjectionHumanizer implements TemplateAstVisitor {
visitElementProperty(ast: BoundElementPropertyAst, context: any): any { return null; }
visitAttr(ast: AttrAst, context: any): any { return null; }
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;
}
visitText(ast: TextAst, context: any): any {