Skip to content

Commit e2edf58

Browse files
authored
Merge pull request #39 from JerryIdoko/feat/test-harness-coverage
feat: Configure Test Harness & Coverage
2 parents 0e401d9 + cb42dfe commit e2edf58

11 files changed

Lines changed: 168 additions & 202 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,4 @@ cache/
4949
artifacts/
5050
out/
5151
.soroban/
52+
tarpaulin-report.html

Cargo.toml

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,3 @@ edition = "2021"
1111
authors = ["GasGuard Team"]
1212
license = "MIT"
1313
description = "Automated Optimization Suite for Stellar Soroban Contracts"
14-
15-
[workspace.dependencies]
16-
# Async runtime
17-
tokio = { version = "1.0", features = ["full"] }
18-
19-
# CLI and argument parsing
20-
clap = { version = "4.0", features = ["derive"] }
21-
22-
# Serialization
23-
serde = { version = "1.0", features = ["derive"] }
24-
serde_json = "1.0"
25-
26-
# Error handling
27-
anyhow = "1.0"
28-
thiserror = "1.0"
29-
30-
# Parsing and AST
31-
syn = { version = "2.0", features = ["full", "extra-traits"] }
32-
quote = "1.0"
33-
proc-macro2 = "1.0"
34-
35-
# Utilities
36-
colored = "2.0"
37-
walkdir = "2.0"
38-
chrono = { version = "0.4", features = ["serde"] }

libs/engine/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@ anyhow = "1.0"
1313
colored = "2.0"
1414
chrono = { version = "0.4", features = ["serde"] }
1515
walkdir = "2.0"
16+
17+
[dev-dependencies]
18+
mockall = "0.14.0"
19+
rstest = "0.26.1"

