makeNoMethodSetStateRule.js 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. /**
  2. * @fileoverview Prevent usage of setState in lifecycle methods
  3. * @author Yannick Croissant
  4. */
  5. 'use strict';
  6. const findLast = require('array.prototype.findlast');
  7. const docsUrl = require('./docsUrl');
  8. const report = require('./report');
  9. const getAncestors = require('./eslint').getAncestors;
  10. const testReactVersion = require('./version').testReactVersion;
  11. // ------------------------------------------------------------------------------
  12. // Rule Definition
  13. // ------------------------------------------------------------------------------
  14. function mapTitle(methodName) {
  15. const map = {
  16. componentDidMount: 'did-mount',
  17. componentDidUpdate: 'did-update',
  18. componentWillUpdate: 'will-update',
  19. };
  20. const title = map[methodName];
  21. if (!title) {
  22. throw Error(`No docsUrl for '${methodName}'`);
  23. }
  24. return `no-${title}-set-state`;
  25. }
  26. const messages = {
  27. noSetState: 'Do not use setState in {{name}}',
  28. };
  29. const methodNoopsAsOf = {
  30. componentDidMount: '>= 16.3.0',
  31. componentDidUpdate: '>= 16.3.0',
  32. };
  33. function shouldBeNoop(context, methodName) {
  34. return methodName in methodNoopsAsOf
  35. && testReactVersion(context, methodNoopsAsOf[methodName])
  36. && !testReactVersion(context, '999.999.999'); // for when the version is not specified
  37. }
  38. // eslint-disable-next-line valid-jsdoc
  39. /**
  40. * @param {string} methodName
  41. * @param {(context: import('eslint').Rule.RuleContext) => boolean} [shouldCheckUnsafeCb]
  42. * @returns {import('eslint').Rule.RuleModule}
  43. */
  44. module.exports = function makeNoMethodSetStateRule(methodName, shouldCheckUnsafeCb) {
  45. return {
  46. meta: {
  47. docs: {
  48. description: `Disallow usage of setState in ${methodName}`,
  49. category: 'Best Practices',
  50. recommended: false,
  51. url: docsUrl(mapTitle(methodName)),
  52. },
  53. messages,
  54. schema: [{
  55. enum: ['disallow-in-func'],
  56. }],
  57. },
  58. create(context) {
  59. const mode = context.options[0] || 'allow-in-func';
  60. function nameMatches(name) {
  61. if (name === methodName) {
  62. return true;
  63. }
  64. if (typeof shouldCheckUnsafeCb === 'function' && shouldCheckUnsafeCb(context)) {
  65. return name === `UNSAFE_${methodName}`;
  66. }
  67. return false;
  68. }
  69. if (shouldBeNoop(context, methodName)) {
  70. return {};
  71. }
  72. // --------------------------------------------------------------------------
  73. // Public
  74. // --------------------------------------------------------------------------
  75. return {
  76. CallExpression(node) {
  77. const callee = node.callee;
  78. if (
  79. callee.type !== 'MemberExpression'
  80. || callee.object.type !== 'ThisExpression'
  81. || !('name' in callee.property)
  82. || callee.property.name !== 'setState'
  83. ) {
  84. return;
  85. }
  86. const ancestors = getAncestors(context, node);
  87. let depth = 0;
  88. findLast(ancestors, (ancestor) => {
  89. // ancestors.some((ancestor) => {
  90. if (/Function(Expression|Declaration)$/.test(ancestor.type)) {
  91. depth += 1;
  92. }
  93. if (
  94. (ancestor.type !== 'Property' && ancestor.type !== 'MethodDefinition' && ancestor.type !== 'ClassProperty' && ancestor.type !== 'PropertyDefinition')
  95. || !nameMatches(ancestor.key.name)
  96. || (mode !== 'disallow-in-func' && depth > 1)
  97. ) {
  98. return false;
  99. }
  100. report(context, messages.noSetState, 'noSetState', {
  101. node: callee,
  102. data: {
  103. name: ancestor.key.name,
  104. },
  105. });
  106. return true;
  107. });
  108. },
  109. };
  110. },
  111. };
  112. };