main
rs 319 lines 11.9 KB
Raw
1 use rustc_hash::{FxHashMap, FxHashSet};
2
3 use react_compiler_diagnostics::{
4 CompilerDiagnostic, CompilerDiagnosticDetail, CompilerError, ErrorCategory, SourceLocation,
5 };
6 use react_compiler_hir::environment::Environment;
7 use react_compiler_hir::visitors::{
8 each_instruction_value_operand_with_functions, each_terminal_operand,
9 };
10 use react_compiler_hir::{
11 FunctionId, HirFunction, IdentifierId, InstructionValue, ParamPattern, Place, PlaceOrSpread,
12 ReturnVariant, Terminal,
13 };
14
15 /// Validates useMemo() usage patterns.
16 ///
17 /// Port of ValidateUseMemo.ts.
18 /// Returns VoidUseMemo errors separately (for logging via logErrors, not as compile errors).
19 pub fn validate_use_memo(func: &HirFunction, env: &mut Environment) -> CompilerError {
20 validate_use_memo_impl(
21 func,
22 &env.functions,
23 &mut env.errors,
24 env.config.validate_no_void_use_memo,
25 )
26 }
27
28 /// Information about a FunctionExpression needed for validation.
29 struct FuncExprInfo {
30 func_id: FunctionId,
31 loc: Option<SourceLocation>,
32 }
33
34 fn validate_use_memo_impl(
35 func: &HirFunction,
36 functions: &[HirFunction],
37 errors: &mut CompilerError,
38 validate_no_void_use_memo: bool,
39 ) -> CompilerError {
40 let mut void_memo_errors = CompilerError::new();
41 let mut use_memos: FxHashSet<IdentifierId> = FxHashSet::default();
42 let mut react: FxHashSet<IdentifierId> = FxHashSet::default();
43 let mut func_exprs: FxHashMap<IdentifierId, FuncExprInfo> = FxHashMap::default();
44 let mut unused_use_memos: FxHashMap<IdentifierId, (SourceLocation, Option<String>)> =
45 FxHashMap::default();
46
47 for (_block_id, block) in &func.body.blocks {
48 for &instr_id in &block.instructions {
49 let instr = &func.instructions[instr_id.0 as usize];
50 let lvalue = &instr.lvalue;
51 let value = &instr.value;
52
53 // Remove used operands from unused_use_memos
54 if !unused_use_memos.is_empty() {
55 for operand_id in each_instruction_value_operand_ids(value, functions) {
56 unused_use_memos.remove(&operand_id);
57 }
58 }
59
60 match value {
61 InstructionValue::LoadGlobal { binding, .. } => {
62 let name = binding.name();
63 if name == "useMemo" {
64 use_memos.insert(lvalue.identifier);
65 } else if name == "React" {
66 react.insert(lvalue.identifier);
67 }
68 }
69 InstructionValue::PropertyLoad {
70 object, property, ..
71 } => {
72 if react.contains(&object.identifier) {
73 if let react_compiler_hir::PropertyLiteral::String(prop_name) = property {
74 if prop_name == "useMemo" {
75 use_memos.insert(lvalue.identifier);
76 }
77 }
78 }
79 }
80 InstructionValue::FunctionExpression {
81 lowered_func, loc, ..
82 } => {
83 func_exprs.insert(
84 lvalue.identifier,
85 FuncExprInfo {
86 func_id: lowered_func.func,
87 loc: *loc,
88 },
89 );
90 }
91 InstructionValue::CallExpression { callee, args, .. } => {
92 handle_possible_use_memo_call(
93 functions,
94 errors,
95 &mut void_memo_errors,
96 &use_memos,
97 &func_exprs,
98 &mut unused_use_memos,
99 callee,
100 args,
101 lvalue,
102 validate_no_void_use_memo,
103 );
104 }
105 InstructionValue::MethodCall { property, args, .. } => {
106 handle_possible_use_memo_call(
107 functions,
108 errors,
109 &mut void_memo_errors,
110 &use_memos,
111 &func_exprs,
112 &mut unused_use_memos,
113 property,
114 args,
115 lvalue,
116 validate_no_void_use_memo,
117 );
118 }
119 _ => {}
120 }
121 }
122
123 // Check terminal operands for unused_use_memos
124 if !unused_use_memos.is_empty() {
125 for operand_id in each_terminal_operand_ids(&block.terminal) {
126 unused_use_memos.remove(&operand_id);
127 }
128 }
129 }
130
131 // Report unused useMemo results
132 if !unused_use_memos.is_empty() {
133 for (loc, ident_name) in unused_use_memos.values() {
134 void_memo_errors.push_diagnostic(
135 CompilerDiagnostic::new(
136 ErrorCategory::VoidUseMemo,
137 "useMemo() result is unused",
138 Some(
139 "This useMemo() value is unused. useMemo() is for computing and caching values, not for arbitrary side effects"
140 .to_string(),
141 ),
142 )
143 .with_detail(CompilerDiagnosticDetail::Error {
144 loc: Some(*loc),
145 message: Some("useMemo() result is unused".to_string()),
146 identifier_name: ident_name.clone(),
147 }),
148 );
149 }
150 }
151
152 void_memo_errors
153 }
154
155 #[allow(clippy::too_many_arguments)]
156 fn handle_possible_use_memo_call(
157 functions: &[HirFunction],
158 errors: &mut CompilerError,
159 void_memo_errors: &mut CompilerError,
160 use_memos: &FxHashSet<IdentifierId>,
161 func_exprs: &FxHashMap<IdentifierId, FuncExprInfo>,
162 unused_use_memos: &mut FxHashMap<IdentifierId, (SourceLocation, Option<String>)>,
163 callee: &Place,
164 args: &[PlaceOrSpread],
165 lvalue: &Place,
166 validate_no_void_use_memo: bool,
167 ) {
168 let is_use_memo = use_memos.contains(&callee.identifier);
169 if !is_use_memo || args.is_empty() {
170 return;
171 }
172
173 let first_arg = match &args[0] {
174 PlaceOrSpread::Place(place) => place,
175 PlaceOrSpread::Spread(_) => return,
176 };
177
178 let body_info = match func_exprs.get(&first_arg.identifier) {
179 Some(info) => info,
180 None => return,
181 };
182
183 let body_func = &functions[body_info.func_id.0 as usize];
184
185 // Validate no parameters
186 if !body_func.params.is_empty() {
187 let first_param = &body_func.params[0];
188 let loc = match first_param {
189 ParamPattern::Place(place) => place.loc,
190 ParamPattern::Spread(spread) => spread.place.loc,
191 };
192 errors.push_diagnostic(
193 CompilerDiagnostic::new(
194 ErrorCategory::UseMemo,
195 "useMemo() callbacks may not accept parameters",
196 Some(
197 "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"
198 .to_string(),
199 ),
200 )
201 .with_detail(CompilerDiagnosticDetail::Error {
202 loc,
203 message: Some("Callbacks with parameters are not supported".to_string()),
204 identifier_name: None,
205 }),
206 );
207 }
208
209 // Validate not async or generator
210 if body_func.is_async || body_func.generator {
211 errors.push_diagnostic(
212 CompilerDiagnostic::new(
213 ErrorCategory::UseMemo,
214 "useMemo() callbacks may not be async or generator functions",
215 Some(
216 "useMemo() callbacks are called once and must synchronously return a value"
217 .to_string(),
218 ),
219 )
220 .with_detail(CompilerDiagnosticDetail::Error {
221 loc: body_info.loc,
222 message: Some("Async and generator functions are not supported".to_string()),
223 identifier_name: None,
224 }),
225 );
226 }
227
228 // Validate no context variable assignment
229 validate_no_context_variable_assignment(body_func, errors);
230
231 if validate_no_void_use_memo && !has_non_void_return(body_func) {
232 void_memo_errors.push_diagnostic(
233 CompilerDiagnostic::new(
234 ErrorCategory::VoidUseMemo,
235 "useMemo() callbacks must return a value",
236 Some(
237 "This useMemo() callback doesn't return a value. useMemo() is for computing and caching values, not for arbitrary side effects"
238 .to_string(),
239 ),
240 )
241 .with_detail(CompilerDiagnosticDetail::Error {
242 loc: body_info.loc,
243 message: Some("useMemo() callbacks must return a value".to_string()),
244 identifier_name: None,
245 }),
246 );
247 } else if validate_no_void_use_memo {
248 if let Some(callee_loc) = callee.loc {
249 // The callee is always useMemo/React.useMemo since we checked is_use_memo above.
250 // The identifierName in Babel's AST SourceLocation is "useMemo".
251 unused_use_memos.insert(lvalue.identifier, (callee_loc, Some("useMemo".to_string())));
252 }
253 }
254 }
255
256 fn validate_no_context_variable_assignment(func: &HirFunction, errors: &mut CompilerError) {
257 let context: FxHashSet<IdentifierId> =
258 func.context.iter().map(|place| place.identifier).collect();
259
260 for (_block_id, block) in &func.body.blocks {
261 for &instr_id in &block.instructions {
262 let instr = &func.instructions[instr_id.0 as usize];
263 if let InstructionValue::StoreContext { lvalue, .. } = &instr.value {
264 if context.contains(&lvalue.place.identifier) {
265 errors.push_diagnostic(
266 CompilerDiagnostic::new(
267 ErrorCategory::UseMemo,
268 "useMemo() callbacks may not reassign variables declared outside of the callback",
269 Some(
270 "useMemo() callbacks must be pure functions and cannot reassign variables defined outside of the callback function"
271 .to_string(),
272 ),
273 )
274 .with_detail(CompilerDiagnosticDetail::Error {
275 loc: lvalue.place.loc,
276 message: Some("Cannot reassign variable".to_string()),
277 identifier_name: None,
278 }),
279 );
280 }
281 }
282 }
283 }
284 }
285
286 fn has_non_void_return(func: &HirFunction) -> bool {
287 for (_block_id, block) in &func.body.blocks {
288 if let Terminal::Return { return_variant, .. } = &block.terminal {
289 if matches!(
290 return_variant,
291 ReturnVariant::Explicit | ReturnVariant::Implicit
292 ) {
293 return true;
294 }
295 }
296 }
297 false
298 }
299
300 /// Collect all operand IdentifierIds from an InstructionValue.
301 /// Thin wrapper around canonical `each_instruction_value_operand_with_functions` that maps to ids.
302 fn each_instruction_value_operand_ids(
303 value: &InstructionValue,
304 functions: &[HirFunction],
305 ) -> Vec<IdentifierId> {
306 each_instruction_value_operand_with_functions(value, functions)
307 .into_iter()
308 .map(|p| p.identifier)
309 .collect()
310 }
311
312 /// Collect all operand IdentifierIds from a Terminal.
313 /// Thin wrapper around canonical `each_terminal_operand` that maps to ids.
314 fn each_terminal_operand_ids(terminal: &Terminal) -> Vec<IdentifierId> {
315 each_terminal_operand(terminal)
316 .into_iter()
317 .map(|p| p.identifier)
318 .collect()
319 }