diff --git a/CHANGELOG.md b/CHANGELOG.md
index b4e0c6475c..b4cd79ece1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,11 +12,13 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
* [`forbid-component-props`]: add `propNamePattern` to allow / disallow prop name patterns ([#3774][] @akulsr0)
* [`jsx-handler-names`]: support ignoring component names ([#3772][] @akulsr0)
* version settings: Allow react defaultVersion to be configurable ([#3771][] @onlywei)
+* [`jsx-closing-tag-location`]: add `line-aligned` option ([#3777] @kimtaejin3)
### Changed
* [Refactor] `variableUtil`: Avoid creating a single flat variable scope for each lookup ([#3782][] @DanielRosenwasser)
-[#3782]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3782
+e[#3782]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3782
+[#3777]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3777
[#3774]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3774
[#3772]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3772
[#3771]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3771
diff --git a/docs/rules/jsx-closing-tag-location.md b/docs/rules/jsx-closing-tag-location.md
index c740fc27bc..8282982393 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.
diff --git a/lib/rules/jsx-closing-tag-location.js b/lib/rules/jsx-closing-tag-location.js
index a2828d12ae..5f6f6efe38 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,87 @@ 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;
+ if (option === 'tag-aligned') return opening.loc.start.column;
+ }
+
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 4992a2e9d8..23b71cdca5 100644
--- a/tests/lib/rules/jsx-closing-tag-location.js
+++ b/tests/lib/rules/jsx-closing-tag-location.js
@@ -29,12 +29,79 @@ 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: `
+
+ foo
+
+ `,
+ },
{
code: `
foo
`,
+ options: ['line-aligned'],
},
{
code: `
@@ -110,5 +177,36 @@ ruleTester.run('jsx-closing-tag-location', rule, {
`,
errors: [{ messageId: 'onOwnLine' }],
},
+ {
+ code: `
+ const x = () => {
+ return
+ foo
+ }
+ `,
+ output: `
+ const x = () => {
+ return
+ foo
+
+ }
+ `,
+ errors: [{ messageId: 'onOwnLine' }],
+ options: ['line-aligned'],
+ },
+ {
+ code: `
+ const x =
+ foo
+
+ `,
+ output: `
+ const x =
+ foo
+
+ `,
+ errors: [{ messageId: 'alignWithOpening' }],
+ options: ['line-aligned'],
+ },
]),
});