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            let has_prefix = q.contains("path:")
159                || q.contains("pathre:")
160                || q.contains("symbol:")
161                || q.contains("id:")
162                || q.contains("text:")
163                || q.contains("re:");
164            if let Some(context) = self.context {
165                if has_prefix {
166                    format!("context:{context} {q}")
167                } else {
168                    format!("context:{context} text:{q}")
169                }
170            } else {
171                q.clone()
172            }
173        } else {
174            String::new()
175        }
176    }
177}
178
179pub struct SearchResult {
180    pub path: String,
181    pub line_number: usize,
182    pub line: String,
183    pub context_before: Vec<String>,
184    pub context_after: Vec<String>,
185}
186
187impl SearchfoxClient {
188    pub async fn search(&self, options: &SearchOptions) -> Result<Vec<SearchResult>> {
189        let query = options.build_query();
190
191        let mut url = Url::parse(&format!("https://searchfox.org/{}/search", self.repo))?;
192        url.query_pairs_mut()
193            .append_pair("q", &query)
194            .append_pair("case", if options.case { "true" } else { "false" })
195            .append_pair("regexp", if options.regexp { "true" } else { "false" });
196        if let Some(path) = &options.path {
197            url.query_pairs_mut().append_pair("path", path);
198        }
199
200        let response = self.get(url).await?;
201
202        if !response.status().is_success() {
203            anyhow::bail!("Request failed: {}", response.status());
204        }
205
206        let response_text = response.text().await?;
207        let json: SearchfoxResponse = serde_json::from_str(&response_text)?;
208
209        let mut results = Vec::new();
210        let mut count = 0;
211
212        for (key, value) in &json {
213            if key.starts_with('*') {
214                continue;
215            }
216
217            if !options.category_filter.should_include(key) {
218                continue;
219            }
220
221            if let Some(files_array) = value.as_array() {
222                for file in files_array {
223                    let file: File = match serde_json::from_value(file.clone()) {
224                        Ok(f) => f,
225                        Err(e) => {
226                            warn!("Failed to parse file JSON: {e}");
227                            continue;
228                        }
229                    };
230
231                    if !options.matches_language_filter(&file.path) {
232                        continue;
233                    }
234
235                    if options.path.is_some()
236                        && options.query.is_none()
237                        && options.symbol.is_none()
238                        && options.id.is_none()
239                    {
240                        if count >= options.limit {
241                            break;
242                        }
243                        results.push(SearchResult {
244                            path: file.path.clone(),
245                            line_number: 0,
246                            line: String::new(),
247                            context_before: vec![],
248                            context_after: vec![],
249                        });
250                        count += 1;
251                    } else {
252                        for line in file.lines {
253                            if count >= options.limit {
254                                break;
255                            }
256                            results.push(SearchResult {
257                                path: file.path.clone(),
258                                line_number: line.lno,
259                                line: line.line.trim_end().to_string(),
260                                context_before: line.context_before.unwrap_or_default(),
261                                context_after: line.context_after.unwrap_or_default(),
262                            });
263                            count += 1;
264                        }
265                    }
266                }
267            } else if let Some(obj) = value.as_object() {
268                for (_category, file_list) in obj {
269                    if let Some(files) = file_list.as_array() {
270                        for file in files {
271                            let file: File = match serde_json::from_value(file.clone()) {
272                                Ok(f) => f,
273                                Err(_) => continue,
274                            };
275
276                            if !options.matches_language_filter(&file.path) {
277                                continue;
278                            }
279
280                            if options.path.is_some()
281                                && options.query.is_none()
282                                && options.symbol.is_none()
283                                && options.id.is_none()
284                            {
285                                if count >= options.limit {
286                                    break;
287                                }
288                                results.push(SearchResult {
289                                    path: file.path.clone(),
290                                    line_number: 0,
291                                    line: String::new(),
292                                    context_before: vec![],
293                                    context_after: vec![],
294                                });
295                                count += 1;
296                            } else {
297                                for line in file.lines {
298                                    if count >= options.limit {
299                                        break;
300                                    }
301                                    results.push(SearchResult {
302                                        path: file.path.clone(),
303                                        line_number: line.lno,
304                                        line: line.line.trim_end().to_string(),
305                                        context_before: line.context_before.unwrap_or_default(),
306                                        context_after: line.context_after.unwrap_or_default(),
307                                    });
308                                    count += 1;
309                                }
310                            }
311                        }
312                    }
313                }
314            }
315
316            if count >= options.limit {
317                break;
318            }
319        }
320
321        Ok(results)
322    }
323
324    pub async fn find_symbol_locations(
325        &self,
326        symbol: &str,
327        path_filter: Option<&str>,
328        options: &SearchOptions,
329    ) -> Result<Vec<(String, usize)>> {
330        let is_ctor = is_constructor_pattern(symbol);
331        let search_symbol = if is_ctor {
332            extract_class_name_from_constructor(symbol)
333        } else {
334            symbol.to_string()
335        };
336        let query = format!("id:{search_symbol}");
337        let mut url = Url::parse(&format!("https://searchfox.org/{}/search", self.repo))?;
338        url.query_pairs_mut().append_pair("q", &query);
339        if let Some(path) = path_filter {
340            url.query_pairs_mut().append_pair("path", path);
341        }
342
343        let response = self.get(url).await?;
344
345        if !response.status().is_success() {
346            anyhow::bail!("Request failed: {}", response.status());
347        }
348
349        let response_text = response.text().await?;
350        let json: SearchfoxResponse = serde_json::from_str(&response_text)?;
351        let mut file_locations = Vec::new();
352
353        debug!("Analyzing search results...");
354
355        for (key, value) in &json {
356            if key.starts_with('*') {
357                continue;
358            }
359
360            if let Some(files_array) = value.as_array() {
361                debug!("Found {} files in array for key {}", files_array.len(), key);
362                for file in files_array {
363                    match serde_json::from_value::<File>(file.clone()) {
364                        Ok(file) => {
365                            if !options.matches_language_filter(&file.path) {
366                                continue;
367                            }
368
369                            debug!(
370                                "Processing file: {} with {} lines",
371                                file.path,
372                                file.lines.len()
373                            );
374                            for line in file.lines {
375                                if crate::utils::is_potential_definition(&line, symbol) {
376                                    debug!(
377                                        "Found potential definition: {}:{} - {}",
378                                        file.path,
379                                        line.lno,
380                                        line.line.trim()
381                                    );
382                                    file_locations.push((file.path.clone(), line.lno));
383                                }
384                            }
385                        }
386                        Err(e) => {
387                            warn!("Failed to parse file JSON: {e}");
388                        }
389                    }
390                }
391            } else if let Some(categories) = value.as_object() {
392                let symbol_name = symbol.strip_prefix("id:").unwrap_or(symbol);
393                let is_method_search = symbol_name.contains("::") && !is_ctor;
394
395                if !is_method_search && !is_ctor {
396                    for (category_name, category_value) in categories {
397                        let is_class_def_category = category_name.starts_with("Definitions (")
398                            && (category_name.ends_with(&format!("::{symbol_name})"))
399                                || category_name.ends_with(&format!("({symbol_name})")));
400                        let is_not_constructor =
401                            !category_name.contains(&format!("::{symbol_name}::{symbol_name})"));
402
403                        if !is_class_def_category || !is_not_constructor {
404                            continue;
405                        }
406
407                        debug!("Found class definition category: {}", category_name);
408                        let Some(files_array) = category_value.as_array() else {
409                            continue;
410                        };
411
412                        for file in files_array {
413                            let Ok(file) = serde_json::from_value::<File>(file.clone()) else {
414                                continue;
415                            };
416
417                            if !options.matches_language_filter(&file.path) {
418                                continue;
419                            }
420
421                            let mut class_lines = Vec::new();
422                            for line in file.lines {
423                                if line.line.contains("class ") || line.line.contains("struct ") {
424                                    debug!(
425                                        "Found class/struct definition: {}:{} - {}",
426                                        file.path,
427                                        line.lno,
428                                        line.line.trim()
429                                    );
430                                    class_lines.push((
431                                        file.path.clone(),
432                                        line.lno,
433                                        line.line.clone(),
434                                    ));
435                                }
436                            }
437
438                            if class_lines.is_empty() {
439                                continue;
440                            }
441
442                            for (path, lno, line_text) in &class_lines {
443                                if !line_text.contains("{}") {
444                                    return Ok(vec![(path.clone(), *lno)]);
445                                }
446                            }
447                            let (path, lno, _) = &class_lines[0];
448                            return Ok(vec![(path.clone(), *lno)]);
449                        }
450                    }
451                }
452
453                if is_ctor {
454                    let ctor_method_part = if let Some(colon_pos) = symbol.rfind("::") {
455                        &symbol[colon_pos + 2..]
456                    } else {
457                        symbol
458                    };
459
460                    let mut all_ctor_lines = Vec::new();
461                    for (category_name, category_value) in categories {
462                        if category_name.contains("Definitions")
463                            && category_name.ends_with(&format!("::{ctor_method_part})"))
464                        {
465                            debug!("Found constructor category: {}", category_name);
466                            if let Some(files_array) = category_value.as_array() {
467                                for file in files_array {
468                                    match serde_json::from_value::<File>(file.clone()) {
469                                        Ok(file) => {
470                                            if !options.matches_language_filter(&file.path) {
471                                                continue;
472                                            }
473
474                                            for line in file.lines {
475                                                if crate::utils::is_potential_definition(
476                                                    &line, symbol,
477                                                ) {
478                                                    debug!(
479                                                        "Found constructor definition: {}:{} - {}",
480                                                        file.path,
481                                                        line.lno,
482                                                        line.line.trim()
483                                                    );
484                                                    all_ctor_lines
485                                                        .push((file.path.clone(), line.lno));
486                                                }
487                                            }
488                                        }
489                                        Err(_) => continue,
490                                    }
491                                }
492                            }
493                        }
494                    }
495
496                    if !all_ctor_lines.is_empty() {
497                        return Ok(all_ctor_lines);
498                    }
499                }
500
501                let search_order = if is_method_search || is_ctor {
502                    vec!["Definitions", "Declarations"]
503                } else {
504                    vec!["Declarations", "Definitions"]
505                };
506
507                for search_type in search_order {
508                    for (category_name, category_value) in categories {
509                        if category_name.contains(search_type)
510                            && (category_name.contains(symbol_name)
511                                || category_name
512                                    .to_lowercase()
513                                    .contains(&symbol_name.to_lowercase()))
514                        {
515                            if let Some(files_array) = category_value.as_array() {
516                                for file in files_array {
517                                    match serde_json::from_value::<File>(file.clone()) {
518                                        Ok(file) => {
519                                            if !options.matches_language_filter(&file.path) {
520                                                continue;
521                                            }
522
523                                            for line in file.lines {
524                                                if let Some(upsearch) = &line.upsearch {
525                                                    if upsearch.starts_with("symbol:_Z") {
526                                                        return Ok(vec![(
527                                                            file.path.clone(),
528                                                            line.lno,
529                                                        )]);
530                                                    }
531                                                }
532                                                file_locations.push((file.path.clone(), line.lno));
533                                            }
534                                        }
535                                        Err(_) => continue,
536                                    }
537                                }
538                            }
539                        }
540                    }
541
542                    if !file_locations.is_empty() {
543                        break;
544                    }
545                }
546            }
547        }
548
549        Ok(file_locations)
550    }
551}