searchfox_lib/
call_graph.rs1use crate::client::SearchfoxClient;
2use crate::types::SearchfoxResponse;
3use anyhow::Result;
4use reqwest::Url;
5use serde_json;
6
7pub struct CallGraphQuery {
8 pub calls_from: Option<String>,
9 pub calls_to: Option<String>,
10 pub calls_between: Option<(String, String)>,
11 pub depth: u32,
12}
13
14pub fn format_call_graph_markdown(query_text: &str, json: &serde_json::Value) -> String {
15 use std::collections::{BTreeMap, BTreeSet};
16
17 let mut output = String::new();
18 output.push_str(&format!("# {}\n\n", query_text));
19
20 let is_calls_between = query_text.contains("calls-between");
21
22 if is_calls_between {
23 if let Some(hierarchical_graphs) = json.get("hierarchicalGraphs").and_then(|v| v.as_array())
24 {
25 let jumprefs = json.get("jumprefs").and_then(|v| v.as_object());
26
27 let mut all_edges = Vec::new();
28
29 fn collect_edges(node: &serde_json::Value, edges: &mut Vec<(String, String)>) {
30 if let Some(node_edges) = node.get("edges").and_then(|e| e.as_array()) {
31 for edge in node_edges {
32 if let Some(edge_obj) = edge.as_object() {
33 let from = edge_obj.get("from").and_then(|f| f.as_str()).unwrap_or("");
34 let to = edge_obj.get("to").and_then(|t| t.as_str()).unwrap_or("");
35 if !from.is_empty() && !to.is_empty() {
36 edges.push((from.to_string(), to.to_string()));
37 }
38 }
39 }
40 }
41
42 if let Some(children) = node.get("children").and_then(|c| c.as_array()) {
43 for child in children {
44 collect_edges(child, edges);
45 }
46 }
47 }
48
49 for hg in hierarchical_graphs {
50 collect_edges(hg, &mut all_edges);
51 }
52
53 if all_edges.is_empty() {
54 output.push_str("No direct calls found between source and target.\n");
55 } else {
56 output.push_str("## Direct calls from source to target\n\n");
57
58 for (from_sym, to_sym) in all_edges {
59 let from_pretty = if let Some(jumprefs) = jumprefs {
60 jumprefs
61 .get(&from_sym)
62 .and_then(|s| s.get("pretty"))
63 .and_then(|p| p.as_str())
64 .unwrap_or(&from_sym)
65 } else {
66 &from_sym
67 };
68
69 let from_location = if let Some(jumprefs) = jumprefs {
70 jumprefs
71 .get(&from_sym)
72 .and_then(|s| s.get("jumps"))
73 .and_then(|j| j.get("def"))
74 .and_then(|d| d.as_str())
75 .unwrap_or("")
76 } else {
77 ""
78 };
79
80 let to_pretty = if let Some(jumprefs) = jumprefs {
81 jumprefs
82 .get(&to_sym)
83 .and_then(|s| s.get("pretty"))
84 .and_then(|p| p.as_str())
85 .unwrap_or(&to_sym)
86 } else {
87 &to_sym
88 };
89
90 let to_location = if let Some(jumprefs) = jumprefs {
91 jumprefs
92 .get(&to_sym)
93 .and_then(|s| s.get("jumps"))
94 .and_then(|j| j.get("def"))
95 .and_then(|d| d.as_str())
96 .unwrap_or("")
97 } else {
98 ""
99 };
100
101 output.push_str(&format!(
102 "- **{}** ({}) calls **{}** ({})\n",
103 from_pretty, from_location, to_pretty, to_location
104 ));
105 output.push_str(&format!(" - From: `{}`\n", from_sym));
106 output.push_str(&format!(" - To: `{}`\n", to_sym));
107 }
108 }
109
110 return output;
111 }
112 }
113
114 let mut grouped_by_parent: BTreeMap<String, BTreeSet<(String, String, String, String)>> =
115 BTreeMap::new();
116
117 let jumprefs = json.get("jumprefs").and_then(|v| v.as_object());
118
119 let is_calls_to = query_text.contains("calls-to:");
120
121 if let Some(graphs) = json.get("graphs").and_then(|v| v.as_array()) {
122 for graph in graphs {
123 if let Some(edges) = graph.get("edges").and_then(|v| v.as_array()) {
124 for edge in edges {
125 if let Some(edge_obj) = edge.as_object() {
126 let target_sym = if is_calls_to {
127 edge_obj.get("from").and_then(|v| v.as_str()).unwrap_or("")
128 } else {
129 edge_obj.get("to").and_then(|v| v.as_str()).unwrap_or("")
130 };
131
132 if let Some(jumprefs) = jumprefs {
133 if let Some(symbol_info) = jumprefs.get(target_sym) {
134 let pretty_name = symbol_info
135 .get("pretty")
136 .and_then(|v| v.as_str())
137 .unwrap_or("");
138 let mangled = symbol_info
139 .get("sym")
140 .and_then(|v| v.as_str())
141 .unwrap_or(target_sym);
142
143 let jumps = symbol_info.get("jumps");
144
145 let decl_location = jumps
146 .and_then(|j| j.get("decl"))
147 .and_then(|v| v.as_str())
148 .unwrap_or("");
149
150 let def_location = jumps
151 .and_then(|j| j.get("def"))
152 .and_then(|v| v.as_str())
153 .unwrap_or("");
154
155 let location = if !def_location.is_empty()
156 && !decl_location.is_empty()
157 && def_location != decl_location
158 {
159 format!("{} (decl: {})", def_location, decl_location)
160 } else if !def_location.is_empty() {
161 def_location.to_string()
162 } else if !decl_location.is_empty() {
163 decl_location.to_string()
164 } else {
165 String::new()
166 };
167
168 let parent_sym = symbol_info
169 .get("meta")
170 .and_then(|m| m.get("parentsym"))
171 .and_then(|v| v.as_str())
172 .unwrap_or("Free functions");
173
174 let parent_sym_clean =
175 parent_sym.strip_prefix("T_").unwrap_or(parent_sym);
176
177 if !pretty_name.is_empty() && !location.is_empty() {
178 grouped_by_parent
179 .entry(parent_sym_clean.to_string())
180 .or_default()
181 .insert((
182 pretty_name.to_string(),
183 mangled.to_string(),
184 location,
185 String::new(),
186 ));
187 }
188 }
189 }
190 }
191 }
192 }
193 }
194 }
195
196 for (parent_sym, items) in grouped_by_parent {
197 output.push_str(&format!("## {}\n\n", parent_sym));
198
199 let mut grouped_items: Vec<(String, Vec<(String, String)>)> = Vec::new();
200
201 for (pretty_name, mangled, location, _) in items {
202 if let Some((last_pretty, last_overloads)) = grouped_items.last_mut() {
203 if last_pretty == &pretty_name {
204 last_overloads.push((mangled, location));
205 continue;
206 }
207 }
208 grouped_items.push((pretty_name, vec![(mangled, location)]));
209 }
210
211 for (pretty_name, overloads) in grouped_items {
212 if overloads.len() == 1 {
213 let (mangled, location) = &overloads[0];
214 output.push_str(&format!(
215 "- {} (`{}`, {})\n",
216 pretty_name, mangled, location
217 ));
218 } else {
219 output.push_str(&format!(
220 "- {} ({} overloads)\n",
221 pretty_name,
222 overloads.len()
223 ));
224 for (mangled, location) in overloads {
225 output.push_str(&format!(" - `{}`, {}\n", mangled, location));
226 }
227 }
228 }
229 output.push('\n');
230 }
231
232 output
233}
234
235impl SearchfoxClient {
236 pub async fn search_call_graph(&self, query: &CallGraphQuery) -> Result<serde_json::Value> {
237 let query_string = if let Some(symbol) = &query.calls_from {
238 format!(
239 "calls-from:'{}' depth:{} graph-format:json",
240 symbol, query.depth
241 )
242 } else if let Some(symbol) = &query.calls_to {
243 format!(
244 "calls-to:'{}' depth:{} graph-format:json",
245 symbol, query.depth
246 )
247 } else if let Some((source, target)) = &query.calls_between {
248 format!(
249 "calls-between-source:'{}' calls-between-target:'{}' depth:{} graph-format:json",
250 source.trim(),
251 target.trim(),
252 query.depth
253 )
254 } else {
255 anyhow::bail!("No call graph query specified");
256 };
257
258 let mut url = Url::parse(&format!(
259 "https://searchfox.org/{}/query/default",
260 self.repo
261 ))?;
262 url.query_pairs_mut().append_pair("q", &query_string);
263
264 let response = self.get(url).await?;
265
266 if !response.status().is_success() {
267 anyhow::bail!("Request failed: {}", response.status());
268 }
269
270 let response_text = response.text().await?;
271
272 match serde_json::from_str::<serde_json::Value>(&response_text) {
273 Ok(json) => {
274 if let Some(symbol_graph) = json.get("SymbolGraphCollection") {
275 Ok(symbol_graph.clone())
276 } else {
277 match serde_json::from_str::<SearchfoxResponse>(&response_text) {
278 Ok(parsed_json) => {
279 let mut result = serde_json::json!({});
280 for (key, value) in &parsed_json {
281 if !key.starts_with('*')
282 && (value.as_array().is_some() || value.as_object().is_some())
283 {
284 result[key] = value.clone();
285 }
286 }
287 Ok(result)
288 }
289 Err(_) => Ok(json),
290 }
291 }
292 }
293 Err(_) => Ok(serde_json::json!({
294 "error": "Failed to parse response as JSON",
295 "raw_response": response_text
296 })),
297 }
298 }
299}