* @fileoverview enforce or disallow capitalization of the first letter of a comment
* @author Kevin Partington
*/
"use strict";
const LETTER_PATTERN = require("./utils/patterns/letters");
const astUtils = require("./utils/ast-utils");
const DEFAULT_IGNORE_PATTERN = astUtils.COMMENTS_IGNORE_PATTERN,
WHITESPACE = /\s/gu,
MAYBE_URL = /^\s*[^:/?#\s]+:\/\/[^?#]/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;
}
});
}
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "enforce or disallow capitalization of the first letter of a comment",
category: "Stylistic Issues",
recommended: false,
url: "https://eslint.org/docs/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.getSourceCode();
createRegExpForIgnorePatterns(normalizedOptions);
* 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"].indexOf(previousTokenOrComment.type) !== -1
);
}
* 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) {
if (DEFAULT_IGNORE_PATTERN.test(comment.value)) {
return true;
}
const commentWithoutAsterisks = comment.value
.replace(/\*/gu, "");
if (options.ignorePatternRegExp && options.ignorePatternRegExp.test(commentWithoutAsterisks)) {
return true;
}
if (options.ignoreInlineComments && isInlineComment(comment)) {
return true;
}
if (options.ignoreConsecutiveComments && isConsecutiveComment(comment)) {
return true;
}
if (MAYBE_URL.test(commentWithoutAsterisks)) {
return true;
}
const commentWordCharsOnly = commentWithoutAsterisks
.replace(WHITESPACE, "");
if (commentWordCharsOnly.length === 0) {
return true;
}
const firstWordChar = commentWordCharsOnly[0];
if (!LETTER_PATTERN.test(firstWordChar)) {
return true;
}
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,
loc: comment.loc,
messageId,
fix(fixer) {
const match = comment.value.match(LETTER_PATTERN);
return fixer.replaceTextRange(
[comment.range[0] + match.index + 2, comment.range[0] + match.index + 3],
capitalize === "always" ? match[0].toLocaleUpperCase() : match[0].toLocaleLowerCase()
);
}
});
}
}
return {
Program() {
const comments = sourceCode.getAllComments();
comments.filter(token => token.type !== "Shebang").forEach(processComment);
}
};
}
};