]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview A rule to ensure whitespace before blocks. | |
3 | * @author Mathias Schreck <https://github.com/lo1tuma> | |
4 | */ | |
5 | ||
6 | "use strict"; | |
7 | ||
6f036462 TL |
8 | //------------------------------------------------------------------------------ |
9 | // Requirements | |
10 | //------------------------------------------------------------------------------ | |
11 | ||
eb39fafa DC |
12 | const astUtils = require("./utils/ast-utils"); |
13 | ||
6f036462 TL |
14 | //------------------------------------------------------------------------------ |
15 | // Helpers | |
16 | //------------------------------------------------------------------------------ | |
17 | ||
18 | /** | |
19 | * Checks whether the given node represents the body of a function. | |
20 | * @param {ASTNode} node the node to check. | |
21 | * @returns {boolean} `true` if the node is function body. | |
22 | */ | |
23 | function isFunctionBody(node) { | |
24 | const parent = node.parent; | |
25 | ||
26 | return ( | |
27 | node.type === "BlockStatement" && | |
28 | astUtils.isFunction(parent) && | |
29 | parent.body === node | |
30 | ); | |
31 | } | |
32 | ||
eb39fafa DC |
33 | //------------------------------------------------------------------------------ |
34 | // Rule Definition | |
35 | //------------------------------------------------------------------------------ | |
36 | ||
34eeec05 | 37 | /** @type {import('../shared/types').Rule} */ |
eb39fafa DC |
38 | module.exports = { |
39 | meta: { | |
40 | type: "layout", | |
41 | ||
42 | docs: { | |
8f9d1d4d | 43 | description: "Enforce consistent spacing before blocks", |
eb39fafa DC |
44 | recommended: false, |
45 | url: "https://eslint.org/docs/rules/space-before-blocks" | |
46 | }, | |
47 | ||
48 | fixable: "whitespace", | |
49 | ||
50 | schema: [ | |
51 | { | |
52 | oneOf: [ | |
53 | { | |
54 | enum: ["always", "never"] | |
55 | }, | |
56 | { | |
57 | type: "object", | |
58 | properties: { | |
59 | keywords: { | |
60 | enum: ["always", "never", "off"] | |
61 | }, | |
62 | functions: { | |
63 | enum: ["always", "never", "off"] | |
64 | }, | |
65 | classes: { | |
66 | enum: ["always", "never", "off"] | |
67 | } | |
68 | }, | |
69 | additionalProperties: false | |
70 | } | |
71 | ] | |
72 | } | |
73 | ], | |
74 | ||
75 | messages: { | |
76 | unexpectedSpace: "Unexpected space before opening brace.", | |
77 | missingSpace: "Missing space before opening brace." | |
78 | } | |
79 | }, | |
80 | ||
81 | create(context) { | |
82 | const config = context.options[0], | |
83 | sourceCode = context.getSourceCode(); | |
84 | let alwaysFunctions = true, | |
85 | alwaysKeywords = true, | |
86 | alwaysClasses = true, | |
87 | neverFunctions = false, | |
88 | neverKeywords = false, | |
89 | neverClasses = false; | |
90 | ||
91 | if (typeof config === "object") { | |
92 | alwaysFunctions = config.functions === "always"; | |
93 | alwaysKeywords = config.keywords === "always"; | |
94 | alwaysClasses = config.classes === "always"; | |
95 | neverFunctions = config.functions === "never"; | |
96 | neverKeywords = config.keywords === "never"; | |
97 | neverClasses = config.classes === "never"; | |
98 | } else if (config === "never") { | |
99 | alwaysFunctions = false; | |
100 | alwaysKeywords = false; | |
101 | alwaysClasses = false; | |
102 | neverFunctions = true; | |
103 | neverKeywords = true; | |
104 | neverClasses = true; | |
105 | } | |
106 | ||
107 | /** | |
6f036462 TL |
108 | * Checks whether the spacing before the given block is already controlled by another rule: |
109 | * - `arrow-spacing` checks spaces after `=>`. | |
110 | * - `keyword-spacing` checks spaces after keywords in certain contexts. | |
609c276f | 111 | * - `switch-colon-spacing` checks spaces after `:` of switch cases. |
6f036462 TL |
112 | * @param {Token} precedingToken first token before the block. |
113 | * @param {ASTNode|Token} node `BlockStatement` node or `{` token of a `SwitchStatement` node. | |
114 | * @returns {boolean} `true` if requiring or disallowing spaces before the given block could produce conflicts with other rules. | |
eb39fafa | 115 | */ |
6f036462 | 116 | function isConflicted(precedingToken, node) { |
609c276f TL |
117 | return ( |
118 | astUtils.isArrowToken(precedingToken) || | |
119 | ( | |
120 | astUtils.isKeywordToken(precedingToken) && | |
121 | !isFunctionBody(node) | |
122 | ) || | |
123 | ( | |
124 | astUtils.isColonToken(precedingToken) && | |
125 | node.parent && | |
126 | node.parent.type === "SwitchCase" && | |
127 | precedingToken === astUtils.getSwitchCaseColonToken(node.parent, sourceCode) | |
128 | ) | |
129 | ); | |
eb39fafa DC |
130 | } |
131 | ||
132 | /** | |
133 | * Checks the given BlockStatement node has a preceding space if it doesn’t start on a new line. | |
134 | * @param {ASTNode|Token} node The AST node of a BlockStatement. | |
135 | * @returns {void} undefined. | |
136 | */ | |
137 | function checkPrecedingSpace(node) { | |
138 | const precedingToken = sourceCode.getTokenBefore(node); | |
139 | ||
6f036462 | 140 | if (precedingToken && !isConflicted(precedingToken, node) && astUtils.isTokenOnSameLine(precedingToken, node)) { |
eb39fafa | 141 | const hasSpace = sourceCode.isSpaceBetweenTokens(precedingToken, node); |
eb39fafa DC |
142 | let requireSpace; |
143 | let requireNoSpace; | |
144 | ||
6f036462 | 145 | if (isFunctionBody(node)) { |
eb39fafa DC |
146 | requireSpace = alwaysFunctions; |
147 | requireNoSpace = neverFunctions; | |
148 | } else if (node.type === "ClassBody") { | |
149 | requireSpace = alwaysClasses; | |
150 | requireNoSpace = neverClasses; | |
151 | } else { | |
152 | requireSpace = alwaysKeywords; | |
153 | requireNoSpace = neverKeywords; | |
154 | } | |
155 | ||
156 | if (requireSpace && !hasSpace) { | |
157 | context.report({ | |
158 | node, | |
159 | messageId: "missingSpace", | |
160 | fix(fixer) { | |
161 | return fixer.insertTextBefore(node, " "); | |
162 | } | |
163 | }); | |
164 | } else if (requireNoSpace && hasSpace) { | |
165 | context.report({ | |
166 | node, | |
167 | messageId: "unexpectedSpace", | |
168 | fix(fixer) { | |
169 | return fixer.removeRange([precedingToken.range[1], node.range[0]]); | |
170 | } | |
171 | }); | |
172 | } | |
173 | } | |
174 | } | |
175 | ||
176 | /** | |
177 | * Checks if the CaseBlock of an given SwitchStatement node has a preceding space. | |
178 | * @param {ASTNode} node The node of a SwitchStatement. | |
179 | * @returns {void} undefined. | |
180 | */ | |
181 | function checkSpaceBeforeCaseBlock(node) { | |
182 | const cases = node.cases; | |
183 | let openingBrace; | |
184 | ||
185 | if (cases.length > 0) { | |
186 | openingBrace = sourceCode.getTokenBefore(cases[0]); | |
187 | } else { | |
188 | openingBrace = sourceCode.getLastToken(node, 1); | |
189 | } | |
190 | ||
191 | checkPrecedingSpace(openingBrace); | |
192 | } | |
193 | ||
194 | return { | |
195 | BlockStatement: checkPrecedingSpace, | |
196 | ClassBody: checkPrecedingSpace, | |
197 | SwitchStatement: checkSpaceBeforeCaseBlock | |
198 | }; | |
199 | ||
200 | } | |
201 | }; |