diff --git a/lib/jsdoc/doclet.js b/lib/jsdoc/doclet.js index 409c2fb3..0e719c98 100644 --- a/lib/jsdoc/doclet.js +++ b/lib/jsdoc/doclet.js @@ -264,7 +264,6 @@ function removeGlobal(longname) { Doclet.prototype.setMemberof = function(sid) { /** * The longname of the symbol that contains this one, if any. - * @alias module:jsdoc/doclet.Doclet#memberof * @type {string} */ this.memberof = removeGlobal(sid) @@ -279,7 +278,6 @@ Doclet.prototype.setMemberof = function(sid) { Doclet.prototype.setLongname = function(name) { /** * The fully resolved symbol name. - * @alias module:jsdoc/doclet.Doclet#longname * @type {string} */ this.longname = removeGlobal(name); @@ -347,7 +345,6 @@ Doclet.prototype.borrow = function(source, target) { if (!this.borrowed) { /** * A list of symbols that are borrowed by this one, if any. - * @alias module:jsdoc/doclet.Doclet#borrowed * @type {Array.} */ this.borrowed = []; @@ -358,7 +355,6 @@ Doclet.prototype.borrow = function(source, target) { Doclet.prototype.mix = function(source) { /** * A list of symbols that are mixed into this one, if any. - * @alias module:jsdoc/doclet.Doclet#mixes * @type Array. */ this.mixes = this.mixes || []; @@ -373,7 +369,6 @@ Doclet.prototype.mix = function(source) { Doclet.prototype.augment = function(base) { /** * A list of symbols that are augmented by this one, if any. - * @alias module:jsdoc/doclet.Doclet#augments * @type Array. */ this.augments = this.augments || []; @@ -388,7 +383,6 @@ Doclet.prototype.augment = function(base) { Doclet.prototype.setMeta = function(meta) { /** * Information about the source code associated with this doclet. - * @alias module:jsdoc/doclet.Doclet#meta * @namespace */ this.meta = this.meta || {}; @@ -396,7 +390,6 @@ Doclet.prototype.setMeta = function(meta) { if (meta.range) { /** * The positions of the first and last characters of the code associated with this doclet. - * @alias module:jsdoc/doclet.Doclet#meta.range * @type Array. */ this.meta.range = meta.range.slice(0); @@ -405,13 +398,11 @@ Doclet.prototype.setMeta = function(meta) { if (meta.lineno) { /** * The name of the file containing the code associated with this doclet. - * @alias module:jsdoc/doclet.Doclet#meta.filename * @type string */ this.meta.filename = path.basename(meta.filename); /** * The line number of the code associated with this doclet. - * @alias module:jsdoc/doclet.Doclet#meta.lineno * @type number */ this.meta.lineno = meta.lineno; @@ -424,7 +415,6 @@ Doclet.prototype.setMeta = function(meta) { /** * Information about the code symbol. - * @alias module:jsdoc/doclet.Doclet#meta.code * @namespace */ this.meta.code = this.meta.code || {}; @@ -433,7 +423,6 @@ Doclet.prototype.setMeta = function(meta) { if (meta.code.name) { /** * The name of the symbol in the source code. - * @alias module:jsdoc/doclet.Doclet#meta.code.name * @type {string} */ this.meta.code.name = meta.code.name; @@ -441,7 +430,6 @@ Doclet.prototype.setMeta = function(meta) { if (meta.code.type) { /** * The type of the symbol in the source code. - * @alias module:jsdoc/doclet.Doclet# * @type {string} */ this.meta.code.type = meta.code.type; @@ -458,7 +446,6 @@ Doclet.prototype.setMeta = function(meta) { if (typeof meta.code.value !== 'undefined') { /** * The value of the symbol in the source code. - * @alias module:jsdoc/doclet.Doclet#meta.code.value * @type {*} */ this.meta.code.value = meta.code.value; diff --git a/lib/jsdoc/src/parser.js b/lib/jsdoc/src/parser.js index 470c1cd5..e0c4eb77 100644 --- a/lib/jsdoc/src/parser.js +++ b/lib/jsdoc/src/parser.js @@ -35,6 +35,27 @@ var PARSERS = exports.PARSERS = { var SCHEMA = 'javascript:'; /* eslint-enable no-script-url */ +function DocletCache() { + this._doclets = {}; +} + +DocletCache.prototype.get = function(name) { + if ( !hasOwnProp.call(this._doclets, name) ) { + return null; + } + + // always return the most recent doclet + return this._doclets[name][this._doclets[name].length - 1]; +}; + +DocletCache.prototype.put = function(name, value) { + if ( !hasOwnProp.call(this._doclets, name) ) { + this._doclets[name] = []; + } + + this._doclets[name].push(value); +}; + // TODO: docs exports.createParser = function(type) { var modulePath; @@ -96,9 +117,11 @@ util.inherits(Parser, events.EventEmitter); // TODO: docs Parser.prototype.clear = function() { this._resultBuffer = []; - this.refs = {}; - this.refs[jsdoc.name.LONGNAMES.GLOBAL] = {}; - this.refs[jsdoc.name.LONGNAMES.GLOBAL].meta = {}; + this._byNodeId = new DocletCache(); + this._byLongname = new DocletCache(); + this._byLongname.put(jsdoc.name.LONGNAMES.GLOBAL, { + meta: {} + }); }; // TODO: update docs @@ -247,35 +270,45 @@ Parser.prototype._walkAst = function(ast, visitor, sourceName) { // TODO: docs Parser.prototype.addDocletRef = function(e) { + var fakeDoclet; var node; if (e && e.code && e.code.node) { node = e.code.node; - // allow lookup from value => doclet if (e.doclet) { - this.refs[node.nodeId] = e.doclet; + // allow lookup from node ID => doclet + this._byNodeId.put(node.nodeId, e.doclet); + this._byLongname.put(e.doclet.longname, e.doclet); } // keep references to undocumented anonymous functions, too, as they might have scoped vars else if ( (node.type === Syntax.FunctionDeclaration || node.type === Syntax.FunctionExpression) && - !this.refs[node.nodeId] ) { - this.refs[node.nodeId] = { + !this._getDocletById(node.nodeId) ) { + fakeDoclet = { longname: jsdoc.name.LONGNAMES.ANONYMOUS, meta: { code: e.code } }; + this._byNodeId.put(node.nodeId, fakeDoclet); + this._byLongname.put(fakeDoclet.longname, fakeDoclet); } } }; // TODO: docs -Parser.prototype._getDoclet = function(id) { - if ( hasOwnProp.call(this.refs, id) ) { - return this.refs[id]; - } +Parser.prototype._getDocletById = function(id) { + return this._byNodeId.get(id); +}; - return null; +/** + * Retrieve the most recently seen doclet that has the given longname. + * + * @param {string} longname - The longname to search for. + * @return {module:jsdoc/doclet.Doclet?} The most recent doclet for the longname. + */ +Parser.prototype._getDocletByLongname = function(longname) { + return this._byLongname.get(longname); }; // TODO: docs @@ -311,7 +344,7 @@ Parser.prototype.astnodeToMemberof = function(node) { if ( (type === Syntax.FunctionDeclaration || type === Syntax.FunctionExpression || type === Syntax.VariableDeclarator) && node.enclosingScope ) { - doclet = this._getDoclet(node.enclosingScope.nodeId); + doclet = this._getDocletById(node.enclosingScope.nodeId); if (!doclet) { result = jsdoc.name.LONGNAMES.ANONYMOUS + jsdoc.name.SCOPE.PUNC.INNER; @@ -327,7 +360,7 @@ Parser.prototype.astnodeToMemberof = function(node) { // walk up the scope chain until we find the scope in which the node is defined while (scope.enclosingScope) { - doclet = this._getDoclet(scope.enclosingScope.nodeId); + doclet = this._getDocletById(scope.enclosingScope.nodeId); if ( doclet && definedInScope(doclet, basename) ) { result = [doclet.meta.vars[basename], basename]; break; @@ -339,13 +372,12 @@ Parser.prototype.astnodeToMemberof = function(node) { } // do we know that it's a global? - doclet = this.refs[jsdoc.name.LONGNAMES.GLOBAL]; - + doclet = this._getDocletByLongname(jsdoc.name.LONGNAMES.GLOBAL); if ( doclet && definedInScope(doclet, basename) ) { result = [doclet.meta.vars[basename], basename]; } else { - doclet = this._getDoclet(node.parent.nodeId); + doclet = this._getDocletById(node.parent.nodeId); // set the result if we found a doclet. (if we didn't, the AST node may describe a // global symbol.) @@ -367,13 +399,39 @@ Parser.prototype.astnodeToMemberof = function(node) { */ Parser.prototype._getParentClass = function(node) { var doclet; + var nameAtoms; + var scope = node.enclosingScope; - do { - doclet = this._getDoclet(node.enclosingScope.nodeId); - node = node.enclosingScope; - } while (node && node.enclosingScope && doclet && doclet.kind !== 'class'); + function isClass(d) { + return d && d.kind === 'class'; + } - return (doclet.kind === 'class' ? doclet : null); + while (scope) { + // get the doclet, if any, for the parent scope + doclet = this._getDocletById(scope.nodeId); + + if (doclet) { + // is the doclet for a class? if so, we're done + if ( isClass(doclet) ) { + break; + } + + // is the doclet for an instance member of a class? if so, try to get the doclet for the + // owning class + nameAtoms = jsdoc.name.shorten(doclet.longname); + if (nameAtoms.scope === jsdoc.name.SCOPE.PUNC.INSTANCE) { + doclet = this._getDocletByLongname(nameAtoms.memberof); + if ( isClass(doclet) ) { + break; + } + } + } + + // move up to the next parent scope + scope = scope.enclosingScope; + } + + return (isClass(doclet) ? doclet : null); }; // TODO: docs @@ -390,7 +448,7 @@ Parser.prototype.resolveThis = function(node) { // In general, if there's an enclosing scope, we use the enclosing scope to resolve `this`. // For object properties, we use the node's parent (the object) instead. if (node.type !== Syntax.Property && node.enclosingScope) { - doclet = this._getDoclet(node.enclosingScope.nodeId); + doclet = this._getDocletById(node.enclosingScope.nodeId); if (!doclet) { result = jsdoc.name.LONGNAMES.ANONYMOUS; // TODO handle global this? @@ -402,6 +460,7 @@ Parser.prototype.resolveThis = function(node) { parentClass = this._getParentClass(node); // like: function Foo() { this.bar = function(n) { /** blah */ this.name = n; }; + // or: Foo.prototype.bar = function(n) { /** blah */ this.name = n; }; // or: var Foo = exports.Foo = function(n) { /** blah */ this.name = n; }; // or: Foo.constructor = function(n) { /** blah */ this.name = n; } if ( parentClass || /\.constructor$/.test(doclet.longname) ) { @@ -425,7 +484,7 @@ Parser.prototype.resolveThis = function(node) { } } else { - doclet = this.refs[node.parent.nodeId]; + doclet = this._getDocletById(node.parent.nodeId); // TODO: is this behavior correct? when do we get here? if (!doclet) { @@ -450,7 +509,7 @@ Parser.prototype.resolveThis = function(node) { * this method returns multiple doclets (in this case, the doclets for `foo` and `exports.FOO`). * * @param {Object} node - An AST node representing an object property. - * @return {Array.} An array of doclets for the parent object or objects, or + * @return {Array.} An array of doclets for the parent object or objects, or * an empty array if no doclets are found. */ Parser.prototype.resolvePropertyParents = function(node) { @@ -460,7 +519,7 @@ Parser.prototype.resolvePropertyParents = function(node) { var doclets = []; while (currentAncestor) { - doclet = this._getDoclet(currentAncestor.nodeId); + doclet = this._getDocletById(currentAncestor.nodeId); if (doclet) { doclets.push(doclet); } @@ -500,7 +559,7 @@ Parser.prototype.resolveVar = function(node, basename) { result = ''; // global } else { - doclet = this._getDoclet(scope.nodeId); + doclet = this._getDocletById(scope.nodeId); if ( definedInScope(doclet, basename) ) { result = doclet.longname; } diff --git a/lib/jsdoc/src/visitor.js b/lib/jsdoc/src/visitor.js index 4918368e..25c9bdce 100644 --- a/lib/jsdoc/src/visitor.js +++ b/lib/jsdoc/src/visitor.js @@ -47,7 +47,7 @@ function makeVarsFinisher(scopeDoclet) { function getParentDocletFromEvent(parser, e) { if (e.doclet && e.doclet.meta && e.doclet.meta.code && e.doclet.meta.code.node && e.doclet.meta.code.node.parent) { - return parser._getDoclet(e.doclet.meta.code.node.parent.nodeId); + return parser._getDocletById(e.doclet.meta.code.node.parent.nodeId); } return null; @@ -269,7 +269,7 @@ function makeDefaultParamFinisher(parser) { function makeConstructorFinisher(parser) { return function(e) { var doclet = e.doclet; - var parentDoclet = parser._getDoclet(e.code.node.parent.parent.nodeId); + var parentDoclet = parser._getDocletById(e.code.node.parent.parent.nodeId); if (!doclet || !parentDoclet || parentDoclet.undocumented) { return; @@ -471,9 +471,15 @@ Visitor.prototype.visitNode = function(node, parser, filename) { // TODO: docs // TODO: note that it's essential to call this function before you try to resolve names! function trackVars(parser, node, e) { - var enclosingScopeId = node.enclosingScope ? node.enclosingScope.nodeId : - jsdoc.name.LONGNAMES.GLOBAL; - var doclet = parser.refs[enclosingScopeId]; + var doclet; + var enclosingScopeId = node.enclosingScope ? node.enclosingScope.nodeId : null; + + if (enclosingScopeId) { + doclet = parser._getDocletById(enclosingScopeId); + } + else { + doclet = parser._getDocletByLongname(jsdoc.name.LONGNAMES.GLOBAL); + } if (doclet) { doclet.meta.vars = doclet.meta.vars || {}; diff --git a/test/fixtures/instanceproperty.js b/test/fixtures/instanceproperty.js new file mode 100644 index 00000000..2dc1b13b --- /dev/null +++ b/test/fixtures/instanceproperty.js @@ -0,0 +1,8 @@ +/** @class */ +function Foo() {} + +/** Set the bar. */ +Foo.prototype.setBar = function(bar) { + /** The bar. */ + this.bar = bar; +}; diff --git a/test/specs/documentation/instanceproperty.js b/test/specs/documentation/instanceproperty.js new file mode 100644 index 00000000..52cfdf32 --- /dev/null +++ b/test/specs/documentation/instanceproperty.js @@ -0,0 +1,12 @@ +'use strict'; + +describe('Properties documented in instance methods', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/instanceproperty.js'); + var bar = docSet.getByLongname('Foo#bar')[0]; + + it('should set the correct longname when a property is documented in an instance method', function() { + expect(bar).toBeDefined(); + expect(bar.name).toBe('bar'); + expect(bar.kind).toBe('member'); + }); +});