@samitouri / QOS-React-2 / commits / b1d10c8a45

[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",