hook-use-state.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  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. // ------------------------------------------------------------------------------
  10. // Rule Definition
  11. // ------------------------------------------------------------------------------
  12. const messages = {
  13. useStateErrorMessage: 'useState call is not destructured into value + setter pair',
  14. };
  15. module.exports = {
  16. meta: {
  17. docs: {
  18. description: 'Ensure destructuring and symmetric naming of useState hook value and setter variables',
  19. category: 'Best Practices',
  20. recommended: false,
  21. url: docsUrl('hook-use-state'),
  22. },
  23. messages,
  24. schema: [],
  25. type: 'suggestion',
  26. hasSuggestions: true,
  27. },
  28. create: Components.detect((context, components, util) => ({
  29. CallExpression(node) {
  30. const isImmediateReturn = node.parent
  31. && node.parent.type === 'ReturnStatement';
  32. if (isImmediateReturn || !util.isReactHookCall(node, ['useState'])) {
  33. return;
  34. }
  35. const isDestructuringDeclarator = node.parent
  36. && node.parent.type === 'VariableDeclarator'
  37. && node.parent.id.type === 'ArrayPattern';
  38. if (!isDestructuringDeclarator) {
  39. report(
  40. context,
  41. messages.useStateErrorMessage,
  42. 'useStateErrorMessage',
  43. { node }
  44. );
  45. return;
  46. }
  47. const variableNodes = node.parent.id.elements;
  48. const valueVariable = variableNodes[0];
  49. const setterVariable = variableNodes[1];
  50. const valueVariableName = valueVariable
  51. ? valueVariable.name
  52. : undefined;
  53. const setterVariableName = setterVariable
  54. ? setterVariable.name
  55. : undefined;
  56. const caseCandidateMatch = valueVariableName ? valueVariableName.match(/(^[a-z]+)(.*)/) : undefined;
  57. const upperCaseCandidatePrefix = caseCandidateMatch ? caseCandidateMatch[1] : undefined;
  58. const caseCandidateSuffix = caseCandidateMatch ? caseCandidateMatch[2] : undefined;
  59. const expectedSetterVariableNames = upperCaseCandidatePrefix ? [
  60. `set${upperCaseCandidatePrefix.charAt(0).toUpperCase()}${upperCaseCandidatePrefix.slice(1)}${caseCandidateSuffix}`,
  61. `set${upperCaseCandidatePrefix.toUpperCase()}${caseCandidateSuffix}`,
  62. ] : [];
  63. const isSymmetricGetterSetterPair = valueVariable
  64. && setterVariable
  65. && expectedSetterVariableNames.indexOf(setterVariableName) !== -1
  66. && variableNodes.length === 2;
  67. if (!isSymmetricGetterSetterPair) {
  68. const suggestions = [
  69. {
  70. desc: 'Destructure useState call into value + setter pair',
  71. fix: (fixer) => {
  72. if (expectedSetterVariableNames.length === 0) {
  73. return;
  74. }
  75. const fix = fixer.replaceTextRange(
  76. node.parent.id.range,
  77. `[${valueVariableName}, ${expectedSetterVariableNames[0]}]`
  78. );
  79. return fix;
  80. },
  81. },
  82. ];
  83. const defaultReactImports = components.getDefaultReactImports();
  84. const defaultReactImportSpecifier = defaultReactImports
  85. ? defaultReactImports[0]
  86. : undefined;
  87. const defaultReactImportName = defaultReactImportSpecifier
  88. ? defaultReactImportSpecifier.local.name
  89. : undefined;
  90. const namedReactImports = components.getNamedReactImports();
  91. const useStateReactImportSpecifier = namedReactImports
  92. ? namedReactImports.find((specifier) => specifier.imported.name === 'useState')
  93. : undefined;
  94. const isSingleGetter = valueVariable && variableNodes.length === 1;
  95. const isUseStateCalledWithSingleArgument = node.arguments.length === 1;
  96. if (isSingleGetter && isUseStateCalledWithSingleArgument) {
  97. const useMemoReactImportSpecifier = namedReactImports
  98. && namedReactImports.find((specifier) => specifier.imported.name === 'useMemo');
  99. let useMemoCode;
  100. if (useMemoReactImportSpecifier) {
  101. useMemoCode = useMemoReactImportSpecifier.local.name;
  102. } else if (defaultReactImportName) {
  103. useMemoCode = `${defaultReactImportName}.useMemo`;
  104. } else {
  105. useMemoCode = 'useMemo';
  106. }
  107. suggestions.unshift({
  108. desc: 'Replace useState call with useMemo',
  109. fix: (fixer) => [
  110. // Add useMemo import, if necessary
  111. useStateReactImportSpecifier
  112. && (!useMemoReactImportSpecifier || defaultReactImportName)
  113. && fixer.insertTextAfter(useStateReactImportSpecifier, ', useMemo'),
  114. // Convert single-value destructure to simple assignment
  115. fixer.replaceTextRange(node.parent.id.range, valueVariableName),
  116. // Convert useState call to useMemo + arrow function + dependency array
  117. fixer.replaceTextRange(
  118. node.range,
  119. `${useMemoCode}(() => ${context.getSourceCode().getText(node.arguments[0])}, [])`
  120. ),
  121. ].filter(Boolean),
  122. });
  123. }
  124. report(
  125. context,
  126. messages.useStateErrorMessage,
  127. 'useStateErrorMessage',
  128. {
  129. node: node.parent.id,
  130. suggest: suggestions,
  131. }
  132. );
  133. }
  134. },
  135. })),
  136. };