From 18ceb4dfa751615cecc73ff38d7bb1744b914c0a Mon Sep 17 00:00:00 2001 From: Kasper Lund Date: Fri, 14 Mar 2014 14:27:24 +0100 Subject: [PATCH] feat(parser): Add support for named arguments. Closes #762 --- bin/parser_generator_for_spec.dart | 53 +++++++ example/pubspec.lock | 18 ++- lib/change_detection/ast.dart | 7 +- lib/change_detection/watch_group.dart | 73 +++++++--- lib/core/parser/dynamic_parser.dart | 22 +-- lib/core/parser/dynamic_parser_impl.dart | 100 +++++++++----- lib/core/parser/eval_calls.dart | 91 ++++++++---- lib/core/parser/parser.dart | 10 +- lib/core/parser/syntax.dart | 19 ++- lib/core/parser/unparser.dart | 18 ++- lib/core/scope.dart | 17 ++- lib/tools/parser_generator/dart_code_gen.dart | 18 ++- lib/tools/parser_getter_setter/generator.dart | 71 ++++++---- perf/watch_group_perf.dart | 7 +- test/animate/css_animate_spec.dart | 12 +- test/change_detection/watch_group_spec.dart | 115 ++++++++++++++-- test/core/parser/parser_spec.dart | 130 +++++++++++++++++- 17 files changed, 622 insertions(+), 159 deletions(-) diff --git a/bin/parser_generator_for_spec.dart b/bin/parser_generator_for_spec.dart index e0dbe26e8..4d12e2164 100644 --- a/bin/parser_generator_for_spec.dart +++ b/bin/parser_generator_for_spec.dart @@ -443,5 +443,58 @@ main(arguments) { 'x."foo"', '{(:0}', '{1234:0}', + + "sub1(1)", + "sub1(3, b: 2)", + "sub2()", + "sub2(a: 3)", + "sub2(a: 3, b: 2)", + "sub2(b: 4)", + + "o.sub1(1)", + "o.sub1(3, b: 2)", + "o.sub2()", + "o.sub2(a: 3)", + "o.sub2(a: 3, b: 2)", + "o.sub2(b: 4)", + + "(sub1)(1)", + "(sub1)(3, b: 2)", + "(sub2)()", + "(sub2)(a: 3)", + "(sub2)(a: 3, b: 2)", + "(sub2)(b: 4)", + + 'foo(a: 0, a: 1)', + 'foo(a: 0, b: 1, a: 2)', + 'foo(0, a: 1, a: 2)', + 'foo(0, a: 1, b: 2, a: 3)', + + 'foo(if: 0)', + 'foo(a: 0, class: 0)', + + 'foo(a: 0)', + 'foo(a: 0, b: 1)', + 'foo(b: 1, a: 0)', + 'foo(0)', + 'foo(0, a: 0)', + 'foo(0, a: 0, b: 1)', + 'foo(0, b: 1, a: 0)', + + 'o.foo(a: 0)', + 'o.foo(a: 0, b: 1)', + 'o.foo(b: 1, a: 0)', + 'o.foo(0)', + 'o.foo(0, a: 0)', + 'o.foo(0, a: 0, b: 1)', + 'o.foo(0, b: 1, a: 0)', + + '(foo)(a: 0)', + '(foo)(a: 0, b: 1)', + '(foo)(b: 1, a: 0)', + '(foo)(0)', + '(foo)(0, a: 0)', + '(foo)(0, a: 0, b: 1)', + '(foo)(0, b: 1, a: 0)', ]); } diff --git a/example/pubspec.lock b/example/pubspec.lock index 08762dadd..6119c93d2 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -4,7 +4,7 @@ packages: analyzer: description: analyzer source: hosted - version: "0.11.10" + version: "0.12.2" angular: description: path: ".." @@ -15,10 +15,18 @@ packages: description: args source: hosted version: "0.9.0" + barback: + description: barback + source: hosted + version: "0.11.1" browser: description: browser source: hosted version: "0.9.1" + code_transformers: + description: code_transformers + source: hosted + version: "0.0.1-dev.2" collection: description: collection source: hosted @@ -26,7 +34,7 @@ packages: di: description: di source: hosted - version: "0.0.33" + version: "0.0.34" html5lib: description: html5lib source: hosted @@ -34,11 +42,15 @@ packages: intl: description: intl source: hosted - version: "0.9.7" + version: "0.8.10+4" logging: description: logging source: hosted version: "0.9.1+1" + meta: + description: meta + source: hosted + version: "0.8.8" path: description: path source: hosted diff --git a/lib/change_detection/ast.dart b/lib/change_detection/ast.dart index 294679722..a82d7d75c 100644 --- a/lib/change_detection/ast.dart +++ b/lib/change_detection/ast.dart @@ -84,7 +84,7 @@ class PureFunctionAST extends AST { super('$name(${_argList(argsAST)})'); WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) => - watchGroup.addFunctionWatch(fn, argsAST, expression); + watchGroup.addFunctionWatch(fn, argsAST, const {}, expression); } /** @@ -96,15 +96,16 @@ class MethodAST extends AST { final AST lhsAST; final String name; final List argsAST; + final Map namedArgsAST; - MethodAST(lhsAST, name, argsAST) + MethodAST(lhsAST, name, argsAST, [this.namedArgsAST = const {}]) : lhsAST = lhsAST, name = name, argsAST = argsAST, super('$lhsAST.$name(${_argList(argsAST)})'); WatchRecord<_Handler> setupWatch(WatchGroup watchGroup) => - watchGroup.addMethodWatch(lhsAST, name, argsAST, expression); + watchGroup.addMethodWatch(lhsAST, name, argsAST, namedArgsAST, expression); } diff --git a/lib/change_detection/watch_group.dart b/lib/change_detection/watch_group.dart index a409a0643..53a850385 100644 --- a/lib/change_detection/watch_group.dart +++ b/lib/change_detection/watch_group.dart @@ -186,8 +186,9 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { * - [expression] normalized expression used for caching. */ _EvalWatchRecord addFunctionWatch(/* dartbug.com/16401 Function */ fn, List argsAST, + Map namedArgsAST, String expression) => - _addEvalWatch(null, fn, null, argsAST, expression); + _addEvalWatch(null, fn, null, argsAST, namedArgsAST, expression); /** * Watch a method [name]ed represented by an [expression]. @@ -198,13 +199,16 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { * - [expression] normalized expression used for caching. */ _EvalWatchRecord addMethodWatch(AST lhs, String name, List argsAST, + Map namedArgsAST, String expression) => - _addEvalWatch(lhs, null, name, argsAST, expression); + _addEvalWatch(lhs, null, name, argsAST, namedArgsAST, expression); _EvalWatchRecord _addEvalWatch(AST lhsAST, /* dartbug.com/16401 Function */ fn, String name, - List argsAST, String expression) { + List argsAST, + Map namedArgsAST, + String expression) { _InvokeHandler invokeHandler = new _InvokeHandler(this, expression); var evalWatchRecord = new _EvalWatchRecord(this, invokeHandler, fn, name, argsAST.length); @@ -218,15 +222,24 @@ class WatchGroup implements _EvalWatchList, _WatchGroupList { } // Convert the args from AST to WatchRecords - var i = 0; - argsAST.map((ast) => - _cache.putIfAbsent(ast.expression, () => ast.setupWatch(this))) - .forEach((WatchRecord<_Handler> record) { - var argHandler = new _ArgHandler(this, evalWatchRecord, i++); - _ArgHandlerList._add(invokeHandler, argHandler); - record.handler.addForwardHandler(argHandler); - argHandler.acceptValue(record.currentValue); - }); + Iterable> records = argsAST.map((ast) => + _cache.putIfAbsent(ast.expression, () => ast.setupWatch(this))); + int i = 0; + records.forEach((WatchRecord<_Handler> record) { + _ArgHandler handler = new _PositionalArgHandler(this, evalWatchRecord, i++); + _ArgHandlerList._add(invokeHandler, handler); + record.handler.addForwardHandler(handler); + handler.acceptValue(record.currentValue); + }); + + namedArgsAST.forEach((Symbol name, AST ast) { + WatchRecord<_Handler> record = _cache.putIfAbsent(ast.expression, + () => ast.setupWatch(this)); + _ArgHandler handler = new _NamedArgHandler(this, evalWatchRecord, name); + _ArgHandlerList._add(invokeHandler, handler); + record.handler.addForwardHandler(handler); + handler.acceptValue(record.currentValue); + }); // Must be done last _EvalWatchList._add(this, evalWatchRecord); @@ -615,18 +628,22 @@ class _CollectionHandler extends _Handler { } } -class _ArgHandler extends _Handler { +abstract class _ArgHandler extends _Handler { _ArgHandler _previousArgHandler, _nextArgHandler; // TODO(misko): Why do we override parent? final _EvalWatchRecord watchRecord; - final int index; + _ArgHandler(WatchGroup watchGrp, String expression, this.watchRecord) + : super(watchGrp, expression); _releaseWatch() => null; +} - _ArgHandler(WatchGroup watchGrp, this.watchRecord, int index) - : index = index, - super(watchGrp, 'arg[$index]'); +class _PositionalArgHandler extends _ArgHandler { + final int index; + _PositionalArgHandler(WatchGroup watchGrp, _EvalWatchRecord record, int index) + : this.index = index, + super(watchGrp, 'arg[$index]', record); void acceptValue(object) { watchRecord.dirtyArgs = true; @@ -634,6 +651,19 @@ class _ArgHandler extends _Handler { } } +class _NamedArgHandler extends _ArgHandler { + final Symbol name; + + _NamedArgHandler(WatchGroup watchGrp, _EvalWatchRecord record, Symbol name) + : this.name = name, + super(watchGrp, 'namedArg[$name]', record); + + void acceptValue(object) { + watchRecord.dirtyArgs = true; + watchRecord.namedArgs[name] = object; + } +} + class _InvokeHandler extends _Handler implements _ArgHandlerList { _ArgHandler _argHandlerHead, _argHandlerTail; @@ -675,6 +705,7 @@ class _EvalWatchRecord implements WatchRecord<_Handler>, Record<_Handler> { WatchGroup watchGrp; final _Handler handler; final List args; + final Map namedArgs = new Map(); final Symbol symbol; final String name; int mode; @@ -751,7 +782,7 @@ class _EvalWatchRecord implements WatchRecord<_Handler>, Record<_Handler> { return false; case _MODE_FUNCTION_: if (!dirtyArgs) return false; - value = Function.apply(fn, args); + value = Function.apply(fn, args, namedArgs); dirtyArgs = false; break; case _MODE_FUNCTION_APPLY_: @@ -761,14 +792,14 @@ class _EvalWatchRecord implements WatchRecord<_Handler>, Record<_Handler> { break; case _MODE_FIELD_CLOSURE_: var closure = _instanceMirror.getField(symbol).reflectee; - value = closure == null ? null : Function.apply(closure, args); + value = closure == null ? null : Function.apply(closure, args, namedArgs); break; case _MODE_MAP_CLOSURE_: var closure = object[name]; - value = closure == null ? null : Function.apply(closure, args); + value = closure == null ? null : Function.apply(closure, args, namedArgs); break; case _MODE_METHOD_: - value = _instanceMirror.invoke(symbol, args).reflectee; + value = _instanceMirror.invoke(symbol, args, namedArgs).reflectee; break; default: assert(false); diff --git a/lib/core/parser/dynamic_parser.dart b/lib/core/parser/dynamic_parser.dart index ef70c5920..a685040ab 100644 --- a/lib/core/parser/dynamic_parser.dart +++ b/lib/core/parser/dynamic_parser.dart @@ -14,7 +14,7 @@ import 'package:angular/core/parser/utils.dart' show EvalError; class ClosureMap { Getter lookupGetter(String name) => null; Setter lookupSetter(String name) => null; - Function lookupFunction(String name, int arity) => null; + Function lookupFunction(String name, CallArguments arguments) => null; } @NgInjectableService() @@ -128,7 +128,7 @@ class DynamicParserBackend extends ParserBackend { Expression newCallScope(name, arguments) { Function constructor = _computeCallConstructor( - _callScopeConstructors, name, arguments.length); + _callScopeConstructors, name, arguments); return (constructor != null) ? constructor(name, arguments, _closures) : new CallScope(name, arguments); @@ -136,25 +136,27 @@ class DynamicParserBackend extends ParserBackend { Expression newCallMember(object, name, arguments) { Function constructor = _computeCallConstructor( - _callMemberConstructors, name, arguments.length); + _callMemberConstructors, name, arguments); return (constructor != null) ? constructor(object, name, arguments, _closures) : new CallMember(object, name, arguments); } - Function _computeCallConstructor(Map constructors, String name, int arity) { - Function function = _closures.lookupFunction(name, arity); - return (function == null) ? null : constructors[arity]; + Function _computeCallConstructor(Map constructors, + String name, + CallArguments arguments) { + Function function = _closures.lookupFunction(name, arguments); + return (function == null) ? null : constructors[arguments.arity]; } static final Map _callScopeConstructors = { - 0: (n, a, c) => new CallScopeFast0(n, a, c.lookupFunction(n, 0)), - 1: (n, a, c) => new CallScopeFast1(n, a, c.lookupFunction(n, 1)), + 0: (n, a, c) => new CallScopeFast0(n, a, c.lookupFunction), + 1: (n, a, c) => new CallScopeFast1(n, a, c.lookupFunction), }; static final Map _callMemberConstructors = { - 0: (o, n, a, c) => new CallMemberFast0(o, n, a, c.lookupFunction(n, 0)), - 1: (o, n, a, c) => new CallMemberFast1(o, n, a, c.lookupFunction(n, 1)), + 0: (o, n, a, c) => new CallMemberFast0(o, n, a, c.lookupFunction), + 1: (o, n, a, c) => new CallMemberFast1(o, n, a, c.lookupFunction), }; } diff --git a/lib/core/parser/dynamic_parser_impl.dart b/lib/core/parser/dynamic_parser_impl.dart index 12a060f9d..a7b5ab8af 100644 --- a/lib/core/parser/dynamic_parser_impl.dart +++ b/lib/core/parser/dynamic_parser_impl.dart @@ -4,6 +4,7 @@ import 'package:angular/core/parser/parser.dart' show ParserBackend; import 'package:angular/core/parser/lexer.dart'; import 'package:angular/core/parser/syntax.dart'; import 'package:angular/core/parser/characters.dart'; +import 'package:angular/utils.dart' show isReservedWord; class DynamicParserImpl { final ParserBackend backend; @@ -14,7 +15,10 @@ class DynamicParserImpl { DynamicParserImpl(Lexer lexer, this.backend, String input) : this.input = input, tokens = lexer.call(input); - Token get peek => index < tokens.length ? tokens[index] : Token.EOF; + Token get next => peek(0); + Token peek(int offset) => (index + offset < tokens.length) + ? tokens[index + offset] + : Token.EOF; parseChain() { bool isChain = false; @@ -23,10 +27,10 @@ class DynamicParserImpl { } List expressions = []; while (index < tokens.length) { - if (peek.isCharacter($RPAREN) || - peek.isCharacter($RBRACE) || - peek.isCharacter($RBRACKET)) { - error('Unconsumed token $peek'); + if (next.isCharacter($RPAREN) || + next.isCharacter($RBRACE) || + next.isCharacter($RBRACKET)) { + error('Unconsumed token $next'); } var expr = parseFilter(); expressions.add(expr); @@ -57,11 +61,11 @@ class DynamicParserImpl { } parseExpression() { - int start = peek.index; + int start = next.index; var result = parseConditional(); - while (peek.isOperator('=')) { + while (next.isOperator('=')) { if (!backend.isAssignable(result)) { - int end = (index < tokens.length) ? peek.index : input.length; + int end = (index < tokens.length) ? next.index : input.length; String expression = input.substring(start, end); error('Expression $expression is not assignable'); } @@ -72,12 +76,12 @@ class DynamicParserImpl { } parseConditional() { - int start = peek.index; + int start = next.index; var result = parseLogicalOr(); if (optionalOperator('?')) { var yes = parseExpression(); if (!optionalCharacter($COLON)) { - int end = (index < tokens.length) ? peek.index : input.length; + int end = (index < tokens.length) ? next.index : input.length; String expression = input.substring(start, end); error('Conditional expression $expression requires all 3 expressions'); } @@ -188,7 +192,7 @@ class DynamicParserImpl { if (optionalCharacter($PERIOD)) { String name = expectIdentifierOrKeyword(); if (optionalCharacter($LPAREN)) { - List arguments = parseExpressionList($RPAREN); + CallArguments arguments = parseCallArguments(); expectCharacter($RPAREN); result = backend.newCallMember(result, name, arguments); } else { @@ -199,7 +203,7 @@ class DynamicParserImpl { expectCharacter($RBRACKET); result = backend.newAccessKeyed(result, key); } else if (optionalCharacter($LPAREN)) { - List arguments = parseExpressionList($RPAREN); + CallArguments arguments = parseCallArguments(); expectCharacter($RPAREN); result = backend.newCallFunction(result, arguments); } else { @@ -213,42 +217,42 @@ class DynamicParserImpl { var result = parseFilter(); expectCharacter($RPAREN); return result; - } else if (peek.isKeywordNull || peek.isKeywordUndefined) { + } else if (next.isKeywordNull || next.isKeywordUndefined) { advance(); return backend.newLiteralNull(); - } else if (peek.isKeywordTrue) { + } else if (next.isKeywordTrue) { advance(); return backend.newLiteralBoolean(true); - } else if (peek.isKeywordFalse) { + } else if (next.isKeywordFalse) { advance(); return backend.newLiteralBoolean(false); } else if (optionalCharacter($LBRACKET)) { List elements = parseExpressionList($RBRACKET); expectCharacter($RBRACKET); return backend.newLiteralArray(elements); - } else if (peek.isCharacter($LBRACE)) { + } else if (next.isCharacter($LBRACE)) { return parseObject(); - } else if (peek.isIdentifier) { + } else if (next.isIdentifier) { return parseAccessOrCallScope(); - } else if (peek.isNumber) { - num value = peek.toNumber(); + } else if (next.isNumber) { + num value = next.toNumber(); advance(); return backend.newLiteralNumber(value); - } else if (peek.isString) { - String value = peek.toString(); + } else if (next.isString) { + String value = next.toString(); advance(); return backend.newLiteralString(value); } else if (index >= tokens.length) { throw 'Unexpected end of expression: $input'; } else { - error('Unexpected token $peek'); + error('Unexpected token $next'); } } parseAccessOrCallScope() { String name = expectIdentifierOrKeyword(); if (!optionalCharacter($LPAREN)) return backend.newAccessScope(name); - List arguments = parseExpressionList($RPAREN); + CallArguments arguments = parseCallArguments(); expectCharacter($RPAREN); return backend.newCallScope(name, arguments); } @@ -271,7 +275,7 @@ class DynamicParserImpl { List parseExpressionList(int terminator) { List result = []; - if (!peek.isCharacter(terminator)) { + if (!next.isCharacter(terminator)) { do { result.add(parseExpression()); } while (optionalCharacter($COMMA)); @@ -279,8 +283,37 @@ class DynamicParserImpl { return result; } + CallArguments parseCallArguments() { + if (next.isCharacter($RPAREN)) { + return const CallArguments(const [], const {}); + } + // Parse the positional arguments. + List positionals = []; + while (true) { + if (peek(1).isCharacter($COLON)) break; + positionals.add(parseExpression()); + if (!optionalCharacter($COMMA)) { + return new CallArguments(positionals, const {}); + } + } + // Parse the named arguments. + Map named = {}; + do { + int marker = index; + String name = expectIdentifierOrKeyword(); + if (isReservedWord(name)) { + error("Cannot use Dart reserved word '$name' as named argument", marker); + } else if (named.containsKey(name)) { + error("Duplicate argument named '$name'", marker); + } + expectCharacter($COLON); + named[name] = parseExpression(); + } while (optionalCharacter($COMMA)); + return new CallArguments(positionals, named); + } + bool optionalCharacter(int code) { - if (peek.isCharacter(code)) { + if (next.isCharacter(code)) { advance(); return true; } else { @@ -289,7 +322,7 @@ class DynamicParserImpl { } bool optionalOperator(String operator) { - if (peek.isOperator(operator)) { + if (next.isOperator(operator)) { advance(); return true; } else { @@ -308,19 +341,19 @@ class DynamicParserImpl { } String expectIdentifierOrKeyword() { - if (!peek.isIdentifier && !peek.isKeyword) { - error('Unexpected token $peek, expected identifier or keyword'); + if (!next.isIdentifier && !next.isKeyword) { + error('Unexpected token $next, expected identifier or keyword'); } - String result = peek.toString(); + String result = next.toString(); advance(); return result; } String expectIdentifierOrKeywordOrString() { - if (!peek.isIdentifier && !peek.isKeyword && !peek.isString) { - error('Unexpected token $peek, expected identifier, keyword, or string'); + if (!next.isIdentifier && !next.isKeyword && !next.isString) { + error('Unexpected token $next, expected identifier, keyword, or string'); } - String result = peek.toString(); + String result = next.toString(); advance(); return result; } @@ -329,7 +362,8 @@ class DynamicParserImpl { index++; } - void error(message) { + void error(message, [int index]) { + if (index == null) index = this.index; String location = (index < tokens.length) ? 'at column ${tokens[index].index + 1} in' : 'the end of the expression'; diff --git a/lib/core/parser/eval_calls.dart b/lib/core/parser/eval_calls.dart index 0cd58f551..12d2f82bf 100644 --- a/lib/core/parser/eval_calls.dart +++ b/lib/core/parser/eval_calls.dart @@ -5,6 +5,8 @@ import 'package:angular/core/parser/syntax.dart' as syntax; import 'package:angular/core/parser/utils.dart'; import 'package:angular/core/module.dart'; +typedef Function LookupFunction(String name, syntax.CallArguments arguments); + class CallScope extends syntax.CallScope with CallReflective { final Symbol symbol; CallScope(name, arguments) @@ -22,29 +24,45 @@ class CallMember extends syntax.CallMember with CallReflective { } class CallScopeFast0 extends syntax.CallScope with CallFast { - final Function function; - CallScopeFast0(name, arguments, this.function) : super(name, arguments); + final Function callProperty; + final Function callMap; + CallScopeFast0(name, arguments, LookupFunction lookup) + : this.callProperty = lookup(name, arguments), + this.callMap = lookup("", arguments), + super(name, arguments); + eval(scope, [FilterMap filters]) => _evaluate0(scope); } class CallScopeFast1 extends syntax.CallScope with CallFast { - final Function function; - CallScopeFast1(name, arguments, this.function) : super(name, arguments); + final Function callProperty; + final Function callMap; + CallScopeFast1(name, arguments, LookupFunction lookup) + : callProperty = lookup(name, arguments), + callMap = lookup("", arguments), + super(name, arguments); + eval(scope, [FilterMap filters]) => _evaluate1(scope, arguments[0].eval(scope, filters)); } class CallMemberFast0 extends syntax.CallMember with CallFast { - final Function function; - CallMemberFast0(object, name, arguments, this.function) - : super(object, name, arguments); + final Function callProperty; + final Function callMap; + CallMemberFast0(object, name, arguments, LookupFunction lookup) + : this.callProperty = lookup(name, arguments) + , this.callMap = lookup("", arguments) + , super(object, name, arguments); eval(scope, [FilterMap filters]) => _evaluate0(object.eval(scope, filters)); } class CallMemberFast1 extends syntax.CallMember with CallFast { - final Function function; - CallMemberFast1(object, name, arguments, this.function) - : super(object, name, arguments); + final Function callProperty; + final Function callMap; + CallMemberFast1(object, name, arguments, LookupFunction lookup) + : this.callProperty = lookup(name, arguments) + , this.callMap = lookup("", arguments) + , super(object, name, arguments); eval(scope, [FilterMap filters]) => _evaluate1(object.eval(scope, filters), arguments[0].eval(scope, filters)); } @@ -56,7 +74,16 @@ class CallFunction extends syntax.CallFunction { if (function is! Function) { throw new EvalError('${this.function} is not a function'); } else { - return relaxFnApply(function, evalList(scope, arguments, filters)); + List positionals = evalList(scope, arguments.positionals, filters); + if (arguments.named.isNotEmpty) { + var named = new Map(); + arguments.named.forEach((String name, value) { + named[new Symbol(name)] = value.eval(scope, filters); + }); + return Function.apply(function, positionals, named); + } else { + return relaxFnApply(function, positionals); + } } } } @@ -77,24 +104,38 @@ abstract class CallReflective { String get name; Symbol get symbol; - List get arguments; + syntax.CallArguments get arguments; + // TODO(kasperl): This seems broken -- it needs filters. _eval(scope, holder) { - List arguments = evalList(scope, this.arguments); + List positionals = evalList(scope, arguments.positionals); + if (arguments.named.isNotEmpty) { + var named = new Map(); + arguments.named.forEach((String name, value) { + named[new Symbol(name)] = value.eval(scope); + }); + if (holder is Map) { + var fn = ensureFunctionFromMap(holder, name); + return Function.apply(fn, positionals, named); + } else { + return reflect(holder).invoke(symbol, positionals, named).reflectee; + } + } + if (!identical(holder, _cachedHolder)) { - return _evaluteUncached(holder, arguments); + return _evaluteUncached(holder, positionals); } return (_cachedKind == CACHED_MAP) - ? relaxFnApply(ensureFunctionFromMap(holder, name), arguments) - : _cachedValue.invoke(symbol, arguments).reflectee; + ? relaxFnApply(ensureFunctionFromMap(holder, name), positionals) + : _cachedValue.invoke(symbol, positionals).reflectee; } - _evaluteUncached(holder, arguments) { + _evaluteUncached(holder, positionals) { _cachedHolder = holder; if (holder is Map) { _cachedKind = CACHED_MAP; _cachedValue = null; - return relaxFnApply(ensureFunctionFromMap(holder, name), arguments); + return relaxFnApply(ensureFunctionFromMap(holder, name), positionals); } else if (symbol == null) { _cachedHolder = UNINITIALIZED; throw new EvalError("Undefined function $name"); @@ -102,7 +143,7 @@ abstract class CallReflective { InstanceMirror mirror = reflect(holder); _cachedKind = CACHED_FUNCTION; _cachedValue = mirror; - return mirror.invoke(symbol, arguments).reflectee; + return mirror.invoke(symbol, positionals).reflectee; } } } @@ -115,12 +156,14 @@ abstract class CallReflective { */ abstract class CallFast { String get name; - Function get function; + + Function get callMap; + Function get callProperty; _evaluate0(holder) => (holder is Map) - ? ensureFunctionFromMap(holder, name)() - : function(holder); + ? callMap(ensureFunctionFromMap(holder, name)) + : callProperty(holder); _evaluate1(holder, a0) => (holder is Map) - ? ensureFunctionFromMap(holder, name)(a0) - : function(holder, a0); + ? callMap(ensureFunctionFromMap(holder, name), a0) + : callProperty(holder, a0); } diff --git a/lib/core/parser/parser.dart b/lib/core/parser/parser.dart index 560497175..56d8ab184 100644 --- a/lib/core/parser/parser.dart +++ b/lib/core/parser/parser.dart @@ -1,7 +1,9 @@ library angular.core.parser; +import 'package:angular/core/parser/syntax.dart' + show CallArguments; export 'package:angular/core/parser/syntax.dart' - show Visitor, Expression, BoundExpression; + show Visitor, Expression, BoundExpression, CallArguments; export 'package:angular/core/parser/dynamic_parser.dart' show DynamicParser, DynamicParserBackend, ClosureMap; export 'package:angular/core/parser/static_parser.dart' @@ -31,9 +33,9 @@ abstract class ParserBackend { T newAccessMember(T object, String name) => null; T newAccessKeyed(T object, T key) => null; - T newCallScope(String name, List arguments) => null; - T newCallFunction(T function, List arguments) => null; - T newCallMember(T object, String name, List arguments) => null; + T newCallScope(String name, CallArguments arguments) => null; + T newCallFunction(T function, CallArguments arguments) => null; + T newCallMember(T object, String name, CallArguments arguments) => null; T newPrefix(String operation, T expression) => null; T newPrefixPlus(T expression) => expression; diff --git a/lib/core/parser/syntax.dart b/lib/core/parser/syntax.dart index 3e705c54b..63777cbf2 100644 --- a/lib/core/parser/syntax.dart +++ b/lib/core/parser/syntax.dart @@ -119,16 +119,29 @@ class AccessKeyed extends Expression { accept(Visitor visitor) => visitor.visitAccessKeyed(this); } +class CallArguments { + final List positionals; + final Map named; + const CallArguments(this.positionals, this.named); + + int get arity => positionals.length + named.length; + + Expression operator[](int index) { + int split = positionals.length; + return index < split ? positionals[index] : named.values.elementAt(index - split); + } +} + class CallScope extends Expression { final String name; - final List arguments; + final CallArguments arguments; CallScope(this.name, this.arguments); accept(Visitor visitor) => visitor.visitCallScope(this); } class CallFunction extends Expression { final Expression function; - final List arguments; + final CallArguments arguments; CallFunction(this.function, this.arguments); accept(Visitor visitor) => visitor.visitCallFunction(this); } @@ -136,7 +149,7 @@ class CallFunction extends Expression { class CallMember extends Expression { final Expression object; final String name; - final List arguments; + final CallArguments arguments; CallMember(this.object, this.name, this.arguments); accept(Visitor visitor) => visitor.visitCallMember(this); } diff --git a/lib/core/parser/unparser.dart b/lib/core/parser/unparser.dart index 8fb71bab4..906e4b912 100644 --- a/lib/core/parser/unparser.dart +++ b/lib/core/parser/unparser.dart @@ -17,12 +17,20 @@ class Unparser extends Visitor { buffer.write(string); } - void writeArguments(List arguments) { + void writeArguments(CallArguments arguments) { + bool first = true; write('('); - for (int i = 0; i < arguments.length; i++) { - if (i != 0) write(','); - visit(arguments[i]); + for (int i = 0; i < arguments.positionals.length; i++) { + if (!first) write(', '); + first = false; + visit(arguments.positionals[i]); } + arguments.named.forEach((String name, value) { + if (!first) write(', '); + first = false; + write('$name: '); + visit(value); + }); write(')'); } @@ -80,7 +88,9 @@ class Unparser extends Visitor { } void visitCallFunction(CallFunction call) { + write('('); visit(call.function); + write(')'); writeArguments(call.arguments); } diff --git a/lib/core/scope.dart b/lib/core/scope.dart index 331ba153c..81476599e 100644 --- a/lib/core/scope.dart +++ b/lib/core/scope.dart @@ -896,11 +896,24 @@ class ExpressionVisitor implements Visitor { List _toAst(List expressions) => expressions.map(_mapToAst).toList(); + Map _toAstMap(Map expressions) { + if (expressions.isEmpty) return const {}; + Map result = new Map(); + expressions.forEach((String name, Expression expression) { + result[new Symbol(name)] = _mapToAst(expression); + }); + return result; + } + void visitCallScope(CallScope exp) { - ast = new MethodAST(contextRef, exp.name, _toAst(exp.arguments)); + List positionals = _toAst(exp.arguments.positionals); + Map named = _toAstMap(exp.arguments.named); + ast = new MethodAST(contextRef, exp.name, positionals, named); } void visitCallMember(CallMember exp) { - ast = new MethodAST(visit(exp.object), exp.name, _toAst(exp.arguments)); + List positionals = _toAst(exp.arguments.positionals); + Map named = _toAstMap(exp.arguments.named); + ast = new MethodAST(visit(exp.object), exp.name, positionals, named); } visitAccessScope(AccessScope exp) { ast = new FieldReadAST(contextRef, exp.name); diff --git a/lib/tools/parser_generator/dart_code_gen.dart b/lib/tools/parser_generator/dart_code_gen.dart index 879a55df5..7a550f899 100644 --- a/lib/tools/parser_generator/dart_code_gen.dart +++ b/lib/tools/parser_generator/dart_code_gen.dart @@ -81,6 +81,18 @@ class DartCodeGenVisitor extends Visitor { } } + String evaluateArguments(CallArguments arguments) { + String positionals = arguments.positionals.map((e) => evaluate(e)).join(', '); + if (arguments.named.isEmpty) return positionals; + String named = arguments.named.keys.map((String name) { + String value = evaluate(arguments.named[name]); + return '$name: $value'; + }).join(', '); + return arguments.positionals.isEmpty + ? named + : "$positionals, $named"; + } + Function assign(Expression target) { int old = state; try { @@ -148,20 +160,20 @@ class DartCodeGenVisitor extends Visitor { } visitCallScope(CallScope call) { - String arguments = call.arguments.map((e) => evaluate(e)).join(', '); + String arguments = evaluateArguments(call.arguments); String getter = lookupGetter(call.name); return safeCallFunction('$getter(scope)', call.name, arguments); } visitCallFunction(CallFunction call) { String function = evaluate(call.function); - String arguments = call.arguments.map((e) => evaluate(e)).join(', '); + String arguments = evaluateArguments(call.arguments); return safeCallFunction(function, "${call.function}", arguments); } visitCallMember(CallMember call) { String object = evaluate(call.object); - String arguments = call.arguments.map((e) => evaluate(e)).join(', '); + String arguments = evaluateArguments(call.arguments); String getter = lookupGetter(call.name); return safeCallFunction('$getter($object)', call.name, arguments); } diff --git a/lib/tools/parser_getter_setter/generator.dart b/lib/tools/parser_getter_setter/generator.dart index b33244e10..f36f2cc99 100644 --- a/lib/tools/parser_getter_setter/generator.dart +++ b/lib/tools/parser_getter_setter/generator.dart @@ -4,7 +4,8 @@ import 'dart:math'; class DartGetterSetterGen extends ParserBackend { final Set properties = new Set(); - final Map> calls = new Map>(); + final Map> calls = + new Map>(); bool isAssignable(expression) => true; @@ -13,19 +14,26 @@ class DartGetterSetterGen extends ParserBackend { properties.add(name); } - registerCall(String name, List arguments) { + registerCall(String name, CallArguments arguments) { if (isReservedWord(name)) return; - Set arities = calls.putIfAbsent(name, () => new Set()); - arities.add(arguments.length); + Map map = calls.putIfAbsent(arguments.arity, + () => new Map()); + if (arguments.named.isEmpty) { + map[name] = map[''] = null; + } else { + Iterable names = arguments.named.keys; + String suffix = names.join(','); + map["$name:$suffix"] = map[":$suffix"] = names; + } } newAccessScope(String name) => registerAccess(name); newAccessMember(var object, String name) => registerAccess(name); - newCallScope(String name, List arguments) + newCallScope(String name, CallArguments arguments) => registerCall(name, arguments); - newCallMember(var object, String name, List arguments) + newCallMember(var object, String name, CallArguments arguments) => registerCall(name, arguments); } @@ -47,7 +55,8 @@ class ParserGetterSetter { print(generateClosureMap(backend.properties, backend.calls)); } - generateClosureMap(Set properties, Map> calls) { + generateClosureMap(Set properties, + Map> calls) { return ''' class StaticClosureMap extends ClosureMap { Map _getters = ${generateGetterMap(properties)}; @@ -58,8 +67,13 @@ class StaticClosureMap extends ClosureMap { => _getters[name]; Setter lookupSetter(String name) => _setters[name]; - lookupFunction(String name, int arity) - => (arity < _functions.length) ? _functions[arity][name] : null; + Function lookupFunction(String name, CallArguments arguments) { + int arity = arguments.arity; + if (arity >= _functions.length) return null; + Iterable named = arguments.named.keys; + Map map = _functions[arity]; + return (named.isEmpty) ? map[name] : map["\$name:\${named.join(',')}"]; + } } '''; } @@ -70,28 +84,39 @@ class StaticClosureMap extends ClosureMap { } generateSetterMap(Iterable keys) { - var lines = keys.map((key) => 'r"${key}": (o, v) => o.$key = v'); + var lines = keys.map((key) => 'r"${key}": (o,v) => o.$key = v'); return '{\n ${lines.join(",\n ")}\n }'; } - generateFunctionMap(Map> calls) { - Map> arities = {}; - calls.forEach((name, callArities) { - callArities.forEach((arity){ - arities.putIfAbsent(arity, () => new Set()).add(name); - }); - }); - - var maxArity = arities.keys.reduce((x, y) => max(x, y)); - + generateFunctionMap(Map> calls) { + var maxArity = calls.keys.reduce((x, y) => max(x, y)); var maps = new Iterable.generate(maxArity, (arity) { - var names = arities[arity]; + Map names = calls[arity]; if (names == null) { return '{\n }'; } else { var args = new List.generate(arity, (e) => "a$e").join(','); - var p = args.isEmpty ? '' : ', $args'; - var lines = names.map((name) => 'r"$name": (o$p) => o.$name($args)'); + var p = args.isEmpty ? '' : ',$args'; + var lines = []; + names.forEach((String name, Iterable names) { + if (name == "") { + lines.add('r"": (f$p) => f($args)'); + } else if (names == null) { + lines.add('r"$name": (o$p) => o.$name($args)'); + } else { + int positionals = arity - names.length; + var pos = new List.generate(positionals, (e) => "a$e").join(','); + if (pos.isNotEmpty) pos = '$pos,'; + int index = positionals; + var n = names.map((e) => "$e:a${index++}").join(','); + if (name.startsWith(':')) { + lines.add('r"$name": (f$p) => f(${pos}${n})'); + } else { + var shortName = name.substring(0, name.indexOf(':')); + lines.add('r"$name": (o$p) => o.$shortName(${pos}${n})'); + } + } + }); return '{\n ${lines.join(",\n ")}\n }'; } }); diff --git a/perf/watch_group_perf.dart b/perf/watch_group_perf.dart index bfc2ab6a5..d1dedc4fc 100644 --- a/perf/watch_group_perf.dart +++ b/perf/watch_group_perf.dart @@ -229,9 +229,10 @@ _function2() { AST _add(id, lhs, rhs) => new PureFunctionAST('add$id', (a, b) => a + b, [lhs, rhs]); -AST _method(lhs, methodName, [args]) { - if (args == null) args = []; - return new MethodAST(_parse(lhs), methodName, args); +AST _method(lhs, methodName, [args, namedArgs]) { + if (args == null) args = const []; + if (namedArgs == null) namedArgs = const {}; + return new MethodAST(_parse(lhs), methodName, args, namedArgs); } AST _parse(String expression) { diff --git a/test/animate/css_animate_spec.dart b/test/animate/css_animate_spec.dart index 70cbe2ee4..597eccfe7 100644 --- a/test/animate/css_animate_spec.dart +++ b/test/animate/css_animate_spec.dart @@ -20,7 +20,7 @@ main() { it('should add a css class to an element node', async(() { _.compile('
'); expect(_.rootElement).not.toHaveClass('foo'); - + animate.addClass(_.rootElement, 'foo'); runner.frame(); @@ -102,7 +102,7 @@ main() { class MockAnimationLoop extends Mock implements AnimationLoop { num time = 0.0; - + Future get onCompleted { var cmp = new Completer(); cmp.complete(AnimationResult.COMPLETED); @@ -110,20 +110,20 @@ class MockAnimationLoop extends Mock implements AnimationLoop { } List animations = []; - + play(LoopedAnimation animation) { animations.add(animation); } - + frame() { for(var animation in animations) { animation.read(time); } - + for(var animation in animations) { animation.update(time); } - + time += 16.0; } diff --git a/test/change_detection/watch_group_spec.dart b/test/change_detection/watch_group_spec.dart index 10dc78f61..3b13b71d0 100644 --- a/test/change_detection/watch_group_spec.dart +++ b/test/change_detection/watch_group_spec.dart @@ -5,19 +5,48 @@ import 'package:angular/change_detection/watch_group.dart'; import 'package:angular/change_detection/dirty_checking_change_detector.dart'; import 'dirty_checking_change_detector_spec.dart' hide main; +class TestData { + sub1(a, {b: 0}) => a - b; + sub2({a: 0, b: 0}) => a - b; +} + void main() { describe('WatchGroup', () { var context; var watchGrp; DirtyCheckingChangeDetector changeDetector; Logger logger; + AstParser parser; - AST parse(String expression) { - var currentAST = new ContextReferenceAST(); - expression.split('.').forEach((name) { - currentAST = new FieldReadAST(currentAST, name); - }); - return currentAST; + beforeEach(inject((Logger _logger, AstParser _parser) { + context = {}; + changeDetector = new DirtyCheckingChangeDetector(new GetterCache({})); + watchGrp = new RootWatchGroup(changeDetector, context); + logger = _logger; + parser = _parser; + })); + + AST parse(String expression) => parser(expression); + + eval(String expression, [evalContext]) { + AST ast = parse(expression); + + if (evalContext == null) evalContext = context; + WatchGroup group = watchGrp.newGroup(evalContext); + + List log = []; + Watch watch = group.watch(ast, (v, p) => log.add(v)); + + watchGrp.detectChanges(); + group.remove(); + + if (log.isEmpty) { + throw new StateError('Expression <$expression> was not evaluated'); + } else if (log.length > 1) { + throw new StateError('Expression <$expression> produced too many values: $log'); + } else { + return log.first; + } } expectOrder(list) { @@ -28,13 +57,6 @@ void main() { expect(logger).toEqual(list); } - beforeEach(inject((Logger _logger) { - context = {}; - changeDetector = new DirtyCheckingChangeDetector(new GetterCache({})); - watchGrp = new RootWatchGroup(changeDetector, context); - logger = _logger; - })); - describe('watch lifecycle', () { it('should prevent reaction fn on removed', () { context['a'] = 'hello'; @@ -560,6 +582,73 @@ void main() { }); }); + describe('evaluation', () { + it('should support simple literals', () { + expect(eval('42')).toBe(42); + expect(eval('87')).toBe(87); + }); + + it('should support context access', () { + context['x'] = 42; + expect(eval('x')).toBe(42); + context['y'] = 87; + expect(eval('y')).toBe(87); + }); + + it('should support custom context', () { + expect(eval('x', {'x': 42})).toBe(42); + expect(eval('x', {'x': 87})).toBe(87); + }); + + it('should support named arguments for scope calls', () { + var data = new TestData(); + expect(eval("sub1(1)", data)).toEqual(1); + expect(eval("sub1(3, b: 2)", data)).toEqual(1); + + expect(eval("sub2()", data)).toEqual(0); + expect(eval("sub2(a: 3)", data)).toEqual(3); + expect(eval("sub2(a: 3, b: 2)", data)).toEqual(1); + expect(eval("sub2(b: 4)", data)).toEqual(-4); + }); + + it('should support named arguments for scope calls (map)', () { + context["sub1"] = (a, {b: 0}) => a - b; + expect(eval("sub1(1)")).toEqual(1); + expect(eval("sub1(3, b: 2)")).toEqual(1); + + context["sub2"] = ({a: 0, b: 0}) => a - b; + expect(eval("sub2()")).toEqual(0); + expect(eval("sub2(a: 3)")).toEqual(3); + expect(eval("sub2(a: 3, b: 2)")).toEqual(1); + expect(eval("sub2(b: 4)")).toEqual(-4); + }); + + it('should support named arguments for member calls', () { + context['o'] = new TestData(); + expect(eval("o.sub1(1)")).toEqual(1); + expect(eval("o.sub1(3, b: 2)")).toEqual(1); + + expect(eval("o.sub2()")).toEqual(0); + expect(eval("o.sub2(a: 3)")).toEqual(3); + expect(eval("o.sub2(a: 3, b: 2)")).toEqual(1); + expect(eval("o.sub2(b: 4)")).toEqual(-4); + }); + + it('should support named arguments for member calls (map)', () { + context['o'] = { + 'sub1': (a, {b: 0}) => a - b, + 'sub2': ({a: 0, b: 0}) => a - b + }; + expect(eval("o.sub1(1)")).toEqual(1); + expect(eval("o.sub1(3, b: 2)")).toEqual(1); + + expect(eval("o.sub2()")).toEqual(0); + expect(eval("o.sub2(a: 3)")).toEqual(3); + expect(eval("o.sub2(a: 3, b: 2)")).toEqual(1); + expect(eval("o.sub2(b: 4)")).toEqual(-4); + }); + }); + describe('child group', () { it('should remove all field watches in group and group\'s children', () { watchGrp.watch(parse('a'), (v, p) => logger('0a')); diff --git a/test/core/parser/parser_spec.dart b/test/core/parser/parser_spec.dart index ff2d78760..190149185 100644 --- a/test/core/parser/parser_spec.dart +++ b/test/core/parser/parser_spec.dart @@ -10,6 +10,8 @@ class TestData { set str(x) => _str = x; method() => "testMethod"; + sub1(a, {b: 0}) => a - b; + sub2({a: 0, b: 0}) => a - b; } class Ident { @@ -48,10 +50,12 @@ main() { Map context; Parser parser; FilterMap filters; + beforeEachModule((Module module) { module.type(IncrementFilter); module.type(SubstringFilter); }); + beforeEach((Parser injectedParser, FilterMap injectedFilters) { parser = injectedParser; filters = injectedFilters; @@ -667,16 +671,15 @@ main() { expect(eval("constN()")).toEqual(123); }); + it('should access a protected keyword on scope', () { context['const'] = 3; expect(eval('const')).toEqual(3); }); - it('should evaluate function call with arguments', () { - context["add"] = (a,b) { - return a+b; - }; + it('should evaluate scope call with arguments', () { + context["add"] = (a,b) => a + b; expect(eval("add(1,2)")).toEqual(3); }); @@ -935,6 +938,7 @@ main() { }); }); + describe('assignable', () { it('should expose assignment function', () { var fn = parser('a'); @@ -945,6 +949,7 @@ main() { }); }); + describe('locals', () { it('should expose local variables', () { expect(parser('a').bind({'a': 6}, ScopeLocals.wrapper)({'a': 1})).toEqual(1); @@ -977,6 +982,123 @@ main() { }); + describe('named arguments', () { + it('should be supported for scope calls', () { + var data = new TestData(); + expect(parser("sub1(1)").eval(data)).toEqual(1); + expect(parser("sub1(3, b: 2)").eval(data)).toEqual(1); + + expect(parser("sub2()").eval(data)).toEqual(0); + expect(parser("sub2(a: 3)").eval(data)).toEqual(3); + expect(parser("sub2(a: 3, b: 2)").eval(data)).toEqual(1); + expect(parser("sub2(b: 4)").eval(data)).toEqual(-4); + }); + + + it('should be supported for scope calls (map)', () { + context["sub1"] = (a, {b: 0}) => a - b; + expect(eval("sub1(1)")).toEqual(1); + expect(eval("sub1(3, b: 2)")).toEqual(1); + + context["sub2"] = ({a: 0, b: 0}) => a - b; + expect(eval("sub2()")).toEqual(0); + expect(eval("sub2(a: 3)")).toEqual(3); + expect(eval("sub2(a: 3, b: 2)")).toEqual(1); + expect(eval("sub2(b: 4)")).toEqual(-4); + }); + + + it('should be supported for member calls', () { + context['o'] = new TestData(); + expect(eval("o.sub1(1)")).toEqual(1); + expect(eval("o.sub1(3, b: 2)")).toEqual(1); + + expect(eval("o.sub2()")).toEqual(0); + expect(eval("o.sub2(a: 3)")).toEqual(3); + expect(eval("o.sub2(a: 3, b: 2)")).toEqual(1); + expect(eval("o.sub2(b: 4)")).toEqual(-4); + }); + + + it('should be supported for member calls (map)', () { + context['o'] = { + 'sub1': (a, {b: 0}) => a - b, + 'sub2': ({a: 0, b: 0}) => a - b + }; + expect(eval("o.sub1(1)")).toEqual(1); + expect(eval("o.sub1(3, b: 2)")).toEqual(1); + + expect(eval("o.sub2()")).toEqual(0); + expect(eval("o.sub2(a: 3)")).toEqual(3); + expect(eval("o.sub2(a: 3, b: 2)")).toEqual(1); + expect(eval("o.sub2(b: 4)")).toEqual(-4); + }); + + + it('should be supported for function calls', () { + context["sub1"] = (a, {b: 0}) => a - b; + expect(eval("(sub1)(1)")).toEqual(1); + expect(eval("(sub1)(3, b: 2)")).toEqual(1); + + context["sub2"] = ({a: 0, b: 0}) => a - b; + expect(eval("(sub2)()")).toEqual(0); + expect(eval("(sub2)(a: 3)")).toEqual(3); + expect(eval("(sub2)(a: 3, b: 2)")).toEqual(1); + expect(eval("(sub2)(b: 4)")).toEqual(-4); + }); + + + it('should be an error to use the same name twice', () { + expect(() => parser('foo(a: 0, a: 1)')).toThrow("Duplicate argument named 'a' at column 11"); + expect(() => parser('foo(a: 0, b: 1, a: 2)')).toThrow("Duplicate argument named 'a' at column 17"); + expect(() => parser('foo(0, a: 1, a: 2)')).toThrow("Duplicate argument named 'a' at column 14"); + expect(() => parser('foo(0, a: 1, b: 2, a: 3)')).toThrow("Duplicate argument named 'a' at column 20"); + }); + + + it('should be an error to use Dart reserved words as names', () { + expect(() => parser('foo(if: 0)')).toThrow("Cannot use Dart reserved word 'if' as named argument at column 5"); + expect(() => parser('foo(a: 0, class: 0)')).toThrow("Cannot use Dart reserved word 'class' as named argument at column 11"); + }); + + + it('should pretty print scope calls correctly', () { + expect(parser('foo(a: 0)').toString()).toEqual('foo(a: 0)'); + expect(parser('foo(a: 0, b: 1)').toString()).toEqual('foo(a: 0, b: 1)'); + expect(parser('foo(b: 1, a: 0)').toString()).toEqual('foo(b: 1, a: 0)'); + + expect(parser('foo(0)').toString()).toEqual('foo(0)'); + expect(parser('foo(0, a: 0)').toString()).toEqual('foo(0, a: 0)'); + expect(parser('foo(0, a: 0, b: 1)').toString()).toEqual('foo(0, a: 0, b: 1)'); + expect(parser('foo(0, b: 1, a: 0)').toString()).toEqual('foo(0, b: 1, a: 0)'); + }); + + + it('should pretty print member calls correctly', () { + expect(parser('o.foo(a: 0)').toString()).toEqual('o.foo(a: 0)'); + expect(parser('o.foo(a: 0, b: 1)').toString()).toEqual('o.foo(a: 0, b: 1)'); + expect(parser('o.foo(b: 1, a: 0)').toString()).toEqual('o.foo(b: 1, a: 0)'); + + expect(parser('o.foo(0)').toString()).toEqual('o.foo(0)'); + expect(parser('o.foo(0, a: 0)').toString()).toEqual('o.foo(0, a: 0)'); + expect(parser('o.foo(0, a: 0, b: 1)').toString()).toEqual('o.foo(0, a: 0, b: 1)'); + expect(parser('o.foo(0, b: 1, a: 0)').toString()).toEqual('o.foo(0, b: 1, a: 0)'); + }); + + + it('should pretty print function calls correctly', () { + expect(parser('(foo)(a: 0)').toString()).toEqual('(foo)(a: 0)'); + expect(parser('(foo)(a: 0, b: 1)').toString()).toEqual('(foo)(a: 0, b: 1)'); + expect(parser('(foo)(b: 1, a: 0)').toString()).toEqual('(foo)(b: 1, a: 0)'); + + expect(parser('(foo)(0)').toString()).toEqual('(foo)(0)'); + expect(parser('(foo)(0, a: 0)').toString()).toEqual('(foo)(0, a: 0)'); + expect(parser('(foo)(0, a: 0, b: 1)').toString()).toEqual('(foo)(0, a: 0, b: 1)'); + expect(parser('(foo)(0, b: 1, a: 0)').toString()).toEqual('(foo)(0, b: 1, a: 0)'); + }); + }); + + describe('filters', () { it('should call a filter', () { expect(eval("'Foo'|uppercase", filters)).toEqual("FOO");