documentation/lib/infer/params.js
Tom MacWright 73747306a0 refactor(nest): Better nesting implementation (#732)
* refactor(nest): Better nesting implementation

This nesting implementation uses a proper recursive tree algorithm

Fixes https://github.com/documentationjs/documentation/issues/554

BREAKING CHANGE: referencing inferred destructure params without
renaming them, like $0.x, from JSDoc comments will no longer
work. To reference them, instead add a param tag to name the
destructuring param, and then refer to members of that name.

Before:

```js
/**
 * @param {number} $0.x a member of x
 */
function a({ x }) {}
```

After:

```js
/**
 * @param {Object} options
 * @param {number} options.x a member of x
 */
function a({ x }) {}
```

* Address review comments

* Reduce testing node requirement back down to 4

* Don't output empty properties, reduce diff noise

* Rearrange and document params

* Simplify param inference, update test fixtures. This is focused around Array destructuring: documenting destructured array elements with indices instead of names, because the names are purely internal details

* Use temporary fork to get through blocker
2017-04-21 17:28:53 -04:00

329 lines
10 KiB
JavaScript

'use strict';
/* @flow */
const t = require('babel-types');
const generate = require('babel-generator').default;
const _ = require('lodash');
const findTarget = require('./finders').findTarget;
const flowDoctrine = require('../flow_doctrine');
const util = require('util');
const debuglog = util.debuglog('infer');
/**
* Infers param tags by reading function parameter names
*
* @param {Object} comment parsed comment
* @returns {Object} comment with parameters
*/
function inferParams(comment /*: Comment */) {
var path = findTarget(comment.context.ast);
// In case of `/** */ var x = function () {}` findTarget returns
// the declarator.
if (t.isVariableDeclarator(path)) {
path = path.get('init');
}
if (!t.isFunction(path)) {
return comment;
}
var inferredParams = path.node.params.map((param, i) =>
paramToDoc(param, '', i));
var mergedParams = mergeTrees(inferredParams, comment.params);
// Then merge the trees. This is the hard part.
return _.assign(comment, {
params: mergedParams
});
}
// Utility methods ============================================================
//
const PATH_SPLIT_CAPTURING = /(\[])?(\.)/g;
/**
* Index tags by their `name` property into an ES6 map.
*/
function mapTags(tags) {
return new Map(
tags.map(tag => {
return [tag.name, tag];
})
);
}
/**
* Babel parses JavaScript source code and produces an abstract syntax
* tree that includes methods and their arguments. This function takes
* that AST and uses it to infer details that would otherwise need
* explicit documentation, like the names of comments and their
* default values.
*
* It is especially careful to allow the user and the machine to collaborate:
* documentation.js should not overwrite any details that the user
* explicitly sets.
*
* @private
* @param {Object} param the abstract syntax tree of the parameter in JavaScript
* @param {number} i the number of this parameter, in argument order
* @param {string} prefix of the comment, if it is nested, like in the case of destructuring
* @returns {Object} parameter with inference.
*/
function paramToDoc(
param,
prefix /*: string */,
i /*: ?number */
) /*: CommentTag|Array<CommentTag> */ {
const autoName = '$' + String(i);
const prefixedName = prefix + '.' + param.name;
switch (param.type) {
case 'AssignmentPattern': // (a = b)
const newAssignmentParam = paramToDoc(param.left, '', i);
if (Array.isArray(newAssignmentParam)) {
throw new Error('Encountered an unexpected parameter type');
}
return _.assign(newAssignmentParam, {
default: generate(param.right, {
compact: true
}).code,
type: {
type: 'OptionalType',
expression: newAssignmentParam.type
}
});
// ObjectPattern <AssignmentProperty | RestElement>
case 'ObjectPattern': // { a }
if (prefix === '') {
// If this is a root-level param, like f({ x }), then we need to name
// it, like $0 or $1, depending on its position.
return {
title: 'param',
name: autoName,
anonymous: true,
type: (param.typeAnnotation && flowDoctrine(param)) || {
type: 'NameExpression',
name: 'Object'
},
properties: _.flatMap(param.properties, prop => {
return paramToDoc(prop, prefix + autoName);
})
};
} else if (param.indexed) {
// Likewise, if this object pattern sits inside of an ArrayPattern,
// like [{ foo }], it shouldn't just look like $0.foo, but like $0.0.foo,
// so make sure it isn't indexed first.
return {
title: 'param',
name: prefixedName,
anonymous: true,
type: (param.typeAnnotation && flowDoctrine(param)) || {
type: 'NameExpression',
name: 'Object'
},
properties: _.flatMap(param.properties, prop => {
return paramToDoc(prop, prefixedName);
})
};
}
// If, otherwise, this is nested, we don't really represent it as
// a parameter in and of itself - we just want its children, and
// it will be the . in obj.prop
return _.flatMap(param.properties, prop => {
return paramToDoc(prop, prefix);
});
// ArrayPattern<Pattern | null>
case 'ArrayPattern': // ([a, b, { c }])
if (prefix === '') {
return {
title: 'param',
name: autoName,
anonymous: true,
type: (param.typeAnnotation && flowDoctrine(param)) || {
type: 'NameExpression',
name: 'Array'
},
// Array destructuring lets you name the elements in the array,
// but those names don't really make sense within the JSDoc
// indexing tradition, or have any external meaning. So
// instead we're going to (immutably) rename the parameters to their
// indices
properties: _.flatMap(param.elements, (element, idx) => {
var indexedElement = _.assign({}, element, {
name: String(idx),
indexed: true
});
return paramToDoc(indexedElement, autoName);
})
};
}
return _.flatMap(param.elements, (element, idx) => {
var indexedElement = _.assign({}, element, {
name: String(idx)
});
return paramToDoc(indexedElement, prefix);
});
case 'ObjectProperty':
return _.assign(paramToDoc(param.value, prefix + '.' + param.key.name), {
name: prefix + '.' + param.key.name
});
case 'RestProperty': // (a, ...b)
case 'RestElement':
let type /*: DoctrineType */ = {
type: 'RestType'
};
if (param.typeAnnotation) {
type.expression = flowDoctrine(param.typeAnnotation.typeAnnotation);
}
return {
title: 'param',
name: param.argument.name,
name: prefix ? `${prefix}.${param.argument.name}` : param.argument.name,
lineNumber: param.loc.start.line,
type
};
default:
// (a)
var newParam /*: CommentTagNamed */ = {
title: 'param',
name: prefix ? prefixedName : param.name,
lineNumber: param.loc.start.line
};
// Flow/TS annotations
if (param.typeAnnotation && param.typeAnnotation.typeAnnotation) {
newParam.type = flowDoctrine(param.typeAnnotation.typeAnnotation);
}
return newParam;
}
}
/**
* Recurse through a potentially nested parameter tag,
* replacing the auto-generated name, like $0, with an explicit
* name provided from a JSDoc comment. For instance, if you have a code
* block like
*
* function f({ x });
*
* It would by default be documented with a first param $0, with a member $0.x
*
* If you specify the name of the param, then it could be documented with, say,
* options and options.x. So we need to recursively rename not just $0 but
* also $0.x and maybe $0.x.y.z all to options.x and options.x.y.z
*/
function renameTree(node, explicitName) {
var parts = node.name.split(PATH_SPLIT_CAPTURING);
parts[0] = explicitName;
node.name = parts.join('');
if (node.properties) {
node.properties.forEach(property => renameTree(property, explicitName));
}
}
function mergeTrees(inferred, explicit) {
// The first order of business is ensuring that the root types are specified
// in the right order. For the order of arguments, the inferred reality
// is the ground-truth: a function like
// function addThem(a, b, c) {}
// Should always see (a, b, c) in that order
// First, if all parameters are specified, allow explicit names to apply
// to destructuring parameters, which do not have inferred names. This is
// _only_ enabled in the case in which all parameters are specified explicitly
if (inferred.length === explicit.length) {
for (var i = 0; i < inferred.length; i++) {
if (inferred[i].anonymous === true) {
renameTree(inferred[i], explicit[i].name);
}
}
}
return mergeTopNodes(inferred, explicit);
}
function mergeTopNodes(inferred, explicit) {
const mapExplicit = mapTags(explicit);
const inferredNames = new Set(inferred.map(tag => tag.name));
const explicitTagsWithoutInference = explicit.filter(
tag => !inferredNames.has(tag.name)
);
if (explicitTagsWithoutInference.length) {
debuglog(
`${explicitTagsWithoutInference.length} tags were specified but didn't match ` +
`inferred information ${explicitTagsWithoutInference
.map(t => t.name)
.join(', ')}`
);
}
return inferred
.map(inferredTag => {
const explicitTag = mapExplicit.get(inferredTag.name);
return explicitTag ? combineTags(inferredTag, explicitTag) : inferredTag;
})
.concat(explicitTagsWithoutInference);
}
// This method is used for _non-root_ properties only - we use mergeTopNodes
// for root properties, which strictly requires inferred only. In this case,
// we combine all tags:
// - inferred & explicit
// - explicit only
// - inferred only
function mergeNodes(inferred, explicit) {
const intersection = _.intersectionBy(inferred, explicit, tag => tag.name);
const explicitOnly = _.differenceBy(explicit, inferred, tag => tag.name);
const inferredOnly = _.differenceBy(inferred, explicit, tag => tag.name);
const mapExplicit = mapTags(explicit);
return intersection
.map(inferredTag => {
const explicitTag = mapExplicit.get(inferredTag.name);
return explicitTag ? combineTags(inferredTag, explicitTag) : inferredTag;
})
.concat(explicitOnly)
.concat(inferredOnly);
}
function combineTags(inferredTag, explicitTag) {
let type = explicitTag.type;
var defaultValue;
if (!explicitTag.type) {
type = inferredTag.type;
} else if (explicitTag.type.type !== 'OptionalType' && inferredTag.default) {
type = {
type: 'OptionalType',
expression: explicitTag.type
};
defaultValue = inferredTag.default;
}
const hasProperties = (inferredTag.properties &&
inferredTag.properties.length) ||
(explicitTag.properties && explicitTag.properties.length);
return _.assign(
explicitTag,
hasProperties
? {
properties: mergeNodes(
inferredTag.properties || [],
explicitTag.properties || []
)
}
: {},
{ type },
defaultValue ? { default: defaultValue } : {}
);
}
module.exports = inferParams;
module.exports.mergeTrees = mergeTrees;