Files
lodash/lib/doc/apply-fp-mapping.js
2016-02-09 19:36:37 -08:00

486 lines
16 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
};