Skip to content

Commit 8867490

Browse files
jzabalaljharb
authored andcommitted
[New] jsx-newline: Enforce a new line after jsx elements and expressions
1 parent 9f0d5c4 commit 8867490

File tree

6 files changed

+364
-0
lines changed

6 files changed

+364
-0
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
77

88
### Added
99
* [`jsx-key`]: added `checkKeyMustBeforeSpread` option for new jsx transform ([#2835][] @morlay)
10+
* [`jsx-newline`]: add new rule ([#2693][] @jzabala)
1011

1112
[#2835]: https://github.com/yannickcr/eslint-plugin-react/pull/2835
13+
[#2693]: https://github.com/yannickcr/eslint-plugin-react/pull/2693
1214

1315
## [7.21.5] - 2020.10.19
1416

@@ -3216,3 +3218,4 @@ If you're still not using React 15 you can keep the old behavior by setting the
32163218
[`jsx-no-script-url`]: docs/rules/jsx-no-script-url.md
32173219
[`no-adjacent-inline-elements`]: docs/rules/no-adjacent-inline-elements.md
32183220
[`function-component-definition`]: docs/rules/function-component-definition.md
3221+
[`jsx-newline`]: docs/rules/jsx-newline.md

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ Enable the rules that you would like to use.
172172
* [react/jsx-key](docs/rules/jsx-key.md): Report missing `key` props in iterators/collection literals
173173
* [react/jsx-max-depth](docs/rules/jsx-max-depth.md): Validate JSX maximum depth
174174
* [react/jsx-max-props-per-line](docs/rules/jsx-max-props-per-line.md): Limit maximum of props on a single line in JSX (fixable)
175+
* [react/jsx-newline](docs/rules/jsx-newline.md): Enforce a new line after jsx elements and expressions (fixable)
175176
* [react/jsx-no-bind](docs/rules/jsx-no-bind.md): Prevents usage of Function.prototype.bind and arrow functions in React component props
176177
* [react/jsx-no-comment-textnodes](docs/rules/jsx-no-comment-textnodes.md): Comments inside children section of tag should be placed inside braces
177178
* [react/jsx-no-duplicate-props](docs/rules/jsx-no-duplicate-props.md): Enforce no duplicate props

docs/rules/jsx-newline.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Enforce a new line after jsx elements and expressions (react/jsx-newline)
2+
3+
**Fixable:** This rule is automatically fixable using the `--fix` flag on the command line.
4+
5+
## Rule Details
6+
7+
This is a stylistic rule intended to make JSX code more readable by enforcing spaces between adjacent JSX elements and expressions.
8+
9+
The following patterns are considered warnings:
10+
11+
```jsx
12+
<div>
13+
<Button>{data.label}</Button>
14+
<List />
15+
</div>
16+
```
17+
18+
```jsx
19+
<div>
20+
<Button>{data.label}</Button>
21+
{showSomething === true && <Something />}
22+
</div>
23+
```
24+
25+
```jsx
26+
<div>
27+
{showSomething === true && <Something />}
28+
{showSomethingElse === true ? (
29+
<SomethingElse />
30+
) : (
31+
<ErrorMessage />
32+
)}
33+
</div>
34+
```
35+
36+
The following patterns are **not** considered warnings:
37+
38+
```jsx
39+
<div>
40+
<Button>{data.label}</Button>
41+
42+
<List />
43+
44+
<Button>
45+
<IconPreview />
46+
Button 2
47+
48+
<span></span>
49+
</Button>
50+
51+
{showSomething === true && <Something />}
52+
53+
<Button>Button 3</Button>
54+
55+
{showSomethingElse === true ? (
56+
<SomethingElse />
57+
) : (
58+
<ErrorMessage />
59+
)}
60+
</div>
61+
```
62+
63+
## When Not To Use It
64+
65+
You can turn this rule off if you are not concerned with spacing between your JSX elements and expressions.

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const allRules = {
3131
'jsx-key': require('./lib/rules/jsx-key'),
3232
'jsx-max-depth': require('./lib/rules/jsx-max-depth'),
3333
'jsx-max-props-per-line': require('./lib/rules/jsx-max-props-per-line'),
34+
'jsx-newline': require('./lib/rules/jsx-newline'),
3435
'jsx-no-bind': require('./lib/rules/jsx-no-bind'),
3536
'jsx-no-comment-textnodes': require('./lib/rules/jsx-no-comment-textnodes'),
3637
'jsx-no-duplicate-props': require('./lib/rules/jsx-no-duplicate-props'),

lib/rules/jsx-newline.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* @fileoverview Enforce a new line after jsx elements and expressions.
3+
* @author Johnny Zabala
4+
*/
5+
6+
'use strict';
7+
8+
const docsUrl = require('../util/docsUrl');
9+
10+
// ------------------------------------------------------------------------------
11+
// Rule Definition
12+
// ------------------------------------------------------------------------------
13+
14+
module.exports = {
15+
meta: {
16+
docs: {
17+
description: 'Enforce a new line after jsx elements and expressions',
18+
category: 'Stylistic Issues',
19+
recommended: false,
20+
url: docsUrl('jsx-newline')
21+
},
22+
fixable: 'code'
23+
},
24+
create(context) {
25+
const jsxElementParents = new Set();
26+
const sourceCode = context.getSourceCode();
27+
return {
28+
'Program:exit'() {
29+
jsxElementParents.forEach((parent) => {
30+
parent.children.forEach((element, index, elements) => {
31+
if (element.type === 'JSXElement' || element.type === 'JSXExpressionContainer') {
32+
const firstAdjacentSibling = elements[index + 1];
33+
const secondAdjacentSibling = elements[index + 2];
34+
if (
35+
firstAdjacentSibling
36+
&& secondAdjacentSibling
37+
&& (firstAdjacentSibling.type === 'Literal' || firstAdjacentSibling.type === 'JSXText')
38+
// Check adjacent sibling has the proper amount of newlines
39+
&& !/\n\s*\n/.test(firstAdjacentSibling.value)
40+
) {
41+
context.report({
42+
node: secondAdjacentSibling,
43+
message: 'JSX element should start in a new line',
44+
fix(fixer) {
45+
return fixer.replaceText(
46+
firstAdjacentSibling,
47+
// double the last newline.
48+
sourceCode.getText(firstAdjacentSibling)
49+
.replace(/(\n)(?!.*\1)/g, '\n\n')
50+
);
51+
}
52+
});
53+
}
54+
}
55+
});
56+
});
57+
},
58+
':matches(JSXElement, JSXFragment) > :matches(JSXElement, JSXExpressionContainer)': (node) => {
59+
jsxElementParents.add(node.parent);
60+
}
61+
};
62+
}
63+
};

tests/lib/rules/jsx-newline.js

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
/**
2+
* @fileoverview Enforce a new line after jsx elements and expressions
3+
* @author Johnny Zabala
4+
*/
5+
6+
'use strict';
7+
8+
// ------------------------------------------------------------------------------
9+
// Requirements
10+
// ------------------------------------------------------------------------------
11+
12+
const RuleTester = require('eslint').RuleTester;
13+
const rule = require('../../../lib/rules/jsx-newline');
14+
const parsers = require('../../helpers/parsers');
15+
16+
const parserOptions = {
17+
ecmaVersion: 2018,
18+
sourceType: 'module',
19+
ecmaFeatures: {
20+
jsx: true
21+
}
22+
};
23+
24+
// ------------------------------------------------------------------------------
25+
// Tests
26+
// ------------------------------------------------------------------------------
27+
28+
const tests = {
29+
valid: [
30+
`
31+
<div>
32+
<Button>{data.label}</Button>
33+
34+
<List />
35+
36+
<Button>
37+
<IconPreview />
38+
Button 2
39+
40+
<span></span>
41+
</Button>
42+
43+
{showSomething === true && <Something />}
44+
45+
<Button>Button 3</Button>
46+
47+
{showSomethingElse === true ? (
48+
<SomethingElse />
49+
) : (
50+
<ErrorMessage />
51+
)}
52+
</div>
53+
`
54+
],
55+
invalid: [
56+
{
57+
code: `
58+
<div>
59+
<Button>{data.label}</Button>
60+
<List />
61+
</div>
62+
`,
63+
output: `
64+
<div>
65+
<Button>{data.label}</Button>
66+
67+
<List />
68+
</div>
69+
`,
70+
errors: [{
71+
message: 'JSX element should start in a new line'
72+
}]
73+
},
74+
{
75+
code: `
76+
<div>
77+
<Button>{data.label}</Button>
78+
{showSomething === true && <Something />}
79+
</div>
80+
`,
81+
output: `
82+
<div>
83+
<Button>{data.label}</Button>
84+
85+
{showSomething === true && <Something />}
86+
</div>
87+
`,
88+
errors: [{
89+
message: 'JSX element should start in a new line'
90+
}]
91+
},
92+
{
93+
code: `
94+
<div>
95+
{showSomething === true && <Something />}
96+
<Button>{data.label}</Button>
97+
</div>
98+
`,
99+
output: `
100+
<div>
101+
{showSomething === true && <Something />}
102+
103+
<Button>{data.label}</Button>
104+
</div>
105+
`,
106+
errors: [{
107+
message: 'JSX element should start in a new line'
108+
}]
109+
},
110+
{
111+
code: `
112+
<div>
113+
{showSomething === true && <Something />}
114+
{showSomethingElse === true ? (
115+
<SomethingElse />
116+
) : (
117+
<ErrorMessage />
118+
)}
119+
</div>
120+
`,
121+
output: `
122+
<div>
123+
{showSomething === true && <Something />}
124+
125+
{showSomethingElse === true ? (
126+
<SomethingElse />
127+
) : (
128+
<ErrorMessage />
129+
)}
130+
</div>
131+
`,
132+
errors: [{
133+
message: 'JSX element should start in a new line'
134+
}]
135+
},
136+
{
137+
code: `
138+
<div>
139+
<div>
140+
<button></button>
141+
<button></button>
142+
</div>
143+
<div>
144+
<span></span>
145+
<span></span>
146+
</div>
147+
</div>
148+
`,
149+
output: `
150+
<div>
151+
<div>
152+
<button></button>
153+
154+
<button></button>
155+
</div>
156+
157+
<div>
158+
<span></span>
159+
160+
<span></span>
161+
</div>
162+
</div>
163+
`,
164+
errors: [
165+
{message: 'JSX element should start in a new line'},
166+
{message: 'JSX element should start in a new line'},
167+
{message: 'JSX element should start in a new line'}
168+
]
169+
}
170+
]
171+
};
172+
173+
const advanceFeatTest = {
174+
valid: [
175+
{
176+
code: `
177+
<>
178+
<Button>{data.label}</Button>
179+
Test
180+
181+
<span>Should be in new line</span>
182+
</>
183+
`
184+
}
185+
],
186+
invalid: [
187+
{
188+
code: `
189+
<>
190+
<Button>{data.label}</Button>
191+
Test
192+
<span>Should be in new line</span>
193+
</>
194+
`,
195+
output: `
196+
<>
197+
<Button>{data.label}</Button>
198+
Test
199+
200+
<span>Should be in new line</span>
201+
</>
202+
`,
203+
errors: [
204+
{message: 'JSX element should start in a new line'}
205+
]
206+
}
207+
]
208+
};
209+
210+
// Run tests with default parser
211+
new RuleTester({parserOptions}).run('jsx-newline', rule, tests);
212+
213+
// Run tests with babel parser
214+
let ruleTester = new RuleTester({parserOptions, parser: parsers.BABEL_ESLINT});
215+
ruleTester.run('jsx-newline', rule, tests);
216+
ruleTester.run('jsx-newline', rule, advanceFeatTest);
217+
218+
// Run tests with typescript parser
219+
ruleTester = new RuleTester({parserOptions, parser: parsers.TYPESCRIPT_ESLINT});
220+
ruleTester.run('jsx-newline', rule, tests);
221+
ruleTester.run('jsx-newline', rule, advanceFeatTest);
222+
223+
ruleTester = new RuleTester({parserOptions, parser: parsers['@TYPESCRIPT_ESLINT']});
224+
ruleTester.run('jsx-newline', rule, {
225+
valid: parsers.TS(tests.valid),
226+
invalid: parsers.TS(tests.invalid)
227+
});
228+
ruleTester.run('jsx-newline', rule, {
229+
valid: parsers.TS(advanceFeatTest.valid),
230+
invalid: parsers.TS(advanceFeatTest.invalid)
231+
});

0 commit comments

Comments
 (0)