Skip to main content

foundry_common/
compile.rs

1//! Support for compiling [foundry_compilers::Project]
2
3use crate::{
4    TestFunctionExt,
5    external_compiler::{ExternalCompilation, is_builtin_compiler_source},
6    fs::canonicalize_path,
7    preprocessor::DynamicTestLinkingPreprocessor,
8    shell,
9    term::SpinnerReporter,
10};
11use alloy_json_abi::JsonAbi;
12use comfy_table::{
13    Cell, Color, Table,
14    presets::{ASCII_FULL, ASCII_MARKDOWN},
15};
16use eyre::{OptionExt, Result};
17use foundry_block_explorers::contract::Metadata;
18use foundry_compilers::{
19    Artifact, Project, ProjectBuilder, ProjectCompileOutput, ProjectPathsConfig, SolcConfig,
20    artifacts::{
21        BytecodeObject, Contract, Error as SolcError, Severity, Source,
22        output_selection::OutputSelection, remappings::Remapping,
23    },
24    compilers::{
25        CompilationError, Compiler,
26        solc::{Solc, SolcCompiler},
27    },
28    info::ContractInfo as CompilerContractInfo,
29    multi::{MultiCompiler, MultiCompilerError, MultiCompilerSettings},
30    project::Preprocessor,
31    report::{BasicStdoutReporter, NoReporter, Report},
32    solc::SolcSettings,
33};
34use foundry_config::Config;
35use num_format::{Locale, ToFormattedString};
36use revm::primitives::{eip170, eip3860, hardfork::SpecId};
37use solar::{
38    ast::{Arena, ContractKind, ItemKind},
39    interface::{Session, source_map::FileName},
40    parse::Parser,
41};
42use std::{
43    any::Any,
44    collections::{BTreeMap, BTreeSet, HashMap, HashSet},
45    fmt::Display,
46    io::IsTerminal,
47    path::{Component, Path, PathBuf},
48    str::FromStr,
49    sync::Arc,
50    time::Instant,
51};
52
53/// A Solar compiler instance, to grant syntactic and semantic analysis capabilities.
54pub type Analysis = Arc<solar::sema::Compiler>;
55
56/// Builder type to configure how to compile a project.
57///
58/// This is merely a wrapper for [`Project::compile()`] which also prints to stdout depending on its
59/// settings.
60#[must_use = "ProjectCompiler does nothing unless you call a `compile*` method"]
61pub struct ProjectCompiler {
62    /// The root of the project.
63    project_root: PathBuf,
64
65    /// Whether to also print contract names.
66    print_names: Option<bool>,
67
68    /// Whether to also print contract sizes.
69    print_sizes: Option<bool>,
70
71    /// Whether to print anything at all. Overrides other `print` options.
72    quiet: Option<bool>,
73
74    /// Whether to print the resolved settings for each compiler invocation.
75    print_compiler_settings: bool,
76
77    /// Whether to bail on compiler errors.
78    bail: Option<bool>,
79
80    /// Whether to ignore the contract initcode size limit introduced by EIP-3860.
81    ignore_eip_3860: bool,
82
83    /// Contract size limits used when reporting compiled contract sizes.
84    size_limits: ContractSizeLimits,
85
86    /// Extra files to include, that are not necessarily in the project's source directory.
87    files: Vec<PathBuf>,
88
89    /// Complete source roots to retry if filtering changed Solidity's inheritance ordering.
90    source_order_fallback: Vec<PathBuf>,
91
92    /// Paths used by external adapters to select compiler-native build units.
93    selected_paths: Vec<PathBuf>,
94
95    /// Whether to compile with dynamic linking tests and scripts.
96    dynamic_test_linking: bool,
97
98    /// Whether compiled contracts may depend on externally linked Solidity libraries.
99    allow_linked_libraries: bool,
100
101    /// Whether ABI acquisition may consult the compiler-owned ABI cache.
102    abi_cache: bool,
103
104    /// External compiler configuration.
105    external_compilers: Option<Config>,
106
107    /// Preserves the caller's artifact policy when ABI caching enables artifacts on a cloned
108    /// project.
109    external_writes: bool,
110}
111
112impl Default for ProjectCompiler {
113    #[inline]
114    fn default() -> Self {
115        Self::new()
116    }
117}
118
119impl ProjectCompiler {
120    /// Create a new builder with the default settings.
121    #[inline]
122    pub fn new() -> Self {
123        Self {
124            project_root: PathBuf::new(),
125            print_names: None,
126            print_sizes: None,
127            quiet: Some(crate::shell::is_quiet()),
128            print_compiler_settings: false,
129            bail: None,
130            ignore_eip_3860: false,
131            size_limits: ContractSizeLimits::default(),
132            files: Vec::new(),
133            source_order_fallback: Vec::new(),
134            selected_paths: Vec::new(),
135            dynamic_test_linking: false,
136            allow_linked_libraries: true,
137            abi_cache: false,
138            external_compilers: None,
139            external_writes: true,
140        }
141    }
142
143    /// Sets whether to print contract names.
144    #[inline]
145    pub const fn print_names(mut self, yes: bool) -> Self {
146        self.print_names = Some(yes);
147        self
148    }
149
150    /// Sets whether to print contract sizes.
151    #[inline]
152    pub const fn print_sizes(mut self, yes: bool) -> Self {
153        self.print_sizes = Some(yes);
154        self
155    }
156
157    /// Sets whether to print anything at all. Overrides other `print` options.
158    #[inline]
159    #[doc(alias = "silent")]
160    pub const fn quiet(mut self, yes: bool) -> Self {
161        self.quiet = Some(yes);
162        self
163    }
164
165    /// Sets whether to print the resolved settings for each compiler invocation.
166    #[inline]
167    pub const fn print_compiler_settings(mut self, yes: bool) -> Self {
168        self.print_compiler_settings = yes;
169        self
170    }
171
172    /// Sets whether to bail on compiler errors.
173    #[inline]
174    pub const fn bail(mut self, yes: bool) -> Self {
175        self.bail = Some(yes);
176        self
177    }
178
179    /// Sets whether to ignore EIP-3860 initcode size limits.
180    #[inline]
181    pub const fn ignore_eip_3860(mut self, yes: bool) -> Self {
182        self.ignore_eip_3860 = yes;
183        self
184    }
185
186    /// Sets the contract size limits for size reports.
187    #[inline]
188    pub const fn size_limits(mut self, limits: ContractSizeLimits) -> Self {
189        self.size_limits = limits;
190        self
191    }
192
193    /// Sets extra files to include, that are not necessarily in the project's source dir.
194    #[inline]
195    pub fn files(mut self, files: impl IntoIterator<Item = PathBuf>) -> Self {
196        self.files.extend(files);
197        self
198    }
199
200    /// Sets paths used by external adapters to select compiler-native build units.
201    pub fn selected_paths(mut self, paths: impl IntoIterator<Item = PathBuf>) -> Self {
202        self.selected_paths.extend(paths);
203        self
204    }
205
206    /// Selects targets for external adapters and built-in sources where applicable.
207    pub fn target_files(mut self, paths: impl IntoIterator<Item = PathBuf>) -> Self {
208        for path in paths {
209            if is_builtin_compiler_source(&path) {
210                self.files.push(path.clone());
211            }
212            self.selected_paths.push(path);
213        }
214        self
215    }
216
217    /// Sets if tests should be dynamically linked.
218    #[inline]
219    pub const fn dynamic_test_linking(mut self, preprocess: bool) -> Self {
220        self.dynamic_test_linking = preprocess;
221        self
222    }
223
224    /// Enables explicitly configured external compilers for this build.
225    pub fn external_compilers(mut self, config: &Config) -> Self {
226        self.external_compilers = Some(config.clone());
227        self
228    }
229
230    /// Controls whether external artifacts and their cache may be published.
231    pub const fn external_artifacts(mut self, write: bool) -> Self {
232        self.external_writes = write;
233        self
234    }
235
236    /// Reuses normal artifacts before consulting the compiler-owned ABI cache.
237    ///
238    /// The project must request ABI output and may include other contract outputs.
239    /// The project's artifact policy controls writes to the secondary cache.
240    pub const fn cache_abi(mut self) -> Self {
241        self.abi_cache = true;
242        self
243    }
244
245    /// Retries once with these roots if compilation fails because a base contract is defined
246    /// after its derived contract. Removing roots can change traversal order in import cycles.
247    pub fn source_order_fallback(mut self, files: impl IntoIterator<Item = PathBuf>) -> Self {
248        self.source_order_fallback.extend(files);
249        self
250    }
251
252    /// Sets whether compiled contracts may depend on externally linked Solidity libraries.
253    ///
254    /// This does not disable dynamic test linking. With `bail(false)`, violations are reported as
255    /// compiler errors in the returned output.
256    pub const fn allow_linked_libraries(mut self, allow: bool) -> Self {
257        self.allow_linked_libraries = allow;
258        self
259    }
260
261    /// Compiles the project.
262    #[instrument(target = "forge::compile", skip_all)]
263    pub fn compile<C: Compiler<CompilerContract = Contract>>(
264        mut self,
265        project: &Project<C>,
266    ) -> Result<ProjectCompileOutput<C>>
267    where
268        DynamicTestLinkingPreprocessor: Preprocessor<C>,
269    {
270        self.project_root = project.root().to_path_buf();
271
272        // TODO: Avoid using std::process::exit(0).
273        // Replacing this with a return (e.g., Ok(ProjectCompileOutput::default())) would be more
274        // idiomatic, but it currently requires a `Default` bound on `C::Language`, which
275        // breaks compatibility with downstream crates like `foundry-cli`. This would need a
276        // broader refactor across the call chain. Leaving it as-is for now until a larger
277        // refactor is feasible.
278        if !project.paths.has_input_files()
279            && self.files.is_empty()
280            && self.selected_paths.is_empty()
281            && self
282                .external_compilers
283                .as_ref()
284                .is_none_or(|config| config.external_compilers.is_empty())
285        {
286            sh_println!("Nothing to compile")?;
287            std::process::exit(0);
288        }
289
290        // Taking is fine since we don't need these in `compile_with`.
291        let files = std::mem::take(&mut self.files);
292        let fallback_files = std::mem::take(&mut self.source_order_fallback);
293        let explicit_selection = !files.is_empty() || !self.selected_paths.is_empty();
294        let selected_paths = std::mem::take(&mut self.selected_paths);
295        let preprocess = self.dynamic_test_linking;
296        let abi_cache = self.abi_cache;
297        let external_compilers = self.external_compilers.take();
298        let external_writes = self.external_writes && !project.no_artifacts;
299        self.compile_with(|| {
300            let external = external_compilers
301                .as_ref()
302                .map(|config| {
303                    ExternalCompilation::compile(config, &selected_paths, external_writes)
304                })
305                .transpose()?;
306            let sources = if explicit_selection {
307                Source::read_all(files)?
308            } else {
309                project.paths.read_input_files()?
310            };
311
312            let preprocessor = DynamicTestLinkingPreprocessor::default();
313            let compile = |preprocess| -> Result<ProjectCompileOutput<C>> {
314                let compile_sources = |sources| {
315                    let mut compiler = foundry_compilers::project::ProjectCompiler::with_sources(
316                        project, sources,
317                    )?;
318                    if preprocess {
319                        compiler = compiler.with_preprocessor(preprocessor.clone());
320                    }
321                    if abi_cache { compiler.compile_abi_cached() } else { compiler.compile() }
322                };
323                let mut output = compile_sources(sources.clone())?;
324                // Solc error 2449 can be introduced by filtering compilation roots in an import
325                // cycle. Retry before reporting diagnostics or bailing on the filtered output.
326                if !fallback_files.is_empty()
327                    && output.output().errors.iter().any(|error| error.error_code() == Some(2449))
328                {
329                    output = compile_sources(Source::read_all(&fallback_files)?)?;
330                }
331                Ok(output)
332            };
333            let mut output = compile(preprocess)?;
334            // A retry can only help if DTL changed the compiler input.
335            if preprocessor.rewrote_sources()
336                && let Some(reason) = dynamic_test_linking_failure(&output.output().errors)
337            {
338                sh_warn!(
339                    "dynamic test linking failed ({reason}); retrying with native test linking. \
340                     Use --no-dynamic-test-linking to skip the failed attempt."
341                )?;
342                // The preprocessor is part of the cache identity: native compilation cannot
343                // reuse DTL artifacts. Return only this attempt, including any compiler errors.
344                output = compile(false)?;
345            }
346            if !output.has_compiler_errors()
347                && let Some(external) = external
348            {
349                external.merge(&mut output)?;
350            }
351            Ok(output)
352        })
353    }
354
355    /// Compiles the project with the given closure
356    fn compile_with<C: Compiler<CompilerContract = Contract>, F>(
357        self,
358        f: F,
359    ) -> Result<ProjectCompileOutput<C>>
360    where
361        F: FnOnce() -> Result<ProjectCompileOutput<C>>,
362    {
363        let quiet = self.quiet.unwrap_or(false);
364        let bail = self.bail.unwrap_or(true);
365
366        let mut output = with_compilation_reporter_and_settings(
367            quiet,
368            Some(self.project_root.clone()),
369            self.print_compiler_settings,
370            || {
371                tracing::debug!("compiling project");
372
373                let timer = Instant::now();
374                let r = f();
375                let elapsed = timer.elapsed();
376
377                tracing::debug!("finished compiling in {:.3}s", elapsed.as_secs_f64());
378                r
379            },
380        )?;
381
382        if bail && output.has_compiler_errors() {
383            eyre::bail!("{output}");
384        }
385
386        let mut linking_error = (!self.allow_linked_libraries && !output.has_compiler_errors())
387            .then(|| linked_libraries_error(&output, &self.project_root))
388            .flatten();
389        if !bail && let Some(message) = linking_error.take() {
390            push_linked_libraries_error(&mut output, message);
391        }
392
393        if !quiet && !shell::is_json() {
394            if output.is_unchanged() {
395                sh_println!("No files changed, compilation skipped")?;
396            } else {
397                // print the compiler output / warnings
398                sh_println!("{output}")?;
399            }
400        }
401
402        // Report compiler warnings before the policy failure.
403        if let Some(message) = linking_error {
404            eyre::bail!("{message}");
405        }
406
407        // Quiet mode suppresses reports, but size limits still apply.
408        if !(shell::is_json() && output.has_compiler_errors()) {
409            self.handle_output(&output)?;
410        }
411
412        Ok(output)
413    }
414
415    /// Prints requested reports and checks contract size limits.
416    fn handle_output<C: Compiler<CompilerContract = Contract>>(
417        &self,
418        output: &ProjectCompileOutput<C>,
419    ) -> Result<()> {
420        let quiet = self.quiet.unwrap_or(false);
421        let print_names = self.print_names.unwrap_or(false) && !quiet;
422        let print_sizes = self.print_sizes.unwrap_or(false);
423
424        // print any sizes or names
425        if print_names {
426            let mut artifacts: BTreeMap<_, Vec<_>> = BTreeMap::new();
427            for (name, (_, version)) in output.versioned_artifacts() {
428                artifacts.entry(version).or_default().push(name);
429            }
430
431            if shell::is_json() {
432                sh_println!("{}", serde_json::to_string(&artifacts).unwrap())?;
433            } else {
434                for (version, names) in artifacts {
435                    sh_println!(
436                        "  compiler version: {}.{}.{}",
437                        version.major,
438                        version.minor,
439                        version.patch
440                    )?;
441                    for name in names {
442                        sh_println!("    - {name}")?;
443                    }
444                }
445            }
446        }
447
448        if print_sizes {
449            // add extra newline if names were already printed
450            if print_names && !shell::is_json() {
451                sh_println!()?;
452            }
453
454            let mut size_report =
455                SizeReport { contracts: BTreeMap::new(), limits: self.size_limits };
456
457            let mut artifacts: BTreeMap<String, Vec<_>> = BTreeMap::new();
458            for (id, artifact) in output.artifact_ids().filter(|(id, _)| {
459                // filter out forge-std specific contracts
460                !id.source.to_string_lossy().contains("/forge-std/src/")
461            }) {
462                artifacts.entry(id.name.clone()).or_default().push((id.source, artifact));
463            }
464
465            // Internal libraries are inlined into consumers and never deployed; skip them.
466            // Only artifacts whose ABI has no functions can be internal libraries, so restrict the
467            // solar parse to those sources to avoid a second full parse pass.
468            let abs_source = |path: &Path| -> PathBuf {
469                if path.is_absolute() { path.to_path_buf() } else { self.project_root.join(path) }
470            };
471            let source_paths = artifacts
472                .values()
473                .flatten()
474                .filter(|(_, artifact)| {
475                    artifact.abi.as_ref().is_some_and(|abi| abi.functions().next().is_none())
476                })
477                .map(|(path, _)| abs_source(path))
478                .collect::<BTreeSet<_>>();
479            let libraries = collect_libraries(&source_paths);
480
481            for (name, artifact_list) in artifacts {
482                // A library with no functions in its ABI is internal-only; fail open if the ABI is
483                // missing. Filter first so the duplicate-name suffix below reflects kept contracts.
484                let kept = artifact_list
485                    .iter()
486                    .filter(|(path, artifact)| {
487                        let is_library = libraries
488                            .get(&abs_source(path))
489                            .is_some_and(|libs| libs.contains(&name));
490                        let has_no_abi_functions = artifact
491                            .abi
492                            .as_ref()
493                            .is_some_and(|abi| abi.functions().next().is_none());
494                        !(is_library && has_no_abi_functions)
495                    })
496                    .collect::<Vec<_>>();
497
498                for (path, artifact) in &kept {
499                    let runtime_size = contract_size(*artifact, false).unwrap_or_default();
500                    let init_size = contract_size(*artifact, true).unwrap_or_default();
501
502                    let is_dev_contract = artifact.abi.as_ref().is_some_and(|abi| {
503                        abi.functions().any(|f| {
504                            f.test_function_kind().is_known()
505                                || matches!(f.name.as_str(), "IS_TEST" | "IS_SCRIPT")
506                        })
507                    });
508
509                    let unique_name = if kept.len() > 1 {
510                        format!(
511                            "{} ({})",
512                            name,
513                            path.strip_prefix(&self.project_root).unwrap_or(path).display()
514                        )
515                    } else {
516                        name.clone()
517                    };
518
519                    size_report.contracts.insert(
520                        unique_name,
521                        ContractInfo { runtime_size, init_size, is_dev_contract },
522                    );
523                }
524            }
525
526            if !quiet {
527                sh_println!("{size_report}")?;
528            }
529
530            let runtime_eip = match size_report.limits.runtime {
531                CONTRACT_RUNTIME_SIZE_LIMIT => "EIP-170: ",
532                AMSTERDAM_CONTRACT_RUNTIME_SIZE_LIMIT => "EIP-7954: ",
533                _ => "",
534            };
535            eyre::ensure!(
536                !size_report.exceeds_runtime_size_limit(),
537                "some contracts exceed the runtime size limit ({runtime_eip}{} bytes)",
538                size_report.limits.runtime
539            );
540            // Check size limits only if not ignoring EIP-3860
541            let initcode_eip = match size_report.limits.initcode {
542                CONTRACT_INITCODE_SIZE_LIMIT => "EIP-3860: ",
543                AMSTERDAM_CONTRACT_INITCODE_SIZE_LIMIT => "EIP-7954: ",
544                _ => "",
545            };
546            eyre::ensure!(
547                self.ignore_eip_3860 || !size_report.exceeds_initcode_size_limit(),
548                "some contracts exceed the initcode size limit ({initcode_eip}{} bytes)",
549                size_report.limits.initcode
550            );
551        }
552
553        Ok(())
554    }
555}
556
557// https://eips.ethereum.org/EIPS/eip-170
558const CONTRACT_RUNTIME_SIZE_LIMIT: usize = eip170::MAX_CODE_SIZE;
559
560// https://eips.ethereum.org/EIPS/eip-3860
561const CONTRACT_INITCODE_SIZE_LIMIT: usize = eip3860::MAX_INITCODE_SIZE;
562
563// https://eips.ethereum.org/EIPS/eip-7954
564const AMSTERDAM_CONTRACT_RUNTIME_SIZE_LIMIT: usize = 65_536;
565const AMSTERDAM_CONTRACT_INITCODE_SIZE_LIMIT: usize = 131_072;
566
567const CONTRACT_RUNTIME_SIZE_WARN_THRESHOLD: usize = 18_000;
568const CONTRACT_INITCODE_SIZE_WARN_THRESHOLD: usize = 36_000;
569
570/// Runtime and initcode byte-size limits for compiled contract size reports.
571#[derive(Clone, Copy, Debug, PartialEq, Eq)]
572pub struct ContractSizeLimits {
573    /// Maximum deployed runtime bytecode size.
574    pub runtime: usize,
575    /// Maximum initcode bytecode size.
576    pub initcode: usize,
577}
578
579impl ContractSizeLimits {
580    /// Creates a new set of contract size limits.
581    pub const fn new(runtime: usize, initcode: usize) -> Self {
582        Self { runtime, initcode }
583    }
584
585    /// Creates limits from a runtime code-size limit, using the EIP-3860 2x initcode ratio.
586    pub const fn with_runtime_limit(runtime: usize) -> Self {
587        Self { runtime, initcode: runtime.saturating_mul(2) }
588    }
589
590    /// Returns the protocol limits active for an EVM specification.
591    pub const fn for_spec_id(spec_id: SpecId) -> Self {
592        if spec_id.is_enabled_in(SpecId::AMSTERDAM) {
593            Self::new(AMSTERDAM_CONTRACT_RUNTIME_SIZE_LIMIT, AMSTERDAM_CONTRACT_INITCODE_SIZE_LIMIT)
594        } else {
595            Self::new(CONTRACT_RUNTIME_SIZE_LIMIT, CONTRACT_INITCODE_SIZE_LIMIT)
596        }
597    }
598
599    const fn runtime_warning_threshold(self) -> usize {
600        scaled_threshold(
601            self.runtime,
602            CONTRACT_RUNTIME_SIZE_WARN_THRESHOLD,
603            CONTRACT_RUNTIME_SIZE_LIMIT,
604        )
605    }
606
607    const fn initcode_warning_threshold(self) -> usize {
608        scaled_threshold(
609            self.initcode,
610            CONTRACT_INITCODE_SIZE_WARN_THRESHOLD,
611            CONTRACT_INITCODE_SIZE_LIMIT,
612        )
613    }
614}
615
616impl Default for ContractSizeLimits {
617    fn default() -> Self {
618        Self::new(CONTRACT_RUNTIME_SIZE_LIMIT, CONTRACT_INITCODE_SIZE_LIMIT)
619    }
620}
621
622const fn scaled_threshold(limit: usize, threshold: usize, default_limit: usize) -> usize {
623    limit.saturating_mul(threshold) / default_limit
624}
625
626/// Contracts with info about their size
627pub struct SizeReport {
628    /// `contract name -> info`
629    pub contracts: BTreeMap<String, ContractInfo>,
630    /// Size limits used to calculate margins and failures.
631    pub limits: ContractSizeLimits,
632}
633
634impl SizeReport {
635    /// Returns the maximum runtime code size, excluding dev contracts.
636    pub fn max_runtime_size(&self) -> usize {
637        self.contracts
638            .values()
639            .filter(|c| !c.is_dev_contract)
640            .map(|c| c.runtime_size)
641            .max()
642            .unwrap_or(0)
643    }
644
645    /// Returns the maximum initcode size, excluding dev contracts.
646    pub fn max_init_size(&self) -> usize {
647        self.contracts
648            .values()
649            .filter(|c| !c.is_dev_contract)
650            .map(|c| c.init_size)
651            .max()
652            .unwrap_or(0)
653    }
654
655    /// Returns true if any contract exceeds the runtime size limit, excluding dev contracts.
656    pub fn exceeds_runtime_size_limit(&self) -> bool {
657        self.max_runtime_size() > self.limits.runtime
658    }
659
660    /// Returns true if any contract exceeds the initcode size limit, excluding dev contracts.
661    pub fn exceeds_initcode_size_limit(&self) -> bool {
662        self.max_init_size() > self.limits.initcode
663    }
664}
665
666impl Display for SizeReport {
667    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
668        if shell::is_json() {
669            writeln!(f, "{}", self.format_json_output())?;
670        } else {
671            writeln!(f, "\n{}", self.format_table_output())?;
672        }
673        Ok(())
674    }
675}
676
677impl SizeReport {
678    fn format_json_output(&self) -> String {
679        let contracts = self
680            .contracts
681            .iter()
682            .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0))
683            .map(|(name, contract)| {
684                (
685                    name.clone(),
686                    serde_json::json!({
687                        "runtime_size": contract.runtime_size,
688                        "init_size": contract.init_size,
689                        "runtime_margin": self.limits.runtime as isize - contract.runtime_size as isize,
690                        "init_margin": self.limits.initcode as isize - contract.init_size as isize,
691                    }),
692                )
693            })
694            .collect::<serde_json::Map<_, _>>();
695
696        serde_json::to_string(&contracts).unwrap()
697    }
698
699    fn format_table_output(&self) -> Table {
700        let mut table = Table::new();
701        if shell::is_markdown() {
702            table.load_style(ASCII_MARKDOWN);
703        } else {
704            table.load_style(ASCII_FULL.with_rounded_corners());
705        }
706
707        table.set_header(vec![
708            Cell::new("Contract"),
709            Cell::new("Runtime Size (B)"),
710            Cell::new("Initcode Size (B)"),
711            Cell::new("Runtime Margin (B)"),
712            Cell::new("Initcode Margin (B)"),
713        ]);
714
715        // Filters out dev contracts (Test or Script)
716        let contracts = self
717            .contracts
718            .iter()
719            .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0));
720        let runtime_warning_threshold = self.limits.runtime_warning_threshold();
721        let initcode_warning_threshold = self.limits.initcode_warning_threshold();
722        for (name, contract) in contracts {
723            let runtime_margin = self.limits.runtime as isize - contract.runtime_size as isize;
724            let init_margin = self.limits.initcode as isize - contract.init_size as isize;
725
726            let runtime_color = if contract.runtime_size < runtime_warning_threshold {
727                Color::Reset
728            } else if contract.runtime_size <= self.limits.runtime {
729                Color::Yellow
730            } else {
731                Color::Red
732            };
733
734            let init_color = if contract.init_size < initcode_warning_threshold {
735                Color::Reset
736            } else if contract.init_size <= self.limits.initcode {
737                Color::Yellow
738            } else {
739                Color::Red
740            };
741
742            let locale = &Locale::en;
743            table.add_row([
744                Cell::new(name),
745                Cell::new(contract.runtime_size.to_formatted_string(locale)).fg(runtime_color),
746                Cell::new(contract.init_size.to_formatted_string(locale)).fg(init_color),
747                Cell::new(runtime_margin.to_formatted_string(locale)).fg(runtime_color),
748                Cell::new(init_margin.to_formatted_string(locale)).fg(init_color),
749            ]);
750        }
751
752        table
753    }
754}
755
756/// Parses each source file with solar and returns the library names declared in it.
757///
758/// Files that fail to parse are skipped, so a missing entry means "unknown", not "no libraries".
759fn collect_libraries(sources: &BTreeSet<PathBuf>) -> HashMap<PathBuf, HashSet<String>> {
760    let mut result: HashMap<PathBuf, HashSet<String>> = HashMap::new();
761    let sess = Session::builder().with_silent_emitter(None).build();
762    let _ = sess.enter(|| -> solar::interface::Result<()> {
763        for path in sources {
764            let arena = Arena::new();
765            let mut parser = match Parser::from_lazy_source_code(
766                &sess,
767                &arena,
768                FileName::from(path.clone()),
769                || std::fs::read_to_string(path),
770            ) {
771                Ok(parser) => parser,
772                Err(_) => continue,
773            };
774            let Ok(ast) = parser.parse_file() else { continue };
775            let libs = ast
776                .items
777                .iter()
778                .filter_map(|item| match &item.kind {
779                    ItemKind::Contract(c) if c.kind == ContractKind::Library => {
780                        Some(c.name.as_str().to_string())
781                    }
782                    _ => None,
783                })
784                .collect::<HashSet<_>>();
785            if !libs.is_empty() {
786                result.insert(path.clone(), libs);
787            }
788        }
789        Ok(())
790    });
791    result
792}
793
794/// Returns the deployed or init size of the contract.
795fn contract_size<T: Artifact>(artifact: &T, initcode: bool) -> Option<usize> {
796    let bytecode = if initcode {
797        artifact.get_bytecode_object()?
798    } else {
799        artifact.get_deployed_bytecode_object()?
800    };
801
802    let size = match bytecode.as_ref() {
803        BytecodeObject::Bytecode(bytes) => bytes.len(),
804        BytecodeObject::Unlinked(unlinked) => {
805            // we don't need to account for placeholders here, because library placeholders take up
806            // 40 characters: `__$<library hash>$__` which is the same as a 20byte address in hex.
807            let mut size = unlinked.len();
808            if unlinked.starts_with("0x") {
809                size -= 2;
810            }
811            // hex -> bytes
812            size / 2
813        }
814    };
815
816    Some(size)
817}
818
819/// How big the contract is and whether it is a dev contract where size limits can be neglected
820#[derive(Clone, Copy, Debug)]
821pub struct ContractInfo {
822    /// Size of the runtime code in bytes
823    pub runtime_size: usize,
824    /// Size of the initcode in bytes
825    pub init_size: usize,
826    /// A development contract is either a Script or a Test contract.
827    pub is_dev_contract: bool,
828}
829
830/// Compiles target file path.
831///
832/// If `quiet` is set, the compilation reporter's progress/status output is suppressed.
833/// (When not suppressed, that output is emitted to stderr; see `with_compilation_reporter`.)
834///
835/// **Note:** this expects the `target_path` to be absolute
836pub fn compile_target<C: Compiler<CompilerContract = Contract>>(
837    target_path: &Path,
838    project: &Project<C>,
839    quiet: bool,
840) -> Result<ProjectCompileOutput<C>>
841where
842    DynamicTestLinkingPreprocessor: Preprocessor<C>,
843{
844    ProjectCompiler::new().quiet(quiet).files([target_path.into()]).compile(project)
845}
846
847/// Compiles the project requesting only ABI output.
848pub fn compile_abi_project<C: Compiler<CompilerContract = Contract>>(
849    project: &mut Project<C>,
850    mut compiler: ProjectCompiler,
851) -> Result<ProjectCompileOutput<C>>
852where
853    DynamicTestLinkingPreprocessor: Preprocessor<C>,
854{
855    project.update_output_selection(|selection| {
856        // Request ABI so compilers populate `contracts` without producing bytecode outputs.
857        *selection = OutputSelection::common_output_selection(["abi".to_string()]);
858    });
859    // Cached ABI artifacts do not retain compiler diagnostics.
860    compiler.abi_cache = (compiler.abi_cache || project.no_artifacts)
861        && project.compiler_severity_filter == Severity::Error;
862    compiler.compile(project)
863}
864
865/// Acquires ABI output with compiler-owned persistence separate from normal artifacts.
866///
867/// Requests for additional files or full build info retain their existing output behavior.
868pub fn compile_abi_project_cached<C: Compiler<CompilerContract = Contract>>(
869    project: &mut Project<C>,
870    mut compiler: ProjectCompiler,
871) -> Result<ProjectCompileOutput<C>>
872where
873    DynamicTestLinkingPreprocessor: Preprocessor<C>,
874{
875    if !project.cached
876        || project.build_info
877        || project.artifacts.additional_files != Default::default()
878        || project.compiler_severity_filter != Severity::Error
879    {
880        return compile_abi_project(project, compiler);
881    }
882    let mut cached_project = project.clone();
883    cached_project.no_artifacts = false;
884    compiler.abi_cache = true;
885    compiler.external_writes &= !project.no_artifacts;
886    compile_abi_project(&mut cached_project, compiler)
887}
888
889/// Compiles the target contract requesting only ABI output and returns its ABI.
890pub fn compile_target_abi(
891    project: &mut Project<MultiCompiler>,
892    target_path: &Path,
893    target_name: &str,
894) -> Result<JsonAbi> {
895    let target_path = canonicalize_path(target_path)?;
896    let output = compile_abi_project(
897        project,
898        ProjectCompiler::new().quiet(true).files([target_path.clone()]),
899    )?;
900
901    let artifact = output
902        .find(&target_path, target_name)
903        .ok_or_eyre("failed to find target artifact when compiling for abi")?;
904    artifact.abi.clone().ok_or_eyre("target artifact does not have an ABI")
905}
906
907/// Creates a [Project] from an Etherscan source.
908pub fn etherscan_project(metadata: &Metadata, target_path: &Path) -> Result<Project> {
909    let target_path = canonicalize_path(target_path)?;
910    let sources_path = target_path.join(&metadata.contract_name);
911    metadata.source_tree().write_to(&target_path)?;
912
913    let mut settings = metadata.settings()?;
914
915    // make remappings absolute with our root
916    //
917    // The remappings come from the explorer's copy of the contract's compiler settings, which is
918    // attacker controlled: a target of `../../..` would otherwise point the compiler at sources
919    // outside the checkout and let a verified contract pull arbitrary local files into the build.
920    for remapping in &mut settings.remappings {
921        let new_path = sources_path.join(sanitize_relative_path(remapping.path.as_ref()));
922        remapping.path = new_path.display().to_string();
923    }
924
925    // add missing remappings
926    if !settings.remappings.iter().any(|remapping| remapping.name.starts_with("@openzeppelin/")) {
927        let oz = Remapping {
928            context: None,
929            name: "@openzeppelin/".into(),
930            path: sources_path.join("@openzeppelin").display().to_string(),
931        };
932        settings.remappings.push(oz);
933    }
934
935    // root/
936    //   ContractName/
937    //     [source code]
938    let paths = ProjectPathsConfig::builder()
939        .sources(sources_path.clone())
940        .remappings(settings.remappings.clone())
941        .build_with_root(sources_path);
942
943    // TODO: detect vyper
944    let v = metadata.compiler_version()?;
945    let solc = Solc::find_or_install(&v)?;
946
947    let compiler = MultiCompiler { solc: Some(SolcCompiler::Specific(solc)), vyper: None };
948
949    Ok(ProjectBuilder::<MultiCompiler>::default()
950        .settings(MultiCompilerSettings {
951            solc: SolcSettings {
952                settings: SolcConfig::builder().settings(settings).build(),
953                ..Default::default()
954            },
955            ..Default::default()
956        })
957        .paths(paths)
958        .ephemeral()
959        .no_artifacts()
960        .build(compiler)?)
961}
962
963/// Normalizes an untrusted path relative to the source root, discarding leading separators,
964/// drive prefixes, and parent components that would escape that root.
965fn sanitize_relative_path(path: &Path) -> PathBuf {
966    let mut normalized = PathBuf::new();
967    for component in path.components() {
968        match component {
969            Component::Normal(part) => normalized.push(part),
970            Component::ParentDir => {
971                normalized.pop();
972            }
973            _ => {}
974        }
975    }
976    normalized
977}
978
979/// Adds `storageLayout` to the compiler output selection for the given project.
980pub fn add_storage_layout_output<C: Compiler<CompilerContract = Contract>>(
981    project: &mut Project<C>,
982) {
983    project.artifacts.additional_values.storage_layout = true;
984    project.update_output_selection(|selection| {
985        for contract_selection in selection.0.values_mut() {
986            for selection in contract_selection.values_mut() {
987                selection.push("storageLayout".to_string());
988            }
989        }
990    })
991}
992
993/// Configures the reporter and runs the given closure.
994///
995/// In TTY mode, [`SpinnerReporter`] paints the progress to stderr. The non-TTY fallback
996/// still writes to stdout via `BasicStdoutReporter`; migrating that path to stderr is
997/// part of the per-command stdout migration tracked in `docs/dev/output-channels.md`
998/// (it would shift many existing snapshot tests at once).
999pub fn with_compilation_reporter<O>(
1000    quiet: bool,
1001    project_root: Option<PathBuf>,
1002    f: impl FnOnce() -> O,
1003) -> O {
1004    with_compilation_reporter_and_settings(quiet, project_root, false, f)
1005}
1006
1007fn with_compilation_reporter_and_settings<O>(
1008    quiet: bool,
1009    project_root: Option<PathBuf>,
1010    print_compiler_settings: bool,
1011    f: impl FnOnce() -> O,
1012) -> O {
1013    #[expect(clippy::collapsible_else_if)]
1014    let reporter = if quiet || shell::is_json() {
1015        Report::new(NoReporter::default())
1016    } else {
1017        if std::io::stderr().is_terminal() {
1018            Report::new(
1019                SpinnerReporter::spawn(project_root)
1020                    .with_compiler_settings(print_compiler_settings),
1021            )
1022        } else {
1023            Report::new(
1024                BasicStdoutReporter::default().with_compiler_settings(print_compiler_settings),
1025            )
1026        }
1027    };
1028
1029    foundry_compilers::report::with_scoped(&reporter, f)
1030}
1031
1032/// Container type for parsing contract identifiers from CLI.
1033///
1034/// Passed string can be of the following forms:
1035/// - `src/Counter.sol` - path to the contract file, in the case where it only contains one contract
1036/// - `src/Counter.sol:Counter` - path to the contract file and the contract name
1037/// - `Counter` - contract name only
1038#[derive(Clone, PartialEq, Eq)]
1039pub enum PathOrContractInfo {
1040    /// Non-canonicalized path provided via CLI.
1041    Path(PathBuf),
1042    /// Contract info provided via CLI.
1043    ContractInfo(CompilerContractInfo),
1044}
1045
1046impl PathOrContractInfo {
1047    /// Returns the path to the contract file if provided.
1048    pub fn path(&self) -> Option<PathBuf> {
1049        match self {
1050            Self::Path(path) => Some(path.clone()),
1051            Self::ContractInfo(info) => info.path.as_ref().map(PathBuf::from),
1052        }
1053    }
1054
1055    /// Returns the contract name if provided.
1056    pub fn name(&self) -> Option<&str> {
1057        match self {
1058            Self::Path(_) => None,
1059            Self::ContractInfo(info) => Some(&info.name),
1060        }
1061    }
1062}
1063
1064impl FromStr for PathOrContractInfo {
1065    type Err = eyre::Error;
1066
1067    fn from_str(s: &str) -> Result<Self> {
1068        if let Ok(contract) = CompilerContractInfo::from_str(s) {
1069            return Ok(Self::ContractInfo(contract));
1070        }
1071        let path = PathBuf::from(s);
1072        if path.extension().is_some_and(|ext| ext == "sol" || ext == "vy") {
1073            return Ok(Self::Path(path));
1074        }
1075        Err(eyre::eyre!("Invalid contract identifier, file is not *.sol or *.vy: {}", s))
1076    }
1077}
1078
1079impl std::fmt::Debug for PathOrContractInfo {
1080    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1081        match self {
1082            Self::Path(path) => write!(f, "Path({})", path.display()),
1083            Self::ContractInfo(info) => {
1084                write!(f, "ContractInfo({info})")
1085            }
1086        }
1087    }
1088}
1089
1090/// Describes the Solidity library dependencies in creation or runtime bytecode, if any.
1091///
1092/// Includes cached artifacts and permits internal libraries and dynamic test linking helpers.
1093fn linked_libraries_error<C: Compiler<CompilerContract = Contract>>(
1094    output: &ProjectCompileOutput<C>,
1095    root: &Path,
1096) -> Option<String> {
1097    let violations = output
1098        .artifact_ids()
1099        .flat_map(|(id, artifact)| {
1100            let contract = id.with_stripped_file_prefixes(root).identifier();
1101            artifact.all_link_references().into_iter().flat_map(move |(file, libs)| {
1102                let contract = contract.clone();
1103                libs.into_keys().map(move |lib| format!("{contract} links {file}:{lib}"))
1104            })
1105        })
1106        .collect::<BTreeSet<_>>();
1107    (!violations.is_empty()).then(|| {
1108        format!(
1109            "Linked libraries are not allowed (`allow_linked_libraries = false`):\n  {}\n\
1110             help: make the library functions `internal` to inline them, or set \
1111             `allow_linked_libraries = true`",
1112            violations.into_iter().collect::<Vec<_>>().join("\n  ")
1113        )
1114    })
1115}
1116
1117/// Appends a linked-library policy error to solc or multi-compiler diagnostics.
1118fn push_linked_libraries_error<C: Compiler<CompilerContract = Contract>>(
1119    output: &mut ProjectCompileOutput<C>,
1120    message: String,
1121) {
1122    let error = SolcError {
1123        source_location: None,
1124        secondary_source_locations: Vec::new(),
1125        r#type: "LinkedLibraryError".to_string(),
1126        component: "foundry".to_string(),
1127        severity: Severity::Error,
1128        error_code: None,
1129        message,
1130        formatted_message: None,
1131    };
1132    // CompilationError has no constructor; downcast to the concrete diagnostics instead.
1133    let errors = &mut output.output_mut().errors as &mut dyn Any;
1134    if let Some(errors) = errors.downcast_mut::<Vec<SolcError>>() {
1135        errors.push(error);
1136    } else if let Some(errors) = errors.downcast_mut::<Vec<MultiCompilerError>>() {
1137        errors.push(MultiCompilerError::Solc(error));
1138    }
1139}
1140
1141/// Recognizes only solc code-generation failures that DTL's source rewrites can introduce.
1142/// Unrelated errors (including other languages) must not trigger a retry.
1143fn dynamic_test_linking_failure<E: CompilationError>(errors: &[E]) -> Option<&'static str> {
1144    let mut reason = None;
1145    for error in errors.iter().filter(|error| error.is_error()) {
1146        // CompilationError has no structured message/type accessors. Inspect the concrete solc
1147        // diagnostic rather than its Display output, which can include source snippets.
1148        let error = error as &dyn Any;
1149        let error = if let Some(error) = error.downcast_ref::<SolcError>() {
1150            error
1151        } else if let Some(MultiCompilerError::Solc(error)) =
1152            error.downcast_ref::<MultiCompilerError>()
1153        {
1154            error
1155        } else {
1156            return None;
1157        };
1158        reason = Some(match error.r#type.as_str() {
1159            "YulException" if error.message.contains("too deep in the stack") => "stack too deep",
1160            "CompilerError" if error.message.contains("Stack too deep") => "stack too deep",
1161            "InternalCompilerError"
1162                if error.message.contains("Tag too large for reserved space") =>
1163            {
1164                "Tag too large for reserved space"
1165            }
1166            _ => return None,
1167        });
1168    }
1169    reason
1170}
1171
1172#[cfg(test)]
1173mod tests {
1174    use super::*;
1175
1176    #[test]
1177    fn parse_contract_identifiers() {
1178        let t = ["src/Counter.sol", "src/Counter.sol:Counter", "Counter"];
1179
1180        let i1 = PathOrContractInfo::from_str(t[0]).unwrap();
1181        assert_eq!(i1, PathOrContractInfo::Path(PathBuf::from(t[0])));
1182
1183        let i2 = PathOrContractInfo::from_str(t[1]).unwrap();
1184        assert_eq!(
1185            i2,
1186            PathOrContractInfo::ContractInfo(CompilerContractInfo {
1187                path: Some("src/Counter.sol".to_string()),
1188                name: "Counter".to_string()
1189            })
1190        );
1191
1192        let i3 = PathOrContractInfo::from_str(t[2]).unwrap();
1193        assert_eq!(
1194            i3,
1195            PathOrContractInfo::ContractInfo(CompilerContractInfo {
1196                path: None,
1197                name: "Counter".to_string()
1198            })
1199        );
1200    }
1201
1202    #[test]
1203    fn size_report_uses_configured_limits() {
1204        let mut contracts = BTreeMap::new();
1205        contracts.insert(
1206            "LargeContract".to_string(),
1207            ContractInfo { runtime_size: 30_000, init_size: 60_000, is_dev_contract: false },
1208        );
1209
1210        let default_report =
1211            SizeReport { contracts: contracts.clone(), limits: ContractSizeLimits::default() };
1212        assert!(default_report.exceeds_runtime_size_limit());
1213        assert!(default_report.exceeds_initcode_size_limit());
1214
1215        let custom_report =
1216            SizeReport { contracts, limits: ContractSizeLimits::new(131_072, 262_144) };
1217        assert!(!custom_report.exceeds_runtime_size_limit());
1218        assert!(!custom_report.exceeds_initcode_size_limit());
1219        let output: serde_json::Value =
1220            serde_json::from_str(&custom_report.format_json_output()).unwrap();
1221        assert_eq!(
1222            output,
1223            serde_json::json!({
1224                "LargeContract": {
1225                    "runtime_size": 30000,
1226                    "init_size": 60000,
1227                    "runtime_margin": 101072,
1228                    "init_margin": 202144,
1229                }
1230            })
1231        );
1232    }
1233
1234    #[test]
1235    fn contract_size_limits_derive_initcode_limit_from_runtime_limit() {
1236        assert_eq!(
1237            ContractSizeLimits::with_runtime_limit(50_000),
1238            ContractSizeLimits::new(50_000, 100_000)
1239        );
1240    }
1241
1242    #[test]
1243    fn contract_size_limits_follow_evm_spec() {
1244        assert_eq!(ContractSizeLimits::for_spec_id(SpecId::OSAKA), ContractSizeLimits::default());
1245        assert_eq!(
1246            ContractSizeLimits::for_spec_id(SpecId::AMSTERDAM),
1247            ContractSizeLimits::new(65_536, 131_072)
1248        );
1249    }
1250
1251    #[test]
1252    fn sanitized_remapping_paths_stay_inside_the_root() {
1253        let root = Path::new("/tmp/sources");
1254        for path in ["../../../etc", "a/../../../etc", "/etc", "./a/../b"] {
1255            let joined = root.join(sanitize_relative_path(Path::new(path)));
1256            assert!(
1257                joined.starts_with(root) && !joined.components().any(|c| c == Component::ParentDir),
1258                "{path} escaped the root: {}",
1259                joined.display()
1260            );
1261        }
1262
1263        for (path, expected) in [("src/../lib/", "lib"), ("./a/../b", "b"), ("a/../../lib", "lib")]
1264        {
1265            assert_eq!(sanitize_relative_path(Path::new(path)), Path::new(expected));
1266        }
1267
1268        // Ordinary relative paths are left alone.
1269        assert_eq!(
1270            sanitize_relative_path(Path::new("lib/openzeppelin")),
1271            Path::new("lib/openzeppelin")
1272        );
1273    }
1274}