diff --git a/docs/rules/no-render-return-undefined.md b/docs/rules/no-render-return-undefined.md new file mode 100644 index 0000000000..e2ea496ae2 --- /dev/null +++ b/docs/rules/no-render-return-undefined.md @@ -0,0 +1,64 @@ +# Disallow returning undefined from react components (`react/no-render-return-undefined`) + +💼 This rule is enabled in the ☑️ `recommended` [config](https://github.com/jsx-eslint/eslint-plugin-react/#shareable-configs). + + + +> Starting in React 18, components may render `undefined`, and React will render nothing to the DOM instead of throwing an error. However, accidentally rendering nothing in a component could still cause surprises. This rule will warn if the `return` statement in a React Component returns `undefined`. + +## Rule Details + +This rule will warn if the `return` statement in a React component returns `undefined`. + +Examples of **incorrect** code for this rule: + +```jsx +function App() {} + +// OR + +function App() { + return undefined; +} + +// OR + +let ui; +function App() { + return ui; +} + +// OR + +function getUI() { + if (condition) return

Hello

; +} +function App() { + return getUI(); +} +``` + +Examples of **correct** code for this rule: + +```jsx +function App() { + return
; +} + +// OR + +let ui =
; +function App() { + return ui; +} + +// OR + +function getUI() { + if (condition) return

Hello

