Skip to content

Commit 3723b07

Browse files
committed
[compiler] Validate against locals being reassigned after render
ghstack-source-id: d689a96e28742d9ae447e65256c918b8d430c7ad Pull Request resolved: #30107
1 parent 363b8e4 commit 3723b07

8 files changed

+215
-129
lines changed

compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts

+3
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ import {
9696
validatePreservedManualMemoization,
9797
validateUseMemo,
9898
} from "../Validation";
99+
import { validateLocalsNotReassignedAfterRender } from "../Validation/ValidateLocalsNotReassignedAfterRender";
99100

100101
export type CompilerPipelineValue =
101102
| { kind: "ast"; name: string; value: CodegenFunction }
@@ -196,6 +197,8 @@ function* runWithEnvironment(
196197
validateNoCapitalizedCalls(hir);
197198
}
198199

200+
validateLocalsNotReassignedAfterRender(hir);
201+
199202
analyseFunctions(hir);
200203
yield log({ kind: "hir", name: "AnalyseFunctions", value: hir });
201204

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import prettyFormat from "pretty-format";
9+
import { CompilerError } from "..";
10+
import {
11+
HIRFunction,
12+
IdentifierId,
13+
Place,
14+
getHookKind,
15+
isUseOperator,
16+
} from "../HIR";
17+
import {
18+
eachInstructionValueOperand,
19+
eachTerminalOperand,
20+
} from "../HIR/visitors";
21+
import { isEffectHook } from "./ValidateMemoizedEffectDependencies";
22+
23+
/**
24+
* Validates that local variables cannot be reassigned after render.
25+
* This prevents a category of bugs in which a closure captures a
26+
* binding from one render but does not update
27+
*/
28+
export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
29+
const contextVariables = new Set<IdentifierId>();
30+
const reassignment = getContextReassignment(fn, contextVariables, false);
31+
if (reassignment !== null) {
32+
CompilerError.throwInvalidJS({
33+
reason:
34+
"This potentially reassigns a local variable after render has completed. Local variables may not be changed after render. ",
35+
description:
36+
reassignment.identifier.name !== null &&
37+
reassignment.identifier.name.kind === "named"
38+
? `Variable \`${reassignment.identifier.name.value}\` cannot be reassigned after render`
39+
: "",
40+
loc: reassignment.loc,
41+
});
42+
}
43+
}
44+
45+
function getContextReassignment(
46+
fn: HIRFunction,
47+
contextVariables: Set<IdentifierId>,
48+
isFunctionExpression: boolean
49+
): Place | null {
50+
const reassigningFunctions = new Map<IdentifierId, Place>();
51+
for (const [, block] of fn.body.blocks) {
52+
for (const instr of block.instructions) {
53+
const { lvalue, value } = instr;
54+
switch (value.kind) {
55+
case "FunctionExpression":
56+
case "ObjectMethod": {
57+
const reassignment = getContextReassignment(
58+
value.loweredFunc.func,
59+
contextVariables,
60+
true
61+
);
62+
if (reassignment !== null) {
63+
reassigningFunctions.set(lvalue.identifier.id, reassignment);
64+
}
65+
for (const operand of eachInstructionValueOperand(value)) {
66+
const reassignment = reassigningFunctions.get(
67+
operand.identifier.id
68+
);
69+
if (reassignment !== undefined) {
70+
reassigningFunctions.set(lvalue.identifier.id, reassignment);
71+
break;
72+
}
73+
}
74+
break;
75+
}
76+
case "StoreLocal": {
77+
const reassignment = reassigningFunctions.get(
78+
value.value.identifier.id
79+
);
80+
if (reassignment !== undefined) {
81+
reassigningFunctions.set(
82+
value.lvalue.place.identifier.id,
83+
reassignment
84+
);
85+
reassigningFunctions.set(lvalue.identifier.id, reassignment);
86+
}
87+
break;
88+
}
89+
case "LoadLocal": {
90+
const reassignment = reassigningFunctions.get(
91+
value.place.identifier.id
92+
);
93+
if (reassignment !== undefined) {
94+
reassigningFunctions.set(lvalue.identifier.id, reassignment);
95+
}
96+
break;
97+
}
98+
case "ArrayExpression":
99+
case "ObjectExpression": {
100+
for (const operand of eachInstructionValueOperand(value)) {
101+
const reassignment = reassigningFunctions.get(
102+
operand.identifier.id
103+
);
104+
if (reassignment !== undefined) {
105+
reassigningFunctions.set(lvalue.identifier.id, reassignment);
106+
break;
107+
}
108+
}
109+
break;
110+
}
111+
case "DeclareContext": {
112+
if (!isFunctionExpression) {
113+
contextVariables.add(value.lvalue.place.identifier.id);
114+
}
115+
break;
116+
}
117+
case "StoreContext": {
118+
if (isFunctionExpression) {
119+
if (contextVariables.has(value.lvalue.place.identifier.id)) {
120+
return value.lvalue.place;
121+
}
122+
} else {
123+
contextVariables.add(value.lvalue.place.identifier.id);
124+
}
125+
break;
126+
}
127+
case "MethodCall":
128+
case "CallExpression": {
129+
const callee =
130+
value.kind === "MethodCall" ? value.property : value.callee;
131+
const isHook =
132+
getHookKind(fn.env, callee.identifier) != null ||
133+
isUseOperator(callee.identifier);
134+
for (const operand of eachInstructionValueOperand(value)) {
135+
const reassignment = reassigningFunctions.get(
136+
operand.identifier.id
137+
);
138+
if (reassignment !== undefined) {
139+
if (isHook) {
140+
return reassignment;
141+
} else {
142+
// reassigningFunctions.set(lvalue.identifier.id, reassignment);
143+
}
144+
}
145+
}
146+
break;
147+
}
148+
case "JsxFragment":
149+
case "JsxExpression": {
150+
for (const operand of eachInstructionValueOperand(value)) {
151+
const reassignment = reassigningFunctions.get(
152+
operand.identifier.id
153+
);
154+
if (reassignment !== undefined) {
155+
reassigningFunctions.set(lvalue.identifier.id, reassignment);
156+
}
157+
}
158+
break;
159+
}
160+
default: {
161+
for (const operand of eachInstructionValueOperand(value)) {
162+
const reassignment = reassigningFunctions.get(
163+
operand.identifier.id
164+
);
165+
if (reassignment !== undefined) {
166+
reassigningFunctions.set(lvalue.identifier.id, reassignment);
167+
break;
168+
}
169+
}
170+
break;
171+
}
172+
}
173+
}
174+
for (const operand of eachTerminalOperand(block.terminal)) {
175+
const reassignment = reassigningFunctions.get(operand.identifier.id);
176+
if (reassignment !== undefined) {
177+
return reassignment;
178+
}
179+
}
180+
}
181+
return null;
182+
}
+10-49
Original file line numberDiff line numberDiff line change
@@ -43,56 +43,17 @@ function Component() {
4343

4444
```
4545

46-
## Code
4746

48-
```javascript
49-
import { c as _c } from "react/compiler-runtime";
50-
import { useEffect } from "react";
51-
52-
function Component() {
53-
const $ = _c(4);
54-
let local;
55-
let t0;
56-
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
57-
t0 = (newValue) => {
58-
local = newValue;
59-
};
60-
$[0] = t0;
61-
} else {
62-
t0 = $[0];
63-
}
64-
const reassignLocal = t0;
65-
let t1;
66-
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
67-
t1 = (newValue_0) => {
68-
reassignLocal("hello");
69-
if (local === newValue_0) {
70-
console.log("`local` was updated!");
71-
} else {
72-
throw new Error("`local` not updated!");
73-
}
74-
};
75-
$[1] = t1;
76-
} else {
77-
t1 = $[1];
78-
}
79-
const onMount = t1;
80-
let t2;
81-
let t3;
82-
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
83-
t2 = () => {
84-
onMount();
85-
};
86-
t3 = [onMount];
87-
$[2] = t2;
88-
$[3] = t3;
89-
} else {
90-
t2 = $[2];
91-
t3 = $[3];
92-
}
93-
useEffect(t2, t3);
94-
return "ok";
95-
}
47+
## Error
9648

