@samitouri / QOS-React-1 / commits / 216696dd88

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;