Skip to content

Commit

Permalink
[New] Symmetric useState hook variable names
Browse files Browse the repository at this point in the history
Ensure two symmetrically-named variables are destructured from useState hook calls
  • Loading branch information
duncanbeevers committed Feb 10, 2021
1 parent 05d35ad commit 01e6a4d
Show file tree
Hide file tree
Showing 5 changed files with 298 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -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 |
Expand Down
23 changes: 23 additions & 0 deletions 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();
```
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -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'),
Expand Down
101 changes: 101 additions & 0 deletions 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
});
}
}
};
}
};
172 changes: 172 additions & 0 deletions 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<string>()',
parser: parsers.TYPESCRIPT_ESLINT
},
{
code: 'const [color, setColor] = useState<string>(\'#ffffff\')',
parser: parsers.TYPESCRIPT_ESLINT
}
].concat(parsers.TS([
{
code: 'const [color, setColor] = useState<string>()',
parser: parsers['@TYPESCRIPT_ESLINT']
},
{
code: 'const [color, setColor] = useState<string>(\'#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<string>()',
errors: [{
message: 'setState call is not destructured into value + setter pair'
}],
output: 'const [color, setColor] = useState<string>()',
parser: parsers.TYPESCRIPT_ESLINT
}
].concat(
parsers.TS([
{
code: 'const [color, setFlavor] = useState<string>()',
errors: [{
message: 'setState call is not destructured into value + setter pair'
}],
output: 'const [color, setColor] = useState<string>()',
parser: parsers['@TYPESCRIPT_ESLINT']
}
])
)
};

ruleTester.run('hook-set-state-names', rule, tests);

0 comments on commit 01e6a4d

Please sign in to comment.