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 5e9edf8 commit a1a3464
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 10 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
78 changes: 78 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,84 @@ 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.
69 changes: 66 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,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 = repeat(' ', opening.loc.start.column + 1);
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
92 changes: 85 additions & 7 deletions tests/lib/rules/jsx-closing-tag-location.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <App>
bar</App>
}
`,
options: ['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>
Expand Down Expand Up @@ -95,20 +154,39 @@ ruleTester.run('jsx-closing-tag-location', rule, {
foo
</>
`,
errors: [{ messageId: 'matchIndent' }],
errors: [{ messageId: 'matchIndent' }], // here
},
{
code: `
<>
foo</>
const x = () => {
return <App>
foo</App>
}
`,
features: ['fragment', 'no-ts-old'], // TODO: FIXME: remove no-ts-old and fix
output: `
<>
foo
</>
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 a1a3464

Please sign in to comment.