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

[New]jsx-newline: Enforce a new line after jsx elements and expressions #2693

Merged
merged 1 commit into from Oct 20, 2020
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Expand Up @@ -7,8 +7,10 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel

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

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

## [7.21.5] - 2020.10.19

Expand Down Expand Up @@ -3216,3 +3218,4 @@ If you're still not using React 15 you can keep the old behavior by setting the
[`jsx-no-script-url`]: docs/rules/jsx-no-script-url.md
[`no-adjacent-inline-elements`]: docs/rules/no-adjacent-inline-elements.md
[`function-component-definition`]: docs/rules/function-component-definition.md
[`jsx-newline`]: docs/rules/jsx-newline.md
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -172,6 +172,7 @@ Enable the rules that you would like to use.
* [react/jsx-key](docs/rules/jsx-key.md): Report missing `key` props in iterators/collection literals
* [react/jsx-max-depth](docs/rules/jsx-max-depth.md): Validate JSX maximum depth
* [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)
* [react/jsx-newline](docs/rules/jsx-newline.md): Enforce a new line after jsx elements and expressions (fixable)
* [react/jsx-no-bind](docs/rules/jsx-no-bind.md): Prevents usage of Function.prototype.bind and arrow functions in React component props
* [react/jsx-no-comment-textnodes](docs/rules/jsx-no-comment-textnodes.md): Comments inside children section of tag should be placed inside braces
* [react/jsx-no-duplicate-props](docs/rules/jsx-no-duplicate-props.md): Enforce no duplicate props
Expand Down
65 changes: 65 additions & 0 deletions docs/rules/jsx-newline.md
@@ -0,0 +1,65 @@
# Enforce a new line after jsx elements and expressions (react/jsx-newline)

**Fixable:** This rule is automatically fixable using the `--fix` flag on the command line.

## Rule Details

This is a stylistic rule intended to make JSX code more readable by enforcing spaces between adjacent JSX elements and expressions.

The following patterns are considered warnings:

```jsx
<div>
<Button>{data.label}</Button>
<List />
</div>
```

```jsx
<div>
<Button>{data.label}</Button>
{showSomething === true && <Something />}
</div>
```

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

The following patterns are **not** considered warnings:

```jsx
<div>
<Button>{data.label}</Button>

<List />

<Button>
<IconPreview />
Button 2
Comment on lines +45 to +46
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how is this not a warning? there's no newline after the element.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the Good part of the issue description the same example is giving without a newline. That is why the rule only warns about newlines between elements and expressions, no literals like text and numbers.


<span></span>
</Button>

{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.
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -31,6 +31,7 @@ const allRules = {
'jsx-key': require('./lib/rules/jsx-key'),
'jsx-max-depth': require('./lib/rules/jsx-max-depth'),
'jsx-max-props-per-line': require('./lib/rules/jsx-max-props-per-line'),
'jsx-newline': require('./lib/rules/jsx-newline'),
'jsx-no-bind': require('./lib/rules/jsx-no-bind'),
'jsx-no-comment-textnodes': require('./lib/rules/jsx-no-comment-textnodes'),
'jsx-no-duplicate-props': require('./lib/rules/jsx-no-duplicate-props'),
Expand Down
63 changes: 63 additions & 0 deletions lib/rules/jsx-newline.js
@@ -0,0 +1,63 @@
/**
* @fileoverview Enforce a new line after jsx elements and expressions.
* @author Johnny Zabala
*/

'use strict';

const docsUrl = require('../util/docsUrl');

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

module.exports = {
meta: {
docs: {
description: 'Enforce a new line after jsx elements and expressions',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('jsx-newline')
},
fixable: 'code'
},
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 firstAdjacentSibling = elements[index + 1];
const secondAdjacentSibling = elements[index + 2];
if (
firstAdjacentSibling
&& secondAdjacentSibling
&& (firstAdjacentSibling.type === 'Literal' || firstAdjacentSibling.type === 'JSXText')
// Check adjacent sibling has the proper amount of newlines
&& !/\n\s*\n/.test(firstAdjacentSibling.value)
) {
context.report({
node: secondAdjacentSibling,
message: 'JSX element should start in a new line',
fix(fixer) {
return fixer.replaceText(
firstAdjacentSibling,
// double the last newline.
sourceCode.getText(firstAdjacentSibling)
.replace(/(\n)(?!.*\1)/g, '\n\n')
);
}
});
}
}
});
});
},
':matches(JSXElement, JSXFragment) > :matches(JSXElement, JSXExpressionContainer)': (node) => {
jsxElementParents.add(node.parent);
}
};
}
};
231 changes: 231 additions & 0 deletions tests/lib/rules/jsx-newline.js
@@ -0,0 +1,231 @@
/**
* @fileoverview Enforce a new line after jsx elements and expressions
* @author Johnny Zabala
*/

'use strict';

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

const RuleTester = require('eslint').RuleTester;
const rule = require('../../../lib/rules/jsx-newline');
const parsers = require('../../helpers/parsers');

const parserOptions = {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
};

// ------------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------------

const tests = {
valid: [
`
<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>
`
],
invalid: [
{
code: `
<div>
<Button>{data.label}</Button>
<List />
</div>
`,
output: `
<div>
<Button>{data.label}</Button>

