foundry_config/providers/
remappings.rs
1use crate::{foundry_toml_dirs, remappings_from_env_var, remappings_from_newline, Config};
2use figment::{
3 value::{Dict, Map},
4 Error, Figment, Metadata, Profile, Provider,
5};
6use foundry_compilers::artifacts::remappings::{RelativeRemapping, Remapping};
7use std::{
8 borrow::Cow,
9 collections::{btree_map::Entry, BTreeMap, HashSet},
10 fs,
11 path::{Path, PathBuf},
12};
13
14#[derive(Clone, Debug, Default)]
16pub struct Remappings {
17 remappings: Vec<Remapping>,
19 project_paths: Vec<Remapping>,
22}
23
24impl Remappings {
25 pub fn new() -> Self {
27 Self { remappings: Vec::new(), project_paths: Vec::new() }
28 }
29
30 pub fn new_with_remappings(remappings: Vec<Remapping>) -> Self {
32 Self { remappings, project_paths: Vec::new() }
33 }
34
35 pub fn with_figment(mut self, figment: &Figment) -> Self {
37 let mut add_project_remapping = |path: &str| {
38 if let Ok(path) = figment.find_value(path) {
39 if let Some(path) = path.into_string() {
40 let remapping = Remapping {
41 context: None,
42 name: format!("{path}/"),
43 path: format!("{path}/"),
44 };
45 self.project_paths.push(remapping);
46 }
47 }
48 };
49 add_project_remapping("src");
50 add_project_remapping("test");
51 add_project_remapping("script");
52 self
53 }
54
55 fn filter_key(r: &Remapping) -> String {
57 match &r.context {
58 Some(str) => str.clone() + &r.name.clone(),
59 None => r.name.clone(),
60 }
61 }
62
63 pub fn into_inner(self) -> Vec<Remapping> {
65 let mut seen = HashSet::new();
66 let remappings =
67 self.remappings.iter().filter(|r| seen.insert(Self::filter_key(r))).cloned().collect();
68 remappings
69 }
70
71 fn push(&mut self, remapping: Remapping) {
73 if remapping.name.ends_with(".sol") && !remapping.path.ends_with(".sol") {
75 return;
76 }
77
78 if self.remappings.iter().any(|existing| {
79 if remapping.name.ends_with(".sol") {
80 return existing.name == remapping.name &&
82 existing.context == remapping.context &&
83 existing.path == remapping.path
84 }
85
86 let mut existing_name_path = existing.name.clone();
97 if !existing_name_path.ends_with('/') {
98 existing_name_path.push('/')
99 }
100 let is_conflicting = remapping.name.starts_with(&existing_name_path) ||
101 existing.name.starts_with(&remapping.name);
102 is_conflicting && existing.context == remapping.context
103 }) {
104 return;
105 };
106
107 if self
110 .project_paths
111 .iter()
112 .any(|project_path| remapping.name.eq_ignore_ascii_case(&project_path.name))
113 {
114 return;
115 };
116
117 self.remappings.push(remapping);
118 }
119
120 pub fn extend(&mut self, remappings: Vec<Remapping>) {
122 for remapping in remappings {
123 self.push(remapping);
124 }
125 }
126}
127
128pub struct RemappingsProvider<'a> {
134 pub auto_detect_remappings: bool,
136 pub lib_paths: Cow<'a, Vec<PathBuf>>,
138 pub root: &'a Path,
141 pub remappings: Result<Vec<Remapping>, Error>,
146}
147
148impl RemappingsProvider<'_> {
149 fn get_remappings(&self, remappings: Vec<Remapping>) -> Result<Vec<Remapping>, Error> {
160 trace!("get all remappings from {:?}", self.root);
161 fn insert_closest(
166 mappings: &mut BTreeMap<Option<String>, BTreeMap<String, PathBuf>>,
167 context: Option<String>,
168 key: String,
169 path: PathBuf,
170 ) {
171 let context_mappings = mappings.entry(context).or_default();
172 match context_mappings.entry(key) {
173 Entry::Occupied(mut e) => {
174 if e.get().components().count() > path.components().count() {
175 e.insert(path);
176 }
177 }
178 Entry::Vacant(e) => {
179 e.insert(path);
180 }
181 }
182 }
183
184 let mut user_remappings = Vec::new();
187
188 if let Some(env_remappings) = remappings_from_env_var("DAPP_REMAPPINGS")
190 .or_else(|| remappings_from_env_var("FOUNDRY_REMAPPINGS"))
191 {
192 user_remappings
193 .extend(env_remappings.map_err::<Error, _>(|err| err.to_string().into())?);
194 }
195
196 let remappings_file = self.root.join("remappings.txt");
198 if remappings_file.is_file() {
199 let content = fs::read_to_string(remappings_file).map_err(|err| err.to_string())?;
200 let remappings_from_file: Result<Vec<_>, _> =
201 remappings_from_newline(&content).collect();
202 user_remappings
203 .extend(remappings_from_file.map_err::<Error, _>(|err| err.to_string().into())?);
204 }
205
206 user_remappings.extend(remappings);
207 let mut all_remappings = Remappings::new_with_remappings(user_remappings);
210
211 if self.auto_detect_remappings {
215 let mut lib_remappings = BTreeMap::new();
216 for r in self.lib_foundry_toml_remappings() {
218 insert_closest(&mut lib_remappings, r.context, r.name, r.path.into());
219 }
220 for r in self
222 .lib_paths
223 .iter()
224 .map(|lib| self.root.join(lib))
225 .inspect(|lib| trace!(?lib, "find all remappings"))
226 .flat_map(|lib| Remapping::find_many(&lib))
227 {
228 if ["lib/", "src/", "contracts/"].contains(&r.name.as_str()) {
230 trace!(target: "forge", "- skipping the remapping");
231 continue
232 }
233 insert_closest(&mut lib_remappings, r.context, r.name, r.path.into());
234 }
235
236 all_remappings.extend(
237 lib_remappings
238 .into_iter()
239 .flat_map(|(context, remappings)| {
240 remappings.into_iter().map(move |(name, path)| Remapping {
241 context: context.clone(),
242 name,
243 path: path.to_string_lossy().into(),
244 })
245 })
246 .collect(),
247 );
248 }
249
250 Ok(all_remappings.into_inner())
251 }
252
253 fn lib_foundry_toml_remappings(&self) -> impl Iterator<Item = Remapping> + '_ {
255 self.lib_paths
256 .iter()
257 .map(|p| if p.is_absolute() { self.root.join("lib") } else { self.root.join(p) })
258 .flat_map(foundry_toml_dirs)
259 .inspect(|lib| {
260 trace!("find all remappings of nested foundry.toml lib: {:?}", lib);
261 })
262 .flat_map(|lib: PathBuf| {
263 let Ok(config) = Config::load_with_root(&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 {
274 if let Some(name) = lib.file_name().and_then(|s| s.to_str()) {
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
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}
301
302impl Provider for RemappingsProvider<'_> {
303 fn metadata(&self) -> Metadata {
304 Metadata::named("Remapping Provider")
305 }
306
307 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
308 let remappings = match &self.remappings {
309 Ok(remappings) => self.get_remappings(remappings.clone()),
310 Err(err) => {
311 if let figment::error::Kind::MissingField(_) = err.kind {
312 self.get_remappings(vec![])
313 } else {
314 return Err(err.clone())
315 }
316 }
317 }?;
318
319 let remappings = remappings
321 .into_iter()
322 .map(|r| RelativeRemapping::new(r, self.root).to_string())
323 .collect::<Vec<_>>();
324
325 Ok(Map::from([(
326 Config::selected_profile(),
327 Dict::from([("remappings".to_string(), figment::value::Value::from(remappings))]),
328 )]))
329 }
330
331 fn profile(&self) -> Option<Profile> {
332 Some(Config::selected_profile())
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339
340 #[test]
341 fn test_sol_file_remappings() {
342 let mut remappings = Remappings::new();
343
344 remappings.push(Remapping {
346 context: None,
347 name: "MyContract.sol".to_string(),
348 path: "implementations/Contract1.sol".to_string(),
349 });
350
351 remappings.push(Remapping {
353 context: None,
354 name: "MyContract.sol".to_string(),
355 path: "implementations/Contract2.sol".to_string(),
356 });
357
358 remappings.push(Remapping {
360 context: None,
361 name: "OtherContract.sol".to_string(),
362 path: "implementations/Contract1.sol".to_string(),
363 });
364
365 remappings.push(Remapping {
367 context: None,
368 name: "MyContract.sol".to_string(),
369 path: "implementations/Contract1.sol".to_string(),
370 });
371
372 remappings.push(Remapping {
374 context: None,
375 name: "Invalid.sol".to_string(),
376 path: "implementations/Contract1.txt".to_string(),
377 });
378
379 let result = remappings.into_inner();
380 assert_eq!(result.len(), 2, "Should only have 2 valid remappings");
381
382 assert!(
384 result
385 .iter()
386 .any(|r| r.name == "MyContract.sol" && r.path == "implementations/Contract1.sol"),
387 "Should keep first mapping of MyContract.sol"
388 );
389 assert!(
390 !result
391 .iter()
392 .any(|r| r.name == "MyContract.sol" && r.path == "implementations/Contract2.sol"),
393 "Should keep first mapping of MyContract.sol"
394 );
395 assert!(result.iter().any(|r| r.name == "OtherContract.sol" && r.path == "implementations/Contract1.sol"),
396 "Should allow different source to same target");
397
398 assert!(
400 !result
401 .iter()
402 .any(|r| r.name == "MyContract.sol" && r.path == "implementations/Contract2.sol"),
403 "Should reject same source to different target"
404 );
405 }
406
407 #[test]
408 fn test_mixed_remappings() {
409 let mut remappings = Remappings::new();
410
411 remappings.push(Remapping {
412 context: None,
413 name: "@openzeppelin-contracts/".to_string(),
414 path: "lib/openzeppelin-contracts/".to_string(),
415 });
416 remappings.push(Remapping {
417 context: None,
418 name: "@openzeppelin/contracts/".to_string(),
419 path: "lib/openzeppelin/contracts/".to_string(),
420 });
421
422 remappings.push(Remapping {
423 context: None,
424 name: "MyContract.sol".to_string(),
425 path: "os/Contract.sol".to_string(),
426 });
427
428 let result = remappings.into_inner();
429 assert_eq!(result.len(), 3, "Should have 3 remappings");
430 assert_eq!(result.first().unwrap().name, "@openzeppelin-contracts/");
431 assert_eq!(result.first().unwrap().path, "lib/openzeppelin-contracts/");
432 assert_eq!(result.get(1).unwrap().name, "@openzeppelin/contracts/");
433 assert_eq!(result.get(1).unwrap().path, "lib/openzeppelin/contracts/");
434 assert_eq!(result.get(2).unwrap().name, "MyContract.sol");
435 assert_eq!(result.get(2).unwrap().path, "os/Contract.sol");
436 }
437
438 #[test]
439 fn test_remappings_with_context() {
440 let mut remappings = Remappings::new();
441
442 remappings.push(Remapping {
444 context: Some("test/".to_string()),
445 name: "MyContract.sol".to_string(),
446 path: "test/Contract.sol".to_string(),
447 });
448 remappings.push(Remapping {
449 context: Some("prod/".to_string()),
450 name: "MyContract.sol".to_string(),
451 path: "prod/Contract.sol".to_string(),
452 });
453
454 let result = remappings.into_inner();
455 assert_eq!(result.len(), 2, "Should allow same name with different contexts");
456 assert!(result
457 .iter()
458 .any(|r| r.context == Some("test/".to_string()) && r.path == "test/Contract.sol"));
459 assert!(result
460 .iter()
461 .any(|r| r.context == Some("prod/".to_string()) && r.path == "prod/Contract.sol"));
462 }
463}