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 //------------------------------------------------------------------------------
102 /** @type {import('../shared/types').Rule} */
108 description
: "Enforce or disallow capitalization of the first letter of a comment",
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"].includes(previousTokenOrComment
.type
)
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
);