<List />
</div>
`,
errors: [{
message: 'JSX element should start in a new line'
}]
},
{
code: `
<div>
<Button>{data.label}</Button>
{showSomething === true && <Something />}
</div>
`,
output: `
<div>
<Button>{data.label}</Button>

{showSomething === true && <Something />}
</div>
`,
errors: [{
message: 'JSX element should start in a new line'
}]
},
{
code: `
<div>
{showSomething === true && <Something />}
<Button>{data.label}</Button>
</div>
`,
output: `
<div>
{showSomething === true && <Something />}

<Button>{data.label}</Button>
</div>
`,
errors: [{
message: 'JSX element should start in a new line'
}]
},
{
code: `
<div>
{showSomething === true && <Something />}
{showSomethingElse === true ? (
<SomethingElse />
) : (
<ErrorMessage />
)}
</div>
`,
output: `
<div>
{showSomething === true && <Something />}

{showSomethingElse === true ? (
<SomethingElse />
) : (
<ErrorMessage />
)}
</div>
`,
errors: [{
message: 'JSX element should start in a new line'
}]
},
{
code: `
<div>
<div>
<button></button>
<button></button>
</div>
<div>
<span></span>
<span></span>
</div>
</div>
`,
output: `
<div>
<div>
<button></button>

<button></button>
</div>

<div>
<span></span>

<span></span>
</div>
</div>
`,
errors: [
{message: 'JSX element should start in a new line'},
{message: 'JSX element should start in a new line'},
{message: 'JSX element should start in a new line'}
]
}
]
};

const advanceFeatTest = {
valid: [
{
code: `
<>
<Button>{data.label}</Button>
Test

<span>Should be in new line</span>
</>
`
}
],
invalid: [
{
code: `
<>
<Button>{data.label}</Button>
Test
<span>Should be in new line</span>
</>
`,
output: `
<>
<Button>{data.label}</Button>
Test

<span>Should be in new line</span>
</>
`,
errors: [
{message: 'JSX element should start in a new line'}
]
}
]
};

// Run tests with default parser
new RuleTester({parserOptions}).run('jsx-newline', rule, tests);

// Run tests with babel parser
let ruleTester = new RuleTester({parserOptions, parser: parsers.BABEL_ESLINT});
ruleTester.run('jsx-newline', rule, tests);
ruleTester.run('jsx-newline', rule, advanceFeatTest);

// Run tests with typescript parser
ruleTester = new RuleTester({parserOptions, parser: parsers.TYPESCRIPT_ESLINT});
ruleTester.run('jsx-newline', rule, tests);
ruleTester.run('jsx-newline', rule, advanceFeatTest);

ruleTester = new RuleTester({parserOptions, parser: parsers['@TYPESCRIPT_ESLINT']});
ruleTester.run('jsx-newline', rule, {
valid: parsers.TS(tests.valid),
invalid: parsers.TS(tests.invalid)
});
ruleTester.run('jsx-newline', rule, {
valid: parsers.TS(advanceFeatTest.valid),
invalid: parsers.TS(advanceFeatTest.invalid)
});