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 })
73 }
74}
75
76struct FileVisitor<'src> {
79 source: &'src [u8],
80 file: PathBuf,
81 nodes: Vec<Node>,
82 edges: Vec<Edge>,
83 type_index: HashMap<String, NodeId>,
84 fn_index: HashMap<String, NodeId>,
85 deferred_calls: Vec<(NodeId, String)>,
86 deferred_uses: Vec<(NodeId, String)>,
87 deferred_implements: Vec<(NodeId, String)>,
88 deferred_imports: Vec<(NodeId, String)>,
89}
90
91impl<'src> FileVisitor<'src> {
92 fn new(file: &Path, source: &'src str) -> Self {
93 Self {
94 source: source.as_bytes(),
95 file: file.to_owned(),
96 nodes: Vec::new(),
97 edges: Vec::new(),
98 type_index: HashMap::new(),
99 fn_index: HashMap::new(),
100 deferred_calls: Vec::new(),
101 deferred_uses: Vec::new(),
102 deferred_implements: Vec::new(),
103 deferred_imports: Vec::new(),
104 }
105 }
106
107 fn text<'t>(&self, node: TsNode<'t>) -> &'src str {
110 node.utf8_text(self.source).unwrap_or("")
111 }
112
113 fn field_text(&self, node: TsNode<'_>, field: &str) -> Option<String> {
114 node.child_by_field_name(field)
115 .and_then(|n| n.utf8_text(self.source).ok())
116 .map(str::to_owned)
117 }
118
119 fn span(node: TsNode<'_>) -> Span {
120 Span {
121 start_line: node.start_position().row as u32 + 1,
122 end_line: node.end_position().row as u32 + 1,
123 }
124 }
125
126 fn visibility(&self, node: TsNode<'_>) -> Visibility {
127 let mut cursor = node.walk();
128 for child in node.children(&mut cursor) {
129 if child.kind() == "visibility_modifier" {
130 let t = self.text(child);
131 return if t.contains("crate") {
132 Visibility::PubCrate
133 } else {
134 Visibility::Pub
135 };
136 }
137 }
138 Visibility::Private
139 }
140
141 fn is_async(&self, node: TsNode<'_>) -> bool {
142 let mut cursor = node.walk();
143 let result = node.children(&mut cursor).any(|c| c.kind() == "async");
144 result
145 }
146
147 fn is_unsafe(&self, node: TsNode<'_>) -> bool {
148 let mut cursor = node.walk();
149 let result = node.children(&mut cursor).any(|c| c.kind() == "unsafe");
150 result
151 }
152
153 fn qualified(scope: &[String], name: &str) -> String {
154 if scope.is_empty() {
155 format!("crate::{name}")
156 } else {
157 format!("crate::{}::{name}", scope.join("::"))
158 }
159 }
160
161 fn make_node(
162 &self,
163 id: NodeId,
164 kind: NodeKind,
165 name: String,
166 scope: &[String],
167 ts_node: TsNode<'_>,
168 ) -> Node {
169 Node {
170 id,
171 qualified_name: Self::qualified(scope, &name),
172 kind,
173 name,
174 file: self.file.clone(),
175 span: Self::span(ts_node),
176 metadata: NodeMetadata {
177 loc: (ts_node.end_position().row - ts_node.start_position().row + 1) as u32,
178 visibility: self.visibility(ts_node),
179 is_async: self.is_async(ts_node),
180 is_unsafe: self.is_unsafe(ts_node),
181 ..Default::default()
182 },
183 }
184 }
185
186 fn type_name(&self, node: TsNode<'_>) -> Option<String> {
187 match node.kind() {
188 "type_identifier" => Some(self.text(node).to_owned()),
189 "generic_type" => node
190 .child_by_field_name("type")
191 .map(|n| self.text(n).to_owned()),
192 "scoped_type_identifier" => node
193 .child_by_field_name("name")
194 .map(|n| self.text(n).to_owned()),
195 "reference_type" => node
196 .child_by_field_name("type")
197 .and_then(|n| self.type_name(n)),
198 "mutable_specifier" => None,
199 _ => Some(self.text(node).to_owned()),
200 }
201 }
202
203 fn collect_names(&mut self, node: TsNode<'_>) {
206 let mut cursor = node.walk();
207 let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
208 for child in children {
209 match child.kind() {
210 "struct_item" | "enum_item" | "trait_item" => {
211 if let Some(name) = self.field_text(child, "name") {
212 self.type_index.entry(name).or_default();
213 }
214 }
215 "function_item" => {
216 if let Some(name) = self.field_text(child, "name") {
217 self.fn_index.entry(name).or_default();
218 }
219 }
220 "impl_item" => {
221 }
225 "mod_item" => {
226 if let Some(body) = child.child_by_field_name("body") {
227 self.collect_names(body);
228 }
229 }
230 _ => {}
231 }
232 }
233 }
234
235 fn visit_items(&mut self, parent: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
238 let mut cursor = parent.walk();
239 let children: Vec<TsNode<'_>> = parent.named_children(&mut cursor).collect();
240 for child in children {
241 self.visit_item(child, scope, container_id.clone());
242 }
243 }
244
245 fn visit_item(&mut self, node: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
246 match node.kind() {
247 "function_item" => self.visit_function(node, scope, container_id, NodeKind::Function),
248 "struct_item" => self.visit_type_item(node, scope, container_id, NodeKind::Struct),
249 "enum_item" => self.visit_type_item(node, scope, container_id, NodeKind::Enum),
250 "trait_item" => self.visit_trait(node, scope, container_id),
251 "impl_item" => self.visit_impl(node, scope),
252 "mod_item" => self.visit_mod(node, scope, container_id),
253 "const_item" | "static_item" => self.visit_const(node, scope, container_id),
254 "type_item" => self.visit_type_alias(node, scope, container_id),
255 "macro_definition" => self.visit_macro_def(node, scope, container_id),
256 _ => {}
257 }
258 }
259
260 fn visit_function(
261 &mut self,
262 node: TsNode<'_>,
263 scope: &[String],
264 container_id: Option<NodeId>,
265 kind: NodeKind,
266 ) {
267 let Some(name) = self.field_text(node, "name") else {
268 return;
269 };
270 let id = if kind == NodeKind::Method {
274 NodeId::new()
275 } else {
276 self.fn_index
277 .get(&name)
278 .cloned()
279 .unwrap_or_else(NodeId::new)
280 };
281 let graph_node = self.make_node(id.clone(), kind, name, scope, node);
282
283 if let Some(cid) = container_id {
284 self.edges.push(Edge {
285 src: cid,
286 dst: id.clone(),
287 kind: EdgeKind::Contains,
288 });
289 }
290
291 self.collect_uses_edges(node, &id);
293
294 self.nodes.push(graph_node);
295
296 if let Some(body) = node.child_by_field_name("body") {
298 self.collect_calls(body, &id);
299 }
300 }
301
302 fn collect_uses_edges(&mut self, fn_node: TsNode<'_>, fn_id: &NodeId) {
305 let mut type_names: Vec<String> = Vec::new();
306
307 if let Some(params) = fn_node.child_by_field_name("parameters") {
308 let mut cursor = params.walk();
309 for param in params.named_children(&mut cursor) {
310 if param.kind() == "parameter" {
311 if let Some(type_node) = param.child_by_field_name("type") {
312 if let Some(tname) = self.type_name(type_node) {
313 type_names.push(tname);
314 }
315 }
316 }
317 }
318 }
319
320 if let Some(ret_type) = fn_node.child_by_field_name("return_type") {
321 if let Some(tname) = self.type_name(ret_type) {
322 type_names.push(tname);
323 }
324 }
325
326 for tname in type_names {
327 if let Some(tid) = self.type_index.get(&tname).cloned() {
328 self.edges.push(Edge {
329 src: fn_id.clone(),
330 dst: tid,
331 kind: EdgeKind::Uses,
332 });
333 } else if !tname.is_empty()
334 && !is_primitive(&tname)
335 && !self
336 .deferred_uses
337 .iter()
338 .any(|(id, n)| id == fn_id && n == &tname)
339 {
340 self.deferred_uses.push((fn_id.clone(), tname));
341 }
342 }
343 }
344
345 fn collect_calls(&mut self, node: TsNode<'_>, caller_id: &NodeId) {
351 match node.kind() {
352 "call_expression" => {
353 if let Some(callee) = self.callee_name(node) {
354 self.record_call(caller_id.clone(), callee);
355 }
356 if let Some(args) = node.child_by_field_name("arguments") {
357 self.collect_calls(args, caller_id);
358 }
359 if let Some(func) = node.child_by_field_name("function") {
364 if let Some(value) = func.child_by_field_name("value") {
365 self.collect_calls(value, caller_id);
366 }
367 }
368 }
369 "method_call_expression" => {
370 if let Some(name_node) = node.child_by_field_name("name") {
373 let method = self.text(name_node).to_owned();
374 self.record_call(caller_id.clone(), method);
375 }
376 if let Some(args) = node.child_by_field_name("arguments") {
377 self.collect_calls(args, caller_id);
378 }
379 if let Some(recv) = node.child_by_field_name("receiver") {
380 self.collect_calls(recv, caller_id);
381 }
382 }
383 _ => {
384 let mut cursor = node.walk();
385 let children: Vec<TsNode<'_>> = node.named_children(&mut cursor).collect();
386 for child in children {
387 self.collect_calls(child, caller_id);
388 }
389 }
390 }
391 }
392
393 fn callee_name(&self, call_expr: TsNode<'_>) -> Option<String> {
395 let func = call_expr.child_by_field_name("function")?;
396 match func.kind() {
397 "identifier" => Some(self.text(func).to_owned()),
398 "scoped_identifier" => func
399 .child_by_field_name("name")
400 .and_then(|n| n.utf8_text(self.source).ok())
401 .map(str::to_owned),
402 "field_expression" => func
403 .child_by_field_name("field")
404 .and_then(|n| n.utf8_text(self.source).ok())
405 .map(str::to_owned),
406 _ => None,
407 }
408 }
409
410 fn record_call(&mut self, caller_id: NodeId, callee_name: String) {
412 if callee_name.is_empty() {
413 return;
414 }
415 if let Some(callee_id) = self.fn_index.get(&callee_name).cloned() {
416 let edge = Edge {
417 src: caller_id,
418 dst: callee_id,
419 kind: EdgeKind::Calls,
420 };
421 if !self.edges.contains(&edge) {
422 self.edges.push(edge);
423 }
424 } else if !self
425 .deferred_calls
426 .iter()
427 .any(|(c, n)| c == &caller_id && n == &callee_name)
428 {
429 self.deferred_calls.push((caller_id, callee_name));
430 }
431 }
432
433 fn visit_type_item(
434 &mut self,
435 node: TsNode<'_>,
436 scope: &[String],
437 container_id: Option<NodeId>,
438 kind: NodeKind,
439 ) {
440 let Some(name) = self.field_text(node, "name") else {
441 return;
442 };
443 let id = self
444 .type_index
445 .get(&name)
446 .cloned()
447 .unwrap_or_else(NodeId::new);
448 let graph_node = self.make_node(id.clone(), kind, name, scope, node);
449 if let Some(cid) = container_id {
450 self.edges.push(Edge {
451 src: cid,
452 dst: id.clone(),
453 kind: EdgeKind::Contains,
454 });
455 }
456 self.nodes.push(graph_node);
457 }
458
459 fn visit_trait(&mut self, node: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
460 let Some(name) = self.field_text(node, "name") else {
461 return;
462 };
463 let id = self
464 .type_index
465 .get(&name)
466 .cloned()
467 .unwrap_or_else(NodeId::new);
468 let graph_node = self.make_node(id.clone(), NodeKind::Trait, name.clone(), scope, node);
469 if let Some(cid) = container_id {
470 self.edges.push(Edge {
471 src: cid,
472 dst: id.clone(),
473 kind: EdgeKind::Contains,
474 });
475 }
476 self.nodes.push(graph_node);
477
478 if let Some(body) = node.child_by_field_name("body") {
479 let mut new_scope = scope.to_vec();
480 new_scope.push(name);
481 self.visit_items(body, &new_scope, Some(id));
482 }
483 }
484
485 fn visit_impl(&mut self, node: TsNode<'_>, scope: &[String]) {
486 let type_node = node.child_by_field_name("type");
487 let type_name = type_node.and_then(|n| self.type_name(n));
488 let Some(type_name) = type_name else { return };
489 let type_id = self.type_index.get(&type_name).cloned();
490
491 if let Some(trait_node) = node.child_by_field_name("trait") {
492 if let Some(trait_name) = self.type_name(trait_node) {
493 let trait_id = self.type_index.get(&trait_name).cloned();
494 match (type_id.clone(), trait_id) {
495 (Some(tid), Some(trid)) => {
496 self.edges.push(Edge {
497 src: tid,
498 dst: trid,
499 kind: EdgeKind::Implements,
500 });
501 }
502 (Some(tid), None)
503 if !is_primitive(&trait_name)
504 && !self
505 .deferred_implements
506 .iter()
507 .any(|(id, n)| id == &tid && n == &trait_name) =>
508 {
509 self.deferred_implements.push((tid, trait_name));
510 }
511 _ => {}
512 }
513 }
514 }
515
516 if let Some(body) = node.child_by_field_name("body") {
517 let mut cursor = body.walk();
518 let children: Vec<TsNode<'_>> = body.named_children(&mut cursor).collect();
519 let mut impl_scope = scope.to_vec();
520 impl_scope.push(type_name);
521
522 for child in children {
523 if child.kind() == "function_item" {
524 self.visit_function(child, &impl_scope, type_id.clone(), NodeKind::Method);
525 }
526 }
527 }
528 }
529
530 fn visit_mod(&mut self, node: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
531 let Some(name) = self.field_text(node, "name") else {
532 return;
533 };
534 let id = NodeId::new();
535 let graph_node = self.make_node(id.clone(), NodeKind::Module, name.clone(), scope, node);
536 if let Some(cid) = container_id {
537 self.edges.push(Edge {
538 src: cid,
539 dst: id.clone(),
540 kind: EdgeKind::Contains,
541 });
542 }
543 self.nodes.push(graph_node);
544
545 if let Some(body) = node.child_by_field_name("body") {
546 let mut new_scope = scope.to_vec();
547 new_scope.push(name);
548 self.visit_items(body, &new_scope, Some(id));
549 }
550 }
551
552 fn visit_const(&mut self, node: TsNode<'_>, scope: &[String], container_id: Option<NodeId>) {
553 let Some(name) = self.field_text(node, "name") else {
554 return;
555 };
556 let id = NodeId::new();
557 let graph_node = self.make_node(id.clone(), NodeKind::Constant, name, scope, node);
558 if let Some(cid) = container_id {
559 self.edges.push(Edge {
560 src: cid,
561 dst: id.clone(),
562 kind: EdgeKind::Contains,
563 });
564 }
565 self.nodes.push(graph_node);
566 }
567
568 fn visit_type_alias(
569 &mut self,
570 node: TsNode<'_>,
571 scope: &[String],
572 container_id: Option<NodeId>,
573 ) {
574 let Some(name) = self.field_text(node, "name") else {
575 return;
576 };
577 let id = NodeId::new();
578 let graph_node = self.make_node(id.clone(), NodeKind::TypeAlias, name, scope, node);
579 if let Some(cid) = container_id {
580 self.edges.push(Edge {
581 src: cid,
582 dst: id.clone(),
583 kind: EdgeKind::Contains,
584 });
585 }
586 self.nodes.push(graph_node);
587 }
588
589 fn visit_macro_def(
590 &mut self,
591 node: TsNode<'_>,
592 scope: &[String],
593 container_id: Option<NodeId>,
594 ) {
595 let Some(name) = self.field_text(node, "name") else {
596 return;
597 };
598 let id = NodeId::new();
599 let graph_node = self.make_node(id.clone(), NodeKind::Macro, name, scope, node);
600 if let Some(cid) = container_id {
601 self.edges.push(Edge {
602 src: cid,
603 dst: id.clone(),
604 kind: EdgeKind::Contains,
605 });
606 }
607 self.nodes.push(graph_node);
608 }
609
610 fn collect_imports(&mut self, root: TsNode<'_>) {
613 let mut cursor = root.walk();
614 for child in root.named_children(&mut cursor) {
615 if child.kind() == "use_declaration" {
616 if let Some(arg) = child.child_by_field_name("argument") {
618 self.collect_import_leaves(arg);
619 }
620 } else if child.kind() == "mod_item" {
621 if let Some(body) = child.child_by_field_name("body") {
622 self.collect_imports(body);
623 }
624 }
625 }
626 }
627
628 fn collect_import_leaves(&mut self, node: TsNode<'_>) {
629 match node.kind() {
630 "identifier" | "type_identifier" => {
631 let name = self.text(node).to_owned();
632 if !name.is_empty()
633 && !is_primitive(&name)
634 && name != "self"
635 && name != "super"
636 && name != "crate"
637 {
638 let placeholder = NodeId::new();
640 self.deferred_imports.push((placeholder, name));
641 }
642 }
643 "use_list" => {
644 let mut cursor = node.walk();
645 for child in node.named_children(&mut cursor) {
646 self.collect_import_leaves(child);
647 }
648 }
649 "scoped_identifier" | "scoped_use_list" => {
650 let mut cursor = node.walk();
652 for child in node.named_children(&mut cursor) {
653 self.collect_import_leaves(child);
654 }
655 }
656 "use_as_clause" => {
657 if let Some(alias) = node.child_by_field_name("alias") {
659 self.collect_import_leaves(alias);
660 }
661 }
662 _ => {}
663 }
664 }
665}
666
667fn is_primitive(name: &str) -> bool {
668 matches!(
669 name,
670 "bool"
671 | "char"
672 | "str"
673 | "i8"
674 | "i16"
675 | "i32"
676 | "i64"
677 | "i128"
678 | "isize"
679 | "u8"
680 | "u16"
681 | "u32"
682 | "u64"
683 | "u128"
684 | "usize"
685 | "f32"
686 | "f64"
687 | "String"
688 | "Vec"
689 | "Option"
690 | "Result"
691 | "Box"
692 | "Rc"
693 | "Arc"
694 | "Cell"
695 | "RefCell"
696 | "Cow"
697 | "HashMap"
698 | "HashSet"
699 | "BTreeMap"
700 | "BTreeSet"
701 | "PathBuf"
702 | "Path"
703 | "OsString"
704 | "OsStr"
705 | "Send"
706 | "Sync"
707 | "Sized"
708 | "Clone"
709 | "Copy"
710 | "Debug"
711 | "Display"
712 | "Default"
713 | "PartialEq"
714 | "Eq"
715 | "PartialOrd"
716 | "Ord"
717 | "Hash"
718 | "Iterator"
719 | "Into"
720 | "From"
721 | "AsRef"
722 | "AsMut"
723 | "Deref"
724 | "DerefMut"
725 | "Error"
726 | "Write"
727 | "Read"
728 | "Seek"
729 | "Self"
730 | "()"
731 | "_"
732 )
733}
734
735#[cfg(test)]
738mod tests {
739 use std::path::Path;
740
741 use gitcortex_core::{
742 graph::{Edge, Node},
743 schema::{EdgeKind, NodeKind},
744 };
745
746 use super::RustParser;
747 use crate::parser::LanguageParser;
748
749 fn parse(src: &str) -> (Vec<Node>, Vec<Edge>) {
750 let r = RustParser::new().parse(Path::new("test.rs"), src).unwrap();
751 (r.nodes, r.edges)
752 }
753
754 #[test]
755 fn parses_free_function() {
756 let (nodes, _) = parse("pub fn greet(name: &str) -> String { name.into() }");
757 assert_eq!(nodes.len(), 1);
758 assert_eq!(nodes[0].kind, NodeKind::Function);
759 assert_eq!(nodes[0].name, "greet");
760 }
761
762 #[test]
763 fn parses_struct() {
764 let (nodes, _) = parse("pub struct Person { pub name: String }");
765 let structs: Vec<_> = nodes
766 .iter()
767 .filter(|n| n.kind == NodeKind::Struct)
768 .collect();
769 assert_eq!(structs.len(), 1);
770 assert_eq!(structs[0].name, "Person");
771 }
772
773 #[test]
774 fn parses_trait_impl_and_method() {
775 let src = r#"
776pub trait Greet { fn greet(&self) -> String; }
777pub struct Person { pub name: String }
778impl Greet for Person {
779 fn greet(&self) -> String { self.name.clone() }
780}
781"#;
782 let (nodes, edges) = parse(src);
783
784 let traits: Vec<_> = nodes.iter().filter(|n| n.kind == NodeKind::Trait).collect();
785 let structs: Vec<_> = nodes
786 .iter()
787 .filter(|n| n.kind == NodeKind::Struct)
788 .collect();
789 let methods: Vec<_> = nodes
790 .iter()
791 .filter(|n| n.kind == NodeKind::Method)
792 .collect();
793 let impl_edges: Vec<_> = edges
794 .iter()
795 .filter(|e| e.kind == EdgeKind::Implements)
796 .collect();
797
798 assert_eq!(traits.len(), 1, "expected Greet trait");
799 assert_eq!(structs.len(), 1, "expected Person struct");
800 assert_eq!(methods.len(), 1, "expected greet method");
801 assert_eq!(impl_edges.len(), 1, "expected Implements edge");
802 }
803
804 #[test]
805 fn parses_module_with_items() {
806 let src = r#"
807pub mod utils {
808 pub fn helper() {}
809 pub struct Config {}
810}
811"#;
812 let (nodes, edges) = parse(src);
813
814 let mods: Vec<_> = nodes
815 .iter()
816 .filter(|n| n.kind == NodeKind::Module)
817 .collect();
818 let fns: Vec<_> = nodes
819 .iter()
820 .filter(|n| n.kind == NodeKind::Function)
821 .collect();
822 let contains: Vec<_> = edges
823 .iter()
824 .filter(|e| e.kind == EdgeKind::Contains)
825 .collect();
826
827 assert_eq!(mods.len(), 1, "expected utils module");
828 assert_eq!(fns.len(), 1, "expected helper function");
829 assert!(!contains.is_empty(), "expected Contains edges");
830 }
831
832 #[test]
833 fn qualified_name_includes_module_path() {
834 let src = r#"
835pub mod inner {
836 pub fn foo() {}
837}
838"#;
839 let (nodes, _) = parse(src);
840 let foo = nodes.iter().find(|n| n.name == "foo").unwrap();
841 assert_eq!(foo.qualified_name, "crate::inner::foo");
842 }
843
844 #[test]
845 fn detects_intra_file_calls() {
846 let src = r#"
847pub fn caller() { callee(); }
848pub fn callee() {}
849"#;
850 let (_, edges) = parse(src);
851 let calls: Vec<_> = edges.iter().filter(|e| e.kind == EdgeKind::Calls).collect();
852 assert_eq!(calls.len(), 1, "expected one Calls edge");
853 }
854
855 #[test]
856 fn detects_uses_edges_for_param_types() {
857 let src = r#"
858pub struct Config {}
859pub fn run(cfg: Config) {}
860"#;
861 let (_, edges) = parse(src);
862 let uses: Vec<_> = edges.iter().filter(|e| e.kind == EdgeKind::Uses).collect();
863 assert_eq!(uses.len(), 1, "expected one Uses edge from run to Config");
864 }
865
866 #[test]
867 fn deferred_calls_capture_unknown_callees() {
868 let src = r#"
869pub fn caller() { external_fn(); }
870"#;
871 let result = RustParser::new().parse(Path::new("test.rs"), src).unwrap();
872 assert_eq!(result.deferred_calls.len(), 1);
873 assert_eq!(result.deferred_calls[0].1, "external_fn");
874 }
875}