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 rustc_hash::FxHashMap;
15
16use react_compiler_hir::environment::Environment;
17use react_compiler_hir::object_shape::HookKind;
18use 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`.
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(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.
87fn 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
105struct 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
118fn 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.
254fn 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}