[react-unused-props-and-state] add support for traversing function components (#824)

* added suite of tests to cover function components

* traverse found arrow functions, function declarations and expressions

* add helper functions to extract type information

* wireup logic to traverse arrow functions and function components

* reorganized tests to help with understanding coverage

* check shorthand types for React function components

* update comment
This commit is contained in:
Brendan Forster 2019-02-19 09:14:47 -04:00 коммит произвёл Andrii Dieiev
Родитель 17080557c1
Коммит 3d5526ebf3
31 изменённых файлов: 518 добавлений и 1 удалений

Просмотреть файл

@ -78,6 +78,8 @@ function walk(ctx: Lint.WalkContext<Options>) {
let stateNames: string[] = [];
let stateNodes: { [index: string]: ts.TypeElement } = {};
const classDeclarations: ts.ClassDeclaration[] = [];
const arrowFunctions: ts.ArrowFunction[] = [];
const functionComponents: ts.FunctionBody[] = [];
let propsAlias: string | undefined;
let stateAlias: string | undefined;
@ -96,6 +98,36 @@ function walk(ctx: Lint.WalkContext<Options>) {
return result;
}
function getTypeLiteralData(node: ts.TypeLiteralNode): { [index: string]: ts.TypeElement } {
const result: { [index: string]: ts.TypeElement } = {};
node.members.forEach(
(typeElement: ts.TypeElement): void => {
if (typeElement.name !== undefined) {
const text = typeElement.name.getText();
if (text !== undefined) {
result[text] = typeElement;
}
}
}
);
return result;
}
function getObjectBindingData(node: ts.ObjectBindingPattern): { [index: string]: ts.BindingElement } {
const result: { [index: string]: ts.BindingElement } = {};
node.elements.forEach(
(element: ts.BindingElement): void => {
if (element.name !== undefined) {
const text = element.name.getText();
if (text !== undefined) {
result[text] = element;
}
}
}
);
return result;
}
function isParentNodeSuperCall(node: ts.Node): boolean {
if (node.parent !== undefined && node.parent.kind === ts.SyntaxKind.CallExpression) {
const call: ts.CallExpression = <ts.CallExpression>node.parent;
@ -104,6 +136,113 @@ function walk(ctx: Lint.WalkContext<Options>) {
return false;
}
function inspectPropUsageInObjectBinding(name: ts.ObjectBindingPattern): void {
const bindingElements = getObjectBindingData(name);
const foundPropNames = Object.keys(bindingElements);
for (const propName of foundPropNames) {
propNames = Utils.remove(propNames, propName);
}
}
function lookForReactSpecificArrowFunction(node: ts.TypeReferenceNode): void {
const nodeTypeText = node.typeName.getText();
const isReactFunctionComponentType =
nodeTypeText === 'React.SFC' ||
nodeTypeText === 'SFC' ||
nodeTypeText === 'React.FC' ||
nodeTypeText === 'FC' ||
nodeTypeText === 'React.StatelessComponent' ||
nodeTypeText === 'StatelessComponent' ||
nodeTypeText === 'React.FunctionComponent' ||
nodeTypeText === 'FunctionComponent';
if (!isReactFunctionComponentType) {
return;
}
if (!node.typeArguments || node.typeArguments.length !== 1) {
return;
}
const typeArgument = node.typeArguments[0];
if (tsutils.isTypeLiteralNode(typeArgument)) {
propNodes = getTypeLiteralData(typeArgument);
propNames = Object.keys(propNodes);
} else {
// we have a TypeReference here which we expect to have been parsed
// previously in the AST
}
// the arrow function should be a sibling of this type reference node
const arrowFunction = tsutils.getChildOfKind(node.parent, ts.SyntaxKind.ArrowFunction);
if (!arrowFunction || !tsutils.isArrowFunction(arrowFunction)) {
return;
}
lookForArrowFunction(arrowFunction);
}
function lookForArrowFunction(node: ts.ArrowFunction): void {
// expect one parameter for the function
const parameters = node.parameters;
if (parameters.length !== 1) {
return;
}
const firstParameter = parameters[0];
const { name, type } = firstParameter;
if (type && tsutils.isTypeReferenceNode(type)) {
const typeName = type.typeName.getText();
// skip any type that doesn't match the expected regex
if (!ctx.options.propsInterfaceRegex.test(typeName)) {
return;
}
}
if (tsutils.isIdentifier(name)) {
propsAlias = name.getText();
} else if (tsutils.isObjectBindingPattern(name)) {
inspectPropUsageInObjectBinding(name);
}
arrowFunctions.push(node);
}
function lookForFunctionComponent(node: ts.FunctionDeclaration | ts.FunctionExpression): void {
// if no body found, no need to traverse
if (!node.body) {
return;
}
// expect one parameter for the function
const parameters = node.parameters;
if (parameters.length !== 1) {
return;
}
const firstParameter = parameters[0];
const { name, type } = firstParameter;
if (type && tsutils.isTypeReferenceNode(type)) {
const typeName = type.typeName.getText();
// skip any type that doesn't match the expected regex
if (!ctx.options.propsInterfaceRegex.test(typeName)) {
return;
}
}
if (tsutils.isIdentifier(name)) {
propsAlias = name.getText();
} else if (tsutils.isObjectBindingPattern(name)) {
inspectPropUsageInObjectBinding(name);
}
functionComponents.push(node.body);
}
function cb(node: ts.Node): void {
// Accumulate class declarations here and only analyze
// them *after* all interfaces have been analyzed.
@ -198,6 +337,14 @@ function walk(ctx: Lint.WalkContext<Options>) {
stateNames = [];
}
}
} else if (tsutils.isTypeReferenceNode(node)) {
lookForReactSpecificArrowFunction(node);
} else if (tsutils.isArrowFunction(node)) {
lookForArrowFunction(node);
} else if (tsutils.isFunctionDeclaration(node)) {
lookForFunctionComponent(node);
} else if (tsutils.isFunctionExpression(node)) {
lookForFunctionComponent(node);
}
return ts.forEachChild(node, cb);
@ -205,9 +352,12 @@ function walk(ctx: Lint.WalkContext<Options>) {
ts.forEachChild(ctx.sourceFile, cb);
// If there are Props or State interfaces, then scan the classes now.
// if there are Props or State interfaces, traverse the identified components
// to find any usage of the members in these interfaces
if (propNames.length > 0 || stateNames.length > 0) {
classDeclarations.forEach(c => ts.forEachChild(c, cb));
arrowFunctions.forEach(c => ts.forEachChild(c.body, cb));
functionComponents.forEach(f => ts.forEachChild(f, cb));
}
propNames.forEach(

Просмотреть файл

@ -0,0 +1,10 @@
import React = require('react');
interface SampleProps {
text: string;
wrap?: boolean;
}
export const SampleArrowFunction = ({ text, wrap }: SampleProps) => {
return wrap ? <span>{text}</span> : text;
};

Просмотреть файл

@ -0,0 +1,11 @@
import React = require('react');
interface Props {
text: string;
wrap?: boolean;
~~~~~~~~~~~~~~~ [Unused React property defined in interface: wrap]
}
export const SampleArrowFunction = ({ text }: Props) => {
return <span>{text}</span>;
};

Просмотреть файл

@ -0,0 +1,5 @@
{
"rules": {
"react-unused-props-and-state": true
}
}

Просмотреть файл

@ -0,0 +1,9 @@
import React = require('react');
interface Props {
readonly text: string
}
export const SpanWithText: React.FunctionComponent<Props> = (p: Props) => (
<span>{p.text}</span>
)

Просмотреть файл

@ -0,0 +1,9 @@
import React = require('react');
interface Props {
readonly text: string
}
export const SpanWithText: React.FunctionComponent<Props> = (props: Props) => (
<span>{props.text}</span>
)

Просмотреть файл

@ -0,0 +1,11 @@
import * as React, { FC } from 'react';
interface Props {
readonly text: string
readonly wrap?: boolean;
~~~~~~~~~~~~~~~~~~~~~~~~ [Unused React property defined in interface: wrap]
}
export const SpanWithText: FC<Props> = (props: Props) => (
<span>{props.text}</span>
)

Просмотреть файл

@ -0,0 +1,11 @@
import * as React, { FunctionComponent } from 'react';
interface Props {
readonly text: string
readonly wrap?: boolean;
~~~~~~~~~~~~~~~~~~~~~~~~ [Unused React property defined in interface: wrap]
}
export const SpanWithText: FunctionComponent<Props> = (props: Props) => (
<span>{props.text}</span>
)

Просмотреть файл

@ -0,0 +1,5 @@
{
"rules": {
"react-unused-props-and-state": true
}
}

Просмотреть файл

@ -0,0 +1,18 @@
import React = require('react');
interface Props {
children?: React.ReactNode;
heading: string;
className?: string;
}
export function TestComponent(props: Props) {
return (
<div className={props.className || ''}>
<h3>{props.heading}</h3>
{props.children}
</div>
)
}
export default TestComponent;

Просмотреть файл

@ -0,0 +1,14 @@
import React = require('react');
interface Props {
heading: string;
className?: string;
}
export function TestComponent(props: Props) {
return (
<div className={props.className}>
<h3>{props.heading}</h3>
</div>
)
}

Просмотреть файл

@ -0,0 +1,10 @@
import React = require('react');
interface SampleProps {
text: string;
wrap?: boolean;
}
export const SampleFunctionExpression = function ({ text, wrap }: SampleProps) {
return wrap ? <span>{text}</span> : text;
};

Просмотреть файл

@ -0,0 +1,14 @@
import React = require('react');
interface Props {
heading: string;
className?: string;
}
export function TestComponent(p: Props) {
return (
<div className={p.className}>
<h3>{p.heading}</h3>
</div>
)
}

Просмотреть файл

@ -0,0 +1,11 @@
import React = require('react');
interface Props {
text: string;
wrap?: boolean;
~~~~~~~~~~~~~~~ [Unused React property defined in interface: wrap]
}
export const SampleFunctionExpression = function ({ text }: Props) {
return <span>{text}</span>;
};

Просмотреть файл

@ -0,0 +1,18 @@
import React = require('react');
interface Props {
children?: React.ReactNode;
~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Unused React property defined in interface: children]
heading: string;
className?: string;
}
export function TestComponent(props: Props) {
return (
<div className={props.className || ''}>
<h3>{props.heading}</h3>
</div>
)
}
export default TestComponent;

Просмотреть файл

@ -0,0 +1,5 @@
{
"rules": {
"react-unused-props-and-state": true
}
}

Просмотреть файл

@ -0,0 +1,14 @@
import React = require('react');
interface Props {
children: JSX.Element | JSX.Element[];
className: string;
}
const ButtonGroup: React.SFC<Props> = (props: Props) => {
return (
<div className={props.className}>
{props.children}
</div>
);
};

Просмотреть файл

@ -0,0 +1,16 @@
import React = require('react');
interface Props {
children: JSX.Element | JSX.Element[];
className: string;
}
const ButtonGroup: React.SFC<Props> = (props: Props) => {
const container = (
<div className={props.className}>
{props.children}
</div>
);
return container;
};

Просмотреть файл

@ -0,0 +1,7 @@
import React = require('react');
export const SpanWithText: React.SFC<{
readonly text: string
}> = (p) => (
<span>{p.text}</span>
)

Просмотреть файл

@ -0,0 +1,9 @@
import React = require('react');
interface Props {
readonly text: string
}
export const SpanWithText: React.SFC<Props> = (p: Props) => (
<span>{p.text}</span>
)

Просмотреть файл

@ -0,0 +1,7 @@
import React = require('react');
export const SpanWithText: React.SFC<{
readonly text: string
}> = (props) => (
<span>{props.text}</span>
)

Просмотреть файл

@ -0,0 +1,9 @@
import React = require('react');
interface Props {
readonly text: string
}
export const SpanWithText: React.SFC<Props> = (props: Props) => (
<span>{props.text}</span>
)

Просмотреть файл

@ -0,0 +1,11 @@
import * as React, { SFC } from 'react';
interface Props {
readonly text: string
readonly wrap?: boolean;
~~~~~~~~~~~~~~~~~~~~~~~~ [Unused React property defined in interface: wrap]
}
export const SpanWithText: SFC<Props> = (props: Props) => (
<span>{props.text}</span>
)

Просмотреть файл

@ -0,0 +1,11 @@
import * as React, { StatelessComponent } from 'react';
interface Props {
readonly text: string
readonly wrap?: boolean;
~~~~~~~~~~~~~~~~~~~~~~~~ [Unused React property defined in interface: wrap]
}
export const SpanWithText: StatelessComponent<Props> = (props: Props) => (
<span>{props.text}</span>
)

Просмотреть файл

@ -0,0 +1,9 @@
import React = require('react');
export const SpanWithText: React.SFC<{
readonly text: string,
readonly somethingElse: string,
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Unused React property defined in interface: somethingElse]
}> = (p) => (
<span>{p.text}</span>
)

Просмотреть файл

@ -0,0 +1,11 @@
import React = require('react');
interface Props {
readonly text: string
readonly somethingElse: string
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Unused React property defined in interface: somethingElse]
}
export const SpanWithText: React.SFC<Props> = (p: Props) => (
<span>{p.text}</span>
)

Просмотреть файл

@ -0,0 +1,37 @@
import React = require('react');
interface Props {
readonly text: string
readonly highlight: ReadonlyArray<number>
readonly somethingElse: string
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Unused React property defined in interface: somethingElse]
}
export const HighlightText: React.SFC<Props> = (props) => (
<span>
{
props.text
.split('')
.map((ch, i): [string, boolean] => [ch, props.highlight.includes(i)])
.concat([['', false]])
.reduce(
(state, [ch, matched], i, arr) => {
if (matched === state.matched && i < arr.length - 1) {
state.str += ch
} else {
const Component = state.matched ? 'mark' : 'span'
state.result.push(<Component key={i}>{state.str}</Component>)
state.str = ch
state.matched = matched
}
return state
},
{
matched: false,
str: '',
result: new Array<React.ReactElement<any>>(),
}
).result
}
</span>
)

Просмотреть файл

@ -0,0 +1,40 @@
import React = require('react');
interface Props {
readonly text: string
readonly highlight: ReadonlyArray<number>
readonly somethingElse: string
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Unused React property defined in interface: somethingElse]
}
export const HighlightText: React.SFC<Props> = ({
text,
highlight,
}) => (
<span>
{
text
.split('')
.map((ch, i): [string, boolean] => [ch, highlight.includes(i)])
.concat([['', false]])
.reduce(
(state, [ch, matched], i, arr) => {
if (matched === state.matched && i < arr.length - 1) {
state.str += ch
} else {
const Component = state.matched ? 'mark' : 'span'
state.result.push(<Component key={i}>{state.str}</Component>)
state.str = ch
state.matched = matched
}
return state
},
{
matched: false,
str: '',
result: new Array<React.ReactElement<any>>(),
}
).result
}
</span>
)

Просмотреть файл

@ -0,0 +1,9 @@
import React = require('react');
export const SpanWithText: React.SFC<{
readonly text: string,
readonly somethingElse: string,
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Unused React property defined in interface: somethingElse]
}> = (prop) => (
<span>{prop.text}</span>
)

Просмотреть файл

@ -0,0 +1,11 @@
import React = require('react');
interface Props {
readonly text: string
readonly somethingElse: string
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Unused React property defined in interface: somethingElse]
}
export const SpanWithText: React.SFC<Props> = (props: Props) => (
<span>{props.text}</span>
)

Просмотреть файл

@ -0,0 +1,5 @@
{
"rules": {
"react-unused-props-and-state": true
}
}