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
+};