use crate::args::Cli;
use crate::commands::graph::loader::{GraphLoadConfig, load_unified_graph_for_cli};
use crate::index_discovery::find_nearest_index;
use crate::output::OutputStreams;
use anyhow::{Context, Result};
use serde::Serialize;
use sqry_core::graph::unified::concurrent::GraphSnapshot;
use sqry_core::graph::unified::node::NodeId;
use sqry_core::query::CircularType;
use std::sync::Arc;
#[derive(Debug, Serialize)]
struct Cycle {
depth: usize,
nodes: Vec<String>,
}
fn materialize_cycle_node_ids(
cycles: &[Vec<NodeId>],
snapshot: &GraphSnapshot,
) -> Vec<Vec<String>> {
let strings = snapshot.strings();
cycles
.iter()
.map(|cycle| {
cycle
.iter()
.filter_map(|&node_id| {
snapshot.get_node(node_id).and_then(|entry| {
entry
.qualified_name
.and_then(|sid| strings.resolve(sid))
.or_else(|| strings.resolve(entry.name))
.map(|s| s.to_string())
})
})
.collect()
})
.filter(|cycle: &Vec<String>| !cycle.is_empty())
.collect()
}
pub fn run_cycles(
cli: &Cli,
path: Option<&str>,
cycle_type: &str,
min_depth: usize,
max_depth: Option<usize>,
include_self: bool,
max_results: usize,
) -> Result<()> {
let mut streams = OutputStreams::new();
let circular_type = CircularType::try_parse(cycle_type).with_context(|| {
format!("Invalid cycle type: {cycle_type}. Use: calls, imports, modules")
})?;
let search_path = path.map_or_else(
|| std::env::current_dir().unwrap_or_default(),
std::path::PathBuf::from,
);
let index_location = find_nearest_index(&search_path);
let Some(ref loc) = index_location else {
streams
.write_diagnostic("No .sqry-index found. Run 'sqry index' first to build the index.")?;
return Ok(());
};
let config = GraphLoadConfig::default();
let graph = load_unified_graph_for_cli(&loc.index_root, &config, cli)
.context("Failed to load graph. Run 'sqry index' to build the graph.")?;
let snapshot = Arc::new(graph.snapshot());
let db = sqry_db::queries::dispatch::make_query_db_cold(Arc::clone(&snapshot), &loc.index_root);
let key = sqry_db::queries::CyclesKey {
circular_type,
bounds: sqry_db::queries::CycleBounds {
min_depth,
max_depth,
max_results,
should_include_self_loops: include_self,
},
};
let cycle_node_ids = db.get::<sqry_db::queries::CyclesQuery>(&key);
let cycles = materialize_cycle_node_ids(&cycle_node_ids, snapshot.as_ref());
let output_cycles: Vec<Cycle> = cycles
.into_iter()
.take(max_results)
.map(|nodes| Cycle {
depth: nodes.len(),
nodes,
})
.collect();
if cli.json {
let json =
serde_json::to_string_pretty(&output_cycles).context("Failed to serialize to JSON")?;
streams.write_result(&json)?;
} else {
let output = format_cycles_text(&output_cycles, circular_type);
streams.write_result(&output)?;
}
Ok(())
}
fn format_cycles_text(cycles: &[Cycle], cycle_type: CircularType) -> String {
let mut lines = Vec::new();
let type_name = match cycle_type {
CircularType::Calls => "call",
CircularType::Imports => "import",
CircularType::Modules => "module",
};
lines.push(format!("Found {} {} cycles", cycles.len(), type_name));
lines.push(String::new());
for (i, cycle) in cycles.iter().enumerate() {
lines.push(format!("Cycle {} (depth {}):", i + 1, cycle.depth));
let mut chain = cycle.nodes.join(" → ");
if let Some(first) = cycle.nodes.first() {
chain.push_str(" → ");
chain.push_str(first);
}
lines.push(format!(" {chain}"));
lines.push(String::new());
}
if cycles.is_empty() {
lines.push("No cycles found.".to_string());
}
lines.join("\n")
}