Skip to content

Commit 65b25f2

Browse files
committed
[compiler] More complete validation against locals being reassigned after render
Summary: This diff extends the existing work on validating against locals being reassigned after render, by propagating the reassignment "effect" into the lvalues of instructions when the rvalue operands include values known to cause reassignments. In particular, this "closes the loop" for function definitions and function calls: a function that returns a function that reassigns will be considered to also perform reassignments, but previous to this we didn't consider the result of a `Call` of a function that reassigns to itself be a value that reassigns. This causes a number of new bailouts in test cases, all of which appear to me to be legit. ghstack-source-id: b3fe541 Pull Request resolved: #30540
1 parent 96f3093 commit 65b25f2

16 files changed

+179
-328
lines changed

compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateLocalsNotReassignedAfterRender.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import {CompilerError, Effect} from '..';
99
import {HIRFunction, IdentifierId, Place} from '../HIR';
1010
import {
11+
eachInstructionLValue,
1112
eachInstructionValueOperand,
1213
eachTerminalOperand,
1314
} from '../HIR/visitors';
@@ -139,15 +140,18 @@ function getContextReassignment(
139140
const reassignment = reassigningFunctions.get(
140141
operand.identifier.id,
141142
);
142-
if (
143-
reassignment !== undefined &&
144-
operand.effect === Effect.Freeze
145-
) {
143+
if (reassignment !== undefined) {
146144
/*
147145
* Functions that reassign local variables are inherently mutable and are unsafe to pass
148146
* to a place that expects a frozen value. Propagate the reassignment upward.
149147
*/
150-
return reassignment;
148+
if (operand.effect === Effect.Freeze) {
149+
return reassignment;
150+
} else {
151+
for (const lval of eachInstructionLValue(instr)) {
152+
reassigningFunctions.set(lval.identifier.id, reassignment);
153+
}
154+
}
151155
}
152156
}
153157
break;

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/context-variable-only-chained-assign.expect.md

Lines changed: 0 additions & 65 deletions
This file was deleted.

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/declare-reassign-variable-in-function-declaration.expect.md

Lines changed: 0 additions & 40 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
2+
## Input
3+
4+
```javascript
5+
import {identity, invoke} from 'shared-runtime';
6+
7+
function foo() {
8+
let x = 2;
9+
const fn1 = () => {
10+
const copy1 = (x = 3);
11+
return identity(copy1);
12+
};
13+
const fn2 = () => {
14+
const copy2 = (x = 4);
15+
return [invoke(fn1), copy2, identity(copy2)];
16+
};
17+
return invoke(fn2);
18+
}
19+
20+
export const FIXTURE_ENTRYPOINT = {
21+
fn: foo,
22+
params: [],
23+
};
24+
25+
```
26+
27+
28+
## Error
29+
30+
```
31+
8 | };
32+
9 | const fn2 = () => {
33+
> 10 | const copy2 = (x = 4);
34+
| ^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `x` cannot be reassigned after render (10:10)
35+
11 | return [invoke(fn1), copy2, identity(copy2)];
36+
12 | };
37+
13 | return invoke(fn2);
38+
```
39+
40+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
2+
## Input
3+
4+
```javascript
5+
function Component() {
6+
let x = null;
7+
function foo() {
8+
x = 9;
9+
}
10+
const y = bar(foo);
11+
return <Child y={y} />;
12+
}
13+
14+
```
15+
16+
17+
## Error
18+
19+
```
20+
2 | let x = null;
21+
3 | function foo() {
22+
> 4 | x = 9;
23+
| ^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `x` cannot be reassigned after render (4:4)
24+
5 | }
25+
6 | const y = bar(foo);
26+
7 | return <Child y={y} />;
27+
```
28+
29+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
2+
## Input
3+
4+
```javascript
5+
import {useEffect, useState} from 'react';
6+
import {mutate} from 'shared-runtime';
7+
8+
function Component(props) {
9+
const x = [{...props.value}];
10+
useEffect(() => {}, []);
11+
const onClick = () => {
12+
console.log(x.length);
13+
};
14+
let y;
15+
return (
16+
<div onClick={onClick}>
17+
{x.map(item => {
18+
y = item;
19+
return <span key={item.id}>{item.text}</span>;
20+
})}
21+
{mutate(y)}
22+
</div>
23+
);
24+
}
25+
26+
export const FIXTURE_ENTRYPOINT = {
27+
fn: Component,
28+
params: [{value: {id: 0, text: 'Hello!'}}],
29+
isComponent: true,
30+
};
31+
32+
```
33+
34+
35+
## Error
36+
37+
```
38+
12 | <div onClick={onClick}>
39+
13 | {x.map(item => {
40+
> 14 | y = item;
41+
| ^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `y` cannot be reassigned after render (14:14)
42+
15 | return <span key={item.id}>{item.text}</span>;
43+
16 | })}
44+
17 | {mutate(y)}
45+
```
46+
47+
Lines changed: 10 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -42,60 +42,17 @@ function Component() {
4242

4343
```
4444

45-
## Code
4645

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

10048
```
49+
5 | // Create the reassignment function inside another function, then return it
50+
6 | const reassignLocal = newValue => {
51+
> 7 | local = newValue;
52+
| ^^^^^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `local` cannot be reassigned after render (7:7)
53+
8 | };
54+
9 | return reassignLocal;
55+
10 | };
56+
```
57+
10158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
2+
## Input
3+
4+
```javascript
5+
// @enableAssumeHooksFollowRulesOfReact @enableTransitivelyFreezeFunctionExpressions
6+
let cond = true;
7+
function Component(props) {
8+
let a;
9+
let b;
10+
const f = () => {
11+
if (cond) {
12+
a = {};
13+
b = [];
14+
} else {
15+
a = {};
16+
b = [];
17+
}
18+
a.property = true;
19+
b.push(false);
20+
};
21+
return <div onClick={f()} />;
22+
}
23+
24+
export const FIXTURE_ENTRYPOINT = {
25+
fn: Component,
26+
params: [{}],
27+
};
28+
29+
```
30+
31+
32+
## Error
33+
34+
```
35+
6 | const f = () => {
36+
7 | if (cond) {
37+
> 8 | a = {};
38+
| ^ InvalidReact: Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead. Variable `a` cannot be reassigned after render (8:8)
39+
9 | b = [];
40+
10 | } else {
41+
11 | a = {};
42+
```
43+
44+

0 commit comments

Comments
 (0)