diff --git a/lib/jsdoc/doclet.js b/lib/jsdoc/doclet.js index 4bfe176c..a78dbe98 100644 --- a/lib/jsdoc/doclet.js +++ b/lib/jsdoc/doclet.js @@ -24,6 +24,7 @@ var jsdoc = { } }; var path = require('jsdoc/path'); +var Syntax = jsdoc.src.Syntax; var util = require('util'); // Longname used for doclets whose actual longname cannot be identified. @@ -48,13 +49,25 @@ function applyTag(doclet, tag) { } // use the meta info about the source code to guess what the doclet kind should be -function codetypeToKind(type) { - var Syntax = jsdoc.src.Syntax; - if (type === Syntax.FunctionDeclaration || type === Syntax.FunctionExpression) { - return 'function'; +function codeToKind(code) { + var parent; + + var astnode = require('jsdoc/src/astnode'); + + // default + var kind = 'member'; + + if (code.type === Syntax.FunctionDeclaration || code.type === Syntax.FunctionExpression) { + kind = 'function'; + } + else if (code.node && code.node.parent) { + parent = code.node.parent; + if ( astnode.isFunction(parent) ) { + kind = 'param'; + } } - return 'member'; + return kind; } function unwrap(docletSrc) { @@ -170,7 +183,7 @@ Doclet.prototype.postProcess = function() { } if (!this.kind && this.meta && this.meta.code) { - this.addTag( 'kind', codetypeToKind(this.meta.code.type) ); + this.addTag( 'kind', codeToKind(this.meta.code) ); } if (this.variation && this.longname && !/\)$/.test(this.longname) ) { diff --git a/lib/jsdoc/schema.js b/lib/jsdoc/schema.js index 3459874b..4a3c562d 100644 --- a/lib/jsdoc/schema.js +++ b/lib/jsdoc/schema.js @@ -362,6 +362,7 @@ var DOCLET_SCHEMA = exports.DOCLET_SCHEMA = { 'module', 'namespace', 'package', + 'param', 'typedef' ] }, diff --git a/lib/jsdoc/src/astbuilder.js b/lib/jsdoc/src/astbuilder.js index e3647770..37852d70 100644 --- a/lib/jsdoc/src/astbuilder.js +++ b/lib/jsdoc/src/astbuilder.js @@ -6,9 +6,11 @@ var Syntax = require('jsdoc/src/syntax').Syntax; var VISITOR_CONTINUE = true; var VISITOR_STOP = false; -// TODO: docs -var acceptsLeadingComments = exports.acceptsLeadingComments = (function() { +// TODO: docs; empty array means any node type, otherwise only the node types in the array +var acceptsLeadingComments = (function() { var accepts = {}; + + // these nodes always accept leading comments var commentable = [ Syntax.AssignmentExpression, Syntax.CallExpression, @@ -21,11 +23,22 @@ var acceptsLeadingComments = exports.acceptsLeadingComments = (function() { Syntax.VariableDeclarator, Syntax.WithStatement ]; - for (var i = 0, l = commentable.length; i < l; i++) { - accepts[commentable[i]] = true; + accepts[commentable[i]] = []; } + // these nodes accept leading comments if they have specific types of parent nodes + // like: function foo(/** @type {string} */ bar) {} + accepts[Syntax.Identifier] = [ + Syntax.CatchClause, + Syntax.FunctionDeclaration, + Syntax.FunctionExpression + ]; + // like: var Foo = Class.create(/** @lends Foo */{ // ... }) + accepts[Syntax.ObjectExpression] = [ + Syntax.CallExpression + ]; + return accepts; })(); @@ -43,6 +56,25 @@ var searchDescendants = (function() { return search; })(); +// TODO: docs +function canAcceptComment(node) { + var canAccept = false; + var spec = acceptsLeadingComments[node.type]; + + if (spec) { + // empty array means we don't care about the parent type + if (spec.length === 0) { + canAccept = true; + } + // we can accept the comment if the spec contains the type of the node's parent + else if (node.parent) { + canAccept = spec.indexOf(node.parent.type) !== -1; + } + } + + return canAccept; +} + // TODO: docs // check whether node1 is before node2 function isBefore(beforeRange, afterRange) { @@ -62,7 +94,7 @@ function isWithin(innerRange, outerRange) { // TODO: docs function isLeadingComment(comment, before, after) { - return !!before && !!after && !!acceptsLeadingComments[after.type] && + return !!before && !!after && !!canAcceptComment(after) && isBefore(before.range, after.range) && isBetween(comment.range, before.range, after.range); } @@ -333,7 +365,7 @@ CommentAttacher.prototype.visit = function(node) { // okay, now that we've done all that bookkeeping, we can check whether the current node accepts // leading comments and add it to the candidate list if needed - if (isEligible && acceptsLeadingComments[node.type]) { + if ( isEligible && canAcceptComment(node) ) { // make sure we don't go past the end of the outermost target node if (!this._pendingCommentRange) { this._pendingCommentRange = node.range.slice(0); diff --git a/lib/jsdoc/src/astnode.js b/lib/jsdoc/src/astnode.js index 0d5c3e8c..da0dd16e 100644 --- a/lib/jsdoc/src/astnode.js +++ b/lib/jsdoc/src/astnode.js @@ -11,6 +11,16 @@ var uid = 100000000; // TODO: currently unused var GLOBAL_NODE_ID = exports.GLOBAL_NODE_ID = require('jsdoc/doclet').GLOBAL_LONGNAME; +/** + * Check whether an AST node represents a function. + * + * @param {Object} node - The AST node to check. + * @return {boolean} Set to `true` if the node is a function or `false` in all other cases. + */ +var isFunction = exports.isFunction = function(node) { + return node.type === Syntax.FunctionDeclaration || node.type === Syntax.FunctionExpression; +}; + /** * Check whether an AST node creates a new scope. * @@ -19,8 +29,8 @@ var GLOBAL_NODE_ID = exports.GLOBAL_NODE_ID = require('jsdoc/doclet').GLOBAL_LON */ var isScope = exports.isScope = function(node) { // TODO: handle blocks with "let" declarations - return !!node && typeof node === 'object' && (node.type === Syntax.CatchClause || - node.type === Syntax.FunctionDeclaration || node.type === Syntax.FunctionExpression); + return !!node && typeof node === 'object' && ( node.type === Syntax.CatchClause || + isFunction(node) ); }; // TODO: docs @@ -269,6 +279,13 @@ var getInfo = exports.getInfo = function(node) { info.paramnames = getParamNames(node); break; + // like the param "bar" in: "function foo(bar) {}" + case Syntax.Identifier: + info.node = node; + info.name = nodeToString(info.node); + info.type = info.node.type; + break; + // like "a.b.c" case Syntax.MemberExpression: info.node = node; diff --git a/lib/jsdoc/src/visitor.js b/lib/jsdoc/src/visitor.js index c665cb22..f53a0cc7 100644 --- a/lib/jsdoc/src/visitor.js +++ b/lib/jsdoc/src/visitor.js @@ -39,6 +39,67 @@ function makeVarsFinisher(scopeDoclet) { }; } +/** + * For function parameters that have inline documentation, create a function that will merge the + * inline documentation into the function's doclet. If the parameter is already documented in the + * function's doclet, the inline documentation will be ignored. + * + * @private + * @param {module:jsdoc/src/parser.Parser} parser - The JSDoc parser. + * @return {function} A function that merges a parameter's inline documentation into the function's + * doclet. + */ +function makeInlineParamsFinisher(parser) { + return function(e) { + var documentedParams; + var knownParams; + var param; + var parentDoclet; + + var i = 0; + + if (e.doclet && e.doclet.meta && e.doclet.meta.code && e.doclet.meta.code.node && + e.doclet.meta.code.node.parent) { + parentDoclet = parser._getDoclet(e.doclet.meta.code.node.parent.nodeId); + } + if (!parentDoclet) { + return; + } + + parentDoclet.params = parentDoclet.params || []; + documentedParams = parentDoclet.params; + knownParams = parentDoclet.meta.code.paramnames; + + while (true) { + param = documentedParams[i]; + + // is the param already documented? if so, we're done + if (param && param.name === e.doclet.name) { + // the doclet is no longer needed + e.doclet.undocumented = true; + break; + } + + // if we ran out of documented params, or we're at the parameter's actual position, + // splice in the param at the current index + if ( !param || i === knownParams.indexOf(e.doclet.name) ) { + documentedParams.splice(i, 0, { + type: e.doclet.type, + description: '', + name: e.doclet.name + }); + + // the doclet is no longer needed + e.doclet.undocumented = true; + + break; + } + + i++; + } + }; +} + // TODO: docs function SymbolFound(node, filename, extras) { var self = this; @@ -249,6 +310,7 @@ Visitor.prototype.makeSymbolFoundEvent = function(node, parser, filename) { var basename; var i; var l; + var parent; var extras = { code: jsdoc.src.astnode.getInfo(node) @@ -285,6 +347,18 @@ Visitor.prototype.makeSymbolFoundEvent = function(node, parser, filename) { break; + // like "bar" in: function foo(/** @type {string} */ bar) {} + // This is an extremely common type of node; we only care about function parameters with + // inline type annotations. No need to fire events unless they're already commented. + case Syntax.Identifier: + parent = node.parent; + if ( node.leadingComments && parent && jsdoc.src.astnode.isFunction(parent) ) { + extras.finishers = [makeInlineParamsFinisher(parser)]; + e = new SymbolFound(node, filename, extras); + } + + break; + // like "obj.prop" in: /** @typedef {string} */ obj.prop; // Closure Compiler uses this pattern extensively for enums. // No need to fire events for them unless they're already commented. diff --git a/rhino/js.jar b/rhino/js.jar index 67b3e9de..696a37d4 100644 Binary files a/rhino/js.jar and b/rhino/js.jar differ diff --git a/test/fixtures/typetaginline.js b/test/fixtures/typetaginline.js new file mode 100644 index 00000000..a7637761 --- /dev/null +++ b/test/fixtures/typetaginline.js @@ -0,0 +1,35 @@ +/** + * Inline type info only. + */ +function dispense(/** @type {string} */ candy) {} + +/** + * Inline type info that conflicts with `@param` tag. + * + * @class + * @param {number} candyId - The candy's identifier. + */ +function Dispenser(/** @type {string} */ candyId) {} + +/** + * Inline type info for leading param only. + * + * @param {string} item + */ +function restock(/** @type {Dispenser} */ dispenser, item) {} + +/** + * Inline type info for trailing param only. + * + * @param {Dispenser} dispenser + */ +function clean(dispenser, /** @type {string} */ cleaner) {} + +/** + * Inline type info for inner param only. + * + * @param {Dispenser} dispenser + * @param {number} shade + * @param {string} brand + */ +function paint(dispenser, /** @type {Color} */ color, shade, brand) {} diff --git a/test/specs/documentation/typetaginline.js b/test/specs/documentation/typetaginline.js new file mode 100644 index 00000000..2b1aec49 --- /dev/null +++ b/test/specs/documentation/typetaginline.js @@ -0,0 +1,71 @@ +/*global beforeEach, describe, expect, it, jasmine */ +describe('@type tag inline with function parameters', function() { + var info; + + var docSet = jasmine.getDocSetFromFile('test/fixtures/typetaginline.js'); + + function checkParams(doclet, paramInfo) { + expect(doclet.params).toBeDefined(); + expect(doclet.params.length).toBe(paramInfo.length); + + doclet.params.forEach(function(param, i) { + expect(param.name).toBe(paramInfo[i].name); + expect(param.type.names[0]).toBe(paramInfo[i].typeName); + if (paramInfo[i].description !== undefined) { + expect(param.description).toBe(paramInfo[i].description); + } + }); + } + + beforeEach(function() { + info = []; + }); + + it('When a function parameter has an inline @type tag, the parameter type is documented', + function() { + var dispense = docSet.getByLongname('dispense')[0]; + info[0] = { name: 'candy', typeName: 'string' }; + + checkParams(dispense, info); + }); + + it('When a function parameter has a standard JSDoc comment and an inline @type tag, the docs ' + + 'reflect the standard JSDoc comment', function() { + var Dispenser = docSet.getByLongname('Dispenser')[0]; + info[0] = { name: 'candyId', typeName: 'number', description: 'The candy\'s identifier.' }; + + checkParams(Dispenser, info); + }); + + it('When a function accepts multiple parameters, and only the first parameter is documented ' + + 'with an inline @type tag, the function parameters are documented in the correct order', + function() { + var restock = docSet.getByLongname('restock')[0]; + info[0] = { name: 'dispenser', typeName: 'Dispenser' }; + info[1] = { name: 'item', typeName: 'string' }; + + checkParams(restock, info); + }); + + it('When a function accepts multiple parameters, and only the last parameter is documented ' + + 'with an inline @type tag, the function parameters are documented in the correct order', + function() { + var clean = docSet.getByLongname('clean')[0]; + info[0] = { name: 'dispenser', typeName: 'Dispenser' }; + info[1] = { name: 'cleaner', typeName: 'string' }; + + checkParams(clean, info); + }); + + it('When a function accepts multiple parameters, and a parameter in the middle is documented ' + + 'with an inline @type tag, the function parameters are documented in the correct order', + function() { + var paint = docSet.getByLongname('paint')[0]; + info[0] = { name: 'dispenser', typeName: 'Dispenser' }; + info[1] = { name: 'color', typeName: 'Color' }; + info[2] = { name: 'shade', typeName: 'number' }; + info[3] = { name: 'brand', typeName: 'string' }; + + checkParams(paint, info); + }); +});