solar_interface/source_map/
file_resolver.rs1use super::SourceFile;
6use crate::{Session, SourceMap};
7use itertools::Itertools;
8use normalize_path::NormalizePath;
9use solar_config::ImportRemapping;
10use solar_data_structures::smallvec::SmallVec;
11use std::{
12 borrow::Cow,
13 io,
14 path::{Path, PathBuf},
15 sync::{Arc, OnceLock},
16};
17
18#[derive(Debug, thiserror::Error)]
20pub enum ResolveError {
21 #[error("couldn't read stdin: {0}")]
22 ReadStdin(#[source] io::Error),
23 #[error("couldn't read {0}: {1}")]
24 ReadFile(PathBuf, #[source] io::Error),
25 #[error("file {0} not found")]
26 NotFound(PathBuf),
27 #[error("multiple files match {}: {}", .0.display(), .1.iter().map(|f| f.name.display()).format(", "))]
28 MultipleMatches(PathBuf, Vec<Arc<SourceFile>>),
29}
30
31#[derive(derive_more::Debug)]
33pub struct FileResolver<'a> {
34 #[debug(skip)]
35 source_map: &'a SourceMap,
36
37 include_paths: Vec<PathBuf>,
39 remappings: Vec<ImportRemapping>,
41
42 custom_current_dir: Option<PathBuf>,
44 env_current_dir: OnceLock<Option<PathBuf>>,
46}
47
48impl<'a> FileResolver<'a> {
49 pub fn new(source_map: &'a SourceMap) -> Self {
51 Self {
52 source_map,
53 include_paths: Vec::new(),
54 remappings: Vec::new(),
55 custom_current_dir: None,
56 env_current_dir: OnceLock::new(),
57 }
58 }
59
60 pub fn configure_from_sess(&mut self, sess: &Session) {
62 self.add_include_paths(sess.opts.include_paths.iter().cloned());
63 self.add_import_remappings(sess.opts.import_remappings.iter().cloned());
64 'b: {
65 if let Some(base_path) = &sess.opts.base_path {
66 let base_path = if base_path.is_absolute() {
67 base_path.as_path()
68 } else {
69 &if let Ok(path) = self.canonicalize_unchecked(base_path) {
70 path
71 } else {
72 break 'b;
73 }
74 };
75 self.set_current_dir(base_path);
76 }
77 }
78 }
79
80 pub fn clear(&mut self) {
82 self.include_paths.clear();
83 self.remappings.clear();
84 self.custom_current_dir = None;
85 self.env_current_dir.take();
86 }
87
88 #[track_caller]
94 #[doc(alias = "set_base_path")]
95 pub fn set_current_dir(&mut self, current_dir: &Path) {
96 if !current_dir.is_absolute() {
97 panic!("current_dir must be an absolute path");
98 }
99 self.custom_current_dir = Some(current_dir.to_path_buf());
100 }
101
102 pub fn add_include_paths(&mut self, paths: impl IntoIterator<Item = PathBuf>) {
104 self.include_paths.extend(paths);
105 }
106
107 pub fn add_include_path(&mut self, path: PathBuf) {
109 self.include_paths.push(path)
110 }
111
112 pub fn add_import_remappings(&mut self, remappings: impl IntoIterator<Item = ImportRemapping>) {
114 self.remappings.extend(remappings);
115 }
116
117 pub fn add_import_remapping(&mut self, remapping: ImportRemapping) {
119 self.remappings.push(remapping);
120 }
121
122 pub fn source_map(&self) -> &'a SourceMap {
124 self.source_map
125 }
126
127 #[doc(alias = "base_path")]
129 pub fn current_dir(&self) -> &Path {
130 self.try_current_dir().unwrap_or(Path::new("."))
131 }
132
133 #[doc(alias = "try_base_path")]
135 pub fn try_current_dir(&self) -> Option<&Path> {
136 self.custom_current_dir.as_deref().or_else(|| self.env_current_dir())
137 }
138
139 fn env_current_dir(&self) -> Option<&Path> {
140 self.env_current_dir
141 .get_or_init(|| {
142 std::env::current_dir()
143 .inspect_err(|e| debug!("failed to get current_dir: {e}"))
144 .ok()
145 })
146 .as_deref()
147 }
148
149 pub fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
151 self.canonicalize_unchecked(&self.make_absolute(path))
152 }
153
154 fn canonicalize_unchecked(&self, path: &Path) -> io::Result<PathBuf> {
155 self.source_map.file_loader().canonicalize_path(path)
156 }
157
158 pub fn normalize<'b>(&self, path: &'b Path) -> Cow<'b, Path> {
162 Cow::Owned(path.normalize())
165 }
166
167 pub fn make_absolute<'b>(&self, path: &'b Path) -> Cow<'b, Path> {
171 if path.is_absolute() {
172 Cow::Borrowed(path)
173 } else if let Some(current_dir) = self.try_current_dir() {
174 Cow::Owned(current_dir.join(path))
175 } else {
176 Cow::Borrowed(path)
177 }
178 }
179
180 #[instrument(level = "debug", skip_all, fields(path = %path.display()))]
184 pub fn resolve_file(
185 &self,
186 path: &Path,
187 parent: Option<&Path>,
188 ) -> Result<Arc<SourceFile>, ResolveError> {
189 let is_relative = path.starts_with("./") || path.starts_with("../");
200 if (is_relative && parent.is_some()) || parent.is_none() {
201 let try_path = if let Some(base) = parent.filter(|_| is_relative).and_then(Path::parent)
202 {
203 &base.join(path)
204 } else {
205 path
206 };
207 if let Some(file) = self.try_file(try_path)? {
208 return Ok(file);
209 }
210 if is_relative {
212 return Err(ResolveError::NotFound(path.into()));
213 }
214 }
215
216 let original_path = path;
217 let path = &*self.remap_path(path, parent);
218
219 let mut candidates = SmallVec::<[_; 1]>::new();
220 let mut push_candidate = |file: Arc<SourceFile>| {
222 if !candidates.iter().any(|f| Arc::ptr_eq(f, &file)) {
223 candidates.push(file);
224 }
225 };
226
227 if self.include_paths.is_empty() || path.is_absolute() {
231 if let Some(file) = self.try_file(path)? {
232 push_candidate(file);
233 }
234 } else {
235 let base_path = self.try_current_dir().into_iter();
237 for include_path in base_path.chain(self.include_paths.iter().map(|p| p.as_path())) {
238 let path = include_path.join(path);
239 if let Some(file) = self.try_file(&path)? {
240 push_candidate(file);
241 }
242 }
243 }
244
245 match candidates.len() {
246 0 => Err(ResolveError::NotFound(original_path.into())),
247 1 => Ok(candidates.pop().unwrap()),
248 _ => Err(ResolveError::MultipleMatches(original_path.into(), candidates.into_vec())),
249 }
250 }
251
252 pub fn remap_path<'b>(&self, path: &'b Path, parent: Option<&Path>) -> Cow<'b, Path> {
255 let remapped = self.remap_path_(path, parent);
256 if remapped != path {
257 trace!(remapped=%remapped.display());
258 }
259 remapped
260 }
261
262 fn remap_path_<'b>(&self, path: &'b Path, parent: Option<&Path>) -> Cow<'b, Path> {
263 let _context = &*parent.map(|p| p.to_string_lossy()).unwrap_or_default();
264
265 let mut longest_prefix = 0;
266 let mut longest_context = 0;
267 let mut best_match_target = None;
268 let mut unprefixed_path = path;
269 for ImportRemapping { context, prefix, path: target } in &self.remappings {
270 let context = &*sanitize_path(context);
271 let prefix = &*sanitize_path(prefix);
272
273 if context.len() < longest_context {
274 continue;
275 }
276 if !_context.starts_with(context) {
277 continue;
278 }
279 if prefix.len() < longest_prefix {
280 continue;
281 }
282 let Ok(up) = path.strip_prefix(prefix) else {
283 continue;
284 };
285 longest_context = context.len();
286 longest_prefix = prefix.len();
287 best_match_target = Some(sanitize_path(target));
288 unprefixed_path = up;
289 }
290 if let Some(best_match_target) = best_match_target {
291 let mut out = PathBuf::from(&*best_match_target);
292 out.push(unprefixed_path);
293 Cow::Owned(out)
294 } else {
295 Cow::Borrowed(unprefixed_path)
296 }
297 }
298
299 pub fn load_stdin(&self) -> Result<Arc<SourceFile>, ResolveError> {
301 self.source_map().load_stdin().map_err(ResolveError::ReadStdin)
302 }
303
304 #[instrument(level = "debug", skip_all, fields(path = %path.display()))]
306 pub fn try_file(&self, path: &Path) -> Result<Option<Arc<SourceFile>>, ResolveError> {
307 let rpath = &*self.normalize(path);
309 if let Some(file) = self.source_map().get_file(rpath) {
310 trace!("loaded from cache 1");
311 return Ok(Some(file));
312 }
313
314 let apath = &*self.make_absolute(rpath);
316 if apath != rpath
317 && let Some(file) = self.source_map().get_file(apath)
318 {
319 trace!("loaded from cache 2");
320 return Ok(Some(file));
321 }
322
323 if let Ok(path) = self.canonicalize_unchecked(apath) {
325 return self
326 .source_map()
327 .load_file_with_name(apath.to_path_buf().into(), &path)
330 .map(Some)
331 .map_err(|e| ResolveError::ReadFile(path, e));
332 }
333
334 trace!("not found");
335 Ok(None)
336 }
337}
338
339fn sanitize_path(s: &str) -> impl std::ops::Deref<Target = str> + '_ {
340 s
342}