Skip to main content

reflex/parsers/
rust.rs

1//! Rust language parser using Tree-sitter
2//!
3//! Extracts symbols from Rust source code:
4//! - Functions (fn)
5//! - Structs
6//! - Enums
7//! - Traits
8//! - Impl blocks
9//! - Constants
10//! - Static variables
11//! - Local variables (let bindings)
12//! - Modules
13//! - Type aliases
14//! - Macros (macro_rules! definitions)
15
16use anyhow::{Context, Result};
17use streaming_iterator::StreamingIterator;
18use tree_sitter::{Parser, Query, QueryCursor};
19use crate::models::{Language, SearchResult, Span, SymbolKind};
20use crate::parsers::{DependencyExtractor, ImportInfo};
21
22/// Parse Rust source code and extract symbols
23pub fn parse(path: &str, source: &str) -> Result<Vec<SearchResult>> {
24    let mut parser = Parser::new();
25    let language = tree_sitter_rust::LANGUAGE;
26
27    parser
28        .set_language(&language.into())
29        .context("Failed to set Rust language")?;
30
31    let tree = parser
32        .parse(source, None)
33        .context("Failed to parse Rust source")?;
34
35    let root_node = tree.root_node();
36
37    let mut symbols = Vec::new();
38
39    // Extract different types of symbols using Tree-sitter queries
40    symbols.extend(extract_functions(source, &root_node)?);
41    symbols.extend(extract_structs(source, &root_node)?);
42    symbols.extend(extract_enums(source, &root_node)?);
43    symbols.extend(extract_traits(source, &root_node)?);
44    symbols.extend(extract_impls(source, &root_node)?);
45    symbols.extend(extract_constants(source, &root_node)?);
46    symbols.extend(extract_statics(source, &root_node)?);
47    symbols.extend(extract_local_variables(source, &root_node)?);
48    symbols.extend(extract_modules(source, &root_node)?);
49    symbols.extend(extract_type_aliases(source, &root_node)?);
50    symbols.extend(extract_macros(source, &root_node)?);
51    symbols.extend(extract_attributes(source, &root_node)?);
52
53
54    // Add file path to all symbols
55    for symbol in &mut symbols {
56        symbol.path = path.to_string();
57        symbol.lang = Language::Rust;
58    }
59
60    Ok(symbols)
61}
62
63/// Extract function definitions
64fn extract_functions(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
65    let language = tree_sitter_rust::LANGUAGE;
66    let query_str = r#"
67        (function_item
68            name: (identifier) @name) @function
69    "#;
70
71    let query = Query::new(&language.into(), query_str)
72        .context("Failed to create function query")?;
73
74    extract_symbols(source, root, &query, SymbolKind::Function, None)
75}
76
77/// Extract struct definitions
78fn extract_structs(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
79    let language = tree_sitter_rust::LANGUAGE;
80    let query_str = r#"
81        (struct_item
82            name: (type_identifier) @name) @struct
83    "#;
84
85    let query = Query::new(&language.into(), query_str)
86        .context("Failed to create struct query")?;
87
88    extract_symbols(source, root, &query, SymbolKind::Struct, None)
89}
90
91/// Extract enum definitions
92fn extract_enums(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
93    let language = tree_sitter_rust::LANGUAGE;
94    let query_str = r#"
95        (enum_item
96            name: (type_identifier) @name) @enum
97    "#;
98
99    let query = Query::new(&language.into(), query_str)
100        .context("Failed to create enum query")?;
101
102    extract_symbols(source, root, &query, SymbolKind::Enum, None)
103}
104
105/// Extract trait definitions
106fn extract_traits(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
107    let language = tree_sitter_rust::LANGUAGE;
108    let query_str = r#"
109        (trait_item
110            name: (type_identifier) @name) @trait
111    "#;
112
113    let query = Query::new(&language.into(), query_str)
114        .context("Failed to create trait query")?;
115
116    extract_symbols(source, root, &query, SymbolKind::Trait, None)
117}
118
119/// Extract impl blocks
120fn extract_impls(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
121    let language = tree_sitter_rust::LANGUAGE;
122
123    // Extract methods from impl blocks
124    let query_str = r#"
125        (impl_item
126            type: (type_identifier) @impl_name
127            body: (declaration_list
128                (function_item
129                    name: (identifier) @method_name))) @impl
130    "#;
131
132    let query = Query::new(&language.into(), query_str)
133        .context("Failed to create impl query")?;
134
135    let mut cursor = QueryCursor::new();
136    let mut matches = cursor.matches(&query, *root, source.as_bytes());
137
138    let mut symbols = Vec::new();
139
140    while let Some(match_) = matches.next() {
141        let mut impl_name = None;
142        let mut method_name = None;
143        let mut method_node = None;
144
145        for capture in match_.captures {
146            let capture_name: &str = &query.capture_names()[capture.index as usize];
147            match capture_name {
148                "impl_name" => {
149                    impl_name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
150                }
151                "method_name" => {
152                    method_name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
153                    // Find the parent function_item node
154                    let mut current = capture.node;
155                    while let Some(parent) = current.parent() {
156                        if parent.kind() == "function_item" {
157                            method_node = Some(parent);
158                            break;
159                        }
160                        current = parent;
161                    }
162                }
163                _ => {}
164            }
165        }
166
167        if let (Some(impl_name), Some(method_name), Some(node)) = (impl_name, method_name, method_node) {
168            let scope = format!("impl {}", impl_name);
169            let span = node_to_span(&node);
170            let preview = extract_preview(source, &span);
171
172            symbols.push(SearchResult::new(
173                String::new(), // Path will be filled in later
174                Language::Rust,
175                SymbolKind::Method,
176                Some(method_name),
177                span,
178                Some(scope),
179                preview,
180            ));
181        }
182    }
183
184    Ok(symbols)
185}
186
187/// Extract constants
188fn extract_constants(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
189    let language = tree_sitter_rust::LANGUAGE;
190    let query_str = r#"
191        (const_item
192            name: (identifier) @name) @const
193    "#;
194
195    let query = Query::new(&language.into(), query_str)
196        .context("Failed to create const query")?;
197
198    extract_symbols(source, root, &query, SymbolKind::Constant, None)
199}
200
201/// Extract static variables
202fn extract_statics(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
203    let language = tree_sitter_rust::LANGUAGE;
204    let query_str = r#"
205        (static_item
206            name: (identifier) @name) @static
207    "#;
208
209    let query = Query::new(&language.into(), query_str)
210        .context("Failed to create static query")?;
211
212    extract_symbols(source, root, &query, SymbolKind::Variable, None)
213}
214
215/// Extract local variable bindings (let statements)
216fn extract_local_variables(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
217    let language = tree_sitter_rust::LANGUAGE;
218    let query_str = r#"
219        (let_declaration
220            pattern: (identifier) @name) @let
221    "#;
222
223    let query = Query::new(&language.into(), query_str)
224        .context("Failed to create let declaration query")?;
225
226    extract_symbols(source, root, &query, SymbolKind::Variable, None)
227}
228
229/// Extract module declarations
230fn extract_modules(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
231    let language = tree_sitter_rust::LANGUAGE;
232    let query_str = r#"
233        (mod_item
234            name: (identifier) @name) @module
235    "#;
236
237    let query = Query::new(&language.into(), query_str)
238        .context("Failed to create module query")?;
239
240    extract_symbols(source, root, &query, SymbolKind::Module, None)
241}
242
243/// Extract type aliases
244fn extract_type_aliases(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
245    let language = tree_sitter_rust::LANGUAGE;
246    let query_str = r#"
247        (type_item
248            name: (type_identifier) @name) @type
249    "#;
250
251    let query = Query::new(&language.into(), query_str)
252        .context("Failed to create type query")?;
253
254    extract_symbols(source, root, &query, SymbolKind::Type, None)
255}
256
257/// Extract macro definitions (macro_rules!)
258fn extract_macros(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
259    let language = tree_sitter_rust::LANGUAGE;
260    let query_str = r#"
261        (macro_definition
262            name: (identifier) @name) @macro
263    "#;
264
265    let query = Query::new(&language.into(), query_str)
266        .context("Failed to create macro query")?;
267
268    extract_symbols(source, root, &query, SymbolKind::Macro, None)
269}
270
271/// Extract attributes: BOTH definitions and uses
272/// Definitions: #[proc_macro_attribute] pub fn route(...)
273/// Uses: #[test] fn my_test(), #[derive(Debug)] struct Foo
274fn extract_attributes(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
275    let language = tree_sitter_rust::LANGUAGE;
276    let mut symbols = Vec::new();
277
278    // Part 1: Extract attribute DEFINITIONS (proc macro attributes)
279    let func_query_str = r#"
280        (function_item
281            name: (identifier) @name) @function
282    "#;
283
284    let func_query = Query::new(&language.into(), func_query_str)
285        .context("Failed to create function query")?;
286
287    let mut cursor = QueryCursor::new();
288    let mut matches = cursor.matches(&func_query, *root, source.as_bytes());
289
290    while let Some(match_) = matches.next() {
291        let mut name = None;
292        let mut func_node = None;
293
294        for capture in match_.captures {
295            let capture_name: &str = &func_query.capture_names()[capture.index as usize];
296            match capture_name {
297                "name" => {
298                    name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
299                }
300                "function" => {
301                    func_node = Some(capture.node);
302                }
303                _ => {}
304            }
305        }
306
307        // Check if this function has #[proc_macro_attribute] attribute
308        if let (Some(name), Some(func_node)) = (name, func_node) {
309            let mut has_proc_macro_attr = false;
310
311            if let Some(parent) = func_node.parent() {
312                let mut func_index = None;
313                for i in 0..parent.child_count() {
314                    if let Some(child) = parent.child(i) {
315                        if child.id() == func_node.id() {
316                            func_index = Some(i);
317                            break;
318                        }
319                    }
320                }
321
322                if let Some(func_idx) = func_index {
323                    for i in (0..func_idx).rev() {
324                        if let Some(child) = parent.child(i) {
325                            if child.kind() == "attribute_item" {
326                                let attr_text = child.utf8_text(source.as_bytes()).unwrap_or("");
327                                if attr_text.contains("proc_macro_attribute") {
328                                    has_proc_macro_attr = true;
329                                }
330                            } else if !child.kind().contains("comment") && child.kind() != "line_comment" {
331                                break;
332                            }
333                        }
334                    }
335                }
336            }
337
338            if has_proc_macro_attr {
339                let span = node_to_span(&func_node);
340                let preview = extract_preview(source, &span);
341
342                symbols.push(SearchResult::new(
343                    String::new(),
344                    Language::Rust,
345                    SymbolKind::Attribute,
346                    Some(name),
347                    span,
348                    None,
349                    preview,
350                ));
351            }
352        }
353    }
354
355    // Part 2: Extract attribute USES (#[test], #[derive(...)], etc.)
356    let attr_query_str = r#"
357        (attribute_item
358            (attribute
359                (identifier) @attr_name)) @attr
360    "#;
361
362    let attr_query = Query::new(&language.into(), attr_query_str)
363        .context("Failed to create attribute use query")?;
364
365    let mut cursor = QueryCursor::new();
366    let mut matches = cursor.matches(&attr_query, *root, source.as_bytes());
367
368    while let Some(match_) = matches.next() {
369        let mut attr_name = None;
370        let mut attr_node = None;
371
372        for capture in match_.captures {
373            let capture_name: &str = &attr_query.capture_names()[capture.index as usize];
374            match capture_name {
375                "attr_name" => {
376                    attr_name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
377                }
378                "attr" => {
379                    attr_node = Some(capture.node);
380                }
381                _ => {}
382            }
383        }
384
385        if let (Some(name), Some(node)) = (attr_name, attr_node) {
386            let span = node_to_span(&node);
387            let preview = extract_preview(source, &span);
388
389            symbols.push(SearchResult::new(
390                String::new(),
391                Language::Rust,
392                SymbolKind::Attribute,
393                Some(name),
394                span,
395                None,
396                preview,
397            ));
398        }
399    }
400
401    Ok(symbols)
402}
403
404/// Generic symbol extraction helper
405fn extract_symbols(
406    source: &str,
407    root: &tree_sitter::Node,
408    query: &Query,
409    kind: SymbolKind,
410    scope: Option<String>,
411) -> Result<Vec<SearchResult>> {
412    let mut cursor = QueryCursor::new();
413    let mut matches = cursor.matches(query, *root, source.as_bytes());
414
415    let mut symbols = Vec::new();
416
417    while let Some(match_) = matches.next() {
418        // Find the name capture and the full node
419        let mut name = None;
420        let mut full_node = None;
421
422        for capture in match_.captures {
423            let capture_name: &str = &query.capture_names()[capture.index as usize];
424            if capture_name == "name" {
425                name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
426            } else {
427                // Assume any other capture is the full node
428                full_node = Some(capture.node);
429            }
430        }
431
432        if let (Some(name), Some(node)) = (name, full_node) {
433            let span = node_to_span(&node);
434            let preview = extract_preview(source, &span);
435
436            symbols.push(SearchResult::new(
437                String::new(), // Path will be filled in later
438                Language::Rust,
439                kind.clone(),
440                Some(name),
441                span,
442                scope.clone(),
443                preview,
444            ));
445        }
446    }
447
448    Ok(symbols)
449}
450
451/// Convert a Tree-sitter node to a Span
452fn node_to_span(node: &tree_sitter::Node) -> Span {
453    let start = node.start_position();
454    let end = node.end_position();
455
456    Span::new(
457        start.row + 1,  // Convert 0-indexed to 1-indexed
458        start.column,
459        end.row + 1,
460        end.column,
461    )
462}
463
464/// Extract a preview (5-7 lines) around the symbol
465fn extract_preview(source: &str, span: &Span) -> String {
466    let lines: Vec<&str> = source.lines().collect();
467
468    // Extract 7 lines: the start line and 6 following lines
469    // This provides enough context for AI agents to understand the code
470    let start_idx = (span.start_line - 1) as usize; // Convert back to 0-indexed
471    let end_idx = (start_idx + 7).min(lines.len());
472
473    lines[start_idx..end_idx].join("\n")
474}
475
476/// Rust dependency extractor implementation
477pub struct RustDependencyExtractor;
478
479impl DependencyExtractor for RustDependencyExtractor {
480    fn extract_dependencies(source: &str) -> Result<Vec<ImportInfo>> {
481        let mut parser = Parser::new();
482        let language = tree_sitter_rust::LANGUAGE;
483
484        parser
485            .set_language(&language.into())
486            .context("Failed to set Rust language")?;
487
488        let tree = parser
489            .parse(source, None)
490            .context("Failed to parse Rust source")?;
491
492        let root_node = tree.root_node();
493
494        let mut imports = Vec::new();
495
496        // Extract use declarations
497        imports.extend(extract_use_declarations(source, &root_node)?);
498
499        // Extract mod items (module declarations)
500        imports.extend(extract_mod_items(source, &root_node)?);
501
502        // Extract extern crate declarations
503        imports.extend(extract_extern_crates(source, &root_node)?);
504
505        Ok(imports)
506    }
507}
508
509/// Extract use declarations (use std::collections::HashMap)
510fn extract_use_declarations(source: &str, root: &tree_sitter::Node) -> Result<Vec<ImportInfo>> {
511    let language = tree_sitter_rust::LANGUAGE;
512    let query_str = r#"
513        (use_declaration) @use
514    "#;
515
516    let query = Query::new(&language.into(), query_str)
517        .context("Failed to create use declaration query")?;
518
519    let mut cursor = QueryCursor::new();
520    let mut matches = cursor.matches(&query, *root, source.as_bytes());
521
522    let mut imports = Vec::new();
523
524    while let Some(match_) = matches.next() {
525        for capture in match_.captures {
526            let node = capture.node;
527            let text = node.utf8_text(source.as_bytes()).unwrap_or("");
528            let line_number = node.start_position().row + 1;
529
530            // Parse the use declaration text
531            let path_info = parse_rust_use_declaration(text);
532
533            for (path, symbols) in path_info {
534                let import_type = classify_rust_import(&path);
535
536                imports.push(ImportInfo {
537                    imported_path: path,
538                    import_type,
539                    line_number,
540                    imported_symbols: symbols,
541                });
542            }
543        }
544    }
545
546    Ok(imports)
547}
548
549/// Extract mod items (mod parser;)
550fn extract_mod_items(source: &str, root: &tree_sitter::Node) -> Result<Vec<ImportInfo>> {
551    let language = tree_sitter_rust::LANGUAGE;
552    let query_str = r#"
553        (mod_item
554            name: (identifier) @name) @mod
555    "#;
556
557    let query = Query::new(&language.into(), query_str)
558        .context("Failed to create mod item query")?;
559
560    let mut cursor = QueryCursor::new();
561    let mut matches = cursor.matches(&query, *root, source.as_bytes());
562
563    let mut imports = Vec::new();
564
565    while let Some(match_) = matches.next() {
566        let mut name = None;
567        let mut mod_node = None;
568
569        for capture in match_.captures {
570            let capture_name: &str = &query.capture_names()[capture.index as usize];
571            match capture_name {
572                "name" => {
573                    name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
574                }
575                "mod" => {
576                    mod_node = Some(capture.node);
577                }
578                _ => {}
579            }
580        }
581
582        if let (Some(name), Some(node)) = (name, mod_node) {
583            // Check if this is an external module declaration (no body)
584            let has_body = node.child_by_field_name("body").is_some();
585
586            if !has_body {
587                // This is an external module reference (mod parser;)
588                let line_number = node.start_position().row + 1;
589
590                imports.push(ImportInfo {
591                    imported_path: name,
592                    import_type: crate::models::ImportType::Internal,
593                    line_number,
594                    imported_symbols: None,
595                });
596            }
597        }
598    }
599
600    Ok(imports)
601}
602
603/// Extract extern crate declarations (extern crate serde;)
604fn extract_extern_crates(source: &str, root: &tree_sitter::Node) -> Result<Vec<ImportInfo>> {
605    let language = tree_sitter_rust::LANGUAGE;
606    let query_str = r#"
607        (extern_crate_declaration
608            name: (identifier) @name) @extern
609    "#;
610
611    let query = Query::new(&language.into(), query_str)
612        .context("Failed to create extern crate query")?;
613
614    let mut cursor = QueryCursor::new();
615    let mut matches = cursor.matches(&query, *root, source.as_bytes());
616
617    let mut imports = Vec::new();
618
619    while let Some(match_) = matches.next() {
620        let mut name = None;
621        let mut extern_node = None;
622
623        for capture in match_.captures {
624            let capture_name: &str = &query.capture_names()[capture.index as usize];
625            match capture_name {
626                "name" => {
627                    name = Some(capture.node.utf8_text(source.as_bytes()).unwrap_or("").to_string());
628                }
629                "extern" => {
630                    extern_node = Some(capture.node);
631                }
632                _ => {}
633            }
634        }
635
636        if let (Some(name), Some(node)) = (name, extern_node) {
637            let line_number = node.start_position().row + 1;
638            let import_type = classify_rust_import(&name);
639
640            imports.push(ImportInfo {
641                imported_path: name,
642                import_type,
643                line_number,
644                imported_symbols: None,
645            });
646        }
647    }
648
649    Ok(imports)
650}
651
652/// Classify a Rust import path as Internal, External, or Stdlib
653fn classify_rust_import(path: &str) -> crate::models::ImportType {
654    use crate::models::ImportType;
655
656    if path.starts_with("std::") || path.starts_with("core::") || path.starts_with("alloc::") {
657        ImportType::Stdlib
658    } else if path.starts_with("crate::") || path.starts_with("super::") || path.starts_with("self::") {
659        ImportType::Internal
660    } else {
661        // External crate
662        ImportType::External
663    }
664}
665
666/// Parse a Rust use declaration and extract path(s) and symbols
667///
668/// Handles:
669/// - Simple: use std::collections::HashMap;
670/// - With symbols: use std::collections::{HashMap, HashSet};
671/// - Nested: use std::{io, fs};
672/// - With aliases: use std::io::Result as IoResult;
673/// - Glob: use std::collections::*;
674fn parse_rust_use_declaration(text: &str) -> Vec<(String, Option<Vec<String>>)> {
675    // Remove visibility modifiers and keywords
676    let text = text.trim()
677        .strip_prefix("pub(crate)").unwrap_or(text)
678        .trim()
679        .strip_prefix("pub(super)").unwrap_or(text)
680        .trim()
681        .strip_prefix("pub").unwrap_or(text)
682        .trim()
683        .strip_prefix("use").unwrap_or(text)
684        .trim()
685        .strip_suffix(";").unwrap_or(text)
686        .trim();
687
688    // Handle different patterns
689    if text.contains('{') {
690        // Has braces - extract base path and symbols
691        if let Some(idx) = text.find('{') {
692            let base_path = text[..idx].trim_end_matches("::").to_string();
693
694            if let Some(end) = text.find('}') {
695                let symbols_str = &text[idx + 1..end];
696                let symbols: Vec<String> = symbols_str
697                    .split(',')
698                    .map(|s| {
699                        // Handle aliases like "HashMap as Map" - extract the imported name
700                        let trimmed = s.trim();
701                        if let Some(as_idx) = trimmed.find(" as ") {
702                            trimmed[..as_idx].trim().to_string()
703                        } else {
704                            trimmed.to_string()
705                        }
706                    })
707                    .filter(|s| !s.is_empty() && s != "*")
708                    .collect();
709
710                if !symbols.is_empty() {
711                    return vec![(base_path, Some(symbols))];
712                }
713            }
714        }
715    }
716
717    // Simple path (possibly with alias)
718    let path = if let Some(as_idx) = text.find(" as ") {
719        text[..as_idx].trim().to_string()
720    } else {
721        text.to_string()
722    };
723
724    vec![(path, None)]
725}
726
727#[cfg(test)]
728mod tests {
729    use super::*;
730
731    #[test]
732    fn test_parse_function() {
733        let source = r#"
734            fn hello_world() {
735                println!("Hello, world!");
736            }
737        "#;
738
739        let symbols = parse("test.rs", source).unwrap();
740        assert_eq!(symbols.len(), 1);
741        assert_eq!(symbols[0].symbol.as_deref(), Some("hello_world"));
742        assert!(matches!(symbols[0].kind, SymbolKind::Function));
743    }
744
745    #[test]
746    fn test_parse_struct() {
747        let source = r#"
748            struct User {
749                name: String,
750                age: u32,
751            }
752        "#;
753
754        let symbols = parse("test.rs", source).unwrap();
755        assert_eq!(symbols.len(), 1);
756        assert_eq!(symbols[0].symbol.as_deref(), Some("User"));
757        assert!(matches!(symbols[0].kind, SymbolKind::Struct));
758    }
759
760    #[test]
761    fn test_parse_impl() {
762        let source = r#"
763            struct User {
764                name: String,
765            }
766
767            impl User {
768                fn new(name: String) -> Self {
769                    User { name }
770                }
771
772                fn get_name(&self) -> &str {
773                    &self.name
774                }
775            }
776        "#;
777
778        let symbols = parse("test.rs", source).unwrap();
779
780        // Should find: struct User, method new, method get_name
781        assert!(symbols.len() >= 3);
782
783        let method_symbols: Vec<_> = symbols.iter()
784            .filter(|s| matches!(s.kind, SymbolKind::Method))
785            .collect();
786
787        assert_eq!(method_symbols.len(), 2);
788        assert!(method_symbols.iter().any(|s| s.symbol.as_deref() == Some("new")));
789        assert!(method_symbols.iter().any(|s| s.symbol.as_deref() == Some("get_name")));
790
791        // Note: scope field was removed from SearchResult for token optimization
792        // Methods are identified by SymbolKind::Method
793    }
794
795    #[test]
796    fn test_parse_enum() {
797        let source = r#"
798            enum Status {
799                Active,
800                Inactive,
801            }
802        "#;
803
804        let symbols = parse("test.rs", source).unwrap();
805        assert_eq!(symbols.len(), 1);
806        assert_eq!(symbols[0].symbol.as_deref(), Some("Status"));
807        assert!(matches!(symbols[0].kind, SymbolKind::Enum));
808    }
809
810    #[test]
811    fn test_parse_trait() {
812        let source = r#"
813            trait Drawable {
814                fn draw(&self);
815            }
816        "#;
817
818        let symbols = parse("test.rs", source).unwrap();
819        assert_eq!(symbols.len(), 1);
820        assert_eq!(symbols[0].symbol.as_deref(), Some("Drawable"));
821        assert!(matches!(symbols[0].kind, SymbolKind::Trait));
822    }
823
824    #[test]
825    fn test_parse_multiple_symbols() {
826        let source = r#"
827            const MAX_SIZE: usize = 100;
828
829            struct Config {
830                size: usize,
831            }
832
833            fn create_config() -> Config {
834                Config { size: MAX_SIZE }
835            }
836        "#;
837
838        let symbols = parse("test.rs", source).unwrap();
839
840        // Should find: const, struct, function
841        assert_eq!(symbols.len(), 3);
842
843        let kinds: Vec<&SymbolKind> = symbols.iter().map(|s| &s.kind).collect();
844        assert!(kinds.contains(&&SymbolKind::Constant));
845        assert!(kinds.contains(&&SymbolKind::Struct));
846        assert!(kinds.contains(&&SymbolKind::Function));
847    }
848
849    #[test]
850    fn test_local_variables_included() {
851        let source = r#"
852            fn calculate(input: i32) -> i32 {
853                let local_var = input * 2;
854                let result = local_var + 10;
855                result
856            }
857
858            struct Calculator;
859
860            impl Calculator {
861                fn compute(&self, value: i32) -> i32 {
862                    let temp = value * 3;
863                    let mut final_value = temp + 5;
864                    final_value += 1;
865                    final_value
866                }
867            }
868        "#;
869
870        let symbols = parse("test.rs", source).unwrap();
871
872        // Filter to just variables
873        let variables: Vec<_> = symbols.iter()
874            .filter(|s| matches!(s.kind, SymbolKind::Variable))
875            .collect();
876
877        // Check that local variables are captured
878        assert!(variables.iter().any(|v| v.symbol.as_deref() == Some("local_var")));
879        assert!(variables.iter().any(|v| v.symbol.as_deref() == Some("result")));
880        assert!(variables.iter().any(|v| v.symbol.as_deref() == Some("temp")));
881        assert!(variables.iter().any(|v| v.symbol.as_deref() == Some("final_value")));
882
883        // Note: scope field was removed from SearchResult for token optimization
884    }
885
886    #[test]
887    fn test_static_variables() {
888        let source = r#"
889            static GLOBAL_COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
890            static mut MUTABLE_GLOBAL: i32 = 0;
891
892            const MAX_SIZE: usize = 100;
893
894            fn increment() {
895                GLOBAL_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
896            }
897        "#;
898
899        let symbols = parse("test.rs", source).unwrap();
900
901        // Filter to statics and constants
902        let statics: Vec<_> = symbols.iter()
903            .filter(|s| matches!(s.kind, SymbolKind::Variable))
904            .collect();
905
906        let constants: Vec<_> = symbols.iter()
907            .filter(|s| matches!(s.kind, SymbolKind::Constant))
908            .collect();
909
910        // Check that static variables are captured
911        assert!(statics.iter().any(|v| v.symbol.as_deref() == Some("GLOBAL_COUNTER")));
912        assert!(statics.iter().any(|v| v.symbol.as_deref() == Some("MUTABLE_GLOBAL")));
913
914        // Check that constants are still separate
915        assert!(constants.iter().any(|c| c.symbol.as_deref() == Some("MAX_SIZE")));
916    }
917
918    #[test]
919    fn test_macros() {
920        let source = r#"
921            macro_rules! say_hello {
922                () => {
923                    println!("Hello!");
924                };
925            }
926
927            macro_rules! vec_of_strings {
928                ($($x:expr),*) => {
929                    vec![$($x.to_string()),*]
930                };
931            }
932
933            fn main() {
934                say_hello!();
935            }
936        "#;
937
938        let symbols = parse("test.rs", source).unwrap();
939
940        // Filter to macros
941        let macros: Vec<_> = symbols.iter()
942            .filter(|s| matches!(s.kind, SymbolKind::Macro))
943            .collect();
944
945        // Check that macros are captured
946        assert!(macros.iter().any(|m| m.symbol.as_deref() == Some("say_hello")));
947        assert!(macros.iter().any(|m| m.symbol.as_deref() == Some("vec_of_strings")));
948        assert_eq!(macros.len(), 2);
949    }
950
951    #[test]
952    fn test_attribute_proc_macros() {
953        let source = r#"
954            use proc_macro::TokenStream;
955
956            #[proc_macro_attribute]
957            pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
958                item
959            }
960
961            #[proc_macro_attribute]
962            pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
963                item
964            }
965
966            // Regular function - should NOT be captured
967            pub fn helper() {}
968        "#;
969
970        let symbols = parse("test.rs", source).unwrap();
971
972        // Filter to attributes
973        let attributes: Vec<_> = symbols.iter()
974            .filter(|s| matches!(s.kind, SymbolKind::Attribute))
975            .collect();
976
977        // Check that attribute proc macro DEFINITIONS are captured
978        assert!(attributes.iter().any(|a| a.symbol.as_deref() == Some("test")));
979        assert!(attributes.iter().any(|a| a.symbol.as_deref() == Some("route")));
980
981        // Verify helper function is NOT captured as attribute
982        assert!(!attributes.iter().any(|a| a.symbol.as_deref() == Some("helper")));
983
984        // Should find 2 proc macro definitions + 2 attribute uses (#[proc_macro_attribute])
985        assert_eq!(attributes.len(), 4);
986    }
987
988    #[test]
989    fn test_attribute_uses() {
990        let source = r#"
991            #[test]
992            fn test_something() {
993                assert_eq!(1, 1);
994            }
995
996            #[test]
997            #[should_panic]
998            fn test_panic() {
999                panic!("expected");
1000            }
1001
1002            #[derive(Debug, Clone)]
1003            struct MyStruct {
1004                field: i32
1005            }
1006
1007            #[cfg(test)]
1008            mod tests {
1009                #[test]
1010                fn nested_test() {}
1011            }
1012        "#;
1013
1014        let symbols = parse("test.rs", source).unwrap();
1015
1016        // Filter to attributes
1017        let attributes: Vec<_> = symbols.iter()
1018            .filter(|s| matches!(s.kind, SymbolKind::Attribute))
1019            .collect();
1020
1021        // Check that attribute USES are captured
1022        assert!(attributes.iter().any(|a| a.symbol.as_deref() == Some("test")));
1023        assert!(attributes.iter().any(|a| a.symbol.as_deref() == Some("should_panic")));
1024        assert!(attributes.iter().any(|a| a.symbol.as_deref() == Some("derive")));
1025        assert!(attributes.iter().any(|a| a.symbol.as_deref() == Some("cfg")));
1026
1027        // Should find: test (3x), should_panic (1x), derive (1x), cfg (1x) = 6 total
1028        assert_eq!(attributes.len(), 6);
1029    }
1030
1031    #[test]
1032    fn test_extract_dependencies_use_declarations() {
1033        let source = r#"
1034            use std::collections::HashMap;
1035            use crate::models::{Language, SearchResult};
1036            use super::utils;
1037            use anyhow::Result;
1038        "#;
1039
1040        let deps = RustDependencyExtractor::extract_dependencies(source).unwrap();
1041
1042        // Should find 4 imports
1043        assert_eq!(deps.len(), 4);
1044
1045        // Check std import
1046        let std_import = deps.iter().find(|d| d.imported_path == "std::collections::HashMap").unwrap();
1047        assert!(matches!(std_import.import_type, crate::models::ImportType::Stdlib));
1048
1049        // Check crate import with symbols
1050        let crate_import = deps.iter().find(|d| d.imported_path == "crate::models").unwrap();
1051        assert!(matches!(crate_import.import_type, crate::models::ImportType::Internal));
1052        assert!(crate_import.imported_symbols.is_some());
1053        let symbols = crate_import.imported_symbols.as_ref().unwrap();
1054        assert_eq!(symbols.len(), 2);
1055        assert!(symbols.contains(&"Language".to_string()));
1056        assert!(symbols.contains(&"SearchResult".to_string()));
1057
1058        // Check super import
1059        let super_import = deps.iter().find(|d| d.imported_path == "super::utils").unwrap();
1060        assert!(matches!(super_import.import_type, crate::models::ImportType::Internal));
1061
1062        // Check external import
1063        let external_import = deps.iter().find(|d| d.imported_path == "anyhow::Result").unwrap();
1064        assert!(matches!(external_import.import_type, crate::models::ImportType::External));
1065    }
1066
1067    #[test]
1068    fn test_extract_dependencies_mod_declarations() {
1069        let source = r#"
1070            mod parser;
1071            mod utils;
1072
1073            mod inline {
1074                fn test() {}
1075            }
1076        "#;
1077
1078        let deps = RustDependencyExtractor::extract_dependencies(source).unwrap();
1079
1080        // Should find 2 external mod declarations (not the inline one)
1081        assert_eq!(deps.len(), 2);
1082        assert!(deps.iter().any(|d| d.imported_path == "parser"));
1083        assert!(deps.iter().any(|d| d.imported_path == "utils"));
1084        assert!(deps.iter().all(|d| matches!(d.import_type, crate::models::ImportType::Internal)));
1085    }
1086
1087    #[test]
1088    fn test_extract_dependencies_extern_crate() {
1089        let source = r#"
1090            extern crate serde;
1091            extern crate serde_json;
1092        "#;
1093
1094        let deps = RustDependencyExtractor::extract_dependencies(source).unwrap();
1095
1096        // Should find 2 extern crate declarations
1097        assert_eq!(deps.len(), 2);
1098        assert!(deps.iter().any(|d| d.imported_path == "serde"));
1099        assert!(deps.iter().any(|d| d.imported_path == "serde_json"));
1100        assert!(deps.iter().all(|d| matches!(d.import_type, crate::models::ImportType::External)));
1101    }
1102
1103    #[test]
1104    fn test_parse_use_with_aliases() {
1105        let source = r#"
1106            use std::io::Result as IoResult;
1107            use std::collections::{HashMap as Map, HashSet};
1108        "#;
1109
1110        let deps = RustDependencyExtractor::extract_dependencies(source).unwrap();
1111
1112        // Check alias handling - should extract the original name
1113        let io_import = deps.iter().find(|d| d.imported_path == "std::io::Result").unwrap();
1114        assert!(matches!(io_import.import_type, crate::models::ImportType::Stdlib));
1115
1116        let collections_import = deps.iter().find(|d| d.imported_path == "std::collections").unwrap();
1117        let symbols = collections_import.imported_symbols.as_ref().unwrap();
1118        assert_eq!(symbols.len(), 2);
1119        assert!(symbols.contains(&"HashMap".to_string()));
1120        assert!(symbols.contains(&"HashSet".to_string()));
1121    }
1122
1123    #[test]
1124    fn test_classify_rust_imports() {
1125        use crate::models::ImportType;
1126
1127        // Stdlib
1128        assert!(matches!(classify_rust_import("std::collections::HashMap"), ImportType::Stdlib));
1129        assert!(matches!(classify_rust_import("core::ptr"), ImportType::Stdlib));
1130        assert!(matches!(classify_rust_import("alloc::vec::Vec"), ImportType::Stdlib));
1131
1132        // Internal
1133        assert!(matches!(classify_rust_import("crate::models::Language"), ImportType::Internal));
1134        assert!(matches!(classify_rust_import("super::utils"), ImportType::Internal));
1135        assert!(matches!(classify_rust_import("self::helper"), ImportType::Internal));
1136
1137        // External
1138        assert!(matches!(classify_rust_import("serde::Serialize"), ImportType::External));
1139        assert!(matches!(classify_rust_import("anyhow::Result"), ImportType::External));
1140        assert!(matches!(classify_rust_import("tokio::runtime"), ImportType::External));
1141    }
1142}
1143
1144// ============================================================================
1145// Path Resolution
1146// ============================================================================
1147
1148/// Find the crate root (directory containing Cargo.toml) by walking up from a given path
1149fn find_crate_root(start_path: &str) -> Option<String> {
1150    let path = std::path::Path::new(start_path);
1151    let mut current = path.parent()?;
1152
1153    // Walk up until we find Cargo.toml
1154    loop {
1155        let cargo_toml = current.join("Cargo.toml");
1156        if cargo_toml.exists() {
1157            return Some(current.to_string_lossy().to_string());
1158        }
1159
1160        // For test paths that don't exist, assume standard Rust structure:
1161        // If we find "/src" in the path, the parent of "src" is likely the crate root
1162        if current.ends_with("src") {
1163            if let Some(parent) = current.parent() {
1164                return Some(parent.to_string_lossy().to_string());
1165            }
1166        }
1167
1168        // Move up to parent directory
1169        current = match current.parent() {
1170            Some(p) if p.as_os_str().is_empty() => return None,
1171            Some(p) => p,
1172            None => return None,
1173        };
1174    }
1175}
1176
1177/// Resolve a Rust use statement to a file path
1178///
1179/// Handles:
1180/// - `crate::` imports: `crate::models::Language` → `src/models.rs` or `src/models/mod.rs`
1181/// - `super::` imports: relative to parent module
1182/// - `self::` imports: relative to current module
1183/// - `mod parser;`: look for `parser.rs` or `parser/mod.rs`
1184///
1185/// Does NOT handle:
1186/// - External crate imports (would require parsing Cargo.toml dependencies)
1187/// - Stdlib imports (std::, core::, alloc::)
1188pub fn resolve_rust_use_to_path(
1189    import_path: &str,
1190    current_file_path: Option<&str>,
1191    _project_root: Option<&str>,
1192) -> Option<String> {
1193    // Only handle internal imports (crate::, super::, self::, or bare module names)
1194    if !import_path.starts_with("crate::")
1195        && !import_path.starts_with("super::")
1196        && !import_path.starts_with("self::") {
1197        // Check if it's a simple module name (no :: separator at all)
1198        if import_path.contains("::") {
1199            return None; // External or stdlib import
1200        }
1201        // Fall through for simple module names like "parser"
1202    }
1203
1204    let current_file = current_file_path?;
1205    let current_path = std::path::Path::new(current_file);
1206
1207    // Find the crate root
1208    let crate_root = find_crate_root(current_file)?;
1209    let crate_root_path = std::path::Path::new(&crate_root);
1210
1211    if import_path.starts_with("crate::") {
1212        // Resolve from crate root (typically src/)
1213        let module_path = import_path.strip_prefix("crate::").unwrap();
1214        let parts: Vec<&str> = module_path.split("::").collect();
1215
1216        // Try src/ first (standard Rust project structure)
1217        let src_root = crate_root_path.join("src");
1218        resolve_rust_module_path(&src_root, &parts)
1219    } else if import_path.starts_with("super::") {
1220        // Resolve relative to parent module
1221        let module_path = import_path.strip_prefix("super::").unwrap();
1222        let parts: Vec<&str> = module_path.split("::").collect();
1223
1224        // Get parent directory (go up one level)
1225        let current_dir = if current_path.file_name().unwrap() == "mod.rs" {
1226            // If current file is mod.rs, go up two levels
1227            current_path.parent()?.parent()?
1228        } else {
1229            // Otherwise, go up one level
1230            current_path.parent()?
1231        };
1232
1233        resolve_rust_module_path(current_dir, &parts)
1234    } else if import_path.starts_with("self::") {
1235        // Resolve relative to current module
1236        let module_path = import_path.strip_prefix("self::").unwrap();
1237        let parts: Vec<&str> = module_path.split("::").collect();
1238
1239        // Get current module directory
1240        let current_dir = if current_path.file_name().unwrap() == "mod.rs" {
1241            // If current file is mod.rs, use parent directory
1242            current_path.parent()?
1243        } else {
1244            // Otherwise, use current directory
1245            current_path.parent()?
1246        };
1247
1248        resolve_rust_module_path(current_dir, &parts)
1249    } else {
1250        // Simple module name (e.g., "parser" in "mod parser;")
1251        // Look for parser.rs or parser/mod.rs in the current directory
1252        let current_dir = current_path.parent()?;
1253        let module_file = current_dir.join(format!("{}.rs", import_path));
1254        let module_dir = current_dir.join(import_path).join("mod.rs");
1255
1256        if module_file.exists() {
1257            Some(module_file.to_string_lossy().to_string())
1258        } else if module_dir.exists() {
1259            Some(module_dir.to_string_lossy().to_string())
1260        } else {
1261            // Return the most likely candidate even if it doesn't exist
1262            // The indexer will check if the file is actually in the index
1263            Some(module_file.to_string_lossy().to_string())
1264        }
1265    }
1266}
1267
1268/// Resolve a Rust module path (list of components) to a file path
1269///
1270/// Examples:
1271/// - `["models"]` → `models.rs` or `models/mod.rs`
1272/// - `["models", "language"]` → `models/language.rs` or `models/language/mod.rs`
1273fn resolve_rust_module_path(base_dir: &std::path::Path, parts: &[&str]) -> Option<String> {
1274    if parts.is_empty() {
1275        return None;
1276    }
1277
1278    // Build the path incrementally
1279    let mut current_path = base_dir.to_path_buf();
1280
1281    for (i, part) in parts.iter().enumerate() {
1282        if i == parts.len() - 1 {
1283            // Last component - try both .rs file and mod.rs
1284            let file_path = current_path.join(format!("{}.rs", part));
1285            let mod_path = current_path.join(part).join("mod.rs");
1286
1287            log::trace!("Checking Rust module path: {}", file_path.display());
1288            log::trace!("Checking Rust module path: {}", mod_path.display());
1289
1290            // Return the first candidate (indexer will validate it exists)
1291            if file_path.exists() {
1292                return Some(file_path.to_string_lossy().to_string());
1293            } else if mod_path.exists() {
1294                return Some(mod_path.to_string_lossy().to_string());
1295            } else {
1296                // Return most likely candidate even if it doesn't exist
1297                return Some(file_path.to_string_lossy().to_string());
1298            }
1299        } else {
1300            // Intermediate component - must be a directory
1301            current_path = current_path.join(part);
1302        }
1303    }
1304
1305    None
1306}
1307
1308#[cfg(test)]
1309mod path_resolution_tests {
1310    use super::*;
1311
1312    #[test]
1313    fn test_resolve_crate_import() {
1314        // crate::models::Language
1315        let result = resolve_rust_use_to_path(
1316            "crate::models",
1317            Some("/home/user/project/src/main.rs"),
1318            Some("/home/user/project"),
1319        );
1320
1321        assert!(result.is_some());
1322        let path = result.unwrap();
1323        // Should resolve to src/models.rs or src/models/mod.rs
1324        assert!(path.contains("models.rs") || path.contains("models/mod.rs"));
1325    }
1326
1327    #[test]
1328    fn test_resolve_super_import() {
1329        // super::utils from src/commands/index.rs
1330        let result = resolve_rust_use_to_path(
1331            "super::utils",
1332            Some("/home/user/project/src/commands/index.rs"),
1333            Some("/home/user/project"),
1334        );
1335
1336        assert!(result.is_some());
1337        let path = result.unwrap();
1338        // Should resolve to src/utils.rs
1339        assert!(path.contains("src") && path.contains("utils.rs"));
1340    }
1341
1342    #[test]
1343    fn test_resolve_self_import() {
1344        // self::helper from src/models/mod.rs
1345        let result = resolve_rust_use_to_path(
1346            "self::helper",
1347            Some("/home/user/project/src/models/mod.rs"),
1348            Some("/home/user/project"),
1349        );
1350
1351        assert!(result.is_some());
1352        let path = result.unwrap();
1353        // Should resolve to src/models/helper.rs
1354        assert!(path.contains("models") && path.contains("helper.rs"));
1355    }
1356
1357    #[test]
1358    fn test_resolve_mod_declaration() {
1359        // mod parser; from src/main.rs
1360        let result = resolve_rust_use_to_path(
1361            "parser",
1362            Some("/home/user/project/src/main.rs"),
1363            Some("/home/user/project"),
1364        );
1365
1366        assert!(result.is_some());
1367        let path = result.unwrap();
1368        // Should resolve to src/parser.rs
1369        assert!(path.contains("parser.rs"));
1370    }
1371
1372    #[test]
1373    fn test_resolve_nested_crate_import() {
1374        // crate::models::language::Language
1375        let result = resolve_rust_use_to_path(
1376            "crate::models::language",
1377            Some("/home/user/project/src/main.rs"),
1378            Some("/home/user/project"),
1379        );
1380
1381        assert!(result.is_some());
1382        let path = result.unwrap();
1383        // Should resolve to src/models/language.rs or src/models/language/mod.rs
1384        assert!(path.contains("models") && (path.contains("language.rs") || path.contains("language/mod.rs")));
1385    }
1386
1387    #[test]
1388    fn test_external_import_not_supported() {
1389        // anyhow::Result (external crate)
1390        let result = resolve_rust_use_to_path(
1391            "anyhow::Result",
1392            Some("/home/user/project/src/main.rs"),
1393            Some("/home/user/project"),
1394        );
1395
1396        // Should return None for external imports
1397        assert!(result.is_none());
1398    }
1399
1400    #[test]
1401    fn test_stdlib_import_not_supported() {
1402        // std::collections::HashMap (stdlib)
1403        let result = resolve_rust_use_to_path(
1404            "std::collections::HashMap",
1405            Some("/home/user/project/src/main.rs"),
1406            Some("/home/user/project"),
1407        );
1408
1409        // Should return None for stdlib imports
1410        assert!(result.is_none());
1411    }
1412
1413    #[test]
1414    fn test_resolve_without_current_file() {
1415        let result = resolve_rust_use_to_path(
1416            "crate::models",
1417            None,
1418            Some("/home/user/project"),
1419        );
1420
1421        // Should return None if no current file provided
1422        assert!(result.is_none());
1423    }
1424}