diff --git a/compiler/packages/babel-plugin-react-forget/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-forget/src/HIR/Environment.ts
index 21c6b1c761471..ec82bbfc3451c 100644
--- a/compiler/packages/babel-plugin-react-forget/src/HIR/Environment.ts
+++ b/compiler/packages/babel-plugin-react-forget/src/HIR/Environment.ts
@@ -28,6 +28,7 @@ import {
makeIdentifierId,
} from "./HIR";
import {
+ BuiltInMixedReadonlyId,
DefaultMutatingHook,
DefaultNonmutatingHook,
FunctionSignature,
@@ -55,6 +56,25 @@ export type Hook = {
* compiler to avoid memoizing arguments.
*/
noAlias?: boolean;
+
+ /**
+ * Specifies whether the hook returns data that is composed of:
+ * - undefined
+ * - null
+ * - boolean
+ * - number
+ * - string
+ * - arrays whose items are also transitiveMixed
+ * - objects whose values are also transitiveMixed
+ *
+ * Many state management and data-fetching APIs return data that meets
+ * this criteria since this is JSON + undefined. Forget can compile
+ * hooks that return transitively mixed data more optimally because it
+ * can make inferences about some method calls (especially array methods
+ * like `data.items.map(...)` since these builtin types have few built-in
+ * methods.
+ */
+ transitiveMixedData?: boolean;
};
// TODO(mofeiZ): User defined global types (with corresponding shapes).
@@ -236,7 +256,9 @@ export class Environment {
addHook(this.#shapes, [], {
positionalParams: [],
restParam: hook.effectKind,
- returnType: { kind: "Poly" },
+ returnType: hook.transitiveMixedData
+ ? { kind: "Object", shapeId: BuiltInMixedReadonlyId }
+ : { kind: "Poly" },
returnValueKind: hook.valueKind,
calleeEffect: Effect.Read,
hookKind: "Custom",
@@ -312,7 +334,9 @@ export class Environment {
loc: null,
suggestions: null,
});
- return shape.properties.get(property) ?? null;
+ return (
+ shape.properties.get(property) ?? shape.properties.get("*") ?? null
+ );
} else {
return null;
}
diff --git a/compiler/packages/babel-plugin-react-forget/src/HIR/ObjectShape.ts b/compiler/packages/babel-plugin-react-forget/src/HIR/ObjectShape.ts
index 66e1c94845f43..ec7f50a0c00b6 100644
--- a/compiler/packages/babel-plugin-react-forget/src/HIR/ObjectShape.ts
+++ b/compiler/packages/babel-plugin-react-forget/src/HIR/ObjectShape.ts
@@ -170,6 +170,7 @@ export const BuiltInUseStateId = "BuiltInUseState";
export const BuiltInSetStateId = "BuiltInSetState";
export const BuiltInUseRefId = "BuiltInUseRefId";
export const BuiltInRefValueId = "BuiltInRefValue";
+export const BuiltInMixedReadonlyId = "BuiltInMixedReadonly";
/**
* ShapeRegistry with default definitions for built-ins.
@@ -294,6 +295,42 @@ addObject(BUILTIN_SHAPES, BuiltInUseRefId, [
addObject(BUILTIN_SHAPES, BuiltInRefValueId, []);
+addObject(BUILTIN_SHAPES, BuiltInMixedReadonlyId, [
+ [
+ "toString",
+ addFunction(BUILTIN_SHAPES, [], {
+ positionalParams: [],
+ restParam: Effect.Read,
+ returnType: PRIMITIVE_TYPE,
+ calleeEffect: Effect.Read,
+ returnValueKind: ValueKind.Immutable,
+ }),
+ ],
+ [
+ "map",
+ addFunction(BUILTIN_SHAPES, [], {
+ positionalParams: [],
+ restParam: Effect.Read,
+ returnType: { kind: "Object", shapeId: BuiltInArrayId },
+ calleeEffect: Effect.ConditionallyMutate,
+ returnValueKind: ValueKind.Mutable,
+ noAlias: true,
+ }),
+ ],
+ [
+ "filter",
+ addFunction(BUILTIN_SHAPES, [], {
+ positionalParams: [],
+ restParam: Effect.Read,
+ returnType: { kind: "Object", shapeId: BuiltInArrayId },
+ calleeEffect: Effect.ConditionallyMutate,
+ returnValueKind: ValueKind.Mutable,
+ noAlias: true,
+ }),
+ ],
+ ["*", { kind: "Object", shapeId: BuiltInMixedReadonlyId }],
+]);
+
export const DefaultMutatingHook = addHook(BUILTIN_SHAPES, [], {
positionalParams: [],
restParam: Effect.ConditionallyMutate,
diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/destructuring-mixed-scope-and-local-variables-with-default.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/destructuring-mixed-scope-and-local-variables-with-default.expect.md
index f12061aa0aef9..247056d02b173 100644
--- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/destructuring-mixed-scope-and-local-variables-with-default.expect.md
+++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/destructuring-mixed-scope-and-local-variables-with-default.expect.md
@@ -56,87 +56,94 @@ function useFragment(_arg1, _arg2) {
}
function Component(props) {
- const $ = useMemoCache(16);
- const post = useFragment(graphql`...`, props.post);
- const c_0 = $[0] !== post;
+ const $ = useMemoCache(17);
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = graphql`...`;
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ const post = useFragment(t0, props.post);
+ const c_1 = $[1] !== post;
let media;
let allUrls;
let onClick;
- if (c_0) {
+ if (c_1) {
allUrls = [];
- const { media: t0, comments: t2, urls: t4 } = post;
- const c_4 = $[4] !== t0;
- let t1;
- if (c_4) {
- t1 = t0 === undefined ? null : t0;
- $[4] = t0;
+ const { media: t1, comments: t3, urls: t5 } = post;
+ const c_5 = $[5] !== t1;
+ let t2;
+ if (c_5) {
+ t2 = t1 === undefined ? null : t1;
$[5] = t1;
+ $[6] = t2;
} else {
- t1 = $[5];
+ t2 = $[6];
}
- media = t1;
- const c_6 = $[6] !== t2;
- let t3;
- if (c_6) {
- t3 = t2 === undefined ? [] : t2;
- $[6] = t2;
+ media = t2;
+ const c_7 = $[7] !== t3;
+ let t4;
+ if (c_7) {
+ t4 = t3 === undefined ? [] : t3;
$[7] = t3;
+ $[8] = t4;
} else {
- t3 = $[7];
+ t4 = $[8];
}
- const comments = t3;
- const c_8 = $[8] !== t4;
- let t5;
- if (c_8) {
- t5 = t4 === undefined ? [] : t4;
- $[8] = t4;
+ const comments = t4;
+ const c_9 = $[9] !== t5;
+ let t6;
+ if (c_9) {
+ t6 = t5 === undefined ? [] : t5;
$[9] = t5;
+ $[10] = t6;
} else {
- t5 = $[9];
+ t6 = $[10];
}
- const urls = t5;
- const c_10 = $[10] !== comments.length;
- let t6;
- if (c_10) {
- t6 = (e) => {
+ const urls = t6;
+ const c_11 = $[11] !== comments.length;
+ let t7;
+ if (c_11) {
+ t7 = (e) => {
if (!comments.length) {
return;
}
console.log(comments.length);
};
- $[10] = comments.length;
- $[11] = t6;
+ $[11] = comments.length;
+ $[12] = t7;
} else {
- t6 = $[11];
+ t7 = $[12];
}
- onClick = t6;
+ onClick = t7;
allUrls.push(...urls);
- $[0] = post;
- $[1] = media;
- $[2] = allUrls;
- $[3] = onClick;
+ $[1] = post;
+ $[2] = media;
+ $[3] = allUrls;
+ $[4] = onClick;
} else {
- media = $[1];
- allUrls = $[2];
- onClick = $[3];
+ media = $[2];
+ allUrls = $[3];
+ onClick = $[4];
}
- const c_12 = $[12] !== media;
- const c_13 = $[13] !== allUrls;
- const c_14 = $[14] !== onClick;
- let t7;
- if (c_12 || c_13 || c_14) {
- t7 = ;
- $[12] = media;
- $[13] = allUrls;
- $[14] = onClick;
- $[15] = t7;
+ const c_13 = $[13] !== media;
+ const c_14 = $[14] !== allUrls;
+ const c_15 = $[15] !== onClick;
+ let t8;
+ if (c_13 || c_14 || c_15) {
+ t8 = ;
+ $[13] = media;
+ $[14] = allUrls;
+ $[15] = onClick;
+ $[16] = t8;
} else {
- t7 = $[15];
+ t8 = $[16];
}
- return t7;
+ return t8;
}
export const FIXTURE_ENTRYPOINT = {
diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/destructuring-mixed-scope-declarations-and-locals.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/destructuring-mixed-scope-declarations-and-locals.expect.md
index 9d606ddc5b7b7..bf056a02927a7 100644
--- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/destructuring-mixed-scope-declarations-and-locals.expect.md
+++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/destructuring-mixed-scope-declarations-and-locals.expect.md
@@ -30,53 +30,60 @@ function Component(props) {
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
function Component(props) {
- const $ = useMemoCache(8);
- const post = useFragment(graphql`...`, props.post);
- const c_0 = $[0] !== post;
+ const $ = useMemoCache(9);
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = graphql`...`;
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ const post = useFragment(t0, props.post);
+ const c_1 = $[1] !== post;
let media;
let onClick;
- if (c_0) {
+ if (c_1) {
const allUrls = [];
const { media: t83, comments, urls } = post;
media = t83;
- const c_3 = $[3] !== comments.length;
- let t0;
- if (c_3) {
- t0 = (e) => {
+ const c_4 = $[4] !== comments.length;
+ let t1;
+ if (c_4) {
+ t1 = (e) => {
if (!comments.length) {
return;
}
console.log(comments.length);
};
- $[3] = comments.length;
- $[4] = t0;
+ $[4] = comments.length;
+ $[5] = t1;
} else {
- t0 = $[4];
+ t1 = $[5];
}
- onClick = t0;
+ onClick = t1;
allUrls.push(...urls);
- $[0] = post;
- $[1] = media;
- $[2] = onClick;
+ $[1] = post;
+ $[2] = media;
+ $[3] = onClick;
} else {
- media = $[1];
- onClick = $[2];
+ media = $[2];
+ onClick = $[3];
}
- const c_5 = $[5] !== media;
- const c_6 = $[6] !== onClick;
- let t1;
- if (c_5 || c_6) {
- t1 = ;
- $[5] = media;
- $[6] = onClick;
- $[7] = t1;
+ const c_6 = $[6] !== media;
+ const c_7 = $[7] !== onClick;
+ let t2;
+ if (c_6 || c_7) {
+ t2 = ;
+ $[6] = media;
+ $[7] = onClick;
+ $[8] = t2;
} else {
- t1 = $[7];
+ t2 = $[8];
}
- return t1;
+ return t2;
}
```
diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/optional-call-logical.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/optional-call-logical.expect.md
index f0b8d40ff3598..3166d8eaf3713 100644
--- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/optional-call-logical.expect.md
+++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/optional-call-logical.expect.md
@@ -12,9 +12,27 @@ function Component(props) {
## Code
```javascript
+import { unstable_useMemoCache as useMemoCache } from "react";
function Component(props) {
- const item = useFragment(graphql`...`, props.item);
- return item.items?.map((item_0) => renderItem(item_0)) ?? [];
+ const $ = useMemoCache(3);
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = graphql`...`;
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ const item = useFragment(t0, props.item);
+ const c_1 = $[1] !== item.items;
+ let t1;
+ if (c_1) {
+ t1 = item.items?.map((item_0) => renderItem(item_0)) ?? [];
+ $[1] = item.items;
+ $[2] = t1;
+ } else {
+ t1 = $[2];
+ }
+ return t1;
}
```
diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/readonly-object-method-calls-mutable-lambda.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/readonly-object-method-calls-mutable-lambda.expect.md
new file mode 100644
index 0000000000000..e3c6f16f683d2
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/readonly-object-method-calls-mutable-lambda.expect.md
@@ -0,0 +1,62 @@
+
+## Input
+
+```javascript
+// @enableNoAliasOptimizations
+function Component(props) {
+ const x = makeObject();
+ const user = useFragment(
+ graphql`fragment Component_user on User { ... }`,
+ props.user
+ );
+ const posts = user.timeline.posts.edges.nodes.map((node) => {
+ x.y = true;
+ return ;
+ });
+ posts.push({});
+ const count = posts.length;
+ foo(count);
+ return <>{posts}>;
+}
+
+```
+
+## Code
+
+```javascript
+import { unstable_useMemoCache as useMemoCache } from "react"; // @enableNoAliasOptimizations
+function Component(props) {
+ const $ = useMemoCache(3);
+ const x = makeObject();
+ const user = useFragment(
+ graphql`fragment Component_user on User { ... }`,
+ props.user
+ );
+ const posts = user.timeline.posts.edges.nodes.map((node) => {
+ x.y = true;
+ return ;
+ });
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = {};
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ posts.push(t0);
+ const count = posts.length;
+ foo(count);
+ const c_1 = $[1] !== posts;
+ let t1;
+ if (c_1) {
+ t1 = <>{posts}>;
+ $[1] = posts;
+ $[2] = t1;
+ } else {
+ t1 = $[2];
+ }
+ return t1;
+}
+
+```
+
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/readonly-object-method-calls-mutable-lambda.js b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/readonly-object-method-calls-mutable-lambda.js
new file mode 100644
index 0000000000000..0bd543d9d3a59
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/readonly-object-method-calls-mutable-lambda.js
@@ -0,0 +1,16 @@
+// @enableNoAliasOptimizations
+function Component(props) {
+ const x = makeObject();
+ const user = useFragment(
+ graphql`fragment Component_user on User { ... }`,
+ props.user
+ );
+ const posts = user.timeline.posts.edges.nodes.map((node) => {
+ x.y = true;
+ return ;
+ });
+ posts.push({});
+ const count = posts.length;
+ foo(count);
+ return <>{posts}>;
+}
diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/readonly-object-method-calls.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/readonly-object-method-calls.expect.md
new file mode 100644
index 0000000000000..0bf19c9303b5d
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/readonly-object-method-calls.expect.md
@@ -0,0 +1,64 @@
+
+## Input
+
+```javascript
+// @enableNoAliasOptimizations
+function Component(props) {
+ const user = useFragment(
+ graphql`fragment Component_user on User { ... }`,
+ props.user
+ );
+ const posts = user.timeline.posts.edges.nodes.map((node) => (
+
+ ));
+ posts.push({});
+ const count = posts.length;
+ foo(count);
+ return <>{posts}>;
+}
+
+```
+
+## Code
+
+```javascript
+import { unstable_useMemoCache as useMemoCache } from "react"; // @enableNoAliasOptimizations
+function Component(props) {
+ const $ = useMemoCache(5);
+ const user = useFragment(
+ graphql`fragment Component_user on User { ... }`,
+ props.user
+ );
+ const c_0 = $[0] !== user.timeline.posts.edges.nodes;
+ let posts;
+ if (c_0) {
+ posts = user.timeline.posts.edges.nodes.map((node) => );
+ let t0;
+ if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = {};
+ $[2] = t0;
+ } else {
+ t0 = $[2];
+ }
+ posts.push(t0);
+ $[0] = user.timeline.posts.edges.nodes;
+ $[1] = posts;
+ } else {
+ posts = $[1];
+ }
+ const count = posts.length;
+ foo(count);
+ const c_3 = $[3] !== posts;
+ let t1;
+ if (c_3) {
+ t1 = <>{posts}>;
+ $[3] = posts;
+ $[4] = t1;
+ } else {
+ t1 = $[4];
+ }
+ return t1;
+}
+
+```
+
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/readonly-object-method-calls.js b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/readonly-object-method-calls.js
new file mode 100644
index 0000000000000..b8e03b19be718
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/readonly-object-method-calls.js
@@ -0,0 +1,14 @@
+// @enableNoAliasOptimizations
+function Component(props) {
+ const user = useFragment(
+ graphql`fragment Component_user on User { ... }`,
+ props.user
+ );
+ const posts = user.timeline.posts.edges.nodes.map((node) => (
+
+ ));
+ posts.push({});
+ const count = posts.length;
+ foo(count);
+ return <>{posts}>;
+}
diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-scope-missing-mutable-range.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-scope-missing-mutable-range.expect.md
index 3014745db0e9e..d5cd24eeac5bc 100644
--- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-scope-missing-mutable-range.expect.md
+++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-scope-missing-mutable-range.expect.md
@@ -20,20 +20,27 @@ function HomeDiscoStoreItemTileRating(props) {
```javascript
import { unstable_useMemoCache as useMemoCache } from "react";
function HomeDiscoStoreItemTileRating(props) {
- const $ = useMemoCache(1);
+ const $ = useMemoCache(3);
const item = useFragment();
+ const c_0 = $[0] !== item;
let count;
- count = 0;
- const aggregates = item?.aggregates || [];
- aggregates.forEach((aggregate) => {
- count = count + (aggregate.count || 0);
- });
+ if (c_0) {
+ count = 0;
+ const aggregates = item?.aggregates || [];
+ aggregates.forEach((aggregate) => {
+ count = count + (aggregate.count || 0);
+ });
+ $[0] = item;
+ $[1] = count;
+ } else {
+ count = $[1];
+ }
let t0;
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
t0 = {count};
- $[0] = t0;
+ $[2] = t0;
} else {
- t0 = $[0];
+ t0 = $[2];
}
return t0;
}
diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/tagged-template-in-hook.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/tagged-template-in-hook.expect.md
index a7db7906f2000..fbfaee47a8896 100644
--- a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/tagged-template-in-hook.expect.md
+++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/tagged-template-in-hook.expect.md
@@ -12,8 +12,17 @@ function Component(props) {
## Code
```javascript
+import { unstable_useMemoCache as useMemoCache } from "react";
function Component(props) {
- const user = useFragment(graphql`fragment on User { name }`, props.user);
+ const $ = useMemoCache(1);
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = graphql`fragment on User { name }`;
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ const user = useFragment(t0, props.user);
return user.name;
}
diff --git a/compiler/packages/fixture-test-utils/src/compiler-utils.ts b/compiler/packages/fixture-test-utils/src/compiler-utils.ts
index f8d2d3d46b39d..a904e0f03b1a5 100644
--- a/compiler/packages/fixture-test-utils/src/compiler-utils.ts
+++ b/compiler/packages/fixture-test-utils/src/compiler-utils.ts
@@ -100,6 +100,17 @@ export function transformFixtureInput(
{
valueKind: "frozen" as ValueKind,
effectKind: "freeze" as Effect,
+ transitiveMixedData: false,
+ noAlias: false,
+ },
+ ],
+ [
+ "useFragment",
+ {
+ valueKind: "frozen" as ValueKind,
+ effectKind: "freeze" as Effect,
+ transitiveMixedData: true,
+ noAlias: true,
},
],
[
@@ -107,6 +118,7 @@ export function transformFixtureInput(
{
valueKind: "mutable" as ValueKind,
effectKind: "read" as Effect,
+ transitiveMixedData: false,
noAlias: true,
},
],
diff --git a/compiler/packages/sprout/src/SproutTodoFilter.ts b/compiler/packages/sprout/src/SproutTodoFilter.ts
index 9ff3b3e406e5f..142e2e9d85470 100644
--- a/compiler/packages/sprout/src/SproutTodoFilter.ts
+++ b/compiler/packages/sprout/src/SproutTodoFilter.ts
@@ -413,6 +413,8 @@ const skipFilter = new Set([
"fbt-template-string-same-scope",
"component-declaration-basic.flow",
"nested-function-with-param-as-captured-dep",
+ "readonly-object-method-calls",
+ "readonly-object-method-calls-mutable-lambda",
// TODO: 🌲
"forest-basic",