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

feat: add minimal ast bindings #1167

Merged
merged 3 commits into from
Apr 23, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions ethers-solc/src/artifact_output/configurable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use crate::{
bytecode::{CompactBytecode, CompactDeployedBytecode},
contract::{CompactContract, CompactContractBytecode, Contract},
output_selection::{ContractOutputSelection, EvmOutputSelection, EwasmOutputSelection},
CompactContractBytecodeCow, CompactEvm, DevDoc, Ewasm, GasEstimates, LosslessAbi, Metadata,
Offsets, Settings, StorageLayout, UserDoc,
Ast, CompactContractBytecodeCow, CompactEvm, DevDoc, Ewasm, GasEstimates, LosslessAbi,
Metadata, Offsets, Settings, StorageLayout, UserDoc,
},
ArtifactOutput, SolcConfig, SolcError, SourceFile,
};
Expand Down Expand Up @@ -47,8 +47,8 @@ pub struct ConfigurableContractArtifact {
pub ir_optimized: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ewasm: Option<Ewasm>,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub ast: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ast: Option<Ast>,
}

impl ConfigurableContractArtifact {
Expand Down Expand Up @@ -284,7 +284,7 @@ impl ArtifactOutput for ConfigurableArtifacts {
ir: artifact_ir,
ir_optimized: artifact_ir_optimized,
ewasm: artifact_ewasm,
ast: source_file.map(|s| s.ast.clone()).unwrap_or_default(),
ast: source_file.and_then(|s| s.ast.clone()),
}
}
}
Expand Down
223 changes: 223 additions & 0 deletions ethers-solc/src/artifacts/ast.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
//! Bindings for solc's `ast` output field

use crate::artifacts::serde_helpers;
use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, fmt, fmt::Write, str::FromStr};

/// Represents the AST field in the solc output
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Ast {
#[serde(rename = "absolutePath")]
pub absolute_path: String,
pub id: usize,
#[serde(default, rename = "exportedSymbols")]
pub exported_symbols: BTreeMap<String, Vec<usize>>,
#[serde(rename = "nodeType")]
pub node_type: NodeType,
#[serde(with = "serde_helpers::display_from_str")]
pub src: SourceLocation,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub nodes: Vec<Node>,
#[serde(flatten)]
pub other: BTreeMap<String, serde_json::Value>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Node {
pub id: usize,
#[serde(rename = "nodeType")]
pub node_type: NodeType,
#[serde(with = "serde_helpers::display_from_str")]
pub src: SourceLocation,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub nodes: Vec<Node>,
#[serde(flatten)]
pub other: BTreeMap<String, serde_json::Value>,
}

/// Represents the source location of a node : `<start>:<length>:<index>`
///
/// The `length` and `index` can be -1 which is represented as `None`
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceLocation {
pub start: usize,
pub length: Option<usize>,
pub index: Option<usize>,
}

impl FromStr for SourceLocation {
type Err = String;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let invalid_location = move || format!("{} invalid source location", s);

let mut split = s.split(':');
let start = split
.next()
.ok_or_else(invalid_location)?
.parse::<usize>()
.map_err(|_| invalid_location())?;
let length = split
.next()
.ok_or_else(invalid_location)?
.parse::<isize>()
.map_err(|_| invalid_location())?;
let index = split
.next()
.ok_or_else(invalid_location)?
.parse::<isize>()
.map_err(|_| invalid_location())?;

let length = if length < 0 { None } else { Some(length as usize) };
let index = if index < 0 { None } else { Some(index as usize) };

Ok(Self { start, length, index })
}
}

