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
* 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;
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;
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:
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
}
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
}
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
}
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
}
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'
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,
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'