Allow ref argument to be mutated
Previously, Forget would throw if _any_ of the arguments to a component are modified. This isn't quite right as a ref argument can be modified. This PR assumes the second argument of a component to be a ref and allows it to be mutable. A future PR will add types to this argument so the validateRefAccessDuringRender can catch if ref is mutated in render. This PR contains a todo test for this.
Sathya Gunasekaran committed
Feb 29, 2024 at 14:47 UTC
19e44a4a69c87ab0fef540f06fa5e65ca0380055
13 files changed
+400
-28
compiler/packages/babel-plugin-react-forget/src/Inference/InferReferenceEffects.ts
+70
-17
@@ -21,6 +21,7 @@ import {
21
MethodCall,
22
Phi,
23
Place,
24
+ SpreadPattern,
25
Type,
26
ValueKind,
27
ValueReason,
@@ -133,26 +134,48 @@ export default function inferReferenceEffects(
134
kind: ValueKind.Frozen,
135
reason: new Set([ValueReason.ReactiveFunctionArgument]),
136
};
136
- for (const param of fn.params) {
137
+
138
+ const isComponent = isComponentName(fn.id);
139
+ if (isComponent) {
140
+ CompilerError.invariant(fn.params.length <= 2, {
141
+ reason:
142
+ "Expected React component to have not more than two parameters: one for props and for ref",
143
+ description: null,
144
+ loc: fn.loc,
145
+ suggestions: null,
146
+ });
147
+ const [props, ref] = fn.params;
148
let value: InstructionValue;
149
let place: Place;
139
- if (param.kind === "Identifier") {
140
- place = param;
141
- value = {
142
- kind: "Primitive",
143
- loc: param.loc,
144
- value: undefined,
145
- };
146
- } else {
147
- place = param.place;
148
- value = {
149
- kind: "Primitive",
150
- loc: param.place.loc,
151
- value: undefined,
152
- };
150
+ if (props) {
151
+ inferParam(props, initialState, paramKind);
152
+ }
153
+ if (ref) {
154
+ if (ref.kind === "Identifier") {
155
+ place = ref;
156
+ value = {
157
+ kind: "ObjectExpression",
158
+ properties: [],
159
+ loc: ref.loc,
160
+ };
161
+ } else {
162
+ place = ref.place;
163
+ value = {
164
+ kind: "ObjectExpression",
165
+ properties: [],
166
+ loc: ref.place.loc,
167
+ };
168
+ }
169
+ initialState.initialize(value, {
170
+ kind: ValueKind.Mutable,
171
+ reason: new Set([ValueReason.Other]),
172
+ });
173
+ initialState.define(place, value);
174
+ }
175
+ } else {
176
+ for (const param of fn.params) {
177
+ inferParam(param, initialState, paramKind);
178
}
154
- initialState.initialize(value, paramKind);
155
- initialState.define(place, value);
179
}
180
181
// Map of blocks to the last (merged) incoming state that was processed
@@ -596,6 +619,36 @@ class InferenceState {
619
}
620
}
621
622
+function isComponentName(name: string | null): boolean {
623
+ return name !== null && /^[A-Z]/.test(name);
624
+}
625
+
626
+function inferParam(
627
+ param: Place | SpreadPattern,
628
+ initialState: InferenceState,
629
+ paramKind: AbstractValue
630
+): void {
631
+ let value: InstructionValue;
632
+ let place: Place;
633
+ if (param.kind === "Identifier") {
634
+ place = param;
635
+ value = {
636
+ kind: "Primitive",
637
+ loc: param.loc,
638
+ value: undefined,
639
+ };
640
+ } else {
641
+ place = param.place;
642
+ value = {
643
+ kind: "Primitive",
644
+ loc: param.place.loc,
645
+ value: undefined,
646
+ };
647
+ }
648
+ initialState.initialize(value, paramKind);
649
+ initialState.define(place, value);
650
+}
651
+
652
/*
653
* Joins two values using the following rules:
654
* == Effect Transitions ==
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/ref-parameter-mutate-in-effect.expect.md
new
+67
@@ -0,0 +1,67 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+import { useEffect } from "react";
6
+
7
+function Foo(props, ref) {
8
+ useEffect(() => {
9
+ ref.current = 2;
10
+ }, []);
11
+ return <div>{props.bar}</div>;
12
+}
13
+
14
+export const FIXTURE_ENTRYPOINT = {
15
+ fn: Foo,
16
+ params: [{ bar: "foo" }, { ref: { cuurrent: 1 } }],
17
+ isComponent: true,
18
+};
19
+
20
+```
21
+
22
+## Code
23
+
24
+```javascript
25
+import { useEffect, unstable_useMemoCache as useMemoCache } from "react";
26
+
27
+function Foo(props, ref) {
28
+ const $ = useMemoCache(5);
29
+ let t0;
30
+ if ($[0] !== ref.current) {
31
+ t0 = () => {
32
+ ref.current = 2;
33
+ };
34
+ $[0] = ref.current;
35
+ $[1] = t0;
36
+ } else {
37
+ t0 = $[1];
38
+ }
39
+ let t1;
40
+ if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
41
+ t1 = [];
42
+ $[2] = t1;
43
+ } else {
44
+ t1 = $[2];
45
+ }
46
+ useEffect(t0, t1);
47
+ let t2;
48
+ if ($[3] !== props.bar) {
49
+ t2 = <div>{props.bar}</div>;
50
+ $[3] = props.bar;
51
+ $[4] = t2;
52
+ } else {
53
+ t2 = $[4];
54
+ }
55
+ return t2;
56
+}
57
+
58
+export const FIXTURE_ENTRYPOINT = {
59
+ fn: Foo,
60
+ params: [{ bar: "foo" }, { ref: { cuurrent: 1 } }],
61
+ isComponent: true,
62
+};
63
+
64
+```
65
+
66
+### Eval output
67
+(kind: ok) <div>foo</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/ref-parameter-mutate-in-effect.js
new
+14
@@ -0,0 +1,14 @@
1
+import { useEffect } from "react";
2
+
3
+function Foo(props, ref) {
4
+ useEffect(() => {
5
+ ref.current = 2;
6
+ }, []);
7
+ return <div>{props.bar}</div>;
8
+}
9
+
10
+export const FIXTURE_ENTRYPOINT = {
11
+ fn: Foo,
12
+ params: [{ bar: "foo" }, { ref: { cuurrent: 1 } }],
13
+ isComponent: true,
14
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/todo.validate-mutate-ref-arg-in-render.expect.md
new
+46
@@ -0,0 +1,46 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @validateRefAccessDuringRender: true
6
+function Foo(props, ref) {
7
+ ref.current = 2;
8
+ return <div>{props.bar}</div>;
9
+}
10
+
11
+export const FIXTURE_ENTRYPOINT = {
12
+ fn: Foo,
13
+ params: [{ bar: "foo" }, { ref: { cuurrent: 1 } }],
14
+ isComponent: true,
15
+};
16
+
17
+```
18
+
19
+## Code
20
+
21
+```javascript
22
+import { unstable_useMemoCache as useMemoCache } from "react"; // @validateRefAccessDuringRender: true
23
+function Foo(props, ref) {
24
+ const $ = useMemoCache(2);
25
+ ref.current = 2;
26
+ let t0;
27
+ if ($[0] !== props.bar) {
28
+ t0 = <div>{props.bar}</div>;
29
+ $[0] = props.bar;
30
+ $[1] = t0;
31
+ } else {
32
+ t0 = $[1];
33
+ }
34
+ return t0;
35
+}
36
+
37
+export const FIXTURE_ENTRYPOINT = {
38
+ fn: Foo,
39
+ params: [{ bar: "foo" }, { ref: { cuurrent: 1 } }],
40
+ isComponent: true,
41
+};
42
+
43
+```
44
+
45
+### Eval output
46
+(kind: ok) <div>foo</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/todo.validate-mutate-ref-arg-in-render.js
new
+11
@@ -0,0 +1,11 @@
1
+// @validateRefAccessDuringRender: true
2
+function Foo(props, ref) {
3
+ ref.current = 2;
4
+ return <div>{props.bar}</div>;
5
+}
6
+
7
+export const FIXTURE_ENTRYPOINT = {
8
+ fn: Foo,
9
+ params: [{ bar: "foo" }, { ref: { cuurrent: 1 } }],
10
+ isComponent: true,
11
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/update-expression-on-function-parameter-1.expect.md
renamed
+8
-8
@@ -2,8 +2,7 @@
2
## Input
3
4
```javascript
5
-// @debug
6
-function Component(a, [b], { c }) {
5
+function Component({ a: a, b: [b], c: { c } }) {
6
let d = a++;
7
let e = ++a;
8
let f = b--;
@@ -15,7 +14,7 @@ function Component(a, [b], { c }) {
14
15
export const FIXTURE_ENTRYPOINT = {
16
fn: Component,
18
- params: [2, [3], { c: 4 }],
17
+ params: [{ a: 2, b: [3], c: { c: 4 } }],
18
isComponent: false,
19
};
20
@@ -24,11 +23,12 @@ export const FIXTURE_ENTRYPOINT = {
23
## Code
24
25
```javascript
27
-import { unstable_useMemoCache as useMemoCache } from "react"; // @debug
28
-function Component(a, t37, t38) {
26
+import { unstable_useMemoCache as useMemoCache } from "react";
27
+function Component(t38) {
28
const $ = useMemoCache(10);
30
- let [b] = t37;
31
- let { c } = t38;
29
+ let { a, b: t40, c: t41 } = t38;
30
+ let [b] = t40;
31
+ let { c } = t41;
32
const d = a++;
33
const e = ++a;
34
const f = b--;
@@ -66,7 +66,7 @@ function Component(a, t37, t38) {
66
67
export const FIXTURE_ENTRYPOINT = {
68
fn: Component,
69
- params: [2, [3], { c: 4 }],
69
+ params: [{ a: 2, b: [3], c: { c: 4 } }],
70
isComponent: false,
71
};
72
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/update-expression-on-function-parameter-1.js
renamed
+2
-3
@@ -1,5 +1,4 @@
1
-// @debug
2
-function Component(a, [b], { c }) {
1
+function Component({ a: a, b: [b], c: { c } }) {
2
let d = a++;
3
let e = ++a;
4
let f = b--;
@@ -11,6 +10,6 @@ function Component(a, [b], { c }) {
10
11
export const FIXTURE_ENTRYPOINT = {
12
fn: Component,
14
- params: [2, [3], { c: 4 }],
13
+ params: [{ a: 2, b: [3], c: { c: 4 } }],
14
isComponent: false,
15
};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/update-expression-on-function-parameter-2.expect.md
new
+49
@@ -0,0 +1,49 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component(a) {
6
+ let d = a++;
7
+ let e = ++a;
8
+ return [a, d, e];
9
+}
10
+
11
+export const FIXTURE_ENTRYPOINT = {
12
+ fn: Component,
13
+ params: [2],
14
+ isComponent: false,
15
+};
16
+
17
+```
18
+
19
+## Code
20
+
21
+```javascript
22
+import { unstable_useMemoCache as useMemoCache } from "react";
23
+function Component(a) {
24
+ const $ = useMemoCache(4);
25
+ const d = a++;
26
+ const e = ++a;
27
+ let t0;
28
+ if ($[0] !== a || $[1] !== d || $[2] !== e) {
29
+ t0 = [a, d, e];
30
+ $[0] = a;
31
+ $[1] = d;
32
+ $[2] = e;
33
+ $[3] = t0;
34
+ } else {
35
+ t0 = $[3];
36
+ }
37
+ return t0;
38
+}
39
+
40
+export const FIXTURE_ENTRYPOINT = {
41
+ fn: Component,
42
+ params: [2],
43
+ isComponent: false,
44
+};
45
+
46
+```
47
+
48
+### Eval output
49
+(kind: ok) [4,2,4]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/update-expression-on-function-parameter-2.js
new
+11
@@ -0,0 +1,11 @@
1
+function Component(a) {
2
+ let d = a++;
3
+ let e = ++a;
4
+ return [a, d, e];
5
+}
6
+
7
+export const FIXTURE_ENTRYPOINT = {
8
+ fn: Component,
9
+ params: [2],
10
+ isComponent: false,
11
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/update-expression-on-function-parameter-3.expect.md
new
+50
@@ -0,0 +1,50 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component({ c }) {
6
+ let h = c++;
7
+ let i = --c;
8
+ return [c, h, i];
9
+}
10
+
11
+export const FIXTURE_ENTRYPOINT = {
12
+ fn: Component,
13
+ params: [{ c: 4 }],
14
+ isComponent: false,
15
+};
16
+
17
+```
18
+
19
+## Code
20
+
21
+```javascript
22
+import { unstable_useMemoCache as useMemoCache } from "react";
23
+function Component(t14) {
24
+ const $ = useMemoCache(4);
25
+ let { c } = t14;
26
+ const h = c++;
27
+ const i = --c;
28
+ let t0;
29
+ if ($[0] !== c || $[1] !== h || $[2] !== i) {
30
+ t0 = [c, h, i];
31
+ $[0] = c;
32
+ $[1] = h;
33
+ $[2] = i;
34
+ $[3] = t0;
35
+ } else {
36
+ t0 = $[3];
37
+ }
38
+ return t0;
39
+}
40
+
41
+export const FIXTURE_ENTRYPOINT = {
42
+ fn: Component,
43
+ params: [{ c: 4 }],
44
+ isComponent: false,
45
+};
46
+
47
+```
48
+
49
+### Eval output
50
+(kind: ok) [4,4,4]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/update-expression-on-function-parameter-3.js
new
+11
@@ -0,0 +1,11 @@
1
+function Component({ c }) {
2
+ let h = c++;
3
+ let i = --c;
4
+ return [c, h, i];
5
+}
6
+
7
+export const FIXTURE_ENTRYPOINT = {
8
+ fn: Component,
9
+ params: [{ c: 4 }],
10
+ isComponent: false,
11
+};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/update-expression-on-function-parameter-4.expect.md
new
+50
@@ -0,0 +1,50 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+function Component([b]) {
6
+ let f = b--;
7
+ let g = --b;
8
+ return [b, f, g];
9
+}
10
+
11
+export const FIXTURE_ENTRYPOINT = {
12
+ fn: Component,
13
+ params: [[3]],
14
+ isComponent: false,
15
+};
16
+
17
+```
18
+
19
+## Code
20
+
21
+```javascript
22
+import { unstable_useMemoCache as useMemoCache } from "react";
23
+function Component(t14) {
24
+ const $ = useMemoCache(4);
25
+ let [b] = t14;
26
+ const f = b--;
27
+ const g = --b;
28
+ let t0;
29
+ if ($[0] !== b || $[1] !== f || $[2] !== g) {
30
+ t0 = [b, f, g];
31
+ $[0] = b;
32
+ $[1] = f;
33
+ $[2] = g;
34
+ $[3] = t0;
35
+ } else {
36
+ t0 = $[3];
37
+ }
38
+ return t0;
39
+}
40
+
41
+export const FIXTURE_ENTRYPOINT = {
42
+ fn: Component,
43
+ params: [[3]],
44
+ isComponent: false,
45
+};
46
+
47
+```
48
+
49
+### Eval output
50
+(kind: ok) [1,3,1]
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/update-expression-on-function-parameter-4.js
new
+11
@@ -0,0 +1,11 @@
1
+function Component([b]) {
2
+ let f = b--;
3
+ let g = --b;
4
+ return [b, f, g];
5
+}
6
+
7
+export const FIXTURE_ENTRYPOINT = {
8
+ fn: Component,
9
+ params: [[3]],
10
+ isComponent: false,
11
+};