Spaces:
Runtime error
Runtime error
/** | |
* @fileoverview enforce or disallow capitalization of the first letter of a comment | |
* @author Kevin Partington | |
*/ | |
; | |
//------------------------------------------------------------------------------ | |
// 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); | |
} | |
}; | |
} | |
}; | |