From 702dd7773b65731ffd9b9a5c14d491825c358bfe 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 --- lib/util/propTypes.js | 58 +++++++++++++++++++-- tests/lib/rules/prop-types.js | 96 +++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 5 deletions(-) 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..b4cd3473b6 100644 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -3411,6 +3411,66 @@ 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 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, { 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' @@ -7139,6 +7199,42 @@ 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'], } )), });