Skip to content

Commit 0e983de

Browse files
authored
Merge pull request #1641 from cjskillingstad/feat/jsx-tag-spacing_before_closing
Add beforeClosing option to jsx-tag-spacing rule
2 parents 07345b4 + 2043520 commit 0e983de

File tree

3 files changed

+215
-10
lines changed

3 files changed

+215
-10
lines changed

docs/rules/jsx-tag-spacing.md

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
# Validate whitespace in and around the JSX opening and closing brackets (react/jsx-tag-spacing)
22

3-
Enforce or forbid spaces after the opening bracket, before the closing bracket of self-closing elements, and between the angle bracket and slash of JSX closing or self-closing elements.
3+
Enforce or forbid spaces after the opening bracket, before the closing bracket, before the closing bracket of self-closing elements, and between the angle bracket and slash of JSX closing or self-closing elements.
44

55
**Fixable:** This rule is automatically fixable using the `--fix` flag on the command line.
66

77
## Rule Details
88

99
This rule checks the whitespace inside and surrounding the JSX syntactic elements.
1010

11-
This rule takes one argument, an object with 3 possible keys: `closingSlash`, `beforeSelfClosing` and `afterOpening`. Each key can receive the value `"allow"` to disable that specific check.
11+
This rule takes one argument, an object with 4 possible keys: `closingSlash`, `beforeSelfClosing`, `afterOpening`, and `beforeClosing`. Each key can receive the value `"allow"` to disable that specific check.
1212

1313
The default values are:
1414

1515
```json
1616
{
1717
"closingSlash": "never",
1818
"beforeSelfClosing": "always",
19-
"afterOpening": "never"
19+
"afterOpening": "never",
20+
"beforeClosing": "allow"
2021
}
2122
```
2223

@@ -176,6 +177,48 @@ The following patterns are **not** considered warnings when configured `"allow-m
176177
/>
177178
```
178179

180+
### `beforeClosing`
181+
182+
This check can be set to `"always"`, `"never"`, or `"allow"` (to disable it).
183+
184+
If it is `"always"` the check warns whenever whitespace is missing before the closing bracket of a JSX opening element or whenever a space is missing before the closing bracket closing element. If `"never"`, then it warns if a space is present before the closing bracket of either a JSX opening element or closing element. This rule will never warn for self closing JSX elements. The default value of this check is `"allow"`.
185+
186+
The following patterns are considered warnings when configured `"always"`:
187+
188+
```jsx
189+
<Hello></Hello>
190+
<Hello></Hello >
191+
<Hello ></Hello>
192+
```
193+
194+
The following patterns are **not** considered warnings when configured `"always"`:
195+
196+
```jsx
197+
<Hello ></Hello >
198+
<Hello
199+
firstName="John"
200+
>
201+
</Hello >
202+
```
203+
204+
The following patterns are considered warnings when configured `"never"`:
205+
206+
```jsx
207+
<Hello ></Hello>
208+
<Hello></Hello >
209+
<Hello ></Hello >
210+
```
211+
212+
The following patterns are **not** considered warnings when configured `"never"`:
213+
214+
```jsx
215+
<Hello></Hello>
216+
<Hello
217+
firstName="John"
218+
>
219+
</Hello>
220+
```
221+
179222
## When Not To Use It
180223

181224
You can turn this rule off if you are not concerned with the consistency of spacing in or around JSX brackets.

lib/rules/jsx-tag-spacing.js

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,52 @@ function validateAfterOpening(context, node, option) {
169169
}
170170
}
171171

