no-direct-mutation-state.js 4.5 KB

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