UNPKG

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