/** * @fileoverview A rule to ensure blank lines within blocks. * @author Mathias Schreck */ "use strict"; //------------------------------------------------------------------------------ // Requirements //------------------------------------------------------------------------------ const astUtils = require("./utils/ast-utils"); //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ /** @type {import('../shared/types').Rule} */ module.exports = { meta: { type: "layout", docs: { description: "Require or disallow padding within blocks", recommended: false, url: "https://eslint.org/docs/latest/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" } }, additionalProperties: false } ], messages: { alwaysPadBlock: "Block must be padded by blank lines.", neverPadBlock: "Block must not be padded by blank lines." } }, 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 sourceCode = context.sourceCode; /** * 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]); } if (node.type === "StaticBlock") { return sourceCode.getFirstToken(node, { skip: 1 }); // skip the `static` token } // `BlockStatement` or `ClassBody` 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 preceded by a blank line. * @param {Token} token The token to check * @returns {boolean} Whether or not the token is preceded 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. * @throws {Error} (Unreachable) * @returns {boolean} True if the node should be padded, false otherwise. */ function requirePaddingFor(node) { switch (node.type) { case "BlockStatement": case "StaticBlock": return options.blocks; case "SwitchStatement": return options.switches; case "ClassBody": return options.classes; /* c8 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: { start: tokenBeforeFirst.loc.start, end: firstBlockToken.loc.start }, fix(fixer) { return fixer.insertTextAfter(tokenBeforeFirst, "\n"); }, messageId: "alwaysPadBlock" }); } if (!blockHasBottomPadding) { context.report({ node, loc: { end: tokenAfterLast.loc.start, start: lastBlockToken.loc.end }, fix(fixer) { return fixer.insertTextBefore(tokenAfterLast, "\n"); }, messageId: "alwaysPadBlock" }); } } else { if (blockHasTopPadding) { context.report({ node, loc: { start: tokenBeforeFirst.loc.start, end: firstBlockToken.loc.start }, fix(fixer) { return fixer.replaceTextRange([tokenBeforeFirst.range[1], firstBlockToken.range[0] - firstBlockToken.loc.start.column], "\n"); }, messageId: "neverPadBlock" }); } if (blockHasBottomPadding) { context.report({ node, loc: { end: tokenAfterLast.loc.start, start: lastBlockToken.loc.end }, messageId: "neverPadBlock", 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); }; rule.StaticBlock = rule.BlockStatement; } if (Object.prototype.hasOwnProperty.call(options, "classes")) { rule.ClassBody = function(node) { if (node.body.length === 0) { return; } checkPadding(node); }; } return rule; } };