Skip to content

Commit

Permalink
[New] jsx-newline: Enforce a new line after jsx elements and expres…
Browse files Browse the repository at this point in the history
…sions
  • Loading branch information
jzabala authored and ljharb committed Jul 1, 2020
1 parent 9f0d5c4 commit 8867490
Show file tree
Hide file tree
Showing 6 changed files with 364 additions and 0 deletions.
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

<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)
});

0 comments on commit 8867490

Please sign in to comment.