[compiler] Track refs through object expressions and property lookups
Summary: This addresses the issue of the compiler being overly restrictive about refs escaping into object expressions. Rather than erroring whenever a ref flows into an object, we will now treat the object itself as a ref, and apply the same escape rules to it. Whenever we look up a property from a ref value, we now don't know whether that value is itself a ref or a ref value, so we assume it's both. The same logic applies to ref-accessing functions--if such a function is stored in an object, we'll propagate that property to the object itself and any properties looked up from it. ghstack-source-id: 5c6fcb895d4a1658ce9dddec286aad3a57a4c9f1 Pull Request resolved: https://github.com/facebook/react/pull/30821
Mike Vitousek committed
Aug 27, 2024 at 10:11 UTC
7771d3a7972cc2483c45fde51b7ec2d926cba097
5 files changed
+273
-128
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccesInRender.ts
+177
-76
@@ -11,12 +11,12 @@ import {
11
IdentifierId,
12
Place,
13
SourceLocation,
14
- isRefOrRefValue,
14
isRefValueType,
15
isUseRefType,
16
} from '../HIR';
17
import {
18
eachInstructionValueOperand,
19
+ eachPatternOperand,
20
eachTerminalOperand,
21
} from '../HIR/visitors';
22
import {Err, Ok, Result} from '../Utils/Result';
@@ -42,58 +42,165 @@ import {isEffectHook} from './ValidateMemoizedEffectDependencies';
42
* In the future we may reject more cases, based on either object names (`fooRef.current` is likely a ref)
43
* or based on property name alone (`foo.current` might be a ref).
44
*/
45
+type State = {
46
+ refs: Set<IdentifierId>;
47
+ refValues: Map<IdentifierId, SourceLocation | null>;
48
+ refAccessingFunctions: Set<IdentifierId>;
49
+};
50
+
51
export function validateNoRefAccessInRender(fn: HIRFunction): void {
46
- const refAccessingFunctions: Set<IdentifierId> = new Set();
47
- validateNoRefAccessInRenderImpl(fn, refAccessingFunctions).unwrap();
52
+ const state = {
53
+ refs: new Set<IdentifierId>(),
54
+ refValues: new Map<IdentifierId, SourceLocation | null>(),
55
+ refAccessingFunctions: new Set<IdentifierId>(),
56
+ };
57
+ validateNoRefAccessInRenderImpl(fn, state).unwrap();
58
}
59
60
function validateNoRefAccessInRenderImpl(
61
fn: HIRFunction,
52
- refAccessingFunctions: Set<IdentifierId>,
62
+ state: State,
63
): Result<void, CompilerError> {
64
+ let place;
65
+ for (const param of fn.params) {
66
+ if (param.kind === 'Identifier') {
67
+ place = param;
68
+ } else {
69
+ place = param.place;
70
+ }
71
+
72
+ if (isRefValueType(place.identifier)) {
73
+ state.refValues.set(place.identifier.id, null);
74
+ }
75
+ if (isUseRefType(place.identifier)) {
76
+ state.refs.add(place.identifier.id);
77
+ }
78
+ }
79
const errors = new CompilerError();
55
- const lookupLocations: Map<IdentifierId, SourceLocation> = new Map();
80
for (const [, block] of fn.body.blocks) {
81
+ for (const phi of block.phis) {
82
+ phi.operands.forEach(operand => {
83
+ if (state.refs.has(operand.id) || isUseRefType(phi.id)) {
84
+ state.refs.add(phi.id.id);
85
+ }
86
+ const refValue = state.refValues.get(operand.id);
87
+ if (refValue !== undefined || isRefValueType(operand)) {
88
+ state.refValues.set(
89
+ phi.id.id,
90
+ refValue ?? state.refValues.get(phi.id.id) ?? null,
91
+ );
92
+ }
93
+ if (state.refAccessingFunctions.has(operand.id)) {
94
+ state.refAccessingFunctions.add(phi.id.id);
95
+ }
96
+ });
97
+ }
98
+
99
for (const instr of block.instructions) {
100
+ for (const operand of eachInstructionValueOperand(instr.value)) {
101
+ if (isRefValueType(operand.identifier)) {
102
+ CompilerError.invariant(state.refValues.has(operand.identifier.id), {
103
+ reason: 'Expected ref value to be in state',
104
+ loc: operand.loc,
105
+ });
106
+ }
107
+ if (isUseRefType(operand.identifier)) {
108
+ CompilerError.invariant(state.refs.has(operand.identifier.id), {
109
+ reason: 'Expected ref to be in state',
110
+ loc: operand.loc,
111
+ });
112
+ }
113
+ }
114
+
115
switch (instr.value.kind) {
116
case 'JsxExpression':
117
case 'JsxFragment': {
118
for (const operand of eachInstructionValueOperand(instr.value)) {
62
- validateNoDirectRefValueAccess(errors, operand, lookupLocations);
119
+ validateNoDirectRefValueAccess(errors, operand, state);
120
}
121
break;
122
}
123
+ case 'ComputedLoad':
124
case 'PropertyLoad': {
125
+ if (typeof instr.value.property !== 'string') {
126
+ validateNoRefValueAccess(errors, state, instr.value.property);
127
+ }
128
if (
68
- isRefValueType(instr.lvalue.identifier) &&
69
- instr.value.property === 'current'
129
+ state.refAccessingFunctions.has(instr.value.object.identifier.id)
130
) {
71
- lookupLocations.set(instr.lvalue.identifier.id, instr.loc);
131
+ state.refAccessingFunctions.add(instr.lvalue.identifier.id);
132
+ }
133
+ if (state.refs.has(instr.value.object.identifier.id)) {
134
+ /*
135
+ * Once an object contains a ref at any level, we treat it as a ref.
136
+ * If we look something up from it, that value may either be a ref
137
+ * or the ref value (or neither), so we conservatively assume it's both.
138
+ */
139
+ state.refs.add(instr.lvalue.identifier.id);
140
+ state.refValues.set(instr.lvalue.identifier.id, instr.loc);
141
}
142
break;
143
}
144
+ case 'LoadContext':
145
case 'LoadLocal': {
76
- if (refAccessingFunctions.has(instr.value.place.identifier.id)) {
77
- refAccessingFunctions.add(instr.lvalue.identifier.id);
146
+ if (
147
+ state.refAccessingFunctions.has(instr.value.place.identifier.id)
148
+ ) {
149
+ state.refAccessingFunctions.add(instr.lvalue.identifier.id);
150
}
79
- if (isRefValueType(instr.lvalue.identifier)) {
80
- const loc = lookupLocations.get(instr.value.place.identifier.id);
81
- if (loc !== undefined) {
82
- lookupLocations.set(instr.lvalue.identifier.id, loc);
83
- }
151
+ const refValue = state.refValues.get(instr.value.place.identifier.id);
152
+ if (refValue !== undefined) {
153
+ state.refValues.set(instr.lvalue.identifier.id, refValue);
154
+ }
155
+ if (state.refs.has(instr.value.place.identifier.id)) {
156
+ state.refs.add(instr.lvalue.identifier.id);
157
}
158
break;
159
}
160
+ case 'StoreContext':
161
case 'StoreLocal': {
88
- if (refAccessingFunctions.has(instr.value.value.identifier.id)) {
89
- refAccessingFunctions.add(instr.value.lvalue.place.identifier.id);
90
- refAccessingFunctions.add(instr.lvalue.identifier.id);
162
+ if (
163
+ state.refAccessingFunctions.has(instr.value.value.identifier.id)
164
+ ) {
165
+ state.refAccessingFunctions.add(
166
+ instr.value.lvalue.place.identifier.id,
167
+ );
168
+ state.refAccessingFunctions.add(instr.lvalue.identifier.id);
169
+ }
170
+ const refValue = state.refValues.get(instr.value.value.identifier.id);
171
+ if (
172
+ refValue !== undefined ||
173
+ isRefValueType(instr.value.lvalue.place.identifier)
174
+ ) {
175
+ state.refValues.set(
176
+ instr.value.lvalue.place.identifier.id,
177
+ refValue ?? null,
178
+ );
179
+ state.refValues.set(instr.lvalue.identifier.id, refValue ?? null);
180
+ }
181
+ if (state.refs.has(instr.value.value.identifier.id)) {
182
+ state.refs.add(instr.value.lvalue.place.identifier.id);
183
+ state.refs.add(instr.lvalue.identifier.id);
184
}
92
- if (isRefValueType(instr.value.lvalue.place.identifier)) {
93
- const loc = lookupLocations.get(instr.value.value.identifier.id);
94
- if (loc !== undefined) {
95
- lookupLocations.set(instr.value.lvalue.place.identifier.id, loc);
96
- lookupLocations.set(instr.lvalue.identifier.id, loc);
185
+ break;
186
+ }
187
+ case 'Destructure': {
188
+ const destructuredFunction = state.refAccessingFunctions.has(
189
+ instr.value.value.identifier.id,
190
+ );
191
+ const destructuredRef = state.refs.has(
192
+ instr.value.value.identifier.id,
193
+ );
194
+ for (const lval of eachPatternOperand(instr.value.lvalue.pattern)) {
195
+ if (isUseRefType(lval.identifier)) {
196
+ state.refs.add(lval.identifier.id);
197
+ }
198
+ if (destructuredRef || isRefValueType(lval.identifier)) {
199
+ state.refs.add(lval.identifier.id);
200
+ state.refValues.set(lval.identifier.id, null);
201
+ }
202
+ if (destructuredFunction) {
203
+ state.refAccessingFunctions.add(lval.identifier.id);
204
}
205
}
206
break;
@@ -107,32 +214,27 @@ function validateNoRefAccessInRenderImpl(
214
*/
215
[...eachInstructionValueOperand(instr.value)].some(
216
operand =>
110
- isRefValueType(operand.identifier) ||
111
- refAccessingFunctions.has(operand.identifier.id),
217
+ state.refValues.has(operand.identifier.id) ||
218
+ state.refAccessingFunctions.has(operand.identifier.id),
219
) ||
220
// check for cases where .current is accessed through an aliased ref
221
([...eachInstructionValueOperand(instr.value)].some(operand =>
115
- isUseRefType(operand.identifier),
222
+ state.refs.has(operand.identifier.id),
223
) &&
224
validateNoRefAccessInRenderImpl(
225
instr.value.loweredFunc.func,
119
- refAccessingFunctions,
226
+ state,
227
).isErr())
228
) {
229
// This function expression unconditionally accesses a ref
123
- refAccessingFunctions.add(instr.lvalue.identifier.id);
230
+ state.refAccessingFunctions.add(instr.lvalue.identifier.id);
231
}
232
break;
233
}
234
case 'MethodCall': {
235
if (!isEffectHook(instr.value.property.identifier)) {
236
for (const operand of eachInstructionValueOperand(instr.value)) {
130
- validateNoRefAccess(
131
- errors,
132
- refAccessingFunctions,
133
- operand,
134
- operand.loc,
135
- );
237
+ validateNoRefAccess(errors, state, operand, operand.loc);
238
}
239
}
240
break;
@@ -142,7 +244,7 @@ function validateNoRefAccessInRenderImpl(
244
const isUseEffect = isEffectHook(callee.identifier);
245
if (!isUseEffect) {
246
// Report a more precise error when calling a local function that accesses a ref
145
- if (refAccessingFunctions.has(callee.identifier.id)) {
247
+ if (state.refAccessingFunctions.has(callee.identifier.id)) {
248
errors.push({
249
severity: ErrorSeverity.InvalidReact,
250
reason:
@@ -159,9 +261,9 @@ function validateNoRefAccessInRenderImpl(
261
for (const operand of eachInstructionValueOperand(instr.value)) {
262
validateNoRefAccess(
263
errors,
162
- refAccessingFunctions,
264
+ state,
265
operand,
164
- lookupLocations.get(operand.identifier.id) ?? operand.loc,
266
+ state.refValues.get(operand.identifier.id) ?? operand.loc,
267
);
268
}
269
}
@@ -170,12 +272,17 @@ function validateNoRefAccessInRenderImpl(
272
case 'ObjectExpression':
273
case 'ArrayExpression': {
274
for (const operand of eachInstructionValueOperand(instr.value)) {
173
- validateNoRefAccess(
174
- errors,
175
- refAccessingFunctions,
176
- operand,
177
- lookupLocations.get(operand.identifier.id) ?? operand.loc,
178
- );
275
+ validateNoDirectRefValueAccess(errors, operand, state);
276
+ if (state.refAccessingFunctions.has(operand.identifier.id)) {
277
+ state.refAccessingFunctions.add(instr.lvalue.identifier.id);
278
+ }
279
+ if (state.refs.has(operand.identifier.id)) {
280
+ state.refs.add(instr.lvalue.identifier.id);
281
+ }
282
+ const refValue = state.refValues.get(operand.identifier.id);
283
+ if (refValue !== undefined) {
284
+ state.refValues.set(instr.lvalue.identifier.id, refValue);
285
+ }
286
}
287
break;
288
}
@@ -185,20 +292,15 @@ function validateNoRefAccessInRenderImpl(
292
case 'ComputedStore': {
293
validateNoRefAccess(
294
errors,
188
- refAccessingFunctions,
295
+ state,
296
instr.value.object,
190
- lookupLocations.get(instr.value.object.identifier.id) ?? instr.loc,
297
+ state.refValues.get(instr.value.object.identifier.id) ?? instr.loc,
298
);
299
for (const operand of eachInstructionValueOperand(instr.value)) {
300
if (operand === instr.value.object) {
301
continue;
302
}
196
- validateNoRefValueAccess(
197
- errors,
198
- refAccessingFunctions,
199
- lookupLocations,
200
- operand,
201
- );
303
+ validateNoRefValueAccess(errors, state, operand);
304
}
305
break;
306
}
@@ -207,28 +309,27 @@ function validateNoRefAccessInRenderImpl(
309
break;
310
default: {
311
for (const operand of eachInstructionValueOperand(instr.value)) {
210
- validateNoRefValueAccess(
211
- errors,
212
- refAccessingFunctions,
213
- lookupLocations,
214
- operand,
215
- );
312
+ validateNoRefValueAccess(errors, state, operand);
313
}
314
break;
315
}
316
}
317
+ if (isUseRefType(instr.lvalue.identifier)) {
318
+ state.refs.add(instr.lvalue.identifier.id);
319
+ }
320
+ if (
321
+ isRefValueType(instr.lvalue.identifier) &&
322
+ !state.refValues.has(instr.lvalue.identifier.id)
323
+ ) {
324
+ state.refValues.set(instr.lvalue.identifier.id, instr.loc);
325
+ }
326
}
327
for (const operand of eachTerminalOperand(block.terminal)) {
328
if (block.terminal.kind !== 'return') {
223
- validateNoRefValueAccess(
224
- errors,
225
- refAccessingFunctions,
226
- lookupLocations,
227
- operand,
228
- );
329
+ validateNoRefValueAccess(errors, state, operand);
330
} else {
331
// Allow functions containing refs to be returned, but not direct ref values
231
- validateNoDirectRefValueAccess(errors, operand, lookupLocations);
332
+ validateNoDirectRefValueAccess(errors, operand, state);
333
}
334
}
335
}
@@ -242,19 +343,18 @@ function validateNoRefAccessInRenderImpl(
343
344
function validateNoRefValueAccess(
345
errors: CompilerError,
245
- refAccessingFunctions: Set<IdentifierId>,
246
- lookupLocations: Map<IdentifierId, SourceLocation>,
346
+ state: State,
347
operand: Place,
348
): void {
349
if (
250
- isRefValueType(operand.identifier) ||
251
- refAccessingFunctions.has(operand.identifier.id)
350
+ state.refValues.has(operand.identifier.id) ||
351
+ state.refAccessingFunctions.has(operand.identifier.id)
352
) {
353
errors.push({
354
severity: ErrorSeverity.InvalidReact,
355
reason:
356
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
257
- loc: lookupLocations.get(operand.identifier.id) ?? operand.loc,
357
+ loc: state.refValues.get(operand.identifier.id) ?? operand.loc,
358
description:
359
operand.identifier.name !== null &&
360
operand.identifier.name.kind === 'named'
@@ -267,13 +367,14 @@ function validateNoRefValueAccess(
367
368
function validateNoRefAccess(
369
errors: CompilerError,
270
- refAccessingFunctions: Set<IdentifierId>,
370
+ state: State,
371
operand: Place,
372
loc: SourceLocation,
373
): void {
374
if (
275
- isRefOrRefValue(operand.identifier) ||
276
- refAccessingFunctions.has(operand.identifier.id)
375
+ state.refs.has(operand.identifier.id) ||
376
+ state.refValues.has(operand.identifier.id) ||
377
+ state.refAccessingFunctions.has(operand.identifier.id)
378
) {
379
errors.push({
380
severity: ErrorSeverity.InvalidReact,
@@ -293,14 +394,14 @@ function validateNoRefAccess(
394
function validateNoDirectRefValueAccess(
395
errors: CompilerError,
396
operand: Place,
296
- lookupLocations: Map<IdentifierId, SourceLocation>,
397
+ state: State,
398
): void {
298
- if (isRefValueType(operand.identifier)) {
399
+ if (state.refValues.has(operand.identifier.id)) {
400
errors.push({
401
severity: ErrorSeverity.InvalidReact,
402
reason:
403
'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)',
303
- loc: lookupLocations.get(operand.identifier.id) ?? operand.loc,
404
+ loc: state.refValues.get(operand.identifier.id) ?? operand.loc,
405
description:
406
operand.identifier.name !== null &&
407
operand.identifier.name.kind === 'named'
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-use-ref-added-to-dep-without-type-info.expect.md
+9
-7
@@ -22,13 +22,15 @@ function Foo({a}) {
22
## Error
23
24
```
25
- 3 | const ref = useRef();
26
- 4 | // type information is lost here as we don't track types of fields
27
-> 5 | const val = {ref};
28
- | ^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (5:5)
29
- 6 | // without type info, we don't know that val.ref.current is a ref value so we
30
- 7 | // *would* end up depending on val.ref.current
31
- 8 | // however, this is an instance of accessing a ref during render and is disallowed
25
+ 8 | // however, this is an instance of accessing a ref during render and is disallowed
26
+ 9 | // under React's rules, so we reject this input
27
+> 10 | const x = {a, val: val.ref.current};
28
+ | ^^^^^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (10:10)
29
+
30
+InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (10:10)
31
+ 11 |
32
+ 12 | return <VideoList videos={x} />;
33
+ 13 | }
34
```
35
36
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.return-ref-callback-structure.expect.md
deleted
-45
@@ -1,45 +0,0 @@
1
-
2
-## Input
3
-
4
-```javascript
5
-// @flow @validateRefAccessDuringRender @validatePreserveExistingMemoizationGuarantees
6
-
7
-import {useRef} from 'react';
8
-
9
-component Foo(cond: boolean, cond2: boolean) {
10
- const ref = useRef();
11
-
12
- const s = () => {
13
- return ref.current;
14
- };
15
-
16
- if (cond) return [s];
17
- else if (cond2) return {s};
18
- else return {s: [s]};
19
-}
20
-
21
-export const FIXTURE_ENTRYPOINT = {
22
- fn: Foo,
23
- params: [{cond: false, cond2: false}],
24
-};
25
-
26
-```
27
-
28
-
29
-## Error
30
-
31
-```
32
- 10 | };
33
- 11 |
34
-> 12 | if (cond) return [s];
35
- | ^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (12:12)
36
-
37
-InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (13:13)
38
-
39
-InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (14:14)
40
- 13 | else if (cond2) return {s};
41
- 14 | else return {s: [s]};
42
- 15 | }
43
-```
44
-
45
-
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/return-ref-callback-structure.expect.md
new
+87
@@ -0,0 +1,87 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @flow @validateRefAccessDuringRender @validatePreserveExistingMemoizationGuarantees
6
+
7
+import {useRef} from 'react';
8
+
9
+component Foo(cond: boolean, cond2: boolean) {
10
+ const ref = useRef();
11
+
12
+ const s = () => {
13
+ return ref.current;
14
+ };
15
+
16
+ if (cond) return [s];
17
+ else if (cond2) return {s};
18
+ else return {s: [s]};
19
+}
20
+
21
+export const FIXTURE_ENTRYPOINT = {
22
+ fn: Foo,
23
+ params: [{cond: false, cond2: false}],
24
+};
25
+
26
+```
27
+
28
+## Code
29
+
30
+```javascript
31
+import { c as _c } from "react/compiler-runtime";
32
+
33
+import { useRef } from "react";
34
+
35
+function Foo(t0) {
36
+ const $ = _c(4);
37
+ const { cond, cond2 } = t0;
38
+ const ref = useRef();
39
+ let t1;
40
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
41
+ t1 = () => ref.current;
42
+ $[0] = t1;
43
+ } else {
44
+ t1 = $[0];
45
+ }
46
+ const s = t1;
47
+ if (cond) {
48
+ let t2;
49
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
50
+ t2 = [s];
51
+ $[1] = t2;
52
+ } else {
53
+ t2 = $[1];
54
+ }
55
+ return t2;
56
+ } else {
57
+ if (cond2) {
58
+ let t2;
59
+ if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
60
+ t2 = { s };
61
+ $[2] = t2;
62
+ } else {
63
+ t2 = $[2];
64
+ }
65
+ return t2;
66
+ } else {
67
+ let t2;
68
+ if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
69
+ t2 = { s: [s] };
70
+ $[3] = t2;
71
+ } else {
72
+ t2 = $[3];
73
+ }
74
+ return t2;
75
+ }
76
+ }
77
+}
78
+
79
+export const FIXTURE_ENTRYPOINT = {
80
+ fn: Foo,
81
+ params: [{ cond: false, cond2: false }],
82
+};
83
+
84
+```
85
+
86
+### Eval output
87
+(kind: ok) {"s":["[[ function params=0 ]]"]}
\ No newline at end of file