332
}
333
}
334
335
+ if (pass.opts.enableInferReactFunctions) {
336
+ const isReactLike = isReactFunctionLike(fn);
337
+ return isReactLike;
338
+ }
339
+
340
return fn.scope.getProgramParent() === fn.scope.parent;
341
}
342
424
425
return body.node;
426
}
427
+
428
+function isHookName(s: string): boolean {
429
+ return /^use[A-Z0-9]/.test(s);
430
+}
431
+
432
+/**
433
+ * We consider hooks to be a hook name identifier or a member expression
434
+ * containing a hook name.
435
+ */
436
+
437
+function isHook(path: NodePath<t.Expression | t.PrivateName>): boolean {
438
+ if (path.isIdentifier()) {
439
+ return isHookName(path.node.name);
440
+ } else if (
441
+ path.isMemberExpression() &&
442
+ !path.node.computed &&
443
+ isHook(path.get("property"))
444
+ ) {
445
+ const obj = path.get("object").node;
446
+ const isPascalCaseNameSpace = /^[A-Z].*/;
447
+ return obj.type === "Identifier" && isPascalCaseNameSpace.test(obj.name);
448
+ } else {
449
+ return false;
450
+ }
451
+}
452
+
453
+/**
454
+ * Checks if the node is a React component name. React component names must
455
+ * always start with an uppercase letter.
456
+ */
457
+
458
+function isComponentName(path: NodePath<t.Expression>): boolean {
459
+ return path.isIdentifier() && /^[A-Z]/.test(path.node.name);
460
+}
461
+
462
+function isReactFunction(
463
+ path: NodePath<t.Expression | t.PrivateName | t.V8IntrinsicIdentifier>,
464
+ functionName: string
465
+): boolean {
466
+ const node = path.node;
467
+ return (
468
+ (node.type === "Identifier" && node.name === functionName) ||
469
+ (node.type === "MemberExpression" &&
470
+ node.object.type === "Identifier" &&
471
+ node.object.name === "React" &&
472
+ node.property.type === "Identifier" &&
473
+ node.property.name === functionName)
474
+ );
475
+}
476
+
477
+/**
478
+ * Checks if the node is a callback argument of forwardRef. This render function
479
+ * should follow the rules of hooks.
480
+ */
481
+
482
+function isForwardRefCallback(path: NodePath<t.Expression>): boolean {
483
+ return !!(
484
+ path.parentPath.isCallExpression() &&
485
+ path.parentPath.get("callee").isExpression() &&
486
+ isReactFunction(path.parentPath.get("callee"), "forwardRef")
487
+ );
488
+}
489
+
490
+/**
491
+ * Checks if the node is a callback argument of React.memo. This anonymous
492
+ * functional component should follow the rules of hooks.
493
+ */
494
+
495
+function isMemoCallback(path: NodePath<t.Expression>): boolean {
496
+ return !!(
497
+ path.parentPath.isCallExpression() &&
498
+ path.parentPath.get("callee").isExpression() &&
499
+ isReactFunction(path.parentPath.get("callee"), "memo")
500
+ );
501
+}
502
+
503
+function isReactFunctionLike(
504
+ node: NodePath<t.FunctionDeclaration | t.ArrowFunctionExpression>
505
+): boolean {
506
+ const functionName = getFunctionName(node);
507
+ if (functionName !== null) {
508
+ if (!isComponentName(functionName) && !isHook(functionName)) {
509
+ return false;
510
+ }
511
+ } else if (
512
+ node.isExpression() &&
513
+ !isForwardRefCallback(node) &&
514
+ !isMemoCallback(node)
515
+ ) {
516
+ return false;
517
+ } else {
518
+ return false;
519
+ }
520
+
521
+ let invokesHooks = false;
522
+ let createsJsx = false;
523
+ node.traverse({
524
+ JSX() {
525
+ createsJsx = true;
526
+ },
527
+ CallExpression(call) {
528
+ const callee = call.get("callee");
529
+ if (callee.isExpression() && isHook(callee)) {
530
+ invokesHooks = true;
531
+ }
532
+ },
533
+ });
534
+
535
+ return invokesHooks || createsJsx;
536
+}
537
+
538
+/**
539
+ * Gets the static name of a function AST node. For function declarations it is
540
+ * easy. For anonymous function expressions it is much harder. If you search for
541
+ * `IsAnonymousFunctionDefinition()` in the ECMAScript spec you'll find places
542
+ * where JS gives anonymous function expressions names. We roughly detect the
543
+ * same AST nodes with some exceptions to better fit our use case.
544
+ */
545
+
546
+function getFunctionName(
547
+ path: NodePath<t.FunctionDeclaration | t.ArrowFunctionExpression>
548
+): NodePath<t.Expression> | null {
549
+ if (path.isFunctionDeclaration()) {
550
+ const id = path.get("id");
551
+ if (id.isIdentifier()) {
552
+ return id;
553
+ }
554
+ return null;
555
+ }
556
+ let id: NodePath<t.LVal | t.Expression | t.PrivateName> | null = null;
557
+ const parent = path.parentPath;
558
+ if (parent.isVariableDeclarator() && parent.get("init").node === path.node) {
559
+ // const useHook = () => {};
560
+ id = parent.get("id");
561
+ } else if (
562
+ parent.isAssignmentExpression() &&
563
+ parent.get("right").node === path.node &&
564
+ parent.get("operator") === "="
565
+ ) {
566
+ // useHook = () => {};
567
+ id = parent.get("left");
568
+ } else if (
569
+ parent.isProperty() &&
570
+ parent.get("value").node === path.node &&
571
+ !parent.get("computed") &&
572
+ parent.get("key").isLVal()
573
+ ) {
574
+ // {useHook: () => {}}
575
+ // {useHook() {}}
576
+ id = parent.get("key");
577
+ } else if (
578
+ parent.isAssignmentPattern() &&
579
+ parent.get("right").node === path.node &&
580
+ !parent.get("computed")
581
+ ) {
582
+ // const {useHook = () => {}} = {};
583
+ // ({useHook = () => {}} = {});
584
+ //
585
+ // Kinda clowny, but we'd said we'd follow spec convention for
586
+ // `IsAnonymousFunctionDefinition()` usage.
587
+ id = parent.get("left");
588
+ }
589
+ if (id !== null && (id.isIdentifier() || id.isMemberExpression())) {
590
+ return id;
591
+ } else {
592
+ return null;
593
+ }
594
+}