Skip to main content

searchfox_lib/
call_graph.rs

1use 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}