doclet.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. /**
  2. * @module jsdoc/doclet
  3. */
  4. const _ = require('underscore');
  5. const jsdoc = {
  6. env: require('jsdoc/env'),
  7. name: require('jsdoc/name'),
  8. src: {
  9. astnode: require('jsdoc/src/astnode'),
  10. Syntax: require('jsdoc/src/syntax').Syntax
  11. },
  12. tag: {
  13. Tag: require('jsdoc/tag').Tag,
  14. dictionary: require('jsdoc/tag/dictionary')
  15. },
  16. util: {
  17. doop: require('jsdoc/util/doop')
  18. }
  19. };
  20. const path = require('jsdoc/path');
  21. const Syntax = jsdoc.src.Syntax;
  22. const util = require('util');
  23. function applyTag(doclet, {title, value}) {
  24. if (title === 'name') {
  25. doclet.name = value;
  26. }
  27. if (title === 'kind') {
  28. doclet.kind = value;
  29. }
  30. if (title === 'description') {
  31. doclet.description = value;
  32. }
  33. }
  34. function fakeMeta(node) {
  35. return {
  36. type: node ? node.type : null,
  37. node: node
  38. };
  39. }
  40. // use the meta info about the source code to guess what the doclet kind should be
  41. // TODO: set this elsewhere (maybe jsdoc/src/astnode.getInfo)
  42. function codeToKind(code) {
  43. const isFunction = jsdoc.src.astnode.isFunction;
  44. let kind = 'member';
  45. const node = code.node;
  46. if ( isFunction(code.type) && code.type !== Syntax.MethodDefinition ) {
  47. kind = 'function';
  48. }
  49. else if (code.type === Syntax.MethodDefinition) {
  50. if (code.node.kind === 'constructor') {
  51. kind = 'class';
  52. }
  53. else if (code.node.kind !== 'get' && code.node.kind !== 'set') {
  54. kind = 'function';
  55. }
  56. }
  57. else if (code.type === Syntax.ClassDeclaration || code.type === Syntax.ClassExpression) {
  58. kind = 'class';
  59. }
  60. else if (code.type === Syntax.ExportAllDeclaration) {
  61. // this value will often be an Identifier for a variable, which isn't very useful
  62. kind = codeToKind(fakeMeta(node.source));
  63. }
  64. else if (code.type === Syntax.ExportDefaultDeclaration ||
  65. code.type === Syntax.ExportNamedDeclaration) {
  66. kind = codeToKind(fakeMeta(node.declaration));
  67. }
  68. else if (code.type === Syntax.ExportSpecifier) {
  69. // this value will often be an Identifier for a variable, which isn't very useful
  70. kind = codeToKind(fakeMeta(node.local));
  71. }
  72. else if ( code.node && code.node.parent && isFunction(code.node.parent) ) {
  73. kind = 'param';
  74. }
  75. return kind;
  76. }
  77. function unwrap(docletSrc) {
  78. if (!docletSrc) { return ''; }
  79. // note: keep trailing whitespace for @examples
  80. // extra opening/closing stars are ignored
  81. // left margin is considered a star and a space
  82. // use the /m flag on regex to avoid having to guess what this platform's newline is
  83. docletSrc =
  84. // remove opening slash+stars
  85. docletSrc.replace(/^\/\*\*+/, '')
  86. // replace closing star slash with end-marker
  87. .replace(/\**\*\/$/, '\\Z')
  88. // remove left margin like: spaces+star or spaces+end-marker
  89. .replace(/^\s*(\* ?|\\Z)/gm, '')
  90. // remove end-marker
  91. .replace(/\s*\\Z$/g, '');
  92. return docletSrc;
  93. }
  94. /**
  95. * Convert the raw source of the doclet comment into an array of pseudo-Tag objects.
  96. * @private
  97. */
  98. function toTags(docletSrc) {
  99. let parsedTag;
  100. const tagData = [];
  101. let tagText;
  102. let tagTitle;
  103. // split out the basic tags, keep surrounding whitespace
  104. // like: @tagTitle tagBody
  105. docletSrc
  106. // replace splitter ats with an arbitrary sequence
  107. .replace(/^(\s*)@(\S)/gm, '$1\\@$2')
  108. // then split on that arbitrary sequence
  109. .split('\\@')
  110. .forEach($ => {
  111. if ($) {
  112. parsedTag = $.match(/^(\S+)(?:\s+(\S[\s\S]*))?/);
  113. if (parsedTag) {
  114. tagTitle = parsedTag[1];
  115. tagText = parsedTag[2];
  116. if (tagTitle) {
  117. tagData.push({
  118. title: tagTitle,
  119. text: tagText
  120. });
  121. }
  122. }
  123. }
  124. });
  125. return tagData;
  126. }
  127. function fixDescription(docletSrc, {code}) {
  128. let isClass;
  129. if (!/^\s*@/.test(docletSrc) && docletSrc.replace(/\s/g, '').length) {
  130. isClass = code &&
  131. (code.type === Syntax.ClassDeclaration ||
  132. code.type === Syntax.ClassExpression);
  133. docletSrc = `${isClass ? '@classdesc' : '@description'} ${docletSrc}`;
  134. }
  135. return docletSrc;
  136. }
  137. /**
  138. * Replace the existing tag dictionary with a new tag dictionary.
  139. *
  140. * Used for testing only.
  141. *
  142. * @private
  143. * @param {module:jsdoc/tag/dictionary.Dictionary} dict - The new tag dictionary.
  144. */
  145. exports._replaceDictionary = function _replaceDictionary(dict) {
  146. jsdoc.tag.dictionary = dict;
  147. require('jsdoc/tag')._replaceDictionary(dict);
  148. require('jsdoc/util/templateHelper')._replaceDictionary(dict);
  149. };
  150. function removeGlobal(longname) {
  151. const globalRegexp = new RegExp(`^${jsdoc.name.LONGNAMES.GLOBAL}\\.?`);
  152. return longname.replace(globalRegexp, '');
  153. }
  154. /**
  155. * Get the full path to the source file that is associated with a doclet.
  156. *
  157. * @private
  158. * @param {module:jsdoc/doclet.Doclet} The doclet to check for a filepath.
  159. * @return {string} The path to the doclet's source file, or an empty string if the path is not
  160. * available.
  161. */
  162. function getFilepath(doclet) {
  163. if (!doclet || !doclet.meta || !doclet.meta.filename) {
  164. return '';
  165. }
  166. return path.join(doclet.meta.path || '', doclet.meta.filename);
  167. }
  168. function dooper(source, target, properties) {
  169. properties.forEach(property => {
  170. switch (typeof source[property]) {
  171. case 'function':
  172. // do nothing
  173. break;
  174. case 'object':
  175. target[property] = jsdoc.util.doop(source[property]);
  176. break;
  177. default:
  178. target[property] = source[property];
  179. }
  180. });
  181. }
  182. /**
  183. * Copy all but a list of excluded properties from one of two doclets onto a target doclet. Prefers
  184. * the primary doclet over the secondary doclet.
  185. *
  186. * @private
  187. * @param {module:jsdoc/doclet.Doclet} primary - The primary doclet.
  188. * @param {module:jsdoc/doclet.Doclet} secondary - The secondary doclet.
  189. * @param {module:jsdoc/doclet.Doclet} target - The doclet to which properties will be copied.
  190. * @param {Array.<string>} exclude - The names of properties to exclude from copying.
  191. */
  192. function copyMostProperties(primary, secondary, target, exclude) {
  193. const primaryProperties = _.difference(Object.getOwnPropertyNames(primary), exclude);
  194. const secondaryProperties = _.difference(Object.getOwnPropertyNames(secondary),
  195. exclude.concat(primaryProperties));
  196. dooper(primary, target, primaryProperties);
  197. dooper(secondary, target, secondaryProperties);
  198. }
  199. /**
  200. * Copy specific properties from one of two doclets onto a target doclet, as long as the property
  201. * has a non-falsy value and a length greater than 0. Prefers the primary doclet over the secondary
  202. * doclet.
  203. *
  204. * @private
  205. * @param {module:jsdoc/doclet.Doclet} primary - The primary doclet.
  206. * @param {module:jsdoc/doclet.Doclet} secondary - The secondary doclet.
  207. * @param {module:jsdoc/doclet.Doclet} target - The doclet to which properties will be copied.
  208. * @param {Array.<string>} include - The names of properties to copy.
  209. */
  210. function copySpecificProperties(primary, secondary, target, include) {
  211. include.forEach(property => {
  212. if ({}.hasOwnProperty.call(primary, property) && primary[property] &&
  213. primary[property].length) {
  214. target[property] = jsdoc.util.doop(primary[property]);
  215. }
  216. else if ({}.hasOwnProperty.call(secondary, property) && secondary[property] &&
  217. secondary[property].length) {
  218. target[property] = jsdoc.util.doop(secondary[property]);
  219. }
  220. });
  221. }
  222. /**
  223. * Represents a single JSDoc comment.
  224. *
  225. * @alias module:jsdoc/doclet.Doclet
  226. */
  227. class Doclet {
  228. /**
  229. * Create a doclet.
  230. *
  231. * @param {string} docletSrc - The raw source code of the jsdoc comment.
  232. * @param {object=} meta - Properties describing the code related to this comment.
  233. */
  234. constructor(docletSrc, meta = {}) {
  235. let newTags = [];
  236. /** The original text of the comment from the source code. */
  237. this.comment = docletSrc;
  238. this.setMeta(meta);
  239. docletSrc = unwrap(docletSrc);
  240. docletSrc = fixDescription(docletSrc, meta);
  241. newTags = toTags.call(this, docletSrc);
  242. for (let i = 0, l = newTags.length; i < l; i++) {
  243. this.addTag(newTags[i].title, newTags[i].text);
  244. }
  245. this.postProcess();
  246. }
  247. /** Called once after all tags have been added. */
  248. postProcess() {
  249. let i;
  250. let l;
  251. if (!this.preserveName) {
  252. jsdoc.name.resolve(this);
  253. }
  254. if (this.name && !this.longname) {
  255. this.setLongname(this.name);
  256. }
  257. if (this.memberof === '') {
  258. delete this.memberof;
  259. }
  260. if (!this.kind && this.meta && this.meta.code) {
  261. this.addTag( 'kind', codeToKind(this.meta.code) );
  262. }
  263. if (this.variation && this.longname && !/\)$/.test(this.longname) ) {
  264. this.longname += `(${this.variation})`;
  265. }
  266. // add in any missing param names
  267. if (this.params && this.meta && this.meta.code && this.meta.code.paramnames) {
  268. for (i = 0, l = this.params.length; i < l; i++) {
  269. if (!this.params[i].name) {
  270. this.params[i].name = this.meta.code.paramnames[i] || '';
  271. }
  272. }
  273. }
  274. }
  275. /**
  276. * Add a tag to the doclet.
  277. *
  278. * @param {string} title - The title of the tag being added.
  279. * @param {string} [text] - The text of the tag being added.
  280. */
  281. addTag(title, text) {
  282. const tagDef = jsdoc.tag.dictionary.lookUp(title);
  283. const newTag = new jsdoc.tag.Tag(title, text, this.meta);
  284. if (tagDef && tagDef.onTagged) {
  285. tagDef.onTagged(this, newTag);
  286. }
  287. if (!tagDef) {
  288. this.tags = this.tags || [];
  289. this.tags.push(newTag);
  290. }
  291. applyTag(this, newTag);
  292. }
  293. /**
  294. * Set the doclet's `memberof` property.
  295. *
  296. * @param {string} sid - The longname of the doclet's parent symbol.
  297. */
  298. setMemberof(sid) {
  299. /**
  300. * The longname of the symbol that contains this one, if any.
  301. * @type {string}
  302. */
  303. this.memberof = removeGlobal(sid)
  304. .replace(/\.prototype/g, jsdoc.name.SCOPE.PUNC.INSTANCE);
  305. }
  306. /**
  307. * Set the doclet's `longname` property.
  308. *
  309. * @param {string} name - The longname for the doclet.
  310. */
  311. setLongname(name) {
  312. /**
  313. * The fully resolved symbol name.
  314. * @type {string}
  315. */
  316. this.longname = removeGlobal(name);
  317. if (jsdoc.tag.dictionary.isNamespace(this.kind)) {
  318. this.longname = jsdoc.name.applyNamespace(this.longname, this.kind);
  319. }
  320. }
  321. /**
  322. * Set the doclet's `scope` property. Must correspond to a scope name that is defined in
  323. * {@link module:jsdoc/name.SCOPE.NAMES}.
  324. *
  325. * @param {module:jsdoc/name.SCOPE.NAMES} scope - The scope for the doclet relative to the
  326. * symbol's parent.
  327. * @throws {Error} If the scope name is not recognized.
  328. */
  329. setScope(scope) {
  330. let errorMessage;
  331. let filepath;
  332. const scopeNames = _.values(jsdoc.name.SCOPE.NAMES);
  333. if (!scopeNames.includes(scope)) {
  334. filepath = getFilepath(this);
  335. errorMessage = util.format('The scope name "%s" is not recognized. Use one of the ' +
  336. 'following values: %j', scope, scopeNames);
  337. if (filepath) {
  338. errorMessage += util.format(' (Source file: %s)', filepath);
  339. }
  340. throw new Error(errorMessage);
  341. }
  342. this.scope = scope;
  343. }
  344. /**
  345. * Add a symbol to this doclet's `borrowed` array.
  346. *
  347. * @param {string} source - The longname of the symbol that is the source.
  348. * @param {string} target - The name the symbol is being assigned to.
  349. */
  350. borrow(source, target) {
  351. const about = { from: source };
  352. if (target) {
  353. about.as = target;
  354. }
  355. if (!this.borrowed) {
  356. /**
  357. * A list of symbols that are borrowed by this one, if any.
  358. * @type {Array.<string>}
  359. */
  360. this.borrowed = [];
  361. }
  362. this.borrowed.push(about);
  363. }
  364. mix(source) {
  365. /**
  366. * A list of symbols that are mixed into this one, if any.
  367. * @type Array.<string>
  368. */
  369. this.mixes = this.mixes || [];
  370. this.mixes.push(source);
  371. }
  372. /**
  373. * Add a symbol to the doclet's `augments` array.
  374. *
  375. * @param {string} base - The longname of the base symbol.
  376. */
  377. augment(base) {
  378. /**
  379. * A list of symbols that are augmented by this one, if any.
  380. * @type Array.<string>
  381. */
  382. this.augments = this.augments || [];
  383. this.augments.push(base);
  384. }
  385. /**
  386. * Set the `meta` property of this doclet.
  387. *
  388. * @param {object} meta
  389. */
  390. setMeta(meta) {
  391. let pathname;
  392. /**
  393. * Information about the source code associated with this doclet.
  394. * @namespace
  395. */
  396. this.meta = this.meta || {};
  397. if (meta.range) {
  398. /**
  399. * The positions of the first and last characters of the code associated with this doclet.
  400. * @type Array.<number>
  401. */
  402. this.meta.range = meta.range.slice(0);
  403. }
  404. if (meta.lineno) {
  405. /**
  406. * The name of the file containing the code associated with this doclet.
  407. * @type string
  408. */
  409. this.meta.filename = path.basename(meta.filename);
  410. /**
  411. * The line number of the code associated with this doclet.
  412. * @type number
  413. */
  414. this.meta.lineno = meta.lineno;
  415. /**
  416. * The column number of the code associated with this doclet.
  417. * @type number
  418. */
  419. this.meta.columnno = meta.columnno;
  420. pathname = path.dirname(meta.filename);
  421. if (pathname && pathname !== '.') {
  422. this.meta.path = pathname;
  423. }
  424. }
  425. /**
  426. * Information about the code symbol.
  427. * @namespace
  428. */
  429. this.meta.code = this.meta.code || {};
  430. if (meta.id) { this.meta.code.id = meta.id; }
  431. if (meta.code) {
  432. if (meta.code.name) {
  433. /**
  434. * The name of the symbol in the source code.
  435. * @type {string}
  436. */
  437. this.meta.code.name = meta.code.name;
  438. }
  439. if (meta.code.type) {
  440. /**
  441. * The type of the symbol in the source code.
  442. * @type {string}
  443. */
  444. this.meta.code.type = meta.code.type;
  445. }
  446. if (meta.code.node) {
  447. Object.defineProperty(this.meta.code, 'node', {
  448. value: meta.code.node,
  449. enumerable: false
  450. });
  451. }
  452. if (meta.code.funcscope) {
  453. this.meta.code.funcscope = meta.code.funcscope;
  454. }
  455. if (typeof meta.code.value !== 'undefined') {
  456. /**
  457. * The value of the symbol in the source code.
  458. * @type {*}
  459. */
  460. this.meta.code.value = meta.code.value;
  461. }
  462. if (meta.code.paramnames) {
  463. this.meta.code.paramnames = meta.code.paramnames.slice(0);
  464. }
  465. }
  466. }
  467. }
  468. exports.Doclet = Doclet;
  469. /**
  470. * Combine two doclets into a new doclet.
  471. *
  472. * @param {module:jsdoc/doclet.Doclet} primary - The doclet whose properties will be used.
  473. * @param {module:jsdoc/doclet.Doclet} secondary - The doclet to use as a fallback for properties
  474. * that the primary doclet does not have.
  475. * @returns {module:jsdoc/doclet.Doclet} A new doclet that combines the primary and secondary
  476. * doclets.
  477. */
  478. exports.combine = (primary, secondary) => {
  479. const copyMostPropertiesExclude = [
  480. 'params',
  481. 'properties',
  482. 'undocumented'
  483. ];
  484. const copySpecificPropertiesInclude = [
  485. 'params',
  486. 'properties'
  487. ];
  488. const target = new Doclet('');
  489. // First, copy most properties to the target doclet.
  490. copyMostProperties(primary, secondary, target, copyMostPropertiesExclude);
  491. // Then copy a few specific properties to the target doclet, as long as they're not falsy and
  492. // have a length greater than 0.
  493. copySpecificProperties(primary, secondary, target, copySpecificPropertiesInclude);
  494. return target;
  495. };