]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview Rule that warns about used warning comments | |
3 | * @author Alexander Schmidt <https://github.com/lxanders> | |
4 | */ | |
5 | ||
6 | "use strict"; | |
7 | ||
5422a9cc | 8 | const escapeRegExp = require("escape-string-regexp"); |
eb39fafa DC |
9 | const astUtils = require("./utils/ast-utils"); |
10 | ||
6f036462 TL |
11 | const CHAR_LIMIT = 40; |
12 | ||
eb39fafa DC |
13 | //------------------------------------------------------------------------------ |
14 | // Rule Definition | |
15 | //------------------------------------------------------------------------------ | |
16 | ||
34eeec05 | 17 | /** @type {import('../shared/types').Rule} */ |
eb39fafa DC |
18 | module.exports = { |
19 | meta: { | |
20 | type: "suggestion", | |
21 | ||
22 | docs: { | |
8f9d1d4d | 23 | description: "Disallow specified warning terms in comments", |
eb39fafa DC |
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"] | |
8f9d1d4d DC |
40 | }, |
41 | decoration: { | |
42 | type: "array", | |
43 | items: { | |
44 | type: "string", | |
45 | pattern: "^\\S$" | |
46 | }, | |
47 | minItems: 1, | |
48 | uniqueItems: true | |
eb39fafa DC |
49 | } |
50 | }, | |
51 | additionalProperties: false | |
52 | } | |
53 | ], | |
54 | ||
55 | messages: { | |
6f036462 | 56 | unexpectedComment: "Unexpected '{{matchedTerm}}' comment: '{{comment}}'." |
eb39fafa DC |
57 | } |
58 | }, | |
59 | ||
60 | create(context) { | |
eb39fafa DC |
61 | const sourceCode = context.getSourceCode(), |
62 | configuration = context.options[0] || {}, | |
63 | warningTerms = configuration.terms || ["todo", "fixme", "xxx"], | |
64 | location = configuration.location || "start", | |
8f9d1d4d | 65 | decoration = [...configuration.decoration || []].join(""), |
eb39fafa DC |
66 | selfConfigRegEx = /\bno-warning-comments\b/u; |
67 | ||
68 | /** | |
69 | * Convert a warning term into a RegExp which will match a comment containing that whole word in the specified | |
70 | * location ("start" or "anywhere"). If the term starts or ends with non word characters, then the match will not | |
71 | * require word boundaries on that side. | |
72 | * @param {string} term A term to convert to a RegExp | |
73 | * @returns {RegExp} The term converted to a RegExp | |
74 | */ | |
75 | function convertToRegExp(term) { | |
76 | const escaped = escapeRegExp(term); | |
8f9d1d4d | 77 | const escapedDecoration = escapeRegExp(decoration); |
eb39fafa DC |
78 | |
79 | /* | |
8f9d1d4d DC |
80 | * When matching at the start, ignore leading whitespace, and |
81 | * there's no need to worry about word boundaries. | |
82 | * | |
83 | * These expressions for the prefix and suffix are designed as follows: | |
84 | * ^ handles any terms at the beginning of a comment. | |
85 | * e.g. terms ["TODO"] matches `//TODO something` | |
86 | * $ handles any terms at the end of a comment | |
87 | * e.g. terms ["TODO"] matches `// something TODO` | |
88 | * \b handles terms preceded/followed by word boundary | |
89 | * e.g. terms: ["!FIX", "FIX!"] matches `// FIX!something` or `// something!FIX` | |
90 | * terms: ["FIX"] matches `// FIX!` or `// !FIX`, but not `// fixed or affix` | |
91 | * | |
92 | * For location start: | |
93 | * [\s]* handles optional leading spaces | |
94 | * e.g. terms ["TODO"] matches `// TODO something` | |
95 | * [\s\*]* (where "\*" is the escaped string of decoration) | |
96 | * handles optional leading spaces or decoration characters (for "start" location only) | |
97 | * e.g. terms ["TODO"] matches `/**** TODO something ... ` | |
eb39fafa | 98 | */ |
8f9d1d4d | 99 | const wordBoundary = "\\b"; |
eb39fafa | 100 | |
8f9d1d4d | 101 | let prefix = ""; |
eb39fafa | 102 | |
8f9d1d4d DC |
103 | if (location === "start") { |
104 | prefix = `^[\\s${escapedDecoration}]*`; | |
eb39fafa DC |
105 | } else if (/^\w/u.test(term)) { |
106 | prefix = wordBoundary; | |
eb39fafa DC |
107 | } |
108 | ||
8f9d1d4d DC |
109 | const suffix = /\w$/u.test(term) ? wordBoundary : ""; |
110 | const flags = "iu"; // Case-insensitive with Unicode case folding. | |
eb39fafa DC |
111 | |
112 | /* | |
8f9d1d4d DC |
113 | * For location "start", the typical regex is: |
114 | * /^[\s]*ESCAPED_TERM\b/iu. | |
115 | * Or if decoration characters are specified (e.g. "*"), then any of | |
116 | * those characters may appear in any order at the start: | |
117 | * /^[\s\*]*ESCAPED_TERM\b/iu. | |
118 | * | |
119 | * For location "anywhere" the typical regex is | |
120 | * /\bESCAPED_TERM\b/iu | |
121 | * | |
122 | * If it starts or ends with non-word character, the prefix and suffix are empty, respectively. | |
eb39fafa | 123 | */ |
8f9d1d4d | 124 | return new RegExp(`${prefix}${escaped}${suffix}`, flags); |
eb39fafa DC |
125 | } |
126 | ||
127 | const warningRegExps = warningTerms.map(convertToRegExp); | |
128 | ||
129 | /** | |
130 | * Checks the specified comment for matches of the configured warning terms and returns the matches. | |
131 | * @param {string} comment The comment which is checked. | |
132 | * @returns {Array} All matched warning terms for this comment. | |
133 | */ | |
134 | function commentContainsWarningTerm(comment) { | |
135 | const matches = []; | |
136 | ||
137 | warningRegExps.forEach((regex, index) => { | |
138 | if (regex.test(comment)) { | |
139 | matches.push(warningTerms[index]); | |
140 | } | |
141 | }); | |
142 | ||
143 | return matches; | |
144 | } | |
145 | ||
146 | /** | |
147 | * Checks the specified node for matching warning comments and reports them. | |
148 | * @param {ASTNode} node The AST node being checked. | |
149 | * @returns {void} undefined. | |
150 | */ | |
151 | function checkComment(node) { | |
6f036462 TL |
152 | const comment = node.value; |
153 | ||
154 | if ( | |
155 | astUtils.isDirectiveComment(node) && | |
156 | selfConfigRegEx.test(comment) | |
157 | ) { | |
eb39fafa DC |
158 | return; |
159 | } | |
160 | ||
6f036462 | 161 | const matches = commentContainsWarningTerm(comment); |
eb39fafa DC |
162 | |
163 | matches.forEach(matchedTerm => { | |
6f036462 TL |
164 | let commentToDisplay = ""; |
165 | let truncated = false; | |
166 | ||
167 | for (const c of comment.trim().split(/\s+/u)) { | |
168 | const tmp = commentToDisplay ? `${commentToDisplay} ${c}` : c; | |
169 | ||
170 | if (tmp.length <= CHAR_LIMIT) { | |
171 | commentToDisplay = tmp; | |
172 | } else { | |
173 | truncated = true; | |
174 | break; | |
175 | } | |
176 | } | |
177 | ||
eb39fafa DC |
178 | context.report({ |
179 | node, | |
180 | messageId: "unexpectedComment", | |
181 | data: { | |
6f036462 TL |
182 | matchedTerm, |
183 | comment: `${commentToDisplay}${ | |
184 | truncated ? "..." : "" | |
185 | }` | |
eb39fafa DC |
186 | } |
187 | }); | |
188 | }); | |
189 | } | |
190 | ||
191 | return { | |
192 | Program() { | |
193 | const comments = sourceCode.getAllComments(); | |
194 | ||
6f036462 TL |
195 | comments |
196 | .filter(token => token.type !== "Shebang") | |
197 | .forEach(checkComment); | |
eb39fafa DC |
198 | } |
199 | }; | |
200 | } | |
201 | }; |