no-warning-comments.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. /**
  2. * @fileoverview Rule that warns about used warning comments
  3. * @author Alexander Schmidt <https://github.com/lxanders>
  4. */
  5. "use strict";
  6. const { escapeRegExp } = require("lodash");
  7. const astUtils = require("./utils/ast-utils");
  8. const CHAR_LIMIT = 40;
  9. //------------------------------------------------------------------------------
  10. // Rule Definition
  11. //------------------------------------------------------------------------------
  12. module.exports = {
  13. meta: {
  14. type: "suggestion",
  15. docs: {
  16. description: "disallow specified warning terms in comments",
  17. category: "Best Practices",
  18. recommended: false,
  19. url: "https://eslint.org/docs/rules/no-warning-comments"
  20. },
  21. schema: [
  22. {
  23. type: "object",
  24. properties: {
  25. terms: {
  26. type: "array",
  27. items: {
  28. type: "string"
  29. }
  30. },
  31. location: {
  32. enum: ["start", "anywhere"]
  33. }
  34. },
  35. additionalProperties: false
  36. }
  37. ],
  38. messages: {
  39. unexpectedComment: "Unexpected '{{matchedTerm}}' comment: '{{comment}}'."
  40. }
  41. },
  42. create(context) {
  43. const sourceCode = context.getSourceCode(),
  44. configuration = context.options[0] || {},
  45. warningTerms = configuration.terms || ["todo", "fixme", "xxx"],
  46. location = configuration.location || "start",
  47. selfConfigRegEx = /\bno-warning-comments\b/u;
  48. /**
  49. * Convert a warning term into a RegExp which will match a comment containing that whole word in the specified
  50. * location ("start" or "anywhere"). If the term starts or ends with non word characters, then the match will not
  51. * require word boundaries on that side.
  52. * @param {string} term A term to convert to a RegExp
  53. * @returns {RegExp} The term converted to a RegExp
  54. */
  55. function convertToRegExp(term) {
  56. const escaped = escapeRegExp(term);
  57. const wordBoundary = "\\b";
  58. const eitherOrWordBoundary = `|${wordBoundary}`;
  59. let prefix;
  60. /*
  61. * If the term ends in a word character (a-z0-9_), ensure a word
  62. * boundary at the end, so that substrings do not get falsely
  63. * matched. eg "todo" in a string such as "mastodon".
  64. * If the term ends in a non-word character, then \b won't match on
  65. * the boundary to the next non-word character, which would likely
  66. * be a space. For example `/\bFIX!\b/.test('FIX! blah') === false`.
  67. * In these cases, use no bounding match. Same applies for the
  68. * prefix, handled below.
  69. */
  70. const suffix = /\w$/u.test(term) ? "\\b" : "";
  71. if (location === "start") {
  72. /*
  73. * When matching at the start, ignore leading whitespace, and
  74. * there's no need to worry about word boundaries.
  75. */
  76. prefix = "^\\s*";
  77. } else if (/^\w/u.test(term)) {
  78. prefix = wordBoundary;
  79. } else {
  80. prefix = "";
  81. }
  82. if (location === "start") {
  83. /*
  84. * For location "start" the regex should be
  85. * ^\s*TERM\b. This checks the word boundary
  86. * at the beginning of the comment.
  87. */
  88. return new RegExp(prefix + escaped + suffix, "iu");
  89. }
  90. /*
  91. * For location "anywhere" the regex should be
  92. * \bTERM\b|\bTERM\b, this checks the entire comment
  93. * for the term.
  94. */
  95. return new RegExp(
  96. prefix +
  97. escaped +
  98. suffix +
  99. eitherOrWordBoundary +
  100. term +
  101. wordBoundary,
  102. "iu"
  103. );
  104. }
  105. const warningRegExps = warningTerms.map(convertToRegExp);
  106. /**
  107. * Checks the specified comment for matches of the configured warning terms and returns the matches.
  108. * @param {string} comment The comment which is checked.
  109. * @returns {Array} All matched warning terms for this comment.
  110. */
  111. function commentContainsWarningTerm(comment) {
  112. const matches = [];
  113. warningRegExps.forEach((regex, index) => {
  114. if (regex.test(comment)) {
  115. matches.push(warningTerms[index]);
  116. }
  117. });
  118. return matches;
  119. }
  120. /**
  121. * Checks the specified node for matching warning comments and reports them.
  122. * @param {ASTNode} node The AST node being checked.
  123. * @returns {void} undefined.
  124. */
  125. function checkComment(node) {
  126. const comment = node.value;
  127. if (
  128. astUtils.isDirectiveComment(node) &&
  129. selfConfigRegEx.test(comment)
  130. ) {
  131. return;
  132. }
  133. const matches = commentContainsWarningTerm(comment);
  134. matches.forEach(matchedTerm => {
  135. let commentToDisplay = "";
  136. let truncated = false;
  137. for (const c of comment.trim().split(/\s+/u)) {
  138. const tmp = commentToDisplay ? `${commentToDisplay} ${c}` : c;
  139. if (tmp.length <= CHAR_LIMIT) {
  140. commentToDisplay = tmp;
  141. } else {
  142. truncated = true;
  143. break;
  144. }
  145. }
  146. context.report({
  147. node,
  148. messageId: "unexpectedComment",
  149. data: {
  150. matchedTerm,
  151. comment: `${commentToDisplay}${
  152. truncated ? "..." : ""
  153. }`
  154. }
  155. });
  156. });
  157. }
  158. return {
  159. Program() {
  160. const comments = sourceCode.getAllComments();
  161. comments
  162. .filter(token => token.type !== "Shebang")
  163. .forEach(checkComment);
  164. }
  165. };
  166. }
  167. };