dgmod/
resolver.rs

1//! Module path resolution
2
3use std::collections::HashSet;
4use std::path::{Path, PathBuf};
5
6use syn::ItemMod;
7
8use crate::graph::ModulePath;
9use crate::parser::get_path_attribute;
10use crate::ResolveError;
11
12/// Find the crate root file (lib.rs or main.rs)
13#[must_use]
14pub fn find_crate_root(crate_dir: &Path) -> Option<PathBuf> {
15    let src_dir = crate_dir.join("src");
16
17    let lib_rs = src_dir.join("lib.rs");
18    if lib_rs.exists() {
19        return Some(lib_rs);
20    }
21
22    let main_rs = src_dir.join("main.rs");
23    if main_rs.exists() {
24        return Some(main_rs);
25    }
26
27    None
28}
29
30/// Resolve the file path for a module declaration
31///
32/// # Errors
33/// Returns `ResolveError::ModuleNotFound` if the module file cannot be found.
34pub fn resolve_module_file(
35    parent_dir: &Path,
36    mod_name: &str,
37    item_mod: &ItemMod,
38) -> Result<PathBuf, ResolveError> {
39    // Check for #[path = "..."] attribute
40    if let Some(custom_path) = get_path_attribute(item_mod) {
41        return Ok(parent_dir.join(custom_path));
42    }
43
44    // Standard module resolution: try mod_name.rs first, then mod_name/mod.rs
45    let direct = parent_dir.join(format!("{mod_name}.rs"));
46    if direct.exists() {
47        return Ok(direct);
48    }
49
50    let nested = parent_dir.join(mod_name).join("mod.rs");
51    if nested.exists() {
52        return Ok(nested);
53    }
54
55    Err(ResolveError::ModuleNotFound {
56        module_name: mod_name.to_string(),
57        expected_paths: vec![direct, nested],
58    })
59}
60
61/// Check if a mod declaration is inline (has body) vs external (just declaration)
62#[must_use]
63pub fn is_inline_module(item_mod: &ItemMod) -> bool {
64    item_mod.content.is_some()
65}
66
67/// Check if a use path is internal (`crate::`, `self::`, `super::`)
68#[must_use]
69pub fn is_internal_path(segments: &[String]) -> bool {
70    if segments.is_empty() {
71        return false;
72    }
73    matches!(segments[0].as_str(), "crate" | "self" | "super")
74}
75
76/// Resolve a use path to its target module path
77/// Returns None if the path refers to an external crate or cannot be resolved
78#[must_use]
79#[allow(clippy::implicit_hasher)]
80pub fn resolve_use_target(
81    segments: &[String],
82    current_module: &ModulePath,
83    known_modules: &HashSet<ModulePath>,
84) -> Option<ModulePath> {
85    if segments.is_empty() {
86        return None;
87    }
88
89    match segments[0].as_str() {
90        "crate" => {
91            // crate::foo::bar -> resolve from root
92            resolve_from_crate_root(&segments[1..], known_modules)
93        }
94        "self" => {
95            // self::foo -> resolve from current module
96            resolve_relative(current_module, &segments[1..], known_modules)
97        }
98        "super" => {
99            // super::foo -> resolve from parent module
100            let parent = get_parent_module(current_module)?;
101            // Handle multiple super:: prefixes
102            let mut current = parent;
103            let mut remaining = &segments[1..];
104            while !remaining.is_empty() && remaining[0] == "super" {
105                current = get_parent_module(&current)?;
106                remaining = &remaining[1..];
107            }
108            resolve_relative(&current, remaining, known_modules)
109        }
110        _ => {
111            // External crate - not internal
112            None
113        }
114    }
115}
116
117/// Resolve path starting from crate root
118fn resolve_from_crate_root(
119    segments: &[String],
120    known_modules: &HashSet<ModulePath>,
121) -> Option<ModulePath> {
122    resolve_relative(&ModulePath::crate_root(), segments, known_modules)
123}
124
125/// Resolve path relative to a module, finding the deepest matching known module
126fn resolve_relative(
127    base: &ModulePath,
128    segments: &[String],
129    known_modules: &HashSet<ModulePath>,
130) -> Option<ModulePath> {
131    if segments.is_empty() {
132        // Importing from the base module itself
133        if known_modules.contains(base) {
134            return Some(base.clone());
135        }
136        return None;
137    }
138
139    // Try to find the deepest module that matches
140    let mut current = base.clone();
141    let mut last_known = if known_modules.contains(&current) {
142        Some(current.clone())
143    } else {
144        None
145    };
146
147    for segment in segments {
148        current = current.child(segment);
149        if known_modules.contains(&current) {
150            last_known = Some(current.clone());
151        }
152    }
153
154    last_known
155}
156
157/// Get parent module path
158fn get_parent_module(module: &ModulePath) -> Option<ModulePath> {
159    let s = module.as_str();
160    if s == "crate" {
161        return None;
162    }
163    if let Some(pos) = s.rfind("::") {
164        Some(ModulePath::crate_root().child(&s[..pos]))
165    } else {
166        // Top-level module, parent is crate
167        Some(ModulePath::crate_root())
168    }
169}