impl fmt::Display for SourceLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.start.fmt(f)?;
f.write_char(':')?;
if let Some(length) = self.length {
length.fmt(f)?;
} else {
f.write_str("-1")?;
}
f.write_char(':')?;
if let Some(index) = self.index {
index.fmt(f)?;
} else {
f.write_str("-1")?;
}
Ok(())
}
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum NodeType {
YulAssignment,
YulBlock,
YulExpressionStatement,
YulForLoop,
YulIf,
YulVariableDeclaration,
YulFunctionDefinition,
SourceUnit,
PragmaDirective,
ContractDefinition,
EventDefinition,
ErrorDefinition,
Other(String),
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn can_parse_ast() {
let ast = r#"
{
"absolutePath": "input.sol",
"exportedSymbols":
{
"Ballot":
[
2
],
"Ballot2":
[
3
],
"Ballot3":
[
4
]
},
"id": 5,
"nodeType": "SourceUnit",
"nodes":
[
{
"id": 1,
"literals":
[
"solidity",
">=",
"0.4",
".0"
],
"nodeType": "PragmaDirective",
"src": "1:24:0"
},
{
"abstract": false,
"baseContracts": [],
"canonicalName": "Ballot",
"contractDependencies": [],
"contractKind": "contract",
"fullyImplemented": true,
"id": 2,
"linearizedBaseContracts":
[
2
],
"name": "Ballot",
"nameLocation": "36:6:0",
"nodeType": "ContractDefinition",
"nodes": [],
"scope": 5,
"src": "27:20:0",
"usedErrors": []
},
{
"abstract": false,
"baseContracts": [],
"canonicalName": "Ballot2",
"contractDependencies": [],
"contractKind": "contract",
"fullyImplemented": true,
"id": 3,
"linearizedBaseContracts":
[
3
],
"name": "Ballot2",
"nameLocation": "58:7:0",
"nodeType": "ContractDefinition",
"nodes": [],
"scope": 5,
"src": "49:21:0",
"usedErrors": []
},
{
"abstract": false,
"baseContracts": [],
"canonicalName": "Ballot3",
"contractDependencies": [],
"contractKind": "contract",
"fullyImplemented": true,
"id": 4,
"linearizedBaseContracts":
[
4
],
"name": "Ballot3",
"nameLocation": "81:7:0",
"nodeType": "ContractDefinition",
"nodes": [],
"scope": 5,
"src": "72:21:0",
"usedErrors": []
}
],
"src": "1:92:0"
}
"#;
let _ast: Ast = serde_json::from_str(ast).unwrap();

dbg!(serde_json::from_str::<serde_json::Value>("{}").unwrap());
}
}
6 changes: 4 additions & 2 deletions ethers-solc/src/artifacts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ use crate::{compile::*, error::SolcIoError, remappings::Remapping, utils};

use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};

pub mod ast;
pub use ast::*;
pub mod bytecode;
pub mod contract;
pub mod output_selection;
Expand Down Expand Up @@ -1344,8 +1346,8 @@ pub struct SecondarySourceLocation {
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
pub struct SourceFile {
pub id: u32,
#[serde(default)]
pub ast: serde_json::Value,
#[serde(default, with = "serde_helpers::empty_json_object_opt")]
pub ast: Option<Ast>,
}

/// A wrapper type for a list of source files
Expand Down
63 changes: 61 additions & 2 deletions ethers-solc/src/artifacts/serde_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
use ethers_core::types::Bytes;
use serde::{Deserialize, Deserializer};

pub fn deserialize_bytes<'de, D>(d: D) -> std::result::Result<Bytes, D::Error>
pub fn deserialize_bytes<'de, D>(d: D) -> Result<Bytes, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(d)?.parse::<Bytes>().map_err(|e| serde::de::Error::custom(e.to_string()))
}

pub fn deserialize_opt_bytes<'de, D>(d: D) -> std::result::Result<Option<Bytes>, D::Error>
pub fn deserialize_opt_bytes<'de, D>(d: D) -> Result<Option<Bytes>, D::Error>
where
D: Deserializer<'de>,
{
Expand Down Expand Up @@ -70,6 +70,43 @@ pub mod json_string_opt {
}
}

/// deserializes empty json object `{}` as `None`
pub mod empty_json_object_opt {
use serde::{
de::{self, DeserializeOwned},
ser, Deserialize, Deserializer, Serialize, Serializer,
};

pub fn serialize<T, S>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: Serialize,
{
if let Some(value) = value {
let value = serde_json::to_string(value).map_err(ser::Error::custom)?;
serializer.serialize_str(&value)
} else {
let empty = serde_json::Value::Object(Default::default());
serde_json::Value::serialize(&empty, serializer)
}
}

pub fn deserialize<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
where
D: Deserializer<'de>,
T: DeserializeOwned,
{
let json = serde_json::Value::deserialize(deserializer)?;
if json.is_null() {
return Ok(None)
}
if json.as_object().map(|obj| obj.is_empty()).unwrap_or_default() {
return Ok(None)
}
serde_json::from_value(json).map_err(de::Error::custom).map(Some)
}
}

/// serde support for string
pub mod string_bytes {
use serde::{Deserialize, Deserializer, Serializer};
Expand Down Expand Up @@ -127,3 +164,25 @@ pub mod display_from_str_opt {
}
}
}

pub mod display_from_str {
use serde::{de, Deserialize, Deserializer, Serializer};
use std::{fmt, str::FromStr};

pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
where
T: fmt::Display,
S: Serializer,
{
serializer.collect_str(value)
}

pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: FromStr,
T::Err: fmt::Display,
{
String::deserialize(deserializer)?.parse().map_err(de::Error::custom)
}
}
4 changes: 2 additions & 2 deletions ethers-solc/src/compile/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -692,9 +692,9 @@ mod tests {
assert_eq!(state.output.sources.len(), 3);
for (f, source) in &state.output.sources {
if f.ends_with("A.sol") {
assert!(source.ast.is_object());
assert!(source.ast.is_some());
} else {
assert!(source.ast.is_null());
assert!(source.ast.is_none());
}
}

Expand Down