1use petgraph::graph::{DiGraph, NodeIndex};
16use petgraph::visit::DfsPostOrder;
17use petgraph::Direction;
18use serde::{Deserialize, Serialize};
19use std::collections::{HashMap, HashSet, VecDeque};
20
21use crate::import_analyzer::{ImportType, ImportedSymbol, PythonImport};
22use crate::stdlib_mapper::StdlibMapper;
23use crate::external_packages::ExternalPackageRegistry;
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
27pub struct ModuleNode {
28 pub identifier: String,
30
31 pub node_type: NodeType,
33
34 pub file_path: Option<String>,
36
37 pub is_local: bool,
39}
40
41#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
43pub enum NodeType {
44 File,
46
47 Package,
49
50 External,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct ImportEdge {
57 pub import_type: ImportType,
59
60 pub items: Vec<ImportedSymbol>,
62
63 pub alias: Option<String>,
65
66 pub line_number: Option<usize>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct CircularDependency {
73 pub cycle: Vec<String>,
75
76 pub suggestion: String,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct ValidationIssue {
83 pub issue_type: IssueType,
85
86 pub module: String,
88
89 pub description: String,
91
92 pub suggestion: Option<String>,
94
95 pub severity: Severity,
97}
98
99#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
101pub enum IssueType {
102 UnknownModule,
104
105 WildcardImport,
107
108 UnusedImport,
110
111 ImportConflict,
113
114 RelativeImportDepth,
116
117 CircularDependency,
119}
120
121#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
123pub enum Severity {
124 Info,
125 Warning,
126 Error,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct OptimizationSuggestion {
132 pub module: String,
134
135 pub optimization_type: OptimizationType,
137
138 pub description: String,
140
141 pub suggested_code: String,
143}
144
145#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
147pub enum OptimizationType {
148 RemoveUnused,
150
151 CombineImports,
153
154 ReplaceStarImport,
156
157 ReorderImports,
159}
160
161pub struct DependencyGraph {
163 graph: DiGraph<ModuleNode, ImportEdge>,
165
166 node_map: HashMap<String, NodeIndex>,
168
169 stdlib_mapper: StdlibMapper,
171
172 external_registry: ExternalPackageRegistry,
174
175 #[allow(dead_code)]
177 symbol_usage: HashMap<String, HashSet<String>>,
178}
179
180impl DependencyGraph {
181 pub fn new() -> Self {
183 Self {
184 graph: DiGraph::new(),
185 node_map: HashMap::new(),
186 stdlib_mapper: StdlibMapper::new(),
187 external_registry: ExternalPackageRegistry::new(),
188 symbol_usage: HashMap::new(),
189 }
190 }
191
192 pub fn add_module(&mut self, identifier: String, node_type: NodeType, file_path: Option<String>) -> NodeIndex {
194 if let Some(&idx) = self.node_map.get(&identifier) {
195 return idx;
196 }
197
198 let is_local = matches!(node_type, NodeType::File | NodeType::Package);
199
200 let node = ModuleNode {
201 identifier: identifier.clone(),
202 node_type,
203 file_path,
204 is_local,
205 };
206
207 let idx = self.graph.add_node(node);
208 self.node_map.insert(identifier, idx);
209 idx
210 }
211
212 pub fn add_import(
214 &mut self,
215 from_module: &str,
216 to_module: &str,
217 import_type: ImportType,
218 items: Vec<ImportedSymbol>,
219 alias: Option<String>,
220 line_number: Option<usize>,
221 ) {
222 let from_idx = self.node_map.get(from_module).copied();
223 let to_idx = self.node_map.get(to_module).copied();
224
225 if let (Some(from), Some(to)) = (from_idx, to_idx) {
226 let edge = ImportEdge {
227 import_type,
228 items,
229 alias,
230 line_number,
231 };
232
233 self.graph.add_edge(from, to, edge);
234 }
235 }
236
237 pub fn add_module_imports(&mut self, module_id: &str, file_path: Option<String>, imports: &[PythonImport]) {
239 let source_idx = self.add_module(
241 module_id.to_string(),
242 NodeType::File,
243 file_path,
244 );
245
246 for import in imports {
248 let node_type = if self.stdlib_mapper.get_module(&import.module).is_some()
250 || self.external_registry.get_package(&import.module).is_some() {
251 NodeType::External
252 } else {
253 NodeType::Package
254 };
255
256 let target_idx = self.add_module(
257 import.module.clone(),
258 node_type,
259 None,
260 );
261
262 let edge = ImportEdge {
264 import_type: import.import_type,
265 items: import.items.clone(),
266 alias: import.alias.clone(),
267 line_number: None,
268 };
269
270 self.graph.add_edge(source_idx, target_idx, edge);
271 }
272 }
273
274 pub fn detect_circular_dependencies(&self) -> Vec<CircularDependency> {
276 let mut cycles = Vec::new();
277 let mut visited = HashSet::new();
278 let mut rec_stack = Vec::new();
279
280 for node_idx in self.graph.node_indices() {
282 if !visited.contains(&node_idx) {
283 self.dfs_detect_cycle(node_idx, &mut visited, &mut rec_stack, &mut cycles);
284 }
285 }
286
287 cycles
288 }
289
290 fn dfs_detect_cycle(
292 &self,
293 node: NodeIndex,
294 visited: &mut HashSet<NodeIndex>,
295 rec_stack: &mut Vec<NodeIndex>,
296 cycles: &mut Vec<CircularDependency>,
297 ) {
298 visited.insert(node);
299 rec_stack.push(node);
300
301 for neighbor in self.graph.neighbors_directed(node, Direction::Outgoing) {
303 if !visited.contains(&neighbor) {
304 self.dfs_detect_cycle(neighbor, visited, rec_stack, cycles);
305 } else if rec_stack.contains(&neighbor) {
306 let cycle_start_pos = rec_stack.iter().position(|&n| n == neighbor).unwrap();
308 let cycle_nodes: Vec<String> = rec_stack[cycle_start_pos..]
309 .iter()
310 .map(|&idx| self.graph[idx].identifier.clone())
311 .collect();
312
313 let suggestion = self.suggest_cycle_fix(&cycle_nodes);
314
315 cycles.push(CircularDependency {
316 cycle: cycle_nodes,
317 suggestion,
318 });
319 }
320 }
321
322 rec_stack.pop();
323 }
324
325 fn suggest_cycle_fix(&self, cycle: &[String]) -> String {
327 if cycle.len() == 2 {
328 format!(
329 "Move shared code to a new module, or use delayed imports (import inside functions) in {}",
330 cycle[0]
331 )
332 } else {
333 format!(
334 "Refactor to break the cycle: {} → ... → {}. Consider:\n\
335 1. Extract shared code to a new module\n\
336 2. Use dependency injection\n\
337 3. Move imports inside functions (delayed imports)",
338 cycle.first().unwrap(),
339 cycle.last().unwrap()
340 )
341 }
342 }
343
344 pub fn validate_imports(&self, used_symbols: &HashMap<String, HashSet<String>>) -> Vec<ValidationIssue> {
346 let mut issues = Vec::new();
347
348 for edge_idx in self.graph.edge_indices() {
350 let (_, target_idx) = self.graph.edge_endpoints(edge_idx).unwrap();
351 let edge = &self.graph[edge_idx];
352 let target_node = &self.graph[target_idx];
353
354 if edge.import_type == ImportType::StarImport {
356 issues.push(ValidationIssue {
357 issue_type: IssueType::WildcardImport,
358 module: target_node.identifier.clone(),
359 description: format!(
360 "Wildcard import 'from {} import *' should be avoided",
361 target_node.identifier
362 ),
363 suggestion: Some("Use explicit imports instead".to_string()),
364 severity: Severity::Warning,
365 });
366 }
367
368 if edge.import_type == ImportType::FromImport && !edge.items.is_empty() {
370 let module_used = used_symbols.get(&target_node.identifier);
371
372 for item in &edge.items {
373 if let Some(used) = module_used {
374 if !used.contains(&item.name) {
375 issues.push(ValidationIssue {
376 issue_type: IssueType::UnusedImport,
377 module: target_node.identifier.clone(),
378 description: format!(
379 "Imported symbol '{}' from '{}' is not used",
380 item.name, target_node.identifier
381 ),
382 suggestion: Some(format!("Remove unused import: {}", item.name)),
383 severity: Severity::Info,
384 });
385 }
386 }
387 }
388 }
389 }
390
391 for node_idx in self.graph.node_indices() {
393 let node = &self.graph[node_idx];
394
395 if matches!(node.node_type, NodeType::Package) && node.file_path.is_none() {
398 if self.stdlib_mapper.get_module(&node.identifier).is_none()
400 && self.external_registry.get_package(&node.identifier).is_none()
401 {
402 issues.push(ValidationIssue {
403 issue_type: IssueType::UnknownModule,
404 module: node.identifier.clone(),
405 description: format!(
406 "Module '{}' is not mapped to a Rust crate",
407 node.identifier
408 ),
409 suggestion: Some("Add mapping to stdlib_mapper or external_packages".to_string()),
410 severity: Severity::Warning,
411 });
412 }
413 }
414 }
415
416 let mut alias_map: HashMap<String, Vec<String>> = HashMap::new();
418
419 for edge_idx in self.graph.edge_indices() {
420 let (_, target_idx) = self.graph.edge_endpoints(edge_idx).unwrap();
421 let edge = &self.graph[edge_idx];
422 let target_node = &self.graph[target_idx];
423
424 if let Some(ref alias) = edge.alias {
425 alias_map.entry(alias.clone())
426 .or_default()
427 .push(target_node.identifier.clone());
428 }
429 }
430
431 for (alias, modules) in alias_map {
432 if modules.len() > 1 {
433 issues.push(ValidationIssue {
434 issue_type: IssueType::ImportConflict,
435 module: modules.join(", "),
436 description: format!(
437 "Alias '{}' is used for multiple modules: {}",
438 alias,
439 modules.join(", ")
440 ),
441 suggestion: Some("Use different aliases for different modules".to_string()),
442 severity: Severity::Error,
443 });
444 }
445 }
446
447 issues
448 }
449
450 pub fn generate_optimizations(&self) -> Vec<OptimizationSuggestion> {
452 let mut suggestions = Vec::new();
453
454 let mut module_imports: HashMap<String, Vec<(NodeIndex, &ImportEdge)>> = HashMap::new();
456
457 for edge_idx in self.graph.edge_indices() {
458 let (source, target) = self.graph.edge_endpoints(edge_idx).unwrap();
459 let edge = &self.graph[edge_idx];
460 let target_module = &self.graph[target].identifier;
461
462 module_imports.entry(target_module.clone())
463 .or_default()
464 .push((source, edge));
465 }
466
467 for (module, imports) in module_imports {
469 if imports.len() > 1 {
470 let sources: HashSet<_> = imports.iter().map(|(src, _)| *src).collect();
472
473 if sources.len() == 1 {
474 let all_items: Vec<String> = imports.iter()
475 .flat_map(|(_, edge)| edge.items.iter().map(|s| s.name.clone()))
476 .collect();
477
478 if !all_items.is_empty() {
479 suggestions.push(OptimizationSuggestion {
480 module: module.clone(),
481 optimization_type: OptimizationType::CombineImports,
482 description: format!(
483 "Multiple imports from '{}' can be combined",
484 module
485 ),
486 suggested_code: format!(
487 "from {} import {}",
488 module,
489 all_items.join(", ")
490 ),
491 });
492 }
493 }
494 }
495 }
496
497 for edge_idx in self.graph.edge_indices() {
499 let (_, target_idx) = self.graph.edge_endpoints(edge_idx).unwrap();
500 let edge = &self.graph[edge_idx];
501 let target = &self.graph[target_idx];
502
503 if edge.import_type == ImportType::StarImport {
504 suggestions.push(OptimizationSuggestion {
505 module: target.identifier.clone(),
506 optimization_type: OptimizationType::ReplaceStarImport,
507 description: format!(
508 "Replace 'from {} import *' with explicit imports",
509 target.identifier
510 ),
511 suggested_code: format!(
512 "from {} import <specific_items>",
513 target.identifier
514 ),
515 });
516 }
517 }
518
519 suggestions
520 }
521
522 pub fn get_dependencies(&self, module: &str) -> HashSet<String> {
524 let mut deps = HashSet::new();
525
526 if let Some(&node_idx) = self.node_map.get(module) {
527 let mut queue = VecDeque::new();
528 queue.push_back(node_idx);
529
530 while let Some(idx) = queue.pop_front() {
531 for neighbor in self.graph.neighbors_directed(idx, Direction::Outgoing) {
532 let dep_module = &self.graph[neighbor].identifier;
533
534 if deps.insert(dep_module.clone()) {
535 queue.push_back(neighbor);
536 }
537 }
538 }
539 }
540
541 deps
542 }
543
544 pub fn get_dependents(&self, module: &str) -> HashSet<String> {
546 let mut dependents = HashSet::new();
547
548 if let Some(&node_idx) = self.node_map.get(module) {
549 for neighbor in self.graph.neighbors_directed(node_idx, Direction::Incoming) {
550 dependents.insert(self.graph[neighbor].identifier.clone());
551 }
552 }
553
554 dependents
555 }
556
557 pub fn topological_sort(&self) -> Result<Vec<String>, String> {
559 let mut post_order = DfsPostOrder::new(&self.graph, self.graph.node_indices().next().unwrap_or(NodeIndex::new(0)));
560 let mut sorted = Vec::new();
561
562 while let Some(node) = post_order.next(&self.graph) {
563 sorted.push(self.graph[node].identifier.clone());
564 }
565
566 sorted.reverse();
567 Ok(sorted)
568 }
569
570 pub fn stats(&self) -> GraphStats {
572 let total_modules = self.graph.node_count();
573 let total_imports = self.graph.edge_count();
574
575 let local_modules = self.graph.node_weights()
576 .filter(|n| n.is_local)
577 .count();
578
579 let external_modules = total_modules - local_modules;
580
581 GraphStats {
582 total_modules,
583 local_modules,
584 external_modules,
585 total_imports,
586 }
587 }
588}
589
590impl Default for DependencyGraph {
591 fn default() -> Self {
592 Self::new()
593 }
594}
595
596#[derive(Debug, Clone, Serialize, Deserialize)]
598pub struct GraphStats {
599 pub total_modules: usize,
600 pub local_modules: usize,
601 pub external_modules: usize,
602 pub total_imports: usize,
603}
604
605#[cfg(test)]
606mod tests {
607 use super::*;
608
609 #[test]
610 fn test_add_module() {
611 let mut graph = DependencyGraph::new();
612
613 let idx1 = graph.add_module("myapp.models".to_string(), NodeType::File, None);
614 let idx2 = graph.add_module("myapp.views".to_string(), NodeType::File, None);
615
616 assert_ne!(idx1, idx2);
617 assert_eq!(graph.graph.node_count(), 2);
618 }
619
620 #[test]
621 fn test_add_import() {
622 let mut graph = DependencyGraph::new();
623
624 graph.add_module("myapp.models".to_string(), NodeType::File, None);
625 graph.add_module("myapp.views".to_string(), NodeType::File, None);
626
627 graph.add_import(
628 "myapp.views",
629 "myapp.models",
630 ImportType::FromImport,
631 vec![ImportedSymbol { name: "User".to_string(), alias: None }],
632 None,
633 Some(1),
634 );
635
636 assert_eq!(graph.graph.edge_count(), 1);
637 }
638
639 #[test]
640 fn test_circular_dependency_detection() {
641 let mut graph = DependencyGraph::new();
642
643 graph.add_module("module_a".to_string(), NodeType::File, None);
645 graph.add_module("module_b".to_string(), NodeType::File, None);
646 graph.add_module("module_c".to_string(), NodeType::File, None);
647
648 graph.add_import("module_a", "module_b", ImportType::Module, vec![], None, None);
649 graph.add_import("module_b", "module_c", ImportType::Module, vec![], None, None);
650 graph.add_import("module_c", "module_a", ImportType::Module, vec![], None, None);
651
652 let cycles = graph.detect_circular_dependencies();
653
654 assert!(!cycles.is_empty(), "Should detect circular dependency");
655 assert!(cycles[0].cycle.contains(&"module_a".to_string()));
656 assert!(cycles[0].cycle.contains(&"module_b".to_string()));
657 }
658
659 #[test]
660 fn test_validate_wildcard_import() {
661 let mut graph = DependencyGraph::new();
662
663 graph.add_module("main".to_string(), NodeType::File, None);
664 graph.add_module("utils".to_string(), NodeType::Package, None);
665
666 graph.add_import(
667 "main",
668 "utils",
669 ImportType::StarImport,
670 vec![],
671 None,
672 None,
673 );
674
675 let used_symbols = HashMap::new();
676 let issues = graph.validate_imports(&used_symbols);
677
678 assert!(!issues.is_empty());
679 assert!(issues.iter().any(|i| i.issue_type == IssueType::WildcardImport));
680 }
681
682 #[test]
683 fn test_get_dependencies() {
684 let mut graph = DependencyGraph::new();
685
686 graph.add_module("app".to_string(), NodeType::File, None);
687 graph.add_module("models".to_string(), NodeType::File, None);
688 graph.add_module("database".to_string(), NodeType::File, None);
689
690 graph.add_import("app", "models", ImportType::Module, vec![], None, None);
691 graph.add_import("models", "database", ImportType::Module, vec![], None, None);
692
693 let deps = graph.get_dependencies("app");
694
695 assert!(deps.contains("models"));
696 assert!(deps.contains("database"));
697 }
698
699 #[test]
700 fn test_optimization_suggestions() {
701 let mut graph = DependencyGraph::new();
702
703 graph.add_module("main".to_string(), NodeType::File, None);
704 graph.add_module("utils".to_string(), NodeType::Package, None);
705
706 graph.add_import("main", "utils", ImportType::StarImport, vec![], None, None);
708
709 let suggestions = graph.generate_optimizations();
710
711 assert!(!suggestions.is_empty());
712 assert!(suggestions.iter().any(|s| s.optimization_type == OptimizationType::ReplaceStarImport));
713 }
714}