1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 | 'use strict';
|
13 |
|
14 | const has = require('has');
|
15 | const docsUrl = require('../util/docsUrl');
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 | const SPACING = {
|
22 | always: 'always',
|
23 | never: 'never'
|
24 | };
|
25 | const SPACING_VALUES = [SPACING.always, SPACING.never];
|
26 |
|
27 | module.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 |
|
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 |
|
144 |
|
145 |
|
146 | |
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 | function isMultiline(left, right) {
|
153 | return left.loc.end.line !== right.loc.start.line;
|
154 | }
|
155 |
|
156 | |
157 |
|
158 |
|
159 |
|
160 |
|
161 |
|
162 |
|
163 |
|
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 |
|
184 |
|
185 |
|
186 |
|
187 |
|
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 |
|
203 |
|
204 |
|
205 |
|
206 |
|
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 |
|
222 |
|
223 |
|
224 |
|
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 |
|
237 | if (sourceCode.getCommentsAfter) {
|
238 | nextComment = sourceCode.getCommentsAfter(token);
|
239 |
|
240 | } else {
|
241 | const potentialComment = sourceCode.getTokenAfter(token, {includeComments: true});
|
242 | nextComment = nextToken === potentialComment ? [] : [potentialComment];
|
243 | }
|
244 |
|
245 |
|
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 |
|
257 |
|
258 |
|
259 |
|
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 |
|
272 | if (sourceCode.getCommentsBefore) {
|
273 | previousComment = sourceCode.getCommentsBefore(token);
|
274 |
|
275 | } else {
|
276 | const potentialComment = sourceCode.getTokenBefore(token, {includeComments: true});
|
277 | previousComment = previousToken === potentialComment ? [] : [potentialComment];
|
278 | }
|
279 |
|
280 |
|
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 |
|
292 |
|
293 |
|
294 |
|
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 |
|
309 |
|
310 |
|
311 |
|
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 |
|
326 |
|
327 |
|
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 |
|
399 |
|
400 |
|
401 | return {
|
402 | JSXExpressionContainer: validateBraceSpacing,
|
403 | JSXSpreadAttribute: validateBraceSpacing
|
404 | };
|
405 | }
|
406 | };
|