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#[derive(Debug)]
21pub enum ParseError {
22 Io {
24 path: PathBuf,
25 error: std::io::Error,
26 },
27 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#[derive(Debug)]
55pub enum ResolveError {
56 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#[derive(Debug)]
84pub enum AnalyzeError {
85 NoCrateRoot { path: PathBuf },
87 Parse(ParseError),
89 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
131struct ParsedModule {
133 path: ModulePath,
134 file: syn::File,
135}
136
137pub 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 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 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 let known_modules: HashSet<ModulePath> = graph.modules().map(|m| m.path.clone()).collect();
174
175 for parsed in &parsed_modules {
177 add_use_edges(&parsed.path, &parsed.file, &known_modules, &mut graph);
178 }
179
180 Ok(graph)
181}
182
183fn 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 graph.add_edge(
199 parent_path.clone(),
200 child_path.clone(),
201 EdgeKind::ModDeclaration,
202 );
203
204 if is_inline_module(item_mod) {
205 graph.add_module(Module {
207 path: child_path.clone(),
208 source_file: parent_file.to_path_buf(),
209 kind: ModuleKind::Inline,
210 });
211
212 if let Some((_, items)) = &item_mod.content {
214 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 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 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
262fn 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}