2 * @fileoverview enforce or disallow capitalization of the first letter of a comment
3 * @author Kevin Partington
7 //------------------------------------------------------------------------------
9 //------------------------------------------------------------------------------
11 const LETTER_PATTERN = require("./utils/patterns/letters");
12 const astUtils = require("./utils/ast-utils");
14 //------------------------------------------------------------------------------
16 //------------------------------------------------------------------------------
18 const DEFAULT_IGNORE_PATTERN = astUtils.COMMENTS_IGNORE_PATTERN,
20 MAYBE_URL = /^\s*[^:/?#\s]+:\/\/[^?#]/u; // TODO: Combine w/ max-len pattern?
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.
33 ignoreInlineComments: {
36 ignoreConsecutiveComments: {
40 additionalProperties: false
44 ignoreInlineComments: false,
45 ignoreConsecutiveComments: false
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.
61 function getNormalizedOptions(rawOptions, which) {
62 return Object.assign({}, DEFAULTS, rawOptions[which] || rawOptions);
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.
71 function getAllNormalizedOptions(rawOptions = {}) {
73 Line: getNormalizedOptions(rawOptions, "line"),
74 Block: getNormalizedOptions(rawOptions, "block")
79 * Creates a regular expression for each ignorePattern defined in the rule
82 * This is done in order to avoid invoking the RegExp constructor repeatedly.
83 * @param {Object} normalizedOptions The normalized rule options.
86 function createRegExpForIgnorePatterns(normalizedOptions) {
87 Object.keys(normalizedOptions).forEach(key => {
88 const ignorePatternStr = normalizedOptions[key].ignorePattern;
90 if (ignorePatternStr) {
91 const regExp = RegExp(`^\\s*(?:${ignorePatternStr})`, "u");
93 normalizedOptions[key].ignorePatternRegExp = regExp;
98 //------------------------------------------------------------------------------
100 //------------------------------------------------------------------------------
107 description: "enforce or disallow capitalization of the first letter of a comment",
108 category: "Stylistic Issues",
110 url: "https://eslint.org/docs/rules/capitalized-comments"
116 { enum: ["always", "never"] },
126 additionalProperties: false
133 unexpectedLowercaseComment: "Comments should not begin with a lowercase character.",
134 unexpectedUppercaseComment: "Comments should not begin with an uppercase character."
140 const capitalize = context.options[0] || "always",
141 normalizedOptions = getAllNormalizedOptions(context.options[1]),
142 sourceCode = context.getSourceCode();
144 createRegExpForIgnorePatterns(normalizedOptions);
146 //----------------------------------------------------------------------
148 //----------------------------------------------------------------------
151 * Checks whether a comment is an inline comment.
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.
157 * Note that the comment itself need not be single-line!
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
166 function isInlineComment(comment) {
167 const previousToken = sourceCode.getTokenBefore(comment, { includeComments: true }),
168 nextToken = sourceCode.getTokenAfter(comment, { includeComments: true });
173 comment.loc.start.line === previousToken.loc.end.line &&
174 comment.loc.end.line === nextToken.loc.start.line
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.
183 function isConsecutiveComment(comment) {
184 const previousTokenOrComment = sourceCode.getTokenBefore(comment, { includeComments: true });
187 previousTokenOrComment &&
188 ["Block", "Line"].indexOf(previousTokenOrComment.type) !== -1
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.
198 function isCommentValid(comment, options) {
200 // 1. Check for default ignore pattern.
201 if (DEFAULT_IGNORE_PATTERN.test(comment.value)) {
205 // 2. Check for custom ignore pattern.
206 const commentWithoutAsterisks = comment.value
207 .replace(/\*/gu, "");
209 if (options.ignorePatternRegExp && options.ignorePatternRegExp.test(commentWithoutAsterisks)) {
213 // 3. Check for inline comments.
214 if (options.ignoreInlineComments && isInlineComment(comment)) {
218 // 4. Is this a consecutive comment (and are we tolerating those)?
219 if (options.ignoreConsecutiveComments && isConsecutiveComment(comment)) {
223 // 5. Does the comment start with a possible URL?
224 if (MAYBE_URL.test(commentWithoutAsterisks)) {
228 // 6. Is the initial word character a letter?
229 const commentWordCharsOnly = commentWithoutAsterisks
230 .replace(WHITESPACE, "");
232 if (commentWordCharsOnly.length === 0) {
236 const firstWordChar = commentWordCharsOnly[0];
238 if (!LETTER_PATTERN.test(firstWordChar)) {
242 // 7. Check the case of the initial word character.
243 const isUppercase = firstWordChar !== firstWordChar.toLocaleLowerCase(),
244 isLowercase = firstWordChar !== firstWordChar.toLocaleUpperCase();
246 if (capitalize === "always" && isLowercase) {
249 if (capitalize === "never" && isUppercase) {
257 * Process a comment to determine if it needs to be reported.
258 * @param {ASTNode} comment The comment node to process.
261 function processComment(comment) {
262 const options = normalizedOptions[comment.type],
263 commentValid = isCommentValid(comment, options);
266 const messageId = capitalize === "always"
267 ? "unexpectedLowercaseComment"
268 : "unexpectedUppercaseComment";
271 node: null, // Intentionally using loc instead
275 const match = comment.value.match(LETTER_PATTERN);
277 return fixer.replaceTextRange(
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()
288 //----------------------------------------------------------------------
290 //----------------------------------------------------------------------
294 const comments = sourceCode.getAllComments();
296 comments.filter(token => token.type !== "Shebang").forEach(processComment);