Skip to content

Commit ae13664

Browse files
committed
Nested hierarchy support
1 parent 9dcf926 commit ae13664

File tree

6 files changed

+211
-86
lines changed

6 files changed

+211
-86
lines changed

lib/hierarchy.js

Lines changed: 108 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,128 @@
11
'use strict';
22

3-
/**
4-
* Add paths to each comment, making it possible to generate permalinks
5-
* that differentiate between instance functions with the same name but
6-
* different `@memberof` values.
7-
*
8-
* Person#say // the instance method named "say."
9-
* Person.say // the static method named "say."
10-
* Person~say // the inner method named "say."
11-
*
12-
* @param {Object} comment the jsdoc comment
13-
* @param {Array<string>} prefix an array of strings representing names
14-
* @param {string} namepath the namepath so far
15-
* @returns {undefined} changes its input by reference.
16-
*/
17-
function addPath(comment, prefix, namepath) {
18-
comment.path = prefix.concat([comment.name]);
19-
comment.members.instance.forEach(function (member) {
20-
addPath(member, comment.path, comment.namepath);
21-
});
22-
comment.members.static.forEach(function (member) {
23-
addPath(member, comment.path, namepath);
24-
});
25-
}
26-
273
/**
284
* @param {Array<Object>} comments an array of parsed comments
295
* @returns {Array<Object>} nested comments, with only root comments
306
* at the top level.
317
*/
32-
function inferHierarchy(comments) {
33-
var nameIndex = {}, i;
34-
35-
// We're going to iterate comments in reverse to generate the memberships so
36-
// to avoid reversing the sort order we reverse the array for the name index.
37-
comments.reverse();
38-
39-
// First, create a fast lookup index of Namespace names
40-
// that might be used in memberof tags, and let all objects
41-
// have members
42-
for (i = 0; i < comments.length; i++) {
43-
nameIndex[comments[i].name] = comments[i];
44-
comments[i].members = { instance: [], static: [] };
45-
}
8+
module.exports = function (comments) {
9+
var id = 0,
10+
root = {
11+
members: {
12+
instance: {},
13+
static: {}
14+
}
15+
};
4616

47-
for (i = comments.length - 1; i >= 0; i--) {
48-
var comment = comments[i];
17+
comments.forEach(function (comment) {
18+
var path = [];
4919

50-
if (!comment.memberof) {
51-
continue;
20+
if (comment.memberof) {
21+
// TODO: full namepath parsing
22+
path = comment.memberof
23+
.split('.')
24+
.map(function (segment) {
25+
return ['static', segment];
26+
});
5227
}
5328

54-
var memberOfTag = comment.tags.filter(function (tag) {
55-
return tag.title === 'memberof'
56-
})[0];
57-
var memberOfTagLineNumber = (memberOfTag && memberOfTag.lineNumber) || 0;
58-
59-
var parent = nameIndex[comment.memberof];
60-
61-
if (!parent) {
29+
if (!comment.name) {
6230
comment.errors.push({
63-
message: 'memberof reference to ' + comment.memberof + ' not found',
64-
commentLineNumber: memberOfTagLineNumber
31+
message: 'could not determine @name for hierarchy'
6532
});
66-
continue;
6733
}
6834

69-
parent.members[comment.scope || 'static'].push(comment);
35+
path.push([
36+
comment.scope || 'static',
37+
comment.name || ('unknown_' + id++)
38+
]);
7039

71-
// remove non-root nodes from the lowest level: these are reachable
72-
// as members of other docs.
73-
comments.splice(i, 1);
74-
}
40+
var node = root;
7541

76-
// Now the members are in the right order but the root comments are reversed
77-
// so we reverse once more.
78-
comments.reverse();
42+
while (path.length) {
43+
var segment = path.shift(),
44+
scope = segment[0],
45+
name = segment[1];
7946

80-
for (i = 0; i < comments.length; i++) {
81-
addPath(comments[i], [], '');
82-
}
47+
if (!node.members[scope][name]) {
48+
node.members[scope][name] = {
49+
comments: [],
50+
members: {
51+
instance: {},
52+
static: {}
53+
}
54+
};
55+
}
56+
57+
node = node.members[scope][name];
58+
}
59+
60+
node.comments.push(comment);
61+
});
62+
63+
/*
64+
* Massage the hierarchy into a format more suitable for downstream consumers:
65+
*
66+
* * Individual top-level scopes are collapsed to a single array
67+
* * Members at intermediate nodes are copied over to the corresponding comments,
68+
* with multisignature comments allowed.
69+
* * Intermediate nodes without corresponding comments indicate an undefined
70+
* @memberof reference. Emit an error, and reparent the offending comment to
71+
* the root.
72+
* * Add paths to each comment, making it possible to generate permalinks
73+
* that differentiate between instance functions with the same name but
74+
* different `@memberof` values.
75+
*
76+
* Person#say // the instance method named "say."
77+
* Person.say // the static method named "say."
78+
* Person~say // the inner method named "say."
79+
*/
80+
function toComments(nodes, root, hasUndefinedParent, path) {
81+
var result = [], scope;
82+
83+
path = path || [];
84+
85+
for (var name in nodes) {
86+
var node = nodes[name];
87+
88+
for (scope in node.members) {
89+
node.members[scope] = toComments(node.members[scope], root || result,
90+
!node.comments.length,
91+
node.comments.length ? path.concat(node.comments[0]) : []);
92+
}
93+
94+
for (var i = 0; i < node.comments.length; i++) {
95+
var comment = node.comments[i];
96+
97+
comment.members = {};
98+
for (scope in node.members) {
99+
comment.members[scope] = node.members[scope];
100+
}
83101

84-
return comments;
85-
}
102+
comment.path = path.map(function (n) {
103+
return n.name;
104+
}).concat(comment.name);
105+
106+
if (hasUndefinedParent) {
107+
var memberOfTag = comment.tags.filter(function (tag) {
108+
return tag.title === 'memberof'
109+
})[0];
110+
var memberOfTagLineNumber = (memberOfTag && memberOfTag.lineNumber) || 0;
111+
112+
comment.errors.push({
113+
message: '@memberof reference to ' + comment.memberof + ' not found',
114+
commentLineNumber: memberOfTagLineNumber
115+
});
116+
117+
root.push(comment);
118+
} else {
119+
result.push(comment);
120+
}
121+
}
122+
}
123+
124+
return result;
125+
}
86126

87-
module.exports = inferHierarchy;
127+
return toComments(root.members.static);
128+
};

test/fixture/factory.output.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@
147147
},
148148
"errors": [
149149
{
150-
"message": "memberof reference to chart not found",
150+
"message": "@memberof reference to chart not found",
151151
"commentLineNumber": 0
152152
}
153153
],

test/fixture/newline-in-description.output.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@
3939
{
4040
"message": "type Number found, number is standard",
4141
"commentLineNumber": 2
42+
},
43+
{
44+
"message": "could not determine @name for hierarchy"
4245
}
4346
],
4447
"params": [

test/fixture/no-name.output.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@
3535
}
3636
}
3737
},
38-
"errors": [],
38+
"errors": [
39+
{
40+
"message": "could not determine @name for hierarchy"
41+
}
42+
],
3943
"params": [
4044
{
4145
"title": "param",

test/fixture/trailing-only.output.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@
3434
}
3535
}
3636
},
37-
"errors": [],
37+
"errors": [
38+
{
39+
"message": "could not determine @name for hierarchy"
40+
}
41+
],
3842
"returns": [
3943
{
4044
"title": "returns",

test/lib/hierarchy.js

Lines changed: 89 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
var test = require('tap').test,
44
parse = require('../../lib/parsers/javascript'),
5-
hierarchy = require('../../lib/hierarchy');
5+
hierarchy = require('../../lib/hierarchy'),
6+
_ = require('lodash');
67

78
function toComments(fn, filename) {
89
return parse({
@@ -16,7 +17,7 @@ function evaluate(fn, callback) {
1617
}
1718

1819
test('hierarchy', function (t) {
19-
var result = evaluate(function () {
20+
var comments = evaluate(function () {
2021
/**
2122
* @name Class
2223
* @class
@@ -47,31 +48,103 @@ test('hierarchy', function (t) {
4748
*/
4849
});
4950

50-
t.equal(result.length, 1);
51+
t.deepEqual(_.pluck(comments, 'name'), ['Class']);
52+
53+
var classMembers = comments[0].members;
5154

52-
t.equal(result[0].members.static.length, 2);
53-
t.deepEqual(result[0].members.static[0].path, ['Class', 'isClass']);
55+
t.deepEqual(_.pluck(classMembers.static, 'name'), ['isClass', 'MAGIC_NUMBER']);
56+
t.deepEqual(_.pluck(classMembers.instance, 'name'), ['getFoo', 'event']);
5457

55-
t.equal(result[0].members.instance.length, 2);
56-
t.deepEqual(result[0].members.instance[0].path, ['Class', 'getFoo']);
57-
t.deepEqual(result[0].members.instance[1].path, ['Class', 'event']);
58+
t.deepEqual(classMembers.static[0].path, ['Class', 'isClass']);
59+
t.deepEqual(classMembers.instance[0].path, ['Class', 'getFoo']);
60+
t.deepEqual(classMembers.instance[1].path, ['Class', 'event']);
5861

5962
t.end();
6063
});
6164

65+
test('hierarchy - nesting', function (t) {
66+
var comments = evaluate(function () {
67+
/**
68+
* @name Parent
69+
* @class
70+
*/
71+
72+
/**
73+
* @name enum
74+
* @memberof Parent
75+
*/
76+
77+
/**
78+
* @name Parent
79+
* @memberof Parent.enum
80+
*/
81+
82+
/**
83+
* @name Child
84+
* @memberof Parent.enum
85+
*/
86+
});
87+
88+
t.deepEqual(_.pluck(comments, 'name'), ['Parent']);
89+
90+
var classMembers = comments[0].members;
91+
t.deepEqual(_.pluck(classMembers.static, 'name'), ['enum']);
92+
93+
var enumMembers = classMembers.static[0].members;
94+
t.deepEqual(_.pluck(enumMembers.static, 'name'), ['Parent', 'Child']);
95+
t.deepEqual(enumMembers.static[0].path, ['Parent', 'enum', 'Parent']);
96+
t.deepEqual(enumMembers.static[1].path, ['Parent', 'enum', 'Child']);
97+
98+
t.end();
99+
});
100+
101+
test('hierarchy - multisignature', function (t) {
102+
var comments = evaluate(function () {
103+
/**
104+
* @name Parent
105+
* @class
106+
*/
107+
108+
/**
109+
* @name foo
110+
* @memberof Parent
111+
* @instance
112+
*/
113+
114+
/**
115+
* @name foo
116+
* @memberof Parent
117+
* @instance
118+
*/
119+
});
120+
121+
t.deepEqual(_.pluck(comments[0].members.instance, 'name'), ['foo', 'foo']);
122+
t.end();
123+
});
124+
62125
test('hierarchy - missing memberof', function (t) {
63-
var result = evaluate(function () {
126+
var test = evaluate(function () {
64127
/**
65-
* Get foo
128+
* @name test
66129
* @memberof DoesNotExist
67-
* @returns {Number} foo
68130
*/
69-
});
131+
})[0];
70132

71-
t.equal(result.length, 1);
72-
t.deepEqual(result[0].errors[0], {
73-
message: 'memberof reference to DoesNotExist not found',
133+
t.deepEqual(test.errors, [{
134+
message: '@memberof reference to DoesNotExist not found',
74135
commentLineNumber: 2
75-
}, 'correct error message');
136+
}], 'correct error message');
137+
t.end();
138+
});
139+
140+
test('hierarchy - anonymous', function (t) {
141+
var result = evaluate(function () {
142+
/** Test */
143+
})[0];
144+
145+
t.equal(result.description, 'Test');
146+
t.deepEqual(result.errors, [{
147+
message: 'could not determine @name for hierarchy'
148+
}]);
76149
t.end();
77150
});

0 commit comments

Comments
 (0)