Skip to content

Commit 978d4a3

Browse files
committed
fix($parse): make promise unwrapping opt-in
Previously promises found anywhere in the expression during expression evaluation would evaluate to undefined while unresolved and to the fulfillment value if fulfilled. This is a feature that didn't prove to be wildly useful or popular, primarily because of the dichotomy between data access in templates (accessed as raw values) and controller code (accessed as promises). In most code we ended up resolving promises manually in controllers anyway and thus unifying the model access there. Other downsides of automatic promise unwrapping: - when building components it's often desirable to receive the raw promises - adds complexity and slows down expression evaluation - makes expression code pre-generation unattractive due to the amount of code that needs to be generated - makes IDE auto-completion and tool support hard - adds too much magic Closes angular#4158 Closes angular#4270
1 parent 49e06ea commit 978d4a3

File tree

2 files changed

+306
-158
lines changed

2 files changed

+306
-158
lines changed

src/ng/parse.js

+121-41
Original file line numberDiff line numberDiff line change
@@ -99,15 +99,16 @@ var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'
9999
/**
100100
* @constructor
101101
*/
102-
var Lexer = function (csp) {
103-
this.csp = csp;
102+
var Lexer = function (options) {
103+
this.options = options;
104104
};
105105

106106
Lexer.prototype = {
107107
constructor: Lexer,
108108

109109
lex: function (text) {
110110
this.text = text;
111+
111112
this.index = 0;
112113
this.ch = undefined;
113114
this.lastCh = ':'; // can start regexp
@@ -294,12 +295,12 @@ Lexer.prototype = {
294295
token.fn = OPERATORS[ident];
295296
token.json = OPERATORS[ident];
296297
} else {
297-
var getter = getterFn(ident, this.csp, this.text);
298+
var getter = getterFn(ident, this.options, this.text);
298299
token.fn = extend(function(self, locals) {
299300
return (getter(self, locals));
300301
}, {
301302
assign: function(self, value) {
302-
return setter(self, ident, value, parser.text);
303+
return setter(self, ident, value, parser.text, parser.options);
303304
}
304305
});
305306
}
@@ -370,10 +371,10 @@ Lexer.prototype = {
370371
/**
371372
* @constructor
372373
*/
373-
var Parser = function (lexer, $filter, csp) {
374+
var Parser = function (lexer, $filter, options) {
374375
this.lexer = lexer;
375376
this.$filter = $filter;
376-
this.csp = csp;
377+
this.options = options;
377378
};
378379

379380
Parser.ZERO = function () { return 0; };
@@ -387,7 +388,7 @@ Parser.prototype = {
387388
//TODO(i): strip all the obsolte json stuff from this file
388389
this.json = json;
389390

390-
this.tokens = this.lexer.lex(text, this.csp);
391+
this.tokens = this.lexer.lex(text);
391392

392393
if (json) {
393394
// The extra level of aliasing is here, just in case the lexer misses something, so that
@@ -687,13 +688,13 @@ Parser.prototype = {
687688
fieldAccess: function(object) {
688689
var parser = this;
689690
var field = this.expect().text;
690-
var getter = getterFn(field, this.csp, this.text);
691+
var getter = getterFn(field, this.options, this.text);
691692

692693
return extend(function(scope, locals, self) {
693694
return getter(self || object(scope, locals), locals);
694695
}, {
695696
assign: function(scope, value, locals) {
696-
return setter(object(scope, locals), field, value, parser.text);
697+
return setter(object(scope, locals), field, value, parser.text, parser.options);
697698
}
698699
});
699700
},
@@ -711,7 +712,7 @@ Parser.prototype = {
711712

712713
if (!o) return undefined;
713714
v = ensureSafeObject(o[i], parser.text);
714-
if (v && v.then) {
715+
if (v && v.then && parser.options.unwrapPromises) {
715716
p = v;
716717
if (!('$$v' in v)) {
717718
p.$$v = undefined;
@@ -758,7 +759,7 @@ Parser.prototype = {
758759
: fnPtr(args[0], args[1], args[2], args[3], args[4]);
759760

760761
// Check for promise
761-
if (v && v.then) {
762+
if (v && v.then && parser.options.unwrapPromises) {
762763
var p = v;
763764
if (!('$$v' in v)) {
764765
p.$$v = undefined;
@@ -826,15 +827,17 @@ Parser.prototype = {
826827
literal: true,
827828
constant: allConstant
828829
});
829-
},
830+
}
830831
};
831832

832833

833834
//////////////////////////////////////////////////
834835
// Parser helper functions
835836
//////////////////////////////////////////////////
836837

837-
function setter(obj, path, setValue, fullExp) {
838+
function setter(obj, path, setValue, fullExp, options) {
839+
options = options || {};
840+
838841
var element = path.split('.'), key;
839842
for (var i = 0; element.length > 1; i++) {
840843
key = ensureSafeMemberName(element.shift(), fullExp);
@@ -844,7 +847,7 @@ function setter(obj, path, setValue, fullExp) {
844847
obj[key] = propertyObj;
845848
}
846849
obj = propertyObj;
847-
if (obj.then) {
850+
if (obj.then && options.unwrapPromises) {
848851
if (!("$$v" in obj)) {
849852
(function(promise) {
850853
promise.then(function(val) { promise.$$v = val; }); }
@@ -868,7 +871,7 @@ var getterFnCache = {};
868871
* - http://jsperf.com/angularjs-parse-getter/4
869872
* - http://jsperf.com/path-evaluation-simplified/7
870873
*/
871-
function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) {
874+
function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp, options) {
872875
ensureSafeMemberName(key0, fullExp);
873876
ensureSafeMemberName(key1, fullExp);
874877
ensureSafeMemberName(key2, fullExp);
@@ -881,7 +884,7 @@ function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) {
881884
if (pathVal === null || pathVal === undefined) return pathVal;
882885

883886
pathVal = pathVal[key0];
884-
if (pathVal && pathVal.then) {
887+
if (pathVal && pathVal.then && options.unwrapPromises) {
885888
if (!("$$v" in pathVal)) {
886889
promise = pathVal;
887890
promise.$$v = undefined;
@@ -892,7 +895,7 @@ function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) {
892895
if (!key1 || pathVal === null || pathVal === undefined) return pathVal;
893896

894897
pathVal = pathVal[key1];
895-
if (pathVal && pathVal.then) {
898+
if (pathVal && pathVal.then && options.unwrapPromises) {
896899
if (!("$$v" in pathVal)) {
897900
promise = pathVal;
898901
promise.$$v = undefined;
@@ -903,7 +906,7 @@ function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) {
903906
if (!key2 || pathVal === null || pathVal === undefined) return pathVal;
904907

905908
pathVal = pathVal[key2];
906-
if (pathVal && pathVal.then) {
909+
if (pathVal && pathVal.then && options.unwrapPromises) {
907910
if (!("$$v" in pathVal)) {
908911
promise = pathVal;
909912
promise.$$v = undefined;
@@ -914,7 +917,7 @@ function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) {
914917
if (!key3 || pathVal === null || pathVal === undefined) return pathVal;
915918

916919
pathVal = pathVal[key3];
917-
if (pathVal && pathVal.then) {
920+
if (pathVal && pathVal.then && options.unwrapPromises) {
918921
if (!("$$v" in pathVal)) {
919922
promise = pathVal;
920923
promise.$$v = undefined;
@@ -925,7 +928,7 @@ function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) {
925928
if (!key4 || pathVal === null || pathVal === undefined) return pathVal;
926929

927930
pathVal = pathVal[key4];
928-
if (pathVal && pathVal.then) {
931+
if (pathVal && pathVal.then && options.unwrapPromises) {
929932
if (!("$$v" in pathVal)) {
930933
promise = pathVal;
931934
promise.$$v = undefined;
@@ -937,23 +940,25 @@ function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp) {
937940
};
938941
}
939942

940-
function getterFn(path, csp, fullExp) {
941-
if (getterFnCache.hasOwnProperty(path)) {
942-
return getterFnCache[path];
943+
function getterFn(path, options, fullExp) {
944+
var cacheKey = path;
945+
cacheKey += '#unwrapPromises:' + (!!options.unwrapPromises).toString();
946+
if (getterFnCache.hasOwnProperty(cacheKey)) {
947+
return getterFnCache[cacheKey];
943948
}
944949

945950
var pathKeys = path.split('.'),
946951
pathKeysLength = pathKeys.length,
947952
fn;
948953

949-
if (csp) {
954+
if (options.csp) {
950955
fn = (pathKeysLength < 6)
951-
? cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp)
956+
? cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp, options)
952957
: function(scope, locals) {
953958
var i = 0, val;
954959
do {
955960
val = cspSafeGetterFn(
956-
pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], fullExp
961+
pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++], fullExp, options
957962
)(scope, locals);
958963

959964
locals = undefined; // clear after first iteration
@@ -972,21 +977,23 @@ function getterFn(path, csp, fullExp) {
972977
? 's'
973978
// but if we are first then we check locals first, and if so read it first
974979
: '((k&&k.hasOwnProperty("' + key + '"))?k:s)') + '["' + key + '"]' + ';\n' +
975-
'if (s && s.then) {\n' +
976-
' if (!("$$v" in s)) {\n' +
977-
' p=s;\n' +
978-
' p.$$v = undefined;\n' +
979-
' p.then(function(v) {p.$$v=v;});\n' +
980-
'}\n' +
981-
' s=s.$$v\n' +
982-
'}\n';
980+
(options.unwrapPromises
981+
? 'if (s && s.then) {\n' +
982+
' if (!("$$v" in s)) {\n' +
983+
' p=s;\n' +
984+
' p.$$v = undefined;\n' +
985+
' p.then(function(v) {p.$$v=v;});\n' +
986+
'}\n' +
987+
' s=s.$$v\n' +
988+
'}\n'
989+
: '');
983990
});
984991
code += 'return s;';
985992
fn = Function('s', 'k', code); // s=scope, k=locals
986993
fn.toString = function() { return code; };
987994
}
988995

989-
return getterFnCache[path] = fn;
996+
return getterFnCache[cacheKey] = fn;
990997
}
991998

992999
///////////////////////////////////
@@ -1030,19 +1037,92 @@ function getterFn(path, csp, fullExp) {
10301037
* set to a function to change its value on the given context.
10311038
*
10321039
*/
1040+
1041+
1042+
/**
1043+
* @ngdoc object
1044+
* @name ng.$parseProvider
1045+
* @function
1046+
*
1047+
* @description
1048+
* `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse} service.
1049+
*/
10331050
function $ParseProvider() {
10341051
var cache = {};
1052+
1053+
var defaultOptions = {
1054+
csp: false,
1055+
unwrapPromises: false
1056+
};
1057+
1058+
1059+
/**
1060+
* @deprecated Promise unwrapping via $parse is deprecated and will be removed in the future.
1061+
*
1062+
* @ngdoc method
1063+
* @name ng.$parseProvider#unwrapPromises
1064+
* @methodOf ng.$parseProvider
1065+
* @description
1066+
*
1067+
* **This feature is deprecated, see deprecation notes below for more info**
1068+
*
1069+
* If set to true (default is false), $parse will unwrap promises automatically when a promise is found at any part of
1070+
* the expression. In other words, if set to true, the expression will always result in a non-promise value.
1071+
*
1072+
* While the promise is unresolved, it's treated as undefined, but once resolved and fulfilled, the fulfillment value
1073+
* is used in place of the promise while evaluating the expression.
1074+
*
1075+
* # Deprecation notice
1076+
*
1077+
* This is a feature that didn't prove to be wildly useful or popular,
1078+
* primarily because of the dichotomy between data access in templates
1079+
* (accessed as raw values) and controller code (accessed as promises).
1080+
*
1081+
* In most code we ended up resolving promises manually in controllers
1082+
* anyway and thus unifying the model access there.
1083+
*
1084+
* Other downsides of automatic promise unwrapping:
1085+
*
1086+
* - when building components it's often desirable to receive the
1087+
* raw promises
1088+
* - adds complexity and slows down expression evaluation
1089+
* - makes expression code pre-generation unattractive due to the
1090+
* amount of code that needs to be generated
1091+
* - makes IDE auto-completion and tool support hard
1092+
*
1093+
*
1094+
* @param {boolean=} value New value.
1095+
* @returns {boolean|self} Returns the current setting when used as getter and self if used as setter.
1096+
*/
1097+
this.unwrapPromises = function(value) {
1098+
if (isDefined(value)) {
1099+
defaultOptions.unwrapPromises = value;
1100+
} else {
1101+
return defaultOptions.unwrapPromises;
1102+
}
1103+
};
1104+
1105+
10351106
this.$get = ['$filter', '$sniffer', function($filter, $sniffer) {
1036-
return function(exp) {
1107+
defaultOptions.csp = $sniffer.csp;
1108+
1109+
return function(exp, options) {
10371110
switch (typeof exp) {
10381111
case 'string':
1039-
if (cache.hasOwnProperty(exp)) {
1040-
return cache[exp];
1112+
options = extend({}, defaultOptions, options);
1113+
1114+
var cacheKey = exp;
1115+
forEach(options, function(optionValue, optionName) {
1116+
cacheKey += '#' + optionName + ':' + optionValue;
1117+
});
1118+
1119+
if (cache.hasOwnProperty(cacheKey)) {
1120+
return cache[cacheKey];
10411121
}
10421122

1043-
var lexer = new Lexer($sniffer.csp);
1044-
var parser = new Parser(lexer, $filter, $sniffer.csp);
1045-
return cache[exp] = parser.parse(exp, false);
1123+
var lexer = new Lexer(options);
1124+
var parser = new Parser(lexer, $filter, options);
1125+
return cache[cacheKey] = parser.parse(exp, false);
10461126

10471127
case 'function':
10481128
return exp;

0 commit comments

Comments
 (0)