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

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