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