sqry_cli/commands/
unused.rs1use crate::args::Cli;
6use crate::commands::graph::loader::{GraphLoadConfig, load_unified_graph};
7use crate::index_discovery::find_nearest_index;
8use crate::output::OutputStreams;
9use anyhow::{Context, Result};
10use serde::Serialize;
11use sqry_core::query::{UnusedScope, compute_reachable_set_graph, is_node_unused};
12use std::collections::HashMap;
13
14#[derive(Debug, Serialize)]
16struct UnusedSymbol {
17 name: String,
18 qualified_name: String,
19 kind: String,
20 file: String,
21 line: u32,
22 language: String,
23 visibility: String,
24}
25
26#[derive(Debug, Serialize)]
28struct UnusedByFile {
29 file: String,
30 count: usize,
31 symbols: Vec<UnusedSymbol>,
32}
33
34pub fn run_unused(
39 cli: &Cli,
40 path: Option<&str>,
41 scope: &str,
42 lang_filter: Option<&str>,
43 kind_filter: Option<&str>,
44 max_results: usize,
45) -> Result<()> {
46 let mut streams = OutputStreams::new();
47
48 let unused_scope = UnusedScope::try_parse(scope).with_context(|| {
50 format!("Invalid scope: {scope}. Use: public, private, function, struct, all")
51 })?;
52
53 let search_path = path.map_or_else(
55 || std::env::current_dir().unwrap_or_default(),
56 std::path::PathBuf::from,
57 );
58
59 let index_location = find_nearest_index(&search_path);
60 let Some(ref loc) = index_location else {
61 streams
62 .write_diagnostic("No .sqry-index found. Run 'sqry index' first to build the index.")?;
63 return Ok(());
64 };
65
66 let config = GraphLoadConfig::default();
68 let graph = load_unified_graph(&loc.index_root, &config)
69 .context("Failed to load graph. Run 'sqry index' to build the graph.")?;
70
71 let reachable = compute_reachable_set_graph(&graph);
73
74 let strings = graph.strings();
75 let files = graph.files();
76
77 let mut unused_symbols: Vec<UnusedSymbol> = Vec::new();
79 let mut count = 0;
80
81 for (node_id, entry) in graph.nodes().iter() {
82 if count >= max_results {
83 break;
84 }
85
86 let language = files
88 .language_for_file(entry.file)
89 .map_or_else(|| "Unknown".to_string(), |l| l.to_string());
90
91 if let Some(lang) = lang_filter
93 && !language.to_lowercase().contains(&lang.to_lowercase())
94 {
95 continue;
96 }
97
98 if let Some(kind) = kind_filter {
100 let kind_str = format!("{:?}", entry.kind).to_lowercase();
101 if !kind_str.contains(&kind.to_lowercase()) {
102 continue;
103 }
104 }
105
106 if is_node_unused(node_id, unused_scope, &graph, Some(&reachable)) {
108 let name = strings
109 .resolve(entry.name)
110 .map(|s| s.to_string())
111 .unwrap_or_default();
112
113 let qualified_name = entry
114 .qualified_name
115 .and_then(|id| strings.resolve(id))
116 .map_or_else(|| name.clone(), |s| s.to_string());
117
118 let file_path = files
119 .resolve(entry.file)
120 .map(|p| p.display().to_string())
121 .unwrap_or_default();
122
123 let visibility = entry
124 .visibility
125 .and_then(|id| strings.resolve(id))
126 .map_or_else(|| "unknown".to_string(), |s| s.to_string());
127
128 unused_symbols.push(UnusedSymbol {
129 name,
130 qualified_name,
131 kind: format!("{:?}", entry.kind),
132 file: file_path,
133 line: entry.start_line,
134 language,
135 visibility,
136 });
137 count += 1;
138 }
139 }
140
141 let mut by_file: HashMap<String, Vec<UnusedSymbol>> = HashMap::new();
143 for sym in unused_symbols {
144 by_file.entry(sym.file.clone()).or_default().push(sym);
145 }
146
147 let mut grouped: Vec<UnusedByFile> = by_file
148 .into_iter()
149 .map(|(file, symbols)| UnusedByFile {
150 file,
151 count: symbols.len(),
152 symbols,
153 })
154 .collect();
155
156 grouped.sort_by(|a, b| a.file.cmp(&b.file));
158
159 if cli.json {
161 let json = serde_json::to_string_pretty(&grouped).context("Failed to serialize to JSON")?;
162 streams.write_result(&json)?;
163 } else {
164 let output = format_unused_text(&grouped, unused_scope);
165 streams.write_result(&output)?;
166 }
167
168 Ok(())
169}
170
171fn format_unused_text(groups: &[UnusedByFile], scope: UnusedScope) -> String {
173 let mut lines = Vec::new();
174
175 let total: usize = groups.iter().map(|g| g.count).sum();
176 let scope_name = match scope {
177 UnusedScope::Public => "public",
178 UnusedScope::Private => "private",
179 UnusedScope::Function => "function",
180 UnusedScope::Struct => "struct",
181 UnusedScope::All => "all",
182 };
183
184 lines.push(format!(
185 "Found {total} unused symbols (scope: {scope_name})"
186 ));
187 lines.push(String::new());
188
189 for group in groups {
190 lines.push(format!("{} ({} unused):", group.file, group.count));
191 for sym in &group.symbols {
192 lines.push(format!(" {} [{}] line {}", sym.name, sym.kind, sym.line));
193 }
194 lines.push(String::new());
195 }
196
197 if groups.is_empty() {
198 lines.push("No unused symbols found.".to_string());
199 }
200
201 lines.join("\n")
202}