From e43527c9fecb0d799b01de51db8c6bcbaf421195 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 24 Jan 2012 02:33:35 -0800 Subject: [PATCH 1/5] fix($parse): get rid of $unboundFn Closes #731 --- src/Angular.js | 1 - src/service/parse.js | 63 +++++++++++++++++++++++++++++++-------- src/widget/input.js | 2 +- test/service/parseSpec.js | 22 ++++++++++++++ test/service/scopeSpec.js | 2 +- 5 files changed, 74 insertions(+), 16 deletions(-) diff --git a/src/Angular.js b/src/Angular.js index 7955ce61fac7..81261ab56591 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -686,7 +686,6 @@ function equals(o1, o2) { return true; } } - if (t1 == 'function' && o1.$unboundFn) return o1.$unboundFn === o2.$unboundFn; } return false; } diff --git a/src/service/parse.js b/src/service/parse.js index e8693819386c..c4a1258a7062 100644 --- a/src/service/parse.js +++ b/src/service/parse.js @@ -144,18 +144,40 @@ function lex(text){ fn:function() {return number;}}); } function readIdent() { - var ident = ""; - var start = index; - var fn; + var ident = "", + start = index, + fn, lastDot, peekIndex, methodName; + while (index < text.length) { var ch = text.charAt(index); if (ch == '.' || isIdent(ch) || isNumber(ch)) { + if (ch == '.') lastDot = index; ident += ch; } else { break; } index++; } + + //check if this is not a method invocation and if it is back out to last dot + if (lastDot) { + peekIndex = index + while(peekIndex < text.length) { + var ch = text.charAt(peekIndex); + if (ch == '(') { + methodName = ident.substr(lastDot - start + 1); + ident = ident.substr(0, lastDot - start); + index = peekIndex; + break; + } + if(isWhitespace(ch)) { + peekIndex++; + } else { + break; + } + } + } + fn = OPERATORS[ident]; tokens.push({ index:start, @@ -167,6 +189,19 @@ function lex(text){ } }) }); + + if (methodName) { + tokens.push({ + index:lastDot, + text: '.', + json: false + }); + tokens.push({ + index: lastDot + 1, + text: methodName, + json: false + }); + } } function readString(quote) { @@ -490,13 +525,17 @@ function parser(text, json, $filter){ throwError("not a primary expression", token); } } - var next; + + var next, context; while ((next = expect('(', '[', '.'))) { if (next.text === '(') { - primary = functionCall(primary); + primary = functionCall(primary, context); + context = null; } else if (next.text === '[') { + context = primary; primary = objectIndex(primary); } else if (next.text === '.') { + context = primary; primary = fieldAccess(primary); } else { throwError("IMPOSSIBLE"); @@ -544,7 +583,7 @@ function parser(text, json, $filter){ }); } - function _functionCall(fn) { + function _functionCall(fn, contextGetter) { var argsFn = []; if (peekToken().text != ')') { do { @@ -553,14 +592,16 @@ function parser(text, json, $filter){ } consume(')'); return function(self){ - var args = []; + var args = [], + context = contextGetter ? contextGetter(self) : self; + for ( var i = 0; i < argsFn.length; i++) { args.push(argsFn[i](self)); } var fnPtr = fn(self) || noop; // IE stupidity! return fnPtr.apply - ? fnPtr.apply(self, args) + ? fnPtr.apply(context, args) : fnPtr(args[0], args[1], args[2], args[3], args[4]); }; } @@ -691,11 +732,7 @@ function getterFn(path) { code += 'if(!s) return s;\n' + 'l=s;\n' + 's=s' + key + ';\n' + - 'if(typeof s=="function" && !(s instanceof RegExp)) {\n' + - ' fn=function(){ return l' + key + '.apply(l, arguments); };\n' + - ' fn.$unboundFn=s;\n' + - ' s=fn;\n' + - '} else if (s && s.then) {\n' + + 'if (s && s.then) {\n' + ' if (!("$$v" in s)) {\n' + ' p=s;\n' + ' p.$$v = undefined;\n' + diff --git a/src/widget/input.js b/src/widget/input.js index 9f522d18d7d5..05390b383ca8 100644 --- a/src/widget/input.js +++ b/src/widget/input.js @@ -741,7 +741,7 @@ var inputDirective = ['$defer', '$formFactory', function($defer, $formFactory) { type = lowercase(type); TypeController = (loadFromScope - ? (assertArgFn(modelScope.$eval(loadFromScope[1]), loadFromScope[1])).$unboundFn + ? assertArgFn(modelScope.$eval(loadFromScope[1]), loadFromScope[1]) : angularInputType(type)) || noop; if (!HTML5_INPUTS_TYPES[type]) { diff --git a/test/service/parseSpec.js b/test/service/parseSpec.js index 08167843071b..d6355ada3631 100644 --- a/test/service/parseSpec.js +++ b/test/service/parseSpec.js @@ -110,6 +110,20 @@ describe('parser', function() { expect(tokens[3].text).toEqual(';'); }); + it('should tokenize method invocation', function() { + var tokens = lex("a.b.c (d) - e.f()"); + expect(tokens[0].text).toBe('a.b'); + expect(tokens[1].text).toBe('.'); + expect(tokens[2].text).toBe('c'); + expect(tokens[3].text).toBe('('); + expect(tokens[4].text).toBe('d'); + expect(tokens[5].text).toBe(')'); + expect(tokens[6].text).toBe('-'); + expect(tokens[7].text).toBe('e'); + expect(tokens[8].text).toBe('.'); + expect(tokens[9].text).toBe('f'); + }); + it('should tokenize number', function() { var tokens = lex("0.5"); expect(tokens[0].text).toEqual(0.5); @@ -244,6 +258,12 @@ describe('parser', function() { expect(scope.$eval("add(1,2)")).toEqual(3); }); + it('should evaluate function call from a return value', function() { + scope.val = 33; + scope.getter = function() { return function() { return this.val; }}; + expect(scope.$eval("getter()()")).toBe(33); + }); + it('should evaluate multiplication and division', function() { scope.taxRate = 8; scope.subTotal = 100; @@ -296,6 +316,7 @@ describe('parser', function() { scope.obj = new C(); expect(scope.$eval("obj.getA()")).toEqual(123); + expect(scope.$eval("obj['getA']()")).toEqual(123); }); it('should evaluate methods in correct context (this) in argument', function() { @@ -311,6 +332,7 @@ describe('parser', function() { scope.obj = new C(); expect(scope.$eval("obj.sum(obj.getA())")).toEqual(246); + expect(scope.$eval("obj['sum'](obj.getA())")).toEqual(246); }); it('should evaluate objects on scope context', function() { diff --git a/test/service/scopeSpec.js b/test/service/scopeSpec.js index b1b870f41ad6..cc3f93f5fb61 100644 --- a/test/service/scopeSpec.js +++ b/test/service/scopeSpec.js @@ -252,7 +252,7 @@ describe('Scope', function() { module(provideLog); inject(function($rootScope, log) { $rootScope.fn = function() {return 'a'}; - $rootScope.$watch('fn', function(scope, fn) { + $rootScope.$watch('fn', function(fn) { log(fn()); }); $rootScope.$digest(); From 91d29a34881da6b43db95c993140b05c3a25ff0d Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 24 Jan 2012 02:40:25 -0800 Subject: [PATCH 2/5] fix($parse): small fixes - typos - dead code removal - remove unneeded variable --- src/service/parse.js | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/src/service/parse.js b/src/service/parse.js index c4a1258a7062..f33aad0d4f94 100644 --- a/src/service/parse.js +++ b/src/service/parse.js @@ -4,7 +4,7 @@ var OPERATORS = { 'null':function(self){return null;}, 'true':function(self){return true;}, 'false':function(self){return false;}, - $undefined:noop, + undefined:noop, '+':function(self, a,b){a=a(self); b=b(self); return (isDefined(a)?a:0)+(isDefined(b)?b:0);}, '-':function(self, a,b){a=a(self); b=b(self); return (isDefined(a)?a:0)-(isDefined(b)?b:0);}, '*':function(self, a,b){return a(self)*b(self);}, @@ -511,9 +511,8 @@ function parser(text, json, $filter){ function primary() { var primary; if (expect('(')) { - var expression = filterChain(); + primary = filterChain(); consume(')'); - primary = expression; } else if (expect('[')) { primary = arrayDeclaration(); } else if (expect('{')) { @@ -646,22 +645,6 @@ function parser(text, json, $filter){ return object; }; } - - function watchDecl () { - var anchorName = expect().text; - consume(":"); - var expressionFn; - if (peekToken().text == '{') { - consume("{"); - expressionFn = statements(); - consume("}"); - } else { - expressionFn = expression(); - } - return function(self) { - return {name:anchorName, fn:expressionFn}; - }; - } } ////////////////////////////////////////////////// From 338258896d528a37041c06a89da1faffe439d618 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 24 Jan 2012 02:42:20 -0800 Subject: [PATCH 3/5] fix($parse): simplify getterFn --- src/service/parse.js | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/service/parse.js b/src/service/parse.js index f33aad0d4f94..4958884f2f12 100644 --- a/src/service/parse.js +++ b/src/service/parse.js @@ -693,17 +693,7 @@ function getter(obj, path, bindFnToScope) { return obj; } -var getterFnCache = {}, - JS_KEYWORDS = {}; - -forEach( - ("abstract,boolean,break,byte,case,catch,char,class,const,continue,debugger,default," + - "delete,do,double,else,enum,export,extends,false,final,finally,float,for,function,goto," + - "if,implements,import,in,instanceof,int,interface,long,native,new,null,package,private," + - "protected,public,return,short,static,super,switch,synchronized,this,throw,throws," + - "transient,true,try,typeof,var,volatile,void,undefined,while,with").split(/,/), - function(key){ JS_KEYWORDS[key] = true;} -); +var getterFnCache = {}; function getterFn(path) { var fn = getterFnCache[path]; @@ -711,10 +701,9 @@ function getterFn(path) { var code = 'var l, fn, p;\n'; forEach(path.split('.'), function(key) { - key = (JS_KEYWORDS[key]) ? '["' + key + '"]' : '.' + key; code += 'if(!s) return s;\n' + 'l=s;\n' + - 's=s' + key + ';\n' + + 's=s' + '["' + key + '"]' + ';\n' + 'if (s && s.then) {\n' + ' if (!("$$v" in s)) {\n' + ' p=s;\n' + From 75cff43529cef8e4fc12f03b4effa8865ebd8158 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 24 Jan 2012 15:58:13 -0800 Subject: [PATCH 4/5] SQUASH: more tests --- test/service/parseSpec.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/test/service/parseSpec.js b/test/service/parseSpec.js index d6355ada3631..d83cc050e045 100644 --- a/test/service/parseSpec.js +++ b/test/service/parseSpec.js @@ -110,18 +110,15 @@ describe('parser', function() { expect(tokens[3].text).toEqual(';'); }); + it('should tokenize function invocation', function() { + var tokens = lex("a()") + expect(map(tokens, function(t) { return t.text;})).toEqual(['a', '(', ')']); + }); + it('should tokenize method invocation', function() { var tokens = lex("a.b.c (d) - e.f()"); - expect(tokens[0].text).toBe('a.b'); - expect(tokens[1].text).toBe('.'); - expect(tokens[2].text).toBe('c'); - expect(tokens[3].text).toBe('('); - expect(tokens[4].text).toBe('d'); - expect(tokens[5].text).toBe(')'); - expect(tokens[6].text).toBe('-'); - expect(tokens[7].text).toBe('e'); - expect(tokens[8].text).toBe('.'); - expect(tokens[9].text).toBe('f'); + expect(map(tokens, function(t) { return t.text;})). + toEqual(['a.b', '.', 'c', '(', 'd', ')', '-', 'e', '.', 'f', '(', ')']); }); it('should tokenize number', function() { @@ -301,7 +298,7 @@ describe('parser', function() { expect(toJson(scope.$eval("[{a:[]}, {b:1}]"))).toEqual('[{"a":[]},{"b":1}]'); }); - it('should evaluate multipple statements', function() { + it('should evaluate multiple statements', function() { expect(scope.$eval("a=1;b=3;a+b")).toEqual(4); expect(scope.$eval(";;1;;")).toEqual(1); }); From fa6f4db4ffd57e28394d6e7af202ecc4bc3f9aff Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Tue, 24 Jan 2012 15:57:59 -0800 Subject: [PATCH 5/5] SQUASH: alternative implementation --- src/service/parse.js | 72 ++++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/src/service/parse.js b/src/service/parse.js index 4958884f2f12..11bfe7156467 100644 --- a/src/service/parse.js +++ b/src/service/parse.js @@ -33,7 +33,8 @@ function lex(text){ index = 0, json = [], ch, - lastCh = ':'; // can start regexp + lastCh = ':', // can start regexp + methodName, lastDot; while (index < text.length) { ch = text.charAt(index); @@ -48,7 +49,39 @@ function lex(text){ (token=tokens[tokens.length-1])) { token.json = token.text.indexOf('.') == -1; } - } else if (is('(){}[].,;:')) { + } else if (is('(')) { + token=tokens[tokens.length-1]; + if (token && !OPERATORS[token.text]) { + lastDot = token.text.lastIndexOf('.'); + if (lastDot !== -1) { + // method invocation + methodName = token.text.substr(lastDot + 1); + token.text = token.text.substr(0, lastDot); + token.fn = extend(getterFn(token.text), { + assign:function(self, value){ + return setter(self, token.text, value); + } + }); + tokens.push({ + index: token.index + token.text.length, + text: '.', + json: false + }); + tokens.push({ + index: token.index + token.text.length + 1, + text: methodName, + json: false + }); + } + } + tokens.push({ + index: index, + text: ch, + json: false + }); + json.unshift(ch); + index++; + } else if (is('){}[].,;:')) { tokens.push({ index:index, text:ch, @@ -146,12 +179,11 @@ function lex(text){ function readIdent() { var ident = "", start = index, - fn, lastDot, peekIndex, methodName; + fn; while (index < text.length) { var ch = text.charAt(index); if (ch == '.' || isIdent(ch) || isNumber(ch)) { - if (ch == '.') lastDot = index; ident += ch; } else { break; @@ -159,25 +191,6 @@ function lex(text){ index++; } - //check if this is not a method invocation and if it is back out to last dot - if (lastDot) { - peekIndex = index - while(peekIndex < text.length) { - var ch = text.charAt(peekIndex); - if (ch == '(') { - methodName = ident.substr(lastDot - start + 1); - ident = ident.substr(0, lastDot - start); - index = peekIndex; - break; - } - if(isWhitespace(ch)) { - peekIndex++; - } else { - break; - } - } - } - fn = OPERATORS[ident]; tokens.push({ index:start, @@ -189,19 +202,6 @@ function lex(text){ } }) }); - - if (methodName) { - tokens.push({ - index:lastDot, - text: '.', - json: false - }); - tokens.push({ - index: lastDot + 1, - text: methodName, - json: false - }); - } } function readString(quote) {