|
1 | | -use std::path::Path; |
| 1 | +use std::io; |
| 2 | +use std::path::{Path, PathBuf}; |
2 | 3 | use std::sync::Arc; |
3 | 4 |
|
| 5 | +use crate::error::{Error, RichError, WithSpan as _}; |
| 6 | +use crate::parse::UseDecl; |
| 7 | + |
4 | 8 | /// Powers error reporting by mapping compiler diagnostics to the specific file. |
5 | 9 | #[derive(Debug, Clone, Eq, PartialEq, Hash)] |
6 | 10 | pub struct SourceFile { |
@@ -44,3 +48,259 @@ impl SourceFile { |
44 | 48 | self.content.clone() |
45 | 49 | } |
46 | 50 | } |
| 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 | + /// Add a dependency mapped to a specific calling file's path prefix. |
| 95 | + /// Re-sorts the vector internally to guarantee the Longest Prefix Match. |
| 96 | + pub fn insert( |
| 97 | + &mut self, |
| 98 | + context: &Path, |
| 99 | + dependency_root_path: String, |
| 100 | + path: &Path, |
| 101 | + ) -> io::Result<()> { |
| 102 | + let canon_context = std::fs::canonicalize(context).map_err(|err| { |
| 103 | + io::Error::new( |
| 104 | + err.kind(), |
| 105 | + format!( |
| 106 | + "Failed to find context directory '{}': {}", |
| 107 | + context.display(), |
| 108 | + err |
| 109 | + ), |
| 110 | + ) |
| 111 | + })?; |
| 112 | + |
| 113 | + let canon_path = std::fs::canonicalize(path).map_err(|err| { |
| 114 | + io::Error::new( |
| 115 | + err.kind(), |
| 116 | + format!( |
| 117 | + "Failed to find library target path '{}': {}", |
| 118 | + path.display(), |
| 119 | + err |
| 120 | + ), |
| 121 | + ) |
| 122 | + })?; |
| 123 | + |
| 124 | + self.inner.push(Remapping { |
| 125 | + context_prefix: canon_context, |
| 126 | + dependency_root_path, |
| 127 | + target: canon_path, |
| 128 | + }); |
| 129 | + |
| 130 | + self.sort_mappings(); |
| 131 | + |
| 132 | + Ok(()) |
| 133 | + } |
| 134 | + |
| 135 | + /// Resolve `use dependency_root_path::...` into a physical file path by finding the |
| 136 | + /// most specific library context that owns the current file. |
| 137 | + pub fn resolve_path( |
| 138 | + &self, |
| 139 | + current_file: &Path, |
| 140 | + use_decl: &UseDecl, |
| 141 | + ) -> Result<PathBuf, RichError> { |
| 142 | + // Safely extract the first segment (the dependency root path) |
| 143 | + let parts: Vec<&str> = use_decl.path().iter().map(|s| s.as_inner()).collect(); |
| 144 | + let first_segment = parts.first().copied().ok_or_else(|| { |
| 145 | + Error::CannotParse("Empty use path".to_string()).with_span(*use_decl.span()) |
| 146 | + })?; |
| 147 | + |
| 148 | + // Because the vector is sorted by longest prefix, |
| 149 | + // the VERY FIRST match we find is guaranteed to be the correct one. |
| 150 | + for remapping in &self.inner { |
| 151 | + // Check if the current file is executing inside the context's directory tree. |
| 152 | + // This prevents a file in `/project_a/` from using a dependency meant for `/project_b/` |
| 153 | + if !current_file.starts_with(&remapping.context_prefix) { |
| 154 | + continue; |
| 155 | + } |
| 156 | + |
| 157 | + // Check if the alias matches what the user typed |
| 158 | + if remapping.dependency_root_path == first_segment { |
| 159 | + let mut resolved_path = remapping.target.clone(); |
| 160 | + resolved_path.extend(&parts[1..]); |
| 161 | + return Ok(resolved_path); |
| 162 | + } |
| 163 | + } |
| 164 | + |
| 165 | + // No matches found |
| 166 | + Err(Error::UnknownLibrary(first_segment.to_string())).with_span(*use_decl.span()) |
| 167 | + } |
| 168 | +} |
| 169 | + |
| 170 | +#[cfg(test)] |
| 171 | +mod tests { |
| 172 | + use crate::str::Identifier; |
| 173 | + |
| 174 | + use super::*; |
| 175 | + |
| 176 | + // --- TEST HELPERS --- |
| 177 | + |
| 178 | + impl DependencyMap { |
| 179 | + /// Inserts a dependency remapping without interacting with the physical file system. |
| 180 | + pub fn test_insert_without_canonicalize( |
| 181 | + &mut self, |
| 182 | + context: &Path, |
| 183 | + dependency_root_path: String, |
| 184 | + path: &Path, |
| 185 | + ) { |
| 186 | + self.inner.push(Remapping { |
| 187 | + context_prefix: context.to_path_buf(), |
| 188 | + dependency_root_path, |
| 189 | + target: path.to_path_buf(), |
| 190 | + }); |
| 191 | + self.sort_mappings(); |
| 192 | + } |
| 193 | + } |
| 194 | + |
| 195 | + /// Helper to easily construct a `UseDecl` for path resolution tests. |
| 196 | + fn create_dummy_use_decl(path_segments: &[&str]) -> UseDecl { |
| 197 | + // Convert string slices into `Identifier` struct. |
| 198 | + // If your macro generates a different constructor, just swap `From::from` |
| 199 | + // with `Identifier::new(s)` or `Identifier(Arc::from(s))`! |
| 200 | + let path: Vec<Identifier> = path_segments |
| 201 | + .iter() |
| 202 | + .map(|&s| Identifier::dummy(s)) |
| 203 | + .collect(); |
| 204 | + |
| 205 | + UseDecl::dummy_path(path) |
| 206 | + } |
| 207 | + |
| 208 | + // --- TESTS --- |
| 209 | + |
| 210 | + #[test] |
| 211 | + fn test_sorting_longest_prefix() { |
| 212 | + let mut map = DependencyMap::new(); |
| 213 | + |
| 214 | + map.test_insert_without_canonicalize( |
| 215 | + Path::new("/workspace"), |
| 216 | + "math".to_string(), |
| 217 | + Path::new("/lib/math_v1"), |
| 218 | + ); |
| 219 | + map.test_insert_without_canonicalize( |
| 220 | + Path::new("/workspace/project_a/nested"), |
| 221 | + "math".to_string(), |
| 222 | + Path::new("/lib/math_v3"), |
| 223 | + ); |
| 224 | + map.test_insert_without_canonicalize( |
| 225 | + Path::new("/workspace/project_a"), |
| 226 | + "math".to_string(), |
| 227 | + Path::new("/lib/math_v2"), |
| 228 | + ); |
| 229 | + |
| 230 | + assert_eq!( |
| 231 | + map.inner[0].context_prefix, |
| 232 | + Path::new("/workspace/project_a/nested") |
| 233 | + ); |
| 234 | + assert_eq!( |
| 235 | + map.inner[1].context_prefix, |
| 236 | + Path::new("/workspace/project_a") |
| 237 | + ); |
| 238 | + assert_eq!(map.inner[2].context_prefix, Path::new("/workspace")); |
| 239 | + } |
| 240 | + |
| 241 | + #[test] |
| 242 | + fn test_context_isolation() { |
| 243 | + let mut map = DependencyMap::new(); |
| 244 | + map.test_insert_without_canonicalize( |
| 245 | + Path::new("/project_a"), |
| 246 | + "utils".to_string(), |
| 247 | + Path::new("/libs/utils_a"), |
| 248 | + ); |
| 249 | + |
| 250 | + let current_file = Path::new("/project_b/main.simf"); |
| 251 | + let use_decl = create_dummy_use_decl(&["utils"]); |
| 252 | + |
| 253 | + let result = map.resolve_path(current_file, &use_decl); |
| 254 | + |
| 255 | + assert!(result.is_err()); |
| 256 | + assert!(matches!( |
| 257 | + result.unwrap_err().error(), |
| 258 | + Error::UnknownLibrary(..) |
| 259 | + )); |
| 260 | + } |
| 261 | + |
| 262 | + #[test] |
| 263 | + fn test_resolve_longest_prefix_match() { |
| 264 | + let mut map = DependencyMap::new(); |
| 265 | + |
| 266 | + map.test_insert_without_canonicalize( |
| 267 | + Path::new("/workspace"), |
| 268 | + "math".to_string(), |
| 269 | + Path::new("/libs/global_math"), |
| 270 | + ); |
| 271 | + map.test_insert_without_canonicalize( |
| 272 | + Path::new("/workspace/frontend"), |
| 273 | + "math".to_string(), |
| 274 | + Path::new("/libs/frontend_math"), |
| 275 | + ); |
| 276 | + |
| 277 | + // 1. Frontend file -> should resolve to frontend_math |
| 278 | + let frontend_file = Path::new("/workspace/frontend/src/main.simf"); |
| 279 | + let use_decl = create_dummy_use_decl(&["math", "vector"]); |
| 280 | + |
| 281 | + let resolved_frontend = map.resolve_path(frontend_file, &use_decl).unwrap(); |
| 282 | + assert_eq!(resolved_frontend, Path::new("/libs/frontend_math/vector")); |
| 283 | + |
| 284 | + // 2. Backend file -> should bypass frontend and hit global_math |
| 285 | + let backend_file = Path::new("/workspace/backend/src/main.simf"); |
| 286 | + |
| 287 | + let resolved_backend = map.resolve_path(backend_file, &use_decl).unwrap(); |
| 288 | + assert_eq!(resolved_backend, Path::new("/libs/global_math/vector")); |
| 289 | + } |
| 290 | + |
| 291 | + #[test] |
| 292 | + fn test_resolve_subpath_extension() { |
| 293 | + let mut map = DependencyMap::new(); |
| 294 | + map.test_insert_without_canonicalize( |
| 295 | + Path::new("/app"), |
| 296 | + "core".to_string(), |
| 297 | + Path::new("/vendor/core_lib"), |
| 298 | + ); |
| 299 | + |
| 300 | + let current_file = Path::new("/app/main.simf"); |
| 301 | + let use_decl = create_dummy_use_decl(&["core", "network", "http"]); |
| 302 | + |
| 303 | + let resolved = map.resolve_path(current_file, &use_decl).unwrap(); |
| 304 | + assert_eq!(resolved, Path::new("/vendor/core_lib/network/http")); |
| 305 | + } |
| 306 | +} |
0 commit comments