main
ts 964 lines 31 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 import {
9 CompilerDiagnostic,
10 CompilerError,
11 ErrorCategory,
12 } from '../CompilerError';
13 import {
14 BlockId,
15 GeneratedSource,
16 HIRFunction,
17 IdentifierId,
18 Place,
19 SourceLocation,
20 getHookKindForType,
21 isRefValueType,
22 isUseRefType,
23 } from '../HIR';
24 import {
25 eachInstructionOperand,
26 eachInstructionValueOperand,
27 eachPatternOperand,
28 eachTerminalOperand,
29 } from '../HIR/visitors';
30 import {retainWhere} from '../Utils/utils';
31
32 /**
33 * Validates that a function does not access a ref value during render. This includes a partial check
34 * for ref values which are accessed indirectly via function expressions.
35 *
36 * ```javascript
37 * // ERROR
38 * const ref = useRef();
39 * ref.current;
40 *
41 * const ref = useRef();
42 * foo(ref); // may access .current
43 *
44 * // ALLOWED
45 * const ref = useHookThatReturnsRef();
46 * ref.current;
47 * ```
48 *
49 * In the future we may reject more cases, based on either object names (`fooRef.current` is likely a ref)
50 * or based on property name alone (`foo.current` might be a ref).
51 */
52
53 const opaqueRefId = Symbol();
54 type RefId = number & {[opaqueRefId]: 'RefId'};
55
56 function makeRefId(id: number): RefId {
57 CompilerError.invariant(id >= 0 && Number.isInteger(id), {
58 reason: 'Expected identifier id to be a non-negative integer',
59 loc: GeneratedSource,
60 });
61 return id as RefId;
62 }
63 let _refId = 0;
64 function nextRefId(): RefId {
65 return makeRefId(_refId++);
66 }
67
68 type RefAccessType =
69 | {kind: 'None'}
70 | {kind: 'Nullable'}
71 | {kind: 'Guard'; refId: RefId}
72 | RefAccessRefType;
73
74 type RefAccessRefType =
75 | {kind: 'Ref'; refId: RefId}
76 | {kind: 'RefValue'; loc?: SourceLocation; refId?: RefId}
77 | {kind: 'Structure'; value: null | RefAccessRefType; fn: null | RefFnType};
78
79 type RefFnType = {readRefEffect: boolean; returnType: RefAccessType};
80
81 class Env {
82 #changed = false;
83 #data: Map<IdentifierId, RefAccessType> = new Map();
84 #temporaries: Map<IdentifierId, Place> = new Map();
85
86 lookup(place: Place): Place {
87 return this.#temporaries.get(place.identifier.id) ?? place;
88 }
89
90 define(place: Place, value: Place): void {
91 this.#temporaries.set(place.identifier.id, value);
92 }
93
94 resetChanged(): void {
95 this.#changed = false;
96 }
97
98 hasChanged(): boolean {
99 return this.#changed;
100 }
101
102 get(key: IdentifierId): RefAccessType | undefined {
103 const operandId = this.#temporaries.get(key)?.identifier.id ?? key;
104 return this.#data.get(operandId);
105 }
106
107 set(key: IdentifierId, value: RefAccessType): this {
108 const operandId = this.#temporaries.get(key)?.identifier.id ?? key;
109 const cur = this.#data.get(operandId);
110 const widenedValue = joinRefAccessTypes(value, cur ?? {kind: 'None'});
111 if (
112 !(cur == null && widenedValue.kind === 'None') &&
113 (cur == null || !tyEqual(cur, widenedValue))
114 ) {
115 this.#changed = true;
116 }
117 this.#data.set(operandId, widenedValue);
118 return this;
119 }
120 }
121
122 export function validateNoRefAccessInRender(fn: HIRFunction): void {
123 const env = new Env();
124 collectTemporariesSidemap(fn, env);
125 const errors = new CompilerError();
126 validateNoRefAccessInRenderImpl(fn, env, errors);
127 for (const detail of errors.details) {
128 fn.env.recordError(detail);
129 }
130 }
131
132 function collectTemporariesSidemap(fn: HIRFunction, env: Env): void {
133 for (const block of fn.body.blocks.values()) {
134 for (const instr of block.instructions) {
135 const {lvalue, value} = instr;
136 switch (value.kind) {
137 case 'LoadLocal': {
138 const temp = env.lookup(value.place);
139 if (temp != null) {
140 env.define(lvalue, temp);
141 }
142 break;
143 }
144 case 'StoreLocal': {
145 const temp = env.lookup(value.value);
146 if (temp != null) {
147 env.define(lvalue, temp);
148 env.define(value.lvalue.place, temp);
149 }
150 break;
151 }
152 case 'PropertyLoad': {
153 if (
154 isUseRefType(value.object.identifier) &&
155 value.property === 'current'
156 ) {
157 continue;
158 }
159 const temp = env.lookup(value.object);
160 if (temp != null) {
161 env.define(lvalue, temp);
162 }
163 break;
164 }
165 }
166 }
167 }
168 }
169
170 function refTypeOfType(place: Place): RefAccessType {
171 if (isRefValueType(place.identifier)) {
172 return {kind: 'RefValue'};
173 } else if (isUseRefType(place.identifier)) {
174 return {kind: 'Ref', refId: nextRefId()};
175 } else {
176 return {kind: 'None'};
177 }
178 }
179
180 function tyEqual(a: RefAccessType, b: RefAccessType): boolean {
181 if (a.kind !== b.kind) {
182 return false;
183 }
184 switch (a.kind) {
185 case 'None':
186 return true;
187 case 'Ref':
188 return true;
189 case 'Nullable':
190 return true;
191 case 'Guard':
192 CompilerError.invariant(b.kind === 'Guard', {
193 reason: 'Expected ref value',
194 loc: GeneratedSource,
195 });
196 return a.refId === b.refId;
197 case 'RefValue':
198 CompilerError.invariant(b.kind === 'RefValue', {
199 reason: 'Expected ref value',
200 loc: GeneratedSource,
201 });
202 return a.loc == b.loc;
203 case 'Structure': {
204 CompilerError.invariant(b.kind === 'Structure', {
205 reason: 'Expected structure',
206 loc: GeneratedSource,
207 });
208 const fnTypesEqual =
209 (a.fn === null && b.fn === null) ||
210 (a.fn !== null &&
211 b.fn !== null &&
212 a.fn.readRefEffect === b.fn.readRefEffect &&
213 tyEqual(a.fn.returnType, b.fn.returnType));
214 return (
215 fnTypesEqual &&
216 (a.value === b.value ||
217 (a.value !== null && b.value !== null && tyEqual(a.value, b.value)))
218 );
219 }
220 }
221 }
222
223 function joinRefAccessTypes(...types: Array<RefAccessType>): RefAccessType {
224 function joinRefAccessRefTypes(
225 a: RefAccessRefType,
226 b: RefAccessRefType,
227 ): RefAccessRefType {
228 if (a.kind === 'RefValue') {
229 if (b.kind === 'RefValue' && a.refId === b.refId) {
230 return a;
231 }
232 return {kind: 'RefValue'};
233 } else if (b.kind === 'RefValue') {
234 return b;
235 } else if (a.kind === 'Ref' || b.kind === 'Ref') {
236 if (a.kind === 'Ref' && b.kind === 'Ref' && a.refId === b.refId) {
237 return a;
238 }
239 return {kind: 'Ref', refId: nextRefId()};
240 } else {
241 CompilerError.invariant(
242 a.kind === 'Structure' && b.kind === 'Structure',
243 {
244 reason: 'Expected structure',
245 loc: GeneratedSource,
246 },
247 );
248 const fn =
249 a.fn === null
250 ? b.fn
251 : b.fn === null
252 ? a.fn
253 : {
254 readRefEffect: a.fn.readRefEffect || b.fn.readRefEffect,
255 returnType: joinRefAccessTypes(
256 a.fn.returnType,
257 b.fn.returnType,
258 ),
259 };
260 const value =
261 a.value === null
262 ? b.value
263 : b.value === null
264 ? a.value
265 : joinRefAccessRefTypes(a.value, b.value);
266 return {
267 kind: 'Structure',
268 fn,
269 value,
270 };
271 }
272 }
273
274 return types.reduce(
275 (a, b) => {
276 if (a.kind === 'None') {
277 return b;
278 } else if (b.kind === 'None') {
279 return a;
280 } else if (a.kind === 'Guard') {
281 if (b.kind === 'Guard' && a.refId === b.refId) {
282 return a;
283 } else if (b.kind === 'Nullable' || b.kind === 'Guard') {
284 return {kind: 'None'};
285 } else {
286 return b;
287 }
288 } else if (b.kind === 'Guard') {
289 if (a.kind === 'Nullable') {
290 return {kind: 'None'};
291 } else {
292 return b;
293 }
294 } else if (a.kind === 'Nullable') {
295 return b;
296 } else if (b.kind === 'Nullable') {
297 return a;
298 } else {
299 return joinRefAccessRefTypes(a, b);
300 }
301 },
302 {kind: 'None'},
303 );
304 }
305
306 function validateNoRefAccessInRenderImpl(
307 fn: HIRFunction,
308 env: Env,
309 errors: CompilerError,
310 ): RefAccessType {
311 let returnValues: Array<undefined | RefAccessType> = [];
312 let place;
313 for (const param of fn.params) {
314 if (param.kind === 'Identifier') {
315 place = param;
316 } else {
317 place = param.place;
318 }
319 const type = refTypeOfType(place);
320 env.set(place.identifier.id, type);
321 }
322
323 const interpolatedAsJsx = new Set<IdentifierId>();
324 for (const block of fn.body.blocks.values()) {
325 for (const instr of block.instructions) {
326 const {value} = instr;
327 if (value.kind === 'JsxExpression' || value.kind === 'JsxFragment') {
328 if (value.children != null) {
329 for (const child of value.children) {
330 interpolatedAsJsx.add(child.identifier.id);
331 }
332 }
333 }
334 }
335 }
336
337 for (let i = 0; (i == 0 || env.hasChanged()) && i < 10; i++) {
338 env.resetChanged();
339 returnValues = [];
340 const safeBlocks: Array<{block: BlockId; ref: RefId}> = [];
341 for (const [, block] of fn.body.blocks) {
342 retainWhere(safeBlocks, entry => entry.block !== block.id);
343 for (const phi of block.phis) {
344 env.set(
345 phi.place.identifier.id,
346 joinRefAccessTypes(
347 ...Array(...phi.operands.values()).map(
348 operand =>
349 env.get(operand.identifier.id) ?? ({kind: 'None'} as const),
350 ),
351 ),
352 );
353 }
354
355 for (const instr of block.instructions) {
356 switch (instr.value.kind) {
357 case 'JsxExpression':
358 case 'JsxFragment': {
359 for (const operand of eachInstructionValueOperand(instr.value)) {
360 validateNoDirectRefValueAccess(errors, operand, env);
361 }
362 break;
363 }
364 case 'ComputedLoad':
365 case 'PropertyLoad': {
366 if (instr.value.kind === 'ComputedLoad') {
367 validateNoDirectRefValueAccess(errors, instr.value.property, env);
368 }
369 const objType = env.get(instr.value.object.identifier.id);
370 let lookupType: null | RefAccessType = null;
371 if (objType?.kind === 'Structure') {
372 lookupType = objType.value;
373 } else if (objType?.kind === 'Ref') {
374 lookupType = {
375 kind: 'RefValue',
376 loc: instr.loc,
377 refId: objType.refId,
378 };
379 }
380 env.set(
381 instr.lvalue.identifier.id,
382 lookupType ?? refTypeOfType(instr.lvalue),
383 );
384 break;
385 }
386 case 'TypeCastExpression': {
387 env.set(
388 instr.lvalue.identifier.id,
389 env.get(instr.value.value.identifier.id) ??
390 refTypeOfType(instr.lvalue),
391 );
392 break;
393 }
394 case 'LoadContext':
395 case 'LoadLocal': {
396 env.set(
397 instr.lvalue.identifier.id,
398 env.get(instr.value.place.identifier.id) ??
399 refTypeOfType(instr.lvalue),
400 );
401 break;
402 }
403 case 'StoreContext':
404 case 'StoreLocal': {
405 env.set(
406 instr.value.lvalue.place.identifier.id,
407 env.get(instr.value.value.identifier.id) ??
408 refTypeOfType(instr.value.lvalue.place),
409 );
410 env.set(
411 instr.lvalue.identifier.id,
412 env.get(instr.value.value.identifier.id) ??
413 refTypeOfType(instr.lvalue),
414 );
415 break;
416 }
417 case 'Destructure': {
418 const objType = env.get(instr.value.value.identifier.id);
419 let lookupType = null;
420 if (objType?.kind === 'Structure') {
421 lookupType = objType.value;
422 }
423 env.set(
424 instr.lvalue.identifier.id,
425 lookupType ?? refTypeOfType(instr.lvalue),
426 );
427 for (const lval of eachPatternOperand(instr.value.lvalue.pattern)) {
428 env.set(lval.identifier.id, lookupType ?? refTypeOfType(lval));
429 }
430 break;
431 }
432 case 'ObjectMethod':
433 case 'FunctionExpression': {
434 let returnType: RefAccessType = {kind: 'None'};
435 let readRefEffect = false;
436 const innerErrors = new CompilerError();
437 const result = validateNoRefAccessInRenderImpl(
438 instr.value.loweredFunc.func,
439 env,
440 innerErrors,
441 );
442 if (!innerErrors.hasAnyErrors()) {
443 returnType = result;
444 } else {
445 readRefEffect = true;
446 }
447 env.set(instr.lvalue.identifier.id, {
448 kind: 'Structure',
449 fn: {
450 readRefEffect,
451 returnType,
452 },
453 value: null,
454 });
455 break;
456 }
457 case 'MethodCall':
458 case 'CallExpression': {
459 const callee =
460 instr.value.kind === 'CallExpression'
461 ? instr.value.callee
462 : instr.value.property;
463 const hookKind = getHookKindForType(fn.env, callee.identifier.type);
464 let returnType: RefAccessType = {kind: 'None'};
465 const fnType = env.get(callee.identifier.id);
466 let didError = false;
467 if (fnType?.kind === 'Structure' && fnType.fn !== null) {
468 returnType = fnType.fn.returnType;
469 if (fnType.fn.readRefEffect) {
470 didError = true;
471 errors.pushDiagnostic(
472 CompilerDiagnostic.create({
473 category: ErrorCategory.Refs,
474 reason: 'Cannot access refs during render',
475 description: ERROR_DESCRIPTION,
476 }).withDetails({
477 kind: 'error',
478 loc: callee.loc,
479 message: `This function accesses a ref value`,
480 }),
481 );
482 }
483 }
484 /*
485 * If we already reported an error on this instruction, don't report
486 * duplicate errors
487 */
488 if (!didError) {
489 const isRefLValue = isUseRefType(instr.lvalue.identifier);
490 if (
491 isRefLValue ||
492 (hookKind != null &&
493 hookKind !== 'useState' &&
494 hookKind !== 'useReducer')
495 ) {
496 for (const operand of eachInstructionValueOperand(
497 instr.value,
498 )) {
499 /**
500 * Allow passing refs or ref-accessing functions when:
501 * 1. lvalue is a ref (mergeRefs pattern: `mergeRefs(ref1, ref2)`)
502 * 2. calling hooks (independently validated for ref safety)
503 */
504 validateNoDirectRefValueAccess(errors, operand, env);
505 }
506 } else if (interpolatedAsJsx.has(instr.lvalue.identifier.id)) {
507 for (const operand of eachInstructionValueOperand(
508 instr.value,
509 )) {
510 /**
511 * Special case: the lvalue is passed as a jsx child
512 *
513 * For example `<Foo>{renderHelper(ref)}</Foo>`. Here we have more
514 * context and infer that the ref is being passed to a component-like
515 * render function which attempts to obey the rules.
516 */
517 validateNoRefValueAccess(errors, env, operand);
518 }
519 } else if (hookKind == null && instr.effects != null) {
520 /**
521 * For non-hook functions with known aliasing effects, use the
522 * effects to determine what validation to apply for each place.
523 * Track visited id:kind pairs to avoid duplicate errors.
524 */
525 const visitedEffects: Set<string> = new Set();
526 for (const effect of instr.effects) {
527 let place: Place | null = null;
528 let validation: 'ref-passed' | 'direct-ref' | 'none' = 'none';
529 switch (effect.kind) {
530 case 'Freeze': {
531 place = effect.value;
532 validation = 'direct-ref';
533 break;
534 }
535 case 'Mutate':
536 case 'MutateTransitive':
537 case 'MutateConditionally':
538 case 'MutateTransitiveConditionally': {
539 place = effect.value;
540 validation = 'ref-passed';
541 break;
542 }
543 case 'Render': {
544 place = effect.place;
545 validation = 'ref-passed';
546 break;
547 }
548 case 'Capture':
549 case 'Alias':
550 case 'MaybeAlias':
551 case 'Assign':
552 case 'CreateFrom': {
553 place = effect.from;
554 validation = 'ref-passed';
555 break;
556 }
557 case 'ImmutableCapture': {
558 /**
559 * ImmutableCapture can come from two sources:
560 * 1. A known signature that explicitly freezes the operand
561 * (e.g. PanResponder.create) — safe, the function doesn't
562 * call callbacks during render.
563 * 2. Downgraded defaults when the operand is already frozen
564 * (e.g. foo(propRef)) — the function is unknown and may
565 * access the ref.
566 *
567 * We distinguish these by checking whether the same operand
568 * also has a Freeze effect on this instruction, which only
569 * comes from known signatures.
570 */
571 place = effect.from;
572 const isFrozen = instr.effects.some(
573 e =>
574 e.kind === 'Freeze' &&
575 e.value.identifier.id === effect.from.identifier.id,
576 );
577 validation = isFrozen ? 'direct-ref' : 'ref-passed';
578 break;
579 }
580 case 'Create':
581 case 'CreateFunction':
582 case 'Apply':
583 case 'Impure':
584 case 'MutateFrozen':
585 case 'MutateGlobal': {
586 break;
587 }
588 }
589 if (place !== null && validation !== 'none') {
590 const key = `${place.identifier.id}:${validation}`;
591 if (!visitedEffects.has(key)) {
592 visitedEffects.add(key);
593 if (validation === 'direct-ref') {
594 validateNoDirectRefValueAccess(errors, place, env);
595 } else {
596 validateNoRefPassedToFunction(
597 errors,
598 env,
599 place,
600 place.loc,
601 );
602 }
603 }
604 }
605 }
606 } else {
607 for (const operand of eachInstructionValueOperand(
608 instr.value,
609 )) {
610 validateNoRefPassedToFunction(
611 errors,
612 env,
613 operand,
614 operand.loc,
615 );
616 }
617 }
618 }
619 env.set(instr.lvalue.identifier.id, returnType);
620 break;
621 }
622 case 'ObjectExpression':
623 case 'ArrayExpression': {
624 const types: Array<RefAccessType> = [];
625 for (const operand of eachInstructionValueOperand(instr.value)) {
626 validateNoDirectRefValueAccess(errors, operand, env);
627 types.push(env.get(operand.identifier.id) ?? {kind: 'None'});
628 }
629 const value = joinRefAccessTypes(...types);
630 if (
631 value.kind === 'None' ||
632 value.kind === 'Guard' ||
633 value.kind === 'Nullable'
634 ) {
635 env.set(instr.lvalue.identifier.id, {kind: 'None'});
636 } else {
637 env.set(instr.lvalue.identifier.id, {
638 kind: 'Structure',
639 value,
640 fn: null,
641 });
642 }
643 break;
644 }
645 case 'PropertyDelete':
646 case 'PropertyStore':
647 case 'ComputedDelete':
648 case 'ComputedStore': {
649 const target = env.get(instr.value.object.identifier.id);
650 let safe: (typeof safeBlocks)['0'] | null | undefined = null;
651 if (
652 instr.value.kind === 'PropertyStore' &&
653 target != null &&
654 target.kind === 'Ref'
655 ) {
656 safe = safeBlocks.find(entry => entry.ref === target.refId);
657 }
658 if (safe != null) {
659 retainWhere(safeBlocks, entry => entry !== safe);
660 } else {
661 validateNoRefUpdate(errors, env, instr.value.object, instr.loc);
662 }
663 if (
664 instr.value.kind === 'ComputedDelete' ||
665 instr.value.kind === 'ComputedStore'
666 ) {
667 validateNoRefValueAccess(errors, env, instr.value.property);
668 }
669 if (
670 instr.value.kind === 'ComputedStore' ||
671 instr.value.kind === 'PropertyStore'
672 ) {
673 validateNoDirectRefValueAccess(errors, instr.value.value, env);
674 const type = env.get(instr.value.value.identifier.id);
675 if (type != null && type.kind === 'Structure') {
676 let objectType: RefAccessType = type;
677 if (target != null) {
678 objectType = joinRefAccessTypes(objectType, target);
679 }
680 env.set(instr.value.object.identifier.id, objectType);
681 }
682 }
683 break;
684 }
685 case 'StartMemoize':
686 case 'FinishMemoize':
687 break;
688 case 'LoadGlobal': {
689 if (instr.value.binding.name === 'undefined') {
690 env.set(instr.lvalue.identifier.id, {kind: 'Nullable'});
691 }
692 break;
693 }
694 case 'Primitive': {
695 if (instr.value.value == null) {
696 env.set(instr.lvalue.identifier.id, {kind: 'Nullable'});
697 }
698 break;
699 }
700 case 'UnaryExpression': {
701 if (instr.value.operator === '!') {
702 const value = env.get(instr.value.value.identifier.id);
703 const refId =
704 value?.kind === 'RefValue' && value.refId != null
705 ? value.refId
706 : null;
707 if (refId !== null) {
708 /*
709 * Record an error suggesting the `if (ref.current == null)` pattern,
710 * but also record the lvalue as a guard so that we don't emit a second
711 * error for the write to the ref
712 */
713 env.set(instr.lvalue.identifier.id, {kind: 'Guard', refId});
714 errors.pushDiagnostic(
715 CompilerDiagnostic.create({
716 category: ErrorCategory.Refs,
717 reason: 'Cannot access refs during render',
718 description: ERROR_DESCRIPTION,
719 })
720 .withDetails({
721 kind: 'error',
722 loc: instr.value.value.loc,
723 message: `Cannot access ref value during render`,
724 })
725 .withDetails({
726 kind: 'hint',
727 message:
728 'To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }`',
729 }),
730 );
731 break;
732 }
733 }
734 validateNoRefValueAccess(errors, env, instr.value.value);
735 break;
736 }
737 case 'BinaryExpression': {
738 const left = env.get(instr.value.left.identifier.id);
739 const right = env.get(instr.value.right.identifier.id);
740 let nullish: boolean = false;
741 let refId: RefId | null = null;
742 if (left?.kind === 'RefValue' && left.refId != null) {
743 refId = left.refId;
744 } else if (right?.kind === 'RefValue' && right.refId != null) {
745 refId = right.refId;
746 }
747
748 if (left?.kind === 'Nullable') {
749 nullish = true;
750 } else if (right?.kind === 'Nullable') {
751 nullish = true;
752 }
753
754 if (refId !== null && nullish) {
755 env.set(instr.lvalue.identifier.id, {kind: 'Guard', refId});
756 } else {
757 for (const operand of eachInstructionValueOperand(instr.value)) {
758 validateNoRefValueAccess(errors, env, operand);
759 }
760 }
761 break;
762 }
763 default: {
764 for (const operand of eachInstructionValueOperand(instr.value)) {
765 validateNoRefValueAccess(errors, env, operand);
766 }
767 break;
768 }
769 }
770
771 // Guard values are derived from ref.current, so they can only be used in if statement targets
772 for (const operand of eachInstructionOperand(instr)) {
773 guardCheck(errors, operand, env);
774 }
775
776 if (
777 isUseRefType(instr.lvalue.identifier) &&
778 env.get(instr.lvalue.identifier.id)?.kind !== 'Ref'
779 ) {
780 env.set(
781 instr.lvalue.identifier.id,
782 joinRefAccessTypes(
783 env.get(instr.lvalue.identifier.id) ?? {kind: 'None'},
784 {kind: 'Ref', refId: nextRefId()},
785 ),
786 );
787 }
788 if (
789 isRefValueType(instr.lvalue.identifier) &&
790 env.get(instr.lvalue.identifier.id)?.kind !== 'RefValue'
791 ) {
792 env.set(
793 instr.lvalue.identifier.id,
794 joinRefAccessTypes(
795 env.get(instr.lvalue.identifier.id) ?? {kind: 'None'},
796 {kind: 'RefValue', loc: instr.loc},
797 ),
798 );
799 }
800 }
801
802 if (block.terminal.kind === 'if') {
803 const test = env.get(block.terminal.test.identifier.id);
804 if (
805 test?.kind === 'Guard' &&
806 safeBlocks.find(entry => entry.ref === test.refId) == null
807 ) {
808 safeBlocks.push({block: block.terminal.fallthrough, ref: test.refId});
809 }
810 }
811
812 for (const operand of eachTerminalOperand(block.terminal)) {
813 if (block.terminal.kind !== 'return') {
814 validateNoRefValueAccess(errors, env, operand);
815 if (block.terminal.kind !== 'if') {
816 guardCheck(errors, operand, env);
817 }
818 } else {
819 // Allow functions containing refs to be returned, but not direct ref values
820 validateNoDirectRefValueAccess(errors, operand, env);
821 guardCheck(errors, operand, env);
822 returnValues.push(env.get(operand.identifier.id));
823 }
824 }
825 }
826
827 if (errors.hasAnyErrors()) {
828 return {kind: 'None'};
829 }
830 }
831
832 CompilerError.invariant(!env.hasChanged(), {
833 reason: 'Ref type environment did not converge',
834 loc: GeneratedSource,
835 });
836
837 return joinRefAccessTypes(
838 ...returnValues.filter((env): env is RefAccessType => env !== undefined),
839 );
840 }
841
842 function destructure(
843 type: RefAccessType | undefined,
844 ): RefAccessType | undefined {
845 if (type?.kind === 'Structure' && type.value !== null) {
846 return destructure(type.value);
847 }
848 return type;
849 }
850
851 function guardCheck(errors: CompilerError, operand: Place, env: Env): void {
852 if (env.get(operand.identifier.id)?.kind === 'Guard') {
853 errors.pushDiagnostic(
854 CompilerDiagnostic.create({
855 category: ErrorCategory.Refs,
856 reason: 'Cannot access refs during render',
857 description: ERROR_DESCRIPTION,
858 }).withDetails({
859 kind: 'error',
860 loc: operand.loc,
861 message: `Cannot access ref value during render`,
862 }),
863 );
864 }
865 }
866
867 function validateNoRefValueAccess(
868 errors: CompilerError,
869 env: Env,
870 operand: Place,
871 ): void {
872 const type = destructure(env.get(operand.identifier.id));
873 if (
874 type?.kind === 'RefValue' ||
875 (type?.kind === 'Structure' && type.fn?.readRefEffect)
876 ) {
877 errors.pushDiagnostic(
878 CompilerDiagnostic.create({
879 category: ErrorCategory.Refs,
880 reason: 'Cannot access refs during render',
881 description: ERROR_DESCRIPTION,
882 }).withDetails({
883 kind: 'error',
884 loc: (type.kind === 'RefValue' && type.loc) || operand.loc,
885 message: `Cannot access ref value during render`,
886 }),
887 );
888 }
889 }
890
891 function validateNoRefPassedToFunction(
892 errors: CompilerError,
893 env: Env,
894 operand: Place,
895 loc: SourceLocation,
896 ): void {
897 const type = destructure(env.get(operand.identifier.id));
898 if (
899 type?.kind === 'Ref' ||
900 type?.kind === 'RefValue' ||
901 (type?.kind === 'Structure' && type.fn?.readRefEffect)
902 ) {
903 errors.pushDiagnostic(
904 CompilerDiagnostic.create({
905 category: ErrorCategory.Refs,
906 reason: 'Cannot access refs during render',
907 description: ERROR_DESCRIPTION,
908 }).withDetails({
909 kind: 'error',
910 loc: (type.kind === 'RefValue' && type.loc) || loc,
911 message: `Passing a ref to a function may read its value during render`,
912 }),
913 );
914 }
915 }
916
917 function validateNoRefUpdate(
918 errors: CompilerError,
919 env: Env,
920 operand: Place,
921 loc: SourceLocation,
922 ): void {
923 const type = destructure(env.get(operand.identifier.id));
924 if (type?.kind === 'Ref' || type?.kind === 'RefValue') {
925 errors.pushDiagnostic(
926 CompilerDiagnostic.create({
927 category: ErrorCategory.Refs,
928 reason: 'Cannot access refs during render',
929 description: ERROR_DESCRIPTION,
930 }).withDetails({
931 kind: 'error',
932 loc: (type.kind === 'RefValue' && type.loc) || loc,
933 message: `Cannot update ref during render`,
934 }),
935 );
936 }
937 }
938
939 function validateNoDirectRefValueAccess(
940 errors: CompilerError,
941 operand: Place,
942 env: Env,
943 ): void {
944 const type = destructure(env.get(operand.identifier.id));
945 if (type?.kind === 'RefValue') {
946 errors.pushDiagnostic(
947 CompilerDiagnostic.create({
948 category: ErrorCategory.Refs,
949 reason: 'Cannot access refs during render',
950 description: ERROR_DESCRIPTION,
951 }).withDetails({
952 kind: 'error',
953 loc: type.loc ?? operand.loc,
954 message: `Cannot access ref value during render`,
955 }),
956 );
957 }
958 }
959
960 const ERROR_DESCRIPTION =
961 'React refs are values that are not needed for rendering. Refs should only be accessed ' +
962 'outside of render, such as in event handlers or effects. ' +
963 'Accessing a ref value (the `current` property) during render can cause your component ' +
964 'not to update as expected (https://react.dev/reference/react/useRef)';