Skip to content

Commit 446dc74

Browse files
committed
jsx-closing-tag-location: add line-aligned option
1 parent 3c1d520 commit 446dc74

File tree

3 files changed

+230
-11
lines changed

3 files changed

+230
-11
lines changed

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

+79-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,84 @@ Examples of **correct** code for this rule:
3535
<Hello>marklar</Hello>
3636
```
3737

38+
39+
## Rule Options
40+
41+
There is one way to configure this rule.
42+
43+
The configuration is a string shortcut corresponding to the `location` values specified below. If omitted, it defaults to `"tag-aligned"`.
44+
45+
```js
46+
"react/jsx-closing-tag-location": <enabled> // -> [<enabled>, "tag-aligned"]
47+
"react/jsx-closing-tag-location": [<enabled>, "<location>"]
48+
```
49+
50+
### `location`
51+
52+
Enforced location for the closing tag.
53+
54+
- `tag-aligned`: must be aligned with the opening tag.
55+
- `line-aligned`: must be aligned with the line containing the opening tag.
56+
57+
Defaults to `tag-aligned`.
58+
59+
For backward compatibility, you may pass an object `{ "location": <location> }` that is equivalent to the first string shortcut form.
60+
61+
Examples of **incorrect** code for this rule:
62+
63+
```jsx
64+
// 'jsx-closing-tag-location': 1
65+
// 'jsx-closing-tag-location': [1, 'tag-aligned']
66+
// 'jsx-closing-tag-location': [1, {"location":'tag-aligned'}]
67+
<Say
68+
firstName="John"
69+
lastName="Smith">
70+
Hello
71+
</Say>;
72+
73+
// 'jsx-closing-tag-location': [1, 'tag-aligned']
74+
// 'jsx-closing-tag-location': [1, {"location":'tag-aligned'}]
75+
const App = <Bar>
76+
Foo
77+
</Bar>;
78+
79+
80+
// 'jsx-closing-tag-location': [1, 'line-aligned']
81+
// 'jsx-closing-tag-location': [1, {"location":'line-aligned'}]
82+
const App = <Bar>
83+
Foo
84+
</Bar>;
85+
86+
87+
```
88+
89+
Examples of **correct** code for this rule:
90+
91+
```jsx
92+
// 'jsx-closing-tag-location': 1
93+
// 'jsx-closing-tag-location': [1, 'tag-aligned']
94+
// 'jsx-closing-tag-location': [1, {"location":'tag-aligned'}]
95+
<Say
96+
firstName="John"
97+
lastName="Smith">
98+
Hello
99+
</Say>;
100+
101+
// 'jsx-closing-tag-location': [1, 'tag-aligned']
102+
// 'jsx-closing-tag-location': [1, {"location":'tag-aligned'}]
103+
const App = <Bar>
104+
Foo
105+
</Bar>;
106+
107+
// 'jsx-closing-tag-location': [1, 'line-aligned']
108+
// 'jsx-closing-tag-location': [1, {"location":'line-aligned'}]
109+
const App = <Bar>
110+
Foo
111+
</Bar>;
112+
113+
114+
```
115+
38116
## When Not To Use It
39117

40-
If you do not care about closing tag JSX alignment then you can disable this rule.
118+
If you do not care about closing tag JSX alignment then you can disable this rule.

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

+66-3
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55

66
'use strict';
77

8+
const has = require('object.hasown/polyfill')();
89
const astUtil = require('../util/ast');
910
const docsUrl = require('../util/docsUrl');
11+
const getSourceCode = require('../util/eslint').getSourceCode;
1012
const report = require('../util/report');
1113

1214
// ------------------------------------------------------------------------------
@@ -16,6 +18,14 @@ const report = require('../util/report');
1618
const messages = {
1719
onOwnLine: 'Closing tag of a multiline JSX expression must be on its own line.',
1820
matchIndent: 'Expected closing tag to match indentation of opening.',
21+
alignWithOpening: 'Expected closing tag to be aligned with the line containing the opening tag',
22+
};
23+
24+
const defaultOption = 'tag-aligned';
25+
26+
const optionMessageMap = {
27+
'tag-aligned': 'matchIndent',
28+
'line-aligned': 'alignWithOpening',
1929
};
2030

2131
/** @type {import('eslint').Rule.RuleModule} */
@@ -29,31 +39,84 @@ module.exports = {
2939
},
3040
fixable: 'whitespace',
3141
messages,
42+
schema: [{
43+
anyOf: [
44+
{
45+
enum: ['tag-aligned', 'line-aligned'],
46+
},
47+
{
48+
type: 'object',
49+
properties: {
50+
location: {
51+
enum: ['tag-aligned', 'line-aligned'],
52+
},
53+
},
54+
additionalProperties: false,
55+
},
56+
],
57+
}],
3258
},
3359

3460
create(context) {
61+
const config = context.options[0];
62+
let option = defaultOption;
63+
64+
if (typeof config === 'string') {
65+
option = config;
66+
} else if (typeof config === 'object') {
67+
if (has(config, 'location')) {
68+
option = config.location;
69+
}
70+
}
71+
72+
function getIndentation(openingStartOfLine, opening) {
73+
switch (option) {
74+
case 'line-aligned':
75+
return openingStartOfLine.column + 1;
76+
case 'tag-aligned':
77+
return opening.loc.start.column + 1;
78+
default:
79+
return null;
80+
}
81+
}
82+
3583
function handleClosingElement(node) {
3684
if (!node.parent) {
3785
return;
3886
}
87+
const sourceCode = getSourceCode(context);
3988

4089
const opening = node.parent.openingElement || node.parent.openingFragment;
90+
const openingLoc = sourceCode.getFirstToken(opening).loc.start;
91+
const openingLine = sourceCode.lines[openingLoc.line - 1];
92+
93+
const openingStartOfLine = {
94+
column: /^\s*/.exec(openingLine)[0].length,
95+
line: openingLoc.line,
96+
};
97+
4198
if (opening.loc.start.line === node.loc.start.line) {
4299
return;
43100
}
44101

45-
if (opening.loc.start.column === node.loc.start.column) {
102+
if (opening.loc.start.column === node.loc.start.column && option === 'tag-aligned') {
103+
return;
104+
}
105+
106+
if (openingStartOfLine.column === node.loc.start.column && option === 'line-aligned') {
46107
return;
47108
}
48109

49110
const messageId = astUtil.isNodeFirstInLine(context, node)
50-
? 'matchIndent'
111+
? optionMessageMap[option]
51112
: 'onOwnLine';
113+
52114
report(context, messages[messageId], messageId, {
53115
node,
54116
loc: node.loc,
55117
fix(fixer) {
56-
const indent = Array(opening.loc.start.column + 1).join(' ');
118+
const indent = Array(getIndentation(openingStartOfLine, opening)).join(' ');
119+
57120
if (astUtil.isNodeFirstInLine(context, node)) {
58121
return fixer.replaceTextRange(
59122
[node.range[0] - node.loc.start.column, node.range[0]],

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

+85-7
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,65 @@ const parserOptions = {
2929
const ruleTester = new RuleTester({ parserOptions });
3030
ruleTester.run('jsx-closing-tag-location', rule, {
3131
valid: parsers.all([
32+
{
33+
code: `
34+
const foo = () => {
35+
return <App>
36+
bar</App>
37+
}
38+
`,
39+
options: ['line-aligned'],
40+
},
41+
{
42+
code: `
43+
const foo = () => {
44+
return <App>
45+
bar</App>
46+
}
47+
`,
48+
},
49+
{
50+
code: `
51+
const foo = () => {
52+
return <App>
53+
bar
54+
</App>
55+
}
56+
`,
57+
options: ['line-aligned'],
58+
},
59+
{
60+
code: `
61+
const foo = <App>
62+
bar
63+
</App>
64+
`,
65+
options: ['line-aligned'],
66+
},
67+
{
68+
code: `
69+
const x = <App>
70+
foo
71+
</App>
72+
`,
73+
},
74+
{
75+
code: `
76+
const foo =
77+
<App>
78+
bar
79+
</App>
80+
`,
81+
options: ['line-aligned'],
82+
},
83+
{
84+
code: `
85+
const foo =
86+
<App>
87+
bar
88+
</App>
89+
`,
90+
},
3291
{
3392
code: `
3493
<App>
@@ -95,20 +154,39 @@ ruleTester.run('jsx-closing-tag-location', rule, {
95154
foo
96155
</>
97156
`,
98-
errors: [{ messageId: 'matchIndent' }],
157+
errors: [{ messageId: 'matchIndent' }], // here
99158
},
100159
{
101160
code: `
102-
<>
103-
foo</>
161+
const x = () => {
162+
return <App>
163+
foo</App>
164+
}
104165
`,
105-
features: ['fragment', 'no-ts-old'], // TODO: FIXME: remove no-ts-old and fix
106166
output: `
107-
<>
108-
foo
109-
</>
167+
const x = () => {
168+
return <App>
169+
foo
170+
</App>
171+
}
110172
`,
111173
errors: [{ messageId: 'onOwnLine' }],
174+
options: ['line-aligned'],
112175
},
176+
{
177+
code: `
178+
const x = <App>
179+
foo
180+
</App>
181+
`,
182+
output: `
183+
const x = <App>
184+
foo
185+
</App>
186+
`,
187+
errors: [{ messageId: 'alignWithOpening' }],
188+
options: ['line-aligned'],
189+
},
190+
113191
]),
114192
});

0 commit comments

Comments
 (0)