main
ts 161 lines 5.6 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 {CompilerDiagnostic, Effect} from '..';
9 import {ErrorCategory} from '../CompilerError';
10 import {
11 HIRFunction,
12 IdentifierId,
13 isRefOrRefLikeMutableType,
14 Place,
15 } from '../HIR';
16 import {
17 eachInstructionValueOperand,
18 eachTerminalOperand,
19 } from '../HIR/visitors';
20 import {AliasingEffect} from '../Inference/AliasingEffects';
21
22 /**
23 * Validates that functions with known mutations (ie due to types) cannot be passed
24 * where a frozen value is expected. Example:
25 *
26 * ```
27 * function Component() {
28 * const cache = new Map();
29 * const onClick = () => {
30 * cache.set(...);
31 * }
32 * useHook(onClick); // ERROR: cannot pass a mutable value
33 * return <Foo onClick={onClick} /> // ERROR: cannot pass a mutable value
34 * }
35 * ```
36 *
37 * Because `onClick` function mutates `cache` when called, `onClick` is equivalent to a mutable
38 * variables. But unlike other mutables values like an array, the receiver of the function has
39 * no way to avoid mutation — for example, a function can receive an array and choose not to mutate
40 * it, but there's no way to know that a function is mutable and avoid calling it.
41 *
42 * This pass detects functions with *known* mutations (Store or Mutate, not ConditionallyMutate)
43 * that are passed where a frozen value is expected and rejects them.
44 */
45 export function validateNoFreezingKnownMutableFunctions(fn: HIRFunction): void {
46 const contextMutationEffects: Map<
47 IdentifierId,
48 Extract<AliasingEffect, {kind: 'Mutate'} | {kind: 'MutateTransitive'}>
49 > = new Map();
50
51 function visitOperand(operand: Place): void {
52 if (operand.effect === Effect.Freeze) {
53 const effect = contextMutationEffects.get(operand.identifier.id);
54 if (effect != null) {
55 const place = effect.value;
56 const variable =
57 place != null &&
58 place.identifier.name != null &&
59 place.identifier.name.kind === 'named'
60 ? `\`${place.identifier.name.value}\``
61 : 'a local variable';
62 fn.env.recordError(
63 CompilerDiagnostic.create({
64 category: ErrorCategory.Immutability,
65 reason: 'Cannot modify local variables after render completes',
66 description: `This argument is a function which may reassign or mutate ${variable} after render, which can cause inconsistent behavior on subsequent renders. Consider using state instead`,
67 })
68 .withDetails({
69 kind: 'error',
70 loc: operand.loc,
71 message: `This function may (indirectly) reassign or modify ${variable} after render`,
72 })
73 .withDetails({
74 kind: 'error',
75 loc: effect.value.loc,
76 message: `This modifies ${variable}`,
77 }),
78 );
79 }
80 }
81 }
82
83 for (const block of fn.body.blocks.values()) {
84 for (const instr of block.instructions) {
85 const {lvalue, value} = instr;
86 switch (value.kind) {
87 case 'LoadLocal': {
88 const effect = contextMutationEffects.get(value.place.identifier.id);
89 if (effect != null) {
90 contextMutationEffects.set(lvalue.identifier.id, effect);
91 }
92 break;
93 }
94 case 'StoreLocal': {
95 const effect = contextMutationEffects.get(value.value.identifier.id);
96 if (effect != null) {
97 contextMutationEffects.set(lvalue.identifier.id, effect);
98 contextMutationEffects.set(
99 value.lvalue.place.identifier.id,
100 effect,
101 );
102 }
103 break;
104 }
105 case 'FunctionExpression': {
106 if (value.loweredFunc.func.aliasingEffects != null) {
107 const context = new Set(
108 value.loweredFunc.func.context.map(p => p.identifier.id),
109 );
110 effects: for (const effect of value.loweredFunc.func
111 .aliasingEffects) {
112 switch (effect.kind) {
113 case 'Mutate':
114 case 'MutateTransitive': {
115 const knownMutation = contextMutationEffects.get(
116 effect.value.identifier.id,
117 );
118 if (knownMutation != null) {
119 contextMutationEffects.set(
120 lvalue.identifier.id,
121 knownMutation,
122 );
123 } else if (
124 context.has(effect.value.identifier.id) &&
125 !isRefOrRefLikeMutableType(effect.value.identifier.type)
126 ) {
127 contextMutationEffects.set(lvalue.identifier.id, effect);
128 break effects;
129 }
130 break;
131 }
132 case 'MutateConditionally':
133 case 'MutateTransitiveConditionally': {
134 const knownMutation = contextMutationEffects.get(
135 effect.value.identifier.id,
136 );
137 if (knownMutation != null) {
138 contextMutationEffects.set(
139 lvalue.identifier.id,
140 knownMutation,
141 );
142 }
143 break;
144 }
145 }
146 }
147 }
148 break;
149 }
150 default: {
151 for (const operand of eachInstructionValueOperand(value)) {
152 visitOperand(operand);
153 }
154 }
155 }
156 }
157 for (const operand of eachTerminalOperand(block.terminal)) {
158 visitOperand(operand);
159 }
160 }
161 }