Skip to content

Commit 810806e

Browse files
vedadeeptaljharb
authored andcommitted
[Fix] prop-types, propTypes: handle implicit children prop in react's generic types
1 parent cc95a82 commit 810806e

File tree

3 files changed

+69
-16
lines changed

3 files changed

+69
-16
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
77

88
### Fixed
99
* [`jsx-no-useless-fragments`]: Handle insignificant whitespace correctly when `allowExpressions` is `true` ([#3061][] @benj-dobs)
10+
* [`prop-types`], `propTypes`: handle implicit `children` prop in react's generic types ([#3064][] @vedadeepta)
1011

12+
[#3064]: https://github.com/yannickcr/eslint-plugin-react/pull/3064
1113
[#3061]: https://github.com/yannickcr/eslint-plugin-react/pull/3061
1214

1315
## [7.25.1] - 2021.08.29

lib/util/propTypes.js

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ 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(['SFC', 'StatelessComponent', 'FunctionComponent', 'FC']);
103+
const allowedGenericTypes = new Set(['PropsWithChildren', 'SFC', 'StatelessComponent', 'FunctionComponent', 'FC']);
104104
const genericReactTypesImport = new Set();
105105

106106
/**
@@ -496,6 +496,36 @@ module.exports = function propTypesInstructions(context, components, utils) {
496496
return {};
497497
}
498498

499+
function isValidReactGenericTypeAnnotation(annotation) {
500+
if (annotation.typeName) {
501+
if (annotation.typeName.name) { // if FC<Props>
502+
const typeName = annotation.typeName.name;
503+
if (!genericReactTypesImport.has(typeName)) {
504+
return false;
505+
}
506+
} else if (annotation.typeName.right.name) { // if React.FC<Props>
507+
const right = annotation.typeName.right.name;
508+
const left = annotation.typeName.left.name;
509+
510+
if (!genericReactTypesImport.has(left) || !allowedGenericTypes.has(right)) {
511+
return false;
512+
}
513+
}
514+
}
515+
return true;
516+
}
517+
518+
/**
519+
* Returns the left most typeName of a node, e.g: FC<Props>, React.FC<Props>
520+
* The representation is used to verify nested used properties.
521+
* @param {ASTNode} node
522+
* @return {string | undefined}
523+
*/
524+
function getTypeName(node) {
525+
if (node.name) return node.name;
526+
if (node.left) return getTypeName(node.left);
527+
}
528+
499529
class DeclarePropTypesForTSTypeAnnotation {
500530
constructor(propTypes, declaredPropTypes) {
501531
this.propTypes = propTypes;
@@ -549,8 +579,13 @@ module.exports = function propTypesInstructions(context, components, utils) {
549579
let typeName;
550580
if (astUtil.isTSTypeReference(node)) {
551581
typeName = node.typeName.name;
552-
const shouldTraverseTypeParams = !typeName || genericReactTypesImport.has(typeName);
582+
const shouldTraverseTypeParams = genericReactTypesImport.has(getTypeName(node.typeName));
553583
if (shouldTraverseTypeParams && node.typeParameters && node.typeParameters.length !== 0) {
584+
// All react Generic types are derived from:
585+
// type PropsWithChildren<P> = P & { children?: ReactNode | undefined }
586+
// So we should construct an optional children prop
587+
this.shouldSpecifyOptionalChildrenProps = true;
588+
554589
const nextNode = node.typeParameters.params[0];
555590
this.visitTSNode(nextNode);
556591
return;
@@ -725,6 +760,14 @@ module.exports = function propTypesInstructions(context, components, utils) {
725760
}
726761

727762
endAndStructDeclaredPropTypes() {
763+
if (this.shouldSpecifyOptionalChildrenProps) {
764+
this.declaredPropTypes.children = {
765+
fullName: 'children',
766+
name: 'children',
767+
node: {},
768+
isRequired: false
769+
};
770+
}
728771
this.foundDeclaredPropertiesList.forEach((tsInterfaceBody) => {
729772
if (tsInterfaceBody && (tsInterfaceBody.type === 'TSPropertySignature' || tsInterfaceBody.type === 'TSMethodSignature')) {
730773
let accessor = 'name';
@@ -928,6 +971,16 @@ module.exports = function propTypesInstructions(context, components, utils) {
928971
}
929972
});
930973
} else {
974+
// check if its a valid generic type when `X<{...}>`
975+
if (
976+
param.typeAnnotation
977+
&& param.typeAnnotation.typeAnnotation
978+
&& param.typeAnnotation.typeAnnotation.type === 'TSTypeReference'
979+
&& param.typeAnnotation.typeAnnotation.typeParameters != null
980+
&& !isValidReactGenericTypeAnnotation(param.typeAnnotation.typeAnnotation)
981+
) {
982+
return;
983+
}
931984
markPropTypesAsDeclared(node, resolveTypeAnnotation(param));
932985
}
933986
} else {
@@ -942,21 +995,8 @@ module.exports = function propTypesInstructions(context, components, utils) {
942995
return;
943996
}
944997

945-
if (annotation.typeName) {
946-
if (annotation.typeName.name) { // if FC<Props>
947-
const typeName = annotation.typeName.name;
948-
if (!genericReactTypesImport.has(typeName)) {
949-
return;
950-
}
951-
} else if (annotation.typeName.right.name) { // if React.FC<Props>
952-
const right = annotation.typeName.right.name;
953-
const left = annotation.typeName.left.name;
998+
if (!isValidReactGenericTypeAnnotation(annotation)) return;
954999

955-
if (!genericReactTypesImport.has(left) || !allowedGenericTypes.has(right)) {
956-
return;
957-
}
958-
}
959-
}
9601000
markPropTypesAsDeclared(node, resolveTypeAnnotation(siblingIdentifier));
9611001
}
9621002
}

tests/lib/rules/prop-types.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3292,6 +3292,17 @@ ruleTester.run('prop-types', rule, {
32923292
}
32933293
`,
32943294
parser: parsers['@TYPESCRIPT_ESLINT']
3295+
},
3296+
{
3297+
code: `
3298+
import React from 'react';
3299+
3300+
const MyComponent = (props: React.PropsWithChildren<{ username: string }>): React.ReactElement => {
3301+
return <>{props.children}{props.username}</>;
3302+
};
3303+
3304+
`,
3305+
parser: parsers['@TYPESCRIPT_ESLINT']
32953306
}
32963307
]),
32973308
{

0 commit comments

Comments
 (0)