Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

feat(solc): emit artifacts for standalone source files #1296

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@

- Bundle svm, svm-builds and sha2 dependencies in new `svm-solc` feature
[#1071](https://github.com/gakonst/ethers-rs/pull/1071)
- Emit artifact files for source files without any ContractDefinition
[#1296](https://github.com/gakonst/ethers-rs/pull/1296)
- Wrap `ethabi::Contract` into new type `LosslessAbi` and `abi: Option<Abi>` with `abi: Option<LosslessAbi>` in `ConfigurableContractArtifact`
[#952](https://github.com/gakonst/ethers-rs/pull/952)
- Let `Project` take ownership of `ArtifactOutput` and change trait interface
Expand Down
13 changes: 13 additions & 0 deletions ethers-solc/src/artifact_output/configurable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::{
Ast, CompactContractBytecodeCow, DevDoc, Evm, Ewasm, FunctionDebugData, GasEstimates,
GeneratedSource, LosslessAbi, Metadata, Offsets, Settings, StorageLayout, UserDoc,
},
sources::VersionedSourceFile,
ArtifactOutput, SolcConfig, SolcError, SourceFile,
};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -329,6 +330,18 @@ impl ArtifactOutput for ConfigurableArtifacts {
generated_sources: generated_sources.unwrap_or_default(),
}
}

fn standalone_source_file_to_artifact(
&self,
_path: &str,
file: &VersionedSourceFile,
) -> Option<Self::Artifact> {
file.source_file.ast.clone().map(|ast| ConfigurableContractArtifact {
id: Some(file.source_file.id),
ast: Some(ast),
..Default::default()
})
}
}

/// Determines the additional values to include in the contract's artifact file
Expand Down
89 changes: 88 additions & 1 deletion ethers-solc/src/artifact_output/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use semver::Version;
use serde::{de::DeserializeOwned, Serialize};
use std::{
borrow::Cow,
collections::btree_map::BTreeMap,
collections::{btree_map::BTreeMap, HashSet},
fmt, fs, io,
path::{Path, PathBuf},
};
Expand All @@ -23,6 +23,7 @@ use crate::{
},
compile::output::{contracts::VersionedContracts, sources::VersionedSourceFiles},
sourcemap::{SourceMap, SyntaxError},
sources::VersionedSourceFile,
};
pub use configurable::*;

