1use crate::{GraphEdge, GraphError, GraphNode};
8use petgraph::graph::{DiGraph, NodeIndex};
9use rustpython_parser::{ast, Parse};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::path::PathBuf;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16pub enum NodeKind {
17 Function,
19 Class,
21 Method,
23 Module,
25 Variable,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31pub enum EdgeKind {
32 Calls,
34 Imports,
36 Inherits,
38 Accesses,
40 References,
42}
43
44pub struct DependencyGraph {
46 pub graph: DiGraph<GraphNode, GraphEdge>,
48 pub node_map: HashMap<String, NodeIndex>,
50 pub source_file: PathBuf,
52 source: String,
54}
55
56impl DependencyGraph {
57 pub fn new() -> Self {
59 Self {
60 graph: DiGraph::new(),
61 node_map: HashMap::new(),
62 source_file: PathBuf::new(),
63 source: String::new(),
64 }
65 }
66
67 pub fn node_count(&self) -> usize {
69 self.graph.node_count()
70 }
71
72 pub fn edge_count(&self) -> usize {
74 self.graph.edge_count()
75 }
76
77 pub fn add_node(&mut self, node: GraphNode) -> NodeIndex {
79 let id = node.id.clone();
80 let idx = self.graph.add_node(node);
81 self.node_map.insert(id, idx);
82 idx
83 }
84
85 pub fn add_edge(&mut self, from: &str, to: &str, edge: GraphEdge) -> bool {
87 if let (Some(&from_idx), Some(&to_idx)) = (self.node_map.get(from), self.node_map.get(to)) {
88 self.graph.add_edge(from_idx, to_idx, edge);
89 true
90 } else {
91 false
92 }
93 }
94
95 pub fn get_node(&self, id: &str) -> Option<&GraphNode> {
97 self.node_map.get(id).map(|&idx| &self.graph[idx])
98 }
99
100 pub fn get_node_mut(&mut self, id: &str) -> Option<&mut GraphNode> {
102 if let Some(&idx) = self.node_map.get(id) {
103 Some(&mut self.graph[idx])
104 } else {
105 None
106 }
107 }
108
109 pub fn node_ids(&self) -> Vec<String> {
111 self.node_map.keys().cloned().collect()
112 }
113
114 pub fn incoming_edges(&self, id: &str) -> Vec<(&GraphNode, &GraphEdge)> {
116 let Some(&idx) = self.node_map.get(id) else {
117 return vec![];
118 };
119
120 self.graph
121 .neighbors_directed(idx, petgraph::Direction::Incoming)
122 .filter_map(|neighbor_idx| {
123 let edge_idx = self.graph.find_edge(neighbor_idx, idx)?;
124 Some((&self.graph[neighbor_idx], &self.graph[edge_idx]))
125 })
126 .collect()
127 }
128
129 pub fn outgoing_edges(&self, id: &str) -> Vec<(&GraphNode, &GraphEdge)> {
131 let Some(&idx) = self.node_map.get(id) else {
132 return vec![];
133 };
134
135 self.graph
136 .neighbors_directed(idx, petgraph::Direction::Outgoing)
137 .filter_map(|neighbor_idx| {
138 let edge_idx = self.graph.find_edge(idx, neighbor_idx)?;
139 Some((&self.graph[neighbor_idx], &self.graph[edge_idx]))
140 })
141 .collect()
142 }
143}
144
145impl Default for DependencyGraph {
146 fn default() -> Self {
147 Self::new()
148 }
149}
150
151pub struct GraphBuilder {
153 current_class: Option<String>,
155 known_functions: HashMap<String, usize>,
157 known_classes: HashMap<String, usize>,
159 source: String,
161}
162
163impl GraphBuilder {
164 pub fn new() -> Self {
166 Self {
167 current_class: None,
168 known_functions: HashMap::new(),
169 known_classes: HashMap::new(),
170 source: String::new(),
171 }
172 }
173
174 fn offset_to_line(&self, offset: usize) -> usize {
176 let mut line = 1;
177 for (i, ch) in self.source.chars().enumerate() {
178 if i >= offset {
179 break;
180 }
181 if ch == '\n' {
182 line += 1;
183 }
184 }
185 line
186 }
187
188 fn offset_to_column(&self, offset: usize) -> usize {
190 let mut column = 1;
191 for (i, ch) in self.source.chars().enumerate() {
192 if i >= offset {
193 break;
194 }
195 if ch == '\n' {
196 column = 1;
197 } else {
198 column += 1;
199 }
200 }
201 column
202 }
203
204 pub fn build_from_source(&mut self, source: &str) -> Result<DependencyGraph, GraphError> {
206 self.source = source.to_string();
207 let mut graph = DependencyGraph::new();
208 graph.source = source.to_string();
209
210 let ast = ast::Suite::parse(source, "<source>")
212 .map_err(|e| GraphError::ParseError(e.to_string()))?;
213
214 self.collect_definitions(&ast);
216
217 for stmt in &ast {
219 self.process_statement(stmt, &mut graph)?;
220 }
221
222 for stmt in &ast {
224 self.process_edges(stmt, &mut graph)?;
225 }
226
227 Ok(graph)
228 }
229
230 fn collect_definitions(&mut self, stmts: &[ast::Stmt]) {
232 for stmt in stmts {
233 match stmt {
234 ast::Stmt::FunctionDef(f) => {
235 let offset: usize = u32::from(f.range.start()).try_into().unwrap_or(0);
236 self.known_functions.insert(f.name.to_string(), offset);
237 }
238 ast::Stmt::ClassDef(c) => {
239 let offset: usize = u32::from(c.range.start()).try_into().unwrap_or(0);
240 self.known_classes.insert(c.name.to_string(), offset);
241 for stmt in &c.body {
243 if let ast::Stmt::FunctionDef(f) = stmt {
244 let method_name = format!("{}.{}", c.name, f.name);
245 let method_offset: usize =
246 u32::from(f.range.start()).try_into().unwrap_or(0);
247 self.known_functions.insert(method_name, method_offset);
248 }
249 }
250 }
251 _ => {}
252 }
253 }
254 }
255
256 fn process_statement(
258 &mut self,
259 stmt: &ast::Stmt,
260 graph: &mut DependencyGraph,
261 ) -> Result<(), GraphError> {
262 match stmt {
263 ast::Stmt::FunctionDef(f) => {
264 let offset: usize = u32::from(f.range.start()).try_into().unwrap_or(0);
265 let node = GraphNode {
266 id: f.name.to_string(),
267 kind: NodeKind::Function,
268 file: PathBuf::new(),
269 line: self.offset_to_line(offset),
270 column: self.offset_to_column(offset),
271 error_count: 0,
272 impact_score: 0.0,
273 };
274 graph.add_node(node);
275 }
276 ast::Stmt::ClassDef(c) => {
277 let offset: usize = u32::from(c.range.start()).try_into().unwrap_or(0);
278 let class_node = GraphNode {
279 id: c.name.to_string(),
280 kind: NodeKind::Class,
281 file: PathBuf::new(),
282 line: self.offset_to_line(offset),
283 column: self.offset_to_column(offset),
284 error_count: 0,
285 impact_score: 0.0,
286 };
287 graph.add_node(class_node);
288
289 self.current_class = Some(c.name.to_string());
290
291 for body_stmt in &c.body {
293 if let ast::Stmt::FunctionDef(f) = body_stmt {
294 let method_offset: usize =
295 u32::from(f.range.start()).try_into().unwrap_or(0);
296 let method_id = format!("{}.{}", c.name, f.name);
297 let method_node = GraphNode {
298 id: method_id,
299 kind: NodeKind::Method,
300 file: PathBuf::new(),
301 line: self.offset_to_line(method_offset),
302 column: self.offset_to_column(method_offset),
303 error_count: 0,
304 impact_score: 0.0,
305 };
306 graph.add_node(method_node);
307 }
308 }
309
310 self.current_class = None;
311 }
312 ast::Stmt::Import(i) => {
313 let offset: usize = u32::from(i.range.start()).try_into().unwrap_or(0);
314 for alias in &i.names {
315 let node = GraphNode {
316 id: format!("import:{}", alias.name),
317 kind: NodeKind::Module,
318 file: PathBuf::new(),
319 line: self.offset_to_line(offset),
320 column: self.offset_to_column(offset),
321 error_count: 0,
322 impact_score: 0.0,
323 };
324 graph.add_node(node);
325 }
326 }
327 _ => {}
328 }
329 Ok(())
330 }
331
332 fn process_edges(
334 &mut self,
335 stmt: &ast::Stmt,
336 graph: &mut DependencyGraph,
337 ) -> Result<(), GraphError> {
338 match stmt {
339 ast::Stmt::FunctionDef(f) => {
340 let caller = f.name.to_string();
341 self.extract_calls_from_body(&f.body, &caller, graph);
342 }
343 ast::Stmt::ClassDef(c) => {
344 for base in &c.bases {
346 if let ast::Expr::Name(name) = base {
347 let edge = GraphEdge {
348 kind: EdgeKind::Inherits,
349 weight: 1.0,
350 };
351 graph.add_edge(c.name.as_ref(), name.id.as_ref(), edge);
352 }
353 }
354
355 for body_stmt in &c.body {
357 if let ast::Stmt::FunctionDef(f) = body_stmt {
358 let caller = format!("{}.{}", c.name, f.name);
359 self.extract_calls_from_body(&f.body, &caller, graph);
360 }
361 }
362 }
363 _ => {}
364 }
365 Ok(())
366 }
367
368 fn extract_calls_from_body(
370 &self,
371 body: &[ast::Stmt],
372 caller: &str,
373 graph: &mut DependencyGraph,
374 ) {
375 for stmt in body {
376 self.extract_calls_from_stmt(stmt, caller, graph);
377 }
378 }
379
380 fn extract_calls_from_stmt(&self, stmt: &ast::Stmt, caller: &str, graph: &mut DependencyGraph) {
382 match stmt {
383 ast::Stmt::Expr(e) => {
384 self.extract_calls_from_expr(&e.value, caller, graph);
385 }
386 ast::Stmt::Return(r) => {
387 if let Some(value) = &r.value {
388 self.extract_calls_from_expr(value, caller, graph);
389 }
390 }
391 ast::Stmt::Assign(a) => {
392 self.extract_calls_from_expr(&a.value, caller, graph);
393 }
394 ast::Stmt::If(i) => {
395 self.extract_calls_from_expr(&i.test, caller, graph);
396 self.extract_calls_from_body(&i.body, caller, graph);
397 self.extract_calls_from_body(&i.orelse, caller, graph);
398 }
399 ast::Stmt::For(f) => {
400 self.extract_calls_from_expr(&f.iter, caller, graph);
401 self.extract_calls_from_body(&f.body, caller, graph);
402 }
403 ast::Stmt::While(w) => {
404 self.extract_calls_from_expr(&w.test, caller, graph);
405 self.extract_calls_from_body(&w.body, caller, graph);
406 }
407 _ => {}
408 }
409 }
410
411 fn extract_calls_from_expr(&self, expr: &ast::Expr, caller: &str, graph: &mut DependencyGraph) {
413 match expr {
414 ast::Expr::Call(c) => {
415 let callee = match c.func.as_ref() {
417 ast::Expr::Name(n) => Some(n.id.to_string()),
418 ast::Expr::Attribute(a) => {
419 if let ast::Expr::Name(n) = a.value.as_ref() {
420 Some(format!("{}.{}", n.id, a.attr))
421 } else {
422 None
423 }
424 }
425 _ => None,
426 };
427
428 if let Some(callee_name) = callee {
429 if self.known_functions.contains_key(&callee_name)
431 || self.known_classes.contains_key(&callee_name)
432 {
433 let edge = GraphEdge {
434 kind: EdgeKind::Calls,
435 weight: 1.0,
436 };
437 graph.add_edge(caller, &callee_name, edge);
438 }
439 }
440
441 for arg in &c.args {
443 self.extract_calls_from_expr(arg, caller, graph);
444 }
445 }
446 ast::Expr::BinOp(b) => {
447 self.extract_calls_from_expr(&b.left, caller, graph);
448 self.extract_calls_from_expr(&b.right, caller, graph);
449 }
450 ast::Expr::Compare(c) => {
451 self.extract_calls_from_expr(&c.left, caller, graph);
452 for comparator in &c.comparators {
453 self.extract_calls_from_expr(comparator, caller, graph);
454 }
455 }
456 ast::Expr::List(l) => {
457 for elt in &l.elts {
458 self.extract_calls_from_expr(elt, caller, graph);
459 }
460 }
461 ast::Expr::Dict(d) => {
462 for key in d.keys.iter().flatten() {
463 self.extract_calls_from_expr(key, caller, graph);
464 }
465 for value in &d.values {
466 self.extract_calls_from_expr(value, caller, graph);
467 }
468 }
469 _ => {}
470 }
471 }
472}
473
474impl Default for GraphBuilder {
475 fn default() -> Self {
476 Self::new()
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483
484 #[test]
485 fn test_build_simple_function() {
486 let python = r#"
487def foo():
488 return 42
489"#;
490
491 let mut builder = GraphBuilder::new();
492 let graph = builder.build_from_source(python).unwrap();
493
494 assert_eq!(graph.node_count(), 1);
495 assert!(graph.get_node("foo").is_some());
496 }
497
498 #[test]
499 fn test_build_function_call() {
500 let python = r#"
501def foo():
502 return 42
503
504def bar():
505 return foo()
506"#;
507
508 let mut builder = GraphBuilder::new();
509 let graph = builder.build_from_source(python).unwrap();
510
511 assert_eq!(graph.node_count(), 2);
512 assert_eq!(graph.edge_count(), 1);
513
514 let edges = graph.outgoing_edges("bar");
516 assert_eq!(edges.len(), 1);
517 assert_eq!(edges[0].0.id, "foo");
518 }
519
520 #[test]
521 fn test_build_class_inheritance() {
522 let python = r#"
523class Base:
524 def method(self):
525 pass
526
527class Derived(Base):
528 def method(self):
529 pass
530"#;
531
532 let mut builder = GraphBuilder::new();
533 let graph = builder.build_from_source(python).unwrap();
534
535 assert!(graph.node_count() >= 4);
537
538 let edges = graph.outgoing_edges("Derived");
540 let inherits_base = edges
541 .iter()
542 .any(|(node, edge)| node.id == "Base" && edge.kind == EdgeKind::Inherits);
543 assert!(inherits_base);
544 }
545
546 #[test]
547 fn test_node_kind() {
548 let python = r#"
549def func():
550 pass
551
552class MyClass:
553 def method(self):
554 pass
555"#;
556
557 let mut builder = GraphBuilder::new();
558 let graph = builder.build_from_source(python).unwrap();
559
560 let func_node = graph.get_node("func").unwrap();
561 assert_eq!(func_node.kind, NodeKind::Function);
562
563 let class_node = graph.get_node("MyClass").unwrap();
564 assert_eq!(class_node.kind, NodeKind::Class);
565
566 let method_node = graph.get_node("MyClass.method").unwrap();
567 assert_eq!(method_node.kind, NodeKind::Method);
568 }
569
570 #[test]
571 fn test_incoming_outgoing_edges() {
572 let python = r#"
573def a():
574 return 1
575
576def b():
577 return a()
578
579def c():
580 return a()
581"#;
582
583 let mut builder = GraphBuilder::new();
584 let graph = builder.build_from_source(python).unwrap();
585
586 let incoming = graph.incoming_edges("a");
588 assert_eq!(incoming.len(), 2);
589
590 let outgoing = graph.outgoing_edges("b");
592 assert_eq!(outgoing.len(), 1);
593 }
594
595 #[test]
596 fn test_line_numbers() {
597 let python = r#"def foo():
598 return 42
599
600def bar():
601 return 100
602"#;
603
604 let mut builder = GraphBuilder::new();
605 let graph = builder.build_from_source(python).unwrap();
606
607 let foo_node = graph.get_node("foo").unwrap();
608 assert_eq!(foo_node.line, 1);
609
610 let bar_node = graph.get_node("bar").unwrap();
611 assert_eq!(bar_node.line, 4);
612 }
613
614 #[test]
615 fn test_empty_source() {
616 let python = "";
617 let mut builder = GraphBuilder::new();
618 let graph = builder.build_from_source(python).unwrap();
619 assert_eq!(graph.node_count(), 0);
620 assert_eq!(graph.edge_count(), 0);
621 }
622
623 #[test]
624 fn test_invalid_python_source() {
625 let python = "def foo(:\n return";
626 let mut builder = GraphBuilder::new();
627 let result = builder.build_from_source(python);
628 assert!(result.is_err());
629 match result {
630 Err(crate::GraphError::ParseError(msg)) => {
631 assert!(!msg.is_empty());
632 }
633 _ => panic!("expected ParseError"),
634 }
635 }
636
637 #[test]
638 fn test_dependency_graph_default() {
639 let graph = DependencyGraph::default();
640 assert_eq!(graph.node_count(), 0);
641 assert_eq!(graph.edge_count(), 0);
642 }
643
644 #[test]
645 fn test_graph_builder_default() {
646 let builder = GraphBuilder::default();
647 let _builder2 = GraphBuilder::new();
649 drop(builder);
651 }
652
653 #[test]
654 fn test_get_node_nonexistent() {
655 let graph = DependencyGraph::new();
656 assert!(graph.get_node("nonexistent").is_none());
657 }
658
659 #[test]
660 fn test_get_node_mut() {
661 let python = "def foo():\n return 42\n";
662 let mut builder = GraphBuilder::new();
663 let mut graph = builder.build_from_source(python).unwrap();
664
665 let node = graph.get_node_mut("foo").unwrap();
667 node.error_count = 5;
668
669 let node = graph.get_node("foo").unwrap();
671 assert_eq!(node.error_count, 5);
672 }
673
674 #[test]
675 fn test_get_node_mut_nonexistent() {
676 let mut graph = DependencyGraph::new();
677 assert!(graph.get_node_mut("nonexistent").is_none());
678 }
679
680 #[test]
681 fn test_node_ids() {
682 let python = "def alpha():\n pass\n\ndef beta():\n pass\n";
683 let mut builder = GraphBuilder::new();
684 let graph = builder.build_from_source(python).unwrap();
685
686 let ids = graph.node_ids();
687 assert_eq!(ids.len(), 2);
688 assert!(ids.contains(&"alpha".to_string()));
689 assert!(ids.contains(&"beta".to_string()));
690 }
691
692 #[test]
693 fn test_add_edge_missing_from_node() {
694 let python = "def foo():\n pass\n";
695 let mut builder = GraphBuilder::new();
696 let mut graph = builder.build_from_source(python).unwrap();
697
698 let edge = GraphEdge {
699 kind: EdgeKind::Calls,
700 weight: 1.0,
701 };
702 let added = graph.add_edge("nonexistent", "foo", edge);
703 assert!(!added);
704 }
705
706 #[test]
707 fn test_add_edge_missing_to_node() {
708 let python = "def foo():\n pass\n";
709 let mut builder = GraphBuilder::new();
710 let mut graph = builder.build_from_source(python).unwrap();
711
712 let edge = GraphEdge {
713 kind: EdgeKind::Calls,
714 weight: 1.0,
715 };
716 let added = graph.add_edge("foo", "nonexistent", edge);
717 assert!(!added);
718 }
719
720 #[test]
721 fn test_add_edge_both_missing() {
722 let mut graph = DependencyGraph::new();
723 let edge = GraphEdge {
724 kind: EdgeKind::Calls,
725 weight: 1.0,
726 };
727 let added = graph.add_edge("a", "b", edge);
728 assert!(!added);
729 }
730
731 #[test]
732 fn test_add_edge_success() {
733 let python = "def foo():\n pass\n\ndef bar():\n pass\n";
734 let mut builder = GraphBuilder::new();
735 let mut graph = builder.build_from_source(python).unwrap();
736
737 let edge = GraphEdge {
738 kind: EdgeKind::Calls,
739 weight: 2.0,
740 };
741 let added = graph.add_edge("bar", "foo", edge);
742 assert!(added);
743 assert_eq!(graph.edge_count(), 1);
744 }
745
746 #[test]
747 fn test_incoming_edges_nonexistent_node() {
748 let graph = DependencyGraph::new();
749 let edges = graph.incoming_edges("nonexistent");
750 assert!(edges.is_empty());
751 }
752
753 #[test]
754 fn test_outgoing_edges_nonexistent_node() {
755 let graph = DependencyGraph::new();
756 let edges = graph.outgoing_edges("nonexistent");
757 assert!(edges.is_empty());
758 }
759
760 #[test]
761 fn test_import_statement_creates_module_node() {
762 let python = "import os\n\ndef foo():\n pass\n";
763 let mut builder = GraphBuilder::new();
764 let graph = builder.build_from_source(python).unwrap();
765
766 let import_node = graph.get_node("import:os");
767 assert!(import_node.is_some());
768 assert_eq!(import_node.unwrap().kind, NodeKind::Module);
769 }
770
771 #[test]
772 fn test_multiple_imports() {
773 let python = "import os\nimport sys\n";
774 let mut builder = GraphBuilder::new();
775 let graph = builder.build_from_source(python).unwrap();
776
777 assert!(graph.get_node("import:os").is_some());
778 assert!(graph.get_node("import:sys").is_some());
779 assert_eq!(graph.node_count(), 2);
780 }
781
782 #[test]
783 fn test_multiple_classes_with_methods() {
784 let python = r#"
785class Dog:
786 def bark(self):
787 pass
788
789class Cat:
790 def meow(self):
791 pass
792"#;
793 let mut builder = GraphBuilder::new();
794 let graph = builder.build_from_source(python).unwrap();
795
796 assert!(graph.get_node("Dog").is_some());
797 assert!(graph.get_node("Cat").is_some());
798 assert!(graph.get_node("Dog.bark").is_some());
799 assert!(graph.get_node("Cat.meow").is_some());
800 assert_eq!(graph.node_count(), 4);
801 }
802
803 #[test]
804 fn test_chained_calls_edges() {
805 let python = r#"
806def a():
807 return 1
808
809def b():
810 return a()
811
812def c():
813 return b()
814"#;
815 let mut builder = GraphBuilder::new();
816 let graph = builder.build_from_source(python).unwrap();
817
818 assert_eq!(graph.node_count(), 3);
819 assert_eq!(graph.edge_count(), 2);
820
821 let c_out = graph.outgoing_edges("c");
823 assert_eq!(c_out.len(), 1);
824 assert_eq!(c_out[0].0.id, "b");
825 assert_eq!(c_out[0].1.kind, EdgeKind::Calls);
826
827 let b_out = graph.outgoing_edges("b");
829 assert_eq!(b_out.len(), 1);
830 assert_eq!(b_out[0].0.id, "a");
831 }
832
833 #[test]
834 fn test_function_calling_multiple_functions() {
835 let python = r#"
836def x():
837 return 1
838
839def y():
840 return 2
841
842def z():
843 return x() + y()
844"#;
845 let mut builder = GraphBuilder::new();
846 let graph = builder.build_from_source(python).unwrap();
847
848 let z_out = graph.outgoing_edges("z");
849 assert_eq!(z_out.len(), 2);
850 let callee_names: Vec<&str> = z_out.iter().map(|(n, _)| n.id.as_str()).collect();
851 assert!(callee_names.contains(&"x"));
852 assert!(callee_names.contains(&"y"));
853 }
854
855 #[test]
856 fn test_class_with_multiple_methods() {
857 let python = r#"
858class Calc:
859 def add(self):
860 pass
861 def sub(self):
862 pass
863 def mul(self):
864 pass
865"#;
866 let mut builder = GraphBuilder::new();
867 let graph = builder.build_from_source(python).unwrap();
868
869 assert!(graph.get_node("Calc").is_some());
870 assert!(graph.get_node("Calc.add").is_some());
871 assert!(graph.get_node("Calc.sub").is_some());
872 assert!(graph.get_node("Calc.mul").is_some());
873 assert_eq!(graph.node_count(), 4);
875 }
876
877 #[test]
878 fn test_edge_kind_variants() {
879 assert_ne!(EdgeKind::Calls, EdgeKind::Imports);
880 assert_ne!(EdgeKind::Calls, EdgeKind::Inherits);
881 assert_ne!(EdgeKind::Calls, EdgeKind::Accesses);
882 assert_ne!(EdgeKind::Calls, EdgeKind::References);
883
884 let kind = EdgeKind::Calls;
886 let _cloned = kind;
887 let _copied = kind;
888 }
889
890 #[test]
891 fn test_node_kind_variants() {
892 assert_ne!(NodeKind::Function, NodeKind::Class);
893 assert_ne!(NodeKind::Function, NodeKind::Method);
894 assert_ne!(NodeKind::Function, NodeKind::Module);
895 assert_ne!(NodeKind::Function, NodeKind::Variable);
896
897 let kind = NodeKind::Function;
899 let _cloned = kind;
900 let _copied = kind;
901 }
902
903 #[test]
904 fn test_node_kind_serde_roundtrip() {
905 let kind = NodeKind::Function;
906 let json = serde_json::to_string(&kind).unwrap();
907 let deserialized: NodeKind = serde_json::from_str(&json).unwrap();
908 assert_eq!(kind, deserialized);
909 }
910
911 #[test]
912 fn test_edge_kind_serde_roundtrip() {
913 let kind = EdgeKind::Inherits;
914 let json = serde_json::to_string(&kind).unwrap();
915 let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
916 assert_eq!(kind, deserialized);
917 }
918
919 #[test]
920 fn test_offset_to_line_boundary() {
921 let mut builder = GraphBuilder::new();
922 builder.source = "line1\nline2\nline3\n".to_string();
923 assert_eq!(builder.offset_to_line(0), 1);
925 assert_eq!(builder.offset_to_line(5), 1);
927 assert_eq!(builder.offset_to_line(6), 2);
929 }
930
931 #[test]
932 fn test_offset_to_column_boundary() {
933 let mut builder = GraphBuilder::new();
934 builder.source = "abc\ndef\n".to_string();
935 assert_eq!(builder.offset_to_column(0), 1);
937 assert_eq!(builder.offset_to_column(1), 2);
939 assert_eq!(builder.offset_to_column(4), 1);
941 assert_eq!(builder.offset_to_column(5), 2);
943 }
944
945 #[test]
946 fn test_calls_in_if_body() {
947 let python = r#"
948def helper():
949 return 1
950
951def main():
952 if True:
953 helper()
954"#;
955 let mut builder = GraphBuilder::new();
956 let graph = builder.build_from_source(python).unwrap();
957
958 let out = graph.outgoing_edges("main");
959 assert_eq!(out.len(), 1);
960 assert_eq!(out[0].0.id, "helper");
961 }
962
963 #[test]
964 fn test_calls_in_for_loop() {
965 let python = r#"
966def process():
967 return 1
968
969def main():
970 for i in range(10):
971 process()
972"#;
973 let mut builder = GraphBuilder::new();
974 let graph = builder.build_from_source(python).unwrap();
975
976 let out = graph.outgoing_edges("main");
977 assert!(out.iter().any(|(n, _)| n.id == "process"));
979 }
980
981 #[test]
982 fn test_calls_in_while_loop() {
983 let python = r#"
984def check():
985 return True
986
987def main():
988 while check():
989 pass
990"#;
991 let mut builder = GraphBuilder::new();
992 let graph = builder.build_from_source(python).unwrap();
993
994 let out = graph.outgoing_edges("main");
995 assert!(out.iter().any(|(n, _)| n.id == "check"));
996 }
997
998 #[test]
999 fn test_calls_in_assignment() {
1000 let python = r#"
1001def compute():
1002 return 42
1003
1004def main():
1005 x = compute()
1006"#;
1007 let mut builder = GraphBuilder::new();
1008 let graph = builder.build_from_source(python).unwrap();
1009
1010 let out = graph.outgoing_edges("main");
1011 assert_eq!(out.len(), 1);
1012 assert_eq!(out[0].0.id, "compute");
1013 }
1014
1015 #[test]
1020 fn test_s9b7_offset_to_line_empty_source() {
1021 let mut builder = GraphBuilder::new();
1022 builder.source = String::new();
1023 assert_eq!(builder.offset_to_line(0), 1);
1024 assert_eq!(builder.offset_to_line(100), 1);
1025 }
1026
1027 #[test]
1028 fn test_s9b7_offset_to_column_empty_source() {
1029 let mut builder = GraphBuilder::new();
1030 builder.source = String::new();
1031 assert_eq!(builder.offset_to_column(0), 1);
1032 }
1033
1034 #[test]
1035 fn test_s9b7_calls_in_if_else() {
1036 let python = r#"
1037def branch_a():
1038 return 1
1039
1040def branch_b():
1041 return 2
1042
1043def main():
1044 if True:
1045 branch_a()
1046 else:
1047 branch_b()
1048"#;
1049 let mut builder = GraphBuilder::new();
1050 let graph = builder.build_from_source(python).unwrap();
1051 let out = graph.outgoing_edges("main");
1052 assert_eq!(out.len(), 2);
1053 let names: Vec<&str> = out.iter().map(|(n, _)| n.id.as_str()).collect();
1054 assert!(names.contains(&"branch_a"));
1055 assert!(names.contains(&"branch_b"));
1056 }
1057
1058 #[test]
1059 fn test_s9b7_calls_in_return_stmt() {
1060 let python = r#"
1061def helper():
1062 return 42
1063
1064def main():
1065 return helper()
1066"#;
1067 let mut builder = GraphBuilder::new();
1068 let graph = builder.build_from_source(python).unwrap();
1069 let out = graph.outgoing_edges("main");
1070 assert_eq!(out.len(), 1);
1071 assert_eq!(out[0].0.id, "helper");
1072 }
1073
1074 #[test]
1075 fn test_s9b7_calls_via_binop() {
1076 let python = r#"
1077def left():
1078 return 1
1079
1080def right():
1081 return 2
1082
1083def main():
1084 return left() + right()
1085"#;
1086 let mut builder = GraphBuilder::new();
1087 let graph = builder.build_from_source(python).unwrap();
1088 let out = graph.outgoing_edges("main");
1089 assert_eq!(out.len(), 2);
1090 }
1091
1092 #[test]
1093 fn test_s9b7_method_calls_method() {
1094 let python = r#"
1095class Foo:
1096 def helper(self):
1097 return 1
1098
1099 def caller(self):
1100 return self.helper()
1101"#;
1102 let mut builder = GraphBuilder::new();
1103 let graph = builder.build_from_source(python).unwrap();
1104 assert_eq!(graph.node_count(), 3);
1106 }
1107
1108 #[test]
1109 fn test_s9b7_node_ids_empty_graph() {
1110 let graph = DependencyGraph::new();
1111 assert!(graph.node_ids().is_empty());
1112 }
1113
1114 #[test]
1115 fn test_s9b7_add_node_returns_index() {
1116 let mut graph = DependencyGraph::new();
1117 let node = GraphNode {
1118 id: "test_node".to_string(),
1119 kind: NodeKind::Function,
1120 file: std::path::PathBuf::new(),
1121 line: 1,
1122 column: 1,
1123 error_count: 0,
1124 impact_score: 0.0,
1125 };
1126 let idx = graph.add_node(node);
1127 assert_eq!(graph.node_count(), 1);
1128 assert_eq!(graph.graph[idx].id, "test_node");
1129 }
1130
1131 #[test]
1132 fn test_s9b7_calls_nested_in_args() {
1133 let python = r#"
1134def inner():
1135 return 1
1136
1137def outer(x):
1138 return x
1139
1140def main():
1141 return outer(inner())
1142"#;
1143 let mut builder = GraphBuilder::new();
1144 let graph = builder.build_from_source(python).unwrap();
1145 let out = graph.outgoing_edges("main");
1146 assert!(out.len() >= 2);
1147 }
1148
1149 #[test]
1150 fn test_s9b7_class_calling_function() {
1151 let python = r#"
1152def utility():
1153 return 42
1154
1155class MyClass:
1156 def method(self):
1157 utility()
1158"#;
1159 let mut builder = GraphBuilder::new();
1160 let graph = builder.build_from_source(python).unwrap();
1161 let out = graph.outgoing_edges("MyClass.method");
1162 assert!(out.iter().any(|(n, _)| n.id == "utility"));
1163 }
1164
1165 #[test]
1166 fn test_multiple_inheritance_edges() {
1167 let python = r#"
1168class A:
1169 pass
1170
1171class B:
1172 pass
1173
1174class C(A, B):
1175 pass
1176"#;
1177 let mut builder = GraphBuilder::new();
1178 let graph = builder.build_from_source(python).unwrap();
1179
1180 let c_out = graph.outgoing_edges("C");
1181 assert_eq!(c_out.len(), 2);
1182 let base_names: Vec<&str> = c_out.iter().map(|(n, _)| n.id.as_str()).collect();
1183 assert!(base_names.contains(&"A"));
1184 assert!(base_names.contains(&"B"));
1185
1186 for (_, edge) in &c_out {
1188 assert_eq!(edge.kind, EdgeKind::Inherits);
1189 }
1190 }
1191
1192 #[test]
1197 fn test_s11_calls_in_dict_values() {
1198 let python = r#"
1199def val_func():
1200 return 42
1201
1202def main():
1203 d = {1: val_func()}
1204"#;
1205 let mut builder = GraphBuilder::new();
1206 let graph = builder.build_from_source(python).unwrap();
1207 let out = graph.outgoing_edges("main");
1208 assert!(out.iter().any(|(n, _)| n.id == "val_func"));
1209 }
1210
1211 #[test]
1212 fn test_s11_calls_in_list_elements() {
1213 let python = r#"
1214def elem_func():
1215 return 1
1216
1217def main():
1218 items = [elem_func(), 2, 3]
1219"#;
1220 let mut builder = GraphBuilder::new();
1221 let graph = builder.build_from_source(python).unwrap();
1222 let out = graph.outgoing_edges("main");
1223 assert!(out.iter().any(|(n, _)| n.id == "elem_func"));
1224 }
1225
1226 #[test]
1227 fn test_s11_calls_in_compare_expr() {
1228 let python = r#"
1229def check():
1230 return 5
1231
1232def main():
1233 if check() > 3:
1234 pass
1235"#;
1236 let mut builder = GraphBuilder::new();
1237 let graph = builder.build_from_source(python).unwrap();
1238 let out = graph.outgoing_edges("main");
1239 assert!(out.iter().any(|(n, _)| n.id == "check"));
1240 }
1241
1242 #[test]
1243 fn test_s11_calls_in_compare_comparators() {
1244 let python = r#"
1245def threshold():
1246 return 10
1247
1248def main():
1249 if 5 > threshold():
1250 pass
1251"#;
1252 let mut builder = GraphBuilder::new();
1253 let graph = builder.build_from_source(python).unwrap();
1254 let out = graph.outgoing_edges("main");
1255 assert!(out.iter().any(|(n, _)| n.id == "threshold"));
1256 }
1257
1258 #[test]
1259 fn test_s11_calls_via_expr_stmt() {
1260 let python = r#"
1262def side_effect():
1263 pass
1264
1265def main():
1266 side_effect()
1267"#;
1268 let mut builder = GraphBuilder::new();
1269 let graph = builder.build_from_source(python).unwrap();
1270 let out = graph.outgoing_edges("main");
1271 assert_eq!(out.len(), 1);
1272 assert_eq!(out[0].0.id, "side_effect");
1273 }
1274
1275 #[test]
1276 fn test_s11_calls_in_for_body() {
1277 let python = r#"
1278def process(x):
1279 return x
1280
1281def main():
1282 for i in [1, 2]:
1283 process(i)
1284"#;
1285 let mut builder = GraphBuilder::new();
1286 let graph = builder.build_from_source(python).unwrap();
1287 let out = graph.outgoing_edges("main");
1288 assert!(out.iter().any(|(n, _)| n.id == "process"));
1289 }
1290
1291 #[test]
1292 fn test_s11_calls_in_while_body() {
1293 let python = r#"
1294def tick():
1295 pass
1296
1297def main():
1298 while True:
1299 tick()
1300"#;
1301 let mut builder = GraphBuilder::new();
1302 let graph = builder.build_from_source(python).unwrap();
1303 let out = graph.outgoing_edges("main");
1304 assert!(out.iter().any(|(n, _)| n.id == "tick"));
1305 }
1306
1307 #[test]
1308 fn test_s11_calls_with_class_constructor() {
1309 let python = r#"
1311class Widget:
1312 def render(self):
1313 pass
1314
1315def main():
1316 w = Widget()
1317"#;
1318 let mut builder = GraphBuilder::new();
1319 let graph = builder.build_from_source(python).unwrap();
1320 let out = graph.outgoing_edges("main");
1321 assert!(out.iter().any(|(n, _)| n.id == "Widget"));
1323 }
1324
1325 #[test]
1326 fn test_s11_no_edge_for_external_function() {
1327 let python = r#"
1328def main():
1329 print("hello")
1330"#;
1331 let mut builder = GraphBuilder::new();
1332 let graph = builder.build_from_source(python).unwrap();
1333 let out = graph.outgoing_edges("main");
1335 assert!(out.is_empty());
1336 }
1337
1338 #[test]
1339 fn test_s11_calls_in_for_iter_expr() {
1340 let python = r#"
1341def get_items():
1342 return [1, 2, 3]
1343
1344def main():
1345 for x in get_items():
1346 pass
1347"#;
1348 let mut builder = GraphBuilder::new();
1349 let graph = builder.build_from_source(python).unwrap();
1350 let out = graph.outgoing_edges("main");
1351 assert!(out.iter().any(|(n, _)| n.id == "get_items"));
1352 }
1353
1354 #[test]
1355 fn test_s11_offset_to_line_multiline() {
1356 let mut builder = GraphBuilder::new();
1357 builder.source = "a\nb\nc\nd\ne".to_string();
1358 assert_eq!(builder.offset_to_line(0), 1); assert_eq!(builder.offset_to_line(2), 2); assert_eq!(builder.offset_to_line(4), 3); assert_eq!(builder.offset_to_line(8), 5); }
1363
1364 #[test]
1369 fn test_s12_calls_in_dict_keys_and_values() {
1370 let python = r#"
1371def key_fn():
1372 return "k"
1373
1374def val_fn():
1375 return 42
1376
1377def main():
1378 d = {key_fn(): val_fn()}
1379"#;
1380 let mut builder = GraphBuilder::new();
1381 let graph = builder.build_from_source(python).unwrap();
1382 let out = graph.outgoing_edges("main");
1383 let names: Vec<&str> = out.iter().map(|(n, _)| n.id.as_str()).collect();
1384 assert!(names.contains(&"key_fn"), "Missing key_fn edge");
1385 assert!(names.contains(&"val_fn"), "Missing val_fn edge");
1386 }
1387
1388 #[test]
1389 fn test_s12_calls_in_list_multiple() {
1390 let python = r#"
1391def a():
1392 return 1
1393def b():
1394 return 2
1395def main():
1396 items = [a(), b(), 3]
1397"#;
1398 let mut builder = GraphBuilder::new();
1399 let graph = builder.build_from_source(python).unwrap();
1400 let out = graph.outgoing_edges("main");
1401 assert!(out.iter().any(|(n, _)| n.id == "a"));
1402 assert!(out.iter().any(|(n, _)| n.id == "b"));
1403 }
1404
1405 #[test]
1406 fn test_s12_calls_in_compare_multiple_comparators() {
1407 let python = r#"
1409def lower():
1410 return 1
1411def upper():
1412 return 10
1413def main():
1414 if lower() < 5 < upper():
1415 pass
1416"#;
1417 let mut builder = GraphBuilder::new();
1418 let graph = builder.build_from_source(python).unwrap();
1419 let out = graph.outgoing_edges("main");
1420 let names: Vec<&str> = out.iter().map(|(n, _)| n.id.as_str()).collect();
1421 assert!(names.contains(&"lower"));
1422 assert!(names.contains(&"upper"));
1423 }
1424
1425 #[test]
1426 fn test_s12_attribute_call_detection() {
1427 let python = r#"
1429class Foo:
1430 def helper(self):
1431 return 1
1432
1433 def caller(self):
1434 x = self.helper()
1435"#;
1436 let mut builder = GraphBuilder::new();
1437 let graph = builder.build_from_source(python).unwrap();
1438 assert!(graph.get_node("Foo").is_some());
1440 assert!(graph.get_node("Foo.helper").is_some());
1441 assert!(graph.get_node("Foo.caller").is_some());
1442 }
1443
1444 #[test]
1445 fn test_s12_empty_class_body() {
1446 let python = r#"
1447class Empty:
1448 pass
1449"#;
1450 let mut builder = GraphBuilder::new();
1451 let graph = builder.build_from_source(python).unwrap();
1452 assert!(graph.get_node("Empty").is_some());
1453 assert_eq!(graph.get_node("Empty").unwrap().kind, NodeKind::Class);
1454 }
1455
1456 #[test]
1457 fn test_s12_class_no_bases() {
1458 let python = r#"
1459class Standalone:
1460 def method(self):
1461 pass
1462"#;
1463 let mut builder = GraphBuilder::new();
1464 let graph = builder.build_from_source(python).unwrap();
1465 let edges = graph.outgoing_edges("Standalone");
1467 assert!(edges.iter().all(|(_, e)| e.kind != EdgeKind::Inherits));
1468 }
1469
1470 #[test]
1471 fn test_s12_from_import_not_tracked() {
1472 let python = "from os import path\n\ndef foo():\n pass\n";
1475 let mut builder = GraphBuilder::new();
1476 let graph = builder.build_from_source(python).unwrap();
1477 assert!(graph.get_node("import:os").is_none());
1479 assert!(graph.get_node("foo").is_some());
1481 }
1482
1483 #[test]
1484 fn test_s12_graph_node_serde() {
1485 let node = GraphNode {
1486 id: "test".to_string(),
1487 kind: NodeKind::Variable,
1488 file: std::path::PathBuf::from("file.py"),
1489 line: 42,
1490 column: 8,
1491 error_count: 3,
1492 impact_score: 1.5,
1493 };
1494 let json = serde_json::to_string(&node).unwrap();
1495 let back: GraphNode = serde_json::from_str(&json).unwrap();
1496 assert_eq!(back.kind, NodeKind::Variable);
1497 assert_eq!(back.line, 42);
1498 assert_eq!(back.error_count, 3);
1499 }
1500
1501 #[test]
1502 fn test_s12_edge_kind_all_variants_serde() {
1503 for kind in [
1504 EdgeKind::Calls,
1505 EdgeKind::Imports,
1506 EdgeKind::Inherits,
1507 EdgeKind::Accesses,
1508 EdgeKind::References,
1509 ] {
1510 let json = serde_json::to_string(&kind).unwrap();
1511 let back: EdgeKind = serde_json::from_str(&json).unwrap();
1512 assert_eq!(kind, back);
1513 }
1514 }
1515
1516 #[test]
1517 fn test_s12_node_kind_all_variants_serde() {
1518 for kind in [
1519 NodeKind::Function,
1520 NodeKind::Class,
1521 NodeKind::Method,
1522 NodeKind::Module,
1523 NodeKind::Variable,
1524 ] {
1525 let json = serde_json::to_string(&kind).unwrap();
1526 let back: NodeKind = serde_json::from_str(&json).unwrap();
1527 assert_eq!(kind, back);
1528 }
1529 }
1530
1531 #[test]
1532 fn test_s12_deeply_nested_call_chain() {
1533 let python = r#"
1534def d():
1535 return 1
1536def c():
1537 return d()
1538def b():
1539 return c()
1540def a():
1541 return b()
1542"#;
1543 let mut builder = GraphBuilder::new();
1544 let graph = builder.build_from_source(python).unwrap();
1545 assert_eq!(graph.edge_count(), 3); let d_incoming = graph.incoming_edges("d");
1547 assert_eq!(d_incoming.len(), 1);
1548 assert_eq!(d_incoming[0].0.id, "c");
1549 }
1550}