123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595 |
- /**
- * A collection of functions relating to JSDoc symbol name manipulation.
- * @module jsdoc/name
- */
- const _ = require('underscore');
- const escape = require('escape-string-regexp');
- const hasOwnProp = Object.prototype.hasOwnProperty;
- /**
- * Longnames that have a special meaning in JSDoc.
- *
- * @enum {string}
- * @static
- * @memberof module:jsdoc/name
- */
- const LONGNAMES = exports.LONGNAMES = {
- /** Longname used for doclets that do not have a longname, such as anonymous functions. */
- ANONYMOUS: '<anonymous>',
- /** Longname that represents global scope. */
- GLOBAL: '<global>'
- };
- // Module namespace prefix.
- const MODULE_NAMESPACE = 'module:';
- /**
- * Names and punctuation marks that identify doclet scopes.
- *
- * @enum {string}
- * @static
- * @memberof module:jsdoc/name
- */
- const SCOPE = exports.SCOPE = {
- NAMES: {
- GLOBAL: 'global',
- INNER: 'inner',
- INSTANCE: 'instance',
- STATIC: 'static'
- },
- PUNC: {
- INNER: '~',
- INSTANCE: '#',
- STATIC: '.'
- }
- };
- // For backwards compatibility, this enum must use lower-case keys
- const scopeToPunc = exports.scopeToPunc = {
- 'inner': SCOPE.PUNC.INNER,
- 'instance': SCOPE.PUNC.INSTANCE,
- 'static': SCOPE.PUNC.STATIC
- };
- const puncToScope = exports.puncToScope = _.invert(scopeToPunc);
- const DEFAULT_SCOPE = SCOPE.NAMES.STATIC;
- const SCOPE_PUNC = _.values(SCOPE.PUNC);
- const SCOPE_PUNC_STRING = `[${SCOPE_PUNC.join()}]`;
- const REGEXP_LEADING_SCOPE = new RegExp(`^(${SCOPE_PUNC_STRING})`);
- const REGEXP_TRAILING_SCOPE = new RegExp(`(${SCOPE_PUNC_STRING})$`);
- const DESCRIPTION = '(?:(?:[ \\t]*\\-\\s*|\\s+)(\\S[\\s\\S]*))?$';
- const REGEXP_DESCRIPTION = new RegExp(DESCRIPTION);
- const REGEXP_NAME_DESCRIPTION = new RegExp(`^(\\[[^\\]]+\\]|\\S+)${DESCRIPTION}`);
- function nameIsLongname(name, memberof) {
- const regexp = new RegExp(`^${escape(memberof)}${SCOPE_PUNC_STRING}`);
- return regexp.test(name);
- }
- function prototypeToPunc(name) {
- // don't mangle symbols named "prototype"
- if (name === 'prototype') {
- return name;
- }
- return name.replace(/(?:^|\.)prototype\.?/g, SCOPE.PUNC.INSTANCE);
- }
- // TODO: docs
- /**
- * @param {string} name - The symbol's longname.
- * @return {string} The symbol's basename.
- */
- exports.getBasename = name => {
- if (name !== undefined) {
- return name.replace(/^([$a-z_][$a-z_0-9]*).*?$/i, '$1');
- }
- return undefined;
- };
- // TODO: deprecate exports.resolve in favor of a better name
- /**
- * Resolves the longname, memberof, variation and name values of the given doclet.
- * @param {module:jsdoc/doclet.Doclet} doclet
- */
- exports.resolve = doclet => {
- let about = {};
- let memberof = doclet.memberof || '';
- let metaName;
- let name = doclet.name ? String(doclet.name) : '';
- let puncAndName;
- let puncAndNameIndex;
- // change MyClass.prototype.instanceMethod to MyClass#instanceMethod
- // (but not in function params, which lack doclet.kind)
- // TODO: check for specific doclet.kind values (probably function, class, and module)
- if (name && doclet.kind) {
- name = prototypeToPunc(name);
- }
- doclet.name = name;
- // does the doclet have an alias that identifies the memberof? if so, use it
- if (doclet.alias) {
- about = exports.shorten(name);
- if (about.memberof) {
- memberof = about.memberof;
- }
- }
- // member of a var in an outer scope?
- else if (name && !memberof && doclet.meta.code && doclet.meta.code.funcscope) {
- name = doclet.longname = doclet.meta.code.funcscope + SCOPE.PUNC.INNER + name;
- }
- if (memberof || doclet.forceMemberof) { // @memberof tag given
- memberof = prototypeToPunc(memberof);
- // the name is a complete longname, like @name foo.bar, @memberof foo
- if (name && nameIsLongname(name, memberof) && name !== memberof) {
- about = exports.shorten(name, (doclet.forceMemberof ? memberof : undefined));
- }
- // the name and memberof are identical and refer to a module,
- // like @name module:foo, @memberof module:foo (probably a member like 'var exports')
- else if (name && name === memberof && name.indexOf(MODULE_NAMESPACE) === 0) {
- about = exports.shorten(name, (doclet.forceMemberof ? memberof : undefined));
- }
- // the name and memberof are identical, like @name foo, @memberof foo
- else if (name && name === memberof) {
- doclet.scope = doclet.scope || DEFAULT_SCOPE;
- name = memberof + scopeToPunc[doclet.scope] + name;
- about = exports.shorten(name, (doclet.forceMemberof ? memberof : undefined));
- }
- // like @memberof foo# or @memberof foo~
- else if (name && REGEXP_TRAILING_SCOPE.test(memberof) ) {
- about = exports.shorten(memberof + name, (doclet.forceMemberof ? memberof : undefined));
- }
- else if (name && doclet.scope) {
- about = exports.shorten(memberof + (scopeToPunc[doclet.scope] || '') + name,
- (doclet.forceMemberof ? memberof : undefined));
- }
- }
- else { // no @memberof
- about = exports.shorten(name);
- }
- if (about.name) {
- doclet.name = about.name;
- }
- if (about.memberof) {
- doclet.setMemberof(about.memberof);
- }
- if (about.longname && (!doclet.longname || doclet.longname === doclet.name)) {
- doclet.setLongname(about.longname);
- }
- if (doclet.scope === SCOPE.NAMES.GLOBAL) { // via @global tag?
- doclet.setLongname(doclet.name);
- delete doclet.memberof;
- }
- else if (about.scope) {
- if (about.memberof === LONGNAMES.GLOBAL) { // via @memberof <global> ?
- doclet.scope = SCOPE.NAMES.GLOBAL;
- }
- else {
- doclet.scope = puncToScope[about.scope];
- }
- }
- else if (doclet.name && doclet.memberof && !doclet.longname) {
- if ( REGEXP_LEADING_SCOPE.test(doclet.name) ) {
- doclet.scope = puncToScope[RegExp.$1];
- doclet.name = doclet.name.substr(1);
- }
- else if (doclet.meta.code && doclet.meta.code.name) {
- // HACK: Handle cases where an ES 2015 class is a static memberof something else, and
- // the class has instance members. In these cases, we have to detect the instance
- // members' scope by looking at the meta info. There's almost certainly a better way to
- // do this...
- metaName = String(doclet.meta.code.name);
- puncAndName = SCOPE.PUNC.INSTANCE + doclet.name;
- puncAndNameIndex = metaName.indexOf(puncAndName);
- if ( puncAndNameIndex !== -1 &&
- (puncAndNameIndex === metaName.length - puncAndName.length) ) {
- doclet.scope = SCOPE.NAMES.INSTANCE;
- }
- }
- doclet.scope = doclet.scope || DEFAULT_SCOPE;
- doclet.setLongname(doclet.memberof + scopeToPunc[doclet.scope] + doclet.name);
- }
- if (about.variation) {
- doclet.variation = about.variation;
- }
- // if we never found a longname, just use an empty string
- if (!doclet.longname) {
- doclet.longname = '';
- }
- };
- /**
- * @param {string} longname The full longname of the symbol.
- * @param {string} ns The namespace to be applied.
- * @returns {string} The longname with the namespace applied.
- */
- exports.applyNamespace = (longname, ns) => {
- const nameParts = exports.shorten(longname);
- const name = nameParts.name;
- longname = nameParts.longname;
- if ( !/^[a-zA-Z]+?:.+$/i.test(name) ) {
- longname = longname.replace( new RegExp(`${escape(name)}$`), `${ns}:${name}` );
- }
- return longname;
- };
- // TODO: docs
- exports.stripNamespace = longname => longname.replace(/^[a-zA-Z]+:/, '');
- /**
- * Check whether a parent longname is an ancestor of a child longname.
- *
- * @param {string} parent - The parent longname.
- * @param {string} child - The child longname.
- * @return {boolean} `true` if the parent is an ancestor of the child; otherwise, `false`.
- */
- exports.hasAncestor = (parent, child) => {
- let hasAncestor = false;
- let memberof = child;
- if (!parent || !child) {
- return hasAncestor;
- }
- // fast path for obvious non-ancestors
- if (child.indexOf(parent) !== 0) {
- return hasAncestor;
- }
- do {
- memberof = exports.shorten(memberof).memberof;
- if (memberof === parent) {
- hasAncestor = true;
- }
- } while (!hasAncestor && memberof);
- return hasAncestor;
- };
- // TODO: docs
- function atomize(longname, sliceChars, forcedMemberof) {
- let i;
- let memberof = '';
- let name = '';
- let parts;
- let partsRegExp;
- let scopePunc = '';
- let token;
- const tokens = [];
- let variation;
- // quoted strings in a longname are atomic, so we convert them to tokens:
- // foo["bar"] => foo.@{1}@
- // Foo.prototype["bar"] => Foo#@{1}
- longname = longname.replace(/(prototype|#)?(\[?["'].+?["']\]?)/g, ($, p1, p2) => {
- let punc = '';
- // is there a leading bracket?
- if ( /^\[/.test(p2) ) {
- // is it a static or instance member?
- punc = p1 ? SCOPE.PUNC.INSTANCE : SCOPE.PUNC.STATIC;
- p2 = p2.replace(/^\[/g, '')
- .replace(/\]$/g, '');
- }
- token = `@{${tokens.length}}@`;
- tokens.push(p2);
- return punc + token;
- });
- longname = prototypeToPunc(longname);
- if (typeof forcedMemberof !== 'undefined') {
- partsRegExp = new RegExp(`^(.*?)([${sliceChars.join()}]?)$`);
- name = longname.substr(forcedMemberof.length);
- parts = forcedMemberof.match(partsRegExp);
- if (parts[1]) {
- memberof = parts[1] || forcedMemberof;
- }
- if (parts[2]) {
- scopePunc = parts[2];
- }
- }
- else if (longname) {
- parts = (longname.match(new RegExp(`^(:?(.+)([${sliceChars.join()}]))?(.+?)$`)) || [])
- .reverse();
- name = parts[0] || '';
- scopePunc = parts[1] || '';
- memberof = parts[2] || '';
- }
- // like /** @name foo.bar(2) */
- if ( /(.+)\(([^)]+)\)$/.test(name) ) {
- name = RegExp.$1;
- variation = RegExp.$2;
- }
- // restore quoted strings
- i = tokens.length;
- while (i--) {
- longname = longname.replace(`@{${i}}@`, tokens[i]);
- memberof = memberof.replace(`@{${i}}@`, tokens[i]);
- scopePunc = scopePunc.replace(`@{${i}}@`, tokens[i]);
- name = name.replace(`@{${i}}@`, tokens[i]);
- }
- return {
- longname: longname,
- memberof: memberof,
- scope: scopePunc,
- name: name,
- variation: variation
- };
- }
- // TODO: deprecate exports.shorten in favor of a better name
- /**
- * Given a longname like "a.b#c(2)", slice it up into an object containing the memberof, the scope,
- * the name, and variation.
- * @param {string} longname
- * @param {string} forcedMemberof
- * @returns {object} Representing the properties of the given name.
- */
- exports.shorten = (longname, forcedMemberof) => atomize(longname, SCOPE_PUNC, forcedMemberof);
- // TODO: docs
- exports.combine = ({memberof, scope, name, variation}) => [
- (memberof || ''),
- (scope || ''),
- (name || ''),
- (variation || '')
- ].join('');
- // TODO: docs
- exports.stripVariation = name => {
- const parts = exports.shorten(name);
- parts.variation = '';
- return exports.combine(parts);
- };
- function splitLongname(longname, options) {
- const chunks = [];
- let currentNameInfo;
- const nameInfo = {};
- let previousName = longname;
- const splitters = SCOPE_PUNC.concat('/');
- options = _.defaults(options || {}, {
- includeVariation: true
- });
- do {
- if (!options.includeVariation) {
- previousName = exports.stripVariation(previousName);
- }
- currentNameInfo = nameInfo[previousName] = atomize(previousName, splitters);
- previousName = currentNameInfo.memberof;
- chunks.push(currentNameInfo.scope + currentNameInfo.name);
- } while (previousName);
- return {
- chunks: chunks.reverse(),
- nameInfo: nameInfo
- };
- }
- /**
- * Convert an array of doclet longnames into a tree structure, optionally attaching doclets to the
- * tree.
- *
- * Each level of the tree is an object with the following properties:
- *
- * + `longname {string}`: The longname.
- * + `memberof {string?}`: The memberof.
- * + `scope {string?}`: The longname's scope, represented as a punctuation mark (for example, `#`
- * for instance and `.` for static).
- * + `name {string}`: The short name.
- * + `doclet {Object?}`: The doclet associated with the longname, or `null` if the doclet was not
- * provided.
- * + `children {Object?}`: The children of the current longname. Not present if there are no
- * children.
- *
- * For example, suppose you have the following array of doclet longnames:
- *
- * ```js
- * [
- * "module:a",
- * "module:a/b",
- * "myNamespace",
- * "myNamespace.Foo",
- * "myNamespace.Foo#bar"
- * ]
- * ```
- *
- * This method converts these longnames to the following tree:
- *
- * ```js
- * {
- * "module:a": {
- * "longname": "module:a",
- * "memberof": "",
- * "scope": "",
- * "name": "module:a",
- * "doclet": null,
- * "children": {
- * "/b": {
- * "longname": "module:a/b",
- * "memberof": "module:a",
- * "scope": "/",
- * "name": "b",
- * "doclet": null
- * }
- * }
- * },
- * "myNamespace": {
- * "longname": "myNamespace",
- * "memberof": "",
- * "scope": "",
- * "name": "myNamespace",
- * "doclet": null,
- * "children": {
- * ".Foo": {
- * "longname": "myNamespace.Foo",
- * "memberof": "myNamespace",
- * "scope": ".",
- * "name": "Foo",
- * "doclet": null,
- * "children": {
- * "#bar": {
- * "longname": "myNamespace.Foo#bar",
- * "memberof": "myNamespace.Foo",
- * "scope": "#",
- * "name": "bar",
- * "doclet": null
- * }
- * }
- * }
- * }
- * }
- * }
- * ```
- *
- * @param {Array<string>} longnames - The longnames to convert into a tree.
- * @param {Object<string, module:jsdoc/doclet.Doclet>} doclets - The doclets to attach to a tree.
- * Each property should be the longname of a doclet, and each value should be the doclet for that
- * longname.
- * @return {Object} A tree with information about each longname in the format shown above.
- */
- exports.longnamesToTree = (longnames, doclets) => {
- const splitOptions = { includeVariation: false };
- const tree = {};
- longnames.forEach(longname => {
- let currentLongname = '';
- let currentParent = tree;
- let nameInfo;
- let processed;
- // don't try to add empty longnames to the tree
- if (!longname) {
- return;
- }
- processed = splitLongname(longname, splitOptions);
- nameInfo = processed.nameInfo;
- processed.chunks.forEach(chunk => {
- currentLongname += chunk;
- if (currentParent !== tree) {
- currentParent.children = currentParent.children || {};
- currentParent = currentParent.children;
- }
- if (!hasOwnProp.call(currentParent, chunk)) {
- currentParent[chunk] = nameInfo[currentLongname];
- }
- if (currentParent[chunk]) {
- currentParent[chunk].doclet = doclets ? doclets[currentLongname] : null;
- currentParent = currentParent[chunk];
- }
- });
- });
- return tree;
- };
- /**
- * Split a string that starts with a name and ends with a description into its parts. Allows the
- * defaultvalue (if present) to contain brackets. If the name is found to have mismatched brackets,
- * null is returned.
- * @param {string} nameDesc
- * @returns {object} Hash with "name" and "description" properties.
- */
- function splitNameMatchingBrackets(nameDesc) {
- const buffer = [];
- let c;
- let stack = 0;
- let stringEnd = null;
- for (var i = 0; i < nameDesc.length; ++i) {
- c = nameDesc[i];
- buffer.push(c);
- if (stringEnd) {
- if (c === '\\' && i + 1 < nameDesc.length) {
- buffer.push(nameDesc[++i]);
- } else if (c === stringEnd) {
- stringEnd = null;
- }
- } else if (c === '"' || c === "'") {
- stringEnd = c;
- } else if (c === '[') {
- ++stack;
- } else if (c === ']') {
- if (--stack === 0) {
- break;
- }
- }
- }
- if (stack || stringEnd) {
- return null;
- }
- nameDesc.substr(i).match(REGEXP_DESCRIPTION);
- return {
- name: buffer.join(''),
- description: RegExp.$1
- };
- }
- // TODO: deprecate exports.splitName in favor of a better name
- /**
- * Split a string that starts with a name and ends with a description into its parts.
- * @param {string} nameDesc
- * @returns {object} Hash with "name" and "description" properties.
- */
- exports.splitName = nameDesc => {
- // like: name, [name], name text, [name] text, name - text, or [name] - text
- // the hyphen must be on the same line as the name; this prevents us from treating a Markdown
- // dash as a separator
- // optional values get special treatment
- let result = null;
- if (nameDesc[0] === '[') {
- result = splitNameMatchingBrackets(nameDesc);
- if (result !== null) {
- return result;
- }
- }
- nameDesc.match(REGEXP_NAME_DESCRIPTION);
- return {
- name: RegExp.$1,
- description: RegExp.$2
- };
- };
|