1use std::{
2 collections::HashMap,
3 path::{Path, PathBuf},
4};
5
6use gitcortex_core::{
7 error::{GitCortexError, Result},
8 graph::{Edge, Node, NodeId, NodeMetadata, Span},
9 schema::{EdgeKind, NodeKind, Visibility},
10};
11use tree_sitter::{Node as TsNode, Parser};
12
13use super::{capture_definition, LanguageParser, ParseResult};
14
15pub struct RustParser {
18 language: tree_sitter::Language,
19}
20
21impl RustParser {
22 pub fn new() -> Self {
23 Self {
24 language: tree_sitter_rust::LANGUAGE.into(),
25 }
26 }
27}
28
29impl Default for RustParser {
30 fn default() -> Self {
31 Self::new()
32 }
33}
34
35impl LanguageParser for RustParser {
36 fn extensions(&self) -> &[&str] {
37 &["rs"]
38 }
39
40 fn parse(&self, path: &Path, source: &str) -> Result<ParseResult> {
41 let mut parser = Parser::new();
42 parser
43 .set_language(&self.language)
44 .map_err(|e| GitCortexError::Parse {
45 file: path.to_owned(),
46 message: e.to_string(),
47 })?;
48
49 let tree = parser
50 .parse(source, None)
51 .ok_or_else(|| GitCortexError::Parse {
52 file: path.to_owned(),
53 message: "tree-sitter returned no parse tree".into(),
54 })?;
55
56 let mut visitor = FileVisitor::new(path, source);
57 visitor.collect_names(tree.root_node());
60 visitor.visit_items(tree.root_node(), &[], None);
62 visitor.collect_imports(tree.root_node());
64
65 Ok(ParseResult {
66 nodes: visitor.nodes,
67 edges: visitor.edges,
68 deferred_calls: visitor.deferred_calls,
69 deferred_uses: visitor.deferred_uses,
70 deferred_implements: visitor.deferred_implements,
71 deferred_imports: visitor.deferred_imports,
72 deferred_inherits: Vec::new(),
73 deferred_throws: Vec::new(),
74 deferred_annotated: visitor.deferred_annotated,
75 })
76 }
77}
78
79struct FileVisitor<'src> {
82 source: &'src [u8],
83 file: PathBuf,
84 nodes: Vec<Node>,
85 edges: Vec<Edge>,
86 type_index: HashMap<String, NodeId>,
87 fn_index: HashMap<String, NodeId>,
88 deferred_calls: Vec<(NodeId, String)>,
89 deferred_uses: Vec<(NodeId, String)>,
90 deferred_implements: Vec<(NodeId, String)>,
91 deferred_imports: Vec<(NodeId, String)>,
92 deferred_annotated: Vec<(NodeId, String)>,
93}
94
95impl<'src> FileVisitor<'src> {
96 fn new(file: &Path, source: &'src str) -> Self {
97 Self {
98 source: source.as_bytes(),
99 file: file.to_owned(),
100 nodes: Vec::new(),
101 edges: Vec::new(),
102 type_index: HashMap::new(),
103 fn_index: HashMap::new(),
104 deferred_calls: Vec::new(),
105 deferred_uses: Vec::new(),
106 deferred_implements: Vec::new(),
107 deferred_imports: Vec::new(),
108 deferred_annotated: Vec::new(),
109 }
110 }
111
112 fn text<'t>(&self, node: TsNode<'t>) -> &'src str {
115 node.utf8_text(self.source).unwrap_or("")
116 }
117
118 fn field_text(&self, node: TsNode<'_>, field: &str) -> Option<String> {
119 node.child_by_field_name(field)
120 .and_then(|n| n.utf8_text(self.source).ok())
121 .map(str::to_owned)
122 }
123
124 fn span(node: TsNode<'_>) -> Span {
125 Span {
126 start_line: node.start_position().row as u32 + 1,
127 end_line: node.end_position().row as u32 + 1,
128 }
129 }
130
131 fn visibility(&self, node: TsNode<'_>) -> Visibility {
132 let mut cursor = node.walk();
133 for child in node.children(&mut cursor) {
134 if child.kind() == "visibility_modifier" {
135 let t = self.text(child);
136 return if t.contains("crate") {
137 Visibility::PubCrate
138 } else {
139 Visibility::Pub
140 };
141 }
142 }
143 Visibility::Private
144 }
145
146 fn is_async(&self, node: TsNode<'_>) -> bool {
147 let mut cursor = node.walk();
148 let result = node.children(&mut cursor).any(|c| c.kind() == "async");
149 result
150 }
151
152 fn is_unsafe(&self, node: TsNode<'_>) -> bool {
153 let mut cursor = node.walk();
154 let result = node.children(&mut cursor).any(|c| c.kind() == "unsafe");
155 result
156 }
157
158 fn is_const(&self, node: TsNode<'_>) -> bool {
159 let mut cursor = node.walk();
160 let result = node.children(&mut cursor).any(|c| c.kind() == "const");
161 result
162 }
163
164 fn collect_generic_bounds(&self, node: TsNode<'_>) -> Vec<String> {
167 let Some(type_params) = node.child_by_field_name("type_parameters") else {
168 return Vec::new();
169 };
170 let mut bounds = Vec::new();
171 let mut cursor = type_params.walk();
172 for child in type_params.named_children(&mut cursor) {
173 if child.kind() == "constrained_type_parameter" {
174 let bound_text = self.text(child).to_owned();
175 if !bound_text.is_empty() {
176 bounds.push(bound_text);
177 }
178 }
179 }
180 bounds
181 }
182
183 fn collect_attributes(&self, node: TsNode<'_>) -> Vec<String> {
187 let mut attrs = Vec::new();
188 let mut cursor = node.walk();
189 let Some(parent) = node.parent() else {
194 return attrs;
195 };
196 let mut found = false;
197 let mut pending: Vec<String> = Vec::new();
198 for sibling in parent.named_children(&mut cursor) {
199 if sibling.id() == node.id() {
200 found = true;
201 break;
202 }
203 if sibling.kind() == "attribute_item" {
204 if let Some(attr_name) = self.extract_attribute_name(sibling) {
205 pending.push(attr_name);
206 }
207 } else {
208 pending.clear();
211 }
212 }
213 if found {
214 attrs.extend(pending);
215 }
216 attrs
217 }
218
219 fn extract_attribute_name(&self, attr_item: TsNode<'_>) -> Option<String> {
223 let mut cursor = attr_item.walk();
227 for child in attr_item.named_children(&mut cursor) {
228 if child.kind() == "attribute" {
229 let mut inner = child.walk();
231 let path_node = child.named_children(&mut inner).next();
232 if let Some(p) = path_node {
233 return Some(self.text(p).to_owned());
234 }
235 }
236 }
237 None
238 }
239
240 fn qualified(scope: &[String], name: &str) -> String {
241 if scope.is_empty() {
242 format!("crate::{name}")
243 } else {
244 format!("crate::{}::{name}", scope.join("::"))
245 }
246 }
247
248 fn make_node(
249 &self,
250 id: NodeId,
251 kind: NodeKind,
252 name: String,
253 scope: &[String],
254 ts_node: TsNode<'_>,
255 ) -> Node {
256 Node {
257 id,
258 qualified_name: Self::qualified(scope, &name),
259 kind,
260 name,
261 file: self.file.clone(),
262 span: Self::span(ts_node),
263 metadata: NodeMetadata {
264 loc: (ts_node.end_position().row - ts_node.start_position().row + 1) as u32,
265 visibility: self.visibility(ts_node),
266 is_async: self.is_async(ts_node),
267 is_unsafe: self.is_unsafe(ts_node),
268 is_const: self.is_const(ts_node),
269 generic_bounds: self.collect_generic_bounds(ts_node),
270 definition: capture_definition(self.source, ts_node),
271 ..Default::default()
272 },
273 }
274 }
275
276 fn type_name(&self, node: TsNode<'_>) -> Option<String> {
277 match node.kind() {
278 "type_identifier" => Some(self.text(node).to_owned()),
279 "generic_type" => node
280 .child_by_field_name("type")
281 .map(|n| self.text(n).to_owned()),
282 "scoped_type_identifier" => node
283 .child_by_field_name("name")
284 .map(|n| self.text(n).to_owned()),
285 "reference_type" => node
286 .child_by_field_name("type")
287 .and_then(|n| self.type_name(n)),
288 "mutable_specifier" => None,
289 _ => Some(self.text(node).to_owned()),
290 }
291 }
292
293 fn collect_names(&mut self, node: TsNode<'_>) {
296 let mut cursor = node.walk();
297 let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
298 for child in children {
299 match child.kind() {
300 "struct_item" | "enum_item" | "trait_item" => {
301 if let Some(name) = self.field_text(child, "name") {
302 self.type_index.entry(name).or_default();
303 }
304 }
305 "function_item" => {
306 if let Some(name) = self.field_text(child, "name") {
307 self.fn_index.entry(name).or_default();
308 }
309 }
310 "impl_item" => {
311 }
315 "mod_item" => {
316 if let Some(body) = child.child_by_field_name("body") {
317 self.collect_names(body);
318 }
319 }
320 _ => {}
321 }
322 }
323 }
324
325 fn visit_items(&mut self, parent: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
328 let mut cursor = parent.walk();
329 let children: Vec<TsNode<'_>> = parent.named_children(&mut cursor).collect();
330 for child in children {
331 self.visit_item(child, scope, container_id.clone());
332 }
333 }
334
335 fn visit_item(&mut self, node: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
336 match node.kind() {
337 "function_item" => self.visit_function(node, scope, container_id, NodeKind::Function),
338 "struct_item" => self.visit_type_item(node, scope, container_id, NodeKind::Struct),
339 "enum_item" => self.visit_type_item(node, scope, container_id, NodeKind::Enum),
340 "trait_item" => self.visit_trait(node, scope, container_id),
341 "impl_item" => self.visit_impl(node, scope),
342 "mod_item" => self.visit_mod(node, scope, container_id),
343 "const_item" | "static_item" => self.visit_const(node, scope, container_id),
344 "type_item" => self.visit_type_alias(node, scope, container_id),
345 "macro_definition" => self.visit_macro_def(node, scope, container_id),
346 _ => {}
347 }
348 }
349
350 fn visit_function(
351 &mut self,
352 node: TsNode<'_>,
353 scope: &[String],
354 container_id: Option<NodeId>,
355 kind: NodeKind,
356 ) {
357 let Some(name) = self.field_text(node, "name") else {
358 return;
359 };
360 let id = if kind == NodeKind::Method {
364 NodeId::new()
365 } else {
366 self.fn_index
367 .get(&name)
368 .cloned()
369 .unwrap_or_else(NodeId::new)
370 };
371 let graph_node = self.make_node(id.clone(), kind, name, scope, node);
372
373 if let Some(cid) = container_id {
374 self.edges.push(Edge {
375 src: cid,
376 dst: id.clone(),
377 kind: EdgeKind::Contains,
378 });
379 }
380
381 self.collect_uses_edges(node, &id);
383
384 for attr_name in self.collect_attributes(node) {
386 self.deferred_annotated.push((id.clone(), attr_name));
387 }
388
389 self.nodes.push(graph_node);
390
391 if let Some(body) = node.child_by_field_name("body") {
393 self.collect_calls(body, &id);
394 }
395 }
396
397 fn collect_uses_edges(&mut self, fn_node: TsNode<'_>, fn_id: &NodeId) {
400 let mut type_names: Vec<String> = Vec::new();
401
402 if let Some(params) = fn_node.child_by_field_name("parameters") {
403 let mut cursor = params.walk();
404 for param in params.named_children(&mut cursor) {
405 if param.kind() == "parameter" {
406 if let Some(type_node) = param.child_by_field_name("type") {
407 if let Some(tname) = self.type_name(type_node) {
408 type_names.push(tname);
409 }
410 }
411 }
412 }
413 }
414
415 if let Some(ret_type) = fn_node.child_by_field_name("return_type") {
416 if let Some(tname) = self.type_name(ret_type) {
417 type_names.push(tname);
418 }
419 }
420
421 for tname in type_names {
422 if let Some(tid) = self.type_index.get(&tname).cloned() {
423 self.edges.push(Edge {
424 src: fn_id.clone(),
425 dst: tid,
426 kind: EdgeKind::Uses,
427 });
428 } else if !tname.is_empty()
429 && !is_primitive(&tname)
430 && !self
431 .deferred_uses
432 .iter()
433 .any(|(id, n)| id == fn_id && n == &tname)
434 {
435 self.deferred_uses.push((fn_id.clone(), tname));
436 }
437 }
438 }
439
440 fn collect_calls(&mut self, node: TsNode<'_>, caller_id: &NodeId) {
446 match node.kind() {
447 "call_expression" => {
448 if let Some(callee) = self.callee_name(node) {
449 self.record_call(caller_id.clone(), callee);
450 }
451 if let Some(args) = node.child_by_field_name("arguments") {
452 self.collect_calls(args, caller_id);
453 }
454 if let Some(func) = node.child_by_field_name("function") {
459 if let Some(value) = func.child_by_field_name("value") {
460 self.collect_calls(value, caller_id);
461 }
462 }
463 }
464 "method_call_expression" => {
465 if let Some(name_node) = node.child_by_field_name("name") {
468 let method = self.text(name_node).to_owned();
469 self.record_call(caller_id.clone(), method);
470 }
471 if let Some(args) = node.child_by_field_name("arguments") {
472 self.collect_calls(args, caller_id);
473 }
474 if let Some(recv) = node.child_by_field_name("receiver") {
475 self.collect_calls(recv, caller_id);
476 }
477 }
478 _ => {
479 let mut cursor = node.walk();
480 let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
481 for child in children {
482 self.collect_calls(child, caller_id);
483 }
484 }
485 }
486 }
487
488 fn callee_name(&self, call_expr: TsNode<'_>) -> Option<String> {
490 let func = call_expr.child_by_field_name("function")?;
491 match func.kind() {
492 "identifier" => Some(self.text(func).to_owned()),
493 "scoped_identifier" => func
494 .child_by_field_name("name")
495 .and_then(|n| n.utf8_text(self.source).ok())
496 .map(str::to_owned),
497 "field_expression" => func
498 .child_by_field_name("field")
499 .and_then(|n| n.utf8_text(self.source).ok())
500 .map(str::to_owned),
501 _ => None,
502 }
503 }
504
505 fn record_call(&mut self, caller_id: NodeId, callee_name: String) {
507 if callee_name.is_empty() {
508 return;
509 }
510 if let Some(callee_id) = self.fn_index.get(&callee_name).cloned() {
511 let edge = Edge {
512 src: caller_id,
513 dst: callee_id,
514 kind: EdgeKind::Calls,
515 };
516 if !self.edges.contains(&edge) {
517 self.edges.push(edge);
518 }
519 } else if !self
520 .deferred_calls
521 .iter()
522 .any(|(c, n)| c == &caller_id && n == &callee_name)
523 {
524 self.deferred_calls.push((caller_id, callee_name));
525 }
526 }
527
528 fn visit_type_item(
529 &mut self,
530 node: TsNode<'_>,
531 scope: &[String],
532 container_id: Option<NodeId>,
533 kind: NodeKind,
534 ) {
535 let Some(name) = self.field_text(node, "name") else {
536 return;
537 };
538 let id = self
539 .type_index
540 .get(&name)
541 .cloned()
542 .unwrap_or_else(NodeId::new);
543 let graph_node = self.make_node(id.clone(), kind, name, scope, node);
544 if let Some(cid) = container_id {
545 self.edges.push(Edge {
546 src: cid,
547 dst: id.clone(),
548 kind: EdgeKind::Contains,
549 });
550 }
551 for attr_name in self.collect_attributes(node) {
552 self.deferred_annotated.push((id.clone(), attr_name));
553 }
554 self.nodes.push(graph_node);
555 }
556
557 fn visit_trait(&mut self, node: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
558 let Some(name) = self.field_text(node, "name") else {
559 return;
560 };
561 let id = self
562 .type_index
563 .get(&name)
564 .cloned()
565 .unwrap_or_else(NodeId::new);
566 let graph_node = self.make_node(id.clone(), NodeKind::Trait, name.clone(), scope, node);
567 if let Some(cid) = container_id {
568 self.edges.push(Edge {
569 src: cid,
570 dst: id.clone(),
571 kind: EdgeKind::Contains,
572 });
573 }
574 for attr_name in self.collect_attributes(node) {
575 self.deferred_annotated.push((id.clone(), attr_name));
576 }
577 self.nodes.push(graph_node);
578
579 if let Some(body) = node.child_by_field_name("body") {
580 let mut new_scope = scope.to_vec();
581 new_scope.push(name);
582 self.visit_items(body, &new_scope, Some(id));
583 }
584 }
585
586 fn visit_impl(&mut self, node: TsNode<'_>, scope: &[String]) {
587 let type_node = node.child_by_field_name("type");
588 let type_name = type_node.and_then(|n| self.type_name(n));
589 let Some(type_name) = type_name else { return };
590 let type_id = self.type_index.get(&type_name).cloned();
591
592 if let Some(trait_node) = node.child_by_field_name("trait") {
593 if let Some(trait_name) = self.type_name(trait_node) {
594 let trait_id = self.type_index.get(&trait_name).cloned();
595 match (type_id.clone(), trait_id) {
596 (Some(tid), Some(trid)) => {
597 self.edges.push(Edge {
598 src: tid,
599 dst: trid,
600 kind: EdgeKind::Implements,
601 });
602 }
603 (Some(tid), None)
604 if !is_primitive(&trait_name)
605 && !self
606 .deferred_implements
607 .iter()
608 .any(|(id, n)| id == &tid && n == &trait_name) =>
609 {
610 self.deferred_implements.push((tid, trait_name));
611 }
612 _ => {}
613 }
614 }
615 }
616
617 if let Some(body) = node.child_by_field_name("body") {
618 let mut cursor = body.walk();
619 let children: Vec<TsNode<'_>> = body.named_children(&mut cursor).collect();
620 let mut impl_scope = scope.to_vec();
621 impl_scope.push(type_name);
622
623 for child in children {
624 if child.kind() == "function_item" {
625 self.visit_function(child, &impl_scope, type_id.clone(), NodeKind::Method);
626 }
627 }
628 }
629 }
630
631 fn visit_mod(&mut self, node: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
632 let Some(name) = self.field_text(node, "name") else {
633 return;
634 };
635 let id = NodeId::new();
636 let graph_node = self.make_node(id.clone(), NodeKind::Module, name.clone(), scope, node);
637 if let Some(cid) = container_id {
638 self.edges.push(Edge {
639 src: cid,
640 dst: id.clone(),
641 kind: EdgeKind::Contains,
642 });
643 }
644 self.nodes.push(graph_node);
645
646 if let Some(body) = node.child_by_field_name("body") {
647 let mut new_scope = scope.to_vec();
648 new_scope.push(name);
649 self.visit_items(body, &new_scope, Some(id));
650 }
651 }
652
653 fn visit_const(&mut self, node: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
654 let Some(name) = self.field_text(node, "name") else {
655 return;
656 };
657 let id = NodeId::new();
658 let graph_node = self.make_node(id.clone(), NodeKind::Constant, name, scope, node);
659 if let Some(cid) = container_id {
660 self.edges.push(Edge {
661 src: cid,
662 dst: id.clone(),
663 kind: EdgeKind::Contains,
664 });
665 }
666 self.nodes.push(graph_node);
667 }
668
669 fn visit_type_alias(
670 &mut self,
671 node: TsNode<'_>,
672 scope: &[String],
673 container_id: Option<NodeId>,
674 ) {
675 let Some(name) = self.field_text(node, "name") else {
676 return;
677 };
678 let id = NodeId::new();
679 let graph_node = self.make_node(id.clone(), NodeKind::TypeAlias, name, scope, node);
680 if let Some(cid) = container_id {
681 self.edges.push(Edge {
682 src: cid,
683 dst: id.clone(),
684 kind: EdgeKind::Contains,
685 });
686 }
687 self.nodes.push(graph_node);
688 }
689
690 fn visit_macro_def(
691 &mut self,
692 node: TsNode<'_>,
693 scope: &[String],
694 container_id: Option<NodeId>,
695 ) {
696 let Some(name) = self.field_text(node, "name") else {
697 return;
698 };
699 let id = NodeId::new();
700 let graph_node = self.make_node(id.clone(), NodeKind::Macro, name, scope, node);
701 if let Some(cid) = container_id {
702 self.edges.push(Edge {
703 src: cid,
704 dst: id.clone(),
705 kind: EdgeKind::Contains,
706 });
707 }
708 self.nodes.push(graph_node);
709 }
710
711 fn collect_imports(&mut self, root: TsNode<'_>) {
714 let mut cursor = root.walk();
715 for child in root.named_children(&mut cursor) {
716 if child.kind() == "use_declaration" {
717 if let Some(arg) = child.child_by_field_name("argument") {
719 self.collect_import_leaves(arg);
720 }
721 } else if child.kind() == "mod_item" {
722 if let Some(body) = child.child_by_field_name("body") {
723 self.collect_imports(body);
724 }
725 }
726 }
727 }
728
729 fn collect_import_leaves(&mut self, node: TsNode<'_>) {
730 match node.kind() {
731 "identifier" | "type_identifier" => {
732 let name = self.text(node).to_owned();
733 if !name.is_empty()
734 && !is_primitive(&name)
735 && name != "self"
736 && name != "super"
737 && name != "crate"
738 {
739 let placeholder = NodeId::new();
741 self.deferred_imports.push((placeholder, name));
742 }
743 }
744 "use_list" => {
745 let mut cursor = node.walk();
746 for child in node.named_children(&mut cursor) {
747 self.collect_import_leaves(child);
748 }
749 }
750 "scoped_identifier" | "scoped_use_list" => {
751 let mut cursor = node.walk();
753 for child in node.named_children(&mut cursor) {
754 self.collect_import_leaves(child);
755 }
756 }
757 "use_as_clause" => {
758 if let Some(alias) = node.child_by_field_name("alias") {
760 self.collect_import_leaves(alias);
761 }
762 }
763 _ => {}
764 }
765 }
766}
767
768fn is_primitive(name: &str) -> bool {
769 matches!(
770 name,
771 "bool"
772 | "char"
773 | "str"
774 | "i8"
775 | "i16"
776 | "i32"
777 | "i64"
778 | "i128"
779 | "isize"
780 | "u8"
781 | "u16"
782 | "u32"
783 | "u64"
784 | "u128"
785 | "usize"
786 | "f32"
787 | "f64"
788 | "String"
789 | "Vec"
790 | "Option"
791 | "Result"
792 | "Box"
793 | "Rc"
794 | "Arc"
795 | "Cell"
796 | "RefCell"
797 | "Cow"
798 | "HashMap"
799 | "HashSet"
800 | "BTreeMap"
801 | "BTreeSet"
802 | "PathBuf"
803 | "Path"
804 | "OsString"
805 | "OsStr"
806 | "Send"
807 | "Sync"
808 | "Sized"
809 | "Clone"
810 | "Copy"
811 | "Debug"
812 | "Display"
813 | "Default"
814 | "PartialEq"
815 | "Eq"
816 | "PartialOrd"
817 | "Ord"
818 | "Hash"
819 | "Iterator"
820 | "Into"
821 | "From"
822 | "AsRef"
823 | "AsMut"
824 | "Deref"
825 | "DerefMut"
826 | "Error"
827 | "Write"
828 | "Read"
829 | "Seek"
830 | "Self"
831 | "()"
832 | "_"
833 )
834}
835
836#[cfg(test)]
839mod tests {
840 use std::path::Path;
841
842 use gitcortex_core::{
843 graph::{Edge, Node},
844 schema::{EdgeKind, NodeKind},
845 };
846
847 use super::RustParser;
848 use crate::parser::LanguageParser;
849
850 fn parse(src: &str) -> (Vec<Node>, Vec<Edge>) {
851 let r = RustParser::new().parse(Path::new("test.rs"), src).unwrap();
852 (r.nodes, r.edges)
853 }
854
855 #[test]
856 fn parses_free_function() {
857 let (nodes, _) = parse("pub fn greet(name: &str) -> String { name.into() }");
858 assert_eq!(nodes.len(), 1);
859 assert_eq!(nodes[0].kind, NodeKind::Function);
860 assert_eq!(nodes[0].name, "greet");
861 }
862
863 #[test]
864 fn parses_struct() {
865 let (nodes, _) = parse("pub struct Person { pub name: String }");
866 let structs: Vec<_> = nodes
867 .iter()
868 .filter(|n| n.kind == NodeKind::Struct)
869 .collect();
870 assert_eq!(structs.len(), 1);
871 assert_eq!(structs[0].name, "Person");
872 }
873
874 #[test]
875 fn parses_trait_impl_and_method() {
876 let src = r#"
877pub trait Greet { fn greet(&self) -> String; }
878pub struct Person { pub name: String }
879impl Greet for Person {
880 fn greet(&self) -> String { self.name.clone() }
881}
882"#;
883 let (nodes, edges) = parse(src);
884
885 let traits: Vec<_> = nodes.iter().filter(|n| n.kind == NodeKind::Trait).collect();
886 let structs: Vec<_> = nodes
887 .iter()
888 .filter(|n| n.kind == NodeKind::Struct)
889 .collect();
890 let methods: Vec<_> = nodes
891 .iter()
892 .filter(|n| n.kind == NodeKind::Method)
893 .collect();
894 let impl_edges: Vec<_> = edges
895 .iter()
896 .filter(|e| e.kind == EdgeKind::Implements)
897 .collect();
898
899 assert_eq!(traits.len(), 1, "expected Greet trait");
900 assert_eq!(structs.len(), 1, "expected Person struct");
901 assert_eq!(methods.len(), 1, "expected greet method");
902 assert_eq!(impl_edges.len(), 1, "expected Implements edge");
903 }
904
905 #[test]
906 fn parses_module_with_items() {
907 let src = r#"
908pub mod utils {
909 pub fn helper() {}
910 pub struct Config {}
911}
912"#;
913 let (nodes, edges) = parse(src);
914
915 let mods: Vec<_> = nodes
916 .iter()
917 .filter(|n| n.kind == NodeKind::Module)
918 .collect();
919 let fns: Vec<_> = nodes
920 .iter()
921 .filter(|n| n.kind == NodeKind::Function)
922 .collect();
923 let contains: Vec<_> = edges
924 .iter()
925 .filter(|e| e.kind == EdgeKind::Contains)
926 .collect();
927
928 assert_eq!(mods.len(), 1, "expected utils module");
929 assert_eq!(fns.len(), 1, "expected helper function");
930 assert!(!contains.is_empty(), "expected Contains edges");
931 }
932
933 #[test]
934 fn qualified_name_includes_module_path() {
935 let src = r#"
936pub mod inner {
937 pub fn foo() {}
938}
939"#;
940 let (nodes, _) = parse(src);
941 let foo = nodes.iter().find(|n| n.name == "foo").unwrap();
942 assert_eq!(foo.qualified_name, "crate::inner::foo");
943 }
944
945 #[test]
946 fn detects_intra_file_calls() {
947 let src = r#"
948pub fn caller() { callee(); }
949pub fn callee() {}
950"#;
951 let (_, edges) = parse(src);
952 let calls: Vec<_> = edges.iter().filter(|e| e.kind == EdgeKind::Calls).collect();
953 assert_eq!(calls.len(), 1, "expected one Calls edge");
954 }
955
956 #[test]
957 fn detects_uses_edges_for_param_types() {
958 let src = r#"
959pub struct Config {}
960pub fn run(cfg: Config) {}
961"#;
962 let (_, edges) = parse(src);
963 let uses: Vec<_> = edges.iter().filter(|e| e.kind == EdgeKind::Uses).collect();
964 assert_eq!(uses.len(), 1, "expected one Uses edge from run to Config");
965 }
966
967 #[test]
968 fn deferred_calls_capture_unknown_callees() {
969 let src = r#"
970pub fn caller() { external_fn(); }
971"#;
972 let result = RustParser::new().parse(Path::new("test.rs"), src).unwrap();
973 assert_eq!(result.deferred_calls.len(), 1);
974 assert_eq!(result.deferred_calls[0].1, "external_fn");
975 }
976}