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

Lines changed: 148 additions & 110 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 0 additions & 1 deletion
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

Lines changed: 2 additions & 1 deletion
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy