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::{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
85impl Default for ProjectCompiler {
86 #[inline]
87 fn default() -> Self {
88 Self::new()
89 }
90}
91
92impl ProjectCompiler {
93 #[inline]
95 pub fn new() -> Self {
96 Self {
97 project_root: PathBuf::new(),
98 print_names: None,
99 print_sizes: None,
100 quiet: Some(crate::shell::is_quiet()),
101 print_compiler_settings: false,
102 bail: None,
103 ignore_eip_3860: false,
104 size_limits: ContractSizeLimits::default(),
105 files: Vec::new(),
106 dynamic_test_linking: false,
107 }
108 }
109
110 #[inline]
112 pub const fn print_names(mut self, yes: bool) -> Self {
113 self.print_names = Some(yes);
114 self
115 }
116
117 #[inline]
119 pub const fn print_sizes(mut self, yes: bool) -> Self {
120 self.print_sizes = Some(yes);
121 self
122 }
123
124 #[inline]
126 #[doc(alias = "silent")]
127 pub const fn quiet(mut self, yes: bool) -> Self {
128 self.quiet = Some(yes);
129 self
130 }
131
132 #[inline]
134 pub const fn print_compiler_settings(mut self, yes: bool) -> Self {
135 self.print_compiler_settings = yes;
136 self
137 }
138
139 #[inline]
141 pub const fn bail(mut self, yes: bool) -> Self {
142 self.bail = Some(yes);
143 self
144 }
145
146 #[inline]
148 pub const fn ignore_eip_3860(mut self, yes: bool) -> Self {
149 self.ignore_eip_3860 = yes;
150 self
151 }
152
153 #[inline]
155 pub const fn size_limits(mut self, limits: ContractSizeLimits) -> Self {
156 self.size_limits = limits;
157 self
158 }
159
160 #[inline]
162 pub fn files(mut self, files: impl IntoIterator<Item = PathBuf>) -> Self {
163 self.files.extend(files);
164 self
165 }
166
167 #[inline]
169 pub const fn dynamic_test_linking(mut self, preprocess: bool) -> Self {
170 self.dynamic_test_linking = preprocess;
171 self
172 }
173
174 #[instrument(target = "forge::compile", skip_all)]
176 pub fn compile<C: Compiler<CompilerContract = Contract>>(
177 mut self,
178 project: &Project<C>,
179 ) -> Result<ProjectCompileOutput<C>>
180 where
181 DynamicTestLinkingPreprocessor: Preprocessor<C>,
182 {
183 self.project_root = project.root().to_path_buf();
184
185 if !project.paths.has_input_files() && self.files.is_empty() {
192 sh_println!("Nothing to compile")?;
193 std::process::exit(0);
194 }
195
196 let files = std::mem::take(&mut self.files);
198 let preprocess = self.dynamic_test_linking;
199 self.compile_with(|| {
200 let sources = if files.is_empty() {
201 project.paths.read_input_files()?
202 } else {
203 Source::read_all(files)?
204 };
205
206 let mut compiler =
207 foundry_compilers::project::ProjectCompiler::with_sources(project, sources)?;
208 if preprocess {
209 compiler = compiler.with_preprocessor(DynamicTestLinkingPreprocessor);
210 }
211 compiler.compile().map_err(Into::into)
212 })
213 }
214
215 fn compile_with<C: Compiler<CompilerContract = Contract>, F>(
217 self,
218 f: F,
219 ) -> Result<ProjectCompileOutput<C>>
220 where
221 F: FnOnce() -> Result<ProjectCompileOutput<C>>,
222 {
223 let quiet = self.quiet.unwrap_or(false);
224 let bail = self.bail.unwrap_or(true);
225
226 let output = with_compilation_reporter_and_settings(
227 quiet,
228 Some(self.project_root.clone()),
229 self.print_compiler_settings,
230 || {
231 tracing::debug!("compiling project");
232
233 let timer = Instant::now();
234 let r = f();
235 let elapsed = timer.elapsed();
236
237 tracing::debug!("finished compiling in {:.3}s", elapsed.as_secs_f64());
238 r
239 },
240 )?;
241
242 if bail && output.has_compiler_errors() {
243 eyre::bail!("{output}");
244 }
245
246 if !quiet {
247 if !shell::is_json() {
248 if output.is_unchanged() {
249 sh_println!("No files changed, compilation skipped")?;
250 } else {
251 sh_println!("{output}")?;
253 }
254 }
255
256 if !(shell::is_json() && output.has_compiler_errors()) {
257 self.handle_output(&output)?;
258 }
259 }
260
261 Ok(output)
262 }
263
264 fn handle_output<C: Compiler<CompilerContract = Contract>>(
266 &self,
267 output: &ProjectCompileOutput<C>,
268 ) -> Result<()> {
269 let print_names = self.print_names.unwrap_or(false);
270 let print_sizes = self.print_sizes.unwrap_or(false);
271
272 if print_names {
274 let mut artifacts: BTreeMap<_, Vec<_>> = BTreeMap::new();
275 for (name, (_, version)) in output.versioned_artifacts() {
276 artifacts.entry(version).or_default().push(name);
277 }
278
279 if shell::is_json() {
280 sh_println!("{}", serde_json::to_string(&artifacts).unwrap())?;
281 } else {
282 for (version, names) in artifacts {
283 sh_println!(
284 " compiler version: {}.{}.{}",
285 version.major,
286 version.minor,
287 version.patch
288 )?;
289 for name in names {
290 sh_println!(" - {name}")?;
291 }
292 }
293 }
294 }
295
296 if print_sizes {
297 if print_names && !shell::is_json() {
299 sh_println!()?;
300 }
301
302 let mut size_report =
303 SizeReport { contracts: BTreeMap::new(), limits: self.size_limits };
304
305 let mut artifacts: BTreeMap<String, Vec<_>> = BTreeMap::new();
306 for (id, artifact) in output.artifact_ids().filter(|(id, _)| {
307 !id.source.to_string_lossy().contains("/forge-std/src/")
309 }) {
310 artifacts.entry(id.name.clone()).or_default().push((id.source.clone(), artifact));
311 }
312
313 let abs_source = |path: &Path| -> PathBuf {
317 if path.is_absolute() { path.to_path_buf() } else { self.project_root.join(path) }
318 };
319 let source_paths = artifacts
320 .values()
321 .flatten()
322 .filter(|(_, artifact)| {
323 artifact.abi.as_ref().is_some_and(|abi| abi.functions().next().is_none())
324 })
325 .map(|(path, _)| abs_source(path))
326 .collect::<BTreeSet<_>>();
327 let libraries = collect_libraries(&source_paths);
328
329 for (name, artifact_list) in artifacts {
330 let kept = artifact_list
333 .iter()
334 .filter(|(path, artifact)| {
335 let is_library = libraries
336 .get(&abs_source(path))
337 .is_some_and(|libs| libs.contains(&name));
338 let has_no_abi_functions = artifact
339 .abi
340 .as_ref()
341 .is_some_and(|abi| abi.functions().next().is_none());
342 !(is_library && has_no_abi_functions)
343 })
344 .collect::<Vec<_>>();
345
346 for (path, artifact) in &kept {
347 let runtime_size = contract_size(*artifact, false).unwrap_or_default();
348 let init_size = contract_size(*artifact, true).unwrap_or_default();
349
350 let is_dev_contract = artifact
351 .abi
352 .as_ref()
353 .map(|abi| {
354 abi.functions().any(|f| {
355 f.test_function_kind().is_known()
356 || matches!(f.name.as_str(), "IS_TEST" | "IS_SCRIPT")
357 })
358 })
359 .unwrap_or(false);
360
361 let unique_name = if kept.len() > 1 {
362 format!(
363 "{} ({})",
364 name,
365 path.strip_prefix(&self.project_root).unwrap_or(path).display()
366 )
367 } else {
368 name.clone()
369 };
370
371 size_report.contracts.insert(
372 unique_name,
373 ContractInfo { runtime_size, init_size, is_dev_contract },
374 );
375 }
376 }
377
378 sh_println!("{size_report}")?;
379
380 let runtime_eip = match size_report.limits.runtime {
381 CONTRACT_RUNTIME_SIZE_LIMIT => "EIP-170: ",
382 AMSTERDAM_CONTRACT_RUNTIME_SIZE_LIMIT => "EIP-7954: ",
383 _ => "",
384 };
385 eyre::ensure!(
386 !size_report.exceeds_runtime_size_limit(),
387 "some contracts exceed the runtime size limit ({runtime_eip}{} bytes)",
388 size_report.limits.runtime
389 );
390 let initcode_eip = match size_report.limits.initcode {
392 CONTRACT_INITCODE_SIZE_LIMIT => "EIP-3860: ",
393 AMSTERDAM_CONTRACT_INITCODE_SIZE_LIMIT => "EIP-7954: ",
394 _ => "",
395 };
396 eyre::ensure!(
397 self.ignore_eip_3860 || !size_report.exceeds_initcode_size_limit(),
398 "some contracts exceed the initcode size limit ({initcode_eip}{} bytes)",
399 size_report.limits.initcode
400 );
401 }
402
403 Ok(())
404 }
405}
406
407const CONTRACT_RUNTIME_SIZE_LIMIT: usize = eip170::MAX_CODE_SIZE;
409
410const CONTRACT_INITCODE_SIZE_LIMIT: usize = eip3860::MAX_INITCODE_SIZE;
412
413const AMSTERDAM_CONTRACT_RUNTIME_SIZE_LIMIT: usize = 65_536;
415const AMSTERDAM_CONTRACT_INITCODE_SIZE_LIMIT: usize = 131_072;
416
417const CONTRACT_RUNTIME_SIZE_WARN_THRESHOLD: usize = 18_000;
418const CONTRACT_INITCODE_SIZE_WARN_THRESHOLD: usize = 36_000;
419
420#[derive(Clone, Copy, Debug, PartialEq, Eq)]
422pub struct ContractSizeLimits {
423 pub runtime: usize,
425 pub initcode: usize,
427}
428
429impl ContractSizeLimits {
430 pub const fn new(runtime: usize, initcode: usize) -> Self {
432 Self { runtime, initcode }
433 }
434
435 pub const fn with_runtime_limit(runtime: usize) -> Self {
437 Self { runtime, initcode: runtime.saturating_mul(2) }
438 }
439
440 pub const fn for_spec_id(spec_id: SpecId) -> Self {
442 if spec_id.is_enabled_in(SpecId::AMSTERDAM) {
443 Self::new(AMSTERDAM_CONTRACT_RUNTIME_SIZE_LIMIT, AMSTERDAM_CONTRACT_INITCODE_SIZE_LIMIT)
444 } else {
445 Self::new(CONTRACT_RUNTIME_SIZE_LIMIT, CONTRACT_INITCODE_SIZE_LIMIT)
446 }
447 }
448
449 const fn runtime_warning_threshold(self) -> usize {
450 scaled_threshold(
451 self.runtime,
452 CONTRACT_RUNTIME_SIZE_WARN_THRESHOLD,
453 CONTRACT_RUNTIME_SIZE_LIMIT,
454 )
455 }
456
457 const fn initcode_warning_threshold(self) -> usize {
458 scaled_threshold(
459 self.initcode,
460 CONTRACT_INITCODE_SIZE_WARN_THRESHOLD,
461 CONTRACT_INITCODE_SIZE_LIMIT,
462 )
463 }
464}
465
466impl Default for ContractSizeLimits {
467 fn default() -> Self {
468 Self::new(CONTRACT_RUNTIME_SIZE_LIMIT, CONTRACT_INITCODE_SIZE_LIMIT)
469 }
470}
471
472const fn scaled_threshold(limit: usize, threshold: usize, default_limit: usize) -> usize {
473 limit.saturating_mul(threshold) / default_limit
474}
475
476pub struct SizeReport {
478 pub contracts: BTreeMap<String, ContractInfo>,
480 pub limits: ContractSizeLimits,
482}
483
484impl SizeReport {
485 pub fn max_runtime_size(&self) -> usize {
487 self.contracts
488 .values()
489 .filter(|c| !c.is_dev_contract)
490 .map(|c| c.runtime_size)
491 .max()
492 .unwrap_or(0)
493 }
494
495 pub fn max_init_size(&self) -> usize {
497 self.contracts
498 .values()
499 .filter(|c| !c.is_dev_contract)
500 .map(|c| c.init_size)
501 .max()
502 .unwrap_or(0)
503 }
504
505 pub fn exceeds_runtime_size_limit(&self) -> bool {
507 self.max_runtime_size() > self.limits.runtime
508 }
509
510 pub fn exceeds_initcode_size_limit(&self) -> bool {
512 self.max_init_size() > self.limits.initcode
513 }
514}
515
516impl Display for SizeReport {
517 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
518 if shell::is_json() {
519 writeln!(f, "{}", self.format_json_output())?;
520 } else {
521 writeln!(f, "\n{}", self.format_table_output())?;
522 }
523 Ok(())
524 }
525}
526
527impl SizeReport {
528 fn format_json_output(&self) -> String {
529 let contracts = self
530 .contracts
531 .iter()
532 .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0))
533 .map(|(name, contract)| {
534 (
535 name.clone(),
536 serde_json::json!({
537 "runtime_size": contract.runtime_size,
538 "init_size": contract.init_size,
539 "runtime_margin": self.limits.runtime as isize - contract.runtime_size as isize,
540 "init_margin": self.limits.initcode as isize - contract.init_size as isize,
541 }),
542 )
543 })
544 .collect::<serde_json::Map<_, _>>();
545
546 serde_json::to_string(&contracts).unwrap()
547 }
548
549 fn format_table_output(&self) -> Table {
550 let mut table = Table::new();
551 if shell::is_markdown() {
552 table.load_style(ASCII_MARKDOWN);
553 } else {
554 table.load_style(ASCII_FULL.with_rounded_corners());
555 }
556
557 table.set_header(vec![
558 Cell::new("Contract"),
559 Cell::new("Runtime Size (B)"),
560 Cell::new("Initcode Size (B)"),
561 Cell::new("Runtime Margin (B)"),
562 Cell::new("Initcode Margin (B)"),
563 ]);
564
565 let contracts = self
567 .contracts
568 .iter()
569 .filter(|(_, c)| !c.is_dev_contract && (c.runtime_size > 0 || c.init_size > 0));
570 let runtime_warning_threshold = self.limits.runtime_warning_threshold();
571 let initcode_warning_threshold = self.limits.initcode_warning_threshold();
572 for (name, contract) in contracts {
573 let runtime_margin = self.limits.runtime as isize - contract.runtime_size as isize;
574 let init_margin = self.limits.initcode as isize - contract.init_size as isize;
575
576 let runtime_color = if contract.runtime_size < runtime_warning_threshold {
577 Color::Reset
578 } else if contract.runtime_size <= self.limits.runtime {
579 Color::Yellow
580 } else {
581 Color::Red
582 };
583
584 let init_color = if contract.init_size < initcode_warning_threshold {
585 Color::Reset
586 } else if contract.init_size <= self.limits.initcode {
587 Color::Yellow
588 } else {
589 Color::Red
590 };
591
592 let locale = &Locale::en;
593 table.add_row([
594 Cell::new(name),
595 Cell::new(contract.runtime_size.to_formatted_string(locale)).fg(runtime_color),
596 Cell::new(contract.init_size.to_formatted_string(locale)).fg(init_color),
597 Cell::new(runtime_margin.to_formatted_string(locale)).fg(runtime_color),
598 Cell::new(init_margin.to_formatted_string(locale)).fg(init_color),
599 ]);
600 }
601
602 table
603 }
604}
605
606fn collect_libraries(sources: &BTreeSet<PathBuf>) -> HashMap<PathBuf, HashSet<String>> {
610 let mut result: HashMap<PathBuf, HashSet<String>> = HashMap::new();
611 let sess = Session::builder().with_silent_emitter(None).build();
612 let _ = sess.enter(|| -> solar::interface::Result<()> {
613 for path in sources {
614 let arena = Arena::new();
615 let mut parser = match Parser::from_lazy_source_code(
616 &sess,
617 &arena,
618 FileName::from(path.clone()),
619 || std::fs::read_to_string(path),
620 ) {
621 Ok(parser) => parser,
622 Err(_) => continue,
623 };
624 let Ok(ast) = parser.parse_file() else { continue };
625 let libs = ast
626 .items
627 .iter()
628 .filter_map(|item| match &item.kind {
629 ItemKind::Contract(c) if c.kind == ContractKind::Library => {
630 Some(c.name.as_str().to_string())
631 }
632 _ => None,
633 })
634 .collect::<HashSet<_>>();
635 if !libs.is_empty() {
636 result.insert(path.clone(), libs);
637 }
638 }
639 Ok(())
640 });
641 result
642}
643
644fn contract_size<T: Artifact>(artifact: &T, initcode: bool) -> Option<usize> {
646 let bytecode = if initcode {
647 artifact.get_bytecode_object()?
648 } else {
649 artifact.get_deployed_bytecode_object()?
650 };
651
652 let size = match bytecode.as_ref() {
653 BytecodeObject::Bytecode(bytes) => bytes.len(),
654 BytecodeObject::Unlinked(unlinked) => {
655 let mut size = unlinked.len();
658 if unlinked.starts_with("0x") {
659 size -= 2;
660 }
661 size / 2
663 }
664 };
665
666 Some(size)
667}
668
669#[derive(Clone, Copy, Debug)]
671pub struct ContractInfo {
672 pub runtime_size: usize,
674 pub init_size: usize,
676 pub is_dev_contract: bool,
678}
679
680pub fn compile_target<C: Compiler<CompilerContract = Contract>>(
687 target_path: &Path,
688 project: &Project<C>,
689 quiet: bool,
690) -> Result<ProjectCompileOutput<C>>
691where
692 DynamicTestLinkingPreprocessor: Preprocessor<C>,
693{
694 ProjectCompiler::new().quiet(quiet).files([target_path.into()]).compile(project)
695}
696
697pub fn compile_abi_project<C: Compiler<CompilerContract = Contract>>(
699 project: &mut Project<C>,
700 compiler: ProjectCompiler,
701) -> Result<ProjectCompileOutput<C>>
702where
703 DynamicTestLinkingPreprocessor: Preprocessor<C>,
704{
705 project.update_output_selection(|selection| {
706 *selection = OutputSelection::common_output_selection(["abi".to_string()]);
708 });
709 compiler.compile(project)
710}
711
712pub fn compile_target_abi(
714 project: &mut Project<MultiCompiler>,
715 target_path: &Path,
716 target_name: &str,
717) -> Result<JsonAbi> {
718 let target_path = dunce::canonicalize(target_path)?;
719 let output = compile_abi_project(
720 project,
721 ProjectCompiler::new().quiet(true).files([target_path.clone()]),
722 )?;
723
724 let artifact = output
725 .find(&target_path, target_name)
726 .ok_or_eyre("failed to find target artifact when compiling for abi")?;
727 artifact.abi.clone().ok_or_eyre("target artifact does not have an ABI")
728}
729
730pub fn etherscan_project(metadata: &Metadata, target_path: &Path) -> Result<Project> {
732 let target_path = dunce::canonicalize(target_path)?;
733 let sources_path = target_path.join(&metadata.contract_name);
734 metadata.source_tree().write_to(&target_path)?;
735
736 let mut settings = metadata.settings()?;
737
738 for remapping in &mut settings.remappings {
740 let new_path = sources_path.join(remapping.path.trim_start_matches('/'));
741 remapping.path = new_path.display().to_string();
742 }
743
744 if !settings.remappings.iter().any(|remapping| remapping.name.starts_with("@openzeppelin/")) {
746 let oz = Remapping {
747 context: None,
748 name: "@openzeppelin/".into(),
749 path: sources_path.join("@openzeppelin").display().to_string(),
750 };
751 settings.remappings.push(oz);
752 }
753
754 let paths = ProjectPathsConfig::builder()
758 .sources(sources_path.clone())
759 .remappings(settings.remappings.clone())
760 .build_with_root(sources_path);
761
762 let v = metadata.compiler_version()?;
764 let solc = Solc::find_or_install(&v)?;
765
766 let compiler = MultiCompiler { solc: Some(SolcCompiler::Specific(solc)), vyper: None };
767
768 Ok(ProjectBuilder::<MultiCompiler>::default()
769 .settings(MultiCompilerSettings {
770 solc: SolcSettings {
771 settings: SolcConfig::builder().settings(settings).build(),
772 ..Default::default()
773 },
774 ..Default::default()
775 })
776 .paths(paths)
777 .ephemeral()
778 .no_artifacts()
779 .build(compiler)?)
780}
781
782pub fn with_compilation_reporter<O>(
789 quiet: bool,
790 project_root: Option<PathBuf>,
791 f: impl FnOnce() -> O,
792) -> O {
793 with_compilation_reporter_and_settings(quiet, project_root, false, f)
794}
795
796fn with_compilation_reporter_and_settings<O>(
797 quiet: bool,
798 project_root: Option<PathBuf>,
799 print_compiler_settings: bool,
800 f: impl FnOnce() -> O,
801) -> O {
802 #[expect(clippy::collapsible_else_if)]
803 let reporter = if quiet || shell::is_json() {
804 Report::new(NoReporter::default())
805 } else {
806 if std::io::stderr().is_terminal() {
807 Report::new(
808 SpinnerReporter::spawn(project_root)
809 .with_compiler_settings(print_compiler_settings),
810 )
811 } else {
812 Report::new(
813 BasicStdoutReporter::default().with_compiler_settings(print_compiler_settings),
814 )
815 }
816 };
817
818 foundry_compilers::report::with_scoped(&reporter, f)
819}
820
821#[derive(Clone, PartialEq, Eq)]
828pub enum PathOrContractInfo {
829 Path(PathBuf),
831 ContractInfo(CompilerContractInfo),
833}
834
835impl PathOrContractInfo {
836 pub fn path(&self) -> Option<PathBuf> {
838 match self {
839 Self::Path(path) => Some(path.clone()),
840 Self::ContractInfo(info) => info.path.as_ref().map(PathBuf::from),
841 }
842 }
843
844 pub fn name(&self) -> Option<&str> {
846 match self {
847 Self::Path(_) => None,
848 Self::ContractInfo(info) => Some(&info.name),
849 }
850 }
851}
852
853impl FromStr for PathOrContractInfo {
854 type Err = eyre::Error;
855
856 fn from_str(s: &str) -> Result<Self> {
857 if let Ok(contract) = CompilerContractInfo::from_str(s) {
858 return Ok(Self::ContractInfo(contract));
859 }
860 let path = PathBuf::from(s);
861 if path.extension().is_some_and(|ext| ext == "sol" || ext == "vy") {
862 return Ok(Self::Path(path));
863 }
864 Err(eyre::eyre!("Invalid contract identifier, file is not *.sol or *.vy: {}", s))
865 }
866}
867
868impl std::fmt::Debug for PathOrContractInfo {
869 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
870 match self {
871 Self::Path(path) => write!(f, "Path({})", path.display()),
872 Self::ContractInfo(info) => {
873 write!(f, "ContractInfo({info})")
874 }
875 }
876 }
877}
878
879#[cfg(test)]
880mod tests {
881 use super::*;
882
883 #[test]
884 fn parse_contract_identifiers() {
885 let t = ["src/Counter.sol", "src/Counter.sol:Counter", "Counter"];
886
887 let i1 = PathOrContractInfo::from_str(t[0]).unwrap();
888 assert_eq!(i1, PathOrContractInfo::Path(PathBuf::from(t[0])));
889
890 let i2 = PathOrContractInfo::from_str(t[1]).unwrap();
891 assert_eq!(
892 i2,
893 PathOrContractInfo::ContractInfo(CompilerContractInfo {
894 path: Some("src/Counter.sol".to_string()),
895 name: "Counter".to_string()
896 })
897 );
898
899 let i3 = PathOrContractInfo::from_str(t[2]).unwrap();
900 assert_eq!(
901 i3,
902 PathOrContractInfo::ContractInfo(CompilerContractInfo {
903 path: None,
904 name: "Counter".to_string()
905 })
906 );
907 }
908
909 #[test]
910 fn size_report_uses_configured_limits() {
911 let mut contracts = BTreeMap::new();
912 contracts.insert(
913 "LargeContract".to_string(),
914 ContractInfo { runtime_size: 30_000, init_size: 60_000, is_dev_contract: false },
915 );
916
917 let default_report =
918 SizeReport { contracts: contracts.clone(), limits: ContractSizeLimits::default() };
919 assert!(default_report.exceeds_runtime_size_limit());
920 assert!(default_report.exceeds_initcode_size_limit());
921
922 let custom_report =
923 SizeReport { contracts, limits: ContractSizeLimits::new(131_072, 262_144) };
924 assert!(!custom_report.exceeds_runtime_size_limit());
925 assert!(!custom_report.exceeds_initcode_size_limit());
926 let output: serde_json::Value =
927 serde_json::from_str(&custom_report.format_json_output()).unwrap();
928 assert_eq!(
929 output,
930 serde_json::json!({
931 "LargeContract": {
932 "runtime_size": 30000,
933 "init_size": 60000,
934 "runtime_margin": 101072,
935 "init_margin": 202144,
936 }
937 })
938 );
939 }
940
941 #[test]
942 fn contract_size_limits_derive_initcode_limit_from_runtime_limit() {
943 assert_eq!(
944 ContractSizeLimits::with_runtime_limit(50_000),
945 ContractSizeLimits::new(50_000, 100_000)
946 );
947 }
948
949 #[test]
950 fn contract_size_limits_follow_evm_spec() {
951 assert_eq!(ContractSizeLimits::for_spec_id(SpecId::OSAKA), ContractSizeLimits::default());
952 assert_eq!(
953 ContractSizeLimits::for_spec_id(SpecId::AMSTERDAM),
954 ContractSizeLimits::new(65_536, 131_072)
955 );
956 }
957}