diff --git a/modules/change_detection/src/parser/ast.js b/modules/change_detection/src/parser/ast.js index cad3fee265..1eaa33e872 100644 --- a/modules/change_detection/src/parser/ast.js +++ b/modules/change_detection/src/parser/ast.js @@ -18,14 +18,23 @@ export class ImplicitReceiver extends AST { } } -export class Expression extends AST { - constructor() { - this.isAssignable = false; - this.isChain = false; +export class Conditional extends AST { + constructor(condition:AST, yes:AST, no:AST){ + this.condition = condition; + this.yes = yes; + this.no = no; + } + + eval(context, formatters) { + if(this.condition.eval(context, formatters)) { + return this.yes.eval(context, formatters); + } else { + return this.no.eval(context, formatters); + } } } -export class FieldRead extends Expression { +export class FieldRead extends AST { constructor(receiver:AST, name:string, getter:Function) { this.receiver = receiver; this.name = name; @@ -41,7 +50,7 @@ export class FieldRead extends Expression { } } -export class LiteralPrimitive extends Expression { +export class LiteralPrimitive extends AST { @FIELD('final value') constructor(value) { this.value = value; @@ -54,11 +63,11 @@ export class LiteralPrimitive extends Expression { } } -export class Binary extends Expression { +export class Binary extends AST { @FIELD('final operation:string') - @FIELD('final left:Expression') - @FIELD('final right:Expression') - constructor(operation:string, left:Expression, right:Expression) { + @FIELD('final left:AST') + @FIELD('final right:AST') + constructor(operation:string, left:AST, right:AST) { this.operation = operation; this.left = left; this.right = right; @@ -112,10 +121,10 @@ export class Binary extends Expression { } } -export class PrefixNot extends Expression { +export class PrefixNot extends AST { @FIELD('final operation:string') - @FIELD('final expression:Expression') - constructor(expression:Expression) { + @FIELD('final expression:AST') + constructor(expression:AST) { this.expression = expression; } visit(visitor) { visitor.visitPrefixNot(this); } diff --git a/modules/change_detection/src/parser/parser.js b/modules/change_detection/src/parser/parser.js index e225c839fd..ce5a15113e 100644 --- a/modules/change_detection/src/parser/parser.js +++ b/modules/change_detection/src/parser/parser.js @@ -1,9 +1,9 @@ -import {FIELD, int} from 'facade/lang'; +import {FIELD, int, isBlank} from 'facade/lang'; import {ListWrapper, List} from 'facade/collection'; -import {Lexer, EOF, Token, $PERIOD} from './lexer'; +import {Lexer, EOF, Token, $PERIOD, $COLON} from './lexer'; import {ClosureMap} from './closure_map'; import {AST, ImplicitReceiver, FieldRead, LiteralPrimitive, Expression, - Binary, PrefixNot } from './ast'; + Binary, PrefixNot, Conditional} from './ast'; var _implicitReceiver = new ImplicitReceiver(); @@ -17,15 +17,17 @@ export class Parser { parse(input:string):AST { var tokens = this._lexer.tokenize(input); - return new _ParseAST(tokens, this._closureMap).parseChain(); + return new _ParseAST(input, tokens, this._closureMap).parseChain(); } } class _ParseAST { + @FIELD('final input:String') @FIELD('final tokens:List') @FIELD('final closureMap:ClosureMap') @FIELD('index:int') - constructor(tokens:List, closureMap:ClosureMap) { + constructor(input:string, tokens:List, closureMap:ClosureMap) { + this.input = input; this.tokens = tokens; this.index = 0; this.closureMap = closureMap; @@ -40,6 +42,10 @@ class _ParseAST { return this.peek(0); } + get inputIndex():int { + return (this.index < this.tokens.length) ? this.next.index : this.input.length; + } + advance() { this.index ++; } @@ -62,6 +68,36 @@ class _ParseAST { } } + parseChain():AST { + var exprs = []; + while (this.index < this.tokens.length) { + ListWrapper.push(exprs, this.parseConditional()); + } + return ListWrapper.first(exprs); + } + + parseExpression() { + return this.parseConditional(); + } + + parseConditional() { + var start = this.inputIndex; + var result = this.parseLogicalOr(); + + if (this.optionalOperator('?')) { + var yes = this.parseExpression(); + if (!this.optionalCharacter($COLON)) { + var end = this.inputIndex; + var expression = this.input.substring(start, end); + this.error(`Conditional expression ${expression} requires all 3 expressions`); + } + var no = this.parseExpression(); + return new Conditional(result, yes, no); + } else { + return result; + } + } + parseLogicalOr() { // '||' var result = this.parseLogicalAnd(); @@ -157,23 +193,6 @@ class _ParseAST { } } - - parseChain():AST { - var exprs = []; - while (this.index < this.tokens.length) { - ListWrapper.push(exprs, this.parseLogicalOr()); - } - return ListWrapper.first(exprs); - } - - parseAccess():AST { - var result = this.parseFieldRead(_implicitReceiver); - while(this.optionalCharacter($PERIOD)) { - result = this.parseFieldRead(result); - } - return result; - } - parseAccessOrCallMember() { var result = this.parsePrimary(); // TODO: add missing cases. @@ -181,9 +200,6 @@ class _ParseAST { } parsePrimary() { - var value; - // TODO: add missing cases. - if (this.next.isKeywordNull() || this.next.isKeywordUndefined()) { this.advance(); return new LiteralPrimitive(null); @@ -196,11 +212,11 @@ class _ParseAST { } else if (this.next.isIdentifier()) { return this.parseAccess(); } else if (this.next.isNumber()) { - value = this.next.toNumber(); + var value = this.next.toNumber(); this.advance(); return new LiteralPrimitive(value); } else if (this.next.isString()) { - value = this.next.toString(); + var value = this.next.toString(); this.advance(); return new LiteralPrimitive(value); } else if (this.index >= this.tokens.length) { @@ -210,6 +226,14 @@ class _ParseAST { } } + parseAccess():AST { + var result = this.parseFieldRead(_implicitReceiver); + while(this.optionalCharacter($PERIOD)) { + result = this.parseFieldRead(result); + } + return result; + } + parseFieldRead(receiver):AST { var id = this.parseIdentifier(); return new FieldRead(receiver, id, this.closureMap.getter(id)); @@ -220,4 +244,24 @@ class _ParseAST { this.advance(); return n.toString(); } + + error(message:string, index:int = null) { + if (isBlank(index)) index = this.index; + + var location = (index < this.tokens.length) + ? `at column ${tokens[index].index + 1} in` + : `at the end of the expression`; + + throw new ParserError(`Parser Error: ${message} ${location} [${this.input}]`); + } } + +class ParserError extends Error { + constructor(message) { + this.message = message; + } + + toString() { + return this.message; + } +} \ No newline at end of file diff --git a/modules/change_detection/test/parser/parser_spec.js b/modules/change_detection/test/parser/parser_spec.js index 466632f192..f788b6c9ec 100644 --- a/modules/change_detection/test/parser/parser_spec.js +++ b/modules/change_detection/test/parser/parser_spec.js @@ -20,7 +20,15 @@ export function main() { function _eval(text) { return new Parser(new Lexer(), new ClosureMap()).parse(text) - .eval(context, formatters); + .eval(context, formatters); + } + + function expectEval(text) { + return expect(_eval(text)); + } + + function expectEvalError(text) { + return expect(() => _eval(text)); } describe("parser", () => { @@ -47,64 +55,75 @@ export function main() { describe('expressions', () => { it('should parse numerical expressions', () => { - expect(_eval("1")).toEqual(1); + expectEval("1").toEqual(1); }); it('should parse unary - expressions', () => { - expect(_eval("-1")).toEqual(-1); - expect(_eval("+1")).toEqual(1); + expectEval("-1").toEqual(-1); + expectEval("+1").toEqual(1); }); it('should parse unary ! expressions', () => { - expect(_eval("!true")).toEqual(!true); + expectEval("!true").toEqual(!true); }); it('should parse multiplicative expressions', () => { - expect(_eval("3*4/2%5")).toEqual(3*4/2%5); + expectEval("3*4/2%5").toEqual(3*4/2%5); // TODO(rado): This exists only in Dart, figure out whether to support it. - // expect(_eval("3*4~/2%5")).toEqual(3*4~/2%5); + // expectEval("3*4~/2%5")).toEqual(3*4~/2%5); }); it('should parse additive expressions', () => { - expect(_eval("3+6-2")).toEqual(3+6-2); + expectEval("3+6-2").toEqual(3+6-2); }); it('should parse relational expressions', () => { - expect(_eval("2<3")).toEqual(2<3); - expect(_eval("2>3")).toEqual(2>3); - expect(_eval("2<=2")).toEqual(2<=2); - expect(_eval("2>=2")).toEqual(2>=2); + expectEval("2<3").toEqual(2<3); + expectEval("2>3").toEqual(2>3); + expectEval("2<=2").toEqual(2<=2); + expectEval("2>=2").toEqual(2>=2); }); it('should parse equality expressions', () => { - expect(_eval("2==3")).toEqual(2==3); - expect(_eval("2!=3")).toEqual(2!=3); + expectEval("2==3").toEqual(2==3); + expectEval("2!=3").toEqual(2!=3); }); it('should parse logicalAND expressions', () => { - expect(_eval("true&&true")).toEqual(true&&true); - expect(_eval("true&&false")).toEqual(true&&false); + expectEval("true&&true").toEqual(true&&true); + expectEval("true&&false").toEqual(true&&false); }); it('should parse logicalOR expressions', () => { - expect(_eval("false||true")).toEqual(false||true); - expect(_eval("false||false")).toEqual(false||false); + expectEval("false||true").toEqual(false||true); + expectEval("false||false").toEqual(false||false); + }); + + it('should parse ternary/conditional expressions', () => { + expectEval("7==3+4?10:20").toEqual(10); + expectEval("false?10:20").toEqual(20); }); it('should auto convert ints to strings', () => { - expect(_eval("'str ' + 4")).toEqual("str 4"); - expect(_eval("4 + ' str'")).toEqual("4 str"); - expect(_eval("4 + 4")).toEqual(8); - expect(_eval("4 + 4 + ' str'")).toEqual("8 str"); - expect(_eval("'str ' + 4 + 4")).toEqual("str 44"); + expectEval("'str ' + 4").toEqual("str 4"); + expectEval("4 + ' str'").toEqual("4 str"); + expectEval("4 + 4").toEqual(8); + expectEval("4 + 4 + ' str'").toEqual("8 str"); + expectEval("'str ' + 4 + 4").toEqual("str 44"); + }); + }); + + describe("error handling", () => { + it('should throw on incorrect ternary operator syntax', () => { + expectEvalError("true?1").toThrowError(new RegExp('Parser Error: Conditional expression true\\?1 requires all 3 expressions')); }); }); });