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

[New] add no-unused-class-component-methods #2239

Merged
merged 1 commit into from Sep 30, 2021
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Expand Up @@ -5,6 +5,11 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel

## Unreleased

### Added
* [`no-unused-class-component-methods`]: Handle unused class component methods ([#2166][] @jakeleventhal @pawelnvk)

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

## [7.26.1] - 2021.09.29

### Fixed
Expand Down Expand Up @@ -3482,6 +3487,7 @@ If you're still not using React 15 you can keep the old behavior by setting the
[`no-unknown-property`]: docs/rules/no-unknown-property.md
[`no-unsafe`]: docs/rules/no-unsafe.md
[`no-unstable-nested-components`]: docs/rules/no-unstable-nested-components.md
[`no-unused-class-component-methods`]: docs/rules/no-unused-class-component-methods.md
[`no-unused-prop-types`]: docs/rules/no-unused-prop-types.md
[`no-unused-state`]: docs/rules/no-unused-state.md
[`no-will-update-set-state`]: docs/rules/no-will-update-set-state.md
Expand Down
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -156,6 +156,7 @@ Enable the rules that you would like to use.
| ✔ | 🔧 | [react/no-unknown-property](docs/rules/no-unknown-property.md) | Prevent usage of unknown DOM property |
| | | [react/no-unsafe](docs/rules/no-unsafe.md) | Prevent usage of unsafe lifecycle methods |
| | | [react/no-unstable-nested-components](docs/rules/no-unstable-nested-components.md) | Prevent creating unstable components inside components |
| | | [react/no-unused-class-component-methods](docs/rules/no-unused-class-component-methods.md) | Prevent declaring unused methods of component class |
| | | [react/no-unused-prop-types](docs/rules/no-unused-prop-types.md) | Prevent definitions of unused prop types |
| | | [react/no-unused-state](docs/rules/no-unused-state.md) | Prevent definition of unused state fields |
| | | [react/no-will-update-set-state](docs/rules/no-will-update-set-state.md) | Prevent usage of setState in componentWillUpdate |
Expand Down
34 changes: 34 additions & 0 deletions docs/rules/no-unused-class-component-methods.md
@@ -0,0 +1,34 @@
# Prevent declaring unused methods of component class (react/no-unused-class-component-methods)

Warns you if you have defined a method or property but it is never being used anywhere.

## Rule Details

The following patterns are considered warnings:

```jsx
class Foo extends React.Component {
handleClick() {}
render() {
return null;
}
}
```

The following patterns are **not** considered warnings:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's add some unused static methods here, so it's clear.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @meowtec

should all static methods be list here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there more than one? Either way we'd only need at least one as an example.


```jsx
class Foo extends React.Component {
static getDerivedStateFromError(error) {
return { hasError: true };
}
action() {}
componentDidMount() {
this.action();
}
render() {
return null;
}
}
});
```
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -78,6 +78,7 @@ const allRules = {
'no-unknown-property': require('./lib/rules/no-unknown-property'),
'no-unsafe': require('./lib/rules/no-unsafe'),
'no-unstable-nested-components': require('./lib/rules/no-unstable-nested-components'),
'no-unused-class-component-methods': require('./lib/rules/no-unused-class-component-methods'),
'no-unused-prop-types': require('./lib/rules/no-unused-prop-types'),
'no-unused-state': require('./lib/rules/no-unused-state'),
'no-will-update-set-state': require('./lib/rules/no-will-update-set-state'),
Expand Down
244 changes: 244 additions & 0 deletions lib/rules/no-unused-class-component-methods.js
@@ -0,0 +1,244 @@
/**
* @fileoverview Prevent declaring unused methods and properties of component class
* @author Paweł Nowak, Berton Zhu
*/

'use strict';

const Components = require('../util/Components');
const docsUrl = require('../util/docsUrl');

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

const LIFECYCLE_METHODS = new Set([
'constructor',
'componentDidCatch',
'componentDidMount',
'componentDidUpdate',
'componentWillMount',
'componentWillReceiveProps',
'componentWillUnmount',
'componentWillUpdate',
'getSnapshotBeforeUpdate',
'render',
'shouldComponentUpdate',
'UNSAFE_componentWillMount',
'UNSAFE_componentWillReceiveProps',
'UNSAFE_componentWillUpdate'
]);

const ES6_LIFECYCLE = new Set([
'state'
]);

const ES5_LIFECYCLE = new Set([
'getInitialState',
'getDefaultProps',
'mixins'
]);

function isKeyLiteralLike(node, property) {
return property.type === 'Literal'
|| (property.type === 'TemplateLiteral' && property.expressions.length === 0)
|| (node.computed === false && property.type === 'Identifier');
}

// Descend through all wrapping TypeCastExpressions and return the expression
// that was cast.
function uncast(node) {
while (node.type === 'TypeCastExpression') {
node = node.expression;
}
return node;
}

// Return the name of an identifier or the string value of a literal. Useful
// anywhere that a literal may be used as a key (e.g., member expressions,
// method definitions, ObjectExpression property keys).
function getName(node) {
node = uncast(node);
const type = node.type;

if (type === 'Identifier') {
return node.name;
}
if (type === 'Literal') {
return String(node.value);
}
if (type === 'TemplateLiteral' && node.expressions.length === 0) {
return node.quasis[0].value.raw;
}
return null;
}

function isThisExpression(node) {
return uncast(node).type === 'ThisExpression';
}

function getInitialClassInfo(node, isClass) {
return {
classNode: node,
isClass,
// Set of nodes where properties were defined.
properties: new Set(),

// Set of names of properties that we've seen used.
usedProperties: new Set(),

inStatic: false
};
}

module.exports = {
meta: {
docs: {
description: 'Prevent declaring unused methods of component class',
category: 'Best Practices',
recommended: false,
url: docsUrl('no-unused-class-component-methods')
},
schema: [
{
type: 'object',
additionalProperties: false
}
]
},

create: Components.detect((context, components, utils) => {
let classInfo = null;

// Takes an ObjectExpression node and adds all named Property nodes to the
// current set of properties.
function addProperty(node) {
classInfo.properties.add(node);
}

// Adds the name of the given node as a used property if the node is an
// Identifier or a Literal. Other node types are ignored.
function addUsedProperty(node) {
const name = getName(node);
if (name) {
classInfo.usedProperties.add(name);
}
}

function reportUnusedProperties() {
// Report all unused properties.
for (const node of classInfo.properties) { // eslint-disable-line no-restricted-syntax
const name = getName(node);
if (
!classInfo.usedProperties.has(name)
&& !LIFECYCLE_METHODS.has(name)
&& (classInfo.isClass ? !ES6_LIFECYCLE.has(name) : !ES5_LIFECYCLE.has(name))
) {
const className = (classInfo.classNode.id && classInfo.classNode.id.name) || '';

context.report({
node,
message: `Unused method or property "${name}"${className ? ` of class "${className}"` : ''}`
});
}
}
}

function exitMethod() {
if (!classInfo || !classInfo.inStatic) {
return;
}

classInfo.inStatic = false;
}

return {
ClassDeclaration(node) {
if (utils.isES6Component(node)) {
classInfo = getInitialClassInfo(node, true);
}
},

ObjectExpression(node) {
if (utils.isES5Component(node)) {
classInfo = getInitialClassInfo(node, false);
}
},

'ClassDeclaration:exit'() {
if (!classInfo) {
return;
}
reportUnusedProperties();
classInfo = null;
},

'ObjectExpression:exit'(node) {
if (!classInfo || classInfo.classNode !== node) {
return;
}
reportUnusedProperties();
classInfo = null;
},

Property(node) {
if (!classInfo || classInfo.classNode !== node.parent) {
return;
}

if (isKeyLiteralLike(node, node.key)) {
addProperty(node.key);
}
},

'ClassProperty, MethodDefinition'(node) {
if (!classInfo) {
return;
}

if (node.static) {
classInfo.inStatic = true;
return;
}

if (isKeyLiteralLike(node, node.key)) {
addProperty(node.key);
}
},

'ClassProperty:exit': exitMethod,
'MethodDefinition:exit': exitMethod,

MemberExpression(node) {
if (!classInfo || classInfo.inStatic) {
return;
}

if (isThisExpression(node.object) && isKeyLiteralLike(node, node.property)) {
if (node.parent.type === 'AssignmentExpression' && node.parent.left === node) {
// detect `this.property = xxx`
addProperty(node.property);
} else {
// detect `this.property()`, `x = this.property`, etc.
addUsedProperty(node.property);
}
}
},

VariableDeclarator(node) {
if (!classInfo || classInfo.inStatic) {
return;
}

// detect `{ foo, bar: baz } = this`
if (node.init && isThisExpression(node.init) && node.id.type === 'ObjectPattern') {
node.id.properties.forEach((prop) => {
if (prop.type === 'Property' && isKeyLiteralLike(prop, prop.key)) {
addUsedProperty(prop.key);
}
});
}
}
};
})
};