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 crate::models::{Language, SearchResult, Span, SymbolKind};
17use crate::parsers::{DependencyExtractor, ImportInfo};
18use anyhow::{Context, Result};
19use streaming_iterator::StreamingIterator;
20use tree_sitter::{Parser, Query, QueryCursor};
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    // Add file path to all symbols
54    for symbol in &mut symbols {
55        symbol.path = path.to_string();
56        symbol.lang = Language::Rust;
57    }
58
59    Ok(symbols)
60}
61
62/// Extract function definitions
63fn extract_functions(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
64    let language = tree_sitter_rust::LANGUAGE;
65    let query_str = r#"
66        (function_item
67            name: (identifier) @name) @function
68    "#;
69
70    let query =
71        Query::new(&language.into(), query_str).context("Failed to create function query")?;
72
73    extract_symbols(source, root, &query, SymbolKind::Function, None)
74}
75
76/// Extract struct definitions
77fn extract_structs(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
78    let language = tree_sitter_rust::LANGUAGE;
79    let query_str = r#"
80        (struct_item
81            name: (type_identifier) @name) @struct
82    "#;
83
84    let query = Query::new(&language.into(), query_str).context("Failed to create struct query")?;
85
86    extract_symbols(source, root, &query, SymbolKind::Struct, None)
87}
88
89/// Extract enum definitions
90fn extract_enums(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
91    let language = tree_sitter_rust::LANGUAGE;
92    let query_str = r#"
93        (enum_item
94            name: (type_identifier) @name) @enum
95    "#;
96
97    let query = Query::new(&language.into(), query_str).context("Failed to create enum query")?;
98
99    extract_symbols(source, root, &query, SymbolKind::Enum, None)
100}
101
102/// Extract trait definitions
103fn extract_traits(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
104    let language = tree_sitter_rust::LANGUAGE;
105    let query_str = r#"
106        (trait_item
107            name: (type_identifier) @name) @trait
108    "#;
109
110    let query = Query::new(&language.into(), query_str).context("Failed to create trait query")?;
111
112    extract_symbols(source, root, &query, SymbolKind::Trait, None)
113}
114
115/// Extract impl blocks
116fn extract_impls(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
117    let language = tree_sitter_rust::LANGUAGE;
118
119    // Extract methods from impl blocks
120    let query_str = r#"
121        (impl_item
122            type: (type_identifier) @impl_name
123            body: (declaration_list
124                (function_item
125                    name: (identifier) @method_name))) @impl
126    "#;
127
128    let query = Query::new(&language.into(), query_str).context("Failed to create impl query")?;
129
130    let mut cursor = QueryCursor::new();
131    let mut matches = cursor.matches(&query, *root, source.as_bytes());
132
133    let mut symbols = Vec::new();
134
135    while let Some(match_) = matches.next() {
136        let mut impl_name = None;
137        let mut method_name = None;
138        let mut method_node = None;
139
140        for capture in match_.captures {
141            let capture_name: &str = query.capture_names()[capture.index as usize];
142            match capture_name {
143                "impl_name" => {
144                    impl_name = Some(
145                        capture
146                            .node
147                            .utf8_text(source.as_bytes())
148                            .unwrap_or("")
149                            .to_string(),
150                    );
151                }
152                "method_name" => {
153                    method_name = Some(
154                        capture
155                            .node
156                            .utf8_text(source.as_bytes())
157                            .unwrap_or("")
158                            .to_string(),
159                    );
160                    // Find the parent function_item node
161                    let mut current = capture.node;
162                    while let Some(parent) = current.parent() {
163                        if parent.kind() == "function_item" {
164                            method_node = Some(parent);
165                            break;
166                        }
167                        current = parent;
168                    }
169                }
170                _ => {}
171            }
172        }
173
174        if let (Some(impl_name), Some(method_name), Some(node)) =
175            (impl_name, method_name, method_node)
176        {
177            let scope = format!("impl {}", impl_name);
178            let span = node_to_span(&node);
179            let preview = extract_preview(source, &span);
180
181            symbols.push(SearchResult::new(
182                String::new(), // Path will be filled in later
183                Language::Rust,
184                SymbolKind::Method,
185                Some(method_name),
186                span,
187                Some(scope),
188                preview,
189            ));
190        }
191    }
192
193    Ok(symbols)
194}
195
196/// Extract constants
197fn extract_constants(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
198    let language = tree_sitter_rust::LANGUAGE;
199    let query_str = r#"
200        (const_item
201            name: (identifier) @name) @const
202    "#;
203
204    let query = Query::new(&language.into(), query_str).context("Failed to create const query")?;
205
206    extract_symbols(source, root, &query, SymbolKind::Constant, None)
207}
208
209/// Extract static variables
210fn extract_statics(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
211    let language = tree_sitter_rust::LANGUAGE;
212    let query_str = r#"
213        (static_item
214            name: (identifier) @name) @static
215    "#;
216
217    let query = Query::new(&language.into(), query_str).context("Failed to create static query")?;
218
219    extract_symbols(source, root, &query, SymbolKind::Variable, None)
220}
221
222/// Extract local variable bindings (let statements)
223fn extract_local_variables(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
224    let language = tree_sitter_rust::LANGUAGE;
225    let query_str = r#"
226        (let_declaration
227            pattern: (identifier) @name) @let
228    "#;
229
230    let query = Query::new(&language.into(), query_str)
231        .context("Failed to create let declaration query")?;
232
233    extract_symbols(source, root, &query, SymbolKind::Variable, None)
234}
235
236/// Extract module declarations
237fn extract_modules(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
238    let language = tree_sitter_rust::LANGUAGE;
239    let query_str = r#"
240        (mod_item
241            name: (identifier) @name) @module
242    "#;
243
244    let query = Query::new(&language.into(), query_str).context("Failed to create module query")?;
245
246    extract_symbols(source, root, &query, SymbolKind::Module, None)
247}
248
249/// Extract type aliases
250fn extract_type_aliases(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
251    let language = tree_sitter_rust::LANGUAGE;
252    let query_str = r#"
253        (type_item
254            name: (type_identifier) @name) @type
255    "#;
256
257    let query = Query::new(&language.into(), query_str).context("Failed to create type query")?;
258
259    extract_symbols(source, root, &query, SymbolKind::Type, None)
260}
261
262/// Extract macro definitions (macro_rules!)
263fn extract_macros(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
264    let language = tree_sitter_rust::LANGUAGE;
265    let query_str = r#"
266        (macro_definition
267            name: (identifier) @name) @macro
268    "#;
269
270    let query = Query::new(&language.into(), query_str).context("Failed to create macro query")?;
271
272    extract_symbols(source, root, &query, SymbolKind::Macro, None)
273}
274
275/// Extract attributes: BOTH definitions and uses
276/// Definitions: #[proc_macro_attribute] pub fn route(...)
277/// Uses: #[test] fn my_test(), #[derive(Debug)] struct Foo
278fn extract_attributes(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
279    let language = tree_sitter_rust::LANGUAGE;
280    let mut symbols = Vec::new();
281
282    // Part 1: Extract attribute DEFINITIONS (proc macro attributes)
283    let func_query_str = r#"
284        (function_item
285            name: (identifier) @name) @function
286    "#;
287
288    let func_query =
289        Query::new(&language.into(), func_query_str).context("Failed to create function query")?;
290
291    let mut cursor = QueryCursor::new();
292    let mut matches = cursor.matches(&func_query, *root, source.as_bytes());
293
294    while let Some(match_) = matches.next() {
295        let mut name = None;
296        let mut func_node = None;
297
298        for capture in match_.captures {
299            let capture_name: &str = func_query.capture_names()[capture.index as usize];
300            match capture_name {
301                "name" => {
302                    name = Some(
303                        capture
304                            .node
305                            .utf8_text(source.as_bytes())
306                            .unwrap_or("")
307                            .to_string(),
308                    );
309                }
310                "function" => {
311                    func_node = Some(capture.node);
312                }
313                _ => {}
314            }
315        }
316
317        // Check if this function has #[proc_macro_attribute] attribute
318        if let (Some(name), Some(func_node)) = (name, func_node) {
319            let mut has_proc_macro_attr = false;
320
321            if let Some(parent) = func_node.parent() {
322                let mut func_index = None;
323                for i in 0..parent.child_count() {
324                    if let Some(child) = parent.child(i as u32)
325                        && child.id() == func_node.id()
326                    {
327                        func_index = Some(i);
328                        break;
329                    }
330                }
331
332                if let Some(func_idx) = func_index {
333                    for i in (0..func_idx).rev() {
334                        if let Some(child) = parent.child(i as u32) {
335                            if child.kind() == "attribute_item" {
336                                let attr_text = child.utf8_text(source.as_bytes()).unwrap_or("");
337                                if attr_text.contains("proc_macro_attribute") {
338                                    has_proc_macro_attr = true;
339                                }
340                            } else if !child.kind().contains("comment")
341                                && child.kind() != "line_comment"
342                            {
343                                break;
344                            }
345                        }
346                    }
347                }
348            }
349
350            if has_proc_macro_attr {
351                let span = node_to_span(&func_node);
352                let preview = extract_preview(source, &span);
353
354                symbols.push(SearchResult::new(
355                    String::new(),
356                    Language::Rust,
357                    SymbolKind::Attribute,
358                    Some(name),
359                    span,
360                    None,
361                    preview,
362                ));
363            }
364        }
365    }
366
367    // Part 2: Extract attribute USES (#[test], #[derive(...)], etc.)
368    let attr_query_str = r#"
369        (attribute_item
370            (attribute
371                (identifier) @attr_name)) @attr
372    "#;
373
374    let attr_query = Query::new(&language.into(), attr_query_str)
375        .context("Failed to create attribute use query")?;
376
377    let mut cursor = QueryCursor::new();
378    let mut matches = cursor.matches(&attr_query, *root, source.as_bytes());
379
380    while let Some(match_) = matches.next() {
381        let mut attr_name = None;
382        let mut attr_node = None;
383
384        for capture in match_.captures {
385            let capture_name: &str = attr_query.capture_names()[capture.index as usize];
386            match capture_name {
387                "attr_name" => {
388                    attr_name = Some(
389                        capture
390                            .node
391                            .utf8_text(source.as_bytes())
392                            .unwrap_or("")
393                            .to_string(),
394                    );
395                }
396                "attr" => {
397                    attr_node = Some(capture.node);
398                }
399                _ => {}
400            }
401        }
402
403        if let (Some(name), Some(node)) = (attr_name, attr_node) {
404            let span = node_to_span(&node);
405            let preview = extract_preview(source, &span);
406
407            symbols.push(SearchResult::new(
408                String::new(),
409                Language::Rust,
410                SymbolKind::Attribute,
411                Some(name),
412                span,
413                None,
414                preview,
415            ));
416        }
417    }
418
419    Ok(symbols)
420}
421
422/// Generic symbol extraction helper
423fn extract_symbols(
424    source: &str,
425    root: &tree_sitter::Node,
426    query: &Query,
427    kind: SymbolKind,
428    scope: Option<String>,
429) -> Result<Vec<SearchResult>> {
430    let mut cursor = QueryCursor::new();
431    let mut matches = cursor.matches(query, *root, source.as_bytes());
432
433    let mut symbols = Vec::new();
434
435    while let Some(match_) = matches.next() {
436        // Find the name capture and the full node
437        let mut name = None;
438        let mut full_node = None;
439
440        for capture in match_.captures {
441            let capture_name: &str = query.capture_names()[capture.index as usize];
442            if capture_name == "name" {
443                name = Some(
444                    capture
445                        .node
446                        .utf8_text(source.as_bytes())
447                        .unwrap_or("")
448                        .to_string(),
449                );
450            } else {
451                // Assume any other capture is the full node
452                full_node = Some(capture.node);
453            }
454        }
455
456        if let (Some(name), Some(node)) = (name, full_node) {
457            let span = node_to_span(&node);
458            let preview = extract_preview(source, &span);
459
460            symbols.push(SearchResult::new(
461                String::new(), // Path will be filled in later
462                Language::Rust,
463                kind.clone(),
464                Some(name),
465                span,
466                scope.clone(),
467                preview,
468            ));
469        }
470    }
471
472    Ok(symbols)
473}
474
475/// Convert a Tree-sitter node to a Span
476fn node_to_span(node: &tree_sitter::Node) -> Span {
477    let start = node.start_position();
478    let end = node.end_position();
479
480    Span::new(
481        start.row + 1, // Convert 0-indexed to 1-indexed
482        start.column,
483        end.row + 1,
484        end.column,
485    )
486}
487
488/// Extract a preview (5-7 lines) around the symbol
489fn extract_preview(source: &str, span: &Span) -> String {
490    let lines: Vec<&str> = source.lines().collect();
491
492    // Extract 7 lines: the start line and 6 following lines
493    // This provides enough context for AI agents to understand the code
494    let start_idx = span.start_line - 1; // Convert back to 0-indexed
495    let end_idx = (start_idx + 7).min(lines.len());
496
497    lines[start_idx..end_idx].join("\n")
498}
499
500/// Rust dependency extractor implementation
501pub struct RustDependencyExtractor;
502
503impl DependencyExtractor for RustDependencyExtractor {
504    fn extract_dependencies(source: &str) -> Result<Vec<ImportInfo>> {
505        let mut parser = Parser::new();
506        let language = tree_sitter_rust::LANGUAGE;
507
508        parser
509            .set_language(&language.into())
510            .context("Failed to set Rust language")?;
511
512        let tree = parser
513            .parse(source, None)
514            .context("Failed to parse Rust source")?;
515
516        let root_node = tree.root_node();
517
518        let mut imports = Vec::new();
519
520        // Extract use declarations
521        imports.extend(extract_use_declarations(source, &root_node)?);
522
523        // Extract mod items (module declarations)
524        imports.extend(extract_mod_items(source, &root_node)?);
525
526        // Extract extern crate declarations
527        imports.extend(extract_extern_crates(source, &root_node)?);
528
529        Ok(imports)
530    }
531}
532
533/// Extract use declarations (use std::collections::HashMap)
534fn extract_use_declarations(source: &str, root: &tree_sitter::Node) -> Result<Vec<ImportInfo>> {
535    let language = tree_sitter_rust::LANGUAGE;
536    let query_str = r#"
537        (use_declaration) @use
538    "#;
539
540    let query = Query::new(&language.into(), query_str)
541        .context("Failed to create use declaration query")?;
542
543    let mut cursor = QueryCursor::new();
544    let mut matches = cursor.matches(&query, *root, source.as_bytes());
545
546    let mut imports = Vec::new();
547
548    while let Some(match_) = matches.next() {
549        for capture in match_.captures {
550            let node = capture.node;
551            let text = node.utf8_text(source.as_bytes()).unwrap_or("");
552            let line_number = node.start_position().row + 1;
553
554            // Parse the use declaration text
555            let path_info = parse_rust_use_declaration(text);
556
557            for (path, symbols) in path_info {
558                let import_type = classify_rust_import(&path);
559
560                imports.push(ImportInfo {
561                    imported_path: path,
562                    import_type,
563                    line_number,
564                    imported_symbols: symbols,
565                });
566            }
567        }
568    }
569
570    Ok(imports)
571}
572
573/// Extract mod items (mod parser;)
574fn extract_mod_items(source: &str, root: &tree_sitter::Node) -> Result<Vec<ImportInfo>> {
575    let language = tree_sitter_rust::LANGUAGE;
576    let query_str = r#"
577        (mod_item
578            name: (identifier) @name) @mod
579    "#;
580
581    let query =
582        Query::new(&language.into(), query_str).context("Failed to create mod item query")?;
583
584    let mut cursor = QueryCursor::new();
585    let mut matches = cursor.matches(&query, *root, source.as_bytes());
586
587    let mut imports = Vec::new();
588
589    while let Some(match_) = matches.next() {
590        let mut name = None;
591        let mut mod_node = None;
592
593        for capture in match_.captures {
594            let capture_name: &str = query.capture_names()[capture.index as usize];
595            match capture_name {
596                "name" => {
597                    name = Some(
598                        capture
599                            .node
600                            .utf8_text(source.as_bytes())
601                            .unwrap_or("")
602                            .to_string(),
603                    );
604                }
605                "mod" => {
606                    mod_node = Some(capture.node);
607                }
608                _ => {}
609            }
610        }
611
612        if let (Some(name), Some(node)) = (name, mod_node) {
613            // Check if this is an external module declaration (no body)
614            let has_body = node.child_by_field_name("body").is_some();
615
616            if !has_body {
617                // This is an external module reference (mod parser;)
618                let line_number = node.start_position().row + 1;
619
620                imports.push(ImportInfo {
621                    imported_path: name,
622                    // ModDecl marks parent→child ownership; excluded from cycle detection (REF-88)
623                    import_type: crate::models::ImportType::ModDecl,
624                    line_number,
625                    imported_symbols: None,
626                });
627            }
628        }
629    }
630
631    Ok(imports)
632}
633
634/// Extract extern crate declarations (extern crate serde;)
635fn extract_extern_crates(source: &str, root: &tree_sitter::Node) -> Result<Vec<ImportInfo>> {
636    let language = tree_sitter_rust::LANGUAGE;
637    let query_str = r#"
638        (extern_crate_declaration
639            name: (identifier) @name) @extern
640    "#;
641
642    let query =
643        Query::new(&language.into(), query_str).context("Failed to create extern crate query")?;
644
645    let mut cursor = QueryCursor::new();
646    let mut matches = cursor.matches(&query, *root, source.as_bytes());
647
648    let mut imports = Vec::new();
649
650    while let Some(match_) = matches.next() {
651        let mut name = None;
652        let mut extern_node = None;
653
654        for capture in match_.captures {
655            let capture_name: &str = query.capture_names()[capture.index as usize];
656            match capture_name {
657                "name" => {
658                    name = Some(
659                        capture
660                            .node
661                            .utf8_text(source.as_bytes())
662                            .unwrap_or("")
663                            .to_string(),
664                    );
665                }
666                "extern" => {
667                    extern_node = Some(capture.node);
668                }
669                _ => {}
670            }
671        }
672
673        if let (Some(name), Some(node)) = (name, extern_node) {
674            let line_number = node.start_position().row + 1;
675            let import_type = classify_rust_import(&name);
676
677            imports.push(ImportInfo {
678                imported_path: name,
679                import_type,
680                line_number,
681                imported_symbols: None,
682            });
683        }
684    }
685
686    Ok(imports)
687}
688
689/// Classify a Rust import path as Internal, External, or Stdlib
690fn classify_rust_import(path: &str) -> crate::models::ImportType {
691    use crate::models::ImportType;
692
693    if path.starts_with("std::") || path.starts_with("core::") || path.starts_with("alloc::") {
694        ImportType::Stdlib
695    } else if path.starts_with("crate::")
696        || path.starts_with("super::")
697        || path.starts_with("self::")
698    {
699        ImportType::Internal
700    } else {
701        // External crate
702        ImportType::External
703    }
704}
705
706/// Parse a Rust use declaration and extract path(s) and symbols
707///
708/// Handles:
709/// - Simple: use std::collections::HashMap;
710/// - With symbols: use std::collections::{HashMap, HashSet};
711/// - Nested: use std::{io, fs};
712/// - With aliases: use std::io::Result as IoResult;
713/// - Glob: use std::collections::*;
714fn parse_rust_use_declaration(text: &str) -> Vec<(String, Option<Vec<String>>)> {
715    // Remove visibility modifiers and keywords
716    let text = text
717        .trim()
718        .strip_prefix("pub(crate)")
719        .unwrap_or(text)
720        .trim()
721        .strip_prefix("pub(super)")
722        .unwrap_or(text)
723        .trim()
724        .strip_prefix("pub")
725        .unwrap_or(text)
726        .trim()
727        .strip_prefix("use")
728        .unwrap_or(text)
729        .trim()
730        .strip_suffix(";")
731        .unwrap_or(text)
732        .trim();
733
734    // Handle different patterns
735    if text.contains('{') {
736        // Has braces - extract base path and symbols
737        if let Some(idx) = text.find('{') {
738            let base_path = text[..idx].trim_end_matches("::").to_string();
739
740            if let Some(end) = text.find('}') {
741                let symbols_str = &text[idx + 1..end];
742                let symbols: Vec<String> = symbols_str
743                    .split(',')
744                    .map(|s| {
745                        // Handle aliases like "HashMap as Map" - extract the imported name
746                        let trimmed = s.trim();
747                        if let Some(as_idx) = trimmed.find(" as ") {
748                            trimmed[..as_idx].trim().to_string()
749                        } else {
750                            trimmed.to_string()
751                        }
752                    })
753                    .filter(|s| !s.is_empty() && s != "*")
754                    .collect();
755
756                if !symbols.is_empty() {
757                    return vec![(base_path, Some(symbols))];
758                }
759            }
760        }
761    }
762
763    // Simple path (possibly with alias)
764    let path = if let Some(as_idx) = text.find(" as ") {
765        text[..as_idx].trim().to_string()
766    } else {
767        text.to_string()
768    };
769
770    vec![(path, None)]
771}
772
773#[cfg(test)]
774mod tests {
775    use super::*;
776
777    #[test]
778    fn test_parse_function() {
779        let source = r#"
780            fn hello_world() {
781                println!("Hello, world!");
782            }
783        "#;
784
785        let symbols = parse("test.rs", source).unwrap();
786        assert_eq!(symbols.len(), 1);
787        assert_eq!(symbols[0].symbol.as_deref(), Some("hello_world"));
788        assert!(matches!(symbols[0].kind, SymbolKind::Function));
789    }
790
791    #[test]
792    fn test_parse_struct() {
793        let source = r#"
794            struct User {
795                name: String,
796                age: u32,
797            }
798        "#;
799
800        let symbols = parse("test.rs", source).unwrap();
801        assert_eq!(symbols.len(), 1);
802        assert_eq!(symbols[0].symbol.as_deref(), Some("User"));
803        assert!(matches!(symbols[0].kind, SymbolKind::Struct));
804    }
805
806    #[test]
807    fn test_parse_impl() {
808        let source = r#"
809            struct User {
810                name: String,
811            }
812
813            impl User {
814                fn new(name: String) -> Self {
815                    User { name }
816                }
817
818                fn get_name(&self) -> &str {
819                    &self.name
820                }
821            }
822        "#;
823
824        let symbols = parse("test.rs", source).unwrap();
825
826        // Should find: struct User, method new, method get_name
827        assert!(symbols.len() >= 3);
828
829        let method_symbols: Vec<_> = symbols
830            .iter()
831            .filter(|s| matches!(s.kind, SymbolKind::Method))
832            .collect();
833
834        assert_eq!(method_symbols.len(), 2);
835        assert!(
836            method_symbols
837                .iter()
838                .any(|s| s.symbol.as_deref() == Some("new"))
839        );
840        assert!(
841            method_symbols
842                .iter()
843                .any(|s| s.symbol.as_deref() == Some("get_name"))
844        );
845
846        // Note: scope field was removed from SearchResult for token optimization
847        // Methods are identified by SymbolKind::Method
848    }
849
850    #[test]
851    fn test_parse_enum() {
852        let source = r#"
853            enum Status {
854                Active,
855                Inactive,
856            }
857        "#;
858
859        let symbols = parse("test.rs", source).unwrap();
860        assert_eq!(symbols.len(), 1);
861        assert_eq!(symbols[0].symbol.as_deref(), Some("Status"));
862        assert!(matches!(symbols[0].kind, SymbolKind::Enum));
863    }
864
865    #[test]
866    fn test_parse_trait() {
867        let source = r#"
868            trait Drawable {
869                fn draw(&self);
870            }
871        "#;
872
873        let symbols = parse("test.rs", source).unwrap();
874        assert_eq!(symbols.len(), 1);
875        assert_eq!(symbols[0].symbol.as_deref(), Some("Drawable"));
876        assert!(matches!(symbols[0].kind, SymbolKind::Trait));
877    }
878
879    #[test]
880    fn test_parse_multiple_symbols() {
881        let source = r#"
882            const MAX_SIZE: usize = 100;
883
884            struct Config {
885                size: usize,
886            }
887
888            fn create_config() -> Config {
889                Config { size: MAX_SIZE }
890            }
891        "#;
892
893        let symbols = parse("test.rs", source).unwrap();
894
895        // Should find: const, struct, function
896        assert_eq!(symbols.len(), 3);
897
898        let kinds: Vec<&SymbolKind> = symbols.iter().map(|s| &s.kind).collect();
899        assert!(kinds.contains(&&SymbolKind::Constant));
900        assert!(kinds.contains(&&SymbolKind::Struct));
901        assert!(kinds.contains(&&SymbolKind::Function));
902    }
903
904    #[test]
905    fn test_local_variables_included() {
906        let source = r#"
907            fn calculate(input: i32) -> i32 {
908                let local_var = input * 2;
909                let result = local_var + 10;
910                result
911            }
912
913            struct Calculator;
914
915            impl Calculator {
916                fn compute(&self, value: i32) -> i32 {
917                    let temp = value * 3;
918                    let mut final_value = temp + 5;
919                    final_value += 1;
920                    final_value
921                }
922            }
923        "#;
924
925        let symbols = parse("test.rs", source).unwrap();
926
927        // Filter to just variables
928        let variables: Vec<_> = symbols
929            .iter()
930            .filter(|s| matches!(s.kind, SymbolKind::Variable))
931            .collect();
932
933        // Check that local variables are captured
934        assert!(
935            variables
936                .iter()
937                .any(|v| v.symbol.as_deref() == Some("local_var"))
938        );
939        assert!(
940            variables
941                .iter()
942                .any(|v| v.symbol.as_deref() == Some("result"))
943        );
944        assert!(
945            variables
946                .iter()
947                .any(|v| v.symbol.as_deref() == Some("temp"))
948        );
949        assert!(
950            variables
951                .iter()
952                .any(|v| v.symbol.as_deref() == Some("final_value"))
953        );
954
955        // Note: scope field was removed from SearchResult for token optimization
956    }
957
958    #[test]
959    fn test_static_variables() {
960        let source = r#"
961            static GLOBAL_COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
962            static mut MUTABLE_GLOBAL: i32 = 0;
963
964            const MAX_SIZE: usize = 100;
965
966            fn increment() {
967                GLOBAL_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
968            }
969        "#;
970
971        let symbols = parse("test.rs", source).unwrap();
972
973        // Filter to statics and constants
974        let statics: Vec<_> = symbols
975            .iter()
976            .filter(|s| matches!(s.kind, SymbolKind::Variable))
977            .collect();
978
979        let constants: Vec<_> = symbols
980            .iter()
981            .filter(|s| matches!(s.kind, SymbolKind::Constant))
982            .collect();
983
984        // Check that static variables are captured
985        assert!(
986            statics
987                .iter()
988                .any(|v| v.symbol.as_deref() == Some("GLOBAL_COUNTER"))
989        );
990        assert!(
991            statics
992                .iter()
993                .any(|v| v.symbol.as_deref() == Some("MUTABLE_GLOBAL"))
994        );
995
996        // Check that constants are still separate
997        assert!(
998            constants
999                .iter()
1000                .any(|c| c.symbol.as_deref() == Some("MAX_SIZE"))
1001        );
1002    }
1003
1004    #[test]
1005    fn test_macros() {
1006        let source = r#"
1007            macro_rules! say_hello {
1008                () => {
1009                    println!("Hello!");
1010                };
1011            }
1012
1013            macro_rules! vec_of_strings {
1014                ($($x:expr),*) => {
1015                    vec![$($x.to_string()),*]
1016                };
1017            }
1018
1019            fn main() {
1020                say_hello!();
1021            }
1022        "#;
1023
1024        let symbols = parse("test.rs", source).unwrap();
1025
1026        // Filter to macros
1027        let macros: Vec<_> = symbols
1028            .iter()
1029            .filter(|s| matches!(s.kind, SymbolKind::Macro))
1030            .collect();
1031
1032        // Check that macros are captured
1033        assert!(
1034            macros
1035                .iter()
1036                .any(|m| m.symbol.as_deref() == Some("say_hello"))
1037        );
1038        assert!(
1039            macros
1040                .iter()
1041                .any(|m| m.symbol.as_deref() == Some("vec_of_strings"))
1042        );
1043        assert_eq!(macros.len(), 2);
1044    }
1045
1046    #[test]
1047    fn test_attribute_proc_macros() {
1048        let source = r#"
1049            use proc_macro::TokenStream;
1050
1051            #[proc_macro_attribute]
1052            pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
1053                item
1054            }
1055
1056            #[proc_macro_attribute]
1057            pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
1058                item
1059            }
1060
1061            // Regular function - should NOT be captured
1062            pub fn helper() {}
1063        "#;
1064
1065        let symbols = parse("test.rs", source).unwrap();
1066
1067        // Filter to attributes
1068        let attributes: Vec<_> = symbols
1069            .iter()
1070            .filter(|s| matches!(s.kind, SymbolKind::Attribute))
1071            .collect();
1072
1073        // Check that attribute proc macro DEFINITIONS are captured
1074        assert!(
1075            attributes
1076                .iter()
1077                .any(|a| a.symbol.as_deref() == Some("test"))
1078        );
1079        assert!(
1080            attributes
1081                .iter()
1082                .any(|a| a.symbol.as_deref() == Some("route"))
1083        );
1084
1085        // Verify helper function is NOT captured as attribute
1086        assert!(
1087            !attributes
1088                .iter()
1089                .any(|a| a.symbol.as_deref() == Some("helper"))
1090        );
1091
1092        // Should find 2 proc macro definitions + 2 attribute uses (#[proc_macro_attribute])
1093        assert_eq!(attributes.len(), 4);
1094    }
1095
1096    #[test]
1097    fn test_attribute_uses() {
1098        let source = r#"
1099            #[test]
1100            fn test_something() {
1101                assert_eq!(1, 1);
1102            }
1103
1104            #[test]
1105            #[should_panic]
1106            fn test_panic() {
1107                panic!("expected");
1108            }
1109
1110            #[derive(Debug, Clone)]
1111            struct MyStruct {
1112                field: i32
1113            }
1114
1115            #[cfg(test)]
1116            mod tests {
1117                #[test]
1118                fn nested_test() {}
1119            }
1120        "#;
1121
1122        let symbols = parse("test.rs", source).unwrap();
1123
1124        // Filter to attributes
1125        let attributes: Vec<_> = symbols
1126            .iter()
1127            .filter(|s| matches!(s.kind, SymbolKind::Attribute))
1128            .collect();
1129
1130        // Check that attribute USES are captured
1131        assert!(
1132            attributes
1133                .iter()
1134                .any(|a| a.symbol.as_deref() == Some("test"))
1135        );
1136        assert!(
1137            attributes
1138                .iter()
1139                .any(|a| a.symbol.as_deref() == Some("should_panic"))
1140        );
1141        assert!(
1142            attributes
1143                .iter()
1144                .any(|a| a.symbol.as_deref() == Some("derive"))
1145        );
1146        assert!(
1147            attributes
1148                .iter()
1149                .any(|a| a.symbol.as_deref() == Some("cfg"))
1150        );
1151
1152        // Should find: test (3x), should_panic (1x), derive (1x), cfg (1x) = 6 total
1153        assert_eq!(attributes.len(), 6);
1154    }
1155
1156    #[test]
1157    fn test_extract_dependencies_use_declarations() {
1158        let source = r#"
1159            use std::collections::HashMap;
1160            use crate::models::{Language, SearchResult};
1161            use super::utils;
1162            use anyhow::Result;
1163        "#;
1164
1165        let deps = RustDependencyExtractor::extract_dependencies(source).unwrap();
1166
1167        // Should find 4 imports
1168        assert_eq!(deps.len(), 4);
1169
1170        // Check std import
1171        let std_import = deps
1172            .iter()
1173            .find(|d| d.imported_path == "std::collections::HashMap")
1174            .unwrap();
1175        assert!(matches!(
1176            std_import.import_type,
1177            crate::models::ImportType::Stdlib
1178        ));
1179
1180        // Check crate import with symbols
1181        let crate_import = deps
1182            .iter()
1183            .find(|d| d.imported_path == "crate::models")
1184            .unwrap();
1185        assert!(matches!(
1186            crate_import.import_type,
1187            crate::models::ImportType::Internal
1188        ));
1189        assert!(crate_import.imported_symbols.is_some());
1190        let symbols = crate_import.imported_symbols.as_ref().unwrap();
1191        assert_eq!(symbols.len(), 2);
1192        assert!(symbols.contains(&"Language".to_string()));
1193        assert!(symbols.contains(&"SearchResult".to_string()));
1194
1195        // Check super import
1196        let super_import = deps
1197            .iter()
1198            .find(|d| d.imported_path == "super::utils")
1199            .unwrap();
1200        assert!(matches!(
1201            super_import.import_type,
1202            crate::models::ImportType::Internal
1203        ));
1204
1205        // Check external import
1206        let external_import = deps
1207            .iter()
1208            .find(|d| d.imported_path == "anyhow::Result")
1209            .unwrap();
1210        assert!(matches!(
1211            external_import.import_type,
1212            crate::models::ImportType::External
1213        ));
1214    }
1215
1216    #[test]
1217    fn test_extract_dependencies_mod_declarations() {
1218        let source = r#"
1219            mod parser;
1220            mod utils;
1221
1222            mod inline {
1223                fn test() {}
1224            }
1225        "#;
1226
1227        let deps = RustDependencyExtractor::extract_dependencies(source).unwrap();
1228
1229        // Should find 2 external mod declarations (not the inline one)
1230        assert_eq!(deps.len(), 2);
1231        assert!(deps.iter().any(|d| d.imported_path == "parser"));
1232        assert!(deps.iter().any(|d| d.imported_path == "utils"));
1233        assert!(
1234            deps.iter()
1235                .all(|d| matches!(d.import_type, crate::models::ImportType::ModDecl))
1236        );
1237    }
1238
1239    #[test]
1240    fn test_extract_dependencies_extern_crate() {
1241        let source = r#"
1242            extern crate serde;
1243            extern crate serde_json;
1244        "#;
1245
1246        let deps = RustDependencyExtractor::extract_dependencies(source).unwrap();
1247
1248        // Should find 2 extern crate declarations
1249        assert_eq!(deps.len(), 2);
1250        assert!(deps.iter().any(|d| d.imported_path == "serde"));
1251        assert!(deps.iter().any(|d| d.imported_path == "serde_json"));
1252        assert!(
1253            deps.iter()
1254                .all(|d| matches!(d.import_type, crate::models::ImportType::External))
1255        );
1256    }
1257
1258    #[test]
1259    fn test_parse_use_with_aliases() {
1260        let source = r#"
1261            use std::io::Result as IoResult;
1262            use std::collections::{HashMap as Map, HashSet};
1263        "#;
1264
1265        let deps = RustDependencyExtractor::extract_dependencies(source).unwrap();
1266
1267        // Check alias handling - should extract the original name
1268        let io_import = deps
1269            .iter()
1270            .find(|d| d.imported_path == "std::io::Result")
1271            .unwrap();
1272        assert!(matches!(
1273            io_import.import_type,
1274            crate::models::ImportType::Stdlib
1275        ));
1276
1277        let collections_import = deps
1278            .iter()
1279            .find(|d| d.imported_path == "std::collections")
1280            .unwrap();
1281        let symbols = collections_import.imported_symbols.as_ref().unwrap();
1282        assert_eq!(symbols.len(), 2);
1283        assert!(symbols.contains(&"HashMap".to_string()));
1284        assert!(symbols.contains(&"HashSet".to_string()));
1285    }
1286
1287    #[test]
1288    fn test_classify_rust_imports() {
1289        use crate::models::ImportType;
1290
1291        // Stdlib
1292        assert!(matches!(
1293            classify_rust_import("std::collections::HashMap"),
1294            ImportType::Stdlib
1295        ));
1296        assert!(matches!(
1297            classify_rust_import("core::ptr"),
1298            ImportType::Stdlib
1299        ));
1300        assert!(matches!(
1301            classify_rust_import("alloc::vec::Vec"),
1302            ImportType::Stdlib
1303        ));
1304
1305        // Internal
1306        assert!(matches!(
1307            classify_rust_import("crate::models::Language"),
1308            ImportType::Internal
1309        ));
1310        assert!(matches!(
1311            classify_rust_import("super::utils"),
1312            ImportType::Internal
1313        ));
1314        assert!(matches!(
1315            classify_rust_import("self::helper"),
1316            ImportType::Internal
1317        ));
1318
1319        // External
1320        assert!(matches!(
1321            classify_rust_import("serde::Serialize"),
1322            ImportType::External
1323        ));
1324        assert!(matches!(
1325            classify_rust_import("anyhow::Result"),
1326            ImportType::External
1327        ));
1328        assert!(matches!(
1329            classify_rust_import("tokio::runtime"),
1330            ImportType::External
1331        ));
1332    }
1333}
1334
1335// ============================================================================
1336// Path Resolution
1337// ============================================================================
1338
1339/// Find the crate root (directory containing Cargo.toml) by walking up from a given path
1340fn find_crate_root(start_path: &str) -> Option<String> {
1341    let path = std::path::Path::new(start_path);
1342    let mut current = path.parent()?;
1343
1344    // Walk up until we find Cargo.toml
1345    loop {
1346        let cargo_toml = current.join("Cargo.toml");
1347        if cargo_toml.exists() {
1348            return Some(current.to_string_lossy().to_string());
1349        }
1350
1351        // For test paths that don't exist, assume standard Rust structure:
1352        // If we find "/src" in the path, the parent of "src" is likely the crate root
1353        if current.ends_with("src")
1354            && let Some(parent) = current.parent()
1355        {
1356            return Some(parent.to_string_lossy().to_string());
1357        }
1358
1359        // Move up to parent directory
1360        current = match current.parent() {
1361            Some(p) if p.as_os_str().is_empty() => return None,
1362            Some(p) => p,
1363            None => return None,
1364        };
1365    }
1366}
1367
1368/// Resolve a Rust use statement to a file path
1369///
1370/// Handles:
1371/// - `crate::` imports: `crate::models::Language` → `src/models.rs` or `src/models/mod.rs`
1372/// - `super::` imports: relative to parent module
1373/// - `self::` imports: relative to current module
1374/// - `mod parser;`: look for `parser.rs` or `parser/mod.rs`
1375///
1376/// Does NOT handle:
1377/// - External crate imports (would require parsing Cargo.toml dependencies)
1378/// - Stdlib imports (std::, core::, alloc::)
1379pub fn resolve_rust_use_to_path(
1380    import_path: &str,
1381    current_file_path: Option<&str>,
1382    project_root: Option<&str>,
1383) -> Option<String> {
1384    // Only handle internal imports (crate::, super::, self::, or bare module names)
1385    if !import_path.starts_with("crate::")
1386        && !import_path.starts_with("super::")
1387        && !import_path.starts_with("self::")
1388    {
1389        // Check if it's a simple module name (no :: separator at all)
1390        if import_path.contains("::") {
1391            return None; // External or stdlib import
1392        }
1393        // Fall through for simple module names like "parser"
1394    }
1395
1396    let current_file = current_file_path?;
1397
1398    // Resolve the current file to an absolute path (against project_root when it is
1399    // relative) so that all `.exists()` checks below are independent of the process's
1400    // current working directory. The resolved path is converted back to a
1401    // workspace-relative path before returning (see `relativize_to_project_root`), so it
1402    // matches the paths stored in the dependency index.
1403    let abs_current = match project_root {
1404        Some(root) if !root.is_empty() && std::path::Path::new(current_file).is_relative() => {
1405            std::path::Path::new(root).join(current_file)
1406        }
1407        _ => std::path::PathBuf::from(current_file),
1408    };
1409    let current_file = abs_current.to_string_lossy().to_string();
1410    let current_path = abs_current.as_path();
1411
1412    // Find the crate root
1413    let crate_root = find_crate_root(&current_file)?;
1414    let crate_root_path = std::path::Path::new(&crate_root);
1415
1416    let resolved = if import_path.starts_with("crate::") {
1417        // Resolve from crate root (typically src/)
1418        let module_path = import_path.strip_prefix("crate::").unwrap();
1419        let parts: Vec<&str> = module_path.split("::").collect();
1420
1421        // Try src/ first (standard Rust project structure)
1422        let src_root = crate_root_path.join("src");
1423        // Resolve the longest prefix of `parts` that names a real module file, popping
1424        // trailing item segments (a type/fn/const/trait is not a module). Fall back to a
1425        // best-guess path so the indexer can still attempt a lookup.
1426        resolve_longest_module_prefix(&src_root, &parts)
1427            .or_else(|| resolve_rust_module_path(&src_root, &parts))
1428    } else if import_path.starts_with("super::") {
1429        // Resolve relative to parent module
1430        let module_path = import_path.strip_prefix("super::").unwrap();
1431        let parts: Vec<&str> = module_path.split("::").collect();
1432
1433        // Get parent directory (go up one level)
1434        let current_dir = if current_path.file_name().unwrap() == "mod.rs" {
1435            // If current file is mod.rs, go up two levels
1436            current_path.parent()?.parent()?
1437        } else {
1438            // Otherwise, go up one level
1439            current_path.parent()?
1440        };
1441
1442        resolve_longest_module_prefix(current_dir, &parts)
1443            .or_else(|| resolve_rust_module_path(current_dir, &parts))
1444    } else if import_path.starts_with("self::") {
1445        // Resolve relative to current module
1446        let module_path = import_path.strip_prefix("self::").unwrap();
1447        let parts: Vec<&str> = module_path.split("::").collect();
1448
1449        // Get current module directory
1450        let current_dir = if current_path.file_name().unwrap() == "mod.rs" {
1451            // If current file is mod.rs, use parent directory
1452            current_path.parent()?
1453        } else {
1454            // Otherwise, use current directory
1455            current_path.parent()?
1456        };
1457
1458        resolve_longest_module_prefix(current_dir, &parts)
1459            .or_else(|| resolve_rust_module_path(current_dir, &parts))
1460    } else {
1461        // Simple module name (e.g., "parser" in "mod parser;")
1462        // Look for parser.rs or parser/mod.rs in the current directory
1463        let current_dir = current_path.parent()?;
1464        let module_file = current_dir.join(format!("{}.rs", import_path));
1465        let module_dir = current_dir.join(import_path).join("mod.rs");
1466
1467        if module_file.exists() {
1468            Some(module_file.to_string_lossy().to_string())
1469        } else if module_dir.exists() {
1470            Some(module_dir.to_string_lossy().to_string())
1471        } else {
1472            // Return the most likely candidate even if it doesn't exist
1473            // The indexer will check if the file is actually in the index
1474            Some(module_file.to_string_lossy().to_string())
1475        }
1476    };
1477
1478    // Convert the resolved absolute path back to a workspace-relative path so it matches
1479    // the paths stored in the dependency index (see `abs_current` above).
1480    resolved.map(|path| relativize_to_project_root(&path, project_root))
1481}
1482
1483/// Resolve the longest prefix of `parts` that names a real module file under `base_dir`.
1484///
1485/// Pops trailing item segments (a type/fn/const/trait is not a module) until a prefix
1486/// resolves to a file that actually exists. Returns `None` if even the one-segment prefix
1487/// has no backing file, leaving the caller to fall back to a best guess or cross-crate
1488/// resolution. Unlike `resolve_rust_module_path`, every returned candidate is checked for
1489/// existence, so that function's best-guess return is harmless here.
1490fn resolve_longest_module_prefix(base_dir: &std::path::Path, parts: &[&str]) -> Option<String> {
1491    let mut k = parts.len();
1492    while k >= 1 {
1493        if let Some(path) = resolve_rust_module_path(base_dir, &parts[..k])
1494            && std::path::Path::new(&path).exists()
1495        {
1496            return Some(path);
1497        }
1498        k -= 1;
1499    }
1500    None
1501}
1502
1503/// Strip `project_root` from an absolute resolved path to yield a workspace-relative path.
1504///
1505/// Returns the path (still normalized to forward slashes) unchanged when `project_root` is
1506/// absent/empty or is not a prefix of `path` (e.g. the input was already relative).
1507///
1508/// The result always uses forward slashes so resolved import paths are deterministic and
1509/// match the forward-slash paths stored in the dependency index on every platform. Without
1510/// this, `PathBuf::join` yields backslash-separated paths on Windows, which would neither
1511/// match the index nor equal the expected output (see the other parsers, which normalize
1512/// the same way).
1513fn relativize_to_project_root(path: &str, project_root: Option<&str>) -> String {
1514    if let Some(root) = project_root
1515        && !root.is_empty()
1516        && let Ok(rel) = std::path::Path::new(path).strip_prefix(root)
1517    {
1518        return rel.to_string_lossy().replace('\\', "/");
1519    }
1520    path.replace('\\', "/")
1521}
1522
1523/// Resolve a Rust module path (list of components) to a file path
1524///
1525/// Examples:
1526/// - `["models"]` → `models.rs` or `models/mod.rs`
1527/// - `["models", "language"]` → `models/language.rs` or `models/language/mod.rs`
1528fn resolve_rust_module_path(base_dir: &std::path::Path, parts: &[&str]) -> Option<String> {
1529    if parts.is_empty() {
1530        return None;
1531    }
1532
1533    // Build the path incrementally
1534    let mut current_path = base_dir.to_path_buf();
1535
1536    for (i, part) in parts.iter().enumerate() {
1537        if i == parts.len() - 1 {
1538            // Last component - try both .rs file and mod.rs
1539            let file_path = current_path.join(format!("{}.rs", part));
1540            let mod_path = current_path.join(part).join("mod.rs");
1541
1542            log::trace!("Checking Rust module path: {}", file_path.display());
1543            log::trace!("Checking Rust module path: {}", mod_path.display());
1544
1545            // Return the first candidate (indexer will validate it exists)
1546            if file_path.exists() {
1547                return Some(file_path.to_string_lossy().to_string());
1548            } else if mod_path.exists() {
1549                return Some(mod_path.to_string_lossy().to_string());
1550            } else {
1551                // Return most likely candidate even if it doesn't exist
1552                return Some(file_path.to_string_lossy().to_string());
1553            }
1554        } else {
1555            // Intermediate component - must be a directory
1556            current_path = current_path.join(part);
1557        }
1558    }
1559
1560    None
1561}
1562
1563#[cfg(test)]
1564mod path_resolution_tests {
1565    use super::*;
1566
1567    #[test]
1568    fn test_resolve_crate_import() {
1569        // crate::models::Language
1570        let result = resolve_rust_use_to_path(
1571            "crate::models",
1572            Some("/home/user/project/src/main.rs"),
1573            Some("/home/user/project"),
1574        );
1575
1576        assert!(result.is_some());
1577        let path = result.unwrap();
1578        // Should resolve to src/models.rs or src/models/mod.rs
1579        assert!(path.contains("models.rs") || path.contains("models/mod.rs"));
1580    }
1581
1582    #[test]
1583    fn test_resolve_super_import() {
1584        // super::utils from src/commands/index.rs
1585        let result = resolve_rust_use_to_path(
1586            "super::utils",
1587            Some("/home/user/project/src/commands/index.rs"),
1588            Some("/home/user/project"),
1589        );
1590
1591        assert!(result.is_some());
1592        let path = result.unwrap();
1593        // Should resolve to src/utils.rs
1594        assert!(path.contains("src") && path.contains("utils.rs"));
1595    }
1596
1597    #[test]
1598    fn test_resolve_self_import() {
1599        // self::helper from src/models/mod.rs
1600        let result = resolve_rust_use_to_path(
1601            "self::helper",
1602            Some("/home/user/project/src/models/mod.rs"),
1603            Some("/home/user/project"),
1604        );
1605
1606        assert!(result.is_some());
1607        let path = result.unwrap();
1608        // Should resolve to src/models/helper.rs
1609        assert!(path.contains("models") && path.contains("helper.rs"));
1610    }
1611
1612    #[test]
1613    fn test_resolve_mod_declaration() {
1614        // mod parser; from src/main.rs
1615        let result = resolve_rust_use_to_path(
1616            "parser",
1617            Some("/home/user/project/src/main.rs"),
1618            Some("/home/user/project"),
1619        );
1620
1621        assert!(result.is_some());
1622        let path = result.unwrap();
1623        // Should resolve to src/parser.rs
1624        assert!(path.contains("parser.rs"));
1625    }
1626
1627    #[test]
1628    fn test_resolve_nested_crate_import() {
1629        // crate::models::language::Language
1630        let result = resolve_rust_use_to_path(
1631            "crate::models::language",
1632            Some("/home/user/project/src/main.rs"),
1633            Some("/home/user/project"),
1634        );
1635
1636        assert!(result.is_some());
1637        let path = result.unwrap();
1638        // Should resolve to src/models/language.rs or src/models/language/mod.rs
1639        assert!(
1640            path.contains("models")
1641                && (path.contains("language.rs") || path.contains("language/mod.rs"))
1642        );
1643    }
1644
1645    #[test]
1646    fn test_external_import_not_supported() {
1647        // anyhow::Result (external crate)
1648        let result = resolve_rust_use_to_path(
1649            "anyhow::Result",
1650            Some("/home/user/project/src/main.rs"),
1651            Some("/home/user/project"),
1652        );
1653
1654        // Should return None for external imports
1655        assert!(result.is_none());
1656    }
1657
1658    #[test]
1659    fn test_stdlib_import_not_supported() {
1660        // std::collections::HashMap (stdlib)
1661        let result = resolve_rust_use_to_path(
1662            "std::collections::HashMap",
1663            Some("/home/user/project/src/main.rs"),
1664            Some("/home/user/project"),
1665        );
1666
1667        // Should return None for stdlib imports
1668        assert!(result.is_none());
1669    }
1670
1671    #[test]
1672    fn test_resolve_without_current_file() {
1673        let result = resolve_rust_use_to_path("crate::models", None, Some("/home/user/project"));
1674
1675        // Should return None if no current file provided
1676        assert!(result.is_none());
1677    }
1678
1679    // ------------------------------------------------------------------
1680    // Filesystem-backed tests for trailing-item resolution (P1) and
1681    // CWD-independence (P2). These build a real temp workspace because the
1682    // resolver gates candidates on `.exists()`; string-only fixtures (above)
1683    // cannot exercise the prefix-popping path.
1684    // ------------------------------------------------------------------
1685
1686    use std::io::Write;
1687    use tempfile::TempDir;
1688
1689    fn write_file(root: &std::path::Path, rel: &str, contents: &str) {
1690        let path = root.join(rel);
1691        std::fs::create_dir_all(path.parent().unwrap()).unwrap();
1692        let mut f = std::fs::File::create(&path).unwrap();
1693        f.write_all(contents.as_bytes()).unwrap();
1694    }
1695
1696    /// Build a single-crate workspace `<tmp>/crates/a` with a few modules.
1697    fn make_workspace() -> TempDir {
1698        let tmp = TempDir::new().unwrap();
1699        let root = tmp.path();
1700        write_file(root, "crates/a/Cargo.toml", "[package]\nname = \"a\"\n");
1701        write_file(
1702            root,
1703            "crates/a/src/lib.rs",
1704            "pub mod thing;\npub mod user;\n",
1705        );
1706        write_file(root, "crates/a/src/thing.rs", "pub struct Thing;\n");
1707        write_file(root, "crates/a/src/thing/inner.rs", "pub struct Deep;\n");
1708        write_file(root, "crates/a/src/user.rs", "use crate::thing::Thing;\n");
1709        tmp
1710    }
1711
1712    #[test]
1713    fn test_trailing_item_single_module() {
1714        // crate::thing::Thing  ->  crates/a/src/thing.rs  (Thing is an item, not a module)
1715        let tmp = make_workspace();
1716        let root = tmp.path().to_str().unwrap();
1717        let result = resolve_rust_use_to_path(
1718            "crate::thing::Thing",
1719            Some("crates/a/src/user.rs"), // workspace-RELATIVE, as the indexer passes it
1720            Some(root),
1721        );
1722        assert_eq!(result.as_deref(), Some("crates/a/src/thing.rs"));
1723    }
1724
1725    #[test]
1726    fn test_trailing_item_nested_module() {
1727        // crate::thing::inner::Deep  ->  crates/a/src/thing/inner.rs
1728        let tmp = make_workspace();
1729        let root = tmp.path().to_str().unwrap();
1730        let result = resolve_rust_use_to_path(
1731            "crate::thing::inner::Deep",
1732            Some("crates/a/src/user.rs"),
1733            Some(root),
1734        );
1735        assert_eq!(result.as_deref(), Some("crates/a/src/thing/inner.rs"));
1736    }
1737
1738    #[test]
1739    fn test_module_only_import_still_resolves() {
1740        // Regression guard: crate::thing (no trailing item) -> crates/a/src/thing.rs
1741        let tmp = make_workspace();
1742        let root = tmp.path().to_str().unwrap();
1743        let result =
1744            resolve_rust_use_to_path("crate::thing", Some("crates/a/src/user.rs"), Some(root));
1745        assert_eq!(result.as_deref(), Some("crates/a/src/thing.rs"));
1746    }
1747
1748    #[test]
1749    fn test_super_trailing_item() {
1750        // From crates/a/src/sub/mod.rs, super::thing::Thing -> crates/a/src/thing.rs
1751        let tmp = make_workspace();
1752        write_file(
1753            tmp.path(),
1754            "crates/a/src/sub/mod.rs",
1755            "use super::thing::Thing;\n",
1756        );
1757        let root = tmp.path().to_str().unwrap();
1758        let result = resolve_rust_use_to_path(
1759            "super::thing::Thing",
1760            Some("crates/a/src/sub/mod.rs"),
1761            Some(root),
1762        );
1763        assert_eq!(result.as_deref(), Some("crates/a/src/thing.rs"));
1764    }
1765
1766    #[test]
1767    fn test_resolution_is_cwd_independent() {
1768        // P2: with a relative current_file and an absolute project_root, resolution must
1769        // not depend on the process CWD. The test process CWD is the repo root, never the
1770        // temp dir, so a correct result here proves CWD-independence.
1771        let tmp = make_workspace();
1772        let root = tmp.path().to_str().unwrap();
1773        let result = resolve_rust_use_to_path(
1774            "crate::thing::Thing",
1775            Some("crates/a/src/user.rs"),
1776            Some(root),
1777        );
1778        assert_eq!(result.as_deref(), Some("crates/a/src/thing.rs"));
1779    }
1780}
1781
1782// ============================================================================
1783// Workspace Support
1784// ============================================================================
1785
1786/// A Rust crate discovered by scanning for Cargo.toml files.
1787#[derive(Debug, Clone)]
1788pub struct RustCrate {
1789    pub name: String,
1790    pub root_path: std::path::PathBuf,
1791}
1792
1793/// Find all Rust crates in the workspace by scanning for Cargo.toml files.
1794///
1795/// Only runs if a root Cargo.toml exists (to avoid wasted work on non-Rust projects).
1796/// Uses the `ignore` crate to respect .gitignore patterns.
1797pub fn parse_all_rust_crates(root: &std::path::Path) -> anyhow::Result<Vec<RustCrate>> {
1798    // Gate: only scan if this looks like a Rust project
1799    if !root.join("Cargo.toml").exists() {
1800        return Ok(Vec::new());
1801    }
1802
1803    let mut crates = Vec::new();
1804    let walker = ignore::WalkBuilder::new(root).git_ignore(true).build();
1805
1806    for entry in walker {
1807        let entry = entry?;
1808        if entry.file_name() == "Cargo.toml" {
1809            let content = std::fs::read_to_string(entry.path())?;
1810            if let Some(name) = extract_crate_name(&content)
1811                && let Some(crate_root) = entry.path().parent()
1812            {
1813                crates.push(RustCrate {
1814                    name,
1815                    root_path: crate_root.to_path_buf(),
1816                });
1817            }
1818        }
1819    }
1820    Ok(crates)
1821}
1822
1823/// Extract the package name from a Cargo.toml file using the `toml` crate.
1824fn extract_crate_name(content: &str) -> Option<String> {
1825    let table: toml::Table = content.parse().ok()?;
1826    table
1827        .get("package")?
1828        .get("name")?
1829        .as_str()
1830        .map(|s| s.to_string())
1831}
1832
1833/// Reclassify an import based on known workspace crates.
1834///
1835/// If the import path matches a workspace crate name (e.g., `my_crate` or
1836/// `my_crate::module`), reclassify it as Internal. Otherwise, fall back to
1837/// the default Rust import classification.
1838pub fn reclassify_rust_import(path: &str, crates: &[RustCrate]) -> crate::models::ImportType {
1839    for krate in crates {
1840        if path == krate.name || path.starts_with(&format!("{}::", krate.name)) {
1841            return crate::models::ImportType::Internal;
1842        }
1843    }
1844    classify_rust_import(path)
1845}
1846
1847/// Resolve a workspace crate import to a file path.
1848///
1849/// Handles imports like `my_crate::config` by finding the matching workspace
1850/// crate and resolving the module path within its `src/` directory.
1851pub fn resolve_rust_workspace_path(import_path: &str, crates: &[RustCrate]) -> Option<String> {
1852    for krate in crates {
1853        if import_path == krate.name || import_path.starts_with(&format!("{}::", krate.name)) {
1854            let relative_module = if import_path == krate.name {
1855                ""
1856            } else {
1857                import_path
1858                    .strip_prefix(&format!("{}::", krate.name))
1859                    .unwrap_or("")
1860            };
1861
1862            let src_root = krate.root_path.join("src");
1863
1864            if relative_module.is_empty() {
1865                // Bare crate import -> lib.rs or main.rs
1866                let lib = src_root.join("lib.rs");
1867                if lib.exists() {
1868                    return Some(lib.to_string_lossy().to_string());
1869                }
1870                let main = src_root.join("main.rs");
1871                if main.exists() {
1872                    return Some(main.to_string_lossy().to_string());
1873                }
1874            } else {
1875                let parts: Vec<&str> = relative_module.split("::").collect();
1876
1877                // Try resolving as a module file (only accept if the file exists)
1878                if let Some(path) = resolve_rust_module_path(&src_root, &parts)
1879                    && std::path::Path::new(&path).exists()
1880                {
1881                    return Some(path);
1882                }
1883
1884                // Try popping the last component (it may be an item like a struct/fn, not a module)
1885                if parts.len() > 1
1886                    && let Some(path) =
1887                        resolve_rust_module_path(&src_root, &parts[..parts.len() - 1])
1888                    && std::path::Path::new(&path).exists()
1889                {
1890                    return Some(path);
1891                }
1892
1893                // Return the best-guess path even if it doesn't exist
1894                // (the indexer will validate against its file database)
1895                if let Some(path) = resolve_rust_module_path(&src_root, &parts) {
1896                    return Some(path);
1897                }
1898            }
1899        }
1900    }
1901    None
1902}
1903
1904#[cfg(test)]
1905mod workspace_tests {
1906    use super::*;
1907    use std::fs;
1908    use tempfile::TempDir;
1909
1910    fn create_workspace(dir: &std::path::Path) {
1911        // Root Cargo.toml with workspace
1912        fs::write(
1913            dir.join("Cargo.toml"),
1914            r#"[workspace]
1915members = ["crate_a", "crate_b"]
1916"#,
1917        )
1918        .unwrap();
1919
1920        // crate_a
1921        let crate_a = dir.join("crate_a");
1922        fs::create_dir_all(crate_a.join("src")).unwrap();
1923        fs::write(
1924            crate_a.join("Cargo.toml"),
1925            r#"[package]
1926name = "crate_a"
1927version = "0.1.0"
1928"#,
1929        )
1930        .unwrap();
1931        fs::write(crate_a.join("src/lib.rs"), "pub mod config;").unwrap();
1932        fs::write(crate_a.join("src/config.rs"), "pub struct Config;").unwrap();
1933
1934        // crate_b with inline table syntax
1935        let crate_b = dir.join("crate_b");
1936        fs::create_dir_all(crate_b.join("src")).unwrap();
1937        fs::write(
1938            crate_b.join("Cargo.toml"),
1939            r#"[package]
1940name = "crate_b" # a comment
1941version = "0.1.0"
1942"#,
1943        )
1944        .unwrap();
1945        fs::write(crate_b.join("src/lib.rs"), "").unwrap();
1946    }
1947
1948    #[test]
1949    fn test_extract_crate_name_standard() {
1950        let toml = r#"[package]
1951name = "my_crate"
1952version = "0.1.0"
1953"#;
1954        assert_eq!(extract_crate_name(toml), Some("my_crate".to_string()));
1955    }
1956
1957    #[test]
1958    fn test_extract_crate_name_inline_table() {
1959        // The toml crate handles this correctly
1960        let toml = r#"[package]
1961name = "inline_crate"
1962version = "0.1.0"
1963edition = "2021"
1964"#;
1965        assert_eq!(extract_crate_name(toml), Some("inline_crate".to_string()));
1966    }
1967
1968    #[test]
1969    fn test_extract_crate_name_with_comment() {
1970        let toml = r#"[package]
1971name = "commented_crate" # my crate
1972version = "0.1.0"
1973"#;
1974        assert_eq!(
1975            extract_crate_name(toml),
1976            Some("commented_crate".to_string())
1977        );
1978    }
1979
1980    #[test]
1981    fn test_extract_crate_name_no_package() {
1982        let toml = r#"[workspace]
1983members = ["crate_a"]
1984"#;
1985        assert_eq!(extract_crate_name(toml), None);
1986    }
1987
1988    #[test]
1989    fn test_parse_all_rust_crates() {
1990        let dir = TempDir::new().unwrap();
1991        create_workspace(dir.path());
1992
1993        let crates = parse_all_rust_crates(dir.path()).unwrap();
1994        assert_eq!(crates.len(), 2);
1995
1996        let names: Vec<&str> = crates.iter().map(|c| c.name.as_str()).collect();
1997        assert!(names.contains(&"crate_a"));
1998        assert!(names.contains(&"crate_b"));
1999    }
2000
2001    #[test]
2002    fn test_parse_all_rust_crates_non_rust_project() {
2003        let dir = TempDir::new().unwrap();
2004        // No Cargo.toml -> should return empty
2005        let crates = parse_all_rust_crates(dir.path()).unwrap();
2006        assert!(crates.is_empty());
2007    }
2008
2009    #[test]
2010    fn test_reclassify_rust_import_workspace_crate() {
2011        let crates = vec![RustCrate {
2012            name: "crate_a".to_string(),
2013            root_path: std::path::PathBuf::from("/workspace/crate_a"),
2014        }];
2015
2016        assert!(matches!(
2017            reclassify_rust_import("crate_a", &crates),
2018            crate::models::ImportType::Internal
2019        ));
2020        assert!(matches!(
2021            reclassify_rust_import("crate_a::config", &crates),
2022            crate::models::ImportType::Internal
2023        ));
2024        // External crate should stay External
2025        assert!(matches!(
2026            reclassify_rust_import("serde::Serialize", &crates),
2027            crate::models::ImportType::External
2028        ));
2029    }
2030
2031    #[test]
2032    fn test_resolve_rust_workspace_path() {
2033        let dir = TempDir::new().unwrap();
2034        create_workspace(dir.path());
2035
2036        let crates = parse_all_rust_crates(dir.path()).unwrap();
2037
2038        // Bare crate import -> lib.rs
2039        let result = resolve_rust_workspace_path("crate_a", &crates);
2040        assert!(result.is_some());
2041        assert!(result.unwrap().ends_with("lib.rs"));
2042
2043        // Module import -> config.rs
2044        let result = resolve_rust_workspace_path("crate_a::config", &crates);
2045        assert!(result.is_some());
2046        assert!(result.unwrap().ends_with("config.rs"));
2047
2048        // Item import -> resolves to the module file
2049        let result = resolve_rust_workspace_path("crate_a::config::Config", &crates);
2050        assert!(result.is_some());
2051        assert!(result.unwrap().ends_with("config.rs"));
2052
2053        // Unknown crate -> None
2054        let result = resolve_rust_workspace_path("unknown_crate::foo", &crates);
2055        assert!(result.is_none());
2056    }
2057}