From 2f2030babf9b712f2e49f291bb5ebad415992c73 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Sat, 5 Jul 2014 07:23:54 -0500 Subject: [PATCH] Add `_.attempt`. --- lodash.js | 81 ++++++++++++++++++++++++++++++---------------------- test/test.js | 79 +++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 110 insertions(+), 50 deletions(-) diff --git a/lodash.js b/lodash.js index 83e01348d..522c82202 100644 --- a/lodash.js +++ b/lodash.js @@ -629,7 +629,7 @@ /** Used to resolve the decompiled source of functions */ var fnToString = Function.prototype.toString; - /** Used as a references for the max length of an array */ + /** Used as a reference for the max length of an array */ var maxArrayLength = Math.pow(2, 32) - 1; /** @@ -751,10 +751,10 @@ * and `zipObject` * * The non-chainable wrapper functions are: - * `camelCase`, `capitalize`, `clone`, `cloneDeep`, `contains`, `endsWith`, - * `escape`, `escapeRegExp`, `every`, `find`, `findIndex`, `findKey`, + * `attempt`, `camelCase`, `capitalize`, `clone`, `cloneDeep`, `contains`, + * `endsWith`, `escape`, `escapeRegExp`, `every`, `find`, `findIndex`, `findKey`, * `findLast`, `findLastIndex`, `findLastKey`, `findWhere`, `first`, `has`, - * `identity`, `indexOf`, `isArguments`, `isArray`, `isBoolean`, `isDate`, + * `identity`, `indexOf`, `isArguments`, `isArray`, `isBoolean`, isDate`, * `isElement`, `isEmpty`, `isEqual`, `isError`, `isFinite`, `isFunction`, * `isNaN`, `isNull`, `isNumber`, `isObject`, `isPlainObject`, `isRegExp`, * `isString`, `isUndefined`, `join`, `kebabCase`, `last`, `lastIndexOf`, @@ -2449,33 +2449,6 @@ return result; } - /** - * Compiles a function from `source` using the `varNames` and `varValues` - * pairs to import free variables into the compiled function. If `sourceURL` - * is provided it is used as the sourceURL for the compiled function. - * - * @private - * @param {string} source The source to compile. - * @param {Array} varNames An array of free variable names. - * @param {Array} varValues An array of free variable values. - * @param {string} [sourceURL=''] The sourceURL of the source. - * @returns {Function} Returns the compiled function. - */ - function compileFunction(source, varNames, varValues, sourceURL) { - sourceURL = sourceURL ? ('\n/*\n//# sourceURL=' + sourceURL + '\n*/') : ''; - - try { - // provide the compiled function's source by its `toString` method or - // the `source` property as a convenience for inlining compiled templates - var result = Function(varNames, 'return ' + source + sourceURL).apply(undefined, varValues); - result.source = source; - } catch(e) { - e.source = source; - throw e; - } - return result; - } - /** * Creates an array that is the composition of partially applied arguments, * placeholders, and provided arguments into a single array of arguments. @@ -8142,6 +8115,7 @@ // use a sourceURL for easier debugging // http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl var sourceURL = options.sourceURL || ('/lodash/template/source[' + (templateCounter++) + ']'); + sourceURL = sourceURL ? ('\n/*\n//# sourceURL=' + sourceURL + '\n*/') : ''; string.replace(reDelimiters, function(match, escapeValue, interpolateValue, esTemplateValue, evaluateValue, offset) { interpolateValue || (interpolateValue = esTemplateValue); @@ -8200,7 +8174,17 @@ source + 'return __p\n}'; - return compileFunction(source, importsKeys, importsValues, sourceURL); + var result = attempt(function() { + // provide the compiled function's source by its `toString` method or + // the `source` property as a convenience for inlining compiled templates + return Function(importsKeys, 'return ' + source + sourceURL).apply(undefined, importsValues); + }); + + result.source = source; + if (result instanceof Error) { + throw result; + } + return result; } /** @@ -8397,6 +8381,34 @@ /*--------------------------------------------------------------------------*/ + /** + * Attempts to execute `func`, returning either the result or the caught + * error object. + * + * @static + * @memberOf _ + * @category Utility + * @param {*} func The function to attempt. + * @returns {*} Returns the `func` result or error object. + * @example + * + * // avoid throwing errors for invalid selectors + * var elements = _.attempt(function() { + * return document.querySelectorAll(selector); + * }); + * + * if (elements instanceof Error) { + * elements = []; + * } + */ + function attempt(func) { + try { + return func(); + } catch(e) { + return isError(e) ? e : Error(e); + } + } + /** * Creates a function bound to an optional `thisArg`. If `func` is a property * name the created callback returns the property value for a given element. @@ -8912,10 +8924,10 @@ iterator = baseCallback(iterator, thisArg, 1); var index = -1, - result = Array(n); + result = Array(nativeMin(n, maxArrayLength)); while (++index < n) { - if (n < maxArrayLength) { + if (index < maxArrayLength) { result[index] = iterator(index); } else { iterator(index); @@ -9057,6 +9069,7 @@ /*--------------------------------------------------------------------------*/ // add functions that return unwrapped values when chaining + lodash.attempt = attempt; lodash.camelCase = camelCase; lodash.capitalize = capitalize; lodash.clone = clone; diff --git a/test/test.js b/test/test.js index 02fffe895..90402489f 100644 --- a/test/test.js +++ b/test/test.js @@ -184,15 +184,15 @@ /** Used to pass empty values to methods */ var empties = [[], {}].concat(falsey.slice(1)); - /** Used to check whether methods support error objects */ - var errorTypes = [ - 'Error', - 'EvalError', - 'RangeError', - 'ReferenceError', - 'SyntaxError', - 'TypeError', - 'URIError' + /** Used to test error objects */ + var errors = [ + new Error, + new EvalError, + new RangeError, + new ReferenceError, + new SyntaxError, + new TypeError, + new URIError ]; /** Used as the property name for wrapper metadata */ @@ -738,6 +738,47 @@ /*--------------------------------------------------------------------------*/ + QUnit.module('lodash.attempt'); + + (function() { + test('should return the result of `func`', 1, function() { + strictEqual(_.attempt(_.constant('x')), 'x'); + }); + + test('should return the caught error', 1, function() { + var expected = _.map(errors, _.constant(true)); + + var actual = _.map(errors, function(error) { + return _.attempt(function() { throw error; }) === error; + }); + + deepEqual(actual, expected); + }); + + test('should coerce errors to error objects', function() { + var actual = _.attempt(function() { throw 'x'; }); + + deepEqual(actual, Error('x')); + }); + + test('should work with an error object from another realm', 1, function() { + if (_._object) { + var expected = _.map(_._errors, _.constant(true)); + + var actual = _.map(_._errors, function(error) { + return _.attempt(function() { throw error; }) === error; + }); + + deepEqual(actual, expected); + } + else { + skipTest(); + } + }); + }()); + + /*--------------------------------------------------------------------------*/ + QUnit.module('lodash.before'); (function() { @@ -1383,9 +1424,8 @@ }); }); - _.each(errorTypes, function(type) { - test('`_.' + methodName + '` should not clone ' + type + ' objects', 1, function() { - var error = new root[type]; + _.each(errors, function(error) { + test('`_.' + methodName + '` should not clone ' + error.name + ' objects', 1, function() { strictEqual(func(error), error); }); }); @@ -5257,7 +5297,15 @@ }); test('should perform comparisons between error objects', 1, function() { - var pairs = _.map(errorTypes, function(type, index) { + var pairs = _.map([ + 'Error', + 'EvalError', + 'RangeError', + 'ReferenceError', + 'SyntaxError', + 'TypeError', + 'URIError' + ], function(type, index, errorTypes) { var otherType = errorTypes[++index % errorTypes.length], CtorA = root[type], CtorB = root[otherType]; @@ -5527,8 +5575,7 @@ var args = arguments; test('should return `true` for error objects', 1, function() { - var errors = [new Error, new EvalError, new RangeError, new ReferenceError, new SyntaxError, new TypeError, new URIError], - expected = _.map(errors, _.constant(true)); + var expected = _.map(errors, _.constant(true)); var actual = _.map(errors, function(error) { return _.isError(error) === true; @@ -11377,7 +11424,7 @@ var acceptFalsey = _.difference(allMethods, rejectFalsey); - test('should accept falsey arguments', 191, function() { + test('should accept falsey arguments', 192, function() { var emptyArrays = _.map(falsey, _.constant([])), isExposed = '_' in root, oldDash = root._;