Skip to content

Commit 67a6eea

Browse files
committed
Merge pull request #218 from evocateur/jsx-closing-bracket-split
Add option to jsx-closing-bracket-location to configure different styles for self-closing and non-empty tags (fixes #208)
2 parents 75f8a27 + d6743ee commit 67a6eea

File tree

3 files changed

+242
-17
lines changed

3 files changed

+242
-17
lines changed

docs/rules/jsx-closing-bracket-location.md

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,22 @@ The following patterns are not considered warnings:
3232

3333
## Rule Options
3434

35+
There are two ways to configure this rule.
36+
37+
The first form is a string shortcut corresponding to the `location` values specified below. If omitted, it defaults to `"tag-aligned"`.
38+
39+
```js
40+
"jsx-closing-bracket-location": <enabled> // -> [<enabled>, "tag-aligned"]
41+
"jsx-closing-bracket-location": [<enabled>, "<location>"]
42+
```
43+
44+
The second form allows you to distinguish between non-empty and self-closing tags. Both properties are optional, and both default to `"tag-aligned"`.
45+
3546
```js
36-
...
37-
"jsx-closing-bracket-location": [<enabled>, { "location": <string> }]
38-
...
47+
"jsx-closing-bracket-location": [<enabled>, {
48+
"nonEmpty": "<location>",
49+
"selfClosing": "<location>"
50+
}]
3951
```
4052

4153
### `location`
@@ -46,48 +58,91 @@ Enforced location for the closing bracket.
4658
* `after-props`: must be placed right after the last prop.
4759
* `props-aligned`: must be aligned with the last prop.
4860

49-
Default to `tag-aligned`.
61+
Defaults to `tag-aligned`.
62+
63+
For backward compatibility, you may pass an object `{ "location": <location> }` that is equivalent to the first string shortcut form.
5064

5165
The following patterns are considered warnings:
5266

5367
```jsx
54-
// [1, {location: 'tag-aligned'}]
68+
// 'jsx-closing-bracket-location': 1
69+
// 'jsx-closing-bracket-location': [1, 'tag-aligned']
5570
<Hello
5671
firstName="John"
5772
lastName="Smith"
5873
/>;
5974

60-
// [1, {location: 'after-props'}]
75+
<Say
76+
firstName="John"
77+
lastName="Smith">
78+
Hello
79+
</Say>;
80+
81+
// 'jsx-closing-bracket-location': [1, 'after-props']
6182
<Hello
6283
firstName="John"
6384
lastName="Smith"
6485
/>;
6586

66-
// [1, {location: 'props-aligned'}]
87+
<Say
88+
firstName="John"
89+
lastName="Smith"
90+
>
91+
Hello
92+
</Say>;
93+
94+
// 'jsx-closing-bracket-location': [1, 'props-aligned']
6795
<Hello
6896
firstName="John"
6997
lastName="Smith" />;
98+
99+
<Say
100+
firstName="John"
101+
lastName="Smith">
102+
Hello
103+
</Say>;
70104
```
71105

72106
The following patterns are not considered warnings:
73107

74108
```jsx
75-
// [1, {location: 'tag-aligned'}]
109+
// 'jsx-closing-bracket-location': 1
110+
// 'jsx-closing-bracket-location': [1, 'tag-aligned']
76111
<Hello
77112
firstName="John"
78113
lastName="Smith"
79114
/>;
80115

81-
// [1, {location: 'after-props'}]
116+
<Say
117+
firstName="John"
118+
lastName="Smith"
119+
>
120+
Hello
121+
</Say>;
122+
123+
// 'jsx-closing-bracket-location': [1, {selfClosing: 'after-props'}]
82124
<Hello
83125
firstName="John"
84126
lastName="Smith" />;
85127

86-
// [1, {location: 'props-aligned'}]
128+
<Say
129+
firstName="John"
130+
lastName="Smith"
131+
>
132+
Hello
133+
</Say>;
134+
135+
// 'jsx-closing-bracket-location': [1, {selfClosing: 'props-aligned', nonEmpty: 'after-props'}]
87136
<Hello
88137
firstName="John"
89138
lastName="Smith"
90139
/>;
140+
141+
<Say
142+
firstName="John"
143+
lastName="Smith">
144+
Hello
145+
</Say>;
91146
```
92147

93148
## When not to use

lib/rules/jsx-closing-bracket-location.js

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,33 @@ module.exports = function(context) {
1616
'props-aligned': 'aligned with the last prop',
1717
'tag-aligned': 'aligned with the opening tag'
1818
};
19+
var DEFAULT_LOCATION = 'tag-aligned';
20+
21+
var config = context.options[0];
22+
var options = {
23+
nonEmpty: DEFAULT_LOCATION,
24+
selfClosing: DEFAULT_LOCATION
25+
};
26+
27+
if (typeof config === 'string') {
28+
// simple shorthand [1, 'something']
29+
options.nonEmpty = config;
30+
options.selfClosing = config;
31+
} else if (typeof config === 'object') {
32+
// [1, {location: 'something'}] (back-compat)
33+
if (config.hasOwnProperty('location') && typeof config.location === 'string') {
34+
options.nonEmpty = config.location;
35+
options.selfClosing = config.location;
36+
}
37+
// [1, {nonEmpty: 'something'}]
38+
if (config.hasOwnProperty('nonEmpty') && typeof config.nonEmpty === 'string') {
39+
options.nonEmpty = config.nonEmpty;
40+
}
41+
// [1, {selfClosing: 'something'}]
42+
if (config.hasOwnProperty('selfClosing') && typeof config.selfClosing === 'string') {
43+
options.selfClosing = config.selfClosing;
44+
}
45+
}
1946

2047
/**
2148
* Get expected location for the closing bracket
@@ -30,9 +57,9 @@ module.exports = function(context) {
3057
// Is always after the last prop if this one is on the same line as the opening bracket
3158
} else if (tokens.opening.line === tokens.lastProp.line) {
3259
location = 'after-props';
33-
// Else use configuration, or default value
60+
// Else use configuration dependent on selfClosing property
3461
} else {
35-
location = context.options[0] && context.options[0].location || 'tag-aligned';
62+
location = tokens.selfClosing ? options.selfClosing : options.nonEmpty;
3663
}
3764
return location;
3865
}
@@ -79,7 +106,8 @@ module.exports = function(context) {
79106
tag: tag,
80107
opening: opening,
81108
closing: closing,
82-
lastProp: lastProp
109+
lastProp: lastProp,
110+
selfClosing: node.selfClosing
83111
};
84112
}
85113

@@ -99,10 +127,29 @@ module.exports = function(context) {
99127
};
100128

101129
module.exports.schema = [{
102-
type: 'object',
103-
properties: {
104-
location: {
130+
oneOf: [
131+
{
105132
enum: ['after-props', 'props-aligned', 'tag-aligned']
133+
},
134+
{
135+
type: 'object',
136+
properties: {
137+
location: {
138+
enum: ['after-props', 'props-aligned', 'tag-aligned']
139+
}
140+
},
141+
additionalProperties: false
142+
}, {
143+
type: 'object',
144+
properties: {
145+
nonEmpty: {
146+
enum: ['after-props', 'props-aligned', 'tag-aligned']
147+
},
148+
selfClosing: {
149+
enum: ['after-props', 'props-aligned', 'tag-aligned']
150+
}
151+
},
152+
additionalProperties: false
106153
}
107-
}
154+
]
108155
}];

tests/lib/rules/jsx-closing-bracket-location.js

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ ruleTester.run('jsx-closing-bracket-location', rule, {
3232
'<App foo />'
3333
].join('\n'),
3434
ecmaFeatures: {jsx: true}
35+
}, {
36+
code: [
37+
'<App ',
38+
' foo',
39+
'/>'
40+
].join('\n'),
41+
ecmaFeatures: {jsx: true}
3542
}, {
3643
code: [
3744
'<App foo />'
@@ -44,6 +51,21 @@ ruleTester.run('jsx-closing-bracket-location', rule, {
4451
].join('\n'),
4552
options: [{location: 'tag-aligned'}],
4653
ecmaFeatures: {jsx: true}
54+
}, {
55+
code: [
56+
'<App ',
57+
' foo />'
58+
].join('\n'),
59+
options: ['after-props'],
60+
ecmaFeatures: {jsx: true}
61+
}, {
62+
code: [
63+
'<App ',
64+
' foo',
65+
' />'
66+
].join('\n'),
67+
options: ['props-aligned'],
68+
ecmaFeatures: {jsx: true}
4769
}, {
4870
code: [
4971
'<App ',
@@ -117,6 +139,59 @@ ruleTester.run('jsx-closing-bracket-location', rule, {
117139
].join('\n'),
118140
options: [{location: 'tag-aligned'}],
119141
ecmaFeatures: {jsx: true}
142+
}, {
143+
code: [
144+
'<Provider store>',
145+
' <App',
146+
' foo />',
147+
'</Provider>'
148+
].join('\n'),
149+
options: [{selfClosing: 'after-props'}],
150+
ecmaFeatures: {jsx: true}
151+
}, {
152+
code: [
153+
'<Provider ',
154+
' store',
155+
'>',
156+
' <App',
157+
' foo />',
158+
'</Provider>'
159+
].join('\n'),
160+
options: [{selfClosing: 'after-props'}],
161+
ecmaFeatures: {jsx: true}
162+
}, {
163+
code: [
164+
'<Provider ',
165+
' store>',
166+
' <App ',
167+
' foo',
168+
' />',
169+
'</Provider>'
170+
].join('\n'),
171+
options: [{nonEmpty: 'after-props'}],
172+
ecmaFeatures: {jsx: true}
173+
}, {
174+
code: [
175+
'<Provider store>',
176+
' <App ',
177+
' foo',
178+
' />',
179+
'</Provider>'
180+
].join('\n'),
181+
options: [{selfClosing: 'props-aligned'}],
182+
ecmaFeatures: {jsx: true}
183+
}, {
184+
code: [
185+
'<Provider',
186+
' store',
187+
' >',
188+
' <App ',
189+
' foo',
190+
' />',
191+
'</Provider>'
192+
].join('\n'),
193+
options: [{nonEmpty: 'props-aligned'}],
194+
ecmaFeatures: {jsx: true}
120195
}],
121196

122197
invalid: [{
@@ -228,5 +303,53 @@ ruleTester.run('jsx-closing-bracket-location', rule, {
228303
options: [{location: 'tag-aligned'}],
229304
ecmaFeatures: {jsx: true},
230305
errors: MESSAGE_TAG_ALIGNED
306+
}, {
307+
code: [
308+
'<Provider ',
309+
' store>', // <--
310+
' <App ',
311+
' foo',
312+
' />',
313+
'</Provider>'
314+
].join('\n'),
315+
options: [{selfClosing: 'props-aligned'}],
316+
ecmaFeatures: {jsx: true},
317+
errors: MESSAGE_TAG_ALIGNED
318+
}, {
319+
code: [
320+
'<Provider',
321+
' store',
322+
' >',
323+
' <App ',
324+
' foo',
325+
' />', // <--
326+
'</Provider>'
327+
].join('\n'),
328+
options: [{nonEmpty: 'props-aligned'}],
329+
ecmaFeatures: {jsx: true},
330+
errors: MESSAGE_TAG_ALIGNED
331+
}, {
332+
code: [
333+
'<Provider ',
334+
' store>', // <--
335+
' <App',
336+
' foo />',
337+
'</Provider>'
338+
].join('\n'),
339+
options: [{selfClosing: 'after-props'}],
340+
ecmaFeatures: {jsx: true},
341+
errors: MESSAGE_TAG_ALIGNED
342+
}, {
343+
code: [
344+
'<Provider ',
345+
' store>',
346+
' <App ',
347+
' foo',
348+
' />', // <--
349+
'</Provider>'
350+
].join('\n'),
351+
options: [{nonEmpty: 'after-props'}],
352+
ecmaFeatures: {jsx: true},
353+
errors: MESSAGE_TAG_ALIGNED
231354
}]
232355
});

0 commit comments

Comments
 (0)