From 01e6a4ded027734765685ffabe5e34e140a3324d Mon Sep 17 00:00:00 2001 From: Duncan Beevers Date: Wed, 9 Dec 2020 09:52:43 -0800 Subject: [PATCH] [New] Symmetric useState hook variable names Ensure two symmetrically-named variables are destructured from useState hook calls --- README.md | 1 + docs/rules/hook-use-state.md | 23 ++++ index.js | 1 + lib/rules/hook-use-state.js | 101 ++++++++++++++++++ tests/lib/rules/hook-use-state.js | 172 ++++++++++++++++++++++++++++++ 5 files changed, 298 insertions(+) create mode 100644 docs/rules/hook-use-state.md create mode 100644 lib/rules/hook-use-state.js create mode 100644 tests/lib/rules/hook-use-state.js diff --git a/README.md b/README.md index 1a25c9d55f..a1627b6f3c 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ Enable the rules that you would like to use. | | | [react/forbid-foreign-prop-types](docs/rules/forbid-foreign-prop-types.md) | Forbid using another component's propTypes | | | | [react/forbid-prop-types](docs/rules/forbid-prop-types.md) | Forbid certain propTypes | | | 🔧 | [react/function-component-definition](docs/rules/function-component-definition.md) | Standardize the way function component get defined | +| | 🔧 | [react/hook-use-state](docs/rules/hook-use-state.md) | Ensure symmetric naming of useState hook value and setter variables | | | | [react/no-access-state-in-setstate](docs/rules/no-access-state-in-setstate.md) | Reports when this.state is accessed within setState | | | | [react/no-adjacent-inline-elements](docs/rules/no-adjacent-inline-elements.md) | Prevent adjacent inline elements not separated by whitespace. | | | | [react/no-array-index-key](docs/rules/no-array-index-key.md) | Prevent usage of Array index in keys | diff --git a/docs/rules/hook-use-state.md b/docs/rules/hook-use-state.md new file mode 100644 index 0000000000..17946a0ec2 --- /dev/null +++ b/docs/rules/hook-use-state.md @@ -0,0 +1,23 @@ +# Ensure destructuring and symmetric naming of useState hook value and setter variables (react/hook-use-state) + +**Fixable:** In some cases, this rule is automatically fixable using the `--fix` flag on the command line. + +## Rule Details + +This rule checks whether the value and setter variables destructured from a `React.useState()` call are named symmetrically. + +Examples of **incorrect** code for this rule: + +```js +const useStateResult = React.useState(); +``` + +```js +const [color, updateColor] = React.useState(); +``` + +Examples of **correct** code for this rule: + +```js +const [color, setColor] = React.useState(); +``` diff --git a/index.js b/index.js index 8edb1177f1..25910dd93e 100644 --- a/index.js +++ b/index.js @@ -16,6 +16,7 @@ const allRules = { 'forbid-foreign-prop-types': require('./lib/rules/forbid-foreign-prop-types'), 'forbid-prop-types': require('./lib/rules/forbid-prop-types'), 'function-component-definition': require('./lib/rules/function-component-definition'), + 'hook-use-state': require('./lib/rules/hook-use-state'), 'jsx-boolean-value': require('./lib/rules/jsx-boolean-value'), 'jsx-child-element-spacing': require('./lib/rules/jsx-child-element-spacing'), 'jsx-closing-bracket-location': require('./lib/rules/jsx-closing-bracket-location'), diff --git a/lib/rules/hook-use-state.js b/lib/rules/hook-use-state.js new file mode 100644 index 0000000000..1587535a60 --- /dev/null +++ b/lib/rules/hook-use-state.js @@ -0,0 +1,101 @@ +/** + * @fileoverview Ensure symmetric naming of useState hook value and setter variables + * @author Duncan Beevers + */ + +'use strict'; + +const docsUrl = require('../util/docsUrl'); + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +const USE_STATE_ERROR_MESSAGE = 'useStateErrorMessage'; + +module.exports = { + meta: { + docs: { + description: 'Ensure symmetric naming of useState hook value and setter variables', + category: 'Best Practices', + recommended: false, + url: docsUrl('hook-use-state') + }, + fixable: 'code', + messages: { + [USE_STATE_ERROR_MESSAGE]: 'setState call is not destructured into value + setter pair' + }, + schema: [{ + type: 'object', + additionalProperties: false + }] + }, + + create(context) { + return { + CallExpression(node) { + const isReactUseStateCall = ( + node.callee.type === 'MemberExpression' + && node.callee.object.type === 'Identifier' + && node.callee.object.name === 'React' + && node.callee.property.type === 'Identifier' + && node.callee.property.name === 'useState' + ); + + const isUseStateCall = ( + node.callee.type === 'Identifier' + && node.callee.name === 'useState' + ); + + // Ignore unless this is a useState() or React.useState() call. + if (!isReactUseStateCall && !isUseStateCall) { + return; + } + + const isDestructuringDeclarator = ( + node.parent.type === 'VariableDeclarator' + && node.parent.id.type === 'ArrayPattern' + ); + + if (!isDestructuringDeclarator) { + context.report({node, messageId: USE_STATE_ERROR_MESSAGE}); + return; + } + + const variableNodes = node.parent.id.elements; + const valueVariable = variableNodes[0]; + const setterVariable = variableNodes[1]; + + const valueVariableName = valueVariable + ? valueVariable.name + : undefined; + + const setterVariableName = setterVariable + ? setterVariable.name + : undefined; + + const expectedSetterVariableName = valueVariableName ? ( + `set${ + valueVariableName.charAt(0).toUpperCase() + }${valueVariableName.slice(1)}` + ) : undefined; + + if ( + !valueVariable + || !setterVariable + || setterVariableName !== expectedSetterVariableName + || variableNodes.length !== 2 + ) { + context.report({ + node: node.parent.id, + messageId: USE_STATE_ERROR_MESSAGE, + fix: valueVariableName ? (fixer) => fixer.replaceTextRange( + [node.parent.id.range[0], node.parent.id.range[1]], + `[${valueVariableName}, ${expectedSetterVariableName}]` + ) : undefined + }); + } + } + }; + } +}; diff --git a/tests/lib/rules/hook-use-state.js b/tests/lib/rules/hook-use-state.js new file mode 100644 index 0000000000..101c05f4ee --- /dev/null +++ b/tests/lib/rules/hook-use-state.js @@ -0,0 +1,172 @@ +/** + * @fileoverview Ensure symmetric naming of setState hook value and setter variables + * @author Duncan Beevers + */ + +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester; +const rule = require('../../../lib/rules/hook-use-state'); +const parsers = require('../../helpers/parsers'); + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module' + } +}); + +const tests = { + valid: [ + { + code: 'const [color, setColor] = useState()' + }, + { + code: 'const [color, setColor] = useState(\'#ffffff\')' + }, + { + code: 'const [color, setColor] = React.useState()' + }, + { + code: 'const [color1, setColor1] = useState()' + }, + { + code: 'const [color, setColor] = useState()', + parser: parsers.TYPESCRIPT_ESLINT + }, + { + code: 'const [color, setColor] = useState(\'#ffffff\')', + parser: parsers.TYPESCRIPT_ESLINT + } + ].concat(parsers.TS([ + { + code: 'const [color, setColor] = useState()', + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: 'const [color, setColor] = useState(\'#ffffff\')', + parser: parsers['@TYPESCRIPT_ESLINT'] + } + ]) + ), + invalid: [ + { + code: 'useState()', + errors: [{ + message: 'setState call is not destructured into value + setter pair' + }] + }, + { + code: 'const result = useState()', + errors: [{ + message: 'setState call is not destructured into value + setter pair' + }] + }, + { + code: 'const result = React.useState()', + errors: [{ + message: 'setState call is not destructured into value + setter pair' + }] + }, + { + code: 'const [, , extra1] = useState()', + errors: [{ + message: 'setState call is not destructured into value + setter pair' + }] + }, + { + code: 'const [, setColor] = useState()', + errors: [{ + message: 'setState call is not destructured into value + setter pair' + }] + }, + { + code: 'const { color } = useState()', + errors: [{ + message: 'setState call is not destructured into value + setter pair' + }] + }, + { + code: 'const [] = useState()', + errors: [{ + message: 'setState call is not destructured into value + setter pair' + }] + }, + { + code: 'const [, , , ,] = useState()', + errors: [{ + message: 'setState call is not destructured into value + setter pair' + }] + }, + { + code: 'const [color] = useState()', + errors: [{ + message: 'setState call is not destructured into value + setter pair' + }], + output: 'const [color, setColor] = useState()' + }, + { + code: 'const [color, , extra1] = useState()', + errors: [{ + message: 'setState call is not destructured into value + setter pair' + }], + output: 'const [color, setColor] = useState()' + }, + { + code: 'const [color, setColor, extra1, extra2, extra3] = useState()', + errors: [{ + message: 'setState call is not destructured into value + setter pair' + }], + output: 'const [color, setColor] = useState()' + }, + { + code: 'const [, makeColor] = useState()', + errors: [{ + message: 'setState call is not destructured into value + setter pair' + }] + }, + { + code: 'const [color, setFlavor, extraneous] = useState()', + errors: [{ + message: 'setState call is not destructured into value + setter pair' + }], + output: 'const [color, setColor] = useState()' + }, + { + code: 'const [color, setFlavor] = useState()', + errors: [{ + message: 'setState call is not destructured into value + setter pair' + }], + output: 'const [color, setColor] = useState()' + }, + { + code: 'const [color, setFlavor] = useState()', + errors: [{ + message: 'setState call is not destructured into value + setter pair' + }], + output: 'const [color, setColor] = useState()', + parser: parsers.TYPESCRIPT_ESLINT + } + ].concat( + parsers.TS([ + { + code: 'const [color, setFlavor] = useState()', + errors: [{ + message: 'setState call is not destructured into value + setter pair' + }], + output: 'const [color, setColor] = useState()', + parser: parsers['@TYPESCRIPT_ESLINT'] + } + ]) + ) +}; + +ruleTester.run('hook-set-state-names', rule, tests);