use std::path::Path;
use oxc_allocator::Allocator;
#[allow(clippy::wildcard_imports, reason = "many AST types used")]
use oxc_ast::ast::*;
use oxc_ast_visit::{Visit, walk};
use oxc_parser::Parser;
use oxc_semantic::ScopeFlags;
use oxc_span::{SourceType, Span};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InventoryEntry {
pub name: String,
pub line: u32,
}
struct InventoryVisitor<'a> {
line_offsets: &'a [u32],
entries: Vec<InventoryEntry>,
pending_name: Option<String>,
anonymous_counter: u32,
}
impl<'a> InventoryVisitor<'a> {
const fn new(line_offsets: &'a [u32]) -> Self {
Self {
line_offsets,
entries: Vec::new(),
pending_name: None,
anonymous_counter: 0,
}
}
fn resolve_name(&mut self, explicit: Option<&str>) -> String {
let n = self.anonymous_counter;
self.anonymous_counter += 1;
if let Some(pending) = self.pending_name.take() {
return pending;
}
if let Some(name) = explicit {
return name.to_owned();
}
format!("(anonymous_{n})")
}
fn record(&mut self, name: String, span: Span) {
let (line, _col) =
fallow_types::extract::byte_offset_to_line_col(self.line_offsets, span.start);
self.entries.push(InventoryEntry { name, line });
}
}
impl<'ast> Visit<'ast> for InventoryVisitor<'_> {
fn visit_function(&mut self, func: &Function<'ast>, flags: ScopeFlags) {
if func.body.is_none() {
walk::walk_function(self, func, flags);
return;
}
let name = self.resolve_name(func.id.as_ref().map(|id| id.name.as_str()));
self.record(name, func.span);
walk::walk_function(self, func, flags);
}
fn visit_arrow_function_expression(&mut self, arrow: &ArrowFunctionExpression<'ast>) {
let name = self.resolve_name(None);
self.record(name, arrow.span);
walk::walk_arrow_function_expression(self, arrow);
}
fn visit_method_definition(&mut self, method: &MethodDefinition<'ast>) {
if let Some(name) = method.key.static_name() {
self.pending_name = Some(name.to_string());
}
walk::walk_method_definition(self, method);
self.pending_name = None;
}
fn visit_variable_declarator(&mut self, decl: &VariableDeclarator<'ast>) {
if let Some(id) = decl.id.get_binding_identifier()
&& decl.init.as_ref().is_some_and(|init| {
matches!(
init,
Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_)
)
})
{
self.pending_name = Some(id.name.to_string());
}
walk::walk_variable_declarator(self, decl);
self.pending_name = None;
}
fn visit_object_property(&mut self, prop: &ObjectProperty<'ast>) {
self.pending_name = None;
walk::walk_object_property(self, prop);
self.pending_name = None;
}
}
#[must_use]
pub fn walk_source(path: &Path, source: &str) -> Vec<InventoryEntry> {
let source_type = SourceType::from_path(path).unwrap_or_default();
let allocator = Allocator::default();
let parser_return = Parser::new(&allocator, source, source_type).parse();
let line_offsets = fallow_types::extract::compute_line_offsets(source);
let mut visitor = InventoryVisitor::new(&line_offsets);
visitor.visit_program(&parser_return.program);
if visitor.entries.is_empty() && !source_type.is_jsx() {
let jsx_type = if source_type.is_typescript() {
SourceType::tsx()
} else {
SourceType::jsx()
};
let allocator2 = Allocator::default();
let retry_return = Parser::new(&allocator2, source, jsx_type).parse();
let mut retry_visitor = InventoryVisitor::new(&line_offsets);
retry_visitor.visit_program(&retry_return.program);
if !retry_visitor.entries.is_empty() {
return retry_visitor.entries;
}
}
visitor.entries
}
#[cfg(all(test, not(miri)))]
mod tests {
use super::*;
use std::path::PathBuf;
fn walk(source: &str) -> Vec<InventoryEntry> {
walk_source(&PathBuf::from("test.ts"), source)
}
#[test]
fn named_function_declaration_uses_its_own_name() {
let entries = walk("function foo() { return 1; }");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "foo");
assert_eq!(entries[0].line, 1);
}
#[test]
fn const_arrow_captures_binding_name() {
let entries = walk("const bar = () => 42;");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "bar");
}
#[test]
fn const_function_expression_captures_binding_name_not_fn_id() {
let entries = walk("const outer = function inner() { return 1; };");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "outer");
}
#[test]
fn class_methods_use_method_names() {
let entries = walk(
r"
class Foo {
bar() { return 1; }
baz() { return 2; }
}",
);
let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
assert_eq!(names, vec!["bar", "baz"]);
}
#[test]
fn anonymous_arrow_passed_as_argument_uses_counter() {
let entries = walk("setTimeout(() => { console.log('hi'); }, 10);");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "(anonymous_0)");
}
#[test]
fn multiple_anonymous_functions_increment_counter_in_source_order() {
let entries = walk(
r"
[1, 2, 3].map(() => 1);
[4, 5, 6].filter(() => true);
",
);
let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
assert_eq!(names, vec!["(anonymous_0)", "(anonymous_1)"]);
}
#[test]
fn named_function_still_advances_counter_matching_instrumenter() {
let entries = walk(
r"
function named() { return 1; }
[1].map(() => 2);
",
);
let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
assert_eq!(names, vec!["named", "(anonymous_1)"]);
}
#[test]
fn anonymous_after_named_chain_uses_next_counter_value() {
let entries = walk(
r"
function a() {}
function b() {}
function c() {}
const d = () => 4;
",
);
let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
assert_eq!(names, vec!["a", "b", "c", "d"]);
}
#[test]
fn typescript_overload_signatures_dont_emit_or_advance_counter() {
let entries = walk(
r"
function foo(): number;
function foo(s: string): string;
function foo(s?: string): number | string { return s ? s : 1; }
[1].map(() => 2);
",
);
let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
assert_eq!(names, vec!["foo", "(anonymous_1)"]);
}
#[test]
fn export_default_named_function_keeps_explicit_name() {
let entries = walk("export default function foo() { return 1; }");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "foo");
}
#[test]
fn export_default_anonymous_function_uses_counter() {
let entries = walk("export default function() { return 1; }");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "(anonymous_0)");
}
#[test]
fn nested_function_numbered_after_parent_in_traversal_order() {
let entries = walk(
r"
function outer() {
return function() { return 1; };
}",
);
let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
assert_eq!(names, vec!["outer", "(anonymous_1)"]);
}
#[test]
fn line_number_is_one_based_from_source_start() {
let entries = walk("\n\nfunction atLineThree() {}");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].line, 3);
}
#[test]
fn short_jsx_in_js_file_retries_with_jsx_parser() {
let entries = walk_source(&PathBuf::from("component.js"), "const A = () => <div />;");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "A");
assert_eq!(entries[0].line, 1);
}
#[test]
fn object_method_shorthand_uses_anonymous_counter() {
let entries = walk("const obj = { run() { return 1; } };");
let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
assert_eq!(names, vec!["(anonymous_0)"]);
}
#[test]
fn class_property_arrow_uses_anonymous_counter() {
let entries = walk(
r"
class Foo {
bar = () => 1;
}",
);
let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
assert_eq!(names, vec!["(anonymous_0)"]);
}
}