]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview Disallows or enforces spaces inside of parentheses. | |
3 | * @author Jonathan Rajavuori | |
4 | */ | |
5 | "use strict"; | |
6 | ||
7 | const astUtils = require("./utils/ast-utils"); | |
8 | ||
9 | //------------------------------------------------------------------------------ | |
10 | // Rule Definition | |
11 | //------------------------------------------------------------------------------ | |
12 | ||
34eeec05 | 13 | /** @type {import('../shared/types').Rule} */ |
eb39fafa DC |
14 | module.exports = { |
15 | meta: { | |
16 | type: "layout", | |
17 | ||
18 | docs: { | |
8f9d1d4d | 19 | description: "Enforce consistent spacing inside parentheses", |
eb39fafa | 20 | recommended: false, |
f2a92ac6 | 21 | url: "https://eslint.org/docs/latest/rules/space-in-parens" |
eb39fafa DC |
22 | }, |
23 | ||
24 | fixable: "whitespace", | |
25 | ||
26 | schema: [ | |
27 | { | |
28 | enum: ["always", "never"] | |
29 | }, | |
30 | { | |
31 | type: "object", | |
32 | properties: { | |
33 | exceptions: { | |
34 | type: "array", | |
35 | items: { | |
36 | enum: ["{}", "[]", "()", "empty"] | |
37 | }, | |
38 | uniqueItems: true | |
39 | } | |
40 | }, | |
41 | additionalProperties: false | |
42 | } | |
43 | ], | |
44 | ||
45 | messages: { | |
46 | missingOpeningSpace: "There must be a space after this paren.", | |
47 | missingClosingSpace: "There must be a space before this paren.", | |
48 | rejectedOpeningSpace: "There should be no space after this paren.", | |
49 | rejectedClosingSpace: "There should be no space before this paren." | |
50 | } | |
51 | }, | |
52 | ||
53 | create(context) { | |
54 | const ALWAYS = context.options[0] === "always", | |
55 | exceptionsArrayOptions = (context.options[1] && context.options[1].exceptions) || [], | |
56 | options = {}; | |
57 | ||
58 | let exceptions; | |
59 | ||
60 | if (exceptionsArrayOptions.length) { | |
61 | options.braceException = exceptionsArrayOptions.includes("{}"); | |
62 | options.bracketException = exceptionsArrayOptions.includes("[]"); | |
63 | options.parenException = exceptionsArrayOptions.includes("()"); | |
64 | options.empty = exceptionsArrayOptions.includes("empty"); | |
65 | } | |
66 | ||
67 | /** | |
68 | * Produces an object with the opener and closer exception values | |
69 | * @returns {Object} `openers` and `closers` exception values | |
70 | * @private | |
71 | */ | |
72 | function getExceptions() { | |
73 | const openers = [], | |
74 | closers = []; | |
75 | ||
76 | if (options.braceException) { | |
77 | openers.push("{"); | |
78 | closers.push("}"); | |
79 | } | |
80 | ||
81 | if (options.bracketException) { | |
82 | openers.push("["); | |
83 | closers.push("]"); | |
84 | } | |
85 | ||
86 | if (options.parenException) { | |
87 | openers.push("("); | |
88 | closers.push(")"); | |
89 | } | |
90 | ||
91 | if (options.empty) { | |
92 | openers.push(")"); | |
93 | closers.push("("); | |
94 | } | |
95 | ||
96 | return { | |
97 | openers, | |
98 | closers | |
99 | }; | |
100 | } | |
101 | ||
102 | //-------------------------------------------------------------------------- | |
103 | // Helpers | |
104 | //-------------------------------------------------------------------------- | |
f2a92ac6 | 105 | const sourceCode = context.sourceCode; |
eb39fafa DC |
106 | |
107 | /** | |
108 | * Determines if a token is one of the exceptions for the opener paren | |
109 | * @param {Object} token The token to check | |
110 | * @returns {boolean} True if the token is one of the exceptions for the opener paren | |
111 | */ | |
112 | function isOpenerException(token) { | |
113 | return exceptions.openers.includes(token.value); | |
114 | } | |
115 | ||
116 | /** | |
117 | * Determines if a token is one of the exceptions for the closer paren | |
118 | * @param {Object} token The token to check | |
119 | * @returns {boolean} True if the token is one of the exceptions for the closer paren | |
120 | */ | |
121 | function isCloserException(token) { | |
122 | return exceptions.closers.includes(token.value); | |
123 | } | |
124 | ||
125 | /** | |
126 | * Determines if an opening paren is immediately followed by a required space | |
127 | * @param {Object} openingParenToken The paren token | |
128 | * @param {Object} tokenAfterOpeningParen The token after it | |
129 | * @returns {boolean} True if the opening paren is missing a required space | |
130 | */ | |
131 | function openerMissingSpace(openingParenToken, tokenAfterOpeningParen) { | |
132 | if (sourceCode.isSpaceBetweenTokens(openingParenToken, tokenAfterOpeningParen)) { | |
133 | return false; | |
134 | } | |
135 | ||
136 | if (!options.empty && astUtils.isClosingParenToken(tokenAfterOpeningParen)) { | |
137 | return false; | |
138 | } | |
139 | ||
140 | if (ALWAYS) { | |
141 | return !isOpenerException(tokenAfterOpeningParen); | |
142 | } | |
143 | return isOpenerException(tokenAfterOpeningParen); | |
144 | } | |
145 | ||
146 | /** | |
147 | * Determines if an opening paren is immediately followed by a disallowed space | |
148 | * @param {Object} openingParenToken The paren token | |
149 | * @param {Object} tokenAfterOpeningParen The token after it | |
150 | * @returns {boolean} True if the opening paren has a disallowed space | |
151 | */ | |
152 | function openerRejectsSpace(openingParenToken, tokenAfterOpeningParen) { | |
153 | if (!astUtils.isTokenOnSameLine(openingParenToken, tokenAfterOpeningParen)) { | |
154 | return false; | |
155 | } | |
156 | ||
157 | if (tokenAfterOpeningParen.type === "Line") { | |
158 | return false; | |
159 | } | |
160 | ||
161 | if (!sourceCode.isSpaceBetweenTokens(openingParenToken, tokenAfterOpeningParen)) { | |
162 | return false; | |
163 | } | |
164 | ||
165 | if (ALWAYS) { | |
166 | return isOpenerException(tokenAfterOpeningParen); | |
167 | } | |
168 | return !isOpenerException(tokenAfterOpeningParen); | |
169 | } | |
170 | ||
171 | /** | |
172 | * Determines if a closing paren is immediately preceded by a required space | |
173 | * @param {Object} tokenBeforeClosingParen The token before the paren | |
174 | * @param {Object} closingParenToken The paren token | |
175 | * @returns {boolean} True if the closing paren is missing a required space | |
176 | */ | |
177 | function closerMissingSpace(tokenBeforeClosingParen, closingParenToken) { | |
178 | if (sourceCode.isSpaceBetweenTokens(tokenBeforeClosingParen, closingParenToken)) { | |
179 | return false; | |
180 | } | |
181 | ||
182 | if (!options.empty && astUtils.isOpeningParenToken(tokenBeforeClosingParen)) { | |
183 | return false; | |
184 | } | |
185 | ||
186 | if (ALWAYS) { | |
187 | return !isCloserException(tokenBeforeClosingParen); | |
188 | } | |
189 | return isCloserException(tokenBeforeClosingParen); | |
190 | } | |
191 | ||
192 | /** | |
193 | * Determines if a closer paren is immediately preceded by a disallowed space | |
194 | * @param {Object} tokenBeforeClosingParen The token before the paren | |
195 | * @param {Object} closingParenToken The paren token | |
196 | * @returns {boolean} True if the closing paren has a disallowed space | |
197 | */ | |
198 | function closerRejectsSpace(tokenBeforeClosingParen, closingParenToken) { | |
199 | if (!astUtils.isTokenOnSameLine(tokenBeforeClosingParen, closingParenToken)) { | |
200 | return false; | |
201 | } | |
202 | ||
203 | if (!sourceCode.isSpaceBetweenTokens(tokenBeforeClosingParen, closingParenToken)) { | |
204 | return false; | |
205 | } | |
206 | ||
207 | if (ALWAYS) { | |
208 | return isCloserException(tokenBeforeClosingParen); | |
209 | } | |
210 | return !isCloserException(tokenBeforeClosingParen); | |
211 | } | |
212 | ||
213 | //-------------------------------------------------------------------------- | |
214 | // Public | |
215 | //-------------------------------------------------------------------------- | |
216 | ||
217 | return { | |
218 | Program: function checkParenSpaces(node) { | |
219 | exceptions = getExceptions(); | |
220 | const tokens = sourceCode.tokensAndComments; | |
221 | ||
222 | tokens.forEach((token, i) => { | |
223 | const prevToken = tokens[i - 1]; | |
224 | const nextToken = tokens[i + 1]; | |
225 | ||
226 | // if token is not an opening or closing paren token, do nothing | |
227 | if (!astUtils.isOpeningParenToken(token) && !astUtils.isClosingParenToken(token)) { | |
228 | return; | |
229 | } | |
230 | ||
231 | // if token is an opening paren and is not followed by a required space | |
232 | if (token.value === "(" && openerMissingSpace(token, nextToken)) { | |
233 | context.report({ | |
234 | node, | |
235 | loc: token.loc, | |
236 | messageId: "missingOpeningSpace", | |
237 | fix(fixer) { | |
238 | return fixer.insertTextAfter(token, " "); | |
239 | } | |
240 | }); | |
241 | } | |
242 | ||
243 | // if token is an opening paren and is followed by a disallowed space | |
244 | if (token.value === "(" && openerRejectsSpace(token, nextToken)) { | |
245 | context.report({ | |
246 | node, | |
247 | loc: { start: token.loc.end, end: nextToken.loc.start }, | |
248 | messageId: "rejectedOpeningSpace", | |
249 | fix(fixer) { | |
250 | return fixer.removeRange([token.range[1], nextToken.range[0]]); | |
251 | } | |
252 | }); | |
253 | } | |
254 | ||
255 | // if token is a closing paren and is not preceded by a required space | |
256 | if (token.value === ")" && closerMissingSpace(prevToken, token)) { | |
257 | context.report({ | |
258 | node, | |
259 | loc: token.loc, | |
260 | messageId: "missingClosingSpace", | |
261 | fix(fixer) { | |
262 | return fixer.insertTextBefore(token, " "); | |
263 | } | |
264 | }); | |
265 | } | |
266 | ||
267 | // if token is a closing paren and is preceded by a disallowed space | |
268 | if (token.value === ")" && closerRejectsSpace(prevToken, token)) { | |
269 | context.report({ | |
270 | node, | |
271 | loc: { start: prevToken.loc.end, end: token.loc.start }, | |
272 | messageId: "rejectedClosingSpace", | |
273 | fix(fixer) { | |
274 | return fixer.removeRange([prevToken.range[1], token.range[0]]); | |
275 | } | |
276 | }); | |
277 | } | |
278 | }); | |
279 | } | |
280 | }; | |
281 | } | |
282 | }; |