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 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.get(&from_sym)
60 .and_then(|s| s.get("pretty"))
61 .and_then(|p| p.as_str())
62 .unwrap_or(&from_sym)
63 } else {
64 &from_sym
65 };
66
67 let from_location = if let Some(jumprefs) = jumprefs {
68 jumprefs.get(&from_sym)
69 .and_then(|s| s.get("jumps"))
70 .and_then(|j| j.get("def"))
71 .and_then(|d| d.as_str())
72 .unwrap_or("")
73 } else {
74 ""
75 };
76
77 let to_pretty = if let Some(jumprefs) = jumprefs {
78 jumprefs.get(&to_sym)
79 .and_then(|s| s.get("pretty"))
80 .and_then(|p| p.as_str())
81 .unwrap_or(&to_sym)
82 } else {
83 &to_sym
84 };
85
86 let to_location = if let Some(jumprefs) = jumprefs {
87 jumprefs.get(&to_sym)
88 .and_then(|s| s.get("jumps"))
89 .and_then(|j| j.get("def"))
90 .and_then(|d| d.as_str())
91 .unwrap_or("")
92 } else {
93 ""
94 };
95
96 output.push_str(&format!("- **{}** ({}) calls **{}** ({})\n",
97 from_pretty, from_location, to_pretty, to_location));
98 output.push_str(&format!(" - From: `{}`\n", from_sym));
99 output.push_str(&format!(" - To: `{}`\n", to_sym));
100 }
101 }
102
103 return output;
104 }
105 }
106
107 let mut grouped_by_parent: BTreeMap<String, BTreeSet<(String, String, String, String)>> = BTreeMap::new();
108
109 let jumprefs = json.get("jumprefs").and_then(|v| v.as_object());
110
111 let is_calls_to = query_text.contains("calls-to:");
112
113 if let Some(graphs) = json.get("graphs").and_then(|v| v.as_array()) {
114 for graph in graphs {
115 if let Some(edges) = graph.get("edges").and_then(|v| v.as_array()) {
116 for edge in edges {
117 if let Some(edge_obj) = edge.as_object() {
118 let target_sym = if is_calls_to {
119 edge_obj.get("from").and_then(|v| v.as_str()).unwrap_or("")
120 } else {
121 edge_obj.get("to").and_then(|v| v.as_str()).unwrap_or("")
122 };
123
124 if let Some(jumprefs) = jumprefs {
125 if let Some(symbol_info) = jumprefs.get(target_sym) {
126 let pretty_name = symbol_info
127 .get("pretty")
128 .and_then(|v| v.as_str())
129 .unwrap_or("");
130 let mangled = symbol_info
131 .get("sym")
132 .and_then(|v| v.as_str())
133 .unwrap_or(target_sym);
134
135 let jumps = symbol_info.get("jumps");
136
137 let decl_location = jumps
138 .and_then(|j| j.get("decl"))
139 .and_then(|v| v.as_str())
140 .unwrap_or("");
141
142 let def_location = jumps
143 .and_then(|j| j.get("def"))
144 .and_then(|v| v.as_str())
145 .unwrap_or("");
146
147 let location = if !def_location.is_empty() && !decl_location.is_empty() && def_location != decl_location {
148 format!("{} (decl: {})", def_location, decl_location)
149 } else if !def_location.is_empty() {
150 def_location.to_string()
151 } else if !decl_location.is_empty() {
152 decl_location.to_string()
153 } else {
154 String::new()
155 };
156
157 let parent_sym = symbol_info
158 .get("meta")
159 .and_then(|m| m.get("parentsym"))
160 .and_then(|v| v.as_str())
161 .unwrap_or("Free functions");
162
163 let parent_sym_clean = if parent_sym.starts_with("T_") {
164 &parent_sym[2..]
165 } else if parent_sym == "Free functions" {
166 parent_sym
167 } else {
168 parent_sym
169 };
170
171 if !pretty_name.is_empty() && !location.is_empty() {
172 grouped_by_parent
173 .entry(parent_sym_clean.to_string())
174 .or_insert_with(BTreeSet::new)
175 .insert((
176 pretty_name.to_string(),
177 mangled.to_string(),
178 location,
179 String::new(),
180 ));
181 }
182 }
183 }
184 }
185 }
186 }
187 }
188 }
189
190 for (parent_sym, items) in grouped_by_parent {
191 output.push_str(&format!("## {}\n\n", parent_sym));
192
193 let mut grouped_items: Vec<(String, Vec<(String, String)>)> = Vec::new();
194
195 for (pretty_name, mangled, location, _) in items {
196 if let Some((last_pretty, last_overloads)) = grouped_items.last_mut() {
197 if last_pretty == &pretty_name {
198 last_overloads.push((mangled, location));
199 continue;
200 }
201 }
202 grouped_items.push((pretty_name, vec![(mangled, location)]));
203 }
204
205 for (pretty_name, overloads) in grouped_items {
206 if overloads.len() == 1 {
207 let (mangled, location) = &overloads[0];
208 output.push_str(&format!("- {} (`{}`, {})\n", pretty_name, mangled, location));
209 } else {
210 output.push_str(&format!("- {} ({} overloads)\n", pretty_name, overloads.len()));
211 for (mangled, location) in overloads {
212 output.push_str(&format!(" - `{}`, {}\n", mangled, location));
213 }
214 }
215 }
216 output.push('\n');
217 }
218
219 output
220}
221
222impl SearchfoxClient {
223 pub async fn search_call_graph(&self, query: &CallGraphQuery) -> Result<serde_json::Value> {
224 let query_string = if let Some(symbol) = &query.calls_from {
225 format!(
226 "calls-from:'{}' depth:{} graph-format:json",
227 symbol, query.depth
228 )
229 } else if let Some(symbol) = &query.calls_to {
230 format!(
231 "calls-to:'{}' depth:{} graph-format:json",
232 symbol, query.depth
233 )
234 } else if let Some((source, target)) = &query.calls_between {
235 format!(
236 "calls-between-source:'{}' calls-between-target:'{}' depth:{} graph-format:json",
237 source.trim(),
238 target.trim(),
239 query.depth
240 )
241 } else {
242 anyhow::bail!("No call graph query specified");
243 };
244
245 let mut url = Url::parse(&format!(
246 "https://searchfox.org/{}/query/default",
247 self.repo
248 ))?;
249 url.query_pairs_mut().append_pair("q", &query_string);
250
251 let response = self.get(url).await?;
252
253 if !response.status().is_success() {
254 anyhow::bail!("Request failed: {}", response.status());
255 }
256
257 let response_text = response.text().await?;
258
259 match serde_json::from_str::<serde_json::Value>(&response_text) {
260 Ok(json) => {
261 if let Some(symbol_graph) = json.get("SymbolGraphCollection") {
262 Ok(symbol_graph.clone())
263 } else {
264 match serde_json::from_str::<SearchfoxResponse>(&response_text) {
265 Ok(parsed_json) => {
266 let mut result = serde_json::json!({});
267 for (key, value) in &parsed_json {
268 if !key.starts_with('*')
269 && (value.as_array().is_some() || value.as_object().is_some())
270 {
271 result[key] = value.clone();
272 }
273 }
274 Ok(result)
275 }
276 Err(_) => Ok(json),
277 }
278 }
279 }
280 Err(_) => Ok(serde_json::json!({
281 "error": "Failed to parse response as JSON",
282 "raw_response": response_text
283 })),
284 }
285 }
286}