foundry_config/providers/
remappings.rs1use crate::{Config, foundry_toml_dirs, remappings_from_env_var, remappings_from_newline};
2use figment::{
3 Error, Figment, Metadata, Profile, Provider,
4 value::{Dict, Map},
5};
6use foundry_compilers::artifacts::remappings::{RelativeRemapping, Remapping};
7use rayon::prelude::*;
8use std::{
9 borrow::Cow,
10 collections::{BTreeMap, HashSet, btree_map::Entry},
11 fs,
12 path::{Path, PathBuf},
13};
14
15#[derive(Clone, Debug, Default)]
17pub struct Remappings {
18 remappings: Vec<Remapping>,
20 project_paths: Vec<Remapping>,
23}
24
25impl Remappings {
26 pub fn new() -> Self {
28 Self { remappings: Vec::new(), project_paths: Vec::new() }
29 }
30
31 pub fn new_with_remappings(remappings: Vec<Remapping>) -> Self {
33 Self { remappings, project_paths: Vec::new() }
34 }
35
36 pub fn with_figment(mut self, figment: &Figment) -> Self {
38 let mut add_project_remapping = |path: &str| {
39 if let Ok(path) = figment.find_value(path)
40 && let Some(path) = path.into_string()
41 {
42 let remapping =
43 Remapping { context: None, name: format!("{path}/"), path: format!("{path}/") };
44 self.project_paths.push(remapping);
45 }
46 };
47 add_project_remapping("src");
48 add_project_remapping("test");
49 add_project_remapping("script");
50 self
51 }
52
53 fn filter_key(r: &Remapping) -> String {
55 match &r.context {
56 Some(str) => str.clone() + &r.name.clone(),
57 None => r.name.clone(),
58 }
59 }
60
61 pub fn into_inner(self) -> Vec<Remapping> {
63 let mut seen = HashSet::new();
64 self.remappings.iter().filter(|r| seen.insert(Self::filter_key(r))).cloned().collect()
65 }
66
67 fn push(&mut self, remapping: Remapping) {
69 if remapping.name.ends_with(".sol") && !remapping.path.ends_with(".sol") {
71 return;
72 }
73
74 if self.remappings.iter().any(|existing| {
75 if remapping.name.ends_with(".sol") {
76 return existing.name == remapping.name
78 && existing.context == remapping.context
79 && existing.path == remapping.path;
80 }
81
82 let mut existing_name_path = existing.name.clone();
93 if !existing_name_path.ends_with('/') {
94 existing_name_path.push('/')
95 }
96 let is_conflicting = remapping.name.starts_with(&existing_name_path)
97 || existing.name.starts_with(&remapping.name);
98 is_conflicting && existing.context == remapping.context
99 }) {
100 return;
101 };
102
103 if self
106 .project_paths
107 .iter()
108 .any(|project_path| remapping.name.eq_ignore_ascii_case(&project_path.name))
109 {
110 return;
111 };
112
113 self.remappings.push(remapping);
114 }
115
116 pub fn extend(&mut self, remappings: Vec<Remapping>) {
118 for remapping in remappings {
119 self.push(remapping);
120 }
121 }
122}
123
124pub struct RemappingsProvider<'a> {
130 pub auto_detect_remappings: bool,
132 pub lib_paths: Cow<'a, Vec<PathBuf>>,
134 pub root: &'a Path,
137 pub remappings: Result<Vec<Remapping>, Error>,
142}
143
144impl RemappingsProvider<'_> {
145 fn get_remappings(&self, remappings: Vec<Remapping>) -> Result<Vec<Remapping>, Error> {
156 trace!("get all remappings from {:?}", self.root);
157 fn insert_closest(
162 mappings: &mut BTreeMap<Option<String>, BTreeMap<String, PathBuf>>,
163 context: Option<String>,
164 key: String,
165 path: PathBuf,
166 ) {
167 let context_mappings = mappings.entry(context).or_default();
168 match context_mappings.entry(key) {
169 Entry::Occupied(mut e) => {
170 if e.get().components().count() > path.components().count() {
171 e.insert(path);
172 }
173 }
174 Entry::Vacant(e) => {
175 e.insert(path);
176 }
177 }
178 }
179
180 let mut user_remappings = Vec::new();
183
184 if let Some(env_remappings) = remappings_from_env_var("DAPP_REMAPPINGS")
186 .or_else(|| remappings_from_env_var("FOUNDRY_REMAPPINGS"))
187 {
188 user_remappings
189 .extend(env_remappings.map_err::<Error, _>(|err| err.to_string().into())?);
190 }
191
192 let remappings_file = self.root.join("remappings.txt");
194 if remappings_file.is_file() {
195 let content = fs::read_to_string(remappings_file).map_err(|err| err.to_string())?;
196 let remappings_from_file: Result<Vec<_>, _> =
197 remappings_from_newline(&content).collect();
198 user_remappings
199 .extend(remappings_from_file.map_err::<Error, _>(|err| err.to_string().into())?);
200 }
201
202 user_remappings.extend(remappings);
203 let mut all_remappings = Remappings::new_with_remappings(user_remappings);
206
207 if self.auto_detect_remappings {
211 let (nested_foundry_remappings, auto_detected_remappings) = rayon::join(
212 || self.find_nested_foundry_remappings(),
213 || self.auto_detect_remappings(),
214 );
215
216 let mut lib_remappings = BTreeMap::new();
217 for r in nested_foundry_remappings {
218 insert_closest(&mut lib_remappings, r.context, r.name, r.path.into());
219 }
220 for r in auto_detected_remappings {
221 if ["lib/", "src/", "contracts/"].contains(&r.name.as_str()) {
223 trace!(target: "forge", "- skipping the remapping");
224 continue;
225 }
226 insert_closest(&mut lib_remappings, r.context, r.name, r.path.into());
227 }
228
229 all_remappings.extend(
230 lib_remappings
231 .into_iter()
232 .flat_map(|(context, remappings)| {
233 remappings.into_iter().map(move |(name, path)| Remapping {
234 context: context.clone(),
235 name,
236 path: path.to_string_lossy().into(),
237 })
238 })
239 .collect(),
240 );
241 }
242
243 Ok(all_remappings.into_inner())
244 }
245
246 fn find_nested_foundry_remappings(&self) -> impl Iterator<Item = Remapping> + '_ {
248 self.lib_paths
249 .par_iter()
250 .map(|p| if p.is_absolute() { self.root.join("lib") } else { self.root.join(p) })
251 .flat_map(foundry_toml_dirs)
252 .flat_map_iter(|lib| {
253 trace!(?lib, "find all remappings of nested foundry.toml");
254 self.nested_foundry_remappings(&lib)
255 })
256 .collect::<Vec<_>>()
257 .into_iter()
258 }
259
260 fn nested_foundry_remappings(&self, lib: &Path) -> Vec<Remapping> {
261 let Ok(config) = Config::load_with_root(lib) else { return vec![] };
263 let config = config.sanitized();
264
265 let mut src_remapping = None;
269 if ![Path::new("src"), Path::new("contracts"), Path::new("lib")]
270 .contains(&config.src.as_path())
271 && let Some(name) = lib.file_name().and_then(|s| s.to_str())
272 {
273 let mut r = Remapping {
274 context: None,
275 name: format!("{name}/"),
276 path: format!("{}", lib.join(&config.src).display()),
277 };
278 if !r.path.ends_with('/') {
279 r.path.push('/')
280 }
281 src_remapping = Some(r);
282 }
283
284 let mut remappings =
289 config.remappings.into_iter().map(Remapping::from).collect::<Vec<Remapping>>();
290
291 if let Some(r) = src_remapping {
292 remappings.push(r);
293 }
294 remappings
295 }
296
297 fn auto_detect_remappings(&self) -> impl Iterator<Item = Remapping> + '_ {
299 self.lib_paths
300 .par_iter()
301 .flat_map_iter(|lib| {
302 let lib = self.root.join(lib);
303 trace!(?lib, "find all remappings");
304 Remapping::find_many(&lib)
305 })
306 .collect::<Vec<_>>()
307 .into_iter()
308 }
309}
310
311impl Provider for RemappingsProvider<'_> {
312 fn metadata(&self) -> Metadata {
313 Metadata::named("Remapping Provider")
314 }
315
316 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
317 let remappings = match &self.remappings {
318 Ok(remappings) => self.get_remappings(remappings.clone()),
319 Err(err) => {
320 if let figment::error::Kind::MissingField(_) = err.kind {
321 self.get_remappings(vec![])
322 } else {
323 return Err(err.clone());
324 }
325 }
326 }?;
327
328 let remappings = remappings
330 .into_iter()
331 .map(|r| RelativeRemapping::new(r, self.root).to_string())
332 .collect::<Vec<_>>();
333
334 Ok(Map::from([(
335 Config::selected_profile(),
336 Dict::from([("remappings".to_string(), figment::value::Value::from(remappings))]),
337 )]))
338 }
339
340 fn profile(&self) -> Option<Profile> {
341 Some(Config::selected_profile())
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348
349 #[test]
350 fn test_sol_file_remappings() {
351 let mut remappings = Remappings::new();
352
353 remappings.push(Remapping {
355 context: None,
356 name: "MyContract.sol".to_string(),
357 path: "implementations/Contract1.sol".to_string(),
358 });
359
360 remappings.push(Remapping {
362 context: None,
363 name: "MyContract.sol".to_string(),
364 path: "implementations/Contract2.sol".to_string(),
365 });
366
367 remappings.push(Remapping {
369 context: None,
370 name: "OtherContract.sol".to_string(),
371 path: "implementations/Contract1.sol".to_string(),
372 });
373
374 remappings.push(Remapping {
376 context: None,
377 name: "MyContract.sol".to_string(),
378 path: "implementations/Contract1.sol".to_string(),
379 });
380
381 remappings.push(Remapping {
383 context: None,
384 name: "Invalid.sol".to_string(),
385 path: "implementations/Contract1.txt".to_string(),
386 });
387
388 let result = remappings.into_inner();
389 assert_eq!(result.len(), 2, "Should only have 2 valid remappings");
390
391 assert!(
393 result
394 .iter()
395 .any(|r| r.name == "MyContract.sol" && r.path == "implementations/Contract1.sol"),
396 "Should keep first mapping of MyContract.sol"
397 );
398 assert!(
399 !result
400 .iter()
401 .any(|r| r.name == "MyContract.sol" && r.path == "implementations/Contract2.sol"),
402 "Should keep first mapping of MyContract.sol"
403 );
404 assert!(result.iter().any(|r| r.name == "OtherContract.sol" && r.path == "implementations/Contract1.sol"),
405 "Should allow different source to same target");
406
407 assert!(
409 !result
410 .iter()
411 .any(|r| r.name == "MyContract.sol" && r.path == "implementations/Contract2.sol"),
412 "Should reject same source to different target"
413 );
414 }
415
416 #[test]
417 fn test_mixed_remappings() {
418 let mut remappings = Remappings::new();
419
420 remappings.push(Remapping {
421 context: None,
422 name: "@openzeppelin-contracts/".to_string(),
423 path: "lib/openzeppelin-contracts/".to_string(),
424 });
425 remappings.push(Remapping {
426 context: None,
427 name: "@openzeppelin/contracts/".to_string(),
428 path: "lib/openzeppelin/contracts/".to_string(),
429 });
430
431 remappings.push(Remapping {
432 context: None,
433 name: "MyContract.sol".to_string(),
434 path: "os/Contract.sol".to_string(),
435 });
436
437 let result = remappings.into_inner();
438 assert_eq!(result.len(), 3, "Should have 3 remappings");
439 assert_eq!(result.first().unwrap().name, "@openzeppelin-contracts/");
440 assert_eq!(result.first().unwrap().path, "lib/openzeppelin-contracts/");
441 assert_eq!(result.get(1).unwrap().name, "@openzeppelin/contracts/");
442 assert_eq!(result.get(1).unwrap().path, "lib/openzeppelin/contracts/");
443 assert_eq!(result.get(2).unwrap().name, "MyContract.sol");
444 assert_eq!(result.get(2).unwrap().path, "os/Contract.sol");
445 }
446
447 #[test]
448 fn test_remappings_with_context() {
449 let mut remappings = Remappings::new();
450
451 remappings.push(Remapping {
453 context: Some("test/".to_string()),
454 name: "MyContract.sol".to_string(),
455 path: "test/Contract.sol".to_string(),
456 });
457 remappings.push(Remapping {
458 context: Some("prod/".to_string()),
459 name: "MyContract.sol".to_string(),
460 path: "prod/Contract.sol".to_string(),
461 });
462
463 let result = remappings.into_inner();
464 assert_eq!(result.len(), 2, "Should allow same name with different contexts");
465 assert!(
466 result
467 .iter()
468 .any(|r| r.context == Some("test/".to_string()) && r.path == "test/Contract.sol")
469 );
470 assert!(
471 result
472 .iter()
473 .any(|r| r.context == Some("prod/".to_string()) && r.path == "prod/Contract.sol")
474 );
475 }
476}