no-access-state-in-setstate.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. /**
  2. * @fileoverview Prevent usage of this.state within setState
  3. * @author Rolf Erik Lekang, Jørgen Aaberg
  4. */
  5. 'use strict';
  6. const docsUrl = require('../util/docsUrl');
  7. const componentUtil = require('../util/componentUtil');
  8. const report = require('../util/report');
  9. // ------------------------------------------------------------------------------
  10. // Rule Definition
  11. // ------------------------------------------------------------------------------
  12. const messages = {
  13. useCallback: 'Use callback in setState when referencing the previous state.',
  14. };
  15. module.exports = {
  16. meta: {
  17. docs: {
  18. description: 'Disallow when this.state is accessed within setState',
  19. category: 'Possible Errors',
  20. recommended: false,
  21. url: docsUrl('no-access-state-in-setstate'),
  22. },
  23. messages,
  24. },
  25. create(context) {
  26. function isSetStateCall(node) {
  27. return node.type === 'CallExpression'
  28. && node.callee.property
  29. && node.callee.property.name === 'setState'
  30. && node.callee.object.type === 'ThisExpression';
  31. }
  32. function isFirstArgumentInSetStateCall(current, node) {
  33. if (!isSetStateCall(current)) {
  34. return false;
  35. }
  36. while (node && node.parent !== current) {
  37. node = node.parent;
  38. }
  39. return current.arguments[0] === node;
  40. }
  41. function isClassComponent() {
  42. return !!(componentUtil.getParentES6Component(context) || componentUtil.getParentES5Component(context));
  43. }
  44. // The methods array contains all methods or functions that are using this.state
  45. // or that are calling another method or function using this.state
  46. const methods = [];
  47. // The vars array contains all variables that contains this.state
  48. const vars = [];
  49. return {
  50. CallExpression(node) {
  51. if (!isClassComponent()) {
  52. return;
  53. }
  54. // Appends all the methods that are calling another
  55. // method containing this.state to the methods array
  56. methods.forEach((method) => {
  57. if (node.callee.name === method.methodName) {
  58. let current = node.parent;
  59. while (current.type !== 'Program') {
  60. if (current.type === 'MethodDefinition') {
  61. methods.push({
  62. methodName: current.key.name,
  63. node: method.node,
  64. });
  65. break;
  66. }
  67. current = current.parent;
  68. }
  69. }
  70. });
  71. // Finding all CallExpressions that is inside a setState
  72. // to further check if they contains this.state
  73. let current = node.parent;
  74. while (current.type !== 'Program') {
  75. if (isFirstArgumentInSetStateCall(current, node)) {
  76. const methodName = node.callee.name;
  77. methods.forEach((method) => {
  78. if (method.methodName === methodName) {
  79. report(context, messages.useCallback, 'useCallback', {
  80. node: method.node,
  81. });
  82. }
  83. });
  84. break;
  85. }
  86. current = current.parent;
  87. }
  88. },
  89. MemberExpression(node) {
  90. if (
  91. node.property.name === 'state'
  92. && node.object.type === 'ThisExpression'
  93. && isClassComponent()
  94. ) {
  95. let current = node;
  96. while (current.type !== 'Program') {
  97. // Reporting if this.state is directly within this.setState
  98. if (isFirstArgumentInSetStateCall(current, node)) {
  99. report(context, messages.useCallback, 'useCallback', {
  100. node,
  101. });
  102. break;
  103. }
  104. // Storing all functions and methods that contains this.state
  105. if (current.type === 'MethodDefinition') {
  106. methods.push({
  107. methodName: current.key.name,
  108. node,
  109. });
  110. break;
  111. } else if (current.type === 'FunctionExpression' && current.parent.key) {
  112. methods.push({
  113. methodName: current.parent.key.name,
  114. node,
  115. });
  116. break;
  117. }
  118. // Storing all variables containing this.state
  119. if (current.type === 'VariableDeclarator') {
  120. vars.push({
  121. node,
  122. scope: context.getScope(),
  123. variableName: current.id.name,
  124. });
  125. break;
  126. }
  127. current = current.parent;
  128. }
  129. }
  130. },
  131. Identifier(node) {
  132. // Checks if the identifier is a variable within an object
  133. let current = node;
  134. while (current.parent.type === 'BinaryExpression') {
  135. current = current.parent;
  136. }
  137. if (
  138. current.parent.value === current
  139. || current.parent.object === current
  140. ) {
  141. while (current.type !== 'Program') {
  142. if (isFirstArgumentInSetStateCall(current, node)) {
  143. vars
  144. .filter((v) => v.scope === context.getScope() && v.variableName === node.name)
  145. .forEach((v) => {
  146. report(context, messages.useCallback, 'useCallback', {
  147. node: v.node,
  148. });
  149. });
  150. }
  151. current = current.parent;
  152. }
  153. }
  154. },
  155. ObjectPattern(node) {
  156. const isDerivedFromThis = node.parent.init && node.parent.init.type === 'ThisExpression';
  157. node.properties.forEach((property) => {
  158. if (property && property.key && property.key.name === 'state' && isDerivedFromThis) {
  159. vars.push({
  160. node: property.key,
  161. scope: context.getScope(),
  162. variableName: property.key.name,
  163. });
  164. }
  165. });
  166. },
  167. };
  168. },
  169. };