• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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