From 4bc34995dbab597492c5081493d77755fd3501e8 Mon Sep 17 00:00:00 2001 From: vedadeepta Date: Sun, 24 Oct 2021 20:33:13 +0530 Subject: [PATCH] [Fix] `prop-types`, `propTypes`: add forwardRef<>, ForwardRefRenderFunction<> prop-types --- CHANGELOG.md | 4 +- lib/util/propTypes.js | 58 ++++++++++- tests/lib/rules/prop-types.js | 189 ++++++++++++++++++++++++++++++++-- 3 files changed, 238 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ca8a10642..b7708d4ae6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,14 +10,16 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * add [`no-arrow-function-lifecycle`] ([#1980][] @ngtan) ### Fixed -* [`propTypes`]: add `VoidFunctionComponent` to react generic list ([#3092][] @vedadeepta) +* `propTypes`: add `VoidFunctionComponent` to react generic list ([#3092][] @vedadeepta) * [`jsx-fragments`], [`jsx-no-useless-fragment`]: avoid a crash on fragment syntax in `typescript-eslint` parser (@ljharb) * [`jsx-props-no-multi-spaces`]: avoid a crash on long member chains in tag names in `typescript-eslint` parser (@ljharb) * [`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) +* [`prop-types`], `propTypes`: add forwardRef<>, ForwardRefRenderFunction<> prop-types ([#3112] @vedadeepta) +[#3112]: https://github.com/yannickcr/eslint-plugin-react/pull/3112 [#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 diff --git a/lib/util/propTypes.js b/lib/util/propTypes.js index 8cba850cda..a89530c737 100644 --- a/lib/util/propTypes.js +++ b/lib/util/propTypes.js @@ -100,8 +100,20 @@ module.exports = function propTypesInstructions(context, components, utils) { const defaults = { customValidators: [] }; const configuration = Object.assign({}, defaults, context.options[0] || {}); const customValidators = configuration.customValidators; - const allowedGenericTypes = new Set(['VoidFunctionComponent', 'PropsWithChildren', 'SFC', 'StatelessComponent', 'FunctionComponent', 'FC']); + const allowedGenericTypes = new Set(['forwardRef', 'ForwardRefRenderFunction', 'VoidFunctionComponent', 'PropsWithChildren', 'SFC', 'StatelessComponent', 'FunctionComponent', 'FC']); + const genericTypeParamIndexWherePropsArePresent = { + ForwardRefRenderFunction: 1, + forwardRef: 1, + VoidFunctionComponent: 0, + PropsWithChildren: 0, + SFC: 0, + StatelessComponent: 0, + FunctionComponent: 0, + FC: 0, + }; const genericReactTypesImport = new Set(); + // import { FC as X } from 'react' -> localToImportedMap = { x: FC } + const localToImportedMap = {}; /** * Returns the full scope. @@ -521,9 +533,14 @@ module.exports = function propTypesInstructions(context, components, utils) { * @param {ASTNode} node * @return {string | undefined} */ - function getTypeName(node) { + function getLeftMostTypeName(node) { + if (node.name) return node.name; + if (node.left) return getLeftMostTypeName(node.left); + } + + function getRightMostTypeName(node) { if (node.name) return node.name; - if (node.left) return getTypeName(node.left); + if (node.right) return getRightMostTypeName(node.right); } class DeclarePropTypesForTSTypeAnnotation { @@ -579,14 +596,20 @@ module.exports = function propTypesInstructions(context, components, utils) { let typeName; if (astUtil.isTSTypeReference(node)) { typeName = node.typeName.name; - const shouldTraverseTypeParams = genericReactTypesImport.has(getTypeName(node.typeName)); + const leftMostName = getLeftMostTypeName(node.typeName); + const shouldTraverseTypeParams = genericReactTypesImport.has(leftMostName); if (shouldTraverseTypeParams && node.typeParameters && node.typeParameters.length !== 0) { // All react Generic types are derived from: // type PropsWithChildren

= P & { children?: ReactNode | undefined } // So we should construct an optional children prop this.shouldSpecifyOptionalChildrenProps = true; - const nextNode = node.typeParameters.params[0]; + const rightMostName = getRightMostTypeName(node.typeName); + const importedName = localToImportedMap[rightMostName]; + const idx = genericTypeParamIndexWherePropsArePresent[ + leftMostName !== rightMostName ? rightMostName : importedName + ]; + const nextNode = node.typeParameters.params[idx]; this.visitTSNode(nextNode); return; } @@ -941,6 +964,30 @@ module.exports = function propTypesInstructions(context, components, utils) { return; } + if ( + node.parent + && node.parent.callee + && node.parent.typeParameters + && node.parent.typeParameters.params + && ( + node.parent.callee.name === 'forwardRef' || ( + node.parent.callee.object + && node.parent.callee.property + && node.parent.callee.object.name === 'React' + && node.parent.callee.property.name === 'forwardRef' + ) + ) + ) { + const propTypes = node.parent.typeParameters.params[1]; + const declaredPropTypes = {}; + const obj = new DeclarePropTypesForTSTypeAnnotation(propTypes, declaredPropTypes); + components.set(node, { + declaredPropTypes: obj.declaredPropTypes, + ignorePropsValidation: false, + }); + return; + } + const siblingIdentifier = node.parent && node.parent.id; const siblingHasTypeAnnotation = siblingIdentifier && siblingIdentifier.typeAnnotation; const isNodeAnnotated = annotations.isAnnotatedFunctionPropsDeclaration(node, context); @@ -1092,6 +1139,7 @@ module.exports = function propTypesInstructions(context, components, utils) { // handles import { FC } from 'react' or import { FC as X } from 'react' if (specifier.type === 'ImportSpecifier' && allowedGenericTypes.has(specifier.imported.name)) { genericReactTypesImport.add(specifier.local.name); + localToImportedMap[specifier.local.name] = specifier.imported.name; } }); } diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js index afc47d6ca5..0c105eaed7 100644 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -462,9 +462,9 @@ ruleTester.run('prop-types', rule, { code: ` class Hello extends React.Component { render() { - var { + var { propX, - "aria-controls": ariaControls, + "aria-controls": ariaControls, ...props } = this.props; return

Hello
; } @@ -1346,7 +1346,7 @@ ruleTester.run('prop-types', rule, { { code: ` import type { FieldProps } from "redux-form" - + type Props = { label: string, type: string, @@ -3411,6 +3411,111 @@ ruleTester.run('prop-types', rule, { `, features: ['ts', 'no-babel'], }, + { + code: ` + import React, { ForwardRefRenderFunction as X } from 'react' + + type IfooProps = { e: string }; + const Foo: X = function Foo (props, ref) { + const { e } = props; + return
hello
; + }; + `, + features: ['ts', 'no-babel'], + }, + { + code: ` + import React, { ForwardRefRenderFunction } from 'react' + + type IfooProps = { e: string }; + const Foo: ForwardRefRenderFunction = function Foo (props, ref) { + const { e } = props; + return
hello
; + }; + `, + features: ['ts', 'no-babel'], + }, + { + code: ` + import React, { ForwardRefRenderFunction } from 'react' + + type IfooProps = { e: string }; + const Foo: ForwardRefRenderFunction = (props, ref) => { + const { e } = props; + return
hello
; + }; + `, + features: ['ts', 'no-babel'], + }, + { + code: ` + import React from 'react' + + type IfooProps = { e: string }; + const Foo= React.forwardRef((props, ref) => { + const { e } = props; + return
hello
; + }); + `, + features: ['ts', 'no-babel'], + }, + { + code: ` + import React, { forwardRef } from 'react' + + type IfooProps = { e: string }; + const Foo= forwardRef((props, ref) => { + const { e } = props; + return
hello
; + }); + `, + features: ['ts', 'no-babel'], + }, + { + code: ` + import React from 'react' + type IfooProps = { e: string }; + const Foo= React.forwardRef(function Foo(props, ref) { + const { e } = props; + return
hello
; + }); + `, + features: ['ts', 'no-babel'], + }, + { + code: ` + import React from 'react' + interface IfooProps { e: string } + const Foo= React.forwardRef(function Foo(props, ref) { + const { e } = props; + return
hello
; + }); + `, + features: ['ts', 'no-babel'], + }, + { + code: ` + import React, { forwardRef } from 'react' + interface IfooProps { e: string } + const Foo= forwardRef(function Foo(props, ref) { + const { e } = props; + return
hello
; + }); + `, + features: ['ts', 'no-babel'], + }, + { + code: ` + import React, { forwardRef as X } from 'react' + + type IfooProps = { e: string }; + const Foo= X((props, ref) => { + const { e } = props; + return
hello
; + }); + `, + features: ['ts', 'no-babel'], + }, { code: ` import React from 'react' @@ -3430,7 +3535,7 @@ ruleTester.run('prop-types', rule, { static propTypes = { value: PropTypes.string }; - + render() { return {this.props.value}; } @@ -3830,7 +3935,7 @@ ruleTester.run('prop-types', rule, { semver.satisfies(babelEslintVersion, '< 9') ? { code: ` class Hello extends React.Component { - static propTypes: { + static propTypes: { firstname: PropTypes.string }; render() { @@ -4019,8 +4124,8 @@ ruleTester.run('prop-types', rule, { code: ` class Hello extends React.Component { render() { - var { - "aria-controls": ariaControls, + var { + "aria-controls": ariaControls, propX, ...props } = this.props; return
Hello
; @@ -7139,6 +7244,76 @@ ruleTester.run('prop-types', rule, { }, ], features: ['ts', 'no-babel'], + }, + { + code: ` + import React from 'react' + + type IfooProps = { e: string }; + const Foo: React.ForwardRefRenderFunction = function Foo (props, ref) { + const { name } = props; + return
{name}
; + }; + `, + errors: [ + { + messageId: 'missingPropType', + data: { name: 'name' }, + }, + ], + features: ['ts', 'no-babel'], + }, + { + code: ` + import React from 'react' + type IfooProps = { k: string, a: number } + const Foo= React.forwardRef((props, ref) => { + return
{props.l}
; + }); + `, + errors: [ + { + messageId: 'missingPropType', + data: { name: 'l' }, + }, + ], + features: ['ts', 'no-babel'], + }, + { + code: ` + import React from 'react' + + type IfooProps = { e: string }; + const Foo= React.forwardRef(function Foo(props, ref) { + const { l } = props; + return
hello
; + }); + `, + errors: [ + { + messageId: 'missingPropType', + data: { name: 'l' }, + }, + ], + features: ['ts', 'no-babel'], + }, + { + code: ` + import React, { forwardRef } from 'react' + + type IfooProps = { e: string }; + const Foo= forwardRef(function Foo(props, ref) { + const { l } = props; + return
hello
; + }); + `, + errors: [ + { + messageId: 'missingPropType', + data: { name: 'l' }, + }, + ], + features: ['ts', 'no-babel'], } )), });