|                                                                                                                                                                                                                                                                                                            |  | /** * @fileoverview enforce or disallow capitalization of the first letter of a comment * @author Kevin Partington */"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const LETTER_PATTERN = require("./utils/patterns/letters");const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
const DEFAULT_IGNORE_PATTERN = astUtils.COMMENTS_IGNORE_PATTERN,    WHITESPACE = /\s/gu,    MAYBE_URL = /^\s*[^:/?#\s]+:\/\/[^?#]/u; // TODO: Combine w/ max-len pattern?
/* * Base schema body for defining the basic capitalization rule, ignorePattern, * and ignoreInlineComments values. * This can be used in a few different ways in the actual schema. */const SCHEMA_BODY = {    type: "object",    properties: {        ignorePattern: {            type: "string"        },        ignoreInlineComments: {            type: "boolean"        },        ignoreConsecutiveComments: {            type: "boolean"        }    },    additionalProperties: false};const DEFAULTS = {    ignorePattern: "",    ignoreInlineComments: false,    ignoreConsecutiveComments: false};
/** * Get normalized options for either block or line comments from the given * user-provided options. * - If the user-provided options is just a string, returns a normalized *   set of options using default values for all other options. * - If the user-provided options is an object, then a normalized option *   set is returned. Options specified in overrides will take priority *   over options specified in the main options object, which will in *   turn take priority over the rule's defaults. * @param {Object|string} rawOptions The user-provided options. * @param {string} which Either "line" or "block". * @returns {Object} The normalized options. */function getNormalizedOptions(rawOptions, which) {    return Object.assign({}, DEFAULTS, rawOptions[which] || rawOptions);}
/** * Get normalized options for block and line comments. * @param {Object|string} rawOptions The user-provided options. * @returns {Object} An object with "Line" and "Block" keys and corresponding * normalized options objects. */function getAllNormalizedOptions(rawOptions = {}) {    return {        Line: getNormalizedOptions(rawOptions, "line"),        Block: getNormalizedOptions(rawOptions, "block")    };}
/** * Creates a regular expression for each ignorePattern defined in the rule * options. * * This is done in order to avoid invoking the RegExp constructor repeatedly. * @param {Object} normalizedOptions The normalized rule options. * @returns {void} */function createRegExpForIgnorePatterns(normalizedOptions) {    Object.keys(normalizedOptions).forEach(key => {        const ignorePatternStr = normalizedOptions[key].ignorePattern;
        if (ignorePatternStr) {            const regExp = RegExp(`^\\s*(?:${ignorePatternStr})`, "u");
            normalizedOptions[key].ignorePatternRegExp = regExp;        }    });}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../shared/types').Rule} */module.exports = {    meta: {        type: "suggestion",
        docs: {            description: "Enforce or disallow capitalization of the first letter of a comment",            recommended: false,            url: "https://eslint.org/docs/latest/rules/capitalized-comments"        },
        fixable: "code",
        schema: [            { enum: ["always", "never"] },            {                oneOf: [                    SCHEMA_BODY,                    {                        type: "object",                        properties: {                            line: SCHEMA_BODY,                            block: SCHEMA_BODY                        },                        additionalProperties: false                    }                ]            }        ],
        messages: {            unexpectedLowercaseComment: "Comments should not begin with a lowercase character.",            unexpectedUppercaseComment: "Comments should not begin with an uppercase character."        }    },
    create(context) {
        const capitalize = context.options[0] || "always",            normalizedOptions = getAllNormalizedOptions(context.options[1]),            sourceCode = context.sourceCode;
        createRegExpForIgnorePatterns(normalizedOptions);
        //----------------------------------------------------------------------
        // Helpers
        //----------------------------------------------------------------------
        /**         * Checks whether a comment is an inline comment.         *         * For the purpose of this rule, a comment is inline if:         * 1. The comment is preceded by a token on the same line; and         * 2. The command is followed by a token on the same line.         *         * Note that the comment itself need not be single-line!         *         * Also, it follows from this definition that only block comments can         * be considered as possibly inline. This is because line comments         * would consume any following tokens on the same line as the comment.         * @param {ASTNode} comment The comment node to check.         * @returns {boolean} True if the comment is an inline comment, false         * otherwise.         */        function isInlineComment(comment) {            const previousToken = sourceCode.getTokenBefore(comment, { includeComments: true }),                nextToken = sourceCode.getTokenAfter(comment, { includeComments: true });
            return Boolean(                previousToken &&                nextToken &&                comment.loc.start.line === previousToken.loc.end.line &&                comment.loc.end.line === nextToken.loc.start.line            );        }
        /**         * Determine if a comment follows another comment.         * @param {ASTNode} comment The comment to check.         * @returns {boolean} True if the comment follows a valid comment.         */        function isConsecutiveComment(comment) {            const previousTokenOrComment = sourceCode.getTokenBefore(comment, { includeComments: true });
            return Boolean(                previousTokenOrComment &&                ["Block", "Line"].includes(previousTokenOrComment.type)            );        }
        /**         * Check a comment to determine if it is valid for this rule.         * @param {ASTNode} comment The comment node to process.         * @param {Object} options The options for checking this comment.         * @returns {boolean} True if the comment is valid, false otherwise.         */        function isCommentValid(comment, options) {
            // 1. Check for default ignore pattern.
            if (DEFAULT_IGNORE_PATTERN.test(comment.value)) {                return true;            }
            // 2. Check for custom ignore pattern.
            const commentWithoutAsterisks = comment.value                .replace(/\*/gu, "");
            if (options.ignorePatternRegExp && options.ignorePatternRegExp.test(commentWithoutAsterisks)) {                return true;            }
            // 3. Check for inline comments.
            if (options.ignoreInlineComments && isInlineComment(comment)) {                return true;            }
            // 4. Is this a consecutive comment (and are we tolerating those)?
            if (options.ignoreConsecutiveComments && isConsecutiveComment(comment)) {                return true;            }
            // 5. Does the comment start with a possible URL?
            if (MAYBE_URL.test(commentWithoutAsterisks)) {                return true;            }
            // 6. Is the initial word character a letter?
            const commentWordCharsOnly = commentWithoutAsterisks                .replace(WHITESPACE, "");
            if (commentWordCharsOnly.length === 0) {                return true;            }
            const firstWordChar = commentWordCharsOnly[0];
            if (!LETTER_PATTERN.test(firstWordChar)) {                return true;            }
            // 7. Check the case of the initial word character.
            const isUppercase = firstWordChar !== firstWordChar.toLocaleLowerCase(),                isLowercase = firstWordChar !== firstWordChar.toLocaleUpperCase();
            if (capitalize === "always" && isLowercase) {                return false;            }            if (capitalize === "never" && isUppercase) {                return false;            }
            return true;        }
        /**         * Process a comment to determine if it needs to be reported.         * @param {ASTNode} comment The comment node to process.         * @returns {void}         */        function processComment(comment) {            const options = normalizedOptions[comment.type],                commentValid = isCommentValid(comment, options);
            if (!commentValid) {                const messageId = capitalize === "always"                    ? "unexpectedLowercaseComment"                    : "unexpectedUppercaseComment";
                context.report({                    node: null, // Intentionally using loc instead
                    loc: comment.loc,                    messageId,                    fix(fixer) {                        const match = comment.value.match(LETTER_PATTERN);
                        return fixer.replaceTextRange(
                            // Offset match.index by 2 to account for the first 2 characters that start the comment (// or /*)
                            [comment.range[0] + match.index + 2, comment.range[0] + match.index + 3],                            capitalize === "always" ? match[0].toLocaleUpperCase() : match[0].toLocaleLowerCase()                        );                    }                });            }        }
        //----------------------------------------------------------------------
        // Public
        //----------------------------------------------------------------------
        return {            Program() {                const comments = sourceCode.getAllComments();
                comments.filter(token => token.type !== "Shebang").forEach(processComment);            }        };    }};
 |