From 1371f71db8284ac087e115b705bfc6ef5ab6b6fe Mon Sep 17 00:00:00 2001 From: Jake Leventhal Date: Thu, 16 Sep 2021 08:37:13 -0400 Subject: [PATCH] [New] add `no-unused-class-component-methods` Co-authored-by: meowtec Co-authored-by: Jake Leventhal --- CHANGELOG.md | 6 + README.md | 1 + .../no-unused-class-component-methods.md | 34 + index.js | 1 + .../no-unused-class-component-methods.js | 244 ++++++ .../no-unused-class-component-methods.js | 804 ++++++++++++++++++ 6 files changed, 1090 insertions(+) create mode 100644 docs/rules/no-unused-class-component-methods.md create mode 100644 lib/rules/no-unused-class-component-methods.js create mode 100644 tests/lib/rules/no-unused-class-component-methods.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 703ac049f5..b9cdd6b7e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/README.md b/README.md index 6c5af3db27..09b4d59131 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/docs/rules/no-unused-class-component-methods.md b/docs/rules/no-unused-class-component-methods.md new file mode 100644 index 0000000000..62873e6a53 --- /dev/null +++ b/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: + +```jsx +class Foo extends React.Component { + static getDerivedStateFromError(error) { + return { hasError: true }; + } + action() {} + componentDidMount() { + this.action(); + } + render() { + return null; + } +} +}); +``` diff --git a/index.js b/index.js index 198af7b37f..700780b3a9 100644 --- a/index.js +++ b/index.js @@ -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'), diff --git a/lib/rules/no-unused-class-component-methods.js b/lib/rules/no-unused-class-component-methods.js new file mode 100644 index 0000000000..4e27cec6e9 --- /dev/null +++ b/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); + } + }); + } + } + }; + }) +}; diff --git a/tests/lib/rules/no-unused-class-component-methods.js b/tests/lib/rules/no-unused-class-component-methods.js new file mode 100644 index 0000000000..ad46ad3765 --- /dev/null +++ b/tests/lib/rules/no-unused-class-component-methods.js @@ -0,0 +1,804 @@ +/** + * @fileoverview Prevent declaring unused methods and properties of component class + * @author Paweł Nowak, Berton Zhu + */ + +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester; +const rule = require('../../../lib/rules/no-unused-class-component-methods'); +const parsers = require('../../helpers/parsers'); + +const parserOptions = { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + jsx: true + } +}; + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({parserOptions}); +ruleTester.run('no-unused-class-component-methods', rule, { + valid: [ + { + code: ` + class SmockTestForTypeOfNullError extends React.Component { + handleClick() {} + foo; + render() { + let a; + return ; + } + } + `, + parser: parsers.BABEL_ESLINT + }, + { + code: ` + class Foo extends React.Component { + handleClick() {} + render() { + return ; + } + } + ` + }, + { + code: ` + var Foo = createReactClass({ + handleClick() {}, + render() { + return ; + }, + }) + ` + }, + { + code: ` + class Foo extends React.Component { + action() {} + componentDidMount() { + this.action(); + } + render() { + return null; + } + } + ` + }, + { + code: ` + var Foo = createReactClass({ + action() {}, + componentDidMount() { + this.action(); + }, + render() { + return null; + }, + }) + ` + }, + { + code: ` + class Foo extends React.Component { + action() {} + componentDidMount() { + const action = this.action; + action(); + } + render() { + return null; + } + } + ` + }, + { + code: ` + class Foo extends React.Component { + getValue() {} + componentDidMount() { + const action = this.getValue(); + } + render() { + return null; + } + } + ` + }, + { + code: ` + class Foo extends React.Component { + handleClick = () => {} + render() { + return ; + } + } + `, + parser: parsers.BABEL_ESLINT + }, + { + code: ` + class Foo extends React.Component { + renderContent() {} + render() { + return
{this.renderContent()}
; + } + } + ` + }, + { + code: ` + class Foo extends React.Component { + renderContent() {} + render() { + return ( +
+
{this.renderContent()}
; +
+ ); + } + } + ` + }, + { + code: ` + class Foo extends React.Component { + property = {} + render() { + return
Example
; + } + } + `, + parser: parsers.BABEL_ESLINT + }, + { + code: ` + class Foo extends React.Component { + action = () => {} + anotherAction = () => { + this.action(); + } + render() { + return ; + } + } + `, + parser: parsers.BABEL_ESLINT + }, + { + code: ` + class Foo extends React.Component { + action = () => {} + anotherAction = () => this.action() + render() { + return ; + } + } + `, + parser: parsers.BABEL_ESLINT + }, + { + code: ` + class Foo extends React.Component { + getValue = () => {} + value = this.getValue() + render() { + return this.value; + } + } + `, + parser: parsers.BABEL_ESLINT + }, + { + code: ` + class Foo { + action = () => {} + anotherAction = () => this.action() + } + `, + parser: parsers.BABEL_ESLINT + }, + { + code: ` + class Foo extends React.Component { + action = async () => {} + render() { + return ; + } + } + `, + parser: parsers.BABEL_ESLINT + }, + { + code: ` + class Foo extends React.Component { + async action() { + console.log('error'); + } + render() { + return ; + } + } + `, + parser: parsers.BABEL_ESLINT + }, + { + code: ` + class Foo extends React.Component { + * action() { + console.log('error'); + } + render() { + return ; + } + } + `, + parser: parsers.BABEL_ESLINT + }, + { + code: ` + class Foo extends React.Component { + async * action() { + console.log('error'); + } + render() { + return ; + } + } + `, + parser: parsers.BABEL_ESLINT + }, + { + code: ` + class Foo extends React.Component { + action = function() { + console.log('error'); + } + render() { + return ; + } + } + `, + parser: parsers.BABEL_ESLINT + }, + `class ClassAssignPropertyInMethodTest extends React.Component { + constructor() { + this.foo = 3;; + } + render() { + return ; + } + }`, + { + code: ` + class ClassPropertyTest extends React.Component { + foo; + render() { + return ; + } + } + `, + parser: parsers.BABEL_ESLINT + }, + { + code: ` + class ClassPropertyTest extends React.Component { + foo = a; + render() { + return ; + } + } + `, + parser: parsers.BABEL_ESLINT + }, + { + code: ` + class Foo extends React.Component { + ['foo'] = a; + render() { + return ; + } + } + `, + parser: parsers.BABEL_ESLINT + }, + { + code: ` + class Foo extends React.Component { + ['foo']; + render() { + return ; + } + } + `, + parser: parsers.BABEL_ESLINT + }, + { + code: ` + class ClassComputedTemplatePropertyTest extends React.Component { + [\`foo\`] = a; + render() { + return ; + } + } + `, + parser: parsers.BABEL_ESLINT + }, + { + code: ` + class ClassComputedTemplatePropertyTest extends React.Component { + state = {} + render() { + return
; + } + } + `, + parser: parsers.BABEL_ESLINT + }, + `class ClassLiteralComputedMemberTest extends React.Component { + ['foo']() {} + render() { + return ; + } + }`, + `class ClassComputedTemplateMemberTest extends React.Component { + [\`foo\`]() {} + render() { + return ; + } + }`, + `class ClassUseAssignTest extends React.Component { + foo() {} + render() { + this.foo; + return ; + } + }`, + `class ClassUseAssignTest extends React.Component { + foo() {} + render() { + const { foo } = this; + return ; + } + }`, + `class ClassUseDestructuringTest extends React.Component { + foo() {} + render() { + const { foo } = this; + return ; + } + }`, + `class ClassUseDestructuringTest extends React.Component { + ['foo']() {} + render() { + const { 'foo': bar } = this; + return ; + } + }`, + `class ClassComputedMemberTest extends React.Component { + [foo]() {} + render() { + return ; + } + }`, + `class ClassWithLifecyleTest extends React.Component { + constructor(props) { + super(props); + } + static getDerivedStateFromProps() {} + componentWillMount() {} + UNSAFE_componentWillMount() {} + componentDidMount() {} + componentWillReceiveProps() {} + UNSAFE_componentWillReceiveProps() {} + shouldComponentUpdate() {} + componentWillUpdate() {} + UNSAFE_componentWillUpdate() {} + static getSnapshotBeforeUpdate() {} + componentDidUpdate() {} + componentDidCatch() {} + componentWillUnmount() {} + render() { + return ; + } + }`, + `var ClassWithLifecyleTest = createReactClass({ + mixins: [], + constructor(props) { + }, + getDefaultProps() { + return {} + }, + getInitialState: function() { + return {x: 0}; + }, + componentWillMount() {}, + UNSAFE_componentWillMount() {}, + componentDidMount() {}, + componentWillReceiveProps() {}, + UNSAFE_componentWillReceiveProps() {}, + shouldComponentUpdate() {}, + componentWillUpdate() {}, + UNSAFE_componentWillUpdate() {}, + componentDidUpdate() {}, + componentDidCatch() {}, + componentWillUnmount() {}, + render() { + return ; + }, + })` + ], + + invalid: [ + { + code: ` + class Foo extends React.Component { + getDerivedStateFromProps() {} + render() { + return
Example
; + } + } + `, + errors: [{ + message: 'Unused method or property "getDerivedStateFromProps" of class "Foo"', + line: 3, + column: 11 + }] + }, + { + code: ` + class Foo extends React.Component { + property = {} + render() { + return
Example
; + } + } + `, + parser: parsers.BABEL_ESLINT, + errors: [{ + message: 'Unused method or property "property" of class "Foo"', + line: 3, + column: 12 + }] + }, + { + code: ` + class Foo extends React.Component { + handleClick() {} + render() { + return null; + } + } + `, + errors: [{ + message: 'Unused method or property "handleClick" of class "Foo"', + line: 3, + column: 12 + }] + }, + { + code: ` + var Foo = createReactClass({ + handleClick() {}, + render() { + return null; + }, + }) + `, + errors: [{ + message: 'Unused method or property "handleClick"', + line: 3, + column: 12 + }] + }, + { + code: ` + var Foo = createReactClass({ + a: 3, + render() { + return null; + }, + }) + `, + errors: [{ + message: 'Unused method or property "a"', + line: 3, + column: 12 + }] + }, + { + code: ` + class Foo extends React.Component { + handleScroll() {} + handleClick() {} + render() { + return null; + } + } + `, + errors: [{ + message: 'Unused method or property "handleScroll" of class "Foo"', + line: 3, + column: 12 + }, { + message: 'Unused method or property "handleClick" of class "Foo"', + line: 4, + column: 12 + }] + }, + { + code: ` + class Foo extends React.Component { + handleClick = () => {} + render() { + return null; + } + } + `, + parser: parsers.BABEL_ESLINT, + errors: [{ + message: 'Unused method or property "handleClick" of class "Foo"', + line: 3, + column: 12 + }] + }, + { + code: ` + class Foo extends React.Component { + action = async () => {} + render() { + return null; + } + } + `, + parser: parsers.BABEL_ESLINT, + errors: [{ + message: 'Unused method or property "action" of class "Foo"', + line: 3, + column: 12 + }] + }, + { + code: ` + class Foo extends React.Component { + async action() { + console.log('error'); + } + render() { + return null; + } + } + `, + parser: parsers.BABEL_ESLINT, + errors: [{ + message: 'Unused method or property "action" of class "Foo"', + line: 3, + column: 18 + }] + }, + { + code: ` + class Foo extends React.Component { + * action() { + console.log('error'); + } + render() { + return null; + } + } + `, + parser: parsers.BABEL_ESLINT, + errors: [{ + message: 'Unused method or property "action" of class "Foo"', + line: 3, + column: 14 + }] + }, + { + code: ` + class Foo extends React.Component { + async * action() { + console.log('error'); + } + render() { + return null; + } + } + `, + parser: parsers.BABEL_ESLINT, + errors: [{ + message: 'Unused method or property "action" of class "Foo"', + line: 3, + column: 20 + }] + }, + { + code: ` + class Foo extends React.Component { + getInitialState() {} + render() { + return null; + } + } + `, + parser: parsers.BABEL_ESLINT, + errors: [{ + message: 'Unused method or property "getInitialState" of class "Foo"', + line: 3, + column: 12 + }] + }, + { + code: ` + class Foo extends React.Component { + action = function() { + console.log('error'); + } + render() { + return null; + } + } + `, + parser: parsers.BABEL_ESLINT, + errors: [{ + message: 'Unused method or property "action" of class "Foo"', + line: 3, + column: 12 + }] + }, + { + code: ` + class ClassAssignPropertyInMethodTest extends React.Component { + constructor() { + this.foo = 3; + } + render() { + return ; + } + } + `, + errors: [{ + message: 'Unused method or property "foo" of class "ClassAssignPropertyInMethodTest"', + line: 4, + column: 19 + }] + }, + { + code: ` + class Foo extends React.Component { + foo; + render() { + return ; + } + } + `, + errors: [{ + message: 'Unused method or property "foo" of class "Foo"', + line: 3, + column: 12 + }], + parser: parsers.BABEL_ESLINT + }, + { + code: ` + class Foo extends React.Component { + foo = a; + render() { + return ; + } + } + `, + errors: [{ + message: 'Unused method or property "foo" of class "Foo"', + line: 3, + column: 12 + }], + parser: parsers.BABEL_ESLINT + }, + { + code: ` + class Foo extends React.Component { + ['foo']; + render() { + return ; + } + } + `, + errors: [{ + message: 'Unused method or property "foo" of class "Foo"', + line: 3, + column: 13 + }], + parser: parsers.BABEL_ESLINT + }, + { + code: ` + class Foo extends React.Component { + ['foo'] = a; + render() { + return ; + } + } + `, + errors: [{ + message: 'Unused method or property "foo" of class "Foo"', + line: 3, + column: 13 + }], + parser: parsers.BABEL_ESLINT + }, + { + code: ` + class Foo extends React.Component { + foo = a; + render() { + return ; + } + } + `, + errors: [{ + message: 'Unused method or property "foo" of class "Foo"', + line: 3, + column: 12 + }], + parser: parsers.BABEL_ESLINT + }, + { + code: ` + class Foo extends React.Component { + private foo; + render() { + return ; + } + } + `, + errors: [{ + message: 'Unused method or property "foo" of class "Foo"', + line: 3, + column: 20 + }], + parser: parsers.TYPESCRIPT_ESLINT + }, + { + code: ` + class Foo extends React.Component { + private foo() {} + render() { + return ; + } + } + `, + errors: [{ + message: 'Unused method or property "foo" of class "Foo"', + line: 3, + column: 20 + }], + parser: parsers.TYPESCRIPT_ESLINT + }, + { + code: ` + class Foo extends React.Component { + private foo = 3; + render() { + return ; + } + } + `, + errors: [{ + message: 'Unused method or property "foo" of class "Foo"', + line: 3, + column: 20 + }], + parser: parsers.TYPESCRIPT_ESLINT + } + ] +});