]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
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 | ||
12 | const astUtils = require("./utils/ast-utils"); | |
13 | ||
14 | //------------------------------------------------------------------------------ | |
15 | // Rule Definition | |
16 | //------------------------------------------------------------------------------ | |
17 | ||
34eeec05 | 18 | /** @type {import('../shared/types').Rule} */ |
eb39fafa DC |
19 | module.exports = { |
20 | meta: { | |
21 | type: "layout", | |
22 | ||
23 | docs: { | |
8f9d1d4d | 24 | description: "Require or disallow padding within blocks", |
eb39fafa | 25 | recommended: false, |
f2a92ac6 | 26 | url: "https://eslint.org/docs/latest/rules/padded-blocks" |
eb39fafa DC |
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 | } | |
6f036462 TL |
61 | }, |
62 | additionalProperties: false | |
eb39fafa DC |
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 | ||
f2a92ac6 | 99 | const sourceCode = context.sourceCode; |
eb39fafa DC |
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 | } | |
609c276f TL |
110 | |
111 | if (node.type === "StaticBlock") { | |
112 | return sourceCode.getFirstToken(node, { skip: 1 }); // skip the `static` token | |
113 | } | |
114 | ||
115 | // `BlockStatement` or `ClassBody` | |
eb39fafa DC |
116 | return sourceCode.getFirstToken(node); |
117 | } | |
118 | ||
119 | /** | |
120 | * Checks if the given parameter is a comment node | |
121 | * @param {ASTNode|Token} node An AST node or token | |
122 | * @returns {boolean} True if node is a comment | |
123 | */ | |
124 | function isComment(node) { | |
125 | return node.type === "Line" || node.type === "Block"; | |
126 | } | |
127 | ||
128 | /** | |
129 | * Checks if there is padding between two tokens | |
130 | * @param {Token} first The first token | |
131 | * @param {Token} second The second token | |
132 | * @returns {boolean} True if there is at least a line between the tokens | |
133 | */ | |
134 | function isPaddingBetweenTokens(first, second) { | |
135 | return second.loc.start.line - first.loc.end.line >= 2; | |
136 | } | |
137 | ||
138 | ||
139 | /** | |
140 | * Checks if the given token has a blank line after it. | |
141 | * @param {Token} token The token to check. | |
142 | * @returns {boolean} Whether or not the token is followed by a blank line. | |
143 | */ | |
144 | function getFirstBlockToken(token) { | |
145 | let prev, | |
146 | first = token; | |
147 | ||
148 | do { | |
149 | prev = first; | |
150 | first = sourceCode.getTokenAfter(first, { includeComments: true }); | |
151 | } while (isComment(first) && first.loc.start.line === prev.loc.end.line); | |
152 | ||
153 | return first; | |
154 | } | |
155 | ||
156 | /** | |
157 | * Checks if the given token is preceded by a blank line. | |
158 | * @param {Token} token The token to check | |
159 | * @returns {boolean} Whether or not the token is preceded by a blank line | |
160 | */ | |
161 | function getLastBlockToken(token) { | |
162 | let last = token, | |
163 | next; | |
164 | ||
165 | do { | |
166 | next = last; | |
167 | last = sourceCode.getTokenBefore(last, { includeComments: true }); | |
168 | } while (isComment(last) && last.loc.end.line === next.loc.start.line); | |
169 | ||
170 | return last; | |
171 | } | |
172 | ||
173 | /** | |
174 | * Checks if a node should be padded, according to the rule config. | |
175 | * @param {ASTNode} node The AST node to check. | |
609c276f | 176 | * @throws {Error} (Unreachable) |
eb39fafa DC |
177 | * @returns {boolean} True if the node should be padded, false otherwise. |
178 | */ | |
179 | function requirePaddingFor(node) { | |
180 | switch (node.type) { | |
181 | case "BlockStatement": | |
609c276f | 182 | case "StaticBlock": |
eb39fafa DC |
183 | return options.blocks; |
184 | case "SwitchStatement": | |
185 | return options.switches; | |
186 | case "ClassBody": | |
187 | return options.classes; | |
188 | ||
8f9d1d4d | 189 | /* c8 ignore next */ |
eb39fafa DC |
190 | default: |
191 | throw new Error("unreachable"); | |
192 | } | |
193 | } | |
194 | ||
195 | /** | |
196 | * Checks the given BlockStatement node to be padded if the block is not empty. | |
197 | * @param {ASTNode} node The AST node of a BlockStatement. | |
198 | * @returns {void} undefined. | |
199 | */ | |
200 | function checkPadding(node) { | |
201 | const openBrace = getOpenBrace(node), | |
202 | firstBlockToken = getFirstBlockToken(openBrace), | |
203 | tokenBeforeFirst = sourceCode.getTokenBefore(firstBlockToken, { includeComments: true }), | |
204 | closeBrace = sourceCode.getLastToken(node), | |
205 | lastBlockToken = getLastBlockToken(closeBrace), | |
206 | tokenAfterLast = sourceCode.getTokenAfter(lastBlockToken, { includeComments: true }), | |
207 | blockHasTopPadding = isPaddingBetweenTokens(tokenBeforeFirst, firstBlockToken), | |
208 | blockHasBottomPadding = isPaddingBetweenTokens(lastBlockToken, tokenAfterLast); | |
209 | ||
210 | if (options.allowSingleLineBlocks && astUtils.isTokenOnSameLine(tokenBeforeFirst, tokenAfterLast)) { | |
211 | return; | |
212 | } | |
213 | ||
214 | if (requirePaddingFor(node)) { | |
ebb53d86 | 215 | |
eb39fafa DC |
216 | if (!blockHasTopPadding) { |
217 | context.report({ | |
218 | node, | |
ebb53d86 TL |
219 | loc: { |
220 | start: tokenBeforeFirst.loc.start, | |
221 | end: firstBlockToken.loc.start | |
222 | }, | |
eb39fafa DC |
223 | fix(fixer) { |
224 | return fixer.insertTextAfter(tokenBeforeFirst, "\n"); | |
225 | }, | |
226 | messageId: "alwaysPadBlock" | |
227 | }); | |
228 | } | |
229 | if (!blockHasBottomPadding) { | |
230 | context.report({ | |
231 | node, | |
ebb53d86 TL |
232 | loc: { |
233 | end: tokenAfterLast.loc.start, | |
234 | start: lastBlockToken.loc.end | |
235 | }, | |
eb39fafa DC |
236 | fix(fixer) { |
237 | return fixer.insertTextBefore(tokenAfterLast, "\n"); | |
238 | }, | |
239 | messageId: "alwaysPadBlock" | |
240 | }); | |
241 | } | |
242 | } else { | |
243 | if (blockHasTopPadding) { | |
244 | ||
245 | context.report({ | |
246 | node, | |
ebb53d86 TL |
247 | loc: { |
248 | start: tokenBeforeFirst.loc.start, | |
249 | end: firstBlockToken.loc.start | |
250 | }, | |
eb39fafa DC |
251 | fix(fixer) { |
252 | return fixer.replaceTextRange([tokenBeforeFirst.range[1], firstBlockToken.range[0] - firstBlockToken.loc.start.column], "\n"); | |
253 | }, | |
254 | messageId: "neverPadBlock" | |
255 | }); | |
256 | } | |
257 | ||
258 | if (blockHasBottomPadding) { | |
259 | ||
260 | context.report({ | |
261 | node, | |
ebb53d86 TL |
262 | loc: { |
263 | end: tokenAfterLast.loc.start, | |
264 | start: lastBlockToken.loc.end | |
265 | }, | |
eb39fafa DC |
266 | messageId: "neverPadBlock", |
267 | fix(fixer) { | |
268 | return fixer.replaceTextRange([lastBlockToken.range[1], tokenAfterLast.range[0] - tokenAfterLast.loc.start.column], "\n"); | |
269 | } | |
270 | }); | |
271 | } | |
272 | } | |
273 | } | |
274 | ||
275 | const rule = {}; | |
276 | ||
277 | if (Object.prototype.hasOwnProperty.call(options, "switches")) { | |
278 | rule.SwitchStatement = function(node) { | |
279 | if (node.cases.length === 0) { | |
280 | return; | |
281 | } | |
282 | checkPadding(node); | |
283 | }; | |
284 | } | |
285 | ||
286 | if (Object.prototype.hasOwnProperty.call(options, "blocks")) { | |
287 | rule.BlockStatement = function(node) { | |
288 | if (node.body.length === 0) { | |
289 | return; | |
290 | } | |
291 | checkPadding(node); | |
292 | }; | |
609c276f | 293 | rule.StaticBlock = rule.BlockStatement; |
eb39fafa DC |
294 | } |
295 | ||
296 | if (Object.prototype.hasOwnProperty.call(options, "classes")) { | |
297 | rule.ClassBody = function(node) { | |
298 | if (node.body.length === 0) { | |
299 | return; | |
300 | } | |
301 | checkPadding(node); | |
302 | }; | |
303 | } | |
304 | ||
305 | return rule; | |
306 | } | |
307 | }; |