Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow multiline JSX expressions when prevent option is true: jsx-newline #3311

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 }],
},
]),
});