augment.js 21 KB

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