Skip to content

Commit

Permalink
[New] jsx-closing-tag-location: add line-aligned option
Browse files Browse the repository at this point in the history
  • Loading branch information
kimtaejin3 authored and ljharb committed Jul 3, 2024
1 parent 4d2fd86 commit 5ff60fb
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 3 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
* add [`jsx-props-no-spread-multi`] ([#3724][] @SimonSchick)
* [`forbid-component-props`]: add `propNamePattern` to allow / disallow prop name patterns ([#3774][] @akulsr0)
* [`jsx-handler-names`]: support ignoring component names ([#3772][] @akulsr0)
* [`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
[#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
[#3759]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3759
Expand Down
76 changes: 76 additions & 0 deletions docs/rules/jsx-closing-tag-location.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,82 @@ Examples of **correct** code for this rule:
<Hello>marklar</Hello>
```

## 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": <enabled> // -> [<enabled>, "tag-aligned"]
"react/jsx-closing-tag-location": [<enabled>, "<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": <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'}]
<Say
firstName="John"
lastName="Smith">
Hello
</Say>;

// 'jsx-closing-tag-location': [1, 'tag-aligned']
// 'jsx-closing-tag-location': [1, {"location":'tag-aligned'}]
const App = <Bar>
Foo
</Bar>;


// 'jsx-closing-tag-location': [1, 'line-aligned']
// 'jsx-closing-tag-location': [1, {"location":'line-aligned'}]
const App = <Bar>
Foo
</Bar>;


```

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'}]
<Say
firstName="John"
lastName="Smith">
Hello
</Say>;

// 'jsx-closing-tag-location': [1, 'tag-aligned']
// 'jsx-closing-tag-location': [1, {"location":'tag-aligned'}]
const App = <Bar>
Foo
</Bar>;

// 'jsx-closing-tag-location': [1, 'line-aligned']
// 'jsx-closing-tag-location': [1, {"location":'line-aligned'}]
const App = <Bar>
Foo
</Bar>;

```

## When Not To Use It

If you do not care about closing tag JSX alignment then you can disable this rule.
72 changes: 69 additions & 3 deletions lib/rules/jsx-closing-tag-location.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

// ------------------------------------------------------------------------------
Expand All @@ -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} */
Expand All @@ -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]],
Expand Down
98 changes: 98 additions & 0 deletions tests/lib/rules/jsx-closing-tag-location.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <App>
bar</App>
}
`,
options: [{ location: 'line-aligned' }],
},
{
code: `
const foo = () => {
return <App>
bar</App>
}
`,
},
{
code: `
const foo = () => {
return <App>
bar
</App>
}
`,
options: ['line-aligned'],
},
{
code: `
const foo = <App>
bar
</App>
`,
options: ['line-aligned'],
},
{
code: `
const x = <App>
foo
</App>
`,
},
{
code: `
const foo =
<App>
bar
</App>
`,
options: ['line-aligned'],
},
{
code: `
const foo =
<App>
bar
</App>
`,
},
{
code: `
<App>
foo
</App>
`,
},
{
code: `
<App>
foo
</App>
`,
options: ['line-aligned'],
},
{
code: `
Expand Down Expand Up @@ -110,5 +177,36 @@ ruleTester.run('jsx-closing-tag-location', rule, {
`,
errors: [{ messageId: 'onOwnLine' }],
},
{
code: `
const x = () => {
return <App>
foo</App>
}
`,
output: `
const x = () => {
return <App>
foo
</App>
}
`,
errors: [{ messageId: 'onOwnLine' }],
options: ['line-aligned'],
},
{
code: `
const x = <App>
foo
</App>
`,
output: `
const x = <App>
foo
</App>
`,
errors: [{ messageId: 'alignWithOpening' }],
options: ['line-aligned'],
},
]),
});

0 comments on commit 5ff60fb

Please sign in to comment.