From c68b46c9edd5c699fd64cc5810604f21c562e780 Mon Sep 17 00:00:00 2001 From: Julian Gonggrijp Date: Sat, 29 Feb 2020 15:37:04 +0100 Subject: [PATCH 1/2] Extend the iteratee test to demonstrate a logic flaw This leads to a "Maximum call stack size exceeded" error. Since this also prevents the builtin iteratee from being restored, the same error turns up in the tests for omit, isArguments, #1929, tap, isMatch, matcher, findKey, mapObject, random and escape+unescape. --- test/functions.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/test/functions.js b/test/functions.js index 696edbd08..0418e67a8 100644 --- a/test/functions.js +++ b/test/functions.js @@ -701,12 +701,12 @@ // Test custom iteratee var builtinIteratee = _.iteratee; - _.iteratee = function(value) { + _.iteratee = function(value, context) { // RegEx values return a function that returns the number of matches if (_.isRegExp(value)) return function(obj) { return (obj.match(value) || []).length; }; - return value; + return builtinIteratee(value, context); }; var collection = ['foo', 'bar', 'bbiz']; @@ -734,6 +734,17 @@ var objCollection = {a: 'foo', b: 'bar', c: 'bbiz'}; assert.deepEqual(_.mapObject(objCollection, /b/g), {a: 0, b: 1, c: 2}); + // Ensure that the overridden iteratee can still fall back on the builtin + // iteratee. + assert.strictEqual(_.iteratee(), _.identity); + assert.deepEqual(_.toArray(_.iteratee(fn)(1, 2, 3)), _.range(1, 4)); + var matcher = _.iteratee({b: 'bar'}); + assert.equal(matcher(objCollection), true); + assert.equal(matcher({}), false); + var property = _.iteratee('b'); + assert.equal(property(objCollection), 'bar'); + assert.equal(property({}), undefined); + // Restore the builtin iteratee _.iteratee = builtinIteratee; }); From 1c822334e3f4332e7f4f0c0cbfb3052aa1cbe1e0 Mon Sep 17 00:00:00 2001 From: Julian Gonggrijp Date: Sat, 29 Feb 2020 15:44:24 +0100 Subject: [PATCH 2/2] Fix the bug --- underscore.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/underscore.js b/underscore.js index 5cdf62ea6..0fff5f28f 100644 --- a/underscore.js +++ b/underscore.js @@ -84,13 +84,10 @@ }; }; - var builtinIteratee; - // An internal function to generate callbacks that can be applied to each // element in a collection, returning the desired result — either `identity`, // an arbitrary callback, a property matcher, or a property accessor. - var cb = function(value, context, argCount) { - if (_.iteratee !== builtinIteratee) return _.iteratee(value, context); + var baseIteratee = function(value, context, argCount) { if (value == null) return _.identity; if (_.isFunction(value)) return optimizeCb(value, context, argCount); if (_.isObject(value) && !_.isArray(value)) return _.matcher(value); @@ -100,8 +97,15 @@ // External wrapper for our callback generator. Users may customize // `_.iteratee` if they want additional predicate/iteratee shorthand styles. // This abstraction hides the internal-only argCount argument. - _.iteratee = builtinIteratee = function(value, context) { - return cb(value, context, Infinity); + var exportIteratee = _.iteratee = function(value, context) { + return baseIteratee(value, context, Infinity); + }; + + // The function we actually call internally. It invokes _.iteratee if + // overridden, otherwise baseIteratee. + var cb = function(value, context, argCount) { + if (_.iteratee !== exportIteratee) return _.iteratee(value, context); + return baseIteratee(value, context, argCount); }; // Some functions take a variable number of arguments, or a few expected