Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel
### Added
* add [`hook-use-state`] rule to enforce symmetric useState hook variable names ([#2921][] @duncanbeevers)

### Fixed
* [`prop-types`], `propTypes`: add support for exported type inference ([#3163][] @vedadeepta)

[#3163]: https://github.com/yannickcr/eslint-plugin-react/pull/3163
[#2921]: https://github.com/yannickcr/eslint-plugin-react/pull/2921

## [7.28.0] - 2021.12.22
Expand Down
23 changes: 21 additions & 2 deletions lib/util/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,13 +320,31 @@ function isTSInterfaceHeritage(node) {

function isTSInterfaceDeclaration(node) {
if (!node) return false;
const nodeType = node.type;
let nodeType = node.type;
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
nodeType = node.declaration.type;
}
return nodeType === 'TSInterfaceDeclaration';
}

function isTSTypeDeclaration(node) {
if (!node) return false;
let nodeType = node.type;
let nodeKind = node.kind;
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
nodeType = node.declaration.type;
nodeKind = node.declaration.kind;
}
return nodeType === 'VariableDeclaration' && nodeKind === 'type';
}

function isTSTypeAliasDeclaration(node) {
if (!node) return false;
const nodeType = node.type;
let nodeType = node.type;
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
nodeType = node.declaration.type;
return nodeType === 'TSTypeAliasDeclaration' && node.exportKind === 'type';
}
return nodeType === 'TSTypeAliasDeclaration';
}

Expand Down Expand Up @@ -380,4 +398,5 @@ module.exports = {
isTSFunctionType,
isTSTypeQuery,
isTSTypeParameterInstantiation,
isTSTypeDeclaration,
};
41 changes: 34 additions & 7 deletions lib/util/propTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,30 @@ module.exports = function propTypesInstructions(context, components, utils) {
if (node.right) return getRightMostTypeName(node.right);
}

/**
* Returns true if the node is either a interface or type alias declaration
* @param {ASTNode} node
* @return {boolean}
*/
function filterInterfaceOrTypeAlias(node) {
return (
astUtil.isTSInterfaceDeclaration(node) || astUtil.isTSTypeAliasDeclaration(node)
);
}

/**
* Returns true if the interface or type alias declaration node name matches the type-name str
* @param {ASTNode} node
* @param {string} typeName
* @return {boolean}
*/
function filterInterfaceOrAliasByName(node, typeName) {
return (
(node.id && node.id.name === typeName)
|| (node.declaration && node.declaration.id && node.declaration.id.name === typeName)
);
}

class DeclarePropTypesForTSTypeAnnotation {
constructor(propTypes, declaredPropTypes) {
this.propTypes = propTypes;
Expand Down Expand Up @@ -644,19 +668,22 @@ module.exports = function propTypesInstructions(context, components, utils) {
* From line 577 to line 581, and line 588 to line 590 are trying to handle typescript-eslint-parser
* Need to be deprecated after remove typescript-eslint-parser support.
*/
const candidateTypes = this.sourceCode.ast.body.filter((item) => item.type === 'VariableDeclaration' && item.kind === 'type');
const declarations = flatMap(candidateTypes, (type) => type.declarations);
const candidateTypes = this.sourceCode.ast.body.filter((item) => astUtil.isTSTypeDeclaration(item));

const declarations = flatMap(
candidateTypes,
(type) => type.declarations || (type.declaration && type.declaration.declarations) || type.declaration);

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

const interfaceDeclarations = this.sourceCode.ast.body
.filter(
(item) => (astUtil.isTSInterfaceDeclaration(item)
|| astUtil.isTSTypeAliasDeclaration(item))
&& item.id.name === typeName);
.filter(filterInterfaceOrTypeAlias)
.filter((item) => filterInterfaceOrAliasByName(item, typeName))
.map((item) => (item.declaration || item));

if (typeDeclaration.length !== 0) {
typeDeclaration.map((t) => t.init).forEach(this.visitTSNode, this);
typeDeclaration.map((t) => t.init || t.typeAnnotation).forEach(this.visitTSNode, this);
} else if (interfaceDeclarations.length !== 0) {
interfaceDeclarations.forEach(this.traverseDeclaredInterfaceOrTypeAlias, this);
} else {
Expand Down
240 changes: 240 additions & 0 deletions tests/lib/rules/prop-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -3248,6 +3248,19 @@ ruleTester.run('prop-types', rule, {
`,
features: ['ts', 'no-babel'],
},
{
code: `
import React from 'react';

export interface PersonProps {
username: string;
}
const Person: React.FC<PersonProps> = (props): React.ReactElement => (
<div>{props.username}</div>
);
`,
features: ['ts', 'no-babel'],
},
{
code: `
import React from 'react';
Expand Down Expand Up @@ -3411,6 +3424,21 @@ ruleTester.run('prop-types', rule, {
`,
features: ['ts', 'no-babel'],
},
{
code: `
import React from 'react'

export interface Props {
age: number
}
const Hello: React.VoidFunctionComponent<Props> = function Hello(props) {
const { age } = props;

return <div>Hello {age}</div>;
}
`,
features: ['ts', 'no-babel'],
},
{
code: `
import React, { ForwardRefRenderFunction as X } from 'react'
Expand All @@ -3423,6 +3451,18 @@ ruleTester.run('prop-types', rule, {
`,
features: ['ts', 'no-babel'],
},
{
code: `
import React, { ForwardRefRenderFunction as X } from 'react'

export type IfooProps = { e: string };
const Foo: X<HTMLDivElement, IfooProps> = function Foo (props, ref) {
const { e } = props;
return <div ref={ref}>hello</div>;
};
`,
features: ['ts', 'no-babel'],
},
{
code: `
import React, { ForwardRefRenderFunction } from 'react'
Expand Down Expand Up @@ -3564,6 +3604,72 @@ ruleTester.run('prop-types', rule, {
}),
};
`,
},
{
code: `
import React, { forwardRef } from "react";

export type Props = { children: React.ReactNode; type: "submit" | "button" };

export const FancyButton = forwardRef<HTMLButtonElement, Props>((props, ref) => (
<button ref={ref} className="MyClassName" type={props.type}>
{props.children}
</button>
));
`,
features: ['ts', 'no-babel'],
},
{
code: `
import React, { forwardRef } from "react";

export type X = { num: number };
export type Props = { children: React.ReactNode; type: "submit" | "button" } & X;

export const FancyButton = forwardRef<HTMLButtonElement, Props>((props, ref) => (
<button ref={ref} className="MyClassName" type={props.type} num={props.num}>
{props.children}
</button>
));
`,
features: ['ts', 'no-babel'],
},
{
code: `
import React, { forwardRef } from "react";

export interface IProps {
children: React.ReactNode;
type: "submit" | "button"
}

export const FancyButton = forwardRef<HTMLButtonElement, IProps>((props, ref) => (
<button ref={ref} className="MyClassName" type={props.type}>
{props.children}
</button>
));
`,
features: ['ts', 'no-babel'],
},
{
code: `
import React, { forwardRef } from "react";

export interface X {
num: number
}
export interface IProps extends X {
children: React.ReactNode;
type: "submit" | "button"
}

export const FancyButton = forwardRef<HTMLButtonElement, IProps>((props, ref) => (
<button ref={ref} className="MyClassName" type={props.type} num={props.num}>
{props.children}
</button>
));
`,
features: ['ts', 'no-babel'],
}
)),

Expand Down Expand Up @@ -7336,6 +7442,140 @@ ruleTester.run('prop-types', rule, {
},
],
features: ['ts', 'no-babel'],
},
{
code: `
import React, { forwardRef } from "react";

export type Props = { children: React.ReactNode; type: "submit" | "button" };

export const FancyButton = forwardRef<HTMLButtonElement, Props>((props, ref) => (
<button ref={ref} className="MyClassName" type={props.nonExistent}>
{props.children}
</button>
));
`,
errors: [
{
messageId: 'missingPropType',
data: { name: 'nonExistent' },
},
],
features: ['ts', 'no-babel'],
},
{
code: `
import React, { forwardRef } from "react";

export interface IProps { children: React.ReactNode; type: "submit" | "button" };

export const FancyButton = forwardRef<HTMLButtonElement, IProps>((props, ref) => (
<button ref={ref} className="MyClassName" type={props.nonExistent}>
{props.children}
</button>
));
`,
errors: [
{
messageId: 'missingPropType',
data: { name: 'nonExistent' },
},
],
features: ['ts', 'no-babel'],
},
{
code: `
import React from 'react';

export interface PersonProps {
username: string;
}
const Person: React.FC<PersonProps> = (props): React.ReactElement => (
<div>{props.nonExistent}</div>
);
`,
errors: [
{
messageId: 'missingPropType',
data: { name: 'nonExistent' },
},
],
features: ['ts', 'no-babel'],
},
{
code: `
import React, { FC } from 'react';

export interface PersonProps {
username: string;
}
const Person: FC<PersonProps> = (props): React.ReactElement => (
<div>{props.nonExistent}</div>
);
`,
errors: [
{
messageId: 'missingPropType',
data: { name: 'nonExistent' },
},
],
features: ['ts', 'no-babel'],
},
{
code: `
import React, { FC as X } from 'react';

export interface PersonProps {
username: string;
}
const Person: X<PersonProps> = (props): React.ReactElement => (
<div>{props.nonExistent}</div>
);
`,
errors: [
{
messageId: 'missingPropType',
data: { name: 'nonExistent' },
},
],
features: ['ts', 'no-babel'],
},
{
code: `
import React, { ForwardRefRenderFunction as X } from 'react'

export type IfooProps = { e: string };
const Foo: X<HTMLDivElement, IfooProps> = function Foo (props, ref) {
const { nonExistent } = props;
return <div ref={ref}>hello</div>;
};
`,
errors: [
{
messageId: 'missingPropType',
data: { name: 'nonExistent' },
},
],
features: ['ts', 'no-babel'],
},
{
code: `
import React from 'react';

export interface PersonProps {
username: string;
}
const Person: React.VoidFunctionComponent<PersonProps> = (props): React.ReactElement => (
<div>{props.nonExistent}</div>
);
`,
errors: [
{
messageId: 'missingPropType',
data: { name: 'nonExistent' },
},
],
features: ['ts', 'no-babel'],
}
)),
});