Skip to main content

depyler_graph/
builder.rs

1//! Dependency Graph Builder
2//!
3//! Parses Python source into a directed dependency graph where:
4//! - Nodes represent functions, classes, and modules
5//! - Edges represent calls, imports, and inheritance
6
7use 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/// Kind of node in the dependency graph
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16pub enum NodeKind {
17    /// A function definition
18    Function,
19    /// A class definition
20    Class,
21    /// A method within a class
22    Method,
23    /// A module import
24    Module,
25    /// A global variable/constant
26    Variable,
27}
28
29/// Kind of edge in the dependency graph
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
31pub enum EdgeKind {
32    /// Function/method call
33    Calls,
34    /// Import dependency
35    Imports,
36    /// Class inheritance
37    Inherits,
38    /// Attribute access
39    Accesses,
40    /// Variable reference
41    References,
42}
43
44/// The dependency graph structure
45pub struct DependencyGraph {
46    /// The underlying petgraph
47    pub graph: DiGraph<GraphNode, GraphEdge>,
48    /// Node lookup by ID
49    pub node_map: HashMap<String, NodeIndex>,
50    /// Source file path
51    pub source_file: PathBuf,
52    /// Source code for line calculation
53    source: String,
54}
55
56impl DependencyGraph {
57    /// Create a new empty graph
58    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    /// Get the number of nodes
68    pub fn node_count(&self) -> usize {
69        self.graph.node_count()
70    }
71
72    /// Get the number of edges
73    pub fn edge_count(&self) -> usize {
74        self.graph.edge_count()
75    }
76
77    /// Add a node to the graph
78    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    /// Add an edge between nodes
86    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    /// Get a node by ID
96    pub fn get_node(&self, id: &str) -> Option<&GraphNode> {
97        self.node_map.get(id).map(|&idx| &self.graph[idx])
98    }
99
100    /// Get a mutable node by ID
101    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    /// Get all node IDs
110    pub fn node_ids(&self) -> Vec<String> {
111        self.node_map.keys().cloned().collect()
112    }
113
114    /// Get incoming edges for a node
115    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    /// Get outgoing edges for a node
130    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
151/// Builder for constructing dependency graphs from Python source
152pub struct GraphBuilder {
153    /// Current class context (for method resolution)
154    current_class: Option<String>,
155    /// Known function names with their byte offsets
156    known_functions: HashMap<String, usize>,
157    /// Known class names with their byte offsets
158    known_classes: HashMap<String, usize>,
159    /// Source code for line calculation
160    source: String,
161}
162
163impl GraphBuilder {
164    /// Create a new graph builder
165    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    /// Convert byte offset to line number
175    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    /// Convert byte offset to column number
189    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    /// Build a dependency graph from Python source code
205    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        // Parse Python source
211        let ast = ast::Suite::parse(source, "<source>")
212            .map_err(|e| GraphError::ParseError(e.to_string()))?;
213
214        // First pass: collect all definitions
215        self.collect_definitions(&ast);
216
217        // Second pass: build nodes
218        for stmt in &ast {
219            self.process_statement(stmt, &mut graph)?;
220        }
221
222        // Third pass: build edges (calls, inheritance)
223        for stmt in &ast {
224            self.process_edges(stmt, &mut graph)?;
225        }
226
227        Ok(graph)
228    }
229
230    /// Collect all function and class definitions
231    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                    // Collect methods
242                    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    /// Process a statement to build nodes
257    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                // Process methods
292                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    /// Process edges (calls, inheritance)
333    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                // Add inheritance edges
345                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                // Process method bodies
356                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    /// Extract call edges from a function body
369    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    /// Extract calls from a statement
381    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    /// Extract calls from an expression
412    fn extract_calls_from_expr(&self, expr: &ast::Expr, caller: &str, graph: &mut DependencyGraph) {
413        match expr {
414            ast::Expr::Call(c) => {
415                // Get the called function name
416                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                    // Only add edge if callee exists in graph
430                    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                // Process arguments recursively
442                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        // bar should have an edge to foo
515        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        // Should have 2 classes + 2 methods
536        assert!(graph.node_count() >= 4);
537
538        // Derived should inherit from Base
539        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        // a should have 2 incoming edges (from b and c)
587        let incoming = graph.incoming_edges("a");
588        assert_eq!(incoming.len(), 2);
589
590        // b should have 1 outgoing edge (to a)
591        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        // GraphBuilder::default() should be equivalent to new()
648        let _builder2 = GraphBuilder::new();
649        // Both should parse the same source identically
650        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        // Mutate error count
666        let node = graph.get_node_mut("foo").unwrap();
667        node.error_count = 5;
668
669        // Verify mutation persisted
670        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        // c -> b
822        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        // b -> a
828        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        // 1 class + 3 methods
874        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        // Test Clone and Copy
885        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        // Test Clone and Copy
898        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        // offset 0 => line 1
924        assert_eq!(builder.offset_to_line(0), 1);
925        // offset 5 ('\n') => line 1 (before incrementing for the \n)
926        assert_eq!(builder.offset_to_line(5), 1);
927        // offset 6 ('l' of line2) => line 2
928        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        // offset 0 => column 1
936        assert_eq!(builder.offset_to_column(0), 1);
937        // offset 1 => column 2
938        assert_eq!(builder.offset_to_column(1), 2);
939        // offset 4 => first char of next line => column 1
940        assert_eq!(builder.offset_to_column(4), 1);
941        // offset 5 => column 2
942        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        // process is called (range is external so no edge)
978        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    // ========================================================================
1016    // S9B7: Coverage tests for builder
1017    // ========================================================================
1018
1019    #[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        // class + 2 methods
1105        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        // All inheritance edges
1187        for (_, edge) in &c_out {
1188            assert_eq!(edge.kind, EdgeKind::Inherits);
1189        }
1190    }
1191
1192    // ========================================================================
1193    // DEPYLER-99MODE-S11: Coverage tests for untested extract_calls branches
1194    // ========================================================================
1195
1196    #[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        // Covers extract_calls_from_stmt for Stmt::Expr
1261        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        // Calling a class is like calling its __init__
1310        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        // Widget is a known class, so edge should exist
1322        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        // print is not a known function, so no edge
1334        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); // 'a'
1359        assert_eq!(builder.offset_to_line(2), 2); // 'b'
1360        assert_eq!(builder.offset_to_line(4), 3); // 'c'
1361        assert_eq!(builder.offset_to_line(8), 5); // 'e'
1362    }
1363
1364    // ========================================================================
1365    // S12: Deep coverage tests for builder
1366    // ========================================================================
1367
1368    #[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        // a < b() < c() -- multiple comparators
1408        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        // self.method() style calls
1428        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        // Verify class and methods are parsed
1439        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        // No inheritance edges
1466        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        // from-imports (ImportFrom) are not currently tracked as module nodes
1473        // Only `import X` creates module nodes
1474        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        // from-import falls into _ => {} catch-all, so no import node
1478        assert!(graph.get_node("import:os").is_none());
1479        // But function should still be tracked
1480        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); // a->b, b->c, c->d
1546        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}