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