main
rs 313 lines 12.4 KB
Raw
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 }