dgmod/graph.rs
1//! Graph data structures for module dependencies
2
3use std::collections::HashMap;
4use std::path::PathBuf;
5
6/// Newtype wrapper for module paths (e.g., "crate", `alpha::delta`)
7#[derive(Debug, Clone, PartialEq, Eq, Hash)]
8pub struct ModulePath(String);
9
10impl ModulePath {
11 /// Create the crate root path
12 #[must_use]
13 pub fn crate_root() -> Self {
14 Self("crate".to_string())
15 }
16
17 /// Create a child module path
18 #[must_use]
19 pub fn child(&self, name: &str) -> Self {
20 if self.0 == "crate" {
21 Self(name.to_string())
22 } else {
23 Self(format!("{}::{name}", self.0))
24 }
25 }
26
27 /// Get the string representation
28 #[must_use]
29 pub fn as_str(&self) -> &str {
30 &self.0
31 }
32
33 /// Check if this path represents a tests module
34 ///
35 /// Returns true if the path is "tests" or ends with `::tests`
36 #[must_use]
37 pub fn is_tests_module(&self) -> bool {
38 self.0 == "tests" || self.0.ends_with("::tests")
39 }
40}
41
42/// The kind of module
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum ModuleKind {
45 /// Crate root (lib.rs or main.rs)
46 Root,
47 /// Inline module: `mod foo { ... }`
48 Inline,
49 /// External file: `mod foo;` → foo.rs or foo/mod.rs
50 External,
51}
52
53/// How a dependency edge was established
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
55pub enum EdgeKind {
56 /// Parent declares child: `mod foo;`
57 ModDeclaration,
58 /// Import via use statement: `use foo::Bar;`
59 UseImport,
60}
61
62/// A module within a crate
63#[derive(Debug, Clone)]
64pub struct Module {
65 /// Fully qualified path (e.g., "crate", `alpha::delta`)
66 pub path: ModulePath,
67 /// Absolute path to the source file
68 pub source_file: PathBuf,
69 /// Whether this is root, inline, or external
70 pub kind: ModuleKind,
71}
72
73/// The complete dependency graph for a crate
74#[derive(Debug)]
75pub struct ModuleGraph {
76 /// Name of the crate
77 pub crate_name: String,
78 /// All modules indexed by path
79 modules: HashMap<ModulePath, Module>,
80 /// Deduplicated edges: (from, to) -> kind
81 /// If both `ModDeclaration` and `UseImport` exist for the same edge,
82 /// `ModDeclaration` takes precedence.
83 edges: HashMap<(ModulePath, ModulePath), EdgeKind>,
84}
85
86impl ModuleGraph {
87 /// Create a new empty graph for the given crate
88 #[must_use]
89 pub fn new(crate_name: String) -> Self {
90 Self {
91 crate_name,
92 modules: HashMap::new(),
93 edges: HashMap::new(),
94 }
95 }
96
97 /// Add a module to the graph
98 pub fn add_module(&mut self, module: Module) {
99 self.modules.insert(module.path.clone(), module);
100 }
101
102 /// Add an edge between two modules (no self-edges allowed)
103 ///
104 /// If an edge already exists between the same modules, `ModDeclaration`
105 /// takes precedence over `UseImport`.
106 pub fn add_edge(&mut self, from: ModulePath, to: ModulePath, kind: EdgeKind) {
107 if from == to {
108 return;
109 }
110 let key = (from, to);
111 self.edges
112 .entry(key)
113 .and_modify(|existing| {
114 // ModDeclaration takes precedence over UseImport
115 if kind == EdgeKind::ModDeclaration {
116 *existing = kind;
117 }
118 })
119 .or_insert(kind);
120 }
121
122 /// Iterate over all modules
123 pub fn modules(&self) -> impl Iterator<Item = &Module> {
124 self.modules.values()
125 }
126
127 /// Iterate over all edges as (from, to, kind) tuples
128 pub fn edges(&self) -> impl Iterator<Item = (&ModulePath, &ModulePath, EdgeKind)> {
129 self.edges
130 .iter()
131 .map(|((from, to), kind)| (from, to, *kind))
132 }
133
134 /// Remove all `tests` modules and their associated edges from the graph
135 ///
136 /// This removes any module whose path is "tests" or ends with `::tests`,
137 /// along with any edges to or from those modules.
138 pub fn exclude_tests_modules(&mut self) {
139 // Collect paths to remove
140 let tests_paths: Vec<ModulePath> = self
141 .modules
142 .keys()
143 .filter(|path| path.is_tests_module())
144 .cloned()
145 .collect();
146
147 // Remove the modules
148 for path in &tests_paths {
149 self.modules.remove(path);
150 }
151
152 // Remove edges to/from tests modules
153 self.edges
154 .retain(|(from, to), _| !from.is_tests_module() && !to.is_tests_module());
155 }
156}