diff --git a/modules/@angular/compiler/src/expression_parser/ast.ts b/modules/@angular/compiler/src/expression_parser/ast.ts index 660d0e151c..331fe62d66 100644 --- a/modules/@angular/compiler/src/expression_parser/ast.ts +++ b/modules/@angular/compiler/src/expression_parser/ast.ts @@ -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); } } diff --git a/modules/@angular/compiler/src/expression_parser/lexer.ts b/modules/@angular/compiler/src/expression_parser/lexer.ts index 7deabc79bc..494fea22c5 100644 --- a/modules/@angular/compiler/src/expression_parser/lexer.ts +++ b/modules/@angular/compiler/src/expression_parser/lexer.ts @@ -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}]`); } } diff --git a/modules/@angular/compiler/src/expression_parser/parser.ts b/modules/@angular/compiler/src/expression_parser/parser.ts index 920c09bed1..d5c17dbd78 100644 --- a/modules/@angular/compiler/src/expression_parser/parser.ts +++ b/modules/@angular/compiler/src/expression_parser/parser.ts @@ -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 { - return result; + 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 { - return result; + 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 { - return result; + 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 { - return result; + 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('+')) { - 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(); + if (this.next.type == TokenType.Operator) { + const start = this.inputIndex; + const operator = this.next.strValue; + switch (operator) { + case '+': + this.advance(); + return this.parsePrefix(); + 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; + return (index < this.tokens.length) ? `at column ${this.tokens[index].index + 1} in` : + `at the end of the expression`; + } - var location = (index < this.tokens.length) ? `at column ${this.tokens[index].index + 1} in` : - `at the end of the expression`; + // Error recovery should skip tokens until it encounters a recovery point. skip() treats + // the end of input and a ';' as unconditionally a recovery point. It also treats ')', + // '}' and ']' as conditional recovery points if one of calling productions is expecting + // one of these symbols. This allows skip() to recover from errors such as '(a.) + 1' allowing + // more of the AST to be retained (it doesn't skip any tokens as the ')' is retained because + // of the '(' begins an '(' ')' 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 ';'. - throw new ParseException(message, this.input, location, this.location); + // 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; + } } } diff --git a/modules/@angular/compiler/src/template_parser.ts b/modules/@angular/compiler/src/template_parser.ts index b6b80cfc9e..71956556be 100644 --- a/modules/@angular/compiler/src/template_parser.ts +++ b/modules/@angular/compiler/src/template_parser.ts @@ -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) && (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); diff --git a/modules/@angular/compiler/test/expression_parser/lexer_spec.ts b/modules/@angular/compiler/test/expression_parser/lexer_spec.ts index e8577a9e65..b0bdd188c6 100644 --- a/modules/@angular/compiler/test/expression_parser/lexer_spec.ts +++ b/modules/@angular/compiler/test/expression_parser/lexer_spec.ts @@ -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,9 +243,9 @@ export function main() { }); it('should throw error on invalid unicode', function() { - expect(() => { lex('\'\\u1\'\'bla\''); }) - .toThrowError( - 'Lexer Error: Invalid unicode escape [\\u1\'\'b] at column 2 in expression [\'\\u1\'\'bla\']'); + expectErrorToken( + lex('\'\\u1\'\'bla\'')[0], 2, + 'Lexer Error: Invalid unicode escape [\\u1\'\'b] at column 2 in expression [\'\\u1\'\'bla\']'); }); it('should tokenize hash as operator', function() { diff --git a/modules/@angular/compiler/test/expression_parser/parser_spec.ts b/modules/@angular/compiler/test/expression_parser/parser_spec.ts index 43a43983ed..23193670a7 100644 --- a/modules/@angular/compiler/test/expression_parser/parser_spec.ts +++ b/modules/@angular/compiler/test/expression_parser/parser_spec.ts @@ -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]')) + }); }); } diff --git a/modules/@angular/compiler/test/expression_parser/unparser.ts b/modules/@angular/compiler/test/expression_parser/unparser.ts index c69dfca53d..c598baf2d5 100644 --- a/modules/@angular/compiler/test/expression_parser/unparser.ts +++ b/modules/@angular/compiler/test/expression_parser/unparser.ts @@ -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); +} diff --git a/modules/@angular/compiler/test/expression_parser/validator.ts b/modules/@angular/compiler/test/expression_parser/validator.ts new file mode 100644 index 0000000000..91791eff52 --- /dev/null +++ b/modules/@angular/compiler/test/expression_parser/validator.ts @@ -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(ast: T): T { + sharedValidator.visit(ast); + return ast; +} diff --git a/modules/@angular/compiler/test/template_parser_spec.ts b/modules/@angular/compiler/test/template_parser_spec.ts index 8bb2f12a5b..313c173830 100644 --- a/modules/@angular/compiler/test/template_parser_spec.ts +++ b/modules/@angular/compiler/test/template_parser_spec.ts @@ -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" ("
]#a="dirA">< 
 expect(() => parse('
', []))
 .toThrowError( `Template parse errors: Reference "#a" is defined several times ("
]#a>
"): TestComp@0:19`); - 
 + }); 


 it( @@ -817,7 +815,7 @@ Reference "#a" is defined several times ("
]#a>
() => { 
 expect(() => parse('
', [])) .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 {