From a537eebf830caf349ec99cc088af392198504ee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Nowak?= Date: Sat, 13 Apr 2019 21:00:03 +0200 Subject: [PATCH] Detect unused class component method --- .../no-unused-class-component-methods.md | 31 +++ .../no-unused-class-component-methods.js | 144 +++++++++++ .../no-unused-class-component-methods.js | 237 ++++++++++++++++++ 3 files changed, 412 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/docs/rules/no-unused-class-component-methods.md b/docs/rules/no-unused-class-component-methods.md new file mode 100644 index 0000000000..61cdc14cce --- /dev/null +++ b/docs/rules/no-unused-class-component-methods.md @@ -0,0 +1,31 @@ +# Prevent declaring unused methods of component class (react/no-unused-prop-types) + +Warns you if you have defined a method 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 { + action() {} + componentDidMount() { + this.action(); + } + render() { + return null; + } +} +}); +``` 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..054438d959 --- /dev/null +++ b/lib/rules/no-unused-class-component-methods.js @@ -0,0 +1,144 @@ +/** + * @fileoverview Prevent declaring unused methods of component class + * @author Paweł Nowak + */ + +'use strict'; + +const Components = require('../util/Components'); +const astUtil = require('../util/ast'); +const docsUrl = require('../util/docsUrl'); + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +const internalMethods = [ + 'constructor', + 'componentDidCatch', + 'componentDidMount', + 'componentDidUpdate', + 'componentWillMount', + 'componentWillReceiveProps', + 'componentWillUnmount', + 'componentWillUpdate', + 'getSnapshotBeforeUpdate', + 'render', + 'shouldComponentUpdate', + 'UNSAFE_componentWillMount', + 'UNSAFE_componentWillReceiveProps', + 'UNSAFE_componentWillUpdate', +] + +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) => { + const isNotComponent = node => ( + !utils.isES5Component(node) && + !utils.isES6Component(node) && + !utils.isCreateElement(node) + ); + const filterAllMethods = node => { + const isMethod = node.type === 'MethodDefinition'; + const isArrowFunction = ( + node.type === 'ClassProperty' && + node.value.type === 'ArrowFunctionExpression' + ); + return isMethod || isArrowFunction; + }; + const checkMethods = node => { + if (isNotComponent(node)) return; + const properties = astUtil.getComponentProperties(node); + let methods = properties + .filter(property => ( + filterAllMethods(property) && + !internalMethods.includes(astUtil.getPropertyName(property)) + )); + const getThisExpressions = subnode => { + if (!methods.length) return; + + switch(subnode.type) { + case 'ClassProperty': + case 'JSXAttribute': + case 'MethodDefinition': + getThisExpressions(subnode.value); + break; + case 'ArrowFunctionExpression': + case 'FunctionExpression': + getThisExpressions(subnode.body); + break; + case 'BlockStatement': + subnode.body.forEach(getThisExpressions); + break; + case 'ReturnStatement': + getThisExpressions(subnode.argument); + break; + case 'JSXElement': + getThisExpressions(subnode.openingElement); + subnode.children.forEach(getThisExpressions); + break; + case 'JSXOpeningElement': + subnode.attributes.forEach(getThisExpressions); + break; + case 'JSXExpressionContainer': + case 'ExpressionStatement': + getThisExpressions(subnode.expression); + break; + case 'CallExpression': + getThisExpressions(subnode.callee); + break; + case 'VariableDeclaration': + subnode.declarations.forEach(getThisExpressions); + break; + case 'VariableDeclarator': + getThisExpressions(subnode.init); + break; + case 'MemberExpression': + if (subnode.object.type !== 'ThisExpression') return; + + methods = methods.filter(method => + subnode.property.name !== astUtil.getPropertyName(method) + ); + break; + default: + break; + } + }; + + properties.forEach(getThisExpressions); + + if (!methods.length) return; + + methods.forEach(method => { + context.report({ + node: method, + message: 'Unused method "{{method}}" of class "{{class}}"', + data: { + class: node.id.name, + method: astUtil.getPropertyName(method), + } + }); + }) + } + + return { + ClassDeclaration: checkMethods, + ClassExpression: checkMethods, + ObjectExpression: checkMethods, + }; + }) +}; 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..92cd7942ea --- /dev/null +++ b/tests/lib/rules/no-unused-class-component-methods.js @@ -0,0 +1,237 @@ +/** + * @fileoverview Prevent declaring unused methods of component class + * @author Paweł Nowak + */ +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/no-unused-class-component-methods'); +const RuleTester = require('eslint').RuleTester; + +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 Foo extends React.Component { + handleClick() {} + render() { + return ; + } + } + ` + }, + { + code: ` + class Foo extends React.Component { + 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: '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: 'babel-eslint' + }, + { + code: ` + class Foo extends React.Component { + action = () => {} + anotherAction = () => { + this.action(); + } + render() { + return ; + } + } + `, + parser: 'babel-eslint' + }, + { + code: ` + class Foo extends React.Component { + action = () => {} + anotherAction = () => this.action() + render() { + return ; + } + } + `, + parser: 'babel-eslint' + }, + { + code: ` + class Foo extends React.Component { + getValue = () => {} + value = this.getValue() + render() { + return null; + } + } + `, + parser: 'babel-eslint' + }, + { + code: ` + var Foo = React.createClass({ + action: function () {}, + render: function () { + return ; + } + }); + ` + }, + { + code: ` + class Foo { + action = () => {} + anotherAction = () => this.action() + } + `, + parser: 'babel-eslint' + } + ], + + invalid: [ + { + code: ` + class Foo extends React.Component { + handleClick() {} + render() { + return null; + } + } + `, + errors: [{ + message: 'Unused method "handleClick" of class "Foo"', + line: 3, + column: 11 + }] + }, + { + code: ` + class Foo extends React.Component { + handleScroll() {} + handleClick() {} + render() { + return null; + } + } + `, + errors: [{ + message: 'Unused method "handleScroll" of class "Foo"', + line: 3, + column: 11 + }, { + message: 'Unused method "handleClick" of class "Foo"', + line: 4, + column: 11 + }] + }, + { + code: ` + class Foo extends React.Component { + handleClick = () => {} + render() { + return null; + } + } + `, + parser: 'babel-eslint', + errors: [{ + message: 'Unused method "handleClick" of class "Foo"', + line: 3, + column: 11 + }] + } + ] +});