UNPKG

19.8 kBJavaScriptView Raw
1'use strict';
2
3/**
4 * Provides methods for augmenting the parse results based on their content.
5 * @module jsdoc/augment
6 */
7
8var doop = require('jsdoc/util/doop');
9var name = require('jsdoc/name');
10
11var hasOwnProp = Object.prototype.hasOwnProperty;
12
13function mapDependencies(index, propertyName) {
14 var dependencies = {};
15 var doc;
16 var doclets;
17 var kinds = ['class', 'external', 'interface', 'mixin'];
18 var len = 0;
19
20 Object.keys(index).forEach(function(indexName) {
21 doclets = index[indexName];
22 for (var i = 0, ii = doclets.length; i < ii; i++) {
23 doc = doclets[i];
24 if (kinds.indexOf(doc.kind) !== -1) {
25 dependencies[indexName] = {};
26 if (hasOwnProp.call(doc, propertyName)) {
27 len = doc[propertyName].length;
28 for (var j = 0; j < len; j++) {
29 dependencies[indexName][doc[propertyName][j]] = true;
30 }
31 }
32 }
33 }
34 });
35
36 return dependencies;
37}
38
39function Sorter(dependencies) {
40 this.dependencies = dependencies;
41 this.visited = {};
42 this.sorted = [];
43}
44
45Sorter.prototype.visit = function(key) {
46 var self = this;
47
48 if (!(key in this.visited)) {
49 this.visited[key] = true;
50
51 if (this.dependencies[key]) {
52 Object.keys(this.dependencies[key]).forEach(function(path) {
53 self.visit(path);
54 });
55 }
56
57 this.sorted.push(key);
58 }
59};
60
61Sorter.prototype.sort = function() {
62 var self = this;
63
64 Object.keys(this.dependencies).forEach(function(key) {
65 self.visit(key);
66 });
67
68 return this.sorted;
69};
70
71function sort(dependencies) {
72 var sorter = new Sorter(dependencies);
73 return sorter.sort();
74}
75
76function getMembers(longname, docs, scopes) {
77 var candidate;
78 var members = [];
79
80 for (var i = 0, l = docs.length; i < l; i++) {
81 candidate = docs[i];
82
83 if (candidate.memberof === longname &&
84 (!scopes || !scopes.length || scopes.indexOf(candidate.scope) !== -1)) {
85 members.push(candidate);
86 }
87 }
88
89 return members;
90}
91
92function addDocletProperty(doclets, propName, value) {
93 for (var i = 0, l = doclets.length; i < l; i++) {
94 doclets[i][propName] = value;
95 }
96}
97
98function reparentDoclet(parent, child) {
99 var parts = name.shorten(child.longname);
100
101 parts.memberof = parent.longname;
102 child.memberof = parent.longname;
103 child.longname = name.combine(parts);
104}
105
106function parentIsClass(parent) {
107 return parent.kind === 'class';
108}
109
110function staticToInstance(doclet) {
111 var parts = name.shorten(doclet.longname);
112
113 parts.scope = name.SCOPE.PUNC.INSTANCE;
114 doclet.longname = name.combine(parts);
115 doclet.scope = name.SCOPE.NAMES.INSTANCE;
116}
117
118/**
119 * Update the list of doclets to be added to another symbol.
120 *
121 * We add only one doclet per longname. For example: If `ClassA` inherits from two classes that both
122 * use the same method name, `ClassA` gets docs for one method rather than two.
123 *
124 * Also, the last symbol wins for any given longname. For example: If you write `@extends Class1
125 * @extends Class2`, and both classes have an instance method called `myMethod`, you get the docs
126 * from `Class2#myMethod`.
127 *
128 * @private
129 * @param {module:jsdoc/doclet.Doclet} doclet - The doclet to be added.
130 * @param {Array.<module:jsdoc/doclet.Doclet>} additions - An array of doclets that will be added to
131 * another symbol.
132 * @param {Object.<string, number>} indexes - A dictionary of indexes into the `additions` array.
133 * Each key is a longname, and each value is the index of the longname's doclet.
134 * @return {void}
135 */
136function updateAddedDoclets(doclet, additions, indexes) {
137 if (typeof indexes[doclet.longname] !== 'undefined') {
138 // replace the existing doclet
139 additions[indexes[doclet.longname]] = doclet;
140 }
141 else {
142 // add the doclet to the array, and track its index
143 additions.push(doclet);
144 indexes[doclet.longname] = additions.length - 1;
145 }
146}
147
148/**
149 * Update the index of doclets whose `undocumented` property is not `true`.
150 *
151 * @private
152 * @param {module:jsdoc/doclet.Doclet} doclet - The doclet to be added to the index.
153 * @param {Object.<string, Array.<module:jsdoc/doclet.Doclet>>} documented - The index of doclets
154 * whose `undocumented` property is not `true`.
155 * @return {void}
156 */
157function updateDocumentedDoclets(doclet, documented) {
158 if ( !hasOwnProp.call(documented, doclet.longname) ) {
159 documented[doclet.longname] = [];
160 }
161
162 documented[doclet.longname].push(doclet);
163}
164
165function explicitlyInherits(doclets) {
166 var doclet;
167 var inherits = false;
168
169 for (var i = 0, l = doclets.length; i < l; i++) {
170 doclet = doclets[i];
171 if (typeof doclet.inheritdoc !== 'undefined' || typeof doclet.override !== 'undefined') {
172 inherits = true;
173 break;
174 }
175 }
176
177 return inherits;
178}
179
180// TODO: try to reduce overlap with similar methods
181function getInheritedAdditions(doclets, docs, documented) {
182 var additionIndexes;
183 var additions = [];
184 var doc;
185 var parents;
186 var members;
187 var member;
188 var parts;
189
190 // doclets will be undefined if the inherited symbol isn't documented
191 doclets = doclets || [];
192
193 for (var i = 0, ii = doclets.length; i < ii; i++) {
194 doc = doclets[i];
195 parents = doc.augments;
196
197 if ( parents && (doc.kind === 'class' || doc.kind === 'interface') ) {
198 // reset the lookup table of added doclet indexes by longname
199 additionIndexes = {};
200
201 for (var j = 0, jj = parents.length; j < jj; j++) {
202 members = getMembers(parents[j], docs, ['instance']);
203
204 for (var k = 0, kk = members.length; k < kk; k++) {
205 // We only care about symbols that are documented.
206 if (members[k].undocumented) {
207 continue;
208 }
209
210 member = doop(members[k]);
211
212 if (!member.inherited) {
213 member.inherits = member.longname;
214 }
215 member.inherited = true;
216
217 // TODO: this will fail on longnames like: MyClass#"quoted#Longname"
218 // and nested instance members like: MyClass#MyOtherClass#myMethod;
219 // switch to updateLongname()!
220 member.memberof = doc.longname;
221 parts = member.longname.split('#');
222 parts[0] = doc.longname;
223 member.longname = parts.join('#');
224
225 // Indicate what the descendant is overriding. (We only care about the closest
226 // ancestor. For classes A > B > C, if B#a overrides A#a, and C#a inherits B#a,
227 // we don't want the doclet for C#a to say that it overrides A#a.)
228 if ( hasOwnProp.call(docs.index.longname, member.longname) ) {
229 member.overrides = members[k].longname;
230 }
231 else {
232 delete member.overrides;
233 }
234
235 // Add the ancestor's docs unless the descendant overrides the ancestor AND
236 // documents the override.
237 if ( !hasOwnProp.call(documented, member.longname) ) {
238 updateAddedDoclets(member, additions, additionIndexes);
239 updateDocumentedDoclets(member, documented);
240 }
241 // If the descendant used an @inheritdoc or @override tag, add the ancestor's
242 // docs, and ignore the existing doclets.
243 else if ( explicitlyInherits(documented[member.longname]) ) {
244 // Ignore any existing doclets. (This is safe because we only get here if
245 // `member.longname` is an own property of `documented`.)
246 addDocletProperty(documented[member.longname], 'ignore', true);
247
248 updateAddedDoclets(member, additions, additionIndexes);
249 updateDocumentedDoclets(member, documented);
250
251 // Remove property that's no longer accurate.
252 if (member.virtual) {
253 delete member.virtual;
254 }
255 // Remove properties that we no longer need.
256 if (member.inheritdoc) {
257 delete member.inheritdoc;
258 }
259 if (member.override) {
260 delete member.override;
261 }
262 }
263 // If the descendant overrides the ancestor and documents the override,
264 // update the doclets to indicate what the descendant is overriding.
265 else {
266 addDocletProperty(documented[member.longname], 'overrides',
267 members[k].longname);
268 }
269 }
270 }
271 }
272 }
273
274 return additions;
275}
276
277function updateMixes(mixedDoclet, mixedLongname) {
278 var idx;
279 var mixedName;
280 var names;
281
282 // take the fast path if there's no array of mixed-in longnames
283 if (!mixedDoclet.mixes) {
284 mixedDoclet.mixes = [mixedLongname];
285 }
286 else {
287 // find the short name of the longname we're mixing in
288 mixedName = name.shorten(mixedLongname).name;
289 // find the short name of each previously mixed-in symbol
290 names = mixedDoclet.mixes.map(function(m) {
291 return name.shorten(mixedDoclet.longname).name;
292 });
293
294 // if we're mixing `myMethod` into `MixinC` from `MixinB`, and `MixinB` had the method mixed
295 // in from `MixinA`, don't show `MixinA.myMethod` in the `mixes` list
296 idx = names.indexOf(mixedName);
297 if (idx !== -1) {
298 mixedDoclet.mixes.splice(idx, 1);
299 }
300
301 mixedDoclet.mixes.push(mixedLongname);
302 }
303}
304
305// TODO: try to reduce overlap with similar methods
306function getMixedInAdditions(mixinDoclets, allDoclets, commentedDoclets) {
307 var additionIndexes;
308 var additions = [];
309 var doclet;
310 var idx;
311 var mixedDoclet;
312 var mixedDoclets;
313 var mixes;
314
315 // mixinDoclets will be undefined if the mixed-in symbol isn't documented
316 mixinDoclets = mixinDoclets || [];
317
318 for (var i = 0, ii = mixinDoclets.length; i < ii; i++) {
319 doclet = mixinDoclets[i];
320 mixes = doclet.mixes;
321
322 if (mixes) {
323 // reset the lookup table of added doclet indexes by longname
324 additionIndexes = {};
325
326 for (var j = 0, jj = mixes.length; j < jj; j++) {
327 mixedDoclets = getMembers(mixes[j], allDoclets, ['static']);
328
329 for (var k = 0, kk = mixedDoclets.length; k < kk; k++) {
330 // We only care about symbols that are documented.
331 if (mixedDoclets[k].undocumented) {
332 continue;
333 }
334
335 mixedDoclet = doop(mixedDoclets[k]);
336
337 updateMixes(mixedDoclet, mixedDoclet.longname);
338 mixedDoclet.mixed = true;
339
340 reparentDoclet(doclet, mixedDoclet);
341
342 // if we're mixing into a class, treat the mixed-in symbol as an instance member
343 if (parentIsClass(doclet)) {
344 staticToInstance(mixedDoclet);
345 }
346
347 updateAddedDoclets(mixedDoclet, additions, additionIndexes);
348 updateDocumentedDoclets(mixedDoclet, commentedDoclets);
349 }
350 }
351 }
352 }
353
354 return additions;
355}
356
357function updateImplements(implDoclets, implementedLongname) {
358 if ( !Array.isArray(implDoclets) ) {
359 implDoclets = [implDoclets];
360 }
361
362 implDoclets.forEach(function(implDoclet) {
363 if ( !hasOwnProp.call(implDoclet, 'implements') ) {
364 implDoclet.implements = [];
365 }
366
367 if (implDoclet.implements.indexOf(implementedLongname) === -1) {
368 implDoclet.implements.push(implementedLongname);
369 }
370 });
371}
372
373// TODO: try to reduce overlap with similar methods
374function getImplementedAdditions(implDoclets, allDoclets, commentedDoclets) {
375 var additionIndexes;
376 var additions = [];
377 var doclet;
378 var idx;
379 var implementations;
380 var implExists;
381 var implementationDoclet;
382 var interfaceDoclets;
383
384 // interfaceDoclets will be undefined if the implemented symbol isn't documented
385 implDoclets = implDoclets || [];
386
387 for (var i = 0, ii = implDoclets.length; i < ii; i++) {
388 doclet = implDoclets[i];
389 implementations = doclet.implements;
390
391 if (implementations) {
392 // reset the lookup table of added doclet indexes by longname
393 additionIndexes = {};
394
395 for (var j = 0, jj = implementations.length; j < jj; j++) {
396 interfaceDoclets = getMembers(implementations[j], allDoclets, ['instance']);
397
398 for (var k = 0, kk = interfaceDoclets.length; k < kk; k++) {
399 // We only care about symbols that are documented.
400 if (interfaceDoclets[k].undocumented) {
401 continue;
402 }
403
404 implementationDoclet = doop(interfaceDoclets[k]);
405
406 reparentDoclet(doclet, implementationDoclet);
407 updateImplements(implementationDoclet, interfaceDoclets[k].longname);
408
409 // If there's no implementation, move along.
410 implExists = hasOwnProp.call(allDoclets.index.longname,
411 implementationDoclet.longname);
412 if (!implExists) {
413 continue;
414 }
415
416 // Add the interface's docs unless the implementation is already documented.
417 if ( !hasOwnProp.call(commentedDoclets, implementationDoclet.longname) ) {
418 updateAddedDoclets(implementationDoclet, additions, additionIndexes);
419 updateDocumentedDoclets(implementationDoclet, commentedDoclets);
420 }
421 // If the implementation used an @inheritdoc or @override tag, add the
422 // interface's docs, and ignore the existing doclets.
423 else if ( explicitlyInherits(commentedDoclets[implementationDoclet.longname]) ) {
424 // Ignore any existing doclets. (This is safe because we only get here if
425 // `implementationDoclet.longname` is an own property of
426 // `commentedDoclets`.)
427 addDocletProperty(commentedDoclets[implementationDoclet.longname], 'ignore',
428 true);
429
430 updateAddedDoclets(implementationDoclet, additions, additionIndexes);
431 updateDocumentedDoclets(implementationDoclet, commentedDoclets);
432
433 // Remove property that's no longer accurate.
434 if (implementationDoclet.virtual) {
435 delete implementationDoclet.virtual;
436 }
437 // Remove properties that we no longer need.
438 if (implementationDoclet.inheritdoc) {
439 delete implementationDoclet.inheritdoc;
440 }
441 if (implementationDoclet.override) {
442 delete implementationDoclet.override;
443 }
444 }
445 // If there's an implementation, and it's documented, update the doclets to
446 // indicate what the implementation is implementing.
447 else {
448 updateImplements(commentedDoclets[implementationDoclet.longname],
449 interfaceDoclets[k].longname);
450 }
451 }
452 }
453 }
454 }
455
456 return additions;
457}
458
459function augment(doclets, propertyName, docletFinder) {
460 var index = doclets.index.longname;
461 var dependencies = sort( mapDependencies(index, propertyName) );
462
463 dependencies.forEach(function(depName) {
464 var additions = docletFinder(index[depName], doclets, doclets.index.documented);
465
466 additions.forEach(function(addition) {
467 var longname = addition.longname;
468
469 if ( !hasOwnProp.call(index, longname) ) {
470 index[longname] = [];
471 }
472 index[longname].push(addition);
473 doclets.push(addition);
474 });
475 });
476}
477
478/**
479 * Add doclets to reflect class inheritance.
480 *
481 * For example, if `ClassA` has the instance method `myMethod`, and `ClassB` inherits from `ClassA`,
482 * calling this method creates a new doclet for `ClassB#myMethod`.
483 *
484 * @param {!Array.<module:jsdoc/doclet.Doclet>} doclets - The doclets generated by JSDoc.
485 * @param {!Object} doclets.index - The doclet index added by {@link module:jsdoc/borrow.indexAll}.
486 * @return {void}
487 */
488exports.addInherited = function(doclets) {
489 augment(doclets, 'augments', getInheritedAdditions);
490};
491
492/**
493 * Add doclets to reflect mixins. When a symbol is mixed into a class, the class' version of the
494 * mixed-in symbol is treated as an instance member.
495 *
496 * For example:
497 *
498 * + If `MixinA` has the static method `myMethod`, and `MixinB` mixes `MixinA`, calling this method
499 * creates a new doclet for the static method `MixinB.myMethod`.
500 * + If `MixinA` has the static method `myMethod`, and `ClassA` mixes `MixinA`, calling this method
501 * creates a new doclet for the instance method `ClassA#myMethod`.
502 *
503 * @param {!Array.<module:jsdoc/doclet.Doclet>} doclets - The doclets generated by JSDoc.
504 * @param {!Object} doclets.index - The doclet index added by {@link module:jsdoc/borrow.indexAll}.
505 * @return {void}
506 */
507exports.addMixedIn = function(doclets) {
508 augment(doclets, 'mixes', getMixedInAdditions);
509};
510
511/**
512 * Add and update doclets to reflect implementations of interfaces.
513 *
514 * For example, if `InterfaceA` has the instance method `myMethod`, and `ClassA` implements
515 * `InterfaceA`, calling this method does the following:
516 *
517 * + Updates `InterfaceA` to indicate that it is implemented by `ClassA`
518 * + Updates `InterfaceA#myMethod` to indicate that it is implemented by `ClassA#myMethod`
519 * + Updates `ClassA#myMethod` to indicate that it implements `InterfaceA#myMethod`
520 *
521 * If `ClassA#myMethod` used the `@override` or `@inheritdoc` tag, calling this method would also
522 * generate a new doclet that reflects the interface's documentation for `InterfaceA#myMethod`.
523 *
524 * @param {!Array.<module:jsdoc/doclet.Doclet>} docs - The doclets generated by JSDoc.
525 * @param {!Object} doclets.index - The doclet index added by {@link module:jsdoc/borrow.indexAll}.
526 * @return {void}
527 */
528exports.addImplemented = function(doclets) {
529 augment(doclets, 'implements', getImplementedAdditions);
530};
531
532/**
533 * Add and update doclets to reflect all of the following:
534 *
535 * + Inherited classes
536 * + Mixins
537 * + Interface implementations
538 *
539 * Calling this method is equivalent to calling all other methods exported by this module.
540 *
541 * @return {void}
542 */
543exports.augmentAll = function(doclets) {
544 exports.addMixedIn(doclets);
545 exports.addImplemented(doclets);
546 exports.addInherited(doclets);
547 // look for implemented doclets again, in case we inherited an interface
548 exports.addImplemented(doclets);
549};