no-arrow-function-lifecycle.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. /**
  2. * @fileoverview Lifecycle methods should be methods on the prototype, not class fields
  3. * @author Tan Nguyen
  4. */
  5. 'use strict';
  6. const values = require('object.values');
  7. const Components = require('../util/Components');
  8. const astUtil = require('../util/ast');
  9. const componentUtil = require('../util/componentUtil');
  10. const docsUrl = require('../util/docsUrl');
  11. const lifecycleMethods = require('../util/lifecycleMethods');
  12. const report = require('../util/report');
  13. const eslintUtil = require('../util/eslint');
  14. const getSourceCode = eslintUtil.getSourceCode;
  15. const getText = eslintUtil.getText;
  16. function getRuleText(node) {
  17. const params = node.value.params.map((p) => p.name);
  18. if (node.type === 'Property') {
  19. return `: function(${params.join(', ')}) `;
  20. }
  21. if (node.type === 'ClassProperty' || node.type === 'PropertyDefinition') {
  22. return `(${params.join(', ')}) `;
  23. }
  24. return null;
  25. }
  26. const messages = {
  27. lifecycle: '{{propertyName}} is a React lifecycle method, and should not be an arrow function or in a class field. Use an instance method instead.',
  28. };
  29. /** @type {import('eslint').Rule.RuleModule} */
  30. module.exports = {
  31. meta: {
  32. docs: {
  33. description: 'Lifecycle methods should be methods on the prototype, not class fields',
  34. category: 'Best Practices',
  35. recommended: false,
  36. url: docsUrl('no-arrow-function-lifecycle'),
  37. },
  38. messages,
  39. schema: [],
  40. fixable: 'code',
  41. },
  42. create: Components.detect((context, components) => {
  43. /**
  44. * @param {Array} properties list of component properties
  45. */
  46. function reportNoArrowFunctionLifecycle(properties) {
  47. properties.forEach((node) => {
  48. if (!node || !node.value) {
  49. return;
  50. }
  51. const propertyName = astUtil.getPropertyName(node);
  52. const nodeType = node.value.type;
  53. const isLifecycleMethod = (
  54. node.static && !componentUtil.isES5Component(node, context)
  55. ? lifecycleMethods.static
  56. : lifecycleMethods.instance
  57. ).indexOf(propertyName) > -1;
  58. if (nodeType === 'ArrowFunctionExpression' && isLifecycleMethod) {
  59. const body = node.value.body;
  60. const isBlockBody = body.type === 'BlockStatement';
  61. const sourceCode = getSourceCode(context);
  62. let nextComment = [];
  63. let previousComment = [];
  64. let bodyRange;
  65. if (!isBlockBody) {
  66. const previousToken = sourceCode.getTokenBefore(body);
  67. if (sourceCode.getCommentsBefore) {
  68. // eslint >=4.x
  69. previousComment = sourceCode.getCommentsBefore(body);
  70. } else {
  71. // eslint 3.x
  72. const potentialComment = sourceCode.getTokenBefore(body, { includeComments: true });
  73. previousComment = previousToken === potentialComment ? [] : [potentialComment];
  74. }
  75. if (sourceCode.getCommentsAfter) {
  76. // eslint >=4.x
  77. nextComment = sourceCode.getCommentsAfter(body);
  78. } else {
  79. // eslint 3.x
  80. const potentialComment = sourceCode.getTokenAfter(body, { includeComments: true });
  81. const nextToken = sourceCode.getTokenAfter(body);
  82. nextComment = nextToken === potentialComment ? [] : [potentialComment];
  83. }
  84. bodyRange = [
  85. (previousComment.length > 0 ? previousComment[0] : body).range[0],
  86. (nextComment.length > 0 ? nextComment[nextComment.length - 1] : body).range[1]
  87. + (node.value.body.type === 'ObjectExpression' ? 1 : 0), // to account for a wrapped end paren
  88. ];
  89. }
  90. const headRange = [
  91. node.key.range[1],
  92. (previousComment.length > 0 ? previousComment[0] : body).range[0],
  93. ];
  94. const hasSemi = node.value.expression && getText(context, node).slice(node.value.range[1] - node.range[0]) === ';';
  95. report(
  96. context,
  97. messages.lifecycle,
  98. 'lifecycle',
  99. {
  100. node,
  101. data: {
  102. propertyName,
  103. },
  104. fix(fixer) {
  105. if (!sourceCode.getCommentsAfter) {
  106. // eslint 3.x
  107. return isBlockBody && fixer.replaceTextRange(headRange, getRuleText(node));
  108. }
  109. return [].concat(
  110. fixer.replaceTextRange(headRange, getRuleText(node)),
  111. isBlockBody ? [] : fixer.replaceTextRange(
  112. [bodyRange[0], bodyRange[1] + (hasSemi ? 1 : 0)],
  113. `{ return ${previousComment.map((x) => getText(context, x)).join('')}${getText(context, body)}${nextComment.map((x) => getText(context, x)).join('')}; }`
  114. )
  115. );
  116. },
  117. }
  118. );
  119. }
  120. });
  121. }
  122. return {
  123. 'Program:exit'() {
  124. values(components.list()).forEach((component) => {
  125. const properties = astUtil.getComponentProperties(component.node);
  126. reportNoArrowFunctionLifecycle(properties);
  127. });
  128. },
  129. };
  130. }),
  131. };