Allow context values to be mutated in callbacks
Adds a new type for representing context values, which is transitive. So `useContext(a).b.c` also gets inferred as a context type. This allows us to refine our inference, and allow passing callbacks that modify context where a "frozen" lambda is exepcted.
Joe Savona committed
Oct 3, 2023 at 08:47 UTC
216696dd889d258b6bfe0c380dd55454dece212e
7 files changed
+71
-2
compiler/packages/babel-plugin-react-forget/src/HIR/Globals.ts
+2
-1
@@ -9,6 +9,7 @@ import { Effect, ValueKind } from "./HIR";
9
import {
10
BUILTIN_SHAPES,
11
BuiltInArrayId,
12
+ BuiltInContextId,
13
BuiltInUseRefId,
14
BuiltInUseStateId,
15
ShapeRegistry,
@@ -239,7 +240,7 @@ const BUILTIN_HOOKS: Array<[string, FunctionType]> = [
240
addHook(DEFAULT_SHAPES, [], {
241
positionalParams: [],
242
restParam: Effect.Read,
242
- returnType: { kind: "Poly" },
243
+ returnType: { kind: "Object", shapeId: BuiltInContextId },
244
calleeEffect: Effect.Read,
245
hookKind: "useContext",
246
returnValueKind: ValueKind.Mutable,
compiler/packages/babel-plugin-react-forget/src/HIR/HIR.ts
+4
@@ -1111,6 +1111,10 @@ export function isSetStateType(id: Identifier): boolean {
1111
return id.type.kind === "Function" && id.type.shapeId === "BuiltInSetState";
1112
}
1113
1114
+export function isContextType(id: Identifier): boolean {
1115
+ return id.type.kind === "Object" && id.type.shapeId === "BuiltInContext";
1116
+}
1117
+
1118
export function getHookKind(env: Environment, id: Identifier): HookKind | null {
1119
const idType = id.type;
1120
if (idType.kind === "Function") {
compiler/packages/babel-plugin-react-forget/src/HIR/ObjectShape.ts
+5
@@ -170,6 +170,7 @@ export const BuiltInUseStateId = "BuiltInUseState";
170
export const BuiltInSetStateId = "BuiltInSetState";
171
export const BuiltInUseRefId = "BuiltInUseRefId";
172
export const BuiltInRefValueId = "BuiltInRefValue";
173
+export const BuiltInContextId = "BuiltInContext";
174
export const BuiltInMixedReadonlyId = "BuiltInMixedReadonly";
175
176
/**
@@ -293,6 +294,10 @@ addObject(BUILTIN_SHAPES, BuiltInUseRefId, [
294
["current", { kind: "Object", shapeId: BuiltInRefValueId }],
295
]);
296
297
+addObject(BUILTIN_SHAPES, BuiltInContextId, [
298
+ ["*", { kind: "Object", shapeId: BuiltInContextId }],
299
+]);
300
+
301
addObject(BUILTIN_SHAPES, BuiltInRefValueId, []);
302
303
addObject(BUILTIN_SHAPES, BuiltInMixedReadonlyId, [
compiler/packages/babel-plugin-react-forget/src/Inference/AnalyseFunctions.ts
+3
-1
@@ -10,6 +10,7 @@ import {
10
Effect,
11
HIRFunction,
12
Identifier,
13
+ isContextType,
14
isRefValueType,
15
isSetStateType,
16
isUseRefType,
@@ -142,7 +143,8 @@ function infer(
143
if (
144
isUseRefType(dep.identifier) ||
145
isRefValueType(dep.identifier) ||
145
- isSetStateType(dep.identifier)
146
+ isSetStateType(dep.identifier) ||
147
+ isContextType(dep.identifier)
148
) {
149
// TODO: this is a hack to ensure we treat functions which reference refs
150
// as having a capture and therefore being considered mutable. this ensures
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/useContext-mutable-value.expect.md
new
+48
@@ -0,0 +1,48 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @debug
6
+function Component(props) {
7
+ const FooContext = useContext(Foo);
8
+ const onClick = () => {
9
+ FooContext.current = true;
10
+ };
11
+ return <div onClick={onClick} />;
12
+}
13
+
14
+```
15
+
16
+## Code
17
+
18
+```javascript
19
+import { unstable_useMemoCache as useMemoCache } from "react"; // @debug
20
+function Component(props) {
21
+ const $ = useMemoCache(4);
22
+ const FooContext = useContext(Foo);
23
+ const c_0 = $[0] !== FooContext.current;
24
+ let t0;
25
+ if (c_0) {
26
+ t0 = () => {
27
+ FooContext.current = true;
28
+ };
29
+ $[0] = FooContext.current;
30
+ $[1] = t0;
31
+ } else {
32
+ t0 = $[1];
33
+ }
34
+ const onClick = t0;
35
+ const c_2 = $[2] !== onClick;
36
+ let t1;
37
+ if (c_2) {
38
+ t1 = <div onClick={onClick} />;
39
+ $[2] = onClick;
40
+ $[3] = t1;
41
+ } else {
42
+ t1 = $[3];
43
+ }
44
+ return t1;
45
+}
46
+
47
+```
48
+
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/useContext-mutable-value.js
new
+8
@@ -0,0 +1,8 @@
1
+// @debug
2
+function Component(props) {
3
+ const FooContext = useContext(Foo);
4
+ const onClick = () => {
5
+ FooContext.current = true;
6
+ };
7
+ return <div onClick={onClick} />;
8
+}
compiler/packages/sprout/src/SproutTodoFilter.ts
+1
@@ -460,6 +460,7 @@ const skipFilter = new Set([
460
"fbtparam-text-must-use-expression-container",
461
"fbtparam-with-jsx-fragment-value",
462
"fbt-preserve-jsxtext",
463
+ "useContext-mutable-value",
464
]);
465
466
export default skipFilter;