• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/**
2 * @fileoverview Disallows or enforces spaces inside of object literals.
3 * @author Jamund Ferguson
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 braces",
19            category: "Stylistic Issues",
20            recommended: false,
21            url: "https://eslint.org/docs/rules/object-curly-spacing"
22        },
23
24        fixable: "whitespace",
25
26        schema: [
27            {
28                enum: ["always", "never"]
29            },
30            {
31                type: "object",
32                properties: {
33                    arraysInObjects: {
34                        type: "boolean"
35                    },
36                    objectsInObjects: {
37                        type: "boolean"
38                    }
39                },
40                additionalProperties: false
41            }
42        ],
43
44        messages: {
45            requireSpaceBefore: "A space is required before '{{token}}'.",
46            requireSpaceAfter: "A space is required after '{{token}}'.",
47            unexpectedSpaceBefore: "There should be no space before '{{token}}'.",
48            unexpectedSpaceAfter: "There should be no space after '{{token}}'."
49        }
50    },
51
52    create(context) {
53        const spaced = context.options[0] === "always",
54            sourceCode = context.getSourceCode();
55
56        /**
57         * Determines whether an option is set, relative to the spacing option.
58         * If spaced is "always", then check whether option is set to false.
59         * If spaced is "never", then check whether option is set to true.
60         * @param {Object} option The option to exclude.
61         * @returns {boolean} Whether or not the property is excluded.
62         */
63        function isOptionSet(option) {
64            return context.options[1] ? context.options[1][option] === !spaced : false;
65        }
66
67        const options = {
68            spaced,
69            arraysInObjectsException: isOptionSet("arraysInObjects"),
70            objectsInObjectsException: isOptionSet("objectsInObjects")
71        };
72
73        //--------------------------------------------------------------------------
74        // Helpers
75        //--------------------------------------------------------------------------
76
77        /**
78         * Reports that there shouldn't be a space after the first token
79         * @param {ASTNode} node The node to report in the event of an error.
80         * @param {Token} token The token to use for the report.
81         * @returns {void}
82         */
83        function reportNoBeginningSpace(node, token) {
84            const nextToken = context.getSourceCode().getTokenAfter(token, { includeComments: true });
85
86            context.report({
87                node,
88                loc: { start: token.loc.end, end: nextToken.loc.start },
89                messageId: "unexpectedSpaceAfter",
90                data: {
91                    token: token.value
92                },
93                fix(fixer) {
94                    return fixer.removeRange([token.range[1], nextToken.range[0]]);
95                }
96            });
97        }
98
99        /**
100         * Reports that there shouldn't be a space before the last token
101         * @param {ASTNode} node The node to report in the event of an error.
102         * @param {Token} token The token to use for the report.
103         * @returns {void}
104         */
105        function reportNoEndingSpace(node, token) {
106            const previousToken = context.getSourceCode().getTokenBefore(token, { includeComments: true });
107
108            context.report({
109                node,
110                loc: { start: previousToken.loc.end, end: token.loc.start },
111                messageId: "unexpectedSpaceBefore",
112                data: {
113                    token: token.value
114                },
115                fix(fixer) {
116                    return fixer.removeRange([previousToken.range[1], token.range[0]]);
117                }
118            });
119        }
120
121        /**
122         * Reports that there should be a space after the first token
123         * @param {ASTNode} node The node to report in the event of an error.
124         * @param {Token} token The token to use for the report.
125         * @returns {void}
126         */
127        function reportRequiredBeginningSpace(node, token) {
128            context.report({
129                node,
130                loc: token.loc,
131                messageId: "requireSpaceAfter",
132                data: {
133                    token: token.value
134                },
135                fix(fixer) {
136                    return fixer.insertTextAfter(token, " ");
137                }
138            });
139        }
140
141        /**
142         * Reports that there should be a space before the last token
143         * @param {ASTNode} node The node to report in the event of an error.
144         * @param {Token} token The token to use for the report.
145         * @returns {void}
146         */
147        function reportRequiredEndingSpace(node, token) {
148            context.report({
149                node,
150                loc: token.loc,
151                messageId: "requireSpaceBefore",
152                data: {
153                    token: token.value
154                },
155                fix(fixer) {
156                    return fixer.insertTextBefore(token, " ");
157                }
158            });
159        }
160
161        /**
162         * Determines if spacing in curly braces is valid.
163         * @param {ASTNode} node The AST node to check.
164         * @param {Token} first The first token to check (should be the opening brace)
165         * @param {Token} second The second token to check (should be first after the opening brace)
166         * @param {Token} penultimate The penultimate token to check (should be last before closing brace)
167         * @param {Token} last The last token to check (should be closing brace)
168         * @returns {void}
169         */
170        function validateBraceSpacing(node, first, second, penultimate, last) {
171            if (astUtils.isTokenOnSameLine(first, second)) {
172                const firstSpaced = sourceCode.isSpaceBetweenTokens(first, second);
173
174                if (options.spaced && !firstSpaced) {
175                    reportRequiredBeginningSpace(node, first);
176                }
177                if (!options.spaced && firstSpaced && second.type !== "Line") {
178                    reportNoBeginningSpace(node, first);
179                }
180            }
181
182            if (astUtils.isTokenOnSameLine(penultimate, last)) {
183                const shouldCheckPenultimate = (
184                    options.arraysInObjectsException && astUtils.isClosingBracketToken(penultimate) ||
185                    options.objectsInObjectsException && astUtils.isClosingBraceToken(penultimate)
186                );
187                const penultimateType = shouldCheckPenultimate && sourceCode.getNodeByRangeIndex(penultimate.range[0]).type;
188
189                const closingCurlyBraceMustBeSpaced = (
190                    options.arraysInObjectsException && penultimateType === "ArrayExpression" ||
191                    options.objectsInObjectsException && (penultimateType === "ObjectExpression" || penultimateType === "ObjectPattern")
192                ) ? !options.spaced : options.spaced;
193
194                const lastSpaced = sourceCode.isSpaceBetweenTokens(penultimate, last);
195
196                if (closingCurlyBraceMustBeSpaced && !lastSpaced) {
197                    reportRequiredEndingSpace(node, last);
198                }
199                if (!closingCurlyBraceMustBeSpaced && lastSpaced) {
200                    reportNoEndingSpace(node, last);
201                }
202            }
203        }
204
205        /**
206         * Gets '}' token of an object node.
207         *
208         * Because the last token of object patterns might be a type annotation,
209         * this traverses tokens preceded by the last property, then returns the
210         * first '}' token.
211         * @param {ASTNode} node The node to get. This node is an
212         *      ObjectExpression or an ObjectPattern. And this node has one or
213         *      more properties.
214         * @returns {Token} '}' token.
215         */
216        function getClosingBraceOfObject(node) {
217            const lastProperty = node.properties[node.properties.length - 1];
218
219            return sourceCode.getTokenAfter(lastProperty, astUtils.isClosingBraceToken);
220        }
221
222        /**
223         * Reports a given object node if spacing in curly braces is invalid.
224         * @param {ASTNode} node An ObjectExpression or ObjectPattern node to check.
225         * @returns {void}
226         */
227        function checkForObject(node) {
228            if (node.properties.length === 0) {
229                return;
230            }
231
232            const first = sourceCode.getFirstToken(node),
233                last = getClosingBraceOfObject(node),
234                second = sourceCode.getTokenAfter(first, { includeComments: true }),
235                penultimate = sourceCode.getTokenBefore(last, { includeComments: true });
236
237            validateBraceSpacing(node, first, second, penultimate, last);
238        }
239
240        /**
241         * Reports a given import node if spacing in curly braces is invalid.
242         * @param {ASTNode} node An ImportDeclaration node to check.
243         * @returns {void}
244         */
245        function checkForImport(node) {
246            if (node.specifiers.length === 0) {
247                return;
248            }
249
250            let firstSpecifier = node.specifiers[0];
251            const lastSpecifier = node.specifiers[node.specifiers.length - 1];
252
253            if (lastSpecifier.type !== "ImportSpecifier") {
254                return;
255            }
256            if (firstSpecifier.type !== "ImportSpecifier") {
257                firstSpecifier = node.specifiers[1];
258            }
259
260            const first = sourceCode.getTokenBefore(firstSpecifier),
261                last = sourceCode.getTokenAfter(lastSpecifier, astUtils.isNotCommaToken),
262                second = sourceCode.getTokenAfter(first, { includeComments: true }),
263                penultimate = sourceCode.getTokenBefore(last, { includeComments: true });
264
265            validateBraceSpacing(node, first, second, penultimate, last);
266        }
267
268        /**
269         * Reports a given export node if spacing in curly braces is invalid.
270         * @param {ASTNode} node An ExportNamedDeclaration node to check.
271         * @returns {void}
272         */
273        function checkForExport(node) {
274            if (node.specifiers.length === 0) {
275                return;
276            }
277
278            const firstSpecifier = node.specifiers[0],
279                lastSpecifier = node.specifiers[node.specifiers.length - 1],
280                first = sourceCode.getTokenBefore(firstSpecifier),
281                last = sourceCode.getTokenAfter(lastSpecifier, astUtils.isNotCommaToken),
282                second = sourceCode.getTokenAfter(first, { includeComments: true }),
283                penultimate = sourceCode.getTokenBefore(last, { includeComments: true });
284
285            validateBraceSpacing(node, first, second, penultimate, last);
286        }
287
288        //--------------------------------------------------------------------------
289        // Public
290        //--------------------------------------------------------------------------
291
292        return {
293
294            // var {x} = y;
295            ObjectPattern: checkForObject,
296
297            // var y = {x: 'y'}
298            ObjectExpression: checkForObject,
299
300            // import {y} from 'x';
301            ImportDeclaration: checkForImport,
302
303            // export {name} from 'yo';
304            ExportNamedDeclaration: checkForExport
305        };
306
307    }
308};
309