Skip to content

Commit 130afb1

Browse files
vedadeeptaljharb
authored andcommitted
[Fix] prop-types, propTypes: add handling for FC<Props>, improve tests
1 parent 77813f0 commit 130afb1

File tree

3 files changed

+174
-6
lines changed

3 files changed

+174
-6
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
2222
* [`no-typos`]: fix crash on private methods ([#3043][] @ljharb)
2323
* [`jsx-no-bind`]: handle local function declarations ([#3048][] @p7g)
2424
* [`prop-types`], `propTypes`: handle React.* TypeScript types ([#3049][] @vedadeepta)
25+
* [`prop-types`], `propTypes`: add handling for `FC<Props>`, improve tests ([#3051][] @vedadeepta)
2526

2627
### Changed
2728
* [Docs] [`jsx-no-bind`]: updates discussion of refs ([#2998][] @dimitropoulos)
@@ -32,6 +33,7 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
3233
* [Docs] improve instructions for `jsx-runtime` config ([#3052][] @ljharb)
3334

3435
[#3052]: https://github.com/yannickcr/eslint-plugin-react/issues/3052
36+
[#3051]: https://github.com/yannickcr/eslint-plugin-react/pull/3051
3537
[#3049]: https://github.com/yannickcr/eslint-plugin-react/pull/3049
3638
[#3048]: https://github.com/yannickcr/eslint-plugin-react/pull/3048
3739
[#3043]: https://github.com/yannickcr/eslint-plugin-react/issues/3043

lib/util/propTypes.js

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ 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 = ['React.SFC', 'React.StatelessComponent', 'React.FunctionComponent', 'React.FC'];
103+
const allowedGenericTypes = new Set(['SFC', 'StatelessComponent', 'FunctionComponent', 'FC']);
104+
const genericReactTypesImport = new Set();
104105

105106
/**
106107
* Returns the full scope.
@@ -548,7 +549,8 @@ module.exports = function propTypesInstructions(context, components, utils) {
548549
let typeName;
549550
if (astUtil.isTSTypeReference(node)) {
550551
typeName = node.typeName.name;
551-
if (!typeName && node.typeParameters && node.typeParameters.length !== 0) {
552+
const shouldTraverseTypeParams = !typeName || genericReactTypesImport.has(typeName);
553+
if (shouldTraverseTypeParams && node.typeParameters && node.typeParameters.length !== 0) {
552554
const nextNode = node.typeParameters.params[0];
553555
this.visitTSNode(nextNode);
554556
return;
@@ -940,12 +942,19 @@ module.exports = function propTypesInstructions(context, components, utils) {
940942
return;
941943
}
942944

943-
const typeName = context.getSourceCode().getText(annotation.typeName).replace(/ /g, '');
945+
if (annotation.typeName.name) { // if FC<Props>
946+
const typeName = annotation.typeName.name;
947+
if (!genericReactTypesImport.has(typeName)) {
948+
return;
949+
}
950+
} else if (annotation.typeName.right.name) { // if React.FC<Props>
951+
const right = annotation.typeName.right.name;
952+
const left = annotation.typeName.left.name;
944953

945-
if (allowedGenericTypes.indexOf(typeName) === -1) {
946-
return;
954+
if (!genericReactTypesImport.has(left) || !allowedGenericTypes.has(right)) {
955+
return;
956+
}
947957
}
948-
949958
markPropTypesAsDeclared(node, resolveTypeAnnotation(siblingIdentifier));
950959
}
951960
}
@@ -1036,6 +1045,27 @@ module.exports = function propTypesInstructions(context, components, utils) {
10361045
}
10371046
},
10381047

1048+
ImportDeclaration(node) {
1049+
// parse `import ... from 'react`
1050+
if (node.source.value === 'react') {
1051+
node.specifiers.forEach((specifier) => {
1052+
if (
1053+
// handles import * as X from 'react'
1054+
specifier.type === 'ImportNamespaceSpecifier'
1055+
// handles import React from 'react'
1056+
|| specifier.type === 'ImportDefaultSpecifier'
1057+
) {
1058+
genericReactTypesImport.add(specifier.local.name);
1059+
}
1060+
1061+
// handles import { FC } from 'react' or import { FC as X } from 'react'
1062+
if (specifier.type === 'ImportSpecifier' && allowedGenericTypes.has(specifier.imported.name)) {
1063+
genericReactTypesImport.add(specifier.local.name);
1064+
}
1065+
});
1066+
}
1067+
},
1068+
10391069
FunctionDeclaration: markAnnotatedFunctionArgumentsAsDeclared,
10401070

10411071
ArrowFunctionExpression: markAnnotatedFunctionArgumentsAsDeclared,

tests/lib/rules/prop-types.js

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3150,6 +3150,8 @@ ruleTester.run('prop-types', rule, {
31503150
},
31513151
{
31523152
code: `
3153+
import React from 'react';
3154+
31533155
interface PersonProps {
31543156
username: string;
31553157
}
@@ -3161,6 +3163,8 @@ ruleTester.run('prop-types', rule, {
31613163
},
31623164
{
31633165
code: `
3166+
import React from 'react';
3167+
31643168
const Person: React.FunctionComponent<PersonProps> = (props): React.ReactElement => (
31653169
<div>{props.username}</div>
31663170
);
@@ -3169,6 +3173,8 @@ ruleTester.run('prop-types', rule, {
31693173
},
31703174
{
31713175
code: `
3176+
import React from 'react';
3177+
31723178
interface PersonProps {
31733179
username: string;
31743180
}
@@ -3180,6 +3186,7 @@ ruleTester.run('prop-types', rule, {
31803186
},
31813187
{
31823188
code: `
3189+
import React from 'react';
31833190
const Person: React.FunctionComponent<{ username: string }> = (props): React.ReactElement => (
31843191
<div>{props.username}</div>
31853192
);
@@ -3188,6 +3195,7 @@ ruleTester.run('prop-types', rule, {
31883195
},
31893196
{
31903197
code: `
3198+
import React from 'react';
31913199
type PersonProps = {
31923200
username: string;
31933201
}
@@ -3196,6 +3204,82 @@ ruleTester.run('prop-types', rule, {
31963204
);
31973205
`,
31983206
parser: parsers['@TYPESCRIPT_ESLINT']
3207+
},
3208+
{
3209+
code: `
3210+
import { FunctionComponent } from 'react';
3211+
3212+
type PersonProps = {
3213+
username: string;
3214+
}
3215+
const Person: FunctionComponent<PersonProps> = (props): React.ReactElement => (
3216+
<div>{props.username}</div>
3217+
);
3218+
`,
3219+
parser: parsers['@TYPESCRIPT_ESLINT']
3220+
},
3221+
{
3222+
code: `
3223+
import { FC } from 'react';
3224+
type PersonProps = {
3225+
username: string;
3226+
}
3227+
const Person: FC<PersonProps> = (props): React.ReactElement => (
3228+
<div>{props.username}</div>
3229+
);
3230+
`,
3231+
parser: parsers['@TYPESCRIPT_ESLINT']
3232+
},
3233+
{
3234+
code: `
3235+
import type { FC } from 'react';
3236+
type PersonProps = {
3237+
username: string;
3238+
}
3239+
const Person: FC<PersonProps> = (props): React.ReactElement => (
3240+
<div>{props.username}</div>
3241+
);
3242+
`,
3243+
parser: parsers['@TYPESCRIPT_ESLINT']
3244+
},
3245+
{
3246+
code: `
3247+
import { FC as X } from 'react';
3248+
interface PersonProps {
3249+
username: string;
3250+
}
3251+
const Person: X<PersonProps> = (props): React.ReactElement => (
3252+
<div>{props.username}</div>
3253+
);
3254+
`,
3255+
parser: parsers['@TYPESCRIPT_ESLINT']
3256+
},
3257+
{
3258+
code: `
3259+
import * as X from 'react';
3260+
interface PersonProps {
3261+
username: string;
3262+
}
3263+
const Person: X.FC<PersonProps> = (props): React.ReactElement => (
3264+
<div>{props.username}</div>
3265+
);
3266+
`,
3267+
parser: parsers['@TYPESCRIPT_ESLINT']
3268+
},
3269+
{
3270+
// issue: https://github.com/yannickcr/eslint-plugin-react/issues/2786
3271+
code: `
3272+
import React from 'react';
3273+
3274+
interface Props {
3275+
item: any;
3276+
}
3277+
3278+
const SomeComponent: React.FC<Props> = ({ item }: Props) => {
3279+
return item ? <></> : <></>;
3280+
};
3281+
`,
3282+
parser: parsers['@TYPESCRIPT_ESLINT']
31993283
}
32003284
]),
32013285
{
@@ -6706,6 +6790,58 @@ ruleTester.run('prop-types', rule, {
67066790
}
67076791
],
67086792
parser: parsers['@TYPESCRIPT_ESLINT']
6793+
},
6794+
{
6795+
code: `
6796+
import React from 'react';
6797+
interface PersonProps {
6798+
username: string;
6799+
}
6800+
const Person: FunctionComponent<PersonProps> = (props): React.ReactElement => (
6801+
<div>{props.test}</div>
6802+
);
6803+
`,
6804+
errors: [
6805+
{
6806+
messageId: 'missingPropType',
6807+
data: {name: 'test'}
6808+
}
6809+
],
6810+
parser: parsers['@TYPESCRIPT_ESLINT']
6811+
},
6812+
{
6813+
code: `
6814+
interface PersonProps {
6815+
username: string;
6816+
}
6817+
const Person: X.FC<PersonProps> = (props): React.ReactElement => (
6818+
<div>{props.username}</div>
6819+
);
6820+
`,
6821+
errors: [
6822+
{
6823+
messageId: 'missingPropType',
6824+
data: {name: 'username'}
6825+
}
6826+
],
6827+
parser: parsers['@TYPESCRIPT_ESLINT']
6828+
},
6829+
{
6830+
code: `
6831+
interface PersonProps {
6832+
username: string;
6833+
}
6834+
const Person: FC<PersonProps> = (props): React.ReactElement => (
6835+
<div>{props.username}</div>
6836+
);
6837+
`,
6838+
errors: [
6839+
{
6840+
messageId: 'missingPropType',
6841+
data: {name: 'username'}
6842+
}
6843+
],
6844+
parser: parsers['@TYPESCRIPT_ESLINT']
67096845
}
67106846
])
67116847
)

0 commit comments

Comments
 (0)