Skip to content

Commit

Permalink
[Fix] prop-types, propTypes: add support for exported type inference
Browse files Browse the repository at this point in the history
  • Loading branch information
vedadeepta committed Dec 31, 2021
1 parent 4f54108 commit d477c12
Show file tree
Hide file tree
Showing 3 changed files with 295 additions and 9 deletions.
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'],
}
)),
});

0 comments on commit d477c12

Please sign in to comment.