libs/engine/src/analyzer.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ impl ScanAnalyzer {
6363
let mut estimated_savings_kb = 0.0;
6464

6565
for violation in violations {
66-
if violation.rule_name == "unused-state-variables" {
66+
if violation.rule_name == "unused-state-variable" {
6767
unused_vars += 1;
6868
// Estimate average storage cost per unused variable
6969
// This is a rough estimate - actual costs vary by type
@@ -92,6 +92,9 @@ impl ScanAnalyzer {
9292
for violation in violations {
9393
match violation.severity {
9494
ViolationSeverity::Error => errors.push(violation),
95+
// Map High and Medium to Warnings for now
96+
ViolationSeverity::High => warnings.push(violation),
97+
ViolationSeverity::Medium => warnings.push(violation),
9598
ViolationSeverity::Warning => warnings.push(violation),
9699
ViolationSeverity::Info => info.push(violation),
97100
}
@@ -136,4 +139,4 @@ impl fmt::Display for StorageSavings {
136139
self.monthly_ledger_rent_savings
137140
)
138141
}
139-
}
142+
}

packages/rules/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ serde = { version = "1.0", features = ["derive"] }
1111
serde_json = "1.0"
1212
thiserror = "1.0"
1313
regex = "1.10"
14+
15+
[dev-dependencies]
16+
mockall = "0.14.0"
17+
rstest = "0.26.1"

packages/rules/src/lib.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,23 @@ pub mod unused_state_variables;
33
pub mod vyper;
44
pub mod soroban;
55

6-
pub use rule_engine::*;
7-
pub use unused_state_variables::*;
8-
pub use vyper::*;
9-
pub use soroban::*;
6+
// Explicitly export core types to avoid ambiguity
7+
pub use rule_engine::{Rule, RuleEngine, RuleViolation, ViolationSeverity, extract_struct_fields, find_variable_usage};
8+
pub use unused_state_variables::UnusedStateVariablesRule;
9+
10+
// Export Soroban types specifically
11+
pub use soroban::{
12+
SorobanAnalyzer,
13+
SorobanContract,
14+
SorobanParser,
15+
SorobanResult,
16+
SorobanRuleEngine,
17+
SorobanStruct,
18+
SorobanImpl,
19+
SorobanFunction,
20+
SorobanField,
21+
SorobanParam
22+
};
23+
24+
// Export Vyper types (keeping glob here is fine if Vyper module is clean, but let's be safe)
25+
pub use vyper::*;

packages/rules/src/rule_engine.rs

Lines changed: 18 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
//! Rule Engine Core
2+
//!
3+
//! Provides the fundamental traits and AST traversal logic for the rules engine.
4+
15
use serde::{Deserialize, Serialize};
26
use std::collections::HashSet;
37
use syn::{Expr, Item, ItemImpl, ItemStruct, Member, Pat};
48

5-
// Re-export from soroban module
6-
pub use crate::soroban::{SorobanRuleEngine, SorobanContract, SorobanParser, SorobanResult};
7-
89
#[derive(Debug, Clone, Serialize, Deserialize)]
910
pub struct RuleViolation {
1011
pub rule_name: String,
@@ -19,6 +20,8 @@ pub struct RuleViolation {
1920
#[derive(Debug, Clone, Serialize, Deserialize)]
2021
pub enum ViolationSeverity {
2122
Error,
23+
High,
24+
Medium,
2225
Warning,
2326
Info,
2427
}
@@ -100,7 +103,6 @@ fn extract_variables_from_expr(expr: &Expr, used_vars: &mut HashSet<String>) {
100103
Expr::Path(path) => {
101104
if let Some(segment) = path.path.segments.last() {
102105
let ident = segment.ident.to_string();
103-
// Skip common Rust keywords and types
104106
if !is_rust_keyword(&ident) {
105107
used_vars.insert(ident);
106108
}
@@ -112,6 +114,10 @@ fn extract_variables_from_expr(expr: &Expr, used_vars: &mut HashSet<String>) {
112114
used_vars.insert(ident.to_string());
113115
}
114116
}
117+
Expr::Assign(assign) => {
118+
extract_variables_from_expr(&assign.left, used_vars);
119+
extract_variables_from_expr(&assign.right, used_vars);
120+
}
115121
Expr::MethodCall(method_call) => {
116122
extract_variables_from_expr(&method_call.receiver, used_vars);
117123
for arg in &method_call.args {
@@ -211,64 +217,12 @@ fn extract_variables_from_pat(pat: &Pat, used_vars: &mut HashSet<String>) {
211217
fn is_rust_keyword(ident: &str) -> bool {
212218
matches!(
213219
ident,
214-
"self"
215-
| "Self"
216-
| "super"
217-
| "crate"
218-
| "mod"
219-
| "use"
220-
| "pub"
221-
| "const"
222-
| "static"
223-
| "let"
224-
| "fn"
225-
| "struct"
226-
| "enum"
227-
| "impl"
228-
| "trait"
229-
| "where"
230-
| "for"
231-
| "while"
232-
| "loop"
233-
| "if"
234-
| "else"
235-
| "match"
236-
| "break"
237-
| "continue"
238-
| "return"
239-
| "async"
240-
| "await"
241-
| "move"
242-
| "ref"
243-
| "mut"
244-
| "unsafe"
245-
| "extern"
246-
| "type"
247-
| "union"
248-
| "macro"
249-
| "Some"
250-
| "None"
251-
| "Ok"
252-
| "Err"
253-
| "Result"
254-
| "Option"
255-
| "Vec"
256-
| "String"
257-
| "str"
258-
| "bool"
259-
| "u8"
260-
| "u16"
261-
| "u32"
262-
| "u64"
263-
| "u128"
264-
| "i8"
265-
| "i16"
266-
| "i32"
267-
| "i64"
268-
| "i128"
269-
| "f32"
270-
| "f64"
271-
| "usize"
272-
| "isize"
220+
"self" | "Self" | "super" | "crate" | "mod" | "use" | "pub" | "const" | "static" | "let"
221+
| "fn" | "struct" | "enum" | "impl" | "trait" | "where" | "for" | "while" | "loop"
222+
| "if" | "else" | "match" | "break" | "continue" | "return" | "async" | "await"
223+
| "move" | "ref" | "mut" | "unsafe" | "extern" | "type" | "union" | "macro" | "Some"
224+
| "None" | "Ok" | "Err" | "Result" | "Option" | "Vec" | "String" | "str" | "bool"
225+
| "u8" | "u16" | "u32" | "u64" | "u128" | "i8" | "i16" | "i32" | "i64" | "i128"
226+
| "f32" | "f64" | "usize" | "isize"
273227
)
274-
}
228+
}

packages/rules/src/soroban/analyzer.rs

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ impl SorobanAnalyzer {
9292
description: "Contract should have a constructor function for initialization".to_string(),
9393
suggestion: "Add a 'new' function that initializes the contract state".to_string(),
9494
line_number: 1,
95+
column_number: 0,
9596
variable_name: contract.name.clone(),
9697
severity: ViolationSeverity::Warning,
9798
});
@@ -112,6 +113,7 @@ impl SorobanAnalyzer {
112113
description: "Consider adding an admin/owner field for access control".to_string(),
113114
suggestion: "Add an 'admin: Address' field to your contract state".to_string(),
114115
line_number: 1,
116+
column_number: 0,
115117
variable_name: contract.name.clone(),
116118
severity: ViolationSeverity::Info,
117119
});
@@ -128,14 +130,15 @@ impl SorobanAnalyzer {
128130
// Count occurrences of field name in the source (excluding struct definition)
129131
let field_usage_count = source.matches(&field.name).count();
130132

131-
// If field is only mentioned in its own declaration, it's likely unused
132-
// (this is a heuristic - a more sophisticated analysis would be needed for production)
133-
if field_usage_count <= 1 {
133+
// Heuristic: Definition + Initialization = 2 occurrences.
134+
// If it's <= 2, it's likely defined and initialized but never accessed again.
135+
if field_usage_count <= 2 {
134136
violations.push(RuleViolation {
135137
rule_name: "unused-state-variable".to_string(),
136138
description: format!("State variable '{}' appears to be unused", field.name),
137139
suggestion: format!("Remove unused state variable '{}' to save ledger storage", field.name),
138140
line_number: field.line_number,
141+
column_number: 0,
139142
variable_name: field.name.clone(),
140143
severity: ViolationSeverity::Warning,
141144
});
@@ -155,8 +158,9 @@ impl SorobanAnalyzer {
155158
violations.push(RuleViolation {
156159
rule_name: "inefficient-integer-type".to_string(),
157160
description: format!("Field '{}' uses {} which may be unnecessarily large", field.name, field.type_name),
158-
suggestion: format!("Consider using a smaller integer type like u64 or u32 if the range permits", field.name),
161+
suggestion: "Consider using a smaller integer type like u64 or u32 if the range permits".to_string(),
159162
line_number: field.line_number,
163+
column_number: 0,
160164
variable_name: field.name.clone(),
161165
severity: ViolationSeverity::Info,
162166
});
@@ -169,6 +173,7 @@ impl SorobanAnalyzer {
169173
description: format!("Field '{}' uses String type", field.name),
170174
suggestion: "Consider using Symbol for fixed string values to save storage costs".to_string(),
171175
line_number: field.line_number,
176+
column_number: 0,
172177
variable_name: field.name.clone(),
173178
severity: ViolationSeverity::Info,
174179
});
@@ -189,6 +194,7 @@ impl SorobanAnalyzer {
189194
description: format!("Field '{}' is private but contract fields should typically be public", field.name),
190195
suggestion: format!("Change '{}' to 'pub {}' to make it accessible", field.name, field.name),
191196
line_number: field.line_number,
197+
column_number: 0,
192198
variable_name: field.name.clone(),
193199
severity: ViolationSeverity::Warning,
194200
});
@@ -199,17 +205,18 @@ impl SorobanAnalyzer {
199205
}
200206

201207
/// Check for expensive operations in functions
202-
fn check_expensive_operations(function: &SorobanFunction, source: &str) -> Vec<RuleViolation> {
208+
fn check_expensive_operations(function: &SorobanFunction, _source: &str) -> Vec<RuleViolation> {
203209
let mut violations = Vec::new();
204210
let function_source = &function.raw_definition;
205211

206212
// Check for string operations
207213
if function_source.contains(".to_string()") || function_source.contains("String::from(") {
208214
violations.push(RuleViolation {
209215
rule_name: "expensive-string-operation".to_string(),
210-
description: "String operations can be expensive in terms of gas/storage",
211-
suggestion: "Consider using Symbol or Bytes for fixed data, or minimize string operations",
216+
description: "String operations can be expensive in terms of gas/storage".to_string(),
217+
suggestion: "Consider using Symbol or Bytes for fixed data, or minimize string operations".to_string(),
212218
line_number: function.line_number,
219+
column_number: 0,
213220
variable_name: function.name.clone(),
214221
severity: ViolationSeverity::Medium,
215222
});
@@ -219,9 +226,10 @@ impl SorobanAnalyzer {
219226
if function_source.contains("Vec::new()") && !function_source.contains("with_capacity") {
220227
violations.push(RuleViolation {
221228
rule_name: "vec-without-capacity".to_string(),
222-
description: "Vec::new() without capacity can cause multiple reallocations",
223-
suggestion: "Use Vec::with_capacity() to pre-allocate memory when size is known",
229+
description: "Vec::new() without capacity can cause multiple reallocations".to_string(),
230+
suggestion: "Use Vec::with_capacity() to pre-allocate memory when size is known".to_string(),
224231
line_number: function.line_number,
232+
column_number: 0,
225233
variable_name: function.name.clone(),
226234
severity: ViolationSeverity::Medium,
227235
});
@@ -231,9 +239,10 @@ impl SorobanAnalyzer {
231239
if function_source.contains(".clone()") {
232240
violations.push(RuleViolation {
233241
rule_name: "unnecessary-clone".to_string(),
234-
description: "Clone operations increase resource usage and gas costs",
235-
suggestion: "Avoid unnecessary cloning, use references where possible",
242+
description: "Clone operations increase resource usage and gas costs".to_string(),
243+
suggestion: "Avoid unnecessary cloning, use references where possible".to_string(),
236244
line_number: function.line_number,
245+
column_number: 0,
237246
variable_name: function.name.clone(),
238247
severity: ViolationSeverity::Medium,
239248
});
@@ -254,8 +263,9 @@ impl SorobanAnalyzer {
254263
violations.push(RuleViolation {
255264
rule_name: "missing-address-validation".to_string(),
256265
description: format!("Function '{}' takes Address parameter but may lack validation", function.name),
257-
suggestion: "Validate Address parameters to prevent invalid addresses",
266+
suggestion: "Validate Address parameters to prevent invalid addresses".to_string(),
258267
line_number: function.line_number,
268+
column_number: 0,
259269
variable_name: function.name.clone(),
260270
severity: ViolationSeverity::Medium,
261271
});
@@ -279,8 +289,9 @@ impl SorobanAnalyzer {
279289
violations.push(RuleViolation {
280290
rule_name: "missing-error-handling".to_string(),
281291
description: format!("Function '{}' should return Result for error handling", function.name),
282-
suggestion: "Return Result<(), Error> to properly handle operation failures",
292+
suggestion: "Return Result<(), Error> to properly handle operation failures".to_string(),
283293
line_number: function.line_number,
294+
column_number: 0,
284295
variable_name: function.name.clone(),
285296
severity: ViolationSeverity::Medium,
286297
});
@@ -291,7 +302,7 @@ impl SorobanAnalyzer {
291302
}
292303

293304
/// Check for unbounded loops
294-
fn check_unbounded_loops(implementation: &SorobanImpl, source: &str) -> Vec<RuleViolation> {
305+
fn check_unbounded_loops(implementation: &SorobanImpl, _source: &str) -> Vec<RuleViolation> {
295306
let mut violations = Vec::new();
296307

297308
for function in &implementation.functions {
@@ -304,8 +315,9 @@ impl SorobanAnalyzer {
304315
violations.push(RuleViolation {
305316
rule_name: "unbounded-loop".to_string(),
306317
description: format!("Function '{}' contains potentially unbounded loop", function.name),
307-
suggestion: "Ensure loops have clear termination conditions to prevent CPU limit exhaustion",
318+
suggestion: "Ensure loops have clear termination conditions to prevent CPU limit exhaustion".to_string(),
308319
line_number: function.line_number,
320+
column_number: 0,
309321
variable_name: function.name.clone(),
310322
severity: ViolationSeverity::High,
311323
});
@@ -316,7 +328,7 @@ impl SorobanAnalyzer {
316328
}
317329

318330
/// Check for inefficient storage patterns
319-
fn check_storage_patterns(implementation: &SorobanImpl, source: &str) -> Vec<RuleViolation> {
331+
fn check_storage_patterns(implementation: &SorobanImpl, _source: &str) -> Vec<RuleViolation> {
320332
let mut violations = Vec::new();
321333

322334
// Check for multiple storage reads of the same key
@@ -339,8 +351,9 @@ impl SorobanAnalyzer {
339351
violations.push(RuleViolation {
340352
rule_name: "inefficient-storage-access".to_string(),
341353
description: format!("Function '{}' performs {} storage reads - consider caching", function.name, read_count),
342-
suggestion: "Cache frequently accessed storage values in local variables",
354+
suggestion: "Cache frequently accessed storage values in local variables".to_string(),
343355
line_number: function.line_number,
356+
column_number: 0,
344357
variable_name: function.name.clone(),
345358
severity: ViolationSeverity::Medium,
346359
});

0 commit comments

Comments
 (0)