diff --git a/packages/rules/src/stellar/linting/gas_optimization_rules.rs b/packages/rules/src/stellar/linting/gas_optimization_rules.rs index dd412bd..59ef2fd 100644 --- a/packages/rules/src/stellar/linting/gas_optimization_rules.rs +++ b/packages/rules/src/stellar/linting/gas_optimization_rules.rs @@ -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> { + 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::>().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; @@ -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) { + 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()); + } +} diff --git a/packages/rules/src/stellar/linting/mod.rs b/packages/rules/src/stellar/linting/mod.rs index af55a59..48486bc 100644 --- a/packages/rules/src/stellar/linting/mod.rs +++ b/packages/rules/src/stellar/linting/mod.rs @@ -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 }