Skip to content

Commit 6537652

Browse files
committed
[compiler] Allow global mutation effects in arguments passed to hooks and in return values
ghstack-source-id: f9ea675 Pull Request resolved: #30576
1 parent 06d0b89 commit 6537652

File tree

7 files changed

+182
-9
lines changed

7 files changed

+182
-9
lines changed

compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
Type,
2727
ValueKind,
2828
ValueReason,
29+
getHookKind,
2930
isArrayType,
3031
isMutableEffect,
3132
isObjectType,
@@ -48,7 +49,6 @@ import {
4849
eachTerminalSuccessor,
4950
} from '../HIR/visitors';
5051
import {assertExhaustive} from '../Utils/utils';
51-
import {isEffectHook} from '../Validation/ValidateMemoizedEffectDependencies';
5252

5353
const UndefinedValue: InstructionValue = {
5454
kind: 'Primitive',
@@ -1151,7 +1151,7 @@ function inferBlock(
11511151
);
11521152
functionEffects.push(
11531153
...propEffects.filter(
1154-
propEffect => propEffect.kind !== 'GlobalMutation',
1154+
effect => !isEffectSafeOutsideRender(effect),
11551155
),
11561156
);
11571157
}
@@ -1330,7 +1330,7 @@ function inferBlock(
13301330
context: new Set(),
13311331
};
13321332
let hasCaptureArgument = false;
1333-
let isUseEffect = isEffectHook(instrValue.callee.identifier);
1333+
let isHook = getHookKind(env, instrValue.callee.identifier) != null;
13341334
for (let i = 0; i < instrValue.args.length; i++) {
13351335
const argumentEffects: Array<FunctionEffect> = [];
13361336
const arg = instrValue.args[i];
@@ -1356,8 +1356,7 @@ function inferBlock(
13561356
*/
13571357
functionEffects.push(
13581358
...argumentEffects.filter(
1359-
argEffect =>
1360-
!isUseEffect || i !== 0 || argEffect.kind !== 'GlobalMutation',
1359+
argEffect => !isHook || !isEffectSafeOutsideRender(argEffect),
13611360
),
13621361
);
13631362
hasCaptureArgument ||= place.effect === Effect.Capture;
@@ -1455,7 +1454,7 @@ function inferBlock(
14551454
const effects =
14561455
signature !== null ? getFunctionEffects(instrValue, signature) : null;
14571456
let hasCaptureArgument = false;
1458-
let isUseEffect = isEffectHook(instrValue.property.identifier);
1457+
let isHook = getHookKind(env, instrValue.property.identifier) != null;
14591458
for (let i = 0; i < instrValue.args.length; i++) {
14601459
const argumentEffects: Array<FunctionEffect> = [];
14611460
const arg = instrValue.args[i];
@@ -1485,8 +1484,7 @@ function inferBlock(
14851484
*/
14861485
functionEffects.push(
14871486
...argumentEffects.filter(
1488-
argEffect =>
1489-
!isUseEffect || i !== 0 || argEffect.kind !== 'GlobalMutation',
1487+
argEffect => !isHook || !isEffectSafeOutsideRender(argEffect),
14901488
),
14911489
);
14921490
hasCaptureArgument ||= place.effect === Effect.Capture;
@@ -2010,11 +2008,15 @@ function inferBlock(
20102008
} else {
20112009
effect = Effect.Read;
20122010
}
2011+
const propEffects: Array<FunctionEffect> = [];
20132012
state.referenceAndRecordEffects(
20142013
operand,
20152014
effect,
20162015
ValueReason.Other,
2017-
functionEffects,
2016+
propEffects,
2017+
);
2018+
functionEffects.push(
2019+
...propEffects.filter(effect => !isEffectSafeOutsideRender(effect)),
20182020
);
20192021
}
20202022
}
@@ -2128,6 +2130,10 @@ function areArgumentsImmutableAndNonMutating(
21282130
return true;
21292131
}
21302132

2133+
function isEffectSafeOutsideRender(effect: FunctionEffect): boolean {
2134+
return effect.kind === 'GlobalMutation';
2135+
}
2136+
21312137
function getWriteErrorReason(abstractValue: AbstractValue): string {
21322138
if (abstractValue.reason.has(ValueReason.Global)) {
21332139
return 'Writing to a variable defined outside a component or hook is not allowed. Consider using an effect';
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
## Input
3+
4+
```javascript
5+
let b = 1;
6+
7+
export default function MyApp() {
8+
const fn = () => {
9+
b = 2;
10+
};
11+
return foo(fn);
12+
}
13+
14+
function foo(fn) {}
15+
16+
export const FIXTURE_ENTRYPOINT = {
17+
fn: MyApp,
18+
params: [],
19+
};
20+
21+
```
22+
23+
24+
## Error
25+
26+
```
27+
3 | export default function MyApp() {
28+
4 | const fn = () => {
29+
> 5 | b = 2;
30+
| ^ 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) (5:5)
31+
6 | };
32+
7 | return foo(fn);
33+
8 | }
34+
```
35+
36+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
let b = 1;
2+
3+
export default function MyApp() {
4+
const fn = () => {
5+
b = 2;
6+
};
7+
return foo(fn);
8+
}
9+
10+
function foo(fn) {}
11+
12+
export const FIXTURE_ENTRYPOINT = {
13+
fn: MyApp,
14+
params: [],
15+
};
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
2+
## Input
3+
4+
```javascript
5+
let b = 1;
6+
7+
export default function MyApp() {
8+
const fn = () => {
9+
b = 2;
10+
};
11+
return useFoo(fn);
12+
}
13+
14+
function useFoo(fn) {}
15+
16+
export const FIXTURE_ENTRYPOINT = {
17+
fn: MyApp,
18+
params: [],
19+
};
20+
21+
```
22+
23+
## Code
24+
25+
```javascript
26+
let b = 1;
27+
28+
export default function MyApp() {
29+
const fn = _temp;
30+
return useFoo(fn);
31+
}
32+
function _temp() {
33+
b = 2;
34+
}
35+
36+
function useFoo(fn) {}
37+
38+
export const FIXTURE_ENTRYPOINT = {
39+
fn: MyApp,
40+
params: [],
41+
};
42+
43+
```
44+
45+
### Eval output
46+
(kind: ok)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
let b = 1;
2+
3+
export default function MyApp() {
4+
const fn = () => {
5+
b = 2;
6+
};
7+
return useFoo(fn);
8+
}
9+
10+
function useFoo(fn) {}
11+
12+
export const FIXTURE_ENTRYPOINT = {
13+
fn: MyApp,
14+
params: [],
15+
};
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
2+
## Input
3+
4+
```javascript
5+
let b = 1;
6+
7+
export default function useMyHook() {
8+
const fn = () => {
9+
b = 2;
10+
};
11+
return fn;
12+
}
13+
14+
export const FIXTURE_ENTRYPOINT = {
15+
fn: useMyHook,
16+
params: [],
17+
};
18+
19+
```
20+
21+
## Code
22+
23+
```javascript
24+
let b = 1;
25+
26+
export default function useMyHook() {
27+
const fn = _temp;
28+
return fn;
29+
}
30+
function _temp() {
31+
b = 2;
32+
}
33+
34+
export const FIXTURE_ENTRYPOINT = {
35+
fn: useMyHook,
36+
params: [],
37+
};
38+
39+
```
40+
41+
### Eval output
42+
(kind: ok) "[[ function params=0 ]]"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
let b = 1;
2+
3+
export default function useMyHook() {
4+
const fn = () => {
5+
b = 2;
6+
};
7+
return fn;
8+
}
9+
10+
export const FIXTURE_ENTRYPOINT = {
11+
fn: useMyHook,
12+
params: [],
13+
};

0 commit comments

Comments
 (0)