hook-use-state.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. /**
  2. * @fileoverview Ensure symmetric naming of useState hook value and setter variables
  3. * @author Duncan Beevers
  4. */
  5. 'use strict';
  6. const Components = require('../util/Components');
  7. const docsUrl = require('../util/docsUrl');
  8. const report = require('../util/report');
  9. const getMessageData = require('../util/message');
  10. // ------------------------------------------------------------------------------
  11. // Rule Definition
  12. // ------------------------------------------------------------------------------
  13. function isNodeDestructuring(node) {
  14. return node && (node.type === 'ArrayPattern' || node.type === 'ObjectPattern');
  15. }
  16. const messages = {
  17. useStateErrorMessage: 'useState call is not destructured into value + setter pair',
  18. useStateErrorMessageOrAddOption: 'useState call is not destructured into value + setter pair (you can allow destructuring by enabling "allowDestructuredState" option)',
  19. suggestPair: 'Destructure useState call into value + setter pair',
  20. suggestMemo: 'Replace useState call with useMemo',
  21. };
  22. module.exports = {
  23. meta: {
  24. docs: {
  25. description: 'Ensure destructuring and symmetric naming of useState hook value and setter variables',
  26. category: 'Best Practices',
  27. recommended: false,
  28. url: docsUrl('hook-use-state'),
  29. },
  30. messages,
  31. schema: [{
  32. type: 'object',
  33. properties: {
  34. allowDestructuredState: {
  35. default: false,
  36. type: 'boolean',
  37. },
  38. },
  39. additionalProperties: false,
  40. }],
  41. type: 'suggestion',
  42. hasSuggestions: true,
  43. },
  44. create: Components.detect((context, components, util) => {
  45. const configuration = context.options[0] || {};
  46. const allowDestructuredState = configuration.allowDestructuredState || false;
  47. return {
  48. CallExpression(node) {
  49. const isImmediateReturn = node.parent
  50. && node.parent.type === 'ReturnStatement';
  51. if (isImmediateReturn || !util.isReactHookCall(node, ['useState'])) {
  52. return;
  53. }
  54. const isDestructuringDeclarator = node.parent
  55. && node.parent.type === 'VariableDeclarator'
  56. && node.parent.id.type === 'ArrayPattern';
  57. if (!isDestructuringDeclarator) {
  58. report(
  59. context,
  60. messages.useStateErrorMessage,
  61. 'useStateErrorMessage',
  62. {
  63. node,
  64. suggest: false,
  65. }
  66. );
  67. return;
  68. }
  69. const variableNodes = node.parent.id.elements;
  70. const valueVariable = variableNodes[0];
  71. const setterVariable = variableNodes[1];
  72. const isOnlyValueDestructuring = isNodeDestructuring(valueVariable) && !isNodeDestructuring(setterVariable);
  73. if (allowDestructuredState && isOnlyValueDestructuring) {
  74. return;
  75. }
  76. const valueVariableName = valueVariable
  77. ? valueVariable.name
  78. : undefined;
  79. const setterVariableName = setterVariable
  80. ? setterVariable.name
  81. : undefined;
  82. const caseCandidateMatch = valueVariableName ? valueVariableName.match(/(^[a-z]+)(.*)/) : undefined;
  83. const upperCaseCandidatePrefix = caseCandidateMatch ? caseCandidateMatch[1] : undefined;
  84. const caseCandidateSuffix = caseCandidateMatch ? caseCandidateMatch[2] : undefined;
  85. const expectedSetterVariableNames = upperCaseCandidatePrefix ? [
  86. `set${upperCaseCandidatePrefix.charAt(0).toUpperCase()}${upperCaseCandidatePrefix.slice(1)}${caseCandidateSuffix}`,
  87. `set${upperCaseCandidatePrefix.toUpperCase()}${caseCandidateSuffix}`,
  88. ] : [];
  89. const isSymmetricGetterSetterPair = valueVariable
  90. && setterVariable
  91. && expectedSetterVariableNames.indexOf(setterVariableName) !== -1
  92. && variableNodes.length === 2;
  93. if (!isSymmetricGetterSetterPair) {
  94. const suggestions = [
  95. Object.assign(
  96. getMessageData('suggestPair', messages.suggestPair),
  97. {
  98. fix(fixer) {
  99. if (expectedSetterVariableNames.length > 0) {
  100. return fixer.replaceTextRange(
  101. node.parent.id.range,
  102. `[${valueVariableName}, ${expectedSetterVariableNames[0]}]`
  103. );
  104. }
  105. },
  106. }
  107. ),
  108. ];
  109. const defaultReactImports = components.getDefaultReactImports();
  110. const defaultReactImportSpecifier = defaultReactImports
  111. ? defaultReactImports[0]
  112. : undefined;
  113. const defaultReactImportName = defaultReactImportSpecifier
  114. ? defaultReactImportSpecifier.local.name
  115. : undefined;
  116. const namedReactImports = components.getNamedReactImports();
  117. const useStateReactImportSpecifier = namedReactImports
  118. ? namedReactImports.find((specifier) => specifier.imported.name === 'useState')
  119. : undefined;
  120. const isSingleGetter = valueVariable && variableNodes.length === 1;
  121. const isUseStateCalledWithSingleArgument = node.arguments.length === 1;
  122. if (isSingleGetter && isUseStateCalledWithSingleArgument) {
  123. const useMemoReactImportSpecifier = namedReactImports
  124. && namedReactImports.find((specifier) => specifier.imported.name === 'useMemo');
  125. let useMemoCode;
  126. if (useMemoReactImportSpecifier) {
  127. useMemoCode = useMemoReactImportSpecifier.local.name;
  128. } else if (defaultReactImportName) {
  129. useMemoCode = `${defaultReactImportName}.useMemo`;
  130. } else {
  131. useMemoCode = 'useMemo';
  132. }
  133. suggestions.unshift(Object.assign(
  134. getMessageData('suggestMemo', messages.suggestMemo),
  135. {
  136. fix: (fixer) => [
  137. // Add useMemo import, if necessary
  138. useStateReactImportSpecifier
  139. && (!useMemoReactImportSpecifier || defaultReactImportName)
  140. && fixer.insertTextAfter(useStateReactImportSpecifier, ', useMemo'),
  141. // Convert single-value destructure to simple assignment
  142. fixer.replaceTextRange(node.parent.id.range, valueVariableName),
  143. // Convert useState call to useMemo + arrow function + dependency array
  144. fixer.replaceTextRange(
  145. node.range,
  146. `${useMemoCode}(() => ${context.getSourceCode().getText(node.arguments[0])}, [])`
  147. ),
  148. ].filter(Boolean),
  149. }
  150. ));
  151. }
  152. if (isOnlyValueDestructuring) {
  153. report(
  154. context,
  155. messages.useStateErrorMessageOrAddOption,
  156. 'useStateErrorMessageOrAddOption',
  157. {
  158. node: node.parent.id,
  159. suggest: false,
  160. }
  161. );
  162. return;
  163. }
  164. report(
  165. context,
  166. messages.useStateErrorMessage,
  167. 'useStateErrorMessage',
  168. {
  169. node: node.parent.id,
  170. suggest: suggestions,
  171. }
  172. );
  173. }
  174. },
  175. };
  176. }),
  177. };