Skip to content

Commit

Permalink
feat(solc): emit artifacts for standalone source files (gakonst#1296)
Browse files Browse the repository at this point in the history
* feat(solc): emit artifact files for sources with no contracts

* test(solc): add tests for emitting standalone sources

* chore: update CHANGELOG

* style: check ast is some
  • Loading branch information
mattsse committed May 28, 2022
1 parent 652cb43 commit 1c51964
Show file tree
Hide file tree
Showing 8 changed files with 305 additions and 15 deletions.
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

0 comments on commit 1c51964

Please sign in to comment.