Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
96 changes: 96 additions & 0 deletions packages/rules/src/stellar/linting/gas_optimization_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,56 @@ impl SorobanLintRule for StorageReadRule {
}
}

/// Rule to detect gas-heavy Soroban Map iteration
pub struct MapIterationRule;

impl SorobanLintRule for MapIterationRule {
fn id(&self) -> &'static str {
"soroban-map-iteration"
}

fn name(&self) -> &'static str {
"Soroban Map Iteration"
}

fn description(&self) -> &'static str {
"Detects heavy iteration over Soroban Map collections that can be expensive for large datasets"
}

fn severity(&self) -> ViolationSeverity {
ViolationSeverity::High
}

fn check(&self, source: &str, file_path: &str) -> Option<Vec<RuleViolation>> {
let mut violations = Vec::new();
let lines: Vec<&str> = source.lines().collect();

for (i, line) in lines.iter().enumerate() {
if (line.contains("for ") || line.contains("while ")) &&
(line.contains(".iter()") || line.contains(".keys(") || line.contains(".values(") || line.contains(".entries(") || line.contains(".range(")) {
let window = lines.iter().skip(i.saturating_sub(8)).take(20).collect::<Vec<_>>().join("\n");
if window.contains("Map<") || window.contains("Map::") || window.contains(": Map") {
violations.push(RuleViolation {
rule_name: self.id().to_string(),
description: "Detected potentially expensive iteration over a Soroban Map".to_string(),
suggestion: "Use pagination or chunked iteration rather than iterating over the entire Map in one call".to_string(),
line_number: i + 1,
column_number: 0,
variable_name: file_path.to_string(),
severity: self.severity(),
});
}
}
}

if violations.is_empty() {
None
} else {
Some(violations)
}
}
}

/// Rule to check for efficient event emission patterns
pub struct EventEmissionRule;

Expand Down Expand Up @@ -136,3 +186,49 @@ impl SorobanLintRule for EventEmissionRule {
}
}
}

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

#[test]
fn test_map_iteration_rule_detects_map_loop() {
let source = r#"
use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, Map};

#[contractimpl]
impl MyContract {
pub fn scan_map(&self, env: Env, data: Map<Address, u64>) {
for (key, value) in data.iter() {
let _ = key;
let _ = value;
}
}
}
"#;

let violations = MapIterationRule.check(&MapIterationRule, source, "test.rs");
assert!(violations.is_some());
let violations = violations.unwrap();
assert!(violations.iter().any(|v| v.rule_name == "soroban-map-iteration"));
}

#[test]
fn test_map_iteration_rule_ignores_non_map_loops() {
let source = r#"
use soroban_sdk::{contract, contractimpl, contracttype, Address};

#[contractimpl]
impl MyContract {
pub fn count(&self) {
for i in 0..10 {
let _ = i;
}
}
}
"#;

let violations = MapIterationRule.check(&MapIterationRule, source, "test.rs");
assert!(violations.is_none());
}
}
1 change: 1 addition & 0 deletions packages/rules/src/stellar/linting/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ impl SorobanLinter {
rules.push(Box::new(stellar_sdk_rules::SdkUsageRule));
rules.push(Box::new(stellar_sdk_rules::AddressValidationRule));
rules.push(Box::new(gas_optimization_rules::StorageReadRule));
rules.push(Box::new(gas_optimization_rules::MapIterationRule));
rules.push(Box::new(gas_optimization_rules::EventEmissionRule));

Self { rules }
Expand Down
Loading