Skip to content

Commit 4bc3499

Browse files
vedadeeptaljharb
authored andcommitted
[Fix] prop-types, propTypes: add forwardRef<>, ForwardRefRenderFunction<> prop-types
1 parent cf47696 commit 4bc3499

File tree

3 files changed

+238
-13
lines changed

3 files changed

+238
-13
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
1010
* add [`no-arrow-function-lifecycle`] ([#1980][] @ngtan)
1111

1212
### Fixed
13-
* [`propTypes`]: add `VoidFunctionComponent` to react generic list ([#3092][] @vedadeepta)
13+
* `propTypes`: add `VoidFunctionComponent` to react generic list ([#3092][] @vedadeepta)
1414
* [`jsx-fragments`], [`jsx-no-useless-fragment`]: avoid a crash on fragment syntax in `typescript-eslint` parser (@ljharb)
1515
* [`jsx-props-no-multi-spaces`]: avoid a crash on long member chains in tag names in `typescript-eslint` parser (@ljharb)
1616
* [`no-unused-prop-types`], `usedPropTypes`: avoid crash with typescript-eslint parser (@ljharb)
1717
* [`display-name`]: unwrap TS `as` expressions ([#3110][] @ljharb)
1818
* [`destructuring-assignment`]: detect refs nested in functions ([#3102] @ljharb)
1919
* [`no-unstable-components`]: improve handling of objects containing render function properties ([#3111] @fizwidget)
20+
* [`prop-types`], `propTypes`: add forwardRef<>, ForwardRefRenderFunction<> prop-types ([#3112] @vedadeepta)
2021

22+
[#3112]: https://github.com/yannickcr/eslint-plugin-react/pull/3112
2123
[#3111]: https://github.com/yannickcr/eslint-plugin-react/pull/3111
2224
[#3110]: https://github.com/yannickcr/eslint-plugin-react/pull/3110
2325
[#3102]: https://github.com/yannickcr/eslint-plugin-react/issue/3102

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: 182 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -462,9 +462,9 @@ ruleTester.run('prop-types', rule, {
462462
code: `
463463
class Hello extends React.Component {
464464
render() {
465-
var {
465+
var {
466466
propX,
467-
"aria-controls": ariaControls,
467+
"aria-controls": ariaControls,
468468
...props } = this.props;
469469
return <div>Hello</div>;
470470
}
@@ -1346,7 +1346,7 @@ ruleTester.run('prop-types', rule, {
13461346
{
13471347
code: `
13481348
import type { FieldProps } from "redux-form"
1349-
1349+
13501350
type Props = {
13511351
label: string,
13521352
type: string,
@@ -3411,6 +3411,111 @@ 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, { ForwardRefRenderFunction } from 'react'
3441+
3442+
type IfooProps = { e: string };
3443+
const Foo: ForwardRefRenderFunction<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 from 'react'
3453+
3454+
type IfooProps = { e: string };
3455+
const Foo= React.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 } from 'react'
3465+
3466+
type IfooProps = { e: string };
3467+
const Foo= forwardRef<HTMLDivElement, IfooProps>((props, ref) => {
3468+
const { e } = props;
3469+
return <div ref={ref}>hello</div>;
3470+
});
3471+
`,
3472+
features: ['ts', 'no-babel'],
3473+
},
3474+
{
3475+
code: `
3476+
import React from 'react'
3477+
type IfooProps = { e: string };
3478+
const Foo= React.forwardRef<HTMLDivElement, IfooProps>(function Foo(props, ref) {
3479+
const { e } = props;
3480+
return <div ref={ref}>hello</div>;
3481+
});
3482+
`,
3483+
features: ['ts', 'no-babel'],
3484+
},
3485+
{
3486+
code: `
3487+
import React from 'react'
3488+
interface IfooProps { e: string }
3489+
const Foo= React.forwardRef<HTMLDivElement, IfooProps>(function Foo(props, ref) {
3490+
const { e } = props;
3491+
return <div ref={ref}>hello</div>;
3492+
});
3493+
`,
3494+
features: ['ts', 'no-babel'],
3495+
},
3496+
{
3497+
code: `
3498+
import React, { forwardRef } from 'react'
3499+
interface IfooProps { e: string }
3500+
const Foo= forwardRef<HTMLDivElement, IfooProps>(function Foo(props, ref) {
3501+
const { e } = props;
3502+
return <div ref={ref}>hello</div>;
3503+
});
3504+
`,
3505+
features: ['ts', 'no-babel'],
3506+
},
3507+
{
3508+
code: `
3509+
import React, { forwardRef as X } from 'react'
3510+
3511+
type IfooProps = { e: string };
3512+
const Foo= X<HTMLDivElement, IfooProps>((props, ref) => {
3513+
const { e } = props;
3514+
return <div ref={ref}>hello</div>;
3515+
});
3516+
`,
3517+
features: ['ts', 'no-babel'],
3518+
},
34143519
{
34153520
code: `
34163521
import React from 'react'
@@ -3430,7 +3535,7 @@ ruleTester.run('prop-types', rule, {
34303535
static propTypes = {
34313536
value: PropTypes.string
34323537
};
3433-
3538+
34343539
render() {
34353540
return <span>{this.props.value}</span>;
34363541
}
@@ -3830,7 +3935,7 @@ ruleTester.run('prop-types', rule, {
38303935
semver.satisfies(babelEslintVersion, '< 9') ? {
38313936
code: `
38323937
class Hello extends React.Component {
3833-
static propTypes: {
3938+
static propTypes: {
38343939
firstname: PropTypes.string
38353940
};
38363941
render() {
@@ -4019,8 +4124,8 @@ ruleTester.run('prop-types', rule, {
40194124
code: `
40204125
class Hello extends React.Component {
40214126
render() {
4022-
var {
4023-
"aria-controls": ariaControls,
4127+
var {
4128+
"aria-controls": ariaControls,
40244129
propX,
40254130
...props } = this.props;
40264131
return <div>Hello</div>;
@@ -7139,6 +7244,76 @@ ruleTester.run('prop-types', rule, {
71397244
},
71407245
],
71417246
features: ['ts', 'no-babel'],
7247+
},
7248+
{
7249+
code: `
7250+
import React from 'react'
7251+
7252+
type IfooProps = { e: string };
7253+
const Foo: React.ForwardRefRenderFunction<HTMLDivElement, IfooProps> = function Foo (props, ref) {
7254+
const { name } = props;
7255+
return <div ref={ref}>{name}</div>;
7256+
};
7257+
`,
7258+
errors: [
7259+
{
7260+
messageId: 'missingPropType',
7261+
data: { name: 'name' },
7262+
},
7263+
],
7264+
features: ['ts', 'no-babel'],
7265+
},
7266+
{
7267+
code: `
7268+
import React from 'react'
7269+
type IfooProps = { k: string, a: number }
7270+
const Foo= React.forwardRef<HTMLDivElement, IfooProps>((props, ref) => {
7271+
return <div ref={ref}>{props.l}</div>;
7272+
});
7273+
`,
7274+
errors: [
7275+
{
7276+
messageId: 'missingPropType',
7277+
data: { name: 'l' },
7278+
},
7279+
],
7280+
features: ['ts', 'no-babel'],
7281+
},
7282+
{
7283+
code: `
7284+
import React from 'react'
7285+
7286+
type IfooProps = { e: string };
7287+
const Foo= React.forwardRef<HTMLDivElement, IfooProps>(function Foo(props, ref) {
7288+
const { l } = props;
7289+
return <div ref={ref}>hello</div>;
7290+
});
7291+
`,
7292+
errors: [
7293+
{
7294+
messageId: 'missingPropType',
7295+
data: { name: 'l' },
7296+
},
7297+
],
7298+
features: ['ts', 'no-babel'],
7299+
},
7300+
{
7301+
code: `
7302+
import React, { forwardRef } from 'react'
7303+
7304+
type IfooProps = { e: string };
7305+
const Foo= forwardRef<HTMLDivElement, IfooProps>(function Foo(props, ref) {
7306+
const { l } = props;
7307+
return <div ref={ref}>hello</div>;
7308+
});
7309+
`,
7310+
errors: [
7311+
{
7312+
messageId: 'missingPropType',
7313+
data: { name: 'l' },
7314+
},
7315+
],
7316+
features: ['ts', 'no-babel'],
71427317
}
71437318
)),
71447319
});

0 commit comments

Comments
 (0)