use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use serde::{Deserialize, Serialize};
fn is_false(v: &bool) -> bool {
!*v
}
use tldr_core::callgraph::cross_file_types::CallType;
use tldr_core::callgraph::{build_project_call_graph_v2, BuildConfig};
use tldr_core::Language;
use crate::commands::daemon_router::{params_with_path, try_daemon_route};
use crate::output::{OutputFormat, OutputWriter};
#[derive(Debug, Args)]
pub struct CallsArgs {
#[arg(default_value = ".")]
pub path: PathBuf,
#[arg(long, short = 'l')]
pub lang: Option<Language>,
#[arg(long, default_value = "true")]
pub respect_ignore: bool,
#[arg(long, default_value = "200")]
pub max_items: usize,
}
#[derive(Debug, Serialize, Deserialize)]
struct CallGraphOutput {
root: PathBuf,
language: Language,
edge_count: usize,
node_count: usize,
nodes: Vec<String>,
edges: Vec<EdgeOutput>,
#[serde(skip_serializing_if = "is_false", default)]
truncated: bool,
total_edges: usize,
shown_edges: usize,
}
#[derive(Debug, Serialize, Deserialize)]
struct EdgeOutput {
src_file: PathBuf,
src_func: String,
dst_file: PathBuf,
dst_func: String,
call_type: CallType,
}
impl CallsArgs {
pub fn run(&self, format: OutputFormat, quiet: bool) -> Result<()> {
let writer = OutputWriter::new(format, quiet);
let language = self
.lang
.unwrap_or_else(|| Language::from_directory(&self.path).unwrap_or(Language::Python));
if let Some(output) = try_daemon_route::<CallGraphOutput>(
&self.path,
"calls",
params_with_path(Some(&self.path)),
) {
if writer.is_text() {
let mut text = String::new();
text.push_str(&format!(
"Call Graph for {} ({:?})\n",
output.root.display(),
output.language
));
text.push_str(&format!("Edges: {}\n\n", output.edge_count));
for edge in &output.edges {
text.push_str(&format!(
"{}:{} -> {}:{}\n",
edge.src_file.display(),
edge.src_func,
edge.dst_file.display(),
edge.dst_func
));
}
writer.write_text(&text)?;
return Ok(());
} else {
writer.write(&output)?;
return Ok(());
}
}
writer.progress(&format!(
"Building call graph for {} ({:?})...",
self.path.display(),
language
));
let config = BuildConfig {
language: language.as_str().to_string(),
respect_ignore: self.respect_ignore,
use_type_resolution: true,
..Default::default()
};
let ir = build_project_call_graph_v2(&self.path, config)?;
let root = self
.path
.canonicalize()
.unwrap_or_else(|_| self.path.clone());
let edges: Vec<EdgeOutput> = ir
.edges
.iter()
.map(|e| {
let src = e.src_file.strip_prefix(&root).unwrap_or(&e.src_file);
let dst = e.dst_file.strip_prefix(&root).unwrap_or(&e.dst_file);
EdgeOutput {
src_file: src.to_path_buf(),
src_func: e.src_func.clone(),
dst_file: dst.to_path_buf(),
dst_func: e.dst_func.clone(),
call_type: e.call_type,
}
})
.collect();
let total_edges = edges.len();
let truncated = total_edges > self.max_items;
let mut edges = edges;
if edges.len() > self.max_items {
edges.sort_by(|a, b| {
let a_key = format!("{}:{}", a.src_file.display(), a.src_func);
let b_key = format!("{}:{}", b.src_file.display(), b.src_func);
a_key.cmp(&b_key)
});
edges.truncate(self.max_items);
}
let shown_edges = edges.len();
let mut node_set = std::collections::BTreeSet::new();
for edge in &edges {
node_set.insert(format!("{}:{}", edge.src_file.display(), edge.src_func));
node_set.insert(format!("{}:{}", edge.dst_file.display(), edge.dst_func));
}
let nodes: Vec<String> = node_set.into_iter().collect();
let output = CallGraphOutput {
root: self.path.clone(),
language,
edge_count: total_edges,
node_count: nodes.len(),
nodes,
edges,
truncated,
total_edges,
shown_edges,
};
if writer.is_text() {
let mut text = String::new();
text.push_str(&format!(
"Call Graph for {} ({:?})\n",
self.path.display(),
language
));
text.push_str(&format!("Edges: {}\n\n", output.edge_count));
for edge in &output.edges {
text.push_str(&format!(
"{}:{} -> {}:{}\n",
edge.src_file.display(),
edge.src_func,
edge.dst_file.display(),
edge.dst_func
));
}
writer.write_text(&text)?;
} else {
writer.write(&output)?;
}
Ok(())
}
}