use crate::core::{Finding, ForgeGuardError, Severity};
pub fn analyze_gas(
files: &[std::path::PathBuf],
_config: &crate::core::ProjectConfig,
) -> Result<Vec<Finding>, ForgeGuardError> {
let mut findings = Vec::new();
for file in files {
let content = std::fs::read_to_string(file)?;
let file_name = file.to_string_lossy().to_string();
let lines: Vec<&str> = content.lines().collect();
findings.extend(check_unnecessary_storage(&file_name, &lines));
findings.extend(check_loop_gas(&file_name, &lines));
findings.extend(check_inefficient_data_types(&file_name, &lines));
}
Ok(findings)
}
fn check_unnecessary_storage(file: &str, lines: &[&str]) -> Vec<Finding> {
let mut findings = Vec::new();
let mut storage_accesses = 0;
for line in lines.iter() {
let trimmed = line.trim();
if trimmed.starts_with("function ") {
storage_accesses = 0;
} else if trimmed.starts_with('}') {
} else if trimmed.contains("storage.")
|| (trimmed.contains('.')
&& !trimmed.contains("memory")
&& !trimmed.contains("calldata"))
{
storage_accesses += 1;
}
}
if storage_accesses > 5 {
findings.push(
Finding::builder()
.id("FA-GAS-001")
.title("Excessive Storage Reads")
.description(
"Function reads from storage multiple times — cache in local variables",
)
.severity(Severity::Medium)
.file(file)
.code("Multiple storage reads detected — consider caching in memory")
.recommendation("Cache storage variables in memory at the start of the function")
.category("Gas")
.build(),
);
}
findings
}
fn check_loop_gas(file: &str, lines: &[&str]) -> Vec<Finding> {
let mut findings = Vec::new();
for (i, line) in lines.iter().enumerate() {
let trimmed = line.trim();
if trimmed.contains("for (") && trimmed.contains(".length") {
findings.push(
Finding::builder()
.id(&format!("FA-GAS-{}", i + 1))
.title("Loop Cache Array Length")
.description("Array length fetched on each iteration — cache before loop")
.severity(Severity::Low)
.file(file)
.location(i + 1, 0)
.code(trimmed)
.recommendation(
"Cache the array length before the loop: uint256 len = array.length",
)
.category("Gas")
.build(),
);
if findings.len() >= 3 {
break;
}
}
}
findings
}
fn check_inefficient_data_types(_file: &str, _lines: &[&str]) -> Vec<Finding> {
Vec::new()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_analyze_gas_empty_files() {
let files: Vec<std::path::PathBuf> = vec![];
let config = crate::core::ProjectConfig::default();
let result = analyze_gas(&files, &config).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_check_unnecessary_storage() {
let lines = vec![
"function doSomething() public {",
" storage.balance = 100;",
" storage.owner = msg.sender;",
" storage.count = 5;",
" storage.locked = true;",
" storage.data = 'test';",
" storage.meta = 'xyz';",
"}",
];
let findings = check_unnecessary_storage("test.sol", &lines);
assert!(!findings.is_empty());
}
#[test]
fn test_check_loop_gas() {
let lines = vec![
"function process() public {",
" for (uint i = 0; i < users.length; i++) {",
" // process each user",
" }",
"}",
];
let findings = check_loop_gas("test.sol", &lines);
assert!(!findings.is_empty());
assert!(findings.iter().any(|f| f.title.contains("Loop")));
}
}