diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c53e403dd..7028aa03df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [`jsx-no-target-blank`]: add fixer ([#2862][] @Nokel81) * [`jsx-pascal-case`]: support minimatch `ignore` option ([#2906][] @bcherny) * [`jsx-pascal-case`]: support `allowNamespace` option ([#2917][] @kev-y-huang) +* [`jsx-newline`]: Add prevent option ([#2935][] @jsphstls) ### Fixed * [`jsx-no-constructed-context-values`]: avoid a crash with `as X` TS code ([#2894][] @ljharb) @@ -28,6 +29,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [Docs] added missing curly braces ([#2923][] @Muditxofficial) [#2943]: https://github.com/yannickcr/eslint-plugin-react/pull/2943 +[#2935]: https://github.com/yannickcr/eslint-plugin-react/pull/2935 [#2930]: https://github.com/yannickcr/eslint-plugin-react/pull/2930 [#2929]: https://github.com/yannickcr/eslint-plugin-react/pull/2929 [#2925]: https://github.com/yannickcr/eslint-plugin-react/pull/2925 diff --git a/README.md b/README.md index 1a25c9d55f..e276ff5a11 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,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 | -| | 🔧 | [react/jsx-newline](docs/rules/jsx-newline.md) | Enforce a new line after jsx elements and expressions | +| | 🔧 | [react/jsx-newline](docs/rules/jsx-newline.md) | Require or prevent a new line after jsx elements and expressions | | | | [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-constructed-context-values](docs/rules/jsx-no-constructed-context-values.md) | Prevents JSX context provider values from taking values that will cause needless rerenders. | diff --git a/docs/rules/jsx-newline.md b/docs/rules/jsx-newline.md index a622c7ae3a..4096c00229 100644 --- a/docs/rules/jsx-newline.md +++ b/docs/rules/jsx-newline.md @@ -1,12 +1,24 @@ -# Enforce a new line after jsx elements and expressions (react/jsx-newline) +# Require or prevent 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. +This is a stylistic rule intended to make JSX code more readable by requiring or preventing lines between adjacent JSX elements and expressions. -Examples of **incorrect** code for this rule: +## Rule Options +```json +... +"react/jsx-new-line": [, { "prevent": }] +... +``` + +* 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`. + +## Examples + +Examples of **incorrect** code for this rule, when configured with `{ "prevent": false }`: ```jsx
@@ -33,7 +45,7 @@ Examples of **incorrect** code for this rule:
``` -Examples of **correct** code for this rule: +Examples of **correct** code for this rule, when configured with `{ "prevent": false }`: ```jsx
@@ -60,6 +72,61 @@ Examples of **correct** code for this rule:
``` +Examples of **incorrect** code for this rule, when configured with `{ "prevent": true }`: + + +```jsx +
+ + + + + + + {showSomething === true && } + + + + {showSomethingElse === true ? ( + + ) : ( + + )} +
+``` + +Examples of **correct** code for this rule, when configured with `{ "prevent": true }`: + +```jsx +
+ + +
+``` + +```jsx +
+ + {showSomething === true && } +
+``` + +```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/lib/rules/jsx-newline.js b/lib/rules/jsx-newline.js index f75ad4105b..9b92beb1e0 100644 --- a/lib/rules/jsx-newline.js +++ b/lib/rules/jsx-newline.js @@ -1,6 +1,7 @@ /** - * @fileoverview Enforce a new line after jsx elements and expressions. + * @fileoverview Require or prevent a new line after jsx elements and expressions. * @author Johnny Zabala + * @author Joseph Stiles */ 'use strict'; @@ -14,7 +15,7 @@ const docsUrl = require('../util/docsUrl'); module.exports = { meta: { docs: { - description: 'Enforce a new line after jsx elements and expressions', + description: 'Require or prevent a new line after jsx elements and expressions.', category: 'Stylistic Issues', recommended: false, url: docsUrl('jsx-newline') @@ -22,8 +23,21 @@ module.exports = { fixable: 'code', messages: { - newLine: 'JSX element should start in a new line' - } + require: 'JSX element should start in a new line', + prevent: 'JSX element should not start in a new line' + }, + schema: [ + { + type: 'object', + properties: { + prevent: { + default: false, + type: 'boolean' + } + }, + additionalProperties: false + } + ] }, create(context) { const jsxElementParents = new Set(); @@ -35,26 +49,44 @@ module.exports = { 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, - messageId: 'newLine', - fix(fixer) { - return fixer.replaceText( - firstAdjacentSibling, - // double the last newline. - sourceCode.getText(firstAdjacentSibling) - .replace(/(\n)(?!.*\1)/g, '\n\n') - ); - } - }); - } + + const hasSibling = firstAdjacentSibling + && secondAdjacentSibling + && (firstAdjacentSibling.type === 'Literal' || firstAdjacentSibling.type === 'JSXText'); + + if (!hasSibling) return; + + // Check adjacent sibling has the proper amount of newlines + const isWithoutNewLine = !/\n\s*\n/.test(firstAdjacentSibling.value); + + const prevent = !!(context.options[0] || {}).prevent; + + if (isWithoutNewLine === prevent) return; + + const messageId = prevent + ? 'prevent' + : 'require'; + + const regex = prevent + ? /(\n\n)(?!.*\1)/g + : /(\n)(?!.*\1)/g; + + const replacement = prevent + ? '\n' + : '\n\n'; + + context.report({ + node: secondAdjacentSibling, + messageId, + fix(fixer) { + return fixer.replaceText( + firstAdjacentSibling, + // double or remove the last newline + sourceCode.getText(firstAdjacentSibling) + .replace(regex, replacement) + ); + } + }); } }); }); diff --git a/tests/lib/rules/jsx-newline.js b/tests/lib/rules/jsx-newline.js index 658c8618ee..6b21de278e 100644 --- a/tests/lib/rules/jsx-newline.js +++ b/tests/lib/rules/jsx-newline.js @@ -1,6 +1,7 @@ /** - * @fileoverview Enforce a new line after jsx elements and expressions + * @fileoverview Require or prevent a new line after jsx elements and expressions * @author Johnny Zabala + * @author Joseph Stiles */ 'use strict'; @@ -68,7 +69,7 @@ const tests = { `, errors: [{ - messageId: 'newLine' + messageId: 'require' }] }, { @@ -86,7 +87,7 @@ const tests = { `, errors: [{ - messageId: 'newLine' + messageId: 'require' }] }, { @@ -104,7 +105,7 @@ const tests = { `, errors: [{ - messageId: 'newLine' + messageId: 'require' }] }, { @@ -130,7 +131,7 @@ const tests = { `, errors: [{ - messageId: 'newLine' + messageId: 'require' }] }, { @@ -162,9 +163,9 @@ const tests = { `, errors: [ - {messageId: 'newLine'}, - {messageId: 'newLine'}, - {messageId: 'newLine'} + {messageId: 'require'}, + {messageId: 'require'}, + {messageId: 'require'} ] } ] @@ -201,7 +202,7 @@ const advanceFeatTest = { `, errors: [ - {messageId: 'newLine'} + {messageId: 'require'} ] } ] @@ -229,3 +230,231 @@ ruleTester.run('jsx-newline', rule, { valid: parsers.TS(advanceFeatTest.valid), invalid: parsers.TS(advanceFeatTest.invalid) }); + +// ------------------------------------------------------------------------------ +// Tests: { prevent: true } +// --------- --------------------------------------------------------------------- + +const preventionTests = { + valid: [ + { + code: ` +
+ + + + {showSomething === true && } + + {showSomethingElse === true ? ( + + ) : ( + + )} +
+ `, + options: [{ + prevent: true + }] + } + ], + invalid: [ + { + output: ` +
+ + +
+ `, + code: ` +
+ + + +
+ `, + errors: [{ + messageId: 'prevent' + }], + options: [{ + prevent: true + }] + }, + { + output: ` +
+ + {showSomething === true && } +
+ `, + code: ` +
+ + + {showSomething === true && } +
+ `, + errors: [{ + messageId: 'prevent' + }], + options: [{ + prevent: true + }] + }, + { + output: ` +
+ {showSomething === true && } + +
+ `, + code: ` +
+ {showSomething === true && } + + +
+ `, + errors: [{ + messageId: 'prevent' + }], + options: [{ + prevent: true + }] + }, + { + output: ` +
+ {showSomething === true && } + {showSomethingElse === true ? ( + + ) : ( + + )} +
+ `, + code: ` +
+ {showSomething === true && } + + {showSomethingElse === true ? ( + + ) : ( + + )} +
+ `, + errors: [{ + messageId: 'prevent' + }], + options: [{ + prevent: true + }] + }, + { + output: ` +
+
+ + +
+
+ + +
+
+ `, + code: ` +
+
+ + + +
+ +
+ + + +
+
+ `, + errors: [ + {messageId: 'prevent'}, + {messageId: 'prevent'}, + {messageId: 'prevent'} + ], + options: [{ + prevent: true + }] + } + ] +}; + +const preventionAdvanceFeatTest = { + valid: [ + { + code: ` + <> + + Test + Should be in new line + + `, + options: [{ + prevent: true + }] + } + ], + invalid: [ + { + output: ` + <> + + Test + Should be in new line + + `, + code: ` + <> + + Test + + Should be in new line + + `, + errors: [ + {messageId: 'prevent'} + ], + options: [{ + prevent: true + }] + } + ] +}; + +// // Run tests with default parser +new RuleTester({parserOptions}).run('jsx-newline', rule, preventionTests); + +// // Run tests with babel parser +ruleTester = new RuleTester({parserOptions, parser: parsers.BABEL_ESLINT}); +ruleTester.run('jsx-newline', rule, preventionTests); +ruleTester.run('jsx-newline', rule, preventionAdvanceFeatTest); + +// // Run tests with typescript parser +ruleTester = new RuleTester({parserOptions, parser: parsers.TYPESCRIPT_ESLINT}); +ruleTester.run('jsx-newline', rule, preventionTests); +ruleTester.run('jsx-newline', rule, preventionAdvanceFeatTest); + +ruleTester = new RuleTester({parserOptions, parser: parsers['@TYPESCRIPT_ESLINT']}); +ruleTester.run('jsx-newline', rule, { + valid: parsers.TS(preventionTests.valid), + invalid: parsers.TS(preventionTests.invalid) +}); +ruleTester.run('jsx-newline', rule, { + valid: parsers.TS(preventionAdvanceFeatTest.valid), + invalid: parsers.TS(preventionAdvanceFeatTest.invalid) +});