304 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			304 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * @fileoverview enforce or disallow capitalization of the first letter of a comment
 | |
|  * @author Kevin Partington
 | |
|  */
 | |
| "use strict";
 | |
| 
 | |
| //------------------------------------------------------------------------------
 | |
| // 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('../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;
 | |
|             }
 | |
| 
 | |
|             // 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);
 | |
|             }
 | |
|         };
 | |
|     }
 | |
| };
 |