no-arrow-function-lifecycle.js 4.9 KB

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