diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5db11b6487..e0175b618f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,10 +5,15 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
## Unreleased
+### Added
+
+* [`jsx-closing-tag-location`]: add `line-aligned` option ([#3777] @kimtaejin3)
+
### Fixed
* [`prop-types`]: fix `className` missing in prop validation false negative ([#3749] @akulsr0)
+[#3777]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3777
[#3749]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3749
## [7.34.3] - 2024.06.18
diff --git a/docs/rules/jsx-closing-tag-location.md b/docs/rules/jsx-closing-tag-location.md
index c740fc27bc..590a7dfba9 100644
--- a/docs/rules/jsx-closing-tag-location.md
+++ b/docs/rules/jsx-closing-tag-location.md
@@ -35,6 +35,82 @@ Examples of **correct** code for this rule:
marklar
```
+## Rule Options
+
+There is one way to configure this rule.
+
+The configuration is a string shortcut corresponding to the `location` values specified below. If omitted, it defaults to `"tag-aligned"`.
+
+```js
+"react/jsx-closing-tag-location": // -> [, "tag-aligned"]
+"react/jsx-closing-tag-location": [, ""]
+```
+
+### `location`
+
+Enforced location for the closing tag.
+
+- `tag-aligned`: must be aligned with the opening tag.
+- `line-aligned`: must be aligned with the line containing the opening tag.
+
+Defaults to `tag-aligned`.
+
+For backward compatibility, you may pass an object `{ "location": }` that is equivalent to the first string shortcut form.
+
+Examples of **incorrect** code for this rule:
+
+```jsx
+// 'jsx-closing-tag-location': 1
+// 'jsx-closing-tag-location': [1, 'tag-aligned']
+// 'jsx-closing-tag-location': [1, {"location":'tag-aligned'}]
+
+ Hello
+ ;
+
+// 'jsx-closing-tag-location': [1, 'tag-aligned']
+// 'jsx-closing-tag-location': [1, {"location":'tag-aligned'}]
+const App =
+ Foo
+;
+
+
+// 'jsx-closing-tag-location': [1, 'line-aligned']
+// 'jsx-closing-tag-location': [1, {"location":'line-aligned'}]
+const App =
+ Foo
+ ;
+
+
+```
+
+Examples of **correct** code for this rule:
+
+```jsx
+// 'jsx-closing-tag-location': 1
+// 'jsx-closing-tag-location': [1, 'tag-aligned']
+// 'jsx-closing-tag-location': [1, {"location":'tag-aligned'}]
+
+ Hello
+;
+
+// 'jsx-closing-tag-location': [1, 'tag-aligned']
+// 'jsx-closing-tag-location': [1, {"location":'tag-aligned'}]
+const App =
+ Foo
+ ;
+
+// 'jsx-closing-tag-location': [1, 'line-aligned']
+// 'jsx-closing-tag-location': [1, {"location":'line-aligned'}]
+const App =
+ Foo
+;
+
+```
+
## When Not To Use It
-If you do not care about closing tag JSX alignment then you can disable this rule.
+If you do not care about closing tag JSX alignment then you can disable this rule.
\ No newline at end of file
diff --git a/lib/rules/jsx-closing-tag-location.js b/lib/rules/jsx-closing-tag-location.js
index a2828d12ae..cfc9a2ee89 100644
--- a/lib/rules/jsx-closing-tag-location.js
+++ b/lib/rules/jsx-closing-tag-location.js
@@ -6,9 +6,11 @@
'use strict';
const repeat = require('string.prototype.repeat');
+const has = require('hasown');
const astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
+const getSourceCode = require('../util/eslint').getSourceCode;
const report = require('../util/report');
// ------------------------------------------------------------------------------
@@ -18,6 +20,14 @@ const report = require('../util/report');
const messages = {
onOwnLine: 'Closing tag of a multiline JSX expression must be on its own line.',
matchIndent: 'Expected closing tag to match indentation of opening.',
+ alignWithOpening: 'Expected closing tag to be aligned with the line containing the opening tag',
+};
+
+const defaultOption = 'tag-aligned';
+
+const optionMessageMap = {
+ 'tag-aligned': 'matchIndent',
+ 'line-aligned': 'alignWithOpening',
};
/** @type {import('eslint').Rule.RuleModule} */
@@ -31,31 +41,78 @@ module.exports = {
},
fixable: 'whitespace',
messages,
+ schema: [{
+ anyOf: [
+ {
+ enum: ['tag-aligned', 'line-aligned'],
+ },
+ {
+ type: 'object',
+ properties: {
+ location: {
+ enum: ['tag-aligned', 'line-aligned'],
+ },
+ },
+ additionalProperties: false,
+ },
+ ],
+ }],
},
create(context) {
+ const config = context.options[0];
+ let option = defaultOption;
+
+ if (typeof config === 'string') {
+ option = config;
+ } else if (typeof config === 'object') {
+ if (has(config, 'location')) {
+ option = config.location;
+ }
+ }
+
+ function getIndentation(openingStartOfLine, opening) {
+ if (option === 'line-aligned') return openingStartOfLine.column + 1;
+ if (option === 'tag-aligned') return opening.loc.start.column + 1;
+ }
+
function handleClosingElement(node) {
if (!node.parent) {
return;
}
+ const sourceCode = getSourceCode(context);
const opening = node.parent.openingElement || node.parent.openingFragment;
+ const openingLoc = sourceCode.getFirstToken(opening).loc.start;
+ const openingLine = sourceCode.lines[openingLoc.line - 1];
+
+ const openingStartOfLine = {
+ column: /^\s*/.exec(openingLine)[0].length,
+ line: openingLoc.line,
+ };
+
if (opening.loc.start.line === node.loc.start.line) {
return;
}
- if (opening.loc.start.column === node.loc.start.column) {
+ if (opening.loc.start.column === node.loc.start.column && option === 'tag-aligned') {
+ return;
+ }
+
+ if (openingStartOfLine.column === node.loc.start.column && option === 'line-aligned') {
return;
}
const messageId = astUtil.isNodeFirstInLine(context, node)
- ? 'matchIndent'
+ ? optionMessageMap[option]
: 'onOwnLine';
+
report(context, messages[messageId], messageId, {
node,
loc: node.loc,
fix(fixer) {
- const indent = repeat(' ', opening.loc.start.column);
+ const indent = repeat(' ', getIndentation(openingStartOfLine, opening));
+
if (astUtil.isNodeFirstInLine(context, node)) {
return fixer.replaceTextRange(
[node.range[0] - node.loc.start.column, node.range[0]],
diff --git a/tests/lib/rules/jsx-closing-tag-location.js b/tests/lib/rules/jsx-closing-tag-location.js
index 4f1b24205f..a66d57cbdc 100644
--- a/tests/lib/rules/jsx-closing-tag-location.js
+++ b/tests/lib/rules/jsx-closing-tag-location.js
@@ -29,6 +29,65 @@ const parserOptions = {
const ruleTester = new RuleTester({ parserOptions });
ruleTester.run('jsx-closing-tag-location', rule, {
valid: parsers.all([
+ {
+ code: `
+ const foo = () => {
+ return
+ bar
+ }
+ `,
+ options: [{ location: 'line-aligned' }],
+ },
+ {
+ code: `
+ const foo = () => {
+ return
+ bar
+ }
+ `,
+ },
+ {
+ code: `
+ const foo = () => {
+ return
+ bar
+
+ }
+ `,
+ options: ['line-aligned'],
+ },
+ {
+ code: `
+ const foo =
+ bar
+
+ `,
+ options: ['line-aligned'],
+ },
+ {
+ code: `
+ const x =
+ foo
+
+ `,
+ },
+ {
+ code: `
+ const foo =
+
+ bar
+
+ `,
+ options: ['line-aligned'],
+ },
+ {
+ code: `
+ const foo =
+
+ bar
+
+ `,
+ },
{
code: `
@@ -36,6 +95,14 @@ ruleTester.run('jsx-closing-tag-location', rule, {
`,
},
+ {
+ code: `
+
+ foo
+
+ `,
+ options: ['line-aligned'],
+ },
{
code: `
foo
@@ -95,20 +162,38 @@ ruleTester.run('jsx-closing-tag-location', rule, {
foo
>
`,
- errors: [{ messageId: 'matchIndent' }],
+ errors: [{ messageId: 'matchIndent' }], // here
},
{
code: `
- <>
- foo>
+ const x = () => {
+ return
+ foo
+ }
`,
- features: ['fragment', 'no-ts-old'], // TODO: FIXME: remove no-ts-old and fix
output: `
- <>
- foo
- >
+ const x = () => {
+ return
+ foo
+
+ }
`,
errors: [{ messageId: 'onOwnLine' }],
+ options: ['line-aligned'],
+ },
+ {
+ code: `
+ const x =
+ foo
+
+ `,
+ output: `
+ const x =
+ foo
+
+ `,
+ errors: [{ messageId: 'alignWithOpening' }],
+ options: ['line-aligned'],
},
]),
});