@samitouri / QOS-React-2 / commits / fcfbfc1d1e

[compiler] Always error on async reassignments

Summary: Addresses the issue in #30109: any mutation of a local in an async function may occur after rendering has finished. ghstack-source-id: 9f15cf0f144c0badd6009ceb51df43a50399d82b Pull Request resolved: https://github.com/facebook/react/pull/30111

Mike Vitousek committed Jun 26, 2024 at 17:07 UTC fcfbfc1d1e3e350e1ea3fbc4a2418f647dc28a31
5 files changed +59 -67
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts
+22 -3
@@ -19,7 +19,12 @@ import {
19 */
20 export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
21 const contextVariables = new Set<IdentifierId>();
22 - const reassignment = getContextReassignment(fn, contextVariables, false);
22 + const reassignment = getContextReassignment(
23 + fn,
24 + contextVariables,
25 + false,
26 + false
27 + );
28 if (reassignment !== null) {
29 CompilerError.throwInvalidReact({
30 reason:
@@ -37,7 +42,8 @@ export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
42 function getContextReassignment(
43 fn: HIRFunction,
44 contextVariables: Set<IdentifierId>,
40 - isFunctionExpression: boolean
45 + isFunctionExpression: boolean,
46 + isAsync: boolean
47 ): Place | null {
48 const reassigningFunctions = new Map<IdentifierId, Place>();
49 for (const [, block] of fn.body.blocks) {
@@ -49,7 +55,8 @@ function getContextReassignment(
55 let reassignment = getContextReassignment(
56 value.loweredFunc.func,
57 contextVariables,
52 - true
58 + true,
59 + isAsync || value.loweredFunc.func.async
60 );
61 if (reassignment === null) {
62 // If the function itself doesn't reassign, does one of its dependencies?
@@ -65,6 +72,18 @@ function getContextReassignment(
72 }
73 // if the function or its depends reassign, propagate that fact on the lvalue
74 if (reassignment !== null) {
75 + if (isAsync || value.loweredFunc.func.async) {
76 + CompilerError.throwInvalidReact({
77 + reason:
78 + "Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead",
79 + description:
80 + reassignment.identifier.name !== null &&
81 + reassignment.identifier.name.kind === "named"
82 + ? `Variable \`${reassignment.identifier.name.value}\` cannot be reassigned after render`
83 + : "",
84 + loc: reassignment.loc,
85 + });
86 + }
87 reassigningFunctions.set(lvalue.identifier.id, reassignment);
88 }
89 break;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-async-callback.expect.md new
+37
@@ -0,0 +1,37 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component() {
6 + let value = null;
7 + const reassign = async () => {
8 + await foo().then((result) => {
9 + // Reassigning a local variable in an async function is *always* mutating
10 + // after render, so this should error regardless of where this ends up
11 + // getting called
12 + value = result;
13 + });
14 + };
15 +
16 + const onClick = async () => {
17 + await reassign();
18 + };
19 + return <div onClick={onClick}>Click</div>;
20 +}
21 +
22 +```
23 +
24 +
25 +## Error
26 +
27 +```
28 + 6 | // after render, so this should error regardless of where this ends up
29 + 7 | // getting called
30 +> 8 | value = result;
31 + | ^^^^^ InvalidReact: Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `value` cannot be reassigned after render (8:8)
32 + 9 | });
33 + 10 | };
34 + 11 |
35 +```
36 +
37 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-reassign-local-variable-in-async-callback.js renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo.invalid-reassign-local-variable-in-async-callback.expect.md deleted
-58
@@ -1,58 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -function Component() {
6 - let value = null;
7 - const reassign = async () => {
8 - await foo().then((result) => {
9 - // Reassigning a local variable in an async function is *always* mutating
10 - // after render, so this should error regardless of where this ends up
11 - // getting called
12 - value = result;
13 - });
14 - };
15 -
16 - const onClick = async () => {
17 - await reassign();
18 - };
19 - return <div onClick={onClick}>Click</div>;
20 -}
21 -
22 -```
23 -
24 -## Code
25 -
26 -```javascript
27 -import { c as _c } from "react/compiler-runtime";
28 -function Component() {
29 - const $ = _c(2);
30 - let value;
31 - if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
32 - value = null;
33 - $[0] = value;
34 - } else {
35 - value = $[0];
36 - }
37 - let t0;
38 - if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
39 - const reassign = async () => {
40 - await foo().then((result) => {
41 - value = result;
42 - });
43 - };
44 -
45 - const onClick = async () => {
46 - await reassign();
47 - };
48 -
49 - t0 = <div onClick={onClick}>Click</div>;
50 - $[1] = t0;
51 - } else {
52 - t0 = $[1];
53 - }
54 - return t0;
55 -}
56 -
57 -```
58 -
\ No newline at end of file
compiler/packages/snap/src/SproutTodoFilter.ts
-6
@@ -483,12 +483,6 @@ const skipFilter = new Set([
483 "rules-of-hooks/rules-of-hooks-93dc5d5e538a",
484 "rules-of-hooks/rules-of-hooks-69521d94fa03",
485
486 - // should error
487 - "todo.invalid-reassign-local-variable-in-jsx-callback",
488 - "todo.invalid-reassign-local-variable-in-hook-argument",
489 - "todo.invalid-reassign-local-variable-in-effect",
490 - "todo.invalid-reassign-local-variable-in-async-callback",
491 -
486 // bugs
487 "bug-invalid-hoisting-functionexpr",
488 "original-reactive-scopes-fork/bug-nonmutating-capture-in-unsplittable-memo-block",