• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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