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