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