9
import {
10
BlockId,
11
HIRFunction,
12
+ IdentifierId,
13
Place,
14
computePostDominatorTree,
15
isSetStateType,
19
import { findBlocksWithBackEdges } from "../Optimization/DeadCodeElimination";
20
import { Err, Ok, Result } from "../Utils/Result";
21
22
+/**
23
+ * Validates that the given function does not have an infinite update loop
24
+ * caused by unconditionally calling setState during render. This validation
25
+ * is conservative and cannot catch all cases of unconditional setState in
26
+ * render, but avoids false positives. Examples of cases that are caught:
27
+ *
28
+ * ```javascript
29
+ * // Direct call of setState:
30
+ * const [state, setState] = useState(false);
31
+ * setState(true);
32
+ *
33
+ * // Indirect via a function:
34
+ * const [state, setState] = useState(false);
35
+ * const setTrue = () => setState(true);
36
+ * setTrue();
37
+ * ```
38
+ *
39
+ * However, storing setState inside another value and accessing it is not yet
40
+ * validated:
41
+ *
42
+ * ```
43
+ * // false negative, not detected but will cause an infinite render loop
44
+ * const [state, setState] = useState(false);
45
+ * const x = [setState];
46
+ * const y = x.pop();
47
+ * y();
48
+ * ```
49
+ */
50
export function validateNoSetStateInRender(
51
fn: HIRFunction
52
+): Result<PostDominator<BlockId>, CompilerError> {
53
+ const unconditionalSetStateFunctions: Set<IdentifierId> = new Set();
54
+ return validateNoSetStateInRenderImpl(fn, unconditionalSetStateFunctions);
55
+}
56
+
57
+function validateNoSetStateInRenderImpl(
58
+ fn: HIRFunction,
59
+ unconditionalSetStateFunctions: Set<IdentifierId>
60
): Result<PostDominator<BlockId>, CompilerError> {
61
// Construct the set of blocks that is always reachable from the entry block.
62
const unconditionalBlocks = new Set<BlockId>();
80
if (unconditionalBlocks.has(block.id)) {
81
for (const instr of block.instructions) {
82
switch (instr.value.kind) {
83
+ case "LoadLocal": {
84
+ if (
85
+ unconditionalSetStateFunctions.has(
86
+ instr.value.place.identifier.id
87
+ )
88
+ ) {
89
+ unconditionalSetStateFunctions.add(instr.lvalue.identifier.id);
90
+ }
91
+ break;
92
+ }
93
+ case "StoreLocal": {
94
+ if (
95
+ unconditionalSetStateFunctions.has(
96
+ instr.value.value.identifier.id
97
+ )
98
+ ) {
99
+ unconditionalSetStateFunctions.add(
100
+ instr.value.lvalue.place.identifier.id
101
+ );
102
+ unconditionalSetStateFunctions.add(instr.lvalue.identifier.id);
103
+ }
104
+ break;
105
+ }
106
case "ObjectMethod":
107
case "FunctionExpression": {
108
if (fn.env.config.validateNoSetStateInRenderFunctionExpressions) {
49
- /*
50
- * TODO: setState's return value is considered Frozen, so the lambda's mutable range
51
- * does not get extended even if the lambda is called in render. The below only catches
52
- * setStates where the lambda has another instruction that extends its mutable range
53
- */
54
- const mutableRange = instr.lvalue.identifier.mutableRange;
55
- if (mutableRange.end > mutableRange.start + 1) {
56
- for (const operand of eachInstructionValueOperand(
57
- instr.value
58
- )) {
59
- validateNonSetState(errors, operand);
60
- }
109
+ if (
110
+ // faster-path to check if the function expression references a setState
111
+ [...eachInstructionValueOperand(instr.value)].some(
112
+ (operand) =>
113
+ isSetStateType(operand.identifier) ||
114
+ unconditionalSetStateFunctions.has(operand.identifier.id)
115
+ ) &&
116
+ // if yes, does it unconditonally call it?
117
+ validateNoSetStateInRenderImpl(
118
+ instr.value.loweredFunc.func,
119
+ unconditionalSetStateFunctions
120
+ ).isErr()
121
+ ) {
122
+ // This function expression unconditionally calls a setState
123
+ unconditionalSetStateFunctions.add(instr.lvalue.identifier.id);
124
}
125
}
126
break;
127
}
128
case "CallExpression": {
66
- validateNonSetState(errors, instr.value.callee);
129
+ validateNonSetState(
130
+ errors,
131
+ unconditionalSetStateFunctions,
132
+ instr.value.callee
133
+ );
134
break;
135
}
136
}
145
}
146
}
147
81
-function validateNonSetState(errors: CompilerError, operand: Place): void {
82
- if (isSetStateType(operand.identifier)) {
148
+function validateNonSetState(
149
+ errors: CompilerError,
150
+ unconditionalSetStateFunctions: Set<IdentifierId>,
151
+ operand: Place
152
+): void {
153
+ if (
154
+ isSetStateType(operand.identifier) ||
155
+ unconditionalSetStateFunctions.has(operand.identifier.id)
156
+ ) {
157
errors.push({
158
reason:
159
"This is an unconditional set state during render, which will trigger an infinite loop. (https://react.dev/reference/react/useState)",