sqry_cli/commands/
explain.rs1use crate::args::Cli;
6use crate::commands::graph::loader::{GraphLoadConfig, load_unified_graph_for_cli, no_op_reporter};
7use crate::commands::impact::{emit_ambiguous_symbol_error, emit_symbol_not_found};
8use crate::index_discovery::find_nearest_index;
9use crate::output::OutputStreams;
10use anyhow::{Context, Result, anyhow};
11use serde::Serialize;
12use sqry_core::graph::unified::FileScope;
13use sqry_core::graph::unified::concurrent::GraphSnapshot;
14use sqry_core::graph::unified::resolution::SymbolResolveError;
15use sqry_core::graph::unified::storage::{FileRegistry, NodeEntry};
16
17#[derive(Debug, Serialize)]
19struct ExplainOutput {
20 name: String,
22 qualified_name: String,
24 kind: String,
26 file: String,
28 line: u32,
30 language: String,
32 visibility: Option<String>,
34 documentation: Option<String>,
36 #[serde(skip_serializing_if = "Option::is_none")]
38 context: Option<SymbolContext>,
39}
40
41#[derive(Debug, Serialize)]
42struct SymbolContext {
43 code: String,
45 start_line: u32,
47 end_line: u32,
49}
50
51fn resolve_symbol_by_file_and_name(
62 snapshot: &GraphSnapshot,
63 file_path: &std::path::Path,
64 symbol_name: &str,
65) -> Result<sqry_core::graph::unified::NodeId, SymbolResolveError> {
66 snapshot.resolve_global_symbol_ambiguity_aware(symbol_name, FileScope::Path(file_path))
67}
68
69fn build_symbol_context(
71 index_root: &std::path::Path,
72 files_registry: &FileRegistry,
73 entry: &NodeEntry,
74) -> Option<SymbolContext> {
75 let file_to_read = files_registry
76 .resolve(entry.file)
77 .map(|p| index_root.join(p.as_ref()));
78
79 let file_to_read = file_to_read?;
80 let content = std::fs::read_to_string(&file_to_read).ok()?;
81 let lines: Vec<&str> = content.lines().collect();
82 let start = entry.start_line.saturating_sub(1) as usize;
83 let end = (entry.end_line as usize).min(lines.len());
84
85 if start < lines.len() {
86 let code_lines: Vec<&str> = lines[start..end].to_vec();
87 Some(SymbolContext {
88 code: code_lines.join("\n"),
89 start_line: entry.start_line,
90 end_line: entry.end_line,
91 })
92 } else {
93 None
94 }
95}
96
97pub fn run_explain(
102 cli: &Cli,
103 file_path: &str,
104 symbol_name: &str,
105 path: Option<&str>,
106 include_context: bool,
107 _include_relations: bool,
108) -> Result<()> {
109 let mut streams = OutputStreams::new();
110
111 let search_path = path.map_or_else(
113 || std::env::current_dir().unwrap_or_default(),
114 std::path::PathBuf::from,
115 );
116
117 let index_location = find_nearest_index(&search_path);
118 let Some(ref loc) = index_location else {
119 streams
120 .write_diagnostic("No .sqry-index found. Run 'sqry index' first to build the index.")?;
121 return Ok(());
122 };
123
124 let config = GraphLoadConfig::default();
126 let graph = load_unified_graph_for_cli(&loc.index_root, &config, cli, no_op_reporter())
127 .context("Failed to load graph. Run 'sqry index' to build the graph.")?;
128
129 let snapshot = graph.snapshot();
130 let strings = snapshot.strings();
131 let files_registry = snapshot.files();
132 let requested_file_path = if std::path::Path::new(file_path).is_absolute() {
133 std::path::PathBuf::from(file_path)
134 } else {
135 loc.index_root.join(file_path)
136 };
137
138 let node_id =
145 match resolve_symbol_by_file_and_name(&snapshot, &requested_file_path, symbol_name) {
146 Ok(id) => id,
147 Err(SymbolResolveError::Ambiguous(err)) => {
148 let exit_code = emit_ambiguous_symbol_error(&mut streams, &err, cli.json);
149 std::process::exit(exit_code);
150 }
151 Err(SymbolResolveError::NotFound { name }) => {
152 let exit_code = emit_symbol_not_found(&mut streams, &name, cli.json);
153 std::process::exit(exit_code);
154 }
155 };
156 let symbol_entry = snapshot
157 .get_node(node_id)
158 .ok_or_else(|| anyhow!("Symbol node not found in graph"))?;
159
160 let file_path_resolved = files_registry
161 .resolve(symbol_entry.file)
162 .map(|p| p.display().to_string())
163 .unwrap_or_default();
164
165 let language = files_registry
166 .language_for_file(symbol_entry.file)
167 .map_or_else(|| "Unknown".to_string(), |l| l.to_string());
168
169 let name = strings
170 .resolve(symbol_entry.name)
171 .map(|s| s.to_string())
172 .unwrap_or_default();
173
174 let qualified_name = symbol_entry
175 .qualified_name
176 .and_then(|id| strings.resolve(id))
177 .map_or_else(|| name.clone(), |s| s.to_string());
178
179 let visibility = symbol_entry
180 .visibility
181 .and_then(|id| strings.resolve(id))
182 .map(|s| s.to_string());
183
184 let documentation = symbol_entry
185 .doc
186 .and_then(|id| strings.resolve(id))
187 .map(|s| s.to_string());
188
189 let context = if include_context {
191 build_symbol_context(&loc.index_root, files_registry, symbol_entry)
192 } else {
193 None
194 };
195
196 let output = ExplainOutput {
197 name,
198 qualified_name,
199 kind: format!("{:?}", symbol_entry.kind),
200 file: file_path_resolved,
201 line: symbol_entry.start_line,
202 language,
203 visibility,
204 documentation,
205 context,
206 };
207
208 if cli.json {
210 let json = serde_json::to_string_pretty(&output).context("Failed to serialize to JSON")?;
211 streams.write_result(&json)?;
212 } else {
213 let text = format_explain_text(&output);
214 streams.write_result(&text)?;
215 }
216
217 Ok(())
218}
219
220fn format_explain_text(output: &ExplainOutput) -> String {
221 let mut lines = Vec::new();
222
223 lines.push(format!("Symbol: {}", output.qualified_name));
224 lines.push(format!(" Kind: {}", output.kind));
225 lines.push(format!(" File: {}:{}", output.file, output.line));
226 lines.push(format!(" Language: {}", output.language));
227
228 if let Some(ref vis) = output.visibility {
229 lines.push(format!(" Visibility: {vis}"));
230 }
231
232 if let Some(ref doc) = output.documentation {
233 lines.push(format!(" Documentation: {doc}"));
234 }
235
236 if let Some(ref ctx) = output.context {
237 lines.push(String::new());
238 lines.push(format!("Code (lines {}-{}):", ctx.start_line, ctx.end_line));
239 for (i, line) in ctx.code.lines().enumerate() {
240 lines.push(format!("{:4} | {}", ctx.start_line as usize + i, line));
241 }
242 }
243
244 lines.join("\n")
245}