Skip to content

Commit 84c9215

Browse files
authored
fix(inference): Refactor membership inference
- Tags with `@lends` are now filtered out early on, so they never generate documentation. Lends tags are structural hints only. - Uses built-in type checks instead of babel-types where appropriate. - More internal documentation - Now reuses code for parsing lends between different forms Previously broken syntax, now working: ```js /** parent */ export default function() { /** child */ this.a = 1; } ```
1 parent 802dc4c commit 84c9215

16 files changed

+1655
-133
lines changed

lib/infer/membership.js

+148-110
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,31 @@ var n = require('babel-types'),
77
isJSDocComment = require('../../lib/is_jsdoc_comment'),
88
parse = require('../../lib/parse');
99

10+
function inferModuleName(comment) {
11+
return (comment.kind === 'module' && comment.name) ||
12+
pathParse(comment.context.file).name;
13+
}
14+
1015
/**
1116
* Given an AST node, try to find a comment in front of it that
1217
* has a `lends` tag, and if it has that, return the tag, split by
1318
* .s.
1419
*
1520
* @private
16-
* @param {Object} node AST node
21+
* @param {Object} path AST node
1722
* @returns {string|undefined} lends identifier, if any
1823
*/
19-
function findLendsIdentifiers(node) {
20-
if (!node || !node.leadingComments) {
24+
function findLendsIdentifiers(path) {
25+
if (!path || !path.get('leadingComments')) {
2126
return;
2227
}
2328

24-
for (var i = 0; i < node.leadingComments.length; i++) {
25-
var comment = node.leadingComments[i];
26-
if (isJSDocComment(comment)) {
27-
var lends = parse(comment.value).lends;
29+
var leadingComments = path.get('leadingComments');
30+
31+
for (var i = 0; i < leadingComments.length; i++) {
32+
var comment = leadingComments[i];
33+
if (isJSDocComment(comment.node)) {
34+
var lends = parse(comment.node.value).lends;
2835
if (lends) {
2936
return lends.split('.');
3037
}
@@ -33,13 +40,16 @@ function findLendsIdentifiers(node) {
3340
}
3441

3542
/**
36-
* Extract and return the identifiers for expressions of type this.foo
43+
* Extract and return the identifiers for expressions of
44+
* type this.foo
3745
*
38-
* @param {NodePath} path AssignmentExpression, MemberExpression, or Identifier
46+
* @param {NodePath} path AssignmentExpression, MemberExpression,
47+
* or Identifier
48+
* @param {Comment} comment
3949
* @returns {Array<string>} identifiers
4050
* @private
4151
*/
42-
function extractThis(path) {
52+
function extractThis(path, comment) {
4353
var identifiers = [];
4454

4555
path.traverse({
@@ -60,14 +70,22 @@ function extractThis(path) {
6070
identifiers.push(scope.path.parentPath.parentPath.node.id.name, 'prototype');
6171
}
6272

73+
// function OldClass() { this.foo = 1 }
6374
if (n.isFunctionDeclaration(scope.block)) {
64-
identifiers.push(scope.block.id.name, 'prototype');
65-
} else if (n.isFunctionExpression(scope.block)) {
66-
if (n.isVariableDeclarator(scope.path.parentPath)) {
75+
// named function like
76+
// function OldClass() { ... }
77+
if (scope.block.id) {
78+
identifiers.push(scope.block.id.name, 'prototype');
79+
} else if (n.isExportDefaultDeclaration(path.scope.parentBlock)) {
80+
identifiers.push(inferModuleName(comment));
81+
}
82+
// var Binding = function OldClass() { this.foo = 1 }
83+
} else if (n.isFunctionExpression((scope.block))) {
84+
if (scope.path.parentPath.isVariableDeclarator()) {
6785
/** var Bar = function(foo) { this.foo = foo; }; */
6886
identifiers = identifiers
6987
.concat(scope.path.parentPath.get('id').node.name).concat('prototype');
70-
} else if (n.isAssignmentExpression(scope.path.parentPath)) {
88+
} else if (scope.path.parentPath.isAssignmentExpression()) {
7189
/** this.Bar = function(foo) { this.foo = foo; }; */
7290
identifiers = identifiers
7391
.concat(extractIdentifiers(scope.path.parentPath.get('left'))).concat('prototype');
@@ -169,10 +187,6 @@ function normalizeMemberof(comment/*: Comment*/)/*: Comment */ {
169187
module.exports = function () {
170188
var currentModule;
171189

172-
function inferModuleName(comment) {
173-
return (comment.kind === 'module' && comment.name) ||
174-
pathParse(comment.context.file).name;
175-
}
176190

177191
/**
178192
* Set `memberof` and `instance`/`static` tags on `comment` based on the
@@ -185,13 +199,13 @@ module.exports = function () {
185199
* @param {Array<string>} identifiers array of identifier names
186200
* @param {string} explicitScope if derived from an es6 class, whether or
187201
* not this method had the static keyword
188-
* @returns {undefined} mutates `comment`
202+
* @returns {Comment} returns mutated `comment`
189203
* @private
190204
*/
191205
function inferMembershipFromIdentifiers(comment, identifiers, explicitScope) {
192206
if (identifiers.length === 1 && identifiers[0] === 'module' && comment.name === 'exports') {
193207
comment.name = inferModuleName(currentModule || comment);
194-
return;
208+
return comment;
195209
}
196210

197211
/*
@@ -215,142 +229,166 @@ module.exports = function () {
215229
comment.scope = 'static';
216230
}
217231
}
232+
return comment;
218233
}
219234

220-
return function inferMembership(comment/*: Comment */) {
221-
235+
function shouldSkipInference(comment/*: Comment */)/*: boolean */ {
236+
// If someone uses the @name tag, they explicitly ask for inference
237+
// to be skipped.
222238
if (comment.tags.some(tag => tag.title === 'name')) {
223-
return comment;
239+
return true;
224240
}
225241

226-
if (comment.kind === 'module') {
227-
currentModule = comment;
242+
// Lends tags are go-betweens that let people reassign membership
243+
// in bulk: they themselves don't get an inference step
244+
if (comment.lends) {
245+
return true;
228246
}
229247

230-
if (comment.lends) {
248+
// If this chunk doesn't have code attached, like if it was the result
249+
// of a polyglot parse, don't try to infer anything.
250+
if (!comment.context.ast) {
251+
return true;
252+
}
253+
254+
return false;
255+
}
256+
257+
return function inferMembership(comment/*: Comment */) {
258+
259+
// First skip inference if the user indicates it or if it isn't possible.
260+
if (shouldSkipInference(comment)) {
231261
return comment;
232262
}
233263

264+
// If someone explicitly specifies the parent of this chunk, don't
265+
// try to infer it, just return what they specified.
234266
if (comment.memberof) {
235267
return normalizeMemberof(comment);
236268
}
237269

238-
if (!comment.context.ast) {
239-
return comment;
270+
if (comment.kind === 'module') {
271+
currentModule = comment;
240272
}
241273

242274
var path = comment.context.ast;
243-
var identifiers;
244-
245275

276+
// INFERENCE ===============================================================
246277
// Deal with an oddity of espree: the jsdoc comment is attached to a different
247278
// node in the two expressions `a.b = c` vs `a.b = function () {}`.
248-
if (n.isExpressionStatement(path.node) &&
249-
n.isAssignmentExpression(path.node.expression) &&
250-
n.isMemberExpression(path.node.expression.left)) {
279+
if (path.isExpressionStatement() &&
280+
path.get('expression').isAssignmentExpression() &&
281+
path.get('expression').get('left').isMemberExpression()) {
251282
path = path.get('expression').get('left');
252283
}
253284

254285
// Same as above but for `b: c` vs `b: function () {}`.
255-
if (n.isObjectProperty(path.node) &&
256-
n.isIdentifier(path.node.key)) {
286+
if (path.isObjectProperty() &&
287+
path.get('key').isIdentifier()) {
257288
path = path.get('key');
258289
}
259290

291+
// Forms:
292+
//
260293
// Foo.bar = ...;
261294
// Foo.prototype.bar = ...;
262295
// Foo.bar.baz = ...;
263-
if (n.isMemberExpression(path.node)) {
264-
identifiers = [].concat(
265-
extractThis(path),
296+
//
297+
// Lends is not supported in this codepath.
298+
if (path.isMemberExpression()) {
299+
var memberIdentifiers = [].concat(
300+
extractThis(path, comment),
266301
extractIdentifiers(path)
267302
);
268-
if (identifiers.length >= 2) {
269-
inferMembershipFromIdentifiers(comment, identifiers.slice(0, -1));
303+
if (memberIdentifiers.length >= 2) {
304+
return inferMembershipFromIdentifiers(comment, memberIdentifiers.slice(0, -1));
270305
}
306+
return comment;
271307
}
272308

273-
// /** @lends Foo */{ bar: ... }
274-
if (n.isIdentifier(path.node) &&
275-
n.isObjectProperty(path.parentPath) &&
276-
n.isObjectExpression(path.parentPath.parentPath)) {
277-
// The @lends comment is sometimes attached to the first property rather than
278-
// the object expression itself.
279-
identifiers = findLendsIdentifiers(path.parentPath.parentPath.node) ||
280-
findLendsIdentifiers(path.parentPath.parentPath.node.properties[0]);
281-
if (identifiers) {
282-
inferMembershipFromIdentifiers(comment, identifiers);
283-
}
284-
}
309+
// Like straight membership, classes don't need
310+
// to support lends.
311+
//
312+
// class Foo { bar() { } }
313+
// var Foo = class { bar() { } }
314+
// class Foo { prop: T }
315+
// var Foo = class { prop: T }
316+
if ((path.isClassMethod() || path.isClassProperty()) &&
317+
path.parentPath.isClassBody() &&
318+
path.parentPath.parentPath.isClass()) {
285319

286-
// Foo = { bar: ... };
287-
// Foo.prototype = { bar: ... };
288-
// Foo.bar = { baz: ... };
289-
if (n.isIdentifier(path.node) &&
290-
n.isObjectProperty(path.parentPath) &&
291-
n.isObjectExpression(path.parentPath.parentPath) &&
292-
n.isAssignmentExpression(path.parentPath.parentPath.parentPath)) {
293-
identifiers = extractIdentifiers(path.parentPath.parentPath.parentPath.get('left'));
294-
if (identifiers.length >= 1) {
295-
inferMembershipFromIdentifiers(comment, identifiers);
320+
var scope = 'instance';
321+
if (path.node.static == true) {
322+
scope = 'static';
296323
}
297-
}
298324

299-
// Shorthand methods on ordinary objects
300-
if (n.isObjectMethod(path.node) &&
301-
n.isObjectExpression(path.parentPath)) {
302-
303-
// Foo = { bar() {} };
304-
// Foo.prototype = { bar() {} };
305-
// Foo.bar = { baz() {} };
306-
if (n.isAssignmentExpression(path.parentPath.parentPath)) {
307-
identifiers = extractIdentifiers(path.parentPath.parentPath.get('left'));
308-
if (identifiers.length >= 1) {
309-
inferMembershipFromIdentifiers(comment, identifiers);
310-
}
325+
if (path.parentPath.parentPath.isExpression()) {
326+
return inferMembershipFromIdentifiers(comment,
327+
extractIdentifiers(path.parentPath.parentPath.parentPath.get('left')),
328+
scope);
311329
}
312330

313-
// var Foo = { bar() {} };
314-
if (n.isVariableDeclarator(path.parentPath.parentPath)) {
315-
identifiers = [path.parentPath.parentPath.get('id').node.name];
316-
inferMembershipFromIdentifiers(comment, identifiers);
331+
var declarationNode = path.parentPath.parentPath.node;
332+
if (!declarationNode.id) {
333+
// export default function () {}
334+
// export default class {}
335+
// Use module name instead.
336+
return inferMembershipFromIdentifiers(comment, [pathParse(comment.context.file).name], scope);
317337
}
338+
339+
return inferMembershipFromIdentifiers(comment, [declarationNode.id.name], scope);
318340
}
319341

320-
// var Foo = { bar: ... }
321-
if (n.isIdentifier(path) &&
322-
n.isObjectProperty(path.parentPath) &&
323-
n.isObjectExpression(path.parentPath.parentPath) &&
324-
n.isVariableDeclarator(path.parentPath.parentPath.parentPath)) {
325-
identifiers = [path.parentPath.parentPath.parentPath.node.id.name];
326-
inferMembershipFromIdentifiers(comment, identifiers);
342+
// Whether something is an ObjectMethod (shorthand like foo() {} )
343+
// or ObjectProperty (old fashioned like foo: function() {} )
344+
// doesn't matter for the membership phase, as long as we end up knowing
345+
// that it belongs to an object. So we first establish objectParent,
346+
// and then have the logic for the numerous ways an object can be named.
347+
var objectParent;
348+
349+
if (path.isIdentifier() &&
350+
path.parentPath.isObjectProperty() &&
351+
path.parentPath.parentPath.isObjectExpression()) {
352+
objectParent = path.parentPath.parentPath;
353+
} else if (path.isObjectMethod() &&
354+
path.parentPath.isObjectExpression()) {
355+
objectParent = path.parentPath;
327356
}
328357

329-
// class Foo { bar() { } }
330-
// var Foo = class { bar() { } }
331-
// class Foo { prop: T }
332-
// var Foo = class { prop: T }
333-
if ((n.isClassMethod(path) || n.isClassProperty(path)) &&
334-
n.isClassBody(path.parentPath) &&
335-
n.isClass(path.parentPath.parentPath)) {
336-
if (n.isExpression(path.parentPath.parentPath)) {
337-
identifiers = extractIdentifiers(path.parentPath.parentPath.parentPath.get('left'));
338-
} else {
339-
var declarationNode = path.parentPath.parentPath.node;
340-
if (!declarationNode.id) {
341-
// export default function () {}
342-
// export default class {}
343-
// Use module name instead.
344-
identifiers = [pathParse(comment.context.file).name];
345-
} else {
346-
identifiers = [declarationNode.id.name];
347-
}
348-
}
349-
var scope = 'instance';
350-
if (path.node.static == true) {
351-
scope = 'static';
358+
// Confirm that the thing being documented is a property of an object.
359+
if (objectParent) {
360+
361+
// The @lends comment is sometimes attached to the first property rather than
362+
// the object expression itself.
363+
var lendsIdentifiers = findLendsIdentifiers(objectParent) ||
364+
findLendsIdentifiers(objectParent.get('properties')[0]);
365+
366+
if (lendsIdentifiers) {
367+
368+
return inferMembershipFromIdentifiers(comment, lendsIdentifiers);
369+
370+
} else if (objectParent.parentPath.isAssignmentExpression()) {
371+
372+
// Foo = { ... };
373+
// Foo.prototype = { ... };
374+
// Foo.bar = { ... };
375+
return inferMembershipFromIdentifiers(comment,
376+
extractIdentifiers(objectParent.parentPath.get('left')));
377+
378+
} else if (objectParent.parentPath.isVariableDeclarator()) {
379+
380+
// var Foo = { ... };
381+
return inferMembershipFromIdentifiers(comment,
382+
[objectParent.parentPath.get('id').node.name]);
383+
384+
} else if (objectParent.parentPath.isExportDefaultDeclaration()) {
385+
386+
// export default { ... };
387+
return inferMembershipFromIdentifiers(comment,
388+
[inferModuleName(currentModule || comment)]);
389+
352390
}
353-
inferMembershipFromIdentifiers(comment, identifiers, scope);
391+
354392
}
355393

356394
// var function Foo() {

lib/is_jsdoc_comment.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* comments.
1212
*
1313
* @name isJSDocComment
14-
* @param {Object} comment an ast node of the comment
14+
* @param {Object} comment an ast path of the comment
1515
* @return {boolean} whether it is valid
1616
*/
1717
module.exports = function isJSDocComment(comment/*: {

lib/module_filters.js

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ var internalModuleRegexp = process.platform === 'win32' ?
1212

1313
/**
1414
* Module filters
15-
* @private
1615
*/
1716
module.exports = {
1817
internalOnly: internalModuleRegexp.test.bind(internalModuleRegexp),

lib/parsers/javascript.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ function parseJavaScript(data/*: Object*/,
4545
walkComments.bind(null, 'leadingComments', true),
4646
walkComments.bind(null, 'innerComments', false),
4747
walkComments.bind(null, 'trailingComments', false)
48-
], fn => fn(ast, data, addComment)).filter(Boolean);
48+
], fn => fn(ast, data, addComment))
49+
.filter(comment => comment && !comment.lends);
4950
}
5051

5152
function _addComment(visited, data, commentValue, commentLoc, path, nodeLoc, includeContext) {

0 commit comments

Comments
 (0)