; + return null; +} +function App() { + return getUI(); +} +``` diff --git a/lib/rules/index.js b/lib/rules/index.js index 784831bba7..054d676da2 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -73,10 +73,11 @@ module.exports = { 'no-is-mounted': require('./no-is-mounted'), 'no-multi-comp': require('./no-multi-comp'), 'no-namespace': require('./no-namespace'), - 'no-set-state': require('./no-set-state'), - 'no-string-refs': require('./no-string-refs'), 'no-redundant-should-component-update': require('./no-redundant-should-component-update'), + 'no-render-return-undefined': require('./no-render-return-undefined'), 'no-render-return-value': require('./no-render-return-value'), + 'no-set-state': require('./no-set-state'), + 'no-string-refs': require('./no-string-refs'), 'no-this-in-sfc': require('./no-this-in-sfc'), 'no-typos': require('./no-typos'), 'no-unescaped-entities': require('./no-unescaped-entities'), diff --git a/lib/rules/no-render-return-undefined.js b/lib/rules/no-render-return-undefined.js new file mode 100644 index 0000000000..3440f0f0f0 --- /dev/null +++ b/lib/rules/no-render-return-undefined.js @@ -0,0 +1,157 @@ +/** + * @fileoverview Prevent returning undefined from react components + * @author Akul Srivastava + */ + +'use strict'; + +const astUtil = require('../util/ast'); +const docsUrl = require('../util/docsUrl'); +const isFirstLetterCapitalized = require('../util/isFirstLetterCapitalized'); +const report = require('../util/report'); +const variableUtil = require('../util/variable'); + +const messages = { + returnsUndefined: "Don't return `undefined` from react components", +}; + +function getReturnValue(context, returnNode) { + const variables = variableUtil.variablesInScope(context, returnNode); + const returnIdentifierName = returnNode && returnNode.name; + const returnIdentifierVar = variableUtil.getVariable( + variables, + returnIdentifierName + ); + + if (!returnNode) return undefined; + + if ( + returnIdentifierVar + && returnIdentifierVar.defs + && returnIdentifierVar.defs[0] + ) { + const value = returnIdentifierVar.defs[0].node.init; + if ( + returnIdentifierVar.defs[0].node + && returnIdentifierVar.defs[0].node.type === 'VariableDeclarator' + && value === null + ) { + return undefined; + } + return value; + } + + switch (returnNode.type) { + case 'LogicalExpression': { + return getReturnValue(context, returnNode.right); + } + case 'ConditionalExpression': { + const possibleReturnValue = [ + getReturnValue(context, returnNode.consequent), + getReturnValue(context, returnNode.alternate), + ]; + const returnsUndefined = possibleReturnValue.some((val) => typeof val === 'undefined'); + if (returnsUndefined) return; + return possibleReturnValue; + } + case 'CallExpression': { + if (returnNode.callee.type === 'MemberExpression') { + const calleeObjName = returnNode.callee.object.name; + const calleePropertyName = returnNode.callee.property.name; + const calleeObjNode = variables.find((item) => item && item.name === calleeObjName); + const isCalleeObjArray = calleeObjNode.defs[0].node.init.type === 'ArrayExpression'; + const isMapCall = isCalleeObjArray && calleePropertyName === 'map'; + if (isMapCall) { + const mapCallback = returnNode.arguments[0]; + const mapReturnStatement = mapCallback.body.type === 'BlockStatement' + && astUtil.findReturnStatement(returnNode.arguments[0]); + const mapReturnNode = (mapReturnStatement && mapReturnStatement.argument) || mapCallback.body; + // console.log('DEBUG', mapReturnNode); + return getReturnValue(context, mapReturnNode); + } + } + const calleeName = returnNode.callee.name; + const calleeNode = variables.find((item) => item && item.name === calleeName); + const calleeDefinitionNode = calleeNode && calleeNode.defs && calleeNode.defs[0] && calleeNode.defs[0].node; + const calleeReturnStatement = astUtil.findReturnStatement(calleeDefinitionNode); + const calleeReturnNode = (calleeReturnStatement && calleeReturnStatement.argument) + || (calleeDefinitionNode.init && calleeDefinitionNode.init.body); + return getReturnValue(context, calleeReturnNode); + } + case 'ArrayExpression': { + return returnNode.elements; + } + case 'JSXElement': { + return returnNode; + } + default: + return returnNode.value; + } +} + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + docs: { + description: 'Disallow returning `undefined` from react components', + category: 'Best Practices', + recommended: false, + url: docsUrl('no-render-return-undefined'), + }, + messages, + schema: [], + }, + + create(context) { + const isReturningUndefined = (returnStatement) => { + const returnNode = returnStatement && returnStatement.argument; + const returnIdentifierName = returnNode && returnNode.name; + + const returnIdentifierValue = getReturnValue(context, returnNode); + + const returnsArrayHavingUndefined = Array.isArray(returnIdentifierValue) + && returnIdentifierValue.some((el) => el && el.type === 'Identifier' && el.name === 'undefined'); + + return !returnStatement + || returnIdentifierName === 'undefined' + || typeof returnIdentifierValue === 'undefined' + || (returnIdentifierValue && returnIdentifierValue.name === 'undefined') + || returnsArrayHavingUndefined; + }; + + const handleFunctionalComponents = (node) => { + const fnName = (node.id && node.id.name) || (node.parent.id && node.parent.id.name); + + // Considering functions starting with Uppercase letters are react components + const isReactComponent = isFirstLetterCapitalized(fnName); + const returnStatement = astUtil.findReturnStatement(node); + + if (!isReactComponent) return; + + if (isReturningUndefined(returnStatement)) { + report(context, messages.returnsUndefined, 'returnsUndefined', { + node, + }); + } + }; + + const handleClassComponents = (node) => { + const componentProperties = astUtil.getComponentProperties(node); + const renderFnNode = componentProperties.find((prop) => prop.key.name === 'render'); + const returnStatement = astUtil.findReturnStatement(renderFnNode); + + if (isReturningUndefined(returnStatement)) { + report(context, messages.returnsUndefined, 'returnsUndefined', { + node, + }); + } + }; + + return { + FunctionDeclaration: handleFunctionalComponents, + ArrowFunctionExpression: handleFunctionalComponents, + ClassDeclaration: handleClassComponents, + ClassExpression: handleClassComponents, + }; + }, +}; diff --git a/lib/util/eslint.js b/lib/util/eslint.js index 79a0537f3b..d5d4813c2a 100644 --- a/lib/util/eslint.js +++ b/lib/util/eslint.js @@ -11,7 +11,7 @@ function getAncestors(context, node) { function getScope(context, node) { const sourceCode = getSourceCode(context); - if (sourceCode.getScope) { + if (sourceCode.getScope && node) { return sourceCode.getScope(node); } diff --git a/tests/lib/rules/no-render-return-undefined.js b/tests/lib/rules/no-render-return-undefined.js new file mode 100644 index 0000000000..0ed62a55ba --- /dev/null +++ b/tests/lib/rules/no-render-return-undefined.js @@ -0,0 +1,928 @@ +/** + * @fileoverview Tests for no-danger + * @author Akul Srivastava + */ + +'use strict'; + +// ----------------------------------------------------------------------------- +// Requirements +// ----------------------------------------------------------------------------- + +const RuleTester = require('eslint').RuleTester; +const rule = require('../../../lib/rules/no-render-return-undefined'); + +const parsers = require('../../helpers/parsers'); + +const parserOptions = { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, +}; + +// ----------------------------------------------------------------------------- +// Tests +// ----------------------------------------------------------------------------- + +const ruleTester = new RuleTester({ parserOptions }); +ruleTester.run('no-render-return-undefined', rule, { + // Valid Cases + valid: parsers.all([ + // NonComponents + { + code: ` + function getUser() { + return undefined; + } + `, + }, + { + code: 'function doSomething() {}', + }, + + // Class Components + { + code: ` + class App { + render() { + return 1; + } + } + `, + }, + { + code: ` + const App = class { + render() { + return 1; + } + } + `, + }, + { + code: ` + class App extends React.Component { + render() { + return 1; + } + } + `, + }, + { + code: ` + class App { + render() { + return "Hello World"; + } + } + `, + }, + { + code: ` + const App = class { + render() { + return "Hello World"; + } + } + `, + }, + { + code: ` + class App extends React.Component { + render() { + return "Hello World"; + } + } + `, + }, + { + code: ` + class App { + render() { + return
; + } + } + `, + }, + { + code: ` + const App = class { + render() { + return
; + } + } + `, + }, + { + code: ` + class App extends React.Component { + render() { + return
; + } + } + `, + }, + { + code: ` + class App { + render() { + return [1, "Hello",
]; + } + } + `, + }, + { + code: ` + class App extends React.Component { + render() { + return [1, "Hello",
]; + } + } + `, + }, + { + code: ` + class App { + render() { + return null; + } + } + `, + }, + { + code: ` + class App extends React.Component { + render() { + return null; + } + } + `, + }, + + // Functional Components + { + code: ` + function App() { + return 123; + } + `, + }, + { + code: ` + const App = () => { + return 123; + } + `, + }, + { + code: ` + function App() { + return 'Hello World'; + } + `, + }, + { + code: ` + function App() { + return null; + } + `, + }, + { + code: ` + function App() { + return []; + } + `, + }, + { + code: ` + function App() { + return
; + } + `, + }, + { + code: ` + function App() { + return
; + } + `, + }, + { + code: ` + function App() { + return
Hello World
; + } + `, + }, + { + code: ` + function App() { + return ; + } + `, + }, + { + code: ` + function App() { + return ; + } + `, + }, + { + code: ` + function App() { + return ( + + + + ); + } + `, + }, + { + code: ` + const ui = ; + function App() { + return ui; + } + `, + }, + { + code: ` + function App() { + return [
, ]; + } + `, + }, + { + code: ` + let ui = ; + function App() { + return ui; + } + `, + }, + { + code: ` + function getUI() { + if(condition) return

Hello

; + return null; + } + function App() { + return getUI(); + } + `, + }, + { + code: ` + function App() { + function getUI() { + return 1; + } + return getUI(); + } + `, + }, + { + code: ` + function getFoo() { + return 1; + } + + function App() { + function getUI() { + return getFoo(); + } + return getUI(); + } + `, + }, + { + code: ` + const getFoo = () => 1; + + function App() { + function getUI() { + return getFoo(); + } + return getUI(); + } + `, + }, + { + code: ` + function getFoo() { + return 1; + }; + + function App() { + function getUI() { + return getFoo(); + } + return getUI(); + } + `, + }, + { + code: ` + function getA() { + return ; + }; + function getB() { + return ; + }; + + function App() { + function getUI() { + return condition ? getA() : getB(); + } + return getUI(); + } + `, + }, + { + code: ` + const getA = () => ; + const getB = () => ; + + function App() { + function getUI() { + return condition ? getA() : getB(); + } + return getUI(); + } + `, + }, + { + code: ` + const getNum = () => 123; + const getString = () => "ABC"; + + function App() { + function getUI() { + return condition ? getNum() : getString(); + } + return getUI(); + } + `, + }, + { + code: ` + const getA = () => null; + const getB = () => [12, "Hello"]; + + function App() { + function getUI() { + return condition ? getA() : getB(); + } + return getUI(); + } + `, + }, + { + code: ` + function App() { + return condition && ; + } + `, + }, + { + code: ` + function getFoo() { + return ; + } + function App() { + return condition && getFoo(); + } + `, + }, + { + code: ` + function App() { + return condition || ; + } + `, + }, + { + code: ` + function getFoo() { + return ; + } + function App() { + return condition || getFoo(); + } + `, + }, + { + code: ` + const data = ['John', 'Ram' , 'Akul']; + function App() { + return data.map(name =>

{name}

); + } + `, + }, + { + code: ` + const data = ['John', 'Ram' , 'Akul']; + function App() { + return data.map(name => { + return

{name}

; + }); + } + `, + }, + { + code: ` + const data = ['John', 'Ram' , 'Akul']; + function App() { + return cond && data.map(name => { + return

{name}

; + }); + } + `, + }, + { + code: ` + const data = ['John', 'Ram' , 'Akul']; + function App() { + return cond ? data.map(name =>

{name}

) : null; + } + `, + }, + ]), + + // Invalid Cases + invalid: parsers.all([ + // Class Components + { + code: ` + class App { + render() { + } + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + class App extends React.Component { + render() { + } + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + const App = class { + render() {} + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + class App { + render() { + return; + } + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + class App extends React.Component { + render() { + return; + } + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + class App { + render() { + return undefined; + } + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + class App extends React.Component { + render() { + return undefined; + } + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + class App { + render() { + return [undefined]; + } + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + class App extends React.Component { + render() { + return [undefined]; + } + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + class App { + render() { + return [1, undefined]; + } + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + class App extends React.Component { + render() { + return [1, undefined]; + } + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + + // Functional Components + { + code: ` + function App() {} + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + const App = () => {} + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + function App() { + return; + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + const App = () => { + return; + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + function App() { + return undefined; + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + const App = () => { + return undefined; + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + function App() { + const toReturn = undefined; + return toReturn; + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + function App() { + var toReturn; + return toReturn; + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + function App() { + let toReturn; + return toReturn; + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + var foo; + function App() { + return foo; + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + let foo; + function App() { + return foo; + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + function App() { + return [undefined]; + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + function App() { + return [undefined, undefined]; + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + function foo() {} + function App() { + return foo(); + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + function App() { + return +
+ } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + function App() { + return + [] + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + function App() { + return + 123 + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + function App() { + return + "abc" + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + function App() { + return;
; + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + let ui; + function App() { + return ui; + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + let ui; + function App() { + if(cond) { + ui =
; + } + return ui; + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + function getUI() { + if(condition) return

Hello

; + } + function App() { + return getUI(); + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + function App() { + function getUI() { + return undefined; + } + return getUI(); + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + function getFoo() { + return undefined; + } + + function App() { + function getUI() { + return getFoo(); + } + return getUI(); + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + const getFoo = () => undefined; + + function App() { + function getUI() { + return getFoo(); + } + return getUI(); + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + function getA() { + return undefined; + }; + function getB() { + return ; + }; + + function App() { + function getUI() { + return condition ? getA() : getB(); + } + return getUI(); + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + const getA = () => undefined; + const getB = () => ; + + function App() { + function getUI() { + return condition ? getA() : getB(); + } + return getUI(); + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + function App() { + return condition && undefined; + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + function App() { + return condition || undefined; + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + function App() { + return condition && [
, undefined]; + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + function App() { + return condition || [
, undefined]; + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + function getFoo() { + return undefined; + } + function App() { + return condition && getFoo(); + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + function getFoo() { + return undefined; + } + function App() { + return condition || getFoo(); + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + const data = ['John', 'Ram' , 'Akul']; + function App() { + return cond ? data.map(name =>

{name}

) : undefined; + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + const data = ['John', 'Ram' , 'Akul']; + function App() { + return data.map(name => { + if(cond) return

{name}

; + }); + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + const data = ['John', 'Ram' , 'Akul']; + function App() { + if(cond) { + return data.map(name =>

{name}

); + } + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + { + code: ` + let x; + const data = ['John', 'Ram' , 'Akul']; + function App() { + return cond ? data.map(name => { + return

{name}

; + }) : x; + } + `, + errors: [{ messageId: 'returnsUndefined' }], + }, + ]), +});