[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:
Родитель
17080557c1
Коммит
3d5526ebf3
|
@ -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
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче