@samitouri / QOS-React / commits / a78da09640

Move global reassignment validation to InferReferenceEffects

ghstack-source-id: fcda8140310d6e1201df35f399020b22b6dccb08 Pull Request resolved: https://github.com/facebook/react-forget/pull/2872

Joe Savona committed Apr 18, 2024 at 13:51 UTC a78da09640bc2899588e9143cfda3f166cc06974
14 files changed +345 -25
compiler/packages/babel-plugin-react-forget/src/HIR/BuildHIR.ts
+58 -19
@@ -16,7 +16,7 @@ import {
16 } from "../CompilerError";
17 import { Err, Ok, Result } from "../Utils/Result";
18 import { assertExhaustive, hasNode } from "../Utils/utils";
19 -import { Environment, printFunctionType } from "./Environment";
19 +import { Environment } from "./Environment";
20 import {
21 ArrayExpression,
22 ArrayPattern,
@@ -2334,6 +2334,14 @@ function lowerExpression(
2334 });
2335 }
2336 return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
2337 + } else if (lvalue.kind === "Global") {
2338 + builder.errors.push({
2339 + reason: `(BuildHIR::lowerExpression) Support UpdateExpression where argument is a global`,
2340 + severity: ErrorSeverity.Todo,
2341 + loc: exprLoc,
2342 + suggestions: null,
2343 + });
2344 + return { kind: "UnsupportedNode", node: exprNode, loc: exprLoc };
2345 }
2346 const value = lowerIdentifier(builder, argument);
2347 if (expr.node.prefix) {
@@ -3269,24 +3277,11 @@ function lowerIdentifierForAssignment(
3277 loc: SourceLocation,
3278 kind: InstructionKind,
3279 path: NodePath<t.Identifier>
3272 -): Place | null {
3280 +): Place | { kind: "Global"; name: string } | null {
3281 const identifier = builder.resolveIdentifier(path);
3282 if (identifier == null) {
3283 if (kind === InstructionKind.Reassign) {
3276 - /*
3277 - * Trying to reassign a global is not allowed
3278 - * TODO: add support for StoreGlobal or similar, and move this error to run conditionally only for render-phase
3279 - * functions in InferReferenceEffects
3280 - */
3281 - builder.errors.push({
3282 - reason: `Unexpected reassignment of a variable which was defined outside of the ${printFunctionType(
3283 - builder.environment.fnType
3284 - )}`,
3285 - description: `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)`,
3286 - severity: ErrorSeverity.InvalidReact,
3287 - loc: path.parentPath.node.loc ?? null,
3288 - suggestions: null,
3289 - });
3284 + return { kind: "Global", name: path.node.name };
3285 } else {
3286 // Else its an internal error bc we couldn't find the binding
3287 builder.errors.push({
@@ -3295,8 +3290,8 @@ function lowerIdentifierForAssignment(
3290 loc: path.node.loc ?? null,
3291 suggestions: null,
3292 });
3293 + return null;
3294 }
3299 - return null;
3295 }
3296
3297 const place: Place = {
@@ -3328,6 +3323,14 @@ function lowerAssignment(
3323 loc: lvalue.node.loc ?? GeneratedSource,
3324 node: lvalue.node,
3325 };
3326 + } else if (place.kind === "Global") {
3327 + const temporary = lowerValueToTemporary(builder, {
3328 + kind: "StoreGlobal",
3329 + name: place.name,
3330 + value,
3331 + loc,
3332 + });
3333 + return { kind: "LoadLocal", place: temporary, loc: temporary.loc };
3334 }
3335 const isHoistedIdentifier = builder.environment.isHoistedIdentifier(
3336 lvalue.node
@@ -3452,7 +3455,8 @@ function lowerAssignment(
3455 elements.some(
3456 (element) =>
3457 element.isIdentifier() &&
3455 - getStoreKind(builder, element) !== "StoreLocal"
3458 + (getStoreKind(builder, element) !== "StoreLocal" ||
3459 + builder.resolveIdentifier(element) == null)
3460 ));
3461 for (let i = 0; i < elements.length; i++) {
3462 const element = elements[i];
@@ -3478,6 +3482,14 @@ function lowerAssignment(
3482 );
3483 if (identifier === null) {
3484 continue;
3485 + } else if (identifier.kind === "Global") {
3486 + builder.errors.push({
3487 + severity: ErrorSeverity.Todo,
3488 + reason:
3489 + "Expected reassignment of globals to enable forceTemporaries",
3490 + loc: element.node.loc ?? GeneratedSource,
3491 + });
3492 + continue;
3493 }
3494 items.push({
3495 kind: "Spread",
@@ -3509,6 +3521,14 @@ function lowerAssignment(
3521 );
3522 if (identifier === null) {
3523 continue;
3524 + } else if (identifier.kind === "Global") {
3525 + builder.errors.push({
3526 + severity: ErrorSeverity.Todo,
3527 + reason:
3528 + "Expected reassignment of globals to enable forceTemporaries",
3529 + loc: element.node.loc ?? GeneratedSource,
3530 + });
3531 + continue;
3532 }
3533 items.push(identifier);
3534 } else {
@@ -3564,7 +3584,10 @@ function lowerAssignment(
3584 (property) =>
3585 property.isRestElement() ||
3586 (property.isObjectProperty() &&
3567 - !property.get("value").isIdentifier())
3587 + (!property.get("value").isIdentifier() ||
3588 + builder.resolveIdentifier(
3589 + property.get("value") as NodePath<t.Identifier>
3590 + ) == null))
3591 );
3592 for (let i = 0; i < propertiesPaths.length; i++) {
3593 const property = propertiesPaths[i];
@@ -3602,6 +3625,14 @@ function lowerAssignment(
3625 );
3626 if (identifier === null) {
3627 continue;
3628 + } else if (identifier.kind === "Global") {
3629 + builder.errors.push({
3630 + severity: ErrorSeverity.Todo,
3631 + reason:
3632 + "Expected reassignment of globals to enable forceTemporaries",
3633 + loc: property.node.loc ?? GeneratedSource,
3634 + });
3635 + continue;
3636 }
3637 properties.push({
3638 kind: "Spread",
@@ -3656,6 +3687,14 @@ function lowerAssignment(
3687 );
3688 if (identifier === null) {
3689 continue;
3690 + } else if (identifier.kind === "Global") {
3691 + builder.errors.push({
3692 + severity: ErrorSeverity.Todo,
3693 + reason:
3694 + "Expected reassignment of globals to enable forceTemporaries",
3695 + loc: element.node.loc ?? GeneratedSource,
3696 + });
3697 + continue;
3698 }
3699 properties.push({
3700 kind: "ObjectProperty",
compiler/packages/babel-plugin-react-forget/src/Inference/InferReferenceEffects.ts
+11
@@ -1581,6 +1581,17 @@ function inferBlock(
1581 );
1582 const lvalue = instr.lvalue;
1583 lvalue.effect = Effect.Store;
1584 +
1585 + functionEffects.push({
1586 + kind: "GlobalMutation",
1587 + error: {
1588 + reason:
1589 + "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)",
1590 + loc: instr.loc,
1591 + suggestions: null,
1592 + severity: ErrorSeverity.InvalidReact,
1593 + },
1594 + });
1595 continue;
1596 }
1597 case "Destructure": {
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/allow-global-reassignment-in-effect.expect.md new
+97
@@ -0,0 +1,97 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { useEffect, useState } from "react";
6 +
7 +let someGlobal = false;
8 +
9 +function Component() {
10 + const [state, setState] = useState(someGlobal);
11 +
12 + useEffect(() => {
13 + someGlobal = true;
14 + }, []);
15 +
16 + useEffect(() => {
17 + setState(someGlobal);
18 + }, [someGlobal]);
19 +
20 + return <div>{String(state)}</div>;
21 +}
22 +
23 +export const FIXTURE_ENTRYPOINT = {
24 + fn: Component,
25 + params: [{}],
26 +};
27 +
28 +```
29 +
30 +## Code
31 +
32 +```javascript
33 +import {
34 + useEffect,
35 + useState,
36 + unstable_useMemoCache as useMemoCache,
37 +} from "react";
38 +
39 +let someGlobal = false;
40 +
41 +function Component() {
42 + const $ = useMemoCache(6);
43 + const [state, setState] = useState(someGlobal);
44 + let t0;
45 + let t1;
46 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
47 + t0 = () => {
48 + someGlobal = true;
49 + };
50 +
51 + t1 = [];
52 + $[0] = t0;
53 + $[1] = t1;
54 + } else {
55 + t0 = $[0];
56 + t1 = $[1];
57 + }
58 + useEffect(t0, t1);
59 + let t2;
60 + if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
61 + t2 = () => {
62 + setState(someGlobal);
63 + };
64 + $[2] = t2;
65 + } else {
66 + t2 = $[2];
67 + }
68 + let t3;
69 + if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
70 + t3 = [someGlobal];
71 + $[3] = t3;
72 + } else {
73 + t3 = $[3];
74 + }
75 + useEffect(t2, t3);
76 +
77 + const t4 = String(state);
78 + let t5;
79 + if ($[4] !== t4) {
80 + t5 = <div>{t4}</div>;
81 + $[4] = t4;
82 + $[5] = t5;
83 + } else {
84 + t5 = $[5];
85 + }
86 + return t5;
87 +}
88 +
89 +export const FIXTURE_ENTRYPOINT = {
90 + fn: Component,
91 + params: [{}],
92 +};
93 +
94 +```
95 +
96 +### Eval output
97 +(kind: ok) <div>true</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/allow-global-reassignment-in-effect.js new
+22
@@ -0,0 +1,22 @@
1 +import { useEffect, useState } from "react";
2 +
3 +let someGlobal = false;
4 +
5 +function Component() {
6 + const [state, setState] = useState(someGlobal);
7 +
8 + useEffect(() => {
9 + someGlobal = true;
10 + }, []);
11 +
12 + useEffect(() => {
13 + setState(someGlobal);
14 + }, [someGlobal]);
15 +
16 + return <div>{String(state)}</div>;
17 +}
18 +
19 +export const FIXTURE_ENTRYPOINT = {
20 + fn: Component,
21 + params: [{}],
22 +};
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.invalid-destructure-assignment-to-global.expect.md
+1 -1
@@ -15,7 +15,7 @@ function useFoo(props) {
15 ```
16 1 | function useFoo(props) {
17 > 2 | [x] = props;
18 - | ^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the function. 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) (2:2)
18 + | ^ 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) (2:2)
19 3 | return { x };
20 4 | }
21 5 |
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.invalid-destructure-to-local-global-variables.expect.md
+1 -1
@@ -18,7 +18,7 @@ function Component(props) {
18 1 | function Component(props) {
19 2 | let a;
20 > 3 | [a, b] = props.value;
21 - | ^^^^^^ InvalidReact: Unexpected reassignment of a variable which was defined outside of the function. 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 + | ^ 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)
22 4 |
23 5 | return [a, b];
24 6 | }
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.mutate-global-increment-op-invalid-react.expect.md
+1 -1
@@ -18,7 +18,7 @@ function NoHooks() {
18 2 |
19 3 | function NoHooks() {
20 > 4 | renderCount++;
21 - | ^^^^^^^^^^^^^ 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)
21 + | ^^^^^^^^^^^^^ Todo: (BuildHIR::lowerExpression) Support UpdateExpression where argument is a global (4:4)
22 5 | return <div />;
23 6 | }
24 7 |
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.reassignment-to-global-function-prop.expect.md new
+32
@@ -0,0 +1,32 @@
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 + // It's possible that this could be an event handler / effect function,
12 + // but we don't know that and conservatively assume it's a render helper
13 + // where it's disallowed to modify globals
14 + return <Foo foo={foo} />;
15 +}
16 +
17 +```
18 +
19 +
20 +## Error
21 +
22 +```
23 + 2 | const foo = () => {
24 + 3 | // Cannot assign to globals
25 +> 4 | someUnknownGlobal = true;
26 + | ^^^^^^^^^^^^^^^^^ 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)
27 + 5 | moduleLocal = true;
28 + 6 | };
29 + 7 | // It's possible that this could be an event handler / effect function,
30 +```
31 +
32 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.reassignment-to-global-function-prop.js new
+11
@@ -0,0 +1,11 @@
1 +function Component() {
2 + const foo = () => {
3 + // Cannot assign to globals
4 + someUnknownGlobal = true;
5 + moduleLocal = true;
6 + };
7 + // It's possible that this could be an event handler / effect function,
8 + // but we don't know that and conservatively assume it's a render helper
9 + // where it's disallowed to modify globals
10 + return <Foo foo={foo} />;
11 +}
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/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-forget/src/__tests__/fixtures/compiler/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-forget/src/__tests__/fixtures/compiler/error.reassignment-to-global.expect.md
+1 -3
@@ -17,9 +17,7 @@ function Component() {
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 function. 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 -
22 -InvalidReact: Unexpected reassignment of a variable which was defined outside of the function. 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)
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 |
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-allow-global-reassignment-in-effect-indirect.expect.md new
+47
@@ -0,0 +1,47 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import { useEffect, useState } from "react";
6 +
7 +let someGlobal = false;
8 +
9 +function Component() {
10 + const [state, setState] = useState(someGlobal);
11 +
12 + const setGlobal = () => {
13 + // TODO: this should be allowed since setGlobal is only used in an effect
14 + someGlobal = true;
15 + };
16 + useEffect(() => {
17 + setGlobal();
18 + }, []);
19 +
20 + useEffect(() => {
21 + setState(someGlobal);
22 + }, [someGlobal]);
23 +
24 + return <div>{String(state)}</div>;
25 +}
26 +
27 +export const FIXTURE_ENTRYPOINT = {
28 + fn: Component,
29 + params: [{}],
30 +};
31 +
32 +```
33 +
34 +
35 +## Error
36 +
37 +```
38 + 8 | const setGlobal = () => {
39 + 9 | // TODO: this should be allowed since setGlobal is only used in an effect
40 +> 10 | someGlobal = true;
41 + | ^^^^^^^^^^ 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) (10:10)
42 + 11 | };
43 + 12 | useEffect(() => {
44 + 13 | setGlobal();
45 +```
46 +
47 +
\ No newline at end of file
compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/error.todo-allow-global-reassignment-in-effect-indirect.js new
+26
@@ -0,0 +1,26 @@
1 +import { useEffect, useState } from "react";
2 +
3 +let someGlobal = false;
4 +
5 +function Component() {
6 + const [state, setState] = useState(someGlobal);
7 +
8 + const setGlobal = () => {
9 + // TODO: this should be allowed since setGlobal is only used in an effect
10 + someGlobal = true;
11 + };
12 + useEffect(() => {
13 + setGlobal();
14 + }, []);
15 +
16 + useEffect(() => {
17 + setState(someGlobal);
18 + }, [someGlobal]);
19 +
20 + return <div>{String(state)}</div>;
21 +}
22 +
23 +export const FIXTURE_ENTRYPOINT = {
24 + fn: Component,
25 + params: [{}],
26 +};