517 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			517 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/**
 | 
						|
 * @fileoverview Validates JSDoc comments are syntactically correct
 | 
						|
 * @author Nicholas C. Zakas
 | 
						|
 * @deprecated in ESLint v5.10.0
 | 
						|
 */
 | 
						|
"use strict";
 | 
						|
 | 
						|
//------------------------------------------------------------------------------
 | 
						|
// Requirements
 | 
						|
//------------------------------------------------------------------------------
 | 
						|
 | 
						|
const doctrine = require("doctrine");
 | 
						|
 | 
						|
//------------------------------------------------------------------------------
 | 
						|
// Rule Definition
 | 
						|
//------------------------------------------------------------------------------
 | 
						|
 | 
						|
/** @type {import('../shared/types').Rule} */
 | 
						|
module.exports = {
 | 
						|
    meta: {
 | 
						|
        type: "suggestion",
 | 
						|
 | 
						|
        docs: {
 | 
						|
            description: "Enforce valid JSDoc comments",
 | 
						|
            recommended: false,
 | 
						|
            url: "https://eslint.org/docs/latest/rules/valid-jsdoc"
 | 
						|
        },
 | 
						|
 | 
						|
        schema: [
 | 
						|
            {
 | 
						|
                type: "object",
 | 
						|
                properties: {
 | 
						|
                    prefer: {
 | 
						|
                        type: "object",
 | 
						|
                        additionalProperties: {
 | 
						|
                            type: "string"
 | 
						|
                        }
 | 
						|
                    },
 | 
						|
                    preferType: {
 | 
						|
                        type: "object",
 | 
						|
                        additionalProperties: {
 | 
						|
                            type: "string"
 | 
						|
                        }
 | 
						|
                    },
 | 
						|
                    requireReturn: {
 | 
						|
                        type: "boolean",
 | 
						|
                        default: true
 | 
						|
                    },
 | 
						|
                    requireParamDescription: {
 | 
						|
                        type: "boolean",
 | 
						|
                        default: true
 | 
						|
                    },
 | 
						|
                    requireReturnDescription: {
 | 
						|
                        type: "boolean",
 | 
						|
                        default: true
 | 
						|
                    },
 | 
						|
                    matchDescription: {
 | 
						|
                        type: "string"
 | 
						|
                    },
 | 
						|
                    requireReturnType: {
 | 
						|
                        type: "boolean",
 | 
						|
                        default: true
 | 
						|
                    },
 | 
						|
                    requireParamType: {
 | 
						|
                        type: "boolean",
 | 
						|
                        default: true
 | 
						|
                    }
 | 
						|
                },
 | 
						|
                additionalProperties: false
 | 
						|
            }
 | 
						|
        ],
 | 
						|
 | 
						|
        fixable: "code",
 | 
						|
        messages: {
 | 
						|
            unexpectedTag: "Unexpected @{{title}} tag; function has no return statement.",
 | 
						|
            expected: "Expected JSDoc for '{{name}}' but found '{{jsdocName}}'.",
 | 
						|
            use: "Use @{{name}} instead.",
 | 
						|
            useType: "Use '{{expectedTypeName}}' instead of '{{currentTypeName}}'.",
 | 
						|
            syntaxError: "JSDoc syntax error.",
 | 
						|
            missingBrace: "JSDoc type missing brace.",
 | 
						|
            missingParamDesc: "Missing JSDoc parameter description for '{{name}}'.",
 | 
						|
            missingParamType: "Missing JSDoc parameter type for '{{name}}'.",
 | 
						|
            missingReturnType: "Missing JSDoc return type.",
 | 
						|
            missingReturnDesc: "Missing JSDoc return description.",
 | 
						|
            missingReturn: "Missing JSDoc @{{returns}} for function.",
 | 
						|
            missingParam: "Missing JSDoc for parameter '{{name}}'.",
 | 
						|
            duplicateParam: "Duplicate JSDoc parameter '{{name}}'.",
 | 
						|
            unsatisfiedDesc: "JSDoc description does not satisfy the regex pattern."
 | 
						|
        },
 | 
						|
 | 
						|
        deprecated: true,
 | 
						|
        replacedBy: []
 | 
						|
    },
 | 
						|
 | 
						|
    create(context) {
 | 
						|
 | 
						|
        const options = context.options[0] || {},
 | 
						|
            prefer = options.prefer || {},
 | 
						|
            sourceCode = context.sourceCode,
 | 
						|
 | 
						|
            // 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,
 | 
						|
                        messageId: "useType",
 | 
						|
                        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/iu.test(ex.message)) {
 | 
						|
                        context.report({ node: jsdocNode, messageId: "missingBrace" });
 | 
						|
                    } else {
 | 
						|
                        context.report({ node: jsdocNode, messageId: "syntaxError" });
 | 
						|
                    }
 | 
						|
 | 
						|
                    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,
 | 
						|
                            messageId: "use",
 | 
						|
                            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,
 | 
						|
                            messageId: "missingParamType",
 | 
						|
                            loc: getAbsoluteRange(jsdocNode, param),
 | 
						|
                            data: { name: param.name }
 | 
						|
                        });
 | 
						|
                    }
 | 
						|
                    if (!param.description && requireParamDescription) {
 | 
						|
                        context.report({
 | 
						|
                            node: jsdocNode,
 | 
						|
                            messageId: "missingParamDesc",
 | 
						|
                            loc: getAbsoluteRange(jsdocNode, param),
 | 
						|
                            data: { name: param.name }
 | 
						|
                        });
 | 
						|
                    }
 | 
						|
                    if (paramTagsByName[param.name]) {
 | 
						|
                        context.report({
 | 
						|
                            node: jsdocNode,
 | 
						|
                            messageId: "duplicateParam",
 | 
						|
                            loc: getAbsoluteRange(jsdocNode, param),
 | 
						|
                            data: { name: param.name }
 | 
						|
                        });
 | 
						|
                    } else if (!param.name.includes(".")) {
 | 
						|
                        paramTagsByName[param.name] = param;
 | 
						|
                    }
 | 
						|
                });
 | 
						|
 | 
						|
                if (hasReturns) {
 | 
						|
                    if (!requireReturn && !functionData.returnPresent && (returnsTag.type === null || !isValidReturnType(returnsTag)) && !isAbstract) {
 | 
						|
                        context.report({
 | 
						|
                            node: jsdocNode,
 | 
						|
                            messageId: "unexpectedTag",
 | 
						|
                            loc: getAbsoluteRange(jsdocNode, returnsTag),
 | 
						|
                            data: {
 | 
						|
                                title: returnsTag.title
 | 
						|
                            }
 | 
						|
                        });
 | 
						|
                    } else {
 | 
						|
                        if (requireReturnType && !returnsTag.type) {
 | 
						|
                            context.report({ node: jsdocNode, messageId: "missingReturnType" });
 | 
						|
                        }
 | 
						|
 | 
						|
                        if (!isValidReturnType(returnsTag) && !returnsTag.description && requireReturnDescription) {
 | 
						|
                            context.report({ node: jsdocNode, messageId: "missingReturnDesc" });
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                }
 | 
						|
 | 
						|
                // 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,
 | 
						|
                            messageId: "missingReturn",
 | 
						|
                            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,
 | 
						|
                                    messageId: "expected",
 | 
						|
                                    loc: getAbsoluteRange(jsdocNode, paramTagsByName[jsdocParamNames[paramsIndex]]),
 | 
						|
                                    data: {
 | 
						|
                                        name,
 | 
						|
                                        jsdocName: jsdocParamNames[paramsIndex]
 | 
						|
                                    }
 | 
						|
                                });
 | 
						|
                            } else if (!paramTagsByName[name] && !isOverride) {
 | 
						|
                                context.report({
 | 
						|
                                    node: jsdocNode,
 | 
						|
                                    messageId: "missingParam",
 | 
						|
                                    data: {
 | 
						|
                                        name
 | 
						|
                                    }
 | 
						|
                                });
 | 
						|
                            }
 | 
						|
                        }
 | 
						|
                    });
 | 
						|
                }
 | 
						|
 | 
						|
                if (options.matchDescription) {
 | 
						|
                    const regex = new RegExp(options.matchDescription, "u");
 | 
						|
 | 
						|
                    if (!regex.test(jsdoc.description)) {
 | 
						|
                        context.report({ node: jsdocNode, messageId: "unsatisfiedDesc" });
 | 
						|
                    }
 | 
						|
                }
 | 
						|
 | 
						|
            }
 | 
						|
 | 
						|
        }
 | 
						|
 | 
						|
        //--------------------------------------------------------------------------
 | 
						|
        // 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
 | 
						|
        };
 | 
						|
 | 
						|
    }
 | 
						|
};
 |