diff --git a/README.md b/README.md index 4e000f5989..d8219c6a06 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,7 @@ Enable the rules that you would like to use. * [react/jsx-no-bind](docs/rules/jsx-no-bind.md): Prevent usage of `.bind()` and arrow functions in JSX props * [react/jsx-no-comment-textnodes](docs/rules/jsx-no-comment-textnodes.md): Prevent comments from being inserted as text nodes * [react/jsx-no-duplicate-props](docs/rules/jsx-no-duplicate-props.md): Prevent duplicate props in JSX +* [react/jsx-no-length-truthiness](docs/rules/jsx-no-length-truthiness.md): Prevent truthiness checks of `.length` that might render unwanted 0:s (fixable) * [react/jsx-no-literals](docs/rules/jsx-no-literals.md): Prevent usage of unwrapped JSX strings * [react/jsx-no-target-blank](docs/rules/jsx-no-target-blank.md): Prevent usage of unsafe `target='_blank'` * [react/jsx-no-undef](docs/rules/jsx-no-undef.md): Disallow undeclared variables in JSX diff --git a/docs/rules/jsx-no-length-truthiness.md b/docs/rules/jsx-no-length-truthiness.md new file mode 100644 index 0000000000..0d328e8956 --- /dev/null +++ b/docs/rules/jsx-no-length-truthiness.md @@ -0,0 +1,25 @@ +# Prevent guarding with length truthiness in JSX (react/jsx-no-length-truthiness) + +Flags all instances of checking `.length` truthiness in JSX. + +**Fixable:** This rule is automatically fixable using the `--fix` flag on the command line. + +## Rule Details + +It is a common pattern to render something or nothing using `&&` as a guard: + +```jsx +
{showHeader &&
} ...
+``` + +A common mistake is to use array length naïvely for this: + +```jsx +
{posts.length && posts.map(p => ...)}
+``` + +The above code will render a presumably unwanted `0` when the array is empty. This rule checks for such erroneous use, and fixes it to the correct boolean check: + +```jsx +
{posts.length > 0 && posts.map(p => ...)}
+``` diff --git a/index.js b/index.js index 25faa607a6..8d31ed4c26 100644 --- a/index.js +++ b/index.js @@ -30,6 +30,7 @@ const allRules = { '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'), + 'jsx-no-length-truthiness': require('./lib/rules/jsx-no-length-truthiness'), 'jsx-no-literals': require('./lib/rules/jsx-no-literals'), 'jsx-no-target-blank': require('./lib/rules/jsx-no-target-blank'), 'jsx-one-expression-per-line': require('./lib/rules/jsx-one-expression-per-line'), diff --git a/lib/rules/jsx-no-length-truthiness.js b/lib/rules/jsx-no-length-truthiness.js new file mode 100644 index 0000000000..ab1c359dcd --- /dev/null +++ b/lib/rules/jsx-no-length-truthiness.js @@ -0,0 +1,69 @@ +/** + * @fileoverview Prevent accidentally rendering zeroes by checking .length truthiness + * @author Carl Mäsak & David Waller + */ +'use strict'; + +const docsUrl = require('../util/docsUrl'); +const jsxUtils = require('../util/jsx'); + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +const message = 'Don\'t check .length truthiness in JSX, might render 0'; + +function findDangerousLengthMember(node) { + if (node.type === 'MemberExpression' && node.property.name === 'length') { + return node; + } + if (node.type === 'LogicalExpression') { + // we're interested in right side of OR:s and both side of AND:s + return ( + findDangerousLengthMember(node.right) || + (node.operator === '&&' && findDangerousLengthMember(node.left)) + ); + } + return null; +} + +module.exports = { + meta: { + docs: { + description: 'Prevent checking .length truthiness in JSX', + category: 'Possible Errors', + recommended: false, + url: docsUrl('jsx-no-length-truthiness') + }, + schema: [], + fixable: true + }, + + create: context => ({ + LogicalExpression(node) { + // We're only interested in AND:s, where we'll check for guards to the left + if (node.operator !== '&&') { + return; + } + // We know we're "rendering" if right side is JSX or we're inside JSX expression + if ( + !jsxUtils.isJSX(node.right) && + node.parent.type !== 'JSXExpressionContainer' + ) { + return; + } + // If left side doesn't check truthiness of .length then all is well + const dangerousLengthMember = findDangerousLengthMember(node.left); + if (!dangerousLengthMember) { + return; + } + context.report({ + node, + message, + fix(fixer) { + return fixer.insertTextAfter(dangerousLengthMember, ' > 0'); + } + }); + } + }) +}; diff --git a/tests/lib/rules/jsx-no-length-truthiness.js b/tests/lib/rules/jsx-no-length-truthiness.js new file mode 100644 index 0000000000..80080397af --- /dev/null +++ b/tests/lib/rules/jsx-no-length-truthiness.js @@ -0,0 +1,81 @@ +/** + * @fileoverview Tests for jsx-no-length-truthiness + * @author Carl Mäsak & David Waller + */ + +'use strict'; + +// ----------------------------------------------------------------------------- +// Requirements +// ----------------------------------------------------------------------------- + +const rule = require('../../../lib/rules/jsx-no-length-truthiness'); +const RuleTester = require('eslint').RuleTester; + +const parserOptions = { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + jsx: true + } +}; + +// ----------------------------------------------------------------------------- +// Tests +// ----------------------------------------------------------------------------- + +const ruleTester = new RuleTester({parserOptions}); + +const expectedError = { + message: 'Don\'t check .length truthiness in JSX, might render 0' +}; + +ruleTester.run('jsx-no-length-truthiness', rule, { + valid: [ + {code: 'arr.length > 0 && '}, + {code: 'arr.length > 1 && '}, + {code: 'arr.length && "foobar"'}, // ok since we can't tell if this is a JSX expression at all + {code: 'arr.longth && '}, + {code: '(arr.length || flag) && '}, + {code: 'Number of posts: {arr.length}'}, + {code: 'Number of posts: {flag && arr.length}'}, + {code: 'You have {arr.length || "no"} posts'} + ], + invalid: [ + { + code: 'arr.length && ', + errors: [expectedError], + output: 'arr.length > 0 && ' + }, + { + code: 'arr.foo.length && ', + errors: [expectedError], + output: 'arr.foo.length > 0 && ' + }, + { + code: 'arr && arr.length && ', + errors: [expectedError], + output: 'arr && arr.length > 0 && ' + }, + { + code: 'arr && arr.length && somethingElse && ', + errors: [expectedError], + output: 'arr && arr.length > 0 && somethingElse && ' + }, + { + code: 'flag || arr.length && ', + errors: [expectedError], + output: 'flag || arr.length > 0 && ' + }, + { + code: '{arr.length && "you have posts!"}', + errors: [expectedError], + output: '{arr.length > 0 && "you have posts!"}' + }, + { + code: '{flag && arr.length && "you have posts!"}', + errors: [expectedError], + output: '{flag && arr.length > 0 && "you have posts!"}' + } + ] +});