UNPKG

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