capitalized-comments.js 11 KB

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