blob: 5cc7266654f5fc75a64fba44d303264fd0ba7dc9 [file] [log] [blame]
/**
* @fileoverview A rule to ensure whitespace before blocks.
* @author Mathias Schreck <https://github.com/lo1tuma>
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Checks whether the given node represents the body of a function.
* @param {ASTNode} node the node to check.
* @returns {boolean} `true` if the node is function body.
*/
function isFunctionBody(node) {
const parent = node.parent;
return (
node.type === "BlockStatement" &&
astUtils.isFunction(parent) &&
parent.body === node
);
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
type: "layout",
docs: {
description: "enforce consistent spacing before blocks",
recommended: false,
url: "https://eslint.org/docs/rules/space-before-blocks"
},
fixable: "whitespace",
schema: [
{
oneOf: [
{
enum: ["always", "never"]
},
{
type: "object",
properties: {
keywords: {
enum: ["always", "never", "off"]
},
functions: {
enum: ["always", "never", "off"]
},
classes: {
enum: ["always", "never", "off"]
}
},
additionalProperties: false
}
]
}
],
messages: {
unexpectedSpace: "Unexpected space before opening brace.",
missingSpace: "Missing space before opening brace."
}
},
create(context) {
const config = context.options[0],
sourceCode = context.getSourceCode();
let alwaysFunctions = true,
alwaysKeywords = true,
alwaysClasses = true,
neverFunctions = false,
neverKeywords = false,
neverClasses = false;
if (typeof config === "object") {
alwaysFunctions = config.functions === "always";
alwaysKeywords = config.keywords === "always";
alwaysClasses = config.classes === "always";
neverFunctions = config.functions === "never";
neverKeywords = config.keywords === "never";
neverClasses = config.classes === "never";
} else if (config === "never") {
alwaysFunctions = false;
alwaysKeywords = false;
alwaysClasses = false;
neverFunctions = true;
neverKeywords = true;
neverClasses = true;
}
/**
* Checks whether the spacing before the given block is already controlled by another rule:
* - `arrow-spacing` checks spaces after `=>`.
* - `keyword-spacing` checks spaces after keywords in certain contexts.
* - `switch-colon-spacing` checks spaces after `:` of switch cases.
* @param {Token} precedingToken first token before the block.
* @param {ASTNode|Token} node `BlockStatement` node or `{` token of a `SwitchStatement` node.
* @returns {boolean} `true` if requiring or disallowing spaces before the given block could produce conflicts with other rules.
*/
function isConflicted(precedingToken, node) {
return (
astUtils.isArrowToken(precedingToken) ||
(
astUtils.isKeywordToken(precedingToken) &&
!isFunctionBody(node)
) ||
(
astUtils.isColonToken(precedingToken) &&
node.parent &&
node.parent.type === "SwitchCase" &&
precedingToken === astUtils.getSwitchCaseColonToken(node.parent, sourceCode)
)
);
}
/**
* Checks the given BlockStatement node has a preceding space if it doesn’t start on a new line.
* @param {ASTNode|Token} node The AST node of a BlockStatement.
* @returns {void} undefined.
*/
function checkPrecedingSpace(node) {
const precedingToken = sourceCode.getTokenBefore(node);
if (precedingToken && !isConflicted(precedingToken, node) && astUtils.isTokenOnSameLine(precedingToken, node)) {
const hasSpace = sourceCode.isSpaceBetweenTokens(precedingToken, node);
let requireSpace;
let requireNoSpace;
if (isFunctionBody(node)) {
requireSpace = alwaysFunctions;
requireNoSpace = neverFunctions;
} else if (node.type === "ClassBody") {
requireSpace = alwaysClasses;
requireNoSpace = neverClasses;
} else {
requireSpace = alwaysKeywords;
requireNoSpace = neverKeywords;
}
if (requireSpace && !hasSpace) {
context.report({
node,
messageId: "missingSpace",
fix(fixer) {
return fixer.insertTextBefore(node, " ");
}
});
} else if (requireNoSpace && hasSpace) {
context.report({
node,
messageId: "unexpectedSpace",
fix(fixer) {
return fixer.removeRange([precedingToken.range[1], node.range[0]]);
}
});
}
}
}
/**
* Checks if the CaseBlock of an given SwitchStatement node has a preceding space.
* @param {ASTNode} node The node of a SwitchStatement.
* @returns {void} undefined.
*/
function checkSpaceBeforeCaseBlock(node) {
const cases = node.cases;
let openingBrace;
if (cases.length > 0) {
openingBrace = sourceCode.getTokenBefore(cases[0]);
} else {
openingBrace = sourceCode.getLastToken(node, 1);
}
checkPrecedingSpace(openingBrace);
}
return {
BlockStatement: checkPrecedingSpace,
ClassBody: checkPrecedingSpace,
SwitchStatement: checkSpaceBeforeCaseBlock
};
}
};