diff --git a/CHANGELOG.md b/CHANGELOG.md index 1731915d1b..3ca8a10642 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,9 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [`no-unused-prop-types`], `usedPropTypes`: avoid crash with typescript-eslint parser (@ljharb) * [`display-name`]: unwrap TS `as` expressions ([#3110][] @ljharb) * [`destructuring-assignment`]: detect refs nested in functions ([#3102] @ljharb) +* [`no-unstable-components`]: improve handling of objects containing render function properties ([#3111] @fizwidget) +[#3111]: https://github.com/yannickcr/eslint-plugin-react/pull/3111 [#3110]: https://github.com/yannickcr/eslint-plugin-react/pull/3110 [#3102]: https://github.com/yannickcr/eslint-plugin-react/issue/3102 [#3092]: https://github.com/yannickcr/eslint-plugin-react/pull/3092 diff --git a/lib/rules/no-unstable-nested-components.js b/lib/rules/no-unstable-nested-components.js index 306f2b9a9e..ca4ddff560 100644 --- a/lib/rules/no-unstable-nested-components.js +++ b/lib/rules/no-unstable-nested-components.js @@ -105,6 +105,19 @@ function isJSXAttributeOfExpressionContainerMatcher(node) { ); } +/** + * Matcher used to check whether given node is an object `Property` + * @param {ASTNode} node The AST node + * @returns {Boolean} True if node is a `Property`, false if not + */ +function isPropertyOfObjectExpressionMatcher(node) { + return ( + node + && node.parent + && node.parent.type === 'Property' + ); +} + /** * Matcher used to check whether given node is a `CallExpression` * @param {ASTNode} node The AST node @@ -358,14 +371,19 @@ module.exports = { } /** - * Check whether given node is declared inside a component prop. + * Check whether given node is declared inside a component/object prop. * ```jsx *
} /> + * { footer: () =>
} * ``` * @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) { + if (isPropertyOfObjectExpressionMatcher(node)) { + return utils.isReturningJSX(node); + } + const jsxAttribute = getClosestMatchingParent(node, context, isJSXAttributeOfExpressionContainerMatcher); if (!jsxAttribute) { diff --git a/tests/lib/rules/no-unstable-nested-components.js b/tests/lib/rules/no-unstable-nested-components.js index 3fdf7f3f38..2c7aea2130 100644 --- a/tests/lib/rules/no-unstable-nested-components.js +++ b/tests/lib/rules/no-unstable-nested-components.js @@ -383,6 +383,78 @@ ruleTester.run('no-unstable-nested-components', rule, { `, options: [{ allowAsProps: true }], }, + { + code: ` + function ParentComponent() { + return ( + + { + thing.match({ + renderLoading: () =>
, + renderSuccess: () =>
, + renderFailure: () =>
, + }) + } + + ) + } + `, + }, + { + code: ` + function ParentComponent() { + const thingElement = thing.match({ + renderLoading: () =>
, + renderSuccess: () =>
, + renderFailure: () =>
, + }); + return ( + + {thingElement} + + ) + } + `, + }, + { + code: ` + function ParentComponent() { + return ( + + { + thing.match({ + loading: () =>
, + success: () =>
, + failure: () =>
, + }) + } + + ) + } + `, + options: [{ + allowAsProps: true, + }], + }, + { + code: ` + function ParentComponent() { + const thingElement = thing.match({ + loading: () =>
, + success: () =>
, + failure: () =>
, + }); + return ( + + {thingElement} + + ) + } + `, + options: [{ + allowAsProps: true, + }], + }, { code: ` function ParentComponent() { @@ -500,6 +572,9 @@ ruleTester.run('no-unstable-nested-components', rule, { return ; } `, + options: [{ + allowAsProps: true, + }], }, /* TODO These minor cases are currently falsely marked due to component detection { @@ -1042,5 +1117,63 @@ ruleTester.run('no-unstable-nested-components', rule, { // Only a single error should be shown. This can get easily marked twice. errors: [{ message: ERROR_MESSAGE }], }, + { + code: ` + function ParentComponent() { + return ( + + { + thing.match({ + loading: () =>
, + success: () =>
, + failure: () =>
, + }) + } + + ) + } + `, + errors: [ + { message: ERROR_MESSAGE_COMPONENT_AS_PROPS }, + { message: ERROR_MESSAGE_COMPONENT_AS_PROPS }, + { message: ERROR_MESSAGE_COMPONENT_AS_PROPS }, + ], + }, + { + code: ` + function ParentComponent() { + const thingElement = thing.match({ + loading: () =>
, + success: () =>
, + failure: () =>
, + }); + return ( + + {thingElement} + + ) + } + `, + errors: [ + { message: ERROR_MESSAGE_COMPONENT_AS_PROPS }, + { message: ERROR_MESSAGE_COMPONENT_AS_PROPS }, + { message: ERROR_MESSAGE_COMPONENT_AS_PROPS }, + ], + }, + { + code: ` + function ParentComponent() { + const rows = [ + { + name: 'A', + notPrefixedWithRender: (props) => + }, + ]; + + return
; + } + `, + errors: [{ message: ERROR_MESSAGE_COMPONENT_AS_PROPS }], + }, ]), });