diff --git a/lib/jsdoc/src/handlers.js b/lib/jsdoc/src/handlers.js index d7f90840..2cfc946c 100644 --- a/lib/jsdoc/src/handlers.js +++ b/lib/jsdoc/src/handlers.js @@ -14,22 +14,61 @@ var jsdoc = { var util = require('util'); var currentModule = null; +var SCOPE_PUNC = jsdoc.name.SCOPE.PUNC; +var unresolvedName = /^((?:module.)?exports|this)(\.|$)/; -var moduleRegExp = /^((?:module.)?exports|this)(\.|$)/; +function filterByLongname(doclet) { + // you can't document prototypes + if ( /#$/.test(doclet.longname) ) { + return true; + } -function getNewDoclet(comment, e) { - var Doclet = jsdoc.doclet.Doclet; + return false; +} + +function createDoclet(comment, e) { var doclet; var err; try { - doclet = new Doclet(comment, e); + doclet = new jsdoc.doclet.Doclet(comment, e); } catch (error) { err = new Error( util.format('cannot create a doclet for the comment "%s": %s', comment.replace(/[\r\n]/g, ''), error.message) ); jsdoc.util.logger.error(err); - doclet = new Doclet('', e); + doclet = new jsdoc.doclet.Doclet('', e); + } + + 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) { + var doclet = createDoclet(comment, e); + + if (doclet.name) { + // try again, without the comment + e.comment = '@undocumented'; + doclet = createDoclet(e.comment, e); } return doclet; @@ -55,153 +94,165 @@ function setDefaultScopeMemberOf(doclet) { } } +function addDoclet(parser, newDoclet) { + var 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) { + var 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(); +} + +function findModuleMemberof(parser, doclet, astNode, nameStartsWith) { + var memberof = ''; + var scopePunc = ''; + + // 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(unresolvedName, ''); + } + + // like /** @module foo */ exports.bar = 1; + // or /** @module foo */ module.exports.bar = 1; + // but not /** @module foo */ module.exports = 1; + if ( (nameStartsWith === 'exports' || nameStartsWith === 'module.exports') && + doclet.name !== 'module.exports' && currentModule ) { + memberof = currentModule; + scopePunc = SCOPE_PUNC.STATIC; + } + else if (doclet.name === 'module.exports' && currentModule) { + doclet.addTag('name', currentModule); + doclet.postProcess(); + } + else { + // like /** @module foo */ exports = {bar: 1}; + // or /** blah */ this.foo = 1; + memberof = parser.resolveThis(astNode); + scopePunc = (nameStartsWith === 'exports') ? + SCOPE_PUNC.STATIC : + SCOPE_PUNC.INSTANCE; + + // like /** @module foo */ this.bar = 1; + if (nameStartsWith === 'this' && currentModule && !memberof) { + memberof = currentModule; + scopePunc = SCOPE_PUNC.STATIC; + } + } + + return { + memberof: memberof, + scopePunc: scopePunc + }; +} + +function addSymbolMemberof(parser, doclet, astNode) { + var basename; + var memberof; + var memberofInfo; + var scopePunc; + var unresolved; + + // TODO: is this the correct behavior, given that we don't always use the AST node? + if (!astNode) { + return; + } + + // check to see if the doclet name is an unresolved reference to the module wrapper + unresolved = unresolvedName.exec(doclet.name); + if (unresolved) { + memberofInfo = findModuleMemberof(parser, doclet, astNode, unresolved[1]); + memberof = memberofInfo.memberof; + scopePunc = memberofInfo.scopePunc; + + if (memberof) { + doclet.name = doclet.name ? + memberof + scopePunc + doclet.name : + memberof; + } + } + else { + memberofInfo = parser.astnodeToMemberof(astNode); + if( Array.isArray(memberofInfo) ) { + basename = memberofInfo[1]; + memberof = memberofInfo[0]; + } + else { + memberof = memberofInfo; + } + } + + // 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, use the defaults + else { + setDefaultScopeMemberOf(doclet); + } +} + +function newSymbolDoclet(parser, docletSrc, e) { + var memberofName = null; + var newDoclet = createSymbolDoclet(docletSrc, e); + + // 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 (e.code && 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 a) the doclet is a memberof something or b) the current + // module exports only this symbol + if (!newDoclet.memberof && currentModule !== newDoclet.name) { + newDoclet.scope = 'global'; + } + + addDoclet(parser, newDoclet); + e.doclet = newDoclet; +} + /** * Attach these event handlers to a particular instance of a parser. * @param parser */ exports.attachTo = function(parser) { - function filter(doclet) { - // you can't document prototypes - if ( /#$/.test(doclet.longname) ) { - return true; - } - - return false; - } - - function addDoclet(newDoclet) { - var e; - if (newDoclet) { - setCurrentModule(newDoclet); - e = { doclet: newDoclet }; - parser.emit('newDoclet', e); - - if ( !e.defaultPrevented && !filter(e.doclet) ) { - parser.addResult(e.doclet); - } - } - } - - // TODO: for clarity, decompose into smaller functions - function newSymbolDoclet(docletSrc, e) { - var memberofName = null, - newDoclet = getNewDoclet(docletSrc, e); - - // 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 quite possible that the comment and symbol have nothing to do with one - // another. - // - // As a result, if we create a doclet for a `symbolFound` event, and we've already added a - // name attribute by parsing the JSDoc comment, we need to create a new doclet that ignores - // the attached JSDoc comment and only looks at the code. - if (newDoclet.name) { - // try again, without the comment - e.comment = '@undocumented'; - newDoclet = getNewDoclet(e.comment, e); - } - - if (newDoclet.alias) { - if (newDoclet.alias === '{@thisClass}') { - memberofName = parser.resolveThis(e.astnode); - - // "class" refers to the owner of the prototype, not the prototype itself - if ( /^(.+?)(\.prototype|#)$/.test(memberofName) ) { - memberofName = RegExp.$1; - } - newDoclet.alias = memberofName; - } - newDoclet.addTag('name', newDoclet.alias); - newDoclet.postProcess(); - } - else if (e.code && e.code.name) { // we need to get the symbol name from code - newDoclet.addTag('name', e.code.name); - if (!newDoclet.memberof && e.astnode) { - var basename = null, - scope = ''; - if ( moduleRegExp.test(newDoclet.name) ) { - var nameStartsWith = RegExp.$1; - - // remove stuff that indicates module membership (but don't touch the name - // `module.exports`, which identifies the module object itself) - if (newDoclet.name !== 'module.exports') { - newDoclet.name = newDoclet.name.replace(moduleRegExp, ''); - } - - // like /** @module foo */ exports.bar = 1; - // or /** @module foo */ module.exports.bar = 1; - // but not /** @module foo */ module.exports = 1; - if ( (nameStartsWith === 'exports' || nameStartsWith === 'module.exports') && - newDoclet.name !== 'module.exports' && currentModule ) { - memberofName = currentModule; - scope = 'static'; - } - else if (newDoclet.name === 'module.exports' && currentModule) { - newDoclet.addTag('name', currentModule); - newDoclet.postProcess(); - } - else { - // like /** @module foo */ exports = {bar: 1}; - // or /** blah */ this.foo = 1; - memberofName = parser.resolveThis(e.astnode); - scope = nameStartsWith === 'exports' ? 'static' : 'instance'; - - // like /** @module foo */ this.bar = 1; - if (nameStartsWith === 'this' && currentModule && !memberofName) { - memberofName = currentModule; - scope = 'static'; - } - } - - if (memberofName) { - if (newDoclet.name) { - newDoclet.name = memberofName + (scope === 'instance' ? '#' : '.') + - newDoclet.name; - } - else { newDoclet.name = memberofName; } - } - } - else { - memberofName = parser.astnodeToMemberof(e.astnode); - if( Array.isArray(memberofName) ) { - basename = memberofName[1]; - memberofName = memberofName[0]; - } - } - - if (memberofName) { - newDoclet.addTag('memberof', memberofName); - if (basename) { - newDoclet.name = (newDoclet.name || '') - .replace(new RegExp('^' + escape(basename) + '.'), ''); - } - } - else { - setDefaultScopeMemberOf(newDoclet); - } - } - - newDoclet.postProcess(); - } - else { - return false; - } - - // set the scope to global unless a) the doclet is a memberof something or b) the current - // module exports only this symbol - if (!newDoclet.memberof && currentModule !== newDoclet.name) { - newDoclet.scope = 'global'; - } - - addDoclet.call(parser, newDoclet); - e.doclet = newDoclet; - } - // Handle JSDoc "virtual comments" that include one of the following: // // + A `@name` tag @@ -211,7 +262,7 @@ exports.attachTo = function(parser) { var newDoclet; for (var i = 0, l = comments.length; i < l; i++) { - newDoclet = getNewDoclet(comments[i], e); + newDoclet = createDoclet(comments[i], e); // we're only interested in virtual comments here if (!newDoclet.name) { @@ -220,7 +271,7 @@ exports.attachTo = function(parser) { setDefaultScopeMemberOf(newDoclet); newDoclet.postProcess(); - addDoclet.call(parser, newDoclet); + addDoclet(parser, newDoclet); e.doclet = newDoclet; } @@ -231,7 +282,7 @@ exports.attachTo = function(parser) { var comments = e.comment.split(/@also\b/g); for (var i = 0, l = comments.length; i < l; i++) { - newSymbolDoclet.call(parser, comments[i], e); + newSymbolDoclet(parser, comments[i], e); } });