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 {
172 e.insert(path);
173 }
174 Entry::Vacant(e) => {
175 e.insert(path);
176 }
177 _ => {}
178 }
179 }
180
181 let mut user_remappings = Vec::new();
184
185 if let Some(env_remappings) = remappings_from_env_var("DAPP_REMAPPINGS")
187 .or_else(|| remappings_from_env_var("FOUNDRY_REMAPPINGS"))
188 {
189 user_remappings
190 .extend(env_remappings.map_err::<Error, _>(|err| err.to_string().into())?);
191 }
192
193 let remappings_file = self.root.join("remappings.txt");
195 if remappings_file.is_file() {
196 let content = fs::read_to_string(remappings_file).map_err(|err| err.to_string())?;
197 let remappings_from_file: Result<Vec<_>, _> =
198 remappings_from_newline(&content).collect();
199 user_remappings
200 .extend(remappings_from_file.map_err::<Error, _>(|err| err.to_string().into())?);
201 }
202
203 user_remappings.extend(remappings);
204 let mut all_remappings = Remappings::new_with_remappings(user_remappings);
207
208 if self.auto_detect_remappings {
212 let (nested_foundry_remappings, auto_detected_remappings) = rayon::join(
213 || self.find_nested_foundry_remappings(),
214 || self.auto_detect_remappings(),
215 );
216
217 let mut lib_remappings = BTreeMap::new();
218 for r in nested_foundry_remappings {
219 insert_closest(&mut lib_remappings, r.context, r.name, r.path.into());
220 }
221 for r in auto_detected_remappings {
222 if ["lib/", "src/", "contracts/"].contains(&r.name.as_str()) {
224 trace!(target: "forge", "- skipping the remapping");
225 continue;
226 }
227 insert_closest(&mut lib_remappings, r.context, r.name, r.path.into());
228 }
229
230 all_remappings.extend(
231 lib_remappings
232 .into_iter()
233 .flat_map(|(context, remappings)| {
234 remappings.into_iter().map(move |(name, path)| Remapping {
235 context: context.clone(),
236 name,
237 path: path.to_string_lossy().into(),
238 })
239 })
240 .collect(),
241 );
242 }
243
244 Ok(all_remappings.into_inner())
245 }
246
247 fn find_nested_foundry_remappings(&self) -> impl Iterator<Item = Remapping> + '_ {
249 self.lib_paths
250 .par_iter()
251 .map(|p| if p.is_absolute() { self.root.join("lib") } else { self.root.join(p) })
252 .flat_map(foundry_toml_dirs)
253 .flat_map_iter(|lib| {
254 trace!(?lib, "find all remappings of nested foundry.toml");
255 self.nested_foundry_remappings(&lib)
256 })
257 .collect::<Vec<_>>()
258 .into_iter()
259 }
260
261 fn nested_foundry_remappings(&self, lib: &Path) -> Vec<Remapping> {
262 let Ok(config) = Config::load_with_root_and_fallback(lib) else { return vec![] };
265 let config = config.sanitized();
266
267 let mut src_remapping = None;
271 if ![Path::new("src"), Path::new("contracts"), Path::new("lib")]
272 .contains(&config.src.as_path())
273 && let Some(name) = lib.file_name().and_then(|s| s.to_str())
274 {
275 let mut r = Remapping {
276 context: None,
277 name: format!("{name}/"),
278 path: format!("{}", lib.join(&config.src).display()),
279 };
280 if !r.path.ends_with('/') {
281 r.path.push('/')
282 }
283 src_remapping = Some(r);
284 }
285
286 let mut remappings =
291 config.remappings.into_iter().map(Remapping::from).collect::<Vec<Remapping>>();
292
293 if let Some(r) = src_remapping {
294 remappings.push(r);
295 }
296 remappings
297 }
298
299 fn auto_detect_remappings(&self) -> impl Iterator<Item = Remapping> + '_ {
301 self.lib_paths
302 .par_iter()
303 .flat_map_iter(|lib| {
304 let lib = self.root.join(lib);
305 trace!(?lib, "find all remappings");
306 Remapping::find_many(&lib)
307 })
308 .collect::<Vec<_>>()
309 .into_iter()
310 }
311}
312
313impl Provider for RemappingsProvider<'_> {
314 fn metadata(&self) -> Metadata {
315 Metadata::named("Remapping Provider")
316 }
317
318 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
319 let remappings = match &self.remappings {
320 Ok(remappings) => self.get_remappings(remappings.clone()),
321 Err(err) => {
322 if let figment::error::Kind::MissingField(_) = err.kind {
323 self.get_remappings(vec![])
324 } else {
325 return Err(err.clone());
326 }
327 }
328 }?;
329
330 let remappings = remappings
332 .into_iter()
333 .map(|r| RelativeRemapping::new(r, self.root).to_string())
334 .collect::<Vec<_>>();
335
336 Ok(Map::from([(
337 Config::selected_profile(),
338 Dict::from([("remappings".to_string(), figment::value::Value::from(remappings))]),
339 )]))
340 }
341
342 fn profile(&self) -> Option<Profile> {
343 Some(Config::selected_profile())
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350
351 #[test]
352 fn test_sol_file_remappings() {
353 let mut remappings = Remappings::new();
354
355 remappings.push(Remapping {
357 context: None,
358 name: "MyContract.sol".to_string(),
359 path: "implementations/Contract1.sol".to_string(),
360 });
361
362 remappings.push(Remapping {
364 context: None,
365 name: "MyContract.sol".to_string(),
366 path: "implementations/Contract2.sol".to_string(),
367 });
368
369 remappings.push(Remapping {
371 context: None,
372 name: "OtherContract.sol".to_string(),
373 path: "implementations/Contract1.sol".to_string(),
374 });
375
376 remappings.push(Remapping {
378 context: None,
379 name: "MyContract.sol".to_string(),
380 path: "implementations/Contract1.sol".to_string(),
381 });
382
383 remappings.push(Remapping {
385 context: None,
386 name: "Invalid.sol".to_string(),
387 path: "implementations/Contract1.txt".to_string(),
388 });
389
390 let result = remappings.into_inner();
391 assert_eq!(result.len(), 2, "Should only have 2 valid remappings");
392
393 assert!(
395 result
396 .iter()
397 .any(|r| r.name == "MyContract.sol" && r.path == "implementations/Contract1.sol"),
398 "Should keep first mapping of MyContract.sol"
399 );
400 assert!(
401 !result
402 .iter()
403 .any(|r| r.name == "MyContract.sol" && r.path == "implementations/Contract2.sol"),
404 "Should keep first mapping of MyContract.sol"
405 );
406 assert!(result.iter().any(|r| r.name == "OtherContract.sol" && r.path == "implementations/Contract1.sol"),
407 "Should allow different source to same target");
408
409 assert!(
411 !result
412 .iter()
413 .any(|r| r.name == "MyContract.sol" && r.path == "implementations/Contract2.sol"),
414 "Should reject same source to different target"
415 );
416 }
417
418 #[test]
419 fn test_mixed_remappings() {
420 let mut remappings = Remappings::new();
421
422 remappings.push(Remapping {
423 context: None,
424 name: "@openzeppelin-contracts/".to_string(),
425 path: "lib/openzeppelin-contracts/".to_string(),
426 });
427 remappings.push(Remapping {
428 context: None,
429 name: "@openzeppelin/contracts/".to_string(),
430 path: "lib/openzeppelin/contracts/".to_string(),
431 });
432
433 remappings.push(Remapping {
434 context: None,
435 name: "MyContract.sol".to_string(),
436 path: "os/Contract.sol".to_string(),
437 });
438
439 let result = remappings.into_inner();
440 assert_eq!(result.len(), 3, "Should have 3 remappings");
441 assert_eq!(result.first().unwrap().name, "@openzeppelin-contracts/");
442 assert_eq!(result.first().unwrap().path, "lib/openzeppelin-contracts/");
443 assert_eq!(result.get(1).unwrap().name, "@openzeppelin/contracts/");
444 assert_eq!(result.get(1).unwrap().path, "lib/openzeppelin/contracts/");
445 assert_eq!(result.get(2).unwrap().name, "MyContract.sol");
446 assert_eq!(result.get(2).unwrap().path, "os/Contract.sol");
447 }
448
449 #[test]
450 fn test_remappings_with_context() {
451 let mut remappings = Remappings::new();
452
453 remappings.push(Remapping {
455 context: Some("test/".to_string()),
456 name: "MyContract.sol".to_string(),
457 path: "test/Contract.sol".to_string(),
458 });
459 remappings.push(Remapping {
460 context: Some("prod/".to_string()),
461 name: "MyContract.sol".to_string(),
462 path: "prod/Contract.sol".to_string(),
463 });
464
465 let result = remappings.into_inner();
466 assert_eq!(result.len(), 2, "Should allow same name with different contexts");
467 assert!(
468 result
469 .iter()
470 .any(|r| r.context == Some("test/".to_string()) && r.path == "test/Contract.sol")
471 );
472 assert!(
473 result
474 .iter()
475 .any(|r| r.context == Some("prod/".to_string()) && r.path == "prod/Contract.sol")
476 );
477 }
478}