• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/**
2 * @fileoverview Disallows or enforces spaces inside of parentheses.
3 * @author Jonathan Rajavuori
4 */
5"use strict";
6
7const astUtils = require("./utils/ast-utils");
8
9//------------------------------------------------------------------------------
10// Rule Definition
11//------------------------------------------------------------------------------
12
13module.exports = {
14    meta: {
15        type: "layout",
16
17        docs: {
18            description: "enforce consistent spacing inside parentheses",
19            category: "Stylistic Issues",
20            recommended: false,
21            url: "https://eslint.org/docs/rules/space-in-parens"
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        //--------------------------------------------------------------------------
105        const sourceCode = context.getSourceCode();
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};
283