172+
function validateBeforeClosing(context, node, option) {
173+
// Don't enforce this rule for self closing tags
174+
if (!node.selfClosing) {
175+
const sourceCode = context.getSourceCode();
176+
177+
const NEVER_MESSAGE = 'A space is forbidden before closing bracket';
178+
const ALWAYS_MESSAGE = 'Whitespace is required before closing bracket';
179+
180+
const lastTokens = sourceCode.getLastTokens(node, 2);
181+
const closingToken = lastTokens[1];
182+
const leftToken = lastTokens[0];
183+
184+
if (leftToken.loc.start.line !== closingToken.loc.start.line) {
185+
return;
186+
}
187+
188+
const adjacent = !sourceCode.isSpaceBetweenTokens(leftToken, closingToken);
189+
190+
if (option === 'never' && !adjacent) {
191+
context.report({
192+
node: node,
193+
loc: {
194+
start: leftToken.loc.end,
195+
end: closingToken.loc.start
196+
},
197+
message: NEVER_MESSAGE,
198+
fix: function(fixer) {
199+
return fixer.removeRange([leftToken.range[1], closingToken.range[0]]);
200+
}
201+
});
202+
} else if (option === 'always' && adjacent) {
203+
context.report({
204+
node: node,
205+
loc: {
206+
start: leftToken.loc.end,
207+
end: closingToken.loc.start
208+
},
209+
message: ALWAYS_MESSAGE,
210+
fix: function(fixer) {
211+
return fixer.insertTextBefore(closingToken, ' ');
212+
}
213+
});
214+
}
215+
}
216+
}
217+
172218
// ------------------------------------------------------------------------------
173219
// Rule Definition
174220
// ------------------------------------------------------------------------------
@@ -191,12 +237,16 @@ module.exports = {
191237
},
192238
afterOpening: {
193239
enum: ['always', 'allow-multiline', 'never', 'allow']
240+
},
241+
beforeClosing: {
242+
enum: ['always', 'never', 'allow']
194243
}
195244
},
196245
default: {
197246
closingSlash: 'never',
198247
beforeSelfClosing: 'always',
199-
afterOpening: 'never'
248+
afterOpening: 'never',
249+
beforeClosing: 'allow'
200250
},
201251
additionalProperties: false
202252
}
@@ -206,7 +256,8 @@ module.exports = {
206256
const options = {
207257
closingSlash: 'never',
208258
beforeSelfClosing: 'always',
209-
afterOpening: 'never'
259+
afterOpening: 'never',
260+
beforeClosing: 'allow'
210261
};
211262
for (const key in options) {
212263
if (has(options, key) && has(context.options[0] || {}, key)) {
@@ -225,6 +276,9 @@ module.exports = {
225276
if (options.beforeSelfClosing !== 'allow' && node.selfClosing) {
226277
validateBeforeSelfClosing(context, node, options.beforeSelfClosing);
227278
}
279+
if (options.beforeClosing !== 'allow') {
280+
validateBeforeClosing(context, node, options.beforeClosing);
281+
}
228282
},
229283
JSXClosingElement: function (node) {
230284
if (options.afterOpening !== 'allow') {
@@ -233,6 +287,9 @@ module.exports = {
233287
if (options.closingSlash !== 'allow') {
234288
validateClosingSlash(context, node, options.closingSlash);
235289
}
290+
if (options.beforeClosing !== 'allow') {
291+
validateBeforeClosing(context, node, options.beforeClosing);
292+
}
236293
}
237294
};
238295
}

tests/lib/rules/jsx-tag-spacing.js

Lines changed: 110 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,35 @@ function closingSlashOptions(option) {
3131
return [{
3232
closingSlash: option,
3333
beforeSelfClosing: 'allow',
34-
afterOpening: 'allow'
34+
afterOpening: 'allow',
35+
beforeClosing: 'allow'
3536
}];
3637
}
3738

3839
function beforeSelfClosingOptions(option) {
3940
return [{
4041
closingSlash: 'allow',
4142
beforeSelfClosing: option,
42-
afterOpening: 'allow'
43+
afterOpening: 'allow',
44+
beforeClosing: 'allow'
4345
}];
4446
}
4547

4648
function afterOpeningOptions(option) {
4749
return [{
4850
closingSlash: 'allow',
4951
beforeSelfClosing: 'allow',
50-
afterOpening: option
52+
afterOpening: option,
53+
beforeClosing: 'allow'
54+
}];
55+
}
56+
57+
function beforeClosingOptions(option) {
58+
return [{
59+
closingSlash: 'allow',
60+
beforeSelfClosing: 'allow',
61+
afterOpening: 'allow',
62+
beforeClosing: option
5163
}];
5264
}
5365

@@ -139,19 +151,62 @@ ruleTester.run('jsx-tag-spacing', rule, {
139151
'App/>'
140152
].join('\n'),
141153
options: afterOpeningOptions('allow-multiline')
154+
}, {
155+
code: '<App />',
156+
options: beforeClosingOptions('never')
157+
}, {
158+
code: '<App></App>',
159+
options: beforeClosingOptions('never')
160+
}, {
161+
code: [
162+
'<App',
163+
'foo="bar"',
164+
'>',
165+
'</App>'
166+
].join('\n'),
167+
options: beforeClosingOptions('never')
168+
}, {
169+
code: [
170+
'<App',
171+
' foo="bar"',
172+
'>',
173+
'</App>'
174+
].join('\n'),
175+
options: beforeClosingOptions('never')
176+
}, {
177+
code: '<App ></App >',
178+
options: beforeClosingOptions('always')
179+
}, {
180+
code: [
181+
'<App',
182+
'foo="bar"',
183+
'>',
184+
'</App >'
185+
].join('\n'),
186+
options: beforeClosingOptions('always')
187+
}, {
188+
code: [
189+
'<App',
190+
' foo="bar"',
191+
'>',
192+
'</App >'
193+
].join('\n'),
194+
options: beforeClosingOptions('always')
142195
}, {
143196
code: '<App/>',
144197
options: [{
145198
closingSlash: 'never',
146199
beforeSelfClosing: 'never',
147-
afterOpening: 'never'
200+
afterOpening: 'never',
201+
beforeClosing: 'never'
148202
}]
149203
}, {
150204
code: '< App / >',
151205
options: [{
152206
closingSlash: 'always',
153207
beforeSelfClosing: 'always',
154-
afterOpening: 'always'
208+
afterOpening: 'always',
209+
beforeClosing: 'always'
155210
}]
156211
}],
157212

@@ -306,5 +361,55 @@ ruleTester.run('jsx-tag-spacing', rule, {
306361
output: '<App/>',
307362
errors: [{message: 'A space is forbidden after opening bracket'}],
308363
options: afterOpeningOptions('allow-multiline')
364+
}, {
365+
code: '<App ></App>',
366+
output: '<App></App>',
367+
errors: [{message: 'A space is forbidden before closing bracket'}],
368+
options: beforeClosingOptions('never')
369+
}, {
370+
code: '<App></App >',
371+
output: '<App></App>',
372+
errors: [{message: 'A space is forbidden before closing bracket'}],
373+
options: beforeClosingOptions('never')
374+
}, {
375+
code: [
376+
'<App',
377+
'foo="bar"',
378+
'>',
379+
'</App >'
380+
].join('\n'),
381+
output: [
382+
'<App',
383+
'foo="bar"',
384+
'>',
385+
'</App>'
386+
].join('\n'),
387+
errors: [{message: 'A space is forbidden before closing bracket'}],
388+
options: beforeClosingOptions('never')
389+
}, {
390+
code: '<App></App >',
391+
output: '<App ></App >',
392+
errors: [{message: 'Whitespace is required before closing bracket'}],
393+
options: beforeClosingOptions('always')
394+
}, {
395+
code: '<App ></App>',
396+
output: '<App ></App >',
397+
errors: [{message: 'Whitespace is required before closing bracket'}],
398+
options: beforeClosingOptions('always')
399+
}, {
400+
code: [
401+
'<App',
402+
'foo="bar"',
403+
'>',
404+
'</App>'
405+
].join('\n'),
406+
output: [
407+
'<App',
408+
'foo="bar"',
409+
'>',
410+
'</App >'
411+
].join('\n'),
412+
errors: [{message: 'Whitespace is required before closing bracket'}],
413+
options: beforeClosingOptions('always')
309414
}]
310415
});

0 commit comments

Comments
 (0)