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