makeNoMethodSetStateRule.js 3.3 KB

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