Spaces:
Sleeping
Sleeping
| /** | |
| * @fileoverview enforce or disallow capitalization of the first letter of a comment | |
| * @author Kevin Partington | |
| */ | |
| ; | |
| //------------------------------------------------------------------------------ | |
| // Requirements | |
| //------------------------------------------------------------------------------ | |
| 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? | |
| LETTER_PATTERN = /\p{L}/u; | |
| /* | |
| * 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('../types').Rule.RuleModule} */ | |
| module.exports = { | |
| meta: { | |
| type: "suggestion", | |
| docs: { | |
| description: | |
| "Enforce or disallow capitalization of the first letter of a comment", | |
| recommended: false, | |
| frozen: true, | |
| 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; | |
| } | |
| // Get the first Unicode character (1 or 2 code units). | |
| const [firstWordChar] = commentWordCharsOnly; | |
| 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); | |
| const char = match[0]; | |
| // Offset match.index by 2 to account for the first 2 characters that start the comment (// or /*) | |
| const charIndex = comment.range[0] + match.index + 2; | |
| return fixer.replaceTextRange( | |
| [charIndex, charIndex + char.length], | |
| capitalize === "always" | |
| ? char.toLocaleUpperCase() | |
| : char.toLocaleLowerCase(), | |
| ); | |
| }, | |
| }); | |
| } | |
| } | |
| //---------------------------------------------------------------------- | |
| // Public | |
| //---------------------------------------------------------------------- | |
| return { | |
| Program() { | |
| const comments = sourceCode.getAllComments(); | |
| comments | |
| .filter(token => token.type !== "Shebang") | |
| .forEach(processComment); | |
| }, | |
| }; | |
| }, | |
| }; | |