1/** 2 * @fileoverview enforce or disallow capitalization of the first letter of a comment 3 * @author Kevin Partington 4 */ 5"use strict"; 6 7//------------------------------------------------------------------------------ 8// Requirements 9//------------------------------------------------------------------------------ 10 11const LETTER_PATTERN = require("./utils/patterns/letters"); 12const astUtils = require("./utils/ast-utils"); 13 14//------------------------------------------------------------------------------ 15// Helpers 16//------------------------------------------------------------------------------ 17 18const DEFAULT_IGNORE_PATTERN = astUtils.COMMENTS_IGNORE_PATTERN, 19 WHITESPACE = /\s/gu, 20 MAYBE_URL = /^\s*[^:/?#\s]+:\/\/[^?#]/u; // TODO: Combine w/ max-len pattern? 21 22/* 23 * Base schema body for defining the basic capitalization rule, ignorePattern, 24 * and ignoreInlineComments values. 25 * This can be used in a few different ways in the actual schema. 26 */ 27const SCHEMA_BODY = { 28 type: "object", 29 properties: { 30 ignorePattern: { 31 type: "string" 32 }, 33 ignoreInlineComments: { 34 type: "boolean" 35 }, 36 ignoreConsecutiveComments: { 37 type: "boolean" 38 } 39 }, 40 additionalProperties: false 41}; 42const DEFAULTS = { 43 ignorePattern: "", 44 ignoreInlineComments: false, 45 ignoreConsecutiveComments: false 46}; 47 48/** 49 * Get normalized options for either block or line comments from the given 50 * user-provided options. 51 * - If the user-provided options is just a string, returns a normalized 52 * set of options using default values for all other options. 53 * - If the user-provided options is an object, then a normalized option 54 * set is returned. Options specified in overrides will take priority 55 * over options specified in the main options object, which will in 56 * turn take priority over the rule's defaults. 57 * @param {Object|string} rawOptions The user-provided options. 58 * @param {string} which Either "line" or "block". 59 * @returns {Object} The normalized options. 60 */ 61function getNormalizedOptions(rawOptions, which) { 62 return Object.assign({}, DEFAULTS, rawOptions[which] || rawOptions); 63} 64 65/** 66 * Get normalized options for block and line comments. 67 * @param {Object|string} rawOptions The user-provided options. 68 * @returns {Object} An object with "Line" and "Block" keys and corresponding 69 * normalized options objects. 70 */ 71function getAllNormalizedOptions(rawOptions = {}) { 72 return { 73 Line: getNormalizedOptions(rawOptions, "line"), 74 Block: getNormalizedOptions(rawOptions, "block") 75 }; 76} 77 78/** 79 * Creates a regular expression for each ignorePattern defined in the rule 80 * options. 81 * 82 * This is done in order to avoid invoking the RegExp constructor repeatedly. 83 * @param {Object} normalizedOptions The normalized rule options. 84 * @returns {void} 85 */ 86function createRegExpForIgnorePatterns(normalizedOptions) { 87 Object.keys(normalizedOptions).forEach(key => { 88 const ignorePatternStr = normalizedOptions[key].ignorePattern; 89 90 if (ignorePatternStr) { 91 const regExp = RegExp(`^\\s*(?:${ignorePatternStr})`, "u"); 92 93 normalizedOptions[key].ignorePatternRegExp = regExp; 94 } 95 }); 96} 97 98//------------------------------------------------------------------------------ 99// Rule Definition 100//------------------------------------------------------------------------------ 101 102module.exports = { 103 meta: { 104 type: "suggestion", 105 106 docs: { 107 description: "enforce or disallow capitalization of the first letter of a comment", 108 category: "Stylistic Issues", 109 recommended: false, 110 url: "https://eslint.org/docs/rules/capitalized-comments" 111 }, 112 113 fixable: "code", 114 115 schema: [ 116 { enum: ["always", "never"] }, 117 { 118 oneOf: [ 119 SCHEMA_BODY, 120 { 121 type: "object", 122 properties: { 123 line: SCHEMA_BODY, 124 block: SCHEMA_BODY 125 }, 126 additionalProperties: false 127 } 128 ] 129 } 130 ], 131 132 messages: { 133 unexpectedLowercaseComment: "Comments should not begin with a lowercase character.", 134 unexpectedUppercaseComment: "Comments should not begin with an uppercase character." 135 } 136 }, 137 138 create(context) { 139 140 const capitalize = context.options[0] || "always", 141 normalizedOptions = getAllNormalizedOptions(context.options[1]), 142 sourceCode = context.getSourceCode(); 143 144 createRegExpForIgnorePatterns(normalizedOptions); 145 146 //---------------------------------------------------------------------- 147 // Helpers 148 //---------------------------------------------------------------------- 149 150 /** 151 * Checks whether a comment is an inline comment. 152 * 153 * For the purpose of this rule, a comment is inline if: 154 * 1. The comment is preceded by a token on the same line; and 155 * 2. The command is followed by a token on the same line. 156 * 157 * Note that the comment itself need not be single-line! 158 * 159 * Also, it follows from this definition that only block comments can 160 * be considered as possibly inline. This is because line comments 161 * would consume any following tokens on the same line as the comment. 162 * @param {ASTNode} comment The comment node to check. 163 * @returns {boolean} True if the comment is an inline comment, false 164 * otherwise. 165 */ 166 function isInlineComment(comment) { 167 const previousToken = sourceCode.getTokenBefore(comment, { includeComments: true }), 168 nextToken = sourceCode.getTokenAfter(comment, { includeComments: true }); 169 170 return Boolean( 171 previousToken && 172 nextToken && 173 comment.loc.start.line === previousToken.loc.end.line && 174 comment.loc.end.line === nextToken.loc.start.line 175 ); 176 } 177 178 /** 179 * Determine if a comment follows another comment. 180 * @param {ASTNode} comment The comment to check. 181 * @returns {boolean} True if the comment follows a valid comment. 182 */ 183 function isConsecutiveComment(comment) { 184 const previousTokenOrComment = sourceCode.getTokenBefore(comment, { includeComments: true }); 185 186 return Boolean( 187 previousTokenOrComment && 188 ["Block", "Line"].indexOf(previousTokenOrComment.type) !== -1 189 ); 190 } 191 192 /** 193 * Check a comment to determine if it is valid for this rule. 194 * @param {ASTNode} comment The comment node to process. 195 * @param {Object} options The options for checking this comment. 196 * @returns {boolean} True if the comment is valid, false otherwise. 197 */ 198 function isCommentValid(comment, options) { 199 200 // 1. Check for default ignore pattern. 201 if (DEFAULT_IGNORE_PATTERN.test(comment.value)) { 202 return true; 203 } 204 205 // 2. Check for custom ignore pattern. 206 const commentWithoutAsterisks = comment.value 207 .replace(/\*/gu, ""); 208 209 if (options.ignorePatternRegExp && options.ignorePatternRegExp.test(commentWithoutAsterisks)) { 210 return true; 211 } 212 213 // 3. Check for inline comments. 214 if (options.ignoreInlineComments && isInlineComment(comment)) { 215 return true; 216 } 217 218 // 4. Is this a consecutive comment (and are we tolerating those)? 219 if (options.ignoreConsecutiveComments && isConsecutiveComment(comment)) { 220 return true; 221 } 222 223 // 5. Does the comment start with a possible URL? 224 if (MAYBE_URL.test(commentWithoutAsterisks)) { 225 return true; 226 } 227 228 // 6. Is the initial word character a letter? 229 const commentWordCharsOnly = commentWithoutAsterisks 230 .replace(WHITESPACE, ""); 231 232 if (commentWordCharsOnly.length === 0) { 233 return true; 234 } 235 236 const firstWordChar = commentWordCharsOnly[0]; 237 238 if (!LETTER_PATTERN.test(firstWordChar)) { 239 return true; 240 } 241 242 // 7. Check the case of the initial word character. 243 const isUppercase = firstWordChar !== firstWordChar.toLocaleLowerCase(), 244 isLowercase = firstWordChar !== firstWordChar.toLocaleUpperCase(); 245 246 if (capitalize === "always" && isLowercase) { 247 return false; 248 } 249 if (capitalize === "never" && isUppercase) { 250 return false; 251 } 252 253 return true; 254 } 255 256 /** 257 * Process a comment to determine if it needs to be reported. 258 * @param {ASTNode} comment The comment node to process. 259 * @returns {void} 260 */ 261 function processComment(comment) { 262 const options = normalizedOptions[comment.type], 263 commentValid = isCommentValid(comment, options); 264 265 if (!commentValid) { 266 const messageId = capitalize === "always" 267 ? "unexpectedLowercaseComment" 268 : "unexpectedUppercaseComment"; 269 270 context.report({ 271 node: null, // Intentionally using loc instead 272 loc: comment.loc, 273 messageId, 274 fix(fixer) { 275 const match = comment.value.match(LETTER_PATTERN); 276 277 return fixer.replaceTextRange( 278 279 // Offset match.index by 2 to account for the first 2 characters that start the comment (// or /*) 280 [comment.range[0] + match.index + 2, comment.range[0] + match.index + 3], 281 capitalize === "always" ? match[0].toLocaleUpperCase() : match[0].toLocaleLowerCase() 282 ); 283 } 284 }); 285 } 286 } 287 288 //---------------------------------------------------------------------- 289 // Public 290 //---------------------------------------------------------------------- 291 292 return { 293 Program() { 294 const comments = sourceCode.getAllComments(); 295 296 comments.filter(token => token.type !== "Shebang").forEach(processComment); 297 } 298 }; 299 } 300}; 301