1use crate::models::{Language, SearchResult, Span, SymbolKind};
17use crate::parsers::{DependencyExtractor, ImportInfo};
18use anyhow::{Context, Result};
19use streaming_iterator::StreamingIterator;
20use tree_sitter::{Parser, Query, QueryCursor};
21
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 for symbol in &mut symbols {
55 symbol.path = path.to_string();
56 symbol.lang = Language::Rust;
57 }
58
59 Ok(symbols)
60}
61
62fn extract_functions(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
64 let language = tree_sitter_rust::LANGUAGE;
65 let query_str = r#"
66 (function_item
67 name: (identifier) @name) @function
68 "#;
69
70 let query =
71 Query::new(&language.into(), query_str).context("Failed to create function query")?;
72
73 extract_symbols(source, root, &query, SymbolKind::Function, None)
74}
75
76fn extract_structs(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
78 let language = tree_sitter_rust::LANGUAGE;
79 let query_str = r#"
80 (struct_item
81 name: (type_identifier) @name) @struct
82 "#;
83
84 let query = Query::new(&language.into(), query_str).context("Failed to create struct query")?;
85
86 extract_symbols(source, root, &query, SymbolKind::Struct, None)
87}
88
89fn extract_enums(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
91 let language = tree_sitter_rust::LANGUAGE;
92 let query_str = r#"
93 (enum_item
94 name: (type_identifier) @name) @enum
95 "#;
96
97 let query = Query::new(&language.into(), query_str).context("Failed to create enum query")?;
98
99 extract_symbols(source, root, &query, SymbolKind::Enum, None)
100}
101
102fn extract_traits(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
104 let language = tree_sitter_rust::LANGUAGE;
105 let query_str = r#"
106 (trait_item
107 name: (type_identifier) @name) @trait
108 "#;
109
110 let query = Query::new(&language.into(), query_str).context("Failed to create trait query")?;
111
112 extract_symbols(source, root, &query, SymbolKind::Trait, None)
113}
114
115fn extract_impls(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
117 let language = tree_sitter_rust::LANGUAGE;
118
119 let query_str = r#"
121 (impl_item
122 type: (type_identifier) @impl_name
123 body: (declaration_list
124 (function_item
125 name: (identifier) @method_name))) @impl
126 "#;
127
128 let query = Query::new(&language.into(), query_str).context("Failed to create impl query")?;
129
130 let mut cursor = QueryCursor::new();
131 let mut matches = cursor.matches(&query, *root, source.as_bytes());
132
133 let mut symbols = Vec::new();
134
135 while let Some(match_) = matches.next() {
136 let mut impl_name = None;
137 let mut method_name = None;
138 let mut method_node = None;
139
140 for capture in match_.captures {
141 let capture_name: &str = &query.capture_names()[capture.index as usize];
142 match capture_name {
143 "impl_name" => {
144 impl_name = Some(
145 capture
146 .node
147 .utf8_text(source.as_bytes())
148 .unwrap_or("")
149 .to_string(),
150 );
151 }
152 "method_name" => {
153 method_name = Some(
154 capture
155 .node
156 .utf8_text(source.as_bytes())
157 .unwrap_or("")
158 .to_string(),
159 );
160 let mut current = capture.node;
162 while let Some(parent) = current.parent() {
163 if parent.kind() == "function_item" {
164 method_node = Some(parent);
165 break;
166 }
167 current = parent;
168 }
169 }
170 _ => {}
171 }
172 }
173
174 if let (Some(impl_name), Some(method_name), Some(node)) =
175 (impl_name, method_name, method_node)
176 {
177 let scope = format!("impl {}", impl_name);
178 let span = node_to_span(&node);
179 let preview = extract_preview(source, &span);
180
181 symbols.push(SearchResult::new(
182 String::new(), Language::Rust,
184 SymbolKind::Method,
185 Some(method_name),
186 span,
187 Some(scope),
188 preview,
189 ));
190 }
191 }
192
193 Ok(symbols)
194}
195
196fn extract_constants(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
198 let language = tree_sitter_rust::LANGUAGE;
199 let query_str = r#"
200 (const_item
201 name: (identifier) @name) @const
202 "#;
203
204 let query = Query::new(&language.into(), query_str).context("Failed to create const query")?;
205
206 extract_symbols(source, root, &query, SymbolKind::Constant, None)
207}
208
209fn extract_statics(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
211 let language = tree_sitter_rust::LANGUAGE;
212 let query_str = r#"
213 (static_item
214 name: (identifier) @name) @static
215 "#;
216
217 let query = Query::new(&language.into(), query_str).context("Failed to create static query")?;
218
219 extract_symbols(source, root, &query, SymbolKind::Variable, None)
220}
221
222fn extract_local_variables(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
224 let language = tree_sitter_rust::LANGUAGE;
225 let query_str = r#"
226 (let_declaration
227 pattern: (identifier) @name) @let
228 "#;
229
230 let query = Query::new(&language.into(), query_str)
231 .context("Failed to create let declaration query")?;
232
233 extract_symbols(source, root, &query, SymbolKind::Variable, None)
234}
235
236fn extract_modules(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
238 let language = tree_sitter_rust::LANGUAGE;
239 let query_str = r#"
240 (mod_item
241 name: (identifier) @name) @module
242 "#;
243
244 let query = Query::new(&language.into(), query_str).context("Failed to create module query")?;
245
246 extract_symbols(source, root, &query, SymbolKind::Module, None)
247}
248
249fn extract_type_aliases(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
251 let language = tree_sitter_rust::LANGUAGE;
252 let query_str = r#"
253 (type_item
254 name: (type_identifier) @name) @type
255 "#;
256
257 let query = Query::new(&language.into(), query_str).context("Failed to create type query")?;
258
259 extract_symbols(source, root, &query, SymbolKind::Type, None)
260}
261
262fn extract_macros(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
264 let language = tree_sitter_rust::LANGUAGE;
265 let query_str = r#"
266 (macro_definition
267 name: (identifier) @name) @macro
268 "#;
269
270 let query = Query::new(&language.into(), query_str).context("Failed to create macro query")?;
271
272 extract_symbols(source, root, &query, SymbolKind::Macro, None)
273}
274
275fn extract_attributes(source: &str, root: &tree_sitter::Node) -> Result<Vec<SearchResult>> {
279 let language = tree_sitter_rust::LANGUAGE;
280 let mut symbols = Vec::new();
281
282 let func_query_str = r#"
284 (function_item
285 name: (identifier) @name) @function
286 "#;
287
288 let func_query =
289 Query::new(&language.into(), func_query_str).context("Failed to create function query")?;
290
291 let mut cursor = QueryCursor::new();
292 let mut matches = cursor.matches(&func_query, *root, source.as_bytes());
293
294 while let Some(match_) = matches.next() {
295 let mut name = None;
296 let mut func_node = None;
297
298 for capture in match_.captures {
299 let capture_name: &str = &func_query.capture_names()[capture.index as usize];
300 match capture_name {
301 "name" => {
302 name = Some(
303 capture
304 .node
305 .utf8_text(source.as_bytes())
306 .unwrap_or("")
307 .to_string(),
308 );
309 }
310 "function" => {
311 func_node = Some(capture.node);
312 }
313 _ => {}
314 }
315 }
316
317 if let (Some(name), Some(func_node)) = (name, func_node) {
319 let mut has_proc_macro_attr = false;
320
321 if let Some(parent) = func_node.parent() {
322 let mut func_index = None;
323 for i in 0..parent.child_count() {
324 if let Some(child) = parent.child(i as u32) {
325 if child.id() == func_node.id() {
326 func_index = Some(i);
327 break;
328 }
329 }
330 }
331
332 if let Some(func_idx) = func_index {
333 for i in (0..func_idx).rev() {
334 if let Some(child) = parent.child(i as u32) {
335 if child.kind() == "attribute_item" {
336 let attr_text = child.utf8_text(source.as_bytes()).unwrap_or("");
337 if attr_text.contains("proc_macro_attribute") {
338 has_proc_macro_attr = true;
339 }
340 } else if !child.kind().contains("comment")
341 && child.kind() != "line_comment"
342 {
343 break;
344 }
345 }
346 }
347 }
348 }
349
350 if has_proc_macro_attr {
351 let span = node_to_span(&func_node);
352 let preview = extract_preview(source, &span);
353
354 symbols.push(SearchResult::new(
355 String::new(),
356 Language::Rust,
357 SymbolKind::Attribute,
358 Some(name),
359 span,
360 None,
361 preview,
362 ));
363 }
364 }
365 }
366
367 let attr_query_str = r#"
369 (attribute_item
370 (attribute
371 (identifier) @attr_name)) @attr
372 "#;
373
374 let attr_query = Query::new(&language.into(), attr_query_str)
375 .context("Failed to create attribute use query")?;
376
377 let mut cursor = QueryCursor::new();
378 let mut matches = cursor.matches(&attr_query, *root, source.as_bytes());
379
380 while let Some(match_) = matches.next() {
381 let mut attr_name = None;
382 let mut attr_node = None;
383
384 for capture in match_.captures {
385 let capture_name: &str = &attr_query.capture_names()[capture.index as usize];
386 match capture_name {
387 "attr_name" => {
388 attr_name = Some(
389 capture
390 .node
391 .utf8_text(source.as_bytes())
392 .unwrap_or("")
393 .to_string(),
394 );
395 }
396 "attr" => {
397 attr_node = Some(capture.node);
398 }
399 _ => {}
400 }
401 }
402
403 if let (Some(name), Some(node)) = (attr_name, attr_node) {
404 let span = node_to_span(&node);
405 let preview = extract_preview(source, &span);
406
407 symbols.push(SearchResult::new(
408 String::new(),
409 Language::Rust,
410 SymbolKind::Attribute,
411 Some(name),
412 span,
413 None,
414 preview,
415 ));
416 }
417 }
418
419 Ok(symbols)
420}
421
422fn extract_symbols(
424 source: &str,
425 root: &tree_sitter::Node,
426 query: &Query,
427 kind: SymbolKind,
428 scope: Option<String>,
429) -> Result<Vec<SearchResult>> {
430 let mut cursor = QueryCursor::new();
431 let mut matches = cursor.matches(query, *root, source.as_bytes());
432
433 let mut symbols = Vec::new();
434
435 while let Some(match_) = matches.next() {
436 let mut name = None;
438 let mut full_node = None;
439
440 for capture in match_.captures {
441 let capture_name: &str = &query.capture_names()[capture.index as usize];
442 if capture_name == "name" {
443 name = Some(
444 capture
445 .node
446 .utf8_text(source.as_bytes())
447 .unwrap_or("")
448 .to_string(),
449 );
450 } else {
451 full_node = Some(capture.node);
453 }
454 }
455
456 if let (Some(name), Some(node)) = (name, full_node) {
457 let span = node_to_span(&node);
458 let preview = extract_preview(source, &span);
459
460 symbols.push(SearchResult::new(
461 String::new(), Language::Rust,
463 kind.clone(),
464 Some(name),
465 span,
466 scope.clone(),
467 preview,
468 ));
469 }
470 }
471
472 Ok(symbols)
473}
474
475fn node_to_span(node: &tree_sitter::Node) -> Span {
477 let start = node.start_position();
478 let end = node.end_position();
479
480 Span::new(
481 start.row + 1, start.column,
483 end.row + 1,
484 end.column,
485 )
486}
487
488fn extract_preview(source: &str, span: &Span) -> String {
490 let lines: Vec<&str> = source.lines().collect();
491
492 let start_idx = (span.start_line - 1) as usize; let end_idx = (start_idx + 7).min(lines.len());
496
497 lines[start_idx..end_idx].join("\n")
498}
499
500pub struct RustDependencyExtractor;
502
503impl DependencyExtractor for RustDependencyExtractor {
504 fn extract_dependencies(source: &str) -> Result<Vec<ImportInfo>> {
505 let mut parser = Parser::new();
506 let language = tree_sitter_rust::LANGUAGE;
507
508 parser
509 .set_language(&language.into())
510 .context("Failed to set Rust language")?;
511
512 let tree = parser
513 .parse(source, None)
514 .context("Failed to parse Rust source")?;
515
516 let root_node = tree.root_node();
517
518 let mut imports = Vec::new();
519
520 imports.extend(extract_use_declarations(source, &root_node)?);
522
523 imports.extend(extract_mod_items(source, &root_node)?);
525
526 imports.extend(extract_extern_crates(source, &root_node)?);
528
529 Ok(imports)
530 }
531}
532
533fn extract_use_declarations(source: &str, root: &tree_sitter::Node) -> Result<Vec<ImportInfo>> {
535 let language = tree_sitter_rust::LANGUAGE;
536 let query_str = r#"
537 (use_declaration) @use
538 "#;
539
540 let query = Query::new(&language.into(), query_str)
541 .context("Failed to create use declaration query")?;
542
543 let mut cursor = QueryCursor::new();
544 let mut matches = cursor.matches(&query, *root, source.as_bytes());
545
546 let mut imports = Vec::new();
547
548 while let Some(match_) = matches.next() {
549 for capture in match_.captures {
550 let node = capture.node;
551 let text = node.utf8_text(source.as_bytes()).unwrap_or("");
552 let line_number = node.start_position().row + 1;
553
554 let path_info = parse_rust_use_declaration(text);
556
557 for (path, symbols) in path_info {
558 let import_type = classify_rust_import(&path);
559
560 imports.push(ImportInfo {
561 imported_path: path,
562 import_type,
563 line_number,
564 imported_symbols: symbols,
565 });
566 }
567 }
568 }
569
570 Ok(imports)
571}
572
573fn extract_mod_items(source: &str, root: &tree_sitter::Node) -> Result<Vec<ImportInfo>> {
575 let language = tree_sitter_rust::LANGUAGE;
576 let query_str = r#"
577 (mod_item
578 name: (identifier) @name) @mod
579 "#;
580
581 let query =
582 Query::new(&language.into(), query_str).context("Failed to create mod item query")?;
583
584 let mut cursor = QueryCursor::new();
585 let mut matches = cursor.matches(&query, *root, source.as_bytes());
586
587 let mut imports = Vec::new();
588
589 while let Some(match_) = matches.next() {
590 let mut name = None;
591 let mut mod_node = None;
592
593 for capture in match_.captures {
594 let capture_name: &str = &query.capture_names()[capture.index as usize];
595 match capture_name {
596 "name" => {
597 name = Some(
598 capture
599 .node
600 .utf8_text(source.as_bytes())
601 .unwrap_or("")
602 .to_string(),
603 );
604 }
605 "mod" => {
606 mod_node = Some(capture.node);
607 }
608 _ => {}
609 }
610 }
611
612 if let (Some(name), Some(node)) = (name, mod_node) {
613 let has_body = node.child_by_field_name("body").is_some();
615
616 if !has_body {
617 let line_number = node.start_position().row + 1;
619
620 imports.push(ImportInfo {
621 imported_path: name,
622 import_type: crate::models::ImportType::ModDecl,
624 line_number,
625 imported_symbols: None,
626 });
627 }
628 }
629 }
630
631 Ok(imports)
632}
633
634fn extract_extern_crates(source: &str, root: &tree_sitter::Node) -> Result<Vec<ImportInfo>> {
636 let language = tree_sitter_rust::LANGUAGE;
637 let query_str = r#"
638 (extern_crate_declaration
639 name: (identifier) @name) @extern
640 "#;
641
642 let query =
643 Query::new(&language.into(), query_str).context("Failed to create extern crate query")?;
644
645 let mut cursor = QueryCursor::new();
646 let mut matches = cursor.matches(&query, *root, source.as_bytes());
647
648 let mut imports = Vec::new();
649
650 while let Some(match_) = matches.next() {
651 let mut name = None;
652 let mut extern_node = None;
653
654 for capture in match_.captures {
655 let capture_name: &str = &query.capture_names()[capture.index as usize];
656 match capture_name {
657 "name" => {
658 name = Some(
659 capture
660 .node
661 .utf8_text(source.as_bytes())
662 .unwrap_or("")
663 .to_string(),
664 );
665 }
666 "extern" => {
667 extern_node = Some(capture.node);
668 }
669 _ => {}
670 }
671 }
672
673 if let (Some(name), Some(node)) = (name, extern_node) {
674 let line_number = node.start_position().row + 1;
675 let import_type = classify_rust_import(&name);
676
677 imports.push(ImportInfo {
678 imported_path: name,
679 import_type,
680 line_number,
681 imported_symbols: None,
682 });
683 }
684 }
685
686 Ok(imports)
687}
688
689fn classify_rust_import(path: &str) -> crate::models::ImportType {
691 use crate::models::ImportType;
692
693 if path.starts_with("std::") || path.starts_with("core::") || path.starts_with("alloc::") {
694 ImportType::Stdlib
695 } else if path.starts_with("crate::")
696 || path.starts_with("super::")
697 || path.starts_with("self::")
698 {
699 ImportType::Internal
700 } else {
701 ImportType::External
703 }
704}
705
706fn parse_rust_use_declaration(text: &str) -> Vec<(String, Option<Vec<String>>)> {
715 let text = text
717 .trim()
718 .strip_prefix("pub(crate)")
719 .unwrap_or(text)
720 .trim()
721 .strip_prefix("pub(super)")
722 .unwrap_or(text)
723 .trim()
724 .strip_prefix("pub")
725 .unwrap_or(text)
726 .trim()
727 .strip_prefix("use")
728 .unwrap_or(text)
729 .trim()
730 .strip_suffix(";")
731 .unwrap_or(text)
732 .trim();
733
734 if text.contains('{') {
736 if let Some(idx) = text.find('{') {
738 let base_path = text[..idx].trim_end_matches("::").to_string();
739
740 if let Some(end) = text.find('}') {
741 let symbols_str = &text[idx + 1..end];
742 let symbols: Vec<String> = symbols_str
743 .split(',')
744 .map(|s| {
745 let trimmed = s.trim();
747 if let Some(as_idx) = trimmed.find(" as ") {
748 trimmed[..as_idx].trim().to_string()
749 } else {
750 trimmed.to_string()
751 }
752 })
753 .filter(|s| !s.is_empty() && s != "*")
754 .collect();
755
756 if !symbols.is_empty() {
757 return vec![(base_path, Some(symbols))];
758 }
759 }
760 }
761 }
762
763 let path = if let Some(as_idx) = text.find(" as ") {
765 text[..as_idx].trim().to_string()
766 } else {
767 text.to_string()
768 };
769
770 vec![(path, None)]
771}
772
773#[cfg(test)]
774mod tests {
775 use super::*;
776
777 #[test]
778 fn test_parse_function() {
779 let source = r#"
780 fn hello_world() {
781 println!("Hello, world!");
782 }
783 "#;
784
785 let symbols = parse("test.rs", source).unwrap();
786 assert_eq!(symbols.len(), 1);
787 assert_eq!(symbols[0].symbol.as_deref(), Some("hello_world"));
788 assert!(matches!(symbols[0].kind, SymbolKind::Function));
789 }
790
791 #[test]
792 fn test_parse_struct() {
793 let source = r#"
794 struct User {
795 name: String,
796 age: u32,
797 }
798 "#;
799
800 let symbols = parse("test.rs", source).unwrap();
801 assert_eq!(symbols.len(), 1);
802 assert_eq!(symbols[0].symbol.as_deref(), Some("User"));
803 assert!(matches!(symbols[0].kind, SymbolKind::Struct));
804 }
805
806 #[test]
807 fn test_parse_impl() {
808 let source = r#"
809 struct User {
810 name: String,
811 }
812
813 impl User {
814 fn new(name: String) -> Self {
815 User { name }
816 }
817
818 fn get_name(&self) -> &str {
819 &self.name
820 }
821 }
822 "#;
823
824 let symbols = parse("test.rs", source).unwrap();
825
826 assert!(symbols.len() >= 3);
828
829 let method_symbols: Vec<_> = symbols
830 .iter()
831 .filter(|s| matches!(s.kind, SymbolKind::Method))
832 .collect();
833
834 assert_eq!(method_symbols.len(), 2);
835 assert!(
836 method_symbols
837 .iter()
838 .any(|s| s.symbol.as_deref() == Some("new"))
839 );
840 assert!(
841 method_symbols
842 .iter()
843 .any(|s| s.symbol.as_deref() == Some("get_name"))
844 );
845
846 }
849
850 #[test]
851 fn test_parse_enum() {
852 let source = r#"
853 enum Status {
854 Active,
855 Inactive,
856 }
857 "#;
858
859 let symbols = parse("test.rs", source).unwrap();
860 assert_eq!(symbols.len(), 1);
861 assert_eq!(symbols[0].symbol.as_deref(), Some("Status"));
862 assert!(matches!(symbols[0].kind, SymbolKind::Enum));
863 }
864
865 #[test]
866 fn test_parse_trait() {
867 let source = r#"
868 trait Drawable {
869 fn draw(&self);
870 }
871 "#;
872
873 let symbols = parse("test.rs", source).unwrap();
874 assert_eq!(symbols.len(), 1);
875 assert_eq!(symbols[0].symbol.as_deref(), Some("Drawable"));
876 assert!(matches!(symbols[0].kind, SymbolKind::Trait));
877 }
878
879 #[test]
880 fn test_parse_multiple_symbols() {
881 let source = r#"
882 const MAX_SIZE: usize = 100;
883
884 struct Config {
885 size: usize,
886 }
887
888 fn create_config() -> Config {
889 Config { size: MAX_SIZE }
890 }
891 "#;
892
893 let symbols = parse("test.rs", source).unwrap();
894
895 assert_eq!(symbols.len(), 3);
897
898 let kinds: Vec<&SymbolKind> = symbols.iter().map(|s| &s.kind).collect();
899 assert!(kinds.contains(&&SymbolKind::Constant));
900 assert!(kinds.contains(&&SymbolKind::Struct));
901 assert!(kinds.contains(&&SymbolKind::Function));
902 }
903
904 #[test]
905 fn test_local_variables_included() {
906 let source = r#"
907 fn calculate(input: i32) -> i32 {
908 let local_var = input * 2;
909 let result = local_var + 10;
910 result
911 }
912
913 struct Calculator;
914
915 impl Calculator {
916 fn compute(&self, value: i32) -> i32 {
917 let temp = value * 3;
918 let mut final_value = temp + 5;
919 final_value += 1;
920 final_value
921 }
922 }
923 "#;
924
925 let symbols = parse("test.rs", source).unwrap();
926
927 let variables: Vec<_> = symbols
929 .iter()
930 .filter(|s| matches!(s.kind, SymbolKind::Variable))
931 .collect();
932
933 assert!(
935 variables
936 .iter()
937 .any(|v| v.symbol.as_deref() == Some("local_var"))
938 );
939 assert!(
940 variables
941 .iter()
942 .any(|v| v.symbol.as_deref() == Some("result"))
943 );
944 assert!(
945 variables
946 .iter()
947 .any(|v| v.symbol.as_deref() == Some("temp"))
948 );
949 assert!(
950 variables
951 .iter()
952 .any(|v| v.symbol.as_deref() == Some("final_value"))
953 );
954
955 }
957
958 #[test]
959 fn test_static_variables() {
960 let source = r#"
961 static GLOBAL_COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
962 static mut MUTABLE_GLOBAL: i32 = 0;
963
964 const MAX_SIZE: usize = 100;
965
966 fn increment() {
967 GLOBAL_COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
968 }
969 "#;
970
971 let symbols = parse("test.rs", source).unwrap();
972
973 let statics: Vec<_> = symbols
975 .iter()
976 .filter(|s| matches!(s.kind, SymbolKind::Variable))
977 .collect();
978
979 let constants: Vec<_> = symbols
980 .iter()
981 .filter(|s| matches!(s.kind, SymbolKind::Constant))
982 .collect();
983
984 assert!(
986 statics
987 .iter()
988 .any(|v| v.symbol.as_deref() == Some("GLOBAL_COUNTER"))
989 );
990 assert!(
991 statics
992 .iter()
993 .any(|v| v.symbol.as_deref() == Some("MUTABLE_GLOBAL"))
994 );
995
996 assert!(
998 constants
999 .iter()
1000 .any(|c| c.symbol.as_deref() == Some("MAX_SIZE"))
1001 );
1002 }
1003
1004 #[test]
1005 fn test_macros() {
1006 let source = r#"
1007 macro_rules! say_hello {
1008 () => {
1009 println!("Hello!");
1010 };
1011 }
1012
1013 macro_rules! vec_of_strings {
1014 ($($x:expr),*) => {
1015 vec![$($x.to_string()),*]
1016 };
1017 }
1018
1019 fn main() {
1020 say_hello!();
1021 }
1022 "#;
1023
1024 let symbols = parse("test.rs", source).unwrap();
1025
1026 let macros: Vec<_> = symbols
1028 .iter()
1029 .filter(|s| matches!(s.kind, SymbolKind::Macro))
1030 .collect();
1031
1032 assert!(
1034 macros
1035 .iter()
1036 .any(|m| m.symbol.as_deref() == Some("say_hello"))
1037 );
1038 assert!(
1039 macros
1040 .iter()
1041 .any(|m| m.symbol.as_deref() == Some("vec_of_strings"))
1042 );
1043 assert_eq!(macros.len(), 2);
1044 }
1045
1046 #[test]
1047 fn test_attribute_proc_macros() {
1048 let source = r#"
1049 use proc_macro::TokenStream;
1050
1051 #[proc_macro_attribute]
1052 pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
1053 item
1054 }
1055
1056 #[proc_macro_attribute]
1057 pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
1058 item
1059 }
1060
1061 // Regular function - should NOT be captured
1062 pub fn helper() {}
1063 "#;
1064
1065 let symbols = parse("test.rs", source).unwrap();
1066
1067 let attributes: Vec<_> = symbols
1069 .iter()
1070 .filter(|s| matches!(s.kind, SymbolKind::Attribute))
1071 .collect();
1072
1073 assert!(
1075 attributes
1076 .iter()
1077 .any(|a| a.symbol.as_deref() == Some("test"))
1078 );
1079 assert!(
1080 attributes
1081 .iter()
1082 .any(|a| a.symbol.as_deref() == Some("route"))
1083 );
1084
1085 assert!(
1087 !attributes
1088 .iter()
1089 .any(|a| a.symbol.as_deref() == Some("helper"))
1090 );
1091
1092 assert_eq!(attributes.len(), 4);
1094 }
1095
1096 #[test]
1097 fn test_attribute_uses() {
1098 let source = r#"
1099 #[test]
1100 fn test_something() {
1101 assert_eq!(1, 1);
1102 }
1103
1104 #[test]
1105 #[should_panic]
1106 fn test_panic() {
1107 panic!("expected");
1108 }
1109
1110 #[derive(Debug, Clone)]
1111 struct MyStruct {
1112 field: i32
1113 }
1114
1115 #[cfg(test)]
1116 mod tests {
1117 #[test]
1118 fn nested_test() {}
1119 }
1120 "#;
1121
1122 let symbols = parse("test.rs", source).unwrap();
1123
1124 let attributes: Vec<_> = symbols
1126 .iter()
1127 .filter(|s| matches!(s.kind, SymbolKind::Attribute))
1128 .collect();
1129
1130 assert!(
1132 attributes
1133 .iter()
1134 .any(|a| a.symbol.as_deref() == Some("test"))
1135 );
1136 assert!(
1137 attributes
1138 .iter()
1139 .any(|a| a.symbol.as_deref() == Some("should_panic"))
1140 );
1141 assert!(
1142 attributes
1143 .iter()
1144 .any(|a| a.symbol.as_deref() == Some("derive"))
1145 );
1146 assert!(
1147 attributes
1148 .iter()
1149 .any(|a| a.symbol.as_deref() == Some("cfg"))
1150 );
1151
1152 assert_eq!(attributes.len(), 6);
1154 }
1155
1156 #[test]
1157 fn test_extract_dependencies_use_declarations() {
1158 let source = r#"
1159 use std::collections::HashMap;
1160 use crate::models::{Language, SearchResult};
1161 use super::utils;
1162 use anyhow::Result;
1163 "#;
1164
1165 let deps = RustDependencyExtractor::extract_dependencies(source).unwrap();
1166
1167 assert_eq!(deps.len(), 4);
1169
1170 let std_import = deps
1172 .iter()
1173 .find(|d| d.imported_path == "std::collections::HashMap")
1174 .unwrap();
1175 assert!(matches!(
1176 std_import.import_type,
1177 crate::models::ImportType::Stdlib
1178 ));
1179
1180 let crate_import = deps
1182 .iter()
1183 .find(|d| d.imported_path == "crate::models")
1184 .unwrap();
1185 assert!(matches!(
1186 crate_import.import_type,
1187 crate::models::ImportType::Internal
1188 ));
1189 assert!(crate_import.imported_symbols.is_some());
1190 let symbols = crate_import.imported_symbols.as_ref().unwrap();
1191 assert_eq!(symbols.len(), 2);
1192 assert!(symbols.contains(&"Language".to_string()));
1193 assert!(symbols.contains(&"SearchResult".to_string()));
1194
1195 let super_import = deps
1197 .iter()
1198 .find(|d| d.imported_path == "super::utils")
1199 .unwrap();
1200 assert!(matches!(
1201 super_import.import_type,
1202 crate::models::ImportType::Internal
1203 ));
1204
1205 let external_import = deps
1207 .iter()
1208 .find(|d| d.imported_path == "anyhow::Result")
1209 .unwrap();
1210 assert!(matches!(
1211 external_import.import_type,
1212 crate::models::ImportType::External
1213 ));
1214 }
1215
1216 #[test]
1217 fn test_extract_dependencies_mod_declarations() {
1218 let source = r#"
1219 mod parser;
1220 mod utils;
1221
1222 mod inline {
1223 fn test() {}
1224 }
1225 "#;
1226
1227 let deps = RustDependencyExtractor::extract_dependencies(source).unwrap();
1228
1229 assert_eq!(deps.len(), 2);
1231 assert!(deps.iter().any(|d| d.imported_path == "parser"));
1232 assert!(deps.iter().any(|d| d.imported_path == "utils"));
1233 assert!(
1234 deps.iter()
1235 .all(|d| matches!(d.import_type, crate::models::ImportType::ModDecl))
1236 );
1237 }
1238
1239 #[test]
1240 fn test_extract_dependencies_extern_crate() {
1241 let source = r#"
1242 extern crate serde;
1243 extern crate serde_json;
1244 "#;
1245
1246 let deps = RustDependencyExtractor::extract_dependencies(source).unwrap();
1247
1248 assert_eq!(deps.len(), 2);
1250 assert!(deps.iter().any(|d| d.imported_path == "serde"));
1251 assert!(deps.iter().any(|d| d.imported_path == "serde_json"));
1252 assert!(
1253 deps.iter()
1254 .all(|d| matches!(d.import_type, crate::models::ImportType::External))
1255 );
1256 }
1257
1258 #[test]
1259 fn test_parse_use_with_aliases() {
1260 let source = r#"
1261 use std::io::Result as IoResult;
1262 use std::collections::{HashMap as Map, HashSet};
1263 "#;
1264
1265 let deps = RustDependencyExtractor::extract_dependencies(source).unwrap();
1266
1267 let io_import = deps
1269 .iter()
1270 .find(|d| d.imported_path == "std::io::Result")
1271 .unwrap();
1272 assert!(matches!(
1273 io_import.import_type,
1274 crate::models::ImportType::Stdlib
1275 ));
1276
1277 let collections_import = deps
1278 .iter()
1279 .find(|d| d.imported_path == "std::collections")
1280 .unwrap();
1281 let symbols = collections_import.imported_symbols.as_ref().unwrap();
1282 assert_eq!(symbols.len(), 2);
1283 assert!(symbols.contains(&"HashMap".to_string()));
1284 assert!(symbols.contains(&"HashSet".to_string()));
1285 }
1286
1287 #[test]
1288 fn test_classify_rust_imports() {
1289 use crate::models::ImportType;
1290
1291 assert!(matches!(
1293 classify_rust_import("std::collections::HashMap"),
1294 ImportType::Stdlib
1295 ));
1296 assert!(matches!(
1297 classify_rust_import("core::ptr"),
1298 ImportType::Stdlib
1299 ));
1300 assert!(matches!(
1301 classify_rust_import("alloc::vec::Vec"),
1302 ImportType::Stdlib
1303 ));
1304
1305 assert!(matches!(
1307 classify_rust_import("crate::models::Language"),
1308 ImportType::Internal
1309 ));
1310 assert!(matches!(
1311 classify_rust_import("super::utils"),
1312 ImportType::Internal
1313 ));
1314 assert!(matches!(
1315 classify_rust_import("self::helper"),
1316 ImportType::Internal
1317 ));
1318
1319 assert!(matches!(
1321 classify_rust_import("serde::Serialize"),
1322 ImportType::External
1323 ));
1324 assert!(matches!(
1325 classify_rust_import("anyhow::Result"),
1326 ImportType::External
1327 ));
1328 assert!(matches!(
1329 classify_rust_import("tokio::runtime"),
1330 ImportType::External
1331 ));
1332 }
1333}
1334
1335fn find_crate_root(start_path: &str) -> Option<String> {
1341 let path = std::path::Path::new(start_path);
1342 let mut current = path.parent()?;
1343
1344 loop {
1346 let cargo_toml = current.join("Cargo.toml");
1347 if cargo_toml.exists() {
1348 return Some(current.to_string_lossy().to_string());
1349 }
1350
1351 if current.ends_with("src") {
1354 if let Some(parent) = current.parent() {
1355 return Some(parent.to_string_lossy().to_string());
1356 }
1357 }
1358
1359 current = match current.parent() {
1361 Some(p) if p.as_os_str().is_empty() => return None,
1362 Some(p) => p,
1363 None => return None,
1364 };
1365 }
1366}
1367
1368pub fn resolve_rust_use_to_path(
1380 import_path: &str,
1381 current_file_path: Option<&str>,
1382 _project_root: Option<&str>,
1383) -> Option<String> {
1384 if !import_path.starts_with("crate::")
1386 && !import_path.starts_with("super::")
1387 && !import_path.starts_with("self::")
1388 {
1389 if import_path.contains("::") {
1391 return None; }
1393 }
1395
1396 let current_file = current_file_path?;
1397 let current_path = std::path::Path::new(current_file);
1398
1399 let crate_root = find_crate_root(current_file)?;
1401 let crate_root_path = std::path::Path::new(&crate_root);
1402
1403 if import_path.starts_with("crate::") {
1404 let module_path = import_path.strip_prefix("crate::").unwrap();
1406 let parts: Vec<&str> = module_path.split("::").collect();
1407
1408 let src_root = crate_root_path.join("src");
1410 resolve_rust_module_path(&src_root, &parts)
1411 } else if import_path.starts_with("super::") {
1412 let module_path = import_path.strip_prefix("super::").unwrap();
1414 let parts: Vec<&str> = module_path.split("::").collect();
1415
1416 let current_dir = if current_path.file_name().unwrap() == "mod.rs" {
1418 current_path.parent()?.parent()?
1420 } else {
1421 current_path.parent()?
1423 };
1424
1425 resolve_rust_module_path(current_dir, &parts)
1426 } else if import_path.starts_with("self::") {
1427 let module_path = import_path.strip_prefix("self::").unwrap();
1429 let parts: Vec<&str> = module_path.split("::").collect();
1430
1431 let current_dir = if current_path.file_name().unwrap() == "mod.rs" {
1433 current_path.parent()?
1435 } else {
1436 current_path.parent()?
1438 };
1439
1440 resolve_rust_module_path(current_dir, &parts)
1441 } else {
1442 let current_dir = current_path.parent()?;
1445 let module_file = current_dir.join(format!("{}.rs", import_path));
1446 let module_dir = current_dir.join(import_path).join("mod.rs");
1447
1448 if module_file.exists() {
1449 Some(module_file.to_string_lossy().to_string())
1450 } else if module_dir.exists() {
1451 Some(module_dir.to_string_lossy().to_string())
1452 } else {
1453 Some(module_file.to_string_lossy().to_string())
1456 }
1457 }
1458}
1459
1460fn resolve_rust_module_path(base_dir: &std::path::Path, parts: &[&str]) -> Option<String> {
1466 if parts.is_empty() {
1467 return None;
1468 }
1469
1470 let mut current_path = base_dir.to_path_buf();
1472
1473 for (i, part) in parts.iter().enumerate() {
1474 if i == parts.len() - 1 {
1475 let file_path = current_path.join(format!("{}.rs", part));
1477 let mod_path = current_path.join(part).join("mod.rs");
1478
1479 log::trace!("Checking Rust module path: {}", file_path.display());
1480 log::trace!("Checking Rust module path: {}", mod_path.display());
1481
1482 if file_path.exists() {
1484 return Some(file_path.to_string_lossy().to_string());
1485 } else if mod_path.exists() {
1486 return Some(mod_path.to_string_lossy().to_string());
1487 } else {
1488 return Some(file_path.to_string_lossy().to_string());
1490 }
1491 } else {
1492 current_path = current_path.join(part);
1494 }
1495 }
1496
1497 None
1498}
1499
1500#[cfg(test)]
1501mod path_resolution_tests {
1502 use super::*;
1503
1504 #[test]
1505 fn test_resolve_crate_import() {
1506 let result = resolve_rust_use_to_path(
1508 "crate::models",
1509 Some("/home/user/project/src/main.rs"),
1510 Some("/home/user/project"),
1511 );
1512
1513 assert!(result.is_some());
1514 let path = result.unwrap();
1515 assert!(path.contains("models.rs") || path.contains("models/mod.rs"));
1517 }
1518
1519 #[test]
1520 fn test_resolve_super_import() {
1521 let result = resolve_rust_use_to_path(
1523 "super::utils",
1524 Some("/home/user/project/src/commands/index.rs"),
1525 Some("/home/user/project"),
1526 );
1527
1528 assert!(result.is_some());
1529 let path = result.unwrap();
1530 assert!(path.contains("src") && path.contains("utils.rs"));
1532 }
1533
1534 #[test]
1535 fn test_resolve_self_import() {
1536 let result = resolve_rust_use_to_path(
1538 "self::helper",
1539 Some("/home/user/project/src/models/mod.rs"),
1540 Some("/home/user/project"),
1541 );
1542
1543 assert!(result.is_some());
1544 let path = result.unwrap();
1545 assert!(path.contains("models") && path.contains("helper.rs"));
1547 }
1548
1549 #[test]
1550 fn test_resolve_mod_declaration() {
1551 let result = resolve_rust_use_to_path(
1553 "parser",
1554 Some("/home/user/project/src/main.rs"),
1555 Some("/home/user/project"),
1556 );
1557
1558 assert!(result.is_some());
1559 let path = result.unwrap();
1560 assert!(path.contains("parser.rs"));
1562 }
1563
1564 #[test]
1565 fn test_resolve_nested_crate_import() {
1566 let result = resolve_rust_use_to_path(
1568 "crate::models::language",
1569 Some("/home/user/project/src/main.rs"),
1570 Some("/home/user/project"),
1571 );
1572
1573 assert!(result.is_some());
1574 let path = result.unwrap();
1575 assert!(
1577 path.contains("models")
1578 && (path.contains("language.rs") || path.contains("language/mod.rs"))
1579 );
1580 }
1581
1582 #[test]
1583 fn test_external_import_not_supported() {
1584 let result = resolve_rust_use_to_path(
1586 "anyhow::Result",
1587 Some("/home/user/project/src/main.rs"),
1588 Some("/home/user/project"),
1589 );
1590
1591 assert!(result.is_none());
1593 }
1594
1595 #[test]
1596 fn test_stdlib_import_not_supported() {
1597 let result = resolve_rust_use_to_path(
1599 "std::collections::HashMap",
1600 Some("/home/user/project/src/main.rs"),
1601 Some("/home/user/project"),
1602 );
1603
1604 assert!(result.is_none());
1606 }
1607
1608 #[test]
1609 fn test_resolve_without_current_file() {
1610 let result = resolve_rust_use_to_path("crate::models", None, Some("/home/user/project"));
1611
1612 assert!(result.is_none());
1614 }
1615}
1616
1617#[derive(Debug, Clone)]
1623pub struct RustCrate {
1624 pub name: String,
1625 pub root_path: std::path::PathBuf,
1626}
1627
1628pub fn parse_all_rust_crates(root: &std::path::Path) -> anyhow::Result<Vec<RustCrate>> {
1633 if !root.join("Cargo.toml").exists() {
1635 return Ok(Vec::new());
1636 }
1637
1638 let mut crates = Vec::new();
1639 let walker = ignore::WalkBuilder::new(root).git_ignore(true).build();
1640
1641 for entry in walker {
1642 let entry = entry?;
1643 if entry.file_name() == "Cargo.toml" {
1644 let content = std::fs::read_to_string(entry.path())?;
1645 if let Some(name) = extract_crate_name(&content) {
1646 if let Some(crate_root) = entry.path().parent() {
1647 crates.push(RustCrate {
1648 name,
1649 root_path: crate_root.to_path_buf(),
1650 });
1651 }
1652 }
1653 }
1654 }
1655 Ok(crates)
1656}
1657
1658fn extract_crate_name(content: &str) -> Option<String> {
1660 let table: toml::Table = content.parse().ok()?;
1661 table
1662 .get("package")?
1663 .get("name")?
1664 .as_str()
1665 .map(|s| s.to_string())
1666}
1667
1668pub fn reclassify_rust_import(path: &str, crates: &[RustCrate]) -> crate::models::ImportType {
1674 for krate in crates {
1675 if path == krate.name || path.starts_with(&format!("{}::", krate.name)) {
1676 return crate::models::ImportType::Internal;
1677 }
1678 }
1679 classify_rust_import(path)
1680}
1681
1682pub fn resolve_rust_workspace_path(import_path: &str, crates: &[RustCrate]) -> Option<String> {
1687 for krate in crates {
1688 if import_path == krate.name || import_path.starts_with(&format!("{}::", krate.name)) {
1689 let relative_module = if import_path == krate.name {
1690 ""
1691 } else {
1692 import_path
1693 .strip_prefix(&format!("{}::", krate.name))
1694 .unwrap_or("")
1695 };
1696
1697 let src_root = krate.root_path.join("src");
1698
1699 if relative_module.is_empty() {
1700 let lib = src_root.join("lib.rs");
1702 if lib.exists() {
1703 return Some(lib.to_string_lossy().to_string());
1704 }
1705 let main = src_root.join("main.rs");
1706 if main.exists() {
1707 return Some(main.to_string_lossy().to_string());
1708 }
1709 } else {
1710 let parts: Vec<&str> = relative_module.split("::").collect();
1711
1712 if let Some(path) = resolve_rust_module_path(&src_root, &parts) {
1714 if std::path::Path::new(&path).exists() {
1715 return Some(path);
1716 }
1717 }
1718
1719 if parts.len() > 1 {
1721 if let Some(path) =
1722 resolve_rust_module_path(&src_root, &parts[..parts.len() - 1])
1723 {
1724 if std::path::Path::new(&path).exists() {
1725 return Some(path);
1726 }
1727 }
1728 }
1729
1730 if let Some(path) = resolve_rust_module_path(&src_root, &parts) {
1733 return Some(path);
1734 }
1735 }
1736 }
1737 }
1738 None
1739}
1740
1741#[cfg(test)]
1742mod workspace_tests {
1743 use super::*;
1744 use std::fs;
1745 use tempfile::TempDir;
1746
1747 fn create_workspace(dir: &std::path::Path) {
1748 fs::write(
1750 dir.join("Cargo.toml"),
1751 r#"[workspace]
1752members = ["crate_a", "crate_b"]
1753"#,
1754 )
1755 .unwrap();
1756
1757 let crate_a = dir.join("crate_a");
1759 fs::create_dir_all(crate_a.join("src")).unwrap();
1760 fs::write(
1761 crate_a.join("Cargo.toml"),
1762 r#"[package]
1763name = "crate_a"
1764version = "0.1.0"
1765"#,
1766 )
1767 .unwrap();
1768 fs::write(crate_a.join("src/lib.rs"), "pub mod config;").unwrap();
1769 fs::write(crate_a.join("src/config.rs"), "pub struct Config;").unwrap();
1770
1771 let crate_b = dir.join("crate_b");
1773 fs::create_dir_all(crate_b.join("src")).unwrap();
1774 fs::write(
1775 crate_b.join("Cargo.toml"),
1776 r#"[package]
1777name = "crate_b" # a comment
1778version = "0.1.0"
1779"#,
1780 )
1781 .unwrap();
1782 fs::write(crate_b.join("src/lib.rs"), "").unwrap();
1783 }
1784
1785 #[test]
1786 fn test_extract_crate_name_standard() {
1787 let toml = r#"[package]
1788name = "my_crate"
1789version = "0.1.0"
1790"#;
1791 assert_eq!(extract_crate_name(toml), Some("my_crate".to_string()));
1792 }
1793
1794 #[test]
1795 fn test_extract_crate_name_inline_table() {
1796 let toml = r#"[package]
1798name = "inline_crate"
1799version = "0.1.0"
1800edition = "2021"
1801"#;
1802 assert_eq!(extract_crate_name(toml), Some("inline_crate".to_string()));
1803 }
1804
1805 #[test]
1806 fn test_extract_crate_name_with_comment() {
1807 let toml = r#"[package]
1808name = "commented_crate" # my crate
1809version = "0.1.0"
1810"#;
1811 assert_eq!(
1812 extract_crate_name(toml),
1813 Some("commented_crate".to_string())
1814 );
1815 }
1816
1817 #[test]
1818 fn test_extract_crate_name_no_package() {
1819 let toml = r#"[workspace]
1820members = ["crate_a"]
1821"#;
1822 assert_eq!(extract_crate_name(toml), None);
1823 }
1824
1825 #[test]
1826 fn test_parse_all_rust_crates() {
1827 let dir = TempDir::new().unwrap();
1828 create_workspace(dir.path());
1829
1830 let crates = parse_all_rust_crates(dir.path()).unwrap();
1831 assert_eq!(crates.len(), 2);
1832
1833 let names: Vec<&str> = crates.iter().map(|c| c.name.as_str()).collect();
1834 assert!(names.contains(&"crate_a"));
1835 assert!(names.contains(&"crate_b"));
1836 }
1837
1838 #[test]
1839 fn test_parse_all_rust_crates_non_rust_project() {
1840 let dir = TempDir::new().unwrap();
1841 let crates = parse_all_rust_crates(dir.path()).unwrap();
1843 assert!(crates.is_empty());
1844 }
1845
1846 #[test]
1847 fn test_reclassify_rust_import_workspace_crate() {
1848 let crates = vec![RustCrate {
1849 name: "crate_a".to_string(),
1850 root_path: std::path::PathBuf::from("/workspace/crate_a"),
1851 }];
1852
1853 assert!(matches!(
1854 reclassify_rust_import("crate_a", &crates),
1855 crate::models::ImportType::Internal
1856 ));
1857 assert!(matches!(
1858 reclassify_rust_import("crate_a::config", &crates),
1859 crate::models::ImportType::Internal
1860 ));
1861 assert!(matches!(
1863 reclassify_rust_import("serde::Serialize", &crates),
1864 crate::models::ImportType::External
1865 ));
1866 }
1867
1868 #[test]
1869 fn test_resolve_rust_workspace_path() {
1870 let dir = TempDir::new().unwrap();
1871 create_workspace(dir.path());
1872
1873 let crates = parse_all_rust_crates(dir.path()).unwrap();
1874
1875 let result = resolve_rust_workspace_path("crate_a", &crates);
1877 assert!(result.is_some());
1878 assert!(result.unwrap().ends_with("lib.rs"));
1879
1880 let result = resolve_rust_workspace_path("crate_a::config", &crates);
1882 assert!(result.is_some());
1883 assert!(result.unwrap().ends_with("config.rs"));
1884
1885 let result = resolve_rust_workspace_path("crate_a::config::Config", &crates);
1887 assert!(result.is_some());
1888 assert!(result.unwrap().ends_with("config.rs"));
1889
1890 let result = resolve_rust_workspace_path("unknown_crate::foo", &crates);
1892 assert!(result.is_none());
1893 }
1894}