factor out solc JSON interface crate (#264)

The differential testing framework will make a second consumer. There
seems to be no re-usable Rust crate for this. But we already have
everything here, just needs a small refactor to make it fully re-usable.

- Mostly decouple the solc JSON-input-output interface types from the
`solidity` frontend crate
- Expose the JSON-input-output interface types in a dedicated crate

---------

Signed-off-by: Cyrill Leutwiler <bigcyrill@hotmail.com>
This commit is contained in:
xermicus
2025-03-20 17:11:40 +01:00
committed by GitHub
parent 36ea69b50f
commit 497dae2494
45 changed files with 328 additions and 245 deletions
+3 -4
View File
@@ -5,12 +5,11 @@ use std::fs::File;
use std::io::Write;
use std::path::Path;
use revive_solc_json_interface::CombinedJsonContract;
use revive_solc_json_interface::SolcStandardJsonOutputContract;
use serde::Deserialize;
use serde::Serialize;
use crate::solc::combined_json::contract::Contract as CombinedJsonContract;
use crate::solc::standard_json::output::contract::Contract as StandardJsonOutputContract;
/// The Solidity contract build.
#[derive(Debug, Serialize, Deserialize)]
pub struct Contract {
@@ -131,7 +130,7 @@ impl Contract {
/// Writes the contract text assembly and bytecode to the standard JSON.
pub fn write_to_standard_json(
self,
standard_json_contract: &mut StandardJsonOutputContract,
standard_json_contract: &mut SolcStandardJsonOutputContract,
) -> anyhow::Result<()> {
standard_json_contract.metadata = Some(self.metadata_json);
+4 -3
View File
@@ -5,8 +5,9 @@ pub mod contract;
use std::collections::BTreeMap;
use std::path::Path;
use crate::solc::combined_json::CombinedJson;
use crate::solc::standard_json::output::Output as StandardJsonOutput;
use revive_solc_json_interface::combined_json::CombinedJson;
use revive_solc_json_interface::SolcStandardJsonOutput;
use crate::solc::version::Version as SolcVersion;
use crate::ResolcVersion;
@@ -66,7 +67,7 @@ impl Build {
/// Writes all contracts assembly and bytecode to the standard JSON.
pub fn write_to_standard_json(
mut self,
standard_json: &mut StandardJsonOutput,
standard_json: &mut SolcStandardJsonOutput,
solc_version: &SolcVersion,
) -> anyhow::Result<()> {
let contracts = match standard_json.contracts.as_mut() {
+25 -27
View File
@@ -7,7 +7,6 @@ pub(crate) mod process;
pub(crate) mod project;
pub(crate) mod solc;
pub(crate) mod version;
pub(crate) mod warning;
pub(crate) mod yul;
pub use self::build::contract::Contract as ContractBuild;
@@ -23,31 +22,15 @@ pub use self::process::Process;
pub use self::project::contract::Contract as ProjectContract;
pub use self::project::Project;
pub use self::r#const::*;
pub use self::solc::combined_json::contract::Contract as SolcCombinedJsonContract;
pub use self::solc::combined_json::CombinedJson as SolcCombinedJson;
#[cfg(not(target_os = "emscripten"))]
pub use self::solc::solc_compiler::SolcCompiler;
#[cfg(target_os = "emscripten")]
pub use self::solc::soljson_compiler::SoljsonCompiler;
pub use self::solc::standard_json::input::language::Language as SolcStandardJsonInputLanguage;
pub use self::solc::standard_json::input::settings::metadata::Metadata as SolcStandardJsonInputSettingsMetadata;
pub use self::solc::standard_json::input::settings::optimizer::Optimizer as SolcStandardJsonInputSettingsOptimizer;
pub use self::solc::standard_json::input::settings::selection::file::flag::Flag as SolcStandardJsonInputSettingsSelectionFileFlag;
pub use self::solc::standard_json::input::settings::selection::file::File as SolcStandardJsonInputSettingsSelectionFile;
pub use self::solc::standard_json::input::settings::selection::Selection as SolcStandardJsonInputSettingsSelection;
pub use self::solc::standard_json::input::settings::Settings as SolcStandardJsonInputSettings;
pub use self::solc::standard_json::input::source::Source as SolcStandardJsonInputSource;
pub use self::solc::standard_json::input::Input as SolcStandardJsonInput;
pub use self::solc::standard_json::output::contract::evm::bytecode::Bytecode as SolcStandardJsonOutputContractEVMBytecode;
pub use self::solc::standard_json::output::contract::evm::EVM as SolcStandardJsonOutputContractEVM;
pub use self::solc::standard_json::output::contract::Contract as SolcStandardJsonOutputContract;
pub use self::solc::standard_json::output::Output as SolcStandardJsonOutput;
pub use self::solc::version::Version as SolcVersion;
pub use self::solc::Compiler;
pub use self::solc::FIRST_SUPPORTED_VERSION as SolcFirstSupportedVersion;
pub use self::solc::LAST_SUPPORTED_VERSION as SolcLastSupportedVersion;
pub use self::version::Version as ResolcVersion;
pub use self::warning::Warning;
#[cfg(not(target_os = "emscripten"))]
pub mod test_utils;
@@ -57,6 +40,13 @@ use std::collections::BTreeSet;
use std::io::Write;
use std::path::PathBuf;
use revive_solc_json_interface::standard_json::input::settings::metadata_hash::MetadataHash;
use revive_solc_json_interface::ResolcWarning;
use revive_solc_json_interface::SolcStandardJsonInput;
use revive_solc_json_interface::SolcStandardJsonInputLanguage;
use revive_solc_json_interface::SolcStandardJsonInputSettingsOptimizer;
use revive_solc_json_interface::SolcStandardJsonInputSettingsSelection;
/// Runs the Yul mode.
pub fn yul<T: Compiler>(
input_files: &[PathBuf],
@@ -126,7 +116,7 @@ pub fn standard_output<T: Compiler>(
include_paths: Vec<String>,
allow_paths: Option<String>,
remappings: Option<BTreeSet<String>>,
suppressed_warnings: Option<Vec<Warning>>,
suppressed_warnings: Option<Vec<ResolcWarning>>,
debug_config: revive_llvm_context::DebugConfig,
) -> anyhow::Result<Build> {
let solc_version = solc.version()?;
@@ -155,7 +145,7 @@ pub fn standard_output<T: Compiler>(
.collect();
let libraries = solc_input.settings.libraries.clone().unwrap_or_default();
let mut solc_output = solc.standard_json(solc_input, base_path, include_paths, allow_paths)?;
let solc_output = solc.standard_json(solc_input, base_path, include_paths, allow_paths)?;
if let Some(errors) = solc_output.errors.as_deref() {
let mut has_errors = false;
@@ -173,8 +163,13 @@ pub fn standard_output<T: Compiler>(
}
}
let project =
solc_output.try_to_project(source_code_files, libraries, &solc_version, &debug_config)?;
let project = Project::try_from_standard_json_output(
&solc_output,
source_code_files,
libraries,
&solc_version,
&debug_config,
)?;
let build = project.compile(optimizer_settings, include_metadata_hash, debug_config)?;
@@ -203,9 +198,7 @@ pub fn standard_json<T: Compiler>(
revive_llvm_context::OptimizerSettings::try_from(&solc_input.settings.optimizer)?;
let include_metadata_hash = match solc_input.settings.metadata {
Some(ref metadata) => {
metadata.bytecode_hash != Some(revive_llvm_context::PolkaVMMetadataHash::None)
}
Some(ref metadata) => metadata.bytecode_hash != Some(MetadataHash::None),
None => true,
};
@@ -221,8 +214,13 @@ pub fn standard_json<T: Compiler>(
}
}
let project =
solc_output.try_to_project(source_code_files, libraries, &solc_version, &debug_config)?;
let project = Project::try_from_standard_json_output(
&solc_output,
source_code_files,
libraries,
&solc_version,
&debug_config,
)?;
if detect_missing_libraries {
let missing_libraries = project.get_missing_libraries();
@@ -250,7 +248,7 @@ pub fn combined_json<T: Compiler>(
include_paths: Vec<String>,
allow_paths: Option<String>,
remappings: Option<BTreeSet<String>>,
suppressed_warnings: Option<Vec<Warning>>,
suppressed_warnings: Option<Vec<ResolcWarning>>,
debug_config: revive_llvm_context::DebugConfig,
output_directory: Option<PathBuf>,
overwrite: bool,
+3 -2
View File
@@ -3,7 +3,8 @@
use std::collections::BTreeMap;
use std::collections::HashSet;
use crate::solc::standard_json::output::Output as StandardJsonOutput;
use revive_solc_json_interface::SolcStandardJsonOutput;
use crate::solc::version::Version as SolcVersion;
use crate::ResolcVersion;
@@ -22,7 +23,7 @@ impl MissingLibraries {
/// Writes the missing libraries to the standard JSON.
pub fn write_to_standard_json(
mut self,
standard_json: &mut StandardJsonOutput,
standard_json: &mut SolcStandardJsonOutput,
solc_version: &SolcVersion,
) -> anyhow::Result<()> {
let contracts = match standard_json.contracts.as_mut() {
+64
View File
@@ -9,6 +9,7 @@ use std::path::Path;
#[cfg(feature = "parallel")]
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use revive_solc_json_interface::SolcStandardJsonOutput;
use serde::Deserialize;
use serde::Serialize;
use sha3::Digest;
@@ -243,6 +244,69 @@ impl Project {
BTreeMap::new(),
))
}
/// Converts the `solc` JSON output into a convenient project.
pub fn try_from_standard_json_output(
output: &SolcStandardJsonOutput,
source_code_files: BTreeMap<String, String>,
libraries: BTreeMap<String, BTreeMap<String, String>>,
solc_version: &SolcVersion,
debug_config: &revive_llvm_context::DebugConfig,
) -> anyhow::Result<Self> {
let files = match output.contracts.as_ref() {
Some(files) => files,
None => match &output.errors {
Some(errors) if errors.iter().any(|e| e.severity == "error") => {
anyhow::bail!(serde_json::to_string_pretty(errors).expect("Always valid"));
}
_ => &BTreeMap::new(),
},
};
let mut project_contracts = BTreeMap::new();
for (path, contracts) in files.iter() {
for (name, contract) in contracts.iter() {
let full_path = format!("{path}:{name}");
let ir_optimized = match contract.ir_optimized.to_owned() {
Some(ir_optimized) => ir_optimized,
None => continue,
};
if ir_optimized.is_empty() {
continue;
}
debug_config.dump_yul(full_path.as_str(), ir_optimized.as_str())?;
let mut lexer = Lexer::new(ir_optimized.to_owned());
let object = Object::parse(&mut lexer, None).map_err(|error| {
anyhow::anyhow!("Contract `{}` parsing error: {:?}", full_path, error)
})?;
let source = IR::new_yul(ir_optimized.to_owned(), object);
let source_code = source_code_files
.get(path.as_str())
.ok_or_else(|| anyhow::anyhow!("Source code for path `{}` not found", path))?;
let source_hash = sha3::Keccak256::digest(source_code.as_bytes()).into();
let project_contract = Contract::new(
full_path.clone(),
source_hash,
solc_version.to_owned(),
source,
contract.metadata.to_owned(),
);
project_contracts.insert(full_path, project_contract);
}
}
Ok(Project::new(
solc_version.to_owned(),
project_contracts,
libraries,
))
}
}
impl revive_llvm_context::PolkaVMDependency for Project {
+5 -3
View File
@@ -104,7 +104,7 @@ fn main_inner() -> anyhow::Result<()> {
let (input_files, remappings) = arguments.split_input_files_and_remappings()?;
let suppressed_warnings = match arguments.suppress_warnings {
Some(warnings) => Some(revive_solidity::Warning::try_from_strings(
Some(warnings) => Some(revive_solc_json_interface::ResolcWarning::try_from_strings(
warnings.as_slice(),
)?),
None => None,
@@ -142,8 +142,10 @@ fn main_inner() -> anyhow::Result<()> {
let include_metadata_hash = match arguments.metadata_hash {
Some(metadata_hash) => {
let metadata =
revive_llvm_context::PolkaVMMetadataHash::from_str(metadata_hash.as_str())?;
metadata != revive_llvm_context::PolkaVMMetadataHash::None
revive_solc_json_interface::SolcStandardJsonInputSettingsMetadataHash::from_str(
metadata_hash.as_str(),
)?;
metadata != revive_solc_json_interface::SolcStandardJsonInputSettingsMetadataHash::None
}
None => true,
};
@@ -1,72 +0,0 @@
//! The `solc --combined-json` contract.
use std::collections::BTreeMap;
use std::collections::HashSet;
use serde::Deserialize;
use serde::Serialize;
/// The contract.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Contract {
/// The `solc` hashes output.
#[serde(skip_serializing_if = "Option::is_none")]
pub hashes: Option<BTreeMap<String, String>>,
/// The `solc` ABI output.
#[serde(skip_serializing_if = "Option::is_none")]
pub abi: Option<serde_json::Value>,
/// The `solc` metadata output.
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<String>,
/// The `solc` developer documentation output.
#[serde(skip_serializing_if = "Option::is_none")]
pub devdoc: Option<serde_json::Value>,
/// The `solc` user documentation output.
#[serde(skip_serializing_if = "Option::is_none")]
pub userdoc: Option<serde_json::Value>,
/// The `solc` storage layout output.
#[serde(skip_serializing_if = "Option::is_none")]
pub storage_layout: Option<serde_json::Value>,
/// The `solc` AST output.
#[serde(skip_serializing_if = "Option::is_none")]
pub ast: Option<serde_json::Value>,
/// The `solc` assembly output.
#[serde(skip_serializing_if = "Option::is_none")]
pub asm: Option<serde_json::Value>,
/// The `solc` hexadecimal binary output.
#[serde(skip_serializing_if = "Option::is_none")]
pub bin: Option<String>,
/// The `solc` hexadecimal binary runtime part output.
#[serde(skip_serializing_if = "Option::is_none")]
pub bin_runtime: Option<String>,
/// The factory dependencies.
#[serde(skip_serializing_if = "Option::is_none")]
pub factory_deps: Option<BTreeMap<String, String>>,
/// The missing libraries.
#[serde(skip_serializing_if = "Option::is_none")]
pub missing_libraries: Option<HashSet<String>>,
}
impl Contract {
/// Returns the signature hash of the specified contract entry.
/// # Panics
/// If the hashes have not been requested in the `solc` call.
pub fn entry(&self, entry: &str) -> u32 {
self.hashes
.as_ref()
.expect("Always exists")
.iter()
.find_map(|(contract_entry, hash)| {
if contract_entry.starts_with(entry) {
Some(
u32::from_str_radix(hash.as_str(), revive_common::BASE_HEXADECIMAL)
.expect("Test hash is always valid"),
)
} else {
None
}
})
.unwrap_or_else(|| panic!("Entry `{entry}` not found"))
}
}
@@ -1,95 +0,0 @@
//! The `solc --combined-json` output.
pub mod contract;
use std::collections::BTreeMap;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use serde::Deserialize;
use serde::Serialize;
use self::contract::Contract;
/// The `solc --combined-json` output.
#[derive(Debug, Serialize, Deserialize)]
pub struct CombinedJson {
/// The contract entries.
pub contracts: BTreeMap<String, Contract>,
/// The list of source files.
#[serde(rename = "sourceList")]
#[serde(skip_serializing_if = "Option::is_none")]
pub source_list: Option<Vec<String>>,
/// The source code extra data, including the AST.
#[serde(skip_serializing_if = "Option::is_none")]
pub sources: Option<serde_json::Value>,
/// The `solc` compiler version.
pub version: String,
/// The `resolc` compiler version.
#[serde(skip_serializing_if = "Option::is_none")]
pub revive_version: Option<String>,
}
impl CombinedJson {
/// Returns the signature hash of the specified contract and entry.
pub fn entry(&self, path: &str, entry: &str) -> u32 {
self.contracts
.iter()
.find_map(|(name, contract)| {
if name.starts_with(path) {
Some(contract)
} else {
None
}
})
.expect("Always exists")
.entry(entry)
}
/// Returns the full contract path which can be found in `combined-json` output.
pub fn get_full_path(&self, name: &str) -> Option<String> {
self.contracts.iter().find_map(|(path, _value)| {
if let Some(last_slash_position) = path.rfind('/') {
if let Some(colon_position) = path.rfind(':') {
if &path[last_slash_position + 1..colon_position] == name {
return Some(path.to_owned());
}
}
}
None
})
}
/// Removes EVM artifacts to prevent their accidental usage.
pub fn remove_evm(&mut self) {
for (_, contract) in self.contracts.iter_mut() {
contract.bin = None;
contract.bin_runtime = None;
}
}
/// Writes the JSON to the specified directory.
pub fn write_to_directory(
self,
output_directory: &Path,
overwrite: bool,
) -> anyhow::Result<()> {
let mut file_path = output_directory.to_owned();
file_path.push(format!("combined.{}", revive_common::EXTENSION_JSON));
if file_path.exists() && !overwrite {
anyhow::bail!(
"Refusing to overwrite an existing file {file_path:?} (use --overwrite to force)."
);
}
File::create(&file_path)
.map_err(|error| anyhow::anyhow!("File {:?} creating error: {}", file_path, error))?
.write_all(serde_json::to_vec(&self).expect("Always valid").as_slice())
.map_err(|error| anyhow::anyhow!("File {:?} writing error: {}", file_path, error))?;
Ok(())
}
}
+6 -7
View File
@@ -1,19 +1,18 @@
//! The Solidity compiler.
pub mod combined_json;
#[cfg(not(target_os = "emscripten"))]
pub mod solc_compiler;
#[cfg(target_os = "emscripten")]
pub mod soljson_compiler;
pub mod standard_json;
pub mod version;
use std::path::Path;
use std::path::PathBuf;
use self::combined_json::CombinedJson;
use self::standard_json::input::Input as StandardJsonInput;
use self::standard_json::output::Output as StandardJsonOutput;
use revive_solc_json_interface::combined_json::CombinedJson;
use revive_solc_json_interface::SolcStandardJsonInput;
use revive_solc_json_interface::SolcStandardJsonOutput;
use self::version::Version;
/// The first version of `solc` with the support of standard JSON interface.
@@ -30,11 +29,11 @@ pub trait Compiler {
/// Compiles the Solidity `--standard-json` input into Yul IR.
fn standard_json(
&mut self,
input: StandardJsonInput,
input: SolcStandardJsonInput,
base_path: Option<String>,
include_paths: Vec<String>,
allow_paths: Option<String>,
) -> anyhow::Result<StandardJsonOutput>;
) -> anyhow::Result<SolcStandardJsonOutput>;
/// The `solc --combined-json abi,hashes...` mirror.
fn combined_json(
+7 -6
View File
@@ -4,9 +4,10 @@ use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use crate::solc::combined_json::CombinedJson;
use crate::solc::standard_json::input::Input as StandardJsonInput;
use crate::solc::standard_json::output::Output as StandardJsonOutput;
use revive_solc_json_interface::combined_json::CombinedJson;
use revive_solc_json_interface::SolcStandardJsonInput;
use revive_solc_json_interface::SolcStandardJsonOutput;
use crate::solc::version::Version;
use super::Compiler;
@@ -39,11 +40,11 @@ impl Compiler for SolcCompiler {
/// Compiles the Solidity `--standard-json` input into Yul IR.
fn standard_json(
&mut self,
mut input: StandardJsonInput,
mut input: SolcStandardJsonInput,
base_path: Option<String>,
include_paths: Vec<String>,
allow_paths: Option<String>,
) -> anyhow::Result<StandardJsonOutput> {
) -> anyhow::Result<SolcStandardJsonOutput> {
let version = self.version()?.validate(&include_paths)?.default;
let mut command = std::process::Command::new(self.executable.as_str());
@@ -93,7 +94,7 @@ impl Compiler for SolcCompiler {
);
}
let mut output: StandardJsonOutput =
let mut output: SolcStandardJsonOutput =
revive_common::deserialize_from_slice(output.stdout.as_slice()).map_err(|error| {
anyhow::anyhow!(
"{} subprocess output parsing error: {}\n{}",
+8 -7
View File
@@ -3,9 +3,10 @@
use std::path::Path;
use std::path::PathBuf;
use crate::solc::combined_json::CombinedJson;
use crate::solc::standard_json::input::Input as StandardJsonInput;
use crate::solc::standard_json::output::Output as StandardJsonOutput;
use revive_solc_json_interface::combined_json::CombinedJson;
use revive_solc_json_interface::SolcStandardJsonInput;
use revive_solc_json_interface::SolcStandardJsonOutput;
use crate::solc::version::Version;
use anyhow::Context;
use std::ffi::{c_char, c_void, CStr, CString};
@@ -24,11 +25,11 @@ impl Compiler for SoljsonCompiler {
/// Compiles the Solidity `--standard-json` input into Yul IR.
fn standard_json(
&mut self,
mut input: StandardJsonInput,
mut input: SolcStandardJsonInput,
base_path: Option<String>,
include_paths: Vec<String>,
allow_paths: Option<String>,
) -> anyhow::Result<StandardJsonOutput> {
) -> anyhow::Result<SolcStandardJsonOutput> {
if !include_paths.is_empty() {
anyhow::bail!("configuring include paths is not supported with solJson")
}
@@ -46,8 +47,8 @@ impl Compiler for SoljsonCompiler {
let input_json = serde_json::to_string(&input).expect("Always valid");
let out = Self::compile_standard_json(input_json)?;
let mut output: StandardJsonOutput = revive_common::deserialize_from_slice(out.as_bytes())
.map_err(|error| {
let mut output: SolcStandardJsonOutput =
revive_common::deserialize_from_slice(out.as_bytes()).map_err(|error| {
anyhow::anyhow!(
"Soljson output parsing error: {}\n{}",
error,
@@ -1,22 +0,0 @@
//! The `solc --standard-json` input language.
use serde::Deserialize;
use serde::Serialize;
/// The `solc --standard-json` input language.
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Language {
/// The Solidity language.
Solidity,
/// The Yul IR.
Yul,
}
impl std::fmt::Display for Language {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Solidity => write!(f, "Solidity"),
Self::Yul => write!(f, "Yul"),
}
}
}
@@ -1,137 +0,0 @@
//! The `solc --standard-json` input.
pub mod language;
pub mod settings;
pub mod source;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::path::PathBuf;
#[cfg(feature = "parallel")]
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use serde::Deserialize;
use serde::Serialize;
use crate::solc::standard_json::input::settings::metadata::Metadata as SolcStandardJsonInputSettingsMetadata;
use crate::solc::standard_json::input::settings::optimizer::Optimizer as SolcStandardJsonInputSettingsOptimizer;
use crate::solc::standard_json::input::settings::selection::Selection as SolcStandardJsonInputSettingsSelection;
use crate::warning::Warning;
use self::language::Language;
use self::settings::Settings;
use self::source::Source;
/// The `solc --standard-json` input.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Input {
/// The input language.
pub language: Language,
/// The input source code files hashmap.
pub sources: BTreeMap<String, Source>,
/// The compiler settings.
pub settings: Settings,
/// The suppressed warnings.
#[serde(skip_serializing)]
pub suppressed_warnings: Option<Vec<Warning>>,
}
impl Input {
/// A shortcut constructor from stdin.
pub fn try_from_stdin() -> anyhow::Result<Self> {
let mut input: Self = serde_json::from_reader(std::io::BufReader::new(std::io::stdin()))?;
input
.settings
.output_selection
.get_or_insert_with(SolcStandardJsonInputSettingsSelection::default)
.extend_with_required();
Ok(input)
}
/// A shortcut constructor from paths.
#[allow(clippy::too_many_arguments)]
pub fn try_from_paths(
language: Language,
evm_version: Option<revive_common::EVMVersion>,
paths: &[PathBuf],
library_map: Vec<String>,
remappings: Option<BTreeSet<String>>,
output_selection: SolcStandardJsonInputSettingsSelection,
optimizer: SolcStandardJsonInputSettingsOptimizer,
metadata: Option<SolcStandardJsonInputSettingsMetadata>,
suppressed_warnings: Option<Vec<Warning>>,
) -> anyhow::Result<Self> {
let mut paths: BTreeSet<PathBuf> = paths.iter().cloned().collect();
let libraries = Settings::parse_libraries(library_map)?;
for library_file in libraries.keys() {
paths.insert(PathBuf::from(library_file));
}
let sources = paths
.iter()
.map(|path| {
let source = Source::try_from(path.as_path()).unwrap_or_else(|error| {
panic!("Source code file {path:?} reading error: {error}")
});
(path.to_string_lossy().to_string(), source)
})
.collect();
Ok(Self {
language,
sources,
settings: Settings::new(
evm_version,
libraries,
remappings,
output_selection,
optimizer,
metadata,
),
suppressed_warnings,
})
}
/// A shortcut constructor from source code.
/// Only for the integration test purposes.
#[allow(clippy::too_many_arguments)]
pub fn try_from_sources(
evm_version: Option<revive_common::EVMVersion>,
sources: BTreeMap<String, String>,
libraries: BTreeMap<String, BTreeMap<String, String>>,
remappings: Option<BTreeSet<String>>,
output_selection: SolcStandardJsonInputSettingsSelection,
optimizer: SolcStandardJsonInputSettingsOptimizer,
metadata: Option<SolcStandardJsonInputSettingsMetadata>,
suppressed_warnings: Option<Vec<Warning>>,
) -> anyhow::Result<Self> {
#[cfg(feature = "parallel")]
let iter = sources.into_par_iter(); // Parallel iterator
#[cfg(not(feature = "parallel"))]
let iter = sources.into_iter(); // Sequential iterator
let sources = iter
.map(|(path, content)| (path, Source::from(content)))
.collect();
Ok(Self {
language: Language::Solidity,
sources,
settings: Settings::new(
evm_version,
libraries,
remappings,
output_selection,
optimizer,
metadata,
),
suppressed_warnings,
})
}
/// Sets the necessary defaults.
pub fn normalize(&mut self, version: &semver::Version) {
self.settings.normalize(version);
}
}
@@ -1,22 +0,0 @@
//! The `solc --standard-json` input settings metadata.
use serde::Deserialize;
use serde::Serialize;
/// The `solc --standard-json` input settings metadata.
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Metadata {
/// The bytecode hash mode.
#[serde(skip_serializing_if = "Option::is_none")]
pub bytecode_hash: Option<revive_llvm_context::PolkaVMMetadataHash>,
}
impl Metadata {
/// A shortcut constructor.
pub fn new(bytecode_hash: revive_llvm_context::PolkaVMMetadataHash) -> Self {
Self {
bytecode_hash: Some(bytecode_hash),
}
}
}
@@ -1,100 +0,0 @@
//! The `solc --standard-json` input settings.
pub mod metadata;
pub mod optimizer;
pub mod selection;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use serde::Deserialize;
use serde::Serialize;
use self::metadata::Metadata;
use self::optimizer::Optimizer;
use self::selection::Selection;
/// The `solc --standard-json` input settings.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Settings {
/// The target EVM version.
#[serde(skip_serializing_if = "Option::is_none")]
pub evm_version: Option<revive_common::EVMVersion>,
/// The linker library addresses.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub libraries: Option<BTreeMap<String, BTreeMap<String, String>>>,
/// The sorted list of remappings.
#[serde(skip_serializing_if = "Option::is_none")]
pub remappings: Option<BTreeSet<String>>,
/// The output selection filters.
#[serde(skip_serializing_if = "Option::is_none")]
pub output_selection: Option<Selection>,
/// Whether to compile via IR. Only for testing with solc >=0.8.13.
#[serde(
rename = "viaIR",
skip_serializing_if = "Option::is_none",
skip_deserializing
)]
pub via_ir: Option<bool>,
/// The optimizer settings.
pub optimizer: Optimizer,
/// The metadata settings.
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Metadata>,
}
impl Settings {
/// A shortcut constructor.
pub fn new(
evm_version: Option<revive_common::EVMVersion>,
libraries: BTreeMap<String, BTreeMap<String, String>>,
remappings: Option<BTreeSet<String>>,
output_selection: Selection,
optimizer: Optimizer,
metadata: Option<Metadata>,
) -> Self {
Self {
evm_version,
libraries: Some(libraries),
remappings,
output_selection: Some(output_selection),
optimizer,
metadata,
via_ir: Some(true),
}
}
/// Sets the necessary defaults.
pub fn normalize(&mut self, version: &semver::Version) {
self.optimizer.normalize(version);
}
/// Parses the library list and returns their double hashmap with path and name as keys.
pub fn parse_libraries(
input: Vec<String>,
) -> anyhow::Result<BTreeMap<String, BTreeMap<String, String>>> {
let mut libraries = BTreeMap::new();
for (index, library) in input.into_iter().enumerate() {
let mut path_and_address = library.split('=');
let path = path_and_address
.next()
.ok_or_else(|| anyhow::anyhow!("The library #{} path is missing", index))?;
let mut file_and_contract = path.split(':');
let file = file_and_contract
.next()
.ok_or_else(|| anyhow::anyhow!("The library `{}` file name is missing", path))?;
let contract = file_and_contract.next().ok_or_else(|| {
anyhow::anyhow!("The library `{}` contract name is missing", path)
})?;
let address = path_and_address
.next()
.ok_or_else(|| anyhow::anyhow!("The library `{}` address is missing", path))?;
libraries
.entry(file.to_owned())
.or_insert_with(BTreeMap::new)
.insert(contract.to_owned(), address.to_owned());
}
Ok(libraries)
}
}
@@ -1,59 +0,0 @@
//! The `solc --standard-json` input settings optimizer details.
use serde::Deserialize;
use serde::Serialize;
/// The `solc --standard-json` input settings optimizer details.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Details {
/// Whether the pass is enabled.
pub peephole: bool,
/// Whether the pass is enabled.
#[serde(skip_serializing_if = "Option::is_none")]
pub inliner: Option<bool>,
/// Whether the pass is enabled.
pub jumpdest_remover: bool,
/// Whether the pass is enabled.
pub order_literals: bool,
/// Whether the pass is enabled.
pub deduplicate: bool,
/// Whether the pass is enabled.
pub cse: bool,
/// Whether the pass is enabled.
pub constant_optimizer: bool,
}
impl Details {
/// A shortcut constructor.
pub fn new(
peephole: bool,
inliner: Option<bool>,
jumpdest_remover: bool,
order_literals: bool,
deduplicate: bool,
cse: bool,
constant_optimizer: bool,
) -> Self {
Self {
peephole,
inliner,
jumpdest_remover,
order_literals,
deduplicate,
cse,
constant_optimizer,
}
}
/// Creates a set of disabled optimizations.
pub fn disabled(version: &semver::Version) -> Self {
let inliner = if version >= &semver::Version::new(0, 8, 5) {
Some(false)
} else {
None
};
Self::new(false, inliner, false, false, false, false, false)
}
}
@@ -1,66 +0,0 @@
//! The `solc --standard-json` input settings optimizer.
pub mod details;
use serde::Deserialize;
use serde::Serialize;
use self::details::Details;
/// The `solc --standard-json` input settings optimizer.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Optimizer {
/// Whether the optimizer is enabled.
pub enabled: bool,
/// The optimization mode string.
#[serde(skip_serializing)]
pub mode: Option<char>,
/// The `solc` optimizer details.
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<Details>,
/// Whether to try to recompile with -Oz if the bytecode is too large.
#[serde(skip_serializing)]
pub fallback_to_optimizing_for_size: Option<bool>,
}
impl Optimizer {
/// A shortcut constructor.
pub fn new(
enabled: bool,
mode: Option<char>,
version: &semver::Version,
fallback_to_optimizing_for_size: bool,
) -> Self {
Self {
enabled,
mode,
details: Some(Details::disabled(version)),
fallback_to_optimizing_for_size: Some(fallback_to_optimizing_for_size),
}
}
/// Sets the necessary defaults.
pub fn normalize(&mut self, version: &semver::Version) {
self.details = if version >= &semver::Version::new(0, 5, 5) {
Some(Details::disabled(version))
} else {
None
};
}
}
impl TryFrom<&Optimizer> for revive_llvm_context::OptimizerSettings {
type Error = anyhow::Error;
fn try_from(value: &Optimizer) -> Result<Self, Self::Error> {
let mut result = match value.mode {
Some(mode) => Self::try_from_cli(mode)?,
None => Self::cycles(),
};
if value.fallback_to_optimizing_for_size.unwrap_or_default() {
result.enable_fallback_to_size();
}
Ok(result)
}
}
@@ -1,66 +0,0 @@
//! The `solc --standard-json` expected output selection flag.
use serde::Deserialize;
use serde::Serialize;
/// The `solc --standard-json` expected output selection flag.
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Flag {
/// The ABI JSON.
#[serde(rename = "abi")]
ABI,
/// The metadata.
#[serde(rename = "metadata")]
Metadata,
/// The developer documentation.
#[serde(rename = "devdoc")]
Devdoc,
/// The user documentation.
#[serde(rename = "userdoc")]
Userdoc,
/// The function signature hashes JSON.
#[serde(rename = "evm.methodIdentifiers")]
MethodIdentifiers,
/// The storage layout.
#[serde(rename = "storageLayout")]
StorageLayout,
/// The AST JSON.
#[serde(rename = "ast")]
AST,
/// The Yul IR.
#[serde(rename = "irOptimized")]
Yul,
/// The EVM legacy assembly JSON.
#[serde(rename = "evm.legacyAssembly")]
EVMLA,
#[serde(rename = "evm.bytecode")]
EVMBC,
#[serde(rename = "evm.deployedBytecode")]
EVMDBC,
/// The assembly code
#[serde(rename = "evm.assembly")]
Assembly,
/// The Ir
#[serde(rename = "ir")]
Ir,
}
impl std::fmt::Display for Flag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ABI => write!(f, "abi"),
Self::Metadata => write!(f, "metadata"),
Self::Devdoc => write!(f, "devdoc"),
Self::Userdoc => write!(f, "userdoc"),
Self::MethodIdentifiers => write!(f, "evm.methodIdentifiers"),
Self::StorageLayout => write!(f, "storageLayout"),
Self::AST => write!(f, "ast"),
Self::Yul => write!(f, "irOptimized"),
Self::EVMLA => write!(f, "evm.legacyAssembly"),
Self::EVMBC => write!(f, "evm.bytecode"),
Self::EVMDBC => write!(f, "evm.deployedBytecode"),
Self::Assembly => write!(f, "evm.assembly"),
Self::Ir => write!(f, "ir"),
}
}
}
@@ -1,51 +0,0 @@
//! The `solc --standard-json` output file selection.
pub mod flag;
use std::collections::HashSet;
use serde::Deserialize;
use serde::Serialize;
use self::flag::Flag as SelectionFlag;
/// The `solc --standard-json` output file selection.
#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct File {
/// The per-file output selections.
#[serde(rename = "", skip_serializing_if = "Option::is_none")]
pub per_file: Option<HashSet<SelectionFlag>>,
/// The per-contract output selections.
#[serde(rename = "*", skip_serializing_if = "Option::is_none")]
pub per_contract: Option<HashSet<SelectionFlag>>,
}
impl File {
/// Creates the selection required by our compilation process.
pub fn new_required() -> Self {
Self {
per_file: Some(HashSet::from_iter([SelectionFlag::AST])),
per_contract: Some(HashSet::from_iter([
SelectionFlag::EVMBC,
SelectionFlag::EVMDBC,
SelectionFlag::MethodIdentifiers,
SelectionFlag::Metadata,
SelectionFlag::Yul,
])),
}
}
/// Extends the user's output selection with flag required by our compilation process.
pub fn extend_with_required(&mut self) -> &mut Self {
let required = Self::new_required();
self.per_file
.get_or_insert_with(HashSet::default)
.extend(required.per_file.unwrap_or_default());
self.per_contract
.get_or_insert_with(HashSet::default)
.extend(required.per_contract.unwrap_or_default());
self
}
}
@@ -1,99 +0,0 @@
//! The `solc --standard-json` output selection.
pub mod file;
use std::collections::BTreeMap;
use serde::Deserialize;
use serde::Serialize;
use self::file::File as FileSelection;
/// The `solc --standard-json` output selection.
#[derive(Debug, Serialize, Deserialize, Default, PartialEq)]
pub struct Selection {
/// Only the 'all' wildcard is available for robustness reasons.
#[serde(rename = "*", skip_serializing_if = "Option::is_none")]
all: Option<FileSelection>,
#[serde(skip_serializing_if = "BTreeMap::is_empty", flatten)]
pub files: BTreeMap<String, FileSelection>,
}
impl Selection {
/// Creates the selection required by our compilation process.
pub fn new_required() -> Self {
Self {
all: Some(FileSelection::new_required()),
files: BTreeMap::new(),
}
}
/// Extends the user's output selection with flag required by our compilation process.
pub fn extend_with_required(&mut self) -> &mut Self {
self.all
.get_or_insert_with(FileSelection::new_required)
.extend_with_required();
for (_, v) in self.files.iter_mut() {
v.extend_with_required();
}
self
}
}
#[cfg(test)]
mod test {
use std::collections::BTreeMap;
use crate::SolcStandardJsonInputSettingsSelectionFile;
use super::Selection;
#[test]
fn per_file() {
let init = Selection {
all: None,
files: BTreeMap::from([(
"Test".to_owned(),
SolcStandardJsonInputSettingsSelectionFile::new_required(),
)]),
};
let deser = serde_json::to_string(&init)
.and_then(|string| serde_json::from_str(&string))
.unwrap();
assert_eq!(init, deser)
}
#[test]
fn all() {
let init = Selection {
all: Some(SolcStandardJsonInputSettingsSelectionFile::new_required()),
files: BTreeMap::new(),
};
let deser = serde_json::to_string(&init)
.and_then(|string| serde_json::from_str(&string))
.unwrap();
assert_eq!(init, deser)
}
#[test]
fn all_and_override() {
let init = Selection {
all: Some(SolcStandardJsonInputSettingsSelectionFile::new_required()),
files: BTreeMap::from([(
"Test".to_owned(),
SolcStandardJsonInputSettingsSelectionFile::new_required(),
)]),
};
let deser = serde_json::to_string(&init)
.and_then(|string| serde_json::from_str(&string))
.unwrap();
assert_eq!(init, deser)
}
}
@@ -1,40 +0,0 @@
//! The `solc --standard-json` input source.
use std::io::Read;
use std::path::Path;
use serde::Deserialize;
use serde::Serialize;
/// The `solc --standard-json` input source.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Source {
/// The source code file content.
pub content: String,
}
impl From<String> for Source {
fn from(content: String) -> Self {
Self { content }
}
}
impl TryFrom<&Path> for Source {
type Error = anyhow::Error;
fn try_from(path: &Path) -> Result<Self, Self::Error> {
let content = if path.to_string_lossy() == "-" {
let mut solidity_code = String::with_capacity(16384);
std::io::stdin()
.read_to_string(&mut solidity_code)
.map_err(|error| anyhow::anyhow!("<stdin> reading error: {}", error))?;
solidity_code
} else {
std::fs::read_to_string(path)
.map_err(|error| anyhow::anyhow!("File {:?} reading error: {}", path, error))?
};
Ok(Self { content })
}
}
@@ -1,4 +0,0 @@
//! The `solc <input>.sol --standard-json`.
pub mod input;
pub mod output;
@@ -1,34 +0,0 @@
//! The `solc --standard-json` output contract EVM bytecode.
use serde::Deserialize;
use serde::Serialize;
/// The `solc --standard-json` output contract EVM bytecode.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Bytecode {
/// The bytecode object.
pub object: String,
}
impl Bytecode {
/// A shortcut constructor.
pub fn new(object: String) -> Self {
Self { object }
}
}
/// The `solc --standard-json` output contract EVM deployed bytecode.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct DeployedBytecode {
/// The bytecode object.
pub object: String,
}
impl DeployedBytecode {
/// A shortcut constructor.
pub fn new(object: String) -> Self {
Self { object }
}
}
@@ -1,42 +0,0 @@
//! The `solc --standard-json` output contract EVM data.
pub mod bytecode;
use std::collections::BTreeMap;
use serde::Deserialize;
use serde::Serialize;
use self::bytecode::Bytecode;
use self::bytecode::DeployedBytecode;
/// The `solc --standard-json` output contract EVM data.
/// It is replaced by PolkaVM data after compiling.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct EVM {
/// The contract PolkaVM assembly code.
#[serde(rename = "assembly", skip_serializing_if = "Option::is_none")]
pub assembly_text: Option<String>,
/// The contract bytecode.
/// Is reset by that of PolkaVM before yielding the compiled project artifacts.
#[serde(skip_serializing_if = "Option::is_none")]
pub bytecode: Option<Bytecode>,
/// The deployed bytecode of the contract.
/// It is overwritten with the PolkaVM blob before yielding the compiled project artifacts.
/// Hence it will be the same as the runtime code but we keep both for compatibility reasons.
#[serde(skip_serializing_if = "Option::is_none")]
pub deployed_bytecode: Option<DeployedBytecode>,
/// The contract function signatures.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub method_identifiers: Option<BTreeMap<String, String>>,
}
impl EVM {
/// Sets the PolkaVM assembly and bytecode.
pub fn modify(&mut self, assembly_text: String, bytecode: String) {
self.assembly_text = Some(assembly_text);
self.bytecode = Some(Bytecode::new(bytecode.clone()));
self.deployed_bytecode = Some(DeployedBytecode::new(bytecode));
}
}
@@ -1,50 +0,0 @@
//! The `solc --standard-json` output contract.
pub mod evm;
use std::collections::BTreeMap;
use std::collections::HashSet;
use serde::Deserialize;
use serde::Serialize;
use self::evm::EVM;
/// The `solc --standard-json` output contract.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Contract {
/// The contract ABI.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub abi: Option<serde_json::Value>,
/// The contract metadata.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
/// The contract developer documentation.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub devdoc: Option<serde_json::Value>,
/// The contract user documentation.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub userdoc: Option<serde_json::Value>,
/// The contract storage layout.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub storage_layout: Option<serde_json::Value>,
/// Contract's bytecode and related objects
#[serde(default, skip_serializing_if = "Option::is_none")]
pub evm: Option<EVM>,
/// The contract IR code.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ir: Option<String>,
/// The contract optimized IR code.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ir_optimized: Option<String>,
/// The contract PolkaVM bytecode hash.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hash: Option<String>,
/// The contract factory dependencies.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub factory_dependencies: Option<BTreeMap<String, String>>,
/// The contract missing libraries.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub missing_libraries: Option<HashSet<String>>,
}
@@ -1,135 +0,0 @@
//! The `solc --standard-json` output error.
pub mod source_location;
use std::str::FromStr;
use serde::Deserialize;
use serde::Serialize;
use self::source_location::SourceLocation;
/// The `solc --standard-json` output error.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Error {
/// The component type.
pub component: String,
/// The error code.
pub error_code: Option<String>,
/// The formatted error message.
pub formatted_message: String,
/// The non-formatted error message.
pub message: String,
/// The error severity.
pub severity: String,
/// The error location data.
pub source_location: Option<SourceLocation>,
/// The error type.
pub r#type: String,
}
impl Error {
/// Returns the `ecrecover` function usage warning.
pub fn message_ecrecover(src: Option<&str>) -> Self {
let message = r#"
Warning: It looks like you are using 'ecrecover' to validate a signature of a user account.
Polkadot comes with native account abstraction support, therefore it is highly recommended NOT
to rely on the fact that the account has an ECDSA private key attached to it since accounts might
implement other signature schemes.
"#
.to_owned();
Self {
component: "general".to_owned(),
error_code: None,
formatted_message: message.clone(),
message,
severity: "warning".to_owned(),
source_location: src.map(SourceLocation::from_str).and_then(Result::ok),
r#type: "Warning".to_owned(),
}
}
/// Returns the `<address payable>`'s `send` and `transfer` methods usage error.
pub fn message_send_and_transfer(src: Option<&str>) -> Self {
let message = r#"
Warning: It looks like you are using '<address payable>.send/transfer(<X>)'.
Using '<address payable>.send/transfer(<X>)' is deprecated and strongly discouraged!
The resolc compiler uses a heuristic to detect '<address payable>.send/transfer(<X>)' calls,
which disables call re-entrancy and supplies all remaining gas instead of the 2300 gas stipend.
However, detection is not guaranteed. You are advised to carefully test this, employ
re-entrancy guards or use the withdrawal pattern instead!
Learn more on https://docs.soliditylang.org/en/latest/security-considerations.html#reentrancy
and https://docs.soliditylang.org/en/latest/common-patterns.html#withdrawal-from-contracts
"#
.to_owned();
Self {
component: "general".to_owned(),
error_code: None,
formatted_message: message.clone(),
message,
severity: "warning".to_owned(),
source_location: src.map(SourceLocation::from_str).and_then(Result::ok),
r#type: "Warning".to_owned(),
}
}
/// Returns the `extcodesize` instruction usage warning.
pub fn message_extcodesize(src: Option<&str>) -> Self {
let message = r#"
Warning: Your code or one of its dependencies uses the 'extcodesize' instruction, which is
usually needed in the following cases:
1. To detect whether an address belongs to a smart contract.
2. To detect whether the deploy code execution has finished.
Polkadot comes with native account abstraction support (so smart contracts are just accounts
coverned by code), and you should avoid differentiating between contracts and non-contract
addresses.
"#
.to_owned();
Self {
component: "general".to_owned(),
error_code: None,
formatted_message: message.clone(),
message,
severity: "warning".to_owned(),
source_location: src.map(SourceLocation::from_str).and_then(Result::ok),
r#type: "Warning".to_owned(),
}
}
/// Returns the `origin` instruction usage warning.
pub fn message_tx_origin(src: Option<&str>) -> Self {
let message = r#"
Warning: You are checking for 'tx.origin' in your code, which might lead to unexpected behavior.
Polkadot comes with native account abstraction support, and therefore the initiator of a
transaction might be different from the contract calling your code. It is highly recommended NOT
to rely on tx.origin, but use msg.sender instead.
"#
.to_owned();
Self {
component: "general".to_owned(),
error_code: None,
formatted_message: message.clone(),
message,
severity: "warning".to_owned(),
source_location: src.map(SourceLocation::from_str).and_then(Result::ok),
r#type: "Warning".to_owned(),
}
}
/// Appends the contract path to the message..
pub fn push_contract_path(&mut self, path: &str) {
self.formatted_message
.push_str(format!("\n--> {path}\n").as_str());
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.formatted_message)
}
}
@@ -1,43 +0,0 @@
//! The `solc --standard-json` output error source location.
use std::str::FromStr;
use serde::Deserialize;
use serde::Serialize;
/// The `solc --standard-json` output error source location.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct SourceLocation {
/// The source file path.
pub file: String,
/// The start location.
pub start: isize,
/// The end location.
pub end: isize,
}
impl FromStr for SourceLocation {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
let mut parts = string.split(':');
let start = parts
.next()
.map(|string| string.parse::<isize>())
.and_then(Result::ok)
.unwrap_or_default();
let length = parts
.next()
.map(|string| string.parse::<isize>())
.and_then(Result::ok)
.unwrap_or_default();
let file = parts.next().unwrap_or_default().to_owned();
Ok(Self {
file,
start,
end: start + length,
})
}
}
@@ -1,138 +0,0 @@
//! The `solc --standard-json` output.
pub mod contract;
pub mod error;
pub mod source;
use std::collections::BTreeMap;
use serde::Deserialize;
use serde::Serialize;
use sha3::Digest;
use crate::project::contract::ir::IR as ProjectContractIR;
use crate::project::contract::Contract as ProjectContract;
use crate::project::Project;
use crate::solc::version::Version as SolcVersion;
use crate::warning::Warning;
use crate::yul::lexer::Lexer;
use crate::yul::parser::statement::object::Object;
use self::contract::Contract;
use self::error::Error as SolcStandardJsonOutputError;
use self::source::Source;
/// The `solc --standard-json` output.
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct Output {
/// The file-contract hashmap.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub contracts: Option<BTreeMap<String, BTreeMap<String, Contract>>>,
/// The source code mapping data.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sources: Option<BTreeMap<String, Source>>,
/// The compilation errors and warnings.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub errors: Option<Vec<SolcStandardJsonOutputError>>,
/// The `solc` compiler version.
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
/// The `solc` compiler long version.
#[serde(skip_serializing_if = "Option::is_none")]
pub long_version: Option<String>,
/// The `resolc` compiler version.
#[serde(skip_serializing_if = "Option::is_none")]
pub revive_version: Option<String>,
}
impl Output {
/// Converts the `solc` JSON output into a convenient project.
pub fn try_to_project(
&mut self,
source_code_files: BTreeMap<String, String>,
libraries: BTreeMap<String, BTreeMap<String, String>>,
solc_version: &SolcVersion,
debug_config: &revive_llvm_context::DebugConfig,
) -> anyhow::Result<Project> {
let files = match self.contracts.as_ref() {
Some(files) => files,
None => match &self.errors {
Some(errors) if errors.iter().any(|e| e.severity == "error") => {
anyhow::bail!(serde_json::to_string_pretty(errors).expect("Always valid"));
}
_ => &BTreeMap::new(),
},
};
let mut project_contracts = BTreeMap::new();
for (path, contracts) in files.iter() {
for (name, contract) in contracts.iter() {
let full_path = format!("{path}:{name}");
let ir_optimized = match contract.ir_optimized.to_owned() {
Some(ir_optimized) => ir_optimized,
None => continue,
};
if ir_optimized.is_empty() {
continue;
}
debug_config.dump_yul(full_path.as_str(), ir_optimized.as_str())?;
let mut lexer = Lexer::new(ir_optimized.to_owned());
let object = Object::parse(&mut lexer, None).map_err(|error| {
anyhow::anyhow!("Contract `{}` parsing error: {:?}", full_path, error)
})?;
let source = ProjectContractIR::new_yul(ir_optimized.to_owned(), object);
let source_code = source_code_files
.get(path.as_str())
.ok_or_else(|| anyhow::anyhow!("Source code for path `{}` not found", path))?;
let source_hash = sha3::Keccak256::digest(source_code.as_bytes()).into();
let project_contract = ProjectContract::new(
full_path.clone(),
source_hash,
solc_version.to_owned(),
source,
contract.metadata.to_owned(),
);
project_contracts.insert(full_path, project_contract);
}
}
Ok(Project::new(
solc_version.to_owned(),
project_contracts,
libraries,
))
}
/// Traverses the AST and returns the list of additional errors and warnings.
pub fn preprocess_ast(&mut self, suppressed_warnings: &[Warning]) -> anyhow::Result<()> {
let sources = match self.sources.as_ref() {
Some(sources) => sources,
None => return Ok(()),
};
let mut messages = Vec::new();
for (path, source) in sources.iter() {
if let Some(ast) = source.ast.as_ref() {
let mut polkavm_messages = Source::get_messages(ast, suppressed_warnings);
for message in polkavm_messages.iter_mut() {
message.push_contract_path(path.as_str());
}
messages.extend(polkavm_messages);
}
}
self.errors = match self.errors.take() {
Some(mut errors) => {
errors.extend(messages);
Some(errors)
}
None => Some(messages),
};
Ok(())
}
}
@@ -1,200 +0,0 @@
//! The `solc --standard-json` output source.
use serde::Deserialize;
use serde::Serialize;
use crate::solc::standard_json::output::error::Error as SolcStandardJsonOutputError;
use crate::warning::Warning;
/// The `solc --standard-json` output source.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Source {
/// The source code ID.
pub id: usize,
/// The source code AST.
pub ast: Option<serde_json::Value>,
}
impl Source {
/// Checks the AST node for the `ecrecover` function usage.
pub fn check_ecrecover(ast: &serde_json::Value) -> Option<SolcStandardJsonOutputError> {
let ast = ast.as_object()?;
if ast.get("nodeType")?.as_str()? != "FunctionCall" {
return None;
}
let expression = ast.get("expression")?.as_object()?;
if expression.get("nodeType")?.as_str()? != "Identifier" {
return None;
}
if expression.get("name")?.as_str()? != "ecrecover" {
return None;
}
Some(SolcStandardJsonOutputError::message_ecrecover(
ast.get("src")?.as_str(),
))
}
/// Checks the AST node for the `<address payable>`'s `send` and `transfer` methods usage.
pub fn check_send_and_transfer(ast: &serde_json::Value) -> Option<SolcStandardJsonOutputError> {
let ast = ast.as_object()?;
if ast.get("nodeType")?.as_str()? != "FunctionCall" {
return None;
}
let expression = ast.get("expression")?.as_object()?;
if expression.get("nodeType")?.as_str()? != "MemberAccess" {
return None;
}
let member_name = expression.get("memberName")?.as_str()?;
if member_name != "send" && member_name != "transfer" {
return None;
}
Some(SolcStandardJsonOutputError::message_send_and_transfer(
ast.get("src")?.as_str(),
))
}
/// Checks the AST node for the `extcodesize` assembly instruction usage.
pub fn check_assembly_extcodesize(
ast: &serde_json::Value,
) -> Option<SolcStandardJsonOutputError> {
let ast = ast.as_object()?;
if ast.get("nodeType")?.as_str()? != "YulFunctionCall" {
return None;
}
if ast
.get("functionName")?
.as_object()?
.get("name")?
.as_str()?
!= "extcodesize"
{
return None;
}
Some(SolcStandardJsonOutputError::message_extcodesize(
ast.get("src")?.as_str(),
))
}
/// Checks the AST node for the `origin` assembly instruction usage.
pub fn check_assembly_origin(ast: &serde_json::Value) -> Option<SolcStandardJsonOutputError> {
let ast = ast.as_object()?;
if ast.get("nodeType")?.as_str()? != "YulFunctionCall" {
return None;
}
if ast
.get("functionName")?
.as_object()?
.get("name")?
.as_str()?
!= "origin"
{
return None;
}
Some(SolcStandardJsonOutputError::message_tx_origin(
ast.get("src")?.as_str(),
))
}
/// Checks the AST node for the `tx.origin` value usage.
pub fn check_tx_origin(ast: &serde_json::Value) -> Option<SolcStandardJsonOutputError> {
let ast = ast.as_object()?;
if ast.get("nodeType")?.as_str()? != "MemberAccess" {
return None;
}
if ast.get("memberName")?.as_str()? != "origin" {
return None;
}
let expression = ast.get("expression")?.as_object()?;
if expression.get("nodeType")?.as_str()? != "Identifier" {
return None;
}
if expression.get("name")?.as_str()? != "tx" {
return None;
}
Some(SolcStandardJsonOutputError::message_tx_origin(
ast.get("src")?.as_str(),
))
}
/// Returns the list of messages for some specific parts of the AST.
pub fn get_messages(
ast: &serde_json::Value,
suppressed_warnings: &[Warning],
) -> Vec<SolcStandardJsonOutputError> {
let mut messages = Vec::new();
if !suppressed_warnings.contains(&Warning::EcRecover) {
if let Some(message) = Self::check_ecrecover(ast) {
messages.push(message);
}
}
if !suppressed_warnings.contains(&Warning::SendTransfer) {
if let Some(message) = Self::check_send_and_transfer(ast) {
messages.push(message);
}
}
if !suppressed_warnings.contains(&Warning::ExtCodeSize) {
if let Some(message) = Self::check_assembly_extcodesize(ast) {
messages.push(message);
}
}
if !suppressed_warnings.contains(&Warning::TxOrigin) {
if let Some(message) = Self::check_assembly_origin(ast) {
messages.push(message);
}
if let Some(message) = Self::check_tx_origin(ast) {
messages.push(message);
}
}
match ast {
serde_json::Value::Array(array) => {
for element in array.iter() {
messages.extend(Self::get_messages(element, suppressed_warnings));
}
}
serde_json::Value::Object(object) => {
for (_key, value) in object.iter() {
messages.extend(Self::get_messages(value, suppressed_warnings));
}
}
_ => {}
}
messages
}
/// Returns the name of the last contract.
pub fn last_contract_name(&self) -> anyhow::Result<String> {
self.ast
.as_ref()
.ok_or_else(|| anyhow::anyhow!("The AST is empty"))?
.get("nodes")
.and_then(|value| value.as_array())
.ok_or_else(|| {
anyhow::anyhow!("The last contract cannot be found in an empty list of nodes")
})?
.iter()
.filter_map(
|node| match node.get("nodeType").and_then(|node| node.as_str()) {
Some("ContractDefinition") => Some(node.get("name")?.as_str()?.to_owned()),
_ => None,
},
)
.last()
.ok_or_else(|| anyhow::anyhow!("The last contract not found in the AST"))
}
}
+21 -9
View File
@@ -7,17 +7,17 @@ use std::sync::Mutex;
use once_cell::sync::Lazy;
use revive_llvm_context::OptimizerSettings;
use revive_solc_json_interface::standard_json::output::contract::evm::bytecode::Bytecode;
use revive_solc_json_interface::standard_json::output::contract::evm::bytecode::DeployedBytecode;
use revive_solc_json_interface::warning::Warning;
use revive_solc_json_interface::SolcStandardJsonInput;
use revive_solc_json_interface::SolcStandardJsonInputSettingsOptimizer;
use revive_solc_json_interface::SolcStandardJsonInputSettingsSelection;
use revive_solc_json_interface::SolcStandardJsonOutput;
use crate::project::Project;
use crate::solc::solc_compiler::SolcCompiler;
use crate::solc::standard_json::input::settings::optimizer::Optimizer as SolcStandardJsonInputSettingsOptimizer;
use crate::solc::standard_json::input::settings::selection::Selection as SolcStandardJsonInputSettingsSelection;
use crate::solc::standard_json::input::Input as SolcStandardJsonInput;
use crate::solc::standard_json::output::contract::evm::bytecode::Bytecode;
use crate::solc::standard_json::output::contract::evm::bytecode::DeployedBytecode;
use crate::solc::standard_json::output::Output as SolcStandardJsonOutput;
use crate::solc::Compiler;
use crate::warning::Warning;
static PVM_BLOB_CACHE: Lazy<Mutex<HashMap<CachedBlob, Vec<u8>>>> = Lazy::new(Default::default);
static EVM_BLOB_CACHE: Lazy<Mutex<HashMap<CachedBlob, Vec<u8>>>> = Lazy::new(Default::default);
@@ -98,7 +98,13 @@ pub fn build_solidity_with_options(
let mut output = solc.standard_json(input, None, vec![], None)?;
let project = output.try_to_project(sources, libraries, &solc_version, &DEBUG_CONFIG)?;
let project = Project::try_from_standard_json_output(
&output,
sources,
libraries,
&solc_version,
&DEBUG_CONFIG,
)?;
let build: crate::Build = project.compile(optimizer_settings, false, DEBUG_CONFIG)?;
build.write_to_standard_json(&mut output, &solc_version)?;
@@ -188,7 +194,13 @@ pub fn build_solidity_and_detect_missing_libraries(
let mut output = solc.standard_json(input, None, vec![], None)?;
let project = output.try_to_project(sources, libraries, &solc_version, &DEBUG_CONFIG)?;
let project = Project::try_from_standard_json_output(
&output,
sources,
libraries,
&solc_version,
&DEBUG_CONFIG,
)?;
let missing_libraries = project.get_missing_libraries();
missing_libraries.write_to_standard_json(&mut output, &solc.version()?)?;
+1 -1
View File
@@ -4,7 +4,7 @@
use std::collections::BTreeMap;
use crate::warning::Warning;
use revive_solc_json_interface::warning::Warning;
pub const ECRECOVER_TEST_SOURCE: &str = r#"
// SPDX-License-Identifier: MIT
-52
View File
@@ -1,52 +0,0 @@
//! The compiler warning.
use std::str::FromStr;
use serde::Deserialize;
use serde::Serialize;
/// The compiler warning.
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Warning {
/// The warning for eponymous feature.
EcRecover,
/// The warning for eponymous feature.
SendTransfer,
/// The warning for eponymous feature.
ExtCodeSize,
/// The warning for eponymous feature.
TxOrigin,
/// The warning for eponymous feature.
BlockTimestamp,
/// The warning for eponymous feature.
BlockNumber,
/// The warning for eponymous feature.
BlockHash,
}
impl Warning {
/// Converts string arguments into an array of warnings.
pub fn try_from_strings(strings: &[String]) -> Result<Vec<Self>, anyhow::Error> {
strings
.iter()
.map(|string| Self::from_str(string))
.collect()
}
}
impl FromStr for Warning {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
match string {
"ecrecover" => Ok(Self::EcRecover),
"sendtransfer" => Ok(Self::SendTransfer),
"extcodesize" => Ok(Self::ExtCodeSize),
"txorigin" => Ok(Self::TxOrigin),
"blocktimestamp" => Ok(Self::BlockTimestamp),
"blocknumber" => Ok(Self::BlockNumber),
"blockhash" => Ok(Self::BlockHash),
_ => Err(anyhow::anyhow!("Invalid warning: {}", string)),
}
}
}