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