Skip to content

Commit db3d9fc

Browse files
authored
feat(common): PathOrContractInfo arg type (foundry-rs#9770)
* feat(`common`): `PathOrContractInfo` * fix * docs * nit * test * move find abi helper to ContractsByArtifact * nit * fix * nit * fix * nit * ensure sol file * account for vyper contracts * nit
1 parent 67be473 commit db3d9fc

File tree

7 files changed

+262
-41
lines changed

7 files changed

+262
-41
lines changed

crates/cast/bin/cmd/interface.rs

+18-15
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ use clap::Parser;
44
use eyre::{Context, Result};
55
use foundry_block_explorers::Client;
66
use foundry_cli::{opts::EtherscanOpts, utils::LoadConfig};
7-
use foundry_common::{compile::ProjectCompiler, fs, shell};
8-
use foundry_compilers::{info::ContractInfo, utils::canonicalize};
7+
use foundry_common::{
8+
compile::{PathOrContractInfo, ProjectCompiler},
9+
find_target_path, fs, shell, ContractsByArtifact,
10+
};
911
use foundry_config::load_config;
1012
use itertools::Itertools;
1113
use serde_json::Value;
@@ -118,19 +120,20 @@ fn load_abi_from_artifact(path_or_contract: &str) -> Result<Vec<(JsonAbi, String
118120
let project = config.project()?;
119121
let compiler = ProjectCompiler::new().quiet(true);
120122

121-
let contract = ContractInfo::new(path_or_contract);
122-
let target_path = if let Some(path) = &contract.path {
123-
canonicalize(project.root().join(path))?
124-
} else {
125-
project.find_contract_path(&contract.name)?
126-
};
127-
let mut output = compiler.files([target_path.clone()]).compile(&project)?;
128-
129-
let artifact = output.remove(&target_path, &contract.name).ok_or_else(|| {
130-
eyre::eyre!("Could not find artifact `{contract}` in the compiled artifacts")
131-
})?;
132-
let abi = artifact.abi.as_ref().ok_or_else(|| eyre::eyre!("Failed to fetch lossless ABI"))?;
133-
Ok(vec![(abi.clone(), contract.name)])
123+
let contract = PathOrContractInfo::from_str(path_or_contract)?;
124+
125+
let target_path = find_target_path(&project, &contract)?;
126+
let output = compiler.files([target_path.clone()]).compile(&project)?;
127+
128+
let contracts_by_artifact = ContractsByArtifact::from(output);
129+
130+
let maybe_abi = contracts_by_artifact
131+
.find_abi_by_name_or_src_path(contract.name().unwrap_or(&target_path.to_string_lossy()));
132+
133+
let (abi, name) =
134+
maybe_abi.as_ref().ok_or_else(|| eyre::eyre!("Failed to fetch lossless ABI"))?;
135+
136+
Ok(vec![(abi.clone(), contract.name().unwrap_or(name).to_string())])
134137
}
135138

136139
/// Fetches the ABI of a contract from Etherscan.

crates/common/src/compile.rs

+91
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use foundry_compilers::{
1515
solc::{Solc, SolcCompiler},
1616
Compiler,
1717
},
18+
info::ContractInfo as CompilerContractInfo,
1819
report::{BasicStdoutReporter, NoReporter, Report},
1920
solc::SolcSettings,
2021
Artifact, Project, ProjectBuilder, ProjectCompileOutput, ProjectPathsConfig, SolcConfig,
@@ -25,6 +26,7 @@ use std::{
2526
fmt::Display,
2627
io::IsTerminal,
2728
path::{Path, PathBuf},
29+
str::FromStr,
2830
time::Instant,
2931
};
3032

@@ -540,3 +542,92 @@ pub fn with_compilation_reporter<O>(quiet: bool, f: impl FnOnce() -> O) -> O {
540542

541543
foundry_compilers::report::with_scoped(&reporter, f)
542544
}
545+
546+
/// Container type for parsing contract identifiers from CLI.
547+
///
548+
/// Passed string can be of the following forms:
549+
/// - `src/Counter.sol` - path to the contract file, in the case where it only contains one contract
550+
/// - `src/Counter.sol:Counter` - path to the contract file and the contract name
551+
/// - `Counter` - contract name only
552+
#[derive(Clone, PartialEq, Eq)]
553+
pub enum PathOrContractInfo {
554+
/// Non-canoncalized path provided via CLI.
555+
Path(PathBuf),
556+
/// Contract info provided via CLI.
557+
ContractInfo(CompilerContractInfo),
558+
}
559+
560+
impl PathOrContractInfo {
561+
/// Returns the path to the contract file if provided.
562+
pub fn path(&self) -> Option<PathBuf> {
563+
match self {
564+
Self::Path(path) => Some(path.to_path_buf()),
565+
Self::ContractInfo(info) => info.path.as_ref().map(PathBuf::from),
566+
}
567+
}
568+
569+
/// Returns the contract name if provided.
570+
pub fn name(&self) -> Option<&str> {
571+
match self {
572+
Self::Path(_) => None,
573+
Self::ContractInfo(info) => Some(&info.name),
574+
}
575+
}
576+
}
577+
578+
impl FromStr for PathOrContractInfo {
579+
type Err = eyre::Error;
580+
581+
fn from_str(s: &str) -> Result<Self> {
582+
if let Ok(contract) = CompilerContractInfo::from_str(s) {
583+
return Ok(Self::ContractInfo(contract));
584+
}
585+
let path = PathBuf::from(s);
586+
if path.extension().is_some_and(|ext| ext == "sol" || ext == "vy") {
587+
return Ok(Self::Path(path));
588+
}
589+
Err(eyre::eyre!("Invalid contract identifier, file is not *.sol or *.vy: {}", s))
590+
}
591+
}
592+
593+
impl std::fmt::Debug for PathOrContractInfo {
594+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
595+
match self {
596+
Self::Path(path) => write!(f, "Path({})", path.display()),
597+
Self::ContractInfo(info) => {
598+
write!(f, "ContractInfo({info})")
599+
}
600+
}
601+
}
602+
}
603+
604+
#[cfg(test)]
605+
mod tests {
606+
use super::*;
607+
608+
#[test]
609+
fn parse_contract_identifiers() {
610+
let t = ["src/Counter.sol", "src/Counter.sol:Counter", "Counter"];
611+
612+
let i1 = PathOrContractInfo::from_str(t[0]).unwrap();
613+
assert_eq!(i1, PathOrContractInfo::Path(PathBuf::from(t[0])));
614+
615+
let i2 = PathOrContractInfo::from_str(t[1]).unwrap();
616+
assert_eq!(
617+
i2,
618+
PathOrContractInfo::ContractInfo(CompilerContractInfo {
619+
path: Some("src/Counter.sol".to_string()),
620+
name: "Counter".to_string()
621+
})
622+
);
623+
624+
let i3 = PathOrContractInfo::from_str(t[2]).unwrap();
625+
assert_eq!(
626+
i3,
627+
PathOrContractInfo::ContractInfo(CompilerContractInfo {
628+
path: None,
629+
name: "Counter".to_string()
630+
})
631+
);
632+
}
633+
}

