[compiler] Expect components to have hook calls or jsx directly in body
Summary: We can tighten our criteria for what is a component by requiring that a component or hook contain JSX or hook calls directly within its body, excluding nested functions . Currently, if we see them within the body anywhere -- including nested functions -- we treat it as a component if the other requirements are met. This change makes this stricter. We also now expect components (but not necessarily hooks) to have return statements, and those returns must be potential React nodes (we can reject functions that return function or object literals, for example). ghstack-source-id: 4507cc3955216c564bf257c0b81bfb551ae6ae55 Pull Request resolved: https://github.com/facebook/react/pull/29865
Mike Vitousek committed
Jun 11, 2024 at 14:08 UTC
057de295d5fd79abb9b38a50f7242005a829beae
9 files changed
+203
-16
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts
+59
-2
@@ -688,7 +688,8 @@ function getComponentOrHookLike(
688
if (functionName !== null && isComponentName(functionName)) {
689
let isComponent =
690
callsHooksOrCreatesJsx(node, hookPattern) &&
691
- isValidComponentParams(node.get("params"));
691
+ isValidComponentParams(node.get("params")) &&
692
+ !returnsNonNode(node);
693
return isComponent ? "Component" : null;
694
} else if (functionName !== null && isHook(functionName, hookPattern)) {
695
// Hooks have hook invocations or JSX, but can take any # of arguments
@@ -708,12 +709,31 @@ function getComponentOrHookLike(
709
return null;
710
}
711
712
+function skipNestedFunctions(
713
+ node: NodePath<
714
+ t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
715
+ >
716
+) {
717
+ return (
718
+ fn: NodePath<
719
+ t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
720
+ >
721
+ ): void => {
722
+ if (fn.node !== node.node) {
723
+ fn.skip();
724
+ }
725
+ };
726
+}
727
+
728
function callsHooksOrCreatesJsx(
712
- node: NodePath<t.Node>,
729
+ node: NodePath<
730
+ t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
731
+ >,
732
hookPattern: string | null
733
): boolean {
734
let invokesHooks = false;
735
let createsJsx = false;
736
+
737
node.traverse({
738
JSX() {
739
createsJsx = true;
@@ -724,11 +744,48 @@ function callsHooksOrCreatesJsx(
744
invokesHooks = true;
745
}
746
},
747
+ ArrowFunctionExpression: skipNestedFunctions(node),
748
+ FunctionExpression: skipNestedFunctions(node),
749
+ FunctionDeclaration: skipNestedFunctions(node),
750
});
751
752
return invokesHooks || createsJsx;
753
}
754
755
+function returnsNonNode(
756
+ node: NodePath<
757
+ t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
758
+ >
759
+): boolean {
760
+ let hasReturn = false;
761
+ let returnsNonNode = false;
762
+
763
+ node.traverse({
764
+ ReturnStatement(ret) {
765
+ hasReturn = true;
766
+ const argument = ret.node.argument;
767
+ if (argument == null) {
768
+ returnsNonNode = true;
769
+ } else {
770
+ switch (argument.type) {
771
+ case "ObjectExpression":
772
+ case "ArrowFunctionExpression":
773
+ case "FunctionExpression":
774
+ case "BigIntLiteral":
775
+ case "ClassExpression":
776
+ case "NewExpression": // technically `new Array()` is legit, but unlikely
777
+ returnsNonNode = true;
778
+ }
779
+ }
780
+ },
781
+ ArrowFunctionExpression: skipNestedFunctions(node),
782
+ FunctionExpression: skipNestedFunctions(node),
783
+ FunctionDeclaration: skipNestedFunctions(node),
784
+ });
785
+
786
+ return !hasReturn || returnsNonNode;
787
+}
788
+
789
/*
790
* Gets the static name of a function AST node. For function declarations it is
791
* easy. For anonymous function expressions it is much harder. If you search for
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-no-component-nested-jsx.expect.md
new
+51
@@ -0,0 +1,51 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @compilationMode(infer)
6
+function Component(props) {
7
+ const result = f(props);
8
+ function helper() {
9
+ return <foo />;
10
+ }
11
+ helper();
12
+ return result;
13
+}
14
+
15
+function f(props) {
16
+ return props;
17
+}
18
+
19
+export const FIXTURE_ENTRYPOINT = {
20
+ fn: Component,
21
+ params: [{}],
22
+};
23
+
24
+```
25
+
26
+## Code
27
+
28
+```javascript
29
+// @compilationMode(infer)
30
+function Component(props) {
31
+ const result = f(props);
32
+ function helper() {
33
+ return <foo />;
34
+ }
35
+ helper();
36
+ return result;
37
+}
38
+
39
+function f(props) {
40
+ return props;
41
+}
42
+
43
+export const FIXTURE_ENTRYPOINT = {
44
+ fn: Component,
45
+ params: [{}],
46
+};
47
+
48
+```
49
+
50
+### Eval output
51
+(kind: ok) {}
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-no-component-nested-jsx.js
new
+18
@@ -0,0 +1,18 @@
1
+// @compilationMode(infer)
2
+function Component(props) {
3
+ const result = f(props);
4
+ function helper() {
5
+ return <foo />;
6
+ }
7
+ helper();
8
+ return result;
9
+}
10
+
11
+function f(props) {
12
+ return props;
13
+}
14
+
15
+export const FIXTURE_ENTRYPOINT = {
16
+ fn: Component,
17
+ params: [{}],
18
+};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-no-component-obj-return.expect.md
new
+43
@@ -0,0 +1,43 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @compilationMode(infer)
6
+function Component(props) {
7
+ const ignore = <foo />;
8
+ return { foo: f(props) };
9
+}
10
+
11
+function f(props) {
12
+ return props;
13
+}
14
+
15
+export const FIXTURE_ENTRYPOINT = {
16
+ fn: Component,
17
+ params: [{}],
18
+};
19
+
20
+```
21
+
22
+## Code
23
+
24
+```javascript
25
+// @compilationMode(infer)
26
+function Component(props) {
27
+ const ignore = <foo />;
28
+ return { foo: f(props) };
29
+}
30
+
31
+function f(props) {
32
+ return props;
33
+}
34
+
35
+export const FIXTURE_ENTRYPOINT = {
36
+ fn: Component,
37
+ params: [{}],
38
+};
39
+
40
+```
41
+
42
+### Eval output
43
+(kind: ok) {"foo":{}}
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-no-component-obj-return.js
new
+14
@@ -0,0 +1,14 @@
1
+// @compilationMode(infer)
2
+function Component(props) {
3
+ const ignore = <foo />;
4
+ return { foo: f(props) };
5
+}
6
+
7
+function f(props) {
8
+ return props;
9
+}
10
+
11
+export const FIXTURE_ENTRYPOINT = {
12
+ fn: Component,
13
+ params: [{}],
14
+};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-in-nested-function-expression-object-expression.expect.md
+8
-7
@@ -4,6 +4,7 @@
4
```javascript
5
// @compilationMode(infer)
6
function Component() {
7
+ "use memo";
8
const f = () => {
9
const x = {
10
outer() {
@@ -27,13 +28,13 @@ function Component() {
28
## Error
29
30
```
30
- 7 | const y = {
31
- 8 | inner() {
32
-> 9 | return useFoo();
33
- | ^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call Custom within a function component (9:9)
34
- 10 | },
35
- 11 | };
36
- 12 | return y;
31
+ 8 | const y = {
32
+ 9 | inner() {
33
+> 10 | return useFoo();
34
+ | ^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call Custom within a function component (10:10)
35
+ 11 | },
36
+ 12 | };
37
+ 13 | return y;
38
```
39
40
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-in-nested-function-expression-object-expression.js
+1
@@ -1,5 +1,6 @@
1
// @compilationMode(infer)
2
function Component() {
3
+ "use memo";
4
const f = () => {
5
const x = {
6
outer() {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-in-nested-object-method.expect.md
+8
-7
@@ -4,6 +4,7 @@
4
```javascript
5
// @compilationMode(infer)
6
function Component() {
7
+ "use memo";
8
const x = {
9
outer() {
10
const y = {
@@ -23,13 +24,13 @@ function Component() {
24
## Error
25
26
```
26
- 5 | const y = {
27
- 6 | inner() {
28
-> 7 | return useFoo();
29
- | ^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call Custom within a function component (7:7)
30
- 8 | },
31
- 9 | };
32
- 10 | return y;
27
+ 6 | const y = {
28
+ 7 | inner() {
29
+> 8 | return useFoo();
30
+ | ^^^^^^ InvalidReact: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning). Cannot call Custom within a function component (8:8)
31
+ 9 | },
32
+ 10 | };
33
+ 11 | return y;
34
```
35
36
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/rules-of-hooks/error.invalid-hook-in-nested-object-method.js
+1
@@ -1,5 +1,6 @@
1
// @compilationMode(infer)
2
function Component() {
3
+ "use memo";
4
const x = {
5
outer() {
6
const y = {