diff --git a/lib/jsdoc/src/parser.js b/lib/jsdoc/src/parser.js index bcea2cb8..950d3573 100644 --- a/lib/jsdoc/src/parser.js +++ b/lib/jsdoc/src/parser.js @@ -359,6 +359,24 @@ Parser.prototype.astnodeToMemberof = function(node) { return result; }; +/** + * Get the doclet for the lowest-level class, if any, that is in the scope chain for a given node. + * + * @param {Object} node - The node whose scope chain will be searched. + * @return {module:jsdoc/doclet.Doclet?} The doclet for the lowest-level class in the node's scope + * chain. + */ +Parser.prototype._getParentClass = function(node) { + var doclet; + + do { + doclet = this._getDoclet(node.enclosingScope.nodeId); + node = node.enclosingScope; + } while (node && node.enclosingScope && doclet && doclet.kind !== 'class'); + + return (doclet.kind === 'class' ? doclet : null); +}; + // TODO: docs /** * Resolve what "this" refers to relative to a node. @@ -367,6 +385,7 @@ Parser.prototype.astnodeToMemberof = function(node) { */ Parser.prototype.resolveThis = function(node) { var doclet; + var parentClass; var result; // In general, if there's an enclosing scope, we use the enclosing scope to resolve `this`. @@ -380,9 +399,19 @@ Parser.prototype.resolveThis = function(node) { else if (doclet.this) { result = doclet.this; } - // like: Foo.constructor = function(n) { /** blah */ this.name = n; } else if (doclet.kind === 'function' && doclet.memberof) { - result = doclet.memberof; + parentClass = this._getParentClass(node); + + // like: function Foo() { this.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) ) { + result = doclet.memberof; + } + // like: function notAClass(n) { /** global this */ this.name = n; } + else { + result = doclet.longname; + } } // like: var foo = function(n) { /** blah */ this.bar = n; } else if ( doclet.kind === 'member' && jsdoc.src.astnode.isAssignment(node) ) { diff --git a/test/fixtures/this4.js b/test/fixtures/this4.js new file mode 100644 index 00000000..d8eccd0b --- /dev/null +++ b/test/fixtures/this4.js @@ -0,0 +1,9 @@ +'use strict'; + +/** @class */ +function Template() {} + +Template.constructor = function() { + /** Render content. */ + this.render = function(data) {}; +}; diff --git a/test/fixtures/this5.js b/test/fixtures/this5.js new file mode 100644 index 00000000..bc7dcc90 --- /dev/null +++ b/test/fixtures/this5.js @@ -0,0 +1,12 @@ +'use strict'; + +/** @module template */ + +/** + * @class + * @alias module:template.Template + */ +var Template = exports.Template = function() { + /** View file. */ + this.view = null; +}; diff --git a/test/specs/documentation/this.js b/test/specs/documentation/this.js index d3735683..bd3f94e4 100644 --- a/test/specs/documentation/this.js +++ b/test/specs/documentation/this.js @@ -81,6 +81,40 @@ describe('this', function() { }); }); + describe('When "this" is assigned inside an explicit definition of the class constructor', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/this4.js'); + var found = docSet.getByLongname('Template#render'); + + it('should have a longname like Constructor#member', function() { + expect(found.length).toBe(1); + }); + + it('should have the correct name', function() { + expect(found[0].name).toBe('render'); + }); + + it('should have the correct memberof', function() { + expect(found[0].memberof).toBe('Template'); + }); + }); + + describe('When "this" is assigned in a chained declaration in a module', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/this5.js'); + var found = docSet.getByLongname('module:template.Template#view'); + + it('should have a longname like Constructor#member', function() { + expect(found.length).toBe(1); + }); + + it('should have the correct name', function() { + expect(found[0].name).toBe('view'); + }); + + it('should have the correct memberof', function() { + expect(found[0].memberof).toBe('module:template.Template'); + }); + }); + describe('When a member is nested inside an objectlit "this" property inside a constructor', function() { var docSet = jasmine.getDocSetFromFile('test/fixtures/this-and-objectlit.js'); var found = docSet.getByLongname('Page#parts.body.heading');