use error_stack::{Report, ResultExt};
use serde::{Deserialize, Serialize};
use std::{error::Error, fmt, fs};
use crate::batbelt::evm::types::{
AccessControlType, EvmContract, EvmContractType, EvmEvent, EvmFileItem, EvmModifierDef,
EvmMutability, EvmParam, EvmVisibility, StorageVariable,
};
#[derive(Debug)]
pub struct EvmMetadataError;
impl fmt::Display for EvmMetadataError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("EvmMetadata error")
}
}
impl Error for EvmMetadataError {}
pub type EvmMetadataResult<T> = error_stack::Result<T, EvmMetadataError>;
const EVM_METADATA_FILE: &str = "BatMetadata.json";
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EvmBatMetadata {
pub contracts: Vec<ContractMetadata>,
pub entry_points: Vec<EntryPointMetadata>,
pub function_dependencies: Vec<FunctionDependency>,
pub interfaces: Vec<InterfaceMetadata>,
#[serde(default)]
pub file_items: Vec<EvmFileItem>,
#[serde(default)]
pub miro: MiroMetadataRef,
#[serde(default)]
pub resolutions: std::collections::HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractMetadata {
pub metadata_id: String,
pub name: String,
pub file_path: String,
pub contract_type: EvmContractType,
pub base_contracts: Vec<String>,
pub functions: Vec<FunctionMetadata>,
pub state_variables: Vec<StorageVariable>,
pub events: Vec<EvmEvent>,
pub modifiers: Vec<EvmModifierDef>,
pub line: usize,
#[serde(default)]
pub external: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionMetadata {
pub metadata_id: String,
pub name: String,
pub contract_name: String,
pub visibility: EvmVisibility,
pub mutability: EvmMutability,
pub modifiers: Vec<String>,
pub params: Vec<EvmParam>,
pub returns: Vec<EvmParam>,
pub line: usize,
#[serde(default)]
pub end_line: usize,
pub is_constructor: bool,
#[serde(default)]
pub is_stub: bool,
#[serde(default)]
pub storage_writes: Vec<String>,
#[serde(default)]
pub storage_write_sites: Vec<StorageWriteSite>,
#[serde(default)]
pub unresolved_calls: Vec<UnresolvedCall>,
#[serde(default)]
pub unknown_external_calls: Vec<ExternalUnknownCall>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExternalUnknownCall {
pub receiver: String,
pub method: String,
#[serde(default)]
pub inferred_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageWriteSite {
pub name: String,
pub line: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnresolvedCall {
pub receiver: String,
pub method: String,
#[serde(default)]
pub inferred_type: String,
#[serde(default)]
pub candidates: Vec<String>,
#[serde(default)]
pub assigned_in: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntryPointMetadata {
pub metadata_id: String,
pub name: String,
pub contract_name: String,
pub function_metadata_id: String,
pub access_control: Vec<AccessControlType>,
pub storage_reads: Vec<String>,
pub storage_writes: Vec<String>,
pub external_calls: Vec<String>,
pub events_emitted: Vec<String>,
pub modifiers: Vec<String>,
pub dependencies: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionDependency {
pub function_metadata_id: String,
pub callees: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InterfaceMetadata {
pub name: String,
pub implemented_by: Vec<String>,
pub functions: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MiroMetadataRef {
pub frames: Vec<MiroFrameRef>,
#[serde(default)]
pub auto: AutoDeployState,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AutoDeployState {
pub region: Option<ShelfState>,
#[serde(default)]
pub frames: Vec<AutoDeployedFrame>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShelfState {
pub origin_x: f64,
pub origin_y: f64,
pub cursor_x: f64,
pub cursor_y: f64,
pub row_height: f64,
pub row_max_width: f64,
pub gutter: f64,
}
impl ShelfState {
pub fn to_allocator(&self) -> crate::batbelt::miro::layout::ShelfAllocator {
crate::batbelt::miro::layout::ShelfAllocator {
origin_x: self.origin_x,
origin_y: self.origin_y,
cursor_x: self.cursor_x,
cursor_y: self.cursor_y,
row_height: self.row_height,
row_max_width: self.row_max_width,
gutter: self.gutter,
}
}
}
impl From<&crate::batbelt::miro::layout::ShelfAllocator> for ShelfState {
fn from(allocator: &crate::batbelt::miro::layout::ShelfAllocator) -> Self {
Self {
origin_x: allocator.origin_x,
origin_y: allocator.origin_y,
cursor_x: allocator.cursor_x,
cursor_y: allocator.cursor_y,
row_height: allocator.row_height,
row_max_width: allocator.row_max_width,
gutter: allocator.gutter,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutoDeployedFrame {
pub entry_point: String,
pub frame_id: String,
pub frame_url: String,
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
pub images: Vec<(String, String)>,
pub connector_ids: Vec<String>,
#[serde(default)]
pub marker_ids: Vec<String>,
#[serde(default)]
pub border_ids: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MiroFrameRef {
pub entry_point_name: String,
pub frame_id: String,
pub frame_url: String,
#[serde(default)]
pub images_deployed: bool,
#[serde(default)]
pub entry_point_image_id: String,
#[serde(default)]
pub validations_image_id: String,
#[serde(default)]
pub dependency_image_ids: Vec<String>,
}
impl EvmBatMetadata {
pub fn read_metadata() -> EvmMetadataResult<Self> {
let content = fs::read_to_string(EVM_METADATA_FILE).map_err(|e| {
Report::new(EvmMetadataError)
.attach_printable(format!("Cannot read {}: {}", EVM_METADATA_FILE, e))
})?;
let metadata: Self = serde_json::from_str(&content).map_err(|e| {
Report::new(EvmMetadataError)
.attach_printable(format!("Cannot parse {}: {}", EVM_METADATA_FILE, e))
})?;
Ok(metadata)
}
pub fn save_metadata(&self) -> EvmMetadataResult<()> {
let content = serde_json::to_string_pretty(self).map_err(|e| {
Report::new(EvmMetadataError)
.attach_printable(format!("Cannot serialize metadata: {}", e))
})?;
fs::write(EVM_METADATA_FILE, content).map_err(|e| {
Report::new(EvmMetadataError)
.attach_printable(format!("Cannot write {}: {}", EVM_METADATA_FILE, e))
})?;
Ok(())
}
pub fn create_empty() -> EvmMetadataResult<()> {
let metadata = Self::default();
metadata.save_metadata()
}
pub fn update_metadata<F>(f: F) -> EvmMetadataResult<()>
where
F: FnOnce(&mut EvmBatMetadata),
{
let mut metadata = Self::read_metadata()?;
f(&mut metadata);
metadata.save_metadata()
}
pub fn get_miro_frame_by_ep_name(&self, ep_name: &str) -> Option<&MiroFrameRef> {
self.miro
.frames
.iter()
.find(|f| f.entry_point_name == ep_name)
}
pub fn get_contract_by_name(&self, name: &str) -> Option<&ContractMetadata> {
self.contracts.iter().find(|c| c.name == name)
}
pub fn get_function_by_id(&self, id: &str) -> Option<&FunctionMetadata> {
self.contracts
.iter()
.flat_map(|c| c.functions.iter())
.find(|f| f.metadata_id == id)
}
pub fn get_entry_point_by_name(&self, name: &str) -> Option<&EntryPointMetadata> {
self.entry_points.iter().find(|ep| ep.name == name)
}
pub fn from_contracts(contracts: Vec<EvmContract>, file_items: Vec<EvmFileItem>) -> Self {
let mut metadata = Self::default();
if let Ok(content) = fs::read_to_string(EVM_METADATA_FILE) {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
if let Some(miro_val) = json.get("miro") {
if let Ok(miro) = serde_json::from_value::<MiroMetadataRef>(miro_val.clone()) {
metadata.miro = miro;
}
}
if let Some(res_val) = json.get("resolutions") {
if let Ok(res) = serde_json::from_value::<
std::collections::HashMap<String, String>,
>(res_val.clone())
{
metadata.resolutions = res;
}
}
}
}
metadata.file_items = file_items;
let mut own_state_vars: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
let mut contract_bases: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
for c in &contracts {
let names: Vec<String> = c
.storage_variables
.iter()
.filter(|v| !v.is_constant && !v.is_immutable)
.map(|v| v.name.clone())
.collect();
own_state_vars.insert(c.name.clone(), names);
contract_bases.insert(c.name.clone(), c.base_contracts.clone());
}
let contract_names: std::collections::HashSet<String> =
contracts.iter().map(|c| c.name.clone()).collect();
let is_interface: std::collections::HashSet<String> = contracts
.iter()
.filter(|c| c.contract_type == EvmContractType::Interface)
.map(|c| c.name.clone())
.collect();
let external_contracts: std::collections::HashSet<String> = contracts
.iter()
.filter(|c| c.external)
.map(|c| c.name.clone())
.collect();
let mut impl_map: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
let mut method_map: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
let mut own_var_types: std::collections::HashMap<
String,
std::collections::HashMap<String, String>,
> = std::collections::HashMap::new();
let mut own_fn_returns: std::collections::HashMap<
String,
std::collections::HashMap<String, String>,
> = std::collections::HashMap::new();
for c in &contracts {
for b in &c.base_contracts {
impl_map.entry(b.clone()).or_default().push(c.name.clone());
}
if c.contract_type != EvmContractType::Interface {
for f in &c.functions {
method_map
.entry(f.name.clone())
.or_default()
.push(c.name.clone());
}
}
let vt: std::collections::HashMap<String, String> = c
.storage_variables
.iter()
.map(|v| (v.name.clone(), v.type_name.clone()))
.collect();
own_var_types.insert(c.name.clone(), vt);
let fr: std::collections::HashMap<String, String> = c
.functions
.iter()
.filter(|f| f.returns.len() == 1)
.map(|f| (f.name.clone(), f.returns[0].type_name.clone()))
.collect();
own_fn_returns.insert(c.name.clone(), fr);
}
let mut struct_fields: std::collections::HashMap<
String,
std::collections::HashMap<String, String>,
> = std::collections::HashMap::new();
for c in &contracts {
for s in &c.structs {
let fields: std::collections::HashMap<String, String> = s
.fields
.iter()
.map(|f| (f.name.clone(), f.type_name.clone()))
.collect();
struct_fields.insert(s.name.clone(), fields);
}
}
let mut all_deps: Vec<FunctionDependency> = Vec::new();
for contract in &contracts {
let contract_id = format!("{}_{}", contract.file_path, contract.name);
let mut var_types: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
{
let mut seen: std::collections::HashSet<String> =
std::collections::HashSet::new();
let mut stack = vec![contract.name.clone()];
while let Some(name) = stack.pop() {
if !seen.insert(name.clone()) {
continue;
}
if let Some(vt) = own_var_types.get(&name) {
for (k, v) in vt {
var_types.entry(k.clone()).or_insert_with(|| v.clone());
}
}
if let Some(bases) = contract_bases.get(&name) {
stack.extend(bases.iter().cloned());
}
}
}
let mut fn_returns: std::collections::HashMap<String, String> =
std::collections::HashMap::new();
{
let mut seen: std::collections::HashSet<String> =
std::collections::HashSet::new();
let mut stack = vec![contract.name.clone()];
while let Some(name) = stack.pop() {
if !seen.insert(name.clone()) {
continue;
}
if let Some(fr) = own_fn_returns.get(&name) {
for (k, v) in fr {
fn_returns.entry(k.clone()).or_insert_with(|| v.clone());
}
}
if let Some(bases) = contract_bases.get(&name) {
stack.extend(bases.iter().cloned());
}
}
}
let mut state_vars: Vec<String> = Vec::new();
let mut seen_contracts: std::collections::HashSet<String> =
std::collections::HashSet::new();
resolve_state_vars(
&contract.name,
&own_state_vars,
&contract_bases,
&mut seen_contracts,
&mut state_vars,
);
let mut functions: Vec<FunctionMetadata> = Vec::new();
for f in &contract.functions {
let func_id = format!("{}_{}_{}", contract.file_path, contract.name, f.name);
let storage_params: Vec<String> = f
.params
.iter()
.filter(|p| p.storage_location.as_deref() == Some("storage"))
.map(|p| p.name.clone())
.collect();
let analysis = crate::batbelt::evm::parser::call_resolver::analyze_body(
&f.body_source,
&state_vars,
&storage_params,
);
let storage_writes = analysis.storage_writes;
let storage_write_sites = analysis
.storage_write_sites
.iter()
.map(|(name, src_line)| StorageWriteSite {
name: name.clone(),
line: f.line + src_line.saturating_sub(1),
})
.collect();
let mut local_var_types = var_types.clone();
for (n, t) in &analysis.local_types {
local_var_types.insert(n.clone(), t.clone());
}
let (unresolved_calls, unknown_external_calls) = compute_unresolved_calls(
&analysis.call_targets,
&local_var_types,
&struct_fields,
&fn_returns,
&contract_names,
&is_interface,
&external_contracts,
&impl_map,
&method_map,
);
all_deps.push(FunctionDependency {
function_metadata_id: func_id.clone(),
callees: analysis.call_names,
});
functions.push(FunctionMetadata {
metadata_id: func_id,
name: f.name.clone(),
contract_name: contract.name.clone(),
visibility: f.visibility.clone(),
mutability: f.mutability.clone(),
modifiers: f.modifiers.clone(),
params: f.params.clone(),
returns: f.returns.clone(),
line: f.line,
end_line: f.end_line,
is_constructor: f.is_constructor,
is_stub: is_stub_body(&f.body_source),
storage_writes,
storage_write_sites,
unresolved_calls,
unknown_external_calls,
});
}
let modifiers: Vec<crate::batbelt::evm::types::EvmModifierDef> = contract
.modifiers
.iter()
.map(|m| {
let mut m = m.clone();
let analysis = crate::batbelt::evm::parser::call_resolver::analyze_body(
&m.body_source,
&state_vars,
&[],
);
m.storage_write_sites = analysis
.storage_write_sites
.iter()
.map(|(name, src_line)| (name.clone(), m.line + src_line.saturating_sub(1)))
.collect();
m.storage_writes = analysis.storage_writes;
m
})
.collect();
let contract_metadata = ContractMetadata {
metadata_id: contract_id,
name: contract.name.clone(),
file_path: contract.file_path.clone(),
contract_type: contract.contract_type.clone(),
base_contracts: contract.base_contracts.clone(),
functions,
state_variables: contract.storage_variables.clone(),
events: contract.events.clone(),
modifiers,
line: contract.line,
external: contract.external,
};
metadata.contracts.push(contract_metadata);
}
metadata.function_dependencies = all_deps;
for contract in &metadata.contracts.clone() {
if contract.external {
continue;
}
if matches!(
contract.contract_type,
EvmContractType::Interface | EvmContractType::Library
) {
continue;
}
let ep_functions: Vec<_> = contract
.functions
.iter()
.filter(|f| {
matches!(
f.visibility,
EvmVisibility::External | EvmVisibility::Public
) && !f.is_constructor
})
.collect();
let mut name_counts: std::collections::HashMap<String, usize> =
std::collections::HashMap::new();
for func in &ep_functions {
*name_counts.entry(func.name.clone()).or_insert(0) += 1;
}
for func in &ep_functions {
let ep_name = if name_counts.get(&func.name).copied().unwrap_or(0) > 1 {
let param_types = func
.params
.iter()
.map(|p| p.type_name.clone())
.collect::<Vec<_>>()
.join(",");
format!("{}.{}({})", contract.name, func.name, param_types)
} else {
format!("{}.{}", contract.name, func.name)
};
let ep = EntryPointMetadata {
metadata_id: format!("ep_{}", func.metadata_id),
name: ep_name,
contract_name: contract.name.clone(),
function_metadata_id: func.metadata_id.clone(),
access_control: detect_access_control(&func.modifiers),
storage_reads: vec![],
storage_writes: func.storage_writes.clone(),
external_calls: vec![],
events_emitted: vec![],
modifiers: func.modifiers.clone(),
dependencies: vec![],
};
metadata.entry_points.push(ep);
}
}
metadata
}
}
fn is_stub_body(body_source: &str) -> bool {
if body_source.trim().is_empty() {
return true;
}
if let (Some(open), Some(close)) = (body_source.find('{'), body_source.rfind('}')) {
if open < close && body_source[open + 1..close].trim().is_empty() {
return true;
}
}
false
}
fn resolve_state_vars(
name: &str,
own: &std::collections::HashMap<String, Vec<String>>,
bases: &std::collections::HashMap<String, Vec<String>>,
seen: &mut std::collections::HashSet<String>,
out: &mut Vec<String>,
) {
if !seen.insert(name.to_string()) {
return;
}
if let Some(vars) = own.get(name) {
out.extend(vars.iter().cloned());
}
if let Some(base_names) = bases.get(name) {
for base in base_names {
resolve_state_vars(base, own, bases, seen, out);
}
}
}
pub fn prune_unresolved_noise(metadata: &mut EvmBatMetadata) {
use std::collections::{HashMap, HashSet};
let contract_names: HashSet<String> =
metadata.contracts.iter().map(|c| c.name.clone()).collect();
let contract_file: HashMap<String, String> = metadata
.contracts
.iter()
.map(|c| (c.name.clone(), c.file_path.clone()))
.collect();
let mut method_map: HashMap<String, Vec<String>> = HashMap::new();
let mut bases: HashMap<String, Vec<String>> = HashMap::new();
let mut caller_contract: HashMap<String, String> = HashMap::new();
for c in &metadata.contracts {
bases.insert(c.name.clone(), c.base_contracts.clone());
for f in &c.functions {
method_map
.entry(f.name.clone())
.or_default()
.push(c.name.clone());
caller_contract.insert(f.metadata_id.clone(), c.name.clone());
}
}
let fid = |contract: &str, method: &str| -> Option<String> {
contract_file
.get(contract)
.map(|fp| format!("{fp}_{contract}_{method}"))
};
let mut edges: HashMap<String, Vec<String>> = HashMap::new();
for dep in &metadata.function_dependencies {
let Some(cname) = caller_contract.get(&dep.function_metadata_id) else {
continue;
};
let out = edges.entry(dep.function_metadata_id.clone()).or_default();
for callee in &dep.callees {
if let Some((tgt, method)) = callee.split_once('.') {
if contract_names.contains(tgt) {
if let Some(id) = fid(tgt, method) {
out.push(id);
}
}
} else {
let mut chain = vec![cname.clone()];
if let Some(bs) = bases.get(cname) {
chain.extend(bs.iter().cloned());
}
for cand in chain {
if method_map.get(callee).is_some_and(|v| v.contains(&cand)) {
if let Some(id) = fid(&cand, callee) {
out.push(id);
}
break;
}
}
}
}
}
let mut taint: HashSet<String> = HashSet::new();
for c in &metadata.contracts {
for f in &c.functions {
if !f.storage_writes.is_empty() {
taint.insert(f.metadata_id.clone());
}
let out = edges.entry(f.metadata_id.clone()).or_default();
for u in &f.unresolved_calls {
for cand in &u.candidates {
if let Some(id) = fid(cand, &u.method) {
out.push(id);
}
}
}
}
}
let mut rev: HashMap<String, Vec<String>> = HashMap::new();
for (caller, outs) in &edges {
for o in outs {
rev.entry(o.clone()).or_default().push(caller.clone());
}
}
let mut work: Vec<String> = taint.iter().cloned().collect();
while let Some(t) = work.pop() {
if let Some(callers) = rev.get(&t) {
for caller in callers.clone() {
if taint.insert(caller.clone()) {
work.push(caller);
}
}
}
}
for c in &mut metadata.contracts {
for f in &mut c.functions {
if f.unresolved_calls.is_empty() {
continue;
}
f.unresolved_calls.retain(|u| {
u.candidates
.iter()
.any(|cand| fid(cand, &u.method).is_some_and(|id| taint.contains(&id)))
});
}
}
let mut write_sites: HashMap<String, Vec<String>> = HashMap::new();
for c in &metadata.contracts {
for f in &c.functions {
for w in &f.storage_writes {
write_sites
.entry(w.clone())
.or_default()
.push(format!("{}.{}", c.name, f.name));
}
}
}
for c in &mut metadata.contracts {
for f in &mut c.functions {
for u in &mut f.unresolved_calls {
if let Some(sites) = write_sites.get(&u.receiver) {
u.assigned_in = sites.clone();
u.assigned_in.sort();
u.assigned_in.dedup();
}
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn compute_unresolved_calls(
targets: &[(String, String)],
var_types: &std::collections::HashMap<String, String>,
struct_fields: &std::collections::HashMap<String, std::collections::HashMap<String, String>>,
fn_returns: &std::collections::HashMap<String, String>,
contract_names: &std::collections::HashSet<String>,
is_interface: &std::collections::HashSet<String>,
external_contracts: &std::collections::HashSet<String>,
impl_map: &std::collections::HashMap<String, Vec<String>>,
method_map: &std::collections::HashMap<String, Vec<String>>,
) -> (Vec<UnresolvedCall>, Vec<ExternalUnknownCall>) {
let mut out: Vec<UnresolvedCall> = Vec::new();
let mut external: Vec<ExternalUnknownCall> = Vec::new();
let mut seen: std::collections::HashSet<(String, String)> = std::collections::HashSet::new();
let mut seen_ext: std::collections::HashSet<(String, String)> = std::collections::HashSet::new();
for (receiver, method) in targets {
let (receiver, method) = (receiver.clone(), method.clone());
if receiver == "this" || receiver == "super" || contract_names.contains(&receiver) {
continue;
}
let inferred_type = receiver_type(&receiver, var_types, struct_fields, fn_returns);
let typed_impls: Vec<String> = if inferred_type.is_empty() {
Vec::new()
} else {
impl_map
.get(&inferred_type)
.map(|impls| {
impls
.iter()
.filter(|c| {
!is_interface.contains(*c)
&& !external_contracts.contains(*c)
&& method_map.get(&method).is_some_and(|v| v.contains(c))
})
.cloned()
.collect()
})
.unwrap_or_default()
};
if typed_impls.len() == 1 {
continue;
}
if typed_impls.is_empty()
&& receiver.ends_with("()")
&& is_interface.contains(&inferred_type)
{
if seen_ext.insert((receiver.clone(), method.clone())) {
external.push(ExternalUnknownCall {
receiver,
method,
inferred_type,
});
}
continue;
}
let mut candidates: Vec<String> = if !typed_impls.is_empty() {
typed_impls
} else {
method_map.get(&method).cloned().unwrap_or_default()
};
candidates.retain(|c| !is_interface.contains(c) && !external_contracts.contains(c));
candidates.sort();
candidates.dedup();
if candidates.is_empty() {
if is_interface.contains(&inferred_type)
&& seen_ext.insert((receiver.clone(), method.clone()))
{
external.push(ExternalUnknownCall {
receiver,
method,
inferred_type,
});
}
continue;
}
if seen.insert((receiver.clone(), method.clone())) {
out.push(UnresolvedCall {
receiver,
method,
inferred_type,
candidates,
assigned_in: Vec::new(),
});
}
}
(out, external)
}
fn receiver_type(
receiver: &str,
var_types: &std::collections::HashMap<String, String>,
struct_fields: &std::collections::HashMap<String, std::collections::HashMap<String, String>>,
fn_returns: &std::collections::HashMap<String, String>,
) -> String {
let unqualify = |t: &str| t.rsplit('.').next().unwrap_or(t).to_string();
let mut segments = receiver.split('.');
let base = match segments.next() {
Some(b) => b.trim_end_matches("[]"),
None => return String::new(),
};
let mut current: Option<String> = if let Some(callee) = base.strip_suffix("()") {
if callee
.chars()
.next()
.is_some_and(|c| c.is_ascii_uppercase())
&& callee.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
{
Some(callee.to_string())
} else {
fn_returns.get(callee).cloned()
}
} else if base.contains('(') {
None
} else {
var_types.get(base).cloned()
};
for seg in segments {
let field = seg.trim_end_matches("[]");
current = current.and_then(|t| {
struct_fields
.get(&unqualify(&t))
.and_then(|fields| fields.get(field))
.cloned()
});
if current.is_none() {
return String::new();
}
}
current.map(|t| unqualify(&t)).unwrap_or_default()
}
fn detect_access_control(modifiers: &[String]) -> Vec<AccessControlType> {
let mut result = Vec::new();
for modifier in modifiers {
match modifier.as_str() {
"onlyOwner" => result.push(AccessControlType::OnlyOwner),
"onlyRole" => result.push(AccessControlType::RoleBased {
role: "DEFAULT_ADMIN_ROLE".to_string(),
}),
other => {
if other.starts_with("only") {
result.push(AccessControlType::CustomModifier {
name: other.to_string(),
});
}
}
}
}
if result.is_empty() {
result.push(AccessControlType::None);
}
result
}