Skip to content

Commit ca3d2fd

Browse files
committed
[compiler] Alternate take on ref validation
Some of the false positives we've seen have to do with the need to align our ref validation with our understanding of which functions may be called during render. The new mutability/aliasing model makes this much more explicit, with the ability to create Impure effects which we then throw as errors if they are reachable during render. This means we can now revisit ref validation by just emitting impure effects. That's what this new pass does. It's a bit simpler: it implements the check for `ref.current == null` guarded if blocks. Otherwise it disallows access to `ref.current` specifically. Unlike before, we intentionally allow passing ref objects to functions — we just see a lot of many false positives on disallowing things like `children({ref})` or similar. Open to feedback! This is also still WIP.
1 parent 2b94a7c commit ca3d2fd

14 files changed

+179
-38
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ function runWithEnvironment(
285285
}
286286

287287
if (env.config.validateRefAccessDuringRender) {
288-
validateNoRefAccessInRender(hir).unwrap();
288+
// validateNoRefAccessInRender(hir).unwrap();
289289
}
290290

291291
if (env.config.validateNoSetStateInRender) {

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

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ import {
2828
isMapType,
2929
isPrimitiveType,
3030
isRefOrRefValue,
31+
isRefValueType,
3132
isSetType,
33+
isUseRefType,
3234
makeIdentifierId,
3335
Phi,
3436
Place,
@@ -219,6 +221,9 @@ export function inferMutationAliasingEffects(
219221
}
220222
}
221223
}
224+
if (fn.env.config.validateRefAccessDuringRender) {
225+
inferRefAccessEffects(fn, isFunctionExpression);
226+
}
222227
return Ok(undefined);
223228
}
224229

