1use crate::utils;
2
3use serde::{Deserialize, Serialize};
4use std::{
5 collections::{hash_map::Entry, HashMap},
6 fmt,
7 path::{Path, PathBuf},
8 str::FromStr,
9};
10
11const DAPPTOOLS_CONTRACTS_DIR: &str = "src";
12const DAPPTOOLS_LIB_DIR: &str = "lib";
13const JS_CONTRACTS_DIR: &str = "contracts";
14const JS_LIB_DIR: &str = "node_modules";
15
16#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord)]
57pub struct Remapping {
58 pub context: Option<String>,
59 pub name: String,
60 pub path: String,
61}
62
63impl Remapping {
64 pub fn into_relative(self, root: impl AsRef<Path>) -> RelativeRemapping {
66 RelativeRemapping::new(self, root)
67 }
68
69 pub fn strip_prefix(&mut self, base: impl AsRef<Path>) -> &mut Self {
71 if let Ok(stripped) = Path::new(&self.path).strip_prefix(base.as_ref()) {
72 self.path = format!("{}", stripped.display());
73 }
74 self
75 }
76}
77
78#[derive(thiserror::Error, Debug, PartialEq, Eq, PartialOrd)]
79pub enum RemappingError {
80 #[error("invalid remapping format, found `{0}`, expected `<key>=<value>`")]
81 InvalidRemapping(String),
82 #[error("remapping key can't be empty, found `{0}`, expected `<key>=<value>`")]
83 EmptyRemappingKey(String),
84 #[error("remapping value must be a path, found `{0}`, expected `<key>=<value>`")]
85 EmptyRemappingValue(String),
86}
87
88impl FromStr for Remapping {
89 type Err = RemappingError;
90
91 fn from_str(remapping: &str) -> Result<Self, Self::Err> {
92 let (name, path) = remapping
93 .split_once('=')
94 .ok_or_else(|| RemappingError::InvalidRemapping(remapping.to_string()))?;
95 let (context, name) = name
96 .split_once(':')
97 .map_or((None, name), |(context, name)| (Some(context.to_string()), name));
98 if name.trim().is_empty() {
99 return Err(RemappingError::EmptyRemappingKey(remapping.to_string()))
100 }
101 if path.trim().is_empty() {
102 return Err(RemappingError::EmptyRemappingValue(remapping.to_string()))
103 }
104 let context =
106 context.and_then(|c| if c.trim().is_empty() { None } else { Some(c.to_string()) });
107 Ok(Remapping { context, name: name.to_string(), path: path.to_string() })
108 }
109}
110
111impl Serialize for Remapping {
112 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
113 where
114 S: serde::ser::Serializer,
115 {
116 serializer.serialize_str(&self.to_string())
117 }
118}
119
120impl<'de> Deserialize<'de> for Remapping {
121 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
122 where
123 D: serde::de::Deserializer<'de>,
124 {
125 let remapping = String::deserialize(deserializer)?;
126 Remapping::from_str(&remapping).map_err(serde::de::Error::custom)
127 }
128}
129
130impl fmt::Display for Remapping {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 let mut s = String::new();
134 if let Some(context) = self.context.as_ref() {
135 #[cfg(target_os = "windows")]
136 {
137 use path_slash::PathExt;
139 s.push_str(&std::path::Path::new(context).to_slash_lossy());
140 }
141 #[cfg(not(target_os = "windows"))]
142 {
143 s.push_str(context);
144 }
145 s.push(':');
146 }
147 s.push_str(&{
148 #[cfg(target_os = "windows")]
149 {
150 use path_slash::PathExt;
152 format!("{}={}", self.name, std::path::Path::new(&self.path).to_slash_lossy())
153 }
154 #[cfg(not(target_os = "windows"))]
155 {
156 format!("{}={}", self.name, self.path)
157 }
158 });
159
160 if !s.ends_with('/') {
161 s.push('/');
162 }
163 f.write_str(&s)
164 }
165}
166
167impl Remapping {
168 pub fn find_many_str(path: &str) -> Vec<String> {
170 Self::find_many(path).into_iter().map(|r| r.to_string()).collect()
171 }
172
173 pub fn find_many(dir: impl AsRef<Path>) -> Vec<Remapping> {
206 fn insert_prioritized(mappings: &mut HashMap<String, PathBuf>, key: String, path: PathBuf) {
210 match mappings.entry(key) {
211 Entry::Occupied(mut e) => {
212 if e.get().components().count() > path.components().count() ||
213 (path.ends_with(DAPPTOOLS_CONTRACTS_DIR) &&
214 !e.get().ends_with(DAPPTOOLS_CONTRACTS_DIR))
215 {
216 e.insert(path);
217 }
218 }
219 Entry::Vacant(e) => {
220 e.insert(path);
221 }
222 }
223 }
224
225 let mut all_remappings = HashMap::new();
227
228 let dir = dir.as_ref();
229 let is_inside_node_modules = dir.ends_with("node_modules");
230
231 for dir in walkdir::WalkDir::new(dir)
233 .follow_links(true)
234 .min_depth(1)
235 .max_depth(1)
236 .into_iter()
237 .filter_entry(|e| !is_hidden(e))
238 .filter_map(Result::ok)
239 .filter(|e| e.file_type().is_dir())
240 {
241 let depth1_dir = dir.path();
242 let candidates =
244 find_remapping_candidates(depth1_dir, depth1_dir, 0, is_inside_node_modules);
245
246 for candidate in candidates {
247 if let Some(name) = candidate.window_start.file_name().and_then(|s| s.to_str()) {
248 insert_prioritized(
249 &mut all_remappings,
250 format!("{name}/"),
251 candidate.source_dir,
252 );
253 }
254 }
255 }
256
257 all_remappings
258 .into_iter()
259 .map(|(name, path)| Remapping {
260 context: None,
261 name,
262 path: format!("{}/", path.display()),
263 })
264 .collect()
265 }
266
267 pub fn slash_path(&mut self) {
269 #[cfg(windows)]
270 {
271 use path_slash::PathExt;
272 self.path = Path::new(&self.path).to_slash_lossy().to_string();
273 if let Some(context) = self.context.as_mut() {
274 *context = Path::new(&context).to_slash_lossy().to_string();
275 }
276 }
277 }
278}
279
280#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord)]
284pub struct RelativeRemapping {
285 pub context: Option<String>,
286 pub name: String,
287 pub path: RelativeRemappingPathBuf,
288}
289
290impl RelativeRemapping {
291 pub fn new(remapping: Remapping, root: impl AsRef<Path>) -> Self {
293 Self {
294 context: remapping.context.map(|c| {
295 RelativeRemappingPathBuf::with_root(root.as_ref(), c)
296 .path
297 .to_string_lossy()
298 .to_string()
299 }),
300 name: remapping.name,
301 path: RelativeRemappingPathBuf::with_root(root, remapping.path),
302 }
303 }
304
305 pub fn to_remapping(mut self, root: PathBuf) -> Remapping {
309 self.path.parent = Some(root);
310 self.into()
311 }
312
313 pub fn to_relative_remapping(mut self) -> Remapping {
315 self.path.parent.take();
316 self.into()
317 }
318}
319
320impl fmt::Display for RelativeRemapping {
322 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323 let mut s = String::new();
324 if let Some(context) = self.context.as_ref() {
325 #[cfg(target_os = "windows")]
326 {
327 use path_slash::PathExt;
329 s.push_str(&std::path::Path::new(context).to_slash_lossy());
330 }
331 #[cfg(not(target_os = "windows"))]
332 {
333 s.push_str(context);
334 }
335 s.push(':');
336 }
337 s.push_str(&{
338 #[cfg(target_os = "windows")]
339 {
340 use path_slash::PathExt;
342 format!("{}={}", self.name, self.path.original().to_slash_lossy())
343 }
344 #[cfg(not(target_os = "windows"))]
345 {
346 format!("{}={}", self.name, self.path.original().display())
347 }
348 });
349
350 if !s.ends_with('/') {
351 s.push('/');
352 }
353 f.write_str(&s)
354 }
355}
356
357impl From<RelativeRemapping> for Remapping {
358 fn from(r: RelativeRemapping) -> Self {
359 let RelativeRemapping { context, mut name, path } = r;
360 let mut path = format!("{}", path.relative().display());
361 if !path.ends_with('/') {
362 path.push('/');
363 }
364 if !name.ends_with('/') {
365 name.push('/');
366 }
367 Remapping { context, name, path }
368 }
369}
370
371impl From<Remapping> for RelativeRemapping {
372 fn from(r: Remapping) -> Self {
373 Self { context: r.context, name: r.name, path: r.path.into() }
374 }
375}
376
377#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord)]
384pub struct RelativeRemappingPathBuf {
385 pub parent: Option<PathBuf>,
386 pub path: PathBuf,
387}
388
389impl RelativeRemappingPathBuf {
390 pub fn with_root(parent: impl AsRef<Path>, path: impl AsRef<Path>) -> Self {
393 let parent = parent.as_ref();
394 let path = path.as_ref();
395 if let Ok(path) = path.strip_prefix(parent) {
396 Self { parent: Some(parent.to_path_buf()), path: path.to_path_buf() }
397 } else if path.has_root() {
398 Self { parent: None, path: path.to_path_buf() }
399 } else {
400 Self { parent: Some(parent.to_path_buf()), path: path.to_path_buf() }
401 }
402 }
403
404 pub fn original(&self) -> &Path {
406 &self.path
407 }
408
409 pub fn relative(&self) -> PathBuf {
413 if self.original().has_root() {
414 return self.original().into()
415 }
416 self.parent
417 .as_ref()
418 .map(|p| p.join(self.original()))
419 .unwrap_or_else(|| self.original().into())
420 }
421}
422
423impl<P: AsRef<Path>> From<P> for RelativeRemappingPathBuf {
424 fn from(path: P) -> RelativeRemappingPathBuf {
425 Self { parent: None, path: path.as_ref().to_path_buf() }
426 }
427}
428
429impl Serialize for RelativeRemapping {
430 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
431 where
432 S: serde::ser::Serializer,
433 {
434 serializer.serialize_str(&self.to_string())
435 }
436}
437
438impl<'de> Deserialize<'de> for RelativeRemapping {
439 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
440 where
441 D: serde::de::Deserializer<'de>,
442 {
443 let remapping = String::deserialize(deserializer)?;
444 let remapping = Remapping::from_str(&remapping).map_err(serde::de::Error::custom)?;
445 Ok(RelativeRemapping {
446 context: remapping.context,
447 name: remapping.name,
448 path: remapping.path.into(),
449 })
450 }
451}
452
453#[derive(Debug, Clone)]
454struct Candidate {
455 window_start: PathBuf,
457 source_dir: PathBuf,
459 window_level: usize,
461}
462
463impl Candidate {
464 fn merge_on_same_level(
531 candidates: &mut Vec<Candidate>,
532 current_dir: &Path,
533 current_level: usize,
534 window_start: PathBuf,
535 is_inside_node_modules: bool,
536 ) {
537 if let Some(pos) = candidates
539 .iter()
540 .enumerate()
541 .fold((0, None), |(mut contracts_dir_count, mut pos), (idx, c)| {
542 if c.source_dir.ends_with(DAPPTOOLS_CONTRACTS_DIR) {
543 contracts_dir_count += 1;
544 if contracts_dir_count == 1 {
545 pos = Some(idx)
546 } else {
547 pos = None;
548 }
549 }
550
551 (contracts_dir_count, pos)
552 })
553 .1
554 {
555 let c = candidates.remove(pos);
556 *candidates = vec![c];
557 } else {
558 candidates.retain(|c| c.window_level != current_level);
562
563 let source_dir = if is_inside_node_modules {
564 window_start.clone()
565 } else {
566 current_dir.to_path_buf()
567 };
568
569 if current_level > 0 &&
572 source_dir == window_start &&
573 (is_source_dir(&source_dir) || is_lib_dir(&source_dir))
574 {
575 return
576 }
577 candidates.push(Candidate { window_start, source_dir, window_level: current_level });
578 }
579 }
580
581 fn source_dir_ends_with_js_source(&self) -> bool {
605 self.source_dir.ends_with(JS_CONTRACTS_DIR) || self.source_dir.ends_with("contracts/src/")
606 }
607}
608
609fn is_source_dir(dir: &Path) -> bool {
610 dir.file_name()
611 .and_then(|p| p.to_str())
612 .map(|name| [DAPPTOOLS_CONTRACTS_DIR, JS_CONTRACTS_DIR].contains(&name))
613 .unwrap_or_default()
614}
615
616fn is_lib_dir(dir: &Path) -> bool {
617 dir.file_name()
618 .and_then(|p| p.to_str())
619 .map(|name| [DAPPTOOLS_LIB_DIR, JS_LIB_DIR].contains(&name))
620 .unwrap_or_default()
621}
622
623fn is_hidden(entry: &walkdir::DirEntry) -> bool {
625 entry.file_name().to_str().map(|s| s.starts_with('.')).unwrap_or(false)
626}
627
628fn find_remapping_candidates(
630 current_dir: &Path,
631 open: &Path,
632 current_level: usize,
633 is_inside_node_modules: bool,
634) -> Vec<Candidate> {
635 let mut is_candidate = false;
637
638 let mut candidates = Vec::new();
640
641 for entry in walkdir::WalkDir::new(current_dir)
643 .follow_links(true)
644 .min_depth(1)
645 .max_depth(1)
646 .into_iter()
647 .filter_entry(|e| !is_hidden(e))
648 .filter_map(Result::ok)
649 {
650 let entry: walkdir::DirEntry = entry;
651
652 if !is_candidate &&
654 entry.file_type().is_file() &&
655 entry.path().extension() == Some("sol".as_ref())
656 {
657 is_candidate = true;
658 } else if entry.file_type().is_dir() {
659 if entry.path_is_symlink() {
668 if let Ok(target) = utils::canonicalize(entry.path()) {
669 if open.components().count() > target.components().count() &&
671 utils::common_ancestor(open, &target).is_some()
672 {
673 return Vec::new()
675 }
676 }
677 }
678
679 let subdir = entry.path();
680 if !(subdir.ends_with("tests") || subdir.ends_with("test") || subdir.ends_with("demo"))
682 {
683 if is_lib_dir(subdir) {
692 candidates.extend(find_remapping_candidates(
693 subdir,
694 subdir,
695 current_level + 1,
696 is_inside_node_modules,
697 ));
698 } else {
699 candidates.extend(find_remapping_candidates(
701 subdir,
702 open,
703 current_level,
704 is_inside_node_modules,
705 ));
706 }
707 }
708 }
709 }
710
711 let window_start = next_nested_window(open, current_dir);
713 if is_candidate ||
715 candidates
716 .iter()
717 .filter(|c| c.window_level == current_level && c.window_start == window_start)
718 .count() >
719 1
720 {
721 Candidate::merge_on_same_level(
722 &mut candidates,
723 current_dir,
724 current_level,
725 window_start,
726 is_inside_node_modules,
727 );
728 } else {
729 if let Some(candidate) = candidates.iter_mut().find(|c| c.window_level == current_level) {
731 let distance = dir_distance(&candidate.window_start, &candidate.source_dir);
735 if distance > 1 && candidate.source_dir_ends_with_js_source() {
736 candidate.source_dir = window_start;
737 } else if !is_source_dir(&candidate.source_dir) &&
738 candidate.source_dir != candidate.window_start
739 {
740 candidate.source_dir = last_nested_source_dir(open, &candidate.source_dir);
741 }
742 }
743 }
744 candidates
745}
746
747fn dir_distance(root: &Path, current: &Path) -> usize {
750 if root == current {
751 return 0
752 }
753 if let Ok(rem) = current.strip_prefix(root) {
754 rem.components().count()
755 } else {
756 0
757 }
758}
759
760fn next_nested_window(root: &Path, current: &Path) -> PathBuf {
764 if !is_lib_dir(root) || root == current {
765 return root.to_path_buf()
766 }
767 if let Ok(rem) = current.strip_prefix(root) {
768 let mut p = root.to_path_buf();
769 for c in rem.components() {
770 let next = p.join(c);
771 if !is_lib_dir(&next) || !next.ends_with(JS_CONTRACTS_DIR) {
772 return next
773 }
774 p = next
775 }
776 }
777 root.to_path_buf()
778}
779
780fn last_nested_source_dir(root: &Path, dir: &Path) -> PathBuf {
782 if is_source_dir(dir) {
783 return dir.to_path_buf()
784 }
785 let mut p = dir;
786 while let Some(parent) = p.parent() {
787 if parent == root {
788 return root.to_path_buf()
789 }
790 if is_source_dir(parent) {
791 return parent.to_path_buf()
792 }
793 p = parent;
794 }
795 root.to_path_buf()
796}
797
798#[cfg(test)]
799mod tests {
800 use super::*;
801 use crate::{utils::tempdir, ProjectPathsConfig};
802
803 #[test]
804 fn relative_remapping() {
805 let remapping = "oz=a/b/c/d";
806 let remapping = Remapping::from_str(remapping).unwrap();
807
808 let relative = RelativeRemapping::new(remapping.clone(), "a/b/c");
809 assert_eq!(relative.path.relative(), Path::new(&remapping.path));
810 assert_eq!(relative.path.original(), Path::new("d"));
811
812 let relative = RelativeRemapping::new(remapping.clone(), "x/y");
813 assert_eq!(relative.path.relative(), Path::new("x/y/a/b/c/d"));
814 assert_eq!(relative.path.original(), Path::new(&remapping.path));
815
816 let remapping = "oz=/a/b/c/d";
817 let remapping = Remapping::from_str(remapping).unwrap();
818 let relative = RelativeRemapping::new(remapping.clone(), "a/b");
819 assert_eq!(relative.path.relative(), Path::new(&remapping.path));
820 assert_eq!(relative.path.original(), Path::new(&remapping.path));
821 assert!(relative.path.parent.is_none());
822
823 let relative = RelativeRemapping::new(remapping, "/a/b");
824 assert_eq!(relative.to_relative_remapping(), Remapping::from_str("oz/=c/d/").unwrap());
825 }
826
827 #[test]
828 fn remapping_errors() {
829 let remapping = "oz=../b/c/d";
830 let remapping = Remapping::from_str(remapping).unwrap();
831 assert_eq!(remapping.name, "oz".to_string());
832 assert_eq!(remapping.path, "../b/c/d".to_string());
833
834 let err = Remapping::from_str("").unwrap_err();
835 matches!(err, RemappingError::InvalidRemapping(_));
836
837 let err = Remapping::from_str("oz=").unwrap_err();
838 matches!(err, RemappingError::EmptyRemappingValue(_));
839 }
840
841 fn touch(path: &std::path::Path) -> std::io::Result<()> {
843 match std::fs::OpenOptions::new().create(true).append(true).open(path) {
844 Ok(_) => Ok(()),
845 Err(e) => Err(e),
846 }
847 }
848
849 fn mkdir_or_touch(tmp: &std::path::Path, paths: &[&str]) {
850 for path in paths {
851 if let Some(parent) = Path::new(path).parent() {
852 std::fs::create_dir_all(tmp.join(parent)).unwrap();
853 }
854 if path.ends_with(".sol") {
855 let path = tmp.join(path);
856 touch(&path).unwrap();
857 } else {
858 let path = tmp.join(path);
859 std::fs::create_dir_all(path).unwrap();
860 }
861 }
862 }
863
864 fn to_str(p: std::path::PathBuf) -> String {
866 format!("{}/", p.display())
867 }
868
869 #[test]
870 #[cfg_attr(windows, ignore = "Windows remappings #2347")]
871 fn find_remapping_dapptools() {
872 let tmp_dir = tempdir("lib").unwrap();
873 let tmp_dir_path = tmp_dir.path();
874 let paths = ["repo1/src/", "repo1/src/contract.sol"];
875 mkdir_or_touch(tmp_dir_path, &paths[..]);
876
877 let path = tmp_dir_path.join("repo1").display().to_string();
878 let remappings = Remapping::find_many(tmp_dir_path);
879 assert_eq!(remappings.len(), 1);
881
882 assert_eq!(remappings[0].name, "repo1/");
883 assert_eq!(remappings[0].path, format!("{path}/src/"));
884 }
885
886 #[test]
887 #[cfg_attr(windows, ignore = "Windows remappings #2347")]
888 fn can_resolve_contract_dir_combinations() {
889 let tmp_dir = tempdir("demo").unwrap();
890 let paths =
891 ["lib/timeless/src/lib/A.sol", "lib/timeless/src/B.sol", "lib/timeless/src/test/C.sol"];
892 mkdir_or_touch(tmp_dir.path(), &paths[..]);
893
894 let tmp_dir_path = tmp_dir.path().join("lib");
895 let remappings = Remapping::find_many(&tmp_dir_path);
896 let expected = vec![Remapping {
897 context: None,
898 name: "timeless/".to_string(),
899 path: to_str(tmp_dir_path.join("timeless/src")),
900 }];
901 pretty_assertions::assert_eq!(remappings, expected);
902 }
903
904 #[test]
905 #[cfg_attr(windows, ignore = "Windows remappings #2347")]
906 fn can_resolve_geb_remappings() {
907 let tmp_dir = tempdir("geb").unwrap();
908 let paths = [
909 "lib/ds-token/src/test/Contract.sol",
910 "lib/ds-token/lib/ds-test/src/Contract.sol",
911 "lib/ds-token/lib/ds-test/aux/Contract.sol",
912 "lib/ds-token/lib/ds-stop/lib/ds-test/src/Contract.sol",
913 "lib/ds-token/lib/ds-stop/lib/ds-note/src/Contract.sol",
914 "lib/ds-token/lib/ds-math/lib/ds-test/aux/Contract.sol",
915 "lib/ds-token/lib/ds-math/src/Contract.sol",
916 "lib/ds-token/lib/ds-stop/lib/ds-test/aux/Contract.sol",
917 "lib/ds-token/lib/ds-stop/lib/ds-note/lib/ds-test/src/Contract.sol",
918 "lib/ds-token/lib/ds-math/lib/ds-test/src/Contract.sol",
919 "lib/ds-token/lib/ds-stop/lib/ds-auth/lib/ds-test/src/Contract.sol",
920 "lib/ds-token/lib/ds-stop/src/Contract.sol",
921 "lib/ds-token/src/Contract.sol",
922 "lib/ds-token/lib/erc20/src/Contract.sol",
923 "lib/ds-token/lib/ds-stop/lib/ds-auth/lib/ds-test/aux/Contract.sol",
924 "lib/ds-token/lib/ds-stop/lib/ds-auth/src/Contract.sol",
925 "lib/ds-token/lib/ds-stop/lib/ds-note/lib/ds-test/aux/Contract.sol",
926 ];
927 mkdir_or_touch(tmp_dir.path(), &paths[..]);
928
929 let tmp_dir_path = tmp_dir.path().join("lib");
930 let mut remappings = Remapping::find_many(&tmp_dir_path);
931 remappings.sort_unstable();
932 let mut expected = vec![
933 Remapping {
934 context: None,
935 name: "ds-auth/".to_string(),
936 path: to_str(tmp_dir_path.join("ds-token/lib/ds-stop/lib/ds-auth/src")),
937 },
938 Remapping {
939 context: None,
940 name: "ds-math/".to_string(),
941 path: to_str(tmp_dir_path.join("ds-token/lib/ds-math/src")),
942 },
943 Remapping {
944 context: None,
945 name: "ds-note/".to_string(),
946 path: to_str(tmp_dir_path.join("ds-token/lib/ds-stop/lib/ds-note/src")),
947 },
948 Remapping {
949 context: None,
950 name: "ds-stop/".to_string(),
951 path: to_str(tmp_dir_path.join("ds-token/lib/ds-stop/src")),
952 },
953 Remapping {
954 context: None,
955 name: "ds-test/".to_string(),
956 path: to_str(tmp_dir_path.join("ds-token/lib/ds-test/src")),
957 },
958 Remapping {
959 context: None,
960 name: "ds-token/".to_string(),
961 path: to_str(tmp_dir_path.join("ds-token/src")),
962 },
963 Remapping {
964 context: None,
965 name: "erc20/".to_string(),
966 path: to_str(tmp_dir_path.join("ds-token/lib/erc20/src")),
967 },
968 ];
969 expected.sort_unstable();
970 pretty_assertions::assert_eq!(remappings, expected);
971 }
972
973 #[test]
974 fn can_resolve_nested_chainlink_remappings() {
975 let tmp_dir = tempdir("root").unwrap();
976 let paths = [
977 "@chainlink/contracts/src/v0.6/vendor/Contract.sol",
978 "@chainlink/contracts/src/v0.8/tests/Contract.sol",
979 "@chainlink/contracts/src/v0.7/Contract.sol",
980 "@chainlink/contracts/src/v0.6/Contract.sol",
981 "@chainlink/contracts/src/v0.5/Contract.sol",
982 "@chainlink/contracts/src/v0.7/tests/Contract.sol",
983 "@chainlink/contracts/src/v0.7/interfaces/Contract.sol",
984 "@chainlink/contracts/src/v0.4/tests/Contract.sol",
985 "@chainlink/contracts/src/v0.6/tests/Contract.sol",
986 "@chainlink/contracts/src/v0.5/tests/Contract.sol",
987 "@chainlink/contracts/src/v0.8/vendor/Contract.sol",
988 "@chainlink/contracts/src/v0.5/dev/Contract.sol",
989 "@chainlink/contracts/src/v0.6/examples/Contract.sol",
990 "@chainlink/contracts/src/v0.5/interfaces/Contract.sol",
991 "@chainlink/contracts/src/v0.4/interfaces/Contract.sol",
992 "@chainlink/contracts/src/v0.4/vendor/Contract.sol",
993 "@chainlink/contracts/src/v0.6/interfaces/Contract.sol",
994 "@chainlink/contracts/src/v0.7/dev/Contract.sol",
995 "@chainlink/contracts/src/v0.8/dev/Contract.sol",
996 "@chainlink/contracts/src/v0.5/vendor/Contract.sol",
997 "@chainlink/contracts/src/v0.7/vendor/Contract.sol",
998 "@chainlink/contracts/src/v0.4/Contract.sol",
999 "@chainlink/contracts/src/v0.8/interfaces/Contract.sol",
1000 "@chainlink/contracts/src/v0.6/dev/Contract.sol",
1001 ];
1002 mkdir_or_touch(tmp_dir.path(), &paths[..]);
1003 let remappings = Remapping::find_many(tmp_dir.path());
1004
1005 let expected = vec![Remapping {
1006 context: None,
1007 name: "@chainlink/".to_string(),
1008 path: to_str(tmp_dir.path().join("@chainlink")),
1009 }];
1010 pretty_assertions::assert_eq!(remappings, expected);
1011 }
1012
1013 #[test]
1014 fn can_resolve_oz_upgradeable_remappings() {
1015 let tmp_dir = tempdir("root").unwrap();
1016 let paths = [
1017 "@openzeppelin/contracts-upgradeable/proxy/ERC1967/Contract.sol",
1018 "@openzeppelin/contracts-upgradeable/token/ERC1155/Contract.sol",
1019 "@openzeppelin/contracts/token/ERC777/Contract.sol",
1020 "@openzeppelin/contracts/token/ERC721/presets/Contract.sol",
1021 "@openzeppelin/contracts/interfaces/Contract.sol",
1022 "@openzeppelin/contracts-upgradeable/token/ERC777/presets/Contract.sol",
1023 "@openzeppelin/contracts/token/ERC1155/extensions/Contract.sol",
1024 "@openzeppelin/contracts/proxy/Contract.sol",
1025 "@openzeppelin/contracts/proxy/utils/Contract.sol",
1026 "@openzeppelin/contracts-upgradeable/security/Contract.sol",
1027 "@openzeppelin/contracts-upgradeable/utils/Contract.sol",
1028 "@openzeppelin/contracts/token/ERC20/Contract.sol",
1029 "@openzeppelin/contracts-upgradeable/utils/introspection/Contract.sol",
1030 "@openzeppelin/contracts/metatx/Contract.sol",
1031 "@openzeppelin/contracts/utils/cryptography/Contract.sol",
1032 "@openzeppelin/contracts/token/ERC20/utils/Contract.sol",
1033 "@openzeppelin/contracts-upgradeable/token/ERC20/utils/Contract.sol",
1034 "@openzeppelin/contracts-upgradeable/proxy/Contract.sol",
1035 "@openzeppelin/contracts-upgradeable/token/ERC20/presets/Contract.sol",
1036 "@openzeppelin/contracts-upgradeable/utils/math/Contract.sol",
1037 "@openzeppelin/contracts-upgradeable/utils/escrow/Contract.sol",
1038 "@openzeppelin/contracts/governance/extensions/Contract.sol",
1039 "@openzeppelin/contracts-upgradeable/interfaces/Contract.sol",
1040 "@openzeppelin/contracts/proxy/transparent/Contract.sol",
1041 "@openzeppelin/contracts/utils/structs/Contract.sol",
1042 "@openzeppelin/contracts-upgradeable/access/Contract.sol",
1043 "@openzeppelin/contracts/governance/compatibility/Contract.sol",
1044 "@openzeppelin/contracts/governance/Contract.sol",
1045 "@openzeppelin/contracts-upgradeable/governance/extensions/Contract.sol",
1046 "@openzeppelin/contracts/security/Contract.sol",
1047 "@openzeppelin/contracts-upgradeable/metatx/Contract.sol",
1048 "@openzeppelin/contracts-upgradeable/token/ERC721/utils/Contract.sol",
1049 "@openzeppelin/contracts/token/ERC721/utils/Contract.sol",
1050 "@openzeppelin/contracts-upgradeable/governance/compatibility/Contract.sol",
1051 "@openzeppelin/contracts/token/common/Contract.sol",
1052 "@openzeppelin/contracts/proxy/beacon/Contract.sol",
1053 "@openzeppelin/contracts-upgradeable/token/ERC721/Contract.sol",
1054 "@openzeppelin/contracts-upgradeable/proxy/beacon/Contract.sol",
1055 "@openzeppelin/contracts/token/ERC1155/utils/Contract.sol",
1056 "@openzeppelin/contracts/token/ERC777/presets/Contract.sol",
1057 "@openzeppelin/contracts-upgradeable/token/ERC20/Contract.sol",
1058 "@openzeppelin/contracts-upgradeable/utils/structs/Contract.sol",
1059 "@openzeppelin/contracts/utils/escrow/Contract.sol",
1060 "@openzeppelin/contracts/utils/Contract.sol",
1061 "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/Contract.sol",
1062 "@openzeppelin/contracts/token/ERC721/extensions/Contract.sol",
1063 "@openzeppelin/contracts-upgradeable/token/ERC777/Contract.sol",
1064 "@openzeppelin/contracts/token/ERC1155/presets/Contract.sol",
1065 "@openzeppelin/contracts/token/ERC721/Contract.sol",
1066 "@openzeppelin/contracts/token/ERC1155/Contract.sol",
1067 "@openzeppelin/contracts-upgradeable/governance/Contract.sol",
1068 "@openzeppelin/contracts/token/ERC20/extensions/Contract.sol",
1069 "@openzeppelin/contracts-upgradeable/utils/cryptography/Contract.sol",
1070 "@openzeppelin/contracts-upgradeable/token/ERC1155/presets/Contract.sol",
1071 "@openzeppelin/contracts/access/Contract.sol",
1072 "@openzeppelin/contracts/governance/utils/Contract.sol",
1073 "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/Contract.sol",
1074 "@openzeppelin/contracts-upgradeable/token/common/Contract.sol",
1075 "@openzeppelin/contracts-upgradeable/token/ERC1155/utils/Contract.sol",
1076 "@openzeppelin/contracts/proxy/ERC1967/Contract.sol",
1077 "@openzeppelin/contracts/finance/Contract.sol",
1078 "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/Contract.sol",
1079 "@openzeppelin/contracts-upgradeable/governance/utils/Contract.sol",
1080 "@openzeppelin/contracts-upgradeable/proxy/utils/Contract.sol",
1081 "@openzeppelin/contracts/token/ERC20/presets/Contract.sol",
1082 "@openzeppelin/contracts/utils/math/Contract.sol",
1083 "@openzeppelin/contracts-upgradeable/token/ERC721/presets/Contract.sol",
1084 "@openzeppelin/contracts-upgradeable/finance/Contract.sol",
1085 "@openzeppelin/contracts/utils/introspection/Contract.sol",
1086 ];
1087 mkdir_or_touch(tmp_dir.path(), &paths[..]);
1088 let remappings = Remapping::find_many(tmp_dir.path());
1089
1090 let expected = vec![Remapping {
1091 context: None,
1092 name: "@openzeppelin/".to_string(),
1093 path: to_str(tmp_dir.path().join("@openzeppelin")),
1094 }];
1095 pretty_assertions::assert_eq!(remappings, expected);
1096 }
1097
1098 #[test]
1099 fn can_resolve_oz_remappings() {
1100 let tmp_dir = tempdir("node_modules").unwrap();
1101 let tmp_dir_node_modules = tmp_dir.path().join("node_modules");
1102 let paths = [
1103 "node_modules/@openzeppelin/contracts/interfaces/IERC1155.sol",
1104 "node_modules/@openzeppelin/contracts/finance/VestingWallet.sol",
1105 "node_modules/@openzeppelin/contracts/proxy/Proxy.sol",
1106 "node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol",
1107 ];
1108 mkdir_or_touch(tmp_dir.path(), &paths[..]);
1109 let remappings = Remapping::find_many(tmp_dir_node_modules);
1110 let mut paths = ProjectPathsConfig::hardhat(tmp_dir.path()).unwrap();
1111 paths.remappings = remappings;
1112
1113 let resolved = paths
1114 .resolve_library_import(
1115 tmp_dir.path(),
1116 Path::new("@openzeppelin/contracts/token/ERC20/IERC20.sol"),
1117 )
1118 .unwrap();
1119 assert!(resolved.exists());
1120
1121 paths.remappings[0].name = "@openzeppelin/".to_string();
1123
1124 let resolved = paths
1125 .resolve_library_import(
1126 tmp_dir.path(),
1127 Path::new("@openzeppelin/contracts/token/ERC20/IERC20.sol"),
1128 )
1129 .unwrap();
1130 assert!(resolved.exists());
1131 }
1132
1133 #[test]
1134 #[cfg_attr(windows, ignore = "Windows remappings #2347")]
1135 fn recursive_remappings() {
1136 let tmp_dir = tempdir("lib").unwrap();
1137 let tmp_dir_path = tmp_dir.path();
1138 let paths = [
1139 "repo1/src/contract.sol",
1140 "repo1/lib/ds-test/src/test.sol",
1141 "repo1/lib/ds-math/src/contract.sol",
1142 "repo1/lib/ds-math/lib/ds-test/src/test.sol",
1143 "repo1/lib/guni-lev/src/contract.sol",
1144 "repo1/lib/solmate/src/auth/contract.sol",
1145 "repo1/lib/solmate/src/tokens/contract.sol",
1146 "repo1/lib/solmate/lib/ds-test/src/test.sol",
1147 "repo1/lib/solmate/lib/ds-test/demo/demo.sol",
1148 "repo1/lib/openzeppelin-contracts/contracts/access/AccessControl.sol",
1149 "repo1/lib/ds-token/lib/ds-stop/src/contract.sol",
1150 "repo1/lib/ds-token/lib/ds-stop/lib/ds-note/src/contract.sol",
1151 ];
1152 mkdir_or_touch(tmp_dir_path, &paths[..]);
1153
1154 let path = tmp_dir_path.display().to_string();
1155 let mut remappings = Remapping::find_many(path);
1156 remappings.sort_unstable();
1157
1158 let mut expected = vec![
1159 Remapping {
1160 context: None,
1161 name: "repo1/".to_string(),
1162 path: to_str(tmp_dir_path.join("repo1").join("src")),
1163 },
1164 Remapping {
1165 context: None,
1166 name: "ds-math/".to_string(),
1167 path: to_str(tmp_dir_path.join("repo1").join("lib").join("ds-math").join("src")),
1168 },
1169 Remapping {
1170 context: None,
1171 name: "ds-test/".to_string(),
1172 path: to_str(tmp_dir_path.join("repo1").join("lib").join("ds-test").join("src")),
1173 },
1174 Remapping {
1175 context: None,
1176 name: "guni-lev/".to_string(),
1177 path: to_str(tmp_dir_path.join("repo1/lib/guni-lev").join("src")),
1178 },
1179 Remapping {
1180 context: None,
1181 name: "solmate/".to_string(),
1182 path: to_str(tmp_dir_path.join("repo1/lib/solmate").join("src")),
1183 },
1184 Remapping {
1185 context: None,
1186 name: "openzeppelin-contracts/".to_string(),
1187 path: to_str(tmp_dir_path.join("repo1/lib/openzeppelin-contracts/contracts")),
1188 },
1189 Remapping {
1190 context: None,
1191 name: "ds-stop/".to_string(),
1192 path: to_str(tmp_dir_path.join("repo1/lib/ds-token/lib/ds-stop/src")),
1193 },
1194 Remapping {
1195 context: None,
1196 name: "ds-note/".to_string(),
1197 path: to_str(tmp_dir_path.join("repo1/lib/ds-token/lib/ds-stop/lib/ds-note/src")),
1198 },
1199 ];
1200 expected.sort_unstable();
1201 pretty_assertions::assert_eq!(remappings, expected);
1202 }
1203
1204 #[test]
1205 fn can_resolve_contexts() {
1206 let remapping = "context:oz=a/b/c/d";
1207 let remapping = Remapping::from_str(remapping).unwrap();
1208
1209 assert_eq!(
1210 remapping,
1211 Remapping {
1212 context: Some("context".to_string()),
1213 name: "oz".to_string(),
1214 path: "a/b/c/d".to_string(),
1215 }
1216 );
1217 assert_eq!(remapping.to_string(), "context:oz=a/b/c/d/".to_string());
1218
1219 let remapping = "context:foo=C:/bar/src/";
1220 let remapping = Remapping::from_str(remapping).unwrap();
1221
1222 assert_eq!(
1223 remapping,
1224 Remapping {
1225 context: Some("context".to_string()),
1226 name: "foo".to_string(),
1227 path: "C:/bar/src/".to_string()
1228 }
1229 );
1230 }
1231
1232 #[test]
1233 fn can_resolve_global_contexts() {
1234 let remapping = ":oz=a/b/c/d/";
1235 let remapping = Remapping::from_str(remapping).unwrap();
1236
1237 assert_eq!(
1238 remapping,
1239 Remapping { context: None, name: "oz".to_string(), path: "a/b/c/d/".to_string() }
1240 );
1241 assert_eq!(remapping.to_string(), "oz=a/b/c/d/".to_string());
1242 }
1243
1244 #[test]
1245 fn remappings() {
1246 let tmp_dir = tempdir("tmp").unwrap();
1247 let tmp_dir_path = tmp_dir.path().join("lib");
1248 let repo1 = tmp_dir_path.join("src_repo");
1249 let repo2 = tmp_dir_path.join("contracts_repo");
1250
1251 let dir1 = repo1.join("src");
1252 std::fs::create_dir_all(&dir1).unwrap();
1253
1254 let dir2 = repo2.join("contracts");
1255 std::fs::create_dir_all(&dir2).unwrap();
1256
1257 let contract1 = dir1.join("contract.sol");
1258 touch(&contract1).unwrap();
1259
1260 let contract2 = dir2.join("contract.sol");
1261 touch(&contract2).unwrap();
1262
1263 let path = tmp_dir_path.display().to_string();
1264 let mut remappings = Remapping::find_many(path);
1265 remappings.sort_unstable();
1266 let mut expected = vec![
1267 Remapping {
1268 context: None,
1269 name: "src_repo/".to_string(),
1270 path: format!("{}/", dir1.into_os_string().into_string().unwrap()),
1271 },
1272 Remapping {
1273 context: None,
1274 name: "contracts_repo/".to_string(),
1275 path: format!(
1276 "{}/",
1277 repo2.join("contracts").into_os_string().into_string().unwrap()
1278 ),
1279 },
1280 ];
1281 expected.sort_unstable();
1282 pretty_assertions::assert_eq!(remappings, expected);
1283 }
1284
1285 #[test]
1286 #[cfg_attr(windows, ignore = "Windows remappings #2347")]
1287 fn simple_dapptools_remappings() {
1288 let tmp_dir = tempdir("lib").unwrap();
1289 let tmp_dir_path = tmp_dir.path();
1290 let paths = [
1291 "ds-test/src",
1292 "ds-test/demo",
1293 "ds-test/demo/demo.sol",
1294 "ds-test/src/test.sol",
1295 "openzeppelin/src/interfaces/c.sol",
1296 "openzeppelin/src/token/ERC/c.sol",
1297 "standards/src/interfaces/iweth.sol",
1298 "uniswapv2/src",
1299 ];
1300 mkdir_or_touch(tmp_dir_path, &paths[..]);
1301
1302 let path = tmp_dir_path.display().to_string();
1303 let mut remappings = Remapping::find_many(path);
1304 remappings.sort_unstable();
1305
1306 let mut expected = vec![
1307 Remapping {
1308 context: None,
1309 name: "ds-test/".to_string(),
1310 path: to_str(tmp_dir_path.join("ds-test/src")),
1311 },
1312 Remapping {
1313 context: None,
1314 name: "openzeppelin/".to_string(),
1315 path: to_str(tmp_dir_path.join("openzeppelin/src")),
1316 },
1317 Remapping {
1318 context: None,
1319 name: "standards/".to_string(),
1320 path: to_str(tmp_dir_path.join("standards/src")),
1321 },
1322 ];
1323 expected.sort_unstable();
1324 pretty_assertions::assert_eq!(remappings, expected);
1325 }
1326
1327 #[test]
1328 #[cfg_attr(windows, ignore = "Windows remappings #2347")]
1329 fn hardhat_remappings() {
1330 let tmp_dir = tempdir("node_modules").unwrap();
1331 let tmp_dir_node_modules = tmp_dir.path().join("node_modules");
1332 let paths = [
1333 "node_modules/@aave/aave-token/contracts/token/AaveToken.sol",
1334 "node_modules/@aave/governance-v2/contracts/governance/Executor.sol",
1335 "node_modules/@aave/protocol-v2/contracts/protocol/lendingpool/",
1336 "node_modules/@aave/protocol-v2/contracts/protocol/lendingpool/LendingPool.sol",
1337 "node_modules/@ensdomains/ens/contracts/contract.sol",
1338 "node_modules/prettier-plugin-solidity/tests/format/ModifierDefinitions/",
1339 "node_modules/prettier-plugin-solidity/tests/format/ModifierDefinitions/
1340 ModifierDefinitions.sol",
1341 "node_modules/@openzeppelin/contracts/tokens/contract.sol",
1342 "node_modules/@openzeppelin/contracts/access/contract.sol",
1343 "node_modules/eth-gas-reporter/mock/contracts/ConvertLib.sol",
1344 "node_modules/eth-gas-reporter/mock/test/TestMetacoin.sol",
1345 ];
1346 mkdir_or_touch(tmp_dir.path(), &paths[..]);
1347 let mut remappings = Remapping::find_many(&tmp_dir_node_modules);
1348 remappings.sort_unstable();
1349 let mut expected = vec![
1350 Remapping {
1351 context: None,
1352 name: "@aave/".to_string(),
1353 path: to_str(tmp_dir_node_modules.join("@aave")),
1354 },
1355 Remapping {
1356 context: None,
1357 name: "@ensdomains/".to_string(),
1358 path: to_str(tmp_dir_node_modules.join("@ensdomains")),
1359 },
1360 Remapping {
1361 context: None,
1362 name: "@openzeppelin/".to_string(),
1363 path: to_str(tmp_dir_node_modules.join("@openzeppelin")),
1364 },
1365 Remapping {
1366 context: None,
1367 name: "eth-gas-reporter/".to_string(),
1368 path: to_str(tmp_dir_node_modules.join("eth-gas-reporter")),
1369 },
1370 ];
1371 expected.sort_unstable();
1372 pretty_assertions::assert_eq!(remappings, expected);
1373 }
1374
1375 #[test]
1376 fn can_determine_nested_window() {
1377 let a = Path::new(
1378 "/var/folders/l5/lprhf87s6xv8djgd017f0b2h0000gn/T/lib.Z6ODLZJQeJQa/repo1/lib",
1379 );
1380 let b = Path::new(
1381 "/var/folders/l5/lprhf87s6xv8djgd017f0b2h0000gn/T/lib.Z6ODLZJQeJQa/repo1/lib/ds-test/src"
1382 );
1383 assert_eq!(next_nested_window(a, b),Path::new(
1384 "/var/folders/l5/lprhf87s6xv8djgd017f0b2h0000gn/T/lib.Z6ODLZJQeJQa/repo1/lib/ds-test"
1385 ));
1386 }
1387
1388 #[test]
1389 #[cfg_attr(windows, ignore = "Windows remappings #2347")]
1390 fn find_openzeppelin_remapping() {
1391 let tmp_dir = tempdir("lib").unwrap();
1392 let tmp_dir_path = tmp_dir.path();
1393 let paths = [
1394 "lib/ds-test/src/test.sol",
1395 "lib/forge-std/src/test.sol",
1396 "openzeppelin/contracts/interfaces/c.sol",
1397 ];
1398 mkdir_or_touch(tmp_dir_path, &paths[..]);
1399
1400 let path = tmp_dir_path.display().to_string();
1401 let mut remappings = Remapping::find_many(path);
1402 remappings.sort_unstable();
1403
1404 let mut expected = vec![
1405 Remapping {
1406 context: None,
1407 name: "ds-test/".to_string(),
1408 path: to_str(tmp_dir_path.join("lib/ds-test/src")),
1409 },
1410 Remapping {
1411 context: None,
1412 name: "openzeppelin/".to_string(),
1413 path: to_str(tmp_dir_path.join("openzeppelin/contracts")),
1414 },
1415 Remapping {
1416 context: None,
1417 name: "forge-std/".to_string(),
1418 path: to_str(tmp_dir_path.join("lib/forge-std/src")),
1419 },
1420 ];
1421 expected.sort_unstable();
1422 pretty_assertions::assert_eq!(remappings, expected);
1423 }
1424}