sqry_cli/commands/
cycles.rs1use crate::args::Cli;
6use crate::commands::graph::loader::{GraphLoadConfig, load_unified_graph_for_cli};
7use crate::index_discovery::find_nearest_index;
8use crate::output::OutputStreams;
9use anyhow::{Context, Result};
10use serde::Serialize;
11use sqry_core::query::{CircularConfig, CircularType, find_all_cycles_graph};
12
13#[derive(Debug, Serialize)]
15struct Cycle {
16 depth: usize,
18 nodes: Vec<String>,
20}
21
22pub fn run_cycles(
27 cli: &Cli,
28 path: Option<&str>,
29 cycle_type: &str,
30 min_depth: usize,
31 max_depth: Option<usize>,
32 include_self: bool,
33 max_results: usize,
34) -> Result<()> {
35 let mut streams = OutputStreams::new();
36
37 let circular_type = CircularType::try_parse(cycle_type).with_context(|| {
39 format!("Invalid cycle type: {cycle_type}. Use: calls, imports, modules")
40 })?;
41
42 let search_path = path.map_or_else(
44 || std::env::current_dir().unwrap_or_default(),
45 std::path::PathBuf::from,
46 );
47
48 let index_location = find_nearest_index(&search_path);
49 let Some(ref loc) = index_location else {
50 streams
51 .write_diagnostic("No .sqry-index found. Run 'sqry index' first to build the index.")?;
52 return Ok(());
53 };
54
55 let config = GraphLoadConfig::default();
57 let graph = load_unified_graph_for_cli(&loc.index_root, &config, cli)
58 .context("Failed to load graph. Run 'sqry index' to build the graph.")?;
59
60 let circular_config = CircularConfig {
62 min_depth,
63 max_depth,
64 max_results,
65 should_include_self_loops: include_self,
66 };
67
68 let all_cycles = find_all_cycles_graph(circular_type, &graph, &circular_config);
70
71 let output_cycles: Vec<Cycle> = all_cycles
73 .into_iter()
74 .take(max_results)
75 .map(|nodes| Cycle {
76 depth: nodes.len(),
77 nodes,
78 })
79 .collect();
80
81 if cli.json {
83 let json =
84 serde_json::to_string_pretty(&output_cycles).context("Failed to serialize to JSON")?;
85 streams.write_result(&json)?;
86 } else {
87 let output = format_cycles_text(&output_cycles, circular_type);
89 streams.write_result(&output)?;
90 }
91
92 Ok(())
93}
94
95fn format_cycles_text(cycles: &[Cycle], cycle_type: CircularType) -> String {
97 let mut lines = Vec::new();
98
99 let type_name = match cycle_type {
100 CircularType::Calls => "call",
101 CircularType::Imports => "import",
102 CircularType::Modules => "module",
103 };
104
105 lines.push(format!("Found {} {} cycles", cycles.len(), type_name));
106 lines.push(String::new());
107
108 for (i, cycle) in cycles.iter().enumerate() {
109 lines.push(format!("Cycle {} (depth {}):", i + 1, cycle.depth));
110
111 let mut chain = cycle.nodes.join(" → ");
113 if let Some(first) = cycle.nodes.first() {
114 chain.push_str(" → ");
115 chain.push_str(first);
116 }
117 lines.push(format!(" {chain}"));
118 lines.push(String::new());
119 }
120
121 if cycles.is_empty() {
122 lines.push("No cycles found.".to_string());
123 }
124
125 lines.join("\n")
126}