diff --git a/docs/rules/jsx-closing-tag-location.md b/docs/rules/jsx-closing-tag-location.md
index c740fc27bc..bcd578df46 100644
--- a/docs/rules/jsx-closing-tag-location.md
+++ b/docs/rules/jsx-closing-tag-location.md
@@ -35,6 +35,84 @@ 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 5d5a95cb7b..038b838013 100644
--- a/lib/rules/jsx-closing-tag-location.js
+++ b/lib/rules/jsx-closing-tag-location.js
@@ -5,8 +5,10 @@
'use strict';
+const has = require('object.hasown/polyfill')();
const astUtil = require('../util/ast');
const docsUrl = require('../util/docsUrl');
+const getSourceCode = require('../util/eslint').getSourceCode;
const report = require('../util/report');
// ------------------------------------------------------------------------------
@@ -16,6 +18,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} */
@@ -29,31 +39,84 @@ 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) {
+ switch (option) {
+ case 'line-aligned':
+ return openingStartOfLine.column + 1;
+ case 'tag-aligned':
+ return opening.loc.start.column + 1;
+ default:
+ return null;
+ }
+ }
+
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 = Array(opening.loc.start.column + 1).join(' ');
+ const indent = Array(getIndentation(openingStartOfLine, opening)).join(' ');
+
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..028336572e 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: ['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: `
@@ -95,20 +154,39 @@ 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'],
+ },
+
]),
});