@@ -2513,3 +2518,127 @@ export type AbstractValue = {
25132518
kind: ValueKind;
25142519
reason: ReadonlySet<ValueReason>;
25152520
};
2521+
2522+
function inferRefAccessEffects(
2523+
fn: HIRFunction,
2524+
_isFunctionExpression: boolean,
2525+
): void {
2526+
const nullish = new Set<IdentifierId>();
2527+
const nullishTest = new Map<IdentifierId, Place>();
2528+
let guard: {ref: IdentifierId; fallthrough: BlockId} | null = null;
2529+
const temporaries: Map<IdentifierId, Place> = new Map();
2530+
2531+
function visitOperand(operand: Place): AliasingEffect | null {
2532+
const nullTestRef = nullishTest.get(operand.identifier.id);
2533+
if (isRefValueType(operand.identifier) || nullTestRef != null) {
2534+
const refOperand = nullTestRef ?? operand;
2535+
return {
2536+
kind: 'Impure',
2537+
error: {
2538+
severity: ErrorSeverity.InvalidReact,
2539+
reason:
2540+
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
2541+
loc: refOperand.loc,
2542+
description:
2543+
refOperand.identifier.name !== null &&
2544+
refOperand.identifier.name.kind === 'named'
2545+
? `Cannot access ref value \`${refOperand.identifier.name.value}\``
2546+
: null,
2547+
suggestions: null,
2548+
},
2549+
place: refOperand,
2550+
};
2551+
}
2552+
return null;
2553+
}
2554+
2555+
for (const block of fn.body.blocks.values()) {
2556+
if (guard !== null && guard.fallthrough === block.id) {
2557+
guard = null;
2558+
}
2559+
for (const instr of block.instructions) {
2560+
const {lvalue, value} = instr;
2561+
if (value.kind === 'LoadLocal' && isUseRefType(value.place.identifier)) {
2562+
temporaries.set(lvalue.identifier.id, value.place);
2563+
} else if (
2564+
value.kind === 'StoreLocal' &&
2565+
isUseRefType(value.value.identifier)
2566+
) {
2567+
temporaries.set(value.lvalue.place.identifier.id, value.value);
2568+
temporaries.set(lvalue.identifier.id, value.value);
2569+
} else if (
2570+
value.kind === 'BinaryExpression' &&
2571+
((isRefValueType(value.left.identifier) &&
2572+
nullish.has(value.right.identifier.id)) ||
2573+
(nullish.has(value.left.identifier.id) &&
2574+
isRefValueType(value.right.identifier)))
2575+
) {
2576+
const refOperand = isRefValueType(value.left.identifier)
2577+
? value.left
2578+
: value.right;
2579+
const operand = temporaries.get(refOperand.identifier.id) ?? refOperand;
2580+
nullishTest.set(lvalue.identifier.id, operand);
2581+
} else if (value.kind === 'Primitive' && value.value == null) {
2582+
nullish.add(lvalue.identifier.id);
2583+
} else if (
2584+
value.kind === 'PropertyLoad' &&
2585+
isUseRefType(value.object.identifier) &&
2586+
value.property === 'current'
2587+
) {
2588+
const refOperand =
2589+
temporaries.get(value.object.identifier.id) ?? value.object;
2590+
temporaries.set(lvalue.identifier.id, refOperand);
2591+
} else if (
2592+
value.kind === 'PropertyStore' &&
2593+
value.property === 'current' &&
2594+
isUseRefType(value.object.identifier)
2595+
) {
2596+
const refOperand =
2597+
temporaries.get(value.object.identifier.id) ?? value.object;
2598+
if (guard != null && refOperand.identifier.id === guard.ref) {
2599+
// Allow a single write within the guard
2600+
guard = null;
2601+
} else {
2602+
instr.effects ??= [];
2603+
instr.effects.push({
2604+
kind: 'Impure',
2605+
error: {
2606+
severity: ErrorSeverity.InvalidReact,
2607+
reason:
2608+
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
2609+
loc: value.loc,
2610+
description:
2611+
value.object.identifier.name !== null &&
2612+
value.object.identifier.name.kind === 'named'
2613+
? `Cannot access ref value \`${value.object.identifier.name.value}\``
2614+
: null,
2615+
suggestions: null,
2616+
},
2617+
place: value.object,
2618+
});
2619+
}
2620+
} else {
2621+
for (const operand of eachInstructionValueOperand(value)) {
2622+
const error = visitOperand(operand);
2623+
if (error) {
2624+
instr.effects ??= [];
2625+
instr.effects.push(error);
2626+
}
2627+
}
2628+
}
2629+
}
2630+
if (
2631+
guard == null &&
2632+
block.terminal.kind === 'if' &&
2633+
nullishTest.has(block.terminal.test.identifier.id)
2634+
) {
2635+
const ref = nullishTest.get(block.terminal.test.identifier.id)!;
2636+
guard = {ref: ref.identifier.id, fallthrough: block.terminal.fallthrough};
2637+
} else {
2638+
for (const operand of eachTerminalOperand(block.terminal)) {
2639+
const _effect = visitOperand(operand);
2640+
// TODO: need a place to store terminal effects generically
2641+
}
2642+
}
2643+
}
2644+
}

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.expect.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ export const FIXTURE_ENTRYPOINT = {
2424
4 | const ref = useRef();
2525
> 5 | useEffect(() => {}, [ref.current]);
2626
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
27-
28-
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
2927
6 | }
3028
7 |
3129
8 | export const FIXTURE_ENTRYPOINT = {

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-during-render.expect.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ function Component(props) {
1919
3 | const ref = useRef(null);
2020
> 4 | const value = ref.current;
2121
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4)
22+
23+
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `value` (5:5)
2224
5 | return value;
2325
6 | }
2426
7 |

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,17 @@ function Component(props) {
1919
## Error
2020

2121
```
22-
7 | return <Foo item={item} current={current} />;
23-
8 | };
24-
> 9 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
25-
| ^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (9:9)
26-
10 | }
27-
11 |
22+
4 | const renderItem = item => {
23+
5 | const aliasedRef = ref;
24+
> 6 | const current = aliasedRef.current;
25+
| ^^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (6:6)
26+
27+
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `current` (7:7)
28+
29+
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (7:7)
30+
7 | return <Foo item={item} current={current} />;
31+
8 | };
32+
9 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
2833
```
2934
3035

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-refs-in-render-transitive.expect.md

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,13 @@ function Component() {
2121
## Error
2222

2323
```
24-
7 | };
25-
8 | const changeRef = setRef;
26-
> 9 | changeRef();
27-
| ^^^^^^^^^ InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (9:9)
28-
29-
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (9:9)
30-
10 |
31-
11 | return <button ref={ref} />;
32-
12 | }
24+
4 |
25+
5 | const setRef = () => {
26+
> 6 | ref.current = false;
27+
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (6:6)
28+
7 | };
29+
8 | const changeRef = setRef;
30+
9 | changeRef();
3331
```
3432
3533

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-destructure.expect.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ function Component({ref}) {
1818
2 | function Component({ref}) {
1919
> 3 | const value = ref.current;
2020
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (3:3)
21+
22+
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `value` (4:4)
23+
24+
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4)
2125
4 | return <div>{value}</div>;
2226
5 | }
2327
6 |

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-read-ref-prop-in-render-property-load.expect.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ function Component(props) {
1818
2 | function Component(props) {
1919
> 3 | const value = props.ref.current;
2020
| ^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (3:3)
21+
22+
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `value` (4:4)
23+
24+
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4)
2125
4 | return <div>{value}</div>;
2226
5 | }
2327
6 |

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-in-callback-invoked-during-render.expect.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,17 @@ function Component(props) {
1818
## Error
1919

2020
```
21-
6 | return <Foo item={item} current={current} />;
22-
7 | };
23-
> 8 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
24-
| ^^^^^^^^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (8:8)
25-
9 | }
26-
10 |
21+
3 | const ref = useRef(null);
22+
4 | const renderItem = item => {
23+
> 5 | const current = ref.current;
24+
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
25+
26+
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `current` (6:6)
27+
28+
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (6:6)
29+
6 | return <Foo item={item} current={current} />;
30+
7 | };
31+
8 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
2732
```
2833
2934

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-set-and-read-ref-during-render.expect.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ function Component(props) {
1919
3 | const ref = useRef(null);
2020
> 4 | ref.current = props.value;
2121
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4)
22-
23-
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
2422
5 | return ref.current;
2523
6 | }
2624
7 |

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-initialization-nonif.expect.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ export const FIXTURE_ENTRYPOINT = {
2727
4 | component C() {
2828
5 | const r = useRef(null);
2929
> 6 | const guard = r.current == null;
30-
| ^^^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (6:6)
30+
| ^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `r` (6:6)
3131
32-
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef). Cannot access ref value `guard` (7:7)
32+
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (8:8)
3333
7 | if (guard) {
3434
8 | r.current = 1;
3535
9 | }

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.useCallback-accesses-ref-mutated-later-via-function-preserve-memoization.expect.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,13 @@ export const FIXTURE_ENTRYPOINT = {
3434
## Error
3535

3636
```
37-
15 | ref.current.inner = null;
37+
13 | // The ref is modified later, extending its range and preventing memoization of onChange
38+
14 | const reset = () => {
39+
> 15 | ref.current.inner = null;
40+
| ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (15:15)
3841
16 | };
39-
> 17 | reset();
40-
| ^^^^^ InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) (17:17)
41-
42-
InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (17:17)
42+
17 | reset();
4343
18 |
44-
19 | return <input onChange={onChange} />;
45-
20 | }
4644
```
4745
4846

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-current-aliased-no-added-to-dep.expect.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
## Input
33

44
```javascript
5-
// @validateRefAccessDuringRender false
5+
// @validateRefAccessDuringRender:false
66
function VideoTab() {
77
const ref = useRef();
88
const t = ref.current;
@@ -18,7 +18,7 @@ function VideoTab() {
1818
## Code
1919

2020
```javascript
21-
import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender false
21+
import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender:false
2222
function VideoTab() {
2323
const $ = _c(1);
2424
const ref = useRef();

compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-current-aliased-no-added-to-dep.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// @validateRefAccessDuringRender false
1+
// @validateRefAccessDuringRender:false
22
function VideoTab() {
33
const ref = useRef();
44
const t = ref.current;

0 commit comments

Comments
 (0)