crates/common/src/contracts.rs

+93-4
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
//! Commonly used contract types and functions.
22
3+
use crate::compile::PathOrContractInfo;
34
use alloy_json_abi::{Event, Function, JsonAbi};
45
use alloy_primitives::{hex, Address, Bytes, Selector, B256};
5-
use eyre::Result;
6+
use eyre::{OptionExt, Result};
67
use foundry_compilers::{
78
artifacts::{
89
BytecodeObject, CompactBytecode, CompactContractBytecode, CompactDeployedBytecode,
9-
ContractBytecodeSome, Offsets,
10+
ConfigurableContractArtifact, ContractBytecodeSome, Offsets,
1011
},
11-
ArtifactId,
12+
utils::canonicalized,
13+
ArtifactId, Project, ProjectCompileOutput,
14+
};
15+
use std::{
16+
collections::BTreeMap,
17+
ops::Deref,
18+
path::{Path, PathBuf},
19+
str::FromStr,
20+
sync::Arc,
1221
};
13-
use std::{collections::BTreeMap, ops::Deref, str::FromStr, sync::Arc};
1422

1523
/// Libraries' runtime code always starts with the following instruction:
1624
/// `PUSH20 0x0000000000000000000000000000000000000000`
@@ -268,6 +276,17 @@ impl ContractsByArtifact {
268276
.map(|(_, contract)| contract.abi.clone())
269277
}
270278

