Skip to content

Commit c58f04b

Browse files
kimtaejin3ljharb
authored andcommitted
[New] jsx-closing-tag-location: add line-aligned option
1 parent 00b89fe commit c58f04b

File tree

4 files changed

+246
-4
lines changed

4 files changed

+246
-4
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
1212
* [`forbid-component-props`]: add `propNamePattern` to allow / disallow prop name patterns ([#3774][] @akulsr0)
1313
* [`jsx-handler-names`]: support ignoring component names ([#3772][] @akulsr0)
1414
* version settings: Allow react defaultVersion to be configurable ([#3771][] @onlywei)
15+
* [`jsx-closing-tag-location`]: add `line-aligned` option ([#3777] @kimtaejin3)
1516

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

19-
[#3782]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3782
20+
e[#3782]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3782
21+
[#3777]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3777
2022
[#3774]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3774
2123
[#3772]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3772
2224
[#3771]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3771

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

Lines changed: 76 additions & 0 deletions
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

Lines changed: 69 additions & 3 deletions
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

Lines changed: 98 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy