From 7d32879929d3a70a7b42094dfc74b31b3ef05f02 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Wed, 10 Jun 2015 11:11:01 +0200 Subject: [PATCH] feat(Parser): support if statements in actions fixes #2022 --- .../src/change_detection/parser/ast.ts | 17 +++++++ .../src/change_detection/parser/lexer.ts | 7 ++- .../src/change_detection/parser/parser.ts | 45 +++++++++++++++++++ .../change_detection/proto_change_detector.ts | 3 ++ .../change_detection/parser/parser_spec.ts | 30 ++++++++++++- .../test/change_detection/parser/unparser.ts | 31 ++++++++++--- .../change_detection/parser/unparser_spec.ts | 22 ++++++++- 7 files changed, 146 insertions(+), 9 deletions(-) diff --git a/modules/angular2/src/change_detection/parser/ast.ts b/modules/angular2/src/change_detection/parser/ast.ts index 45585a90f1..8b425860c8 100644 --- a/modules/angular2/src/change_detection/parser/ast.ts +++ b/modules/angular2/src/change_detection/parser/ast.ts @@ -60,6 +60,20 @@ export class Conditional extends AST { visit(visitor: AstVisitor) { return visitor.visitConditional(this); } } +export class If extends AST { + constructor(public condition: AST, public trueExp: AST, public falseExp?: AST) { super(); } + + eval(context, locals) { + if (this.condition.eval(context, locals)) { + this.trueExp.eval(context, locals); + } else if (isPresent(this.falseExp)) { + this.falseExp.eval(context, locals); + } + } + + visit(visitor: AstVisitor) { return visitor.visitIf(this); } +} + export class AccessMember extends AST { constructor(public receiver: AST, public name: string, public getter: Function, public setter: Function) { @@ -321,6 +335,7 @@ export interface AstVisitor { visitBinary(ast: Binary): any; visitChain(ast: Chain): any; visitConditional(ast: Conditional): any; + visitIf(ast: If): any; visitPipe(ast: Pipe): any; visitFunctionCall(ast: FunctionCall): any; visitImplicitReceiver(ast: ImplicitReceiver): any; @@ -398,6 +413,8 @@ export class AstTransformer implements AstVisitor { visitChain(ast: Chain) { throw new BaseException('Not implemented'); } visitAssignment(ast: Assignment) { throw new BaseException('Not implemented'); } + + visitIf(ast: If) { throw new BaseException('Not implemented'); } } var _evalListCache = [ diff --git a/modules/angular2/src/change_detection/parser/lexer.ts b/modules/angular2/src/change_detection/parser/lexer.ts index 18d761bed6..66d33c3e16 100644 --- a/modules/angular2/src/change_detection/parser/lexer.ts +++ b/modules/angular2/src/change_detection/parser/lexer.ts @@ -59,6 +59,10 @@ export class Token { isKeywordTrue(): boolean { return (this.type == TOKEN_TYPE_KEYWORD && this.strValue == "true"); } + isKeywordIf(): boolean { return (this.type == TOKEN_TYPE_KEYWORD && this.strValue == "if"); } + + isKeywordElse(): boolean { return (this.type == TOKEN_TYPE_KEYWORD && this.strValue == "else"); } + isKeywordFalse(): boolean { return (this.type == TOKEN_TYPE_KEYWORD && this.strValue == "false"); } @@ -463,4 +467,5 @@ var OPERATORS = SetWrapper.createFromList([ ]); -var KEYWORDS = SetWrapper.createFromList(['var', 'null', 'undefined', 'true', 'false']); +var KEYWORDS = + SetWrapper.createFromList(['var', 'null', 'undefined', 'true', 'false', 'if', 'else']); diff --git a/modules/angular2/src/change_detection/parser/parser.ts b/modules/angular2/src/change_detection/parser/parser.ts index ec35bb8c94..2452c8b6ac 100644 --- a/modules/angular2/src/change_detection/parser/parser.ts +++ b/modules/angular2/src/change_detection/parser/parser.ts @@ -33,6 +33,7 @@ import { Binary, PrefixNot, Conditional, + If, Pipe, Assignment, Chain, @@ -412,6 +413,19 @@ class _ParseAST { this.advance(); return new LiteralPrimitive(false); + } else if (this.parseAction && this.next.isKeywordIf()) { + this.advance(); + this.expectCharacter($LPAREN); + let condition = this.parseExpression(); + this.expectCharacter($RPAREN); + let ifExp = this.parseExpressionOrBlock(); + let elseExp; + if (this.next.isKeywordElse()) { + this.advance(); + elseExp = this.parseExpressionOrBlock(); + } + return new If(condition, ifExp, elseExp) + } else if (this.optionalCharacter($LBRACKET)) { var elements = this.parseExpressionList($RBRACKET); this.expectCharacter($RBRACKET); @@ -494,6 +508,37 @@ class _ParseAST { return positionals; } + parseExpressionOrBlock(): AST { + if (this.optionalCharacter($LBRACE)) { + let block = this.parseBlockContent(); + this.expectCharacter($RBRACE); + return block; + } + + return this.parseExpression(); + } + + parseBlockContent(): AST { + if (!this.parseAction) { + this.error("Binding expression cannot contain chained expression"); + } + var exprs = []; + while (this.index < this.tokens.length && !this.next.isCharacter($RBRACE)) { + var expr = this.parseExpression(); + ListWrapper.push(exprs, expr); + + if (this.optionalCharacter($SEMICOLON)) { + while (this.optionalCharacter($SEMICOLON)) { + } // read all semicolons + } + } + if (exprs.length == 0) return new EmptyExpr(); + if (exprs.length == 1) return exprs[0]; + + return new Chain(exprs); + } + + /** * An identifier, a keyword, a string with an optional `-` inbetween. */ diff --git a/modules/angular2/src/change_detection/proto_change_detector.ts b/modules/angular2/src/change_detection/proto_change_detector.ts index 1d542ce76c..4ffc47bb05 100644 --- a/modules/angular2/src/change_detection/proto_change_detector.ts +++ b/modules/angular2/src/change_detection/proto_change_detector.ts @@ -10,6 +10,7 @@ import { Binary, Chain, Conditional, + If, Pipe, FunctionCall, ImplicitReceiver, @@ -201,6 +202,8 @@ class _ConvertAstIntoProtoRecords implements AstVisitor { visitChain(ast: Chain) { throw new BaseException('Not supported'); } + visitIf(ast: If) { throw new BaseException('Not supported'); } + _visitAll(asts: List) { var res = ListWrapper.createFixedSize(asts.length); for (var i = 0; i < asts.length; ++i) { diff --git a/modules/angular2/test/change_detection/parser/parser_spec.ts b/modules/angular2/test/change_detection/parser/parser_spec.ts index c31ec46c02..4d3002d28e 100644 --- a/modules/angular2/test/change_detection/parser/parser_spec.ts +++ b/modules/angular2/test/change_detection/parser/parser_spec.ts @@ -43,10 +43,14 @@ export function main() { function emptyLocals() { return new Locals(null, MapWrapper.create()); } - function expectEval(text, passedInContext = null, passedInLocals = null) { + function evalAction(text, passedInContext = null, passedInLocals = null) { var c = isBlank(passedInContext) ? td() : passedInContext; var l = isBlank(passedInLocals) ? emptyLocals() : passedInLocals; - return expect(parseAction(text).eval(c, l)); + return parseAction(text).eval(c, l); + } + + function expectEval(text, passedInContext = null, passedInLocals = null) { + return expect(evalAction(text, passedInContext, passedInLocals)); } function expectEvalError(text, passedInContext = null, passedInLocals = null) { @@ -280,6 +284,28 @@ export function main() { }); }); + describe("if", () => { + it('should parse if statements', () => { + + var fixtures = [ + ['if (true) a = 0', 0, null], + ['if (false) a = 0', null, null], + ['if (a == null) b = 0', null, 0], + ['if (true) { a = 0; b = 0 }', 0, 0], + ['if (true) { a = 0; b = 0 } else { a = 1; b = 1; }', 0, 0], + ['if (false) { a = 0; b = 0 } else { a = 1; b = 1; }', 1, 1], + ['if (false) { } else { a = 1; b = 1; }', 1, 1], + ]; + + fixtures.forEach(fix => { + var testData = td(null, null); + evalAction(fix[0], testData); + expect(testData.a).toEqual(fix[1]); + expect(testData.b).toEqual(fix[2]); + }); + }); + }); + describe("assignment", () => { it("should support field assignments", () => { var context = td(); diff --git a/modules/angular2/test/change_detection/parser/unparser.ts b/modules/angular2/test/change_detection/parser/unparser.ts index 4b65ee1277..02caa2da8e 100644 --- a/modules/angular2/test/change_detection/parser/unparser.ts +++ b/modules/angular2/test/change_detection/parser/unparser.ts @@ -6,6 +6,8 @@ import { Binary, Chain, Conditional, + EmptyExpr, + If, Pipe, FunctionCall, ImplicitReceiver, @@ -21,7 +23,7 @@ import { } from 'angular2/src/change_detection/parser/ast'; -import {StringWrapper, RegExpWrapper} from 'angular2/src/facade/lang'; +import {StringWrapper, RegExpWrapper, isPresent} from 'angular2/src/facade/lang'; var quoteRegExp = RegExpWrapper.create('"'); @@ -53,10 +55,11 @@ export class Unparser implements AstVisitor { } visitChain(ast: Chain) { - ast.expressions.forEach(expression => { - this._visit(expression); - this._expression += ';' - }); + var len = ast.expressions.length; + for (let i = 0; i < len; i++) { + this._visit(ast.expressions[i]); + this._expression += i == len - 1 ? ';' : '; '; + } } visitConditional(ast: Conditional) { @@ -67,6 +70,17 @@ export class Unparser implements AstVisitor { this._visit(ast.falseExp); } + visitIf(ast: If) { + this._expression += 'if ('; + this._visit(ast.condition); + this._expression += ') '; + this._visitExpOrBlock(ast.trueExp); + if (isPresent(ast.falseExp)) { + this._expression += ' else '; + this._visitExpOrBlock(ast.falseExp); + } + } + visitPipe(ast: Pipe) { this._expression += '('; this._visit(ast.exp); @@ -179,4 +193,11 @@ export class Unparser implements AstVisitor { } private _visit(ast: AST) { ast.visit(this); } + + private _visitExpOrBlock(ast: AST) { + var isBlock = ast instanceof Chain || ast instanceof EmptyExpr; + if (isBlock) this._expression += '{ '; + this._visit(ast); + if (isBlock) this._expression += ' }'; + } } diff --git a/modules/angular2/test/change_detection/parser/unparser_spec.ts b/modules/angular2/test/change_detection/parser/unparser_spec.ts index 3f55eb299b..120758e455 100644 --- a/modules/angular2/test/change_detection/parser/unparser_spec.ts +++ b/modules/angular2/test/change_detection/parser/unparser_spec.ts @@ -8,6 +8,8 @@ import { Binary, Chain, Conditional, + EmptyExpr, + If, Pipe, ImplicitReceiver, Interpolation, @@ -59,7 +61,7 @@ export function main() { it('should support Binary', () => { check('a && b', Binary); }); - it('should support Chain', () => { check('a;b;', Chain); }); + it('should support Chain', () => { check('a; b;', Chain); }); it('should support Conditional', () => { check('a ? b : c', Conditional); }); @@ -93,6 +95,17 @@ export function main() { it('should support SafeMethodCall', () => { check('a?.b(c, d)', SafeMethodCall); }); + it('should support if statements', () => { + var ifs = [ + 'if (true) a()', + 'if (true) a() else b()', + 'if (a()) { b = 1; c = 2; }', + 'if (a()) b = 1 else { c = 2; d = e(); }' + ]; + + ifs.forEach(ifStmt => check(ifStmt, If)); + }); + it('should support complex expression', () => { var originalExp = 'a + 3 * fn([(c + d | e).f], {a: 3})[g].h && i'; var ast = parseBinding(originalExp).ast; @@ -108,5 +121,12 @@ export function main() { expect(ast).toBeAnInstanceOf(Interpolation); expect(unparser.unparse(ast)).toEqual('a {{ b }} c'); }); + + it('should support EmptyExpr', () => { + var ast = parser.parseAction('if (true) { }', null).ast; + expect(ast).toBeAnInstanceOf(If); + expect((ast).trueExp).toBeAnInstanceOf(EmptyExpr); + expect(unparser.unparse(ast)).toEqual('if (true) { }'); + }); }); }