1use std::cell::RefCell;
2use std::path::Path;
3
4use tree_sitter::{Node, Parser};
5use tree_sitter_language::LanguageFn;
6
7use domain::error::CodeGraphError;
8use domain::model::{Edge, EdgeKind, Language, Location, SymbolKind, SymbolNode, Visibility};
9
10use crate::{Export, ImportName, LanguageParser, ParseResult, RawImport};
11
12thread_local! {
13 static RUST_PARSER: RefCell<Parser> = RefCell::new(Parser::new());
14}
15
16pub struct RustParser {
18 lang: LanguageFn,
19}
20
21impl RustParser {
22 pub fn new() -> Self {
23 Self {
24 lang: tree_sitter_rust::LANGUAGE,
25 }
26 }
27}
28
29impl Default for RustParser {
30 fn default() -> Self {
31 Self::new()
32 }
33}
34
35impl LanguageParser for RustParser {
36 fn language(&self) -> Language {
37 Language::Rust
38 }
39
40 fn file_extensions(&self) -> &[&str] {
41 &["rs"]
42 }
43
44 fn parse(&self, source: &[u8], path: &Path) -> domain::error::Result<ParseResult> {
45 let lang: tree_sitter::Language = self.lang.into();
46
47 RUST_PARSER.with(|parser_cell| {
48 let mut parser = parser_cell.borrow_mut();
49 parser
50 .set_language(&lang)
51 .map_err(|e| CodeGraphError::Parse {
52 file: path.to_path_buf(),
53 message: format!("failed to set language: {e}"),
54 })?;
55
56 let tree = parser
57 .parse(source, None)
58 .ok_or_else(|| CodeGraphError::Parse {
59 file: path.to_path_buf(),
60 message: "tree-sitter parse returned None".into(),
61 })?;
62
63 extract_all(source, path, &tree)
64 })
65 }
66}
67
68fn extract_all(
69 source: &[u8],
70 path: &Path,
71 tree: &tree_sitter::Tree,
72) -> domain::error::Result<ParseResult> {
73 let mut symbols = Vec::new();
74 let mut edges = Vec::new();
75 let mut imports = Vec::new();
76 let mut exports = Vec::new();
77 let file_path = path.to_string_lossy().to_string();
78 let root = tree.root_node();
79 let mut cursor = root.walk();
80
81 let mut pending_attrs: Vec<String> = Vec::new();
84
85 for child in root.children(&mut cursor) {
86 if !child.is_named() {
87 continue;
88 }
89 match child.kind() {
90 "attribute_item" => {
91 if let Ok(text) = child.utf8_text(source) {
93 pending_attrs.push(text.to_string());
94 }
95 continue; }
97 "function_item" => {
98 if let Some(sym) = extract_function(source, &file_path, child, None, &pending_attrs)
99 {
100 edges.push(contains_edge(&file_path, &sym.qualified_name));
101 symbols.push(sym);
102 }
103 }
104 "struct_item" => {
105 if let Some(sym) = extract_struct(source, &file_path, child) {
106 edges.push(contains_edge(&file_path, &sym.qualified_name));
107 symbols.push(sym);
108 }
109 }
110 "enum_item" => {
111 if let Some(sym) = extract_enum(source, &file_path, child) {
112 edges.push(contains_edge(&file_path, &sym.qualified_name));
113 symbols.push(sym);
114 }
115 }
116 "trait_item" => {
117 if let Some(sym) = extract_trait(source, &file_path, child) {
118 edges.push(contains_edge(&file_path, &sym.qualified_name));
119 symbols.push(sym);
120 }
121 }
122 "type_item" => {
123 if let Some(sym) = extract_type_alias(source, &file_path, child) {
124 edges.push(contains_edge(&file_path, &sym.qualified_name));
125 symbols.push(sym);
126 }
127 }
128 "const_item" => {
129 if let Some(sym) = extract_const(source, &file_path, child) {
130 edges.push(contains_edge(&file_path, &sym.qualified_name));
131 symbols.push(sym);
132 }
133 }
134 "static_item" => {
135 if let Some(sym) = extract_static(source, &file_path, child) {
136 edges.push(contains_edge(&file_path, &sym.qualified_name));
137 symbols.push(sym);
138 }
139 }
140 "macro_definition" => {
141 if let Some(sym) = extract_macro(source, &file_path, child) {
142 edges.push(contains_edge(&file_path, &sym.qualified_name));
143 symbols.push(sym);
144 }
145 }
146 "impl_item" => {
147 extract_impl(source, &file_path, child, &mut symbols, &mut edges);
148 }
149 "use_declaration" => {
150 extract_use_declaration(source, child, &mut imports, &mut exports);
151 }
152 "mod_item" => {
153 if child.child_by_field_name("body").is_none() {
155 if let Some(name) = node_name(source, child) {
156 let line = child.start_position().row + 1;
157 imports.push(RawImport {
158 specifier: format!("mod::{name}"),
159 names: Vec::new(),
160 is_type_only: false,
161 is_side_effect: false,
162 is_namespace: false,
163 line,
164 });
165 }
166 }
167 }
168 _ => {}
169 }
170 pending_attrs.clear();
172 }
173
174 Ok(ParseResult {
175 symbols,
176 edges,
177 imports,
178 exports,
179 })
180}
181
182fn extract_function(
190 source: &[u8],
191 file_path: &str,
192 node: Node,
193 owner_name: Option<&str>,
194 preceding_attrs: &[String],
195) -> Option<SymbolNode> {
196 let name = node_name(source, node)?;
197 let qualified_name = match owner_name {
198 Some(owner) => format!("{file_path}::{owner}.{name}"),
199 None => format!("{file_path}::{name}"),
200 };
201 let visibility = extract_visibility(source, node);
202 let is_exported = visibility == Visibility::Public;
203 let is_async = is_async_fn(source, node);
204 let is_test = attrs_contain_test(preceding_attrs);
205 let signature = build_rust_signature(source, node);
206 let kind = if owner_name.is_some() {
207 SymbolKind::Method
208 } else {
209 SymbolKind::Function
210 };
211
212 Some(SymbolNode {
213 name,
214 qualified_name,
215 kind,
216 location: node_location(file_path, node),
217 visibility,
218 is_exported,
219 is_async,
220 is_test,
221 decorators: Vec::new(),
222 signature,
223 })
224}
225
226fn extract_struct(source: &[u8], file_path: &str, node: Node) -> Option<SymbolNode> {
227 let name = node_name(source, node)?;
228 let qualified_name = format!("{file_path}::{name}");
229 let visibility = extract_visibility(source, node);
230 let is_exported = visibility == Visibility::Public;
231
232 Some(SymbolNode {
233 name,
234 qualified_name,
235 kind: SymbolKind::Struct,
236 location: node_location(file_path, node),
237 visibility,
238 is_exported,
239 is_async: false,
240 is_test: false,
241 decorators: Vec::new(),
242 signature: None,
243 })
244}
245
246fn extract_enum(source: &[u8], file_path: &str, node: Node) -> Option<SymbolNode> {
247 let name = node_name(source, node)?;
248 let qualified_name = format!("{file_path}::{name}");
249 let visibility = extract_visibility(source, node);
250 let is_exported = visibility == Visibility::Public;
251
252 Some(SymbolNode {
253 name,
254 qualified_name,
255 kind: SymbolKind::Enum,
256 location: node_location(file_path, node),
257 visibility,
258 is_exported,
259 is_async: false,
260 is_test: false,
261 decorators: Vec::new(),
262 signature: None,
263 })
264}
265
266fn extract_trait(source: &[u8], file_path: &str, node: Node) -> Option<SymbolNode> {
267 let name = node_name(source, node)?;
268 let qualified_name = format!("{file_path}::{name}");
269 let visibility = extract_visibility(source, node);
270 let is_exported = visibility == Visibility::Public;
271
272 Some(SymbolNode {
273 name,
274 qualified_name,
275 kind: SymbolKind::Trait,
276 location: node_location(file_path, node),
277 visibility,
278 is_exported,
279 is_async: false,
280 is_test: false,
281 decorators: Vec::new(),
282 signature: None,
283 })
284}
285
286fn extract_type_alias(source: &[u8], file_path: &str, node: Node) -> Option<SymbolNode> {
287 let name = node_name(source, node)?;
288 let qualified_name = format!("{file_path}::{name}");
289 let visibility = extract_visibility(source, node);
290 let is_exported = visibility == Visibility::Public;
291
292 Some(SymbolNode {
293 name,
294 qualified_name,
295 kind: SymbolKind::TypeAlias,
296 location: node_location(file_path, node),
297 visibility,
298 is_exported,
299 is_async: false,
300 is_test: false,
301 decorators: Vec::new(),
302 signature: None,
303 })
304}
305
306fn extract_const(source: &[u8], file_path: &str, node: Node) -> Option<SymbolNode> {
307 let name = node_name(source, node)?;
308 let qualified_name = format!("{file_path}::{name}");
309 let visibility = extract_visibility(source, node);
310 let is_exported = visibility == Visibility::Public;
311
312 Some(SymbolNode {
313 name,
314 qualified_name,
315 kind: SymbolKind::Const,
316 location: node_location(file_path, node),
317 visibility,
318 is_exported,
319 is_async: false,
320 is_test: false,
321 decorators: Vec::new(),
322 signature: None,
323 })
324}
325
326fn extract_static(source: &[u8], file_path: &str, node: Node) -> Option<SymbolNode> {
327 let name = node_name(source, node)?;
328 let qualified_name = format!("{file_path}::{name}");
329 let visibility = extract_visibility(source, node);
330 let is_exported = visibility == Visibility::Public;
331
332 Some(SymbolNode {
333 name,
334 qualified_name,
335 kind: SymbolKind::Variable,
336 location: node_location(file_path, node),
337 visibility,
338 is_exported,
339 is_async: false,
340 is_test: false,
341 decorators: Vec::new(),
342 signature: None,
343 })
344}
345
346fn extract_macro(source: &[u8], file_path: &str, node: Node) -> Option<SymbolNode> {
347 let name = node_name(source, node)?;
348 let qualified_name = format!("{file_path}::{name}");
349
350 Some(SymbolNode {
351 name,
352 qualified_name,
353 kind: SymbolKind::Macro,
354 location: node_location(file_path, node),
355 visibility: Visibility::Public, is_exported: false,
357 is_async: false,
358 is_test: false,
359 decorators: Vec::new(),
360 signature: None,
361 })
362}
363
364fn extract_impl(
366 source: &[u8],
367 file_path: &str,
368 node: Node,
369 symbols: &mut Vec<SymbolNode>,
370 edges: &mut Vec<Edge>,
371) {
372 let type_name = match node.child_by_field_name("type") {
374 Some(t) => match t.utf8_text(source) {
375 Ok(s) => s.to_string(),
376 Err(_) => return,
377 },
378 None => return,
379 };
380
381 let trait_name = node
383 .child_by_field_name("trait")
384 .and_then(|t| t.utf8_text(source).ok())
385 .map(|s| s.to_string());
386
387 if let Some(ref tname) = trait_name {
389 let trait_qn = format!("{file_path}::{tname}");
390 let type_qn = format!("{file_path}::{type_name}");
391 edges.push(Edge {
392 kind: EdgeKind::Implements,
393 source: type_qn,
394 target: trait_qn,
395 metadata: None,
396 });
397 }
398
399 let body = match node.child_by_field_name("body") {
401 Some(b) => b,
402 None => return,
403 };
404
405 let mut body_cursor = body.walk();
406 let mut pending_attrs: Vec<String> = Vec::new();
407 for member in body.children(&mut body_cursor) {
408 if !member.is_named() {
409 continue;
410 }
411 if member.kind() == "attribute_item" {
412 if let Ok(text) = member.utf8_text(source) {
413 pending_attrs.push(text.to_string());
414 }
415 continue;
416 }
417 if member.kind() == "function_item" {
418 if let Some(sym) =
419 extract_function(source, file_path, member, Some(&type_name), &pending_attrs)
420 {
421 edges.push(contains_edge(file_path, &sym.qualified_name));
422 let type_qn = format!("{file_path}::{type_name}");
424 edges.push(Edge {
425 kind: EdgeKind::ChildOf,
426 source: sym.qualified_name.clone(),
427 target: type_qn,
428 metadata: None,
429 });
430 symbols.push(sym);
431 }
432 }
433 pending_attrs.clear();
434 }
435}
436
437fn extract_use_declaration(
444 source: &[u8],
445 node: Node,
446 imports: &mut Vec<RawImport>,
447 exports: &mut Vec<Export>,
448) {
449 let line = node.start_position().row + 1;
450
451 let is_pub_use = {
453 let mut cur = node.walk();
454 let result = node
455 .children(&mut cur)
456 .any(|c| c.kind() == "visibility_modifier");
457 result
458 };
459
460 let argument = match node.child_by_field_name("argument") {
462 Some(a) => a,
463 None => return,
464 };
465
466 process_use_argument(source, argument, &[], line, is_pub_use, imports, exports);
467}
468
469fn process_use_argument(
473 source: &[u8],
474 node: Node,
475 prefix_parts: &[String],
476 line: usize,
477 is_pub_use: bool,
478 imports: &mut Vec<RawImport>,
479 exports: &mut Vec<Export>,
480) {
481 match node.kind() {
482 "scoped_identifier" => {
484 let parts = flatten_scoped_identifier(source, node);
485 let specifier = parts.join("::");
486 let name = parts.last().cloned().unwrap_or_default();
487 emit_import_and_maybe_export(
488 specifier,
489 vec![ImportName {
490 name,
491 alias: None,
492 is_type: false,
493 }],
494 false,
495 line,
496 is_pub_use,
497 imports,
498 exports,
499 );
500 }
501
502 "use_wildcard" => {
505 let embedded_parts: Vec<String> = {
508 let mut cur = node.walk();
509 node.children(&mut cur)
510 .filter(|c| {
511 matches!(
512 c.kind(),
513 "scoped_identifier" | "identifier" | "crate" | "self" | "super"
514 )
515 })
516 .flat_map(|c| flatten_path_node(source, c))
517 .collect()
518 };
519
520 let specifier = if !embedded_parts.is_empty() {
521 embedded_parts.join("::")
522 } else {
523 prefix_parts.join("::")
525 };
526
527 emit_import_and_maybe_export(
528 specifier,
529 Vec::new(),
530 true, line,
532 is_pub_use,
533 imports,
534 exports,
535 );
536 }
537
538 "scoped_use_list" => {
540 let path_parts: Vec<String> = match node.child_by_field_name("path") {
542 Some(p) => flatten_path_node(source, p),
543 None => prefix_parts.to_vec(),
544 };
545
546 let list = match node.child_by_field_name("list") {
547 Some(l) => l,
548 None => return,
549 };
550
551 let mut list_cursor = list.walk();
552 for item in list.children(&mut list_cursor) {
553 if !item.is_named() {
554 continue;
555 }
556 process_use_argument(
557 source,
558 item,
559 &path_parts,
560 line,
561 is_pub_use,
562 imports,
563 exports,
564 );
565 }
566 }
567
568 "use_as_clause" => {
570 let path_node = node.child_by_field_name("path");
571 let alias_node = node.child_by_field_name("alias");
572
573 let (specifier, name) = match path_node {
574 Some(p) => {
575 let parts = flatten_path_node(source, p);
576 let name = parts.last().cloned().unwrap_or_default();
577 (parts.join("::"), name)
578 }
579 None => (prefix_parts.join("::"), String::new()),
580 };
581
582 let alias = alias_node
583 .and_then(|a| a.utf8_text(source).ok())
584 .map(|s| s.to_string());
585
586 emit_import_and_maybe_export(
587 specifier,
588 vec![ImportName {
589 name,
590 alias,
591 is_type: false,
592 }],
593 false,
594 line,
595 is_pub_use,
596 imports,
597 exports,
598 );
599 }
600
601 "identifier" | "crate" | "self" | "super" => {
603 if let Ok(text) = node.utf8_text(source) {
604 let mut parts = prefix_parts.to_vec();
605 let name = text.to_string();
606 parts.push(name.clone());
607 let specifier = parts.join("::");
608 emit_import_and_maybe_export(
609 specifier,
610 vec![ImportName {
611 name,
612 alias: None,
613 is_type: false,
614 }],
615 false,
616 line,
617 is_pub_use,
618 imports,
619 exports,
620 );
621 }
622 }
623
624 "use_list" => {
626 let mut list_cursor = node.walk();
627 for item in node.children(&mut list_cursor) {
628 if !item.is_named() {
629 continue;
630 }
631 process_use_argument(
632 source,
633 item,
634 prefix_parts,
635 line,
636 is_pub_use,
637 imports,
638 exports,
639 );
640 }
641 }
642
643 _ => {}
644 }
645}
646
647fn emit_import_and_maybe_export(
649 specifier: String,
650 names: Vec<ImportName>,
651 is_namespace: bool,
652 line: usize,
653 is_pub_use: bool,
654 imports: &mut Vec<RawImport>,
655 exports: &mut Vec<Export>,
656) {
657 if is_pub_use {
658 for n in &names {
660 exports.push(Export {
661 name: n.alias.clone().unwrap_or_else(|| n.name.clone()),
662 local_name: Some(n.name.clone()),
663 is_default: false,
664 is_type_only: false,
665 is_reexport: true,
666 source_specifier: Some(specifier.clone()),
667 });
668 }
669 if is_namespace {
670 exports.push(Export {
671 name: String::new(),
672 local_name: None,
673 is_default: false,
674 is_type_only: false,
675 is_reexport: true,
676 source_specifier: Some(specifier.clone()),
677 });
678 }
679 }
680
681 imports.push(RawImport {
682 specifier,
683 names,
684 is_type_only: false,
685 is_side_effect: false,
686 is_namespace,
687 line,
688 });
689}
690
691fn flatten_scoped_identifier(source: &[u8], node: Node) -> Vec<String> {
694 let mut parts = Vec::new();
695 flatten_scoped_identifier_into(source, node, &mut parts);
696 parts
697}
698
699fn flatten_scoped_identifier_into(source: &[u8], node: Node, parts: &mut Vec<String>) {
700 if let Some(path) = node.child_by_field_name("path") {
702 match path.kind() {
703 "scoped_identifier" => flatten_scoped_identifier_into(source, path, parts),
704 _ => {
705 if let Ok(text) = path.utf8_text(source) {
706 parts.push(text.to_string());
707 }
708 }
709 }
710 }
711 if let Some(name) = node.child_by_field_name("name") {
712 if let Ok(text) = name.utf8_text(source) {
713 parts.push(text.to_string());
714 }
715 }
716}
717
718fn flatten_path_node(source: &[u8], node: Node) -> Vec<String> {
721 match node.kind() {
722 "scoped_identifier" => flatten_scoped_identifier(source, node),
723 "identifier" | "crate" | "self" | "super" => node
724 .utf8_text(source)
725 .ok()
726 .map(|s| vec![s.to_string()])
727 .unwrap_or_default(),
728 _ => Vec::new(),
729 }
730}
731
732fn node_name(source: &[u8], node: Node) -> Option<String> {
738 node.child_by_field_name("name")
739 .and_then(|n| n.utf8_text(source).ok())
740 .map(|s| s.to_string())
741}
742
743fn node_location(file_path: &str, node: Node) -> Location {
745 let start = node.start_position();
746 let end = node.end_position();
747 Location {
748 file: file_path.into(),
749 line_start: start.row + 1,
750 line_end: end.row + 1,
751 col_start: start.column,
752 col_end: end.column,
753 }
754}
755
756fn contains_edge(file_path: &str, qualified_name: &str) -> Edge {
758 Edge {
759 kind: EdgeKind::Contains,
760 source: file_path.to_string(),
761 target: qualified_name.to_string(),
762 metadata: None,
763 }
764}
765
766fn extract_visibility(source: &[u8], node: Node) -> Visibility {
768 let mut cursor = node.walk();
769 for child in node.children(&mut cursor) {
770 if child.kind() == "visibility_modifier" {
771 let text = child.utf8_text(source).unwrap_or("");
772 return if text.contains("crate") {
773 Visibility::Crate
774 } else {
775 Visibility::Public
777 };
778 }
779 }
780 Visibility::Private
781}
782
783fn is_async_fn(source: &[u8], node: Node) -> bool {
788 let mut cursor = node.walk();
789 for child in node.children(&mut cursor) {
790 if child.kind() == "function_modifiers" {
791 let text = child.utf8_text(source).unwrap_or("");
792 return text.split_whitespace().any(|w| w == "async");
793 }
794 }
795 false
796}
797
798fn attrs_contain_test(attrs: &[String]) -> bool {
804 attrs.iter().any(|a| a.contains("test"))
805}
806
807fn build_rust_signature(source: &[u8], node: Node) -> Option<String> {
809 let params = node
810 .child_by_field_name("parameters")
811 .and_then(|n| n.utf8_text(source).ok())?;
812
813 let return_type = node
814 .child_by_field_name("return_type")
815 .and_then(|n| n.utf8_text(source).ok());
816
817 Some(match return_type {
818 Some(ret) => format!("{params} {ret}"),
819 None => params.to_string(),
820 })
821}
822
823#[cfg(test)]
828mod tests {
829 use super::*;
830
831 fn parse_rust(source: &str) -> ParseResult {
832 let parser = RustParser::new();
833 parser
834 .parse(source.as_bytes(), Path::new("test.rs"))
835 .expect("parse failed")
836 }
837
838 #[test]
843 fn ac6_function_item_extracts_function_symbol() {
844 let result = parse_rust("fn foo() {}");
845 assert_eq!(result.symbols.len(), 1);
846 let sym = &result.symbols[0];
847 assert_eq!(sym.name, "foo");
848 assert_eq!(sym.kind, SymbolKind::Function);
849 }
850
851 #[test]
852 fn function_qualified_name_uses_file_path() {
853 let result = parse_rust("fn foo() {}");
854 assert_eq!(result.symbols[0].qualified_name, "test.rs::foo");
855 }
856
857 #[test]
858 fn function_contains_edge_emitted() {
859 let result = parse_rust("fn foo() {}");
860 let contains: Vec<_> = result
861 .edges
862 .iter()
863 .filter(|e| e.kind == EdgeKind::Contains)
864 .collect();
865 assert_eq!(contains.len(), 1);
866 assert_eq!(contains[0].source, "test.rs");
867 assert_eq!(contains[0].target, "test.rs::foo");
868 }
869
870 #[test]
875 fn ac7_struct_item_extracts_struct_symbol() {
876 let result = parse_rust("struct Bar {}");
877 assert_eq!(result.symbols.len(), 1);
878 let sym = &result.symbols[0];
879 assert_eq!(sym.name, "Bar");
880 assert_eq!(sym.kind, SymbolKind::Struct);
881 }
882
883 #[test]
888 fn ac8_impl_item_extracts_method_symbol() {
889 let result = parse_rust("struct Foo; impl Foo { fn bar(&self) {} }");
890 let method = result
891 .symbols
892 .iter()
893 .find(|s| s.name == "bar")
894 .expect("method 'bar' not found");
895 assert_eq!(method.kind, SymbolKind::Method);
896 assert_eq!(method.qualified_name, "test.rs::Foo.bar");
897 }
898
899 #[test]
900 fn ac8_impl_method_has_child_of_edge() {
901 let result = parse_rust("struct Foo; impl Foo { fn bar(&self) {} }");
902 let child_of = result
903 .edges
904 .iter()
905 .find(|e| e.kind == EdgeKind::ChildOf)
906 .expect("ChildOf edge not found");
907 assert_eq!(child_of.source, "test.rs::Foo.bar");
908 assert_eq!(child_of.target, "test.rs::Foo");
909 }
910
911 #[test]
912 fn impl_method_also_has_contains_edge() {
913 let result = parse_rust("struct Foo; impl Foo { fn bar(&self) {} }");
914 let contains: Vec<_> = result
915 .edges
916 .iter()
917 .filter(|e| e.kind == EdgeKind::Contains && e.target == "test.rs::Foo.bar")
918 .collect();
919 assert_eq!(contains.len(), 1);
920 }
921
922 #[test]
927 fn ac9_trait_item_extracts_trait_symbol() {
928 let result = parse_rust("trait Baz {}");
929 assert_eq!(result.symbols.len(), 1);
930 let sym = &result.symbols[0];
931 assert_eq!(sym.name, "Baz");
932 assert_eq!(sym.kind, SymbolKind::Trait);
933 }
934
935 #[test]
940 fn ac10_enum_item_extracts_enum_symbol() {
941 let result = parse_rust("enum Color { Red, Green }");
942 assert_eq!(result.symbols.len(), 1);
943 let sym = &result.symbols[0];
944 assert_eq!(sym.name, "Color");
945 assert_eq!(sym.kind, SymbolKind::Enum);
946 }
947
948 #[test]
953 fn ac14_pub_fn_is_public() {
954 let result = parse_rust("pub fn visible() {}");
955 let sym = &result.symbols[0];
956 assert_eq!(sym.visibility, Visibility::Public);
957 assert!(sym.is_exported);
958 }
959
960 #[test]
961 fn ac14_pub_crate_fn_is_crate() {
962 let result = parse_rust("pub(crate) fn crate_fn() {}");
963 let sym = &result.symbols[0];
964 assert_eq!(sym.visibility, Visibility::Crate);
965 assert!(!sym.is_exported);
966 }
967
968 #[test]
969 fn ac14_private_fn_is_private() {
970 let result = parse_rust("fn private_fn() {}");
971 let sym = &result.symbols[0];
972 assert_eq!(sym.visibility, Visibility::Private);
973 assert!(!sym.is_exported);
974 }
975
976 #[test]
977 fn pub_struct_is_exported() {
978 let result = parse_rust("pub struct MyStruct {}");
979 let sym = &result.symbols[0];
980 assert_eq!(sym.visibility, Visibility::Public);
981 assert!(sym.is_exported);
982 }
983
984 #[test]
989 fn ac49_empty_source_does_not_panic() {
990 let parser = RustParser::new();
991 let result = parser.parse(b"", Path::new("empty.rs"));
992 assert!(result.is_ok());
993 let r = result.unwrap();
994 assert!(r.symbols.is_empty());
995 assert!(r.edges.is_empty());
996 }
997
998 #[test]
1003 fn ac50_partial_extraction_from_broken_source() {
1004 let source = r#"
1005fn valid() {}
1006fn broken( {{{
1007fn also_valid() {}
1008"#;
1009 let parser = RustParser::new();
1010 let result = parser
1011 .parse(source.as_bytes(), Path::new("broken.rs"))
1012 .expect("should not error on syntax errors");
1013 assert!(
1015 result.symbols.iter().any(|s| s.name == "valid"),
1016 "should find 'valid' function in broken source"
1017 );
1018 }
1019
1020 #[test]
1025 fn trait_impl_emits_implements_edge() {
1026 let source = "trait Display {} struct Foo; impl Display for Foo {}";
1027 let result = parse_rust(source);
1028 let implements = result
1029 .edges
1030 .iter()
1031 .find(|e| e.kind == EdgeKind::Implements)
1032 .expect("Implements edge not found");
1033 assert_eq!(implements.source, "test.rs::Foo");
1034 assert_eq!(implements.target, "test.rs::Display");
1035 }
1036
1037 #[test]
1042 fn type_alias_is_extracted() {
1043 let result = parse_rust("type MyAlias = u32;");
1044 assert_eq!(result.symbols.len(), 1);
1045 assert_eq!(result.symbols[0].name, "MyAlias");
1046 assert_eq!(result.symbols[0].kind, SymbolKind::TypeAlias);
1047 }
1048
1049 #[test]
1050 fn const_item_is_extracted() {
1051 let result = parse_rust("const MAX: u32 = 100;");
1052 assert_eq!(result.symbols.len(), 1);
1053 assert_eq!(result.symbols[0].name, "MAX");
1054 assert_eq!(result.symbols[0].kind, SymbolKind::Const);
1055 }
1056
1057 #[test]
1058 fn static_item_is_extracted_as_variable() {
1059 let result = parse_rust(r#"static GREETING: &str = "hello";"#);
1060 assert_eq!(result.symbols.len(), 1);
1061 assert_eq!(result.symbols[0].name, "GREETING");
1062 assert_eq!(result.symbols[0].kind, SymbolKind::Variable);
1063 }
1064
1065 #[test]
1066 fn macro_definition_is_extracted() {
1067 let result = parse_rust("macro_rules! my_macro { () => {} }");
1068 assert_eq!(result.symbols.len(), 1);
1069 assert_eq!(result.symbols[0].name, "my_macro");
1070 assert_eq!(result.symbols[0].kind, SymbolKind::Macro);
1071 }
1072
1073 #[test]
1078 fn async_fn_is_flagged() {
1079 let result = parse_rust("async fn fetch() {}");
1080 assert!(result.symbols[0].is_async);
1081 }
1082
1083 #[test]
1084 fn sync_fn_is_not_async() {
1085 let result = parse_rust("fn sync_fn() {}");
1086 assert!(!result.symbols[0].is_async);
1087 }
1088
1089 #[test]
1094 fn test_attribute_sets_is_test() {
1095 let result = parse_rust("#[test]\nfn my_test() {}");
1096 assert!(result.symbols[0].is_test);
1097 }
1098
1099 #[test]
1100 fn no_test_attribute_is_not_test() {
1101 let result = parse_rust("fn regular_fn() {}");
1102 assert!(!result.symbols[0].is_test);
1103 }
1104
1105 #[test]
1110 fn function_signature_includes_params_and_return_type() {
1111 let result = parse_rust("fn add(a: i32, b: i32) -> i32 { a + b }");
1112 let sig = result.symbols[0].signature.as_ref().expect("no signature");
1113 assert!(sig.contains("a: i32"));
1114 assert!(sig.contains("b: i32"));
1115 assert!(sig.contains("i32")); }
1117
1118 #[test]
1119 fn function_signature_without_return_type() {
1120 let result = parse_rust("fn greet(name: &str) {}");
1121 let sig = result.symbols[0].signature.as_ref().expect("no signature");
1122 assert!(sig.contains("name: &str"));
1123 }
1124
1125 #[test]
1130 fn location_is_one_based_line_numbers() {
1131 let result = parse_rust("fn foo() {}");
1132 let loc = &result.symbols[0].location;
1133 assert_eq!(loc.file.to_string_lossy(), "test.rs");
1134 assert_eq!(loc.line_start, 1);
1135 assert!(loc.line_end >= 1);
1136 }
1137
1138 #[test]
1143 fn multiple_top_level_items_all_extracted() {
1144 let source = r#"
1145fn foo() {}
1146struct Bar {}
1147enum Baz { A }
1148trait Qux {}
1149"#;
1150 let result = parse_rust(source);
1151 assert_eq!(result.symbols.len(), 4);
1152 assert!(result
1153 .symbols
1154 .iter()
1155 .any(|s| s.name == "foo" && s.kind == SymbolKind::Function));
1156 assert!(result
1157 .symbols
1158 .iter()
1159 .any(|s| s.name == "Bar" && s.kind == SymbolKind::Struct));
1160 assert!(result
1161 .symbols
1162 .iter()
1163 .any(|s| s.name == "Baz" && s.kind == SymbolKind::Enum));
1164 assert!(result
1165 .symbols
1166 .iter()
1167 .any(|s| s.name == "Qux" && s.kind == SymbolKind::Trait));
1168 let contains_count = result
1169 .edges
1170 .iter()
1171 .filter(|e| e.kind == EdgeKind::Contains)
1172 .count();
1173 assert_eq!(contains_count, 4);
1174 }
1175
1176 #[test]
1181 fn impl_with_multiple_methods() {
1182 let source = r#"
1183struct Counter;
1184impl Counter {
1185 fn new() -> Self { Counter }
1186 fn increment(&mut self) {}
1187 fn value(&self) -> u32 { 0 }
1188}
1189"#;
1190 let result = parse_rust(source);
1191 let methods: Vec<_> = result
1192 .symbols
1193 .iter()
1194 .filter(|s| s.kind == SymbolKind::Method)
1195 .collect();
1196 assert_eq!(methods.len(), 3);
1197 let child_of_count = result
1198 .edges
1199 .iter()
1200 .filter(|e| e.kind == EdgeKind::ChildOf)
1201 .count();
1202 assert_eq!(child_of_count, 3);
1203 }
1204
1205 #[test]
1210 fn ac11_use_scoped_identifier_extracts_raw_import() {
1211 let result = parse_rust("use crate::auth::validate;");
1212 assert_eq!(result.imports.len(), 1);
1213 let imp = &result.imports[0];
1214 assert_eq!(imp.specifier, "crate::auth::validate");
1215 assert_eq!(imp.names.len(), 1);
1216 assert_eq!(imp.names[0].name, "validate");
1217 assert!(!imp.is_namespace);
1218 }
1219
1220 #[test]
1221 fn use_simple_identifier_extracts_raw_import() {
1222 let result = parse_rust("use std::fmt;");
1223 assert_eq!(result.imports.len(), 1);
1224 let imp = &result.imports[0];
1225 assert_eq!(imp.specifier, "std::fmt");
1226 assert_eq!(imp.names[0].name, "fmt");
1227 }
1228
1229 #[test]
1234 fn ac12_pub_use_creates_reexport_export_entry() {
1235 let result = parse_rust("pub use self::greetings::hello;");
1236 assert_eq!(result.imports.len(), 1);
1237 let imp = &result.imports[0];
1238 assert_eq!(imp.specifier, "self::greetings::hello");
1239
1240 assert_eq!(result.exports.len(), 1);
1242 let exp = &result.exports[0];
1243 assert!(exp.is_reexport);
1244 assert_eq!(
1245 exp.source_specifier.as_deref(),
1246 Some("self::greetings::hello")
1247 );
1248 assert_eq!(exp.name, "hello");
1249 }
1250
1251 #[test]
1256 fn use_wildcard_sets_is_namespace() {
1257 let result = parse_rust("use foo::bar::*;");
1258 assert_eq!(result.imports.len(), 1);
1259 let imp = &result.imports[0];
1260 assert!(imp.is_namespace);
1261 assert_eq!(imp.specifier, "foo::bar");
1262 }
1263
1264 #[test]
1269 fn use_scoped_list_extracts_multiple_names() {
1270 let result = parse_rust("use foo::{A, B};");
1271 assert_eq!(result.imports.len(), 2);
1273 let specifiers: Vec<_> = result
1274 .imports
1275 .iter()
1276 .map(|i| i.specifier.as_str())
1277 .collect();
1278 assert!(
1279 specifiers.contains(&"foo::A"),
1280 "expected foo::A, got {specifiers:?}"
1281 );
1282 assert!(
1283 specifiers.contains(&"foo::B"),
1284 "expected foo::B, got {specifiers:?}"
1285 );
1286 }
1287
1288 #[test]
1293 fn use_as_clause_extracts_alias() {
1294 let result = parse_rust("use foo as bar;");
1295 assert_eq!(result.imports.len(), 1);
1296 let imp = &result.imports[0];
1297 assert_eq!(imp.specifier, "foo");
1298 assert_eq!(imp.names.len(), 1);
1299 assert_eq!(imp.names[0].name, "foo");
1300 assert_eq!(imp.names[0].alias.as_deref(), Some("bar"));
1301 }
1302
1303 #[test]
1308 fn ac13_mod_declaration_captured_with_mod_prefix() {
1309 let result = parse_rust("mod submodule;");
1310 assert_eq!(result.imports.len(), 1);
1311 let imp = &result.imports[0];
1312 assert_eq!(imp.specifier, "mod::submodule");
1313 assert!(imp.names.is_empty());
1314 }
1315
1316 #[test]
1317 fn inline_mod_not_captured_as_import() {
1318 let result = parse_rust("mod inline { fn inner() {} }");
1319 assert!(
1321 result.imports.is_empty(),
1322 "inline mod should not produce an import, got: {:?}",
1323 result.imports
1324 );
1325 }
1326
1327 #[test]
1332 fn integration_realistic_module() {
1333 let source = r#"
1334use std::fmt;
1335
1336pub struct Point {
1337 pub x: f64,
1338 pub y: f64,
1339}
1340
1341impl Point {
1342 pub fn new(x: f64, y: f64) -> Self {
1343 Point { x, y }
1344 }
1345
1346 pub fn distance(&self, other: &Point) -> f64 {
1347 ((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
1348 }
1349}
1350
1351impl fmt::Display for Point {
1352 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1353 write!(f, "({}, {})", self.x, self.y)
1354 }
1355}
1356
1357pub trait Shape {
1358 fn area(&self) -> f64;
1359}
1360
1361pub enum Color {
1362 Red,
1363 Green,
1364 Blue,
1365}
1366
1367pub const MAX_POINTS: usize = 1000;
1368
1369pub async fn fetch_data() -> Vec<Point> {
1370 vec![]
1371}
1372
1373#[test]
1374fn test_distance() {
1375 let a = Point { x: 0.0, y: 0.0 };
1376 let b = Point { x: 3.0, y: 4.0 };
1377 assert_eq!(a.distance(&b), 5.0);
1378}
1379"#;
1380 let result = parse_rust(source);
1381
1382 assert!(result
1384 .symbols
1385 .iter()
1386 .any(|s| s.name == "Point" && s.kind == SymbolKind::Struct));
1387 assert!(result
1388 .symbols
1389 .iter()
1390 .any(|s| s.name == "Shape" && s.kind == SymbolKind::Trait));
1391 assert!(result
1392 .symbols
1393 .iter()
1394 .any(|s| s.name == "Color" && s.kind == SymbolKind::Enum));
1395 assert!(result
1396 .symbols
1397 .iter()
1398 .any(|s| s.name == "MAX_POINTS" && s.kind == SymbolKind::Const));
1399 assert!(result
1400 .symbols
1401 .iter()
1402 .any(|s| s.name == "fetch_data" && s.is_async));
1403 assert!(result
1404 .symbols
1405 .iter()
1406 .any(|s| s.name == "test_distance" && s.is_test));
1407
1408 assert!(result.symbols.iter().any(|s| s.name == "new"
1410 && s.kind == SymbolKind::Method
1411 && s.qualified_name == "test.rs::Point.new"));
1412 assert!(result
1413 .symbols
1414 .iter()
1415 .any(|s| s.name == "distance" && s.kind == SymbolKind::Method));
1416
1417 assert!(result
1419 .symbols
1420 .iter()
1421 .any(|s| s.name == "fmt" && s.kind == SymbolKind::Method));
1422
1423 let point_sym = result.symbols.iter().find(|s| s.name == "Point").unwrap();
1425 assert_eq!(point_sym.visibility, Visibility::Public);
1426 assert!(point_sym.is_exported);
1427
1428 let implements = result
1430 .edges
1431 .iter()
1432 .filter(|e| e.kind == EdgeKind::Implements)
1433 .collect::<Vec<_>>();
1434 assert!(
1435 !implements.is_empty(),
1436 "should have at least one Implements edge"
1437 );
1438
1439 let child_of_count = result
1441 .edges
1442 .iter()
1443 .filter(|e| e.kind == EdgeKind::ChildOf)
1444 .count();
1445 assert!(
1446 child_of_count >= 3,
1447 "expected at least 3 ChildOf edges, got {child_of_count}"
1448 );
1449 }
1450}