Skip to main content

project_examer/
dependency_graph.rs

1use crate::simple_parser::{ParsedFile, Function, Class};
2use petgraph::{Graph, Directed, graph::NodeIndex};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::PathBuf;
6
7pub type DependencyGraph = Graph<Node, Edge, Directed>;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Node {
11    pub id: String,
12    pub node_type: NodeType,
13    pub file_path: PathBuf,
14    pub line_number: usize,
15    pub metadata: NodeMetadata,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub enum NodeType {
20    File,
21    Module,
22    Function,
23    Class,
24    Variable,
25    Import,
26    Export,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct NodeMetadata {
31    pub name: String,
32    pub language: Option<String>,
33    pub size: Option<u64>,
34    pub complexity: Option<usize>,
35    pub parameters: Vec<String>,
36    pub return_type: Option<String>,
37    pub is_async: bool,
38    pub is_exported: bool,
39    pub docstring: Option<String>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct Edge {
44    pub edge_type: EdgeType,
45    pub weight: f64,
46    pub metadata: EdgeMetadata,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub enum EdgeType {
51    Imports,
52    Calls,
53    Extends,
54    Implements,
55    Contains,
56    References,
57    DependsOn,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct EdgeMetadata {
62    pub call_count: usize,
63    pub is_direct: bool,
64    pub line_numbers: Vec<usize>,
65}
66
67pub struct GraphBuilder {
68    graph: DependencyGraph,
69    node_map: HashMap<String, NodeIndex>,
70    file_nodes: HashMap<PathBuf, NodeIndex>,
71}
72
73impl GraphBuilder {
74    pub fn new() -> Self {
75        Self {
76            graph: Graph::new(),
77            node_map: HashMap::new(),
78            file_nodes: HashMap::new(),
79        }
80    }
81
82    pub fn build_graph(&mut self, parsed_files: &[ParsedFile]) -> &DependencyGraph {
83        for parsed_file in parsed_files {
84            self.add_file_node(parsed_file);
85            self.add_imports(parsed_file);
86            self.add_functions(parsed_file);
87            self.add_classes(parsed_file);
88        }
89
90        self.add_call_relationships(parsed_files);
91        &self.graph
92    }
93
94    fn add_file_node(&mut self, parsed_file: &ParsedFile) {
95        let node_id = format!("file:{}", parsed_file.file_info.path.display());
96        
97        let node = Node {
98            id: node_id.clone(),
99            node_type: NodeType::File,
100            file_path: parsed_file.file_info.path.clone(),
101            line_number: 1,
102            metadata: NodeMetadata {
103                name: parsed_file.file_info.path
104                    .file_name()
105                    .and_then(|n| n.to_str())
106                    .unwrap_or("unknown")
107                    .to_string(),
108                language: parsed_file.file_info.language.clone(),
109                size: Some(parsed_file.file_info.size),
110                complexity: Some(self.calculate_file_complexity(parsed_file)),
111                parameters: Vec::new(),
112                return_type: None,
113                is_async: false,
114                is_exported: false,
115                docstring: None,
116            },
117        };
118
119        let node_index = self.graph.add_node(node);
120        self.node_map.insert(node_id, node_index);
121        self.file_nodes.insert(parsed_file.file_info.path.clone(), node_index);
122    }
123
124    fn add_imports(&mut self, parsed_file: &ParsedFile) {
125        let file_node = self.file_nodes[&parsed_file.file_info.path];
126
127        for import in &parsed_file.imports {
128            let import_id = format!("import:{}:{}", parsed_file.file_info.path.display(), import.module);
129            
130            let node = Node {
131                id: import_id.clone(),
132                node_type: NodeType::Import,
133                file_path: parsed_file.file_info.path.clone(),
134                line_number: import.line_number,
135                metadata: NodeMetadata {
136                    name: import.module.clone(),
137                    language: parsed_file.file_info.language.clone(),
138                    size: None,
139                    complexity: None,
140                    parameters: import.items.clone(),
141                    return_type: None,
142                    is_async: false,
143                    is_exported: false,
144                    docstring: None,
145                },
146            };
147
148            let import_node = self.graph.add_node(node);
149            self.node_map.insert(import_id, import_node);
150
151            let edge = Edge {
152                edge_type: EdgeType::Contains,
153                weight: 1.0,
154                metadata: EdgeMetadata {
155                    call_count: 1,
156                    is_direct: true,
157                    line_numbers: vec![import.line_number],
158                },
159            };
160
161            self.graph.add_edge(file_node, import_node, edge);
162        }
163    }
164
165    fn add_functions(&mut self, parsed_file: &ParsedFile) {
166        let file_node = self.file_nodes[&parsed_file.file_info.path];
167
168        for function in &parsed_file.functions {
169            let function_id = format!("function:{}:{}", parsed_file.file_info.path.display(), function.name);
170            
171            let node = Node {
172                id: function_id.clone(),
173                node_type: NodeType::Function,
174                file_path: parsed_file.file_info.path.clone(),
175                line_number: function.line_number,
176                metadata: NodeMetadata {
177                    name: function.name.clone(),
178                    language: parsed_file.file_info.language.clone(),
179                    size: None,
180                    complexity: Some(self.calculate_function_complexity(function)),
181                    parameters: function.parameters.clone(),
182                    return_type: function.return_type.clone(),
183                    is_async: function.is_async,
184                    is_exported: self.is_function_exported(parsed_file, function),
185                    docstring: None,
186                },
187            };
188
189            let function_node = self.graph.add_node(node);
190            self.node_map.insert(function_id, function_node);
191
192            let edge = Edge {
193                edge_type: EdgeType::Contains,
194                weight: 1.0,
195                metadata: EdgeMetadata {
196                    call_count: 1,
197                    is_direct: true,
198                    line_numbers: vec![function.line_number],
199                },
200            };
201
202            self.graph.add_edge(file_node, function_node, edge);
203        }
204    }
205
206    fn add_classes(&mut self, parsed_file: &ParsedFile) {
207        let file_node = self.file_nodes[&parsed_file.file_info.path];
208
209        for class in &parsed_file.classes {
210            let class_id = format!("class:{}:{}", parsed_file.file_info.path.display(), class.name);
211            
212            let node = Node {
213                id: class_id.clone(),
214                node_type: NodeType::Class,
215                file_path: parsed_file.file_info.path.clone(),
216                line_number: class.line_number,
217                metadata: NodeMetadata {
218                    name: class.name.clone(),
219                    language: parsed_file.file_info.language.clone(),
220                    size: None,
221                    complexity: Some(self.calculate_class_complexity(class)),
222                    parameters: Vec::new(),
223                    return_type: None,
224                    is_async: false,
225                    is_exported: self.is_class_exported(parsed_file, class),
226                    docstring: None,
227                },
228            };
229
230            let class_node = self.graph.add_node(node);
231            self.node_map.insert(class_id, class_node);
232
233            let edge = Edge {
234                edge_type: EdgeType::Contains,
235                weight: 1.0,
236                metadata: EdgeMetadata {
237                    call_count: 1,
238                    is_direct: true,
239                    line_numbers: vec![class.line_number],
240                },
241            };
242
243            self.graph.add_edge(file_node, class_node, edge);
244
245            for method in &class.methods {
246                let method_id = format!("method:{}:{}:{}", parsed_file.file_info.path.display(), class.name, method.name);
247                
248                let method_node_data = Node {
249                    id: method_id.clone(),
250                    node_type: NodeType::Function,
251                    file_path: parsed_file.file_info.path.clone(),
252                    line_number: method.line_number,
253                    metadata: NodeMetadata {
254                        name: format!("{}.{}", class.name, method.name),
255                        language: parsed_file.file_info.language.clone(),
256                        size: None,
257                        complexity: Some(self.calculate_function_complexity(method)),
258                        parameters: method.parameters.clone(),
259                        return_type: method.return_type.clone(),
260                        is_async: method.is_async,
261                        is_exported: false,
262                        docstring: None,
263                    },
264                };
265
266                let method_node = self.graph.add_node(method_node_data);
267                self.node_map.insert(method_id, method_node);
268
269                let method_edge = Edge {
270                    edge_type: EdgeType::Contains,
271                    weight: 1.0,
272                    metadata: EdgeMetadata {
273                        call_count: 1,
274                        is_direct: true,
275                        line_numbers: vec![method.line_number],
276                    },
277                };
278
279                self.graph.add_edge(class_node, method_node, method_edge);
280            }
281        }
282    }
283
284    fn add_call_relationships(&mut self, parsed_files: &[ParsedFile]) {
285        for parsed_file in parsed_files {
286            for import in &parsed_file.imports {
287                if let Some(target_file) = self.find_imported_file(parsed_files, &import.module) {
288                    if let Some(&import_node) = self.node_map.get(&format!("import:{}:{}", parsed_file.file_info.path.display(), import.module)) {
289                        if let Some(&target_node) = self.file_nodes.get(&target_file.file_info.path) {
290                            let edge = Edge {
291                                edge_type: EdgeType::DependsOn,
292                                weight: 1.0,
293                                metadata: EdgeMetadata {
294                                    call_count: 1,
295                                    is_direct: true,
296                                    line_numbers: vec![import.line_number],
297                                },
298                            };
299
300                            self.graph.add_edge(import_node, target_node, edge);
301                        }
302                    }
303                }
304            }
305        }
306    }
307
308    fn find_imported_file<'a>(&self, parsed_files: &'a [ParsedFile], module_name: &str) -> Option<&'a ParsedFile> {
309        parsed_files.iter().find(|f| {
310            f.file_info.path
311                .file_stem()
312                .and_then(|s| s.to_str())
313                .map(|s| s == module_name)
314                .unwrap_or(false)
315        })
316    }
317
318    fn calculate_file_complexity(&self, parsed_file: &ParsedFile) -> usize {
319        parsed_file.functions.len() + parsed_file.classes.len() + parsed_file.imports.len()
320    }
321
322    fn calculate_function_complexity(&self, function: &Function) -> usize {
323        function.parameters.len() + if function.is_async { 2 } else { 1 }
324    }
325
326    fn calculate_class_complexity(&self, class: &Class) -> usize {
327        class.methods.len() + class.implements.len() + if class.extends.is_some() { 1 } else { 0 }
328    }
329
330    fn is_function_exported(&self, parsed_file: &ParsedFile, function: &Function) -> bool {
331        parsed_file.exports.iter().any(|e| e.name == function.name)
332    }
333
334    fn is_class_exported(&self, parsed_file: &ParsedFile, class: &Class) -> bool {
335        parsed_file.exports.iter().any(|e| e.name == class.name)
336    }
337
338    pub fn get_graph(&self) -> &DependencyGraph {
339        &self.graph
340    }
341
342    pub fn get_node_map(&self) -> &HashMap<String, NodeIndex> {
343        &self.node_map
344    }
345
346    pub fn analyze_dependencies(&self) -> DependencyAnalysis {
347        let total_nodes = self.graph.node_count();
348        let total_edges = self.graph.edge_count();
349        
350        let mut node_types = HashMap::new();
351        let mut edge_types = HashMap::new();
352        let strongly_connected_components = 0;
353        
354        for node_weight in self.graph.node_weights() {
355            *node_types.entry(format!("{:?}", node_weight.node_type)).or_insert(0) += 1;
356        }
357        
358        for edge_weight in self.graph.edge_weights() {
359            *edge_types.entry(format!("{:?}", edge_weight.edge_type)).or_insert(0) += 1;
360        }
361
362        DependencyAnalysis {
363            total_nodes,
364            total_edges,
365            node_types,
366            edge_types,
367            strongly_connected_components,
368            avg_degree: if total_nodes > 0 { total_edges as f64 / total_nodes as f64 } else { 0.0 },
369        }
370    }
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize)]
374pub struct DependencyAnalysis {
375    pub total_nodes: usize,
376    pub total_edges: usize,
377    pub node_types: HashMap<String, usize>,
378    pub edge_types: HashMap<String, usize>,
379    pub strongly_connected_components: usize,
380    pub avg_degree: f64,
381}
382
383impl DependencyAnalysis {
384    pub fn print_summary(&self) {
385        println!("Dependency Graph Analysis:");
386        println!("  Total nodes: {}", self.total_nodes);
387        println!("  Total edges: {}", self.total_edges);
388        println!("  Average degree: {:.2}", self.avg_degree);
389        
390        println!("  Node types:");
391        for (node_type, count) in &self.node_types {
392            println!("    {}: {}", node_type, count);
393        }
394        
395        println!("  Edge types:");
396        for (edge_type, count) in &self.edge_types {
397            println!("    {}: {}", edge_type, count);
398        }
399    }
400}