Skip to main content

searchfox_lib/
call_graph.rs

1use 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        // Searchfox returns HTML with the result embedded in a script tag:
277        // var QUERY_RESULTS_JSON = { "SymbolGraphCollection": { ... } };
278        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}