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 { CompilerError, Effect } from "..";
9
+import { HIRFunction, IdentifierId, Place } from "../HIR";
10
+import {
11
+ eachInstructionValueOperand,
12
+ eachTerminalOperand,
13
+} from "../HIR/visitors";
14
+
15
+/**
16
+ * Validates that local variables cannot be reassigned after render.
17
+ * This prevents a category of bugs in which a closure captures a
18
+ * binding from one render but does not update
19
+ */
20
+export function validateLocalsNotReassignedAfterRender(fn: HIRFunction): void {
21
+ const contextVariables = new Set<IdentifierId>();
22
+ const reassignment = getContextReassignment(fn, contextVariables, false);
23
+ if (reassignment !== null) {
24
+ CompilerError.throwInvalidReact({
25
+ reason:
26
+ "Reassigning a variable after render has completed can cause inconsistent behavior on subsequent renders. Consider using state instead",
27
+ description:
28
+ reassignment.identifier.name !== null &&
29
+ reassignment.identifier.name.kind === "named"
30
+ ? `Variable \`${reassignment.identifier.name.value}\` cannot be reassigned after render`
31
+ : "",
32
+ loc: reassignment.loc,
33
+ });
34
+ }
35
+}
36
+
37
+function getContextReassignment(
38
+ fn: HIRFunction,
39
+ contextVariables: Set<IdentifierId>,
40
+ isFunctionExpression: boolean
41
+): Place | null {
42
+ const reassigningFunctions = new Map<IdentifierId, Place>();
43
+ for (const [, block] of fn.body.blocks) {
44
+ for (const instr of block.instructions) {
45
+ const { lvalue, value } = instr;
46
+ switch (value.kind) {
47
+ case "FunctionExpression":
48
+ case "ObjectMethod": {
49
+ let reassignment = getContextReassignment(
50
+ value.loweredFunc.func,
51
+ contextVariables,
52
+ true
53
+ );
54
+ if (reassignment === null) {
55
+ // If the function itself doesn't reassign, does one of its dependencies?
56
+ for (const operand of eachInstructionValueOperand(value)) {
57
+ const reassignmentFromOperand = reassigningFunctions.get(
58
+ operand.identifier.id
59
+ );
60
+ if (reassignmentFromOperand !== undefined) {
61
+ reassignment = reassignmentFromOperand;
62
+ break;
63
+ }
64
+ }
65
+ }
66
+ // if the function or its depends reassign, propagate that fact on the lvalue
67
+ if (reassignment !== null) {
68
+ reassigningFunctions.set(lvalue.identifier.id, reassignment);
69
+ }
70
+ break;
71
+ }
72
+ case "StoreLocal": {
73
+ const reassignment = reassigningFunctions.get(
74
+ value.value.identifier.id
75
+ );
76
+ if (reassignment !== undefined) {
77
+ reassigningFunctions.set(
78
+ value.lvalue.place.identifier.id,
79
+ reassignment
80
+ );
81
+ reassigningFunctions.set(lvalue.identifier.id, reassignment);
82
+ }
83
+ break;
84
+ }
85
+ case "LoadLocal": {
86
+ const reassignment = reassigningFunctions.get(
87
+ value.place.identifier.id
88
+ );
89
+ if (reassignment !== undefined) {
90
+ reassigningFunctions.set(lvalue.identifier.id, reassignment);
91
+ }
92
+ break;
93
+ }
94
+ case "DeclareContext": {
95
+ if (!isFunctionExpression) {
96
+ contextVariables.add(value.lvalue.place.identifier.id);
97
+ }
98
+ break;
99
+ }
100
+ case "StoreContext": {
101
+ if (isFunctionExpression) {
102
+ if (contextVariables.has(value.lvalue.place.identifier.id)) {
103
+ return value.lvalue.place;
104
+ }
105
+ } else {
106
+ /*
107
+ * We only track reassignments of variables defined in the outer
108
+ * component or hook.
109
+ */
110
+ contextVariables.add(value.lvalue.place.identifier.id);
111
+ }
112
+ break;
113
+ }
114
+ default: {
115
+ for (const operand of eachInstructionValueOperand(value)) {
116
+ CompilerError.invariant(operand.effect !== Effect.Unknown, {
117
+ reason: `Expected effects to be inferred prior to ValidateLocalsNotReassignedAfterRender`,
118
+ loc: operand.loc,
119
+ });
120
+ const reassignment = reassigningFunctions.get(
121
+ operand.identifier.id
122
+ );
123
+ if (
124
+ reassignment !== undefined &&
125
+ operand.effect === Effect.Freeze
126
+ ) {
127
+ /*
128
+ * Functions that reassign local variables are inherently mutable and are unsafe to pass
129
+ * to a place that expects a frozen value. Propagate the reassignment upward.
130
+ */
131
+ return reassignment;
132
+ }
133
+ }
134
+ break;
135
+ }
136
+ }
137
+ }
138
+ for (const operand of eachTerminalOperand(block.terminal)) {
139
+ const reassignment = reassigningFunctions.get(operand.identifier.id);
140
+ if (reassignment !== undefined) {
141
+ return reassignment;
142
+ }
143
+ }
144
+ }
145
+ return null;
146
+}