no-warning-comments.js 3.68 KB
/**
 * @fileoverview Rule that warns about used warning comments
 * @author Alexander Schmidt <https://github.com/lxanders>
 */

"use strict";

var astUtils = require("../ast-utils");

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

module.exports = function(context) {

    var configuration = context.options[0] || {},
        warningTerms = configuration.terms || ["todo", "fixme", "xxx"],
        location = configuration.location || "start",
        selfConfigRegEx = /\bno-warning-comments\b/,
        warningRegExps;

    /**
     * Convert a warning term into a RegExp which will match a comment containing that whole word in the specified
     * location ("start" or "anywhere"). If the term starts or ends with non word characters, then the match will not
     * require word boundaries on that side.
     *
     * @param {String} term A term to convert to a RegExp
     * @returns {RegExp} The term converted to a RegExp
     */
    function convertToRegExp(term) {
        var escaped = term.replace(/[-\/\\$\^*+?.()|\[\]{}]/g, "\\$&"),
            // If the term ends in a word character (a-z0-9_), ensure a word boundary at the end, so that substrings do
            // not get falsely matched. eg "todo" in a string such as "mastodon".
            // If the term ends in a non-word character, then \b won't match on the boundary to the next non-word
            // character, which would likely be a space. For example `/\bFIX!\b/.test('FIX! blah') === false`.
            // In these cases, use no bounding match. Same applies for the prefix, handled below.
            suffix = /\w$/.test(term) ? "\\b" : "",
            prefix;

        if (location === "start") {
            // When matching at the start, ignore leading whitespace, and there's no need to worry about word boundaries
            prefix = "^\\s*";
        } else if (/^\w/.test(term)) {
            prefix = "\\b";
        } else {
            prefix = "";
        }

        return new RegExp(prefix + escaped + suffix, "i");
    }

    /**
     * Checks the specified comment for matches of the configured warning terms and returns the matches.
     * @param {String} comment The comment which is checked.
     * @returns {Array} All matched warning terms for this comment.
     */
    function commentContainsWarningTerm(comment) {
        var matches = [];

        warningRegExps.forEach(function(regex, index) {
            if (regex.test(comment)) {
                matches.push(warningTerms[index]);
            }
        });

        return matches;
    }

    /**
     * Checks the specified node for matching warning comments and reports them.
     * @param {ASTNode} node The AST node being checked.
     * @returns {void} undefined.
     */
    function checkComment(node) {
        if (astUtils.isDirectiveComment(node) && selfConfigRegEx.test(node.value)) {
            return;
        }

        var matches = commentContainsWarningTerm(node.value);

        matches.forEach(function(matchedTerm) {
            context.report(node, "Unexpected \"" + matchedTerm + "\" comment.");
        });
    }

    warningRegExps = warningTerms.map(convertToRegExp);
    return {
        "BlockComment": checkComment,
        "LineComment": checkComment
    };
};

module.exports.schema = [
    {
        "type": "object",
        "properties": {
            "terms": {
                "type": "array",
                "items": {
                    "type": "string"
                }
            },
            "location": {
                "enum": ["start", "anywhere"]
            }
        },
        "additionalProperties": false
    }
];