componentMethodsHandler.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import getMemberValuePath from '../utils/getMemberValuePath.js';
  2. import getMethodDocumentation from '../utils/getMethodDocumentation.js';
  3. import isReactComponentClass from '../utils/isReactComponentClass.js';
  4. import isReactComponentMethod from '../utils/isReactComponentMethod.js';
  5. import { shallowIgnoreVisitors } from '../utils/traverse.js';
  6. import resolveToValue from '../utils/resolveToValue.js';
  7. import { visitors } from '@babel/traverse';
  8. import { isReactBuiltinCall, isReactForwardRefCall, isStatelessComponent, findFunctionReturn, } from '../utils/index.js';
  9. /**
  10. * The following values/constructs are considered methods:
  11. *
  12. * - Method declarations in classes (except "constructor" and React lifecycle
  13. * methods
  14. * - Public class fields in classes whose value are a functions
  15. * - Object properties whose values are functions
  16. */
  17. function isMethod(path) {
  18. let isProbablyMethod = (path.isClassMethod() && path.node.kind !== 'constructor') ||
  19. path.isObjectMethod();
  20. if (!isProbablyMethod &&
  21. (path.isClassProperty() || path.isObjectProperty())) {
  22. const value = resolveToValue(path.get('value'));
  23. isProbablyMethod = value.isFunction();
  24. }
  25. return isProbablyMethod && !isReactComponentMethod(path);
  26. }
  27. const explodedVisitors = visitors.explode({
  28. ...shallowIgnoreVisitors,
  29. AssignmentExpression: {
  30. enter: function (assignmentPath, state) {
  31. const { name, scope } = state;
  32. const left = assignmentPath.get('left');
  33. const binding = assignmentPath.scope.getBinding(name);
  34. if (binding &&
  35. left.isMemberExpression() &&
  36. left.get('object').isIdentifier({ name }) &&
  37. binding.scope === scope &&
  38. resolveToValue(assignmentPath.get('right')).isFunction()) {
  39. state.methods.push(assignmentPath);
  40. }
  41. assignmentPath.skip();
  42. },
  43. },
  44. });
  45. function isObjectExpression(path) {
  46. return path.isObjectExpression();
  47. }
  48. const explodedImperativeHandleVisitors = visitors.explode({
  49. ...shallowIgnoreVisitors,
  50. CallExpression: {
  51. enter: function (path, state) {
  52. if (!isReactBuiltinCall(path, 'useImperativeHandle')) {
  53. return path.skip();
  54. }
  55. // useImperativeHandle(ref, () => ({ name: () => {}, ...}))
  56. const arg = path.get('arguments')[1];
  57. if (!arg || !arg.isFunction()) {
  58. return path.skip();
  59. }
  60. const body = resolveToValue(arg.get('body'));
  61. let definition;
  62. if (body.isObjectExpression()) {
  63. definition = body;
  64. }
  65. else {
  66. definition = findFunctionReturn(arg, isObjectExpression);
  67. }
  68. // We found the object body, now add all of the properties as methods.
  69. definition?.get('properties').forEach((p) => {
  70. if (isMethod(p)) {
  71. state.results.push(p);
  72. }
  73. });
  74. path.skip();
  75. },
  76. },
  77. });
  78. function findStatelessComponentBody(componentDefinition) {
  79. if (isStatelessComponent(componentDefinition)) {
  80. const body = componentDefinition.get('body');
  81. if (body.isBlockStatement()) {
  82. return body;
  83. }
  84. }
  85. else if (isReactForwardRefCall(componentDefinition)) {
  86. const inner = resolveToValue(componentDefinition.get('arguments')[0]);
  87. return findStatelessComponentBody(inner);
  88. }
  89. return undefined;
  90. }
  91. function findImperativeHandleMethods(componentDefinition) {
  92. const body = findStatelessComponentBody(componentDefinition);
  93. if (!body) {
  94. return [];
  95. }
  96. const state = { results: [] };
  97. body.traverse(explodedImperativeHandleVisitors, state);
  98. return state.results.map((p) => ({ path: p }));
  99. }
  100. function findAssignedMethods(path, idPath) {
  101. if (!idPath.hasNode() || !idPath.isIdentifier()) {
  102. return [];
  103. }
  104. const name = idPath.node.name;
  105. const binding = idPath.scope.getBinding(name);
  106. if (!binding) {
  107. return [];
  108. }
  109. const scope = binding.scope;
  110. const state = {
  111. scope,
  112. name,
  113. methods: [],
  114. };
  115. path.traverse(explodedVisitors, state);
  116. return state.methods.map((p) => ({ path: p }));
  117. }
  118. /**
  119. * Extract all flow types for the methods of a react component. Doesn't
  120. * return any react specific lifecycle methods.
  121. */
  122. const componentMethodsHandler = function (documentation, componentDefinition) {
  123. // Extract all methods from the class or object.
  124. let methodPaths = [];
  125. const parent = componentDefinition.parentPath;
  126. if (isReactComponentClass(componentDefinition)) {
  127. methodPaths = componentDefinition
  128. .get('body')
  129. .get('body')
  130. .filter(isMethod).map((p) => ({ path: p }));
  131. }
  132. else if (componentDefinition.isObjectExpression()) {
  133. methodPaths = componentDefinition.get('properties').filter(isMethod).map((p) => ({ path: p }));
  134. // Add the statics object properties.
  135. const statics = getMemberValuePath(componentDefinition, 'statics');
  136. if (statics && statics.isObjectExpression()) {
  137. statics.get('properties').forEach((property) => {
  138. if (isMethod(property)) {
  139. methodPaths.push({
  140. path: property,
  141. isStatic: true,
  142. });
  143. }
  144. });
  145. }
  146. }
  147. else if (parent.isVariableDeclarator() &&
  148. parent.node.init === componentDefinition.node &&
  149. parent.get('id').isIdentifier()) {
  150. methodPaths = findAssignedMethods(parent.scope.path, parent.get('id'));
  151. }
  152. else if (parent.isAssignmentExpression() &&
  153. parent.node.right === componentDefinition.node &&
  154. parent.get('left').isIdentifier()) {
  155. methodPaths = findAssignedMethods(parent.scope.path, parent.get('left'));
  156. }
  157. else if (componentDefinition.isFunctionDeclaration()) {
  158. methodPaths = findAssignedMethods(parent.scope.path, componentDefinition.get('id'));
  159. }
  160. const imperativeHandles = findImperativeHandleMethods(componentDefinition);
  161. if (imperativeHandles) {
  162. methodPaths = [...methodPaths, ...imperativeHandles];
  163. }
  164. documentation.set('methods', methodPaths
  165. .map(({ path: p, isStatic }) => getMethodDocumentation(p, { isStatic }))
  166. .filter(Boolean));
  167. };
  168. export default componentMethodsHandler;