no-direct-mutation-state.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. /**
  2. * @fileoverview Prevent direct mutation of this.state
  3. * @author David Petersen
  4. * @author Nicolas Fernandez <@burabure>
  5. */
  6. 'use strict';
  7. const values = require('object.values');
  8. const Components = require('../util/Components');
  9. const componentUtil = require('../util/componentUtil');
  10. const docsUrl = require('../util/docsUrl');
  11. const report = require('../util/report');
  12. // ------------------------------------------------------------------------------
  13. // Rule Definition
  14. // ------------------------------------------------------------------------------
  15. const messages = {
  16. noDirectMutation: 'Do not mutate state directly. Use setState().',
  17. };
  18. /** @type {import('eslint').Rule.RuleModule} */
  19. module.exports = {
  20. meta: {
  21. docs: {
  22. description: 'Disallow direct mutation of this.state',
  23. category: 'Possible Errors',
  24. recommended: true,
  25. url: docsUrl('no-direct-mutation-state'),
  26. },
  27. messages,
  28. },
  29. create: Components.detect((context, components, utils) => {
  30. /**
  31. * Checks if the component is valid
  32. * @param {Object} component The component to process
  33. * @returns {boolean} True if the component is valid, false if not.
  34. */
  35. function isValid(component) {
  36. return !!component && !component.mutateSetState;
  37. }
  38. /**
  39. * Reports undeclared proptypes for a given component
  40. * @param {Object} component The component to process
  41. */
  42. function reportMutations(component) {
  43. let mutation;
  44. for (let i = 0, j = component.mutations.length; i < j; i++) {
  45. mutation = component.mutations[i];
  46. report(context, messages.noDirectMutation, 'noDirectMutation', {
  47. node: mutation,
  48. });
  49. }
  50. }
  51. /**
  52. * Walks through the MemberExpression to the top-most property.
  53. * @param {Object} node The node to process
  54. * @returns {Object} The outer-most MemberExpression
  55. */
  56. function getOuterMemberExpression(node) {
  57. while (node.object && node.object.property) {
  58. node = node.object;
  59. }
  60. return node;
  61. }
  62. /**
  63. * Determine if we should currently ignore assignments in this component.
  64. * @param {?Object} component The component to process
  65. * @returns {boolean} True if we should skip assignment checks.
  66. */
  67. function shouldIgnoreComponent(component) {
  68. return !component || (component.inConstructor && !component.inCallExpression);
  69. }
  70. // --------------------------------------------------------------------------
  71. // Public
  72. // --------------------------------------------------------------------------
  73. return {
  74. MethodDefinition(node) {
  75. if (node.kind === 'constructor') {
  76. components.set(node, {
  77. inConstructor: true,
  78. });
  79. }
  80. },
  81. CallExpression(node) {
  82. components.set(node, {
  83. inCallExpression: true,
  84. });
  85. },
  86. AssignmentExpression(node) {
  87. const component = components.get(utils.getParentComponent(node));
  88. if (shouldIgnoreComponent(component) || !node.left || !node.left.object) {
  89. return;
  90. }
  91. const item = getOuterMemberExpression(node.left);
  92. if (componentUtil.isStateMemberExpression(item)) {
  93. const mutations = (component && component.mutations) || [];
  94. mutations.push(node.left.object);
  95. components.set(node, {
  96. mutateSetState: true,
  97. mutations,
  98. });
  99. }
  100. },
  101. UpdateExpression(node) {
  102. const component = components.get(utils.getParentComponent(node));
  103. if (shouldIgnoreComponent(component) || node.argument.type !== 'MemberExpression') {
  104. return;
  105. }
  106. const item = getOuterMemberExpression(node.argument);
  107. if (componentUtil.isStateMemberExpression(item)) {
  108. const mutations = (component && component.mutations) || [];
  109. mutations.push(item);
  110. components.set(node, {
  111. mutateSetState: true,
  112. mutations,
  113. });
  114. }
  115. },
  116. 'CallExpression:exit'(node) {
  117. components.set(node, {
  118. inCallExpression: false,
  119. });
  120. },
  121. 'MethodDefinition:exit'(node) {
  122. if (node.kind === 'constructor') {
  123. components.set(node, {
  124. inConstructor: false,
  125. });
  126. }
  127. },
  128. 'Program:exit'() {
  129. values(components.list())
  130. .filter((component) => !isValid(component))
  131. .forEach((component) => {
  132. reportMutations(component);
  133. });
  134. },
  135. };
  136. }),
  137. };