Skip to main content

searchfox_lib/
can_gc.rs

1use crate::call_graph::CallGraphQuery;
2use crate::client::SearchfoxClient;
3use anyhow::Result;
4use reqwest::Url;
5use std::collections::HashSet;
6
7pub struct GcInfo {
8    pub pretty: String,
9    pub mangled: String,
10    pub can_gc: bool,
11    pub gc_path: Option<String>,
12}
13
14impl SearchfoxClient {
15    pub async fn get_gc_info(&self, symbol: &str) -> Result<Vec<GcInfo>> {
16        let results = self.query_gc_from_call_graph(symbol).await?;
17        if !results.is_empty() {
18            return Ok(results);
19        }
20
21        // The partial name didn't work with the call graph — resolve to fully-qualified
22        // names via the id: search endpoint and retry.
23        let full_names = self.resolve_full_names(symbol).await?;
24        let mut all_results = Vec::new();
25        let mut seen_keys: HashSet<(String, bool, Option<String>)> = HashSet::new();
26
27        for full_name in full_names {
28            if full_name == symbol {
29                continue;
30            }
31            for info in self.query_gc_from_call_graph(&full_name).await? {
32                let key = (info.pretty.clone(), info.can_gc, info.gc_path.clone());
33                if seen_keys.insert(key) {
34                    all_results.push(info);
35                }
36            }
37        }
38
39        Ok(all_results)
40    }
41
42    async fn query_gc_from_call_graph(&self, symbol: &str) -> Result<Vec<GcInfo>> {
43        let query = CallGraphQuery {
44            calls_from: Some(symbol.to_string()),
45            calls_to: None,
46            calls_between: None,
47            depth: 1,
48        };
49
50        let result = self.search_call_graph(&query).await?;
51
52        let mut results = Vec::new();
53        let mut seen: HashSet<(String, bool, Option<String>)> = HashSet::new();
54
55        if let Some(jumprefs) = result.get("jumprefs").and_then(|v| v.as_object()) {
56            for (mangled, info) in jumprefs {
57                let pretty = match info.get("pretty").and_then(|v| v.as_str()) {
58                    Some(p) => p,
59                    None => continue,
60                };
61
62                if !symbol_matches(pretty, symbol) {
63                    continue;
64                }
65
66                let meta = match info.get("meta") {
67                    Some(m) => m,
68                    None => continue,
69                };
70
71                let can_gc = match meta.get("canGC").and_then(|v| v.as_bool()) {
72                    Some(v) => v,
73                    None => continue,
74                };
75
76                let gc_path = meta
77                    .get("gcPath")
78                    .and_then(|v| v.as_str())
79                    .map(String::from);
80
81                let key = (pretty.to_string(), can_gc, gc_path.clone());
82                if seen.insert(key) {
83                    results.push(GcInfo {
84                        pretty: pretty.to_string(),
85                        mangled: mangled.clone(),
86                        can_gc,
87                        gc_path,
88                    });
89                }
90            }
91        }
92
93        Ok(results)
94    }
95
96    async fn resolve_full_names(&self, symbol: &str) -> Result<Vec<String>> {
97        let query = format!("id:{symbol}");
98        let mut url = Url::parse(&format!("https://searchfox.org/{}/search", self.repo))?;
99        url.query_pairs_mut().append_pair("q", &query);
100
101        let response = self.get(url).await?;
102        if !response.status().is_success() {
103            return Ok(vec![]);
104        }
105
106        let text = response.text().await?;
107        let json: serde_json::Value = match serde_json::from_str(&text) {
108            Ok(v) => v,
109            Err(_) => return Ok(vec![]),
110        };
111
112        let top = match json.as_object() {
113            Some(o) => o,
114            None => return Ok(vec![]),
115        };
116
117        let symbol_suffix = if let Some(pos) = symbol.rfind("::") {
118            &symbol[pos + 2..]
119        } else {
120            symbol
121        };
122
123        let mut names = Vec::new();
124
125        // The response has top-level keys like "normal"/"generated" whose values are
126        // objects mapping category names like "Definitions (full::Name)" to file arrays.
127        for (top_key, top_val) in top {
128            if top_key.starts_with('*') {
129                continue;
130            }
131            let categories = match top_val.as_object() {
132                Some(o) => o,
133                None => continue,
134            };
135            for cat_key in categories.keys() {
136                if (cat_key.contains("Definitions") || cat_key.contains("Declarations"))
137                    && cat_key.ends_with(')')
138                {
139                    if let Some(open) = cat_key.rfind('(') {
140                        let name = &cat_key[open + 1..cat_key.len() - 1];
141                        if name == symbol
142                            || name.ends_with(&format!("::{symbol}"))
143                            || name.ends_with(&format!("::{symbol_suffix}"))
144                        {
145                            names.push(name.to_string());
146                        }
147                    }
148                }
149            }
150        }
151
152        names.sort();
153        names.dedup();
154        Ok(names)
155    }
156}
157
158fn symbol_matches(pretty: &str, query: &str) -> bool {
159    pretty == query
160        || pretty.ends_with(&format!("::{query}"))
161        || pretty.starts_with(&format!("{query}("))
162        || pretty.starts_with(&format!("{query}<"))
163        || pretty.contains(&format!("::{query}("))
164        || pretty.contains(&format!("::{query}<"))
165}