Expand Down Expand Up @@ -632,14 +633,25 @@ pub trait ArtifactOutput {
sources: &VersionedSourceFiles,
) -> Artifacts<Self::Artifact> {
let mut artifacts = ArtifactsMap::new();

// this tracks all the `SourceFile`s that we successfully mapped to a contract
let mut non_standalone_sources = HashSet::new();

// loop over all files and their contracts
for (file, contracts) in contracts.as_ref().iter() {
let mut entries = BTreeMap::new();

// loop over all contracts and their versions
for (name, versioned_contracts) in contracts {
let mut contracts = Vec::with_capacity(versioned_contracts.len());
// check if the same contract compiled with multiple solc versions
for contract in versioned_contracts {
let source_file = sources.find_file_and_version(file, &contract.version);

if let Some(source) = source_file {
non_standalone_sources.insert((source.id, &contract.version));
}

let artifact_path = if versioned_contracts.len() > 1 {
Self::output_file_versioned(file, name, &contract.version)
} else {
Expand All @@ -664,8 +676,67 @@ pub trait ArtifactOutput {
artifacts.insert(file.to_string(), entries);
}

// extend with standalone source files and convert them to artifacts
for (file, sources) in sources.as_ref().iter() {
for source in sources {
if !non_standalone_sources.contains(&(source.source_file.id, &source.version)) {
// scan the ast as a safe measure to ensure this file does not include any
// source units
// there's also no need to create a standalone artifact for source files that
// don't contain an ast
if source.source_file.contains_contract_definition() ||
source.source_file.ast.is_none()
{
continue
}

// we use file and file stem
if let Some(name) = Path::new(file).file_stem().and_then(|stem| stem.to_str()) {
if let Some(artifact) =
self.standalone_source_file_to_artifact(file, source)
{
let artifact_path = if sources.len() > 1 {
Self::output_file_versioned(file, name, &source.version)
} else {
Self::output_file(file, name)
};

let entries = artifacts
.entry(file.to_string())
.or_default()
.entry(name.to_string())
.or_default();

if entries.iter().all(|entry| entry.version != source.version) {
entries.push(ArtifactFile {
artifact,
file: artifact_path,
version: source.version.clone(),
});
}
}
}
}
}
}

Artifacts(artifacts)
}

/// This converts a `SourceFile` that doesn't contain _any_ contract definitions (interfaces,
/// contracts, libraries) to an artifact.
///
/// We do this because not all `SourceFile`s emitted by solc have at least 1 corresponding entry
/// in the `contracts`
/// section of the solc output. For example for an `errors.sol` that only contains custom error
/// definitions and no contract, no `Contract` object will be generated by solc. However, we
/// still want to emit an `Artifact` for that file that may include the `ast`, docs etc.,
/// because other tools depend on this, such as slither.
fn standalone_source_file_to_artifact(
&self,
_path: &str,
_file: &VersionedSourceFile,
) -> Option<Self::Artifact>;
}

/// An `Artifact` implementation that uses a compact representation
Expand Down Expand Up @@ -695,6 +766,14 @@ impl ArtifactOutput for MinimalCombinedArtifacts {
) -> Self::Artifact {
Self::Artifact::from(contract)
}

fn standalone_source_file_to_artifact(
&self,
_path: &str,
_file: &VersionedSourceFile,
) -> Option<Self::Artifact> {
None
}
}

/// An Artifacts handler implementation that works the same as `MinimalCombinedArtifacts` but also
Expand Down Expand Up @@ -739,6 +818,14 @@ impl ArtifactOutput for MinimalCombinedArtifactsHardhatFallback {
) -> Self::Artifact {
MinimalCombinedArtifacts::default().contract_to_artifact(file, name, contract, source_file)
}

fn standalone_source_file_to_artifact(
&self,
path: &str,
file: &VersionedSourceFile,
) -> Option<Self::Artifact> {
MinimalCombinedArtifacts::default().standalone_source_file_to_artifact(path, file)
}
}

#[cfg(test)]
Expand Down
16 changes: 16 additions & 0 deletions ethers-solc/src/artifacts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1634,6 +1634,22 @@ pub struct SourceFile {
pub ast: Option<Ast>,
}

// === impl SourceFile ===

impl SourceFile {
/// Returns `true` if the source file contains at least 1 `ContractDefinition` such as
/// `contract`, `abstract contract`, `interface` or `library`
pub fn contains_contract_definition(&self) -> bool {
if let Some(ref ast) = self.ast {
// contract definitions are only allowed at the source-unit level <https://docs.soliditylang.org/en/latest/grammar.html>
return ast.nodes.iter().any(|node| node.node_type == NodeType::ContractDefinition)
// abstract contract, interfaces: ContractDefinition
}

false
}
}

/// A wrapper type for a list of source files
/// `path -> SourceFile`
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
Expand Down
26 changes: 14 additions & 12 deletions ethers-solc/src/compile/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,23 +265,25 @@ impl<'a, T: ArtifactOutput> CompiledState<'a, T> {
fn write_artifacts(self) -> Result<ArtifactsState<'a, T>> {
let CompiledState { output, cache } = self;

// write all artifacts via the handler but only if the build succeeded
let compiled_artifacts = if cache.project().no_artifacts {
cache
.project()
.artifacts_handler()
.output_to_artifacts(&output.contracts, &output.sources)
let project = cache.project();
// write all artifacts via the handler but only if the build succeeded and project wasn't
// configured with `no_artifacts == true`
let compiled_artifacts = if project.no_artifacts {
project.artifacts_handler().output_to_artifacts(&output.contracts, &output.sources)
} else if output.has_error() {
tracing::trace!("skip writing cache file due to solc errors: {:?}", output.errors);
cache
.project()
.artifacts_handler()
.output_to_artifacts(&output.contracts, &output.sources)
project.artifacts_handler().output_to_artifacts(&output.contracts, &output.sources)
} else {
cache.project().artifacts_handler().on_output(
tracing::trace!(
"handling artifact output for {} contracts and {} sources",
output.contracts.len(),
output.sources.len()
);
// this emits the artifacts via the project's artifacts handler
project.artifacts_handler().on_output(
&output.contracts,
&output.sources,
&cache.project().paths,
&project.paths,
)?
};

Expand Down
10 changes: 9 additions & 1 deletion ethers-solc/src/hh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::{
contract::{CompactContract, CompactContractBytecode, Contract, ContractBytecode},
CompactContractBytecodeCow, LosslessAbi, Offsets,
},
ArtifactOutput, SourceFile,
ArtifactOutput, SourceFile, VersionedSourceFile,
};
use serde::{Deserialize, Serialize};
use std::{borrow::Cow, collections::btree_map::BTreeMap};
Expand Down Expand Up @@ -135,6 +135,14 @@ impl ArtifactOutput for HardhatArtifacts {
deployed_link_references,
}
}

fn standalone_source_file_to_artifact(
&self,
_path: &str,
_file: &VersionedSourceFile,
) -> Option<Self::Artifact> {
None
}
}

#[cfg(test)]
Expand Down
10 changes: 9 additions & 1 deletion ethers-solc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ use crate::{
artifacts::Sources,
cache::SolFilesCache,
error::{SolcError, SolcIoError},
sources::VersionedSourceFiles,
sources::{VersionedSourceFile, VersionedSourceFiles},
};
use artifacts::contract::Contract;
use compile::output::contracts::VersionedContracts;
Expand Down Expand Up @@ -830,6 +830,14 @@ impl<T: ArtifactOutput> ArtifactOutput for Project<T> {
) -> Artifacts<Self::Artifact> {
self.artifacts_handler().output_to_artifacts(contracts, sources)
}

fn standalone_source_file_to_artifact(
&self,
path: &str,
file: &VersionedSourceFile,
) -> Option<Self::Artifact> {
self.artifacts_handler().standalone_source_file_to_artifact(path, file)
}
}

#[cfg(test)]
Expand Down
Loading