|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491 |
- /**
- * @fileoverview Validates JSDoc comments are syntactically correct
- * @author Nicholas C. Zakas
- */
- "use strict";
-
- //------------------------------------------------------------------------------
- // Requirements
- //------------------------------------------------------------------------------
-
- const doctrine = require("doctrine");
-
- //------------------------------------------------------------------------------
- // Rule Definition
- //------------------------------------------------------------------------------
-
- module.exports = {
- meta: {
- type: "suggestion",
-
- docs: {
- description: "enforce valid JSDoc comments",
- category: "Possible Errors",
- recommended: false,
- url: "https://eslint.org/docs/rules/valid-jsdoc"
- },
-
- schema: [
- {
- type: "object",
- properties: {
- prefer: {
- type: "object",
- additionalProperties: {
- type: "string"
- }
- },
- preferType: {
- type: "object",
- additionalProperties: {
- type: "string"
- }
- },
- requireReturn: {
- type: "boolean"
- },
- requireParamDescription: {
- type: "boolean"
- },
- requireReturnDescription: {
- type: "boolean"
- },
- matchDescription: {
- type: "string"
- },
- requireReturnType: {
- type: "boolean"
- },
- requireParamType: {
- type: "boolean"
- }
- },
- additionalProperties: false
- }
- ],
-
- fixable: "code"
- },
-
- create(context) {
-
- const options = context.options[0] || {},
- prefer = options.prefer || {},
- sourceCode = context.getSourceCode(),
-
- // 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,
- requireReturnType = options.requireReturnType !== false,
- requireParamType = options.requireParamType !== false,
- preferType = options.preferType || {},
- checkPreferType = Object.keys(preferType).length !== 0;
-
- //--------------------------------------------------------------------------
- // Helpers
- //--------------------------------------------------------------------------
-
- // Using a stack to store if a function returns or not (handling nested functions)
- const fns = [];
-
- /**
- * Check if node type is a Class
- * @param {ASTNode} node node to check.
- * @returns {boolean} True is its a class
- * @private
- */
- function isTypeClass(node) {
- return node.type === "ClassExpression" || node.type === "ClassDeclaration";
- }
-
- /**
- * 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") ||
- isTypeClass(node) || node.async
- });
- }
-
- /**
- * Indicate that return has been found in the current function.
- * @param {ASTNode} node The return node.
- * @returns {void}
- * @private
- */
- function addReturn(node) {
- const functionState = fns[fns.length - 1];
-
- if (functionState && node.argument !== null) {
- functionState.returnPresent = true;
- }
- }
-
- /**
- * Check if return tag type is void or undefined
- * @param {Object} tag JSDoc tag
- * @returns {boolean} True if its of type void or undefined
- * @private
- */
- function isValidReturnType(tag) {
- return tag.type === null || tag.type.name === "void" || tag.type.type === "UndefinedLiteral";
- }
-
- /**
- * Check if type should be validated based on some exceptions
- * @param {Object} type JSDoc tag
- * @returns {boolean} True if it can be validated
- * @private
- */
- function canTypeBeValidated(type) {
- return type !== "UndefinedLiteral" && // {undefined} as there is no name property available.
- type !== "NullLiteral" && // {null}
- type !== "NullableLiteral" && // {?}
- type !== "FunctionType" && // {function(a)}
- type !== "AllLiteral"; // {*}
- }
-
- /**
- * Extract the current and expected type based on the input type object
- * @param {Object} type JSDoc tag
- * @returns {{currentType: Doctrine.Type, expectedTypeName: string}} The current type annotation and
- * the expected name of the annotation
- * @private
- */
- function getCurrentExpectedTypes(type) {
- let currentType;
-
- if (type.name) {
- currentType = type;
- } else if (type.expression) {
- currentType = type.expression;
- }
-
- return {
- currentType,
- expectedTypeName: currentType && preferType[currentType.name]
- };
- }
-
- /**
- * Gets the location of a JSDoc node in a file
- * @param {Token} jsdocComment The comment that this node is parsed from
- * @param {{range: number[]}} parsedJsdocNode A tag or other node which was parsed from this comment
- * @returns {{start: SourceLocation, end: SourceLocation}} The 0-based source location for the tag
- */
- function getAbsoluteRange(jsdocComment, parsedJsdocNode) {
- return {
- start: sourceCode.getLocFromIndex(jsdocComment.range[0] + 2 + parsedJsdocNode.range[0]),
- end: sourceCode.getLocFromIndex(jsdocComment.range[0] + 2 + parsedJsdocNode.range[1])
- };
- }
-
- /**
- * Validate type for a given JSDoc node
- * @param {Object} jsdocNode JSDoc node
- * @param {Object} type JSDoc tag
- * @returns {void}
- * @private
- */
- function validateType(jsdocNode, type) {
- if (!type || !canTypeBeValidated(type.type)) {
- return;
- }
-
- const typesToCheck = [];
- let elements = [];
-
- switch (type.type) {
- case "TypeApplication": // {Array.<String>}
- elements = type.applications[0].type === "UnionType" ? type.applications[0].elements : type.applications;
- typesToCheck.push(getCurrentExpectedTypes(type));
- break;
- case "RecordType": // {{20:String}}
- elements = type.fields;
- break;
- case "UnionType": // {String|number|Test}
- case "ArrayType": // {[String, number, Test]}
- elements = type.elements;
- break;
- case "FieldType": // Array.<{count: number, votes: number}>
- if (type.value) {
- typesToCheck.push(getCurrentExpectedTypes(type.value));
- }
- break;
- default:
- typesToCheck.push(getCurrentExpectedTypes(type));
- }
-
- elements.forEach(validateType.bind(null, jsdocNode));
-
- typesToCheck.forEach(typeToCheck => {
- if (typeToCheck.expectedTypeName &&
- typeToCheck.expectedTypeName !== typeToCheck.currentType.name) {
- context.report({
- node: jsdocNode,
- message: "Use '{{expectedTypeName}}' instead of '{{currentTypeName}}'.",
- loc: getAbsoluteRange(jsdocNode, typeToCheck.currentType),
- data: {
- currentTypeName: typeToCheck.currentType.name,
- expectedTypeName: typeToCheck.expectedTypeName
- },
- fix(fixer) {
- return fixer.replaceTextRange(
- typeToCheck.currentType.range.map(indexInComment => jsdocNode.range[0] + 2 + indexInComment),
- typeToCheck.expectedTypeName
- );
- }
- });
- }
- });
- }
-
- /**
- * 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) {
- const jsdocNode = sourceCode.getJSDocComment(node),
- functionData = fns.pop(),
- paramTagsByName = Object.create(null),
- paramTags = [];
- let hasReturns = false,
- returnsTag,
- hasConstructor = false,
- isInterface = false,
- isOverride = false,
- isAbstract = false;
-
- // make sure only to validate JSDoc comments
- if (jsdocNode) {
- let jsdoc;
-
- try {
- jsdoc = doctrine.parse(jsdocNode.value, {
- strict: true,
- unwrap: true,
- sloppy: true,
- range: true
- });
- } catch (ex) {
-
- if (/braces/i.test(ex.message)) {
- context.report({ node: jsdocNode, message: "JSDoc type missing brace." });
- } else {
- context.report({ node: jsdocNode, message: "JSDoc syntax error." });
- }
-
- return;
- }
-
- jsdoc.tags.forEach(tag => {
-
- switch (tag.title.toLowerCase()) {
-
- case "param":
- case "arg":
- case "argument":
- paramTags.push(tag);
- break;
-
- case "return":
- case "returns":
- hasReturns = true;
- returnsTag = tag;
- break;
-
- case "constructor":
- case "class":
- hasConstructor = true;
- break;
-
- case "override":
- case "inheritdoc":
- isOverride = true;
- break;
-
- case "abstract":
- case "virtual":
- isAbstract = true;
- break;
-
- case "interface":
- isInterface = true;
- break;
-
- // no default
- }
-
- // check tag preferences
- if (Object.prototype.hasOwnProperty.call(prefer, tag.title) && tag.title !== prefer[tag.title]) {
- const entireTagRange = getAbsoluteRange(jsdocNode, tag);
-
- context.report({
- node: jsdocNode,
- message: "Use @{{name}} instead.",
- loc: {
- start: entireTagRange.start,
- end: {
- line: entireTagRange.start.line,
- column: entireTagRange.start.column + `@${tag.title}`.length
- }
- },
- data: { name: prefer[tag.title] },
- fix(fixer) {
- return fixer.replaceTextRange(
- [
- jsdocNode.range[0] + tag.range[0] + 3,
- jsdocNode.range[0] + tag.range[0] + tag.title.length + 3
- ],
- prefer[tag.title]
- );
- }
- });
- }
-
- // validate the types
- if (checkPreferType && tag.type) {
- validateType(jsdocNode, tag.type);
- }
- });
-
- paramTags.forEach(param => {
- if (requireParamType && !param.type) {
- context.report({
- node: jsdocNode,
- message: "Missing JSDoc parameter type for '{{name}}'.",
- loc: getAbsoluteRange(jsdocNode, param),
- data: { name: param.name }
- });
- }
- if (!param.description && requireParamDescription) {
- context.report({
- node: jsdocNode,
- message: "Missing JSDoc parameter description for '{{name}}'.",
- loc: getAbsoluteRange(jsdocNode, param),
- data: { name: param.name }
- });
- }
- if (paramTagsByName[param.name]) {
- context.report({
- node: jsdocNode,
- message: "Duplicate JSDoc parameter '{{name}}'.",
- loc: getAbsoluteRange(jsdocNode, param),
- data: { name: param.name }
- });
- } else if (param.name.indexOf(".") === -1) {
- paramTagsByName[param.name] = param;
- }
- });
-
- if (hasReturns) {
- if (!requireReturn && !functionData.returnPresent && (returnsTag.type === null || !isValidReturnType(returnsTag)) && !isAbstract) {
- context.report({
- node: jsdocNode,
- message: "Unexpected @{{title}} tag; function has no return statement.",
- loc: getAbsoluteRange(jsdocNode, returnsTag),
- data: {
- title: returnsTag.title
- }
- });
- } else {
- if (requireReturnType && !returnsTag.type) {
- context.report({ node: jsdocNode, message: "Missing JSDoc return type." });
- }
-
- if (!isValidReturnType(returnsTag) && !returnsTag.description && requireReturnDescription) {
- context.report({ node: jsdocNode, message: "Missing JSDoc return description." });
- }
- }
- }
-
- // check for functions missing @returns
- if (!isOverride && !hasReturns && !hasConstructor && !isInterface &&
- node.parent.kind !== "get" && node.parent.kind !== "constructor" &&
- node.parent.kind !== "set" && !isTypeClass(node)) {
- if (requireReturn || (functionData.returnPresent && !node.async)) {
- context.report({
- node: jsdocNode,
- message: "Missing JSDoc @{{returns}} for function.",
- data: {
- returns: prefer.returns || "returns"
- }
- });
- }
- }
-
- // check the parameters
- const jsdocParamNames = Object.keys(paramTagsByName);
-
- if (node.params) {
- node.params.forEach((param, paramsIndex) => {
- const bindingParam = param.type === "AssignmentPattern"
- ? param.left
- : param;
-
- // TODO(nzakas): Figure out logical things to do with destructured, default, rest params
- if (bindingParam.type === "Identifier") {
- const name = bindingParam.name;
-
- if (jsdocParamNames[paramsIndex] && (name !== jsdocParamNames[paramsIndex])) {
- context.report({
- node: jsdocNode,
- message: "Expected JSDoc for '{{name}}' but found '{{jsdocName}}'.",
- loc: getAbsoluteRange(jsdocNode, paramTagsByName[jsdocParamNames[paramsIndex]]),
- data: {
- name,
- jsdocName: jsdocParamNames[paramsIndex]
- }
- });
- } else if (!paramTagsByName[name] && !isOverride) {
- context.report({
- node: jsdocNode,
- message: "Missing JSDoc for parameter '{{name}}'.",
- data: {
- name
- }
- });
- }
- }
- });
- }
-
- if (options.matchDescription) {
- const regex = new RegExp(options.matchDescription);
-
- if (!regex.test(jsdoc.description)) {
- context.report({ node: jsdocNode, message: "JSDoc description does not satisfy the regex pattern." });
- }
- }
-
- }
-
- }
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- return {
- ArrowFunctionExpression: startFunction,
- FunctionExpression: startFunction,
- FunctionDeclaration: startFunction,
- ClassExpression: startFunction,
- ClassDeclaration: startFunction,
- "ArrowFunctionExpression:exit": checkJSDoc,
- "FunctionExpression:exit": checkJSDoc,
- "FunctionDeclaration:exit": checkJSDoc,
- "ClassExpression:exit": checkJSDoc,
- "ClassDeclaration:exit": checkJSDoc,
- ReturnStatement: addReturn
- };
-
- }
- };
|