Skip to content

Commit 6a40d48

Browse files
vedadeeptaljharb
authored andcommitted
[Fix] prop-types, propTypes: add support for exported type inference
1 parent 4f54108 commit 6a40d48

File tree

4 files changed

+299
-9
lines changed

4 files changed

+299
-9
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
88
### Added
99
* add [`hook-use-state`] rule to enforce symmetric useState hook variable names ([#2921][] @duncanbeevers)
1010

11+
### Fixed
12+
* [`prop-types`], `propTypes`: add support for exported type inference ([#3163][] @vedadeepta)
13+
14+
[#3163]: https://github.com/yannickcr/eslint-plugin-react/pull/3163
1115
[#2921]: https://github.com/yannickcr/eslint-plugin-react/pull/2921
1216

1317
## [7.28.0] - 2021.12.22

lib/util/ast.js

+21-2
Original file line numberDiff line numberDiff line change
@@ -320,13 +320,31 @@ function isTSInterfaceHeritage(node) {
320320

321321
function isTSInterfaceDeclaration(node) {
322322
if (!node) return false;
323-
const nodeType = node.type;
323+
let nodeType = node.type;
324+
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
325+
nodeType = node.declaration.type;
326+
}
324327
return nodeType === 'TSInterfaceDeclaration';
325328
}
326329

330+
function isTSTypeDeclaration(node) {
331+
if (!node) return false;
332+
let nodeType = node.type;
333+
let nodeKind = node.kind;
334+
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
335+
nodeType = node.declaration.type;
336+
nodeKind = node.declaration.kind;
337+
}
338+
return nodeType === 'VariableDeclaration' && nodeKind === 'type';
339+
}
340+
327341
function isTSTypeAliasDeclaration(node) {
328342
if (!node) return false;
329-
const nodeType = node.type;
343+
let nodeType = node.type;
344+
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
345+
nodeType = node.declaration.type;
346+
return nodeType === 'TSTypeAliasDeclaration' && node.exportKind === 'type';
347+
}
330348
return nodeType === 'TSTypeAliasDeclaration';
331349
}
332350

@@ -380,4 +398,5 @@ module.exports = {
380398
isTSFunctionType,
381399
isTSTypeQuery,
382400
isTSTypeParameterInstantiation,
401+
isTSTypeDeclaration,
383402
};

lib/util/propTypes.js

+34-7
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,30 @@ module.exports = function propTypesInstructions(context, components, utils) {
547547
if (node.right) return getRightMostTypeName(node.right);
548548
}
549549

550+
/**
551+
* Returns true if the node is either a interface or type alias declaration
552+
* @param {ASTNode} node
553+
* @return {boolean}
554+
*/
555+
function filterInterfaceOrTypeAlias(node) {
556+
return (
557+
astUtil.isTSInterfaceDeclaration(node) || astUtil.isTSTypeAliasDeclaration(node)
558+
);
559+
}
560+
561+
/**
562+
* Returns true if the interface or type alias declaration node name matches the type-name str
563+
* @param {ASTNode} node
564+
* @param {string} typeName
565+
* @return {boolean}
566+
*/
567+
function filterInterfaceOrAliasByName(node, typeName) {
568+
return (
569+
(node.id && node.id.name === typeName)
570+
|| (node.declaration && node.declaration.id && node.declaration.id.name === typeName)
571+
);
572+
}
573+
550574
class DeclarePropTypesForTSTypeAnnotation {
551575
constructor(propTypes, declaredPropTypes) {
552576
this.propTypes = propTypes;
@@ -644,19 +668,22 @@ module.exports = function propTypesInstructions(context, components, utils) {
644668
* From line 577 to line 581, and line 588 to line 590 are trying to handle typescript-eslint-parser
645669
* Need to be deprecated after remove typescript-eslint-parser support.
646670
*/
647-
const candidateTypes = this.sourceCode.ast.body.filter((item) => item.type === 'VariableDeclaration' && item.kind === 'type');
648-
const declarations = flatMap(candidateTypes, (type) => type.declarations);
671+
const candidateTypes = this.sourceCode.ast.body.filter((item) => astUtil.isTSTypeDeclaration(item));
672+
673+
const declarations = flatMap(
674+
candidateTypes,
675+
(type) => type.declarations || (type.declaration && type.declaration.declarations) || type.declaration);
649676

650677
// we tried to find either an interface or a type with the TypeReference name
651678
const typeDeclaration = declarations.filter((dec) => dec.id.name === typeName);
652679

653680
const interfaceDeclarations = this.sourceCode.ast.body
654-
.filter(
655-
(item) => (astUtil.isTSInterfaceDeclaration(item)
656-
|| astUtil.isTSTypeAliasDeclaration(item))
657-
&& item.id.name === typeName);
681+
.filter(filterInterfaceOrTypeAlias)
682+
.filter((item) => filterInterfaceOrAliasByName(item, typeName))
683+
.map((item) => (item.declaration || item));
684+
658685
if (typeDeclaration.length !== 0) {
659-
typeDeclaration.map((t) => t.init).forEach(this.visitTSNode, this);
686+
typeDeclaration.map((t) => t.init || t.typeAnnotation).forEach(this.visitTSNode, this);
660687
} else if (interfaceDeclarations.length !== 0) {
661688
interfaceDeclarations.forEach(this.traverseDeclaredInterfaceOrTypeAlias, this);
662689
} else {

tests/lib/rules/prop-types.js

+240
Original file line numberDiff line numberDiff line change
@@ -3248,6 +3248,19 @@ ruleTester.run('prop-types', rule, {
32483248
`,
32493249
features: ['ts', 'no-babel'],
32503250
},
3251+
{
3252+
code: `
3253+
import React from 'react';
3254+
3255+
export interface PersonProps {
3256+
username: string;
3257+
}
3258+
const Person: React.FC<PersonProps> = (props): React.ReactElement => (
3259+
<div>{props.username}</div>
3260+
);
3261+
`,
3262+
features: ['ts', 'no-babel'],
3263+
},
32513264
{
32523265
code: `
32533266
import React from 'react';
@@ -3411,6 +3424,21 @@ ruleTester.run('prop-types', rule, {
34113424
`,
34123425
features: ['ts', 'no-babel'],
34133426
},
3427+
{
3428+
code: `
3429+
import React from 'react'
3430+
3431+
export interface Props {
3432+
age: number
3433+
}
3434+
const Hello: React.VoidFunctionComponent<Props> = function Hello(props) {
3435+
const { age } = props;
3436+
3437+
return <div>Hello {age}</div>;
3438+
}
3439+
`,
3440+
features: ['ts', 'no-babel'],
3441+
},
34143442
{
34153443
code: `
34163444
import React, { ForwardRefRenderFunction as X } from 'react'
@@ -3423,6 +3451,18 @@ ruleTester.run('prop-types', rule, {
34233451
`,
34243452
features: ['ts', 'no-babel'],
34253453
},
3454+
{
3455+
code: `
3456+
import React, { ForwardRefRenderFunction as X } from 'react'
3457+
3458+
export type IfooProps = { e: string };
3459+
const Foo: X<HTMLDivElement, IfooProps> = function Foo (props, ref) {
3460+
const { e } = props;
3461+
return <div ref={ref}>hello</div>;
3462+
};
3463+
`,
3464+
features: ['ts', 'no-babel'],
3465+
},
34263466
{
34273467
code: `
34283468
import React, { ForwardRefRenderFunction } from 'react'
@@ -3564,6 +3604,72 @@ ruleTester.run('prop-types', rule, {
35643604
}),
35653605
};
35663606
`,
3607+
},
3608+
{
3609+
code: `
3610+
import React, { forwardRef } from "react";
3611+
3612+
export type Props = { children: React.ReactNode; type: "submit" | "button" };
3613+
3614+
export const FancyButton = forwardRef<HTMLButtonElement, Props>((props, ref) => (
3615+
<button ref={ref} className="MyClassName" type={props.type}>
3616+
{props.children}
3617+
</button>
3618+
));
3619+
`,
3620+
features: ['ts', 'no-babel'],
3621+
},
3622+
{
3623+
code: `
3624+
import React, { forwardRef } from "react";
3625+
3626+
export type X = { num: number };
3627+
export type Props = { children: React.ReactNode; type: "submit" | "button" } & X;
3628+
3629+
export const FancyButton = forwardRef<HTMLButtonElement, Props>((props, ref) => (
3630+
<button ref={ref} className="MyClassName" type={props.type} num={props.num}>
3631+
{props.children}
3632+
</button>
3633+
));
3634+
`,
3635+
features: ['ts', 'no-babel'],
3636+
},
3637+
{
3638+
code: `
3639+
import React, { forwardRef } from "react";
3640+
3641+
export interface IProps {
3642+
children: React.ReactNode;
3643+
type: "submit" | "button"
3644+
}
3645+
3646+
export const FancyButton = forwardRef<HTMLButtonElement, IProps>((props, ref) => (
3647+
<button ref={ref} className="MyClassName" type={props.type}>
3648+
{props.children}
3649+
</button>
3650+
));
3651+
`,
3652+
features: ['ts', 'no-babel'],
3653+
},
3654+
{
3655+
code: `
3656+
import React, { forwardRef } from "react";
3657+
3658+
export interface X {
3659+
num: number
3660+
}
3661+
export interface IProps extends X {
3662+
children: React.ReactNode;
3663+
type: "submit" | "button"
3664+
}
3665+
3666+
export const FancyButton = forwardRef<HTMLButtonElement, IProps>((props, ref) => (
3667+
<button ref={ref} className="MyClassName" type={props.type} num={props.num}>
3668+
{props.children}
3669+
</button>
3670+
));
3671+
`,
3672+
features: ['ts', 'no-babel'],
35673673
}
35683674
)),
35693675

@@ -7336,6 +7442,140 @@ ruleTester.run('prop-types', rule, {
73367442
},
73377443
],
73387444
features: ['ts', 'no-babel'],
7445+
},
7446+
{
7447+
code: `
7448+
import React, { forwardRef } from "react";
7449+
7450+
export type Props = { children: React.ReactNode; type: "submit" | "button" };
7451+
7452+
export const FancyButton = forwardRef<HTMLButtonElement, Props>((props, ref) => (
7453+
<button ref={ref} className="MyClassName" type={props.nonExistent}>
7454+
{props.children}
7455+
</button>
7456+
));
7457+
`,
7458+
errors: [
7459+
{
7460+
messageId: 'missingPropType',
7461+
data: { name: 'nonExistent' },
7462+
},
7463+
],
7464+
features: ['ts', 'no-babel'],
7465+
},
7466+
{
7467+
code: `
7468+
import React, { forwardRef } from "react";
7469+
7470+
export interface IProps { children: React.ReactNode; type: "submit" | "button" };
7471+
7472+
export const FancyButton = forwardRef<HTMLButtonElement, IProps>((props, ref) => (
7473+
<button ref={ref} className="MyClassName" type={props.nonExistent}>
7474+
{props.children}
7475+
</button>
7476+
));
7477+
`,
7478+
errors: [
7479+
{
7480+
messageId: 'missingPropType',
7481+
data: { name: 'nonExistent' },
7482+
},
7483+
],
7484+
features: ['ts', 'no-babel'],
7485+
},
7486+
{
7487+
code: `
7488+
import React from 'react';
7489+
7490+
export interface PersonProps {
7491+
username: string;
7492+
}
7493+
const Person: React.FC<PersonProps> = (props): React.ReactElement => (
7494+
<div>{props.nonExistent}</div>
7495+
);
7496+
`,
7497+
errors: [
7498+
{
7499+
messageId: 'missingPropType',
7500+
data: { name: 'nonExistent' },
7501+
},
7502+
],
7503+
features: ['ts', 'no-babel'],
7504+
},
7505+
{
7506+
code: `
7507+
import React, { FC } from 'react';
7508+
7509+
export interface PersonProps {
7510+
username: string;
7511+
}
7512+
const Person: FC<PersonProps> = (props): React.ReactElement => (
7513+
<div>{props.nonExistent}</div>
7514+
);
7515+
`,
7516+
errors: [
7517+
{
7518+
messageId: 'missingPropType',
7519+
data: { name: 'nonExistent' },
7520+
},
7521+
],
7522+
features: ['ts', 'no-babel'],
7523+
},
7524+
{
7525+
code: `
7526+
import React, { FC as X } from 'react';
7527+
7528+
export interface PersonProps {
7529+
username: string;
7530+
}
7531+
const Person: X<PersonProps> = (props): React.ReactElement => (
7532+
<div>{props.nonExistent}</div>
7533+
);
7534+
`,
7535+
errors: [
7536+
{
7537+
messageId: 'missingPropType',
7538+
data: { name: 'nonExistent' },
7539+
},
7540+
],
7541+
features: ['ts', 'no-babel'],
7542+
},
7543+
{
7544+
code: `
7545+
import React, { ForwardRefRenderFunction as X } from 'react'
7546+
7547+
export type IfooProps = { e: string };
7548+
const Foo: X<HTMLDivElement, IfooProps> = function Foo (props, ref) {
7549+
const { nonExistent } = props;
7550+
return <div ref={ref}>hello</div>;
7551+
};
7552+
`,
7553+
errors: [
7554+
{
7555+
messageId: 'missingPropType',
7556+
data: { name: 'nonExistent' },
7557+
},
7558+
],
7559+
features: ['ts', 'no-babel'],
7560+
},
7561+
{
7562+
code: `
7563+
import React from 'react';
7564+
7565+
export interface PersonProps {
7566+
username: string;
7567+
}
7568+
const Person: React.VoidFunctionComponent<PersonProps> = (props): React.ReactElement => (
7569+
<div>{props.nonExistent}</div>
7570+
);
7571+
`,
7572+
errors: [
7573+
{
7574+
messageId: 'missingPropType',
7575+
data: { name: 'nonExistent' },
7576+
},
7577+
],
7578+
features: ['ts', 'no-babel'],
73397579
}
73407580
)),
73417581
});

0 commit comments

Comments
 (0)