UNPKG

6.99 kBJavaScriptView Raw
1const _ = require('lodash');
2const hasOwnProperty = Object.prototype.hasOwnProperty;
3
4/**
5 * Check if a given member object is of kind `event`.
6 * @param {Object} member - The member to check.
7 * @returns {boolean} `true` if it is of kind `event`, otherwise false.
8 */
9const isEvent = member => member.kind === 'event';
10
11/**
12 * We need to have members of all valid JSDoc scopes.
13 * @private
14 */
15const getMembers = () => ({
16 global: Object.create(null),
17 inner: Object.create(null),
18 instance: Object.create(null),
19 events: Object.create(null),
20 static: Object.create(null)
21});
22
23/**
24 * Pick only relevant properties from a comment to store them in
25 * an inheritance chain
26 * @param comment a parsed comment
27 * @returns reduced comment
28 * @private
29 */
30function pick(comment) {
31 if (typeof comment.name !== 'string') {
32 return undefined;
33 }
34
35 const item = {
36 name: comment.name,
37 kind: comment.kind
38 };
39
40 if (comment.scope) {
41 item.scope = comment.scope;
42 }
43
44 return item;
45}
46
47/**
48 * @param {Array<Object>} comments an array of parsed comments
49 * @returns {Array<Object>} nested comments, with only root comments
50 * at the top level.
51 */
52module.exports = function(comments) {
53 let id = 0;
54 const root = {
55 members: getMembers()
56 };
57
58 const namesToUnroot = [];
59
60 comments.forEach(comment => {
61 let path = comment.path;
62 if (!path) {
63 path = [];
64
65 if (comment.memberof) {
66 // TODO: full namepath parsing
67 path = comment.memberof
68 .split('.')
69 .map(segment => ({ scope: 'static', name: segment }));
70 }
71
72 if (!comment.name) {
73 comment.errors.push({
74 message: 'could not determine @name for hierarchy'
75 });
76 }
77
78 path.push({
79 scope: comment.scope || 'static',
80 name: comment.name || 'unknown_' + id++
81 });
82 }
83
84 let node = root;
85
86 while (path.length) {
87 const segment = path.shift();
88 const scope = segment.scope;
89 const name = segment.name;
90
91 if (!hasOwnProperty.call(node.members[scope], name)) {
92 // If segment.toc is true, everything up to this point in the path
93 // represents how the documentation should be nested, but not how the
94 // actual code is nested. To ensure that child members end up in the
95 // right places in the tree, we temporarily push the same node a second
96 // time to the root of the tree, and unroot it after all the comments
97 // have found their homes.
98 if (
99 segment.toc &&
100 node !== root &&
101 hasOwnProperty.call(root.members[scope], name)
102 ) {
103 node.members[scope][name] = root.members[scope][name];
104 namesToUnroot.push(name);
105 } else {
106 const newNode = (node.members[scope][name] = {
107 comments: [],
108 members: getMembers()
109 });
110 if (segment.toc && node !== root) {
111 root.members[scope][name] = newNode;
112 namesToUnroot.push(name);
113 }
114 }
115 }
116
117 node = node.members[scope][name];
118 }
119
120 node.comments.push(comment);
121 });
122 namesToUnroot.forEach(function(name) {
123 delete root.members.static[name];
124 });
125
126 /*
127 * Massage the hierarchy into a format more suitable for downstream consumers:
128 *
129 * * Individual top-level scopes are collapsed to a single array
130 * * Members at intermediate nodes are copied over to the corresponding comments,
131 * with multisignature comments allowed.
132 * * Intermediate nodes without corresponding comments indicate an undefined
133 * @memberof reference. Emit an error, and reparent the offending comment to
134 * the root.
135 * * Add paths to each comment, making it possible to generate permalinks
136 * that differentiate between instance functions with the same name but
137 * different `@memberof` values.
138 *
139 * Person#say // the instance method named "say."
140 * Person.say // the static method named "say."
141 * Person~say // the inner method named "say."
142 */
143 function toComments(nodes, root, hasUndefinedParent, path) {
144 const result = [];
145 let scope;
146
147 path = path || [];
148
149 for (const name in nodes) {
150 const node = nodes[name];
151
152 for (scope in node.members) {
153 node.members[scope] = toComments(
154 node.members[scope],
155 root || result,
156 !node.comments.length,
157 node.comments.length && node.comments[0].kind !== 'note'
158 ? path.concat(node.comments[0])
159 : []
160 );
161 }
162
163 for (let i = 0; i < node.comments.length; i++) {
164 const comment = node.comments[i];
165
166 comment.members = {};
167 for (scope in node.members) {
168 comment.members[scope] = node.members[scope];
169 }
170
171 let events = comment.members.events;
172 let groups = [];
173
174 if (comment.members.instance.length) {
175 groups = _.groupBy(comment.members.instance, isEvent);
176
177 events = events.concat(groups[true] || []);
178 comment.members.instance = groups[false] || [];
179 }
180
181 if (comment.members.static.length) {
182 groups = _.groupBy(comment.members.static, isEvent);
183
184 events = events.concat(groups[true] || []);
185 comment.members.static = groups[false] || [];
186 }
187
188 if (comment.members.inner.length) {
189 groups = _.groupBy(comment.members.inner, isEvent);
190
191 events = events.concat(groups[true] || []);
192 comment.members.inner = groups[false] || [];
193 }
194
195 if (comment.members.global.length) {
196 groups = _.groupBy(comment.members.global, isEvent);
197
198 events = events.concat(groups[true] || []);
199 comment.members.global = groups[false] || [];
200 }
201
202 comment.members.events = events;
203
204 comment.path = path
205 .map(pick)
206 .concat(pick(comment))
207 .filter(Boolean);
208
209 const scopeChars = {
210 instance: '#',
211 static: '.',
212 inner: '~',
213 global: ''
214 };
215
216 comment.namespace = comment.path.reduce((memo, part) => {
217 if (part.kind === 'event') {
218 return memo + '.event:' + part.name;
219 }
220 let scopeChar = '';
221 if (part.scope) {
222 scopeChar = scopeChars[part.scope];
223 }
224 return memo + scopeChar + part.name;
225 }, '');
226
227 if (hasUndefinedParent) {
228 const memberOfTag = comment.tags.filter(
229 tag => tag.title === 'memberof'
230 )[0];
231 const memberOfTagLineNumber =
232 (memberOfTag && memberOfTag.lineNumber) || 0;
233
234 comment.errors.push({
235 message: `@memberof reference to ${comment.memberof} not found`,
236 commentLineNumber: memberOfTagLineNumber
237 });
238
239 root.push(comment);
240 } else {
241 result.push(comment);
242 }
243 }
244 }
245
246 return result;
247 }
248
249 return toComments(root.members.static);
250};