use anyhow::Result;
use magellan::graph::{Cycle, CycleKind};
use magellan::output::{output_json, JsonResponse, OutputFormat};
use magellan::CodeGraph;
use std::path::PathBuf;
pub fn run_cycles(
db_path: PathBuf,
symbol_id: Option<String>,
output_format: OutputFormat,
) -> Result<()> {
let mut args = vec!["cycles".to_string()];
if let Some(ref sym) = symbol_id {
args.push("--symbol".to_string());
args.push(sym.clone());
}
let graph = CodeGraph::open(&db_path)?;
let exec_id = magellan::output::generate_execution_id();
let db_path_str = db_path.to_string_lossy().to_string();
graph.execution_log().start_execution(
&exec_id,
env!("CARGO_PKG_VERSION"),
&args,
None,
&db_path_str,
)?;
let cycles = if let Some(ref sym) = symbol_id {
graph.find_cycles_containing(sym)?
} else {
graph.detect_cycles()?.cycles
};
if output_format == OutputFormat::Json || output_format == OutputFormat::Pretty {
graph
.execution_log()
.finish_execution(&exec_id, "success", None, 0, 0, 0)?;
return output_json_mode(symbol_id.as_deref(), cycles, &exec_id, output_format);
}
if cycles.is_empty() {
println!("No cycles detected in the call graph.");
} else {
println!("Detected {} cycle(s):", cycles.len());
for (idx, cycle) in cycles.iter().enumerate() {
println!(" [{}] {}:", idx + 1, cycle_kind_display(&cycle.kind));
for member in &cycle.members {
let fqn_display = member.fqn.as_deref().unwrap_or("?");
println!(
" {} ({}) in {}",
fqn_display, member.kind, member.file_path
);
}
}
}
graph
.execution_log()
.finish_execution(&exec_id, "success", None, 0, 0, 0)?;
Ok(())
}
fn cycle_kind_display(kind: &CycleKind) -> &'static str {
match kind {
CycleKind::MutualRecursion => "Mutual Recursion",
CycleKind::SelfLoop => "Self Loop",
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct CyclesResponse {
pub symbol_id: Option<String>,
pub count: usize,
pub cycles: Vec<CycleJson>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct CycleJson {
pub kind: String,
pub members: Vec<SymbolInfoJson>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct SymbolInfoJson {
pub symbol_id: Option<String>,
pub fqn: Option<String>,
pub file_path: String,
pub kind: String,
}
impl From<Cycle> for CycleJson {
fn from(cycle: Cycle) -> Self {
Self {
kind: cycle_kind_json(&cycle.kind),
members: cycle
.members
.into_iter()
.map(SymbolInfoJson::from)
.collect(),
}
}
}
fn cycle_kind_json(kind: &CycleKind) -> String {
match kind {
CycleKind::MutualRecursion => "mutual_recursion".to_string(),
CycleKind::SelfLoop => "self_loop".to_string(),
}
}
impl From<magellan::graph::SymbolInfo> for SymbolInfoJson {
fn from(info: magellan::graph::SymbolInfo) -> Self {
Self {
symbol_id: info.symbol_id,
fqn: info.fqn,
file_path: info.file_path,
kind: info.kind,
}
}
}
fn output_json_mode(
symbol_id: Option<&str>,
cycles: Vec<Cycle>,
exec_id: &str,
output_format: OutputFormat,
) -> Result<()> {
let cycles_json: Vec<CycleJson> = cycles.into_iter().map(CycleJson::from).collect();
let response = CyclesResponse {
symbol_id: symbol_id.map(|s| s.to_string()),
count: cycles_json.len(),
cycles: cycles_json,
};
let json_response = JsonResponse::new(response, exec_id);
output_json(&json_response, output_format)?;
Ok(())
}