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