diff --git a/CHANGELOG.md b/CHANGELOG.md index 6343320b51..209e4e9a09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/README.md b/README.md index bca97870bf..fa206b90b6 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/rules/jsx-newline.md b/docs/rules/jsx-newline.md new file mode 100644 index 0000000000..7d61e88cdc --- /dev/null +++ b/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 +
+ + +
+``` + +```jsx +
+ + {showSomething === true && } +
+``` + +```jsx +
+ {showSomething === true && } + {showSomethingElse === true ? ( + + ) : ( + + )} +
+``` + +The following patterns are **not** considered warnings: + +```jsx +
+ + + + + + + {showSomething === true && } + + + + {showSomethingElse === true ? ( + + ) : ( + + )} +
+``` + +## When Not To Use It + +You can turn this rule off if you are not concerned with spacing between your JSX elements and expressions. \ No newline at end of file diff --git a/index.js b/index.js index bc0c30a43d..a533fe3e8f 100644 --- a/index.js +++ b/index.js @@ -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'), diff --git a/lib/rules/jsx-newline.js b/lib/rules/jsx-newline.js new file mode 100644 index 0000000000..f0a1f1cca5 --- /dev/null +++ b/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); + } + }; + } +}; diff --git a/tests/lib/rules/jsx-newline.js b/tests/lib/rules/jsx-newline.js new file mode 100644 index 0000000000..687a7c7a06 --- /dev/null +++ b/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: [ + ` +
+ + + + + + + {showSomething === true && } + + + + {showSomethingElse === true ? ( + + ) : ( + + )} +
+ ` + ], + invalid: [ + { + code: ` +
+ + +
+ `, + output: ` +
+ + + +
+ `, + errors: [{ + message: 'JSX element should start in a new line' + }] + }, + { + code: ` +
+ + {showSomething === true && } +
+ `, + output: ` +
+ + + {showSomething === true && } +
+ `, + errors: [{ + message: 'JSX element should start in a new line' + }] + }, + { + code: ` +
+ {showSomething === true && } + +
+ `, + output: ` +
+ {showSomething === true && } + + +
+ `, + errors: [{ + message: 'JSX element should start in a new line' + }] + }, + { + code: ` +
+ {showSomething === true && } + {showSomethingElse === true ? ( + + ) : ( + + )} +
+ `, + output: ` +
+ {showSomething === true && } + + {showSomethingElse === true ? ( + + ) : ( + + )} +
+ `, + errors: [{ + message: 'JSX element should start in a new line' + }] + }, + { + code: ` +
+
+ + +
+
+ + +
+
+ `, + output: ` +
+
+ + + +
+ +
+ + + +
+
+ `, + 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: ` + <> + + Test + + Should be in new line + + ` + } + ], + invalid: [ + { + code: ` + <> + + Test + Should be in new line + + `, + output: ` + <> + + Test + + Should be in new line + + `, + 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) +});