foundry_config/providers/
remappings.rs
1use 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 std::{
8 borrow::Cow,
9 collections::{BTreeMap, HashSet, btree_map::Entry},
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 && let Some(path) = path.into_string()
40 {
41 let remapping =
42 Remapping { context: None, name: format!("{path}/"), path: format!("{path}/") };
43 self.project_paths.push(remapping);
44 }
45 };
46 add_project_remapping("src");
47 add_project_remapping("test");
48 add_project_remapping("script");
49 self
50 }
51
52 fn filter_key(r: &Remapping) -> String {
54 match &r.context {
55 Some(str) => str.clone() + &r.name.clone(),
56 None => r.name.clone(),
57 }
58 }
59
60 pub fn into_inner(self) -> Vec<Remapping> {
62 let mut seen = HashSet::new();
63 self.remappings.iter().filter(|r| seen.insert(Self::filter_key(r))).cloned().collect()
64 }
65
66 fn push(&mut self, remapping: Remapping) {
68 if remapping.name.ends_with(".sol") && !remapping.path.ends_with(".sol") {
70 return;
71 }
72
73 if self.remappings.iter().any(|existing| {
74 if remapping.name.ends_with(".sol") {
75 return existing.name == remapping.name
77 && existing.context == remapping.context
78 && existing.path == remapping.path;
79 }
80
81 let mut existing_name_path = existing.name.clone();
92 if !existing_name_path.ends_with('/') {
93 existing_name_path.push('/')
94 }
95 let is_conflicting = remapping.name.starts_with(&existing_name_path)
96 || existing.name.starts_with(&remapping.name);
97 is_conflicting && existing.context == remapping.context
98 }) {
99 return;
100 };
101
102 if self
105 .project_paths
106 .iter()
107 .any(|project_path| remapping.name.eq_ignore_ascii_case(&project_path.name))
108 {
109 return;
110 };
111
112 self.remappings.push(remapping);
113 }
114
115 pub fn extend(&mut self, remappings: Vec<Remapping>) {
117 for remapping in remappings {
118 self.push(remapping);
119 }
120 }
121}
122
123pub struct RemappingsProvider<'a> {
129 pub auto_detect_remappings: bool,
131 pub lib_paths: Cow<'a, Vec<PathBuf>>,
133 pub root: &'a Path,
136 pub remappings: Result<Vec<Remapping>, Error>,
141}
142
143impl RemappingsProvider<'_> {
144 fn get_remappings(&self, remappings: Vec<Remapping>) -> Result<Vec<Remapping>, Error> {
155 trace!("get all remappings from {:?}", self.root);
156 fn insert_closest(
161 mappings: &mut BTreeMap<Option<String>, BTreeMap<String, PathBuf>>,
162 context: Option<String>,
163 key: String,
164 path: PathBuf,
165 ) {
166 let context_mappings = mappings.entry(context).or_default();
167 match context_mappings.entry(key) {
168 Entry::Occupied(mut e) => {
169 if e.get().components().count() > path.components().count() {
170 e.insert(path);
171 }
172 }
173 Entry::Vacant(e) => {
174 e.insert(path);
175 }
176 }
177 }
178
179 let mut user_remappings = Vec::new();
182
183 if let Some(env_remappings) = remappings_from_env_var("DAPP_REMAPPINGS")
185 .or_else(|| remappings_from_env_var("FOUNDRY_REMAPPINGS"))
186 {
187 user_remappings
188 .extend(env_remappings.map_err::<Error, _>(|err| err.to_string().into())?);
189 }
190
191 let remappings_file = self.root.join("remappings.txt");
193 if remappings_file.is_file() {
194 let content = fs::read_to_string(remappings_file).map_err(|err| err.to_string())?;
195 let remappings_from_file: Result<Vec<_>, _> =
196 remappings_from_newline(&content).collect();
197 user_remappings
198 .extend(remappings_from_file.map_err::<Error, _>(|err| err.to_string().into())?);
199 }
200
201 user_remappings.extend(remappings);
202 let mut all_remappings = Remappings::new_with_remappings(user_remappings);
205
206 if self.auto_detect_remappings {
210 let mut lib_remappings = BTreeMap::new();
211 for r in self.lib_foundry_toml_remappings() {
213 insert_closest(&mut lib_remappings, r.context, r.name, r.path.into());
214 }
215 for r in self
217 .lib_paths
218 .iter()
219 .map(|lib| self.root.join(lib))
220 .inspect(|lib| trace!(?lib, "find all remappings"))
221 .flat_map(|lib| Remapping::find_many(&lib))
222 {
223 if ["lib/", "src/", "contracts/"].contains(&r.name.as_str()) {
225 trace!(target: "forge", "- skipping the remapping");
226 continue;
227 }
228 insert_closest(&mut lib_remappings, r.context, r.name, r.path.into());
229 }
230
231 all_remappings.extend(
232 lib_remappings
233 .into_iter()
234 .flat_map(|(context, remappings)| {
235 remappings.into_iter().map(move |(name, path)| Remapping {
236 context: context.clone(),
237 name,
238 path: path.to_string_lossy().into(),
239 })
240 })
241 .collect(),
242 );
243 }
244
245 Ok(all_remappings.into_inner())
246 }
247
248 fn lib_foundry_toml_remappings(&self) -> impl Iterator<Item = Remapping> + '_ {
250 self.lib_paths
251 .iter()
252 .map(|p| if p.is_absolute() { self.root.join("lib") } else { self.root.join(p) })
253 .flat_map(foundry_toml_dirs)
254 .inspect(|lib| {
255 trace!("find all remappings of nested foundry.toml lib: {:?}", lib);
256 })
257 .flat_map(|lib: PathBuf| {
258 let Ok(config) = Config::load_with_root(&lib) else { return vec![] };
260 let config = config.sanitized();
261
262 let mut src_remapping = None;
266 if ![Path::new("src"), Path::new("contracts"), Path::new("lib")]
267 .contains(&config.src.as_path())
268 && let Some(name) = lib.file_name().and_then(|s| s.to_str())
269 {
270 let mut r = Remapping {
271 context: None,
272 name: format!("{name}/"),
273 path: format!("{}", lib.join(&config.src).display()),
274 };
275 if !r.path.ends_with('/') {
276 r.path.push('/')
277 }
278 src_remapping = Some(r);
279 }
280
281 let mut remappings =
286 config.remappings.into_iter().map(Remapping::from).collect::<Vec<Remapping>>();
287
288 if let Some(r) = src_remapping {
289 remappings.push(r);
290 }
291 remappings
292 })
293 }
294}
295
296impl Provider for RemappingsProvider<'_> {
297 fn metadata(&self) -> Metadata {
298 Metadata::named("Remapping Provider")
299 }
300
301 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
302 let remappings = match &self.remappings {
303 Ok(remappings) => self.get_remappings(remappings.clone()),
304 Err(err) => {
305 if let figment::error::Kind::MissingField(_) = err.kind {
306 self.get_remappings(vec![])
307 } else {
308 return Err(err.clone());
309 }
310 }
311 }?;
312
313 let remappings = remappings
315 .into_iter()
316 .map(|r| RelativeRemapping::new(r, self.root).to_string())
317 .collect::<Vec<_>>();
318
319 Ok(Map::from([(
320 Config::selected_profile(),
321 Dict::from([("remappings".to_string(), figment::value::Value::from(remappings))]),
322 )]))
323 }
324
325 fn profile(&self) -> Option<Profile> {
326 Some(Config::selected_profile())
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn test_sol_file_remappings() {
336 let mut remappings = Remappings::new();
337
338 remappings.push(Remapping {
340 context: None,
341 name: "MyContract.sol".to_string(),
342 path: "implementations/Contract1.sol".to_string(),
343 });
344
345 remappings.push(Remapping {
347 context: None,
348 name: "MyContract.sol".to_string(),
349 path: "implementations/Contract2.sol".to_string(),
350 });
351
352 remappings.push(Remapping {
354 context: None,
355 name: "OtherContract.sol".to_string(),
356 path: "implementations/Contract1.sol".to_string(),
357 });
358
359 remappings.push(Remapping {
361 context: None,
362 name: "MyContract.sol".to_string(),
363 path: "implementations/Contract1.sol".to_string(),
364 });
365
366 remappings.push(Remapping {
368 context: None,
369 name: "Invalid.sol".to_string(),
370 path: "implementations/Contract1.txt".to_string(),
371 });
372
373 let result = remappings.into_inner();
374 assert_eq!(result.len(), 2, "Should only have 2 valid remappings");
375
376 assert!(
378 result
379 .iter()
380 .any(|r| r.name == "MyContract.sol" && r.path == "implementations/Contract1.sol"),
381 "Should keep first mapping of MyContract.sol"
382 );
383 assert!(
384 !result
385 .iter()
386 .any(|r| r.name == "MyContract.sol" && r.path == "implementations/Contract2.sol"),
387 "Should keep first mapping of MyContract.sol"
388 );
389 assert!(result.iter().any(|r| r.name == "OtherContract.sol" && r.path == "implementations/Contract1.sol"),
390 "Should allow different source to same target");
391
392 assert!(
394 !result
395 .iter()
396 .any(|r| r.name == "MyContract.sol" && r.path == "implementations/Contract2.sol"),
397 "Should reject same source to different target"
398 );
399 }
400
401 #[test]
402 fn test_mixed_remappings() {
403 let mut remappings = Remappings::new();
404
405 remappings.push(Remapping {
406 context: None,
407 name: "@openzeppelin-contracts/".to_string(),
408 path: "lib/openzeppelin-contracts/".to_string(),
409 });
410 remappings.push(Remapping {
411 context: None,
412 name: "@openzeppelin/contracts/".to_string(),
413 path: "lib/openzeppelin/contracts/".to_string(),
414 });
415
416 remappings.push(Remapping {
417 context: None,
418 name: "MyContract.sol".to_string(),
419 path: "os/Contract.sol".to_string(),
420 });
421
422 let result = remappings.into_inner();
423 assert_eq!(result.len(), 3, "Should have 3 remappings");
424 assert_eq!(result.first().unwrap().name, "@openzeppelin-contracts/");
425 assert_eq!(result.first().unwrap().path, "lib/openzeppelin-contracts/");
426 assert_eq!(result.get(1).unwrap().name, "@openzeppelin/contracts/");
427 assert_eq!(result.get(1).unwrap().path, "lib/openzeppelin/contracts/");
428 assert_eq!(result.get(2).unwrap().name, "MyContract.sol");
429 assert_eq!(result.get(2).unwrap().path, "os/Contract.sol");
430 }
431
432 #[test]
433 fn test_remappings_with_context() {
434 let mut remappings = Remappings::new();
435
436 remappings.push(Remapping {
438 context: Some("test/".to_string()),
439 name: "MyContract.sol".to_string(),
440 path: "test/Contract.sol".to_string(),
441 });
442 remappings.push(Remapping {
443 context: Some("prod/".to_string()),
444 name: "MyContract.sol".to_string(),
445 path: "prod/Contract.sol".to_string(),
446 });
447
448 let result = remappings.into_inner();
449 assert_eq!(result.len(), 2, "Should allow same name with different contexts");
450 assert!(
451 result
452 .iter()
453 .any(|r| r.context == Some("test/".to_string()) && r.path == "test/Contract.sol")
454 );
455 assert!(
456 result
457 .iter()
458 .any(|r| r.context == Some("prod/".to_string()) && r.path == "prod/Contract.sol")
459 );
460 }
461}