From 6f4099c20bc2695327923c13da3a308ad80275c8 Mon Sep 17 00:00:00 2001 From: Jeroen Engels Date: Tue, 9 Feb 2016 11:03:34 +0100 Subject: [PATCH] fp docs - Split lib/doc/apply-fp-mapping into multiples files. --- lib/doc/apply-fp-mapping.js | 485 ----------------------- lib/doc/apply-fp-mapping/common.js | 72 ++++ lib/doc/apply-fp-mapping/example.js | 268 +++++++++++++ lib/doc/apply-fp-mapping/index.js | 11 + lib/doc/apply-fp-mapping/parameters.js | 206 ++++++++++ lib/doc/test.js | 438 --------------------- package.json | 3 +- test/test-fp-doc.js | 508 +++++++++++++++++++++++++ 8 files changed, 1067 insertions(+), 924 deletions(-) delete mode 100644 lib/doc/apply-fp-mapping.js create mode 100644 lib/doc/apply-fp-mapping/common.js create mode 100644 lib/doc/apply-fp-mapping/example.js create mode 100644 lib/doc/apply-fp-mapping/index.js create mode 100644 lib/doc/apply-fp-mapping/parameters.js delete mode 100644 lib/doc/test.js create mode 100644 test/test-fp-doc.js diff --git a/lib/doc/apply-fp-mapping.js b/lib/doc/apply-fp-mapping.js deleted file mode 100644 index fa6792379..000000000 --- a/lib/doc/apply-fp-mapping.js +++ /dev/null @@ -1,485 +0,0 @@ -var _ = require('lodash'), - j = require('jscodeshift'), - recast = require('recast'), - Entry = require('docdown/lib/entry'); - -var baseGetParams = Entry.prototype.getParams; - -// Function copied from docdown/lib/entry that is not exported. -function getMultilineValue(string, tagName) { - var prelude = tagName == 'description' ? '^ */\\*\\*(?: *\\n *\\* *)?' : ('^ *\\*[\\t ]*@' + _.escapeRegExp(tagName) + '\\b'), - postlude = '(?=\\*\\s+\\@[a-z]|\\*/)', - result = _.result(RegExp(prelude + '([\\s\\S]*?)' + postlude, 'gm').exec(string), 1, ''); - - return _.trim(result.replace(RegExp('(?:^|\\n)[\\t ]*\\*[\\t ]' + (tagName == 'example' ? '?' : '*'), 'g'), '\n')); - -} - -// Function copied from docdown/lib/entry that is not exported. -function hasTag(string, tagName) { - tagName = tagName == '*' ? '\\w+' : _.escapeRegExp(tagName); - return RegExp('^ *\\*[\\t ]*@' + tagName + '\\b', 'm').test(string); -} - -function isWrapped(entry) { - return !hasTag(entry, 'static'); -} - -/** - * Extract the entry's `name` data. - * Sub-part of Entry.prototype.getCall() that fetches the name. Using `Entry.prototype.getCall()` - * makes a call to getParams(), which itself call getBaseName --> infinite recursion. - * - * @param {object} entry Entry whose name to extract. - * @returns {string} The entry's `name` data. - */ -function getBaseName(entry) { - var result = /\*\/\s*(?:function\s+([^(]*)|(.*?)(?=[:=,]))/.exec(entry); - if (result) { - result = (result[1] || result[2]).split('.').pop(); - result = _.trim(_.trim(result), "'").split('var ').pop(); - result = _.trim(result); - } - // Get the function name. - return _.result(/\*[\t ]*@name\s+(.+)/.exec(entry), 1, result || ''); -} - -/** - * Return the new ary of a given function. - * - * @param {object} mapping Mapping object that defines the arity of all functions. - * @param {String} name Name of the function associated to the call/function definition. - * @param {boolean} wrapped Flag indicating whether method is wrapped. Will decrement ary if true. - * @return {number} Ary of the function as an integer - */ -function getMethodAry(mapping, name, wrapped) { - var ary = _.find(mapping.caps, function(cap) { - return _.includes(mapping.aryMethod[cap], name) && cap; - }); - if (_.isNumber(ary) && wrapped) { - return ary - 1; - } - return ary; -} - -/** - * Reorder `params` for a given function definition/call. - * - * @param {object} mapping Mapping object that defines if and how the `params` will be reordered. - * @param {String} name Name of the function associated to the call/function definition. - * @param {*[]} params Parameters/arguments to reorder. - * @param {boolean} wrapped Flag indicating whether method is wrapped. Will decrement ary if true. - * @returns {*[]} Reordered parameters/arguments. - */ -function reorderParams(mapping, name, params, wrapped) { - // Check if reordering is needed. - if (!mapping || mapping.skipRearg[name]) { - return params; - } - var reargOrder = mapping.methodRearg[name] || mapping.aryRearg[getMethodAry(mapping, name, wrapped)]; - if (!reargOrder) { - return params; - } - // Reorder params. - var newParams = []; - reargOrder.forEach(function(newPosition, index) { - newParams[newPosition] = params[index]; - }); - return newParams; -} - -var dotsRegex = /^\.\.\./; -var parensRegex = /^\((.*)\)$/; -var squareBracketsRegex = /^\[(.*)\]$/; -var arrayRegex = /\[\]$/; - -/** - * Return `types` as '(X|Y|...)' if `types` contains multiple values, `types[0]` otherwise. - * - * @param {string[]} types Possible types of the parameter. - * @return {string} `types` as a string. - */ -function wrapInParensIfMultiple(types) { - if (types.length > 1) { - return '(' + types.join('|') + ')'; - } - return types[0]; -} - -/** - * Transform parameter type from 'X' to 'X|X[]'. - * - * @param {string[]} param Array whose first item is a description of the parameter type. - * @return {string[]} `param` with the updated type. - */ -function singleItemOrArrayOf(type) { - return type + '|' + type + '[]'; -} - -/** - * Replace parameter type from something like `...number` to `number|number[]`. - * - * @param {string[]} param Array whose first item is a description of the parameter type. - * @return {string[]} `param` with the updated type. - */ -function removeDotsFromTypeAndAllowMultiple(param) { - var type = param[0]; - if (!dotsRegex.test(type)) { - return param; - } - - var newType = _.chain(type) - .replace(dotsRegex, '') - .replace(parensRegex, '$1') - .split('|') - .map(function(s) { - return s.replace(arrayRegex, ''); - }) - .uniq() - .thru(wrapInParensIfMultiple) - .thru(singleItemOrArrayOf) - .value(); - - return [newType].concat(_.tail(param)); -} - -/** - * Replace parameter type from something like `...number` to `number|number[]`. - * - * @param {string[]} param Array whose first item is a description of the parameter type. - * @return {string[]} `param` with the updated type. - */ -function removeDotsFromType(param) { - var type = param[0]; - if (!dotsRegex.test(type)) { - return param; - } - - var newType = type - .replace(dotsRegex, '') - .replace(parensRegex, '$1'); - - return [newType].concat(_.tail(param)); -} - -/** - * Find and duplicate the parameter with a type of the form '...x'. - * - * @param {string} name Name of the method. - * @param {string[]} params Description of the parameters of the method. - * @return {string[]} Updated parameters. - */ -function duplicateRestArrays(name, params) { - var indexOfRestParam = _.findIndex(params, function(param) { - return dotsRegex.test(param[0]); - }); - if (indexOfRestParam === -1) { - console.log('WARNING: method `' + name + '`', - 'is capped to more arguments than its declared number of parameters,', - 'but does not have a parameter like `...x`'); - } - // duplicates param[indexOfRestParam] at its position - return params.slice(0, indexOfRestParam + 1) - .concat(params.slice(indexOfRestParam)); -} - -/** - * Remove the optional default value and brackets around the name of the method. - * - * @param {string[]} param Array whose second item is the name of the param of the form - * 'name', '[name]' or [name=defaultValue]. - * @return {string[]} `param` with the updated name. - */ -function removeDefaultValue(param) { - var paramName = param[1] - .replace(squareBracketsRegex, '$1') - .split('=') - [0]; - - return [param[0], paramName, param[2]]; -} - -function updateParamsDescription(mapping, entry, params) { - var tmpParams; - var name = getBaseName(entry); - var ary = getMethodAry(mapping, name); - - var wrapped = isWrapped(entry); - if (wrapped) { - // Needs one less argument when wrapped - ary = ary - 1; - params.shift(); - } - - if (ary > params.length) { - tmpParams = duplicateRestArrays(name, params) - .map(removeDotsFromType); - } else { - tmpParams = params - .map(removeDotsFromTypeAndAllowMultiple); - } - tmpParams = tmpParams.map(removeDefaultValue); - return reorderParams(mapping, name, tmpParams, wrapped); -} - -/** - * Return a function that extracts the entry's `param` data, reordered according to `mapping`. - * - * @param {object} mapping Mapping object that defines if and how the `params` will be reordered. - * @returns {Function} Function that returns the entry's `param` data. - */ -function getReorderedParams(mapping) { - return function(index) { - if (!this._params) { - // Call baseGetParams in order to compute `this._params`. - baseGetParams.call(this); - // Reorder params according to the `mapping`. - this._params = updateParamsDescription(mapping, this.entry, this._params); - } - return baseGetParams.call(this, index); - }; -} - -function getDefaultValue(paramDescription) { - var paramName = paramDescription[1]; - if (paramName[0] !== '[') { - return null; - } - return paramName - .slice(1, paramName.length - 1) - .split('=') - [1] || null; -} - -/** - * Return an AST node representation of `str`. - * - * @param {object} j JSCodeShift object. - * @param {string} str String to convert. - * @return {ASTObject} AST node. - */ -function stringToASTNode(j, str) { - return j(str).find(j.Expression).paths()[0].value; -} - -/** - * Return the name of a parameter from its description. - * @param {string[]} paramDescription Parameter description. - * @return {string} name of the parameter. - */ -function paramName(paramDescription) { - var paramName = paramDescription[1]; - if (paramName[0] !== '[') { - return paramName; - } - return paramName - .slice(1, paramName.length - 1) - .split('=') - [0]; -} - -/** - * Return a AST node representation of `object.property`. - * If `object.property` can be evaluated (ex: [].length --> 0), the node will be simplified. - * If `defaultValue` references another argument, it will be replaced by the value of that argument. - * - * @param {object} j JSCodeShift object. - * @param {ASTObject} object Object of the member expression. - * @param {string} property Property of the member expression. - * @return {ASTObject} AST node. - */ -function memberExpressiontoASTNode(j, object, property) { - var node = j.memberExpression(object, j.identifier(property)); - try { - // Attempt to evaluate the value of the node to have simpler calls - // [1, 2, 3, 4].length --> 4 - var evaluatedNode = eval(recast.print(node).code); - return stringToASTNode(j, JSON.stringify(evaluatedNode)); - } catch (e) { - return node; - } -} - -/** - * Return a AST node representation of `defaultValue`. - * If `defaultValue` references another argument, it will be replaced by the value of that argument. - * - * @param {object} j JSCodeShift object. - * @param {string} defaultValue Value to convert. - * @param {ASTObject[]} args Arguments given to the function. - * @param {string[]} paramNames Name of the expected parameters. - * @return {ASTObject} AST node representation of `defaultValue`. - */ -function defaultValueToASTNode(j, defaultValue, args, paramNames) { - // var endValue = replaceValueByArgValue(j, defaultValue, args, paramNames); - var splitDefaultValue = defaultValue.split('.'); - var indexOfReferencedParam = paramNames.indexOf(splitDefaultValue[0]); - if (indexOfReferencedParam !== -1) { - if (splitDefaultValue.length > 1) { - // defaultValue is probably of the type 'someArg.length' - // Other more complicated cases could be handled but none exist as of this writing. - return memberExpressiontoASTNode(j, args[indexOfReferencedParam], splitDefaultValue[1]); - } - return args[indexOfReferencedParam]; - } - return stringToASTNode(j, defaultValue); -} - -function mapRight(array, fn) { - var res = []; - var index = array.length; - while (index--) { - res = [fn(array[index], index)].concat(res); - } - return res; -} - -/** - * Return the list of arguments, augmented by the default value of the arguments that were ommitted. - * The augmentation only happens when the method call is made without some of the optional arguments, - * and when the arguments these optional arguments have become compulsory. - * For a `function fn(a, b, c=0, d=b.length) { ... }` with an arity of 4, - * when called with `args` [a, ['b']], returns [a, ['b'], 0, ['b'].length]. - * If possible, the value will be evaluated such that ̀`['b'].length` becomes `1`. - * - * @param {object} j JSCodeShift object. - * @param {object} mapping Mapping object that defines if and how the arguments will be reordered. - * @param {String} name Name of the function associated to the call/function definition. - * @param {ASTObject[]} args Arguments to concatenate. - * @param {string[][]} paramsDescription Description of the expected params. - * @return {ASTObject[]} Args along with missing arguments. - */ -function addMissingArguments(j, mapping, name, args, paramsDescription) { - var ary = getMethodAry(mapping, name); - - if (ary === undefined) { - console.log('WARNING: method `' + name + '` is not capped'); - } - - ary = ary || 1; - if (ary <= args.length) { - return args; - } - var paramNames = paramsDescription.map(paramName); - var tmpArgs = _.clone(args); - var newArgs = mapRight(_.take(paramsDescription, ary), function(paramDescription, index) { - if (index === tmpArgs.length - 1) { - return tmpArgs.pop(); - } - var defaultValue = getDefaultValue(paramDescription); - if (defaultValue !== null) { - return defaultValueToASTNode(j, defaultValue, args, paramNames); - } - return tmpArgs.pop(); - }); - return newArgs; -} - -/** - * Concatenate arguments into an array of arguments. - * For a `function fn(a, b, ...args) { ... }` with an arity of 3, - * when called with `args` [a, b, c, d, e, f], returns [a, b, [c, d, e, f]]. - * - * @param {object} j JSCodeShift object. - * @param {object} mapping Mapping object that defines if and how the arguments will be reordered. - * @param {String} name Name of the function associated to the call/function definition. - * @param {ASTObject[]} args Arguments to concatenate. - * @return {ASTObject[]} Concatenated arguments - */ -function concatExtraArgs(j, mapping, name, args) { - var ary = getMethodAry(mapping, name); - if (args.length <= ary) { - return args; - } - - var concatenatedArgs = j.arrayExpression(_.takeRight(args, args.length - ary + 1)); - return _.take(args, ary - 1).concat(concatenatedArgs); -} - -/** - * Reorder the args in the example if needed, and eventually merges them when - * the method is called with more args than the method's ary. - * - * @param {object} j JSCodeShift object. - * @param {ASTObject} root AST representation of the example - * @param {object} mapping Mapping object that defines if and how the arguments will be reordered. - * @return {ASTObject} AST object where the arguments are reordered/merged - */ -function reorderMethodArgs(j, root, mapping, paramsDescription) { - root.find(j.CallExpression, { callee: { object: {name: '_' }}}) - .replaceWith(function(callExpr, i) { - var value = callExpr.value; - var name = value.callee.property.name; - var argsIncludingMissingOnes = addMissingArguments(j, mapping, name, value.arguments, paramsDescription) - var args = concatExtraArgs(j, mapping, name, argsIncludingMissingOnes); - return j.callExpression( - value.callee, - reorderParams(mapping, name, args) - ); - }); -} - -function removeConsoleLogs(codeSample) { - return codeSample - .split('\n') - .filter(function(line) { - return !line.startsWith('console.log'); - }) - .join('\n'); -} - -/** - * Updates a code sample so that the arguments in the call are reordered according to `mapping`. - * - * @param {object} mapping Mapping object that defines if and how the arguments will be reordered. - * @param {string} codeSample Code sample to update. - * @returns {string} Updated code sample. - */ -function reorderParamsInExample(mapping, codeSample, paramsDescription) { - var root = j(removeConsoleLogs(codeSample)); - try { - reorderMethodArgs(j, root, mapping, paramsDescription); - } catch (error) { - console.error(codeSample); - console.error(error.stack); - process.exit(1); - } - return root.toSource(); -} - -function getOriginalParams() { - var prev = this._params; - this._params = undefined; - baseGetParams.call(this); - var result = this._params; - this._params = prev; - return result; -} - -/** - * Returns a function that extracts the entry's `example` data, - * where function call arguments are reordered according to `mapping`. - * - * @param {object} mapping Mapping object that defines if and how the `params` will be reordered. - * @returns {Function} Function that returns the entry's `example` data. - */ -function getReorderedExample(mapping) { - return function() { - var result = getMultilineValue(this.entry, 'example'); - if (!result) { - return result; - } - - var paramsDescription = getOriginalParams.call(this); - var resultReordered = reorderParamsInExample(mapping, result, paramsDescription); - return '```' + this.lang + '\n' + resultReordered + '\n```'; - }; -} - -/** - * Updates `docdown` `Entry`'s prototype so that parameters/arguments are reordered according to `mapping`. - */ -module.exports = function applyFPMapping(mapping) { - Entry.prototype.getParams = getReorderedParams(mapping); - Entry.prototype.getExample = getReorderedExample(mapping); -}; diff --git a/lib/doc/apply-fp-mapping/common.js b/lib/doc/apply-fp-mapping/common.js new file mode 100644 index 000000000..ed510aa50 --- /dev/null +++ b/lib/doc/apply-fp-mapping/common.js @@ -0,0 +1,72 @@ +var _ = require('lodash'), + Entry = require('docdown/lib/entry'); + +var baseGetParams = Entry.prototype.getParams; + +// Function copied from docdown/lib/entry that is not exported. +function getMultilineValue(string, tagName) { + var prelude = tagName == 'description' ? '^ */\\*\\*(?: *\\n *\\* *)?' : ('^ *\\*[\\t ]*@' + _.escapeRegExp(tagName) + '\\b'), + postlude = '(?=\\*\\s+\\@[a-z]|\\*/)', + result = _.result(RegExp(prelude + '([\\s\\S]*?)' + postlude, 'gm').exec(string), 1, ''); + + return _.trim(result.replace(RegExp('(?:^|\\n)[\\t ]*\\*[\\t ]' + (tagName == 'example' ? '?' : '*'), 'g'), '\n')); + +} + +// Function copied from docdown/lib/entry that is not exported. +function hasTag(string, tagName) { + tagName = tagName == '*' ? '\\w+' : _.escapeRegExp(tagName); + return RegExp('^ *\\*[\\t ]*@' + tagName + '\\b', 'm').test(string); +} + +/** + * Return the new ary of a given function. + * + * @param {object} mapping Mapping object that defines the arity of all functions. + * @param {String} name Name of the function associated to the call/function definition. + * @param {boolean} wrapped Flag indicating whether method is wrapped. Will decrement ary if true. + * @return {number} Ary of the function as an integer + */ +function getMethodAry(mapping, name, wrapped) { + var ary = _.find(mapping.caps, function(cap) { + return _.includes(mapping.aryMethod[cap], name) && cap; + }); + if (_.isNumber(ary) && wrapped) { + return ary - 1; + } + return ary; +} + +/** + * Reorder `params` for a given function definition/call. + * + * @param {object} mapping Mapping object that defines if and how the `params` will be reordered. + * @param {String} name Name of the function associated to the call/function definition. + * @param {*[]} params Parameters/arguments to reorder. + * @param {boolean} wrapped Flag indicating whether method is wrapped. Will decrement ary if true. + * @returns {*[]} Reordered parameters/arguments. + */ +function reorderParams(mapping, name, params, wrapped) { + // Check if reordering is needed. + if (!mapping || mapping.skipRearg[name]) { + return params; + } + var reargOrder = mapping.methodRearg[name] || mapping.aryRearg[getMethodAry(mapping, name, wrapped)]; + if (!reargOrder) { + return params; + } + // Reorder params. + var newParams = []; + reargOrder.forEach(function(newPosition, index) { + newParams[newPosition] = params[index]; + }); + return newParams; +} + +module.exports = { + baseGetParams: baseGetParams, + getMultilineValue: getMultilineValue, + hasTag: hasTag, + getMethodAry: getMethodAry, + reorderParams: reorderParams +}; diff --git a/lib/doc/apply-fp-mapping/example.js b/lib/doc/apply-fp-mapping/example.js new file mode 100644 index 000000000..fefe4109d --- /dev/null +++ b/lib/doc/apply-fp-mapping/example.js @@ -0,0 +1,268 @@ +var _ = require('lodash'), + recast = require('recast'), + j = require('jscodeshift'), + common = require('./common'); + +/** + * Return the name of a parameter from its description. + * + * @param {string[]} paramDescription Parameter description. + * @return {string} name of the parameter. + */ +function paramName(paramDescription) { + var name = paramDescription[1]; + if (name[0] !== '[') { + return name; + } + return name + .slice(1, name.length - 1) + .split('=') + [0]; +} + +/** + * Return the default value of the given parameter. + * + * @param {string[]} paramDescription Parameter description. + * @return {string} Default value as string if found, null otherwise. + */ +function getDefaultValue(paramDescription) { + var name = paramDescription[1]; + if (name[0] !== '[') { + return null; + } + return name + .slice(1, name.length - 1) + .split('=') + [1] || null; +} + +/** + * Return an AST node representation of `str`. + * + * @param {object} j JSCodeShift object. + * @param {string} str String to convert. + * @return {ASTObject} AST node. + */ +function stringToASTNode(j, str) { + return j(str).find(j.Expression).paths()[0].value; +} + +/** + * Return a AST node representation of `object.property`. + * If `object.property` can be evaluated (ex: [].length --> 0), the node will be simplified. + * If `defaultValue` references another argument, it will be replaced by the value of that argument. + * + * @param {object} j JSCodeShift object. + * @param {ASTObject} object Object of the member expression. + * @param {string} property Property of the member expression. + * @return {ASTObject} AST node. + */ +function memberExpressiontoASTNode(j, object, property) { + var node = j.memberExpression(object, j.identifier(property)); + try { + // Attempt to evaluate the value of the node to have simpler calls + // [1, 2, 3, 4].length --> 4 + var evaluatedNode = eval(recast.print(node).code); + return stringToASTNode(j, JSON.stringify(evaluatedNode)); + } catch (e) { + return node; + } +} + +/** + * Return a AST node representation of `defaultValue`. + * If `defaultValue` references another argument, it will be replaced by the value of that argument. + * + * @param {object} j JSCodeShift object. + * @param {string} defaultValue Value to convert. + * @param {ASTObject[]} args Arguments given to the function. + * @param {string[]} paramNames Name of the expected parameters. + * @return {ASTObject} AST node representation of `defaultValue`. + */ +function defaultValueToASTNode(j, defaultValue, args, paramNames) { + // var endValue = replaceValueByArgValue(j, defaultValue, args, paramNames); + var splitDefaultValue = defaultValue.split('.'); + var indexOfReferencedParam = paramNames.indexOf(splitDefaultValue[0]); + if (indexOfReferencedParam !== -1) { + if (splitDefaultValue.length > 1) { + // defaultValue is probably of the type 'someArg.length' + // Other more complicated cases could be handled but none exist as of this writing. + return memberExpressiontoASTNode(j, args[indexOfReferencedParam], splitDefaultValue[1]); + } + return args[indexOfReferencedParam]; + } + return stringToASTNode(j, defaultValue); +} + +/** + * Same as _.map, but applied in reverse order. + * + * @param {Array} collection The collection to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @return {Array} Returns the new mapped array. + */ +function mapRight(array, iteratee) { + var res = []; + var index = array.length; + while (index--) { + res = [iteratee(array[index], index)].concat(res); + } + return res; +} + +/** + * Return the list of arguments, augmented by the default value of the arguments that were ommitted. + * The augmentation only happens when the method call is made without some of the optional arguments, + * and when the arguments these optional arguments have become compulsory. + * For a `function fn(a, b, c=0, d=b.length) { ... }` with an arity of 4, + * when called with `args` [a, ['b']], returns [a, ['b'], 0, ['b'].length]. + * If possible, the value will be evaluated such that ̀`['b'].length` becomes `1`. + * + * @param {object} j JSCodeShift object. + * @param {object} mapping Mapping object that defines if and how the arguments will be reordered. + * @param {String} name Name of the function associated to the call/function definition. + * @param {ASTObject[]} args Arguments to concatenate. + * @param {string[][]} paramsDescription Description of the expected params. + * @return {ASTObject[]} Args along with missing arguments. + */ +function addMissingArguments(j, mapping, name, args, paramsDescription) { + var ary = common.getMethodAry(mapping, name); + + if (ary === undefined) { + console.log('WARNING: method `' + name + '` is not capped'); + } + + ary = ary || 1; + if (ary <= args.length) { + return args; + } + var paramNames = paramsDescription.map(paramName); + var tmpArgs = _.clone(args); + var newArgs = mapRight(_.take(paramsDescription, ary), function(paramDescription, index) { + if (index === tmpArgs.length - 1) { + return tmpArgs.pop(); + } + var defaultValue = getDefaultValue(paramDescription); + if (defaultValue !== null) { + return defaultValueToASTNode(j, defaultValue, args, paramNames); + } + return tmpArgs.pop(); + }); + return newArgs; +} + +/** + * Concatenate arguments into an array of arguments. + * For a `function fn(a, b, ...args) { ... }` with an arity of 3, + * when called with `args` [a, b, c, d, e, f], returns [a, b, [c, d, e, f]]. + * + * @param {object} j JSCodeShift object. + * @param {object} mapping Mapping object that defines if and how the arguments will be reordered. + * @param {String} name Name of the function associated to the call/function definition. + * @param {ASTObject[]} args Arguments to concatenate. + * @return {ASTObject[]} Concatenated arguments + */ +function concatExtraArgs(j, mapping, name, args) { + var ary = common.getMethodAry(mapping, name); + if (args.length <= ary) { + return args; + } + + var concatenatedArgs = j.arrayExpression(_.takeRight(args, args.length - ary + 1)); + return _.take(args, ary - 1).concat(concatenatedArgs); +} + +/** + * Reorder the args in the example if needed, and eventually merges them when + * the method is called with more args than the method's ary. + * + * @param {object} j JSCodeShift object. + * @param {ASTObject} root AST representation of the example + * @param {object} mapping Mapping object that defines if and how the arguments will be reordered. + * @return {ASTObject} AST object where the arguments are reordered/merged + */ +function reorderMethodArgs(j, root, mapping, paramsDescription) { + root.find(j.CallExpression, { callee: { object: {name: '_' }}}) + .replaceWith(function(callExpr, i) { + var value = callExpr.value; + var name = value.callee.property.name; + var argsIncludingMissingOnes = addMissingArguments(j, mapping, name, value.arguments, paramsDescription); + var args = concatExtraArgs(j, mapping, name, argsIncludingMissingOnes); + return j.callExpression( + value.callee, + common.reorderParams(mapping, name, args) + ); + }); +} + +/** + * Remove calls to `console.log` from `codeSample`. + * + * @param {string} codeSample string to remove the calls from. + * @return {string} Updated code sample. + */ +function removeConsoleLogs(codeSample) { + return codeSample + .split('\n') + .filter(function(line) { + return !line.startsWith('console.log'); + }) + .join('\n'); +} + +/** + * Updates a code sample so that the arguments in the call are reordered according to `mapping`. + * + * @param {object} mapping Mapping object that defines if and how the arguments will be reordered. + * @param {string} codeSample Code sample to update. + * @returns {string} Updated code sample. + */ +function reorderParamsInExample(mapping, codeSample, paramsDescription) { + var root = j(removeConsoleLogs(codeSample)); + try { + reorderMethodArgs(j, root, mapping, paramsDescription); + } catch (error) { + console.error(codeSample); + console.error(error.stack); + process.exit(1); + } + return root.toSource(); +} + +/** + * Returns the original, not reordered, list of parameters. + * + * @param {Entry} entryItem Entry whose parameters to get. + * @return {string[][]} List of args. + */ +function getOriginalParams(entryItem) { + var prev = entryItem._params; + entryItem._params = undefined; + common.baseGetParams.call(entryItem); + var result = entryItem._params; + entryItem._params = prev; + return result; +} + +/** + * Returns a function that extracts the entry's `example` data, + * where function call arguments are reordered according to `mapping`. + * + * @param {object} mapping Mapping object that defines if and how the `params` will be reordered. + * @returns {Function} Function that returns the entry's `example` data. + */ +function getReorderedExample(mapping) { + return function() { + var result = common.getMultilineValue(this.entry, 'example'); + if (!result) { + return result; + } + + var paramsDescription = getOriginalParams(this); + var resultReordered = reorderParamsInExample(mapping, result, paramsDescription); + return '```' + this.lang + '\n' + resultReordered + '\n```'; + }; +} + +module.exports = getReorderedExample; diff --git a/lib/doc/apply-fp-mapping/index.js b/lib/doc/apply-fp-mapping/index.js new file mode 100644 index 000000000..0b9bbc5f4 --- /dev/null +++ b/lib/doc/apply-fp-mapping/index.js @@ -0,0 +1,11 @@ +var Entry = require('docdown/lib/entry'), + getReorderedParams = require('./parameters'), + getReorderedExample = require('./example'); + +/** + * Updates `docdown` `Entry`'s prototype so that parameters/arguments are reordered according to `mapping`. + */ +module.exports = function applyFPMapping(mapping) { + Entry.prototype.getParams = getReorderedParams(mapping); + Entry.prototype.getExample = getReorderedExample(mapping); +}; diff --git a/lib/doc/apply-fp-mapping/parameters.js b/lib/doc/apply-fp-mapping/parameters.js new file mode 100644 index 000000000..3b0713aa2 --- /dev/null +++ b/lib/doc/apply-fp-mapping/parameters.js @@ -0,0 +1,206 @@ +var _ = require('lodash'), + j = require('jscodeshift'), + Entry = require('docdown/lib/entry'), + common = require('./common'); + +var baseGetParams = Entry.prototype.getParams; + +var dotsRegex = /^\.\.\./; +var parensRegex = /^\((.*)\)$/; +var squareBracketsRegex = /^\[(.*)\]$/; +var arrayRegex = /\[\]$/; + +/** + * Return whether method is wrapped. + * + * @param {Entry} entry Entry to look at. + * @return {Boolean} true if the method is wrapped, false if it is static. + */ +function isWrapped(entry) { + return !common.hasTag(entry, 'static'); +} + +/** + * Extract the entry's `name` data. + * Sub-part of Entry.prototype.getCall() that fetches the name. Using `Entry.prototype.getCall()` + * makes a call to getParams(), which itself call getBaseName --> infinite recursion. + * + * @param {object} entry Entry whose name to extract. + * @returns {string} The entry's `name` data. + */ +function getBaseName(entry) { + var result = /\*\/\s*(?:function\s+([^(]*)|(.*?)(?=[:=,]))/.exec(entry); + if (result) { + result = (result[1] || result[2]).split('.').pop(); + result = _.trim(_.trim(result), "'").split('var ').pop(); + result = _.trim(result); + } + // Get the function name. + return _.result(/\*[\t ]*@name\s+(.+)/.exec(entry), 1, result || ''); +} + +/** + * Return `types` as '(X|Y|...)' if `types` contains multiple values, `types[0]` otherwise. + * + * @param {string[]} types Possible types of the parameter. + * @return {string} `types` as a string. + */ +function wrapInParensIfMultiple(types) { + if (types.length > 1) { + return '(' + types.join('|') + ')'; + } + return types[0]; +} + +/** + * Transform parameter type from 'X' to 'X|X[]'. + * + * @param {string[]} param Array whose first item is a description of the parameter type. + * @return {string[]} `param` with the updated type. + */ +function singleItemOrArrayOf(type) { + return type + '|' + type + '[]'; +} + +/** + * Replace parameter type from something like `...number` to `number|number[]`. + * + * @param {string[]} param Array whose first item is a description of the parameter type. + * @return {string[]} `param` with the updated type. + */ +function removeDotsFromTypeAndAllowMultiple(param) { + var type = param[0]; + if (!dotsRegex.test(type)) { + return param; + } + + var newType = _.chain(type) + .replace(dotsRegex, '') + .replace(parensRegex, '$1') + .split('|') + .map(function(s) { + return s.replace(arrayRegex, ''); + }) + .uniq() + .thru(wrapInParensIfMultiple) + .thru(singleItemOrArrayOf) + .value(); + + return [newType].concat(_.tail(param)); +} + +/** + * Replace parameter type from something like `...number` to `number|number[]`. + * + * @param {string[]} param Array whose first item is a description of the parameter type. + * @return {string[]} `param` with the updated type. + */ +function removeDotsFromType(param) { + var type = param[0]; + if (!dotsRegex.test(type)) { + return param; + } + + var newType = type + .replace(dotsRegex, '') + .replace(parensRegex, '$1'); + + return [newType].concat(_.tail(param)); +} + +/** + * Find and duplicate the parameter with a type of the form '...x'. + * + * @param {string} name Name of the method. + * @param {string[]} params Description of the parameters of the method. + * @return {string[]} Updated parameters. + */ +function duplicateRestArrays(name, params) { + var indexOfRestParam = _.findIndex(params, function(param) { + return dotsRegex.test(param[0]); + }); + if (indexOfRestParam === -1) { + console.log('WARNING: method `' + name + '`', + 'is capped to more arguments than its declared number of parameters,', + 'but does not have a parameter like `...x`'); + } + // duplicates param[indexOfRestParam] at its position + return params.slice(0, indexOfRestParam + 1) + .concat(params.slice(indexOfRestParam)); +} + +/** + * Remove the optional default value and brackets around the name of the method. + * + * @param {string[]} param Array whose second item is the name of the param of the form + * 'name', '[name]' or [name=defaultValue]. + * @return {string[]} `param` with the updated name. + */ +function removeDefaultValue(param) { + var name = param[1] + .replace(squareBracketsRegex, '$1') + .split('=') + [0]; + + return [param[0], name, param[2]]; +} + +/** + * Return the updated list of parameters of a method described by `entry`, + * according to changes described by `mapping`. Will, if needed: + * - reorder the arguments + * - remove default values and brackets around previously optional arguments + * - remove ignored arguments + * - duplicate rest arguments if the number of params is less than its cap + * - de-restify arguments + * + * @param {object} mapping Mapping object that defines if and how the `params` will be reordered. + * @param {Entry} entry Method to update. + * @param {string[][]} params List of the original parameters of the method. + * @return {string[][]} Updated list of params. + */ +function updateParamsDescription(mapping, entry, params) { + var tmpParams; + var name = getBaseName(entry); + var ary = common.getMethodAry(mapping, name); + + var wrapped = isWrapped(entry); + if (wrapped) { + // Needs one less argument when wrapped + ary = ary - 1; + params.shift(); + } + + if (ary > params.length) { + tmpParams = duplicateRestArrays(name, params) + .map(removeDotsFromType); + } else { + tmpParams = params + .map(removeDotsFromTypeAndAllowMultiple); + } + tmpParams = _.take(tmpParams, ary).map(removeDefaultValue); + return common.reorderParams(mapping, name, tmpParams, wrapped); +} + +/** + * Return a function that extracts the entry's `param` data, reordered according to `mapping`. + * + * @param {object} mapping Mapping object that defines if and how the `params` will be reordered. + * @returns {Function} Function that returns the entry's `param` data. + */ +function getReorderedParams(mapping) { + return function(index) { + if (!this._params) { + // Call baseGetParams in order to compute `this._params`. + baseGetParams.call(this); + // Reorder params according to the `mapping`. + this._params = updateParamsDescription(mapping, this.entry, this._params); + } + return baseGetParams.call(this, index); + }; +} + +/** + * Updates `docdown` `Entry`'s prototype so that parameters/arguments are reordered according to `mapping`. + */ +module.exports = getReorderedParams; diff --git a/lib/doc/test.js b/lib/doc/test.js deleted file mode 100644 index 53c0c8ef5..000000000 --- a/lib/doc/test.js +++ /dev/null @@ -1,438 +0,0 @@ -var assert = require('assert'); - -var Entry = require('docdown/lib/entry'); - -var applyFPMapping = require('./apply-fp-mapping'); -var mapping = require('../../fp/_mapping'); - -function toSource(name, paramLines, exampleLines, attachedToPrototype) { - var start = [ - "/**", - " * ", - " * Foo", - " * " - ]; - var end = [ - " */", - "function " + name + "(a, b, c) {", - "", - "}" - ]; - var staticLine = attachedToPrototype ? [] : [' * @static']; - var params = paramLines.map(function(line) { - return ' * @param ' + line; - }); - var example = (exampleLines || []).map(function(line) { - return ' * ' + line; - }); - - return [].concat(start, staticLine, params, [' * @example'], example, end).join('\n'); -} - -function toParams(name, lines, wrapped) { - var start = [ - "/**", - " * ", - " * Foo", - " * " - ]; - var end = [ - " * @returns Foo bar", - " */", - "function " + name + "(a, b, c) {", - "", - "}" - ]; - var staticLine = wrapped ? [] : [' * @static']; - var params = lines.map(function(line) { - return ' * @param ' + line; - }); - return [].concat(start, staticLine, params, end).join('\n'); -} - -var differenceBySource = toSource('differenceBy', [ - '{Array} array The array to inspect.', - '{...Array} [values] The values to exclude.', - '{Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.' -], [ - "_.differenceBy([3.1, 2.2, 1.3], [4.4, 2.5], Math.floor);", - "// → [3.1, 1.3]", - "", - "// The `_.property` iteratee shorthand.", - "_.differenceBy([{ 'x': 2 }, { 'x': 1 }], [{ 'x': 1 }], 'x');", - "// → [{ 'x': 2 }]" -]); - -var setParams = [ - '{Object} object The object to modify.', - '{Array|string} path The path of the property to set.', - '{*} value The value to set.' -]; - -describe('Docs FP mapping', function() { - var oldgetParams; - var oldgetExample; - - before(function() { - oldgetParams = Entry.prototype.getParams; - oldgetExample = Entry.prototype.getExample; - mapping.aryMethod[2].push('customFun'); - applyFPMapping(mapping); - }); - - after(function() { - Entry.prototype.getParams = oldgetParams; - Entry.prototype.getExample = oldgetExample; - }); - - describe('getExample', function() { - it('should reorder parameters', function() { - var entry = new Entry(differenceBySource, differenceBySource); - - var actual = entry.getExample(); - - assert.equal(actual, [ - "```js", - "_.differenceBy(Math.floor, [4.4, 2.5], [3.1, 2.2, 1.3]);", - "// → [3.1, 1.3]", - "", - "// The `_.property` iteratee shorthand.", - "_.differenceBy('x', [{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }]);", - "// → [{ 'x': 2 }]", - "```" - ].join('\n')); - }); - - it('should reorder parameters that have a special order', function() { - var example = toSource('set', setParams, [ - "var object = { 'a': [{ 'b': { 'c': 3 } }] };", - "_.set(object, 'a[0].b.c', 4);", - "_.set(object, 'x[0].y.z', 5);", - ]); - var entry = new Entry(example, example); - - var actual = entry.getExample(); - - assert.equal(actual, [ - "```js", - "var object = { 'a': [{ 'b': { 'c': 3 } }] };", - "_.set('a[0].b.c', 4, object);", - "_.set('x[0].y.z', 5, object);", - "```" - ].join('\n')); - }); - - it('should preserve comments', function() { - var example = toSource('set', setParams, [ - "var object = { 'a': [{ 'b': { 'c': 3 } }] };", - "_.set(object, 'a[0].b.c', 4);", - "// => 4", - "_.set(object, 'x[0].y.z', 5);", - ]); - var entry = new Entry(example, example); - - var actual = entry.getExample(); - - assert.equal(actual, [ - "```js", - "var object = { 'a': [{ 'b': { 'c': 3 } }] };", - "_.set('a[0].b.c', 4, object);", - "// => 4", - "_.set('x[0].y.z', 5, object);", - "```" - ].join('\n')); - }); - - it('should remove console.logs from example', function() { - var example = toSource('set', setParams, [ - "var object = { 'a': [{ 'b': { 'c': 3 } }] };", - "", - "_.set(object, 'a[0].b.c', 4);", - "console.log(object.a[0].b.c);", - "// => 4", - "", - "_.set(object, 'x[0].y.z', 5);", - "console.log(object.x[0].y.z);", - "// => 5" - ]); - var entry = new Entry(example, example); - - var actual = entry.getExample(); - - assert.equal(actual, [ - "```js", - "var object = { 'a': [{ 'b': { 'c': 3 } }] };", - "", - "_.set('a[0].b.c', 4, object);", - "// => 4", - "", - "_.set('x[0].y.z', 5, object);", - "// => 5", - "```" - ].join('\n')); - }); - - it('should merge extra arguments into an array', function() { - var example = toSource('pullAt', [ - '{Array} array The array to modify.', - '{...(number|number[])} [indexes] The indexes of elements to remove,\n' + - ' * specified individually or in arrays.' - ], [ - "var array = [5, 10, 15, 20];", - "var evens = _.pullAt(array, 1, 3);", - "", - "console.log(array);", - "// => [5, 15]", - "", - "console.log(evens);", - "// => [10, 20]", - ]); - var entry = new Entry(example, example); - - var actual = entry.getExample(); - - assert.equal(actual, [ - "```js", - "var array = [5, 10, 15, 20];", - "var evens = _.pullAt([1, 3], array);", - "", - "// => [5, 15]", - "", - "// => [10, 20]", - "```" - ].join('\n')); - }); - - it('should inject default values into optional arguments that became compulsory', function() { - var example = toSource('sampleSize', [ - '{Array|Object} collection The collection to sample.', - '{number} [n=0] The number of elements to sample.' - ], [ - "_.sampleSize([1, 2, 3]);", - "// => [3, 1]", - "", - "_.sampleSize([1, 2, 3], 4);", - "// => [2, 3, 1]" - ]); - var entry = new Entry(example, example); - - var actual = entry.getExample(); - - assert.equal(actual, [ - "```js", - "_.sampleSize(0, [1, 2, 3]);", - "// => [3, 1]", - "", - "_.sampleSize(4, [1, 2, 3]);", - "// => [2, 3, 1]", - "```" - ].join('\n')); - }); - - it('should inject referenced values into optional arguments that became compulsory, ' - + 'if a parameter\'s default value references parameter (direct reference)', - function() { - var example = toSource('customFun', [ - '{Array} array Array', - '{number} [foo=array] Foo' - ], [ - "_.customFun([1, 2, 3]);", - ]); - var entry = new Entry(example, example); - - var actual = entry.getExample(); - - assert.equal(actual, [ - "```js", - "_.customFun([1, 2, 3], [1, 2, 3]);", - "```" - ].join('\n')); - }); - - it('should inject referenced values into optional arguments that became compulsory, ' - + 'if a parameter\'s default value references parameter (member expression)', - function() { - var example = toSource('fill', [ - '{Array} array The array to fill.', - '{*} value The value to fill `array` with.', - '{number} [start=0] The start position.', - '{number} [end=array.length] The end position.' - ], [ - "var array = [1, 2, 3];", - "", - "_.fill(array, 'a');", - "console.log(array);", - "// => ['a', 'a', 'a']", - "", - "_.fill(Array(3), 2, 1);", - "// => [undefined, 2, 2]", - "", - "_.fill([4, 6, 8, 10], '*');", - "// => [*, '*', '*', *]" - ]); - var entry = new Entry(example, example); - - var actual = entry.getExample(); - - assert.equal(actual, [ - "```js", - "var array = [1, 2, 3];", - "", - "_.fill(0, array.length, 'a', array);", - "// => ['a', 'a', 'a']", - "", - "_.fill(1, 3, 2, Array(3));", - "// => [undefined, 2, 2]", - "", - "_.fill(0, 4, '*', [4, 6, 8, 10]);", - "// => [*, '*', '*', *]", - "```" - ].join('\n')); - }); - - it('should inject default values in the middle of the arguments', function() { - var example = toSource('inRange', [ - '{number} number The number to check.', - '{number} [start=0] The start of the range.', - '{number} end The end of the range.' - ], [ - "_.inRange(4, 8);", - "// => true" - ]); - var entry = new Entry(example, example); - - var actual = entry.getExample(); - - assert.equal(actual, [ - "```js", - "_.inRange(8, 0, 4);", - "// => true", - "```" - ].join('\n')); - }); - - it('should not use ignored params as default values', function() { - var example = toSource('drop', [ - '{Array} array The array to query.', - '{number} [n=1] The number of elements to drop.', - '{Object} [guard] Enables use as an iteratee for functions like `_.map`.' - ], [ - "_.drop([1, 2, 3]);", - "// => [2, 3]" - ]); - var entry = new Entry(example, example); - - var actual = entry.getExample(); - - assert.equal(actual, [ - "```js", - "_.drop(1, [1, 2, 3]);", - "// => [2, 3]", - "```" - ].join('\n')); - }); - }); - - describe('getParams', function() { - it('should reorder arguments and remove default values', function() { - var example = toParams('differenceBy', [ - '{Array} array The array to inspect.', - '{...Array} [values] The values to exclude.', - '{Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.' - ]); - var entry = new Entry(example, example); - - var actual = entry.getParams(); - - assert.deepEqual(actual, [ - ['Function|Object|string', 'iteratee', 'The iteratee invoked per element. '], - ['Array|Array[]', 'values', 'The values to exclude. '], - ['Array', 'array', 'The array to inspect. '] - ]); - }); - - it('should reorder arguments that have a special order', function() { - var example = toParams('set', [ - '{Object} object The object to modify.', - '{Array|string} path The path of the property to set.', - '{*} value The value to set.' - ]); - var entry = new Entry(example, example); - - var actual = entry.getParams(); - - assert.deepEqual(actual, [ - ['Array|string', 'path', 'The path of the property to set. '], - ['*', 'value', 'The value to set. '], - ['Object', 'object', 'The object to modify. '], - ]); - }); - - it('should transform rest arguments into an array', function() { - var example = toParams('pullAt', [ - '{Array} array The array to modify.', - '{...(number|number[])} [indexes] The indexes of elements to remove,\n' + - ' * specified individually or in arrays.' - ]); - var entry = new Entry(example, example); - - var actual = entry.getParams(); - - assert.deepEqual(actual, [ - // TODO Remove this line in favor of the commented one. - // Is linked to a docdown (https://github.com/jdalton/docdown/pull/37) - // that does not handle parens in the arguments well - ['((number|number)|((number|number)[]', 'indexes', 'The indexes of elements to remove, specified individually or in arrays. '], - // ['number|number[]', '[indexes]', 'The indexes of elements to remove, specified individually or in arrays. '], - ['Array', 'array', 'The array to modify. '], - ]); - }); - }); - - it('should duplicate and de-restify "rest" parameters if there are less parameters than cap', function() { - var example = toParams('intersectionWith', [ - '{...Array} [arrays] The arrays to inspect.', - '{Function} [comparator] The comparator invoked per element.' - ]); - var entry = new Entry(example, example); - - var actual = entry.getParams(); - - assert.deepEqual(actual, [ - ['Function', 'comparator', 'The comparator invoked per element. '], - ['Array', 'arrays', 'The arrays to inspect. '], - ['Array', 'arrays', 'The arrays to inspect. '] - ]); - }); - - it('should consider method to have an ary of `ary - 1` when capped and wrapped', function() { - var wrapped = true; - var example = toParams('flatMap', [ - '{Array} array The array to iterate over.', - '{Function|Object|string} [iteratee=_.identity] The function invoked per iteration.' - ], wrapped); - var entry = new Entry(example, example); - - var actual = entry.getParams(); - - assert.deepEqual(actual, [ - ['Function|Object|string', 'iteratee', 'The function invoked per iteration. '] - ]); - }); - - it('should remove arguments ignored because of capping', function() { - var example = toParams('includes', [ - '{Array|Object|string} collection The collection to search.', - '{*} value The value to search for.', - '{number} [fromIndex=0] The index to search from.' - ]); - var entry = new Entry(example, example); - - var actual = entry.getParams(); - - assert.deepEqual(actual, [ - ['*', 'value', 'The value to search for. '], - ['Array|Object|string', 'collection', 'The collection to search. '] - ]); - }); -}); diff --git a/package.json b/package.json index 6066accfc..a757b2d06 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,8 @@ "style:main": "jscs lodash.js", "style:perf": "jscs perf/*.js perf/**/*.js", "style:test": "jscs test/*.js test/**/*.js", - "test": "npm run test:main && npm run test:fp", + "test": "npm run test:main && npm run test:fp && npm run test:docs", + "test:docs": "node test/test-fp-doc", "test:fp": "node test/test-fp", "test:main": "node test/test" } diff --git a/test/test-fp-doc.js b/test/test-fp-doc.js new file mode 100644 index 000000000..a1569c96b --- /dev/null +++ b/test/test-fp-doc.js @@ -0,0 +1,508 @@ +;(function() { + /** Used as a safe reference for `undefined` in pre-ES5 environments. */ + var undefined; + + /** Used as a reference to the global object. */ + var root = (typeof global == 'object' && global) || this; + + var phantom = root.phantom, + amd = root.define && define.amd, + document = !phantom && root.document, + noop = function() {}, + argv = root.process && process.argv; + + /** Use a single "load" function. */ + var load = (!amd && typeof require == 'function') + ? require + : noop; + + /** The unit testing framework. */ + var QUnit = root.QUnit || (root.QUnit = ( + QUnit = load('../node_modules/qunitjs/qunit/qunit.js') || root.QUnit, + QUnit = QUnit.QUnit || QUnit + )); + + /** Load stable Lodash and QUnit Extras. */ + var _ = root._ || load('../lodash.js'); + if (_) { + _ = _.runInContext(root); + } + var QUnitExtras = load('../node_modules/qunit-extras/qunit-extras.js'); + if (QUnitExtras) { + QUnitExtras.runInContext(root); + } + + var mapping = root.mapping || load('../fp/_mapping.js'), + applyFPMapping = load('../lib/doc/apply-fp-mapping'), + Entry = load('docdown/lib/entry'); + + /*--------------------------------------------------------------------------*/ + + function toEntry(name, paramLines, exampleLines, attachedToPrototype) { + var start = [ + '/**', + ' * ', + ' * Foo', + ' * ' + ]; + var end = [ + ' */', + 'function ' + name + '(a, b, c) {', + '', + '}' + ]; + var staticLine = attachedToPrototype ? [] : [' * @static']; + var params = paramLines.map(function(line) { + return ' * @param ' + line; + }); + var example = (exampleLines || []).map(function(line) { + return ' * ' + line; + }); + + return [].concat(start, staticLine, params, [' * @example'], example, end).join('\n'); + } + + var differenceBySource = toEntry('differenceBy', [ + '{Array} array The array to inspect.', + '{...Array} [values] The values to exclude.', + '{Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.' + ], [ + '_.differenceBy([3.1, 2.2, 1.3], [4.4, 2.5], Math.floor);', + '// → [3.1, 1.3]', + '', + '// The `_.property` iteratee shorthand.', + "_.differenceBy([{ 'x': 2 }, { 'x': 1 }], [{ 'x': 1 }], 'x');", + "// → [{ 'x': 2 }]" + ]); + + var setParams = [ + '{Object} object The object to modify.', + '{Array|string} path The path of the property to set.', + '{*} value The value to set.' + ]; + + /*--------------------------------------------------------------------------*/ + + if (argv) { + console.log('Running doc generation tests.'); + } + + mapping.aryMethod[2].push('customFun'); + applyFPMapping(mapping); + + /*--------------------------------------------------------------------------*/ + + QUnit.module('getExample'); + + (function() { + QUnit.test('should reorder parameters', function(assert) { + assert.expect(1); + + var entry = new Entry(differenceBySource, differenceBySource); + + var actual = entry.getExample(); + + assert.equal(actual, [ + '```js', + '_.differenceBy(Math.floor, [4.4, 2.5], [3.1, 2.2, 1.3]);', + '// → [3.1, 1.3]', + '', + '// The `_.property` iteratee shorthand.', + "_.differenceBy('x', [{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }]);", + "// → [{ 'x': 2 }]", + '```' + ].join('\n')); + }); + + QUnit.test('should reorder parameters that have a special order', function(assert) { + assert.expect(1); + + var example = toEntry('set', setParams, [ + "var object = { 'a': [{ 'b': { 'c': 3 } }] };", + "_.set(object, 'a[0].b.c', 4);", + "_.set(object, 'x[0].y.z', 5);", + ]); + var entry = new Entry(example, example); + + var actual = entry.getExample(); + + assert.equal(actual, [ + '```js', + "var object = { 'a': [{ 'b': { 'c': 3 } }] };", + "_.set('a[0].b.c', 4, object);", + "_.set('x[0].y.z', 5, object);", + '```' + ].join('\n')); + }); + + QUnit.test('should preserve comments', function(assert) { + assert.expect(1); + + var example = toEntry('set', setParams, [ + "var object = { 'a': [{ 'b': { 'c': 3 } }] };", + "_.set(object, 'a[0].b.c', 4);", + '// => 4', + "_.set(object, 'x[0].y.z', 5);", + ]); + var entry = new Entry(example, example); + + var actual = entry.getExample(); + + assert.equal(actual, [ + '```js', + "var object = { 'a': [{ 'b': { 'c': 3 } }] };", + "_.set('a[0].b.c', 4, object);", + '// => 4', + "_.set('x[0].y.z', 5, object);", + '```' + ].join('\n')); + }); + + QUnit.test('should remove console.logs from example', function(assert) { + assert.expect(1); + + var example = toEntry('set', setParams, [ + "var object = { 'a': [{ 'b': { 'c': 3 } }] };", + '', + "_.set(object, 'a[0].b.c', 4);", + 'console.log(object.a[0].b.c);', + '// => 4', + '', + "_.set(object, 'x[0].y.z', 5);", + 'console.log(object.x[0].y.z);', + '// => 5' + ]); + var entry = new Entry(example, example); + + var actual = entry.getExample(); + + assert.equal(actual, [ + '```js', + "var object = { 'a': [{ 'b': { 'c': 3 } }] };", + '', + "_.set('a[0].b.c', 4, object);", + '// => 4', + '', + "_.set('x[0].y.z', 5, object);", + '// => 5', + '```' + ].join('\n')); + }); + + QUnit.test('should merge extra arguments into an array', function(assert) { + assert.expect(1); + + var example = toEntry('pullAt', [ + '{Array} array The array to modify.', + '{...(number|number[])} [indexes] The indexes of elements to remove,\n' + + ' * specified individually or in arrays.' + ], [ + 'var array = [5, 10, 15, 20];', + 'var evens = _.pullAt(array, 1, 3);', + '', + 'console.log(array);', + '// => [5, 15]', + '', + 'console.log(evens);', + '// => [10, 20]' + ]); + var entry = new Entry(example, example); + + var actual = entry.getExample(); + + assert.equal(actual, [ + '```js', + 'var array = [5, 10, 15, 20];', + 'var evens = _.pullAt([1, 3], array);', + '', + '// => [5, 15]', + '', + '// => [10, 20]', + '```' + ].join('\n')); + }); + + QUnit.test('should inject default values into optional arguments that became compulsory', function(assert) { + assert.expect(1); + + var example = toEntry('sampleSize', [ + '{Array|Object} collection The collection to sample.', + '{number} [n=0] The number of elements to sample.' + ], [ + '_.sampleSize([1, 2, 3]);', + '// => [3, 1]', + '', + '_.sampleSize([1, 2, 3], 4);', + '// => [2, 3, 1]' + ]); + var entry = new Entry(example, example); + + var actual = entry.getExample(); + + assert.equal(actual, [ + '```js', + '_.sampleSize(0, [1, 2, 3]);', + '// => [3, 1]', + '', + '_.sampleSize(4, [1, 2, 3]);', + '// => [2, 3, 1]', + '```' + ].join('\n')); + }); + + QUnit.test('should inject referenced values into optional arguments that became compulsory, ' + + + 'if a parameter\'s default value references parameter (direct reference)', + function(assert) { + assert.expect(1); + + var example = toEntry('customFun', [ + '{Array} array Array', + '{number} [foo=array] Foo' + ], [ + '_.customFun([1, 2, 3]);', + ]); + var entry = new Entry(example, example); + + var actual = entry.getExample(); + + assert.equal(actual, [ + '```js', + '_.customFun([1, 2, 3], [1, 2, 3]);', + '```' + ].join('\n')); + }); + + QUnit.test('should inject referenced values into optional arguments that became compulsory, ' + + 'if a parameter\'s default value references parameter (member expression)', + function(assert) { + assert.expect(1); + + var example = toEntry('fill', [ + '{Array} array The array to fill.', + '{*} value The value to fill `array` with.', + '{number} [start=0] The start position.', + '{number} [end=array.length] The end position.' + ], [ + 'var array = [1, 2, 3];', + '', + "_.fill(array, 'a');", + 'console.log(array);', + "// => ['a', 'a', 'a']", + '', + '_.fill(Array(3), 2, 1);', + '// => [undefined, 2, 2]', + '', + "_.fill([4, 6, 8, 10], '*');", + "// => [*, '*', '*', *]" + ]); + var entry = new Entry(example, example); + + var actual = entry.getExample(); + + assert.equal(actual, [ + '```js', + 'var array = [1, 2, 3];', + '', + "_.fill(0, array.length, 'a', array);", + "// => ['a', 'a', 'a']", + '', + '_.fill(1, 3, 2, Array(3));', + '// => [undefined, 2, 2]', + '', + "_.fill(0, 4, '*', [4, 6, 8, 10]);", + "// => [*, '*', '*', *]", + '```' + ].join('\n')); + }); + + QUnit.test('should inject default values in the middle of the arguments', function(assert) { + assert.expect(1); + + var example = toEntry('inRange', [ + '{number} number The number to check.', + '{number} [start=0] The start of the range.', + '{number} end The end of the range.' + ], [ + '_.inRange(4, 8);', + '// => true' + ]); + var entry = new Entry(example, example); + + var actual = entry.getExample(); + + assert.equal(actual, [ + '```js', + '_.inRange(8, 0, 4);', + '// => true', + '```' + ].join('\n')); + }); + + QUnit.test('should not use ignored params as default values', function(assert) { + assert.expect(1); + + var example = toEntry('drop', [ + '{Array} array The array to query.', + '{number} [n=1] The number of elements to drop.', + '{Object} [guard] Enables use as an iteratee for functions like `_.map`.' + ], [ + '_.drop([1, 2, 3]);', + '// => [2, 3]' + ]); + var entry = new Entry(example, example); + + var actual = entry.getExample(); + + assert.equal(actual, [ + '```js', + '_.drop(1, [1, 2, 3]);', + '// => [2, 3]', + '```' + ].join('\n')); + }); + }()); + + /*--------------------------------------------------------------------------*/ + + QUnit.module('getParams'); + + (function() { + QUnit.test('should reorder arguments and remove default values', function(assert) { + assert.expect(1); + + var example = toEntry('differenceBy', [ + '{Array} array The array to inspect.', + '{...Array} [values] The values to exclude.', + '{Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.' + ]); + var entry = new Entry(example, example); + + var actual = entry.getParams(); + + assert.deepEqual(actual, [ + ['Function|Object|string', 'iteratee', 'The iteratee invoked per element. '], + ['Array|Array[]', 'values', 'The values to exclude. '], + ['Array', 'array', 'The array to inspect. '] + ]); + }); + + QUnit.test('should reorder arguments that have a special order', function(assert) { + assert.expect(1); + + var example = toEntry('set', [ + '{Object} object The object to modify.', + '{Array|string} path The path of the property to set.', + '{*} value The value to set.' + ]); + var entry = new Entry(example, example); + + var actual = entry.getParams(); + + assert.deepEqual(actual, [ + ['Array|string', 'path', 'The path of the property to set. '], + ['*', 'value', 'The value to set. '], + ['Object', 'object', 'The object to modify. '], + ]); + }); + + QUnit.test('should transform rest arguments into an array', function(assert) { + assert.expect(1); + + var example = toEntry('pullAt', [ + '{Array} array The array to modify.', + '{...(number|number[])} [indexes] The indexes of elements to remove,\n' + + ' * specified individually or in arrays.' + ]); + var entry = new Entry(example, example); + + var actual = entry.getParams(); + + assert.deepEqual(actual, [ + // TODO Remove this line in favor of the commented one. + // Is linked to a docdown issue (https://github.com/jdalton/docdown/pull/37) + // that does not handle parens in the arguments well + ['((number|number)|((number|number)[]', 'indexes', 'The indexes of elements to remove, specified individually or in arrays. '], + // ['number|number[]', '[indexes]', 'The indexes of elements to remove, specified individually or in arrays. '], + ['Array', 'array', 'The array to modify. '], + ]); + }); + + QUnit.test('should duplicate and de-restify "rest" parameters if there are less parameters than cap', function(assert) { + assert.expect(1); + + var example = toEntry('intersectionWith', [ + '{...Array} [arrays] The arrays to inspect.', + '{Function} [comparator] The comparator invoked per element.' + ]); + var entry = new Entry(example, example); + + var actual = entry.getParams(); + + assert.deepEqual(actual, [ + ['Function', 'comparator', 'The comparator invoked per element. '], + ['Array', 'arrays', 'The arrays to inspect. '], + ['Array', 'arrays', 'The arrays to inspect. '] + ]); + }); + + QUnit.test('should consider method to have an ary of `ary - 1` when capped and wrapped', function(assert) { + assert.expect(1); + + var wrapped = true; + var example = toEntry('flatMap', [ + '{Array} array The array to iterate over.', + '{Function|Object|string} [iteratee=_.identity] The function invoked per iteration.' + ], [], wrapped); + var entry = new Entry(example, example); + + var actual = entry.getParams(); + + assert.deepEqual(actual, [ + ['Function|Object|string', 'iteratee', 'The function invoked per iteration. '] + ]); + }); + + QUnit.test('should remove arguments ignored because of capping (includes)', function(assert) { + assert.expect(1); + + var example = toEntry('includes', [ + '{Array|Object|string} collection The collection to search.', + '{*} value The value to search for.', + '{number} [fromIndex=0] The index to search from.' + ]); + var entry = new Entry(example, example); + + var actual = entry.getParams(); + + assert.deepEqual(actual, [ + ['*', 'value', 'The value to search for. '], + ['Array|Object|string', 'collection', 'The collection to search. '] + ]); + }); + + QUnit.test('should remove arguments ignored because of capping (trim)', function(assert) { + assert.expect(1); + + var example = toEntry('trim', [ + "{string} [string=''] The string to trim.", + '{string} [chars=whitespace] The characters to trim.' + ]); + var entry = new Entry(example, example); + + var actual = entry.getParams(); + + assert.deepEqual(actual, [ + ['string', 'string', 'The string to trim. '] + ]); + }); + }()); + + QUnit.config.asyncRetries = 10; + QUnit.config.hidepassed = true; + + if (!document) { + QUnit.config.noglobals = true; + QUnit.load(); + } +}.call(this));