#![allow(missing_docs)]
use crate::errors::{AnalysisError, AnalysisResult};
use serde_json::Value;
use std::path::Path;
use tracing::{debug, info};
#[derive(Debug, Clone)]
pub struct CompilationOutput {
pub source: String,
pub bytecode: String,
pub runtime_bytecode: String,
pub ast: Value,
pub contract_name: String,
}
#[derive(Debug, Clone)]
pub struct AstContract {
pub name: String,
pub id: u64,
pub state_vars: Vec<AstStateVar>,
pub functions: Vec<AstFunction>,
pub events: Vec<AstEvent>,
pub modifiers: Vec<AstModifier>,
pub base_contracts: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct AstStateVar {
pub name: String,
pub type_name: String,
pub is_mutable: bool,
pub visibility: Visibility,
pub id: u64,
}
#[derive(Debug, Clone)]
pub struct AstFunction {
pub name: String,
pub parameters: Vec<AstParam>,
pub returns: Vec<AstParam>,
pub visibility: Visibility,
pub is_mutable: bool,
pub is_pure: bool,
pub is_view: bool,
pub modifiers: Vec<String>,
pub body: Vec<String>,
pub id: u64,
}
#[derive(Debug, Clone)]
pub struct AstParam {
pub name: String,
pub type_name: String,
}
#[derive(Debug, Clone)]
pub struct AstEvent {
pub name: String,
pub parameters: Vec<AstParam>,
pub id: u64,
}
#[derive(Debug, Clone)]
pub struct AstModifier {
pub name: String,
pub parameters: Vec<AstParam>,
pub id: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Visibility {
Public,
Internal,
Private,
External,
}
impl std::str::FromStr for Visibility {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"public" => Ok(Visibility::Public),
"internal" => Ok(Visibility::Internal),
"private" => Ok(Visibility::Private),
"external" => Ok(Visibility::External),
_ => Err(format!("Unknown visibility: {}", s)),
}
}
}
impl std::fmt::Display for Visibility {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Visibility::Public => write!(f, "public"),
Visibility::Internal => write!(f, "internal"),
Visibility::Private => write!(f, "private"),
Visibility::External => write!(f, "external"),
}
}
}
pub struct SolidityParser;
impl SolidityParser {
pub fn parse(path: &Path) -> AnalysisResult<AstContract> {
info!("Parsing Solidity file: {:?}", path);
let source = std::fs::read_to_string(path).map_err(AnalysisError::IoError)?;
let filename = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("contract");
Self::parse_source(&source, filename)
}
pub fn parse_source(source: &str, filename: &str) -> AnalysisResult<AstContract> {
debug!("Compiling {} with solc", filename);
let compilation = Self::compile_to_ast(source, filename)?;
Self::extract_contract(&compilation)
}
fn compile_to_ast(source: &str, filename: &str) -> AnalysisResult<CompilationOutput> {
use std::io::Write;
let temp_dir = tempfile::tempdir().map_err(|e| {
AnalysisError::CompilationError(format!("Failed to create temp dir: {}", e))
})?;
let temp_file = temp_dir.path().join(filename);
let mut file = std::fs::File::create(&temp_file).map_err(AnalysisError::IoError)?;
file.write_all(source.as_bytes())
.map_err(AnalysisError::IoError)?;
drop(file);
let output = std::process::Command::new("solc")
.arg("--combined-json")
.arg("bin,bin-runtime,ast")
.arg("--optimize")
.arg("--optimize-runs=200")
.arg(&temp_file)
.output()
.map_err(|e| {
debug!("solc invocation failed: {}", e);
AnalysisError::CompilationError(
"solc compiler not found. Install with: apt-get install solc (or brew install solidity on macOS)".to_string(),
)
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("Error:") {
return Err(AnalysisError::CompilationError(format!(
"solc compilation failed:\n{}",
stderr
)));
}
}
let stdout = String::from_utf8(output.stdout).map_err(|e| {
AnalysisError::CompilationError(format!("Invalid UTF-8 in solc output: {}", e))
})?;
let json: Value = serde_json::from_str(&stdout).map_err(|e| {
AnalysisError::AstParsingError(format!("Failed to parse solc JSON output: {}", e))
})?;
let contract_name = Self::extract_contract_name_from_json(&json);
let bytecode = Self::extract_bytecode_from_json(&json, false);
let runtime_bytecode = Self::extract_bytecode_from_json(&json, true);
let ast = json
.get("contracts")
.and_then(|c| c.as_object())
.and_then(|obj| obj.values().next())
.and_then(|c| c.get("ast"))
.cloned()
.unwrap_or_else(|| Value::Object(Default::default()));
Ok(CompilationOutput {
source: source.to_string(),
bytecode,
runtime_bytecode,
ast,
contract_name,
})
}
fn extract_contract_name_from_json(json: &Value) -> String {
json.get("contracts")
.and_then(|contracts| contracts.as_object())
.and_then(|obj| obj.values().next())
.and_then(|contract| contract.as_object())
.and_then(|obj| obj.keys().next())
.map(|name| name.trim_start_matches(':').to_string())
.unwrap_or_else(|| "Unknown".to_string())
}
fn extract_bytecode_from_json(json: &Value, runtime: bool) -> String {
let key = if runtime { "bin-runtime" } else { "bin" };
json.get("contracts")
.and_then(|contracts| contracts.as_object())
.and_then(|obj| obj.values().next())
.and_then(|contract| contract.as_object())
.and_then(|obj| obj.values().next())
.and_then(|details| details.get(key))
.and_then(|bytecode| bytecode.as_str())
.unwrap_or("")
.to_string()
}
fn extract_contract(output: &CompilationOutput) -> AnalysisResult<AstContract> {
let ast = &output.ast;
let contract = AstContract {
name: output.contract_name.clone(),
id: ast.get("id").and_then(|id| id.as_u64()).unwrap_or(0),
state_vars: Self::extract_state_vars(ast)?,
functions: Self::extract_functions(ast)?,
events: Self::extract_events(ast)?,
modifiers: Self::extract_modifiers(ast)?,
base_contracts: Self::extract_base_contracts(ast),
};
info!(
"Extracted contract '{}' with {} functions, {} state vars, {} events",
contract.name,
contract.functions.len(),
contract.state_vars.len(),
contract.events.len()
);
Ok(contract)
}
fn extract_state_vars(ast: &Value) -> AnalysisResult<Vec<AstStateVar>> {
let mut vars = Vec::new();
if let Some(nodes) = ast.get("nodes").and_then(|n| n.as_array()) {
for node in nodes {
Self::collect_state_vars(node, &mut vars);
}
}
debug!("Found {} state variables", vars.len());
Ok(vars)
}
fn collect_state_vars(node: &Value, vars: &mut Vec<AstStateVar>) {
if let Some(node_type) = node.get("nodeType").and_then(|t| t.as_str()) {
if node_type == "VariableDeclaration" {
if !Self::is_parameter(node) {
if let Some(name) = node.get("name").and_then(|n| n.as_str()) {
let var = AstStateVar {
name: name.to_string(),
type_name: Self::extract_type_name(node),
is_mutable: !node
.get("constant")
.and_then(|c| c.as_bool())
.unwrap_or(false),
visibility: node
.get("visibility")
.and_then(|v| v.as_str())
.and_then(|v| v.parse().ok())
.unwrap_or(Visibility::Internal),
id: node.get("id").and_then(|i| i.as_u64()).unwrap_or(0),
};
vars.push(var);
}
}
}
}
if let Some(nodes) = node.get("nodes").and_then(|n| n.as_array()) {
for child in nodes {
Self::collect_state_vars(child, vars);
}
}
}
fn is_parameter(node: &Value) -> bool {
node.get("constant")
.and_then(|c| c.as_bool())
.unwrap_or(false)
|| node
.get("isStateVar")
.and_then(|isv| isv.as_bool())
.map(|isv| !isv)
.unwrap_or(false)
}
fn extract_type_name(node: &Value) -> String {
if let Some(type_name) = node.get("typeName") {
if let Some(name) = type_name.get("name").and_then(|n| n.as_str()) {
return name.to_string();
}
if let Some(type_str) = type_name.as_str() {
return type_str.to_string();
}
if let Some(node_type) = type_name.get("nodeType").and_then(|nt| nt.as_str()) {
return node_type.to_string();
}
}
"unknown".to_string()
}
fn extract_functions(ast: &Value) -> AnalysisResult<Vec<AstFunction>> {
let mut functions = Vec::new();
if let Some(nodes) = ast.get("nodes").and_then(|n| n.as_array()) {
for node in nodes {
Self::collect_functions(node, &mut functions);
}
}
debug!("Found {} functions", functions.len());
Ok(functions)
}
fn collect_functions(node: &Value, functions: &mut Vec<AstFunction>) {
if let Some(node_type) = node.get("nodeType").and_then(|t| t.as_str()) {
if node_type == "FunctionDefinition" {
if let Some(name) = node.get("name").and_then(|n| n.as_str()) {
let func = AstFunction {
name: name.to_string(),
parameters: Self::extract_params(node, "params"),
returns: Self::extract_params(node, "returns"),
visibility: node
.get("visibility")
.and_then(|v| v.as_str())
.and_then(|v| v.parse().ok())
.unwrap_or(Visibility::Internal),
is_mutable: !node
.get("stateMutability")
.and_then(|s| s.as_str())
.map(|s| ["pure", "view"].contains(&s))
.unwrap_or(false),
is_pure: node
.get("stateMutability")
.and_then(|s| s.as_str())
.map(|s| s == "pure")
.unwrap_or(false),
is_view: node
.get("stateMutability")
.and_then(|s| s.as_str())
.map(|s| s == "view")
.unwrap_or(false),
modifiers: Self::extract_modifier_names(node),
body: Self::extract_function_body(node),
id: node.get("id").and_then(|id| id.as_u64()).unwrap_or(0),
};
functions.push(func);
}
}
}
if let Some(children) = node.get("nodes").and_then(|n| n.as_array()) {
for child in children {
Self::collect_functions(child, functions);
}
}
}
fn extract_modifier_names(node: &Value) -> Vec<String> {
let mut names = Vec::new();
if let Some(modifiers) = node.get("modifiers").and_then(|m| m.as_array()) {
for modifier in modifiers {
if let Some(name) = modifier.get("name").and_then(|n| n.as_str()) {
names.push(name.to_string());
}
}
}
names
}
fn extract_function_body(node: &Value) -> Vec<String> {
let mut body = Vec::new();
if node.get("body").is_some() {
body.push("[function body parsed]".to_string());
}
body
}
fn extract_params(node: &Value, field: &str) -> Vec<AstParam> {
let mut params = Vec::new();
let params_node = node.get(field).or_else(|| node.get("parameters"));
if let Some(params_obj) = params_node {
if let Some(arr) = params_obj.get("parameters").and_then(|p| p.as_array()) {
for param in arr {
if let Some(name) = param.get("name").and_then(|n| n.as_str()) {
let type_name = Self::extract_type_name(param);
params.push(AstParam {
name: name.to_string(),
type_name,
});
}
}
}
}
params
}
fn extract_events(ast: &Value) -> AnalysisResult<Vec<AstEvent>> {
let mut events = Vec::new();
if let Some(nodes) = ast.get("nodes").and_then(|n| n.as_array()) {
for node in nodes {
Self::collect_events(node, &mut events);
}
}
debug!("Found {} events", events.len());
Ok(events)
}
fn collect_events(node: &Value, events: &mut Vec<AstEvent>) {
if let Some(node_type) = node.get("nodeType").and_then(|t| t.as_str()) {
if node_type == "EventDefinition" {
if let Some(name) = node.get("name").and_then(|n| n.as_str()) {
let event = AstEvent {
name: name.to_string(),
parameters: Self::extract_event_params(node),
id: node.get("id").and_then(|id| id.as_u64()).unwrap_or(0),
};
events.push(event);
}
}
}
if let Some(children) = node.get("nodes").and_then(|n| n.as_array()) {
for child in children {
Self::collect_events(child, events);
}
}
}
fn extract_event_params(node: &Value) -> Vec<AstParam> {
let mut params = Vec::new();
if let Some(arr) = node.get("parameters").and_then(|p| p.as_array()) {
for param in arr {
if let Some(name) = param.get("name").and_then(|n| n.as_str()) {
let type_name = Self::extract_type_name(param);
params.push(AstParam {
name: name.to_string(),
type_name,
});
}
}
}
params
}
fn extract_modifiers(ast: &Value) -> AnalysisResult<Vec<AstModifier>> {
let mut modifiers = Vec::new();
if let Some(nodes) = ast.get("nodes").and_then(|n| n.as_array()) {
for node in nodes {
Self::collect_modifiers(node, &mut modifiers);
}
}
debug!("Found {} modifiers", modifiers.len());
Ok(modifiers)
}
fn collect_modifiers(node: &Value, modifiers: &mut Vec<AstModifier>) {
if let Some(node_type) = node.get("nodeType").and_then(|t| t.as_str()) {
if node_type == "ModifierDefinition" {
if let Some(name) = node.get("name").and_then(|n| n.as_str()) {
let modifier = AstModifier {
name: name.to_string(),
parameters: Self::extract_params(node, "params"),
id: node.get("id").and_then(|id| id.as_u64()).unwrap_or(0),
};
modifiers.push(modifier);
}
}
}
if let Some(children) = node.get("nodes").and_then(|n| n.as_array()) {
for child in children {
Self::collect_modifiers(child, modifiers);
}
}
}
fn extract_base_contracts(ast: &Value) -> Vec<String> {
let mut bases = Vec::new();
if let Some(bases_arr) = ast.get("baseContracts").and_then(|b| b.as_array()) {
for base in bases_arr {
if let Some(name) = base.get("baseName").and_then(|n| n.as_str()) {
bases.push(name.to_string());
}
}
}
bases
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_visibility_parsing() {
assert_eq!("public".parse::<Visibility>(), Ok(Visibility::Public));
assert_eq!("internal".parse::<Visibility>(), Ok(Visibility::Internal));
assert_eq!("private".parse::<Visibility>(), Ok(Visibility::Private));
assert_eq!("external".parse::<Visibility>(), Ok(Visibility::External));
}
#[test]
fn test_visibility_display() {
assert_eq!(Visibility::Public.to_string(), "public");
assert_eq!(Visibility::Internal.to_string(), "internal");
assert_eq!(Visibility::Private.to_string(), "private");
assert_eq!(Visibility::External.to_string(), "external");
}
}