1use rustc_hash::{FxHashMap, FxHashSet};23use react_compiler_diagnostics::{4 CompilerDiagnostic, CompilerDiagnosticDetail, CompilerError, ErrorCategory, SourceLocation,5};6use react_compiler_hir::environment::Environment;7use react_compiler_hir::visitors::{8 each_instruction_value_operand_with_functions, each_terminal_operand,9};10use react_compiler_hir::{11 FunctionId, HirFunction, IdentifierId, InstructionValue, ParamPattern, Place, PlaceOrSpread,12 ReturnVariant, Terminal,13};1415/// Validates useMemo() usage patterns.16///17/// Port of ValidateUseMemo.ts.18/// Returns VoidUseMemo errors separately (for logging via logErrors, not as compile errors).19pub 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}2728/// Information about a FunctionExpression needed for validation.29struct FuncExprInfo {30 func_id: FunctionId,31 loc: Option<SourceLocation>,32}3334fn 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();4647 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;5253 // Remove used operands from unused_use_memos54 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 }5960 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 }122123 // Check terminal operands for unused_use_memos124 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 }130131 // Report unused useMemo results132 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 }151152 void_memo_errors153}154155#[allow(clippy::too_many_arguments)]156fn 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 }172173 let first_arg = match &args[0] {174 PlaceOrSpread::Place(place) => place,175 PlaceOrSpread::Spread(_) => return,176 };177178 let body_info = match func_exprs.get(&first_arg.identifier) {179 Some(info) => info,180 None => return,181 };182183 let body_func = &functions[body_info.func_id.0 as usize];184185 // Validate no parameters186 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 }208209 // Validate not async or generator210 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 }227228 // Validate no context variable assignment229 validate_no_context_variable_assignment(body_func, errors);230231 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}255256fn validate_no_context_variable_assignment(func: &HirFunction, errors: &mut CompilerError) {257 let context: FxHashSet<IdentifierId> =258 func.context.iter().map(|place| place.identifier).collect();259260 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}285286fn 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::Implicit292 ) {293 return true;294 }295 }296 }297 false298}299300/// Collect all operand IdentifierIds from an InstructionValue.301/// Thin wrapper around canonical `each_instruction_value_operand_with_functions` that maps to ids.302fn 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}311312/// Collect all operand IdentifierIds from a Terminal.313/// Thin wrapper around canonical `each_terminal_operand` that maps to ids.314fn each_terminal_operand_ids(terminal: &Terminal) -> Vec<IdentifierId> {315 each_terminal_operand(terminal)316 .into_iter()317 .map(|p| p.identifier)318 .collect()319}