From 3adeaa058c6081c5043e96ba40159728b0e8d4db Mon Sep 17 00:00:00 2001 From: Ari Perkkio Date: Sun, 8 Nov 2020 20:18:46 +0200 Subject: [PATCH] [New] `no-unstable-nested-components`: Prevent creating unstable components inside components --- README.md | 1 + docs/rules/no-unstable-nested-components.md | 142 +++ index.js | 1 + lib/rules/no-unstable-nested-components.js | 442 ++++++++ .../rules/no-unstable-nested-components.js | 966 ++++++++++++++++++ 5 files changed, 1552 insertions(+) create mode 100644 docs/rules/no-unstable-nested-components.md create mode 100644 lib/rules/no-unstable-nested-components.js create mode 100644 tests/lib/rules/no-unstable-nested-components.js diff --git a/README.md b/README.md index 68461e0ca9..00999a30d7 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,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 (fixable) * [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 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..eed703fe73 --- /dev/null +++ b/lib/rules/no-unstable-nested-components.js @@ -0,0 +1,442 @@ +/** + * @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 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 created by Array.map call, e.g. {items.map(() =>
)} + // These should be valid, mark them as render prop + if ( + node + && node.parent + && node.parent.type === 'CallExpression' + && node.parent.callee + && node.parent.callee.type === 'MemberExpression' + && node.parent.callee.property + && node.parent.callee.property.name === 'map' + ) { + 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 starts with render, e.g.
} /> + return ( + jsxExpressionContainer + && jsxExpressionContainer.parent + && jsxExpressionContainer.parent.type === 'JSXAttribute' + && jsxExpressionContainer.parent.name + && jsxExpressionContainer.parent.name.type === 'JSXIdentifier' + && startsWithRender(jsxExpressionContainer.parent.name.name) + ); +} + +/** + * 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); + } + + // Check if we are in Array.map call, e.g.
  • )} /> + if ( + jsxAttribute + && jsxAttribute.value + && jsxAttribute.value.expression + && jsxAttribute.value.expression.callee + && jsxAttribute.value.expression.callee.property + && jsxAttribute.value.expression.callee.property.name === 'map' + ) { + return false; + } + + return utils.isReturningJSXOrNull(node); + } + + /** + * 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))) + + // 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) + ) { + 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..b78bd311e4 --- /dev/null +++ b/tests/lib/rules/no-unstable-nested-components.js @@ -0,0 +1,966 @@ +/** + * @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 ( + ( +
      + {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 ( + null }} /> + ) + } + `, + 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 ; + } + ` + } + ], + + 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}] + } + ] +});