UNPKG

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