[compiler] Examples of invalid code reassigning locals outside of render
ghstack-source-id: c48d0d0b86e47e2449982263c88cc2cfd44e326b Pull Request resolved: https://github.com/facebook/react/pull/30106
Joe Savona committed
Jun 26, 2024 at 15:09 UTC
b1d10c8a45629fe70c82c85167b01830e115b9d4
7 files changed
+385
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-effect.expect.md
new
+98
@@ -0,0 +1,98 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { useEffect } from "react";
6
+
7
+function Component() {
8
+ let local;
9
+
10
+ const reassignLocal = (newValue) => {
11
+ local = newValue;
12
+ };
13
+
14
+ const onMount = (newValue) => {
15
+ reassignLocal("hello");
16
+
17
+ if (local === newValue) {
18
+ // Without React Compiler, `reassignLocal` is freshly created
19
+ // on each render, capturing a binding to the latest `local`,
20
+ // such that invoking reassignLocal will reassign the same
21
+ // binding that we are observing in the if condition, and
22
+ // we reach this branch
23
+ console.log("`local` was updated!");
24
+ } else {
25
+ // With React Compiler enabled, `reassignLocal` is only created
26
+ // once, capturing a binding to `local` in that render pass.
27
+ // Therefore, calling `reassignLocal` will reassign the wrong
28
+ // version of `local`, and not update the binding we are checking
29
+ // in the if condition.
30
+ //
31
+ // To protect against this, we disallow reassigning locals from
32
+ // functions that escape
33
+ throw new Error("`local` not updated!");
34
+ }
35
+ };
36
+
37
+ useEffect(() => {
38
+ onMount();
39
+ }, [onMount]);
40
+
41
+ return "ok";
42
+}
43
+
44
+```
45
+
46
+## Code
47
+
48
+```javascript
49
+import { c as _c } from "react/compiler-runtime";
50
+import { useEffect } from "react";
51
+
52
+function Component() {
53
+ const $ = _c(4);
54
+ let local;
55
+ let t0;
56
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
57
+ t0 = (newValue) => {
58
+ local = newValue;
59
+ };
60
+ $[0] = t0;
61
+ } else {
62
+ t0 = $[0];
63
+ }
64
+ const reassignLocal = t0;
65
+ let t1;
66
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
67
+ t1 = (newValue_0) => {
68
+ reassignLocal("hello");
69
+ if (local === newValue_0) {
70
+ console.log("`local` was updated!");
71
+ } else {
72
+ throw new Error("`local` not updated!");
73
+ }
74
+ };
75
+ $[1] = t1;
76
+ } else {
77
+ t1 = $[1];
78
+ }
79
+ const onMount = t1;
80
+ let t2;
81
+ let t3;
82
+ if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
83
+ t2 = () => {
84
+ onMount();
85
+ };
86
+ t3 = [onMount];
87
+ $[2] = t2;
88
+ $[3] = t3;
89
+ } else {
90
+ t2 = $[2];
91
+ t3 = $[3];
92
+ }
93
+ useEffect(t2, t3);
94
+ return "ok";
95
+}
96
+
97
+```
98
+
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-effect.js
new
+38
@@ -0,0 +1,38 @@
1
+import { useEffect } from "react";
2
+
3
+function Component() {
4
+ let local;
5
+
6
+ const reassignLocal = (newValue) => {
7
+ local = newValue;
8
+ };
9
+
10
+ const onMount = (newValue) => {
11
+ reassignLocal("hello");
12
+
13
+ if (local === newValue) {
14
+ // Without React Compiler, `reassignLocal` is freshly created
15
+ // on each render, capturing a binding to the latest `local`,
16
+ // such that invoking reassignLocal will reassign the same
17
+ // binding that we are observing in the if condition, and
18
+ // we reach this branch
19
+ console.log("`local` was updated!");
20
+ } else {
21
+ // With React Compiler enabled, `reassignLocal` is only created
22
+ // once, capturing a binding to `local` in that render pass.
23
+ // Therefore, calling `reassignLocal` will reassign the wrong
24
+ // version of `local`, and not update the binding we are checking
25
+ // in the if condition.
26
+ //
27
+ // To protect against this, we disallow reassigning locals from
28
+ // functions that escape
29
+ throw new Error("`local` not updated!");
30
+ }
31
+ };
32
+
33
+ useEffect(() => {
34
+ onMount();
35
+ }, [onMount]);
36
+
37
+ return "ok";
38
+}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-hook-argument.expect.md
new
+96
@@ -0,0 +1,96 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { useEffect } from "react";
6
+import { useIdentity } from "shared-runtime";
7
+
8
+function Component() {
9
+ let local;
10
+
11
+ const reassignLocal = (newValue) => {
12
+ local = newValue;
13
+ };
14
+
15
+ const callback = (newValue) => {
16
+ reassignLocal("hello");
17
+
18
+ if (local === newValue) {
19
+ // Without React Compiler, `reassignLocal` is freshly created
20
+ // on each render, capturing a binding to the latest `local`,
21
+ // such that invoking reassignLocal will reassign the same
22
+ // binding that we are observing in the if condition, and
23
+ // we reach this branch
24
+ console.log("`local` was updated!");
25
+ } else {
26
+ // With React Compiler enabled, `reassignLocal` is only created
27
+ // once, capturing a binding to `local` in that render pass.
28
+ // Therefore, calling `reassignLocal` will reassign the wrong
29
+ // version of `local`, and not update the binding we are checking
30
+ // in the if condition.
31
+ //
32
+ // To protect against this, we disallow reassigning locals from
33
+ // functions that escape
34
+ throw new Error("`local` not updated!");
35
+ }
36
+ };
37
+
38
+ useIdentity(() => {
39
+ callback();
40
+ });
41
+
42
+ return "ok";
43
+}
44
+
45
+```
46
+
47
+## Code
48
+
49
+```javascript
50
+import { c as _c } from "react/compiler-runtime";
51
+import { useEffect } from "react";
52
+import { useIdentity } from "shared-runtime";
53
+
54
+function Component() {
55
+ const $ = _c(3);
56
+ let local;
57
+ let t0;
58
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
59
+ t0 = (newValue) => {
60
+ local = newValue;
61
+ };
62
+ $[0] = t0;
63
+ } else {
64
+ t0 = $[0];
65
+ }
66
+ const reassignLocal = t0;
67
+ let t1;
68
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
69
+ t1 = (newValue_0) => {
70
+ reassignLocal("hello");
71
+ if (local === newValue_0) {
72
+ console.log("`local` was updated!");
73
+ } else {
74
+ throw new Error("`local` not updated!");
75
+ }
76
+ };
77
+ $[1] = t1;
78
+ } else {
79
+ t1 = $[1];
80
+ }
81
+ const callback = t1;
82
+ let t2;
83
+ if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
84
+ t2 = () => {
85
+ callback();
86
+ };
87
+ $[2] = t2;
88
+ } else {
89
+ t2 = $[2];
90
+ }
91
+ useIdentity(t2);
92
+ return "ok";
93
+}
94
+
95
+```
96
+
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-hook-argument.js
new
+39
@@ -0,0 +1,39 @@
1
+import { useEffect } from "react";
2
+import { useIdentity } from "shared-runtime";
3
+
4
+function Component() {
5
+ let local;
6
+
7
+ const reassignLocal = (newValue) => {
8
+ local = newValue;
9
+ };
10
+
11
+ const callback = (newValue) => {
12
+ reassignLocal("hello");
13
+
14
+ if (local === newValue) {
15
+ // Without React Compiler, `reassignLocal` is freshly created
16
+ // on each render, capturing a binding to the latest `local`,
17
+ // such that invoking reassignLocal will reassign the same
18
+ // binding that we are observing in the if condition, and
19
+ // we reach this branch
20
+ console.log("`local` was updated!");
21
+ } else {
22
+ // With React Compiler enabled, `reassignLocal` is only created
23
+ // once, capturing a binding to `local` in that render pass.
24
+ // Therefore, calling `reassignLocal` will reassign the wrong
25
+ // version of `local`, and not update the binding we are checking
26
+ // in the if condition.
27
+ //
28
+ // To protect against this, we disallow reassigning locals from
29
+ // functions that escape
30
+ throw new Error("`local` not updated!");
31
+ }
32
+ };
33
+
34
+ useIdentity(() => {
35
+ callback();
36
+ });
37
+
38
+ return "ok";
39
+}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-jsx-callback.expect.md
new
+77
@@ -0,0 +1,77 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component() {
6
+ let local;
7
+
8
+ const reassignLocal = (newValue) => {
9
+ local = newValue;
10
+ };
11
+
12
+ const onClick = (newValue) => {
13
+ reassignLocal("hello");
14
+
15
+ if (local === newValue) {
16
+ // Without React Compiler, `reassignLocal` is freshly created
17
+ // on each render, capturing a binding to the latest `local`,
18
+ // such that invoking reassignLocal will reassign the same
19
+ // binding that we are observing in the if condition, and
20
+ // we reach this branch
21
+ console.log("`local` was updated!");
22
+ } else {
23
+ // With React Compiler enabled, `reassignLocal` is only created
24
+ // once, capturing a binding to `local` in that render pass.
25
+ // Therefore, calling `reassignLocal` will reassign the wrong
26
+ // version of `local`, and not update the binding we are checking
27
+ // in the if condition.
28
+ //
29
+ // To protect against this, we disallow reassigning locals from
30
+ // functions that escape
31
+ throw new Error("`local` not updated!");
32
+ }
33
+ };
34
+
35
+ return <button onClick={onClick}>Submit</button>;
36
+}
37
+
38
+```
39
+
40
+## Code
41
+
42
+```javascript
43
+import { c as _c } from "react/compiler-runtime";
44
+function Component() {
45
+ const $ = _c(2);
46
+ let local;
47
+ let t0;
48
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
49
+ t0 = (newValue) => {
50
+ local = newValue;
51
+ };
52
+ $[0] = t0;
53
+ } else {
54
+ t0 = $[0];
55
+ }
56
+ const reassignLocal = t0;
57
+ let t1;
58
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
59
+ const onClick = (newValue_0) => {
60
+ reassignLocal("hello");
61
+ if (local === newValue_0) {
62
+ console.log("`local` was updated!");
63
+ } else {
64
+ throw new Error("`local` not updated!");
65
+ }
66
+ };
67
+
68
+ t1 = <button onClick={onClick}>Submit</button>;
69
+ $[1] = t1;
70
+ } else {
71
+ t1 = $[1];
72
+ }
73
+ return t1;
74
+}
75
+
76
+```
77
+
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-jsx-callback.js
new
+32
@@ -0,0 +1,32 @@
1
+function Component() {
2
+ let local;
3
+
4
+ const reassignLocal = (newValue) => {
5
+ local = newValue;
6
+ };
7
+
8
+ const onClick = (newValue) => {
9
+ reassignLocal("hello");
10
+
11
+ if (local === newValue) {
12
+ // Without React Compiler, `reassignLocal` is freshly created
13
+ // on each render, capturing a binding to the latest `local`,
14
+ // such that invoking reassignLocal will reassign the same
15
+ // binding that we are observing in the if condition, and
16
+ // we reach this branch
17
+ console.log("`local` was updated!");
18
+ } else {
19
+ // With React Compiler enabled, `reassignLocal` is only created
20
+ // once, capturing a binding to `local` in that render pass.
21
+ // Therefore, calling `reassignLocal` will reassign the wrong
22
+ // version of `local`, and not update the binding we are checking
23
+ // in the if condition.
24
+ //
25
+ // To protect against this, we disallow reassigning locals from
26
+ // functions that escape
27
+ throw new Error("`local` not updated!");
28
+ }
29
+ };
30
+
31
+ return <button onClick={onClick}>Submit</button>;
32
+}
compiler/packages/snap/src/SproutTodoFilter.ts
+5
@@ -483,6 +483,11 @@ const skipFilter = new Set([
483
"rules-of-hooks/rules-of-hooks-93dc5d5e538a",
484
"rules-of-hooks/rules-of-hooks-69521d94fa03",
485
486
+ // should error
487
+ "todo.invalid-reassign-local-variable-in-jsx-callback",
488
+ "todo.invalid-reassign-local-variable-in-hook-argument",
489
+ "todo.invalid-reassign-local-variable-in-effect",
490
+
491
// bugs
492
"bug-invalid-hoisting-functionexpr",
493
"original-reactive-scopes-fork/bug-nonmutating-capture-in-unsplittable-memo-block",