Skip to content

Commit 5ff60fb

Browse files
kimtaejin3ljharb
authored andcommitted
[New] jsx-closing-tag-location: add line-aligned option
1 parent 4d2fd86 commit 5ff60fb

File tree

4 files changed

+245
-3
lines changed

4 files changed

+245
-3
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
1111
* add [`jsx-props-no-spread-multi`] ([#3724][] @SimonSchick)
1212
* [`forbid-component-props`]: add `propNamePattern` to allow / disallow prop name patterns ([#3774][] @akulsr0)
1313
* [`jsx-handler-names`]: support ignoring component names ([#3772][] @akulsr0)
14+
* [`jsx-closing-tag-location`]: add `line-aligned` option ([#3777] @kimtaejin3)
1415

1516
### Changed
1617
* [Refactor] `variableUtil`: Avoid creating a single flat variable scope for each lookup ([#3782][] @DanielRosenwasser)
1718

1819
[#3782]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3782
20+
[#3777]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3777
1921
[#3774]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3774
2022
[#3772]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3772
2123
[#3759]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3759

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

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

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

40116
If you do not care about closing tag JSX alignment then you can disable this rule.

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

+69-3
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
'use strict';
77

88
const repeat = require('string.prototype.repeat');
9+
const has = require('hasown');
910

1011
const astUtil = require('../util/ast');
1112
const docsUrl = require('../util/docsUrl');
13+
const getSourceCode = require('../util/eslint').getSourceCode;
1214
const report = require('../util/report');
1315

1416
// ------------------------------------------------------------------------------
@@ -18,6 +20,14 @@ const report = require('../util/report');
1820
const messages = {
1921
onOwnLine: 'Closing tag of a multiline JSX expression must be on its own line.',
2022
matchIndent: 'Expected closing tag to match indentation of opening.',
23+
alignWithOpening: 'Expected closing tag to be aligned with the line containing the opening tag',
24+
};
25+
26+
const defaultOption = 'tag-aligned';
27+
28+
const optionMessageMap = {
29+
'tag-aligned': 'matchIndent',
30+
'line-aligned': 'alignWithOpening',
2131
};
2232

2333
/** @type {import('eslint').Rule.RuleModule} */
@@ -31,31 +41,87 @@ module.exports = {
3141
},
3242
fixable: 'whitespace',
3343
messages,
44+
schema: [{
45+
anyOf: [
46+
{
47+
enum: ['tag-aligned', 'line-aligned'],
48+
},
49+
{
50+
type: 'object',
51+
properties: {
52+
location: {
53+
enum: ['tag-aligned', 'line-aligned'],
54+
},
55+
},
56+
additionalProperties: false,
57+
},
58+
],
59+
}],
3460
},
3561

3662
create(context) {
63+
const config = context.options[0];
64+
let option = defaultOption;
65+
66+
if (typeof config === 'string') {
67+
option = config;
68+
} else if (typeof config === 'object') {
69+
if (has(config, 'location')) {
70+
option = config.location;
71+
}
72+
}
73+
74+
function getIndentation(openingStartOfLine, opening) {
75+
if (option === 'line-aligned') return openingStartOfLine.column;
76+
if (option === 'tag-aligned') return opening.loc.start.column;
77+
}
78+
3779
function handleClosingElement(node) {
3880
if (!node.parent) {
3981
return;
4082
}
83+
const sourceCode = getSourceCode(context);
4184

4285
const opening = node.parent.openingElement || node.parent.openingFragment;
86+
const openingLoc = sourceCode.getFirstToken(opening).loc.start;
87+
const openingLine = sourceCode.lines[openingLoc.line - 1];
88+
89+
const openingStartOfLine = {
90+
column: /^\s*/.exec(openingLine)[0].length,
91+
line: openingLoc.line,
92+
};
93+
4394
if (opening.loc.start.line === node.loc.start.line) {
4495
return;
4596
}
4697

47-
if (opening.loc.start.column === node.loc.start.column) {
98+
if (
99+
opening.loc.start.column === node.loc.start.column
100+
&& option === 'tag-aligned'
101+
) {
102+
return;
103+
}
104+
105+
if (
106+
openingStartOfLine.column === node.loc.start.column
107+
&& option === 'line-aligned'
108+
) {
48109
return;
49110
}
50111

51112
const messageId = astUtil.isNodeFirstInLine(context, node)
52-
? 'matchIndent'
113+
? optionMessageMap[option]
53114
: 'onOwnLine';
115+
54116
report(context, messages[messageId], messageId, {
55117
node,
56118
loc: node.loc,
57119
fix(fixer) {
58-
const indent = repeat(' ', opening.loc.start.column);
120+
const indent = repeat(
121+
' ',
122+
getIndentation(openingStartOfLine, opening)
123+
);
124+
59125
if (astUtil.isNodeFirstInLine(context, node)) {
60126
return fixer.replaceTextRange(
61127
[node.range[0] - node.loc.start.column, node.range[0]],

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

+98
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,79 @@ 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: [{ location: '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+
},
91+
{
92+
code: `
93+
<App>
94+
foo
95+
</App>
96+
`,
97+
},
3298
{
3399
code: `
34100
<App>
35101
foo
36102
</App>
37103
`,
104+
options: ['line-aligned'],
38105
},
39106
{
40107
code: `
@@ -110,5 +177,36 @@ ruleTester.run('jsx-closing-tag-location', rule, {
110177
`,
111178
errors: [{ messageId: 'onOwnLine' }],
112179
},
180+
{
181+
code: `
182+
const x = () => {
183+
return <App>
184+
foo</App>
185+
}
186+
`,
187+
output: `
188+
const x = () => {
189+
return <App>
190+
foo
191+
</App>
192+
}
193+
`,
194+
errors: [{ messageId: 'onOwnLine' }],
195+
options: ['line-aligned'],
196+
},
197+
{
198+
code: `
199+
const x = <App>
200+
foo
201+
</App>
202+
`,
203+
output: `
204+
const x = <App>
205+
foo
206+
</App>
207+
`,
208+
errors: [{ messageId: 'alignWithOpening' }],
209+
options: ['line-aligned'],
210+
},
113211
]),
114212
});

0 commit comments

Comments
 (0)