Skip to main content

reflex/parsers/
rust.rs

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