Skip to main content

react_compiler_optimization/
name_anonymous_functions.rs

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