Skip to content

Commit 8887a19

Browse files
TildaDaresljharb
authored andcommitted
[New] jsx-newline: add allowMultiline option when prevent option is true
Fixes #3033
1 parent ed26b15 commit 8887a19

File tree

4 files changed

+224
-3
lines changed

4 files changed

+224
-3
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
55

66
## Unreleased
77

8+
### Added
9+
* [`jsx-newline`]: add `allowMultiline` option when prevent option is true ([#3311][] @TildaDares)
10+
11+
[#3311]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3311
12+
813
## [7.30.1] - 2022.06.23
914

1015
### Fixed
@@ -32,6 +37,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
3237
* [`function-component-definition`]: replace `var` by `const` in certain situations ([#3248][] @JohnBerd @SimeonC)
3338
* add [`jsx-no-leaked-render`] ([#3203][] @Belco90)
3439
* [`require-default-props`]: add option `functions` ([#3249][] @nix6839)
40+
* [`jsx-newline`]: Add `allowMultilines` option ([#3311][] @TildaDares)
3541

3642
### Fixed
3743
* [`hook-use-state`]: Allow UPPERCASE setState setter prefixes ([#3244][] @duncanbeevers)

docs/rules/jsx-newline.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ This is a stylistic rule intended to make JSX code more readable by requiring or
99
## Rule Options
1010
```json
1111
...
12-
"react/jsx-newline": [<enabled>, { "prevent": <boolean> }]
12+
"react/jsx-newline": [<enabled>, { "prevent": <boolean>, "allowMultilines": <boolean> }]
1313
...
1414
```
1515

1616
* enabled: for enabling the rule. 0=off, 1=warn, 2=error. Defaults to 0.
1717
* prevent: optional boolean. If `true` prevents empty lines between adjacent JSX elements and expressions. Defaults to `false`.
18+
* allowMultilines: optional boolean. If `true` and `prevent` is also equal to `true`, it allows newlines after multiline JSX elements and expressions. Defaults to `false`.
1819

1920
## Examples
2021

@@ -127,6 +128,37 @@ Examples of **correct** code for this rule, when configured with `{ "prevent": t
127128
</div>
128129
```
129130

131+
Examples of **incorrect** code for this rule, when configured with `{ "prevent": true, "allowMultilines": true }`:
132+
133+
```jsx
134+
<div>
135+
{showSomething === true && <Something />}
136+
137+
<Button>Button 3</Button>
138+
{showSomethingElse === true ? (
139+
<SomethingElse />
140+
) : (
141+
<ErrorMessage />
142+
)}
143+
</div>
144+
```
145+
146+
Examples of **correct** code for this rule, when configured with `{ "prevent": true, "allowMultilines": true }`:
147+
148+
```jsx
149+
<div>
150+
{showSomething === true && <Something />}
151+
152+
<Button>Button 3</Button>
153+
154+
{showSomethingElse === true ? (
155+
<SomethingElse />
156+
) : (
157+
<ErrorMessage />
158+
)}
159+
</div>
160+
```
161+
130162
## When Not To Use It
131163

132164
You can turn this rule off if you are not concerned with spacing between your JSX elements and expressions.

lib/rules/jsx-newline.js

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,13 @@ const report = require('../util/report');
1616
const messages = {
1717
require: 'JSX element should start in a new line',
1818
prevent: 'JSX element should not start in a new line',
19+
allowMultilines: 'Multiline JSX elements should start in a new line',
1920
};
2021

22+
function isMultilined(node) {
23+
return node.loc.start.line !== node.loc.end.line;
24+
}
25+
2126
module.exports = {
2227
meta: {
2328
docs: {
@@ -37,19 +42,45 @@ module.exports = {
3742
default: false,
3843
type: 'boolean',
3944
},
45+
allowMultilines: {
46+
default: false,
47+
type: 'boolean',
48+
},
4049
},
4150
additionalProperties: false,
51+
if: {
52+
properties: {
53+
allowMultilines: {
54+
const: true,
55+
},
56+
},
57+
},
58+
then: {
59+
properties: {
60+
prevent: {
61+
const: true,
62+
},
63+
},
64+
required: [
65+
'prevent',
66+
],
67+
},
4268
},
4369
],
4470
},
4571
create(context) {
4672
const jsxElementParents = new Set();
4773
const sourceCode = context.getSourceCode();
74+
4875
return {
4976
'Program:exit'() {
5077
jsxElementParents.forEach((parent) => {
5178
parent.children.forEach((element, index, elements) => {
5279
if (element.type === 'JSXElement' || element.type === 'JSXExpressionContainer') {
80+
const configuration = context.options[0] || {};
81+
const prevent = configuration.prevent || false;
82+
const allowMultilines = configuration.allowMultilines || false;
83+
5384
const firstAdjacentSibling = elements[index + 1];
5485
const secondAdjacentSibling = elements[index + 2];
5586

@@ -62,10 +93,28 @@ module.exports = {
6293
// Check adjacent sibling has the proper amount of newlines
6394
const isWithoutNewLine = !/\n\s*\n/.test(firstAdjacentSibling.value);
6495

65-
const prevent = !!(context.options[0] || {}).prevent;
96+
if (allowMultilines && (isMultilined(element) || isMultilined(secondAdjacentSibling))) {
97+
if (!isWithoutNewLine) return;
6698

67-
if (isWithoutNewLine === prevent) return;
99+
const regex = /(\n)(?!.*\1)/g;
100+
const replacement = '\n\n';
101+
const messageId = 'allowMultilines';
68102

103+
report(context, messages[messageId], messageId, {
104+
node: secondAdjacentSibling,
105+
fix(fixer) {
106+
return fixer.replaceText(
107+
firstAdjacentSibling,
108+
sourceCode.getText(firstAdjacentSibling)
109+
.replace(regex, replacement)
110+
);
111+
},
112+
});
113+
114+
return;
115+
}
116+
117+
if (isWithoutNewLine === prevent) return;
69118
const messageId = prevent
70119
? 'prevent'
71120
: 'require';

tests/lib/rules/jsx-newline.js

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,48 @@ new RuleTester({ parserOptions }).run('jsx-newline', rule, {
105105
</Button>
106106
`,
107107
},
108+
{
109+
code: `
110+
<>
111+
<OneLineComponent />
112+
<AnotherOneLineComponent prop={prop} />
113+
114+
<MultilineComponent
115+
prop1={prop1}
116+
prop2={prop2}
117+
/>
118+
119+
<OneLineComponent />
120+
</>
121+
`,
122+
features: ['fragment'],
123+
options: [{ prevent: true, allowMultilines: true }],
124+
},
125+
{
126+
code: `
127+
<div>
128+
<Button>{data.label}</Button>
129+
<List />
130+
131+
<Button>
132+
<IconPreview />
133+
Button 2
134+
<span></span>
135+
</Button>
136+
137+
{showSomething === true && <Something />}
138+
<Button>Button 3</Button>
139+
140+
{showSomethingElse === true ? (
141+
<SomethingElse />
142+
) : (
143+
<ErrorMessage />
144+
)}
145+
146+
</div>
147+
`,
148+
options: [{ prevent: true, allowMultilines: true }],
149+
},
108150
]),
109151
invalid: parsers.all([
110152
{
@@ -365,5 +407,97 @@ new RuleTester({ parserOptions }).run('jsx-newline', rule, {
365407
options: [{ prevent: true }],
366408
features: ['fragment'],
367409
},
410+
{
411+
code: `
412+
<>
413+
<OneLineComponent />
414+
<AnotherOneLineComponent prop={prop} />
415+
<MultilineComponent
416+
prop1={prop1}
417+
prop2={prop2}
418+
/>
419+
<OneLineComponent />
420+
</>
421+
`,
422+
output: `
423+
<>
424+
<OneLineComponent />
425+
<AnotherOneLineComponent prop={prop} />
426+
427+
<MultilineComponent
428+
prop1={prop1}
429+
prop2={prop2}
430+
/>
431+
432+
<OneLineComponent />
433+
</>
434+
`,
435+
features: ['fragment'],
436+
errors: [
437+
{ messageId: 'allowMultilines' },
438+
{ messageId: 'allowMultilines' },
439+
],
440+
options: [{ prevent: true, allowMultilines: true }],
441+
},
442+
{
443+
code: `
444+
<div>
445+
{showSomething === true && <Something />}
446+
{showSomethingElse === true ? (
447+
<SomethingElse />
448+
) : (
449+
<ErrorMessage />
450+
)}
451+
</div>
452+
`,
453+
output: `
454+
<div>
455+
{showSomething === true && <Something />}
456+
457+
{showSomethingElse === true ? (
458+
<SomethingElse />
459+
) : (
460+
<ErrorMessage />
461+
)}
462+
</div>
463+
`,
464+
errors: [{ messageId: 'allowMultilines' }],
465+
options: [{ prevent: true, allowMultilines: true }],
466+
},
467+
{
468+
output: `
469+
<div>
470+
<div>
471+
<button></button>
472+
<button></button>
473+
</div>
474+
475+
<div>
476+
<span></span>
477+
<span></span>
478+
</div>
479+
</div>
480+
`,
481+
code: `
482+
<div>
483+
<div>
484+
<button></button>
485+
486+
<button></button>
487+
</div>
488+
<div>
489+
<span></span>
490+
491+
<span></span>
492+
</div>
493+
</div>
494+
`,
495+
errors: [
496+
{ messageId: 'prevent' },
497+
{ messageId: 'allowMultilines' },
498+
{ messageId: 'prevent' },
499+
],
500+
options: [{ prevent: true, allowMultilines: true }],
501+
},
368502
]),
369503
});

0 commit comments

Comments
 (0)