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