name.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  1. /**
  2. * A collection of functions relating to JSDoc symbol name manipulation.
  3. * @module jsdoc/name
  4. */
  5. const _ = require('underscore');
  6. const escape = require('escape-string-regexp');
  7. const hasOwnProp = Object.prototype.hasOwnProperty;
  8. /**
  9. * Longnames that have a special meaning in JSDoc.
  10. *
  11. * @enum {string}
  12. * @static
  13. * @memberof module:jsdoc/name
  14. */
  15. const LONGNAMES = exports.LONGNAMES = {
  16. /** Longname used for doclets that do not have a longname, such as anonymous functions. */
  17. ANONYMOUS: '<anonymous>',
  18. /** Longname that represents global scope. */
  19. GLOBAL: '<global>'
  20. };
  21. // Module namespace prefix.
  22. const MODULE_NAMESPACE = 'module:';
  23. /**
  24. * Names and punctuation marks that identify doclet scopes.
  25. *
  26. * @enum {string}
  27. * @static
  28. * @memberof module:jsdoc/name
  29. */
  30. const SCOPE = exports.SCOPE = {
  31. NAMES: {
  32. GLOBAL: 'global',
  33. INNER: 'inner',
  34. INSTANCE: 'instance',
  35. STATIC: 'static'
  36. },
  37. PUNC: {
  38. INNER: '~',
  39. INSTANCE: '#',
  40. STATIC: '.'
  41. }
  42. };
  43. // For backwards compatibility, this enum must use lower-case keys
  44. const scopeToPunc = exports.scopeToPunc = {
  45. 'inner': SCOPE.PUNC.INNER,
  46. 'instance': SCOPE.PUNC.INSTANCE,
  47. 'static': SCOPE.PUNC.STATIC
  48. };
  49. const puncToScope = exports.puncToScope = _.invert(scopeToPunc);
  50. const DEFAULT_SCOPE = SCOPE.NAMES.STATIC;
  51. const SCOPE_PUNC = _.values(SCOPE.PUNC);
  52. const SCOPE_PUNC_STRING = `[${SCOPE_PUNC.join()}]`;
  53. const REGEXP_LEADING_SCOPE = new RegExp(`^(${SCOPE_PUNC_STRING})`);
  54. const REGEXP_TRAILING_SCOPE = new RegExp(`(${SCOPE_PUNC_STRING})$`);
  55. const DESCRIPTION = '(?:(?:[ \\t]*\\-\\s*|\\s+)(\\S[\\s\\S]*))?$';
  56. const REGEXP_DESCRIPTION = new RegExp(DESCRIPTION);
  57. const REGEXP_NAME_DESCRIPTION = new RegExp(`^(\\[[^\\]]+\\]|\\S+)${DESCRIPTION}`);
  58. function nameIsLongname(name, memberof) {
  59. const regexp = new RegExp(`^${escape(memberof)}${SCOPE_PUNC_STRING}`);
  60. return regexp.test(name);
  61. }
  62. function prototypeToPunc(name) {
  63. // don't mangle symbols named "prototype"
  64. if (name === 'prototype') {
  65. return name;
  66. }
  67. return name.replace(/(?:^|\.)prototype\.?/g, SCOPE.PUNC.INSTANCE);
  68. }
  69. // TODO: docs
  70. /**
  71. * @param {string} name - The symbol's longname.
  72. * @return {string} The symbol's basename.
  73. */
  74. exports.getBasename = name => {
  75. if (name !== undefined) {
  76. return name.replace(/^([$a-z_][$a-z_0-9]*).*?$/i, '$1');
  77. }
  78. return undefined;
  79. };
  80. // TODO: deprecate exports.resolve in favor of a better name
  81. /**
  82. * Resolves the longname, memberof, variation and name values of the given doclet.
  83. * @param {module:jsdoc/doclet.Doclet} doclet
  84. */
  85. exports.resolve = doclet => {
  86. let about = {};
  87. let memberof = doclet.memberof || '';
  88. let metaName;
  89. let name = doclet.name ? String(doclet.name) : '';
  90. let puncAndName;
  91. let puncAndNameIndex;
  92. // change MyClass.prototype.instanceMethod to MyClass#instanceMethod
  93. // (but not in function params, which lack doclet.kind)
  94. // TODO: check for specific doclet.kind values (probably function, class, and module)
  95. if (name && doclet.kind) {
  96. name = prototypeToPunc(name);
  97. }
  98. doclet.name = name;
  99. // does the doclet have an alias that identifies the memberof? if so, use it
  100. if (doclet.alias) {
  101. about = exports.shorten(name);
  102. if (about.memberof) {
  103. memberof = about.memberof;
  104. }
  105. }
  106. // member of a var in an outer scope?
  107. else if (name && !memberof && doclet.meta.code && doclet.meta.code.funcscope) {
  108. name = doclet.longname = doclet.meta.code.funcscope + SCOPE.PUNC.INNER + name;
  109. }
  110. if (memberof || doclet.forceMemberof) { // @memberof tag given
  111. memberof = prototypeToPunc(memberof);
  112. // the name is a complete longname, like @name foo.bar, @memberof foo
  113. if (name && nameIsLongname(name, memberof) && name !== memberof) {
  114. about = exports.shorten(name, (doclet.forceMemberof ? memberof : undefined));
  115. }
  116. // the name and memberof are identical and refer to a module,
  117. // like @name module:foo, @memberof module:foo (probably a member like 'var exports')
  118. else if (name && name === memberof && name.indexOf(MODULE_NAMESPACE) === 0) {
  119. about = exports.shorten(name, (doclet.forceMemberof ? memberof : undefined));
  120. }
  121. // the name and memberof are identical, like @name foo, @memberof foo
  122. else if (name && name === memberof) {
  123. doclet.scope = doclet.scope || DEFAULT_SCOPE;
  124. name = memberof + scopeToPunc[doclet.scope] + name;
  125. about = exports.shorten(name, (doclet.forceMemberof ? memberof : undefined));
  126. }
  127. // like @memberof foo# or @memberof foo~
  128. else if (name && REGEXP_TRAILING_SCOPE.test(memberof) ) {
  129. about = exports.shorten(memberof + name, (doclet.forceMemberof ? memberof : undefined));
  130. }
  131. else if (name && doclet.scope) {
  132. about = exports.shorten(memberof + (scopeToPunc[doclet.scope] || '') + name,
  133. (doclet.forceMemberof ? memberof : undefined));
  134. }
  135. }
  136. else { // no @memberof
  137. about = exports.shorten(name);
  138. }
  139. if (about.name) {
  140. doclet.name = about.name;
  141. }
  142. if (about.memberof) {
  143. doclet.setMemberof(about.memberof);
  144. }
  145. if (about.longname && (!doclet.longname || doclet.longname === doclet.name)) {
  146. doclet.setLongname(about.longname);
  147. }
  148. if (doclet.scope === SCOPE.NAMES.GLOBAL) { // via @global tag?
  149. doclet.setLongname(doclet.name);
  150. delete doclet.memberof;
  151. }
  152. else if (about.scope) {
  153. if (about.memberof === LONGNAMES.GLOBAL) { // via @memberof <global> ?
  154. doclet.scope = SCOPE.NAMES.GLOBAL;
  155. }
  156. else {
  157. doclet.scope = puncToScope[about.scope];
  158. }
  159. }
  160. else if (doclet.name && doclet.memberof && !doclet.longname) {
  161. if ( REGEXP_LEADING_SCOPE.test(doclet.name) ) {
  162. doclet.scope = puncToScope[RegExp.$1];
  163. doclet.name = doclet.name.substr(1);
  164. }
  165. else if (doclet.meta.code && doclet.meta.code.name) {
  166. // HACK: Handle cases where an ES 2015 class is a static memberof something else, and
  167. // the class has instance members. In these cases, we have to detect the instance
  168. // members' scope by looking at the meta info. There's almost certainly a better way to
  169. // do this...
  170. metaName = String(doclet.meta.code.name);
  171. puncAndName = SCOPE.PUNC.INSTANCE + doclet.name;
  172. puncAndNameIndex = metaName.indexOf(puncAndName);
  173. if ( puncAndNameIndex !== -1 &&
  174. (puncAndNameIndex === metaName.length - puncAndName.length) ) {
  175. doclet.scope = SCOPE.NAMES.INSTANCE;
  176. }
  177. }
  178. doclet.scope = doclet.scope || DEFAULT_SCOPE;
  179. doclet.setLongname(doclet.memberof + scopeToPunc[doclet.scope] + doclet.name);
  180. }
  181. if (about.variation) {
  182. doclet.variation = about.variation;
  183. }
  184. // if we never found a longname, just use an empty string
  185. if (!doclet.longname) {
  186. doclet.longname = '';
  187. }
  188. };
  189. /**
  190. * @param {string} longname The full longname of the symbol.
  191. * @param {string} ns The namespace to be applied.
  192. * @returns {string} The longname with the namespace applied.
  193. */
  194. exports.applyNamespace = (longname, ns) => {
  195. const nameParts = exports.shorten(longname);
  196. const name = nameParts.name;
  197. longname = nameParts.longname;
  198. if ( !/^[a-zA-Z]+?:.+$/i.test(name) ) {
  199. longname = longname.replace( new RegExp(`${escape(name)}$`), `${ns}:${name}` );
  200. }
  201. return longname;
  202. };
  203. // TODO: docs
  204. exports.stripNamespace = longname => longname.replace(/^[a-zA-Z]+:/, '');
  205. /**
  206. * Check whether a parent longname is an ancestor of a child longname.
  207. *
  208. * @param {string} parent - The parent longname.
  209. * @param {string} child - The child longname.
  210. * @return {boolean} `true` if the parent is an ancestor of the child; otherwise, `false`.
  211. */
  212. exports.hasAncestor = (parent, child) => {
  213. let hasAncestor = false;
  214. let memberof = child;
  215. if (!parent || !child) {
  216. return hasAncestor;
  217. }
  218. // fast path for obvious non-ancestors
  219. if (child.indexOf(parent) !== 0) {
  220. return hasAncestor;
  221. }
  222. do {
  223. memberof = exports.shorten(memberof).memberof;
  224. if (memberof === parent) {
  225. hasAncestor = true;
  226. }
  227. } while (!hasAncestor && memberof);
  228. return hasAncestor;
  229. };
  230. // TODO: docs
  231. function atomize(longname, sliceChars, forcedMemberof) {
  232. let i;
  233. let memberof = '';
  234. let name = '';
  235. let parts;
  236. let partsRegExp;
  237. let scopePunc = '';
  238. let token;
  239. const tokens = [];
  240. let variation;
  241. // quoted strings in a longname are atomic, so we convert them to tokens:
  242. // foo["bar"] => foo.@{1}@
  243. // Foo.prototype["bar"] => Foo#@{1}
  244. longname = longname.replace(/(prototype|#)?(\[?["'].+?["']\]?)/g, ($, p1, p2) => {
  245. let punc = '';
  246. // is there a leading bracket?
  247. if ( /^\[/.test(p2) ) {
  248. // is it a static or instance member?
  249. punc = p1 ? SCOPE.PUNC.INSTANCE : SCOPE.PUNC.STATIC;
  250. p2 = p2.replace(/^\[/g, '')
  251. .replace(/\]$/g, '');
  252. }
  253. token = `@{${tokens.length}}@`;
  254. tokens.push(p2);
  255. return punc + token;
  256. });
  257. longname = prototypeToPunc(longname);
  258. if (typeof forcedMemberof !== 'undefined') {
  259. partsRegExp = new RegExp(`^(.*?)([${sliceChars.join()}]?)$`);
  260. name = longname.substr(forcedMemberof.length);
  261. parts = forcedMemberof.match(partsRegExp);
  262. if (parts[1]) {
  263. memberof = parts[1] || forcedMemberof;
  264. }
  265. if (parts[2]) {
  266. scopePunc = parts[2];
  267. }
  268. }
  269. else if (longname) {
  270. parts = (longname.match(new RegExp(`^(:?(.+)([${sliceChars.join()}]))?(.+?)$`)) || [])
  271. .reverse();
  272. name = parts[0] || '';
  273. scopePunc = parts[1] || '';
  274. memberof = parts[2] || '';
  275. }
  276. // like /** @name foo.bar(2) */
  277. if ( /(.+)\(([^)]+)\)$/.test(name) ) {
  278. name = RegExp.$1;
  279. variation = RegExp.$2;
  280. }
  281. // restore quoted strings
  282. i = tokens.length;
  283. while (i--) {
  284. longname = longname.replace(`@{${i}}@`, tokens[i]);
  285. memberof = memberof.replace(`@{${i}}@`, tokens[i]);
  286. scopePunc = scopePunc.replace(`@{${i}}@`, tokens[i]);
  287. name = name.replace(`@{${i}}@`, tokens[i]);
  288. }
  289. return {
  290. longname: longname,
  291. memberof: memberof,
  292. scope: scopePunc,
  293. name: name,
  294. variation: variation
  295. };
  296. }
  297. // TODO: deprecate exports.shorten in favor of a better name
  298. /**
  299. * Given a longname like "a.b#c(2)", slice it up into an object containing the memberof, the scope,
  300. * the name, and variation.
  301. * @param {string} longname
  302. * @param {string} forcedMemberof
  303. * @returns {object} Representing the properties of the given name.
  304. */
  305. exports.shorten = (longname, forcedMemberof) => atomize(longname, SCOPE_PUNC, forcedMemberof);
  306. // TODO: docs
  307. exports.combine = ({memberof, scope, name, variation}) => [
  308. (memberof || ''),
  309. (scope || ''),
  310. (name || ''),
  311. (variation || '')
  312. ].join('');
  313. // TODO: docs
  314. exports.stripVariation = name => {
  315. const parts = exports.shorten(name);
  316. parts.variation = '';
  317. return exports.combine(parts);
  318. };
  319. function splitLongname(longname, options) {
  320. const chunks = [];
  321. let currentNameInfo;
  322. const nameInfo = {};
  323. let previousName = longname;
  324. const splitters = SCOPE_PUNC.concat('/');
  325. options = _.defaults(options || {}, {
  326. includeVariation: true
  327. });
  328. do {
  329. if (!options.includeVariation) {
  330. previousName = exports.stripVariation(previousName);
  331. }
  332. currentNameInfo = nameInfo[previousName] = atomize(previousName, splitters);
  333. previousName = currentNameInfo.memberof;
  334. chunks.push(currentNameInfo.scope + currentNameInfo.name);
  335. } while (previousName);
  336. return {
  337. chunks: chunks.reverse(),
  338. nameInfo: nameInfo
  339. };
  340. }
  341. /**
  342. * Convert an array of doclet longnames into a tree structure, optionally attaching doclets to the
  343. * tree.
  344. *
  345. * Each level of the tree is an object with the following properties:
  346. *
  347. * + `longname {string}`: The longname.
  348. * + `memberof {string?}`: The memberof.
  349. * + `scope {string?}`: The longname's scope, represented as a punctuation mark (for example, `#`
  350. * for instance and `.` for static).
  351. * + `name {string}`: The short name.
  352. * + `doclet {Object?}`: The doclet associated with the longname, or `null` if the doclet was not
  353. * provided.
  354. * + `children {Object?}`: The children of the current longname. Not present if there are no
  355. * children.
  356. *
  357. * For example, suppose you have the following array of doclet longnames:
  358. *
  359. * ```js
  360. * [
  361. * "module:a",
  362. * "module:a/b",
  363. * "myNamespace",
  364. * "myNamespace.Foo",
  365. * "myNamespace.Foo#bar"
  366. * ]
  367. * ```
  368. *
  369. * This method converts these longnames to the following tree:
  370. *
  371. * ```js
  372. * {
  373. * "module:a": {
  374. * "longname": "module:a",
  375. * "memberof": "",
  376. * "scope": "",
  377. * "name": "module:a",
  378. * "doclet": null,
  379. * "children": {
  380. * "/b": {
  381. * "longname": "module:a/b",
  382. * "memberof": "module:a",
  383. * "scope": "/",
  384. * "name": "b",
  385. * "doclet": null
  386. * }
  387. * }
  388. * },
  389. * "myNamespace": {
  390. * "longname": "myNamespace",
  391. * "memberof": "",
  392. * "scope": "",
  393. * "name": "myNamespace",
  394. * "doclet": null,
  395. * "children": {
  396. * ".Foo": {
  397. * "longname": "myNamespace.Foo",
  398. * "memberof": "myNamespace",
  399. * "scope": ".",
  400. * "name": "Foo",
  401. * "doclet": null,
  402. * "children": {
  403. * "#bar": {
  404. * "longname": "myNamespace.Foo#bar",
  405. * "memberof": "myNamespace.Foo",
  406. * "scope": "#",
  407. * "name": "bar",
  408. * "doclet": null
  409. * }
  410. * }
  411. * }
  412. * }
  413. * }
  414. * }
  415. * ```
  416. *
  417. * @param {Array<string>} longnames - The longnames to convert into a tree.
  418. * @param {Object<string, module:jsdoc/doclet.Doclet>} doclets - The doclets to attach to a tree.
  419. * Each property should be the longname of a doclet, and each value should be the doclet for that
  420. * longname.
  421. * @return {Object} A tree with information about each longname in the format shown above.
  422. */
  423. exports.longnamesToTree = (longnames, doclets) => {
  424. const splitOptions = { includeVariation: false };
  425. const tree = {};
  426. longnames.forEach(longname => {
  427. let currentLongname = '';
  428. let currentParent = tree;
  429. let nameInfo;
  430. let processed;
  431. // don't try to add empty longnames to the tree
  432. if (!longname) {
  433. return;
  434. }
  435. processed = splitLongname(longname, splitOptions);
  436. nameInfo = processed.nameInfo;
  437. processed.chunks.forEach(chunk => {
  438. currentLongname += chunk;
  439. if (currentParent !== tree) {
  440. currentParent.children = currentParent.children || {};
  441. currentParent = currentParent.children;
  442. }
  443. if (!hasOwnProp.call(currentParent, chunk)) {
  444. currentParent[chunk] = nameInfo[currentLongname];
  445. }
  446. if (currentParent[chunk]) {
  447. currentParent[chunk].doclet = doclets ? doclets[currentLongname] : null;
  448. currentParent = currentParent[chunk];
  449. }
  450. });
  451. });
  452. return tree;
  453. };
  454. /**
  455. * Split a string that starts with a name and ends with a description into its parts. Allows the
  456. * defaultvalue (if present) to contain brackets. If the name is found to have mismatched brackets,
  457. * null is returned.
  458. * @param {string} nameDesc
  459. * @returns {object} Hash with "name" and "description" properties.
  460. */
  461. function splitNameMatchingBrackets(nameDesc) {
  462. const buffer = [];
  463. let c;
  464. let stack = 0;
  465. let stringEnd = null;
  466. for (var i = 0; i < nameDesc.length; ++i) {
  467. c = nameDesc[i];
  468. buffer.push(c);
  469. if (stringEnd) {
  470. if (c === '\\' && i + 1 < nameDesc.length) {
  471. buffer.push(nameDesc[++i]);
  472. } else if (c === stringEnd) {
  473. stringEnd = null;
  474. }
  475. } else if (c === '"' || c === "'") {
  476. stringEnd = c;
  477. } else if (c === '[') {
  478. ++stack;
  479. } else if (c === ']') {
  480. if (--stack === 0) {
  481. break;
  482. }
  483. }
  484. }
  485. if (stack || stringEnd) {
  486. return null;
  487. }
  488. nameDesc.substr(i).match(REGEXP_DESCRIPTION);
  489. return {
  490. name: buffer.join(''),
  491. description: RegExp.$1
  492. };
  493. }
  494. // TODO: deprecate exports.splitName in favor of a better name
  495. /**
  496. * Split a string that starts with a name and ends with a description into its parts.
  497. * @param {string} nameDesc
  498. * @returns {object} Hash with "name" and "description" properties.
  499. */
  500. exports.splitName = nameDesc => {
  501. // like: name, [name], name text, [name] text, name - text, or [name] - text
  502. // the hyphen must be on the same line as the name; this prevents us from treating a Markdown
  503. // dash as a separator
  504. // optional values get special treatment
  505. let result = null;
  506. if (nameDesc[0] === '[') {
  507. result = splitNameMatchingBrackets(nameDesc);
  508. if (result !== null) {
  509. return result;
  510. }
  511. }
  512. nameDesc.match(REGEXP_NAME_DESCRIPTION);
  513. return {
  514. name: RegExp.$1,
  515. description: RegExp.$2
  516. };
  517. };