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}