1/** 2 * @fileoverview A rule to ensure blank lines within blocks. 3 * @author Mathias Schreck <https://github.com/lo1tuma> 4 */ 5 6"use strict"; 7 8//------------------------------------------------------------------------------ 9// Requirements 10//------------------------------------------------------------------------------ 11 12const astUtils = require("./utils/ast-utils"); 13 14//------------------------------------------------------------------------------ 15// Rule Definition 16//------------------------------------------------------------------------------ 17 18module.exports = { 19 meta: { 20 type: "layout", 21 22 docs: { 23 description: "require or disallow padding within blocks", 24 category: "Stylistic Issues", 25 recommended: false, 26 url: "https://eslint.org/docs/rules/padded-blocks" 27 }, 28 29 fixable: "whitespace", 30 31 schema: [ 32 { 33 oneOf: [ 34 { 35 enum: ["always", "never"] 36 }, 37 { 38 type: "object", 39 properties: { 40 blocks: { 41 enum: ["always", "never"] 42 }, 43 switches: { 44 enum: ["always", "never"] 45 }, 46 classes: { 47 enum: ["always", "never"] 48 } 49 }, 50 additionalProperties: false, 51 minProperties: 1 52 } 53 ] 54 }, 55 { 56 type: "object", 57 properties: { 58 allowSingleLineBlocks: { 59 type: "boolean" 60 } 61 } 62 } 63 ], 64 65 messages: { 66 alwaysPadBlock: "Block must be padded by blank lines.", 67 neverPadBlock: "Block must not be padded by blank lines." 68 } 69 }, 70 71 create(context) { 72 const options = {}; 73 const typeOptions = context.options[0] || "always"; 74 const exceptOptions = context.options[1] || {}; 75 76 if (typeof typeOptions === "string") { 77 const shouldHavePadding = typeOptions === "always"; 78 79 options.blocks = shouldHavePadding; 80 options.switches = shouldHavePadding; 81 options.classes = shouldHavePadding; 82 } else { 83 if (Object.prototype.hasOwnProperty.call(typeOptions, "blocks")) { 84 options.blocks = typeOptions.blocks === "always"; 85 } 86 if (Object.prototype.hasOwnProperty.call(typeOptions, "switches")) { 87 options.switches = typeOptions.switches === "always"; 88 } 89 if (Object.prototype.hasOwnProperty.call(typeOptions, "classes")) { 90 options.classes = typeOptions.classes === "always"; 91 } 92 } 93 94 if (Object.prototype.hasOwnProperty.call(exceptOptions, "allowSingleLineBlocks")) { 95 options.allowSingleLineBlocks = exceptOptions.allowSingleLineBlocks === true; 96 } 97 98 const sourceCode = context.getSourceCode(); 99 100 /** 101 * Gets the open brace token from a given node. 102 * @param {ASTNode} node A BlockStatement or SwitchStatement node from which to get the open brace. 103 * @returns {Token} The token of the open brace. 104 */ 105 function getOpenBrace(node) { 106 if (node.type === "SwitchStatement") { 107 return sourceCode.getTokenBefore(node.cases[0]); 108 } 109 return sourceCode.getFirstToken(node); 110 } 111 112 /** 113 * Checks if the given parameter is a comment node 114 * @param {ASTNode|Token} node An AST node or token 115 * @returns {boolean} True if node is a comment 116 */ 117 function isComment(node) { 118 return node.type === "Line" || node.type === "Block"; 119 } 120 121 /** 122 * Checks if there is padding between two tokens 123 * @param {Token} first The first token 124 * @param {Token} second The second token 125 * @returns {boolean} True if there is at least a line between the tokens 126 */ 127 function isPaddingBetweenTokens(first, second) { 128 return second.loc.start.line - first.loc.end.line >= 2; 129 } 130 131 132 /** 133 * Checks if the given token has a blank line after it. 134 * @param {Token} token The token to check. 135 * @returns {boolean} Whether or not the token is followed by a blank line. 136 */ 137 function getFirstBlockToken(token) { 138 let prev, 139 first = token; 140 141 do { 142 prev = first; 143 first = sourceCode.getTokenAfter(first, { includeComments: true }); 144 } while (isComment(first) && first.loc.start.line === prev.loc.end.line); 145 146 return first; 147 } 148 149 /** 150 * Checks if the given token is preceded by a blank line. 151 * @param {Token} token The token to check 152 * @returns {boolean} Whether or not the token is preceded by a blank line 153 */ 154 function getLastBlockToken(token) { 155 let last = token, 156 next; 157 158 do { 159 next = last; 160 last = sourceCode.getTokenBefore(last, { includeComments: true }); 161 } while (isComment(last) && last.loc.end.line === next.loc.start.line); 162 163 return last; 164 } 165 166 /** 167 * Checks if a node should be padded, according to the rule config. 168 * @param {ASTNode} node The AST node to check. 169 * @returns {boolean} True if the node should be padded, false otherwise. 170 */ 171 function requirePaddingFor(node) { 172 switch (node.type) { 173 case "BlockStatement": 174 return options.blocks; 175 case "SwitchStatement": 176 return options.switches; 177 case "ClassBody": 178 return options.classes; 179 180 /* istanbul ignore next */ 181 default: 182 throw new Error("unreachable"); 183 } 184 } 185 186 /** 187 * Checks the given BlockStatement node to be padded if the block is not empty. 188 * @param {ASTNode} node The AST node of a BlockStatement. 189 * @returns {void} undefined. 190 */ 191 function checkPadding(node) { 192 const openBrace = getOpenBrace(node), 193 firstBlockToken = getFirstBlockToken(openBrace), 194 tokenBeforeFirst = sourceCode.getTokenBefore(firstBlockToken, { includeComments: true }), 195 closeBrace = sourceCode.getLastToken(node), 196 lastBlockToken = getLastBlockToken(closeBrace), 197 tokenAfterLast = sourceCode.getTokenAfter(lastBlockToken, { includeComments: true }), 198 blockHasTopPadding = isPaddingBetweenTokens(tokenBeforeFirst, firstBlockToken), 199 blockHasBottomPadding = isPaddingBetweenTokens(lastBlockToken, tokenAfterLast); 200 201 if (options.allowSingleLineBlocks && astUtils.isTokenOnSameLine(tokenBeforeFirst, tokenAfterLast)) { 202 return; 203 } 204 205 if (requirePaddingFor(node)) { 206 207 if (!blockHasTopPadding) { 208 context.report({ 209 node, 210 loc: { 211 start: tokenBeforeFirst.loc.start, 212 end: firstBlockToken.loc.start 213 }, 214 fix(fixer) { 215 return fixer.insertTextAfter(tokenBeforeFirst, "\n"); 216 }, 217 messageId: "alwaysPadBlock" 218 }); 219 } 220 if (!blockHasBottomPadding) { 221 context.report({ 222 node, 223 loc: { 224 end: tokenAfterLast.loc.start, 225 start: lastBlockToken.loc.end 226 }, 227 fix(fixer) { 228 return fixer.insertTextBefore(tokenAfterLast, "\n"); 229 }, 230 messageId: "alwaysPadBlock" 231 }); 232 } 233 } else { 234 if (blockHasTopPadding) { 235 236 context.report({ 237 node, 238 loc: { 239 start: tokenBeforeFirst.loc.start, 240 end: firstBlockToken.loc.start 241 }, 242 fix(fixer) { 243 return fixer.replaceTextRange([tokenBeforeFirst.range[1], firstBlockToken.range[0] - firstBlockToken.loc.start.column], "\n"); 244 }, 245 messageId: "neverPadBlock" 246 }); 247 } 248 249 if (blockHasBottomPadding) { 250 251 context.report({ 252 node, 253 loc: { 254 end: tokenAfterLast.loc.start, 255 start: lastBlockToken.loc.end 256 }, 257 messageId: "neverPadBlock", 258 fix(fixer) { 259 return fixer.replaceTextRange([lastBlockToken.range[1], tokenAfterLast.range[0] - tokenAfterLast.loc.start.column], "\n"); 260 } 261 }); 262 } 263 } 264 } 265 266 const rule = {}; 267 268 if (Object.prototype.hasOwnProperty.call(options, "switches")) { 269 rule.SwitchStatement = function(node) { 270 if (node.cases.length === 0) { 271 return; 272 } 273 checkPadding(node); 274 }; 275 } 276 277 if (Object.prototype.hasOwnProperty.call(options, "blocks")) { 278 rule.BlockStatement = function(node) { 279 if (node.body.length === 0) { 280 return; 281 } 282 checkPadding(node); 283 }; 284 } 285 286 if (Object.prototype.hasOwnProperty.call(options, "classes")) { 287 rule.ClassBody = function(node) { 288 if (node.body.length === 0) { 289 return; 290 } 291 checkPadding(node); 292 }; 293 } 294 295 return rule; 296 } 297}; 298