| 1 | // Copyright (c) Meta Platforms, Inc. and affiliates. |
| 2 | // |
| 3 | // This source code is licensed under the MIT license found in the |
| 4 | // LICENSE file in the root directory of this source tree. |
| 5 | |
| 6 | //! Port of NameAnonymousFunctions from TypeScript. |
| 7 | //! |
| 8 | //! Generates descriptive names for anonymous function expressions based on |
| 9 | //! how they are used (assigned to variables, passed as arguments to hooks/functions, |
| 10 | //! used as JSX props, etc.). These names appear in React DevTools and error stacks. |
| 11 | //! |
| 12 | //! Conditional on `env.config.enable_name_anonymous_functions`. |
| 13 | |
| 14 | use rustc_hash::FxHashMap; |
| 15 | |
| 16 | use react_compiler_hir::environment::Environment; |
| 17 | use react_compiler_hir::object_shape::HookKind; |
| 18 | use react_compiler_hir::{ |
| 19 | FunctionId, HirFunction, IdentifierId, IdentifierName, Instruction, InstructionValue, |
| 20 | JsxAttribute, JsxTag, PlaceOrSpread, |
| 21 | }; |
| 22 | |
| 23 | /// Assign generated names to anonymous function expressions. |
| 24 | /// |
| 25 | /// Ported from TS `nameAnonymousFunctions` in `Transform/NameAnonymousFunctions.ts`. |
| 26 | pub fn name_anonymous_functions(func: &mut HirFunction, env: &mut Environment) { |
| 27 | let fn_id = match &func.id { |
| 28 | Some(id) => id.clone(), |
| 29 | None => return, |
| 30 | }; |
| 31 | |
| 32 | let nodes = name_anonymous_functions_impl(func, env); |
| 33 | |
| 34 | fn visit(node: &Node, prefix: &str, updates: &mut Vec<(FunctionId, String)>) { |
| 35 | if node.generated_name.is_some() && node.existing_name_hint.is_none() { |
| 36 | // Only add the prefix to anonymous functions regardless of nesting depth |
| 37 | let name = format!("{}{}]", prefix, node.generated_name.as_ref().unwrap()); |
| 38 | updates.push((node.function_id, name)); |
| 39 | } |
| 40 | // Whether or not we generated a name for the function at this node, |
| 41 | // traverse into its nested functions to assign them names |
| 42 | let fallback; |
| 43 | let label = if let Some(ref gen_name) = node.generated_name { |
| 44 | gen_name.as_str() |
| 45 | } else if let Some(ref existing) = node.fn_name { |
| 46 | existing.as_str() |
| 47 | } else { |
| 48 | fallback = "<anonymous>"; |
| 49 | fallback |
| 50 | }; |
| 51 | let next_prefix = format!("{}{} > ", prefix, label); |
| 52 | for inner in &node.inner { |
| 53 | visit(inner, &next_prefix, updates); |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | let mut updates: Vec<(FunctionId, String)> = Vec::new(); |
| 58 | let prefix = format!("{}[", fn_id); |
| 59 | for node in &nodes { |
| 60 | visit(node, &prefix, &mut updates); |
| 61 | } |
| 62 | |
| 63 | if updates.is_empty() { |
| 64 | return; |
| 65 | } |
| 66 | let update_map: FxHashMap<FunctionId, &String> = |
| 67 | updates.iter().map(|(fid, name)| (*fid, name)).collect(); |
| 68 | |
| 69 | // Apply name updates to the inner HirFunction in the arena |
| 70 | for (function_id, name) in &updates { |
| 71 | env.functions[function_id.0 as usize].name_hint = Some(name.clone()); |
| 72 | } |
| 73 | |
| 74 | // Update name_hint on FunctionExpression instruction values in the outer function |
| 75 | apply_name_hints_to_instructions(&mut func.instructions, &update_map); |
| 76 | |
| 77 | // Update name_hint on FunctionExpression instruction values in all arena functions |
| 78 | for i in 0..env.functions.len() { |
| 79 | // We need to temporarily take the instructions to avoid borrow issues |
| 80 | let mut instructions = std::mem::take(&mut env.functions[i].instructions); |
| 81 | apply_name_hints_to_instructions(&mut instructions, &update_map); |
| 82 | env.functions[i].instructions = instructions; |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | /// Apply name hints to FunctionExpression instruction values. |
| 87 | fn apply_name_hints_to_instructions( |
| 88 | instructions: &mut [Instruction], |
| 89 | update_map: &FxHashMap<FunctionId, &String>, |
| 90 | ) { |
| 91 | for instr in instructions.iter_mut() { |
| 92 | if let InstructionValue::FunctionExpression { |
| 93 | lowered_func, |
| 94 | name_hint, |
| 95 | .. |
| 96 | } = &mut instr.value |
| 97 | { |
| 98 | if let Some(new_name) = update_map.get(&lowered_func.func) { |
| 99 | *name_hint = Some((*new_name).clone()); |
| 100 | } |
| 101 | } |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | struct Node { |
| 106 | /// The FunctionId for the inner function (via lowered_func.func) |
| 107 | function_id: FunctionId, |
| 108 | /// The generated name for this anonymous function (set based on usage context) |
| 109 | generated_name: Option<String>, |
| 110 | /// The existing `name` on the FunctionExpression (non-anonymous functions have this) |
| 111 | fn_name: Option<String>, |
| 112 | /// Whether the inner HirFunction already has a name_hint |
| 113 | existing_name_hint: Option<String>, |
| 114 | /// Nested function nodes |
| 115 | inner: Vec<Node>, |
| 116 | } |
| 117 | |
| 118 | fn name_anonymous_functions_impl(func: &HirFunction, env: &Environment) -> Vec<Node> { |
| 119 | // Functions that we track to generate names for |
| 120 | let mut functions: FxHashMap<IdentifierId, usize> = FxHashMap::default(); |
| 121 | // Tracks temporaries that read from variables/globals/properties |
| 122 | let mut names: FxHashMap<IdentifierId, String> = FxHashMap::default(); |
| 123 | // Tracks all function nodes |
| 124 | let mut nodes: Vec<Node> = Vec::new(); |
| 125 | |
| 126 | for block in func.body.blocks.values() { |
| 127 | for instr_id in &block.instructions { |
| 128 | let instr = &func.instructions[instr_id.0 as usize]; |
| 129 | let lvalue_id = instr.lvalue.identifier; |
| 130 | match &instr.value { |
| 131 | InstructionValue::LoadGlobal { binding, .. } => { |
| 132 | names.insert(lvalue_id, binding.name().to_string()); |
| 133 | } |
| 134 | InstructionValue::LoadContext { place, .. } |
| 135 | | InstructionValue::LoadLocal { place, .. } => { |
| 136 | let ident = &env.identifiers[place.identifier.0 as usize]; |
| 137 | if let Some(IdentifierName::Named(ref name)) = ident.name { |
| 138 | names.insert(lvalue_id, name.clone()); |
| 139 | } |
| 140 | // If the loaded place was tracked as a function, propagate |
| 141 | if let Some(&node_idx) = functions.get(&place.identifier) { |
| 142 | functions.insert(lvalue_id, node_idx); |
| 143 | } |
| 144 | } |
| 145 | InstructionValue::PropertyLoad { |
| 146 | object, property, .. |
| 147 | } => { |
| 148 | if let Some(object_name) = names.get(&object.identifier) { |
| 149 | names.insert(lvalue_id, format!("{}.{}", object_name, property)); |
| 150 | } |
| 151 | } |
| 152 | InstructionValue::FunctionExpression { |
| 153 | name, lowered_func, .. |
| 154 | } => { |
| 155 | let inner_func = &env.functions[lowered_func.func.0 as usize]; |
| 156 | let inner = name_anonymous_functions_impl(inner_func, env); |
| 157 | let node = Node { |
| 158 | function_id: lowered_func.func, |
| 159 | generated_name: None, |
| 160 | fn_name: name.clone(), |
| 161 | existing_name_hint: inner_func.name_hint.clone(), |
| 162 | inner, |
| 163 | }; |
| 164 | let idx = nodes.len(); |
| 165 | nodes.push(node); |
| 166 | if name.is_none() { |
| 167 | // Only generate names for anonymous functions |
| 168 | functions.insert(lvalue_id, idx); |
| 169 | } |
| 170 | } |
| 171 | InstructionValue::StoreContext { |
| 172 | lvalue: store_lvalue, |
| 173 | value, |
| 174 | .. |
| 175 | } |
| 176 | | InstructionValue::StoreLocal { |
| 177 | lvalue: store_lvalue, |
| 178 | value, |
| 179 | .. |
| 180 | } => { |
| 181 | if let Some(&node_idx) = functions.get(&value.identifier) { |
| 182 | let node = &mut nodes[node_idx]; |
| 183 | let var_ident = &env.identifiers[store_lvalue.place.identifier.0 as usize]; |
| 184 | if node.generated_name.is_none() { |
| 185 | if let Some(IdentifierName::Named(ref var_name)) = var_ident.name { |
| 186 | node.generated_name = Some(var_name.clone()); |
| 187 | functions.remove(&value.identifier); |
| 188 | } |
| 189 | } |
| 190 | } |
| 191 | } |
| 192 | InstructionValue::CallExpression { callee, args, .. } => { |
| 193 | handle_call( |
| 194 | env, |
| 195 | func, |
| 196 | callee.identifier, |
| 197 | args, |
| 198 | &mut functions, |
| 199 | &names, |
| 200 | &mut nodes, |
| 201 | ); |
| 202 | } |
| 203 | InstructionValue::MethodCall { property, args, .. } => { |
| 204 | handle_call( |
| 205 | env, |
| 206 | func, |
| 207 | property.identifier, |
| 208 | args, |
| 209 | &mut functions, |
| 210 | &names, |
| 211 | &mut nodes, |
| 212 | ); |
| 213 | } |
| 214 | InstructionValue::JsxExpression { tag, props, .. } => { |
| 215 | for attr in props { |
| 216 | match attr { |
| 217 | JsxAttribute::SpreadAttribute { .. } => continue, |
| 218 | JsxAttribute::Attribute { |
| 219 | name: attr_name, |
| 220 | place, |
| 221 | } => { |
| 222 | if let Some(&node_idx) = functions.get(&place.identifier) { |
| 223 | let node = &mut nodes[node_idx]; |
| 224 | if node.generated_name.is_none() { |
| 225 | let element_name = match tag { |
| 226 | JsxTag::Builtin(builtin) => Some(builtin.name.clone()), |
| 227 | JsxTag::Place(tag_place) => { |
| 228 | names.get(&tag_place.identifier).cloned() |
| 229 | } |
| 230 | }; |
| 231 | let prop_name = match element_name { |
| 232 | None => attr_name.clone(), |
| 233 | Some(ref el_name) => { |
| 234 | format!("<{}>.{}", el_name, attr_name) |
| 235 | } |
| 236 | }; |
| 237 | node.generated_name = Some(prop_name); |
| 238 | functions.remove(&place.identifier); |
| 239 | } |
| 240 | } |
| 241 | } |
| 242 | } |
| 243 | } |
| 244 | } |
| 245 | _ => {} |
| 246 | } |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | nodes |
| 251 | } |
| 252 | |
| 253 | /// Handle CallExpression / MethodCall to generate names for function arguments. |
| 254 | fn handle_call( |
| 255 | env: &Environment, |
| 256 | _func: &HirFunction, |
| 257 | callee_id: IdentifierId, |
| 258 | args: &[PlaceOrSpread], |
| 259 | functions: &mut FxHashMap<IdentifierId, usize>, |
| 260 | names: &FxHashMap<IdentifierId, String>, |
| 261 | nodes: &mut Vec<Node>, |
| 262 | ) { |
| 263 | let callee_ident = &env.identifiers[callee_id.0 as usize]; |
| 264 | let callee_ty = &env.types[callee_ident.type_.0 as usize]; |
| 265 | let hook_kind = env.get_hook_kind_for_type(callee_ty).ok().flatten(); |
| 266 | |
| 267 | let callee_name: String = if let Some(hk) = hook_kind { |
| 268 | if *hk != HookKind::Custom { |
| 269 | hk.to_string() |
| 270 | } else { |
| 271 | names |
| 272 | .get(&callee_id) |
| 273 | .cloned() |
| 274 | .unwrap_or_else(|| "(anonymous)".to_string()) |
| 275 | } |
| 276 | } else { |
| 277 | names |
| 278 | .get(&callee_id) |
| 279 | .cloned() |
| 280 | .unwrap_or_else(|| "(anonymous)".to_string()) |
| 281 | }; |
| 282 | |
| 283 | // Count how many args are tracked functions |
| 284 | let fn_arg_count = args |
| 285 | .iter() |
| 286 | .filter(|arg| { |
| 287 | if let PlaceOrSpread::Place(p) = arg { |
| 288 | functions.contains_key(&p.identifier) |
| 289 | } else { |
| 290 | false |
| 291 | } |
| 292 | }) |
| 293 | .count(); |
| 294 | |
| 295 | for (i, arg) in args.iter().enumerate() { |
| 296 | let place = match arg { |
| 297 | PlaceOrSpread::Spread(_) => continue, |
| 298 | PlaceOrSpread::Place(p) => p, |
| 299 | }; |
| 300 | if let Some(&node_idx) = functions.get(&place.identifier) { |
| 301 | let node = &mut nodes[node_idx]; |
| 302 | if node.generated_name.is_none() { |
| 303 | let generated_name = if fn_arg_count > 1 { |
| 304 | format!("{}(arg{})", callee_name, i) |
| 305 | } else { |
| 306 | format!("{}()", callee_name) |
| 307 | }; |
| 308 | node.generated_name = Some(generated_name); |
| 309 | functions.remove(&place.identifier); |
| 310 | } |
| 311 | } |
| 312 | } |
| 313 | } |