1use 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
22pub 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 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 for symbol in &mut symbols {
56 symbol.path = path.to_string();
57 symbol.lang = Language::Rust;
58 }
59
60 Ok(symbols)
61}
62
63fn 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
77fn 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
91fn 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
105fn 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
119fn extract_impls(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
121 let language = tree_sitter_rust::LANGUAGE;
122
123 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 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(), 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
187fn 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
201fn 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
215fn 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
229fn 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
243fn 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
257fn 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
271fn 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 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 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 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
404fn 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 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 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(), Language::Rust,
439 kind.clone(),
440 Some(name),
441 span,
442 scope.clone(),
443 preview,
444 ));
445 }
446 }
447
448 Ok(symbols)
449}
450
451fn 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, start.column,
459 end.row + 1,
460 end.column,
461 )
462}
463
464fn extract_preview(source: &str, span: &Span) -> String {
466 let lines: Vec<&str> = source.lines().collect();
467
468 let start_idx = (span.start_line - 1) as usize; let end_idx = (start_idx + 7).min(lines.len());
472
473 lines[start_idx..end_idx].join("\n")
474}
475
476pub 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 imports.extend(extract_use_declarations(source, &root_node)?);
498
499 imports.extend(extract_mod_items(source, &root_node)?);
501
502 imports.extend(extract_extern_crates(source, &root_node)?);
504
505 Ok(imports)
506 }
507}
508
509fn 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 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
549fn 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 let has_body = node.child_by_field_name("body").is_some();
585
586 if !has_body {
587 let line_number = node.start_position().row + 1;
589
590 imports.push(ImportInfo {
591 imported_path: name,
592 import_type: crate::models::ImportType::ModDecl,
594 line_number,
595 imported_symbols: None,
596 });
597 }
598 }
599 }
600
601 Ok(imports)
602}
603
604fn 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
653fn 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 ImportType::External
664 }
665}
666
667fn parse_rust_use_declaration(text: &str) -> Vec<(String, Option<Vec<String>>)> {
676 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 if text.contains('{') {
691 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 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 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 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 }
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 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 let variables: Vec<_> = symbols.iter()
875 .filter(|s| matches!(s.kind, SymbolKind::Variable))
876 .collect();
877
878 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 }
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 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 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 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 let macros: Vec<_> = symbols.iter()
943 .filter(|s| matches!(s.kind, SymbolKind::Macro))
944 .collect();
945
946 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 let attributes: Vec<_> = symbols.iter()
975 .filter(|s| matches!(s.kind, SymbolKind::Attribute))
976 .collect();
977
978 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 assert!(!attributes.iter().any(|a| a.symbol.as_deref() == Some("helper")));
984
985 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 let attributes: Vec<_> = symbols.iter()
1019 .filter(|s| matches!(s.kind, SymbolKind::Attribute))
1020 .collect();
1021
1022 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 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 assert_eq!(deps.len(), 4);
1045
1046 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 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 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 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 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 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 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 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 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 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
1145fn 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 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 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 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
1178pub 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 if !import_path.starts_with("crate::")
1196 && !import_path.starts_with("super::")
1197 && !import_path.starts_with("self::") {
1198 if import_path.contains("::") {
1200 return None; }
1202 }
1204
1205 let current_file = current_file_path?;
1206 let current_path = std::path::Path::new(current_file);
1207
1208 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 let module_path = import_path.strip_prefix("crate::").unwrap();
1215 let parts: Vec<&str> = module_path.split("::").collect();
1216
1217 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 let module_path = import_path.strip_prefix("super::").unwrap();
1223 let parts: Vec<&str> = module_path.split("::").collect();
1224
1225 let current_dir = if current_path.file_name().unwrap() == "mod.rs" {
1227 current_path.parent()?.parent()?
1229 } else {
1230 current_path.parent()?
1232 };
1233
1234 resolve_rust_module_path(current_dir, &parts)
1235 } else if import_path.starts_with("self::") {
1236 let module_path = import_path.strip_prefix("self::").unwrap();
1238 let parts: Vec<&str> = module_path.split("::").collect();
1239
1240 let current_dir = if current_path.file_name().unwrap() == "mod.rs" {
1242 current_path.parent()?
1244 } else {
1245 current_path.parent()?
1247 };
1248
1249 resolve_rust_module_path(current_dir, &parts)
1250 } else {
1251 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 Some(module_file.to_string_lossy().to_string())
1265 }
1266 }
1267}
1268
1269fn resolve_rust_module_path(base_dir: &std::path::Path, parts: &[&str]) -> Option<String> {
1275 if parts.is_empty() {
1276 return None;
1277 }
1278
1279 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 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 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 Some(file_path.to_string_lossy().to_string());
1299 }
1300 } else {
1301 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 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 assert!(path.contains("models.rs") || path.contains("models/mod.rs"));
1326 }
1327
1328 #[test]
1329 fn test_resolve_super_import() {
1330 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 assert!(path.contains("src") && path.contains("utils.rs"));
1341 }
1342
1343 #[test]
1344 fn test_resolve_self_import() {
1345 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 assert!(path.contains("models") && path.contains("helper.rs"));
1356 }
1357
1358 #[test]
1359 fn test_resolve_mod_declaration() {
1360 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 assert!(path.contains("parser.rs"));
1371 }
1372
1373 #[test]
1374 fn test_resolve_nested_crate_import() {
1375 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 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 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 assert!(result.is_none());
1399 }
1400
1401 #[test]
1402 fn test_stdlib_import_not_supported() {
1403 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 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 assert!(result.is_none());
1424 }
1425}
1426
1427#[derive(Debug, Clone)]
1433pub struct RustCrate {
1434 pub name: String,
1435 pub root_path: std::path::PathBuf,
1436}
1437
1438pub fn parse_all_rust_crates(root: &std::path::Path) -> anyhow::Result<Vec<RustCrate>> {
1443 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
1470fn 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
1480pub 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
1497pub 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 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 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 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 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 fs::write(
1564 dir.join("Cargo.toml"),
1565 r#"[workspace]
1566members = ["crate_a", "crate_b"]
1567"#,
1568 ).unwrap();
1569
1570 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 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 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 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 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 let result = resolve_rust_workspace_path("crate_a", &crates);
1687 assert!(result.is_some());
1688 assert!(result.unwrap().ends_with("lib.rs"));
1689
1690 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 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 let result = resolve_rust_workspace_path("unknown_crate::foo", &crates);
1702 assert!(result.is_none());
1703 }
1704}