Skip to content

Commit

Permalink
[Fix] prop-types, propTypes: handle implicit children prop in r…
Browse files Browse the repository at this point in the history
…eact's generic types
  • Loading branch information
vedadeepta committed Aug 30, 2021
1 parent 9487a17 commit 9507a97
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 16 deletions.
72 changes: 56 additions & 16 deletions lib/util/propTypes.js
Expand Up @@ -100,7 +100,7 @@ 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(['SFC', 'StatelessComponent', 'FunctionComponent', 'FC']);
const allowedGenericTypes = new Set(['PropsWithChildren', 'SFC', 'StatelessComponent', 'FunctionComponent', 'FC']);
const genericReactTypesImport = new Set();

/**
Expand Down Expand Up @@ -496,6 +496,36 @@ module.exports = function propTypesInstructions(context, components, utils) {
return {};
}

function isValidReactGenericTypeAnnotation(annotation) {
if (annotation.typeName) {
if (annotation.typeName.name) { // if FC<Props>
const typeName = annotation.typeName.name;
if (!genericReactTypesImport.has(typeName)) {
return false;
}
} else if (annotation.typeName.right.name) { // if React.FC<Props>
const right = annotation.typeName.right.name;
const left = annotation.typeName.left.name;

if (!genericReactTypesImport.has(left) || !allowedGenericTypes.has(right)) {
return false;
}
}
}
return true;
}

/**
* Returns the left most typeName of a node, e.g: FC<Props>, React.FC<Props>
* The representation is used to verify nested used properties.
* @param {ASTNode} node
* @return {string | undefined}
*/
function getTypeName(node) {
if (node.name) return node.name;
if (node.left) return getTypeName(node.left);
}

class DeclarePropTypesForTSTypeAnnotation {
constructor(propTypes, declaredPropTypes) {
this.propTypes = propTypes;
Expand Down Expand Up @@ -549,8 +579,13 @@ module.exports = function propTypesInstructions(context, components, utils) {
let typeName;
if (astUtil.isTSTypeReference(node)) {
typeName = node.typeName.name;
const shouldTraverseTypeParams = !typeName || genericReactTypesImport.has(typeName);
const shouldTraverseTypeParams = genericReactTypesImport.has(getTypeName(node.typeName));
if (shouldTraverseTypeParams && node.typeParameters && node.typeParameters.length !== 0) {
// All react Generic types are derived from:
// type PropsWithChildren<P> = P & { children?: ReactNode | undefined }
// So we should construct an optional children prop
this.shouldSpecifyOptionalChildrenProps = true;

const nextNode = node.typeParameters.params[0];
this.visitTSNode(nextNode);
return;
Expand Down Expand Up @@ -725,6 +760,14 @@ module.exports = function propTypesInstructions(context, components, utils) {
}

endAndStructDeclaredPropTypes() {
if (this.shouldSpecifyOptionalChildrenProps) {
this.declaredPropTypes.children = {
fullName: 'children',
name: 'children',
node: {},
isRequired: false
};
}
this.foundDeclaredPropertiesList.forEach((tsInterfaceBody) => {
if (tsInterfaceBody && (tsInterfaceBody.type === 'TSPropertySignature' || tsInterfaceBody.type === 'TSMethodSignature')) {
let accessor = 'name';
Expand Down Expand Up @@ -928,6 +971,16 @@ module.exports = function propTypesInstructions(context, components, utils) {
}
});
} else {
// check if its a valid generic type when `X<{...}>`
if (
param.typeAnnotation
&& param.typeAnnotation.typeAnnotation
&& param.typeAnnotation.typeAnnotation.type === 'TSTypeReference'
&& param.typeAnnotation.typeAnnotation.typeParameters != null
&& !isValidReactGenericTypeAnnotation(param.typeAnnotation.typeAnnotation)
) {
return;
}
markPropTypesAsDeclared(node, resolveTypeAnnotation(param));
}
} else {
Expand All @@ -942,21 +995,8 @@ module.exports = function propTypesInstructions(context, components, utils) {
return;
}

if (annotation.typeName) {
if (annotation.typeName.name) { // if FC<Props>
const typeName = annotation.typeName.name;
if (!genericReactTypesImport.has(typeName)) {
return;
}
} else if (annotation.typeName.right.name) { // if React.FC<Props>
const right = annotation.typeName.right.name;
const left = annotation.typeName.left.name;
if (!isValidReactGenericTypeAnnotation(annotation)) return;

if (!genericReactTypesImport.has(left) || !allowedGenericTypes.has(right)) {
return;
}
}
}
markPropTypesAsDeclared(node, resolveTypeAnnotation(siblingIdentifier));
}
}
Expand Down
11 changes: 11 additions & 0 deletions tests/lib/rules/prop-types.js
Expand Up @@ -3292,6 +3292,17 @@ ruleTester.run('prop-types', rule, {
}
`,
parser: parsers['@TYPESCRIPT_ESLINT']
},
{
code: `
import React from 'react';
const MyComponent = (props: React.PropsWithChildren<{ username: string }>): React.ReactElement => {
return <>{props.children}{props.username}</>;
};
`,
parser: parsers['@TYPESCRIPT_ESLINT']
}
]),
{
Expand Down

0 comments on commit 9507a97

Please sign in to comment.