9749
```
50+
5 |
51+
6 | const reassignLocal = (newValue) => {
52+
> 7 | local = newValue;
53+
| ^^^^^ InvalidJS: This potentially reassigns a local variable after render has completed. Local variables may not be changed after render. . Variable `local` cannot be reassigned after render (7:7)
54+
8 | };
55+
9 |
56+
10 | const onMount = (newValue) => {
57+
```
58+
9859
Original file line numberDiff line numberDiff line change
@@ -44,53 +44,17 @@ function Component() {
4444

4545
```
4646

47-
## Code
4847

49-
```javascript
50-
import { c as _c } from "react/compiler-runtime";
51-
import { useEffect } from "react";
52-
import { useIdentity } from "shared-runtime";
53-
54-
function Component() {
55-
const $ = _c(3);
56-
let local;
57-
let t0;
58-
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
59-
t0 = (newValue) => {
60-
local = newValue;
61-
};
62-
$[0] = t0;
63-
} else {
64-
t0 = $[0];
65-
}
66-
const reassignLocal = t0;
67-
let t1;
68-
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
69-
t1 = (newValue_0) => {
70-
reassignLocal("hello");
71-
if (local === newValue_0) {
72-
console.log("`local` was updated!");
73-
} else {
74-
throw new Error("`local` not updated!");
75-
}
76-
};
77-
$[1] = t1;
78-
} else {
79-
t1 = $[1];
80-
}
81-
const callback = t1;
82-
let t2;
83-
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
84-
t2 = () => {
85-
callback();
86-
};
87-
$[2] = t2;
88-
} else {
89-
t2 = $[2];
90-
}
91-
useIdentity(t2);
92-
return "ok";
93-
}
48+
## Error
9449

