use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tracing::warn;
use crate::{
analysis::{
AnalysisError, Analyzer, ContractRef, FunctionRef, SourceAnalysis, StepRef,
UserDefinedTypeRef, UCID, UFID, UTID,
},
ASTPruner, Artifact, VariableRef, USID, UVID,
};
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct AnalysisResult {
pub sources: HashMap<u32, SourceAnalysis>,
pub ucid_to_contract: HashMap<UCID, ContractRef>,
pub ufid_to_function: HashMap<UFID, FunctionRef>,
pub usid_to_step: HashMap<USID, StepRef>,
pub uvid_to_variable: HashMap<UVID, VariableRef>,
pub utid_to_user_defined_type: HashMap<UTID, UserDefinedTypeRef>,
pub user_defined_types: HashMap<usize, UserDefinedTypeRef>,
}
pub fn analyze(artifact: &Artifact) -> Result<AnalysisResult, AnalysisError> {
let source_results: Vec<SourceAnalysis> = artifact
.output
.sources
.par_iter()
.map(|(path, source_result)| {
let source_id = source_result.id;
let mut source_ast = source_result.ast.clone().ok_or(AnalysisError::MissingAst)?;
let source_unit = ASTPruner::convert(&mut source_ast, false)
.map_err(AnalysisError::ASTConversionError)?;
let analyzer = Analyzer::new(source_id);
let mut source_result = analyzer.analyze(source_id, path, &source_unit)?;
source_result.steps.sort_unstable_by_key(|step| step.read().src.start);
source_result.steps.reverse();
for (i, step) in source_result.steps.iter().enumerate() {
if i > 0 {
let prev = &source_result.steps[i - 1];
let end = step
.read()
.src
.start
.map(|start| start + step.read().src.length.unwrap_or(0));
if end > prev.read().src.start {
warn!("Overlapping steps detected: {:?}", step);
}
}
}
Ok(source_result)
})
.collect::<Result<Vec<_>, AnalysisError>>()?;
let mut ucid_to_contract = HashMap::new();
let mut ufid_to_function = HashMap::new();
let mut usid_to_step = HashMap::new();
let mut uvid_to_variable = HashMap::new();
let mut utid_to_user_defined_type = HashMap::new();
let mut user_defined_types = HashMap::new();
for result in source_results.iter() {
ucid_to_contract.extend(result.contract_table().into_iter());
ufid_to_function.extend(result.function_table().into_iter());
usid_to_step.extend(result.step_table().into_iter());
uvid_to_variable.extend(result.variable_table().into_iter());
utid_to_user_defined_type.extend(result.user_defined_type_table().into_iter());
user_defined_types.extend(result.user_defined_types().into_iter());
}
let sources = source_results.into_iter().map(|s| (s.id, s)).collect();
Ok(AnalysisResult {
sources,
ucid_to_contract,
ufid_to_function,
usid_to_step,
uvid_to_variable,
utid_to_user_defined_type,
user_defined_types,
})
}
#[cfg(test)]
mod tests {
use super::*;
use foundry_block_explorers::contract::Metadata;
use foundry_compilers::{
artifacts::{
output_selection::OutputSelection, EvmVersion, Settings, SolcInput, Source, Sources,
},
solc::{Solc, SolcLanguage},
};
use std::path::PathBuf;
#[test]
#[allow(clippy::field_reassign_with_default)]
fn test_analyze_contract_with_three_statements() {
let contract_source = r#"
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleContract {
uint256 public value;
function setValue(uint256 newValue) public {
value = newValue; // Statement 1: Assignment
emit ValueSet(newValue); // Statement 2: Event emission
value = value + 1; // Statement 3: Increment
}
event ValueSet(uint256 value);
}
"#;
let file_path = PathBuf::from("SimpleContract.sol");
let sources =
Sources::from_iter([(file_path.clone(), Source::new(contract_source.to_string()))]);
let mut settings = Settings::default();
settings.output_selection = OutputSelection::complete_output_selection();
settings.evm_version = Some(EvmVersion::Paris);
let input = SolcInput::new(SolcLanguage::Solidity, sources, settings);
let version = semver::Version::new(0, 8, 19);
let compiler = Solc::find_or_install(&version).expect("Failed to find or install Solc");
let output = compiler.compile_exact(&input).expect("Compilation failed");
println!("input {input:?}");
println!("output {output:?}");
let source_code_meta = foundry_block_explorers::contract::SourceCodeMetadata::SourceCode(
contract_source.to_string(),
);
let meta = Metadata {
source_code: source_code_meta,
abi: String::new(),
contract_name: "SimpleContract".to_string(),
compiler_version: "0.8.19".to_string(),
optimization_used: 200,
runs: 200,
constructor_arguments: Default::default(),
evm_version: "paris".to_string(),
library: String::new(),
license_type: String::new(),
proxy: 0,
implementation: None,
swarm_source: String::new(),
};
let artifact = Artifact { meta, input, output };
let result = analyze(&artifact).expect("Analysis should succeed");
assert!(!result.sources.is_empty(), "Should have analyzed at least one source file");
let source_result = result.sources.get(&0).expect("Should find the source file");
assert_eq!(source_result.path, file_path);
assert!(!source_result.steps.is_empty(), "Should have analyzed steps in the contract");
let step_count = source_result.steps.len();
assert!(step_count > 0, "Should have found steps in the contract");
}
}