no-direct-mutation-state.js 4.4 KB

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