capitalized-comments.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. /**
  2. * @fileoverview enforce or disallow capitalization of the first letter of a comment
  3. * @author Kevin Partington
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const LETTER_PATTERN = require("./utils/patterns/letters");
  10. const astUtils = require("./utils/ast-utils");
  11. //------------------------------------------------------------------------------
  12. // Helpers
  13. //------------------------------------------------------------------------------
  14. const DEFAULT_IGNORE_PATTERN = astUtils.COMMENTS_IGNORE_PATTERN,
  15. WHITESPACE = /\s/gu,
  16. MAYBE_URL = /^\s*[^:/?#\s]+:\/\/[^?#]/u; // TODO: Combine w/ max-len pattern?
  17. /*
  18. * Base schema body for defining the basic capitalization rule, ignorePattern,
  19. * and ignoreInlineComments values.
  20. * This can be used in a few different ways in the actual schema.
  21. */
  22. const SCHEMA_BODY = {
  23. type: "object",
  24. properties: {
  25. ignorePattern: {
  26. type: "string"
  27. },
  28. ignoreInlineComments: {
  29. type: "boolean"
  30. },
  31. ignoreConsecutiveComments: {
  32. type: "boolean"
  33. }
  34. },
  35. additionalProperties: false
  36. };
  37. const DEFAULTS = {
  38. ignorePattern: "",
  39. ignoreInlineComments: false,
  40. ignoreConsecutiveComments: false
  41. };
  42. /**
  43. * Get normalized options for either block or line comments from the given
  44. * user-provided options.
  45. * - If the user-provided options is just a string, returns a normalized
  46. * set of options using default values for all other options.
  47. * - If the user-provided options is an object, then a normalized option
  48. * set is returned. Options specified in overrides will take priority
  49. * over options specified in the main options object, which will in
  50. * turn take priority over the rule's defaults.
  51. * @param {Object|string} rawOptions The user-provided options.
  52. * @param {string} which Either "line" or "block".
  53. * @returns {Object} The normalized options.
  54. */
  55. function getNormalizedOptions(rawOptions, which) {
  56. return Object.assign({}, DEFAULTS, rawOptions[which] || rawOptions);
  57. }
  58. /**
  59. * Get normalized options for block and line comments.
  60. * @param {Object|string} rawOptions The user-provided options.
  61. * @returns {Object} An object with "Line" and "Block" keys and corresponding
  62. * normalized options objects.
  63. */
  64. function getAllNormalizedOptions(rawOptions = {}) {
  65. return {
  66. Line: getNormalizedOptions(rawOptions, "line"),
  67. Block: getNormalizedOptions(rawOptions, "block")
  68. };
  69. }
  70. /**
  71. * Creates a regular expression for each ignorePattern defined in the rule
  72. * options.
  73. *
  74. * This is done in order to avoid invoking the RegExp constructor repeatedly.
  75. * @param {Object} normalizedOptions The normalized rule options.
  76. * @returns {void}
  77. */
  78. function createRegExpForIgnorePatterns(normalizedOptions) {
  79. Object.keys(normalizedOptions).forEach(key => {
  80. const ignorePatternStr = normalizedOptions[key].ignorePattern;
  81. if (ignorePatternStr) {
  82. const regExp = RegExp(`^\\s*(?:${ignorePatternStr})`, "u");
  83. normalizedOptions[key].ignorePatternRegExp = regExp;
  84. }
  85. });
  86. }
  87. //------------------------------------------------------------------------------
  88. // Rule Definition
  89. //------------------------------------------------------------------------------
  90. /** @type {import('../shared/types').Rule} */
  91. module.exports = {
  92. meta: {
  93. type: "suggestion",
  94. docs: {
  95. description: "Enforce or disallow capitalization of the first letter of a comment",
  96. recommended: false,
  97. url: "https://eslint.org/docs/rules/capitalized-comments"
  98. },
  99. fixable: "code",
  100. schema: [
  101. { enum: ["always", "never"] },
  102. {
  103. oneOf: [
  104. SCHEMA_BODY,
  105. {
  106. type: "object",
  107. properties: {
  108. line: SCHEMA_BODY,
  109. block: SCHEMA_BODY
  110. },
  111. additionalProperties: false
  112. }
  113. ]
  114. }
  115. ],
  116. messages: {
  117. unexpectedLowercaseComment: "Comments should not begin with a lowercase character.",
  118. unexpectedUppercaseComment: "Comments should not begin with an uppercase character."
  119. }
  120. },
  121. create(context) {
  122. const capitalize = context.options[0] || "always",
  123. normalizedOptions = getAllNormalizedOptions(context.options[1]),
  124. sourceCode = context.getSourceCode();
  125. createRegExpForIgnorePatterns(normalizedOptions);
  126. //----------------------------------------------------------------------
  127. // Helpers
  128. //----------------------------------------------------------------------
  129. /**
  130. * Checks whether a comment is an inline comment.
  131. *
  132. * For the purpose of this rule, a comment is inline if:
  133. * 1. The comment is preceded by a token on the same line; and
  134. * 2. The command is followed by a token on the same line.
  135. *
  136. * Note that the comment itself need not be single-line!
  137. *
  138. * Also, it follows from this definition that only block comments can
  139. * be considered as possibly inline. This is because line comments
  140. * would consume any following tokens on the same line as the comment.
  141. * @param {ASTNode} comment The comment node to check.
  142. * @returns {boolean} True if the comment is an inline comment, false
  143. * otherwise.
  144. */
  145. function isInlineComment(comment) {
  146. const previousToken = sourceCode.getTokenBefore(comment, { includeComments: true }),
  147. nextToken = sourceCode.getTokenAfter(comment, { includeComments: true });
  148. return Boolean(
  149. previousToken &&
  150. nextToken &&
  151. comment.loc.start.line === previousToken.loc.end.line &&
  152. comment.loc.end.line === nextToken.loc.start.line
  153. );
  154. }
  155. /**
  156. * Determine if a comment follows another comment.
  157. * @param {ASTNode} comment The comment to check.
  158. * @returns {boolean} True if the comment follows a valid comment.
  159. */
  160. function isConsecutiveComment(comment) {
  161. const previousTokenOrComment = sourceCode.getTokenBefore(comment, { includeComments: true });
  162. return Boolean(
  163. previousTokenOrComment &&
  164. ["Block", "Line"].includes(previousTokenOrComment.type)
  165. );
  166. }
  167. /**
  168. * Check a comment to determine if it is valid for this rule.
  169. * @param {ASTNode} comment The comment node to process.
  170. * @param {Object} options The options for checking this comment.
  171. * @returns {boolean} True if the comment is valid, false otherwise.
  172. */
  173. function isCommentValid(comment, options) {
  174. // 1. Check for default ignore pattern.
  175. if (DEFAULT_IGNORE_PATTERN.test(comment.value)) {
  176. return true;
  177. }
  178. // 2. Check for custom ignore pattern.
  179. const commentWithoutAsterisks = comment.value
  180. .replace(/\*/gu, "");
  181. if (options.ignorePatternRegExp && options.ignorePatternRegExp.test(commentWithoutAsterisks)) {
  182. return true;
  183. }
  184. // 3. Check for inline comments.
  185. if (options.ignoreInlineComments && isInlineComment(comment)) {
  186. return true;
  187. }
  188. // 4. Is this a consecutive comment (and are we tolerating those)?
  189. if (options.ignoreConsecutiveComments && isConsecutiveComment(comment)) {
  190. return true;
  191. }
  192. // 5. Does the comment start with a possible URL?
  193. if (MAYBE_URL.test(commentWithoutAsterisks)) {
  194. return true;
  195. }
  196. // 6. Is the initial word character a letter?
  197. const commentWordCharsOnly = commentWithoutAsterisks
  198. .replace(WHITESPACE, "");
  199. if (commentWordCharsOnly.length === 0) {
  200. return true;
  201. }
  202. const firstWordChar = commentWordCharsOnly[0];
  203. if (!LETTER_PATTERN.test(firstWordChar)) {
  204. return true;
  205. }
  206. // 7. Check the case of the initial word character.
  207. const isUppercase = firstWordChar !== firstWordChar.toLocaleLowerCase(),
  208. isLowercase = firstWordChar !== firstWordChar.toLocaleUpperCase();
  209. if (capitalize === "always" && isLowercase) {
  210. return false;
  211. }
  212. if (capitalize === "never" && isUppercase) {
  213. return false;
  214. }
  215. return true;
  216. }
  217. /**
  218. * Process a comment to determine if it needs to be reported.
  219. * @param {ASTNode} comment The comment node to process.
  220. * @returns {void}
  221. */
  222. function processComment(comment) {
  223. const options = normalizedOptions[comment.type],
  224. commentValid = isCommentValid(comment, options);
  225. if (!commentValid) {
  226. const messageId = capitalize === "always"
  227. ? "unexpectedLowercaseComment"
  228. : "unexpectedUppercaseComment";
  229. context.report({
  230. node: null, // Intentionally using loc instead
  231. loc: comment.loc,
  232. messageId,
  233. fix(fixer) {
  234. const match = comment.value.match(LETTER_PATTERN);
  235. return fixer.replaceTextRange(
  236. // Offset match.index by 2 to account for the first 2 characters that start the comment (// or /*)
  237. [comment.range[0] + match.index + 2, comment.range[0] + match.index + 3],
  238. capitalize === "always" ? match[0].toLocaleUpperCase() : match[0].toLocaleLowerCase()
  239. );
  240. }
  241. });
  242. }
  243. }
  244. //----------------------------------------------------------------------
  245. // Public
  246. //----------------------------------------------------------------------
  247. return {
  248. Program() {
  249. const comments = sourceCode.getAllComments();
  250. comments.filter(token => token.type !== "Shebang").forEach(processComment);
  251. }
  252. };
  253. }
  254. };