--- /dev/null
+/**
+ * @fileoverview A rule to ensure blank lines within blocks.
+ * @author Mathias Schreck <https://github.com/lo1tuma>
+ */
+
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const astUtils = require("./utils/ast-utils");
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+module.exports = {
+ meta: {
+ type: "layout",
+
+ docs: {
+ description: "require or disallow padding within blocks",
+ category: "Stylistic Issues",
+ recommended: false,
+ url: "https://eslint.org/docs/rules/padded-blocks"
+ },
+
+ fixable: "whitespace",
+
+ schema: [
+ {
+ oneOf: [
+ {
+ enum: ["always", "never"]
+ },
+ {
+ type: "object",
+ properties: {
+ blocks: {
+ enum: ["always", "never"]
+ },
+ switches: {
+ enum: ["always", "never"]
+ },
+ classes: {
+ enum: ["always", "never"]
+ }
+ },
+ additionalProperties: false,
+ minProperties: 1
+ }
+ ]
+ },
+ {
+ type: "object",
+ properties: {
+ allowSingleLineBlocks: {
+ type: "boolean"
+ }
+ }
+ }
+ ]
+ },
+
+ create(context) {
+ const options = {};
+ const typeOptions = context.options[0] || "always";
+ const exceptOptions = context.options[1] || {};
+
+ if (typeof typeOptions === "string") {
+ const shouldHavePadding = typeOptions === "always";
+
+ options.blocks = shouldHavePadding;
+ options.switches = shouldHavePadding;
+ options.classes = shouldHavePadding;
+ } else {
+ if (Object.prototype.hasOwnProperty.call(typeOptions, "blocks")) {
+ options.blocks = typeOptions.blocks === "always";
+ }
+ if (Object.prototype.hasOwnProperty.call(typeOptions, "switches")) {
+ options.switches = typeOptions.switches === "always";
+ }
+ if (Object.prototype.hasOwnProperty.call(typeOptions, "classes")) {
+ options.classes = typeOptions.classes === "always";
+ }
+ }
+
+ if (Object.prototype.hasOwnProperty.call(exceptOptions, "allowSingleLineBlocks")) {
+ options.allowSingleLineBlocks = exceptOptions.allowSingleLineBlocks === true;
+ }
+
+ const ALWAYS_MESSAGE = "Block must be padded by blank lines.",
+ NEVER_MESSAGE = "Block must not be padded by blank lines.";
+
+ const sourceCode = context.getSourceCode();
+
+ /**
+ * Gets the open brace token from a given node.
+ * @param {ASTNode} node A BlockStatement or SwitchStatement node from which to get the open brace.
+ * @returns {Token} The token of the open brace.
+ */
+ function getOpenBrace(node) {
+ if (node.type === "SwitchStatement") {
+ return sourceCode.getTokenBefore(node.cases[0]);
+ }
+ return sourceCode.getFirstToken(node);
+ }
+
+ /**
+ * Checks if the given parameter is a comment node
+ * @param {ASTNode|Token} node An AST node or token
+ * @returns {boolean} True if node is a comment
+ */
+ function isComment(node) {
+ return node.type === "Line" || node.type === "Block";
+ }
+
+ /**
+ * Checks if there is padding between two tokens
+ * @param {Token} first The first token
+ * @param {Token} second The second token
+ * @returns {boolean} True if there is at least a line between the tokens
+ */
+ function isPaddingBetweenTokens(first, second) {
+ return second.loc.start.line - first.loc.end.line >= 2;
+ }
+
+
+ /**
+ * Checks if the given token has a blank line after it.
+ * @param {Token} token The token to check.
+ * @returns {boolean} Whether or not the token is followed by a blank line.
+ */
+ function getFirstBlockToken(token) {
+ let prev,
+ first = token;
+
+ do {
+ prev = first;
+ first = sourceCode.getTokenAfter(first, { includeComments: true });
+ } while (isComment(first) && first.loc.start.line === prev.loc.end.line);
+
+ return first;
+ }
+
+ /**
+ * Checks if the given token is preceeded by a blank line.
+ * @param {Token} token The token to check
+ * @returns {boolean} Whether or not the token is preceeded by a blank line
+ */
+ function getLastBlockToken(token) {
+ let last = token,
+ next;
+
+ do {
+ next = last;
+ last = sourceCode.getTokenBefore(last, { includeComments: true });
+ } while (isComment(last) && last.loc.end.line === next.loc.start.line);
+
+ return last;
+ }
+
+ /**
+ * Checks if a node should be padded, according to the rule config.
+ * @param {ASTNode} node The AST node to check.
+ * @returns {boolean} True if the node should be padded, false otherwise.
+ */
+ function requirePaddingFor(node) {
+ switch (node.type) {
+ case "BlockStatement":
+ return options.blocks;
+ case "SwitchStatement":
+ return options.switches;
+ case "ClassBody":
+ return options.classes;
+
+ /* istanbul ignore next */
+ default:
+ throw new Error("unreachable");
+ }
+ }
+
+ /**
+ * Checks the given BlockStatement node to be padded if the block is not empty.
+ * @param {ASTNode} node The AST node of a BlockStatement.
+ * @returns {void} undefined.
+ */
+ function checkPadding(node) {
+ const openBrace = getOpenBrace(node),
+ firstBlockToken = getFirstBlockToken(openBrace),
+ tokenBeforeFirst = sourceCode.getTokenBefore(firstBlockToken, { includeComments: true }),
+ closeBrace = sourceCode.getLastToken(node),
+ lastBlockToken = getLastBlockToken(closeBrace),
+ tokenAfterLast = sourceCode.getTokenAfter(lastBlockToken, { includeComments: true }),
+ blockHasTopPadding = isPaddingBetweenTokens(tokenBeforeFirst, firstBlockToken),
+ blockHasBottomPadding = isPaddingBetweenTokens(lastBlockToken, tokenAfterLast);
+
+ if (options.allowSingleLineBlocks && astUtils.isTokenOnSameLine(tokenBeforeFirst, tokenAfterLast)) {
+ return;
+ }
+
+ if (requirePaddingFor(node)) {
+ if (!blockHasTopPadding) {
+ context.report({
+ node,
+ loc: { line: tokenBeforeFirst.loc.start.line, column: tokenBeforeFirst.loc.start.column },
+ fix(fixer) {
+ return fixer.insertTextAfter(tokenBeforeFirst, "\n");
+ },
+ message: ALWAYS_MESSAGE
+ });
+ }
+ if (!blockHasBottomPadding) {
+ context.report({
+ node,
+ loc: { line: tokenAfterLast.loc.end.line, column: tokenAfterLast.loc.end.column - 1 },
+ fix(fixer) {
+ return fixer.insertTextBefore(tokenAfterLast, "\n");
+ },
+ message: ALWAYS_MESSAGE
+ });
+ }
+ } else {
+ if (blockHasTopPadding) {
+
+ context.report({
+ node,
+ loc: { line: tokenBeforeFirst.loc.start.line, column: tokenBeforeFirst.loc.start.column },
+ fix(fixer) {
+ return fixer.replaceTextRange([tokenBeforeFirst.range[1], firstBlockToken.range[0] - firstBlockToken.loc.start.column], "\n");
+ },
+ message: NEVER_MESSAGE
+ });
+ }
+
+ if (blockHasBottomPadding) {
+
+ context.report({
+ node,
+ loc: { line: tokenAfterLast.loc.end.line, column: tokenAfterLast.loc.end.column - 1 },
+ message: NEVER_MESSAGE,
+ fix(fixer) {
+ return fixer.replaceTextRange([lastBlockToken.range[1], tokenAfterLast.range[0] - tokenAfterLast.loc.start.column], "\n");
+ }
+ });
+ }
+ }
+ }
+
+ const rule = {};
+
+ if (Object.prototype.hasOwnProperty.call(options, "switches")) {
+ rule.SwitchStatement = function(node) {
+ if (node.cases.length === 0) {
+ return;
+ }
+ checkPadding(node);
+ };
+ }
+
+ if (Object.prototype.hasOwnProperty.call(options, "blocks")) {
+ rule.BlockStatement = function(node) {
+ if (node.body.length === 0) {
+ return;
+ }
+ checkPadding(node);
+ };
+ }
+
+ if (Object.prototype.hasOwnProperty.call(options, "classes")) {
+ rule.ClassBody = function(node) {
+ if (node.body.length === 0) {
+ return;
+ }
+ checkPadding(node);
+ };
+ }
+
+ return rule;
+ }
+};