Skip to main content

searchfox_lib/
search.rs

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