UNPKG

13.9 kBJavaScriptView Raw
1/**
2 * @fileoverview Enforce or disallow spaces inside of curly braces in JSX attributes.
3 * @author Jamund Ferguson
4 * @author Brandyn Bennett
5 * @author Michael Ficarra
6 * @author Vignesh Anand
7 * @author Jamund Ferguson
8 * @author Yannick Croissant
9 * @author Erik Wendel
10 */
11
12'use strict';
13
14const has = require('has');
15const docsUrl = require('../util/docsUrl');
16
17// ------------------------------------------------------------------------------
18// Rule Definition
19// ------------------------------------------------------------------------------
20
21const SPACING = {
22 always: 'always',
23 never: 'never'
24};
25const SPACING_VALUES = [SPACING.always, SPACING.never];
26
27module.exports = {
28 meta: {
29 docs: {
30 description: 'Enforce or disallow spaces inside of curly braces in JSX attributes',
31 category: 'Stylistic Issues',
32 recommended: false,
33 url: docsUrl('jsx-curly-spacing')
34 },
35 fixable: 'code',
36
37 schema: {
38 definitions: {
39 basicConfig: {
40 type: 'object',
41 properties: {
42 when: {
43 enum: SPACING_VALUES
44 },
45 allowMultiline: {
46 type: 'boolean'
47 },
48 spacing: {
49 type: 'object',
50 properties: {
51 objectLiterals: {
52 enum: SPACING_VALUES
53 }
54 }
55 }
56 }
57 },
58 basicConfigOrBoolean: {
59 oneOf: [{
60 $ref: '#/definitions/basicConfig'
61 }, {
62 type: 'boolean'
63 }]
64 }
65 },
66 type: 'array',
67 items: [{
68 oneOf: [{
69 allOf: [{
70 $ref: '#/definitions/basicConfig'
71 }, {
72 type: 'object',
73 properties: {
74 attributes: {
75 $ref: '#/definitions/basicConfigOrBoolean'
76 },
77 children: {
78 $ref: '#/definitions/basicConfigOrBoolean'
79 }
80 }
81 }]
82 }, {
83 enum: SPACING_VALUES
84 }]
85 }, {
86 type: 'object',
87 properties: {
88 allowMultiline: {
89 type: 'boolean'
90 },
91 spacing: {
92 type: 'object',
93 properties: {
94 objectLiterals: {
95 enum: SPACING_VALUES
96 }
97 }
98 }
99 },
100 additionalProperties: false
101 }]
102 }
103 },
104
105 create(context) {
106 function normalizeConfig(configOrTrue, defaults, lastPass) {
107 const config = configOrTrue === true ? {} : configOrTrue;
108 const when = config.when || defaults.when;
109 const allowMultiline = has(config, 'allowMultiline') ? config.allowMultiline : defaults.allowMultiline;
110 const spacing = config.spacing || {};
111 let objectLiteralSpaces = spacing.objectLiterals || defaults.objectLiteralSpaces;
112 if (lastPass) {
113 // On the final pass assign the values that should be derived from others if they are still undefined
114 objectLiteralSpaces = objectLiteralSpaces || when;
115 }
116
117 return {
118 when,
119 allowMultiline,
120 objectLiteralSpaces
121 };
122 }
123
124 const DEFAULT_WHEN = SPACING.never;
125 const DEFAULT_ALLOW_MULTILINE = true;
126 const DEFAULT_ATTRIBUTES = true;
127 const DEFAULT_CHILDREN = false;
128
129 let originalConfig = context.options[0] || {};
130 if (SPACING_VALUES.indexOf(originalConfig) !== -1) {
131 originalConfig = Object.assign({when: context.options[0]}, context.options[1]);
132 }
133 const defaultConfig = normalizeConfig(originalConfig, {
134 when: DEFAULT_WHEN,
135 allowMultiline: DEFAULT_ALLOW_MULTILINE
136 });
137 const attributes = has(originalConfig, 'attributes') ? originalConfig.attributes : DEFAULT_ATTRIBUTES;
138 const attributesConfig = attributes ? normalizeConfig(attributes, defaultConfig, true) : null;
139 const children = has(originalConfig, 'children') ? originalConfig.children : DEFAULT_CHILDREN;
140 const childrenConfig = children ? normalizeConfig(children, defaultConfig, true) : null;
141
142 // --------------------------------------------------------------------------
143 // Helpers
144 // --------------------------------------------------------------------------
145
146 /**
147 * Determines whether two adjacent tokens have a newline between them.
148 * @param {Object} left - The left token object.
149 * @param {Object} right - The right token object.
150 * @returns {boolean} Whether or not there is a newline between the tokens.
151 */
152 function isMultiline(left, right) {
153 return left.loc.end.line !== right.loc.start.line;
154 }
155
156 /**
157 * Trims text of whitespace between two ranges
158 * @param {Fixer} fixer - the eslint fixer object
159 * @param {number} fromLoc - the start location
160 * @param {number} toLoc - the end location
161 * @param {string} mode - either 'start' or 'end'
162 * @param {string=} spacing - a spacing value that will optionally add a space to the removed text
163 * @returns {Object|*|{range, text}}
164 */
165 function fixByTrimmingWhitespace(fixer, fromLoc, toLoc, mode, spacing) {
166 let replacementText = context.getSourceCode().text.slice(fromLoc, toLoc);
167 if (mode === 'start') {
168 replacementText = replacementText.replace(/^\s+/gm, '');
169 } else {
170 replacementText = replacementText.replace(/\s+$/gm, '');
171 }
172 if (spacing === SPACING.always) {
173 if (mode === 'start') {
174 replacementText += ' ';
175 } else {
176 replacementText = ` ${replacementText}`;
177 }
178 }
179 return fixer.replaceTextRange([fromLoc, toLoc], replacementText);
180 }
181
182 /**
183 * Reports that there shouldn't be a newline after the first token
184 * @param {ASTNode} node - The node to report in the event of an error.
185 * @param {Token} token - The token to use for the report.
186 * @param {string} spacing
187 * @returns {void}
188 */
189 function reportNoBeginningNewline(node, token, spacing) {
190 context.report({
191 node,
192 loc: token.loc.start,
193 message: `There should be no newline after '${token.value}'`,
194 fix(fixer) {
195 const nextToken = context.getSourceCode().getTokenAfter(token);
196 return fixByTrimmingWhitespace(fixer, token.range[1], nextToken.range[0], 'start', spacing);
197 }
198 });
199 }
200
201 /**
202 * Reports that there shouldn't be a newline before the last token
203 * @param {ASTNode} node - The node to report in the event of an error.
204 * @param {Token} token - The token to use for the report.
205 * @param {string} spacing
206 * @returns {void}
207 */
208 function reportNoEndingNewline(node, token, spacing) {
209 context.report({
210 node,
211 loc: token.loc.start,
212 message: `There should be no newline before '${token.value}'`,
213 fix(fixer) {
214 const previousToken = context.getSourceCode().getTokenBefore(token);
215 return fixByTrimmingWhitespace(fixer, previousToken.range[1], token.range[0], 'end', spacing);
216 }
217 });
218 }
219
220 /**
221 * Reports that there shouldn't be a space after the first token
222 * @param {ASTNode} node - The node to report in the event of an error.
223 * @param {Token} token - The token to use for the report.
224 * @returns {void}
225 */
226 function reportNoBeginningSpace(node, token) {
227 context.report({
228 node,
229 loc: token.loc.start,
230 message: `There should be no space after '${token.value}'`,
231 fix(fixer) {
232 const sourceCode = context.getSourceCode();
233 const nextToken = sourceCode.getTokenAfter(token);
234 let nextComment;
235
236 // ESLint >=4.x
237 if (sourceCode.getCommentsAfter) {
238 nextComment = sourceCode.getCommentsAfter(token);
239 // ESLint 3.x
240 } else {
241 const potentialComment = sourceCode.getTokenAfter(token, {includeComments: true});
242 nextComment = nextToken === potentialComment ? [] : [potentialComment];
243 }
244
245 // Take comments into consideration to narrow the fix range to what is actually affected. (See #1414)
246 if (nextComment.length > 0) {
247 return fixByTrimmingWhitespace(fixer, token.range[1], Math.min(nextToken.range[0], nextComment[0].range[0]), 'start');
248 }
249
250 return fixByTrimmingWhitespace(fixer, token.range[1], nextToken.range[0], 'start');
251 }
252 });
253 }
254
255 /**
256 * Reports that there shouldn't be a space before the last token
257 * @param {ASTNode} node - The node to report in the event of an error.
258 * @param {Token} token - The token to use for the report.
259 * @returns {void}
260 */
261 function reportNoEndingSpace(node, token) {
262 context.report({
263 node,
264 loc: token.loc.start,
265 message: `There should be no space before '${token.value}'`,
266 fix(fixer) {
267 const sourceCode = context.getSourceCode();
268 const previousToken = sourceCode.getTokenBefore(token);
269 let previousComment;
270
271 // ESLint >=4.x
272 if (sourceCode.getCommentsBefore) {
273 previousComment = sourceCode.getCommentsBefore(token);
274 // ESLint 3.x
275 } else {
276 const potentialComment = sourceCode.getTokenBefore(token, {includeComments: true});
277 previousComment = previousToken === potentialComment ? [] : [potentialComment];
278 }
279
280 // Take comments into consideration to narrow the fix range to what is actually affected. (See #1414)
281 if (previousComment.length > 0) {
282 return fixByTrimmingWhitespace(fixer, Math.max(previousToken.range[1], previousComment[0].range[1]), token.range[0], 'end');
283 }
284
285 return fixByTrimmingWhitespace(fixer, previousToken.range[1], token.range[0], 'end');
286 }
287 });
288 }
289
290 /**
291 * Reports that there should be a space after the first token
292 * @param {ASTNode} node - The node to report in the event of an error.
293 * @param {Token} token - The token to use for the report.
294 * @returns {void}
295 */
296 function reportRequiredBeginningSpace(node, token) {
297 context.report({
298 node,
299 loc: token.loc.start,
300 message: `A space is required after '${token.value}'`,
301 fix(fixer) {
302 return fixer.insertTextAfter(token, ' ');
303 }
304 });
305 }
306
307 /**
308 * Reports that there should be a space before the last token
309 * @param {ASTNode} node - The node to report in the event of an error.
310 * @param {Token} token - The token to use for the report.
311 * @returns {void}
312 */
313 function reportRequiredEndingSpace(node, token) {
314 context.report({
315 node,
316 loc: token.loc.start,
317 message: `A space is required before '${token.value}'`,
318 fix(fixer) {
319 return fixer.insertTextBefore(token, ' ');
320 }
321 });
322 }
323
324 /**
325 * Determines if spacing in curly braces is valid.
326 * @param {ASTNode} node The AST node to check.
327 * @returns {void}
328 */
329 function validateBraceSpacing(node) {
330 let config;
331 switch (node.parent.type) {
332 case 'JSXAttribute':
333 case 'JSXOpeningElement':
334 config = attributesConfig;
335 break;
336
337 case 'JSXElement':
338 case 'JSXFragment':
339 config = childrenConfig;
340 break;
341
342 default:
343 return;
344 }
345 if (config === null) {
346 return;
347 }
348
349 const sourceCode = context.getSourceCode();
350 const first = context.getFirstToken(node);
351 const last = sourceCode.getLastToken(node);
352 let second = context.getTokenAfter(first, {includeComments: true});
353 let penultimate = sourceCode.getTokenBefore(last, {includeComments: true});
354
355 if (!second) {
356 second = context.getTokenAfter(first);
357 const leadingComments = sourceCode.getNodeByRangeIndex(second.range[0]).leadingComments;
358 second = leadingComments ? leadingComments[0] : second;
359 }
360 if (!penultimate) {
361 penultimate = sourceCode.getTokenBefore(last);
362 const trailingComments = sourceCode.getNodeByRangeIndex(penultimate.range[0]).trailingComments;
363 penultimate = trailingComments ? trailingComments[trailingComments.length - 1] : penultimate;
364 }
365
366 const isObjectLiteral = first.value === second.value;
367 const spacing = isObjectLiteral ? config.objectLiteralSpaces : config.when;
368 if (spacing === SPACING.always) {
369 if (!sourceCode.isSpaceBetweenTokens(first, second)) {
370 reportRequiredBeginningSpace(node, first);
371 } else if (!config.allowMultiline && isMultiline(first, second)) {
372 reportNoBeginningNewline(node, first, spacing);
373 }
374 if (!sourceCode.isSpaceBetweenTokens(penultimate, last)) {
375 reportRequiredEndingSpace(node, last);
376 } else if (!config.allowMultiline && isMultiline(penultimate, last)) {
377 reportNoEndingNewline(node, last, spacing);
378 }
379 } else if (spacing === SPACING.never) {
380 if (isMultiline(first, second)) {
381 if (!config.allowMultiline) {
382 reportNoBeginningNewline(node, first, spacing);
383 }
384 } else if (sourceCode.isSpaceBetweenTokens(first, second)) {
385 reportNoBeginningSpace(node, first);
386 }
387 if (isMultiline(penultimate, last)) {
388 if (!config.allowMultiline) {
389 reportNoEndingNewline(node, last, spacing);
390 }
391 } else if (sourceCode.isSpaceBetweenTokens(penultimate, last)) {
392 reportNoEndingSpace(node, last);
393 }
394 }
395 }
396
397 // --------------------------------------------------------------------------
398 // Public
399 // --------------------------------------------------------------------------
400
401 return {
402 JSXExpressionContainer: validateBraceSpacing,
403 JSXSpreadAttribute: validateBraceSpacing
404 };
405 }
406};