279+
/// Finds abi by name or source path
280+
///
281+
/// Returns the abi and the contract name.
282+
pub fn find_abi_by_name_or_src_path(&self, name_or_path: &str) -> Option<(JsonAbi, String)> {
283+
self.iter()
284+
.find(|(artifact, _)| {
285+
artifact.name == name_or_path || artifact.source == PathBuf::from(name_or_path)
286+
})
287+
.map(|(_, contract)| (contract.abi.clone(), contract.name.clone()))
288+
}
289+
271290
/// Flattens the contracts into functions, events and errors.
272291
pub fn flatten(&self) -> (BTreeMap<Selector, Function>, BTreeMap<B256, Event>, JsonAbi) {
273292
let mut funcs = BTreeMap::new();
@@ -288,6 +307,21 @@ impl ContractsByArtifact {
288307
}
289308
}
290309

310+
impl From<ProjectCompileOutput> for ContractsByArtifact {
311+
fn from(value: ProjectCompileOutput) -> Self {
312+
Self::new(value.into_artifacts().map(|(id, ar)| {
313+
(
314+
id,
315+
CompactContractBytecode {
316+
abi: ar.abi,
317+
bytecode: ar.bytecode,
318+
deployed_bytecode: ar.deployed_bytecode,
319+
},
320+
)
321+
}))
322+
}
323+
}
324+
291325
impl Deref for ContractsByArtifact {
292326
type Target = BTreeMap<ArtifactId, ContractData>;
293327

@@ -399,6 +433,61 @@ pub fn compact_to_contract(contract: CompactContractBytecode) -> Result<Contract
399433
})
400434
}
401435

