Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fix] prop-types, propTypes: handle implicit children prop in react's generic types #3064

Merged
merged 1 commit into from Aug 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -7,7 +7,9 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel

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

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

## [7.25.1] - 2021.08.29
Expand Down
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