300 lines
10 KiB
JavaScript
300 lines
10 KiB
JavaScript
/**
|
|
* @fileoverview enforce or disallow capitalization of the first letter of a comment
|
|
* @author Kevin Partington
|
|
*/
|
|
"use strict";
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Requirements
|
|
//------------------------------------------------------------------------------
|
|
|
|
const LETTER_PATTERN = require("./utils/patterns/letters");
|
|
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?
|
|
|
|
/*
|
|
* 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
|
|
//------------------------------------------------------------------------------
|
|
|
|
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);
|
|
|
|
//----------------------------------------------------------------------
|
|
// 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"].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) {
|
|
|
|
// 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;
|
|
}
|
|
|
|
const firstWordChar = commentWordCharsOnly[0];
|
|
|
|
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);
|
|
|
|
return fixer.replaceTextRange(
|
|
|
|
// Offset match.index by 2 to account for the first 2 characters that start the comment (// or /*)
|
|
[comment.range[0] + match.index + 2, comment.range[0] + match.index + 3],
|
|
capitalize === "always" ? match[0].toLocaleUpperCase() : match[0].toLocaleLowerCase()
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
//----------------------------------------------------------------------
|
|
// Public
|
|
//----------------------------------------------------------------------
|
|
|
|
return {
|
|
Program() {
|
|
const comments = sourceCode.getAllComments();
|
|
|
|
comments.filter(token => token.type !== "Shebang").forEach(processComment);
|
|
}
|
|
};
|
|
}
|
|
};
|