attach inline type annotations to function params (#611)

includes a new Rhino jar: jsdoc3/rhino@bb2446ad
This commit is contained in:
Jeff Williams 2014-03-30 22:00:33 -07:00
parent 8df4472a2d
commit 3e4e48accd
8 changed files with 257 additions and 14 deletions

View File

@ -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) ) {

View File

@ -362,6 +362,7 @@ var DOCLET_SCHEMA = exports.DOCLET_SCHEMA = {
'module',
'namespace',
'package',
'param',
'typedef'
]
},

View File

@ -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);

View File

@ -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;

View File

@ -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.

Binary file not shown.

35
test/fixtures/typetaginline.js vendored Normal file
View File

@ -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) {}

View File

@ -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);
});
});