1use crate::{
4 TestFunctionExt, preprocessor::DynamicTestLinkingPreprocessor, shell, term::SpinnerReporter,
5};
6use alloy_json_abi::JsonAbi;
7use comfy_table::{
8 Cell, Color, Table,
9 presets::{ASCII_FULL, ASCII_MARKDOWN},
10};
11use eyre::{OptionExt, Result};
12use foundry_block_explorers::contract::Metadata;
13use foundry_compilers::{
14 Artifact, Project, ProjectBuilder, ProjectCompileOutput, ProjectPathsConfig, SolcConfig,
15 artifacts::{
16 BytecodeObject, Contract, Source, output_selection::OutputSelection, remappings::Remapping,
17 },
18 compilers::{
19 Compiler,
20 solc::{Solc, SolcCompiler},
21 },
22 info::ContractInfo as CompilerContractInfo,
23 multi::{MultiCompiler, MultiCompilerSettings},
24 project::Preprocessor,
25 report::{BasicStdoutReporter, NoReporter, Report},
26 solc::SolcSettings,
27};
28use num_format::{Locale, ToFormattedString};
29use revm::primitives::{eip170, eip3860, hardfork::SpecId};
30use solar::{
31 ast::{Arena, ContractKind, ItemKind},
32 interface::{Session, source_map::FileName},
33 parse::Parser,
34};
35use std::{
36 collections::{BTreeMap, BTreeSet, HashMap, HashSet},
37 fmt::Display,
38 io::IsTerminal,
39 path::{Component, Path, PathBuf},
40 str::FromStr,
41 sync::Arc,
42 time::Instant,
43};
44
45pub type Analysis = Arc<solar::sema::Compiler>;
47
48#[must_use = "ProjectCompiler does nothing unless you call a `compile*` method"]
53pub struct ProjectCompiler {
54 project_root: PathBuf,
56
57 print_names: Option<bool>,
59
60 print_sizes: Option<bool>,
62
63 quiet: Option<bool>,
65
66 print_compiler_settings: bool,
68
69 bail: Option<bool>,
71
72 ignore_eip_3860: bool,
74
75 size_limits: ContractSizeLimits,
77
78 files: Vec<PathBuf>,
80
81 dynamic_test_linking: bool,
83
84 abi_cache: bool,
86}
87
88impl Default for ProjectCompiler {
89 #[inline]
90 fn default() -> Self {
91 Self::new()
92 }
93}
94
95impl ProjectCompiler {
96 #[inline]
98 pub fn new() -> Self {
99 Self {
100 project_root: PathBuf::new(),
101 print_names: None,
102 print_sizes: None,
103 quiet: Some(crate::shell::is_quiet()),
104 print_compiler_settings: false,
105 bail: None,
106 ignore_eip_3860: false,
107 size_limits: ContractSizeLimits::default(),
108 files: Vec::new(),
109 dynamic_test_linking: false,
110 abi_cache: false,
111 }
112 }
113
114 #[inline]
116 pub const fn print_names(mut self, yes: bool) -> Self {
117 self.print_names = Some(yes);
118 self
119 }
120
121 #[inline]
123 pub const fn print_sizes(mut self, yes: bool) -> Self {
124 self.print_sizes = Some(yes);
125 self
126 }
127
128 #[inline]
130 #[doc(alias = "silent")]
131 pub const fn quiet(mut self, yes: bool) -> Self {
132 self.quiet = Some(yes);
133 self
134 }
135
136 #[inline]
138 pub const fn print_compiler_settings(mut self, yes: bool) -> Self {
139 self.print_compiler_settings = yes;
140 self
141 }
142
143 #[inline]
145 pub const fn bail(mut self, yes: bool) -> Self {
146 self.bail = Some(yes);
147 self
148 }
149
150 #[inline]
152 pub const fn ignore_eip_3860(mut self, yes: bool) -> Self {
153 self.ignore_eip_3860 = yes;
154 self
155 }
156
157 #[inline]
159 pub const fn size_limits(mut self, limits: ContractSizeLimits) -> Self {
160 self.size_limits = limits;
161 self
162 }
163
164 #[inline]
166 pub fn files(mut self, files: impl IntoIterator<Item = PathBuf>) -> Self {
167 self.files.extend(files);
168 self
169 }
170
171 #[inline]
173 pub const fn dynamic_test_linking(mut self, preprocess: bool) -> Self {
174 self.dynamic_test_linking = preprocess;
175 self
176 }
177
178 #[instrument(target = "forge::compile", skip_all)]
180 pub fn compile<C: Compiler<CompilerContract = Contract>>(
181 mut self,
182 project: &Project<C>,
183 ) -> Result<ProjectCompileOutput<C>>
184 where
185 DynamicTestLinkingPreprocessor: Preprocessor<C>,
186 {
187 self.project_root = project.root().to_path_buf();
188
189 if !project.paths.has_input_files() && self.files.is_empty() {
196 sh_println!("Nothing to compile")?;
197 std::process::exit(0);
198 }
199
200 let files = std::mem::take(&mut self.files);
202 let preprocess = self.dynamic_test_linking;
203 let abi_cache = self.abi_cache;
204 self.compile_with(|| {
205 let sources = if files.is_empty() {
206 project.paths.read_input_files()?
207 } else {
208 Source::read_all(files)?
209 };
210
211 let mut compiler =
212 foundry_compilers::project::ProjectCompiler::with_sources(project, sources)?;
213 if preprocess {
214 compiler = compiler.with_preprocessor(DynamicTestLinkingPreprocessor);
215 }
216 if abi_cache {
217 compiler.compile_abi_cached().map_err(Into::into)
218 } else {
219 compiler.compile().map_err(Into::into)
220 }
221 })
222 }
223
224 fn compile_with<C: Compiler<CompilerContract = Contract>, F>(
226 self,
227 f: F,
228 ) -> Result<ProjectCompileOutput<C>>
229 where
230 F: FnOnce() -> Result<ProjectCompileOutput<C>>,
231 {
232 let quiet = self.quiet.unwrap_or(false);
233 let bail = self.bail.unwrap_or(true);
234
235 let output = with_compilation_reporter_and_settings(
236 quiet,
237 Some(self.project_root.clone()),
238 self.print_compiler_settings,
239 || {
240 tracing::debug!("compiling project");
241
242 let timer = Instant::now();
243 let r = f();
244 let elapsed = timer.elapsed();
245
246 tracing::debug!("finished compiling in {:.3}s", elapsed.as_secs_f64());
247 r
248 },
249 )?;
250
251 if bail && output.has_compiler_errors() {
252 eyre::bail!("{output}");
253 }
254
255 if !quiet {
256 if !shell::is_json() {
257 if output.is_unchanged() {
258 sh_println!("No files changed, compilation skipped")?;
259 } else {
260 sh_println!("{output}")?;
262 }
263 }
264
265 if !(shell::is_json() && output.has_compiler_errors()) {
266 self.handle_output(&output)?;
267 }
268 }
269
270 Ok(output)
271 }
272
273 fn handle_output<C: Compiler<CompilerContract = Contract>>(
275 &self,
276 output: &ProjectCompileOutput<C>,
277 ) -> Result<()> {
278 let print_names = self.print_names.unwrap_or(false);
279 let print_sizes = self.print_sizes.unwrap_or(false);
280
281 if print_names {
283 let mut artifacts: BTreeMap<_, Vec<_>> = BTreeMap::new();
284 for (name, (_, version)) in output.versioned_artifacts() {
285 artifacts.entry(version).or_default().push(name);
286 }
287
288 if shell::is_json() {
289 sh_println!("{}", serde_json::to_string(&artifacts).unwrap())?;
290 } else {
291 for (version, names) in artifacts {
292 sh_println!(
293 " compiler version: {}.{}.{}",
294 version.major,
295 version.minor,
296 version.patch
297 )?;
298 for name in names {
299 sh_println!(" - {name}")?;
300 }
301 }
302 }
303 }
304
305 if print_sizes {
306 if print_names && !shell::is_json() {
308 sh_println!()?;
309 }
310
311 let mut size_report =
312 SizeReport { contracts: BTreeMap::new(), limits: self.size_limits };
313
314 let mut artifacts: BTreeMap<String, Vec<_>> = BTreeMap::new();
315 for (id, artifact) in output.artifact_ids().filter(|(id, _)| {
316 !id.source.to_string_lossy().contains("/forge-std/src/")
318 }) {
319 artifacts.entry(id.name.clone()).or_default().push((id.source, artifact));
320 }
321
322 let abs_source = |path: &Path| -> PathBuf {
326 if path.is_absolute() { path.to_path_buf() } else { self.project_root.join(path) }
327 };
328 let source_paths = artifacts
329 .values()
330 .flatten()
331 .filter(|(_, artifact)| {
332 artifact.abi.as_ref().is_some_and(|abi| abi.functions().next().is_none())
333 })
334 .map(|(path, _)| abs_source(path))
335 .collect::<BTreeSet<_>>();
336 let libraries = collect_libraries(&source_paths);
337
338 for (name, artifact_list) in artifacts {
339 let kept = artifact_list
342 .iter()
343 .filter(|(path, artifact)| {
344 let is_library = libraries
345 .get(&abs_source(path))
346 .is_some_and(|libs| libs.contains(&name));
347 let has_no_abi_functions = artifact
348 .abi
349 .as_ref()
350 .is_some_and(|abi| abi.functions().next().is_none());
351 !(is_library && has_no_abi_functions)
352 })
353 .collect::<Vec<_>>();
354
355 for (path, artifact) in &kept {
356 let runtime_size = contract_size(*artifact, false).unwrap_or_default();
357 let init_size = contract_size(*artifact, true).unwrap_or_default();
358
359 let is_dev_contract = artifact
360 .abi
361 .as_ref()
362 .map(|abi| {
363 abi.functions().any(|f| {
364 f.test_function_kind().is_known()
365 || matches!(f.name.as_str(), "IS_TEST" | "IS_SCRIPT")
366 })
367 })
368 .unwrap_or(false);
369
370 let unique_name = if kept.len() > 1 {
371 format!(
372 "{} ({})",
373 name,
374 path.strip_prefix(&self.project_root).unwrap_or(path).display()
375 )
376 } else {
377 name.clone()
378 };
379
380 size_report.contracts.insert(
381 unique_name,
382 ContractInfo { runtime_size, init_size, is_dev_contract },
383 );
384 }
385 }
386
387 sh_println!("{size_report}")?;
388
389 let runtime_eip = match size_report.limits.runtime {
390 CONTRACT_RUNTIME_SIZE_LIMIT => "EIP-170: ",
391 AMSTERDAM_CONTRACT_RUNTIME_SIZE_LIMIT => "EIP-7954: ",
392 _ => "",
393 };
394 eyre::ensure!(
395 !size_report.exceeds_runtime_size_limit(),
396 "some contracts exceed the runtime size limit ({runtime_eip}{} bytes)",
397 size_report.limits.runtime
398 );
399 let initcode_eip = match size_report.limits.initcode {
401 CONTRACT_INITCODE_SIZE_LIMIT => "EIP-3860: ",
402 AMSTERDAM_CONTRACT_INITCODE_SIZE_LIMIT => "EIP-7954: ",
403 _ => "",
404 };
405 eyre::ensure!(
406 self.ignore_eip_3860 || !size_report.exceeds_initcode_size_limit(),
407 "some contracts exceed the initcode size limit ({initcode_eip}{} bytes)",
408 size_report.limits.initcode
409 );
410 }
411
412 Ok(())
413 }
414}
415
416const CONTRACT_RUNTIME_SIZE_LIMIT: usize = eip170::MAX_CODE_SIZE;
418
419const CONTRACT_INITCODE_SIZE_LIMIT: usize = eip3860::MAX_INITCODE_SIZE;
421
422const AMSTERDAM_CONTRACT_RUNTIME_SIZE_LIMIT: usize = 65_536;
424const AMSTERDAM_CONTRACT_INITCODE_SIZE_LIMIT: usize = 131_072;
425
426const CONTRACT_RUNTIME_SIZE_WARN_THRESHOLD: usize = 18_000;
427const CONTRACT_INITCODE_SIZE_WARN_THRESHOLD: usize = 36_000;
428
429#[derive(Clone, Copy, Debug, PartialEq, Eq)]
431pub struct ContractSizeLimits {
432 pub runtime: usize,
434 pub initcode: usize,
436}
437
438impl ContractSizeLimits {
439 pub const fn new(runtime: usize, initcode: usize) -> Self {
441 Self { runtime, initcode }
442 }
443
444 pub const fn with_runtime_limit(runtime: usize) -> Self {
446 Self { runtime, initcode: runtime.saturating_mul(2) }
447 }
448
449 pub const fn for_spec_id(spec_id: SpecId) -> Self {
451 if spec_id.is_enabled_in(SpecId::AMSTERDAM) {
452 Self::new(AMSTERDAM_CONTRACT_RUNTIME_SIZE_LIMIT, AMSTERDAM_CONTRACT_INITCODE_SIZE_LIMIT)
453 } else {
454 Self::new(CONTRACT_RUNTIME_SIZE_LIMIT, CONTRACT_INITCODE_SIZE_LIMIT)
455 }
456 }
457
458 const fn runtime_warning_threshold(self) -> usize {
459 scaled_threshold(
460 self.runtime,
461 CONTRACT_RUNTIME_SIZE_WARN_THRESHOLD,
462 CONTRACT_RUNTIME_SIZE_LIMIT,
463 )
464 }
465
466 const fn initcode_warning_threshold(self) -> usize {
467 scaled_threshold(
468 self.initcode,
469 CONTRACT_INITCODE_SIZE_WARN_THRESHOLD,
470 CONTRACT_INITCODE_SIZE_LIMIT,
471 )
472 }
473}
474
475impl Default for ContractSizeLimits {
476 fn default() -> Self {
477 Self::new(CONTRACT_RUNTIME_SIZE_LIMIT, CONTRACT_INITCODE_SIZE_LIMIT)
478 }
479}
480
481const fn scaled_threshold(limit: usize, threshold: usize, default_limit: usize) -> usize {
482 limit.saturating_mul(threshold) / default_limit
483}
484
485pub struct SizeReport {
487 pub contracts: BTreeMap<String, ContractInfo>,
489 pub limits: ContractSizeLimits,
491}
492
493impl SizeReport {
494 pub fn max_runtime_size(&self) -> usize {
496 self.contracts
497 .values()
498 .filter(|c| !c.is_dev_contract)
499 .map(|c| c.runtime_size)
500 .max()
501 .unwrap_or(0)
502 }
503
504 pub fn max_init_size(&self) -> usize {
506 self.contracts
507 .values()
508 .filter(|c| !c.is_dev_contract)
509 .map(|c| c.init_size)
510 .max()
511 .unwrap_or(0)
512 }
513
514 pub fn exceeds_runtime_size_limit(&self) -> bool {
516 self.max_runtime_size() > self.limits.runtime
517 }
518
519 pub fn exceeds_initcode_size_limit(&self) -> bool {
521 self.max_init_size() > self.limits.initcode
522 }
523}
524
525impl Display for SizeReport {
526 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
527 if shell::is_json() {
528 writeln!(f, "{}", self.format_json_output())?;
529 } else {
530 writeln!(f, "\n{}", self.format_table_output())?;
531 }
532 Ok(())
533 }
534}
535
536impl SizeReport {
537 fn format_json_output(&self) -> String {
538 let contracts = self
539 .contracts
540 .iter()
541 .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0))
542 .map(|(name, contract)| {
543 (
544 name.clone(),
545 serde_json::json!({
546 "runtime_size": contract.runtime_size,
547 "init_size": contract.init_size,
548 "runtime_margin": self.limits.runtime as isize - contract.runtime_size as isize,
549 "init_margin": self.limits.initcode as isize - contract.init_size as isize,
550 }),
551 )
552 })
553 .collect::<serde_json::Map<_, _>>();
554
555 serde_json::to_string(&contracts).unwrap()
556 }
557
558 fn format_table_output(&self) -> Table {
559 let mut table = Table::new();
560 if shell::is_markdown() {
561 table.load_style(ASCII_MARKDOWN);
562 } else {
563 table.load_style(ASCII_FULL.with_rounded_corners());
564 }
565
566 table.set_header(vec![
567 Cell::new("Contract"),
568 Cell::new("Runtime Size (B)"),
569 Cell::new("Initcode Size (B)"),
570 Cell::new("Runtime Margin (B)"),
571 Cell::new("Initcode Margin (B)"),
572 ]);
573
574 let contracts = self
576 .contracts
577 .iter()
578 .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0));
579 let runtime_warning_threshold = self.limits.runtime_warning_threshold();
580 let initcode_warning_threshold = self.limits.initcode_warning_threshold();
581 for (name, contract) in contracts {
582 let runtime_margin = self.limits.runtime as isize - contract.runtime_size as isize;
583 let init_margin = self.limits.initcode as isize - contract.init_size as isize;
584
585 let runtime_color = if contract.runtime_size < runtime_warning_threshold {
586 Color::Reset
587 } else if contract.runtime_size <= self.limits.runtime {
588 Color::Yellow
589 } else {
590 Color::Red
591 };
592
593 let init_color = if contract.init_size < initcode_warning_threshold {
594 Color::Reset
595 } else if contract.init_size <= self.limits.initcode {
596 Color::Yellow
597 } else {
598 Color::Red
599 };
600
601 let locale = &Locale::en;
602 table.add_row([
603 Cell::new(name),
604 Cell::new(contract.runtime_size.to_formatted_string(locale)).fg(runtime_color),
605 Cell::new(contract.init_size.to_formatted_string(locale)).fg(init_color),
606 Cell::new(runtime_margin.to_formatted_string(locale)).fg(runtime_color),
607 Cell::new(init_margin.to_formatted_string(locale)).fg(init_color),
608 ]);
609 }
610
611 table
612 }
613}
614
615fn collect_libraries(sources: &BTreeSet<PathBuf>) -> HashMap<PathBuf, HashSet<String>> {
619 let mut result: HashMap<PathBuf, HashSet<String>> = HashMap::new();
620 let sess = Session::builder().with_silent_emitter(None).build();
621 let _ = sess.enter(|| -> solar::interface::Result<()> {
622 for path in sources {
623 let arena = Arena::new();
624 let mut parser = match Parser::from_lazy_source_code(
625 &sess,
626 &arena,
627 FileName::from(path.clone()),
628 || std::fs::read_to_string(path),
629 ) {
630 Ok(parser) => parser,
631 Err(_) => continue,
632 };
633 let Ok(ast) = parser.parse_file() else { continue };
634 let libs = ast
635 .items
636 .iter()
637 .filter_map(|item| match &item.kind {
638 ItemKind::Contract(c) if c.kind == ContractKind::Library => {
639 Some(c.name.as_str().to_string())
640 }
641 _ => None,
642 })
643 .collect::<HashSet<_>>();
644 if !libs.is_empty() {
645 result.insert(path.clone(), libs);
646 }
647 }
648 Ok(())
649 });
650 result
651}
652
653fn contract_size<T: Artifact>(artifact: &T, initcode: bool) -> Option<usize> {
655 let bytecode = if initcode {
656 artifact.get_bytecode_object()?
657 } else {
658 artifact.get_deployed_bytecode_object()?
659 };
660
661 let size = match bytecode.as_ref() {
662 BytecodeObject::Bytecode(bytes) => bytes.len(),
663 BytecodeObject::Unlinked(unlinked) => {
664 let mut size = unlinked.len();
667 if unlinked.starts_with("0x") {
668 size -= 2;
669 }
670 size / 2
672 }
673 };
674
675 Some(size)
676}
677
678#[derive(Clone, Copy, Debug)]
680pub struct ContractInfo {
681 pub runtime_size: usize,
683 pub init_size: usize,
685 pub is_dev_contract: bool,
687}
688
689pub fn compile_target<C: Compiler<CompilerContract = Contract>>(
696 target_path: &Path,
697 project: &Project<C>,
698 quiet: bool,
699) -> Result<ProjectCompileOutput<C>>
700where
701 DynamicTestLinkingPreprocessor: Preprocessor<C>,
702{
703 ProjectCompiler::new().quiet(quiet).files([target_path.into()]).compile(project)
704}
705
706pub fn compile_abi_project<C: Compiler<CompilerContract = Contract>>(
708 project: &mut Project<C>,
709 mut compiler: ProjectCompiler,
710) -> Result<ProjectCompileOutput<C>>
711where
712 DynamicTestLinkingPreprocessor: Preprocessor<C>,
713{
714 project.update_output_selection(|selection| {
715 *selection = OutputSelection::common_output_selection(["abi".to_string()]);
717 });
718 compiler.abi_cache |= project.no_artifacts;
719 compiler.compile(project)
720}
721
722pub fn compile_abi_project_cached<C: Compiler<CompilerContract = Contract>>(
726 project: &mut Project<C>,
727 mut compiler: ProjectCompiler,
728) -> Result<ProjectCompileOutput<C>>
729where
730 DynamicTestLinkingPreprocessor: Preprocessor<C>,
731{
732 if !project.cached
733 || project.build_info
734 || project.artifacts.additional_files != Default::default()
735 {
736 return compile_abi_project(project, compiler);
737 }
738 let mut cached_project = project.clone();
739 cached_project.no_artifacts = false;
740 compiler.abi_cache = true;
741 compile_abi_project(&mut cached_project, compiler)
742}
743
744pub fn compile_target_abi(
746 project: &mut Project<MultiCompiler>,
747 target_path: &Path,
748 target_name: &str,
749) -> Result<JsonAbi> {
750 let target_path = dunce::canonicalize(target_path)?;
751 let output = compile_abi_project(
752 project,
753 ProjectCompiler::new().quiet(true).files([target_path.clone()]),
754 )?;
755
756 let artifact = output
757 .find(&target_path, target_name)
758 .ok_or_eyre("failed to find target artifact when compiling for abi")?;
759 artifact.abi.clone().ok_or_eyre("target artifact does not have an ABI")
760}
761
762pub fn etherscan_project(metadata: &Metadata, target_path: &Path) -> Result<Project> {
764 let target_path = dunce::canonicalize(target_path)?;
765 let sources_path = target_path.join(&metadata.contract_name);
766 metadata.source_tree().write_to(&target_path)?;
767
768 let mut settings = metadata.settings()?;
769
770 for remapping in &mut settings.remappings {
776 let new_path = sources_path.join(sanitize_relative_path(remapping.path.as_ref()));
777 remapping.path = new_path.display().to_string();
778 }
779
780 if !settings.remappings.iter().any(|remapping| remapping.name.starts_with("@openzeppelin/")) {
782 let oz = Remapping {
783 context: None,
784 name: "@openzeppelin/".into(),
785 path: sources_path.join("@openzeppelin").display().to_string(),
786 };
787 settings.remappings.push(oz);
788 }
789
790 let paths = ProjectPathsConfig::builder()
794 .sources(sources_path.clone())
795 .remappings(settings.remappings.clone())
796 .build_with_root(sources_path);
797
798 let v = metadata.compiler_version()?;
800 let solc = Solc::find_or_install(&v)?;
801
802 let compiler = MultiCompiler { solc: Some(SolcCompiler::Specific(solc)), vyper: None };
803
804 Ok(ProjectBuilder::<MultiCompiler>::default()
805 .settings(MultiCompilerSettings {
806 solc: SolcSettings {
807 settings: SolcConfig::builder().settings(settings).build(),
808 ..Default::default()
809 },
810 ..Default::default()
811 })
812 .paths(paths)
813 .ephemeral()
814 .no_artifacts()
815 .build(compiler)?)
816}
817
818fn sanitize_relative_path(path: &Path) -> PathBuf {
821 let mut normalized = PathBuf::new();
822 for component in path.components() {
823 match component {
824 Component::Normal(part) => normalized.push(part),
825 Component::ParentDir => {
826 normalized.pop();
827 }
828 _ => {}
829 }
830 }
831 normalized
832}
833
834pub fn add_storage_layout_output<C: Compiler<CompilerContract = Contract>>(
836 project: &mut Project<C>,
837) {
838 project.artifacts.additional_values.storage_layout = true;
839 project.update_output_selection(|selection| {
840 for contract_selection in selection.0.values_mut() {
841 for selection in contract_selection.values_mut() {
842 selection.push("storageLayout".to_string());
843 }
844 }
845 })
846}
847
848pub fn with_compilation_reporter<O>(
855 quiet: bool,
856 project_root: Option<PathBuf>,
857 f: impl FnOnce() -> O,
858) -> O {
859 with_compilation_reporter_and_settings(quiet, project_root, false, f)
860}
861
862fn with_compilation_reporter_and_settings<O>(
863 quiet: bool,
864 project_root: Option<PathBuf>,
865 print_compiler_settings: bool,
866 f: impl FnOnce() -> O,
867) -> O {
868 #[expect(clippy::collapsible_else_if)]
869 let reporter = if quiet || shell::is_json() {
870 Report::new(NoReporter::default())
871 } else {
872 if std::io::stderr().is_terminal() {
873 Report::new(
874 SpinnerReporter::spawn(project_root)
875 .with_compiler_settings(print_compiler_settings),
876 )
877 } else {
878 Report::new(
879 BasicStdoutReporter::default().with_compiler_settings(print_compiler_settings),
880 )
881 }
882 };
883
884 foundry_compilers::report::with_scoped(&reporter, f)
885}
886
887#[derive(Clone, PartialEq, Eq)]
894pub enum PathOrContractInfo {
895 Path(PathBuf),
897 ContractInfo(CompilerContractInfo),
899}
900
901impl PathOrContractInfo {
902 pub fn path(&self) -> Option<PathBuf> {
904 match self {
905 Self::Path(path) => Some(path.clone()),
906 Self::ContractInfo(info) => info.path.as_ref().map(PathBuf::from),
907 }
908 }
909
910 pub fn name(&self) -> Option<&str> {
912 match self {
913 Self::Path(_) => None,
914 Self::ContractInfo(info) => Some(&info.name),
915 }
916 }
917}
918
919impl FromStr for PathOrContractInfo {
920 type Err = eyre::Error;
921
922 fn from_str(s: &str) -> Result<Self> {
923 if let Ok(contract) = CompilerContractInfo::from_str(s) {
924 return Ok(Self::ContractInfo(contract));
925 }
926 let path = PathBuf::from(s);
927 if path.extension().is_some_and(|ext| ext == "sol" || ext == "vy") {
928 return Ok(Self::Path(path));
929 }
930 Err(eyre::eyre!("Invalid contract identifier, file is not *.sol or *.vy: {}", s))
931 }
932}
933
934impl std::fmt::Debug for PathOrContractInfo {
935 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
936 match self {
937 Self::Path(path) => write!(f, "Path({})", path.display()),
938 Self::ContractInfo(info) => {
939 write!(f, "ContractInfo({info})")
940 }
941 }
942 }
943}
944
945#[cfg(test)]
946mod tests {
947 use super::*;
948
949 #[test]
950 fn parse_contract_identifiers() {
951 let t = ["src/Counter.sol", "src/Counter.sol:Counter", "Counter"];
952
953 let i1 = PathOrContractInfo::from_str(t[0]).unwrap();
954 assert_eq!(i1, PathOrContractInfo::Path(PathBuf::from(t[0])));
955
956 let i2 = PathOrContractInfo::from_str(t[1]).unwrap();
957 assert_eq!(
958 i2,
959 PathOrContractInfo::ContractInfo(CompilerContractInfo {
960 path: Some("src/Counter.sol".to_string()),
961 name: "Counter".to_string()
962 })
963 );
964
965 let i3 = PathOrContractInfo::from_str(t[2]).unwrap();
966 assert_eq!(
967 i3,
968 PathOrContractInfo::ContractInfo(CompilerContractInfo {
969 path: None,
970 name: "Counter".to_string()
971 })
972 );
973 }
974
975 #[test]
976 fn size_report_uses_configured_limits() {
977 let mut contracts = BTreeMap::new();
978 contracts.insert(
979 "LargeContract".to_string(),
980 ContractInfo { runtime_size: 30_000, init_size: 60_000, is_dev_contract: false },
981 );
982
983 let default_report =
984 SizeReport { contracts: contracts.clone(), limits: ContractSizeLimits::default() };
985 assert!(default_report.exceeds_runtime_size_limit());
986 assert!(default_report.exceeds_initcode_size_limit());
987
988 let custom_report =
989 SizeReport { contracts, limits: ContractSizeLimits::new(131_072, 262_144) };
990 assert!(!custom_report.exceeds_runtime_size_limit());
991 assert!(!custom_report.exceeds_initcode_size_limit());
992 let output: serde_json::Value =
993 serde_json::from_str(&custom_report.format_json_output()).unwrap();
994 assert_eq!(
995 output,
996 serde_json::json!({
997 "LargeContract": {
998 "runtime_size": 30000,
999 "init_size": 60000,
1000 "runtime_margin": 101072,
1001 "init_margin": 202144,
1002 }
1003 })
1004 );
1005 }
1006
1007 #[test]
1008 fn contract_size_limits_derive_initcode_limit_from_runtime_limit() {
1009 assert_eq!(
1010 ContractSizeLimits::with_runtime_limit(50_000),
1011 ContractSizeLimits::new(50_000, 100_000)
1012 );
1013 }
1014
1015 #[test]
1016 fn contract_size_limits_follow_evm_spec() {
1017 assert_eq!(ContractSizeLimits::for_spec_id(SpecId::OSAKA), ContractSizeLimits::default());
1018 assert_eq!(
1019 ContractSizeLimits::for_spec_id(SpecId::AMSTERDAM),
1020 ContractSizeLimits::new(65_536, 131_072)
1021 );
1022 }
1023
1024 #[test]
1025 fn sanitized_remapping_paths_stay_inside_the_root() {
1026 let root = Path::new("/tmp/sources");
1027 for path in ["../../../etc", "a/../../../etc", "/etc", "./a/../b"] {
1028 let joined = root.join(sanitize_relative_path(Path::new(path)));
1029 assert!(
1030 joined.starts_with(root) && !joined.components().any(|c| c == Component::ParentDir),
1031 "{path} escaped the root: {}",
1032 joined.display()
1033 );
1034 }
1035
1036 for (path, expected) in [("src/../lib/", "lib"), ("./a/../b", "b"), ("a/../../lib", "lib")]
1037 {
1038 assert_eq!(sanitize_relative_path(Path::new(path)), Path::new(expected));
1039 }
1040
1041 assert_eq!(
1043 sanitize_relative_path(Path::new("lib/openzeppelin")),
1044 Path::new("lib/openzeppelin")
1045 );
1046 }
1047}