main
md 299 lines 8.42 KB
Rendered Raw
1 # validateUseMemo
2
3 ## File
4 `src/Validation/ValidateUseMemo.ts`
5
6 ## Purpose
7 This validation pass ensures that `useMemo()` callbacks follow React's requirements. The pass checks for several common mistakes that developers make when using `useMemo()`:
8
9 1. Callbacks should not accept parameters (useMemo callbacks are called with no arguments)
10 2. Callbacks should not be async or generator functions (must return a value synchronously)
11 3. Callbacks should not reassign variables declared outside the callback (must be pure)
12 4. Callbacks should return a value (useMemo is for computing values, not side effects)
13 5. The result of useMemo should be used (not discarded)
14
15 ## Input Invariants
16 - The function has been lowered to HIR
17 - `useMemo` is either imported directly or accessed via `React.useMemo`
18 - Function expressions have been lowered with their parameters and async/generator flags preserved
19
20 ## Validation Rules
21
22 ### Rule 1: No Parameters
23 useMemo callbacks must not accept parameters.
24
25 **Error:**
26 ```
27 Error: useMemo() callbacks may not accept parameters
28
29 useMemo() callbacks are called by React to cache calculations across re-renders. They should not take parameters. Instead, directly reference the props, state, or local variables needed for the computation.
30 ```
31
32 ### Rule 2: No Async or Generator Functions
33 useMemo callbacks must synchronously return a value.
34
35 **Error:**
36 ```
37 Error: useMemo() callbacks may not be async or generator functions
38
39 useMemo() callbacks are called once and must synchronously return a value.
40 ```
41
42 ### Rule 3: No Reassigning Outer Variables
43 useMemo callbacks cannot reassign variables declared outside the callback.
44
45 **Error:**
46 ```
47 Error: useMemo() callbacks may not reassign variables declared outside of the callback
48
49 useMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function.
50 ```
51
52 ### Rule 4: Must Return a Value (when `validateNoVoidUseMemo` is enabled)
53 useMemo callbacks should return a value.
54
55 **Error:**
56 ```
57 Error: useMemo() callbacks must return a value
58
59 This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects.
60 ```
61
62 ### Rule 5: Result Must Be Used (when `validateNoVoidUseMemo` is enabled)
63 The result of useMemo should be used somewhere.
64
65 **Error:**
66 ```
67 Error: useMemo() result is unused
68
69 This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects.
70 ```
71
72 ## Algorithm
73
74 ### Phase 1: Track useMemo References
75 ```typescript
76 const useMemos = new Set<IdentifierId>();
77 const react = new Set<IdentifierId>();
78 const functions = new Map<IdentifierId, FunctionExpression>();
79 const unusedUseMemos = new Map<IdentifierId, SourceLocation>();
80 ```
81
82 The pass tracks:
83 - Direct `useMemo` imports via `LoadGlobal`
84 - `React` imports to detect `React.useMemo` pattern
85 - Function expressions that might be useMemo callbacks
86 - Unused useMemo results
87
88 ### Phase 2: Identify useMemo Calls
89 ```typescript
90 for (const instr of block.instructions) {
91 switch (value.kind) {
92 case 'LoadGlobal':
93 if (value.binding.name === 'useMemo') {
94 useMemos.add(lvalue.identifier.id);
95 } else if (value.binding.name === 'React') {
96 react.add(lvalue.identifier.id);
97 }
98 break;
99 case 'PropertyLoad':
100 if (react.has(value.object.identifier.id) && value.property === 'useMemo') {
101 useMemos.add(lvalue.identifier.id);
102 }
103 break;
104 case 'CallExpression':
105 case 'MethodCall':
106 // Check if callee is useMemo
107 const callee = value.kind === 'CallExpression' ? value.callee : value.property;
108 if (useMemos.has(callee.identifier.id) && value.args.length > 0) {
109 // Validate the callback
110 }
111 break;
112 }
113 }
114 ```
115
116 ### Phase 3: Validate Callback
117 For each useMemo call, the pass retrieves the callback function expression and validates:
118
119 ```typescript
120 const body = functions.get(arg.identifier.id);
121
122 // Check for parameters
123 if (body.loweredFunc.func.params.length > 0) {
124 errors.push("useMemo() callbacks may not accept parameters");
125 }
126
127 // Check for async/generator
128 if (body.loweredFunc.func.async || body.loweredFunc.func.generator) {
129 errors.push("useMemo() callbacks may not be async or generator functions");
130 }
131
132 // Check for context variable reassignment
133 validateNoContextVariableAssignment(body.loweredFunc.func, errors);
134
135 // Check for return value (if config enabled)
136 if (fn.env.config.validateNoVoidUseMemo) {
137 if (!hasNonVoidReturn(body.loweredFunc.func)) {
138 errors.push("useMemo() callbacks must return a value");
139 }
140 }
141 ```
142
143 ### Phase 4: Validate No Context Variable Assignment
144 ```typescript
145 function validateNoContextVariableAssignment(fn: HIRFunction, errors: CompilerError) {
146 const context = new Set(fn.context.map(place => place.identifier.id));
147 for (const block of fn.body.blocks.values()) {
148 for (const instr of block.instructions) {
149 if (value.kind === 'StoreContext') {
150 if (context.has(value.lvalue.place.identifier.id)) {
151 errors.push("Cannot reassign variable");
152 }
153 }
154 }
155 }
156 }
157 ```
158
159 ### Phase 5: Check for Unused Results
160 ```typescript
161 // Track which useMemo results are referenced
162 for (const operand of eachInstructionValueOperand(value)) {
163 unusedUseMemos.delete(operand.identifier.id);
164 }
165
166 // At the end, report any unused useMemos
167 for (const loc of unusedUseMemos.values()) {
168 errors.push("useMemo() result is unused");
169 }
170 ```
171
172 ### Return Value Helper
173 ```typescript
174 function hasNonVoidReturn(func: HIRFunction): boolean {
175 for (const [, block] of func.body.blocks) {
176 if (block.terminal.kind === 'return') {
177 if (block.terminal.returnVariant === 'Explicit' ||
178 block.terminal.returnVariant === 'Implicit') {
179 return true;
180 }
181 }
182 }
183 return false;
184 }
185 ```
186
187 ## Edge Cases
188
189 ### React.useMemo vs useMemo
190 The pass handles both import styles:
191 ```javascript
192 import {useMemo} from 'react';
193 useMemo(() => x, [x]);
194
195 import React from 'react';
196 React.useMemo(() => x, [x]);
197 ```
198
199 ### Immediately Used Results
200 Results that are used immediately don't trigger the "unused" warning:
201 ```javascript
202 const x = useMemo(() => compute(), [dep]);
203 return x; // x is used
204 ```
205
206 ### Void Return Detection
207 The pass checks for explicit and implicit returns. A function with only `return;` statements (void returns) will trigger the "must return a value" error.
208
209 ### VoidUseMemo Errors as Logged Errors
210 The void useMemo errors (no return value, unused result) are logged via `fn.env.logErrors()` rather than thrown immediately. This allows them to be treated differently (e.g., as warnings) based on configuration.
211
212 ## TODOs
213 None in the source file.
214
215 ## Example
216
217 ### Fixture: `error.invalid-useMemo-callback-args.js`
218
219 **Input:**
220 ```javascript
221 function component(a, b) {
222 let x = useMemo(c => a, []);
223 return x;
224 }
225 ```
226
227 **Error:**
228 ```
229 Error: useMemo() callbacks may not accept parameters
230
231 useMemo() callbacks are called by React to cache calculations across re-renders. They should not take parameters. Instead, directly reference the props, state, or local variables needed for the computation.
232
233 error.invalid-useMemo-callback-args.ts:2:18
234 1 | function component(a, b) {
235 > 2 | let x = useMemo(c => a, []);
236 | ^ Callbacks with parameters are not supported
237 3 | return x;
238 4 | }
239 ```
240
241 ### Fixture: `error.invalid-useMemo-async-callback.js`
242
243 **Input:**
244 ```javascript
245 function component(a, b) {
246 let x = useMemo(async () => {
247 await a;
248 }, []);
249 return x;
250 }
251 ```
252
253 **Error:**
254 ```
255 Error: useMemo() callbacks may not be async or generator functions
256
257 useMemo() callbacks are called once and must synchronously return a value.
258
259 error.invalid-useMemo-async-callback.ts:2:18
260 1 | function component(a, b) {
261 > 2 | let x = useMemo(async () => {
262 | ^^^^^^^^^^^^^
263 > 3 | await a;
264 | ^^^^^^^^^^^^
265 > 4 | }, []);
266 | ^^^^ Async and generator functions are not supported
267 ```
268
269 ### Fixture: `error.invalid-reassign-variable-in-usememo.js`
270
271 **Input:**
272 ```javascript
273 function Component() {
274 let x;
275 const y = useMemo(() => {
276 let z;
277 x = [];
278 z = true;
279 return z;
280 }, []);
281 return [x, y];
282 }
283 ```
284
285 **Error:**
286 ```
287 Error: useMemo() callbacks may not reassign variables declared outside of the callback
288
289 useMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function.
290
291 error.invalid-reassign-variable-in-usememo.ts:5:4
292 3 | const y = useMemo(() => {
293 4 | let z;
294 > 5 | x = [];
295 | ^ Cannot reassign variable
296 6 | z = true;
297 7 | return z;
298 8 | }, []);
299 ```