@samitouri / QOS-React / commits / df080d228b

[compiler] Copy fixtures affected by new inference (#33495)

--- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33495). * #33571 * #33558 * #33547 * #33543 * #33533 * #33532 * #33530 * #33526 * #33522 * #33518 * #33514 * #33513 * #33512 * #33504 * #33500 * #33497 * #33496 * __->__ #33495 * #33494 * #33572

Joseph Savona committed Jun 18, 2025 at 12:58 UTC df080d228bdf5260067235c64daaa57ec3cfac23
46 files changed +1912
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/aliased-nested-scope-truncated-dep.expect.md new
+221
@@ -0,0 +1,221 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {
6 + Stringify,
7 + mutate,
8 + identity,
9 + shallowCopy,
10 + setPropertyByKey,
11 +} from 'shared-runtime';
12 +
13 +/**
14 + * This fixture is similar to `bug-aliased-capture-aliased-mutate` and
15 + * `nonmutating-capture-in-unsplittable-memo-block`, but with a focus on
16 + * dependency extraction.
17 + *
18 + * NOTE: this fixture is currently valid, but will break with optimizations:
19 + * - Scope and mutable-range based reordering may move the array creation
20 + * *after* the `mutate(aliasedObj)` call. This is invalid if mutate
21 + * reassigns inner properties.
22 + * - RecycleInto or other deeper-equality optimizations may produce invalid
23 + * output -- it may compare the array's contents / dependencies too early.
24 + * - Runtime validation for immutable values will break if `mutate` does
25 + * interior mutation of the value captured into the array.
26 + *
27 + * Before scope block creation, HIR looks like this:
28 + * //
29 + * // $1 is unscoped as obj's mutable range will be
30 + * // extended in a later pass
31 + * //
32 + * $1 = LoadLocal obj@0[0:12]
33 + * $2 = PropertyLoad $1.id
34 + * //
35 + * // $3 gets assigned a scope as Array is an allocating
36 + * // instruction, but this does *not* get extended or
37 + * // merged into the later mutation site.
38 + * // (explained in `bug-aliased-capture-aliased-mutate`)
39 + * //
40 + * $3@1 = Array[$2]
41 + * ...
42 + * $10@0 = LoadLocal shallowCopy@0[0, 12]
43 + * $11 = LoadGlobal mutate
44 + * $12 = $11($10@0[0, 12])
45 + *
46 + * When filling in scope dependencies, we find that it's incorrect to depend on
47 + * PropertyLoads from obj as it hasn't completed its mutable range. Following
48 + * the immutable / mutable-new typing system, we check the identity of obj to
49 + * detect whether it was newly created (and thus mutable) in this render pass.
50 + *
51 + * HIR with scopes looks like this.
52 + * bb0:
53 + * $1 = LoadLocal obj@0[0:12]
54 + * $2 = PropertyLoad $1.id
55 + * scopeTerminal deps=[obj@0] block=bb1 fallt=bb2
56 + * bb1:
57 + * $3@1 = Array[$2]
58 + * goto bb2
59 + * bb2:
60 + * ...
61 + *
62 + * This is surprising as deps now is entirely decoupled from temporaries used
63 + * by the block itself. scope @1's instructions now reference a value (1)
64 + * produced outside its scope range and (2) not represented in its dependencies
65 + *
66 + * The right thing to do is to ensure that all Loads from a value get assigned
67 + * the value's reactive scope. This also requires track mutating and aliasing
68 + * separately from scope range. In this example, that would correctly merge
69 + * the scopes of $3 with obj.
70 + * Runtime validation and optimizations such as ReactiveGraph-based reordering
71 + * require this as well.
72 + *
73 + * A tempting fix is to instead extend $3's ReactiveScope range up to include
74 + * $2 (the PropertyLoad). This fixes dependency deduping but not reordering
75 + * and mutability.
76 + */
77 +function Component({prop}) {
78 + let obj = shallowCopy(prop);
79 + const aliasedObj = identity(obj);
80 +
81 + // [obj.id] currently is assigned its own reactive scope
82 + const id = [obj.id];
83 +
84 + // Writing to the alias may reassign to previously captured references.
85 + // The compiler currently produces valid output, but this breaks with
86 + // reordering, recycleInto, and other potential optimizations.
87 + mutate(aliasedObj);
88 + setPropertyByKey(aliasedObj, 'id', prop.id + 1);
89 +
90 + return <Stringify id={id} />;
91 +}
92 +
93 +export const FIXTURE_ENTRYPOINT = {
94 + fn: Component,
95 + params: [{prop: {id: 1}}],
96 + sequentialRenders: [{prop: {id: 1}}, {prop: {id: 1}}, {prop: {id: 2}}],
97 +};
98 +
99 +```
100 +
101 +## Code
102 +
103 +```javascript
104 +import { c as _c } from "react/compiler-runtime";
105 +import {
106 + Stringify,
107 + mutate,
108 + identity,
109 + shallowCopy,
110 + setPropertyByKey,
111 +} from "shared-runtime";
112 +
113 +/**
114 + * This fixture is similar to `bug-aliased-capture-aliased-mutate` and
115 + * `nonmutating-capture-in-unsplittable-memo-block`, but with a focus on
116 + * dependency extraction.
117 + *
118 + * NOTE: this fixture is currently valid, but will break with optimizations:
119 + * - Scope and mutable-range based reordering may move the array creation
120 + * *after* the `mutate(aliasedObj)` call. This is invalid if mutate
121 + * reassigns inner properties.
122 + * - RecycleInto or other deeper-equality optimizations may produce invalid
123 + * output -- it may compare the array's contents / dependencies too early.
124 + * - Runtime validation for immutable values will break if `mutate` does
125 + * interior mutation of the value captured into the array.
126 + *
127 + * Before scope block creation, HIR looks like this:
128 + * //
129 + * // $1 is unscoped as obj's mutable range will be
130 + * // extended in a later pass
131 + * //
132 + * $1 = LoadLocal obj@0[0:12]
133 + * $2 = PropertyLoad $1.id
134 + * //
135 + * // $3 gets assigned a scope as Array is an allocating
136 + * // instruction, but this does *not* get extended or
137 + * // merged into the later mutation site.
138 + * // (explained in `bug-aliased-capture-aliased-mutate`)
139 + * //
140 + * $3@1 = Array[$2]
141 + * ...
142 + * $10@0 = LoadLocal shallowCopy@0[0, 12]
143 + * $11 = LoadGlobal mutate
144 + * $12 = $11($10@0[0, 12])
145 + *
146 + * When filling in scope dependencies, we find that it's incorrect to depend on
147 + * PropertyLoads from obj as it hasn't completed its mutable range. Following
148 + * the immutable / mutable-new typing system, we check the identity of obj to
149 + * detect whether it was newly created (and thus mutable) in this render pass.
150 + *
151 + * HIR with scopes looks like this.
152 + * bb0:
153 + * $1 = LoadLocal obj@0[0:12]
154 + * $2 = PropertyLoad $1.id
155 + * scopeTerminal deps=[obj@0] block=bb1 fallt=bb2
156 + * bb1:
157 + * $3@1 = Array[$2]
158 + * goto bb2
159 + * bb2:
160 + * ...
161 + *
162 + * This is surprising as deps now is entirely decoupled from temporaries used
163 + * by the block itself. scope @1's instructions now reference a value (1)
164 + * produced outside its scope range and (2) not represented in its dependencies
165 + *
166 + * The right thing to do is to ensure that all Loads from a value get assigned
167 + * the value's reactive scope. This also requires track mutating and aliasing
168 + * separately from scope range. In this example, that would correctly merge
169 + * the scopes of $3 with obj.
170 + * Runtime validation and optimizations such as ReactiveGraph-based reordering
171 + * require this as well.
172 + *
173 + * A tempting fix is to instead extend $3's ReactiveScope range up to include
174 + * $2 (the PropertyLoad). This fixes dependency deduping but not reordering
175 + * and mutability.
176 + */
177 +function Component(t0) {
178 + const $ = _c(4);
179 + const { prop } = t0;
180 + let t1;
181 + if ($[0] !== prop) {
182 + const obj = shallowCopy(prop);
183 + const aliasedObj = identity(obj);
184 + let t2;
185 + if ($[2] !== obj) {
186 + t2 = [obj.id];
187 + $[2] = obj;
188 + $[3] = t2;
189 + } else {
190 + t2 = $[3];
191 + }
192 + const id = t2;
193 +
194 + mutate(aliasedObj);
195 + setPropertyByKey(aliasedObj, "id", prop.id + 1);
196 +
197 + t1 = <Stringify id={id} />;
198 + $[0] = prop;
199 + $[1] = t1;
200 + } else {
201 + t1 = $[1];
202 + }
203 + return t1;
204 +}
205 +
206 +export const FIXTURE_ENTRYPOINT = {
207 + fn: Component,
208 + params: [{ prop: { id: 1 } }],
209 + sequentialRenders: [
210 + { prop: { id: 1 } },
211 + { prop: { id: 1 } },
212 + { prop: { id: 2 } },
213 + ],
214 +};
215 +
216 +```
217 +
218 +### Eval output
219 +(kind: ok) <div>{"id":[1]}</div>
220 +<div>{"id":[1]}</div>
221 +<div>{"id":[2]}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/aliased-nested-scope-truncated-dep.tsx new
+93
@@ -0,0 +1,93 @@
1 +import {
2 + Stringify,
3 + mutate,
4 + identity,
5 + shallowCopy,
6 + setPropertyByKey,
7 +} from 'shared-runtime';
8 +
9 +/**
10 + * This fixture is similar to `bug-aliased-capture-aliased-mutate` and
11 + * `nonmutating-capture-in-unsplittable-memo-block`, but with a focus on
12 + * dependency extraction.
13 + *
14 + * NOTE: this fixture is currently valid, but will break with optimizations:
15 + * - Scope and mutable-range based reordering may move the array creation
16 + * *after* the `mutate(aliasedObj)` call. This is invalid if mutate
17 + * reassigns inner properties.
18 + * - RecycleInto or other deeper-equality optimizations may produce invalid
19 + * output -- it may compare the array's contents / dependencies too early.
20 + * - Runtime validation for immutable values will break if `mutate` does
21 + * interior mutation of the value captured into the array.
22 + *
23 + * Before scope block creation, HIR looks like this:
24 + * //
25 + * // $1 is unscoped as obj's mutable range will be
26 + * // extended in a later pass
27 + * //
28 + * $1 = LoadLocal obj@0[0:12]
29 + * $2 = PropertyLoad $1.id
30 + * //
31 + * // $3 gets assigned a scope as Array is an allocating
32 + * // instruction, but this does *not* get extended or
33 + * // merged into the later mutation site.
34 + * // (explained in `bug-aliased-capture-aliased-mutate`)
35 + * //
36 + * $3@1 = Array[$2]
37 + * ...
38 + * $10@0 = LoadLocal shallowCopy@0[0, 12]
39 + * $11 = LoadGlobal mutate
40 + * $12 = $11($10@0[0, 12])
41 + *
42 + * When filling in scope dependencies, we find that it's incorrect to depend on
43 + * PropertyLoads from obj as it hasn't completed its mutable range. Following
44 + * the immutable / mutable-new typing system, we check the identity of obj to
45 + * detect whether it was newly created (and thus mutable) in this render pass.
46 + *
47 + * HIR with scopes looks like this.
48 + * bb0:
49 + * $1 = LoadLocal obj@0[0:12]
50 + * $2 = PropertyLoad $1.id
51 + * scopeTerminal deps=[obj@0] block=bb1 fallt=bb2
52 + * bb1:
53 + * $3@1 = Array[$2]
54 + * goto bb2
55 + * bb2:
56 + * ...
57 + *
58 + * This is surprising as deps now is entirely decoupled from temporaries used
59 + * by the block itself. scope @1's instructions now reference a value (1)
60 + * produced outside its scope range and (2) not represented in its dependencies
61 + *
62 + * The right thing to do is to ensure that all Loads from a value get assigned
63 + * the value's reactive scope. This also requires track mutating and aliasing
64 + * separately from scope range. In this example, that would correctly merge
65 + * the scopes of $3 with obj.
66 + * Runtime validation and optimizations such as ReactiveGraph-based reordering
67 + * require this as well.
68 + *
69 + * A tempting fix is to instead extend $3's ReactiveScope range up to include
70 + * $2 (the PropertyLoad). This fixes dependency deduping but not reordering
71 + * and mutability.
72 + */
73 +function Component({prop}) {
74 + let obj = shallowCopy(prop);
75 + const aliasedObj = identity(obj);
76 +
77 + // [obj.id] currently is assigned its own reactive scope
78 + const id = [obj.id];
79 +
80 + // Writing to the alias may reassign to previously captured references.
81 + // The compiler currently produces valid output, but this breaks with
82 + // reordering, recycleInto, and other potential optimizations.
83 + mutate(aliasedObj);
84 + setPropertyByKey(aliasedObj, 'id', prop.id + 1);
85 +
86 + return <Stringify id={id} />;
87 +}
88 +
89 +export const FIXTURE_ENTRYPOINT = {
90 + fn: Component,
91 + params: [{prop: {id: 1}}],
92 + sequentialRenders: [{prop: {id: 1}}, {prop: {id: 1}}, {prop: {id: 2}}],
93 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-named-callback-cross-context.expect.md new
+133
@@ -0,0 +1,133 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {Stringify} from 'shared-runtime';
6 +
7 +/**
8 + * Forked from array-map-simple.js
9 + *
10 + * Named lambdas (e.g. cb1) may be defined in the top scope of a function and
11 + * used in a different lambda (getArrMap1).
12 + *
13 + * Here, we should try to determine if cb1 is actually called. In this case:
14 + * - getArrMap1 is assumed to be called as it's passed to JSX
15 + * - cb1 is not assumed to be called since it's only used as a call operand
16 + */
17 +function useFoo({arr1, arr2}) {
18 + const cb1 = e => arr1[0].value + e.value;
19 + const getArrMap1 = () => arr1.map(cb1);
20 + const cb2 = e => arr2[0].value + e.value;
21 + const getArrMap2 = () => arr1.map(cb2);
22 + return (
23 + <Stringify
24 + getArrMap1={getArrMap1}
25 + getArrMap2={getArrMap2}
26 + shouldInvokeFns={true}
27 + />
28 + );
29 +}
30 +
31 +export const FIXTURE_ENTRYPOINT = {
32 + fn: useFoo,
33 + params: [{arr1: [], arr2: []}],
34 + sequentialRenders: [
35 + {arr1: [], arr2: []},
36 + {arr1: [], arr2: null},
37 + {arr1: [{value: 1}, {value: 2}], arr2: [{value: -1}]},
38 + ],
39 +};
40 +
41 +```
42 +
43 +## Code
44 +
45 +```javascript
46 +import { c as _c } from "react/compiler-runtime";
47 +import { Stringify } from "shared-runtime";
48 +
49 +/**
50 + * Forked from array-map-simple.js
51 + *
52 + * Named lambdas (e.g. cb1) may be defined in the top scope of a function and
53 + * used in a different lambda (getArrMap1).
54 + *
55 + * Here, we should try to determine if cb1 is actually called. In this case:
56 + * - getArrMap1 is assumed to be called as it's passed to JSX
57 + * - cb1 is not assumed to be called since it's only used as a call operand
58 + */
59 +function useFoo(t0) {
60 + const $ = _c(13);
61 + const { arr1, arr2 } = t0;
62 + let t1;
63 + if ($[0] !== arr1[0]) {
64 + t1 = (e) => arr1[0].value + e.value;
65 + $[0] = arr1[0];
66 + $[1] = t1;
67 + } else {
68 + t1 = $[1];
69 + }
70 + const cb1 = t1;
71 + let t2;
72 + if ($[2] !== arr1 || $[3] !== cb1) {
73 + t2 = () => arr1.map(cb1);
74 + $[2] = arr1;
75 + $[3] = cb1;
76 + $[4] = t2;
77 + } else {
78 + t2 = $[4];
79 + }
80 + const getArrMap1 = t2;
81 + let t3;
82 + if ($[5] !== arr2) {
83 + t3 = (e_0) => arr2[0].value + e_0.value;
84 + $[5] = arr2;
85 + $[6] = t3;
86 + } else {
87 + t3 = $[6];
88 + }
89 + const cb2 = t3;
90 + let t4;
91 + if ($[7] !== arr1 || $[8] !== cb2) {
92 + t4 = () => arr1.map(cb2);
93 + $[7] = arr1;
94 + $[8] = cb2;
95 + $[9] = t4;
96 + } else {
97 + t4 = $[9];
98 + }
99 + const getArrMap2 = t4;
100 + let t5;
101 + if ($[10] !== getArrMap1 || $[11] !== getArrMap2) {
102 + t5 = (
103 + <Stringify
104 + getArrMap1={getArrMap1}
105 + getArrMap2={getArrMap2}
106 + shouldInvokeFns={true}
107 + />
108 + );
109 + $[10] = getArrMap1;
110 + $[11] = getArrMap2;
111 + $[12] = t5;
112 + } else {
113 + t5 = $[12];
114 + }
115 + return t5;
116 +}
117 +
118 +export const FIXTURE_ENTRYPOINT = {
119 + fn: useFoo,
120 + params: [{ arr1: [], arr2: [] }],
121 + sequentialRenders: [
122 + { arr1: [], arr2: [] },
123 + { arr1: [], arr2: null },
124 + { arr1: [{ value: 1 }, { value: 2 }], arr2: [{ value: -1 }] },
125 + ],
126 +};
127 +
128 +```
129 +
130 +### Eval output
131 +(kind: ok) <div>{"getArrMap1":{"kind":"Function","result":[]},"getArrMap2":{"kind":"Function","result":[]},"shouldInvokeFns":true}</div>
132 +<div>{"getArrMap1":{"kind":"Function","result":[]},"getArrMap2":{"kind":"Function","result":[]},"shouldInvokeFns":true}</div>
133 +<div>{"getArrMap1":{"kind":"Function","result":[2,3]},"getArrMap2":{"kind":"Function","result":[0,1]},"shouldInvokeFns":true}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/array-map-named-callback-cross-context.js new
+35
@@ -0,0 +1,35 @@
1 +import {Stringify} from 'shared-runtime';
2 +
3 +/**
4 + * Forked from array-map-simple.js
5 + *
6 + * Named lambdas (e.g. cb1) may be defined in the top scope of a function and
7 + * used in a different lambda (getArrMap1).
8 + *
9 + * Here, we should try to determine if cb1 is actually called. In this case:
10 + * - getArrMap1 is assumed to be called as it's passed to JSX
11 + * - cb1 is not assumed to be called since it's only used as a call operand
12 + */
13 +function useFoo({arr1, arr2}) {
14 + const cb1 = e => arr1[0].value + e.value;
15 + const getArrMap1 = () => arr1.map(cb1);
16 + const cb2 = e => arr2[0].value + e.value;
17 + const getArrMap2 = () => arr1.map(cb2);
18 + return (
19 + <Stringify
20 + getArrMap1={getArrMap1}
21 + getArrMap2={getArrMap2}
22 + shouldInvokeFns={true}
23 + />
24 + );
25 +}
26 +
27 +export const FIXTURE_ENTRYPOINT = {
28 + fn: useFoo,
29 + params: [{arr1: [], arr2: []}],
30 + sequentialRenders: [
31 + {arr1: [], arr2: []},
32 + {arr1: [], arr2: null},
33 + {arr1: [{value: 1}, {value: 2}], arr2: [{value: -1}]},
34 + ],
35 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-2-iife.expect.md new
+52
@@ -0,0 +1,52 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function bar(a) {
6 + let x = [a];
7 + let y = {};
8 + (function () {
9 + y = x[0][1];
10 + })();
11 +
12 + return y;
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: bar,
17 + params: [['val1', 'val2']],
18 + isComponent: false,
19 +};
20 +
21 +```
22 +
23 +## Code
24 +
25 +```javascript
26 +import { c as _c } from "react/compiler-runtime";
27 +function bar(a) {
28 + const $ = _c(2);
29 + let y;
30 + if ($[0] !== a) {
31 + const x = [a];
32 + y = {};
33 +
34 + y = x[0][1];
35 + $[0] = a;
36 + $[1] = y;
37 + } else {
38 + y = $[1];
39 + }
40 + return y;
41 +}
42 +
43 +export const FIXTURE_ENTRYPOINT = {
44 + fn: bar,
45 + params: [["val1", "val2"]],
46 + isComponent: false,
47 +};
48 +
49 +```
50 +
51 +### Eval output
52 +(kind: ok) "val2"
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-2-iife.js new
+15
@@ -0,0 +1,15 @@
1 +function bar(a) {
2 + let x = [a];
3 + let y = {};
4 + (function () {
5 + y = x[0][1];
6 + })();
7 +
8 + return y;
9 +}
10 +
11 +export const FIXTURE_ENTRYPOINT = {
12 + fn: bar,
13 + params: [['val1', 'val2']],
14 + isComponent: false,
15 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-3-iife.expect.md new
+61
@@ -0,0 +1,61 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function bar(a, b) {
6 + let x = [a, b];
7 + let y = {};
8 + let t = {};
9 + (function () {
10 + y = x[0][1];
11 + t = x[1][0];
12 + })();
13 +
14 + return y;
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: bar,
19 + params: [
20 + [1, 2],
21 + [2, 3],
22 + ],
23 +};
24 +
25 +```
26 +
27 +## Code
28 +
29 +```javascript
30 +import { c as _c } from "react/compiler-runtime";
31 +function bar(a, b) {
32 + const $ = _c(3);
33 + let y;
34 + if ($[0] !== a || $[1] !== b) {
35 + const x = [a, b];
36 + y = {};
37 + let t = {};
38 +
39 + y = x[0][1];
40 + t = x[1][0];
41 + $[0] = a;
42 + $[1] = b;
43 + $[2] = y;
44 + } else {
45 + y = $[2];
46 + }
47 + return y;
48 +}
49 +
50 +export const FIXTURE_ENTRYPOINT = {
51 + fn: bar,
52 + params: [
53 + [1, 2],
54 + [2, 3],
55 + ],
56 +};
57 +
58 +```
59 +
60 +### Eval output
61 +(kind: ok) 2
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-3-iife.js new
+19
@@ -0,0 +1,19 @@
1 +function bar(a, b) {
2 + let x = [a, b];
3 + let y = {};
4 + let t = {};
5 + (function () {
6 + y = x[0][1];
7 + t = x[1][0];
8 + })();
9 +
10 + return y;
11 +}
12 +
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: bar,
15 + params: [
16 + [1, 2],
17 + [2, 3],
18 + ],
19 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-4-iife.expect.md new
+52
@@ -0,0 +1,52 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function bar(a) {
6 + let x = [a];
7 + let y = {};
8 + (function () {
9 + y = x[0].a[1];
10 + })();
11 +
12 + return y;
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: bar,
17 + params: [{a: ['val1', 'val2']}],
18 + isComponent: false,
19 +};
20 +
21 +```
22 +
23 +## Code
24 +
25 +```javascript
26 +import { c as _c } from "react/compiler-runtime";
27 +function bar(a) {
28 + const $ = _c(2);
29 + let y;
30 + if ($[0] !== a) {
31 + const x = [a];
32 + y = {};
33 +
34 + y = x[0].a[1];
35 + $[0] = a;
36 + $[1] = y;
37 + } else {
38 + y = $[1];
39 + }
40 + return y;
41 +}
42 +
43 +export const FIXTURE_ENTRYPOINT = {
44 + fn: bar,
45 + params: [{ a: ["val1", "val2"] }],
46 + isComponent: false,
47 +};
48 +
49 +```
50 +
51 +### Eval output
52 +(kind: ok) "val2"
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-4-iife.js new
+15
@@ -0,0 +1,15 @@
1 +function bar(a) {
2 + let x = [a];
3 + let y = {};
4 + (function () {
5 + y = x[0].a[1];
6 + })();
7 +
8 + return y;
9 +}
10 +
11 +export const FIXTURE_ENTRYPOINT = {
12 + fn: bar,
13 + params: [{a: ['val1', 'val2']}],
14 + isComponent: false,
15 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-iife.expect.md new
+50
@@ -0,0 +1,50 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function bar(a) {
6 + let x = [a];
7 + let y = {};
8 + (function () {
9 + y = x[0];
10 + })();
11 +
12 + return y;
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: bar,
17 + params: ['TodoAdd'],
18 +};
19 +
20 +```
21 +
22 +## Code
23 +
24 +```javascript
25 +import { c as _c } from "react/compiler-runtime";
26 +function bar(a) {
27 + const $ = _c(2);
28 + let y;
29 + if ($[0] !== a) {
30 + const x = [a];
31 + y = {};
32 +
33 + y = x[0];
34 + $[0] = a;
35 + $[1] = y;
36 + } else {
37 + y = $[1];
38 + }
39 + return y;
40 +}
41 +
42 +export const FIXTURE_ENTRYPOINT = {
43 + fn: bar,
44 + params: ["TodoAdd"],
45 +};
46 +
47 +```
48 +
49 +### Eval output
50 +(kind: ok) "TodoAdd"
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/capturing-function-alias-computed-load-iife.js new
+14
@@ -0,0 +1,14 @@
1 +function bar(a) {
2 + let x = [a];
3 + let y = {};
4 + (function () {
5 + y = x[0];
6 + })();
7 +
8 + return y;
9 +}
10 +
11 +export const FIXTURE_ENTRYPOINT = {
12 + fn: bar,
13 + params: ['TodoAdd'],
14 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-impure-functions-in-render.expect.md new
+33
@@ -0,0 +1,33 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateNoImpureFunctionsInRender
6 +
7 +function Component() {
8 + const date = Date.now();
9 + const now = performance.now();
10 + const rand = Math.random();
11 + return <Foo date={date} now={now} rand={rand} />;
12 +}
13 +
14 +```
15 +
16 +
17 +## Error
18 +
19 +```
20 + 2 |
21 + 3 | function Component() {
22 +> 4 | const date = Date.now();
23 + | ^^^^^^^^ InvalidReact: Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). `Date.now` is an impure function whose results may change on every call (4:4)
24 +
25 +InvalidReact: Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). `performance.now` is an impure function whose results may change on every call (5:5)
26 +
27 +InvalidReact: Calling an impure function can produce unstable results. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). `Math.random` is an impure function whose results may change on every call (6:6)
28 + 5 | const now = performance.now();
29 + 6 | const rand = Math.random();
30 + 7 | return <Foo date={date} now={now} rand={rand} />;
31 +```
32 +
33 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.invalid-impure-functions-in-render.js new
+8
@@ -0,0 +1,8 @@
1 +// @validateNoImpureFunctionsInRender
2 +
3 +function Component() {
4 + const date = Date.now();
5 + const now = performance.now();
6 + const rand = Math.random();
7 + return <Foo date={date} now={now} rand={rand} />;
8 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-hook-argument.expect.md new
+24
@@ -0,0 +1,24 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function useHook(a, b) {
6 + b.test = 1;
7 + a.test = 2;
8 +}
9 +
10 +```
11 +
12 +
13 +## Error
14 +
15 +```
16 + 1 | function useHook(a, b) {
17 +> 2 | b.test = 1;
18 + | ^ InvalidReact: Mutating component props or hook arguments is not allowed. Consider using a local variable instead (2:2)
19 + 3 | a.test = 2;
20 + 4 | }
21 + 5 |
22 +```
23 +
24 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.mutate-hook-argument.js new
+4
@@ -0,0 +1,4 @@
1 +function useHook(a, b) {
2 + b.test = 1;
3 + a.test = 2;
4 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.not-useEffect-external-mutate.expect.md new
+29
@@ -0,0 +1,29 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +let x = {a: 42};
6 +
7 +function Component(props) {
8 + foo(() => {
9 + x.a = 10;
10 + x.a = 20;
11 + });
12 +}
13 +
14 +```
15 +
16 +
17 +## Error
18 +
19 +```
20 + 3 | function Component(props) {
21 + 4 | foo(() => {
22 +> 5 | x.a = 10;
23 + | ^ InvalidReact: Writing to a variable defined outside a component or hook is not allowed. Consider using an effect (5:5)
24 + 6 | x.a = 20;
25 + 7 | });
26 + 8 | }
27 +```
28 +
29 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.not-useEffect-external-mutate.js new
+8
@@ -0,0 +1,8 @@
1 +let x = {a: 42};
2 +
3 +function Component(props) {
4 + foo(() => {
5 + x.a = 10;
6 + x.a = 20;
7 + });
8 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global-indirect.expect.md new
+29
@@ -0,0 +1,29 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component() {
6 + const foo = () => {
7 + // Cannot assign to globals
8 + someUnknownGlobal = true;
9 + moduleLocal = true;
10 + };
11 + foo();
12 +}
13 +
14 +```
15 +
16 +
17 +## Error
18 +
19 +```
20 + 2 | const foo = () => {
21 + 3 | // Cannot assign to globals
22 +> 4 | someUnknownGlobal = true;
23 + | ^^^^^^^^^^^^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (4:4)
24 + 5 | moduleLocal = true;
25 + 6 | };
26 + 7 | foo();
27 +```
28 +
29 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global-indirect.js new
+8
@@ -0,0 +1,8 @@
1 +function Component() {
2 + const foo = () => {
3 + // Cannot assign to globals
4 + someUnknownGlobal = true;
5 + moduleLocal = true;
6 + };
7 + foo();
8 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global.expect.md new
+26
@@ -0,0 +1,26 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component() {
6 + // Cannot assign to globals
7 + someUnknownGlobal = true;
8 + moduleLocal = true;
9 +}
10 +
11 +```
12 +
13 +
14 +## Error
15 +
16 +```
17 + 1 | function Component() {
18 + 2 | // Cannot assign to globals
19 +> 3 | someUnknownGlobal = true;
20 + | ^^^^^^^^^^^^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the component. Components and hooks should be pure and side-effect free, but variable reassignment is a form of side-effect. If this variable is used in rendering, use useState instead. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#side-effects-must-run-outside-of-render) (3:3)
21 + 4 | moduleLocal = true;
22 + 5 | }
23 + 6 |
24 +```
25 +
26 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.reassignment-to-global.js new
+5
@@ -0,0 +1,5 @@
1 +function Component() {
2 + // Cannot assign to globals
3 + someUnknownGlobal = true;
4 + moduleLocal = true;
5 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.todo-repro-named-function-with-shadowed-local-same-name.expect.md new
+30
@@ -0,0 +1,30 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component(props) {
6 + function hasErrors() {
7 + let hasErrors = false;
8 + if (props.items == null) {
9 + hasErrors = true;
10 + }
11 + return hasErrors;
12 + }
13 + return hasErrors();
14 +}
15 +
16 +```
17 +
18 +
19 +## Error
20 +
21 +```
22 + 7 | return hasErrors;
23 + 8 | }
24 +> 9 | return hasErrors();
25 + | ^^^^^^^^^ Invariant: [hoisting] Expected value for identifier to be initialized. hasErrors_0$15 (9:9)
26 + 10 | }
27 + 11 |
28 +```
29 +
30 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/error.todo-repro-named-function-with-shadowed-local-same-name.js new
+10
@@ -0,0 +1,10 @@
1 +function Component(props) {
2 + function hasErrors() {
3 + let hasErrors = false;
4 + if (props.items == null) {
5 + hasErrors = true;
6 + }
7 + return hasErrors;
8 + }
9 + return hasErrors();
10 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-optional-chain.expect.md new
+58
@@ -0,0 +1,58 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
6 +import {useEffect} from 'react';
7 +import {print} from 'shared-runtime';
8 +
9 +function Component({foo}) {
10 + const arr = [];
11 + // Taking either arr[0].value or arr as a dependency is reasonable
12 + // as long as developers know what to expect.
13 + useEffect(() => print(arr[0]?.value));
14 + arr.push({value: foo});
15 + return arr;
16 +}
17 +
18 +export const FIXTURE_ENTRYPOINT = {
19 + fn: Component,
20 + params: [{foo: 1}],
21 +};
22 +
23 +```
24 +
25 +## Code
26 +
27 +```javascript
28 +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
29 +import { useEffect } from "react";
30 +import { print } from "shared-runtime";
31 +
32 +function Component(t0) {
33 + const { foo } = t0;
34 + const arr = [];
35 +
36 + useEffect(() => print(arr[0]?.value), [arr[0]?.value]);
37 + arr.push({ value: foo });
38 + return arr;
39 +}
40 +
41 +export const FIXTURE_ENTRYPOINT = {
42 + fn: Component,
43 + params: [{ foo: 1 }],
44 +};
45 +
46 +```
47 +
48 +## Logs
49 +
50 +```
51 +{"kind":"CompileError","fnLoc":{"start":{"line":5,"column":0,"index":139},"end":{"line":12,"column":1,"index":384},"filename":"mutate-after-useeffect-optional-chain.ts"},"detail":{"reason":"This mutates a variable that React considers immutable","description":null,"loc":{"start":{"line":10,"column":2,"index":345},"end":{"line":10,"column":5,"index":348},"filename":"mutate-after-useeffect-optional-chain.ts","identifierName":"arr"},"suggestions":null,"severity":"InvalidReact"}}
52 +{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":9,"column":2,"index":304},"end":{"line":9,"column":39,"index":341},"filename":"mutate-after-useeffect-optional-chain.ts"},"decorations":[{"start":{"line":9,"column":24,"index":326},"end":{"line":9,"column":27,"index":329},"filename":"mutate-after-useeffect-optional-chain.ts","identifierName":"arr"}]}
53 +{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":139},"end":{"line":12,"column":1,"index":384},"filename":"mutate-after-useeffect-optional-chain.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
54 +```
55 +
56 +### Eval output
57 +(kind: ok) [{"value":1}]
58 +logs: [1]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-optional-chain.js new
+17
@@ -0,0 +1,17 @@
1 +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
2 +import {useEffect} from 'react';
3 +import {print} from 'shared-runtime';
4 +
5 +function Component({foo}) {
6 + const arr = [];
7 + // Taking either arr[0].value or arr as a dependency is reasonable
8 + // as long as developers know what to expect.
9 + useEffect(() => print(arr[0]?.value));
10 + arr.push({value: foo});
11 + return arr;
12 +}
13 +
14 +export const FIXTURE_ENTRYPOINT = {
15 + fn: Component,
16 + params: [{foo: 1}],
17 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-ref-access.expect.md new
+57
@@ -0,0 +1,57 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
6 +
7 +import {useEffect, useRef} from 'react';
8 +import {print} from 'shared-runtime';
9 +
10 +function Component({arrRef}) {
11 + // Avoid taking arr.current as a dependency
12 + useEffect(() => print(arrRef.current));
13 + arrRef.current.val = 2;
14 + return arrRef;
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Component,
19 + params: [{arrRef: {current: {val: 'initial ref value'}}}],
20 +};
21 +
22 +```
23 +
24 +## Code
25 +
26 +```javascript
27 +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
28 +
29 +import { useEffect, useRef } from "react";
30 +import { print } from "shared-runtime";
31 +
32 +function Component(t0) {
33 + const { arrRef } = t0;
34 +
35 + useEffect(() => print(arrRef.current), [arrRef]);
36 + arrRef.current.val = 2;
37 + return arrRef;
38 +}
39 +
40 +export const FIXTURE_ENTRYPOINT = {
41 + fn: Component,
42 + params: [{ arrRef: { current: { val: "initial ref value" } } }],
43 +};
44 +
45 +```
46 +
47 +## Logs
48 +
49 +```
50 +{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":148},"end":{"line":11,"column":1,"index":311},"filename":"mutate-after-useeffect-ref-access.ts"},"detail":{"reason":"Mutating component props or hook arguments is not allowed. Consider using a local variable instead","description":null,"loc":{"start":{"line":9,"column":2,"index":269},"end":{"line":9,"column":16,"index":283},"filename":"mutate-after-useeffect-ref-access.ts"},"suggestions":null,"severity":"InvalidReact"}}
51 +{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":8,"column":2,"index":227},"end":{"line":8,"column":40,"index":265},"filename":"mutate-after-useeffect-ref-access.ts"},"decorations":[{"start":{"line":8,"column":24,"index":249},"end":{"line":8,"column":30,"index":255},"filename":"mutate-after-useeffect-ref-access.ts","identifierName":"arrRef"}]}
52 +{"kind":"CompileSuccess","fnLoc":{"start":{"line":6,"column":0,"index":148},"end":{"line":11,"column":1,"index":311},"filename":"mutate-after-useeffect-ref-access.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
53 +```
54 +
55 +### Eval output
56 +(kind: ok) {"current":{"val":2}}
57 +logs: [{ val: 2 }]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-ref-access.js new
+16
@@ -0,0 +1,16 @@
1 +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
2 +
3 +import {useEffect, useRef} from 'react';
4 +import {print} from 'shared-runtime';
5 +
6 +function Component({arrRef}) {
7 + // Avoid taking arr.current as a dependency
8 + useEffect(() => print(arrRef.current));
9 + arrRef.current.val = 2;
10 + return arrRef;
11 +}
12 +
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: Component,
15 + params: [{arrRef: {current: {val: 'initial ref value'}}}],
16 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect.expect.md new
+56
@@ -0,0 +1,56 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
6 +import {useEffect} from 'react';
7 +
8 +function Component({foo}) {
9 + const arr = [];
10 + useEffect(() => {
11 + arr.push(foo);
12 + });
13 + arr.push(2);
14 + return arr;
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Component,
19 + params: [{foo: 1}],
20 +};
21 +
22 +```
23 +
24 +## Code
25 +
26 +```javascript
27 +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
28 +import { useEffect } from "react";
29 +
30 +function Component(t0) {
31 + const { foo } = t0;
32 + const arr = [];
33 + useEffect(() => {
34 + arr.push(foo);
35 + }, [arr, foo]);
36 + arr.push(2);
37 + return arr;
38 +}
39 +
40 +export const FIXTURE_ENTRYPOINT = {
41 + fn: Component,
42 + params: [{ foo: 1 }],
43 +};
44 +
45 +```
46 +
47 +## Logs
48 +
49 +```
50 +{"kind":"CompileError","fnLoc":{"start":{"line":4,"column":0,"index":101},"end":{"line":11,"column":1,"index":222},"filename":"mutate-after-useeffect.ts"},"detail":{"reason":"This mutates a variable that React considers immutable","description":null,"loc":{"start":{"line":9,"column":2,"index":194},"end":{"line":9,"column":5,"index":197},"filename":"mutate-after-useeffect.ts","identifierName":"arr"},"suggestions":null,"severity":"InvalidReact"}}
51 +{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":6,"column":2,"index":149},"end":{"line":8,"column":4,"index":190},"filename":"mutate-after-useeffect.ts"},"decorations":[{"start":{"line":7,"column":4,"index":171},"end":{"line":7,"column":7,"index":174},"filename":"mutate-after-useeffect.ts","identifierName":"arr"},{"start":{"line":7,"column":4,"index":171},"end":{"line":7,"column":7,"index":174},"filename":"mutate-after-useeffect.ts","identifierName":"arr"},{"start":{"line":7,"column":13,"index":180},"end":{"line":7,"column":16,"index":183},"filename":"mutate-after-useeffect.ts","identifierName":"foo"}]}
52 +{"kind":"CompileSuccess","fnLoc":{"start":{"line":4,"column":0,"index":101},"end":{"line":11,"column":1,"index":222},"filename":"mutate-after-useeffect.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
53 +```
54 +
55 +### Eval output
56 +(kind: ok) [2]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect.js new
+16
@@ -0,0 +1,16 @@
1 +// @inferEffectDependencies @panicThreshold:"none" @loggerTestOnly
2 +import {useEffect} from 'react';
3 +
4 +function Component({foo}) {
5 + const arr = [];
6 + useEffect(() => {
7 + arr.push(foo);
8 + });
9 + arr.push(2);
10 + return arr;
11 +}
12 +
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: Component,
15 + params: [{foo: 1}],
16 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-key-object-mutated-later.expect.md new
+69
@@ -0,0 +1,69 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {identity, mutate} from 'shared-runtime';
6 +
7 +function Component(props) {
8 + const key = {};
9 + const context = {
10 + [key]: identity([props.value]),
11 + };
12 + mutate(key);
13 + return context;
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: Component,
18 + params: [{value: 42}],
19 +};
20 +
21 +```
22 +
23 +## Code
24 +
25 +```javascript
26 +import { c as _c } from "react/compiler-runtime";
27 +import { identity, mutate } from "shared-runtime";
28 +
29 +function Component(props) {
30 + const $ = _c(5);
31 + let t0;
32 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
33 + t0 = {};
34 + $[0] = t0;
35 + } else {
36 + t0 = $[0];
37 + }
38 + const key = t0;
39 + let t1;
40 + if ($[1] !== props.value) {
41 + t1 = identity([props.value]);
42 + $[1] = props.value;
43 + $[2] = t1;
44 + } else {
45 + t1 = $[2];
46 + }
47 + let t2;
48 + if ($[3] !== t1) {
49 + t2 = { [key]: t1 };
50 + $[3] = t1;
51 + $[4] = t2;
52 + } else {
53 + t2 = $[4];
54 + }
55 + const context = t2;
56 +
57 + mutate(key);
58 + return context;
59 +}
60 +
61 +export const FIXTURE_ENTRYPOINT = {
62 + fn: Component,
63 + params: [{ value: 42 }],
64 +};
65 +
66 +```
67 +
68 +### Eval output
69 +(kind: ok) {"[object Object]":[42]}
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-key-object-mutated-later.js new
+15
@@ -0,0 +1,15 @@
1 +import {identity, mutate} from 'shared-runtime';
2 +
3 +function Component(props) {
4 + const key = {};
5 + const context = {
6 + [key]: identity([props.value]),
7 + };
8 + mutate(key);
9 + return context;
10 +}
11 +
12 +export const FIXTURE_ENTRYPOINT = {
13 + fn: Component,
14 + params: [{value: 42}],
15 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-member.expect.md new
+53
@@ -0,0 +1,53 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {identity, mutate, mutateAndReturn} from 'shared-runtime';
6 +
7 +function Component(props) {
8 + const key = {a: 'key'};
9 + const context = {
10 + [key.a]: identity([props.value]),
11 + };
12 + mutate(key);
13 + return context;
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: Component,
18 + params: [{value: 42}],
19 +};
20 +
21 +```
22 +
23 +## Code
24 +
25 +```javascript
26 +import { c as _c } from "react/compiler-runtime";
27 +import { identity, mutate, mutateAndReturn } from "shared-runtime";
28 +
29 +function Component(props) {
30 + const $ = _c(2);
31 + let context;
32 + if ($[0] !== props.value) {
33 + const key = { a: "key" };
34 + context = { [key.a]: identity([props.value]) };
35 +
36 + mutate(key);
37 + $[0] = props.value;
38 + $[1] = context;
39 + } else {
40 + context = $[1];
41 + }
42 + return context;
43 +}
44 +
45 +export const FIXTURE_ENTRYPOINT = {
46 + fn: Component,
47 + params: [{ value: 42 }],
48 +};
49 +
50 +```
51 +
52 +### Eval output
53 +(kind: ok) {"key":[42]}
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/object-expression-computed-member.js new
+15
@@ -0,0 +1,15 @@
1 +import {identity, mutate, mutateAndReturn} from 'shared-runtime';
2 +
3 +function Component(props) {
4 + const key = {a: 'key'};
5 + const context = {
6 + [key.a]: identity([props.value]),
7 + };
8 + mutate(key);
9 + return context;
10 +}
11 +
12 +export const FIXTURE_ENTRYPOINT = {
13 + fn: Component,
14 + params: [{value: 42}],
15 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-setState.expect.md new
+60
@@ -0,0 +1,60 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @inferEffectDependencies
6 +import {useEffect, useState} from 'react';
7 +import {print} from 'shared-runtime';
8 +
9 +/*
10 + * setState types are not enough to determine to omit from deps. Must also take reactivity into account.
11 + */
12 +function ReactiveRefInEffect(props) {
13 + const [_state1, setState1] = useRef('initial value');
14 + const [_state2, setState2] = useRef('initial value');
15 + let setState;
16 + if (props.foo) {
17 + setState = setState1;
18 + } else {
19 + setState = setState2;
20 + }
21 + useEffect(() => print(setState));
22 +}
23 +
24 +```
25 +
26 +## Code
27 +
28 +```javascript
29 +import { c as _c } from "react/compiler-runtime"; // @inferEffectDependencies
30 +import { useEffect, useState } from "react";
31 +import { print } from "shared-runtime";
32 +
33 +/*
34 + * setState types are not enough to determine to omit from deps. Must also take reactivity into account.
35 + */
36 +function ReactiveRefInEffect(props) {
37 + const $ = _c(2);
38 + const [, setState1] = useRef("initial value");
39 + const [, setState2] = useRef("initial value");
40 + let setState;
41 + if (props.foo) {
42 + setState = setState1;
43 + } else {
44 + setState = setState2;
45 + }
46 + let t0;
47 + if ($[0] !== setState) {
48 + t0 = () => print(setState);
49 + $[0] = setState;
50 + $[1] = t0;
51 + } else {
52 + t0 = $[1];
53 + }
54 + useEffect(t0, [setState]);
55 +}
56 +
57 +```
58 +
59 +### Eval output
60 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/reactive-setState.js new
+18
@@ -0,0 +1,18 @@
1 +// @inferEffectDependencies
2 +import {useEffect, useState} from 'react';
3 +import {print} from 'shared-runtime';
4 +
5 +/*
6 + * setState types are not enough to determine to omit from deps. Must also take reactivity into account.
7 + */
8 +function ReactiveRefInEffect(props) {
9 + const [_state1, setState1] = useRef('initial value');
10 + const [_state2, setState2] = useRef('initial value');
11 + let setState;
12 + if (props.foo) {
13 + setState = setState1;
14 + } else {
15 + setState = setState2;
16 + }
17 + useEffect(() => print(setState));
18 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/retry-no-emit.expect.md new
+64
@@ -0,0 +1,64 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly
6 +import {print} from 'shared-runtime';
7 +import useEffectWrapper from 'useEffectWrapper';
8 +
9 +function Foo({propVal}) {
10 + const arr = [propVal];
11 + useEffectWrapper(() => print(arr));
12 +
13 + const arr2 = [];
14 + useEffectWrapper(() => arr2.push(propVal));
15 + arr2.push(2);
16 + return {arr, arr2};
17 +}
18 +
19 +export const FIXTURE_ENTRYPOINT = {
20 + fn: Foo,
21 + params: [{propVal: 1}],
22 + sequentialRenders: [{propVal: 1}, {propVal: 2}],
23 +};
24 +
25 +```
26 +
27 +## Code
28 +
29 +```javascript
30 +// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly
31 +import { print } from "shared-runtime";
32 +import useEffectWrapper from "useEffectWrapper";
33 +
34 +function Foo({ propVal }) {
35 + const arr = [propVal];
36 + useEffectWrapper(() => print(arr));
37 +
38 + const arr2 = [];
39 + useEffectWrapper(() => arr2.push(propVal));
40 + arr2.push(2);
41 + return { arr, arr2 };
42 +}
43 +
44 +export const FIXTURE_ENTRYPOINT = {
45 + fn: Foo,
46 + params: [{ propVal: 1 }],
47 + sequentialRenders: [{ propVal: 1 }, { propVal: 2 }],
48 +};
49 +
50 +```
51 +
52 +## Logs
53 +
54 +```
55 +{"kind":"CompileError","fnLoc":{"start":{"line":5,"column":0,"index":163},"end":{"line":13,"column":1,"index":357},"filename":"retry-no-emit.ts"},"detail":{"reason":"This mutates a variable that React considers immutable","description":null,"loc":{"start":{"line":11,"column":2,"index":320},"end":{"line":11,"column":6,"index":324},"filename":"retry-no-emit.ts","identifierName":"arr2"},"suggestions":null,"severity":"InvalidReact"}}
56 +{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":7,"column":2,"index":216},"end":{"line":7,"column":36,"index":250},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":7,"column":31,"index":245},"end":{"line":7,"column":34,"index":248},"filename":"retry-no-emit.ts","identifierName":"arr"}]}
57 +{"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":10,"column":2,"index":274},"end":{"line":10,"column":44,"index":316},"filename":"retry-no-emit.ts"},"decorations":[{"start":{"line":10,"column":25,"index":297},"end":{"line":10,"column":29,"index":301},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":10,"column":25,"index":297},"end":{"line":10,"column":29,"index":301},"filename":"retry-no-emit.ts","identifierName":"arr2"},{"start":{"line":10,"column":35,"index":307},"end":{"line":10,"column":42,"index":314},"filename":"retry-no-emit.ts","identifierName":"propVal"}]}
58 +{"kind":"CompileSuccess","fnLoc":{"start":{"line":5,"column":0,"index":163},"end":{"line":13,"column":1,"index":357},"filename":"retry-no-emit.ts"},"fnName":"Foo","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
59 +```
60 +
61 +### Eval output
62 +(kind: ok) {"arr":[1],"arr2":[2]}
63 +{"arr":[2],"arr2":[2]}
64 +logs: [[ 1 ],[ 2 ]]
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/retry-no-emit.js new
+19
@@ -0,0 +1,19 @@
1 +// @inferEffectDependencies @noEmit @panicThreshold:"none" @loggerTestOnly
2 +import {print} from 'shared-runtime';
3 +import useEffectWrapper from 'useEffectWrapper';
4 +
5 +function Foo({propVal}) {
6 + const arr = [propVal];
7 + useEffectWrapper(() => print(arr));
8 +
9 + const arr2 = [];
10 + useEffectWrapper(() => arr2.push(propVal));
11 + arr2.push(2);
12 + return {arr, arr2};
13 +}
14 +
15 +export const FIXTURE_ENTRYPOINT = {
16 + fn: Foo,
17 + params: [{propVal: 1}],
18 + sequentialRenders: [{propVal: 1}, {propVal: 2}],
19 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/shared-hook-calls.expect.md new
+80
@@ -0,0 +1,80 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableFire
6 +import {fire} from 'react';
7 +
8 +function Component({bar, baz}) {
9 + const foo = () => {
10 + console.log(bar);
11 + };
12 + useEffect(() => {
13 + fire(foo(bar));
14 + fire(baz(bar));
15 + });
16 +
17 + useEffect(() => {
18 + fire(foo(bar));
19 + });
20 +
21 + return null;
22 +}
23 +
24 +```
25 +
26 +## Code
27 +
28 +```javascript
29 +import { c as _c, useFire } from "react/compiler-runtime"; // @enableFire
30 +import { fire } from "react";
31 +
32 +function Component(t0) {
33 + const $ = _c(9);
34 + const { bar, baz } = t0;
35 + let t1;
36 + if ($[0] !== bar) {
37 + t1 = () => {
38 + console.log(bar);
39 + };
40 + $[0] = bar;
41 + $[1] = t1;
42 + } else {
43 + t1 = $[1];
44 + }
45 + const foo = t1;
46 + const t2 = useFire(foo);
47 + const t3 = useFire(baz);
48 + let t4;
49 + if ($[2] !== bar || $[3] !== t2 || $[4] !== t3) {
50 + t4 = () => {
51 + t2(bar);
52 + t3(bar);
53 + };
54 + $[2] = bar;
55 + $[3] = t2;
56 + $[4] = t3;
57 + $[5] = t4;
58 + } else {
59 + t4 = $[5];
60 + }
61 + useEffect(t4);
62 + let t5;
63 + if ($[6] !== bar || $[7] !== t2) {
64 + t5 = () => {
65 + t2(bar);
66 + };
67 + $[6] = bar;
68 + $[7] = t2;
69 + $[8] = t5;
70 + } else {
71 + t5 = $[8];
72 + }
73 + useEffect(t5);
74 + return null;
75 +}
76 +
77 +```
78 +
79 +### Eval output
80 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/shared-hook-calls.js new
+18
@@ -0,0 +1,18 @@
1 +// @enableFire
2 +import {fire} from 'react';
3 +
4 +function Component({bar, baz}) {
5 + const foo = () => {
6 + console.log(bar);
7 + };
8 + useEffect(() => {
9 + fire(foo(bar));
10 + fire(baz(bar));
11 + });
12 +
13 + useEffect(() => {
14 + fire(foo(bar));
15 + });
16 +
17 + return null;
18 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-deplist-controlflow.expect.md new
+94
@@ -0,0 +1,94 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {useCallback} from 'react';
6 +import {Stringify} from 'shared-runtime';
7 +
8 +function Foo({arr1, arr2, foo}) {
9 + const x = [arr1];
10 +
11 + let y = [];
12 +
13 + const getVal1 = useCallback(() => {
14 + return {x: 2};
15 + }, []);
16 +
17 + const getVal2 = useCallback(() => {
18 + return [y];
19 + }, [foo ? (y = x.concat(arr2)) : y]);
20 +
21 + return <Stringify val1={getVal1} val2={getVal2} shouldInvokeFns={true} />;
22 +}
23 +
24 +export const FIXTURE_ENTRYPOINT = {
25 + fn: Foo,
26 + params: [{arr1: [1, 2], arr2: [3, 4], foo: true}],
27 + sequentialRenders: [
28 + {arr1: [1, 2], arr2: [3, 4], foo: true},
29 + {arr1: [1, 2], arr2: [3, 4], foo: false},
30 + ],
31 +};
32 +
33 +```
34 +
35 +## Code
36 +
37 +```javascript
38 +import { c as _c } from "react/compiler-runtime";
39 +import { useCallback } from "react";
40 +import { Stringify } from "shared-runtime";
41 +
42 +function Foo(t0) {
43 + const $ = _c(8);
44 + const { arr1, arr2, foo } = t0;
45 + let getVal1;
46 + let t1;
47 + if ($[0] !== arr1 || $[1] !== arr2 || $[2] !== foo) {
48 + const x = [arr1];
49 +
50 + let y = [];
51 +
52 + getVal1 = _temp;
53 +
54 + t1 = () => [y];
55 + foo ? (y = x.concat(arr2)) : y;
56 + $[0] = arr1;
57 + $[1] = arr2;
58 + $[2] = foo;
59 + $[3] = getVal1;
60 + $[4] = t1;
61 + } else {
62 + getVal1 = $[3];
63 + t1 = $[4];
64 + }
65 + const getVal2 = t1;
66 + let t2;
67 + if ($[5] !== getVal1 || $[6] !== getVal2) {
68 + t2 = <Stringify val1={getVal1} val2={getVal2} shouldInvokeFns={true} />;
69 + $[5] = getVal1;
70 + $[6] = getVal2;
71 + $[7] = t2;
72 + } else {
73 + t2 = $[7];
74 + }
75 + return t2;
76 +}
77 +function _temp() {
78 + return { x: 2 };
79 +}
80 +
81 +export const FIXTURE_ENTRYPOINT = {
82 + fn: Foo,
83 + params: [{ arr1: [1, 2], arr2: [3, 4], foo: true }],
84 + sequentialRenders: [
85 + { arr1: [1, 2], arr2: [3, 4], foo: true },
86 + { arr1: [1, 2], arr2: [3, 4], foo: false },
87 + ],
88 +};
89 +
90 +```
91 +
92 +### Eval output
93 +(kind: ok) <div>{"val1":{"kind":"Function","result":{"x":2}},"val2":{"kind":"Function","result":[[[1,2],3,4]]},"shouldInvokeFns":true}</div>
94 +<div>{"val1":{"kind":"Function","result":{"x":2}},"val2":{"kind":"Function","result":[[]]},"shouldInvokeFns":true}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-deplist-controlflow.tsx new
+27
@@ -0,0 +1,27 @@
1 +import {useCallback} from 'react';
2 +import {Stringify} from 'shared-runtime';
3 +
4 +function Foo({arr1, arr2, foo}) {
5 + const x = [arr1];
6 +
7 + let y = [];
8 +
9 + const getVal1 = useCallback(() => {
10 + return {x: 2};
11 + }, []);
12 +
13 + const getVal2 = useCallback(() => {
14 + return [y];
15 + }, [foo ? (y = x.concat(arr2)) : y]);
16 +
17 + return <Stringify val1={getVal1} val2={getVal2} shouldInvokeFns={true} />;
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: Foo,
22 + params: [{arr1: [1, 2], arr2: [3, 4], foo: true}],
23 + sequentialRenders: [
24 + {arr1: [1, 2], arr2: [3, 4], foo: true},
25 + {arr1: [1, 2], arr2: [3, 4], foo: false},
26 + ],
27 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-depslist-assignment.expect.md new
+77
@@ -0,0 +1,77 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {useCallback} from 'react';
6 +import {Stringify} from 'shared-runtime';
7 +
8 +// We currently produce invalid output (incorrect scoping for `y` declaration)
9 +function useFoo(arr1, arr2) {
10 + const x = [arr1];
11 +
12 + let y;
13 + const getVal = useCallback(() => {
14 + return {y};
15 + }, [((y = x.concat(arr2)), y)]);
16 +
17 + return <Stringify getVal={getVal} shouldInvokeFns={true} />;
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: useFoo,
22 + params: [
23 + [1, 2],
24 + [3, 4],
25 + ],
26 +};
27 +
28 +```
29 +
30 +## Code
31 +
32 +```javascript
33 +import { c as _c } from "react/compiler-runtime";
34 +import { useCallback } from "react";
35 +import { Stringify } from "shared-runtime";
36 +
37 +// We currently produce invalid output (incorrect scoping for `y` declaration)
38 +function useFoo(arr1, arr2) {
39 + const $ = _c(5);
40 + let t0;
41 + if ($[0] !== arr1 || $[1] !== arr2) {
42 + const x = [arr1];
43 +
44 + let y;
45 + t0 = () => ({ y });
46 +
47 + (y = x.concat(arr2)), y;
48 + $[0] = arr1;
49 + $[1] = arr2;
50 + $[2] = t0;
51 + } else {
52 + t0 = $[2];
53 + }
54 + const getVal = t0;
55 + let t1;
56 + if ($[3] !== getVal) {
57 + t1 = <Stringify getVal={getVal} shouldInvokeFns={true} />;
58 + $[3] = getVal;
59 + $[4] = t1;
60 + } else {
61 + t1 = $[4];
62 + }
63 + return t1;
64 +}
65 +
66 +export const FIXTURE_ENTRYPOINT = {
67 + fn: useFoo,
68 + params: [
69 + [1, 2],
70 + [3, 4],
71 + ],
72 +};
73 +
74 +```
75 +
76 +### Eval output
77 +(kind: ok) <div>{"getVal":{"kind":"Function","result":{"y":[[1,2],3,4]}},"shouldInvokeFns":true}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useCallback-reordering-depslist-assignment.tsx new
+22
@@ -0,0 +1,22 @@
1 +import {useCallback} from 'react';
2 +import {Stringify} from 'shared-runtime';
3 +
4 +// We currently produce invalid output (incorrect scoping for `y` declaration)
5 +function useFoo(arr1, arr2) {
6 + const x = [arr1];
7 +
8 + let y;
9 + const getVal = useCallback(() => {
10 + return {y};
11 + }, [((y = x.concat(arr2)), y)]);
12 +
13 + return <Stringify getVal={getVal} shouldInvokeFns={true} />;
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: useFoo,
18 + params: [
19 + [1, 2],
20 + [3, 4],
21 + ],
22 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useMemo-reordering-depslist-assignment.expect.md new
+69
@@ -0,0 +1,69 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {useMemo} from 'react';
6 +
7 +function useFoo(arr1, arr2) {
8 + const x = [arr1];
9 +
10 + let y;
11 + return useMemo(() => {
12 + return {y};
13 + }, [((y = x.concat(arr2)), y)]);
14 +}
15 +
16 +export const FIXTURE_ENTRYPOINT = {
17 + fn: useFoo,
18 + params: [
19 + [1, 2],
20 + [3, 4],
21 + ],
22 +};
23 +
24 +```
25 +
26 +## Code
27 +
28 +```javascript
29 +import { c as _c } from "react/compiler-runtime";
30 +import { useMemo } from "react";
31 +
32 +function useFoo(arr1, arr2) {
33 + const $ = _c(5);
34 + let y;
35 + if ($[0] !== arr1 || $[1] !== arr2) {
36 + const x = [arr1];
37 +
38 + (y = x.concat(arr2)), y;
39 + $[0] = arr1;
40 + $[1] = arr2;
41 + $[2] = y;
42 + } else {
43 + y = $[2];
44 + }
45 + let t0;
46 + let t1;
47 + if ($[3] !== y) {
48 + t1 = { y };
49 + $[3] = y;
50 + $[4] = t1;
51 + } else {
52 + t1 = $[4];
53 + }
54 + t0 = t1;
55 + return t0;
56 +}
57 +
58 +export const FIXTURE_ENTRYPOINT = {
59 + fn: useFoo,
60 + params: [
61 + [1, 2],
62 + [3, 4],
63 + ],
64 +};
65 +
66 +```
67 +
68 +### Eval output
69 +(kind: ok) {"y":[[1,2],3,4]}
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/useMemo-reordering-depslist-assignment.ts new
+18
@@ -0,0 +1,18 @@
1 +import {useMemo} from 'react';
2 +
3 +function useFoo(arr1, arr2) {
4 + const x = [arr1];
5 +
6 + let y;
7 + return useMemo(() => {
8 + return {y};
9 + }, [((y = x.concat(arr2)), y)]);
10 +}
11 +
12 +export const FIXTURE_ENTRYPOINT = {
13 + fn: useFoo,
14 + params: [
15 + [1, 2],
16 + [3, 4],
17 + ],
18 +};