436+
/// Returns the canonicalized target path for the given identifier.
437+
pub fn find_target_path(project: &Project, identifier: &PathOrContractInfo) -> Result<PathBuf> {
438+
match identifier {
439+
PathOrContractInfo::Path(path) => Ok(canonicalized(project.root().join(path))),
440+
PathOrContractInfo::ContractInfo(info) => {
441+
let path = project.find_contract_path(&info.name)?;
442+
Ok(path)
443+
}
444+
}
445+
}
446+
447+
/// Returns the target artifact given the path and name.
448+
pub fn find_matching_contract_artifact(
449+
output: &mut ProjectCompileOutput,
450+
target_path: &Path,
451+
target_name: Option<&str>,
452+
) -> eyre::Result<ConfigurableContractArtifact> {
453+
if let Some(name) = target_name {
454+
output
455+
.remove(target_path, name)
456+
.ok_or_eyre(format!("Could not find artifact `{name}` in the compiled artifacts"))
457+
} else {
458+
let possible_targets = output
459+
.artifact_ids()
460+
.filter(|(id, _artifact)| id.source == target_path)
461+
.collect::<Vec<_>>();
462+
463+
if possible_targets.is_empty() {
464+
eyre::bail!("Could not find artifact linked to source `{target_path:?}` in the compiled artifacts");
465+
}
466+
467+
let (target_id, target_artifact) = possible_targets[0].clone();
468+
if possible_targets.len() == 1 {
469+
return Ok(target_artifact.clone());
470+
}
471+
472+
// If all artifact_ids in `possible_targets` have the same name (without ".", indicates
473+
// additional compiler profiles), it means that there are multiple contracts in the
474+
// same file.
475+
if !target_id.name.contains(".") &&
476+
possible_targets.iter().any(|(id, _)| id.name != target_id.name)
477+
{
478+
eyre::bail!("Multiple contracts found in the same file, please specify the target <path>:<contract> or <contract>");
479+
}
480+
481+
// Otherwise, we're dealing with additional compiler profiles wherein `id.source` is the
482+
// same but `id.path` is different.
483+
let artifact = possible_targets
484+
.iter()
485+
.find_map(|(id, artifact)| if id.profile == "default" { Some(*artifact) } else { None })
486+
.unwrap_or(target_artifact);
487+
488+
Ok(artifact.clone())
489+
}
490+
}
402491
#[cfg(test)]
403492
mod tests {
404493
use super::*;

crates/common/src/utils.rs

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
//! Uncategorised utilities.
22
33
use alloy_primitives::{keccak256, B256, U256};
4-
54
/// Block on a future using the current tokio runtime on the current thread.
65
pub fn block_on<F: std::future::Future>(future: F) -> F::Output {
76
block_on_handle(&tokio::runtime::Handle::current(), future)

crates/config/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ mod bind_json;
122122
use bind_json::BindJsonConfig;
123123

124124
mod compilation;
125-
use compilation::{CompilationRestrictions, SettingsOverrides};
125+
pub use compilation::{CompilationRestrictions, SettingsOverrides};
126126

127127
/// Foundry configuration
128128
///

crates/forge/bin/cmd/inspect.rs

+16-20
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,29 @@ use comfy_table::{modifiers::UTF8_ROUND_CORNERS, Cell, Table};
55
use eyre::{Context, Result};
66
use forge::revm::primitives::Eof;
77
use foundry_cli::opts::{BuildOpts, CompilerOpts};
8-
use foundry_common::{compile::ProjectCompiler, fmt::pretty_eof, shell};
9-
use foundry_compilers::{
10-
artifacts::{
11-
output_selection::{
12-
BytecodeOutputSelection, ContractOutputSelection, DeployedBytecodeOutputSelection,
13-
EvmOutputSelection, EwasmOutputSelection,
14-
},
15-
CompactBytecode, StorageLayout,
8+
use foundry_common::{
9+
compile::{PathOrContractInfo, ProjectCompiler},
10+
find_matching_contract_artifact, find_target_path,
11+
fmt::pretty_eof,
12+
shell,
13+
};
14+
use foundry_compilers::artifacts::{
15+
output_selection::{
16+
BytecodeOutputSelection, ContractOutputSelection, DeployedBytecodeOutputSelection,
17+
EvmOutputSelection, EwasmOutputSelection,
1618
},
17-
info::ContractInfo,
18-
utils::canonicalize,
19+
CompactBytecode, StorageLayout,
1920
};
2021
use regex::Regex;
2122
use serde_json::{Map, Value};
22-
use std::{collections::BTreeMap, fmt, sync::LazyLock};
23+
use std::{collections::BTreeMap, fmt, str::FromStr, sync::LazyLock};
2324

2425
/// CLI arguments for `forge inspect`.
2526
#[derive(Clone, Debug, Parser)]
2627
pub struct InspectArgs {
2728
/// The identifier of the contract to inspect in the form `(<path>:)?<contractname>`.
28-
pub contract: ContractInfo,
29+
#[arg(value_parser = PathOrContractInfo::from_str)]
30+
pub contract: PathOrContractInfo,
2931

3032
/// The contract artifact field to inspect.
3133
#[arg(value_enum)]
@@ -64,17 +66,11 @@ impl InspectArgs {
6466
// Build the project
6567
let project = modified_build_args.project()?;
6668
let compiler = ProjectCompiler::new().quiet(true);
67-
let target_path = if let Some(path) = &contract.path {
68-
canonicalize(project.root().join(path))?
69-
} else {
70-
project.find_contract_path(&contract.name)?
71-
};
69+
let target_path = find_target_path(&project, &contract)?;
7270
let mut output = compiler.files([target_path.clone()]).compile(&project)?;
7371

7472
// Find the artifact
75-
let artifact = output.remove(&target_path, &contract.name).ok_or_else(|| {
76-
eyre::eyre!("Could not find artifact `{contract}` in the compiled artifacts")
77-
})?;
73+
let artifact = find_matching_contract_artifact(&mut output, &target_path, contract.name())?;
7874

7975
// Match on ContractArtifactFields and pretty-print
8076
match field {

0 commit comments

Comments
 (0)