mirror of
https://github.com/jsdoc/jsdoc.git
synced 2025-12-08 19:46:11 +00:00
attach inline type annotations to function params (#611)
includes a new Rhino jar: jsdoc3/rhino@bb2446ad
This commit is contained in:
parent
8df4472a2d
commit
3e4e48accd
@ -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) ) {
|
||||
|
||||
@ -362,6 +362,7 @@ var DOCLET_SCHEMA = exports.DOCLET_SCHEMA = {
|
||||
'module',
|
||||
'namespace',
|
||||
'package',
|
||||
'param',
|
||||
'typedef'
|
||||
]
|
||||
},
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.
|
||||
|
||||
BIN
rhino/js.jar
BIN
rhino/js.jar
Binary file not shown.
35
test/fixtures/typetaginline.js
vendored
Normal file
35
test/fixtures/typetaginline.js
vendored
Normal 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) {}
|
||||
71
test/specs/documentation/typetaginline.js
Normal file
71
test/specs/documentation/typetaginline.js
Normal 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);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user