UNPKG

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