From 7a1634a26a187cbd475742781bbb78eb60d74e31 Mon Sep 17 00:00:00 2001 From: TildaDares Date: Tue, 21 Jun 2022 11:35:49 +0100 Subject: [PATCH] [New] `jsx-newline`: add `allowMultiline` option when prevent option is true Fixes #3033 --- CHANGELOG.md | 6 ++ docs/rules/jsx-newline.md | 34 ++++++++- lib/rules/jsx-newline.js | 53 ++++++++++++- tests/lib/rules/jsx-newline.js | 132 +++++++++++++++++++++++++++++++++ 4 files changed, 222 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a89e1557d2..d8abcb2101 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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) diff --git a/docs/rules/jsx-newline.md b/docs/rules/jsx-newline.md index 2135e70e12..fa0b987a16 100644 --- a/docs/rules/jsx-newline.md +++ b/docs/rules/jsx-newline.md @@ -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": [, { "prevent": }] +"react/jsx-newline": [, { "prevent": , "allowMultilines": }] ... ``` * 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 @@ -127,6 +128,37 @@ Examples of **correct** code for this rule, when configured with `{ "prevent": t ``` +Examples of **incorrect** code for this rule, when configured with `{ "prevent": true, "allowMultilines": true }`: + +```jsx +
+ {showSomething === true && } + + + {showSomethingElse === true ? ( + + ) : ( + + )} +
+``` + +Examples of **correct** code for this rule, when configured with `{ "prevent": true, "allowMultilines": 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. diff --git a/lib/rules/jsx-newline.js b/lib/rules/jsx-newline.js index e3de9daef3..cbc5e58283 100644 --- a/lib/rules/jsx-newline.js +++ b/lib/rules/jsx-newline.js @@ -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: { @@ -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]; @@ -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'; diff --git a/tests/lib/rules/jsx-newline.js b/tests/lib/rules/jsx-newline.js index 54c5a68cce..da8a1b72a6 100644 --- a/tests/lib/rules/jsx-newline.js +++ b/tests/lib/rules/jsx-newline.js @@ -105,6 +105,47 @@ new RuleTester({ parserOptions }).run('jsx-newline', rule, { `, }, + { + code: ` + <> + + + + + + + + `, + options: [{ prevent: true, allowMultilines: true }], + }, + { + code: ` +
+ + + + + + {showSomething === true && } + + + {showSomethingElse === true ? ( + + ) : ( + + )} + +
+ `, + options: [{ prevent: true, allowMultilines: true }], + }, ]), invalid: parsers.all([ { @@ -365,5 +406,96 @@ new RuleTester({ parserOptions }).run('jsx-newline', rule, { options: [{ prevent: true }], features: ['fragment'], }, + { + code: ` + <> + + + + + + `, + output: ` + <> + + + + + + + + `, + errors: [ + { messageId: 'allowMultilines' }, + { messageId: 'allowMultilines' }, + ], + options: [{ prevent: true, allowMultilines: true }], + }, + { + code: ` +
+ {showSomething === true && } + {showSomethingElse === true ? ( + + ) : ( + + )} +
+ `, + output: ` +
+ {showSomething === true && } + + {showSomethingElse === true ? ( + + ) : ( + + )} +
+ `, + errors: [{ messageId: 'allowMultilines' }], + options: [{ prevent: true, allowMultilines: true }], + }, + { + output: ` +
+
+ + +
+ +
+ + +
+
+ `, + code: ` +
+
+ + + +
+
+ + + +
+
+ `, + errors: [ + { messageId: 'prevent' }, + { messageId: 'allowMultilines' }, + { messageId: 'prevent' }, + ], + options: [{ prevent: true, allowMultilines: true }], + }, ]), });