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'], + }, ]), });