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: add support for exported type inference #3163

Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
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
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
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
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'],
}
)),
});