9550
```
51+
6 |
52+
7 | const reassignLocal = (newValue) => {
53+
> 8 | local = newValue;
54+
| ^^^^^ InvalidJS: This potentially reassigns a local variable after render has completed. Local variables may not be changed after render. . Variable `local` cannot be reassigned after render (8:8)
55+
9 | };
56+
10 |
57+
11 | const callback = (newValue) => {
58+
```
59+
9660
+10-34
Original file line numberDiff line numberDiff line change
@@ -37,41 +37,17 @@ function Component() {
3737

3838
```
3939

40-
## Code
4140

42-
```javascript
43-
import { c as _c } from "react/compiler-runtime";
44-
function Component() {
45-
const $ = _c(2);
46-
let local;
47-
let t0;
48-
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
49-
t0 = (newValue) => {
50-
local = newValue;
51-
};
52-
$[0] = t0;
53-
} else {
54-
t0 = $[0];
55-
}
56-
const reassignLocal = t0;
57-
let t1;
58-
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
59-
const onClick = (newValue_0) => {
60-
reassignLocal("hello");
61-
if (local === newValue_0) {
62-
console.log("`local` was updated!");
63-
} else {
64-
throw new Error("`local` not updated!");
65-
}
66-
};
67-
68-
t1 = <button onClick={onClick}>Submit</button>;
69-
$[1] = t1;
70-
} else {
71-
t1 = $[1];
72-
}
73-
return t1;
74-
}
41+
## Error
7542

7643
```
44+
3 |
45+
4 | const reassignLocal = (newValue) => {
46+
> 5 | local = newValue;
47+
| ^^^^^ InvalidJS: This potentially reassigns a local variable after render has completed. Local variables may not be changed after render. . Variable `local` cannot be reassigned after render (5:5)
48+
6 | };
49+
7 |
50+
8 | const onClick = (newValue) => {
51+
```
52+
7753

0 commit comments

Comments
 (0)