UNPKG

10.6 kBJavaScriptView Raw
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 additionalProperties: false
63 }
64 ],
65
66 messages: {
67 alwaysPadBlock: "Block must be padded by blank lines.",
68 neverPadBlock: "Block must not be padded by blank lines."
69 }
70 },
71
72 create(context) {
73 const options = {};
74 const typeOptions = context.options[0] || "always";
75 const exceptOptions = context.options[1] || {};
76
77 if (typeof typeOptions === "string") {
78 const shouldHavePadding = typeOptions === "always";
79
80 options.blocks = shouldHavePadding;
81 options.switches = shouldHavePadding;
82 options.classes = shouldHavePadding;
83 } else {
84 if (Object.prototype.hasOwnProperty.call(typeOptions, "blocks")) {
85 options.blocks = typeOptions.blocks === "always";
86 }
87 if (Object.prototype.hasOwnProperty.call(typeOptions, "switches")) {
88 options.switches = typeOptions.switches === "always";
89 }
90 if (Object.prototype.hasOwnProperty.call(typeOptions, "classes")) {
91 options.classes = typeOptions.classes === "always";
92 }
93 }
94
95 if (Object.prototype.hasOwnProperty.call(exceptOptions, "allowSingleLineBlocks")) {
96 options.allowSingleLineBlocks = exceptOptions.allowSingleLineBlocks === true;
97 }
98
99 const sourceCode = context.getSourceCode();
100
101 /**
102 * Gets the open brace token from a given node.
103 * @param {ASTNode} node A BlockStatement or SwitchStatement node from which to get the open brace.
104 * @returns {Token} The token of the open brace.
105 */
106 function getOpenBrace(node) {
107 if (node.type === "SwitchStatement") {
108 return sourceCode.getTokenBefore(node.cases[0]);
109 }
110 return sourceCode.getFirstToken(node);
111 }
112
113 /**
114 * Checks if the given parameter is a comment node
115 * @param {ASTNode|Token} node An AST node or token
116 * @returns {boolean} True if node is a comment
117 */
118 function isComment(node) {
119 return node.type === "Line" || node.type === "Block";
120 }
121
122 /**
123 * Checks if there is padding between two tokens
124 * @param {Token} first The first token
125 * @param {Token} second The second token
126 * @returns {boolean} True if there is at least a line between the tokens
127 */
128 function isPaddingBetweenTokens(first, second) {
129 return second.loc.start.line - first.loc.end.line >= 2;
130 }
131
132
133 /**
134 * Checks if the given token has a blank line after it.
135 * @param {Token} token The token to check.
136 * @returns {boolean} Whether or not the token is followed by a blank line.
137 */
138 function getFirstBlockToken(token) {
139 let prev,
140 first = token;
141
142 do {
143 prev = first;
144 first = sourceCode.getTokenAfter(first, { includeComments: true });
145 } while (isComment(first) && first.loc.start.line === prev.loc.end.line);
146
147 return first;
148 }
149
150 /**
151 * Checks if the given token is preceded by a blank line.
152 * @param {Token} token The token to check
153 * @returns {boolean} Whether or not the token is preceded by a blank line
154 */
155 function getLastBlockToken(token) {
156 let last = token,
157 next;
158
159 do {
160 next = last;
161 last = sourceCode.getTokenBefore(last, { includeComments: true });
162 } while (isComment(last) && last.loc.end.line === next.loc.start.line);
163
164 return last;
165 }
166
167 /**
168 * Checks if a node should be padded, according to the rule config.
169 * @param {ASTNode} node The AST node to check.
170 * @returns {boolean} True if the node should be padded, false otherwise.
171 */
172 function requirePaddingFor(node) {
173 switch (node.type) {
174 case "BlockStatement":
175 return options.blocks;
176 case "SwitchStatement":
177 return options.switches;
178 case "ClassBody":
179 return options.classes;
180
181 /* istanbul ignore next */
182 default:
183 throw new Error("unreachable");
184 }
185 }
186
187 /**
188 * Checks the given BlockStatement node to be padded if the block is not empty.
189 * @param {ASTNode} node The AST node of a BlockStatement.
190 * @returns {void} undefined.
191 */
192 function checkPadding(node) {
193 const openBrace = getOpenBrace(node),
194 firstBlockToken = getFirstBlockToken(openBrace),
195 tokenBeforeFirst = sourceCode.getTokenBefore(firstBlockToken, { includeComments: true }),
196 closeBrace = sourceCode.getLastToken(node),
197 lastBlockToken = getLastBlockToken(closeBrace),
198 tokenAfterLast = sourceCode.getTokenAfter(lastBlockToken, { includeComments: true }),
199 blockHasTopPadding = isPaddingBetweenTokens(tokenBeforeFirst, firstBlockToken),
200 blockHasBottomPadding = isPaddingBetweenTokens(lastBlockToken, tokenAfterLast);
201
202 if (options.allowSingleLineBlocks && astUtils.isTokenOnSameLine(tokenBeforeFirst, tokenAfterLast)) {
203 return;
204 }
205
206 if (requirePaddingFor(node)) {
207
208 if (!blockHasTopPadding) {
209 context.report({
210 node,
211 loc: {
212 start: tokenBeforeFirst.loc.start,
213 end: firstBlockToken.loc.start
214 },
215 fix(fixer) {
216 return fixer.insertTextAfter(tokenBeforeFirst, "\n");
217 },
218 messageId: "alwaysPadBlock"
219 });
220 }
221 if (!blockHasBottomPadding) {
222 context.report({
223 node,
224 loc: {
225 end: tokenAfterLast.loc.start,
226 start: lastBlockToken.loc.end
227 },
228 fix(fixer) {
229 return fixer.insertTextBefore(tokenAfterLast, "\n");
230 },
231 messageId: "alwaysPadBlock"
232 });
233 }
234 } else {
235 if (blockHasTopPadding) {
236
237 context.report({
238 node,
239 loc: {
240 start: tokenBeforeFirst.loc.start,
241 end: firstBlockToken.loc.start
242 },
243 fix(fixer) {
244 return fixer.replaceTextRange([tokenBeforeFirst.range[1], firstBlockToken.range[0] - firstBlockToken.loc.start.column], "\n");
245 },
246 messageId: "neverPadBlock"
247 });
248 }
249
250 if (blockHasBottomPadding) {
251
252 context.report({
253 node,
254 loc: {
255 end: tokenAfterLast.loc.start,
256 start: lastBlockToken.loc.end
257 },
258 messageId: "neverPadBlock",
259 fix(fixer) {
260 return fixer.replaceTextRange([lastBlockToken.range[1], tokenAfterLast.range[0] - tokenAfterLast.loc.start.column], "\n");
261 }
262 });
263 }
264 }
265 }
266
267 const rule = {};
268
269 if (Object.prototype.hasOwnProperty.call(options, "switches")) {
270 rule.SwitchStatement = function(node) {
271 if (node.cases.length === 0) {
272 return;
273 }
274 checkPadding(node);
275 };
276 }
277
278 if (Object.prototype.hasOwnProperty.call(options, "blocks")) {
279 rule.BlockStatement = function(node) {
280 if (node.body.length === 0) {
281 return;
282 }
283 checkPadding(node);
284 };
285 }
286
287 if (Object.prototype.hasOwnProperty.call(options, "classes")) {
288 rule.ClassBody = function(node) {
289 if (node.body.length === 0) {
290 return;
291 }
292 checkPadding(node);
293 };
294 }
295
296 return rule;
297 }
298};