Skip to content

Commit ac47d4b

Browse files
committed
[Fix] prop-types, propTypes: add forwardRef<>, ForwardRefRenderFunction<> prop-types
1 parent 182e95d commit ac47d4b

File tree

2 files changed

+149
-5
lines changed

2 files changed

+149
-5
lines changed

lib/util/propTypes.js

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,20 @@ module.exports = function propTypesInstructions(context, components, utils) {
100100
const defaults = { customValidators: [] };
101101
const configuration = Object.assign({}, defaults, context.options[0] || {});
102102
const customValidators = configuration.customValidators;
103-
const allowedGenericTypes = new Set(['VoidFunctionComponent', 'PropsWithChildren', 'SFC', 'StatelessComponent', 'FunctionComponent', 'FC']);
103+
const allowedGenericTypes = new Set(['forwardRef', 'ForwardRefRenderFunction', 'VoidFunctionComponent', 'PropsWithChildren', 'SFC', 'StatelessComponent', 'FunctionComponent', 'FC']);
104+
const genericTypeParamIndexWherePropsArePresent = {
105+
ForwardRefRenderFunction: 1,
106+
forwardRef: 1,
107+
VoidFunctionComponent: 0,
108+
PropsWithChildren: 0,
109+
SFC: 0,
110+
StatelessComponent: 0,
111+
FunctionComponent: 0,
112+
FC: 0,
113+
};
104114
const genericReactTypesImport = new Set();
115+
// import { FC as X } from 'react' -> localToImportedMap = { x: FC }
116+
const localToImportedMap = {};
105117

106118
/**
107119
* Returns the full scope.
@@ -521,9 +533,14 @@ module.exports = function propTypesInstructions(context, components, utils) {
521533
* @param {ASTNode} node
522534
* @return {string | undefined}
523535
*/
524-
function getTypeName(node) {
536+
function getLeftMostTypeName(node) {
537+
if (node.name) return node.name;
538+
if (node.left) return getLeftMostTypeName(node.left);
539+
}
540+
541+
function getRightMostTypeName(node) {
525542
if (node.name) return node.name;
526-
if (node.left) return getTypeName(node.left);
543+
if (node.right) return getRightMostTypeName(node.right);
527544
}
528545

529546
class DeclarePropTypesForTSTypeAnnotation {
@@ -579,14 +596,20 @@ module.exports = function propTypesInstructions(context, components, utils) {
579596
let typeName;
580597
if (astUtil.isTSTypeReference(node)) {
581598
typeName = node.typeName.name;
582-
const shouldTraverseTypeParams = genericReactTypesImport.has(getTypeName(node.typeName));
599+
const leftMostName = getLeftMostTypeName(node.typeName);
600+
const shouldTraverseTypeParams = genericReactTypesImport.has(leftMostName);
583601
if (shouldTraverseTypeParams && node.typeParameters && node.typeParameters.length !== 0) {
584602
// All react Generic types are derived from:
585603
// type PropsWithChildren<P> = P & { children?: ReactNode | undefined }
586604
// So we should construct an optional children prop
587605
this.shouldSpecifyOptionalChildrenProps = true;
588606

589-
const nextNode = node.typeParameters.params[0];
607+
const rightMostName = getRightMostTypeName(node.typeName);
608+
const importedName = localToImportedMap[rightMostName];
609+
const idx = genericTypeParamIndexWherePropsArePresent[
610+
leftMostName !== rightMostName ? rightMostName : importedName
611+
];
612+
const nextNode = node.typeParameters.params[idx];
590613
this.visitTSNode(nextNode);
591614
return;
592615
}
@@ -941,6 +964,30 @@ module.exports = function propTypesInstructions(context, components, utils) {
941964
return;
942965
}
943966

967+
if (
968+
node.parent
969+
&& node.parent.callee
970+
&& node.parent.typeParameters
971+
&& node.parent.typeParameters.params
972+
&& (
973+
node.parent.callee.name === 'forwardRef' || (
974+
node.parent.callee.object
975+
&& node.parent.callee.property
976+
&& node.parent.callee.object.name === 'React'
977+
&& node.parent.callee.property.name === 'forwardRef'
978+
)
979+
)
980+
) {
981+
const propTypes = node.parent.typeParameters.params[1];
982+
const declaredPropTypes = {};
983+
const obj = new DeclarePropTypesForTSTypeAnnotation(propTypes, declaredPropTypes);
984+
components.set(node, {
985+
declaredPropTypes: obj.declaredPropTypes,
986+
ignorePropsValidation: false,
987+
});
988+
return;
989+
}
990+
944991
const siblingIdentifier = node.parent && node.parent.id;
945992
const siblingHasTypeAnnotation = siblingIdentifier && siblingIdentifier.typeAnnotation;
946993
const isNodeAnnotated = annotations.isAnnotatedFunctionPropsDeclaration(node, context);
@@ -1092,6 +1139,7 @@ module.exports = function propTypesInstructions(context, components, utils) {
10921139
// handles import { FC } from 'react' or import { FC as X } from 'react'
10931140
if (specifier.type === 'ImportSpecifier' && allowedGenericTypes.has(specifier.imported.name)) {
10941141
genericReactTypesImport.add(specifier.local.name);
1142+
localToImportedMap[specifier.local.name] = specifier.imported.name;
10951143
}
10961144
});
10971145
}

tests/lib/rules/prop-types.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3411,6 +3411,66 @@ ruleTester.run('prop-types', rule, {
34113411
`,
34123412
features: ['ts', 'no-babel'],
34133413
},
3414+
{
3415+
code: `
3416+
import React, { ForwardRefRenderFunction as X } from 'react'
3417+
3418+
type IfooProps = { e: string };
3419+
const Foo: X<HTMLDivElement, IfooProps> = function Foo (props, ref) {
3420+
const { e } = props;
3421+
return <div ref={ref}>hello</div>;
3422+
};
3423+
`,
3424+
features: ['ts', 'no-babel'],
3425+
},
3426+
{
3427+
code: `
3428+
import React, { ForwardRefRenderFunction } from 'react'
3429+
3430+
type IfooProps = { e: string };
3431+
const Foo: ForwardRefRenderFunction<HTMLDivElement, IfooProps> = function Foo (props, ref) {
3432+
const { e } = props;
3433+
return <div ref={ref}>hello</div>;
3434+
};
3435+
`,
3436+
features: ['ts', 'no-babel'],
3437+
},
3438+
{
3439+
code: `
3440+
import React from 'react'
3441+
3442+
type IfooProps = { e: string };
3443+
const Foo= React.forwardRef<HTMLDivElement, IfooProps>((props, ref) => {
3444+
const { e } = props;
3445+
return <div ref={ref}>hello</div>;
3446+
});
3447+
`,
3448+
features: ['ts', 'no-babel'],
3449+
},
3450+
{
3451+
code: `
3452+
import React, { forwardRef } from 'react'
3453+
3454+
type IfooProps = { e: string };
3455+
const Foo= forwardRef<HTMLDivElement, IfooProps>((props, ref) => {
3456+
const { e } = props;
3457+
return <div ref={ref}>hello</div>;
3458+
});
3459+
`,
3460+
features: ['ts', 'no-babel'],
3461+
},
3462+
{
3463+
code: `
3464+
import React, { forwardRef as X } from 'react'
3465+
3466+
type IfooProps = { e: string };
3467+
const Foo= X<HTMLDivElement, IfooProps>((props, ref) => {
3468+
const { e } = props;
3469+
return <div ref={ref}>hello</div>;
3470+
});
3471+
`,
3472+
features: ['ts', 'no-babel'],
3473+
},
34143474
{
34153475
code: `
34163476
import React from 'react'
@@ -7139,6 +7199,42 @@ ruleTester.run('prop-types', rule, {
71397199
},
71407200
],
71417201
features: ['ts', 'no-babel'],
7202+
},
7203+
{
7204+
code: `
7205+
import React from 'react'
7206+
7207+
type IfooProps = { e: string };
7208+
const Foo: React.ForwardRefRenderFunction<HTMLDivElement, IfooProps> = function Foo (props, ref) {
7209+
const { name } = props;
7210+
return <div ref={ref}>{name}</div>;
7211+
};
7212+
7213+
`,
7214+
errors: [
7215+
{
7216+
messageId: 'missingPropType',
7217+
data: { name: 'name' },
7218+
},
7219+
],
7220+
features: ['ts', 'no-babel'],
7221+
},
7222+
{
7223+
code: `
7224+
import React from 'react'
7225+
type IfooProps = { k: string, a: number }
7226+
const Foo= React.forwardRef<HTMLDivElement, IfooProps>((props, ref) => {
7227+
return <div ref={ref}>{props.l}</div>;
7228+
});
7229+
7230+
`,
7231+
errors: [
7232+
{
7233+
messageId: 'missingPropType',
7234+
data: { name: 'l' },
7235+
},
7236+
],
7237+
features: ['ts', 'no-babel'],
71427238
}
71437239
)),
71447240
});

0 commit comments

Comments
 (0)