Skip to content

Commit 72a8241

Browse files
committed
feat: add data structure for dependency resolution
1 parent 1e16a74 commit 72a8241

1 file changed

Lines changed: 143 additions & 1 deletion

File tree

src/resolution.rs

Lines changed: 143 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
use std::path::Path;
1+
use std::io;
2+
use std::path::{Path, PathBuf};
23
use std::sync::Arc;
34

5+
use crate::error::{Error, RichError, WithSpan as _};
6+
use crate::parse::UseDecl;
7+
48
/// Powers error reporting by mapping compiler diagnostics to the specific file.
59
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
610
pub struct SourceFile {
@@ -44,3 +48,141 @@ impl SourceFile {
4448
self.content.clone()
4549
}
4650
}
51+
52+
/// This defines how a specific dependency root path (e.g. "math")
53+
/// should be resolved to a physical path on the disk, restricted to
54+
/// files executing within the `context_prefix`.
55+
#[derive(Debug, Clone)]
56+
pub struct Remapping {
57+
/// The base directory that owns this dependency mapping.
58+
pub context_prefix: PathBuf,
59+
/// The name used in the `use` statement (e.g., "math").
60+
pub dependency_root_path: String,
61+
/// The physical path this dependency root path points to.
62+
pub target: PathBuf,
63+
}
64+
65+
/// A router for resolving dependencies across multi-file workspaces.
66+
///
67+
/// Mappings are strictly sorted by the longest `context_prefix` match.
68+
/// This mathematical guarantee ensures that if multiple nested directories
69+
/// define the same dependency root path, the most specific (deepest) context wins.
70+
#[derive(Debug, Default)]
71+
pub struct DependencyMap {
72+
inner: Vec<Remapping>,
73+
}
74+
75+
impl DependencyMap {
76+
pub fn new() -> Self {
77+
Self::default()
78+
}
79+
80+
pub fn is_empty(&self) -> bool {
81+
self.inner.is_empty()
82+
}
83+
84+
/// Re-sort the vector in descending order so the longest context paths are always at the front.
85+
/// This mathematically guarantees that the first match we find is the most specific.
86+
fn sort_mappings(&mut self) {
87+
self.inner.sort_by(|a, b| {
88+
let len_a = a.context_prefix.as_os_str().len();
89+
let len_b = b.context_prefix.as_os_str().len();
90+
len_b.cmp(&len_a)
91+
});
92+
}
93+
94+
/// Inserts a dependency remapping without interacting with the physical file system.
95+
///
96+
/// **Warning:** This method completely bypasses OS path canonicalization (`std::fs::canonicalize`).
97+
/// It is designed strictly for unit testing and virtual file environments where the
98+
/// provided paths might not actually exist on the hard drive.
99+
#[cfg(test)]
100+
pub fn test_insert_without_canonicalize(
101+
&mut self,
102+
context: &Path,
103+
dependency_root_path: String,
104+
path: &Path,
105+
) {
106+
self.inner.push(Remapping {
107+
context_prefix: context.to_path_buf(),
108+
dependency_root_path,
109+
target: path.to_path_buf(),
110+
});
111+
self.sort_mappings();
112+
}
113+
114+
/// Add a dependency mapped to a specific calling file's path prefix.
115+
/// Re-sorts the vector internally to guarantee the Longest Prefix Match.
116+
pub fn insert(
117+
&mut self,
118+
context: &Path,
119+
dependency_root_path: String,
120+
path: &Path,
121+
) -> io::Result<()> {
122+
let canon_context = std::fs::canonicalize(context).map_err(|err| {
123+
io::Error::new(
124+
err.kind(),
125+
format!(
126+
"Failed to find context directory '{}': {}",
127+
context.display(),
128+
err
129+
),
130+
)
131+
})?;
132+
133+
let canon_path = std::fs::canonicalize(path).map_err(|err| {
134+
io::Error::new(
135+
err.kind(),
136+
format!(
137+
"Failed to find library target path '{}': {}",
138+
path.display(),
139+
err
140+
),
141+
)
142+
})?;
143+
144+
self.inner.push(Remapping {
145+
context_prefix: canon_context,
146+
dependency_root_path,
147+
target: canon_path,
148+
});
149+
150+
self.sort_mappings();
151+
152+
Ok(())
153+
}
154+
155+
/// Resolve `use dependency_root_path::...` into a physical file path by finding the
156+
/// most specific library context that owns the current file.
157+
pub fn resolve_path(
158+
&self,
159+
current_file: &Path,
160+
use_decl: &UseDecl,
161+
) -> Result<PathBuf, RichError> {
162+
// Safely extract the first segment (the dependency root path)
163+
let parts: Vec<&str> = use_decl.path().iter().map(|s| s.as_inner()).collect();
164+
let first_segment = parts.first().copied().ok_or_else(|| {
165+
Error::CannotParse("Empty use path".to_string()).with_span(*use_decl.span())
166+
})?;
167+
168+
// Because the vector is sorted by longest prefix,
169+
// the VERY FIRST match we find is guaranteed to be the correct one.
170+
for remapping in &self.inner {
171+
// Check if the current file is executing inside the context's directory tree.
172+
// This prevents a file in `/project_a/` from using a dependency meant for `/project_b/`
173+
if !current_file.starts_with(&remapping.context_prefix) {
174+
continue;
175+
}
176+
177+
// Check if the alias matches what the user typed
178+
if remapping.dependency_root_path == first_segment {
179+
let mut resolved_path = remapping.target.clone();
180+
resolved_path.extend(&parts[1..]);
181+
return Ok(resolved_path);
182+
}
183+
}
184+
185+
// No matches found
186+
Err(Error::UnknownLibrary(first_segment.to_string())).with_span(*use_decl.span())
187+
}
188+
}

0 commit comments

Comments
 (0)