/* Copyright 2010 the JSDoc Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ import { Syntax } from '@jsdoc/ast'; import { name } from '@jsdoc/core'; import { Doclet } from '@jsdoc/doclet'; import { log } from '@jsdoc/util'; import escape from 'escape-string-regexp'; const { SCOPE } = name; let currentModule = null; class CurrentModule { constructor(doclet) { this.doclet = doclet; this.longname = doclet.longname; this.originalName = doclet.meta.code.name || ''; } } function filterByLongname({ longname }) { // you can't document prototypes if (/#$/.test(longname)) { return true; } return false; } function createDoclet(comment, e, deps) { let doclet; let flatComment; let msg; try { doclet = new Doclet(comment, e, deps); } catch (error) { flatComment = comment.replace(/[\r\n]/g, ''); msg = `cannot create a doclet for the comment "${flatComment}": ${error.message}`; log.error(msg); doclet = new Doclet('', e, deps); } return doclet; } /** * Create a doclet for a `symbolFound` event. The doclet represents an actual symbol that is defined * in the code. * * Here's why this function is useful. A JSDoc comment can define a symbol name by including: * * + A `@name` tag * + Another tag that accepts a name, such as `@function` * * When the JSDoc comment defines a symbol name, we treat it as a "virtual comment" for a symbol * that isn't actually present in the code. And if a virtual comment is attached to a symbol, it's * possible that the comment and symbol have nothing to do with one another. * * To handle this case, this function checks the new doclet to see if we've already added a name * property by parsing the JSDoc comment. If so, this method creates a replacement doclet that * ignores the attached JSDoc comment and only looks at the code. * * @private */ function createSymbolDoclet(comment, e, deps) { let doclet = createDoclet(comment, e, deps); if (doclet.name) { // try again, without the comment e.comment = '@undocumented'; doclet = createDoclet(e.comment, e, deps); } return doclet; } function setCurrentModule(doclet) { if (doclet.kind === 'module') { currentModule = new CurrentModule(doclet); } } function setModuleScopeMemberOf(parser, doclet) { let parentDoclet; let skipMemberof; // handle module symbols that are _not_ assigned to module.exports if (currentModule && currentModule.longname !== doclet.name) { if (!doclet.scope) { // is this a method definition? if so, we usually get the scope from the node directly if (doclet.meta?.code?.node?.type === Syntax.MethodDefinition) { // special case for constructors of classes that have @alias tags if (doclet.meta.code.node.kind === 'constructor') { parentDoclet = parser._getDocletById(doclet.meta.code.node.parent.parent.nodeId); if (parentDoclet?.alias) { // the constructor should use the same name as the class doclet.addTag('alias', parentDoclet.alias); doclet.addTag('name', parentDoclet.alias); // and we shouldn't try to set a memberof value skipMemberof = true; } } else if (doclet.meta.code.node.static) { doclet.addTag('static'); } else { doclet.addTag('instance'); } } // is this something that the module exports? if so, it's a static member else if (doclet.meta?.code?.node?.parent?.type === Syntax.ExportNamedDeclaration) { doclet.addTag('static'); } // otherwise, it must be an inner member else { doclet.addTag('inner'); } } // if the doclet isn't a memberof anything yet, and it's not a global, it must be a memberof // the current module (unless we were told to skip adding memberof) if (!doclet.memberof && doclet.scope !== SCOPE.NAMES.GLOBAL && !skipMemberof) { doclet.addTag('memberof', currentModule.longname); } } } function setDefaultScope(doclet) { // module doclets don't get a default scope if (!doclet.scope && doclet.kind !== 'module') { doclet.setScope(SCOPE.NAMES.GLOBAL); } } function addDoclet(parser, newDoclet) { let e; if (newDoclet) { setCurrentModule(newDoclet); e = { doclet: newDoclet }; parser.emit('newDoclet', e); if (!e.defaultPrevented && !filterByLongname(e.doclet)) { parser.addResult(e.doclet); } } } function processAlias(parser, doclet, astNode) { let memberofName; if (doclet.alias === '{@thisClass}') { memberofName = parser.resolveThis(astNode); // "class" refers to the owner of the prototype, not the prototype itself if (/^(.+?)(\.prototype|#)$/.test(memberofName)) { memberofName = RegExp.$1; } doclet.alias = memberofName; } doclet.addTag('name', doclet.alias); doclet.postProcess(); } // TODO: separate code that resolves `this` from code that resolves the module object function findSymbolMemberof(parser, doclet, astNode, nameStartsWith, trailingPunc) { let memberof = ''; let nameAndPunc; let scopePunc = ''; // handle computed properties like foo['bar'] if (trailingPunc === '[') { // we don't know yet whether the symbol is a static or instance member trailingPunc = null; } nameAndPunc = nameStartsWith + (trailingPunc || ''); // remove stuff that indicates module membership (but don't touch the name `module.exports`, // which identifies the module object itself) if (doclet.name !== 'module.exports') { doclet.name = doclet.name.replace(nameAndPunc, ''); } // like `bar` in: // exports.bar = 1; // module.exports.bar = 1; // module.exports = MyModuleObject; MyModuleObject.bar = 1; if (nameStartsWith !== 'this' && currentModule && doclet.name !== 'module.exports') { memberof = currentModule.longname; scopePunc = SCOPE.PUNC.STATIC; } // like: module.exports = 1; else if (doclet.name === 'module.exports' && currentModule) { doclet.addTag('name', currentModule.longname); doclet.postProcess(); } else { memberof = parser.resolveThis(astNode); // like the following at the top level of a module: // this.foo = 1; if (nameStartsWith === 'this' && currentModule && !memberof) { memberof = currentModule.longname; scopePunc = SCOPE.PUNC.STATIC; } else { scopePunc = SCOPE.PUNC.INSTANCE; } } return { memberof: memberof, scopePunc: scopePunc, }; } function addSymbolMemberof(parser, doclet, astNode) { let basename; let memberof; let memberofInfo; let moduleOriginalName = ''; let resolveTargetRegExp; let scopePunc; let unresolved; if (!astNode) { return; } // check to see if the doclet name is an unresolved reference to the module object, or to `this` // TODO: handle cases where the module object is shadowed in the current scope if (currentModule) { moduleOriginalName = `|${currentModule.originalName}`; } resolveTargetRegExp = new RegExp(`^((?:module.)?exports|this${moduleOriginalName})(\\.|\\[|$)`); unresolved = resolveTargetRegExp.exec(doclet.name); if (unresolved) { memberofInfo = findSymbolMemberof(parser, doclet, astNode, unresolved[1], unresolved[2]); memberof = memberofInfo.memberof; scopePunc = memberofInfo.scopePunc; if (memberof) { doclet.name = doclet.name ? memberof + scopePunc + doclet.name : memberof; } } else { memberofInfo = parser.astnodeToMemberof(astNode); basename = memberofInfo.basename; memberof = memberofInfo.memberof; } // if we found a memberof name, apply it to the doclet if (memberof) { doclet.addTag('memberof', memberof); if (basename) { doclet.name = (doclet.name || '').replace(new RegExp(`^${escape(basename)}.`), ''); } } // otherwise, add the defaults for a module (if we're currently in a module) else { setModuleScopeMemberOf(parser, doclet); } } function newSymbolDoclet(parser, docletSrc, e) { const newDoclet = createSymbolDoclet(docletSrc, e, parser.dependencies); // if there's an alias, use that as the symbol name if (newDoclet.alias) { processAlias(parser, newDoclet, e.astnode); } // otherwise, get the symbol name from the code else if (typeof e.code?.name !== 'undefined' && e.code?.name !== '') { newDoclet.addTag('name', e.code.name); if (!newDoclet.memberof) { addSymbolMemberof(parser, newDoclet, e.astnode); } newDoclet.postProcess(); } else { return false; } // set the scope to global unless any of the following are true: // a) the doclet is a memberof something // b) the doclet represents a module // c) we're in a module that exports only this symbol if ( !newDoclet.memberof && newDoclet.kind !== 'module' && (!currentModule || currentModule.longname !== newDoclet.name) ) { newDoclet.scope = SCOPE.NAMES.GLOBAL; } // handle cases where the doclet kind is auto-detected from the node type if (e.code.kind && newDoclet.kind === 'member') { newDoclet.kind = e.code.kind; } addDoclet(parser, newDoclet); e.doclet = newDoclet; return true; } /** * Attach these event handlers to a particular instance of a parser. * @param parser */ export function attachTo(parser) { // Handle JSDoc "virtual comments" that include one of the following: // + A `@name` tag // + Another tag that accepts a name, such as `@function` parser.on('jsdocCommentFound', (e) => { const comments = e.comment.split(/@also\b/g); let newDoclet; for (let i = 0, l = comments.length; i < l; i++) { newDoclet = createDoclet(comments[i], e, parser.dependencies); // we're only interested in virtual comments here if (!newDoclet.name) { continue; } // add the default scope/memberof for a module (if we're in a module) setModuleScopeMemberOf(parser, newDoclet); newDoclet.postProcess(); // if we _still_ don't have a scope, use the default setDefaultScope(newDoclet); addDoclet(parser, newDoclet); e.doclet = newDoclet; } }); // Handle named symbols in the code. May or may not have a JSDoc comment attached. parser.on('symbolFound', (e) => { const comments = e.comment.split(/@also\b/g); for (let i = 0, l = comments.length; i < l; i++) { newSymbolDoclet(parser, comments[i], e); } }); parser.on('fileComplete', () => { currentModule = null; }); }