diff --git a/src/utils/__tests__/isStatelessComponent-test.js b/src/utils/__tests__/isStatelessComponent-test.js index abfcc5ece6d..10de014a609 100644 --- a/src/utils/__tests__/isStatelessComponent-test.js +++ b/src/utils/__tests__/isStatelessComponent-test.js @@ -6,7 +6,7 @@ * */ -import { statement, parse } from '../../../tests/utils'; +import { parse, statement } from '../../../tests/utils'; import isStatelessComponent from '../isStatelessComponent'; describe('isStatelessComponent', () => { @@ -233,6 +233,16 @@ describe('isStatelessComponent', () => { expect(isStatelessComponent(def)).toBe(true); }); + it('handles recursive function calls', () => { + const def = statement(` + function Foo (props) { + return props && Foo(props); + } + `); + + expect(isStatelessComponent(def)).toBe(false); + }); + test( 'handles simple resolves', ` diff --git a/src/utils/isStatelessComponent.js b/src/utils/isStatelessComponent.js index a9d0ca47720..a912c6ed4ba 100644 --- a/src/utils/isStatelessComponent.js +++ b/src/utils/isStatelessComponent.js @@ -33,7 +33,14 @@ function isJSXElementOrReactCall(path) { ); } -function resolvesToJSXElementOrReactCall(path) { +function resolvesToJSXElementOrReactCall(path, seen) { + // avoid returns with recursive function calls + if (seen.has(path)) { + return false; + } + + seen.add(path); + // Is the path is already a JSX element or a call to one of the React.* functions if (isJSXElementOrReactCall(path)) { return true; @@ -45,8 +52,8 @@ function resolvesToJSXElementOrReactCall(path) { // the two possible paths if (resolvedPath.node.type === 'ConditionalExpression') { return ( - resolvesToJSXElementOrReactCall(resolvedPath.get('consequent')) || - resolvesToJSXElementOrReactCall(resolvedPath.get('alternate')) + resolvesToJSXElementOrReactCall(resolvedPath.get('consequent'), seen) || + resolvesToJSXElementOrReactCall(resolvedPath.get('alternate'), seen) ); } @@ -54,8 +61,8 @@ function resolvesToJSXElementOrReactCall(path) { // the two possible paths if (resolvedPath.node.type === 'LogicalExpression') { return ( - resolvesToJSXElementOrReactCall(resolvedPath.get('left')) || - resolvesToJSXElementOrReactCall(resolvedPath.get('right')) + resolvesToJSXElementOrReactCall(resolvedPath.get('left'), seen) || + resolvesToJSXElementOrReactCall(resolvedPath.get('right'), seen) ); } @@ -69,7 +76,7 @@ function resolvesToJSXElementOrReactCall(path) { if (resolvedPath.node.type === 'CallExpression') { let calleeValue = resolveToValue(resolvedPath.get('callee')); - if (returnsJSXElementOrReactCall(calleeValue)) { + if (returnsJSXElementOrReactCall(calleeValue, seen)) { return true; } @@ -110,7 +117,7 @@ function resolvesToJSXElementOrReactCall(path) { if ( !resolvedMemberExpression || - returnsJSXElementOrReactCall(resolvedMemberExpression) + returnsJSXElementOrReactCall(resolvedMemberExpression, seen) ) { return true; } @@ -120,14 +127,14 @@ function resolvesToJSXElementOrReactCall(path) { return false; } -function returnsJSXElementOrReactCall(path) { +function returnsJSXElementOrReactCall(path, seen = new WeakSet()) { let visited = false; // early exit for ArrowFunctionExpressions if ( path.node.type === 'ArrowFunctionExpression' && path.get('body').node.type !== 'BlockStatement' && - resolvesToJSXElementOrReactCall(path.get('body')) + resolvesToJSXElementOrReactCall(path.get('body'), seen) ) { return true; } @@ -143,7 +150,7 @@ function returnsJSXElementOrReactCall(path) { // Only check return statements which are part of the checked function scope if (returnPath.scope !== scope) return false; - if (resolvesToJSXElementOrReactCall(returnPath.get('argument'))) { + if (resolvesToJSXElementOrReactCall(returnPath.get('argument'), seen)) { visited = true; return false; }