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::{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 ..Default::default()
271 },
272 }
273 }
274
275 fn type_name(&self, node: TsNode<'_>) -> Option<String> {
276 match node.kind() {
277 "type_identifier" => Some(self.text(node).to_owned()),
278 "generic_type" => node
279 .child_by_field_name("type")
280 .map(|n| self.text(n).to_owned()),
281 "scoped_type_identifier" => node
282 .child_by_field_name("name")
283 .map(|n| self.text(n).to_owned()),
284 "reference_type" => node
285 .child_by_field_name("type")
286 .and_then(|n| self.type_name(n)),
287 "mutable_specifier" => None,
288 _ => Some(self.text(node).to_owned()),
289 }
290 }
291
292 fn collect_names(&mut self, node: TsNode<'_>) {
295 let mut cursor = node.walk();
296 let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
297 for child in children {
298 match child.kind() {
299 "struct_item" | "enum_item" | "trait_item" => {
300 if let Some(name) = self.field_text(child, "name") {
301 self.type_index.entry(name).or_default();
302 }
303 }
304 "function_item" => {
305 if let Some(name) = self.field_text(child, "name") {
306 self.fn_index.entry(name).or_default();
307 }
308 }
309 "impl_item" => {
310 }
314 "mod_item" => {
315 if let Some(body) = child.child_by_field_name("body") {
316 self.collect_names(body);
317 }
318 }
319 _ => {}
320 }
321 }
322 }
323
324 fn visit_items(&mut self, parent: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
327 let mut cursor = parent.walk();
328 let children: Vec<TsNode<'_>> = parent.named_children(&mut cursor).collect();
329 for child in children {
330 self.visit_item(child, scope, container_id.clone());
331 }
332 }
333
334 fn visit_item(&mut self, node: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
335 match node.kind() {
336 "function_item" => self.visit_function(node, scope, container_id, NodeKind::Function),
337 "struct_item" => self.visit_type_item(node, scope, container_id, NodeKind::Struct),
338 "enum_item" => self.visit_type_item(node, scope, container_id, NodeKind::Enum),
339 "trait_item" => self.visit_trait(node, scope, container_id),
340 "impl_item" => self.visit_impl(node, scope),
341 "mod_item" => self.visit_mod(node, scope, container_id),
342 "const_item" | "static_item" => self.visit_const(node, scope, container_id),
343 "type_item" => self.visit_type_alias(node, scope, container_id),
344 "macro_definition" => self.visit_macro_def(node, scope, container_id),
345 _ => {}
346 }
347 }
348
349 fn visit_function(
350 &mut self,
351 node: TsNode<'_>,
352 scope: &[String],
353 container_id: Option<NodeId>,
354 kind: NodeKind,
355 ) {
356 let Some(name) = self.field_text(node, "name") else {
357 return;
358 };
359 let id = if kind == NodeKind::Method {
363 NodeId::new()
364 } else {
365 self.fn_index
366 .get(&name)
367 .cloned()
368 .unwrap_or_else(NodeId::new)
369 };
370 let graph_node = self.make_node(id.clone(), kind, name, scope, node);
371
372 if let Some(cid) = container_id {
373 self.edges.push(Edge {
374 src: cid,
375 dst: id.clone(),
376 kind: EdgeKind::Contains,
377 });
378 }
379
380 self.collect_uses_edges(node, &id);
382
383 for attr_name in self.collect_attributes(node) {
385 self.deferred_annotated.push((id.clone(), attr_name));
386 }
387
388 self.nodes.push(graph_node);
389
390 if let Some(body) = node.child_by_field_name("body") {
392 self.collect_calls(body, &id);
393 }
394 }
395
396 fn collect_uses_edges(&mut self, fn_node: TsNode<'_>, fn_id: &NodeId) {
399 let mut type_names: Vec<String> = Vec::new();
400
401 if let Some(params) = fn_node.child_by_field_name("parameters") {
402 let mut cursor = params.walk();
403 for param in params.named_children(&mut cursor) {
404 if param.kind() == "parameter" {
405 if let Some(type_node) = param.child_by_field_name("type") {
406 if let Some(tname) = self.type_name(type_node) {
407 type_names.push(tname);
408 }
409 }
410 }
411 }
412 }
413
414 if let Some(ret_type) = fn_node.child_by_field_name("return_type") {
415 if let Some(tname) = self.type_name(ret_type) {
416 type_names.push(tname);
417 }
418 }
419
420 for tname in type_names {
421 if let Some(tid) = self.type_index.get(&tname).cloned() {
422 self.edges.push(Edge {
423 src: fn_id.clone(),
424 dst: tid,
425 kind: EdgeKind::Uses,
426 });
427 } else if !tname.is_empty()
428 && !is_primitive(&tname)
429 && !self
430 .deferred_uses
431 .iter()
432 .any(|(id, n)| id == fn_id && n == &tname)
433 {
434 self.deferred_uses.push((fn_id.clone(), tname));
435 }
436 }
437 }
438
439 fn collect_calls(&mut self, node: TsNode<'_>, caller_id: &NodeId) {
445 match node.kind() {
446 "call_expression" => {
447 if let Some(callee) = self.callee_name(node) {
448 self.record_call(caller_id.clone(), callee);
449 }
450 if let Some(args) = node.child_by_field_name("arguments") {
451 self.collect_calls(args, caller_id);
452 }
453 if let Some(func) = node.child_by_field_name("function") {
458 if let Some(value) = func.child_by_field_name("value") {
459 self.collect_calls(value, caller_id);
460 }
461 }
462 }
463 "method_call_expression" => {
464 if let Some(name_node) = node.child_by_field_name("name") {
467 let method = self.text(name_node).to_owned();
468 self.record_call(caller_id.clone(), method);
469 }
470 if let Some(args) = node.child_by_field_name("arguments") {
471 self.collect_calls(args, caller_id);
472 }
473 if let Some(recv) = node.child_by_field_name("receiver") {
474 self.collect_calls(recv, caller_id);
475 }
476 }
477 _ => {
478 let mut cursor = node.walk();
479 let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
480 for child in children {
481 self.collect_calls(child, caller_id);
482 }
483 }
484 }
485 }
486
487 fn callee_name(&self, call_expr: TsNode<'_>) -> Option<String> {
489 let func = call_expr.child_by_field_name("function")?;
490 match func.kind() {
491 "identifier" => Some(self.text(func).to_owned()),
492 "scoped_identifier" => func
493 .child_by_field_name("name")
494 .and_then(|n| n.utf8_text(self.source).ok())
495 .map(str::to_owned),
496 "field_expression" => func
497 .child_by_field_name("field")
498 .and_then(|n| n.utf8_text(self.source).ok())
499 .map(str::to_owned),
500 _ => None,
501 }
502 }
503
504 fn record_call(&mut self, caller_id: NodeId, callee_name: String) {
506 if callee_name.is_empty() {
507 return;
508 }
509 if let Some(callee_id) = self.fn_index.get(&callee_name).cloned() {
510 let edge = Edge {
511 src: caller_id,
512 dst: callee_id,
513 kind: EdgeKind::Calls,
514 };
515 if !self.edges.contains(&edge) {
516 self.edges.push(edge);
517 }
518 } else if !self
519 .deferred_calls
520 .iter()
521 .any(|(c, n)| c == &caller_id && n == &callee_name)
522 {
523 self.deferred_calls.push((caller_id, callee_name));
524 }
525 }
526
527 fn visit_type_item(
528 &mut self,
529 node: TsNode<'_>,
530 scope: &[String],
531 container_id: Option<NodeId>,
532 kind: NodeKind,
533 ) {
534 let Some(name) = self.field_text(node, "name") else {
535 return;
536 };
537 let id = self
538 .type_index
539 .get(&name)
540 .cloned()
541 .unwrap_or_else(NodeId::new);
542 let graph_node = self.make_node(id.clone(), kind, name, scope, node);
543 if let Some(cid) = container_id {
544 self.edges.push(Edge {
545 src: cid,
546 dst: id.clone(),
547 kind: EdgeKind::Contains,
548 });
549 }
550 for attr_name in self.collect_attributes(node) {
551 self.deferred_annotated.push((id.clone(), attr_name));
552 }
553 self.nodes.push(graph_node);
554 }
555
556 fn visit_trait(&mut self, node: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
557 let Some(name) = self.field_text(node, "name") else {
558 return;
559 };
560 let id = self
561 .type_index
562 .get(&name)
563 .cloned()
564 .unwrap_or_else(NodeId::new);
565 let graph_node = self.make_node(id.clone(), NodeKind::Trait, name.clone(), scope, node);
566 if let Some(cid) = container_id {
567 self.edges.push(Edge {
568 src: cid,
569 dst: id.clone(),
570 kind: EdgeKind::Contains,
571 });
572 }
573 for attr_name in self.collect_attributes(node) {
574 self.deferred_annotated.push((id.clone(), attr_name));
575 }
576 self.nodes.push(graph_node);
577
578 if let Some(body) = node.child_by_field_name("body") {
579 let mut new_scope = scope.to_vec();
580 new_scope.push(name);
581 self.visit_items(body, &new_scope, Some(id));
582 }
583 }
584
585 fn visit_impl(&mut self, node: TsNode<'_>, scope: &[String]) {
586 let type_node = node.child_by_field_name("type");
587 let type_name = type_node.and_then(|n| self.type_name(n));
588 let Some(type_name) = type_name else { return };
589 let type_id = self.type_index.get(&type_name).cloned();
590
591 if let Some(trait_node) = node.child_by_field_name("trait") {
592 if let Some(trait_name) = self.type_name(trait_node) {
593 let trait_id = self.type_index.get(&trait_name).cloned();
594 match (type_id.clone(), trait_id) {
595 (Some(tid), Some(trid)) => {
596 self.edges.push(Edge {
597 src: tid,
598 dst: trid,
599 kind: EdgeKind::Implements,
600 });
601 }
602 (Some(tid), None)
603 if !is_primitive(&trait_name)
604 && !self
605 .deferred_implements
606 .iter()
607 .any(|(id, n)| id == &tid && n == &trait_name) =>
608 {
609 self.deferred_implements.push((tid, trait_name));
610 }
611 _ => {}
612 }
613 }
614 }
615
616 if let Some(body) = node.child_by_field_name("body") {
617 let mut cursor = body.walk();
618 let children: Vec<TsNode<'_>> = body.named_children(&mut cursor).collect();
619 let mut impl_scope = scope.to_vec();
620 impl_scope.push(type_name);
621
622 for child in children {
623 if child.kind() == "function_item" {
624 self.visit_function(child, &impl_scope, type_id.clone(), NodeKind::Method);
625 }
626 }
627 }
628 }
629
630 fn visit_mod(&mut self, node: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
631 let Some(name) = self.field_text(node, "name") else {
632 return;
633 };
634 let id = NodeId::new();
635 let graph_node = self.make_node(id.clone(), NodeKind::Module, name.clone(), scope, node);
636 if let Some(cid) = container_id {
637 self.edges.push(Edge {
638 src: cid,
639 dst: id.clone(),
640 kind: EdgeKind::Contains,
641 });
642 }
643 self.nodes.push(graph_node);
644
645 if let Some(body) = node.child_by_field_name("body") {
646 let mut new_scope = scope.to_vec();
647 new_scope.push(name);
648 self.visit_items(body, &new_scope, Some(id));
649 }
650 }
651
652 fn visit_const(&mut self, node: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
653 let Some(name) = self.field_text(node, "name") else {
654 return;
655 };
656 let id = NodeId::new();
657 let graph_node = self.make_node(id.clone(), NodeKind::Constant, name, scope, node);
658 if let Some(cid) = container_id {
659 self.edges.push(Edge {
660 src: cid,
661 dst: id.clone(),
662 kind: EdgeKind::Contains,
663 });
664 }
665 self.nodes.push(graph_node);
666 }
667
668 fn visit_type_alias(
669 &mut self,
670 node: TsNode<'_>,
671 scope: &[String],
672 container_id: Option<NodeId>,
673 ) {
674 let Some(name) = self.field_text(node, "name") else {
675 return;
676 };
677 let id = NodeId::new();
678 let graph_node = self.make_node(id.clone(), NodeKind::TypeAlias, name, scope, node);
679 if let Some(cid) = container_id {
680 self.edges.push(Edge {
681 src: cid,
682 dst: id.clone(),
683 kind: EdgeKind::Contains,
684 });
685 }
686 self.nodes.push(graph_node);
687 }
688
689 fn visit_macro_def(
690 &mut self,
691 node: TsNode<'_>,
692 scope: &[String],
693 container_id: Option<NodeId>,
694 ) {
695 let Some(name) = self.field_text(node, "name") else {
696 return;
697 };
698 let id = NodeId::new();
699 let graph_node = self.make_node(id.clone(), NodeKind::Macro, name, scope, node);
700 if let Some(cid) = container_id {
701 self.edges.push(Edge {
702 src: cid,
703 dst: id.clone(),
704 kind: EdgeKind::Contains,
705 });
706 }
707 self.nodes.push(graph_node);
708 }
709
710 fn collect_imports(&mut self, root: TsNode<'_>) {
713 let mut cursor = root.walk();
714 for child in root.named_children(&mut cursor) {
715 if child.kind() == "use_declaration" {
716 if let Some(arg) = child.child_by_field_name("argument") {
718 self.collect_import_leaves(arg);
719 }
720 } else if child.kind() == "mod_item" {
721 if let Some(body) = child.child_by_field_name("body") {
722 self.collect_imports(body);
723 }
724 }
725 }
726 }
727
728 fn collect_import_leaves(&mut self, node: TsNode<'_>) {
729 match node.kind() {
730 "identifier" | "type_identifier" => {
731 let name = self.text(node).to_owned();
732 if !name.is_empty()
733 && !is_primitive(&name)
734 && name != "self"
735 && name != "super"
736 && name != "crate"
737 {
738 let placeholder = NodeId::new();
740 self.deferred_imports.push((placeholder, name));
741 }
742 }
743 "use_list" => {
744 let mut cursor = node.walk();
745 for child in node.named_children(&mut cursor) {
746 self.collect_import_leaves(child);
747 }
748 }
749 "scoped_identifier" | "scoped_use_list" => {
750 let mut cursor = node.walk();
752 for child in node.named_children(&mut cursor) {
753 self.collect_import_leaves(child);
754 }
755 }
756 "use_as_clause" => {
757 if let Some(alias) = node.child_by_field_name("alias") {
759 self.collect_import_leaves(alias);
760 }
761 }
762 _ => {}
763 }
764 }
765}
766
767fn is_primitive(name: &str) -> bool {
768 matches!(
769 name,
770 "bool"
771 | "char"
772 | "str"
773 | "i8"
774 | "i16"
775 | "i32"
776 | "i64"
777 | "i128"
778 | "isize"
779 | "u8"
780 | "u16"
781 | "u32"
782 | "u64"
783 | "u128"
784 | "usize"
785 | "f32"
786 | "f64"
787 | "String"
788 | "Vec"
789 | "Option"
790 | "Result"
791 | "Box"
792 | "Rc"
793 | "Arc"
794 | "Cell"
795 | "RefCell"
796 | "Cow"
797 | "HashMap"
798 | "HashSet"
799 | "BTreeMap"
800 | "BTreeSet"
801 | "PathBuf"
802 | "Path"
803 | "OsString"
804 | "OsStr"
805 | "Send"
806 | "Sync"
807 | "Sized"
808 | "Clone"
809 | "Copy"
810 | "Debug"
811 | "Display"
812 | "Default"
813 | "PartialEq"
814 | "Eq"
815 | "PartialOrd"
816 | "Ord"
817 | "Hash"
818 | "Iterator"
819 | "Into"
820 | "From"
821 | "AsRef"
822 | "AsMut"
823 | "Deref"
824 | "DerefMut"
825 | "Error"
826 | "Write"
827 | "Read"
828 | "Seek"
829 | "Self"
830 | "()"
831 | "_"
832 )
833}
834
835#[cfg(test)]
838mod tests {
839 use std::path::Path;
840
841 use gitcortex_core::{
842 graph::{Edge, Node},
843 schema::{EdgeKind, NodeKind},
844 };
845
846 use super::RustParser;
847 use crate::parser::LanguageParser;
848
849 fn parse(src: &str) -> (Vec<Node>, Vec<Edge>) {
850 let r = RustParser::new().parse(Path::new("test.rs"), src).unwrap();
851 (r.nodes, r.edges)
852 }
853
854 #[test]
855 fn parses_free_function() {
856 let (nodes, _) = parse("pub fn greet(name: &str) -> String { name.into() }");
857 assert_eq!(nodes.len(), 1);
858 assert_eq!(nodes[0].kind, NodeKind::Function);
859 assert_eq!(nodes[0].name, "greet");
860 }
861
862 #[test]
863 fn parses_struct() {
864 let (nodes, _) = parse("pub struct Person { pub name: String }");
865 let structs: Vec<_> = nodes
866 .iter()
867 .filter(|n| n.kind == NodeKind::Struct)
868 .collect();
869 assert_eq!(structs.len(), 1);
870 assert_eq!(structs[0].name, "Person");
871 }
872
873 #[test]
874 fn parses_trait_impl_and_method() {
875 let src = r#"
876pub trait Greet { fn greet(&self) -> String; }
877pub struct Person { pub name: String }
878impl Greet for Person {
879 fn greet(&self) -> String { self.name.clone() }
880}
881"#;
882 let (nodes, edges) = parse(src);
883
884 let traits: Vec<_> = nodes.iter().filter(|n| n.kind == NodeKind::Trait).collect();
885 let structs: Vec<_> = nodes
886 .iter()
887 .filter(|n| n.kind == NodeKind::Struct)
888 .collect();
889 let methods: Vec<_> = nodes
890 .iter()
891 .filter(|n| n.kind == NodeKind::Method)
892 .collect();
893 let impl_edges: Vec<_> = edges
894 .iter()
895 .filter(|e| e.kind == EdgeKind::Implements)
896 .collect();
897
898 assert_eq!(traits.len(), 1, "expected Greet trait");
899 assert_eq!(structs.len(), 1, "expected Person struct");
900 assert_eq!(methods.len(), 1, "expected greet method");
901 assert_eq!(impl_edges.len(), 1, "expected Implements edge");
902 }
903
904 #[test]
905 fn parses_module_with_items() {
906 let src = r#"
907pub mod utils {
908 pub fn helper() {}
909 pub struct Config {}
910}
911"#;
912 let (nodes, edges) = parse(src);
913
914 let mods: Vec<_> = nodes
915 .iter()
916 .filter(|n| n.kind == NodeKind::Module)
917 .collect();
918 let fns: Vec<_> = nodes
919 .iter()
920 .filter(|n| n.kind == NodeKind::Function)
921 .collect();
922 let contains: Vec<_> = edges
923 .iter()
924 .filter(|e| e.kind == EdgeKind::Contains)
925 .collect();
926
927 assert_eq!(mods.len(), 1, "expected utils module");
928 assert_eq!(fns.len(), 1, "expected helper function");
929 assert!(!contains.is_empty(), "expected Contains edges");
930 }
931
932 #[test]
933 fn qualified_name_includes_module_path() {
934 let src = r#"
935pub mod inner {
936 pub fn foo() {}
937}
938"#;
939 let (nodes, _) = parse(src);
940 let foo = nodes.iter().find(|n| n.name == "foo").unwrap();
941 assert_eq!(foo.qualified_name, "crate::inner::foo");
942 }
943
944 #[test]
945 fn detects_intra_file_calls() {
946 let src = r#"
947pub fn caller() { callee(); }
948pub fn callee() {}
949"#;
950 let (_, edges) = parse(src);
951 let calls: Vec<_> = edges.iter().filter(|e| e.kind == EdgeKind::Calls).collect();
952 assert_eq!(calls.len(), 1, "expected one Calls edge");
953 }
954
955 #[test]
956 fn detects_uses_edges_for_param_types() {
957 let src = r#"
958pub struct Config {}
959pub fn run(cfg: Config) {}
960"#;
961 let (_, edges) = parse(src);
962 let uses: Vec<_> = edges.iter().filter(|e| e.kind == EdgeKind::Uses).collect();
963 assert_eq!(uses.len(), 1, "expected one Uses edge from run to Config");
964 }
965
966 #[test]
967 fn deferred_calls_capture_unknown_callees() {
968 let src = r#"
969pub fn caller() { external_fn(); }
970"#;
971 let result = RustParser::new().parse(Path::new("test.rs"), src).unwrap();
972 assert_eq!(result.deferred_calls.len(), 1);
973 assert_eq!(result.deferred_calls[0].1, "external_fn");
974 }
975}