dgmod/
lib.rs

1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3
4pub mod graph;
5pub mod mermaid;
6pub mod parser;
7pub mod resolver;
8pub mod workspace;
9
10use std::collections::HashSet;
11use std::path::{Path, PathBuf};
12
13use graph::{EdgeKind, Module, ModuleGraph, ModuleKind, ModulePath};
14use parser::{extract_mod_declarations, extract_use_paths, extract_use_statements, parse_file};
15use resolver::{
16    find_crate_root, is_inline_module, is_internal_path, resolve_module_file, resolve_use_target,
17};
18
19/// Errors that can occur during parsing
20#[derive(Debug)]
21pub enum ParseError {
22    /// Failed to read a source file
23    Io {
24        path: PathBuf,
25        error: std::io::Error,
26    },
27    /// Failed to parse Rust syntax
28    Syntax { path: PathBuf, error: syn::Error },
29}
30
31impl std::fmt::Display for ParseError {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        match self {
34            Self::Io { path, error } => {
35                write!(f, "Failed to read {}: {error}", path.display())
36            }
37            Self::Syntax { path, error } => {
38                write!(f, "Parse error in {}: {error}", path.display())
39            }
40        }
41    }
42}
43
44impl std::error::Error for ParseError {
45    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
46        match self {
47            Self::Io { error, .. } => Some(error),
48            Self::Syntax { error, .. } => Some(error),
49        }
50    }
51}
52
53/// Errors that can occur during module resolution
54#[derive(Debug)]
55pub enum ResolveError {
56    /// Module file not found
57    ModuleNotFound {
58        module_name: String,
59        expected_paths: Vec<PathBuf>,
60    },
61}
62
63impl std::fmt::Display for ResolveError {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        match self {
66            Self::ModuleNotFound {
67                module_name,
68                expected_paths,
69            } => {
70                write!(f, "Module '{module_name}' not found. Expected:")?;
71                for path in expected_paths {
72                    write!(f, "\n  - {}", path.display())?;
73                }
74                Ok(())
75            }
76        }
77    }
78}
79
80impl std::error::Error for ResolveError {}
81
82/// Errors that can occur during crate analysis
83#[derive(Debug)]
84pub enum AnalyzeError {
85    /// No crate root found
86    NoCrateRoot { path: PathBuf },
87    /// Parse error
88    Parse(ParseError),
89    /// Resolution error
90    Resolve(ResolveError),
91}
92
93impl std::fmt::Display for AnalyzeError {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        match self {
96            Self::NoCrateRoot { path } => {
97                write!(
98                    f,
99                    "No crate root found at {}. Expected src/lib.rs or src/main.rs",
100                    path.display()
101                )
102            }
103            Self::Parse(e) => write!(f, "{e}"),
104            Self::Resolve(e) => write!(f, "{e}"),
105        }
106    }
107}
108
109impl std::error::Error for AnalyzeError {
110    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
111        match self {
112            Self::NoCrateRoot { .. } => None,
113            Self::Parse(e) => Some(e),
114            Self::Resolve(e) => Some(e),
115        }
116    }
117}
118
119impl From<ParseError> for AnalyzeError {
120    fn from(e: ParseError) -> Self {
121        Self::Parse(e)
122    }
123}
124
125impl From<ResolveError> for AnalyzeError {
126    fn from(e: ResolveError) -> Self {
127        Self::Resolve(e)
128    }
129}
130
131/// Parsed module info for use statement resolution
132struct ParsedModule {
133    path: ModulePath,
134    file: syn::File,
135}
136
137/// Analyze a crate and build its module dependency graph
138///
139/// # Errors
140/// Returns `AnalyzeError` if the crate root is not found, if any source file
141/// cannot be parsed, or if a module file cannot be resolved.
142pub fn analyze_crate(crate_dir: &Path, crate_name: &str) -> Result<ModuleGraph, AnalyzeError> {
143    let root_file = find_crate_root(crate_dir).ok_or_else(|| AnalyzeError::NoCrateRoot {
144        path: crate_dir.to_path_buf(),
145    })?;
146
147    let mut graph = ModuleGraph::new(crate_name.to_string());
148    let mut parsed_modules = Vec::new();
149
150    // Add root module
151    let root_path = ModulePath::crate_root();
152    graph.add_module(Module {
153        path: root_path.clone(),
154        source_file: root_file.clone(),
155        kind: ModuleKind::Root,
156    });
157
158    // Parse root and discover all modules
159    let root_parsed = parse_file(&root_file)?;
160    discover_modules(
161        &root_parsed,
162        &root_path,
163        &root_file,
164        &mut graph,
165        &mut parsed_modules,
166    )?;
167    parsed_modules.push(ParsedModule {
168        path: root_path,
169        file: root_parsed,
170    });
171
172    // Build known modules set for use resolution
173    let known_modules: HashSet<ModulePath> = graph.modules().map(|m| m.path.clone()).collect();
174
175    // Process use statements in all parsed modules
176    for parsed in &parsed_modules {
177        add_use_edges(&parsed.path, &parsed.file, &known_modules, &mut graph);
178    }
179
180    Ok(graph)
181}
182
183/// Recursively discover modules from mod declarations
184fn discover_modules(
185    file: &syn::File,
186    parent_path: &ModulePath,
187    parent_file: &Path,
188    graph: &mut ModuleGraph,
189    parsed_modules: &mut Vec<ParsedModule>,
190) -> Result<(), AnalyzeError> {
191    let parent_dir = parent_file.parent().unwrap_or(Path::new("."));
192
193    for item_mod in extract_mod_declarations(file) {
194        let mod_name = item_mod.ident.to_string();
195        let child_path = parent_path.child(&mod_name);
196
197        // Add mod declaration edge
198        graph.add_edge(
199            parent_path.clone(),
200            child_path.clone(),
201            EdgeKind::ModDeclaration,
202        );
203
204        if is_inline_module(item_mod) {
205            // Inline module - content is in the same file
206            graph.add_module(Module {
207                path: child_path.clone(),
208                source_file: parent_file.to_path_buf(),
209                kind: ModuleKind::Inline,
210            });
211
212            // Process inline module's items
213            if let Some((_, items)) = &item_mod.content {
214                // Create a temporary syn::File for the inline module
215                let inline_file = syn::File {
216                    shebang: None,
217                    attrs: vec![],
218                    items: items.clone(),
219                };
220                discover_modules(
221                    &inline_file,
222                    &child_path,
223                    parent_file,
224                    graph,
225                    parsed_modules,
226                )?;
227                parsed_modules.push(ParsedModule {
228                    path: child_path,
229                    file: inline_file,
230                });
231            }
232        } else {
233            // External module - resolve file path
234            let child_file = resolve_module_file(parent_dir, &mod_name, item_mod)?;
235
236            graph.add_module(Module {
237                path: child_path.clone(),
238                source_file: child_file.clone(),
239                kind: ModuleKind::External,
240            });
241
242            // Parse and recursively discover
243            let child_parsed = parse_file(&child_file)?;
244
245            discover_modules(
246                &child_parsed,
247                &child_path,
248                &child_file,
249                graph,
250                parsed_modules,
251            )?;
252            parsed_modules.push(ParsedModule {
253                path: child_path,
254                file: child_parsed,
255            });
256        }
257    }
258
259    Ok(())
260}
261
262/// Add edges for use statements in a module
263fn add_use_edges(
264    module_path: &ModulePath,
265    file: &syn::File,
266    known_modules: &HashSet<ModulePath>,
267    graph: &mut ModuleGraph,
268) {
269    for item_use in extract_use_statements(file) {
270        for segments in extract_use_paths(item_use) {
271            if !is_internal_path(&segments) {
272                continue;
273            }
274
275            if let Some(target) = resolve_use_target(&segments, module_path, known_modules) {
276                graph.add_edge(module_path.clone(), target, EdgeKind::UseImport);
277            }
278        }
279    }
280}