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 const fn new() -> Self {
28 Self { remappings: Vec::new(), project_paths: Vec::new() }
29 }
30
31 pub const 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 src_remapping = if ![Path::new("src"), Path::new("contracts"), Path::new("lib")]
271 .contains(&config.src.as_path())
272 && let Some(name) = lib.file_name().and_then(|s| s.to_str())
273 {
274 let mut r = Remapping {
275 context: None,
276 name: format!("{name}/"),
277 path: format!("{}", lib.join(&config.src).display()),
278 };
279 if !r.path.ends_with('/') {
280 r.path.push('/')
281 }
282 Some(r)
283 } else {
284 None
285 };
286
287 let mut remappings =
292 config.remappings.into_iter().map(Remapping::from).collect::<Vec<Remapping>>();
293
294 if let Some(r) = src_remapping {
295 remappings.push(r);
296 }
297 remappings
298 }
299
300 fn auto_detect_remappings(&self) -> impl Iterator<Item = Remapping> + '_ {
302 self.lib_paths
303 .par_iter()
304 .flat_map_iter(|lib| {
305 let lib = self.root.join(lib);
306 trace!(?lib, "find all remappings");
307 Remapping::find_many(&lib)
308 })
309 .collect::<Vec<_>>()
310 .into_iter()
311 }
312}
313
314impl Provider for RemappingsProvider<'_> {
315 fn metadata(&self) -> Metadata {
316 Metadata::named("Remapping Provider")
317 }
318
319 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
320 let remappings = match &self.remappings {
321 Ok(remappings) => self.get_remappings(remappings.clone()),
322 Err(err) => {
323 if let figment::error::Kind::MissingField(_) = err.kind {
324 self.get_remappings(vec![])
325 } else {
326 return Err(err.clone());
327 }
328 }
329 }?;
330
331 let remappings = remappings
333 .into_iter()
334 .map(|r| RelativeRemapping::new(r, self.root).to_string())
335 .collect::<Vec<_>>();
336
337 Ok(Map::from([(
338 Config::selected_profile(),
339 Dict::from([("remappings".to_string(), figment::value::Value::from(remappings))]),
340 )]))
341 }
342
343 fn profile(&self) -> Option<Profile> {
344 Some(Config::selected_profile())
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn test_sol_file_remappings() {
354 let mut remappings = Remappings::new();
355
356 remappings.push(Remapping {
358 context: None,
359 name: "MyContract.sol".to_string(),
360 path: "implementations/Contract1.sol".to_string(),
361 });
362
363 remappings.push(Remapping {
365 context: None,
366 name: "MyContract.sol".to_string(),
367 path: "implementations/Contract2.sol".to_string(),
368 });
369
370 remappings.push(Remapping {
372 context: None,
373 name: "OtherContract.sol".to_string(),
374 path: "implementations/Contract1.sol".to_string(),
375 });
376
377 remappings.push(Remapping {
379 context: None,
380 name: "MyContract.sol".to_string(),
381 path: "implementations/Contract1.sol".to_string(),
382 });
383
384 remappings.push(Remapping {
386 context: None,
387 name: "Invalid.sol".to_string(),
388 path: "implementations/Contract1.txt".to_string(),
389 });
390
391 let result = remappings.into_inner();
392 assert_eq!(result.len(), 2, "Should only have 2 valid remappings");
393
394 assert!(
396 result
397 .iter()
398 .any(|r| r.name == "MyContract.sol" && r.path == "implementations/Contract1.sol"),
399 "Should keep first mapping of MyContract.sol"
400 );
401 assert!(
402 !result
403 .iter()
404 .any(|r| r.name == "MyContract.sol" && r.path == "implementations/Contract2.sol"),
405 "Should keep first mapping of MyContract.sol"
406 );
407 assert!(result.iter().any(|r| r.name == "OtherContract.sol" && r.path == "implementations/Contract1.sol"),
408 "Should allow different source to same target");
409
410 assert!(
412 !result
413 .iter()
414 .any(|r| r.name == "MyContract.sol" && r.path == "implementations/Contract2.sol"),
415 "Should reject same source to different target"
416 );
417 }
418
419 #[test]
420 fn test_mixed_remappings() {
421 let mut remappings = Remappings::new();
422
423 remappings.push(Remapping {
424 context: None,
425 name: "@openzeppelin-contracts/".to_string(),
426 path: "lib/openzeppelin-contracts/".to_string(),
427 });
428 remappings.push(Remapping {
429 context: None,
430 name: "@openzeppelin/contracts/".to_string(),
431 path: "lib/openzeppelin/contracts/".to_string(),
432 });
433
434 remappings.push(Remapping {
435 context: None,
436 name: "MyContract.sol".to_string(),
437 path: "os/Contract.sol".to_string(),
438 });
439
440 let result = remappings.into_inner();
441 assert_eq!(result.len(), 3, "Should have 3 remappings");
442 assert_eq!(result.first().unwrap().name, "@openzeppelin-contracts/");
443 assert_eq!(result.first().unwrap().path, "lib/openzeppelin-contracts/");
444 assert_eq!(result.get(1).unwrap().name, "@openzeppelin/contracts/");
445 assert_eq!(result.get(1).unwrap().path, "lib/openzeppelin/contracts/");
446 assert_eq!(result.get(2).unwrap().name, "MyContract.sol");
447 assert_eq!(result.get(2).unwrap().path, "os/Contract.sol");
448 }
449
450 #[test]
451 fn test_remappings_with_context() {
452 let mut remappings = Remappings::new();
453
454 remappings.push(Remapping {
456 context: Some("test/".to_string()),
457 name: "MyContract.sol".to_string(),
458 path: "test/Contract.sol".to_string(),
459 });
460 remappings.push(Remapping {
461 context: Some("prod/".to_string()),
462 name: "MyContract.sol".to_string(),
463 path: "prod/Contract.sol".to_string(),
464 });
465
466 let result = remappings.into_inner();
467 assert_eq!(result.len(), 2, "Should allow same name with different contexts");
468 assert!(
469 result
470 .iter()
471 .any(|r| r.context == Some("test/".to_string()) && r.path == "test/Contract.sol")
472 );
473 assert!(
474 result
475 .iter()
476 .any(|r| r.context == Some("prod/".to_string()) && r.path == "prod/Contract.sol")
477 );
478 }
479}