diff --git a/CHANGELOG.md b/CHANGELOG.md index f3318c5f65..cb836c853a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [`jsx-pascal-case`]: support minimatch `ignore` option ([#2906][] @bcherny) * [`jsx-pascal-case`]: support `allowNamespace` option ([#2917][] @kev-y-huang) * [`jsx-newline`]: Add prevent option ([#2935][] @jsphstls) +* [`no-unstable-nested-components`]: Prevent creating unstable components inside components ([#2750][] @AriPerkkio) ### Fixed * [`jsx-no-constructed-context-values`]: avoid a crash with `as X` TS code ([#2894][] @ljharb) @@ -47,6 +48,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel [#2894]: https://github.com/yannickcr/eslint-plugin-react/issues/2894 [#2893]: https://github.com/yannickcr/eslint-plugin-react/pull/2893 [#2862]: https://github.com/yannickcr/eslint-plugin-react/pull/2862 +[#2750]: https://github.com/yannickcr/eslint-plugin-react/pull/2750 ## [7.22.0] - 2020.12.29 @@ -3306,3 +3308,4 @@ If you're still not using React 15 you can keep the old behavior by setting the [`function-component-definition`]: docs/rules/function-component-definition.md [`jsx-newline`]: docs/rules/jsx-newline.md [`jsx-no-constructed-context-values`]: docs/rules/jsx-no-constructed-context-values.md +[`no-unstable-nested-components`]: docs/rules/no-unstable-nested-components.md \ No newline at end of file diff --git a/README.md b/README.md index e276ff5a11..929a8d5aee 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ Enable the rules that you would like to use. | ✔ | | [react/no-unescaped-entities](docs/rules/no-unescaped-entities.md) | Detect unescaped HTML entities, which might represent malformed tags | | ✔ | 🔧 | [react/no-unknown-property](docs/rules/no-unknown-property.md) | Prevent usage of unknown DOM property | | | | [react/no-unsafe](docs/rules/no-unsafe.md) | Prevent usage of unsafe lifecycle methods | +| | | [react/no-unstable-nested-components](docs/rules/no-unstable-nested-components.md) | Prevent creating unstable components inside components | | | | [react/no-unused-prop-types](docs/rules/no-unused-prop-types.md) | Prevent definitions of unused prop types | | | | [react/no-unused-state](docs/rules/no-unused-state.md) | Prevent definition of unused state fields | | | | [react/no-will-update-set-state](docs/rules/no-will-update-set-state.md) | Prevent usage of setState in componentWillUpdate | @@ -179,7 +180,7 @@ Enable the rules that you would like to use. | ✔ | | [react/jsx-key](docs/rules/jsx-key.md) | Report missing `key` props in iterators/collection literals | | | | [react/jsx-max-depth](docs/rules/jsx-max-depth.md) | Validate JSX maximum depth | | | 🔧 | [react/jsx-max-props-per-line](docs/rules/jsx-max-props-per-line.md) | Limit maximum of props on a single line in JSX | -| | 🔧 | [react/jsx-newline](docs/rules/jsx-newline.md) | Require or prevent a new line after jsx elements and expressions | +| | 🔧 | [react/jsx-newline](docs/rules/jsx-newline.md) | Require or prevent a new line after jsx elements and expressions. | | | | [react/jsx-no-bind](docs/rules/jsx-no-bind.md) | Prevents usage of Function.prototype.bind and arrow functions in React component props | | ✔ | | [react/jsx-no-comment-textnodes](docs/rules/jsx-no-comment-textnodes.md) | Comments inside children section of tag should be placed inside braces | | | | [react/jsx-no-constructed-context-values](docs/rules/jsx-no-constructed-context-values.md) | Prevents JSX context provider values from taking values that will cause needless rerenders. | diff --git a/docs/rules/no-unstable-nested-components.md b/docs/rules/no-unstable-nested-components.md new file mode 100644 index 0000000000..507d83d56e --- /dev/null +++ b/docs/rules/no-unstable-nested-components.md @@ -0,0 +1,142 @@ +# Prevent creating unstable components inside components (react/no-unstable-nested-components) + +Creating components inside components without memoization leads to unstable components. The nested component and all its children are recreated during each re-render. Given stateful children of the nested component will lose their state on each re-render. + +React reconcilation performs element type comparison with [reference equality](https://github.com/facebook/react/blob/v16.13.1/packages/react-reconciler/src/ReactChildFiber.js#L407). The reference to the same element changes on each re-render when defining components inside the render block. This leads to complete recreation of the current node and all its children. As a result the virtual DOM has to do extra unnecessary work and [possible bugs are introduced](https://codepen.io/ariperkkio/pen/vYLodLB). + +## Rule Details + +The following patterns are considered warnings: + +```jsx +function Component() { + function UnstableNestedComponent() { + return
; + } + + return ( +
+ +
+ ); +} +``` + +```jsx +function SomeComponent({ footer: Footer }) { + return ( +
+
+
+ ); +} + +function Component() { + return ( +
+
} /> +
+ ); +} +``` + +```jsx +class Component extends React.Component { + render() { + function UnstableNestedComponent() { + return
; + } + + return ( +
+ +
+ ); + } +} +``` + +The following patterns are **not** considered warnings: + +```jsx +function OutsideDefinedComponent(props) { + return
; +} + +function Component() { + return ( +
+ +
+ ); +} +``` + +```jsx +function Component() { + const MemoizedNestedComponent = React.useCallback(() =>
, []); + + return ( +
+ +
+ ); +} +``` + +```jsx +function Component() { + return ( + } /> + ) +} +``` + +By default component creation is allowed inside component props only if prop name starts with `render`. See `allowAsProps` option for disabling this limitation completely. + +```jsx +function SomeComponent(props) { + return
{props.renderFooter()}
; +} + +function Component() { + return ( +
+
} /> +
+ ); +} +``` + +## Rule Options + +```js +... +"react/no-unstable-nested-components": [ + "off" | "warn" | "error", + { "allowAsProps": true | false } +] +... +``` + +You can allow component creation inside component props by setting `allowAsProps` option to true. When using this option make sure you are **calling** the props in the receiving component and not using them as elements. + +The following patterns are **not** considered warnings: + +```jsx +function SomeComponent(props) { + return
{props.footer()}
; +} + +function Component() { + return ( +
+
} /> +
+ ); +} +``` + +## When Not To Use It + +If you are not interested in preventing bugs related to re-creation of the nested components or do not care about optimization of virtual DOM. diff --git a/index.js b/index.js index 8edb1177f1..f5d9872cc0 100644 --- a/index.js +++ b/index.js @@ -76,6 +76,7 @@ const allRules = { 'no-unescaped-entities': require('./lib/rules/no-unescaped-entities'), 'no-unknown-property': require('./lib/rules/no-unknown-property'), 'no-unsafe': require('./lib/rules/no-unsafe'), + 'no-unstable-nested-components': require('./lib/rules/no-unstable-nested-components'), 'no-unused-prop-types': require('./lib/rules/no-unused-prop-types'), 'no-unused-state': require('./lib/rules/no-unused-state'), 'no-will-update-set-state': require('./lib/rules/no-will-update-set-state'), diff --git a/lib/rules/no-unstable-nested-components.js b/lib/rules/no-unstable-nested-components.js new file mode 100644 index 0000000000..56db0e06e4 --- /dev/null +++ b/lib/rules/no-unstable-nested-components.js @@ -0,0 +1,468 @@ +/** + * @fileoverview Prevent creating unstable components inside components + * @author Ari Perkkiö + */ + +'use strict'; + +const Components = require('../util/Components'); +const docsUrl = require('../util/docsUrl'); + +// ------------------------------------------------------------------------------ +// Constants +// ------------------------------------------------------------------------------ + +const ERROR_MESSAGE_WITHOUT_NAME = 'Declare this component outside parent component or memoize it.'; +const COMPONENT_AS_PROPS_INFO = ' If you want to allow component creation in props, set allowAsProps option to true.'; +const HOOK_REGEXP = /^use[A-Z0-9].*$/; + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +/** + * Generate error message with given parent component name + * @param {String} parentName Name of the parent component + * @returns {String} Error message with parent component name + */ +function generateErrorMessageWithParentName(parentName) { + return `Declare this component outside parent component "${parentName}" or memoize it.`; +} + +/** + * Check whether given text starts with `render`. Comparison is case-sensitive. + * @param {String} text Text to validate + * @returns {Boolean} + */ +function startsWithRender(text) { + return (text || '').startsWith('render'); +} + +/** + * Get closest parent matching given matcher + * @param {ASTNode} node The AST node + * @param {Function} matcher Method used to match the parent + * @returns {ASTNode} The matching parent node, if any + */ +function getClosestMatchingParent(node, matcher) { + if (!node || !node.parent || node.parent.type === 'Program') { + return; + } + + if (matcher(node.parent)) { + return node.parent; + } + + return getClosestMatchingParent(node.parent, matcher); +} + +/** + * Matcher used to check whether given node is a `createElement` call + * @param {ASTNode} node The AST node + * @returns {Boolean} True if node is a `createElement` call, false if not + */ +function isCreateElementMatcher(node) { + return ( + node + && node.type === 'CallExpression' + && node.callee + && node.callee.property + && node.callee.property.name === 'createElement' + ); +} + +/** + * Matcher used to check whether given node is a `ObjectExpression` + * @param {ASTNode} node The AST node + * @returns {Boolean} True if node is a `ObjectExpression`, false if not + */ +function isObjectExpressionMatcher(node) { + return node && node.type === 'ObjectExpression'; +} + +/** + * Matcher used to check whether given node is a `JSXExpressionContainer` + * @param {ASTNode} node The AST node + * @returns {Boolean} True if node is a `JSXExpressionContainer`, false if not + */ +function isJSXExpressionContainerMatcher(node) { + return node && node.type === 'JSXExpressionContainer'; +} + +/** + * Matcher used to check whether given node is a `JSXAttribute` of `JSXExpressionContainer` + * @param {ASTNode} node The AST node + * @returns {Boolean} True if node is a `JSXAttribute` of `JSXExpressionContainer`, false if not + */ +function isJSXAttributeOfExpressionContainerMatcher(node) { + return ( + node + && node.type === 'JSXAttribute' + && node.value + && node.value.type === 'JSXExpressionContainer' + ); +} + +/** + * Matcher used to check whether given node is a `CallExpression` + * @param {ASTNode} node The AST node + * @returns {Boolean} True if node is a `CallExpression`, false if not + */ +function isCallExpressionMatcher(node) { + return node && node.type === 'CallExpression'; +} + +/** + * Check whether given node or its parent is directly inside `map` call + * ```jsx + * {items.map(item =>
  • )} + * ``` + * @param {ASTNode} node The AST node + * @returns {Boolean} True if node is directly inside `map` call, false if not + */ +function isMapCall(node) { + return ( + node + && node.callee + && node.callee.property + && node.callee.property.name === 'map' + ); +} + +/** + * Check whether given node is `ReturnStatement` of a React hook + * @param {ASTNode} node The AST node + * @returns {Boolean} True if node is a `ReturnStatement` of a React hook, false if not + */ +function isReturnStatementOfHook(node) { + if ( + !node + || !node.parent + || node.parent.type !== 'ReturnStatement' + ) { + return false; + } + + const callExpression = getClosestMatchingParent(node, isCallExpressionMatcher); + return ( + callExpression + && callExpression.callee + && HOOK_REGEXP.test(callExpression.callee.name) + ); +} + +/** + * Check whether given node is declared inside a render prop + * ```jsx + *
    } /> + * {() =>
    } + * ``` + * @param {ASTNode} node The AST node + * @returns {Boolean} True if component is declared inside a render prop, false if not + */ +function isComponentInRenderProp(node) { + if ( + node + && node.parent + && node.parent.type === 'Property' + && node.parent.key + && startsWithRender(node.parent.key.name) + ) { + return true; + } + + // Check whether component is a render prop used as direct children, e.g. {() =>
    } + if ( + node + && node.parent + && node.parent.type === 'JSXExpressionContainer' + && node.parent.parent + && node.parent.parent.type === 'JSXElement' + ) { + return true; + } + + const jsxExpressionContainer = getClosestMatchingParent(node, isJSXExpressionContainerMatcher); + + // Check whether prop name indicates accepted patterns + if ( + jsxExpressionContainer + && jsxExpressionContainer.parent + && jsxExpressionContainer.parent.type === 'JSXAttribute' + && jsxExpressionContainer.parent.name + && jsxExpressionContainer.parent.name.type === 'JSXIdentifier' + ) { + const propName = jsxExpressionContainer.parent.name.name; + + // Starts with render, e.g.
    } /> + if (startsWithRender(propName)) { + return true; + } + + // Uses children prop explicitly, e.g.
    } /> + if (propName === 'children') { + return true; + } + } + + return false; +} + +/** + * Check whether given node is declared directly inside a render property + * ```jsx + * const rows = { render: () =>
    } + *
    }] } /> + * ``` + * @param {ASTNode} node The AST node + * @returns {Boolean} True if component is declared inside a render property, false if not + */ +function isDirectValueOfRenderProperty(node) { + return ( + node + && node.parent + && node.parent.type === 'Property' + && node.parent.key + && node.parent.key.type === 'Identifier' + && startsWithRender(node.parent.key.name) + ); +} + +/** + * Resolve the component name of given node + * @param {ASTNode} node The AST node of the component + * @returns {String} Name of the component, if any + */ +function resolveComponentName(node) { + const parentName = node.id && node.id.name; + if (parentName) return parentName; + + return ( + node.type === 'ArrowFunctionExpression' + && node.parent + && node.parent.id + && node.parent.id.name + ); +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: 'Prevent creating unstable components inside components', + category: 'Possible Errors', + recommended: false, + url: docsUrl('no-unstable-nested-components') + }, + schema: [{ + type: 'object', + properties: { + customValidators: { + type: 'array', + items: { + type: 'string' + } + }, + allowAsProps: { + type: 'boolean' + } + }, + additionalProperties: false + }] + }, + + create: Components.detect((context, components, utils) => { + const allowAsProps = context.options.some((option) => option && option.allowAsProps); + + /** + * Check whether given node is declared inside class component's render block + * ```jsx + * class Component extends React.Component { + * render() { + * class NestedClassComponent extends React.Component { + * ... + * ``` + * @param {ASTNode} node The AST node being checked + * @returns {Boolean} True if node is inside class component's render block, false if not + */ + function isInsideRenderMethod(node) { + const parentComponent = utils.getParentComponent(); + + if (!parentComponent || parentComponent.type !== 'ClassDeclaration') { + return false; + } + + return ( + node + && node.parent + && node.parent.type === 'MethodDefinition' + && node.parent.key + && node.parent.key.name === 'render' + ); + } + + /** + * Check whether given node is a function component declared inside class component. + * Util's component detection fails to detect function components inside class components. + * ```jsx + * class Component extends React.Component { + * render() { + * const NestedComponent = () =>
    ; + * ... + * ``` + * @param {ASTNode} node The AST node being checked + * @returns {Boolean} True if given node a function component declared inside class component, false if not + */ + function isFunctionComponentInsideClassComponent(node) { + const parentComponent = utils.getParentComponent(); + const parentStatelessComponent = utils.getParentStatelessComponent(); + + return ( + parentComponent + && parentStatelessComponent + && parentComponent.type === 'ClassDeclaration' + && utils.getStatelessComponent(parentStatelessComponent) + && utils.isReturningJSX(node) + ); + } + + /** + * Check whether given node is declared inside `createElement` call's props + * ```js + * React.createElement(Component, { + * footer: () => React.createElement("div", null) + * }) + * ``` + * @param {ASTNode} node The AST node + * @returns {Boolean} True if node is declare inside `createElement` call's props, false if not + */ + function isComponentInsideCreateElementsProp(node) { + if (!components.get(node)) { + return false; + } + + const createElementParent = getClosestMatchingParent(node, isCreateElementMatcher); + + return ( + createElementParent + && createElementParent.arguments + && createElementParent.arguments[1] === getClosestMatchingParent(node, isObjectExpressionMatcher) + ); + } + + /** + * Check whether given node is declared inside a component prop. + * ```jsx + *
    } /> + * ``` + * @param {ASTNode} node The AST node being checked + * @returns {Boolean} True if node is a component declared inside prop, false if not + */ + function isComponentInProp(node) { + const jsxAttribute = getClosestMatchingParent(node, isJSXAttributeOfExpressionContainerMatcher); + + if (!jsxAttribute) { + return isComponentInsideCreateElementsProp(node); + } + + return utils.isReturningJSX(node); + } + + /** + * Check whether given node is a stateless component returning non-JSX + * ```jsx + * {{ a: () => null }} + * ``` + * @param {ASTNode} node The AST node being checked + * @returns {Boolean} True if node is a stateless component returning non-JSX, false if not + */ + function isStatelessComponentReturningNull(node) { + const component = utils.getStatelessComponent(node); + + return component && !utils.isReturningJSX(component); + } + + /** + * Check whether given node is a unstable nested component + * @param {ASTNode} node The AST node being checked + */ + function validate(node) { + if (!node || !node.parent) { + return; + } + + const isDeclaredInsideProps = isComponentInProp(node); + + if ( + !components.get(node) + && !isFunctionComponentInsideClassComponent(node) + && !isDeclaredInsideProps) { + return; + } + + if ( + // Support allowAsProps option + (isDeclaredInsideProps && (allowAsProps || isComponentInRenderProp(node))) + + // Prevent reporting components created inside Array.map calls + || isMapCall(node) + || isMapCall(node.parent) + + // Do not mark components declared inside hooks (or falsly '() => null' clean-up methods) + || isReturnStatementOfHook(node) + + // Do not mark objects containing render methods + || isDirectValueOfRenderProperty(node) + + // Prevent reporting nested class components twice + || isInsideRenderMethod(node) + + // Prevent falsely reporting deteceted "components" which do not return JSX + || isStatelessComponentReturningNull(node) + ) { + return; + } + + // Get the closest parent component + const parentComponent = getClosestMatchingParent( + node, + (nodeToMatch) => components.get(nodeToMatch) + ); + + if (parentComponent) { + const parentName = resolveComponentName(parentComponent); + + // Exclude lowercase parents, e.g. function createTestComponent() + // React-dom prevents creating lowercase components + if (parentName && parentName[0] === parentName[0].toLowerCase()) { + return; + } + + let message = parentName + ? generateErrorMessageWithParentName(parentName) + : ERROR_MESSAGE_WITHOUT_NAME; + + // Add information about allowAsProps option when component is declared inside prop + if (isDeclaredInsideProps && !allowAsProps) { + message += COMPONENT_AS_PROPS_INFO; + } + + context.report({node, message}); + } + } + + // -------------------------------------------------------------------------- + // Public + // -------------------------------------------------------------------------- + + return { + FunctionDeclaration(node) { validate(node); }, + ArrowFunctionExpression(node) { validate(node); }, + FunctionExpression(node) { validate(node); }, + ClassDeclaration(node) { validate(node); } + }; + }) +}; diff --git a/tests/lib/rules/no-unstable-nested-components.js b/tests/lib/rules/no-unstable-nested-components.js new file mode 100644 index 0000000000..d6df486d86 --- /dev/null +++ b/tests/lib/rules/no-unstable-nested-components.js @@ -0,0 +1,1051 @@ +/** + * @fileoverview Prevent creating unstable components inside components + * @author Ari Perkkiö + */ + +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester; +const rule = require('../../../lib/rules/no-unstable-nested-components'); + +const parserOptions = { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + jsx: true + } +}; + +const ERROR_MESSAGE = 'Declare this component outside parent component "ParentComponent" or memoize it.'; +const ERROR_MESSAGE_WITHOUT_NAME = 'Declare this component outside parent component or memoize it.'; +const ERROR_MESSAGE_COMPONENT_AS_PROPS = `${ERROR_MESSAGE} If you want to allow component creation in props, set allowAsProps option to true.`; + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({parserOptions}); +ruleTester.run('no-unstable-nested-components', rule, { + valid: [ + { + code: ` + function ParentComponent() { + return ( +
    + +
    + ); + } + ` + }, + { + code: ` + function ParentComponent() { + return React.createElement( + "div", + null, + React.createElement(OutsideDefinedFunctionComponent, null) + ); + } + ` + }, + { + code: ` + function ParentComponent() { + return ( + } + header={
    } + /> + ); + } + ` + }, + { + code: ` + function ParentComponent() { + return React.createElement(SomeComponent, { + footer: React.createElement(OutsideDefinedComponent, null), + header: React.createElement("div", null) + }); + } + ` + }, + { + code: ` + function ParentComponent() { + const MemoizedNestedComponent = React.useCallback(() =>
    , []); + + return ( +
    + +
    + ); + } + ` + }, + { + code: ` + function ParentComponent() { + const MemoizedNestedComponent = React.useCallback( + () => React.createElement("div", null), + [] + ); + + return React.createElement( + "div", + null, + React.createElement(MemoizedNestedComponent, null) + ); + } + ` + }, + { + code: ` + function ParentComponent() { + const MemoizedNestedFunctionComponent = React.useCallback( + function () { + return
    ; + }, + [] + ); + + return ( +
    + +
    + ); + } + ` + }, + { + code: ` + function ParentComponent() { + const MemoizedNestedFunctionComponent = React.useCallback( + function () { + return React.createElement("div", null); + }, + [] + ); + + return React.createElement( + "div", + null, + React.createElement(MemoizedNestedFunctionComponent, null) + ); + } + ` + }, + { + code: ` + function ParentComponent(props) { + // Should not interfere handler declarations + function onClick(event) { + props.onClick(event.target.value); + } + + const onKeyPress = () => null; + + function getOnHover() { + return function onHover(event) { + props.onHover(event.target); + } + } + + return ( +
    +
    + ); + } + ` + }, + { + code: ` + function ParentComponent() { + function getComponent() { + return
    ; + } + + return ( +
    + {getComponent()} +
    + ); + } + ` + }, + { + code: ` + function ParentComponent() { + function getComponent() { + return React.createElement("div", null); + } + + return React.createElement("div", null, getComponent()); + } + ` + }, + { + code: ` + function ParentComponent() { + return ( + + {() =>
    } + + ); + } + ` + }, + { + code: ` + function ParentComponent() { + return ( +
    } /> + ); + } + ` + }, + { + code: ` + function ParentComponent() { + return ( + ( +
      + {items[index].map((item) => +
    • + {item} +
    • + )} +
    + )) + } + /> + ); + } + ` + }, + { + code: ` + function ParentComponent() { + return React.createElement( + RenderPropComponent, + null, + () => React.createElement("div", null) + ); + } + ` + }, + { + code: ` + function ParentComponent(props) { + return ( +
      + {props.items.map(item => ( +
    • + {item.name} +
    • + ))} +
    + ); + } + ` + }, + { + code: ` + function ParentComponent(props) { + return ( + { + return ( +
  • + {item.name} +
  • + ); + })} + /> + ); + } + ` + }, + { + code: ` + function ParentComponent(props) { + return React.createElement( + "ul", + null, + props.items.map(() => + React.createElement( + "li", + { key: item.id }, + item.name + ) + ) + ) + } + ` + }, + { + code: ` + function ParentComponent(props) { + return ( +
      + {props.items.map(function Item(item) { + return ( +
    • + {item.name} +
    • + ); + })} +
    + ); + } + ` + }, + { + code: ` + function ParentComponent(props) { + return React.createElement( + "ul", + null, + props.items.map(function Item() { + return React.createElement( + "li", + { key: item.id }, + item.name + ); + }) + ); + } + ` + }, + { + code: ` + function createTestComponent(props) { + return ( +
    + ); + } + ` + }, + { + code: ` + function createTestComponent(props) { + return React.createElement("div", null); + } + ` + }, + { + code: ` + function ParentComponent() { + return ( +
    } /> + ); + } + `, + options: [{ + allowAsProps: true + }] + }, + { + code: ` + function ParentComponent() { + return React.createElement(ComponentWithProps, { + footer: () => React.createElement("div", null) + }); + } + `, + options: [{ + allowAsProps: true + }] + }, + { + code: ` + function ParentComponent() { + return ( +
    }} /> + ) + } + `, + options: [{ + allowAsProps: true + }] + }, + { + code: ` + function ParentComponent() { + return ( +
    } /> + ); + } + ` + }, + { + code: ` + function ParentComponent() { + return React.createElement(ComponentForProps, { + renderFooter: () => React.createElement("div", null) + }); + } + ` + }, + { + code: ` + function ParentComponent() { + useEffect(() => { + return () => null; + }); + + return
    ; + } + ` + }, + { + code: ` + function ParentComponent() { + return ( +
    }} /> + ) + } + ` + }, + { + code: ` + function ParentComponent() { + return ( + ( + + {items.map(item => ( +
  • {item}
  • + ))} +
    + )} /> + ) + } + ` + }, + { + code: ` + const ParentComponent = () => ( + + {list.map(item => ( +
  • {item}
  • + ))} + , + ]} + /> + ); + ` + }, + { + code: ` + function ParentComponent() { + const rows = [ + { + name: 'A', + render: (props) => + }, + ]; + + return ; + } + ` + }, + { + code: ` + function ParentComponent() { + return null }} />; + } + ` + }, + { + code: ` + const ParentComponent = createReactClass({ + displayName: "ParentComponent", + statics: { + getSnapshotBeforeUpdate: function () { + return null; + }, + }, + render() { + return
    ; + }, + }); + ` + } + /* TODO These minor cases are currently falsely marked due to component detection + { + code: ` + function ParentComponent() { + const _renderHeader = () =>
    ; + return
    {_renderHeader()}
    ; + } + ` + }, + { + // https://github.com/emotion-js/emotion/blob/a89d4257b0246a1640a99f77838e5edad4ec4386/packages/jest/test/react-enzyme.test.js#L26-L28 + code: ` + const testCases = { + basic: { + render() { + const Component = () =>
    ; + return
    ; + } + } + } + ` + }, + */ + ], + + invalid: [ + { + code: ` + function ParentComponent() { + function UnstableNestedFunctionComponent() { + return
    ; + } + + return ( +
    + +
    + ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + function ParentComponent() { + function UnstableNestedFunctionComponent() { + return React.createElement("div", null); + } + + return React.createElement( + "div", + null, + React.createElement(UnstableNestedFunctionComponent, null) + ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + function ParentComponent() { + const UnstableNestedVariableComponent = () => { + return
    ; + } + + return ( +
    + +
    + ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + function ParentComponent() { + const UnstableNestedVariableComponent = () => { + return React.createElement("div", null); + } + + return React.createElement( + "div", + null, + React.createElement(UnstableNestedVariableComponent, null) + ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + const ParentComponent = () => { + function UnstableNestedFunctionComponent() { + return
    ; + } + + return ( +
    + +
    + ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + const ParentComponent = () => { + function UnstableNestedFunctionComponent() { + return React.createElement("div", null); + } + + return React.createElement( + "div", + null, + React.createElement(UnstableNestedFunctionComponent, null) + ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + export default () => { + function UnstableNestedFunctionComponent() { + return
    ; + } + + return ( +
    + +
    + ); + } + `, + errors: [{message: ERROR_MESSAGE_WITHOUT_NAME}] + }, + { + code: ` + export default () => { + function UnstableNestedFunctionComponent() { + return React.createElement("div", null); + } + + return React.createElement( + "div", + null, + React.createElement(UnstableNestedFunctionComponent, null) + ); + }; + `, + errors: [{message: ERROR_MESSAGE_WITHOUT_NAME}] + }, + { + code: ` + const ParentComponent = () => { + const UnstableNestedVariableComponent = () => { + return
    ; + } + + return ( +
    + +
    + ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + const ParentComponent = () => { + const UnstableNestedVariableComponent = () => { + return React.createElement("div", null); + } + + return React.createElement( + "div", + null, + React.createElement(UnstableNestedVariableComponent, null) + ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + function ParentComponent() { + class UnstableNestedClassComponent extends React.Component { + render() { + return
    ; + } + }; + + return ( +
    + +
    + ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + function ParentComponent() { + class UnstableNestedClassComponent extends React.Component { + render() { + return React.createElement("div", null); + } + } + + return React.createElement( + "div", + null, + React.createElement(UnstableNestedClassComponent, null) + ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + class ParentComponent extends React.Component { + render() { + class UnstableNestedClassComponent extends React.Component { + render() { + return
    ; + } + }; + + return ( +
    + +
    + ); + } + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + class ParentComponent extends React.Component { + render() { + class UnstableNestedClassComponent extends React.Component { + render() { + return React.createElement("div", null); + } + } + + return React.createElement( + "div", + null, + React.createElement(UnstableNestedClassComponent, null) + ); + } + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + class ParentComponent extends React.Component { + render() { + function UnstableNestedFunctionComponent() { + return
    ; + } + + return ( +
    + +
    + ); + } + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + class ParentComponent extends React.Component { + render() { + function UnstableNestedClassComponent() { + return React.createElement("div", null); + } + + return React.createElement( + "div", + null, + React.createElement(UnstableNestedClassComponent, null) + ); + } + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + class ParentComponent extends React.Component { + render() { + const UnstableNestedVariableComponent = () => { + return
    ; + } + + return ( +
    + +
    + ); + } + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + class ParentComponent extends React.Component { + render() { + const UnstableNestedClassComponent = () => { + return React.createElement("div", null); + } + + return React.createElement( + "div", + null, + React.createElement(UnstableNestedClassComponent, null) + ); + } + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + function ParentComponent() { + function getComponent() { + function NestedUnstableFunctionComponent() { + return
    ; + }; + + return ; + } + + return ( +
    + {getComponent()} +
    + ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + function ParentComponent() { + function getComponent() { + function NestedUnstableFunctionComponent() { + return React.createElement("div", null); + } + + return React.createElement(NestedUnstableFunctionComponent, null); + } + + return React.createElement("div", null, getComponent()); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + function ComponentWithProps(props) { + return
    ; + } + + function ParentComponent() { + return ( + ; + } + } /> + ); + } + `, + errors: [{message: ERROR_MESSAGE_COMPONENT_AS_PROPS}] + }, + { + code: ` + function ComponentWithProps(props) { + return React.createElement("div", null); + } + + function ParentComponent() { + return React.createElement(ComponentWithProps, { + footer: function SomeFooter() { + return React.createElement("div", null); + } + }); + } + `, + errors: [{message: ERROR_MESSAGE_COMPONENT_AS_PROPS}] + }, + { + code: ` + function ComponentWithProps(props) { + return
    ; + } + + function ParentComponent() { + return ( +
    } /> + ); + } + `, + errors: [{message: ERROR_MESSAGE_COMPONENT_AS_PROPS}] + }, + { + code: ` + function ComponentWithProps(props) { + return React.createElement("div", null); + } + + function ParentComponent() { + return React.createElement(ComponentWithProps, { + footer: () => React.createElement("div", null) + }); + } + `, + errors: [{message: ERROR_MESSAGE_COMPONENT_AS_PROPS}] + }, + { + code: ` + function ParentComponent() { + return ( + + {() => { + function UnstableNestedComponent() { + return
    ; + } + + return ( +
    + +
    + ); + }} + + ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + function RenderPropComponent(props) { + return props.render({}); + } + + function ParentComponent() { + return React.createElement( + RenderPropComponent, + null, + () => { + function UnstableNestedComponent() { + return React.createElement("div", null); + } + + return React.createElement( + "div", + null, + React.createElement(UnstableNestedComponent, null) + ); + } + ); + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + function ComponentForProps(props) { + return
    ; + } + + function ParentComponent() { + return ( +
    } /> + ); + } + `, + errors: [{message: ERROR_MESSAGE_COMPONENT_AS_PROPS}] + }, + { + code: ` + function ComponentForProps(props) { + return React.createElement("div", null); + } + + function ParentComponent() { + return React.createElement(ComponentForProps, { + notPrefixedWithRender: () => React.createElement("div", null) + }); + } + `, + errors: [{message: ERROR_MESSAGE_COMPONENT_AS_PROPS}] + }, + { + code: ` + function ParentComponent() { + return ( +
    }} /> + ); + } + `, + errors: [{message: ERROR_MESSAGE_COMPONENT_AS_PROPS}] + }, + { + code: ` + function ParentComponent() { + const rows = [ + { + name: 'A', + notPrefixedWithRender: (props) => + }, + ]; + + return
    ; + } + `, + errors: [{message: ERROR_MESSAGE}] + }, + { + code: ` + class ParentComponent extends React.Component { + render() { + const List = (props) => { + const items = props.items + .map((item) => ( +
  • + {item.name} +
  • + )); + + return
      {items}
    ; + }; + + return ; + } + } + `, + // Only a single error should be shown. This can get easily marked twice. + errors: [{message: ERROR_MESSAGE}] + } + ] +});