foundry_compilers/
lib.rs

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
41/// Updates to be applied to the sources.
42///
43/// `source_path -> (start, end, new_value)`
44pub type Updates = HashMap<PathBuf, BTreeSet<(usize, usize, String)>>;
45
46/// Utilities for creating, mocking and testing of (temporary) projects
47#[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/// Represents a project workspace and handles `solc` compiling of all contracts in that workspace.
80#[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    /// The layout of the project
87    pub paths: ProjectPathsConfig<C::Language>,
88    /// The compiler settings
89    pub settings: C::Settings,
90    /// Additional settings for cases when default compiler settings are not enough to cover all
91    /// possible restrictions.
92    pub additional_settings: BTreeMap<String, C::Settings>,
93    /// Mapping from file path to requirements on settings to compile it.
94    ///
95    /// This file will only be included into compiler inputs with profiles which satisfy the
96    /// restrictions.
97    pub restrictions:
98        BTreeMap<PathBuf, RestrictionsWithVersion<<C::Settings as CompilerSettings>::Restrictions>>,
99    /// Whether caching is enabled
100    pub cached: bool,
101    /// Whether to output build information with each solc call.
102    pub build_info: bool,
103    /// Whether writing artifacts to disk is enabled
104    pub no_artifacts: bool,
105    /// Handles all artifacts related tasks, reading and writing from the artifact dir.
106    pub artifacts: T,
107    /// Errors/Warnings which match these error codes are not going to be logged
108    pub ignored_error_codes: Vec<u64>,
109    /// Errors/Warnings which match these file paths are not going to be logged
110    pub ignored_file_paths: Vec<PathBuf>,
111    /// The minimum severity level that is treated as a compiler error
112    pub compiler_severity_filter: Severity,
113    /// Maximum number of `solc` processes to run simultaneously.
114    solc_jobs: usize,
115    /// Offline mode, if set, network access (download solc) is disallowed
116    pub offline: bool,
117    /// Windows only config value to ensure the all paths use `/` instead of `\\`, same as `solc`
118    ///
119    /// This is a noop on other platforms
120    pub slash_paths: bool,
121    /// Optional sparse output filter used to optimize compilation.
122    #[debug(skip)]
123    pub sparse_output: Option<Box<dyn FileFilter>>,
124}
125
126impl Project {
127    /// Convenience function to call `ProjectBuilder::default()`.
128    ///
129    /// # Examples
130    ///
131    /// Configure with [ConfigurableArtifacts] artifacts output and [MultiCompiler] compiler:
132    /// ```no_run
133    /// use foundry_compilers::Project;
134    ///
135    /// let config = Project::builder().build(Default::default())?;
136    /// # Ok::<(), Box<dyn std::error::Error>>(())
137    /// ```
138    ///
139    /// To configure any a project with any `ArtifactOutput` use either:
140    /// ```no_run
141    /// use foundry_compilers::Project;
142    ///
143    /// let config = Project::builder().build(Default::default())?;
144    /// # Ok::<(), Box<dyn std::error::Error>>(())
145    /// ```
146    ///
147    /// or use the builder directly:
148    /// ```no_run
149    /// use foundry_compilers::{multi::MultiCompiler, ConfigurableArtifacts, ProjectBuilder};
150    ///
151    /// let config = ProjectBuilder::<MultiCompiler>::default().build(Default::default())?;
152    /// # Ok::<(), Box<dyn std::error::Error>>(())
153    /// ```
154    pub fn builder() -> ProjectBuilder {
155        ProjectBuilder::default()
156    }
157}
158
159impl<T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler> Project<C, T> {
160    /// Returns the handler that takes care of processing all artifacts
161    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    /// Returns standard-json-input to compile the target contract
176    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        // strip the path to the project root from all remappings
203        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    /// Returns the path to the artifacts directory
219    pub fn artifacts_path(&self) -> &Path {
220        &self.paths.artifacts
221    }
222
223    /// Returns the path to the sources directory
224    pub fn sources_path(&self) -> &Path {
225        &self.paths.sources
226    }
227
228    /// Returns the path to the cache file
229    pub fn cache_path(&self) -> &Path {
230        &self.paths.cache
231    }
232
233    /// Returns the path to the `build-info` directory nested in the artifacts dir
234    pub fn build_info_path(&self) -> &Path {
235        &self.paths.build_infos
236    }
237
238    /// Returns the root directory of the project
239    pub fn root(&self) -> &Path {
240        &self.paths.root
241    }
242
243    /// Convenience function to read the cache file.
244    /// See also [CompilerCache::read_joined()]
245    pub fn read_cache_file(&self) -> Result<CompilerCache<C::Settings>> {
246        CompilerCache::read_joined(&self.paths)
247    }
248
249    /// Sets the maximum number of parallel `solc` processes to run simultaneously.
250    ///
251    /// # Panics
252    ///
253    /// if `jobs == 0`
254    pub fn set_solc_jobs(&mut self, jobs: usize) {
255        assert!(jobs > 0);
256        self.solc_jobs = jobs;
257    }
258
259    /// Returns all sources found under the project's configured sources path
260    #[instrument(skip_all, fields(name = "sources"))]
261    pub fn sources(&self) -> Result<Sources> {
262        self.paths.read_sources()
263    }
264
265    /// Emit the cargo [`rerun-if-changed`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#cargorerun-if-changedpath) instruction.
266    ///
267    /// This tells Cargo to re-run the build script if a file inside the project's sources directory
268    /// has changed.
269    ///
270    /// Use this if you compile a project in a `build.rs` file.
271    ///
272    /// # Examples
273    /// ```no_run
274    /// use foundry_compilers::{Project, ProjectPathsConfig};
275    ///
276    /// // Configure the project with all its paths, solc, cache etc.
277    /// // where the root dir is the current Rust project.
278    /// let paths = ProjectPathsConfig::hardhat(env!("CARGO_MANIFEST_DIR").as_ref())?;
279    /// let project = Project::builder().paths(paths).build(Default::default())?;
280    /// let output = project.compile()?;
281    ///
282    /// // Tell Cargo to rerun this build script that if a source file changes.
283    /// project.rerun_if_sources_changed();
284    /// # Ok::<_, Box<dyn std::error::Error>>(())
285    /// ```
286    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    /// Convenience function to compile a single solidity file with the project's settings.
295    ///
296    /// # Examples
297    /// ```no_run
298    /// use foundry_compilers::Project;
299    ///
300    /// let project = Project::builder().build(Default::default())?;
301    /// let output = project.compile_file("example/Greeter.sol")?;
302    /// # Ok::<(), Box<dyn std::error::Error>>(())
303    /// ```
304    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    /// Convenience function to compile a series of solidity files with the project's settings.
311    /// Same as [`Self::compile()`] but with the given `files` as input.
312    ///
313    /// # Examples
314    /// ```no_run
315    /// use foundry_compilers::Project;
316    ///
317    /// let project = Project::builder().build(Default::default())?;
318    /// let output = project.compile_files(["examples/Foo.sol", "examples/Bar.sol"])?;
319    /// # Ok::<(), Box<dyn std::error::Error>>(())
320    /// ```
321    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    /// Removes the project's artifacts and cache file
332    ///
333    /// If the cache file was the only file in the folder, this also removes the empty folder.
334    ///
335    /// # Examples
336    /// ```
337    /// use foundry_compilers::Project;
338    ///
339    /// let project = Project::builder().build(Default::default())?;
340    /// let _ = project.compile()?;
341    /// assert!(project.artifacts_path().exists());
342    /// assert!(project.cache_path().exists());
343    ///
344    /// project.cleanup();
345    /// assert!(!project.artifacts_path().exists());
346    /// assert!(!project.cache_path().exists());
347    /// # Ok::<(), Box<dyn std::error::Error>>(())
348    /// ```
349    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                // remove the cache folder if the cache file was the only file
358                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        // clean the artifacts dir
372        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        // also clean the build-info dir, in case it's not nested in the artifacts dir
379        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    /// Parses the sources in memory and collects all the contract names mapped to their file paths.
389    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    /// Finds the path of the contract with the given name.
410    /// Throws error if multiple or no contracts with the same name are found.
411    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    /// Invokes [CompilerSettings::update_output_selection] on the project's settings and all
432    /// additional settings profiles.
433    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    /// The layout of the
446    paths: Option<ProjectPathsConfig<C::Language>>,
447    /// How solc invocation should be configured.
448    settings: Option<C::Settings>,
449    additional_settings: BTreeMap<String, C::Settings>,
450    restrictions:
451        BTreeMap<PathBuf, RestrictionsWithVersion<<C::Settings as CompilerSettings>::Restrictions>>,
452    /// Whether caching is enabled, default is true.
453    cached: bool,
454    /// Whether to output build information with each solc call.
455    build_info: bool,
456    /// Whether writing artifacts to disk is enabled, default is true.
457    no_artifacts: bool,
458    /// Use offline mode
459    offline: bool,
460    /// Whether to slash paths of the `ProjectCompilerOutput`
461    slash_paths: bool,
462    /// handles all artifacts related tasks
463    artifacts: T,
464    /// Which error codes to ignore
465    pub ignored_error_codes: Vec<u64>,
466    /// Which file paths to ignore
467    pub ignored_file_paths: Vec<PathBuf>,
468    /// The minimum severity level that is treated as a compiler error
469    compiler_severity_filter: Severity,
470    solc_jobs: Option<usize>,
471    /// Optional sparse output filter used to optimize compilation.
472    sparse_output: Option<Box<dyn FileFilter>>,
473}
474
475impl<C: Compiler, T: ArtifactOutput<CompilerContract = C::CompilerContract>> ProjectBuilder<C, T> {
476    /// Create a new builder with the given artifacts handler
477    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    /// Disables cached builds
535    #[must_use]
536    pub fn ephemeral(self) -> Self {
537        self.set_cached(false)
538    }
539
540    /// Sets the cache status
541    #[must_use]
542    pub fn set_cached(mut self, cached: bool) -> Self {
543        self.cached = cached;
544        self
545    }
546
547    /// Sets the build info value
548    #[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    /// Activates offline mode
555    ///
556    /// Prevents network possible access to download/check solc installs
557    #[must_use]
558    pub fn offline(self) -> Self {
559        self.set_offline(true)
560    }
561
562    /// Sets the offline status
563    #[must_use]
564    pub fn set_offline(mut self, offline: bool) -> Self {
565        self.offline = offline;
566        self
567    }
568
569    /// Sets whether to slash all paths on windows
570    ///
571    /// If set to `true` all `\\` separators are replaced with `/`, same as solc
572    #[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    /// Disables writing artifacts to disk
579    #[must_use]
580    pub fn no_artifacts(self) -> Self {
581        self.set_no_artifacts(true)
582    }
583
584    /// Sets the no artifacts status
585    #[must_use]
586    pub fn set_no_artifacts(mut self, artifacts: bool) -> Self {
587        self.no_artifacts = artifacts;
588        self
589    }
590
591    /// Sets the maximum number of parallel `solc` processes to run simultaneously.
592    ///
593    /// # Panics
594    ///
595    /// `jobs` must be at least 1
596    #[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    /// Sets the number of parallel `solc` processes to `1`, no parallelization
604    #[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    /// Set arbitrary `ArtifactOutputHandler`
637    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            // ensures we always use `/` paths
700            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
840// Rebases the given path to the base directory lexically.
841//
842// For instance, given the base `/home/user/project` and the path `/home/user/project/src/A.sol`,
843// this function returns `src/A.sol`.
844//
845// This function transforms a path into a form that is relative to the base directory. The returned
846// path starts either with a normal component (e.g., `src`) or a parent directory component (i.e.,
847// `..`). It also converts the path into a UTF-8 string and replaces all separators with forward
848// slashes (`/`), if they're not.
849//
850// The rebasing process can be conceptualized as follows:
851//
852// 1. Remove the leading components from the path that match those in the base.
853// 2. Prepend `..` components to the path, matching the number of remaining components in the base.
854//
855// # Examples
856//
857// `rebase_path("/home/user/project", "/home/user/project/src/A.sol")` returns `src/A.sol`. The
858// common part, `/home/user/project`, is removed from the path.
859//
860// `rebase_path("/home/user/project", "/home/user/A.sol")` returns `../A.sol`. First, the common
861// part, `/home/user`, is removed, leaving `A.sol`. Next, as `project` remains in the base, `..` is
862// prepended to the path.
863//
864// On Windows, paths like `a\b\c` are converted to `a/b/c`.
865//
866// For more examples, see the test.
867fn 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
896/// Utility function to apply a set of updates to provided sources.
897pub 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
908/// Utility function to change source content ranges with provided updates.
909/// Assumes that the updates are sorted.
910pub 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    // Return if any diagnostics emitted during content parsing.
940    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        // Contracts A to F
971        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        // adjust remappings
1086        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            // Replace function libFn() visibility to external
1119            (36..44, "external"),
1120            // Replace contract A name to contract B
1121            (80..90, "contract B"),
1122            // Remove function c()
1123            (159..222, ""),
1124            // Replace function e() logic
1125            (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}