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                        if child.id() == func_node.id() {
326                            func_index = Some(i);
327                            break;
328                        }
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) as usize; // 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            if let Some(parent) = current.parent() {
1355                return Some(parent.to_string_lossy().to_string());
1356            }
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    let current_path = std::path::Path::new(current_file);
1398
1399    // Find the crate root
1400    let crate_root = find_crate_root(current_file)?;
1401    let crate_root_path = std::path::Path::new(&crate_root);
1402
1403    if import_path.starts_with("crate::") {
1404        // Resolve from crate root (typically src/)
1405        let module_path = import_path.strip_prefix("crate::").unwrap();
1406        let parts: Vec<&str> = module_path.split("::").collect();
1407
1408        // Try src/ first (standard Rust project structure)
1409        let src_root = crate_root_path.join("src");
1410        resolve_rust_module_path(&src_root, &parts)
1411    } else if import_path.starts_with("super::") {
1412        // Resolve relative to parent module
1413        let module_path = import_path.strip_prefix("super::").unwrap();
1414        let parts: Vec<&str> = module_path.split("::").collect();
1415
1416        // Get parent directory (go up one level)
1417        let current_dir = if current_path.file_name().unwrap() == "mod.rs" {
1418            // If current file is mod.rs, go up two levels
1419            current_path.parent()?.parent()?
1420        } else {
1421            // Otherwise, go up one level
1422            current_path.parent()?
1423        };
1424
1425        resolve_rust_module_path(current_dir, &parts)
1426    } else if import_path.starts_with("self::") {
1427        // Resolve relative to current module
1428        let module_path = import_path.strip_prefix("self::").unwrap();
1429        let parts: Vec<&str> = module_path.split("::").collect();
1430
1431        // Get current module directory
1432        let current_dir = if current_path.file_name().unwrap() == "mod.rs" {
1433            // If current file is mod.rs, use parent directory
1434            current_path.parent()?
1435        } else {
1436            // Otherwise, use current directory
1437            current_path.parent()?
1438        };
1439
1440        resolve_rust_module_path(current_dir, &parts)
1441    } else {
1442        // Simple module name (e.g., "parser" in "mod parser;")
1443        // Look for parser.rs or parser/mod.rs in the current directory
1444        let current_dir = current_path.parent()?;
1445        let module_file = current_dir.join(format!("{}.rs", import_path));
1446        let module_dir = current_dir.join(import_path).join("mod.rs");
1447
1448        if module_file.exists() {
1449            Some(module_file.to_string_lossy().to_string())
1450        } else if module_dir.exists() {
1451            Some(module_dir.to_string_lossy().to_string())
1452        } else {
1453            // Return the most likely candidate even if it doesn't exist
1454            // The indexer will check if the file is actually in the index
1455            Some(module_file.to_string_lossy().to_string())
1456        }
1457    }
1458}
1459
1460/// Resolve a Rust module path (list of components) to a file path
1461///
1462/// Examples:
1463/// - `["models"]` → `models.rs` or `models/mod.rs`
1464/// - `["models", "language"]` → `models/language.rs` or `models/language/mod.rs`
1465fn resolve_rust_module_path(base_dir: &std::path::Path, parts: &[&str]) -> Option<String> {
1466    if parts.is_empty() {
1467        return None;
1468    }
1469
1470    // Build the path incrementally
1471    let mut current_path = base_dir.to_path_buf();
1472
1473    for (i, part) in parts.iter().enumerate() {
1474        if i == parts.len() - 1 {
1475            // Last component - try both .rs file and mod.rs
1476            let file_path = current_path.join(format!("{}.rs", part));
1477            let mod_path = current_path.join(part).join("mod.rs");
1478
1479            log::trace!("Checking Rust module path: {}", file_path.display());
1480            log::trace!("Checking Rust module path: {}", mod_path.display());
1481
1482            // Return the first candidate (indexer will validate it exists)
1483            if file_path.exists() {
1484                return Some(file_path.to_string_lossy().to_string());
1485            } else if mod_path.exists() {
1486                return Some(mod_path.to_string_lossy().to_string());
1487            } else {
1488                // Return most likely candidate even if it doesn't exist
1489                return Some(file_path.to_string_lossy().to_string());
1490            }
1491        } else {
1492            // Intermediate component - must be a directory
1493            current_path = current_path.join(part);
1494        }
1495    }
1496
1497    None
1498}
1499
1500#[cfg(test)]
1501mod path_resolution_tests {
1502    use super::*;
1503
1504    #[test]
1505    fn test_resolve_crate_import() {
1506        // crate::models::Language
1507        let result = resolve_rust_use_to_path(
1508            "crate::models",
1509            Some("/home/user/project/src/main.rs"),
1510            Some("/home/user/project"),
1511        );
1512
1513        assert!(result.is_some());
1514        let path = result.unwrap();
1515        // Should resolve to src/models.rs or src/models/mod.rs
1516        assert!(path.contains("models.rs") || path.contains("models/mod.rs"));
1517    }
1518
1519    #[test]
1520    fn test_resolve_super_import() {
1521        // super::utils from src/commands/index.rs
1522        let result = resolve_rust_use_to_path(
1523            "super::utils",
1524            Some("/home/user/project/src/commands/index.rs"),
1525            Some("/home/user/project"),
1526        );
1527
1528        assert!(result.is_some());
1529        let path = result.unwrap();
1530        // Should resolve to src/utils.rs
1531        assert!(path.contains("src") && path.contains("utils.rs"));
1532    }
1533
1534    #[test]
1535    fn test_resolve_self_import() {
1536        // self::helper from src/models/mod.rs
1537        let result = resolve_rust_use_to_path(
1538            "self::helper",
1539            Some("/home/user/project/src/models/mod.rs"),
1540            Some("/home/user/project"),
1541        );
1542
1543        assert!(result.is_some());
1544        let path = result.unwrap();
1545        // Should resolve to src/models/helper.rs
1546        assert!(path.contains("models") && path.contains("helper.rs"));
1547    }
1548
1549    #[test]
1550    fn test_resolve_mod_declaration() {
1551        // mod parser; from src/main.rs
1552        let result = resolve_rust_use_to_path(
1553            "parser",
1554            Some("/home/user/project/src/main.rs"),
1555            Some("/home/user/project"),
1556        );
1557
1558        assert!(result.is_some());
1559        let path = result.unwrap();
1560        // Should resolve to src/parser.rs
1561        assert!(path.contains("parser.rs"));
1562    }
1563
1564    #[test]
1565    fn test_resolve_nested_crate_import() {
1566        // crate::models::language::Language
1567        let result = resolve_rust_use_to_path(
1568            "crate::models::language",
1569            Some("/home/user/project/src/main.rs"),
1570            Some("/home/user/project"),
1571        );
1572
1573        assert!(result.is_some());
1574        let path = result.unwrap();
1575        // Should resolve to src/models/language.rs or src/models/language/mod.rs
1576        assert!(
1577            path.contains("models")
1578                && (path.contains("language.rs") || path.contains("language/mod.rs"))
1579        );
1580    }
1581
1582    #[test]
1583    fn test_external_import_not_supported() {
1584        // anyhow::Result (external crate)
1585        let result = resolve_rust_use_to_path(
1586            "anyhow::Result",
1587            Some("/home/user/project/src/main.rs"),
1588            Some("/home/user/project"),
1589        );
1590
1591        // Should return None for external imports
1592        assert!(result.is_none());
1593    }
1594
1595    #[test]
1596    fn test_stdlib_import_not_supported() {
1597        // std::collections::HashMap (stdlib)
1598        let result = resolve_rust_use_to_path(
1599            "std::collections::HashMap",
1600            Some("/home/user/project/src/main.rs"),
1601            Some("/home/user/project"),
1602        );
1603
1604        // Should return None for stdlib imports
1605        assert!(result.is_none());
1606    }
1607
1608    #[test]
1609    fn test_resolve_without_current_file() {
1610        let result = resolve_rust_use_to_path("crate::models", None, Some("/home/user/project"));
1611
1612        // Should return None if no current file provided
1613        assert!(result.is_none());
1614    }
1615}
1616
1617// ============================================================================
1618// Workspace Support
1619// ============================================================================
1620
1621/// A Rust crate discovered by scanning for Cargo.toml files.
1622#[derive(Debug, Clone)]
1623pub struct RustCrate {
1624    pub name: String,
1625    pub root_path: std::path::PathBuf,
1626}
1627
1628/// Find all Rust crates in the workspace by scanning for Cargo.toml files.
1629///
1630/// Only runs if a root Cargo.toml exists (to avoid wasted work on non-Rust projects).
1631/// Uses the `ignore` crate to respect .gitignore patterns.
1632pub fn parse_all_rust_crates(root: &std::path::Path) -> anyhow::Result<Vec<RustCrate>> {
1633    // Gate: only scan if this looks like a Rust project
1634    if !root.join("Cargo.toml").exists() {
1635        return Ok(Vec::new());
1636    }
1637
1638    let mut crates = Vec::new();
1639    let walker = ignore::WalkBuilder::new(root).git_ignore(true).build();
1640
1641    for entry in walker {
1642        let entry = entry?;
1643        if entry.file_name() == "Cargo.toml" {
1644            let content = std::fs::read_to_string(entry.path())?;
1645            if let Some(name) = extract_crate_name(&content) {
1646                if let Some(crate_root) = entry.path().parent() {
1647                    crates.push(RustCrate {
1648                        name,
1649                        root_path: crate_root.to_path_buf(),
1650                    });
1651                }
1652            }
1653        }
1654    }
1655    Ok(crates)
1656}
1657
1658/// Extract the package name from a Cargo.toml file using the `toml` crate.
1659fn extract_crate_name(content: &str) -> Option<String> {
1660    let table: toml::Table = content.parse().ok()?;
1661    table
1662        .get("package")?
1663        .get("name")?
1664        .as_str()
1665        .map(|s| s.to_string())
1666}
1667
1668/// Reclassify an import based on known workspace crates.
1669///
1670/// If the import path matches a workspace crate name (e.g., `my_crate` or
1671/// `my_crate::module`), reclassify it as Internal. Otherwise, fall back to
1672/// the default Rust import classification.
1673pub fn reclassify_rust_import(path: &str, crates: &[RustCrate]) -> crate::models::ImportType {
1674    for krate in crates {
1675        if path == krate.name || path.starts_with(&format!("{}::", krate.name)) {
1676            return crate::models::ImportType::Internal;
1677        }
1678    }
1679    classify_rust_import(path)
1680}
1681
1682/// Resolve a workspace crate import to a file path.
1683///
1684/// Handles imports like `my_crate::config` by finding the matching workspace
1685/// crate and resolving the module path within its `src/` directory.
1686pub fn resolve_rust_workspace_path(import_path: &str, crates: &[RustCrate]) -> Option<String> {
1687    for krate in crates {
1688        if import_path == krate.name || import_path.starts_with(&format!("{}::", krate.name)) {
1689            let relative_module = if import_path == krate.name {
1690                ""
1691            } else {
1692                import_path
1693                    .strip_prefix(&format!("{}::", krate.name))
1694                    .unwrap_or("")
1695            };
1696
1697            let src_root = krate.root_path.join("src");
1698
1699            if relative_module.is_empty() {
1700                // Bare crate import -> lib.rs or main.rs
1701                let lib = src_root.join("lib.rs");
1702                if lib.exists() {
1703                    return Some(lib.to_string_lossy().to_string());
1704                }
1705                let main = src_root.join("main.rs");
1706                if main.exists() {
1707                    return Some(main.to_string_lossy().to_string());
1708                }
1709            } else {
1710                let parts: Vec<&str> = relative_module.split("::").collect();
1711
1712                // Try resolving as a module file (only accept if the file exists)
1713                if let Some(path) = resolve_rust_module_path(&src_root, &parts) {
1714                    if std::path::Path::new(&path).exists() {
1715                        return Some(path);
1716                    }
1717                }
1718
1719                // Try popping the last component (it may be an item like a struct/fn, not a module)
1720                if parts.len() > 1 {
1721                    if let Some(path) =
1722                        resolve_rust_module_path(&src_root, &parts[..parts.len() - 1])
1723                    {
1724                        if std::path::Path::new(&path).exists() {
1725                            return Some(path);
1726                        }
1727                    }
1728                }
1729
1730                // Return the best-guess path even if it doesn't exist
1731                // (the indexer will validate against its file database)
1732                if let Some(path) = resolve_rust_module_path(&src_root, &parts) {
1733                    return Some(path);
1734                }
1735            }
1736        }
1737    }
1738    None
1739}
1740
1741#[cfg(test)]
1742mod workspace_tests {
1743    use super::*;
1744    use std::fs;
1745    use tempfile::TempDir;
1746
1747    fn create_workspace(dir: &std::path::Path) {
1748        // Root Cargo.toml with workspace
1749        fs::write(
1750            dir.join("Cargo.toml"),
1751            r#"[workspace]
1752members = ["crate_a", "crate_b"]
1753"#,
1754        )
1755        .unwrap();
1756
1757        // crate_a
1758        let crate_a = dir.join("crate_a");
1759        fs::create_dir_all(crate_a.join("src")).unwrap();
1760        fs::write(
1761            crate_a.join("Cargo.toml"),
1762            r#"[package]
1763name = "crate_a"
1764version = "0.1.0"
1765"#,
1766        )
1767        .unwrap();
1768        fs::write(crate_a.join("src/lib.rs"), "pub mod config;").unwrap();
1769        fs::write(crate_a.join("src/config.rs"), "pub struct Config;").unwrap();
1770
1771        // crate_b with inline table syntax
1772        let crate_b = dir.join("crate_b");
1773        fs::create_dir_all(crate_b.join("src")).unwrap();
1774        fs::write(
1775            crate_b.join("Cargo.toml"),
1776            r#"[package]
1777name = "crate_b" # a comment
1778version = "0.1.0"
1779"#,
1780        )
1781        .unwrap();
1782        fs::write(crate_b.join("src/lib.rs"), "").unwrap();
1783    }
1784
1785    #[test]
1786    fn test_extract_crate_name_standard() {
1787        let toml = r#"[package]
1788name = "my_crate"
1789version = "0.1.0"
1790"#;
1791        assert_eq!(extract_crate_name(toml), Some("my_crate".to_string()));
1792    }
1793
1794    #[test]
1795    fn test_extract_crate_name_inline_table() {
1796        // The toml crate handles this correctly
1797        let toml = r#"[package]
1798name = "inline_crate"
1799version = "0.1.0"
1800edition = "2021"
1801"#;
1802        assert_eq!(extract_crate_name(toml), Some("inline_crate".to_string()));
1803    }
1804
1805    #[test]
1806    fn test_extract_crate_name_with_comment() {
1807        let toml = r#"[package]
1808name = "commented_crate" # my crate
1809version = "0.1.0"
1810"#;
1811        assert_eq!(
1812            extract_crate_name(toml),
1813            Some("commented_crate".to_string())
1814        );
1815    }
1816
1817    #[test]
1818    fn test_extract_crate_name_no_package() {
1819        let toml = r#"[workspace]
1820members = ["crate_a"]
1821"#;
1822        assert_eq!(extract_crate_name(toml), None);
1823    }
1824
1825    #[test]
1826    fn test_parse_all_rust_crates() {
1827        let dir = TempDir::new().unwrap();
1828        create_workspace(dir.path());
1829
1830        let crates = parse_all_rust_crates(dir.path()).unwrap();
1831        assert_eq!(crates.len(), 2);
1832
1833        let names: Vec<&str> = crates.iter().map(|c| c.name.as_str()).collect();
1834        assert!(names.contains(&"crate_a"));
1835        assert!(names.contains(&"crate_b"));
1836    }
1837
1838    #[test]
1839    fn test_parse_all_rust_crates_non_rust_project() {
1840        let dir = TempDir::new().unwrap();
1841        // No Cargo.toml -> should return empty
1842        let crates = parse_all_rust_crates(dir.path()).unwrap();
1843        assert!(crates.is_empty());
1844    }
1845
1846    #[test]
1847    fn test_reclassify_rust_import_workspace_crate() {
1848        let crates = vec![RustCrate {
1849            name: "crate_a".to_string(),
1850            root_path: std::path::PathBuf::from("/workspace/crate_a"),
1851        }];
1852
1853        assert!(matches!(
1854            reclassify_rust_import("crate_a", &crates),
1855            crate::models::ImportType::Internal
1856        ));
1857        assert!(matches!(
1858            reclassify_rust_import("crate_a::config", &crates),
1859            crate::models::ImportType::Internal
1860        ));
1861        // External crate should stay External
1862        assert!(matches!(
1863            reclassify_rust_import("serde::Serialize", &crates),
1864            crate::models::ImportType::External
1865        ));
1866    }
1867
1868    #[test]
1869    fn test_resolve_rust_workspace_path() {
1870        let dir = TempDir::new().unwrap();
1871        create_workspace(dir.path());
1872
1873        let crates = parse_all_rust_crates(dir.path()).unwrap();
1874
1875        // Bare crate import -> lib.rs
1876        let result = resolve_rust_workspace_path("crate_a", &crates);
1877        assert!(result.is_some());
1878        assert!(result.unwrap().ends_with("lib.rs"));
1879
1880        // Module import -> config.rs
1881        let result = resolve_rust_workspace_path("crate_a::config", &crates);
1882        assert!(result.is_some());
1883        assert!(result.unwrap().ends_with("config.rs"));
1884
1885        // Item import -> resolves to the module file
1886        let result = resolve_rust_workspace_path("crate_a::config::Config", &crates);
1887        assert!(result.is_some());
1888        assert!(result.unwrap().ends_with("config.rs"));
1889
1890        // Unknown crate -> None
1891        let result = resolve_rust_workspace_path("unknown_crate::foo", &crates);
1892        assert!(result.is_none());
1893    }
1894}