1use std::path::Path;
29
30use oxc_allocator::Allocator;
31#[allow(clippy::wildcard_imports, reason = "many AST types used")]
32use oxc_ast::ast::*;
33use oxc_ast_visit::{Visit, walk};
34use oxc_parser::Parser;
35use oxc_semantic::ScopeFlags;
36use oxc_span::{SourceType, Span};
37
38#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct InventoryEntry {
44 pub name: String,
46 pub line: u32,
48}
49
50struct InventoryVisitor<'a> {
52 line_offsets: &'a [u32],
53 entries: Vec<InventoryEntry>,
54 pending_name: Option<String>,
56 anonymous_counter: u32,
58}
59
60impl<'a> InventoryVisitor<'a> {
61 const fn new(line_offsets: &'a [u32]) -> Self {
62 Self {
63 line_offsets,
64 entries: Vec::new(),
65 pending_name: None,
66 anonymous_counter: 0,
67 }
68 }
69
70 fn resolve_name(&mut self, explicit: Option<&str>) -> String {
80 let n = self.anonymous_counter;
81 self.anonymous_counter += 1;
82 if let Some(pending) = self.pending_name.take() {
83 return pending;
84 }
85 if let Some(name) = explicit {
86 return name.to_owned();
87 }
88 format!("(anonymous_{n})")
89 }
90
91 fn record(&mut self, name: String, span: Span) {
92 let (line, _col) =
93 fallow_types::extract::byte_offset_to_line_col(self.line_offsets, span.start);
94 self.entries.push(InventoryEntry { name, line });
95 }
96}
97
98impl<'ast> Visit<'ast> for InventoryVisitor<'_> {
99 fn visit_function(&mut self, func: &Function<'ast>, flags: ScopeFlags) {
100 if func.body.is_none() {
106 walk::walk_function(self, func, flags);
107 return;
108 }
109 let name = self.resolve_name(func.id.as_ref().map(|id| id.name.as_str()));
110 self.record(name, func.span);
111 walk::walk_function(self, func, flags);
112 }
113
114 fn visit_arrow_function_expression(&mut self, arrow: &ArrowFunctionExpression<'ast>) {
115 let name = self.resolve_name(None);
116 self.record(name, arrow.span);
117 walk::walk_arrow_function_expression(self, arrow);
118 }
119
120 fn visit_method_definition(&mut self, method: &MethodDefinition<'ast>) {
121 if let Some(name) = method.key.static_name() {
122 self.pending_name = Some(name.to_string());
123 }
124 walk::walk_method_definition(self, method);
125 self.pending_name = None;
126 }
127
128 fn visit_variable_declarator(&mut self, decl: &VariableDeclarator<'ast>) {
129 if let Some(id) = decl.id.get_binding_identifier()
130 && decl.init.as_ref().is_some_and(|init| {
131 matches!(
132 init,
133 Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_)
134 )
135 })
136 {
137 self.pending_name = Some(id.name.to_string());
138 }
139 walk::walk_variable_declarator(self, decl);
140 self.pending_name = None;
141 }
142
143 fn visit_object_property(&mut self, prop: &ObjectProperty<'ast>) {
144 self.pending_name = None;
151 walk::walk_object_property(self, prop);
152 self.pending_name = None;
153 }
154}
155
156#[must_use]
166pub fn walk_source(path: &Path, source: &str) -> Vec<InventoryEntry> {
167 let source_type = SourceType::from_path(path).unwrap_or_default();
168 let allocator = Allocator::default();
169 let parser_return = Parser::new(&allocator, source, source_type).parse();
170
171 let line_offsets = fallow_types::extract::compute_line_offsets(source);
172 let mut visitor = InventoryVisitor::new(&line_offsets);
173 visitor.visit_program(&parser_return.program);
174
175 if visitor.entries.is_empty() && !source_type.is_jsx() {
180 let jsx_type = if source_type.is_typescript() {
181 SourceType::tsx()
182 } else {
183 SourceType::jsx()
184 };
185 let allocator2 = Allocator::default();
186 let retry_return = Parser::new(&allocator2, source, jsx_type).parse();
187 let mut retry_visitor = InventoryVisitor::new(&line_offsets);
188 retry_visitor.visit_program(&retry_return.program);
189 if !retry_visitor.entries.is_empty() {
190 return retry_visitor.entries;
191 }
192 }
193
194 visitor.entries
195}
196
197#[cfg(all(test, not(miri)))]
198mod tests {
199 use super::*;
200 use std::path::PathBuf;
201
202 fn walk(source: &str) -> Vec<InventoryEntry> {
203 walk_source(&PathBuf::from("test.ts"), source)
204 }
205
206 #[test]
207 fn named_function_declaration_uses_its_own_name() {
208 let entries = walk("function foo() { return 1; }");
209 assert_eq!(entries.len(), 1);
210 assert_eq!(entries[0].name, "foo");
211 assert_eq!(entries[0].line, 1);
212 }
213
214 #[test]
215 fn const_arrow_captures_binding_name() {
216 let entries = walk("const bar = () => 42;");
217 assert_eq!(entries.len(), 1);
218 assert_eq!(entries[0].name, "bar");
219 }
220
221 #[test]
222 fn const_function_expression_captures_binding_name_not_fn_id() {
223 let entries = walk("const outer = function inner() { return 1; };");
227 assert_eq!(entries.len(), 1);
228 assert_eq!(entries[0].name, "outer");
229 }
230
231 #[test]
232 fn class_methods_use_method_names() {
233 let entries = walk(
234 r"
235 class Foo {
236 bar() { return 1; }
237 baz() { return 2; }
238 }",
239 );
240 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
241 assert_eq!(names, vec!["bar", "baz"]);
242 }
243
244 #[test]
245 fn anonymous_arrow_passed_as_argument_uses_counter() {
246 let entries = walk("setTimeout(() => { console.log('hi'); }, 10);");
247 assert_eq!(entries.len(), 1);
248 assert_eq!(entries[0].name, "(anonymous_0)");
249 }
250
251 #[test]
252 fn multiple_anonymous_functions_increment_counter_in_source_order() {
253 let entries = walk(
254 r"
255 [1, 2, 3].map(() => 1);
256 [4, 5, 6].filter(() => true);
257 ",
258 );
259 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
260 assert_eq!(names, vec!["(anonymous_0)", "(anonymous_1)"]);
261 }
262
263 #[test]
264 fn named_function_still_advances_counter_matching_instrumenter() {
265 let entries = walk(
269 r"
270 function named() { return 1; }
271 [1].map(() => 2);
272 ",
273 );
274 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
275 assert_eq!(names, vec!["named", "(anonymous_1)"]);
276 }
277
278 #[test]
279 fn anonymous_after_named_chain_uses_next_counter_value() {
280 let entries = walk(
284 r"
285 function a() {}
286 function b() {}
287 function c() {}
288 const d = () => 4;
289 ",
290 );
291 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
292 assert_eq!(names, vec!["a", "b", "c", "d"]);
296 }
297
298 #[test]
299 fn typescript_overload_signatures_dont_emit_or_advance_counter() {
300 let entries = walk(
305 r"
306 function foo(): number;
307 function foo(s: string): string;
308 function foo(s?: string): number | string { return s ? s : 1; }
309 [1].map(() => 2);
310 ",
311 );
312 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
313 assert_eq!(names, vec!["foo", "(anonymous_1)"]);
314 }
315
316 #[test]
317 fn export_default_named_function_keeps_explicit_name() {
318 let entries = walk("export default function foo() { return 1; }");
319 assert_eq!(entries.len(), 1);
320 assert_eq!(entries[0].name, "foo");
321 }
322
323 #[test]
324 fn export_default_anonymous_function_uses_counter() {
325 let entries = walk("export default function() { return 1; }");
326 assert_eq!(entries.len(), 1);
327 assert_eq!(entries[0].name, "(anonymous_0)");
328 }
329
330 #[test]
331 fn nested_function_numbered_after_parent_in_traversal_order() {
332 let entries = walk(
333 r"
334 function outer() {
335 return function() { return 1; };
336 }",
337 );
338 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
339 assert_eq!(names, vec!["outer", "(anonymous_1)"]);
343 }
344
345 #[test]
346 fn line_number_is_one_based_from_source_start() {
347 let entries = walk("\n\nfunction atLineThree() {}");
348 assert_eq!(entries.len(), 1);
349 assert_eq!(entries[0].line, 3);
350 }
351
352 #[test]
353 fn short_jsx_in_js_file_retries_with_jsx_parser() {
354 let entries = walk_source(&PathBuf::from("component.js"), "const A = () => <div />;");
355 assert_eq!(entries.len(), 1);
356 assert_eq!(entries[0].name, "A");
357 assert_eq!(entries[0].line, 1);
358 }
359
360 #[test]
361 fn object_method_shorthand_uses_anonymous_counter() {
362 let entries = walk("const obj = { run() { return 1; } };");
363 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
364 assert_eq!(names, vec!["(anonymous_0)"]);
365 }
366
367 #[test]
368 fn class_property_arrow_uses_anonymous_counter() {
369 let entries = walk(
370 r"
371 class Foo {
372 bar = () => 1;
373 }",
374 );
375 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
376 assert_eq!(names, vec!["(anonymous_0)"]);
377 }
378}