1 | /**
|
2 | * @fileoverview Rule that warns about used warning comments
|
3 | * @author Alexander Schmidt <https://github.com/lxanders>
|
4 | */
|
5 |
|
6 | ;
|
7 |
|
8 | const { escapeRegExp } = require("lodash");
|
9 | const astUtils = require("./utils/ast-utils");
|
10 |
|
11 | const CHAR_LIMIT = 40;
|
12 |
|
13 | //------------------------------------------------------------------------------
|
14 | // Rule Definition
|
15 | //------------------------------------------------------------------------------
|
16 |
|
17 | module.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 | };
|