searchfox_lib/
search.rs

1use crate::client::SearchfoxClient;
2use crate::types::{File, SearchfoxResponse};
3use anyhow::Result;
4use log::{debug, warn};
5use reqwest::Url;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum CategoryFilter {
9    All,
10    ExcludeTests,
11    ExcludeGenerated,
12    ExcludeTestsAndGenerated,
13    OnlyTests,
14    OnlyGenerated,
15    OnlyNormal,
16}
17
18impl CategoryFilter {
19    pub fn should_include(&self, category: &str) -> bool {
20        match self {
21            CategoryFilter::All => true,
22            CategoryFilter::ExcludeTests => category != "test",
23            CategoryFilter::ExcludeGenerated => category != "generated",
24            CategoryFilter::ExcludeTestsAndGenerated => {
25                category != "test" && category != "generated"
26            }
27            CategoryFilter::OnlyTests => category == "test",
28            CategoryFilter::OnlyGenerated => category == "generated",
29            CategoryFilter::OnlyNormal => category == "normal",
30        }
31    }
32}
33
34#[derive(Debug, Clone)]
35pub struct SearchOptions {
36    pub query: Option<String>,
37    pub path: Option<String>,
38    pub case: bool,
39    pub regexp: bool,
40    pub limit: usize,
41    pub context: Option<usize>,
42    pub symbol: Option<String>,
43    pub id: Option<String>,
44    pub cpp: bool,
45    pub c_lang: bool,
46    pub webidl: bool,
47    pub js: bool,
48    pub category_filter: CategoryFilter,
49}
50
51impl Default for SearchOptions {
52    fn default() -> Self {
53        Self {
54            query: None,
55            path: None,
56            case: false,
57            regexp: false,
58            limit: 50,
59            context: None,
60            symbol: None,
61            id: None,
62            cpp: false,
63            c_lang: false,
64            webidl: false,
65            js: false,
66            category_filter: CategoryFilter::All,
67        }
68    }
69}
70
71impl SearchOptions {
72    pub fn matches_language_filter(&self, path: &str) -> bool {
73        if !self.cpp && !self.c_lang && !self.webidl && !self.js {
74            return true;
75        }
76
77        let path_lower = path.to_lowercase();
78
79        if self.cpp
80            && (path_lower.ends_with(".cc")
81                || path_lower.ends_with(".cpp")
82                || path_lower.ends_with(".h")
83                || path_lower.ends_with(".hh")
84                || path_lower.ends_with(".hpp"))
85        {
86            return true;
87        }
88
89        if self.c_lang && (path_lower.ends_with(".c") || path_lower.ends_with(".h")) {
90            return true;
91        }
92
93        if self.webidl && path_lower.ends_with(".webidl") {
94            return true;
95        }
96
97        if self.js
98            && (path_lower.ends_with(".js")
99                || path_lower.ends_with(".mjs")
100                || path_lower.ends_with(".ts")
101                || path_lower.ends_with(".cjs")
102                || path_lower.ends_with(".jsx")
103                || path_lower.ends_with(".tsx"))
104        {
105            return true;
106        }
107
108        false
109    }
110
111    pub fn build_query(&self) -> String {
112        if let Some(symbol) = &self.symbol {
113            format!("symbol:{symbol}")
114        } else if let Some(id) = &self.id {
115            format!("id:{id}")
116        } else if let Some(q) = &self.query {
117            if q.contains("path:")
118                || q.contains("pathre:")
119                || q.contains("symbol:")
120                || q.contains("id:")
121                || q.contains("text:")
122                || q.contains("re:")
123            {
124                q.clone()
125            } else if let Some(context) = self.context {
126                format!("context:{context} text:{q}")
127            } else {
128                q.clone()
129            }
130        } else {
131            String::new()
132        }
133    }
134}
135
136pub struct SearchResult {
137    pub path: String,
138    pub line_number: usize,
139    pub line: String,
140}
141
142impl SearchfoxClient {
143    pub async fn search(&self, options: &SearchOptions) -> Result<Vec<SearchResult>> {
144        let query = options.build_query();
145
146        let mut url = Url::parse(&format!("https://searchfox.org/{}/search", self.repo))?;
147        url.query_pairs_mut()
148            .append_pair("q", &query)
149            .append_pair("case", if options.case { "true" } else { "false" })
150            .append_pair("regexp", if options.regexp { "true" } else { "false" });
151        if let Some(path) = &options.path {
152            url.query_pairs_mut().append_pair("path", path);
153        }
154
155        let response = self.get(url).await?;
156
157        if !response.status().is_success() {
158            anyhow::bail!("Request failed: {}", response.status());
159        }
160
161        let response_text = response.text().await?;
162        let json: SearchfoxResponse = serde_json::from_str(&response_text)?;
163
164        let mut results = Vec::new();
165        let mut count = 0;
166
167        for (key, value) in &json {
168            if key.starts_with('*') {
169                continue;
170            }
171
172            if !options.category_filter.should_include(key) {
173                continue;
174            }
175
176            if let Some(files_array) = value.as_array() {
177                for file in files_array {
178                    let file: File = match serde_json::from_value(file.clone()) {
179                        Ok(f) => f,
180                        Err(e) => {
181                            warn!("Failed to parse file JSON: {e}");
182                            continue;
183                        }
184                    };
185
186                    if !options.matches_language_filter(&file.path) {
187                        continue;
188                    }
189
190                    if options.path.is_some()
191                        && options.query.is_none()
192                        && options.symbol.is_none()
193                        && options.id.is_none()
194                    {
195                        if count >= options.limit {
196                            break;
197                        }
198                        results.push(SearchResult {
199                            path: file.path.clone(),
200                            line_number: 0,
201                            line: String::new(),
202                        });
203                        count += 1;
204                    } else {
205                        for line in file.lines {
206                            if count >= options.limit {
207                                break;
208                            }
209                            results.push(SearchResult {
210                                path: file.path.clone(),
211                                line_number: line.lno,
212                                line: line.line.trim_end().to_string(),
213                            });
214                            count += 1;
215                        }
216                    }
217                }
218            } else if let Some(obj) = value.as_object() {
219                for (_category, file_list) in obj {
220                    if let Some(files) = file_list.as_array() {
221                        for file in files {
222                            let file: File = match serde_json::from_value(file.clone()) {
223                                Ok(f) => f,
224                                Err(_) => continue,
225                            };
226
227                            if !options.matches_language_filter(&file.path) {
228                                continue;
229                            }
230
231                            if options.path.is_some()
232                                && options.query.is_none()
233                                && options.symbol.is_none()
234                                && options.id.is_none()
235                            {
236                                if count >= options.limit {
237                                    break;
238                                }
239                                results.push(SearchResult {
240                                    path: file.path.clone(),
241                                    line_number: 0,
242                                    line: String::new(),
243                                });
244                                count += 1;
245                            } else {
246                                for line in file.lines {
247                                    if count >= options.limit {
248                                        break;
249                                    }
250                                    results.push(SearchResult {
251                                        path: file.path.clone(),
252                                        line_number: line.lno,
253                                        line: line.line.trim_end().to_string(),
254                                    });
255                                    count += 1;
256                                }
257                            }
258                        }
259                    }
260                }
261            }
262
263            if count >= options.limit {
264                break;
265            }
266        }
267
268        Ok(results)
269    }
270
271    pub async fn find_symbol_locations(
272        &self,
273        symbol: &str,
274        path_filter: Option<&str>,
275        options: &SearchOptions,
276    ) -> Result<Vec<(String, usize)>> {
277        let query = format!("id:{symbol}");
278        let mut url = Url::parse(&format!("https://searchfox.org/{}/search", self.repo))?;
279        url.query_pairs_mut().append_pair("q", &query);
280        if let Some(path) = path_filter {
281            url.query_pairs_mut().append_pair("path", path);
282        }
283
284        let response = self.get(url).await?;
285
286        if !response.status().is_success() {
287            anyhow::bail!("Request failed: {}", response.status());
288        }
289
290        let response_text = response.text().await?;
291        let json: SearchfoxResponse = serde_json::from_str(&response_text)?;
292        let mut file_locations = Vec::new();
293
294        debug!("Analyzing search results...");
295
296        for (key, value) in &json {
297            if key.starts_with('*') {
298                continue;
299            }
300
301            if let Some(files_array) = value.as_array() {
302                debug!("Found {} files in array for key {}", files_array.len(), key);
303                for file in files_array {
304                    match serde_json::from_value::<File>(file.clone()) {
305                        Ok(file) => {
306                            if !options.matches_language_filter(&file.path) {
307                                continue;
308                            }
309
310                            debug!(
311                                "Processing file: {} with {} lines",
312                                file.path,
313                                file.lines.len()
314                            );
315                            for line in file.lines {
316                                if crate::utils::is_potential_definition(&line, symbol) {
317                                    debug!(
318                                        "Found potential definition: {}:{} - {}",
319                                        file.path,
320                                        line.lno,
321                                        line.line.trim()
322                                    );
323                                    file_locations.push((file.path.clone(), line.lno));
324                                }
325                            }
326                        }
327                        Err(e) => {
328                            warn!("Failed to parse file JSON: {e}");
329                        }
330                    }
331                }
332            } else if let Some(categories) = value.as_object() {
333                let symbol_name = symbol.strip_prefix("id:").unwrap_or(symbol);
334                let is_method_search = symbol_name.contains("::");
335
336                if !is_method_search {
337                    let class_def_key = format!("Definitions ({symbol_name})");
338                    if let Some(files_array) =
339                        categories.get(&class_def_key).and_then(|v| v.as_array())
340                    {
341                        for file in files_array {
342                            match serde_json::from_value::<File>(file.clone()) {
343                                Ok(file) => {
344                                    if !options.matches_language_filter(&file.path) {
345                                        continue;
346                                    }
347
348                                    for line in file.lines {
349                                        if line.line.contains("class ")
350                                            || line.line.contains("struct ")
351                                        {
352                                            debug!(
353                                                "Found class/struct definition: {}:{} - {}",
354                                                file.path,
355                                                line.lno,
356                                                line.line.trim()
357                                            );
358                                            file_locations.push((file.path.clone(), line.lno));
359                                        }
360                                    }
361                                }
362                                Err(_) => continue,
363                            }
364                        }
365                    }
366                }
367
368                let search_order = if is_method_search {
369                    vec!["Definitions", "Declarations"]
370                } else {
371                    vec!["Declarations", "Definitions"]
372                };
373
374                for search_type in search_order {
375                    for (category_name, category_value) in categories {
376                        if !is_method_search {
377                            let class_def_key = format!("Definitions ({symbol_name})");
378                            if category_name == &class_def_key {
379                                continue;
380                            }
381                        }
382
383                        if category_name.contains(search_type)
384                            && (category_name.contains(symbol_name)
385                                || category_name
386                                    .to_lowercase()
387                                    .contains(&symbol_name.to_lowercase()))
388                        {
389                            if let Some(files_array) = category_value.as_array() {
390                                for file in files_array {
391                                    match serde_json::from_value::<File>(file.clone()) {
392                                        Ok(file) => {
393                                            if !options.matches_language_filter(&file.path) {
394                                                continue;
395                                            }
396
397                                            for line in file.lines {
398                                                if let Some(upsearch) = &line.upsearch {
399                                                    if upsearch.starts_with("symbol:_Z") {
400                                                        return Ok(vec![(
401                                                            file.path.clone(),
402                                                            line.lno,
403                                                        )]);
404                                                    }
405                                                }
406                                                file_locations.push((file.path.clone(), line.lno));
407                                            }
408                                        }
409                                        Err(_) => continue,
410                                    }
411                                }
412                            }
413                        }
414                    }
415
416                    if !file_locations.is_empty() {
417                        break;
418                    }
419                }
420            }
421        }
422
423        Ok(file_locations)
424    }
425}