Skip to content

Commit

Permalink
[New] jsx-newline: add allowMultiline option when prevent option …
Browse files Browse the repository at this point in the history
…is true

Fixes #3033
  • Loading branch information
TildaDares authored and ljharb committed Jun 21, 2022
1 parent ed26b15 commit 8887a19
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 3 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Expand Up @@ -5,6 +5,11 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange

## Unreleased

### Added
* [`jsx-newline`]: add `allowMultiline` option when prevent option is true ([#3311][] @TildaDares)

[#3311]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3311

## [7.30.1] - 2022.06.23

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

### Fixed
* [`hook-use-state`]: Allow UPPERCASE setState setter prefixes ([#3244][] @duncanbeevers)
Expand Down
34 changes: 33 additions & 1 deletion docs/rules/jsx-newline.md
Expand Up @@ -9,12 +9,13 @@ This is a stylistic rule intended to make JSX code more readable by requiring or
## Rule Options
```json
...
"react/jsx-newline": [<enabled>, { "prevent": <boolean> }]
"react/jsx-newline": [<enabled>, { "prevent": <boolean>, "allowMultilines": <boolean> }]
...
```

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

## Examples

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

Examples of **incorrect** code for this rule, when configured with `{ "prevent": true, "allowMultilines": true }`:

```jsx
<div>
{showSomething === true && <Something />}

<Button>Button 3</Button>
{showSomethingElse === true ? (
<SomethingElse />
) : (
<ErrorMessage />
)}
</div>
```

Examples of **correct** code for this rule, when configured with `{ "prevent": true, "allowMultilines": true }`:

```jsx
<div>
{showSomething === true && <Something />}

<Button>Button 3</Button>

{showSomethingElse === true ? (
<SomethingElse />
) : (
<ErrorMessage />
)}
</div>
```

## When Not To Use It

You can turn this rule off if you are not concerned with spacing between your JSX elements and expressions.
53 changes: 51 additions & 2 deletions lib/rules/jsx-newline.js
Expand Up @@ -16,8 +16,13 @@ const report = require('../util/report');
const messages = {
require: 'JSX element should start in a new line',
prevent: 'JSX element should not start in a new line',
allowMultilines: 'Multiline JSX elements should start in a new line',
};

function isMultilined(node) {
return node.loc.start.line !== node.loc.end.line;
}

module.exports = {
meta: {
docs: {
Expand All @@ -37,19 +42,45 @@ module.exports = {
default: false,
type: 'boolean',
},
allowMultilines: {
default: false,
type: 'boolean',
},
},
additionalProperties: false,
if: {
properties: {
allowMultilines: {
const: true,
},
},
},
then: {
properties: {
prevent: {
const: true,
},
},
required: [
'prevent',
],
},
},
],
},
create(context) {
const jsxElementParents = new Set();
const sourceCode = context.getSourceCode();

return {
'Program:exit'() {
jsxElementParents.forEach((parent) => {
parent.children.forEach((element, index, elements) => {
if (element.type === 'JSXElement' || element.type === 'JSXExpressionContainer') {
const configuration = context.options[0] || {};
const prevent = configuration.prevent || false;
const allowMultilines = configuration.allowMultilines || false;

const firstAdjacentSibling = elements[index + 1];
const secondAdjacentSibling = elements[index + 2];

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

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

if (isWithoutNewLine === prevent) return;
const regex = /(\n)(?!.*\1)/g;
const replacement = '\n\n';
const messageId = 'allowMultilines';

report(context, messages[messageId], messageId, {
node: secondAdjacentSibling,
fix(fixer) {
return fixer.replaceText(
firstAdjacentSibling,
sourceCode.getText(firstAdjacentSibling)
.replace(regex, replacement)
);
},
});

return;
}

if (isWithoutNewLine === prevent) return;
const messageId = prevent
? 'prevent'
: 'require';
Expand Down
134 changes: 134 additions & 0 deletions tests/lib/rules/jsx-newline.js
Expand Up @@ -105,6 +105,48 @@ new RuleTester({ parserOptions }).run('jsx-newline', rule, {
</Button>
`,
},
{
code: `
<>
<OneLineComponent />
<AnotherOneLineComponent prop={prop} />
<MultilineComponent
prop1={prop1}
prop2={prop2}
/>
<OneLineComponent />
</>
`,
features: ['fragment'],
options: [{ prevent: true, allowMultilines: true }],
},
{
code: `
<div>
<Button>{data.label}</Button>
<List />
<Button>
<IconPreview />
Button 2
<span></span>
</Button>
{showSomething === true && <Something />}
<Button>Button 3</Button>
{showSomethingElse === true ? (
<SomethingElse />
) : (
<ErrorMessage />
)}
</div>
`,
options: [{ prevent: true, allowMultilines: true }],
},
]),
invalid: parsers.all([
{
Expand Down Expand Up @@ -365,5 +407,97 @@ new RuleTester({ parserOptions }).run('jsx-newline', rule, {
options: [{ prevent: true }],
features: ['fragment'],
},
{
code: `
<>
<OneLineComponent />
<AnotherOneLineComponent prop={prop} />
<MultilineComponent
prop1={prop1}
prop2={prop2}
/>
<OneLineComponent />
</>
`,
output: `
<>
<OneLineComponent />
<AnotherOneLineComponent prop={prop} />
<MultilineComponent
prop1={prop1}
prop2={prop2}
/>
<OneLineComponent />
</>
`,
features: ['fragment'],
errors: [
{ messageId: 'allowMultilines' },
{ messageId: 'allowMultilines' },
],
options: [{ prevent: true, allowMultilines: true }],
},
{
code: `
<div>
{showSomething === true && <Something />}
{showSomethingElse === true ? (
<SomethingElse />
) : (
<ErrorMessage />
)}
</div>
`,
output: `
<div>
{showSomething === true && <Something />}
{showSomethingElse === true ? (
<SomethingElse />
) : (
<ErrorMessage />
)}
</div>
`,
errors: [{ messageId: 'allowMultilines' }],
options: [{ prevent: true, allowMultilines: true }],
},
{
output: `
<div>
<div>
<button></button>
<button></button>
</div>
<div>
<span></span>
<span></span>
</div>
</div>
`,
code: `
<div>
<div>
<button></button>
<button></button>
</div>
<div>
<span></span>
<span></span>
</div>
</div>
`,
errors: [
{ messageId: 'prevent' },
{ messageId: 'allowMultilines' },
{ messageId: 'prevent' },
],
options: [{ prevent: true, allowMultilines: true }],
},
]),
});

0 comments on commit 8887a19

Please sign in to comment.