space-before-keywords.js 6.71 KB
/**
 * @fileoverview Require or disallow spaces before keywords
 * @author Marko Raatikka
 * @copyright 2015 Marko Raatikka. All rights reserved.
 * See LICENSE file in root directory for full license.
 */
"use strict";

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

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

var ERROR_MSG_SPACE_EXPECTED = "Missing space before keyword \"{{keyword}}\".";
var ERROR_MSG_NO_SPACE_EXPECTED = "Unexpected space before keyword \"{{keyword}}\".";

module.exports = function(context) {

    var sourceCode = context.getSourceCode();

    var SPACE_REQUIRED = context.options[0] !== "never";

    //--------------------------------------------------------------------------
    // Helpers
    //--------------------------------------------------------------------------

    /**
     * Report the error message
     * @param {ASTNode} node node to report
     * @param {string} message Error message to be displayed
     * @param {object} data Data object for the rule message
     * @returns {void}
     */
    function report(node, message, data) {
        context.report({
            node: node,
            message: message,
            data: data,
            fix: function(fixer) {
                if (SPACE_REQUIRED) {
                    return fixer.insertTextBefore(node, " ");
                } else {
                    var tokenBefore = context.getTokenBefore(node);
                    return fixer.removeRange([tokenBefore.range[1], node.range[0]]);
                }
            }
        });
    }

    /**
     * Check if a token meets the criteria
     *
     * @param   {ASTNode} node    The node to check
     * @param   {Object}  left    The left-hand side token of the node
     * @param   {Object}  right   The right-hand side token of the node
     * @param   {Object}  options See check()
     * @returns {void}
     */
    function checkTokens(node, left, right, options) {

        if (!left) {
            return;
        }

        if (left.type === "Keyword") {
            return;
        }

        if (!SPACE_REQUIRED && typeof options.requireSpace === "undefined") {
            return;
        }

        options = options || {};
        options.allowedPrecedingChars = options.allowedPrecedingChars || [ "{" ];
        options.requireSpace = typeof options.requireSpace === "undefined" ? SPACE_REQUIRED : options.requireSpace;

        var hasSpace = sourceCode.isSpaceBetweenTokens(left, right);
        var spaceOk = hasSpace === options.requireSpace;

        if (spaceOk) {
            return;
        }

        if (!astUtils.isTokenOnSameLine(left, right)) {
            if (!options.requireSpace) {
                report(node, ERROR_MSG_NO_SPACE_EXPECTED, { keyword: right.value });
            }
            return;
        }

        if (!options.requireSpace) {
            report(node, ERROR_MSG_NO_SPACE_EXPECTED, { keyword: right.value });
            return;
        }

        if (options.allowedPrecedingChars.indexOf(left.value) !== -1) {
            return;
        }

        report(node, ERROR_MSG_SPACE_EXPECTED, { keyword: right.value });

    }

    /**
     * Get right and left tokens of a node and check to see if they meet the given criteria
     *
     * @param   {ASTNode}  node                          The node to check
     * @param   {Object}   options                       Options to validate the node against
     * @param   {Array}    options.allowedPrecedingChars Characters that can precede the right token
     * @param   {Boolean}  options.requireSpace          Whether or not the right token needs to be
     *                                                   preceded by a space
     * @returns {void}
     */
    function check(node, options) {

        options = options || {};

        var left = context.getTokenBefore(node);
        var right = context.getFirstToken(node);

        checkTokens(node, left, right, options);

    }

    //--------------------------------------------------------------------------
    // Public
    //--------------------------------------------------------------------------

    return {

        "IfStatement": function(node) {
            // if
            check(node);
            // else
            if (node.alternate) {
                var tokens = context.getTokensBefore(node.alternate, 2);
                if (tokens[0].value === "}") {
                    check(tokens[1], { requireSpace: SPACE_REQUIRED });
                }
            }
        },
        "ForStatement": check,
        "ForInStatement": check,
        "WhileStatement": check,
        "DoWhileStatement": function(node) {
            var whileNode = context.getTokenAfter(node.body);
            // do
            check(node);
            // while
            check(whileNode, { requireSpace: SPACE_REQUIRED });
        },
        "SwitchStatement": function(node) {
            // switch
            check(node);
            // case/default
            node.cases.forEach(function(caseNode) {
                check(caseNode);
            });
        },
        "ThrowStatement": check,
        "TryStatement": function(node) {
            // try
            check(node);
            // finally
            if (node.finalizer) {
                check(context.getTokenBefore(node.finalizer), { requireSpace: SPACE_REQUIRED });
            }
        },
        "CatchClause": function(node) {
            check(node, { requireSpace: SPACE_REQUIRED });
        },
        "WithStatement": check,
        "VariableDeclaration": function(node) {
            check(node, { allowedPrecedingChars: [ "(", "{" ] });
        },
        "ReturnStatement": check,
        "BreakStatement": check,
        "LabeledStatement": check,
        "ContinueStatement": check,
        "FunctionDeclaration": check,
        "FunctionExpression": function(node) {
            var left = context.getTokenBefore(node);
            var right = context.getFirstToken(node);

            // If it's a method, a getter, or a setter, the first token is not the `function` keyword.
            if (right.type !== "Keyword") {
                return;
            }

            checkTokens(node, left, right, { allowedPrecedingChars: [ "(", "{", "[" ] });
        },
        "YieldExpression": function(node) {
            check(node, { allowedPrecedingChars: [ "(", "{" ] });
        },
        "ForOfStatement": check,
        "ClassBody": function(node) {

            // Find the 'class' token
            while (node.value !== "class") {
                node = context.getTokenBefore(node);
            }

            check(node);
        }
    };

};

module.exports.schema = [
    {
        "enum": ["always", "never"]
    }
];