123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- /**
- * @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/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"].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);
- }
- };
- }
- };
|