1#![doc = include_str!("../README.md")]
2#![cfg_attr(not(test), warn(unused_crate_dependencies))]
3#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
4
5#[macro_use]
6extern crate tracing;
7
8#[cfg(feature = "project-util")]
9#[macro_use]
10extern crate foundry_compilers_core;
11
12mod artifact_output;
13pub use artifact_output::*;
14
15pub mod buildinfo;
16
17pub mod cache;
18
19pub mod flatten;
20
21pub mod resolver;
22pub use resolver::Graph;
23
24pub mod compilers;
25pub use compilers::*;
26
27mod compile;
28pub use compile::{
29 output::{AggregatedCompilerOutput, ProjectCompileOutput},
30 *,
31};
32
33mod config;
34pub use config::{PathStyle, ProjectPaths, ProjectPathsConfig, SolcConfig};
35
36mod filter;
37pub use filter::{FileFilter, SparseOutputFilter, TestFileFilter};
38
39pub mod report;
40
41pub type Updates = HashMap<PathBuf, BTreeSet<(usize, usize, String)>>;
45
46#[cfg(feature = "project-util")]
48pub mod project_util;
49
50pub use foundry_compilers_artifacts as artifacts;
51pub use foundry_compilers_core::{error, utils};
52
53use cache::CompilerCache;
54use compile::output::contracts::VersionedContracts;
55use compilers::multi::MultiCompiler;
56use foundry_compilers_artifacts::{
57 output_selection::OutputSelection,
58 solc::{
59 sources::{Source, SourceCompilationKind, Sources},
60 Severity, SourceFile, StandardJsonCompilerInput,
61 },
62};
63use foundry_compilers_core::error::{Result, SolcError, SolcIoError};
64use output::sources::{VersionedSourceFile, VersionedSourceFiles};
65use project::ProjectCompiler;
66use semver::Version;
67use solar_parse::{
68 interface::{diagnostics::EmittedDiagnostics, source_map::FileName, Session},
69 Parser,
70};
71use solc::SolcSettings;
72use std::{
73 collections::{BTreeMap, BTreeSet, HashMap, HashSet},
74 ops::Range,
75 path::{Path, PathBuf},
76 sync::Arc,
77};
78
79#[derive(Clone, derive_more::Debug)]
81pub struct Project<
82 C: Compiler = MultiCompiler,
83 T: ArtifactOutput<CompilerContract = C::CompilerContract> = ConfigurableArtifacts,
84> {
85 pub compiler: C,
86 pub paths: ProjectPathsConfig<C::Language>,
88 pub settings: C::Settings,
90 pub additional_settings: BTreeMap<String, C::Settings>,
93 pub restrictions:
98 BTreeMap<PathBuf, RestrictionsWithVersion<<C::Settings as CompilerSettings>::Restrictions>>,
99 pub cached: bool,
101 pub build_info: bool,
103 pub no_artifacts: bool,
105 pub artifacts: T,
107 pub ignored_error_codes: Vec<u64>,
109 pub ignored_file_paths: Vec<PathBuf>,
111 pub compiler_severity_filter: Severity,
113 solc_jobs: usize,
115 pub offline: bool,
117 pub slash_paths: bool,
121 #[debug(skip)]
123 pub sparse_output: Option<Box<dyn FileFilter>>,
124}
125
126impl Project {
127 pub fn builder() -> ProjectBuilder {
155 ProjectBuilder::default()
156 }
157}
158
159impl<T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler> Project<C, T> {
160 pub fn artifacts_handler(&self) -> &T {
162 &self.artifacts
163 }
164
165 pub fn settings_profiles(&self) -> impl Iterator<Item = (&str, &C::Settings)> {
166 std::iter::once(("default", &self.settings))
167 .chain(self.additional_settings.iter().map(|(p, s)| (p.as_str(), s)))
168 }
169}
170
171impl<C: Compiler, T: ArtifactOutput<CompilerContract = C::CompilerContract>> Project<C, T>
172where
173 C::Settings: Into<SolcSettings>,
174{
175 pub fn standard_json_input(&self, target: &Path) -> Result<StandardJsonCompilerInput> {
177 trace!(?target, "Building standard-json-input");
178 let graph = Graph::<C::Parser>::resolve(&self.paths)?;
179 let target_index = graph.files().get(target).ok_or_else(|| {
180 SolcError::msg(format!("cannot resolve file at {:?}", target.display()))
181 })?;
182
183 let mut sources = Vec::new();
184 let mut unique_paths = HashSet::new();
185 let (path, source) = graph.node(*target_index).unpack();
186 unique_paths.insert(path);
187 sources.push((path, source));
188 sources.extend(
189 graph
190 .all_imported_nodes(*target_index)
191 .map(|index| graph.node(index).unpack())
192 .filter(|(p, _)| unique_paths.insert(*p)),
193 );
194
195 let root = self.root();
196 let sources = sources
197 .into_iter()
198 .map(|(path, source)| (rebase_path(root, path), source.clone()))
199 .collect();
200
201 let mut settings = self.settings.clone().into();
202 settings.remappings = self
204 .paths
205 .remappings
206 .clone()
207 .into_iter()
208 .map(|r| r.into_relative(self.root()).to_relative_remapping())
209 .collect::<Vec<_>>();
210
211 let input = StandardJsonCompilerInput::new(sources, settings.settings);
212
213 Ok(input)
214 }
215}
216
217impl<T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler> Project<C, T> {
218 pub fn artifacts_path(&self) -> &Path {
220 &self.paths.artifacts
221 }
222
223 pub fn sources_path(&self) -> &Path {
225 &self.paths.sources
226 }
227
228 pub fn cache_path(&self) -> &Path {
230 &self.paths.cache
231 }
232
233 pub fn build_info_path(&self) -> &Path {
235 &self.paths.build_infos
236 }
237
238 pub fn root(&self) -> &Path {
240 &self.paths.root
241 }
242
243 pub fn read_cache_file(&self) -> Result<CompilerCache<C::Settings>> {
246 CompilerCache::read_joined(&self.paths)
247 }
248
249 pub fn set_solc_jobs(&mut self, jobs: usize) {
255 assert!(jobs > 0);
256 self.solc_jobs = jobs;
257 }
258
259 #[instrument(skip_all, fields(name = "sources"))]
261 pub fn sources(&self) -> Result<Sources> {
262 self.paths.read_sources()
263 }
264
265 pub fn rerun_if_sources_changed(&self) {
287 println!("cargo:rerun-if-changed={}", self.paths.sources.display())
288 }
289
290 pub fn compile(&self) -> Result<ProjectCompileOutput<C, T>> {
291 project::ProjectCompiler::new(self)?.compile()
292 }
293
294 pub fn compile_file(&self, file: impl Into<PathBuf>) -> Result<ProjectCompileOutput<C, T>> {
305 let file = file.into();
306 let source = Source::read(&file)?;
307 project::ProjectCompiler::with_sources(self, Sources::from([(file, source)]))?.compile()
308 }
309
310 pub fn compile_files<P, I>(&self, files: I) -> Result<ProjectCompileOutput<C, T>>
322 where
323 I: IntoIterator<Item = P>,
324 P: Into<PathBuf>,
325 {
326 let sources = Source::read_all(files)?;
327
328 ProjectCompiler::with_sources(self, sources)?.compile()
329 }
330
331 pub fn cleanup(&self) -> std::result::Result<(), SolcIoError> {
350 trace!("clean up project");
351 if self.cache_path().exists() {
352 std::fs::remove_file(self.cache_path())
353 .map_err(|err| SolcIoError::new(err, self.cache_path()))?;
354 if let Some(cache_folder) =
355 self.cache_path().parent().filter(|cache_folder| self.root() != *cache_folder)
356 {
357 if cache_folder
359 .read_dir()
360 .map_err(|err| SolcIoError::new(err, cache_folder))?
361 .next()
362 .is_none()
363 {
364 std::fs::remove_dir(cache_folder)
365 .map_err(|err| SolcIoError::new(err, cache_folder))?;
366 }
367 }
368 trace!("removed cache file \"{}\"", self.cache_path().display());
369 }
370
371 if self.artifacts_path().exists() && self.root() != self.artifacts_path() {
373 std::fs::remove_dir_all(self.artifacts_path())
374 .map_err(|err| SolcIoError::new(err, self.artifacts_path()))?;
375 trace!("removed artifacts dir \"{}\"", self.artifacts_path().display());
376 }
377
378 if self.build_info_path().exists() && self.root() != self.build_info_path() {
380 std::fs::remove_dir_all(self.build_info_path())
381 .map_err(|err| SolcIoError::new(err, self.build_info_path()))?;
382 tracing::trace!("removed build-info dir \"{}\"", self.build_info_path().display());
383 }
384
385 Ok(())
386 }
387
388 fn collect_contract_names(&self) -> Result<HashMap<String, Vec<PathBuf>>>
390 where
391 T: Clone,
392 C: Clone,
393 {
394 let graph = Graph::<C::Parser>::resolve(&self.paths)?;
395 let mut contracts: HashMap<String, Vec<PathBuf>> = HashMap::new();
396 if !graph.is_empty() {
397 for node in &graph.nodes {
398 for contract_name in node.data.contract_names() {
399 contracts
400 .entry(contract_name.clone())
401 .or_default()
402 .push(node.path().to_path_buf());
403 }
404 }
405 }
406 Ok(contracts)
407 }
408
409 pub fn find_contract_path(&self, target_name: &str) -> Result<PathBuf>
412 where
413 T: Clone,
414 C: Clone,
415 {
416 let mut contracts = self.collect_contract_names()?;
417
418 if contracts.get(target_name).is_none_or(|paths| paths.is_empty()) {
419 return Err(SolcError::msg(format!("No contract found with the name `{target_name}`")));
420 }
421 let mut paths = contracts.remove(target_name).unwrap();
422 if paths.len() > 1 {
423 return Err(SolcError::msg(format!(
424 "Multiple contracts found with the name `{target_name}`"
425 )));
426 }
427
428 Ok(paths.remove(0))
429 }
430
431 pub fn update_output_selection(&mut self, f: impl FnOnce(&mut OutputSelection) + Copy) {
434 self.settings.update_output_selection(f);
435 self.additional_settings.iter_mut().for_each(|(_, s)| {
436 s.update_output_selection(f);
437 });
438 }
439}
440
441pub struct ProjectBuilder<
442 C: Compiler = MultiCompiler,
443 T: ArtifactOutput<CompilerContract = C::CompilerContract> = ConfigurableArtifacts,
444> {
445 paths: Option<ProjectPathsConfig<C::Language>>,
447 settings: Option<C::Settings>,
449 additional_settings: BTreeMap<String, C::Settings>,
450 restrictions:
451 BTreeMap<PathBuf, RestrictionsWithVersion<<C::Settings as CompilerSettings>::Restrictions>>,
452 cached: bool,
454 build_info: bool,
456 no_artifacts: bool,
458 offline: bool,
460 slash_paths: bool,
462 artifacts: T,
464 pub ignored_error_codes: Vec<u64>,
466 pub ignored_file_paths: Vec<PathBuf>,
468 compiler_severity_filter: Severity,
470 solc_jobs: Option<usize>,
471 sparse_output: Option<Box<dyn FileFilter>>,
473}
474
475impl<C: Compiler, T: ArtifactOutput<CompilerContract = C::CompilerContract>> ProjectBuilder<C, T> {
476 pub fn new(artifacts: T) -> Self {
478 Self {
479 paths: None,
480 cached: true,
481 build_info: false,
482 no_artifacts: false,
483 offline: false,
484 slash_paths: true,
485 artifacts,
486 ignored_error_codes: Vec::new(),
487 ignored_file_paths: Vec::new(),
488 compiler_severity_filter: Severity::Error,
489 solc_jobs: None,
490 settings: None,
491 sparse_output: None,
492 additional_settings: BTreeMap::new(),
493 restrictions: BTreeMap::new(),
494 }
495 }
496
497 #[must_use]
498 pub fn paths(mut self, paths: ProjectPathsConfig<C::Language>) -> Self {
499 self.paths = Some(paths);
500 self
501 }
502
503 #[must_use]
504 pub fn settings(mut self, settings: C::Settings) -> Self {
505 self.settings = Some(settings);
506 self
507 }
508
509 #[must_use]
510 pub fn ignore_error_code(mut self, code: u64) -> Self {
511 self.ignored_error_codes.push(code);
512 self
513 }
514
515 #[must_use]
516 pub fn ignore_error_codes(mut self, codes: impl IntoIterator<Item = u64>) -> Self {
517 for code in codes {
518 self = self.ignore_error_code(code);
519 }
520 self
521 }
522
523 pub fn ignore_paths(mut self, paths: Vec<PathBuf>) -> Self {
524 self.ignored_file_paths = paths;
525 self
526 }
527
528 #[must_use]
529 pub fn set_compiler_severity_filter(mut self, compiler_severity_filter: Severity) -> Self {
530 self.compiler_severity_filter = compiler_severity_filter;
531 self
532 }
533
534 #[must_use]
536 pub fn ephemeral(self) -> Self {
537 self.set_cached(false)
538 }
539
540 #[must_use]
542 pub fn set_cached(mut self, cached: bool) -> Self {
543 self.cached = cached;
544 self
545 }
546
547 #[must_use]
549 pub fn set_build_info(mut self, build_info: bool) -> Self {
550 self.build_info = build_info;
551 self
552 }
553
554 #[must_use]
558 pub fn offline(self) -> Self {
559 self.set_offline(true)
560 }
561
562 #[must_use]
564 pub fn set_offline(mut self, offline: bool) -> Self {
565 self.offline = offline;
566 self
567 }
568
569 #[must_use]
573 pub fn set_slashed_paths(mut self, slashed_paths: bool) -> Self {
574 self.slash_paths = slashed_paths;
575 self
576 }
577
578 #[must_use]
580 pub fn no_artifacts(self) -> Self {
581 self.set_no_artifacts(true)
582 }
583
584 #[must_use]
586 pub fn set_no_artifacts(mut self, artifacts: bool) -> Self {
587 self.no_artifacts = artifacts;
588 self
589 }
590
591 #[must_use]
597 pub fn solc_jobs(mut self, jobs: usize) -> Self {
598 assert!(jobs > 0);
599 self.solc_jobs = Some(jobs);
600 self
601 }
602
603 #[must_use]
605 pub fn single_solc_jobs(self) -> Self {
606 self.solc_jobs(1)
607 }
608
609 #[must_use]
610 pub fn sparse_output<F>(mut self, filter: F) -> Self
611 where
612 F: FileFilter + 'static,
613 {
614 self.sparse_output = Some(Box::new(filter));
615 self
616 }
617
618 #[must_use]
619 pub fn additional_settings(mut self, additional: BTreeMap<String, C::Settings>) -> Self {
620 self.additional_settings = additional;
621 self
622 }
623
624 #[must_use]
625 pub fn restrictions(
626 mut self,
627 restrictions: BTreeMap<
628 PathBuf,
629 RestrictionsWithVersion<<C::Settings as CompilerSettings>::Restrictions>,
630 >,
631 ) -> Self {
632 self.restrictions = restrictions;
633 self
634 }
635
636 pub fn artifacts<A: ArtifactOutput<CompilerContract = C::CompilerContract>>(
638 self,
639 artifacts: A,
640 ) -> ProjectBuilder<C, A> {
641 let Self {
642 paths,
643 cached,
644 no_artifacts,
645 ignored_error_codes,
646 compiler_severity_filter,
647 solc_jobs,
648 offline,
649 build_info,
650 slash_paths,
651 ignored_file_paths,
652 settings,
653 sparse_output,
654 additional_settings,
655 restrictions,
656 ..
657 } = self;
658 ProjectBuilder {
659 paths,
660 cached,
661 no_artifacts,
662 additional_settings,
663 restrictions,
664 offline,
665 slash_paths,
666 artifacts,
667 ignored_error_codes,
668 ignored_file_paths,
669 compiler_severity_filter,
670 solc_jobs,
671 build_info,
672 settings,
673 sparse_output,
674 }
675 }
676
677 pub fn build(self, compiler: C) -> Result<Project<C, T>> {
678 let Self {
679 paths,
680 cached,
681 no_artifacts,
682 artifacts,
683 ignored_error_codes,
684 ignored_file_paths,
685 compiler_severity_filter,
686 solc_jobs,
687 offline,
688 build_info,
689 slash_paths,
690 settings,
691 sparse_output,
692 additional_settings,
693 restrictions,
694 } = self;
695
696 let mut paths = paths.map(Ok).unwrap_or_else(ProjectPathsConfig::current_hardhat)?;
697
698 if slash_paths {
699 paths.slash_paths();
701 }
702
703 Ok(Project {
704 compiler,
705 paths,
706 cached,
707 build_info,
708 no_artifacts,
709 artifacts,
710 ignored_error_codes,
711 ignored_file_paths,
712 compiler_severity_filter,
713 solc_jobs: solc_jobs
714 .or_else(|| std::thread::available_parallelism().ok().map(|n| n.get()))
715 .unwrap_or(1),
716 offline,
717 slash_paths,
718 settings: settings.unwrap_or_default(),
719 sparse_output,
720 additional_settings,
721 restrictions,
722 })
723 }
724}
725
726impl<C: Compiler, T: ArtifactOutput<CompilerContract = C::CompilerContract> + Default> Default
727 for ProjectBuilder<C, T>
728{
729 fn default() -> Self {
730 Self::new(T::default())
731 }
732}
733
734impl<T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler> ArtifactOutput
735 for Project<C, T>
736{
737 type Artifact = T::Artifact;
738 type CompilerContract = C::CompilerContract;
739
740 fn on_output<CP>(
741 &self,
742 contracts: &VersionedContracts<C::CompilerContract>,
743 sources: &VersionedSourceFiles,
744 layout: &ProjectPathsConfig<CP>,
745 ctx: OutputContext<'_>,
746 primary_profiles: &HashMap<PathBuf, &str>,
747 ) -> Result<Artifacts<Self::Artifact>> {
748 self.artifacts_handler().on_output(contracts, sources, layout, ctx, primary_profiles)
749 }
750
751 fn handle_artifacts(
752 &self,
753 contracts: &VersionedContracts<C::CompilerContract>,
754 artifacts: &Artifacts<Self::Artifact>,
755 ) -> Result<()> {
756 self.artifacts_handler().handle_artifacts(contracts, artifacts)
757 }
758
759 fn output_file_name(
760 name: &str,
761 version: &Version,
762 profile: &str,
763 with_version: bool,
764 with_profile: bool,
765 ) -> PathBuf {
766 T::output_file_name(name, version, profile, with_version, with_profile)
767 }
768
769 fn output_file(
770 contract_file: &Path,
771 name: &str,
772 version: &Version,
773 profile: &str,
774 with_version: bool,
775 with_profile: bool,
776 ) -> PathBuf {
777 T::output_file(contract_file, name, version, profile, with_version, with_profile)
778 }
779
780 fn contract_name(file: &Path) -> Option<String> {
781 T::contract_name(file)
782 }
783
784 fn read_cached_artifact(path: &Path) -> Result<Self::Artifact> {
785 T::read_cached_artifact(path)
786 }
787
788 fn read_cached_artifacts<P, I>(files: I) -> Result<BTreeMap<PathBuf, Self::Artifact>>
789 where
790 I: IntoIterator<Item = P>,
791 P: Into<PathBuf>,
792 {
793 T::read_cached_artifacts(files)
794 }
795
796 fn contract_to_artifact(
797 &self,
798 file: &Path,
799 name: &str,
800 contract: C::CompilerContract,
801 source_file: Option<&SourceFile>,
802 ) -> Self::Artifact {
803 self.artifacts_handler().contract_to_artifact(file, name, contract, source_file)
804 }
805
806 fn output_to_artifacts<CP>(
807 &self,
808 contracts: &VersionedContracts<C::CompilerContract>,
809 sources: &VersionedSourceFiles,
810 ctx: OutputContext<'_>,
811 layout: &ProjectPathsConfig<CP>,
812 primary_profiles: &HashMap<PathBuf, &str>,
813 ) -> Artifacts<Self::Artifact> {
814 self.artifacts_handler().output_to_artifacts(
815 contracts,
816 sources,
817 ctx,
818 layout,
819 primary_profiles,
820 )
821 }
822
823 fn standalone_source_file_to_artifact(
824 &self,
825 path: &Path,
826 file: &VersionedSourceFile,
827 ) -> Option<Self::Artifact> {
828 self.artifacts_handler().standalone_source_file_to_artifact(path, file)
829 }
830
831 fn is_dirty(&self, artifact_file: &ArtifactFile<Self::Artifact>) -> Result<bool> {
832 self.artifacts_handler().is_dirty(artifact_file)
833 }
834
835 fn handle_cached_artifacts(&self, artifacts: &Artifacts<Self::Artifact>) -> Result<()> {
836 self.artifacts_handler().handle_cached_artifacts(artifacts)
837 }
838}
839
840fn rebase_path(base: &Path, path: &Path) -> PathBuf {
868 use path_slash::PathExt;
869
870 let mut base_components = base.components();
871 let mut path_components = path.components();
872
873 let mut new_path = PathBuf::new();
874
875 while let Some(path_component) = path_components.next() {
876 let base_component = base_components.next();
877
878 if Some(path_component) != base_component {
879 if base_component.is_some() {
880 new_path.extend(std::iter::repeat_n(
881 std::path::Component::ParentDir,
882 base_components.count() + 1,
883 ));
884 }
885
886 new_path.push(path_component);
887 new_path.extend(path_components);
888
889 break;
890 }
891 }
892
893 new_path.to_slash_lossy().into_owned().into()
894}
895
896pub fn apply_updates(sources: &mut Sources, updates: Updates) {
898 for (path, source) in sources {
899 if let Some(updates) = updates.get(path) {
900 source.content = Arc::new(replace_source_content(
901 source.content.as_str(),
902 updates.iter().map(|(start, end, update)| ((*start..*end), update.as_str())),
903 ));
904 }
905 }
906}
907
908pub fn replace_source_content(
911 source: impl Into<String>,
912 updates: impl IntoIterator<Item = (Range<usize>, impl AsRef<str>)>,
913) -> String {
914 let mut offset = 0;
915 let mut content = source.into();
916 for (range, new_value) in updates {
917 let update_range = utils::range_by_offset(&range, offset);
918 let new_value = new_value.as_ref();
919 content.replace_range(update_range.clone(), new_value);
920 offset += new_value.len() as isize - (update_range.end - update_range.start) as isize;
921 }
922 content
923}
924
925pub(crate) fn parse_one_source<R>(
926 content: &str,
927 path: &Path,
928 f: impl FnOnce(&Session, &solar_parse::ast::SourceUnit<'_>) -> R,
929) -> Result<R, EmittedDiagnostics> {
930 let sess = Session::builder().with_buffer_emitter(Default::default()).build();
931 let res = sess.enter_sequential(|| -> solar_parse::interface::Result<_> {
932 let arena = solar_parse::ast::Arena::new();
933 let filename = FileName::Real(path.to_path_buf());
934 let mut parser = Parser::from_source_code(&sess, &arena, filename, content.to_string())?;
935 let ast = parser.parse_file().map_err(|e| e.emit())?;
936 Ok(f(&sess, &ast))
937 });
938
939 if let Err(err) = sess.emitted_errors().unwrap() {
941 trace!("failed parsing {path:?}:\n{err}");
942 return Err(err);
943 }
944
945 Ok(res.unwrap())
946}
947
948#[cfg(test)]
949#[cfg(feature = "svm-solc")]
950mod tests {
951 use super::*;
952 use foundry_compilers_artifacts::Remapping;
953 use foundry_compilers_core::utils::{self, mkdir_or_touch, tempdir};
954
955 #[test]
956 #[cfg_attr(windows, ignore = "<0.7 solc is flaky")]
957 fn test_build_all_versions() {
958 let paths = ProjectPathsConfig::builder()
959 .root("../../test-data/test-contract-versions")
960 .sources("../../test-data/test-contract-versions")
961 .build()
962 .unwrap();
963 let project = Project::builder()
964 .paths(paths)
965 .no_artifacts()
966 .ephemeral()
967 .build(Default::default())
968 .unwrap();
969 let contracts = project.compile().unwrap().succeeded().into_output().contracts;
970 assert_eq!(contracts.contracts().count(), 3);
972 }
973
974 #[test]
975 fn test_build_many_libs() {
976 let root = utils::canonicalize("../../test-data/test-contract-libs").unwrap();
977
978 let paths = ProjectPathsConfig::builder()
979 .root(&root)
980 .sources(root.join("src"))
981 .lib(root.join("lib1"))
982 .lib(root.join("lib2"))
983 .remappings(
984 Remapping::find_many(&root.join("lib1"))
985 .into_iter()
986 .chain(Remapping::find_many(&root.join("lib2"))),
987 )
988 .build()
989 .unwrap();
990 let project = Project::builder()
991 .paths(paths)
992 .no_artifacts()
993 .ephemeral()
994 .no_artifacts()
995 .build(Default::default())
996 .unwrap();
997 let contracts = project.compile().unwrap().succeeded().into_output().contracts;
998 assert_eq!(contracts.contracts().count(), 3);
999 }
1000
1001 #[test]
1002 fn test_build_remappings() {
1003 let root = utils::canonicalize("../../test-data/test-contract-remappings").unwrap();
1004 let paths = ProjectPathsConfig::builder()
1005 .root(&root)
1006 .sources(root.join("src"))
1007 .lib(root.join("lib"))
1008 .remappings(Remapping::find_many(&root.join("lib")))
1009 .build()
1010 .unwrap();
1011 let project = Project::builder()
1012 .no_artifacts()
1013 .paths(paths)
1014 .ephemeral()
1015 .build(Default::default())
1016 .unwrap();
1017 let contracts = project.compile().unwrap().succeeded().into_output().contracts;
1018 assert_eq!(contracts.contracts().count(), 2);
1019 }
1020
1021 #[test]
1022 fn can_rebase_path() {
1023 let rebase_path = |a: &str, b: &str| rebase_path(a.as_ref(), b.as_ref());
1024
1025 assert_eq!(rebase_path("a/b", "a/b/c"), PathBuf::from("c"));
1026 assert_eq!(rebase_path("a/b", "a/c"), PathBuf::from("../c"));
1027 assert_eq!(rebase_path("a/b", "c"), PathBuf::from("../../c"));
1028
1029 assert_eq!(
1030 rebase_path("/home/user/project", "/home/user/project/A.sol"),
1031 PathBuf::from("A.sol")
1032 );
1033 assert_eq!(
1034 rebase_path("/home/user/project", "/home/user/project/src/A.sol"),
1035 PathBuf::from("src/A.sol")
1036 );
1037 assert_eq!(
1038 rebase_path("/home/user/project", "/home/user/project/lib/forge-std/src/Test.sol"),
1039 PathBuf::from("lib/forge-std/src/Test.sol")
1040 );
1041 assert_eq!(
1042 rebase_path("/home/user/project", "/home/user/A.sol"),
1043 PathBuf::from("../A.sol")
1044 );
1045 assert_eq!(rebase_path("/home/user/project", "/home/A.sol"), PathBuf::from("../../A.sol"));
1046 assert_eq!(rebase_path("/home/user/project", "/A.sol"), PathBuf::from("../../../A.sol"));
1047 assert_eq!(
1048 rebase_path("/home/user/project", "/tmp/A.sol"),
1049 PathBuf::from("../../../tmp/A.sol")
1050 );
1051
1052 assert_eq!(
1053 rebase_path("/Users/ah/temp/verif", "/Users/ah/temp/remapped/Child.sol"),
1054 PathBuf::from("../remapped/Child.sol")
1055 );
1056 assert_eq!(
1057 rebase_path("/Users/ah/temp/verif", "/Users/ah/temp/verif/../remapped/Parent.sol"),
1058 PathBuf::from("../remapped/Parent.sol")
1059 );
1060 }
1061
1062 #[test]
1063 fn can_resolve_oz_remappings() {
1064 let tmp_dir = tempdir("node_modules").unwrap();
1065 let tmp_dir_node_modules = tmp_dir.path().join("node_modules");
1066 let paths = [
1067 "node_modules/@openzeppelin/contracts/interfaces/IERC1155.sol",
1068 "node_modules/@openzeppelin/contracts/finance/VestingWallet.sol",
1069 "node_modules/@openzeppelin/contracts/proxy/Proxy.sol",
1070 "node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol",
1071 ];
1072 mkdir_or_touch(tmp_dir.path(), &paths[..]);
1073 let remappings = Remapping::find_many(&tmp_dir_node_modules);
1074 let mut paths = ProjectPathsConfig::<()>::hardhat(tmp_dir.path()).unwrap();
1075 paths.remappings = remappings;
1076
1077 let resolved = paths
1078 .resolve_library_import(
1079 tmp_dir.path(),
1080 Path::new("@openzeppelin/contracts/token/ERC20/IERC20.sol"),
1081 )
1082 .unwrap();
1083 assert!(resolved.exists());
1084
1085 paths.remappings[0].name = "@openzeppelin/".to_string();
1087
1088 let resolved = paths
1089 .resolve_library_import(
1090 tmp_dir.path(),
1091 Path::new("@openzeppelin/contracts/token/ERC20/IERC20.sol"),
1092 )
1093 .unwrap();
1094 assert!(resolved.exists());
1095 }
1096
1097 #[test]
1098 fn test_replace_source_content() {
1099 let original_content = r#"
1100library Lib {
1101 function libFn() internal {
1102 // logic to keep
1103 }
1104}
1105contract A {
1106 function a() external {}
1107 function b() public {}
1108 function c() internal {
1109 // logic logic logic
1110 }
1111 function d() private {}
1112 function e() external {
1113 // logic logic logic
1114 }
1115}"#;
1116
1117 let updates = vec![
1118 (36..44, "external"),
1120 (80..90, "contract B"),
1122 (159..222, ""),
1124 (276..296, "// no logic"),
1126 ];
1127
1128 assert_eq!(
1129 replace_source_content(original_content, updates),
1130 r#"
1131library Lib {
1132 function libFn() external {
1133 // logic to keep
1134 }
1135}
1136contract B {
1137 function a() external {}
1138 function b() public {}
1139 function d() private {}
1140 function e() external {
1141 // no logic
1142 }
1143}"#
1144 );
1145 }
1146}