Skip to content

Instantly share code, notes, and snippets.

@gajus
Created August 21, 2015 12:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gajus/e5858033042a8ac6d476 to your computer and use it in GitHub Desktop.
Save gajus/e5858033042a8ac6d476 to your computer and use it in GitHub Desktop.
/**
* @fileoverview Validates JSDoc comments are syntactically correct
* @author Nicholas C. Zakas
* @copyright 2014 Nicholas C. Zakas. All rights reserved.
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
var doctrine = require("doctrine");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = function(context) {
var options = context.options[0] || {},
prefer = options.prefer || {},
// these both default to true, so you have to explicitly make them false
requireReturn = options.requireReturn !== false,
requireParamDescription = options.requireParamDescription !== false,
requireReturnDescription = options.requireReturnDescription !== false;
//--------------------------------------------------------------------------
// Helpers
//--------------------------------------------------------------------------
// Using a stack to store if a function returns or not (handling nested functions)
var fns = [];
/**
* When parsing a new function, store it in our function stack.
* @param {ASTNode} node A function node to check.
* @returns {void}
* @private
*/
function startFunction(node) {
fns.push({
returnPresent: (node.type === "ArrowFunctionExpression" && node.body.type !== "BlockStatement")
});
}
/**
* Indicate that return has been found in the current function.
* @param {ASTNode} node The return node.
* @returns {void}
* @private
*/
function addReturn(node) {
var functionState = fns[fns.length - 1];
if (functionState && node.argument !== null) {
functionState.returnPresent = true;
}
}
/**
* Validate the JSDoc node and output warnings if anything is wrong.
* @param {ASTNode} node The AST node to check.
* @returns {void}
* @private
*/
function checkJSDoc(node) {
var jsdocNode = context.getJSDocComment(node),
functionData = fns.pop(),
hasReturns = false,
hasConstructor = false,
params = Object.create(null),
jsdoc;
// make sure only to validate JSDoc comments
if (jsdocNode) {
try {
jsdoc = doctrine.parse(jsdocNode.value, {
strict: true,
unwrap: true,
sloppy: true
});
} catch (ex) {
if (/braces/i.test(ex.message)) {
context.report(jsdocNode, "JSDoc type missing brace.");
} else {
context.report(jsdocNode, "JSDoc syntax error.");
}
return;
}
jsdoc.tags.forEach(function(tag) {
switch (tag.title) {
case "param":
case "arg":
case "argument":
if (!tag.type) {
context.report(jsdocNode, "Missing JSDoc parameter type for '{{name}}'.", { name: tag.name });
}
if (!tag.description && requireParamDescription) {
context.report(jsdocNode, "Missing JSDoc parameter description for '{{name}}'.", { name: tag.name });
}
if (tag.description && tag.description.indexOf('[].') === 0) {
tag.name += tag.description.split(' ')[0];
}
if (params[tag.name]) {
context.report(jsdocNode, "Duplicate JSDoc parameter '{{name}}'.", { name: tag.name });
} else if (tag.name.indexOf(".") === -1) {
params[tag.name] = 1;
}
break;
case "return":
case "returns":
hasReturns = true;
if (!requireReturn && !functionData.returnPresent && tag.type.name !== "void" && tag.type.name !== "undefined") {
context.report(jsdocNode, "Unexpected @" + tag.title + " tag; function has no return statement.");
} else {
if (!tag.type) {
context.report(jsdocNode, "Missing JSDoc return type.");
}
if (tag.type.name !== "void" && !tag.description && requireReturnDescription) {
context.report(jsdocNode, "Missing JSDoc return description.");
}
}
break;
case "constructor":
case "class":
hasConstructor = true;
break;
// no default
}
// check tag preferences
if (prefer.hasOwnProperty(tag.title)) {
context.report(jsdocNode, "Use @{{name}} instead.", { name: prefer[tag.title] });
}
});
// check for functions missing @returns
if (!hasReturns && !hasConstructor && node.parent.kind !== "get") {
if (requireReturn || functionData.returnPresent) {
context.report(jsdocNode, "Missing JSDoc @returns for function.");
}
}
// check the parameters
var jsdocParams = Object.keys(params);
node.params.forEach(function(param, i) {
var name = param.name;
// TODO(nzakas): Figure out logical things to do with destructured, default, rest params
if (param.type === "Identifier") {
if (jsdocParams[i] && (name !== jsdocParams[i])) {
context.report(jsdocNode, "Expected JSDoc for '{{name}}' but found '{{jsdocName}}'.", {
name: name,
jsdocName: jsdocParams[i]
});
} else if (!params[name]) {
context.report(jsdocNode, "Missing JSDoc for parameter '{{name}}'.", {
name: name
});
}
}
});
}
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
return {
"ArrowFunctionExpression": startFunction,
"FunctionExpression": startFunction,
"FunctionDeclaration": startFunction,
"ArrowFunctionExpression:exit": checkJSDoc,
"FunctionExpression:exit": checkJSDoc,
"FunctionDeclaration:exit": checkJSDoc,
"ReturnStatement": addReturn
};
};
module.exports.schema = [
{
"type": "object",
"properties": {
"prefer": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"requireReturn": {
"type": "boolean"
},
"requireParamDescription": {
"type": "boolean"
},
"requireReturnDescription": {
"type": "boolean"
}
},
"additionalProperties": false
}
];
@brewster1134
Copy link

how did to avoid Cannot find name 'ASTNode'. in your JSDocs? what eslint config prevents that?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment