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