main
ts 230 lines 8.19 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, CompilerError, Effect} from '..';
9 import {ErrorCategory} from '../CompilerError';
10 import {Environment} from '../HIR/Environment';
11 import {HIRFunction, IdentifierId, Place} from '../HIR';
12 import {
13 eachInstructionLValue,
14 eachInstructionValueOperand,
15 eachTerminalOperand,
16 } from '../HIR/visitors';
17 import {getFunctionCallSignature} from '../Inference/InferMutationAliasingEffects';
18
19 /**
20 * Validates that local variables cannot be reassigned after render.
21 * This prevents a category of bugs in which a closure captures a
22 * binding from one render but does not update
23 */
24 export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
25 const contextVariables = new Set<IdentifierId>();
26 const reassignment = getContextReassignment(
27 fn,
28 contextVariables,
29 false,
30 false,
31 fn.env,
32 );
33 if (reassignment !== null) {
34 const variable =
35 reassignment.identifier.name != null &&
36 reassignment.identifier.name.kind === 'named'
37 ? `\`${reassignment.identifier.name.value}\``
38 : 'variable';
39 fn.env.recordError(
40 CompilerDiagnostic.create({
41 category: ErrorCategory.Immutability,
42 reason: 'Cannot reassign variable after render completes',
43 description: `Reassigning ${variable} after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead`,
44 }).withDetails({
45 kind: 'error',
46 loc: reassignment.loc,
47 message: `Cannot reassign ${variable} after render completes`,
48 }),
49 );
50 }
51 }
52
53 function getContextReassignment(
54 fn: HIRFunction,
55 contextVariables: Set<IdentifierId>,
56 isFunctionExpression: boolean,
57 isAsync: boolean,
58 env: Environment,
59 ): Place | null {
60 const reassigningFunctions = new Map<IdentifierId, Place>();
61 for (const [, block] of fn.body.blocks) {
62 for (const instr of block.instructions) {
63 const {lvalue, value} = instr;
64 switch (value.kind) {
65 case 'FunctionExpression':
66 case 'ObjectMethod': {
67 let reassignment = getContextReassignment(
68 value.loweredFunc.func,
69 contextVariables,
70 true,
71 isAsync || value.loweredFunc.func.async,
72 env,
73 );
74 if (reassignment === null) {
75 // If the function itself doesn't reassign, does one of its dependencies?
76 for (const operand of eachInstructionValueOperand(value)) {
77 const reassignmentFromOperand = reassigningFunctions.get(
78 operand.identifier.id,
79 );
80 if (reassignmentFromOperand !== undefined) {
81 reassignment = reassignmentFromOperand;
82 break;
83 }
84 }
85 }
86 // if the function or its depends reassign, propagate that fact on the lvalue
87 if (reassignment !== null) {
88 if (isAsync || value.loweredFunc.func.async) {
89 const variable =
90 reassignment.identifier.name !== null &&
91 reassignment.identifier.name.kind === 'named'
92 ? `\`${reassignment.identifier.name.value}\``
93 : 'variable';
94 env.recordError(
95 CompilerDiagnostic.create({
96 category: ErrorCategory.Immutability,
97 reason: 'Cannot reassign variable in async function',
98 description:
99 'Reassigning a variable in an async function can cause inconsistent behavior on subsequent renders. Consider using state instead',
100 }).withDetails({
101 kind: 'error',
102 loc: reassignment.loc,
103 message: `Cannot reassign ${variable}`,
104 }),
105 );
106 return null;
107 }
108 reassigningFunctions.set(lvalue.identifier.id, reassignment);
109 }
110 break;
111 }
112 case 'StoreLocal': {
113 const reassignment = reassigningFunctions.get(
114 value.value.identifier.id,
115 );
116 if (reassignment !== undefined) {
117 reassigningFunctions.set(
118 value.lvalue.place.identifier.id,
119 reassignment,
120 );
121 reassigningFunctions.set(lvalue.identifier.id, reassignment);
122 }
123 break;
124 }
125 case 'LoadLocal': {
126 const reassignment = reassigningFunctions.get(
127 value.place.identifier.id,
128 );
129 if (reassignment !== undefined) {
130 reassigningFunctions.set(lvalue.identifier.id, reassignment);
131 }
132 break;
133 }
134 case 'DeclareContext': {
135 if (!isFunctionExpression) {
136 contextVariables.add(value.lvalue.place.identifier.id);
137 }
138 break;
139 }
140 case 'StoreContext': {
141 if (isFunctionExpression) {
142 if (contextVariables.has(value.lvalue.place.identifier.id)) {
143 return value.lvalue.place;
144 }
145 } else {
146 /*
147 * We only track reassignments of variables defined in the outer
148 * component or hook.
149 */
150 contextVariables.add(value.lvalue.place.identifier.id);
151 }
152 const reassignment = reassigningFunctions.get(
153 value.value.identifier.id,
154 );
155 if (reassignment !== undefined) {
156 reassigningFunctions.set(
157 value.lvalue.place.identifier.id,
158 reassignment,
159 );
160 reassigningFunctions.set(lvalue.identifier.id, reassignment);
161 }
162 break;
163 }
164 default: {
165 let operands = eachInstructionValueOperand(value);
166 // If we're calling a function that doesn't let its arguments escape, only test the callee
167 if (value.kind === 'CallExpression') {
168 const signature = getFunctionCallSignature(
169 fn.env,
170 value.callee.identifier.type,
171 );
172 if (signature?.noAlias) {
173 operands = [value.callee];
174 }
175 } else if (value.kind === 'MethodCall') {
176 const signature = getFunctionCallSignature(
177 fn.env,
178 value.property.identifier.type,
179 );
180 if (signature?.noAlias) {
181 operands = [value.receiver, value.property];
182 }
183 } else if (value.kind === 'TaggedTemplateExpression') {
184 const signature = getFunctionCallSignature(
185 fn.env,
186 value.tag.identifier.type,
187 );
188 if (signature?.noAlias) {
189 operands = [value.tag];
190 }
191 }
192 for (const operand of operands) {
193 CompilerError.invariant(operand.effect !== Effect.Unknown, {
194 reason: `Expected effects to be inferred prior to ValidateLocalsNotReassignedAfterRender`,
195 loc: operand.loc,
196 });
197 const reassignment = reassigningFunctions.get(
198 operand.identifier.id,
199 );
200 if (reassignment !== undefined) {
201 /*
202 * Functions that reassign local variables are inherently mutable and are unsafe to pass
203 * to a place that expects a frozen value. Propagate the reassignment upward.
204 */
205 if (operand.effect === Effect.Freeze) {
206 return reassignment;
207 } else {
208 /*
209 * If the operand is not frozen but it does reassign, then the lvalues
210 * of the instruction could also be reassigning
211 */
212 for (const lval of eachInstructionLValue(instr)) {
213 reassigningFunctions.set(lval.identifier.id, reassignment);
214 }
215 }
216 }
217 }
218 break;
219 }
220 }
221 }
222 for (const operand of eachTerminalOperand(block.terminal)) {
223 const reassignment = reassigningFunctions.get(operand.identifier.id);
224 if (reassignment !== undefined) {
225 return reassignment;
226 }
227 }
228 }
229 return null;
230 }