You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

163 lines
5.5 KiB

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