Separate compilation and linker phases (#376)

Separate between compilation and linker phases to allow deploy time
linking and back-porting era compiler changes to fix #91. Unlinked
contract binaries (caused by missing libraries or missing factory
dependencies in turn) are emitted as raw ELF object.

Few drive by fixes:
- #98
- A compiler panic on missing libraries definitions.
- Fixes some incosistent type forwarding in JSON output (empty string
vs. null object).
- Remove the unused fallback for size optimization setting.
- Remove the broken `--lvm-ir`  mode.
- CI workflow fixes.

---------

Signed-off-by: Cyrill Leutwiler <bigcyrill@hotmail.com>
Signed-off-by: xermicus <bigcyrill@hotmail.com>
Signed-off-by: xermicus <cyrill@parity.io>
This commit is contained in:
xermicus
2025-09-27 20:52:22 +02:00
committed by GitHub
parent 13faedf08a
commit 94ec34c4d5
169 changed files with 6288 additions and 5206 deletions
@@ -1,72 +1,64 @@
//! 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)]
#[derive(Debug, Default, 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>>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub hashes: BTreeMap<String, String>,
/// The `solc` ABI output.
#[serde(skip_serializing_if = "Option::is_none")]
pub abi: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub abi: 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>,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub devdoc: serde_json::Value,
/// The `solc` user documentation output.
#[serde(skip_serializing_if = "Option::is_none")]
pub userdoc: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub userdoc: serde_json::Value,
/// The `solc` storage layout output.
#[serde(skip_serializing_if = "Option::is_none")]
pub storage_layout: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub storage_layout: serde_json::Value,
/// The `solc` AST output.
#[serde(skip_serializing_if = "Option::is_none")]
pub ast: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub ast: serde_json::Value,
/// The `solc` assembly output.
#[serde(skip_serializing_if = "Option::is_none")]
pub asm: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub asm: serde_json::Value,
/// LLVM-generated assembly.
#[cfg(feature = "resolc")]
#[serde(default, skip_serializing_if = "Option::is_none", skip_deserializing)]
pub assembly: Option<String>,
/// The `solc` hexadecimal binary output.
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default, skip_serializing_if = "Option::is_none", skip_deserializing)]
pub bin: Option<String>,
/// The `solc` hexadecimal binary runtime part output.
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default, skip_serializing_if = "Option::is_none", skip_deserializing)]
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"))
}
/// The unlinked factory dependencies.
#[cfg(feature = "resolc")]
#[serde(default, skip_deserializing)]
pub factory_deps_unlinked: std::collections::BTreeSet<String>,
/// The factory dependencies.
#[cfg(feature = "resolc")]
#[serde(default, skip_deserializing)]
pub factory_deps: BTreeMap<String, String>,
/// The missing libraries.
#[cfg(feature = "resolc")]
#[serde(default, skip_deserializing)]
pub missing_libraries: std::collections::BTreeSet<String>,
/// The binary object format.
#[cfg(feature = "resolc")]
#[serde(default, skip_deserializing)]
pub object_format: Option<revive_common::ObjectFormat>,
}
@@ -1,79 +1,51 @@
//! 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;
pub mod contract;
pub mod selector;
/// 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>>,
#[serde(default, rename = "sourceList", skip_serializing_if = "Vec::is_empty")]
pub source_list: Vec<String>,
/// The source code extra data, including the AST.
#[serde(skip_serializing_if = "Option::is_none")]
pub sources: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub sources: 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>,
#[cfg(feature = "resolc")]
pub resolc_version: Option<String>,
}
#[cfg(feature = "resolc")]
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;
/// A shortcut constructor.
pub fn new(solc_version: semver::Version, resolc_version: Option<String>) -> Self {
Self {
contracts: BTreeMap::new(),
source_list: Vec::new(),
sources: serde_json::Value::Null,
version: solc_version.to_string(),
resolc_version,
}
}
/// Writes the JSON to the specified directory.
pub fn write_to_directory(
self,
output_directory: &Path,
output_directory: &std::path::Path,
overwrite: bool,
) -> anyhow::Result<()> {
let mut file_path = output_directory.to_owned();
@@ -85,10 +57,11 @@ impl CombinedJson {
);
}
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))?;
std::fs::write(
file_path.as_path(),
serde_json::to_vec(&self).expect("Always valid").as_slice(),
)
.map_err(|error| anyhow::anyhow!("File {file_path:?} writing: {error}"))?;
Ok(())
}
@@ -0,0 +1,115 @@
//! The `solc --combined-json` expected output selection flag.
use std::str::FromStr;
use serde::{Deserialize, Serialize};
/// The solc `--combind-json` invalid selector message.
pub const MESSAGE_SELECTOR_INVALID: &str = "Invalid option to --combined-json";
/// The `solc --combined-json` expected output selection flag.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Selector {
/// The ABI JSON.
#[serde(rename = "abi")]
ABI,
/// The function signature hashes JSON.
#[serde(rename = "hashes")]
Hashes,
/// The metadata.
#[serde(rename = "metadata")]
Metadata,
/// The developer documentation.
#[serde(rename = "devdoc")]
Devdoc,
/// The user documentation.
#[serde(rename = "userdoc")]
Userdoc,
/// The storage layout.
#[serde(rename = "storage-layout")]
StorageLayout,
/// The transient storage layout.
#[serde(rename = "transient-storage-layout")]
TransientStorageLayout,
/// The AST JSON.
#[serde(rename = "ast")]
AST,
/// The EVM assembly.
#[serde(rename = "asm")]
ASM,
/// The assembly.
#[serde(rename = "assembly", skip_serializing)]
Assembly,
/// The deploy bytecode.
#[serde(rename = "bin", skip_serializing)]
Bytecode,
/// The runtime bytecode.
#[serde(rename = "bin-runtime", skip_serializing)]
BytecodeRuntime,
}
impl Selector {
/// Converts the comma-separated CLI argument into an array of flags.
pub fn from_cli(format: &str) -> Vec<anyhow::Result<Self>> {
format
.split(',')
.map(|flag| Self::from_str(flag.trim()))
.collect()
}
/// Whether the selector is available in `solc`.
pub fn is_source_solc(&self) -> bool {
!matches!(
self,
Self::Assembly | Self::Bytecode | Self::BytecodeRuntime
)
}
}
impl FromStr for Selector {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
match string {
"abi" => Ok(Self::ABI),
"hashes" => Ok(Self::Hashes),
"metadata" => Ok(Self::Metadata),
"devdoc" => Ok(Self::Devdoc),
"userdoc" => Ok(Self::Userdoc),
"storage-layout" => Ok(Self::StorageLayout),
"transient-storage-layout" => Ok(Self::TransientStorageLayout),
"ast" => Ok(Self::AST),
"asm" => Ok(Self::ASM),
"bin" => Ok(Self::Bytecode),
"bin-runtime" => Ok(Self::BytecodeRuntime),
"assembly" => Ok(Self::Assembly),
selector => anyhow::bail!("{MESSAGE_SELECTOR_INVALID}: {selector}"),
}
}
}
impl std::fmt::Display for Selector {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::ABI => write!(f, "abi"),
Self::Hashes => write!(f, "hashes"),
Self::Metadata => write!(f, "metadata"),
Self::Devdoc => write!(f, "devdoc"),
Self::Userdoc => write!(f, "userdoc"),
Self::StorageLayout => write!(f, "storage-layout"),
Self::TransientStorageLayout => write!(f, "transient-storage-layout"),
Self::AST => write!(f, "ast"),
Self::ASM => write!(f, "asm"),
Self::Bytecode => write!(f, "bin"),
Self::BytecodeRuntime => write!(f, "bin-runtime"),
Self::Assembly => write!(f, "assembly"),
}
}
}
+11 -4
View File
@@ -3,8 +3,13 @@
//!
//! [0]: https://docs.soliditylang.org/en/latest/using-the-compiler.html#compiler-input-and-output-json-description
#![allow(clippy::too_many_arguments)]
pub use self::combined_json::contract::Contract as CombinedJsonContract;
pub use self::combined_json::selector::Selector as CombinedJsonSelector;
pub use self::combined_json::selector::MESSAGE_SELECTOR_INVALID as CombinedJsonInvalidSelectorMessage;
pub use self::standard_json::input::language::Language as SolcStandardJsonInputLanguage;
pub use self::standard_json::input::settings::libraries::Libraries as SolcStandardJsonInputSettingsLibraries;
pub use self::standard_json::input::settings::metadata::Metadata as SolcStandardJsonInputSettingsMetadata;
pub use self::standard_json::input::settings::metadata_hash::MetadataHash as SolcStandardJsonInputSettingsMetadataHash;
pub use self::standard_json::input::settings::optimizer::yul_details::YulDetails as SolcStandardJsonInputSettingsYulOptimizerDetails;
@@ -16,17 +21,19 @@ pub use self::standard_json::input::settings::polkavm::PolkaVM as SolcStandardJs
pub use self::standard_json::input::settings::selection::file::flag::Flag as SolcStandardJsonInputSettingsSelectionFileFlag;
pub use self::standard_json::input::settings::selection::file::File as SolcStandardJsonInputSettingsSelectionFile;
pub use self::standard_json::input::settings::selection::Selection as SolcStandardJsonInputSettingsSelection;
#[cfg(feature = "resolc")]
pub use self::standard_json::input::settings::warning::Warning as ResolcWarning;
pub use self::standard_json::input::settings::Settings as SolcStandardJsonInputSettings;
pub use self::standard_json::input::source::Source as SolcStandardJsonInputSource;
pub use self::standard_json::input::Input as SolcStandardJsonInput;
pub use self::standard_json::output::contract::evm::bytecode::Bytecode as SolcStandardJsonOutputContractEVMBytecode;
pub use self::standard_json::output::contract::evm::EVM as SolcStandardJsonOutputContractEVM;
pub use self::standard_json::output::contract::Contract as SolcStandardJsonOutputContract;
pub use self::standard_json::output::Output as SolcStandardJsonOutput;
#[cfg(feature = "resolc")]
pub use self::warning::Warning as ResolcWarning;
pub use self::standard_json::output::error::error_handler::ErrorHandler as SolcStandardJsonOutputErrorHandler;
pub use self::standard_json::output::error::mapped_location::MappedLocation as SolcStandardJsonOutputErrorMappedLocation;
pub use self::standard_json::output::error::Error as SolcStandardJsonOutputError;
pub use self::standard_json::output::Output as SolcStandardJsonOutput;
pub mod combined_json;
pub mod standard_json;
#[cfg(feature = "resolc")]
pub mod warning;
@@ -1,29 +1,39 @@
//! The `solc --standard-json` input.
use std::collections::BTreeMap;
#[cfg(feature = "resolc")]
use std::collections::BTreeSet;
#[cfg(feature = "resolc")]
use std::path::Path;
#[cfg(feature = "resolc")]
use std::path::PathBuf;
#[cfg(all(feature = "parallel", feature = "resolc"))]
use rayon::iter::{IntoParallelIterator, IntoParallelRefMutIterator, ParallelIterator};
use serde::Deserialize;
use serde::Serialize;
#[cfg(feature = "resolc")]
use crate::standard_json::input::settings::metadata::Metadata as SolcStandardJsonInputSettingsMetadata;
#[cfg(feature = "resolc")]
use crate::standard_json::input::settings::optimizer::Optimizer as SolcStandardJsonInputSettingsOptimizer;
#[cfg(feature = "resolc")]
use crate::standard_json::input::settings::selection::Selection as SolcStandardJsonInputSettingsSelection;
#[cfg(feature = "resolc")]
use crate::SolcStandardJsonInputSettingsLibraries;
#[cfg(feature = "resolc")]
use crate::SolcStandardJsonInputSettingsPolkaVM;
use self::language::Language;
#[cfg(feature = "resolc")]
use self::settings::warning::Warning;
use self::settings::Settings;
use self::source::Source;
pub mod language;
pub mod settings;
pub mod source;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::path::PathBuf;
#[cfg(all(feature = "parallel", feature = "resolc"))]
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use serde::Deserialize;
use serde::Serialize;
use crate::standard_json::input::settings::metadata::Metadata as SolcStandardJsonInputSettingsMetadata;
use crate::standard_json::input::settings::optimizer::Optimizer as SolcStandardJsonInputSettingsOptimizer;
use crate::standard_json::input::settings::selection::Selection as SolcStandardJsonInputSettingsSelection;
#[cfg(feature = "resolc")]
use crate::warning::Warning;
use crate::SolcStandardJsonInputSettingsPolkaVM;
use self::language::Language;
use self::settings::Settings;
use self::source::Source;
/// The `solc --standard-json` input.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -36,93 +46,88 @@ pub struct Input {
pub settings: Settings,
/// The suppressed warnings.
#[cfg(feature = "resolc")]
#[serde(skip_serializing)]
pub suppressed_warnings: Option<Vec<Warning>>,
#[serde(default, skip_serializing)]
pub suppressed_warnings: Vec<Warning>,
}
#[cfg(feature = "resolc")]
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.
///
/// If the `path` is `None`, the input is read from the stdin.
pub fn try_from(path: Option<&Path>) -> anyhow::Result<Self> {
let input_json = match path {
Some(path) => std::fs::read_to_string(path)
.map_err(|error| anyhow::anyhow!("Standard JSON file {path:?} reading: {error}")),
None => std::io::read_to_string(std::io::stdin())
.map_err(|error| anyhow::anyhow!("Standard JSON reading from stdin: {error}")),
}?;
revive_common::deserialize_from_str::<Self>(input_json.as_str())
.map_err(|error| anyhow::anyhow!("Standard JSON parsing: {error}"))
}
/// A shortcut constructor from paths.
#[allow(clippy::too_many_arguments)]
pub fn try_from_paths(
language: Language,
pub fn try_from_solidity_paths(
evm_version: Option<revive_common::EVMVersion>,
paths: &[PathBuf],
library_map: Vec<String>,
remappings: Option<BTreeSet<String>>,
libraries: &[String],
remappings: BTreeSet<String>,
output_selection: SolcStandardJsonInputSettingsSelection,
optimizer: SolcStandardJsonInputSettingsOptimizer,
metadata: Option<SolcStandardJsonInputSettingsMetadata>,
#[cfg(feature = "resolc")] suppressed_warnings: Option<Vec<Warning>>,
polkavm: Option<SolcStandardJsonInputSettingsPolkaVM>,
metadata: SolcStandardJsonInputSettingsMetadata,
suppressed_warnings: Vec<Warning>,
polkavm: SolcStandardJsonInputSettingsPolkaVM,
llvm_arguments: Vec<String>,
detect_missing_libraries: bool,
) -> 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() {
let libraries = SolcStandardJsonInputSettingsLibraries::try_from(libraries)?;
for library_file in libraries.as_inner().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();
#[cfg(feature = "parallel")]
let iter = paths.into_par_iter(); // Parallel iterator
#[cfg(not(feature = "parallel"))]
let iter = paths.into_iter(); // Sequential iterator
Ok(Self {
language,
let sources = iter
.map(|path| {
let source = Source::try_read(path.as_path())?;
Ok((path.to_string_lossy().to_string(), source))
})
.collect::<anyhow::Result<BTreeMap<String, Source>>>()?;
Self::try_from_solidity_sources(
evm_version,
sources,
settings: Settings::new(
evm_version,
libraries,
remappings,
output_selection,
optimizer,
metadata,
polkavm,
),
#[cfg(feature = "resolc")]
libraries,
remappings,
output_selection,
optimizer,
metadata,
suppressed_warnings,
})
polkavm,
llvm_arguments,
detect_missing_libraries,
)
}
/// A shortcut constructor from source code.
/// Only for the integration test purposes.
#[cfg(feature = "resolc")]
#[allow(clippy::too_many_arguments)]
pub fn try_from_sources(
pub fn try_from_solidity_sources(
evm_version: Option<revive_common::EVMVersion>,
sources: BTreeMap<String, String>,
libraries: BTreeMap<String, BTreeMap<String, String>>,
remappings: Option<BTreeSet<String>>,
sources: BTreeMap<String, Source>,
libraries: SolcStandardJsonInputSettingsLibraries,
remappings: BTreeSet<String>,
output_selection: SolcStandardJsonInputSettingsSelection,
optimizer: SolcStandardJsonInputSettingsOptimizer,
metadata: Option<SolcStandardJsonInputSettingsMetadata>,
suppressed_warnings: Option<Vec<Warning>>,
polkavm: Option<SolcStandardJsonInputSettingsPolkaVM>,
metadata: SolcStandardJsonInputSettingsMetadata,
suppressed_warnings: Vec<Warning>,
polkavm: SolcStandardJsonInputSettingsPolkaVM,
llvm_arguments: Vec<String>,
detect_missing_libraries: bool,
) -> 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,
@@ -134,13 +139,75 @@ impl Input {
optimizer,
metadata,
polkavm,
suppressed_warnings.clone(),
llvm_arguments,
detect_missing_libraries,
),
suppressed_warnings,
})
}
/// Sets the necessary defaults.
pub fn normalize(&mut self) {
self.settings.normalize();
/// A shortcut constructor from paths to Yul source files.
pub fn from_yul_paths(
paths: &[PathBuf],
libraries: SolcStandardJsonInputSettingsLibraries,
optimizer: SolcStandardJsonInputSettingsOptimizer,
llvm_options: Vec<String>,
) -> Self {
let sources = paths
.iter()
.map(|path| {
(
path.to_string_lossy().to_string(),
Source::from(path.as_path()),
)
})
.collect();
Self::from_yul_sources(sources, libraries, optimizer, llvm_options)
}
/// A shortcut constructor from Yul source code.
pub fn from_yul_sources(
sources: BTreeMap<String, Source>,
libraries: SolcStandardJsonInputSettingsLibraries,
optimizer: SolcStandardJsonInputSettingsOptimizer,
llvm_arguments: Vec<String>,
) -> Self {
let output_selection = SolcStandardJsonInputSettingsSelection::new_yul_validation();
Self {
language: Language::Yul,
sources,
settings: Settings::new(
None,
libraries,
Default::default(),
output_selection,
optimizer,
Default::default(),
Default::default(),
vec![],
llvm_arguments,
false,
),
suppressed_warnings: vec![],
}
}
/// Extends the output selection with another one.
pub fn extend_selection(&mut self, selection: SolcStandardJsonInputSettingsSelection) {
self.settings.extend_selection(selection);
}
/// Tries to resolve all sources.
pub fn resolve_sources(&mut self) {
#[cfg(feature = "parallel")]
let iter = self.sources.par_iter_mut();
#[cfg(not(feature = "parallel"))]
let iter = self.sources.iter_mut();
iter.for_each(|(_path, source)| {
let _ = source.try_resolve();
});
}
}
@@ -0,0 +1,107 @@
//! The Solidity libraries.
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use serde::Deserialize;
use serde::Serialize;
/// The Solidity libraries.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Libraries {
/// The unified representation of libraries.
#[serde(flatten)]
pub inner: BTreeMap<String, BTreeMap<String, String>>,
}
impl Libraries {
/// Returns a representation of libraries suitable for the LLD linker.
pub fn as_linker_symbols(
&self,
) -> anyhow::Result<BTreeMap<String, [u8; revive_common::BYTE_LENGTH_ETH_ADDRESS]>> {
let mut linker_symbols = BTreeMap::new();
for (file, contracts) in self.inner.iter() {
for (name, address) in contracts.iter() {
let path = format!("{file}:{name}");
let address_stripped = address.strip_prefix("0x").unwrap_or(address.as_str());
let address_vec = hex::decode(address_stripped).map_err(|error| {
anyhow::anyhow!("Invalid address `{address}` of library `{path}`: {error}.")
})?;
let address_array: [u8; revive_common::BYTE_LENGTH_ETH_ADDRESS] = address_vec.try_into().map_err(|address_vec: Vec<u8>| {
anyhow::anyhow!(
"Incorrect size of address `{address}` of library `{path}`: expected {}, found {}.",
revive_common::BYTE_LENGTH_ETH_ADDRESS,
address_vec.len(),
)
})?;
linker_symbols.insert(path, address_array);
}
}
Ok(linker_symbols)
}
/// Returns a representation of libraries suitable for filtering.
pub fn as_paths(&self) -> BTreeSet<String> {
self.inner
.iter()
.flat_map(|(file, names)| {
names
.keys()
.map(|name| format!("{file}:{name}"))
.collect::<BTreeSet<String>>()
})
.collect::<BTreeSet<String>>()
}
/// Checks whether the libraries are empty.
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
/// Returns a reference to the inner value.
pub fn as_inner(&self) -> &BTreeMap<String, BTreeMap<String, String>> {
&self.inner
}
/// Returns a mutable reference to the inner value.
pub fn as_inner_mut(&mut self) -> &mut BTreeMap<String, BTreeMap<String, String>> {
&mut self.inner
}
}
impl From<BTreeMap<String, BTreeMap<String, String>>> for Libraries {
fn from(inner: BTreeMap<String, BTreeMap<String, String>>) -> Self {
Self { inner }
}
}
impl TryFrom<&[String]> for Libraries {
type Error = anyhow::Error;
fn try_from(arguments: &[String]) -> Result<Self, Self::Error> {
let mut libraries = BTreeMap::new();
for (index, library) in arguments.iter().enumerate() {
let mut path_and_address = library.split('=');
let path = path_and_address
.next()
.ok_or_else(|| anyhow::anyhow!("Library #{index} path is missing."))?;
let mut file_and_contract = path.split(':');
let file = file_and_contract
.next()
.ok_or_else(|| anyhow::anyhow!("Library `{path}` file name is missing."))?;
let contract = file_and_contract
.next()
.ok_or_else(|| anyhow::anyhow!("Library `{path}` contract name is missing."))?;
let address = path_and_address
.next()
.ok_or_else(|| anyhow::anyhow!("Library `{path}` address is missing."))?;
libraries
.entry(file.to_owned())
.or_insert_with(BTreeMap::new)
.insert(contract.to_owned(), address.to_owned());
}
Ok(Self { inner: libraries })
}
}
@@ -1,21 +1,26 @@
//! The `solc --standard-json` input settings.
pub mod libraries;
pub mod metadata;
pub mod metadata_hash;
pub mod optimizer;
pub mod polkavm;
pub mod selection;
#[cfg(feature = "resolc")]
pub mod warning;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use serde::Deserialize;
use serde::Serialize;
use self::libraries::Libraries;
use self::metadata::Metadata;
use self::optimizer::Optimizer;
use self::polkavm::PolkaVM;
use self::selection::Selection;
#[cfg(feature = "resolc")]
use self::warning::Warning;
/// The `solc --standard-json` input settings.
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -25,14 +30,14 @@ pub struct Settings {
#[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>>>,
#[serde(default, skip_serializing_if = "Libraries::is_empty")]
pub libraries: Libraries,
/// The sorted list of remappings.
#[serde(skip_serializing_if = "Option::is_none")]
pub remappings: Option<BTreeSet<String>>,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
pub remappings: BTreeSet<String>,
/// The output selection filters.
#[serde(skip_serializing_if = "Option::is_none")]
pub output_selection: Option<Selection>,
#[serde(default)]
pub output_selection: Selection,
/// Whether to compile via IR. Only for testing with solc >=0.8.13.
#[serde(
rename = "viaIR",
@@ -43,67 +48,68 @@ pub struct Settings {
/// The optimizer settings.
pub optimizer: Optimizer,
/// The metadata settings.
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<Metadata>,
#[serde(default)]
pub metadata: Metadata,
/// The resolc custom PolkaVM settings.
#[serde(skip_serializing_if = "Option::is_none")]
pub polkavm: Option<PolkaVM>,
#[serde(default, skip_serializing)]
pub polkavm: PolkaVM,
/// The suppressed warnings.
#[cfg(feature = "resolc")]
#[serde(default, skip_serializing)]
pub suppressed_warnings: Vec<self::warning::Warning>,
/// The extra LLVM arguments.
#[cfg(feature = "resolc")]
#[serde(default, alias = "LLVMOptions", skip_serializing)]
pub llvm_arguments: Vec<String>,
/// Whether to enable the missing libraries detection mode.
/// Deprecated in favor of post-compile-time linking.
#[serde(default, rename = "detectMissingLibraries", skip_serializing)]
pub detect_missing_libraries: bool,
}
#[cfg(feature = "resolc")]
impl Settings {
/// A shortcut constructor.
pub fn new(
evm_version: Option<revive_common::EVMVersion>,
libraries: BTreeMap<String, BTreeMap<String, String>>,
remappings: Option<BTreeSet<String>>,
libraries: Libraries,
remappings: BTreeSet<String>,
output_selection: Selection,
optimizer: Optimizer,
metadata: Option<Metadata>,
polkavm: Option<PolkaVM>,
metadata: Metadata,
polkavm: PolkaVM,
suppressed_warnings: Vec<Warning>,
llvm_arguments: Vec<String>,
detect_missing_libraries: bool,
) -> Self {
Self {
evm_version,
libraries: Some(libraries),
libraries,
remappings,
output_selection: Some(output_selection),
output_selection,
optimizer,
metadata,
via_ir: Some(true),
polkavm,
suppressed_warnings,
llvm_arguments,
detect_missing_libraries,
}
}
/// Sets the necessary defaults.
pub fn normalize(&mut self) {
self.polkavm = None;
self.optimizer.normalize();
/// Extends the output selection with another one.
pub fn extend_selection(&mut self, selection: Selection) {
self.output_selection.extend(selection);
}
/// 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)
/// Returns flags that are going to be automatically added by the compiler,
/// but were not explicitly requested by the user.
///
/// Afterwards, the flags are used to prune JSON output before returning it.
pub fn selection_to_prune(&self) -> Selection {
self.output_selection.selection_to_prune()
}
}
@@ -6,7 +6,7 @@ use serde::Serialize;
use crate::standard_json::input::settings::optimizer::yul_details::YulDetails;
/// The `solc --standard-json` input settings optimizer details.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Details {
/// Whether the pass is enabled.
@@ -40,7 +40,6 @@ pub struct Details {
impl Details {
/// A shortcut constructor.
#[allow(clippy::too_many_arguments)]
pub fn new(
peephole: Option<bool>,
inliner: Option<bool>,
@@ -15,35 +15,31 @@ pub struct Optimizer {
/// Whether the optimizer is enabled.
pub enabled: bool,
/// The optimization mode string.
#[serde(skip_serializing_if = "Option::is_none")]
pub mode: Option<char>,
#[serde(default = "Optimizer::default_mode", skip_serializing)]
pub mode: 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_if = "Option::is_none")]
pub fallback_to_optimizing_for_size: Option<bool>,
#[serde(default)]
pub details: Details,
}
impl Optimizer {
/// A shortcut constructor.
pub fn new(
enabled: bool,
mode: Option<char>,
version: &semver::Version,
fallback_to_optimizing_for_size: bool,
) -> Self {
pub fn new(enabled: bool, mode: char, details: Details) -> Self {
Self {
enabled,
mode,
details: Some(Details::disabled(version)),
fallback_to_optimizing_for_size: Some(fallback_to_optimizing_for_size),
details,
}
}
/// Sets the necessary defaults.
pub fn normalize(&mut self) {
self.mode = None;
self.fallback_to_optimizing_for_size = None;
/// The default optimization mode.
pub fn default_mode() -> char {
'z'
}
}
impl Default for Optimizer {
fn default() -> Self {
Self::new(true, Self::default_mode(), Details::default())
}
}
@@ -4,7 +4,7 @@ use serde::Deserialize;
use serde::Serialize;
/// The `solc --standard-json` input settings optimizer YUL details.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct YulDetails {
/// Whether the stack allocation pass is enabled.
@@ -30,6 +30,9 @@ pub enum Flag {
/// The Yul IR.
#[serde(rename = "irOptimized")]
Yul,
/// The EVM bytecode.
#[serde(rename = "evm")]
EVM,
/// The EVM legacy assembly JSON.
#[serde(rename = "evm.legacyAssembly")]
EVMLA,
@@ -45,22 +48,23 @@ pub enum Flag {
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"),
}
}
}
//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::EVM => write!(f, "evm"),
// 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"),
// }
// }
//}
@@ -13,37 +13,56 @@ use self::flag::Flag as SelectionFlag;
#[derive(Clone, 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>>,
#[serde(default, rename = "", skip_serializing_if = "HashSet::is_empty")]
pub per_file: HashSet<SelectionFlag>,
/// The per-contract output selections.
#[serde(rename = "*", skip_serializing_if = "Option::is_none")]
pub per_contract: Option<HashSet<SelectionFlag>>,
#[serde(default, rename = "*", skip_serializing_if = "HashSet::is_empty")]
pub per_contract: HashSet<SelectionFlag>,
}
impl File {
/// A shortcut constructor.
pub fn new(flags: Vec<SelectionFlag>) -> Self {
let mut per_file = HashSet::new();
let mut per_contract = HashSet::new();
for flag in flags.into_iter() {
match flag {
SelectionFlag::AST => {
per_file.insert(SelectionFlag::AST);
}
flag => {
per_contract.insert(flag);
}
}
}
Self {
per_file,
per_contract,
}
}
/// Creates the selection required for production compilation (excludes EVM bytecode).
pub fn new_required() -> Self {
Self {
per_file: Some(HashSet::from_iter([SelectionFlag::AST])),
per_contract: Some(HashSet::from_iter([
per_file: HashSet::from_iter([SelectionFlag::AST]),
per_contract: HashSet::from_iter([
SelectionFlag::MethodIdentifiers,
SelectionFlag::Metadata,
SelectionFlag::Yul,
])),
]),
}
}
/// Creates the selection required for test compilation (includes EVM bytecode).
pub fn new_required_for_tests() -> Self {
Self {
per_file: Some(HashSet::from_iter([SelectionFlag::AST])),
per_contract: Some(HashSet::from_iter([
per_file: HashSet::from_iter([SelectionFlag::AST]),
per_contract: HashSet::from_iter([
SelectionFlag::EVMBC,
SelectionFlag::EVMDBC,
SelectionFlag::MethodIdentifiers,
SelectionFlag::Metadata,
SelectionFlag::Yul,
])),
]),
}
}
@@ -51,48 +70,59 @@ impl File {
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.per_file.extend(required.per_file);
self.per_contract.extend(required.per_contract);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn production_excludes_evm_bytecode() {
let selection = File::new_required();
let per_contract = selection.per_contract.unwrap();
// Production should NOT include EVM bytecode flags
assert!(!per_contract.contains(&SelectionFlag::EVMBC));
assert!(!per_contract.contains(&SelectionFlag::EVMDBC));
// But should include other required flags
assert!(per_contract.contains(&SelectionFlag::MethodIdentifiers));
assert!(per_contract.contains(&SelectionFlag::Metadata));
assert!(per_contract.contains(&SelectionFlag::Yul));
/// Extends the output selection with another one.
pub fn extend(&mut self, other: Self) -> &mut Self {
self.per_file.extend(other.per_file);
self.per_contract.extend(other.per_contract);
self
}
#[test]
fn tests_include_evm_bytecode() {
let selection = File::new_required_for_tests();
let per_contract = selection.per_contract.unwrap();
/// Returns flags that are going to be automatically added by the compiler,
/// but were not explicitly requested by the user.
///
/// Afterwards, the flags are used to prune JSON output before returning it.
pub fn selection_to_prune(&self) -> Self {
let required_per_file = vec![SelectionFlag::AST];
let required_per_contract = vec![
SelectionFlag::MethodIdentifiers,
SelectionFlag::Metadata,
SelectionFlag::Yul,
];
// Tests should include EVM bytecode flags
assert!(per_contract.contains(&SelectionFlag::EVMBC));
assert!(per_contract.contains(&SelectionFlag::EVMDBC));
let mut unset_per_file = HashSet::with_capacity(required_per_file.len());
let mut unset_per_contract = HashSet::with_capacity(required_per_contract.len());
// And should also include other required flags
assert!(per_contract.contains(&SelectionFlag::MethodIdentifiers));
assert!(per_contract.contains(&SelectionFlag::Metadata));
assert!(per_contract.contains(&SelectionFlag::Yul));
for flag in required_per_file {
if !self.per_file.contains(&flag) {
unset_per_file.insert(flag);
}
}
for flag in required_per_contract {
if !self.per_contract.contains(&flag) {
unset_per_contract.insert(flag);
}
}
Self {
per_file: unset_per_file,
per_contract: unset_per_contract,
}
}
/// Whether the flag is requested.
pub fn contains(&self, flag: &SelectionFlag) -> bool {
match flag {
flag @ SelectionFlag::AST => self.per_file.contains(flag),
flag => self.per_contract.contains(flag),
}
}
/// Checks whether the selection is empty.
pub fn is_empty(&self) -> bool {
self.per_file.is_empty() && self.per_contract.is_empty()
}
}
@@ -7,24 +7,33 @@ use std::collections::BTreeMap;
use serde::Deserialize;
use serde::Serialize;
use self::file::flag::Flag;
use self::file::File as FileSelection;
/// The `solc --standard-json` output selection.
#[derive(Clone, 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(default, rename = "*", skip_serializing_if = "FileSelection::is_empty")]
all: FileSelection,
#[serde(skip_serializing_if = "BTreeMap::is_empty", flatten)]
pub files: BTreeMap<String, FileSelection>,
}
impl Selection {
/// Creates the selection with arbitrary flags.
pub fn new(flags: Vec<Flag>) -> Self {
Self {
all: FileSelection::new(flags),
files: Default::default(),
}
}
/// Creates the selection required by our compilation process.
pub fn new_required() -> Self {
Self {
all: Some(FileSelection::new_required()),
all: FileSelection::new_required(),
files: BTreeMap::new(),
}
}
@@ -32,76 +41,35 @@ impl Selection {
/// Creates the selection required for test compilation (includes EVM bytecode).
pub fn new_required_for_tests() -> Self {
Self {
all: Some(FileSelection::new_required_for_tests()),
all: FileSelection::new_required_for_tests(),
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();
}
/// Creates the selection required by Yul validation process.
pub fn new_yul_validation() -> Self {
Self::new(vec![Flag::EVM])
}
/// Extends the output selection with another one.
pub fn extend(&mut self, other: Self) -> &mut Self {
self.all.extend(other.all);
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)
/// Returns flags that are going to be automatically added by the compiler,
/// but were not explicitly requested by the user.
///
/// Afterwards, the flags are used to prune JSON output before returning it.
pub fn selection_to_prune(&self) -> Self {
Self {
all: self.all.selection_to_prune(),
files: Default::default(),
}
}
#[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)
/// Whether the flag is requested.
pub fn contains(&self, flag: &Flag) -> bool {
self.all.contains(flag)
}
}
@@ -0,0 +1,93 @@
//! `resolc` custom compiler warnings.
//!
//! The revive compiler adds warnings only applicable when compilng
//! to the revive stack on Polkadot to the output.
use std::collections::BTreeMap;
use std::str::FromStr;
use serde::Deserialize;
use serde::Serialize;
use crate::standard_json::output::error::source_location::SourceLocation;
use crate::SolcStandardJsonInputSource;
use crate::SolcStandardJsonOutputError;
// The `resolc` custom compiler warning.
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Warning {
/// The `<address payable>`'s `send` and `transfer` methods usage warning.
SendAndTransfer,
/// The `origin` instruction usage warning.
TxOrigin,
}
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()
}
/// The displayed warning messages.
pub fn as_message(&self) -> &'static str {
match self {
Self::SendAndTransfer => {
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
"#
}
Self::TxOrigin => {
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.
"#
}
}
}
pub fn as_error(
&self,
node: Option<&str>,
id_paths: &BTreeMap<usize, &String>,
sources: &BTreeMap<String, SolcStandardJsonInputSource>,
) -> SolcStandardJsonOutputError {
SolcStandardJsonOutputError::new_warning(
self.as_message(),
node.and_then(|node| SourceLocation::try_from_ast(node, id_paths)),
Some(sources),
)
}
}
impl FromStr for Warning {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
match string {
"sendandtransfer" => Ok(Self::SendAndTransfer),
"txorigin" => Ok(Self::TxOrigin),
_ => Err(anyhow::anyhow!("Invalid warning: {}", string)),
}
}
}
impl std::fmt::Display for Warning {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::SendAndTransfer => write!(f, "sendandtransfer"),
Self::TxOrigin => write!(f, "txorigin"),
}
}
}
@@ -1,7 +1,7 @@
//! The `solc --standard-json` input source.
use std::io::Read;
use std::path::Path;
use std::path::PathBuf;
use serde::Deserialize;
use serde::Serialize;
@@ -11,30 +11,90 @@ use serde::Serialize;
#[serde(rename_all = "camelCase")]
pub struct Source {
/// The source code file content.
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
/// The source file URLs.
#[serde(skip_serializing_if = "Option::is_none")]
pub urls: Option<Vec<String>>,
}
impl Source {
/// Reads the source from the file system.
pub fn try_read(path: &Path) -> anyhow::Result<Self> {
let content = if path.to_string_lossy() == "-" {
std::io::read_to_string(std::io::stdin())
.map_err(|error| anyhow::anyhow!("<stdin> reading: {error}"))
} else {
std::fs::read_to_string(path)
.map_err(|error| anyhow::anyhow!("File {path:?} reading: {error}"))
}?;
Ok(Self {
content: Some(content),
urls: None,
})
}
/// Tries to resolve the source code.
///
/// At the moment only one URL pointing to the file system is supported.
pub fn try_resolve(&mut self) -> anyhow::Result<()> {
match (self.content.as_ref(), self.urls.as_ref()) {
(Some(_), None) => Ok(()),
(None, Some(urls)) => {
let mut errors = Vec::with_capacity(urls.len());
for url in urls.iter() {
let url_path = PathBuf::from(url);
match Source::try_read(url_path.as_path()) {
Ok(resolved) => {
*self = resolved;
break;
}
Err(error) => errors.push(error),
}
}
if !errors.is_empty() {
anyhow::bail!(
"{}",
errors
.into_iter()
.map(|error| error.to_string())
.collect::<Vec<String>>()
.join("\n")
);
}
Ok(())
}
(Some(_), Some(_)) => anyhow::bail!("Both `content` and `urls` cannot be set."),
(None, None) => anyhow::bail!("Either `content` or `urls` must be set."),
}
}
/// Takes ownership of the source code and returns it.
pub fn take_content(&mut self) -> Option<String> {
self.content.take()
}
/// Returns the source code reference, if the source has been previously read or resolved.
pub fn content(&self) -> Option<&str> {
self.content.as_deref()
}
}
impl From<String> for Source {
fn from(content: String) -> Self {
Self { content }
Self {
content: Some(content),
urls: None,
}
}
}
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 })
impl From<&Path> for Source {
fn from(path: &Path) -> Self {
Self {
content: None,
urls: Some(vec![path.to_string_lossy().to_string()]),
}
}
}
@@ -1,7 +1,5 @@
//! The `solc --standard-json` output contract EVM data.
pub mod bytecode;
use std::collections::BTreeMap;
use serde::Deserialize;
@@ -10,9 +8,11 @@ use serde::Serialize;
use self::bytecode::Bytecode;
use self::bytecode::DeployedBytecode;
pub mod bytecode;
/// The `solc --standard-json` output contract EVM data.
/// It is replaced by PolkaVM data after compiling.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct EVM {
/// The contract PolkaVM assembly code.
@@ -28,8 +28,8 @@ pub struct EVM {
#[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>>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub method_identifiers: BTreeMap<String, String>,
}
impl EVM {
@@ -3,7 +3,7 @@
pub mod evm;
use std::collections::BTreeMap;
use std::collections::HashSet;
use std::collections::BTreeSet;
use serde::Deserialize;
use serde::Serialize;
@@ -11,24 +11,27 @@ use serde::Serialize;
use self::evm::EVM;
/// The `solc --standard-json` output contract.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Default, 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>,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub abi: serde_json::Value,
/// The contract metadata.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub metadata: serde_json::Value,
/// The contract developer documentation.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub devdoc: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub devdoc: serde_json::Value,
/// The contract user documentation.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub userdoc: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub userdoc: serde_json::Value,
/// The contract storage layout.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub storage_layout: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub storage_layout: serde_json::Value,
/// The contract storage layout.
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub transient_storage_layout: serde_json::Value,
/// Contract's bytecode and related objects
#[serde(default, skip_serializing_if = "Option::is_none")]
pub evm: Option<EVM>,
@@ -36,15 +39,39 @@ pub struct Contract {
#[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>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub ir_optimized: String,
/// The contract PolkaVM bytecode hash.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hash: Option<String>,
/// Unlinked factory dependencies.
#[serde(default, skip_deserializing)]
pub factory_dependencies_unlinked: BTreeSet<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>>,
#[serde(default, skip_deserializing)]
pub factory_dependencies: BTreeMap<String, String>,
/// Missing linkable libraries.
#[serde(default, skip_deserializing)]
pub missing_libraries: BTreeSet<String>,
/// Binary object format.
#[serde(default, skip_deserializing)]
pub object_format: Option<revive_common::ObjectFormat>,
}
impl Contract {
/// Checks if all fields are unset or empty.
pub fn is_empty(&self) -> bool {
self.abi.is_null()
&& self.storage_layout.is_null()
&& self.transient_storage_layout.is_null()
&& self.metadata.is_null()
&& self.devdoc.is_null()
&& self.userdoc.is_null()
&& self.ir_optimized.is_empty()
&& self.evm.is_none()
&& self.hash.is_none()
&& self.factory_dependencies_unlinked.is_empty()
&& self.factory_dependencies.is_empty()
&& self.missing_libraries.is_empty()
}
}
@@ -0,0 +1,74 @@
//! Unifies error handling between different Solidity compilers.
use std::io::Write;
use crate::standard_json::output::error::Error;
/// The Solidity compiler error handler trait.
///
/// This is implemented by entities that can collect and handle errors.
pub trait ErrorHandler {
/// Returns errors as a list.
fn errors(&self) -> Vec<&Error>;
/// Extracts warnings from the list of messages.
fn take_warnings(&mut self) -> Vec<Error>;
/// Checks if there is at least one error.
fn has_errors(&self) -> bool {
!self.errors().is_empty()
}
/// Collects errors into one message and bails, if there is at least one error.
fn check_errors(&self) -> anyhow::Result<()> {
if !self.has_errors() {
return Ok(());
}
anyhow::bail!(
"{}",
self.errors()
.iter()
.map(|error| error.to_string())
.collect::<Vec<String>>()
.join("\n")
);
}
/// Checks for errors, exiting the application if there is at least one error.
fn exit_on_error(&self) {
if !self.has_errors() {
return;
}
std::io::stderr()
.write_all(
self.errors()
.iter()
.map(|error| error.to_string())
.collect::<Vec<String>>()
.join("\n")
.as_bytes(),
)
.expect("Stderr writing error");
std::process::exit(revive_common::EXIT_CODE_FAILURE);
}
/// Removes warnings from the list of messages and prints them to stderr.
fn take_and_write_warnings(&mut self) {
let warnings = self.take_warnings();
if warnings.is_empty() {
return;
}
writeln!(
std::io::stderr(),
"{}",
warnings
.into_iter()
.map(|error| error.to_string())
.collect::<Vec<String>>()
.join("\n")
)
.expect("Stderr writing error");
}
}
@@ -0,0 +1,119 @@
//! The mapped error location.
use crate::standard_json::output::error::source_location::SourceLocation;
/// The mapped error location.
///
/// It can be resolved from `solc` AST error location if the source code is provided.
#[derive(Debug)]
pub struct MappedLocation<'a> {
/// The source file path.
pub path: String,
/// The line number.
pub line: Option<usize>,
/// The column number.
pub column: Option<usize>,
/// The error area length.
pub length: Option<usize>,
/// The source code line to print.
pub source_code_line: Option<&'a str>,
}
impl<'a> MappedLocation<'a> {
/// A shortcut constructor.
pub fn new(path: String) -> Self {
Self {
path,
line: None,
column: None,
length: None,
source_code_line: None,
}
}
/// A shortcut constructor.
pub fn new_with_location(
path: String,
line: usize,
column: usize,
length: usize,
source_code_line: Option<&'a str>,
) -> Self {
Self {
path,
line: Some(line),
column: Some(column),
length: Some(length),
source_code_line,
}
}
/// A shortcut constructor from `solc` AST source location.
pub fn try_from_source_location(
source_location: &SourceLocation,
source_code: Option<&'a str>,
) -> Self {
let source_code = match source_code {
Some(source_code) => source_code,
None => return Self::new(source_location.file.to_owned()),
};
if source_location.start <= 0 || source_location.end <= 0 {
return Self::new(source_location.file.to_owned());
}
let start = source_location.start as usize;
let end = source_location.end as usize;
let mut cursor = 1;
for (line, source_line) in source_code.lines().enumerate() {
let cursor_next = cursor + source_line.len() + 1;
if cursor <= start && start <= cursor_next {
let line = line + 1;
let column = start - cursor;
let length = end - start;
return Self::new_with_location(
source_location.file.to_owned(),
line,
column,
length,
Some(source_line),
);
}
cursor = cursor_next;
}
Self::new(source_location.file.to_owned())
}
}
impl std::fmt::Display for MappedLocation<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let mut path = self.path.clone();
if let Some(line) = self.line {
path.push(':');
path.push_str(line.to_string().as_str());
if let Some(column) = self.column {
path.push(':');
path.push_str(column.to_string().as_str());
if let (Some(source_code_line), Some(length)) = (self.source_code_line, self.length)
{
let line_number_length = line.to_string().len();
writeln!(f, "{} --> {path}", " ".repeat(line_number_length))?;
writeln!(f, " {} |", " ".repeat(line_number_length))?;
writeln!(f, " {line} | {source_code_line}")?;
writeln!(
f,
" {} | {} {}",
" ".repeat(line_number_length),
" ".repeat(column),
"^".repeat(std::cmp::min(length, source_code_line.len() - column))
)?;
}
}
} else {
writeln!(f, "--> {path}")?;
}
Ok(())
}
}
@@ -1,14 +1,19 @@
//! The `solc --standard-json` output error.
pub mod source_location;
use std::str::FromStr;
use std::collections::BTreeMap;
use serde::Deserialize;
use serde::Serialize;
use crate::SolcStandardJsonInputSource;
use self::mapped_location::MappedLocation;
use self::source_location::SourceLocation;
pub mod error_handler;
pub mod mapped_location;
pub mod source_location;
/// The `solc --standard-json` output error.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
@@ -30,29 +35,81 @@ pub struct Error {
}
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();
/// The list of ignored `solc` warnings that are strictly EVM-related.
pub const IGNORED_WARNING_CODES: [&'static str; 5] = ["1699", "3860", "5159", "5574", "6417"];
/// A shortcut constructor.
pub fn new<S>(
r#type: &str,
message: S,
source_location: Option<SourceLocation>,
sources: Option<&BTreeMap<String, SolcStandardJsonInputSource>>,
) -> Self
where
S: std::fmt::Display,
{
let message = message.to_string();
let message_trimmed = message.trim();
let mut formatted_message = if message_trimmed.starts_with(r#type) {
message_trimmed.to_owned()
} else {
format!("{type}: {message_trimmed}")
};
formatted_message.push('\n');
if let Some(ref source_location) = source_location {
let source_code = sources.and_then(|sources| {
sources
.get(source_location.file.as_str())
.and_then(|source| source.content())
});
let mapped_location =
MappedLocation::try_from_source_location(source_location, source_code);
formatted_message.push_str(mapped_location.to_string().as_str());
formatted_message.push('\n');
}
Self {
component: "general".to_owned(),
error_code: None,
formatted_message: message.clone(),
formatted_message,
message,
severity: "warning".to_owned(),
source_location: src.map(SourceLocation::from_str).and_then(Result::ok),
r#type: "Warning".to_owned(),
severity: r#type.to_lowercase(),
source_location,
r#type: r#type.to_owned(),
}
}
/// A shortcut constructor.
pub fn new_error<S>(
message: S,
source_location: Option<SourceLocation>,
sources: Option<&BTreeMap<String, SolcStandardJsonInputSource>>,
) -> Self
where
S: std::fmt::Display,
{
Self::new("Error", message, source_location, sources)
}
/// A shortcut constructor.
pub fn new_warning<S>(
message: S,
source_location: Option<SourceLocation>,
sources: Option<&BTreeMap<String, SolcStandardJsonInputSource>>,
) -> Self
where
S: std::fmt::Display,
{
Self::new("Warning", message, source_location, sources)
}
/// Returns the `<address payable>`'s `send` and `transfer` methods usage error.
pub fn message_send_and_transfer(src: Option<&str>) -> Self {
pub fn warning_send_and_transfer(
node: Option<&str>,
id_paths: &BTreeMap<usize, &String>,
sources: &BTreeMap<String, SolcStandardJsonInputSource>,
) -> 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!
@@ -65,43 +122,19 @@ and https://docs.soliditylang.org/en/latest/common-patterns.html#withdrawal-from
"#
.to_owned();
Self {
component: "general".to_owned(),
error_code: None,
formatted_message: message.clone(),
Self::new_warning(
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(),
}
node.and_then(|node| SourceLocation::try_from_ast(node, id_paths)),
Some(sources),
)
}
/// Returns the `origin` instruction usage warning.
pub fn message_tx_origin(src: Option<&str>) -> Self {
pub fn warning_tx_origin(
node: Option<&str>,
id_paths: &BTreeMap<usize, &String>,
sources: &BTreeMap<String, SolcStandardJsonInputSource>,
) -> 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
@@ -110,15 +143,28 @@ to rely on tx.origin, but use msg.sender instead.
"#
.to_owned();
Self {
component: "general".to_owned(),
error_code: None,
formatted_message: message.clone(),
Self::new_warning(
message,
severity: "warning".to_owned(),
source_location: src.map(SourceLocation::from_str).and_then(Result::ok),
r#type: "Warning".to_owned(),
}
node.and_then(|node| SourceLocation::try_from_ast(node, id_paths)),
Some(sources),
)
}
/// Returns the `runtimeCode` code usage error.
pub fn error_runtime_code(
node: Option<&str>,
id_paths: &BTreeMap<usize, &String>,
sources: &BTreeMap<String, SolcStandardJsonInputSource>,
) -> Self {
let message = r#"
Deploy and runtime code are merged in PVM, accessing `type(T).runtimeCode` is not possible.
Please consider changing the functionality relying on reading runtime code to a different approach.
"#;
Self::new_error(
message,
node.and_then(|node| SourceLocation::try_from_ast(node, id_paths)),
Some(sources),
)
}
/// Appends the contract path to the message..
@@ -126,6 +172,16 @@ to rely on tx.origin, but use msg.sender instead.
self.formatted_message
.push_str(format!("\n--> {path}\n").as_str());
}
/// Returns true if this is an error.
pub fn is_error(&self) -> bool {
self.severity == "error"
}
/// Returns true if this is a warning.
pub fn is_warning(&self) -> bool {
self.severity == "warning"
}
}
impl std::fmt::Display for Error {
@@ -1,6 +1,6 @@
//! The `solc --standard-json` output error source location.
use std::str::FromStr;
use std::collections::BTreeMap;
use serde::Deserialize;
use serde::Serialize;
@@ -17,11 +17,27 @@ pub struct SourceLocation {
pub end: isize,
}
impl FromStr for SourceLocation {
type Err = anyhow::Error;
impl SourceLocation {
/// A shortcut constructor.
pub fn new(file: String) -> Self {
Self {
file,
start: -1,
end: -1,
}
}
fn from_str(string: &str) -> Result<Self, Self::Err> {
let mut parts = string.split(':');
/// A shortcut constructor.
///
/// Please note that `start` and `end` are not line and column,
/// but absolute char offsets in the source code file.
pub fn new_with_offsets(file: String, start: isize, end: isize) -> Self {
Self { file, start, end }
}
/// A shortcut constructor from a `solc` AST node.
pub fn try_from_ast(source: &str, id_paths: &BTreeMap<usize, &String>) -> Option<Self> {
let mut parts = source.split(':');
let start = parts
.next()
.map(|string| string.parse::<isize>())
@@ -32,12 +48,15 @@ impl FromStr for SourceLocation {
.map(|string| string.parse::<isize>())
.and_then(Result::ok)
.unwrap_or_default();
let file = parts.next().unwrap_or_default().to_owned();
let path = parts
.next()
.and_then(|string| string.parse::<usize>().ok())
.and_then(|file_id| id_paths.get(&file_id))?;
Ok(Self {
file,
Some(Self::new_with_offsets(
(*path).to_owned(),
start,
end: start + length,
})
start + length,
))
}
}
@@ -1,33 +1,40 @@
//! The `solc --standard-json` output.
pub mod contract;
pub mod error;
pub mod source;
use std::collections::BTreeMap;
use serde::Deserialize;
use serde::Serialize;
#[cfg(feature = "resolc")]
use crate::warning::Warning;
use crate::standard_json::input::settings::warning::Warning;
use crate::standard_json::output::error::error_handler::ErrorHandler;
#[cfg(feature = "resolc")]
use crate::SolcStandardJsonInputSettingsSelection;
#[cfg(feature = "resolc")]
use crate::SolcStandardJsonInputSource;
#[cfg(feature = "parallel")]
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use self::contract::Contract;
use self::error::Error as SolcStandardJsonOutputError;
use self::source::Source;
pub mod contract;
pub mod error;
pub mod 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>>>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub contracts: BTreeMap<String, BTreeMap<String, Contract>>,
/// The source code mapping data.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sources: Option<BTreeMap<String, Source>>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub sources: BTreeMap<String, Source>,
/// The compilation errors and warnings.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub errors: Option<Vec<SolcStandardJsonOutputError>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<SolcStandardJsonOutputError>,
/// The `solc` compiler version.
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
@@ -39,33 +46,156 @@ pub struct Output {
pub revive_version: Option<String>,
}
#[cfg(feature = "resolc")]
impl Output {
/// Traverses the AST and returns the list of additional errors and warnings.
#[cfg(feature = "resolc")]
pub fn preprocess_ast(&mut self, suppressed_warnings: &[Warning]) -> anyhow::Result<()> {
let sources = match self.sources.as_ref() {
Some(sources) => sources,
None => return Ok(()),
};
/// Initializes a standard JSON output.
///
/// Is used for projects compiled without `solc`.
pub fn new(
sources: &BTreeMap<String, SolcStandardJsonInputSource>,
messages: &mut Vec<SolcStandardJsonOutputError>,
) -> Self {
let sources = sources
.keys()
.enumerate()
.map(|(index, path)| (path.to_owned(), Source::new(index)))
.collect::<BTreeMap<String, Source>>();
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 {
contracts: BTreeMap::new(),
sources,
errors: std::mem::take(messages),
version: None,
long_version: None,
revive_version: None,
}
}
/// Initializes a standard JSON output with messages.
///
/// Is used to emit errors in standard JSON mode.
pub fn new_with_messages(messages: Vec<SolcStandardJsonOutputError>) -> Self {
Self {
contracts: BTreeMap::new(),
sources: BTreeMap::new(),
errors: messages,
version: None,
long_version: None,
revive_version: None,
}
}
/// Prunes the output JSON and prints it to stdout.
pub fn write_and_exit(
mut self,
selection_to_prune: SolcStandardJsonInputSettingsSelection,
) -> ! {
let sources = self.sources.values_mut().collect::<Vec<&mut Source>>();
for source in sources.into_iter() {
if selection_to_prune
.contains(&crate::SolcStandardJsonInputSettingsSelectionFileFlag::AST)
{
source.ast = None;
}
}
self.errors = match self.errors.take() {
Some(mut errors) => {
errors.extend(messages);
Some(errors)
let contracts = self
.contracts
.values_mut()
.flat_map(|contracts| contracts.values_mut())
.collect::<Vec<&mut Contract>>();
for contract in contracts.into_iter() {
if selection_to_prune
.contains(&crate::SolcStandardJsonInputSettingsSelectionFileFlag::Metadata)
{
contract.metadata = serde_json::Value::Null;
}
None => Some(messages),
};
if selection_to_prune
.contains(&crate::SolcStandardJsonInputSettingsSelectionFileFlag::Yul)
{
contract.ir_optimized = String::new();
}
if let Some(ref mut evm) = contract.evm {
if selection_to_prune.contains(
&crate::SolcStandardJsonInputSettingsSelectionFileFlag::MethodIdentifiers,
) {
evm.method_identifiers.clear();
}
}
}
self.contracts.retain(|_, contracts| {
contracts.retain(|_, contract| !contract.is_empty());
!contracts.is_empty()
});
serde_json::to_writer(std::io::stdout(), &self).expect("Stdout writing error");
std::process::exit(revive_common::EXIT_CODE_SUCCESS);
}
/// Traverses the AST and returns the list of additional errors and warnings.
pub fn preprocess_ast(
&mut self,
sources: &BTreeMap<String, SolcStandardJsonInputSource>,
suppressed_warnings: &[Warning],
) -> anyhow::Result<()> {
let id_paths: BTreeMap<usize, &String> = self
.sources
.iter()
.map(|(path, source)| (source.id, path))
.collect();
#[cfg(feature = "parallel")]
let iter = self.sources.par_iter();
#[cfg(not(feature = "parallel"))]
let iter = self.sources.iter();
let messages: Vec<SolcStandardJsonOutputError> = iter
.flat_map(|(_path, source)| {
source
.ast
.as_ref()
.map(|ast| Source::get_messages(ast, &id_paths, sources, suppressed_warnings))
.unwrap_or_default()
})
.collect();
self.errors.extend(messages);
Ok(())
}
/// Pushes an arbitrary error with path.
///
/// Please do not push project-general errors without paths here.
pub fn push_error(&mut self, path: Option<String>, error: anyhow::Error) {
use crate::standard_json::output::error::source_location::SourceLocation;
self.errors.push(SolcStandardJsonOutputError::new_error(
error,
path.map(SourceLocation::new),
None,
));
}
}
impl ErrorHandler for Output {
fn errors(&self) -> Vec<&SolcStandardJsonOutputError> {
self.errors
.iter()
.filter(|error| error.is_error())
.collect()
}
fn take_warnings(&mut self) -> Vec<SolcStandardJsonOutputError> {
let warnings = self
.errors
.iter()
.filter(|message| message.is_warning())
.cloned()
.collect();
self.errors.retain(|message| !message.is_warning());
warnings
}
}
@@ -1,11 +1,17 @@
//! The `solc --standard-json` output source.
#[cfg(feature = "resolc")]
use std::collections::BTreeMap;
use serde::Deserialize;
use serde::Serialize;
#[cfg(feature = "resolc")]
use crate::standard_json::input::settings::warning::Warning;
#[cfg(feature = "resolc")]
use crate::standard_json::output::error::Error as SolcStandardJsonOutputError;
#[cfg(feature = "resolc")]
use crate::warning::Warning;
use crate::SolcStandardJsonInputSource;
/// The `solc --standard-json` output source.
#[derive(Debug, Serialize, Deserialize, Clone)]
@@ -17,160 +23,126 @@ pub struct Source {
pub ast: Option<serde_json::Value>,
}
#[cfg(feature = "resolc")]
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(),
))
/// Initializes a standard JSON source.
///
/// Is used for projects compiled without `solc`.
pub fn new(id: usize) -> Self {
Self { id, ast: None }
}
/// 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(
/// Checks the AST node for the usage of send or transfer address methods.
pub fn check_send_and_transfer(
ast: &serde_json::Value,
id_paths: &BTreeMap<usize, &String>,
sources: &BTreeMap<String, SolcStandardJsonInputSource>,
) -> 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;
}
(ast.get("nodeType")?.as_str()? == "FunctionCall").then_some(())?;
Some(SolcStandardJsonOutputError::message_extcodesize(
ast.get("src")?.as_str(),
))
let expression = ast.get("expression")?.as_object()?;
(expression.get("nodeType")?.as_str()? == "MemberAccess").then_some(())?;
let member_name = expression.get("memberName")?.as_str()?;
["send", "transfer"].contains(&member_name).then_some(())?;
let expression = expression.get("expression")?.as_object()?;
let type_descriptions = expression.get("typeDescriptions")?.as_object()?;
let type_identifier = type_descriptions.get("typeIdentifier")?.as_str()?;
["t_address_payable"]
.contains(&type_identifier)
.then_some(())?;
Some(Warning::SendAndTransfer.as_error(ast.get("src")?.as_str(), id_paths, sources))
}
/// Checks the AST node for the `origin` assembly instruction usage.
pub fn check_assembly_origin(ast: &serde_json::Value) -> Option<SolcStandardJsonOutputError> {
/// Checks the AST node for the usage of runtime code.
pub fn check_runtime_code(
ast: &serde_json::Value,
id_paths: &BTreeMap<usize, &String>,
sources: &BTreeMap<String, SolcStandardJsonInputSource>,
) -> 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;
}
(ast.get("nodeType")?.as_str()? == "MemberAccess").then_some(())?;
(ast.get("memberName")?.as_str()? == "runtimeCode").then_some(())?;
Some(SolcStandardJsonOutputError::message_tx_origin(
let expression = ast.get("expression")?.as_object()?;
let type_descriptions = expression.get("typeDescriptions")?.as_object()?;
type_descriptions
.get("typeIdentifier")?
.as_str()?
.starts_with("t_magic_meta_type")
.then_some(())?;
Some(SolcStandardJsonOutputError::error_runtime_code(
ast.get("src")?.as_str(),
id_paths,
sources,
))
}
/// Checks the AST node for the `tx.origin` value usage.
pub fn check_tx_origin(ast: &serde_json::Value) -> Option<SolcStandardJsonOutputError> {
pub fn check_tx_origin(
ast: &serde_json::Value,
id_paths: &BTreeMap<usize, &String>,
sources: &BTreeMap<String, SolcStandardJsonInputSource>,
) -> 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;
}
(ast.get("nodeType")?.as_str()? == "MemberAccess").then_some(())?;
(ast.get("memberName")?.as_str()? == "origin").then_some(())?;
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;
}
(expression.get("nodeType")?.as_str()? == "Identifier").then_some(())?;
(expression.get("name")?.as_str()? == "tx").then_some(())?;
Some(SolcStandardJsonOutputError::message_tx_origin(
ast.get("src")?.as_str(),
))
Some(Warning::TxOrigin.as_error(ast.get("src")?.as_str(), id_paths, sources))
}
/// Returns the list of messages for some specific parts of the AST.
#[cfg(feature = "resolc")]
pub fn get_messages(
ast: &serde_json::Value,
id_paths: &BTreeMap<usize, &String>,
sources: &BTreeMap<String, SolcStandardJsonInputSource>,
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) {
if !suppressed_warnings.contains(&Warning::SendAndTransfer) {
if let Some(message) = Self::check_send_and_transfer(ast, id_paths, sources) {
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) {
if let Some(message) = Self::check_tx_origin(ast, id_paths, sources) {
messages.push(message);
}
}
if let Some(message) = Self::check_runtime_code(ast, id_paths, sources) {
messages.push(message);
}
match ast {
serde_json::Value::Array(array) => {
for element in array.iter() {
messages.extend(Self::get_messages(element, suppressed_warnings));
messages.extend(Self::get_messages(
element,
id_paths,
sources,
suppressed_warnings,
));
}
}
serde_json::Value::Object(object) => {
for (_key, value) in object.iter() {
messages.extend(Self::get_messages(value, suppressed_warnings));
messages.extend(Self::get_messages(
value,
id_paths,
sources,
suppressed_warnings,
));
}
}
_ => {}
@@ -178,25 +150,4 @@ impl Source {
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,
},
)
.next_back()
.ok_or_else(|| anyhow::anyhow!("The last contract not found in the AST"))
}
}
-48
View File
@@ -1,48 +0,0 @@
//! `resolc` custom compiler warnings.
//!
//! The revive compiler adds warnings only applicable when compilng
//! to the revive stack on Polkadot to the output.
use std::str::FromStr;
use serde::Deserialize;
use serde::Serialize;
// The `resolc` custom compiler warning.
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Warning {
EcRecover,
SendTransfer,
ExtCodeSize,
TxOrigin,
BlockTimestamp,
BlockNumber,
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)),
}
}
}