Skip to content

Commit

Permalink
[New] add no-unused-class-component-methods
Browse files Browse the repository at this point in the history
Co-authored-by: meowtec <[email protected]>
Co-authored-by: Jake Leventhal <[email protected]>
  • Loading branch information
2 people authored and ljharb committed Sep 16, 2021
1 parent 119a7f3 commit 1371f71
Show file tree
Hide file tree
Showing 6 changed files with 1,090 additions and 0 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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:

```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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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);
}
});
}
}
};
})
};
Loading

0 comments on commit 1371f71

Please sign in to comment.