solar_interface/source_map/
file_resolver.rs1use super::SourceFile;
6use crate::SourceMap;
7use itertools::Itertools;
8use normalize_path::NormalizePath;
9use solar_config::ImportRemapping;
10use std::{
11 borrow::Cow,
12 io,
13 path::{Path, PathBuf},
14 sync::Arc,
15};
16
17#[derive(Debug, thiserror::Error)]
19pub enum ResolveError {
20 #[error("couldn't read stdin: {0}")]
21 ReadStdin(#[source] io::Error),
22 #[error("couldn't read {0}: {1}")]
23 ReadFile(PathBuf, #[source] io::Error),
24 #[error("file {0} not found")]
25 NotFound(PathBuf),
26 #[error("multiple files match {}: {}", .0.display(), .1.iter().map(|f| f.name.display()).format(", "))]
27 MultipleMatches(PathBuf, Vec<Arc<SourceFile>>),
28}
29
30#[derive(derive_more::Debug)]
32pub struct FileResolver<'a> {
33 #[debug(skip)]
34 source_map: &'a SourceMap,
35
36 include_paths: Vec<PathBuf>,
38 remappings: Vec<ImportRemapping>,
40
41 env_current_dir: Option<PathBuf>,
43 custom_current_dir: Option<PathBuf>,
45}
46
47impl<'a> FileResolver<'a> {
48 pub fn new(source_map: &'a SourceMap) -> Self {
50 Self {
51 source_map,
52 include_paths: Vec::new(),
53 remappings: Vec::new(),
54 env_current_dir: std::env::current_dir()
55 .inspect_err(|e| debug!("failed to get current_dir: {e}"))
56 .ok(),
57 custom_current_dir: None,
58 }
59 }
60
61 #[track_caller]
67 pub fn set_current_dir(&mut self, current_dir: &Path) {
68 if !current_dir.is_absolute() {
69 panic!("current_dir must be an absolute path");
70 }
71 self.custom_current_dir = Some(current_dir.to_path_buf());
72 }
73
74 pub fn add_include_paths(&mut self, paths: impl IntoIterator<Item = PathBuf>) {
76 self.include_paths.extend(paths);
77 }
78
79 pub fn add_include_path(&mut self, path: PathBuf) {
81 self.include_paths.push(path)
82 }
83
84 pub fn add_import_remappings(&mut self, remappings: impl IntoIterator<Item = ImportRemapping>) {
86 self.remappings.extend(remappings);
87 }
88
89 pub fn add_import_remapping(&mut self, remapping: ImportRemapping) {
91 self.remappings.push(remapping);
92 }
93
94 pub fn source_map(&self) -> &'a SourceMap {
96 self.source_map
97 }
98
99 pub fn current_dir(&self) -> &Path {
101 self.try_current_dir().unwrap_or(Path::new("."))
102 }
103
104 pub fn try_current_dir(&self) -> Option<&Path> {
106 self.custom_current_dir.as_deref().or(self.env_current_dir.as_deref())
107 }
108
109 pub fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
111 self.canonicalize_absolute(&self.make_absolute(path))
112 }
113
114 fn canonicalize_absolute(&self, path: &Path) -> io::Result<PathBuf> {
115 debug_assert!(path.is_absolute());
116 crate::canonicalize(path)
117 }
118
119 pub fn normalize<'b>(&self, path: &'b Path) -> Cow<'b, Path> {
123 Cow::Owned(path.normalize())
124 }
125
126 pub fn make_absolute<'b>(&self, path: &'b Path) -> Cow<'b, Path> {
130 if path.is_absolute() {
131 Cow::Borrowed(path)
132 } else if let Some(current_dir) = self.try_current_dir() {
133 Cow::Owned(current_dir.join(path))
134 } else {
135 Cow::Borrowed(path)
136 }
137 }
138
139 #[instrument(level = "debug", skip_all, fields(path = %path.display()))]
143 pub fn resolve_file(
144 &self,
145 path: &Path,
146 parent: Option<&Path>,
147 ) -> Result<Arc<SourceFile>, ResolveError> {
148 let is_relative = path.starts_with("./") || path.starts_with("../");
159 if (is_relative && parent.is_some()) || parent.is_none() {
160 let try_path = if let Some(base) = parent.filter(|_| is_relative).and_then(Path::parent)
161 {
162 &base.join(path)
163 } else {
164 path
165 };
166 if let Some(file) = self.try_file(try_path)? {
167 return Ok(file);
168 }
169 if is_relative {
171 return Err(ResolveError::NotFound(path.into()));
172 }
173 }
174
175 let original_path = path;
176 let path = &*self.remap_path(path, parent);
177 let mut result = Vec::with_capacity(1);
178 let mut push_file = |file: Arc<SourceFile>| {
180 if !result.iter().any(|f| Arc::ptr_eq(f, &file)) {
181 result.push(file);
182 }
183 };
184
185 if self.include_paths.is_empty() || path.is_absolute() {
189 if let Some(file) = self.try_file(path)? {
190 result.push(file);
191 }
192 } else {
193 for include_path in &self.include_paths {
195 let path = include_path.join(path);
196 if let Some(file) = self.try_file(&path)? {
197 push_file(file);
198 }
199 }
200 }
201
202 match result.len() {
203 0 => Err(ResolveError::NotFound(original_path.into())),
204 1 => Ok(result.pop().unwrap()),
205 _ => Err(ResolveError::MultipleMatches(original_path.into(), result)),
206 }
207 }
208
209 pub fn remap_path<'b>(&self, path: &'b Path, parent: Option<&Path>) -> Cow<'b, Path> {
212 let remapped = self.remap_path_(path, parent);
213 if remapped != path {
214 trace!(remapped=%remapped.display());
215 }
216 remapped
217 }
218
219 fn remap_path_<'b>(&self, path: &'b Path, parent: Option<&Path>) -> Cow<'b, Path> {
220 let _context = &*parent.map(|p| p.to_string_lossy()).unwrap_or_default();
221
222 let mut longest_prefix = 0;
223 let mut longest_context = 0;
224 let mut best_match_target = None;
225 let mut unprefixed_path = path;
226 for ImportRemapping { context, prefix, path: target } in &self.remappings {
227 let context = &*sanitize_path(context);
228 let prefix = &*sanitize_path(prefix);
229
230 if context.len() < longest_context {
231 continue;
232 }
233 if !_context.starts_with(context) {
234 continue;
235 }
236 if prefix.len() < longest_prefix {
237 continue;
238 }
239 let Ok(up) = path.strip_prefix(prefix) else {
240 continue;
241 };
242 longest_context = context.len();
243 longest_prefix = prefix.len();
244 best_match_target = Some(sanitize_path(target));
245 unprefixed_path = up;
246 }
247 if let Some(best_match_target) = best_match_target {
248 let mut out = PathBuf::from(&*best_match_target);
249 out.push(unprefixed_path);
250 Cow::Owned(out)
251 } else {
252 Cow::Borrowed(unprefixed_path)
253 }
254 }
255
256 pub fn load_stdin(&self) -> Result<Arc<SourceFile>, ResolveError> {
258 self.source_map().load_stdin().map_err(ResolveError::ReadStdin)
259 }
260
261 #[instrument(level = "debug", skip_all, fields(path = %path.display()))]
263 pub fn try_file(&self, path: &Path) -> Result<Option<Arc<SourceFile>>, ResolveError> {
264 let rpath = &*self.normalize(path);
266 if let Some(file) = self.source_map().get_file(rpath) {
267 trace!("loaded from cache 1");
268 return Ok(Some(file));
269 }
270
271 let apath = &*self.make_absolute(rpath);
275 if apath != rpath
276 && let Some(file) = self.source_map().get_file(apath)
277 {
278 trace!("loaded from cache 2");
279 return Ok(Some(file));
280 }
281
282 if let Ok(path) = self.canonicalize_absolute(apath) {
284 let mut relpath = path.as_path();
286 if let Ok(p) = relpath.strip_prefix(self.current_dir()) {
287 relpath = p;
288 }
289 trace!("canonicalized to {}", relpath.display());
290 return self
291 .source_map()
292 .load_file_with_name(relpath.to_path_buf().into(), &path)
294 .map(Some)
295 .map_err(|e| ResolveError::ReadFile(relpath.into(), e));
296 }
297
298 trace!("not found");
299 Ok(None)
300 }
301}
302
303fn sanitize_path(s: &str) -> impl std::ops::Deref<Target = str> + '_ {
304 s
306}