use crate::{
analysis::call_graph::RustCallGraphBuilder,
analyzers::rust_call_graph::extract_call_graph_multi_file,
builders::parallel_call_graph::{CallGraphPhase, CallGraphProgress},
config,
core::FunctionMetrics,
core::Language,
io, priority,
};
use anyhow::{Context, Result};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
type ParsedFile = (PathBuf, syn::File);
type ExpandedFile = (syn::File, PathBuf);
pub struct CallGraphResult {
pub framework_exclusions: HashSet<priority::call_graph::FunctionId>,
pub function_pointer_used: HashSet<priority::call_graph::FunctionId>,
}
pub fn build_initial_call_graph(metrics: &[FunctionMetrics]) -> priority::CallGraph {
let mut call_graph = priority::CallGraph::new();
for metric in metrics {
let func_id = priority::call_graph::FunctionId::new(
metric.file.clone(),
metric.name.clone(),
metric.line,
);
call_graph.add_function(
func_id,
is_entry_point(&metric.name),
is_test_function(&metric.name, &metric.file, metric.is_test),
metric.cyclomatic,
metric.length,
);
}
call_graph
}
fn is_entry_point(function_name: &str) -> bool {
match function_name {
"main" => true,
name if name.starts_with("handle_") => true,
name if name.starts_with("run_") => true,
_ => false,
}
}
fn is_test_function(function_name: &str, file_path: &Path, is_test_attr: bool) -> bool {
is_test_attr
|| function_name.starts_with("test_")
|| file_path.to_string_lossy().contains("test")
}
pub fn process_rust_files_for_call_graph<F>(
project_path: &Path,
call_graph: &mut priority::CallGraph,
verbose_macro_warnings: bool,
show_macro_stats: bool,
progress_callback: F,
) -> Result<(
HashSet<priority::call_graph::FunctionId>,
HashSet<priority::call_graph::FunctionId>,
)>
where
F: FnMut(CallGraphProgress),
{
process_rust_files_for_call_graph_with_files(
project_path,
call_graph,
verbose_macro_warnings,
show_macro_stats,
None,
progress_callback,
)
}
pub fn process_rust_files_for_call_graph_with_files<F>(
project_path: &Path,
call_graph: &mut priority::CallGraph,
_verbose_macro_warnings: bool,
_show_macro_stats: bool,
rust_files: Option<&[PathBuf]>,
mut progress_callback: F,
) -> Result<(
HashSet<priority::call_graph::FunctionId>,
HashSet<priority::call_graph::FunctionId>,
)>
where
F: FnMut(CallGraphProgress),
{
let discovered_files = discover_rust_files(project_path, rust_files, &mut progress_callback)?;
let rust_files = rust_files.unwrap_or(&discovered_files);
let total_files = rust_files.len();
let (workspace_files, expanded_files) =
parse_rust_files(rust_files, total_files, &mut progress_callback);
let enhanced_builder = analyze_workspace_calls(
call_graph,
&workspace_files,
&expanded_files,
&mut progress_callback,
)?;
let result = finalize_call_graph(call_graph, enhanced_builder, &mut progress_callback)?;
crate::core::parsing::reset_span_locations();
Ok((result.framework_exclusions, result.function_pointer_used))
}
fn discover_rust_files<F>(
project_path: &Path,
pre_discovered: Option<&[PathBuf]>,
progress_callback: &mut F,
) -> Result<Vec<PathBuf>>
where
F: FnMut(CallGraphProgress),
{
if let Some(files) = pre_discovered {
log::info!("Using {} pre-discovered Rust files", files.len());
return Ok(Vec::new());
}
progress_callback(CallGraphProgress {
phase: CallGraphPhase::DiscoveringFiles,
current: 0,
total: 0,
});
let config = config::get_config();
let discovered_files =
io::walker::find_project_files_with_config(project_path, vec![Language::Rust], config)
.context("Failed to find Rust files for call graph")?;
log::info!("Discovered {} Rust files", discovered_files.len());
progress_callback(CallGraphProgress {
phase: CallGraphPhase::DiscoveringFiles,
current: discovered_files.len(),
total: discovered_files.len(),
});
Ok(discovered_files)
}
fn parse_rust_files<F>(
rust_files: &[PathBuf],
total_files: usize,
progress_callback: &mut F,
) -> (Vec<ParsedFile>, Vec<ExpandedFile>)
where
F: FnMut(CallGraphProgress),
{
progress_callback(CallGraphProgress {
phase: CallGraphPhase::ParsingASTs,
current: 0,
total: total_files,
});
let mut workspace_files = Vec::with_capacity(rust_files.len());
let mut expanded_files = Vec::with_capacity(rust_files.len());
for (idx, file_path) in rust_files.iter().enumerate() {
if let Some((parsed, expanded)) = parse_single_file(file_path) {
workspace_files.push(parsed);
expanded_files.push(expanded);
}
report_progress_throttled(
idx + 1,
total_files,
CallGraphPhase::ParsingASTs,
progress_callback,
);
}
(workspace_files, expanded_files)
}
fn parse_single_file(file_path: &Path) -> Option<(ParsedFile, ExpandedFile)> {
let content = io::read_file(file_path).ok()?;
let parsed = syn::parse_file(&content).ok()?;
let workspace = (file_path.to_path_buf(), parsed.clone());
let expanded = (parsed, file_path.to_path_buf());
Some((workspace, expanded))
}
fn analyze_workspace_calls<F>(
call_graph: &mut priority::CallGraph,
workspace_files: &[ParsedFile],
expanded_files: &[ExpandedFile],
progress_callback: &mut F,
) -> Result<RustCallGraphBuilder>
where
F: FnMut(CallGraphProgress),
{
progress_callback(CallGraphProgress {
phase: CallGraphPhase::ExtractingCalls,
current: 0,
total: workspace_files.len(),
});
if !expanded_files.is_empty() {
let multi_file_call_graph = extract_call_graph_multi_file(expanded_files);
call_graph.merge(multi_file_call_graph);
}
let mut enhanced_builder = RustCallGraphBuilder::from_base_graph(call_graph.clone());
for (file_path, parsed) in workspace_files {
enhanced_builder
.analyze_basic_calls(file_path, parsed)?
.analyze_trait_dispatch(file_path, parsed)?
.analyze_function_pointers(file_path, parsed)?
.analyze_framework_patterns(file_path, parsed)?;
}
enhanced_builder.analyze_cross_module(workspace_files)?;
Ok(enhanced_builder)
}
fn finalize_call_graph<F>(
call_graph: &mut priority::CallGraph,
mut enhanced_builder: RustCallGraphBuilder,
progress_callback: &mut F,
) -> Result<CallGraphResult>
where
F: FnMut(CallGraphProgress),
{
progress_callback(CallGraphProgress {
phase: CallGraphPhase::LinkingModules,
current: 0,
total: 0,
});
log_status("Resolving trait patterns and method calls...");
enhanced_builder.finalize_trait_analysis()?;
log_status_done();
let enhanced_graph = enhanced_builder.build();
let framework_exclusions: HashSet<priority::call_graph::FunctionId> = enhanced_graph
.framework_patterns
.get_exclusions()
.into_iter()
.collect();
let function_pointer_used: HashSet<priority::call_graph::FunctionId> = enhanced_graph
.function_pointer_tracker
.get_definitely_used_functions()
.into_iter()
.collect();
call_graph.merge(enhanced_graph.base_graph);
call_graph.resolve_cross_file_calls();
Ok(CallGraphResult {
framework_exclusions,
function_pointer_used,
})
}
fn report_progress_throttled<F>(
current: usize,
total: usize,
phase: CallGraphPhase,
progress_callback: &mut F,
) where
F: FnMut(CallGraphProgress),
{
if current % 10 == 0 || current == total {
progress_callback(CallGraphProgress {
phase,
current,
total,
});
}
}
fn log_status(message: &str) {
if !is_quiet_mode() {
eprint!("{}", message);
std::io::Write::flush(&mut std::io::stderr()).ok();
}
}
fn log_status_done() {
if !is_quiet_mode() {
eprintln!(" done");
}
}
fn is_quiet_mode() -> bool {
std::env::var("DEBTMAP_QUIET").is_ok()
}
pub fn process_typescript_files_for_call_graph(
project_path: &Path,
call_graph: &mut priority::CallGraph,
js_ts_files: Option<&[PathBuf]>,
) -> Result<()> {
use crate::analyzers::typescript::call_graph::extract_call_graph;
use crate::analyzers::typescript::parser::parse_source;
use crate::core::ast::JsLanguageVariant;
let files = if let Some(files) = js_ts_files {
files.to_vec()
} else {
let config = config::get_config();
io::walker::find_project_files_with_config(
project_path,
vec![Language::JavaScript, Language::TypeScript],
config,
)
.context("Failed to find JS/TS files for call graph")?
};
if files.is_empty() {
return Ok(());
}
log::info!("Processing {} JS/TS files for call graph", files.len());
for file_path in &files {
let content = match io::read_file(file_path) {
Ok(c) => c,
Err(e) => {
log::debug!("Failed to read file {:?}: {}", file_path, e);
continue;
}
};
let variant = match file_path.extension().and_then(|e| e.to_str()) {
Some("ts" | "tsx" | "mts" | "cts") => JsLanguageVariant::TypeScript,
Some("jsx") => JsLanguageVariant::Jsx,
_ => JsLanguageVariant::JavaScript,
};
let ast = match parse_source(&content, file_path, variant) {
Ok(ast) => ast,
Err(e) => {
log::debug!("Failed to parse {:?}: {}", file_path, e);
continue;
}
};
let file_call_graph = extract_call_graph(&ast);
call_graph.merge(file_call_graph);
}
log::info!(
"Merged JS/TS call graph: {} total functions",
call_graph.node_count()
);
Ok(())
}