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

main
Last change on this file since 0c6b92a was 0c6b92a, checked in by stefan toskovski <stefantoska84@…>, 5 weeks ago

Pred finalna verzija

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