Skip to content

Commit 81c8e35

Browse files
authored
Merge pull request #1215 from rsolomon/jsx-closing-tag-location
[New] Add indentation rule for closing tag of multi-line jsx
2 parents 52f54be + ba0b25b commit 81c8e35

File tree

5 files changed

+192
-0
lines changed

5 files changed

+192
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ Finally, enable all of the rules that you would like to use. Use [our preset](#
122122

123123
* [react/jsx-boolean-value](docs/rules/jsx-boolean-value.md): Enforce boolean attributes notation in JSX (fixable)
124124
* [react/jsx-closing-bracket-location](docs/rules/jsx-closing-bracket-location.md): Validate closing bracket location in JSX (fixable)
125+
* [react/jsx-closing-tag-location](docs/rules/jsx-closing-tag-location.md): Validate closing tag location in JSX (fixable)
125126
* [react/jsx-curly-spacing](docs/rules/jsx-curly-spacing.md): Enforce or disallow spaces inside of curly braces in JSX attributes (fixable)
126127
* [react/jsx-equals-spacing](docs/rules/jsx-equals-spacing.md): Enforce or disallow spaces around equal signs in JSX attributes (fixable)
127128
* [react/jsx-filename-extension](docs/rules/jsx-filename-extension.md): Restrict file extensions that may contain JSX
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Validate closing tag location in JSX (react/jsx-closing-tag-location)
2+
3+
Enforce the closing tag location for multiline JSX elements.
4+
5+
**Fixable:** This rule is automatically fixable using the `--fix` flag on the command line.
6+
7+
## Rule Details
8+
9+
This rule checks all JSX multiline elements with children (non-self-closing) and verifies the location of the closing tag. The expectation is that the closing tag is aligned with the opening tag on its own line.
10+
11+
The following patterns are considered warnings:
12+
13+
```jsx
14+
<Hello>
15+
marklar
16+
</Hello>
17+
```
18+
19+
```jsx
20+
<Hello>
21+
marklar</Hello>
22+
```
23+
24+
The following are not considered warnings:
25+
26+
```jsx
27+
<Hello>
28+
marklar
29+
</Hello>
30+
```
31+
32+
```jsx
33+
<Hello>marklar</Hello>
34+
```
35+
36+
## When not to use
37+
38+
If you do not care about closing tag JSX alignment then you can disable this rule.

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ var allRules = {
4141
'jsx-indent-props': require('./lib/rules/jsx-indent-props'),
4242
'jsx-indent': require('./lib/rules/jsx-indent'),
4343
'jsx-closing-bracket-location': require('./lib/rules/jsx-closing-bracket-location'),
44+
'jsx-closing-tag-location': require('./lib/rules/jsx-closing-tag-location'),
4445
'jsx-space-before-closing': require('./lib/rules/jsx-space-before-closing'),
4546
'no-direct-mutation-state': require('./lib/rules/no-direct-mutation-state'),
4647
'forbid-component-props': require('./lib/rules/forbid-component-props'),

lib/rules/jsx-closing-tag-location.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* @fileoverview Validate closing tag location in JSX
3+
* @author Ross Solomon
4+
*/
5+
'use strict';
6+
7+
// ------------------------------------------------------------------------------
8+
// Rule Definition
9+
// ------------------------------------------------------------------------------
10+
module.exports = {
11+
meta: {
12+
docs: {
13+
description: 'Validate closing tag location for multiline JSX',
14+
category: 'Stylistic Issues',
15+
recommended: false
16+
},
17+
fixable: 'whitespace'
18+
},
19+
20+
create: function(context) {
21+
var sourceCode = context.getSourceCode();
22+
23+
/**
24+
* Checks if the node is the first in its line, excluding whitespace.
25+
* @param {ASTNode} node The node to check
26+
* @return {Boolean} true if its the first node in its line
27+
*/
28+
function isNodeFirstInLine(node) {
29+
let token = node;
30+
let lines;
31+
do {
32+
token = sourceCode.getTokenBefore(token);
33+
lines = token.type === 'JSXText'
34+
? token.value.split('\n')
35+
: null;
36+
} while (
37+
token.type === 'JSXText' &&
38+
/^\s*$/.test(lines[lines.length - 1])
39+
);
40+
41+
var startLine = node.loc.start.line;
42+
var endLine = token ? token.loc.end.line : -1;
43+
return startLine !== endLine;
44+
}
45+
46+
return {
47+
JSXClosingElement: function(node) {
48+
if (!node.parent) {
49+
return;
50+
}
51+
52+
const opening = node.parent.openingElement;
53+
if (opening.loc.start.line === node.loc.start.line) {
54+
return;
55+
}
56+
57+
if (opening.loc.start.column === node.loc.start.column) {
58+
return;
59+
}
60+
61+
let message;
62+
if (!isNodeFirstInLine(node)) {
63+
message = 'Closing tag of a multiline JSX expression must be on its own line.';
64+
} else {
65+
message = 'Expected closing tag to match indentation of opening.';
66+
}
67+
68+
context.report({
69+
node: node,
70+
loc: node.loc,
71+
message,
72+
fix: function(fixer) {
73+
const indent = Array(opening.loc.start.column + 1).join(' ');
74+
if (isNodeFirstInLine(node)) {
75+
return fixer.replaceTextRange(
76+
[node.start - node.loc.start.column, node.start],
77+
indent
78+
);
79+
}
80+
81+
return fixer.insertTextBefore(node, `\n${indent}`);
82+
}
83+
});
84+
}
85+
};
86+
}
87+
};
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* @fileoverview Validate closing tag location in JSX
3+
* @author Ross Solomon
4+
*/
5+
'use strict';
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const rule = require('../../../lib/rules/jsx-closing-tag-location');
12+
const RuleTester = require('eslint').RuleTester;
13+
const parserOptions = {
14+
sourceType: 'module',
15+
ecmaFeatures: {
16+
jsx: true
17+
}
18+
};
19+
20+
const MESSAGE_MATCH_INDENTATION = [{message: 'Expected closing tag to match indentation of opening.'}];
21+
const MESSAGE_OWN_LINE = [{message: 'Closing tag of a multiline JSX expression must be on its own line.'}];
22+
23+
// ------------------------------------------------------------------------------
24+
// Tests
25+
// ------------------------------------------------------------------------------
26+
27+
const ruleTester = new RuleTester({parserOptions});
28+
ruleTester.run('jsx-closing-tag-location', rule, {
29+
valid: [{
30+
code: [
31+
'<App>',
32+
' foo',
33+
'</App>'
34+
].join('\n')
35+
}, {
36+
code: [
37+
'<App>foo</App>'
38+
].join('\n')
39+
}],
40+
41+
invalid: [{
42+
code: [
43+
'<App>',
44+
' foo',
45+
' </App>'
46+
].join('\n'),
47+
output: [
48+
'<App>',
49+
' foo',
50+
'</App>'
51+
].join('\n'),
52+
errors: MESSAGE_MATCH_INDENTATION
53+
}, {
54+
code: [
55+
'<App>',
56+
' foo</App>'
57+
].join('\n'),
58+
output: [
59+
'<App>',
60+
' foo',
61+
'</App>'
62+
].join('\n'),
63+
errors: MESSAGE_OWN_LINE
64+
}]
65+
});

0 commit comments

Comments
 (0)