|
|
/** * @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
//------------------------------------------------------------------------------
module.exports = { meta: { type: "suggestion",
docs: { description: "enforce or disallow capitalization of the first letter of a comment", category: "Stylistic Issues", recommended: false, url: "https://eslint.org/docs/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.getSourceCode();
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"].indexOf(previousTokenOrComment.type) !== -1 ); }
/** * 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); } }; } };
|