|
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,141 @@ 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 | + /// 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