Skip to content

Commit a1a3464

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

File tree

4 files changed

+234
-10
lines changed

4 files changed

+234
-10
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,15 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
55

66
## Unreleased
77

8+
### Added
9+
10+
* [`jsx-closing-tag-location`]: add `line-aligned` option ([#3777] @kimtaejin3)
11+
812
### Fixed
913

1014
* [`prop-types`]: fix `className` missing in prop validation false negative ([#3749] @akulsr0)
1115

16+
[#3777]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3777
1217
[#3749]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3749
1318

1419
## [7.34.3] - 2024.06.18

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

+78
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

40118
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
@@ -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,84 @@ 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+
switch (option) {
76+
case 'line-aligned':
77+
return openingStartOfLine.column + 1;
78+
case 'tag-aligned':
79+
return opening.loc.start.column + 1;
80+
default:
81+
return null;
82+
}
83+
}
84+
3785
function handleClosingElement(node) {
3886
if (!node.parent) {
3987
return;
4088
}
89+
const sourceCode = getSourceCode(context);
4190

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

47-
if (opening.loc.start.column === node.loc.start.column) {
104+
if (opening.loc.start.column === node.loc.start.column && option === 'tag-aligned') {
105+
return;
106+
}
107+
108+
if (openingStartOfLine.column === node.loc.start.column && option === 'line-aligned') {
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 + 1);
120+
const indent = repeat(' ', getIndentation(openingStartOfLine, opening));
121+
59122
if (astUtil.isNodeFirstInLine(context, node)) {
60123
return fixer.replaceTextRange(
61124
[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)