diff --git a/docs/rules/jsx-inline-conditional.md b/docs/rules/jsx-inline-conditional.md new file mode 100644 index 0000000000..a2ca940807 --- /dev/null +++ b/docs/rules/jsx-inline-conditional.md @@ -0,0 +1,33 @@ +# Enforce JSX inline conditional as a ternary (react/jsx-inline-conditional) + +This rule helps avoid common rendering bugs where the left side of an inline conditional is falsy (e.g. zero) and renders the value of the condition (e.g. `0`) instead of nothing. See the note in the [official React docs](https://reactjs.org/docs/conditional-rendering.html#inline-if-with-logical--operator). + +**Fixable:** This rule is automatically fixable using the `--fix` flag on the command line. +Fixer will fix whitespace and tabs indentation. + +## Rule Details + +This rule is aimed to enforce consistent indentation style. The default style is `4 spaces`. + +Examples of **incorrect** code for this rule: + +```jsx +
+ {someCondition && } +
+
+ {someCondition || someOtherCondition && } +
+``` + +Examples of **correct** code for this rule: + +```jsx +
+ {someCondition ? : null} +
+// -- +
+ {someCondition || someOtherCondition ? : null} +
+``` diff --git a/index.js b/index.js index be3c94a992..cd43d09150 100644 --- a/index.js +++ b/index.js @@ -30,6 +30,7 @@ const allRules = { 'jsx-handler-names': require('./lib/rules/jsx-handler-names'), 'jsx-indent': require('./lib/rules/jsx-indent'), 'jsx-indent-props': require('./lib/rules/jsx-indent-props'), + 'jsx-inline-conditional': require('./lib/rules/jsx-inline-conditional'), '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'), @@ -124,9 +125,7 @@ module.exports = { rules: allRules, configs: { recommended: { - plugins: [ - 'react', - ], + plugins: ['react'], parserOptions: { ecmaFeatures: { jsx: true, @@ -134,6 +133,7 @@ module.exports = { }, rules: { 'react/display-name': 2, + 'react/jsx-inline-conditional': 2, 'react/jsx-key': 2, 'react/jsx-no-comment-textnodes': 2, 'react/jsx-no-duplicate-props': 2, @@ -158,9 +158,7 @@ module.exports = { }, }, all: { - plugins: [ - 'react', - ], + plugins: ['react'], parserOptions: { ecmaFeatures: { jsx: true, @@ -169,9 +167,7 @@ module.exports = { rules: activeRulesConfig, }, 'jsx-runtime': { - plugins: [ - 'react', - ], + plugins: ['react'], parserOptions: { ecmaFeatures: { jsx: true, diff --git a/lib/rules/jsx-inline-conditional.js b/lib/rules/jsx-inline-conditional.js new file mode 100644 index 0000000000..4e17939a90 --- /dev/null +++ b/lib/rules/jsx-inline-conditional.js @@ -0,0 +1,55 @@ +/** + * @fileoverview Enforce JSX inline conditional as a ternary + * @author Kevin Ingersoll + */ + +'use strict'; + +const docsUrl = require('../util/docsUrl'); + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +const messages = { + inlineConditional: 'Conditional rendering in JSX should use a full ternary expression to avoid unintentionally rendering falsy values (i.e. zero)', +}; + +module.exports = { + meta: { + docs: { + description: 'Enforce JSX inline conditional as a ternary', + category: 'Possible Errors', + recommended: true, + url: docsUrl('jsx-inline-conditional'), + }, + fixable: 'code', + messages, + schema: [], + }, + + create(context) { + const sourceCode = context.getSourceCode(); + + return { + JSXExpressionContainer(node) { + if ( + node.expression.type === 'LogicalExpression' + && node.expression.operator === '&&' + && node.expression.right.type === 'JSXElement' + ) { + context.report({ + node, + messageId: 'inlineConditional', + fix: (fixer) => fixer.replaceText( + node, + `{${sourceCode.getText( + node.expression.left + )} ? ${sourceCode.getText(node.expression.right)} : null}` + ), + }); + } + }, + }; + }, +}; diff --git a/tests/lib/rules/jsx-inline-conditional.js b/tests/lib/rules/jsx-inline-conditional.js new file mode 100644 index 0000000000..d2563de52e --- /dev/null +++ b/tests/lib/rules/jsx-inline-conditional.js @@ -0,0 +1,76 @@ +/** + * @fileoverview Enforce JSX inline conditional as a ternary + * @author Kevin Ingersoll + */ + +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester; +const rule = require('../../../lib/rules/jsx-inline-conditional'); + +const parsers = require('../../helpers/parsers'); + +const parserOptions = { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, +}; + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ parserOptions }); +ruleTester.run('jsx-inline-conditional', rule, { + valid: parsers.all([ + { code: '
{someCondition ?
: null}
' }, + { code: '
{someCondition ? : null}
' }, + { + code: '
{someCondition ?
{anotherCondition ? : null}
: null}
', + }, + { + code: '
{someCondition && someOtherCondition ? : null}
', + }, + { + code: '
{possiblyNull ?? }
', + parserOptions: { + ecmaVersion: 2020, + }, + }, + { + code: '
{possiblyNull ?? }
', + parser: parsers.TYPESCRIPT_ESLINT, + }, + { + code: '
{possiblyNull ?? }
', + parser: parsers['@TYPESCRIPT_ESLINT'], + }, + ]), + invalid: parsers.all([ + { + code: '
{someCondition && }
', + output: '
{someCondition ? : null}
', + errors: [ + { + messageId: 'inlineConditional', + }, + ], + }, + { + code: '
{someCondition && someOtherCondition && }
', + output: + '
{someCondition && someOtherCondition ? : null}
', + errors: [ + { + messageId: 'inlineConditional', + }, + ], + }, + ]), +});