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.custom_current_dir
102 .as_deref()
103 .or(self.env_current_dir.as_deref())
104 .unwrap_or(Path::new("."))
105 }
106
107 pub fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
109 let path = if path.is_absolute() {
110 path
111 } else if let Some(current_dir) = &self.custom_current_dir {
112 ¤t_dir.join(path)
113 } else {
114 path
115 };
116 crate::canonicalize(path)
117 }
118
119 #[instrument(level = "debug", skip_all, fields(path = %path.display()))]
123 pub fn resolve_file(
124 &self,
125 path: &Path,
126 parent: Option<&Path>,
127 ) -> Result<Arc<SourceFile>, ResolveError> {
128 let is_relative = path.starts_with("./") || path.starts_with("../");
139 if (is_relative && parent.is_some()) || parent.is_none() {
140 let try_path = if let Some(base) = parent.filter(|_| is_relative).and_then(Path::parent)
141 {
142 &base.join(path)
143 } else {
144 path
145 };
146 if let Some(file) = self.try_file(try_path)? {
147 return Ok(file);
148 }
149 if is_relative {
151 return Err(ResolveError::NotFound(path.into()));
152 }
153 }
154
155 let original_path = path;
156 let path = &*self.remap_path(path, parent);
157 let mut result = Vec::with_capacity(1);
158 let mut push_file = |file: Arc<SourceFile>| {
160 if !result.iter().any(|f| Arc::ptr_eq(f, &file)) {
161 result.push(file);
162 }
163 };
164
165 if self.include_paths.is_empty() || path.is_absolute() {
169 if let Some(file) = self.try_file(path)? {
170 result.push(file);
171 }
172 } else {
173 for include_path in &self.include_paths {
175 let path = include_path.join(path);
176 if let Some(file) = self.try_file(&path)? {
177 push_file(file);
178 }
179 }
180 }
181
182 match result.len() {
183 0 => Err(ResolveError::NotFound(original_path.into())),
184 1 => Ok(result.pop().unwrap()),
185 _ => Err(ResolveError::MultipleMatches(original_path.into(), result)),
186 }
187 }
188
189 #[instrument(level = "trace", skip_all, ret)]
192 pub fn remap_path<'b>(&self, path: &'b Path, parent: Option<&Path>) -> Cow<'b, Path> {
193 let _context = &*parent.map(|p| p.to_string_lossy()).unwrap_or_default();
194
195 let mut longest_prefix = 0;
196 let mut longest_context = 0;
197 let mut best_match_target = None;
198 let mut unprefixed_path = path;
199 for ImportRemapping { context, prefix, path: target } in &self.remappings {
200 let context = &*sanitize_path(context);
201 let prefix = &*sanitize_path(prefix);
202
203 if context.len() < longest_context {
204 continue;
205 }
206 if !_context.starts_with(context) {
207 continue;
208 }
209 if prefix.len() < longest_prefix {
210 continue;
211 }
212 let Ok(up) = path.strip_prefix(prefix) else {
213 continue;
214 };
215 longest_context = context.len();
216 longest_prefix = prefix.len();
217 best_match_target = Some(sanitize_path(target));
218 unprefixed_path = up;
219 }
220 if let Some(best_match_target) = best_match_target {
221 let mut out = PathBuf::from(&*best_match_target);
222 out.push(unprefixed_path);
223 out.into()
224 } else {
225 Cow::Borrowed(unprefixed_path)
226 }
227 }
228
229 pub fn load_stdin(&self) -> Result<Arc<SourceFile>, ResolveError> {
231 self.source_map().load_stdin().map_err(ResolveError::ReadStdin)
232 }
233
234 #[instrument(level = "debug", skip_all, fields(path = %path.display()))]
236 pub fn try_file(&self, path: &Path) -> Result<Option<Arc<SourceFile>>, ResolveError> {
237 let path = &*path.normalize();
238 if let Some(file) = self.source_map().get_file(path) {
239 trace!("loaded from cache");
240 return Ok(Some(file));
241 }
242
243 if let Ok(path) = self.canonicalize(path) {
244 let mut relpath = path.as_path();
246 if let Ok(p) = relpath.strip_prefix(self.current_dir()) {
247 relpath = p;
248 }
249 trace!("canonicalized to {}", relpath.display());
250 return self
251 .source_map()
252 .load_file_with_name(relpath.to_path_buf().into(), &path)
254 .map(Some)
255 .map_err(|e| ResolveError::ReadFile(relpath.into(), e));
256 }
257
258 trace!("not found");
259 Ok(None)
260 }
261}
262
263fn sanitize_path(s: &str) -> impl std::ops::Deref<Target = str> + '_ {
264 s
266}