source: imaps-frontend/node_modules/eslint-plugin-react/lib/rules/jsx-curly-spacing.js

main
Last change on this file was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

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