mirror of
https://github.com/whoisclebs/lodash.git
synced 2026-02-01 07:47:49 +00:00
486 lines
16 KiB
JavaScript
486 lines
16 KiB
JavaScript
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);
|
||
};
|