From 985982926ea380336de52f9ccffb57e715a5bb0a Mon Sep 17 00:00:00 2001 From: Sadeequ <70214653+Sadeequ@users.noreply.github.com> Date: Sat, 29 Aug 2026 04:46:59 +0000 Subject: [PATCH] max impl and Detection set complete --- packages/rules/src/lib.rs | 7 +- packages/rules/src/soroban/mod.rs | 6 + packages/rules/src/soroban/rule_engine.rs | 871 +++++++++++++++++++++- 3 files changed, 880 insertions(+), 4 deletions(-) diff --git a/packages/rules/src/lib.rs b/packages/rules/src/lib.rs index 5ae18df..247f778 100644 --- a/packages/rules/src/lib.rs +++ b/packages/rules/src/lib.rs @@ -27,9 +27,10 @@ pub use unused_state_variables::UnusedStateVariablesRule; // Export Soroban types specifically pub use soroban::{ EventEmissionCostRule, InefficientBytesAllocationRule, InefficientErrorConstructionRule, - MemoryAllocationRule, SorobanAnalyzer, SorobanContract, SorobanField, SorobanFunction, - SorobanImpl, SorobanParam, SorobanParser, SorobanResult, SorobanRuleEngine, SorobanStruct, - UnnecessaryCloningRule, + MemoryAllocationRule, SorobanAnalyzer, SorobanContract, SorobanDeadCodeRule, + SorobanDeepNestingRule, SorobanField, SorobanFunction, SorobanFunctionComplexityRule, + SorobanImpl, SorobanParam, SorobanParser, SorobanRepeatedComputationsRule, SorobanResult, + SorobanRuleEngine, SorobanStruct, UnnecessaryCloningRule, }; // Export Vyper types (keeping glob here is fine if Vyper module is clean, but let's be safe) diff --git a/packages/rules/src/soroban/mod.rs b/packages/rules/src/soroban/mod.rs index d2190c7..cfb3fad 100644 --- a/packages/rules/src/soroban/mod.rs +++ b/packages/rules/src/soroban/mod.rs @@ -26,6 +26,12 @@ pub use parser::*; pub use rule_engine::*; pub use unnecessary_cloning::UnnecessaryCloningRule; +// New Soroban analysis rules +pub use rule_engine::{ + SorobanDeadCodeRule, SorobanDeepNestingRule, SorobanFunctionComplexityRule, + SorobanRepeatedComputationsRule, +}; + /// Represents a Soroban contract structure #[derive(Debug, Clone, PartialEq)] pub struct SorobanContract { diff --git a/packages/rules/src/soroban/rule_engine.rs b/packages/rules/src/soroban/rule_engine.rs index c888fce..634c178 100644 --- a/packages/rules/src/soroban/rule_engine.rs +++ b/packages/rules/src/soroban/rule_engine.rs @@ -70,7 +70,11 @@ impl SorobanRuleEngine { .add_rule(RedundantEventEmissionsRule::default()) // #779 .add_rule(AuthorizationCostRule::default()) // #780 .add_rule(ResourceBudgetEstimatorRule::default()) // #781 - .add_rule(OptimizationPriorityRule::default()); // #782 + .add_rule(OptimizationPriorityRule::default()) // #782 + .add_rule(SorobanFunctionComplexityRule::default()) // #783 + .add_rule(SorobanDeepNestingRule::default()) // #784 + .add_rule(SorobanRepeatedComputationsRule::default()) // #785 + .add_rule(SorobanDeadCodeRule::default()); // #786 } /// Analyze Soroban contract source code @@ -1061,6 +1065,625 @@ impl SorobanRule for OptimizationPriorityRule { } } +// ── Issue #783: Soroban Function Complexity Analyzer ───────────────────────── + +/// Calculates cyclomatic complexity for Soroban functions and flags those +/// exceeding a configurable threshold. +pub struct SorobanFunctionComplexityRule { + enabled: bool, + /// Complexity threshold above which a warning is emitted. + warning_threshold: usize, + /// Complexity threshold above which a high-severity issue is emitted. + high_threshold: usize, +} + +impl Default for SorobanFunctionComplexityRule { + fn default() -> Self { + Self { + enabled: true, + warning_threshold: 7, + high_threshold: 12, + } + } +} + +impl SorobanRule for SorobanFunctionComplexityRule { + fn id(&self) -> &str { + "soroban-function-complexity" + } + + fn name(&self) -> &str { + "Soroban Function Complexity Analyzer" + } + + fn description(&self) -> &str { + "Calculates cyclomatic complexity and flags functions that are too complex, increasing audit cost and bug risk" + } + + fn severity(&self) -> ViolationSeverity { + ViolationSeverity::Medium + } + + fn is_enabled(&self) -> bool { + self.enabled + } + + fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } + + fn apply(&self, contract: &SorobanContract) -> Vec { + let mut violations = Vec::new(); + + for implementation in &contract.implementations { + for function in &implementation.functions { + let complexity = Self::calculate_complexity(&function.raw_definition); + + if complexity >= self.high_threshold { + violations.push(RuleViolation { + rule_name: self.id().to_string(), + description: format!( + "Function '{}' has very high cyclomatic complexity ({}) — refactor recommended", + function.name, complexity + ), + suggestion: "Break this function into smaller, single-responsibility functions to reduce complexity and improve testability".to_string(), + line_number: function.line_number, + column_number: 0, + variable_name: function.name.clone(), + severity: ViolationSeverity::High, + }); + } else if complexity >= self.warning_threshold { + violations.push(RuleViolation { + rule_name: self.id().to_string(), + description: format!( + "Function '{}' has elevated cyclomatic complexity ({})", + function.name, complexity + ), + suggestion: "Consider simplifying control flow or extracting helper functions to reduce complexity".to_string(), + line_number: function.line_number, + column_number: 0, + variable_name: function.name.clone(), + severity: ViolationSeverity::Medium, + }); + } + } + } + + violations + } +} + +impl SorobanFunctionComplexityRule { + /// Calculate cyclomatic complexity of a function body. + /// Starts at 1 and increments for each decision point. + fn calculate_complexity(source: &str) -> usize { + let mut complexity: usize = 1; + + // Remove string literals and comments to avoid false positives + let cleaned = Self::strip_strings_and_comments(source); + + // Count decision points + complexity += Self::count_occurrences(&cleaned, "if "); + complexity += Self::count_occurrences(&cleaned, "else if"); + complexity += Self::count_occurrences(&cleaned, "while "); + complexity += Self::count_occurrences(&cleaned, "for "); + complexity += Self::count_occurrences(&cleaned, "loop {"); + complexity += Self::count_occurrences(&cleaned, "match "); + complexity += Self::count_occurrences(&cleaned, "&&"); + complexity += Self::count_occurrences(&cleaned, "||"); + complexity += Self::count_occurrences(&cleaned, "?"); + complexity += Self::count_occurrences(&cleaned, "unwrap_or"); + complexity += Self::count_occurrences(&cleaned, "unwrap_or_else"); + // Count match arms (patterns separated by =>) + complexity += Self::count_occurrences(&cleaned, "=>"); + + complexity + } + + /// Strip string literals and comments from source to avoid counting + /// keywords inside strings or comments. + fn strip_strings_and_comments(source: &str) -> String { + let mut result = String::with_capacity(source.len()); + let mut chars = source.chars().peekable(); + + while let Some(ch) = chars.next() { + match ch { + // Skip line comments + '/' if chars.peek() == Some(&'/') => { + while let Some(c) = chars.next() { + if c == '\n' { + result.push('\n'); + break; + } + } + } + // Skip block comments + '/' if chars.peek() == Some(&'*') => { + chars.next(); // consume '*' + while let Some(c) = chars.next() { + if c == '*' && chars.peek() == Some(&'/') { + chars.next(); + break; + } + } + } + // Skip string literals + '"' => { + while let Some(c) = chars.next() { + if c == '\\' { + chars.next(); // skip escaped char + } else if c == '"' { + break; + } + } + result.push('"'); + } + _ => result.push(ch), + } + } + + result + } + + /// Count non-overlapping occurrences of a pattern in text. + fn count_occurrences(text: &str, pattern: &str) -> usize { + if pattern.is_empty() { + return 0; + } + text.match_indices(pattern).count() + } +} + +// ── Issue #784: Deep Soroban Control-Flow Nesting ──────────────────────────── + +/// Detects deeply nested control-flow structures that harm readability +/// and increase the risk of logic errors. +pub struct SorobanDeepNestingRule { + enabled: bool, + /// Maximum allowed nesting depth before flagging. + max_depth: usize, +} + +impl Default for SorobanDeepNestingRule { + fn default() -> Self { + Self { + enabled: true, + max_depth: 4, + } + } +} + +impl SorobanRule for SorobanDeepNestingRule { + fn id(&self) -> &str { + "soroban-deep-nesting" + } + + fn name(&self) -> &str { + "Deep Soroban Control-Flow Nesting" + } + + fn description(&self) -> &str { + "Detects control-flow nesting beyond a safe threshold, which increases bug risk and audit difficulty" + } + + fn severity(&self) -> ViolationSeverity { + ViolationSeverity::Medium + } + + fn is_enabled(&self) -> bool { + self.enabled + } + + fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } + + fn apply(&self, contract: &SorobanContract) -> Vec { + let mut violations = Vec::new(); + + for implementation in &contract.implementations { + for function in &implementation.functions { + let source = &function.raw_definition; + let cleaned = SorobanFunctionComplexityRule::strip_strings_and_comments(source); + let max_depth = Self::compute_max_nesting(&cleaned); + + if max_depth > self.max_depth { + violations.push(RuleViolation { + rule_name: self.id().to_string(), + description: format!( + "Function '{}' has control-flow nesting depth of {} (max recommended: {})", + function.name, max_depth, self.max_depth + ), + suggestion: "Extract nested logic into helper functions or use early returns to flatten control flow".to_string(), + line_number: function.line_number, + column_number: 0, + variable_name: function.name.clone(), + severity: ViolationSeverity::Medium, + }); + } + } + } + + violations + } +} + +impl SorobanDeepNestingRule { + /// Compute the maximum control-flow nesting depth in a function body. + /// Tracks brace depth and identifies which brace levels contain control-flow keywords. + fn compute_max_nesting(source: &str) -> usize { + let control_keywords = ["if ", "for ", "while ", "loop {", "match "]; + let mut max_depth: usize = 0; + let mut current_depth: usize = 0; + let lines: Vec<&str> = source.lines().collect(); + + for line in &lines { + let trimmed = line.trim(); + + // Skip empty lines + if trimmed.is_empty() { + continue; + } + + // Count opening and closing braces on this line + let open_braces = trimmed.matches('{').count(); + let close_braces = trimmed.matches('}').count(); + + // Check if this line starts a control-flow construct + let is_control = control_keywords.iter().any(|kw| trimmed.starts_with(kw)) + || (trimmed.starts_with("else") && trimmed.contains("if ")) + || trimmed.starts_with("else {"); + + // Process closing braces first (they end the current scope) + for _ in 0..close_braces { + current_depth = current_depth.saturating_sub(1); + } + + // If this is a control-flow line, record its nesting depth + if is_control { + // The depth is the current depth (inside the parent scope) + if current_depth > max_depth { + max_depth = current_depth; + } + // The control structure itself adds one level for its body + current_depth += 1; + } else { + // For non-control lines, opening braces increase depth + for _ in 0..open_braces { + current_depth += 1; + } + } + } + + max_depth + } +} + +// ── Issue #785: Repeated Soroban Computations ──────────────────────────────── + +/// Detects repeated identical computations within a function that could be +/// cached in a local variable to save CPU and ledger costs. +pub struct SorobanRepeatedComputationsRule { + enabled: bool, + /// Minimum occurrences before flagging. + min_occurrences: usize, + /// Minimum token length of an expression to consider. + min_expr_length: usize, +} + +impl Default for SorobanRepeatedComputationsRule { + fn default() -> Self { + Self { + enabled: true, + min_occurrences: 3, + min_expr_length: 10, + } + } +} + +impl SorobanRule for SorobanRepeatedComputationsRule { + fn id(&self) -> &str { + "soroban-repeated-computations" + } + + fn name(&self) -> &str { + "Repeated Soroban Computations" + } + + fn description(&self) -> &str { + "Detects identical expressions computed multiple times that could be cached to reduce CPU costs" + } + + fn severity(&self) -> ViolationSeverity { + ViolationSeverity::Medium + } + + fn is_enabled(&self) -> bool { + self.enabled + } + + fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } + + fn apply(&self, contract: &SorobanContract) -> Vec { + let mut violations = Vec::new(); + + for implementation in &contract.implementations { + for function in &implementation.functions { + let source = &function.raw_definition; + let repeated = Self::find_repeated_expressions(source, self.min_occurrences, self.min_expr_length); + + for expr in repeated { + violations.push(RuleViolation { + rule_name: self.id().to_string(), + description: format!( + "Function '{}' repeats the computation `{}` multiple times — cache in a local variable", + function.name, expr + ), + suggestion: format!( + "Store the result of `{}` in a local variable and reuse it to avoid redundant computation", + expr + ), + line_number: function.line_number, + column_number: 0, + variable_name: function.name.clone(), + severity: ViolationSeverity::Medium, + }); + } + } + } + + violations + } +} + +impl SorobanRepeatedComputationsRule { + /// Find expressions that appear multiple times in the function body. + /// Extracts meaningful sub-expressions (method chains, function calls, field accesses). + fn find_repeated_expressions(source: &str, min_occurrences: usize, min_length: usize) -> Vec { + let cleaned = SorobanFunctionComplexityRule::strip_strings_and_comments(source); + let mut expr_counts: std::collections::HashMap = std::collections::HashMap::new(); + + // Extract method chains and function calls as candidate expressions + let lines: Vec<&str> = cleaned.lines().collect(); + + for line in &lines { + let trimmed = line.trim(); + if trimmed.is_empty() || trimmed.starts_with("//") { + continue; + } + + // Extract sub-expressions: look for patterns like `obj.method(...)` or `obj.field` + let expressions = Self::extract_expressions(trimmed); + + for expr in expressions { + if expr.len() >= min_length { + *expr_counts.entry(expr).or_insert(0) += 1; + } + } + } + + expr_counts + .into_iter() + .filter(|(_, count)| *count >= min_occurrences) + .map(|(expr, _)| expr) + .collect() + } + + /// Extract meaningful sub-expressions from a line of code. + fn extract_expressions(line: &str) -> Vec { + let mut expressions = Vec::new(); + + // Pattern: method chains like `env.storage().instance().get(&key)` + // We extract progressively longer sub-chains + let tokens: Vec<&str> = line.split_whitespace().collect(); + + for token in &tokens { + // Clean the token of trailing punctuation + let clean = token.trim_end_matches(',').trim_end_matches(';').trim_end_matches(')'); + + // Look for method chain patterns: word.word(...) + if clean.contains('.') && clean.len() > 5 { + // Extract the full chain + expressions.push(clean.to_string()); + + // Also extract sub-chains starting from each component + let parts: Vec<&str> = clean.split('.').collect(); + if parts.len() >= 2 { + for i in 0..parts.len() - 1 { + let sub_chain = parts[i..].join("."); + if sub_chain.len() >= 8 { + expressions.push(sub_chain); + } + } + } + } + } + + // Also extract parenthesized expressions + let mut in_parens = false; + let mut current_expr = String::new(); + for ch in line.chars() { + if ch == '(' { + if in_parens && !current_expr.is_empty() { + if current_expr.len() >= 8 { + expressions.push(current_expr.clone()); + } + } + in_parens = true; + current_expr.clear(); + } else if ch == ')' { + if in_parens && current_expr.len() >= 8 { + expressions.push(current_expr.clone()); + } + in_parens = false; + current_expr.clear(); + } else if in_parens { + current_expr.push(ch); + } + } + + expressions + } +} + +// ── Issue #786: Soroban Dead Code Detector ─────────────────────────────────── + +/// Detects unreachable code after terminating statements (return, panic!, break, continue). +pub struct SorobanDeadCodeRule { + enabled: bool, +} + +impl Default for SorobanDeadCodeRule { + fn default() -> Self { + Self { enabled: true } + } +} + +impl SorobanRule for SorobanDeadCodeRule { + fn id(&self) -> &str { + "soroban-dead-code" + } + + fn name(&self) -> &str { + "Soroban Dead Code Detector" + } + + fn description(&self) -> &str { + "Detects unreachable code after return, panic!, break, or continue statements" + } + + fn severity(&self) -> ViolationSeverity { + ViolationSeverity::Warning + } + + fn is_enabled(&self) -> bool { + self.enabled + } + + fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } + + fn apply(&self, contract: &SorobanContract) -> Vec { + let mut violations = Vec::new(); + + for implementation in &contract.implementations { + for function in &implementation.functions { + let dead_code_lines = Self::find_dead_code(&function.raw_definition); + + for line_num in dead_code_lines { + violations.push(RuleViolation { + rule_name: self.id().to_string(), + description: format!( + "Function '{}' contains unreachable code at line {} after a terminating statement", + function.name, line_num + ), + suggestion: "Remove unreachable code or restructure control flow to eliminate dead paths".to_string(), + line_number: line_num, + column_number: 0, + variable_name: function.name.clone(), + severity: ViolationSeverity::Warning, + }); + } + } + } + + violations + } +} + +impl SorobanDeadCodeRule { + /// Find lines of dead code after terminating statements. + /// Returns a list of absolute line numbers where dead code starts. + fn find_dead_code(source: &str) -> Vec { + let mut dead_lines = Vec::new(); + let lines: Vec<&str> = source.lines().collect(); + + // Track brace depth to know when we're in the same block + let mut brace_depth: usize = 0; + let mut found_terminator: bool = false; + let mut terminator_depth: usize = 0; + + for (idx, line) in lines.iter().enumerate() { + let trimmed = line.trim(); + let line_num = idx + 1; + + // Skip empty lines and comments + if trimmed.is_empty() || trimmed.starts_with("//") { + continue; + } + + // Count braces + let open_braces = trimmed.matches('{').count(); + let close_braces = trimmed.matches('}').count(); + + // Check if this line is a terminating statement + let is_terminator = Self::is_terminating_statement(trimmed); + + if is_terminator && !found_terminator { + found_terminator = true; + terminator_depth = brace_depth; + // Update brace depth after this line + brace_depth = brace_depth + open_braces - close_braces; + continue; + } + + // If we previously found a terminator at this depth + if found_terminator { + if brace_depth == terminator_depth { + // We're in the same block after a terminator — this is dead code + // But only if it's not just a closing brace + if !trimmed.starts_with('}') && !trimmed.starts_with("/*") { + dead_lines.push(line_num); + } + } else if brace_depth < terminator_depth { + // We've exited the block, reset + found_terminator = false; + } + } + + // Update brace depth + brace_depth = brace_depth + open_braces - close_braces; + } + + dead_lines + } + + /// Check if a line contains a terminating statement. + fn is_terminating_statement(line: &str) -> bool { + let trimmed = line.trim(); + + // return statement + if trimmed.starts_with("return") { + return true; + } + + // panic! macro + if trimmed.starts_with("panic!") || trimmed.starts_with("panic!(") { + return true; + } + + // break statement + if trimmed.starts_with("break") { + return true; + } + + // continue statement + if trimmed.starts_with("continue") { + return true; + } + + // A function call that ends the flow (like env.panic_with_error) + if trimmed.contains("panic_with_error") { + return true; + } + + false + } +} + #[cfg(test)] mod tests { use super::*; @@ -1542,4 +2165,250 @@ impl MyContract { .iter() .any(|v| v.rule_name == "soroban-upgrade-version-tracking")); } + + // ── Issue #783: Soroban Function Complexity Analyzer Tests ─────────────── + + #[test] + fn test_function_complexity_simple() { + let source = r#" +use soroban_sdk::{contract, contractimpl, Env}; + +#[contract] +pub struct MyContract; + +#[contractimpl] +impl MyContract { + pub fn simple_fn(env: Env) -> u64 { + 42 + } +} +"#; + let rule = SorobanFunctionComplexityRule::default(); + let contract = SorobanParser::parse_contract(source, "test.rs").unwrap(); + let violations = rule.apply(&contract); + + // Simple function should have no complexity violations + assert!(violations.iter().all(|v| v.variable_name != "simple_fn")); + } + + #[test] + fn test_function_complexity_high() { + let source = r#" +use soroban_sdk::{contract, contractimpl, Env, Address}; + +#[contract] +pub struct MyContract; + +#[contractimpl] +impl MyContract { + pub fn complex_fn(env: Env, user: Address, amount: u64) -> u64 { + if amount > 0 { + if amount < 100 { + if user == env.current_contract_address() { + return 1; + } else if amount > 50 { + return 2; + } else { + return 3; + } + } else if amount < 1000 { + for i in 0..10 { + if i > 5 { + while i < 8 { + if i == 7 { + return 4; + } + } + } + } + } else { + match amount { + 1000 => return 5, + 2000 => return 6, + _ => return 7, + } + } + } + 0 + } +} +"#; + let rule = SorobanFunctionComplexityRule::default(); + let contract = SorobanParser::parse_contract(source, "test.rs").unwrap(); + let violations = rule.apply(&contract); + + // Complex function should have a complexity violation + assert!(violations + .iter() + .any(|v| v.variable_name == "complex_fn" && v.severity == ViolationSeverity::High)); + } + + // ── Issue #784: Deep Soroban Control-Flow Nesting Tests ────────────────── + + #[test] + fn test_deep_nesting_detection() { + let source = r#" +use soroban_sdk::{contract, contractimpl, Env}; + +#[contract] +pub struct MyContract; + +#[contractimpl] +impl MyContract { + pub fn nested_fn(env: Env) { + if true { + if true { + if true { + if true { + if true { + // 5 levels deep - exceeds threshold of 4 + } + } + } + } + } + } +} +"#; + let rule = SorobanDeepNestingRule::default(); + let contract = SorobanParser::parse_contract(source, "test.rs").unwrap(); + let violations = rule.apply(&contract); + + assert!(violations + .iter() + .any(|v| v.variable_name == "nested_fn" && v.rule_name == "soroban-deep-nesting")); + } + + #[test] + fn test_no_shallow_nesting() { + let source = r#" +use soroban_sdk::{contract, contractimpl, Env}; + +#[contract] +pub struct MyContract; + +#[contractimpl] +impl MyContract { + pub fn shallow_fn(env: Env) { + if true { + for i in 0..10 { + // Only 2 levels deep + } + } + } +} +"#; + let rule = SorobanDeepNestingRule::default(); + let contract = SorobanParser::parse_contract(source, "test.rs").unwrap(); + let violations = rule.apply(&contract); + + assert!(!violations + .iter() + .any(|v| v.variable_name == "shallow_fn")); + } + + // ── Issue #785: Repeated Soroban Computations Tests ────────────────────── + + #[test] + fn test_repeated_computations_detection() { + let source = r#" +use soroban_sdk::{contract, contractimpl, Env, Address}; + +#[contract] +pub struct MyContract; + +#[contractimpl] +impl MyContract { + pub fn repeated_compute(env: Env, user: Address) { + let a = env.storage().instance().get(&user); + let b = env.storage().instance().get(&user); + let c = env.storage().instance().get(&user); + } +} +"#; + let rule = SorobanRepeatedComputationsRule::default(); + let contract = SorobanParser::parse_contract(source, "test.rs").unwrap(); + let violations = rule.apply(&contract); + + assert!(violations + .iter() + .any(|v| v.variable_name == "repeated_compute" && v.rule_name == "soroban-repeated-computations")); + } + + // ── Issue #786: Soroban Dead Code Detector Tests ───────────────────────── + + #[test] + fn test_dead_code_after_return() { + let source = r#" +use soroban_sdk::{contract, contractimpl, Env}; + +#[contract] +pub struct MyContract; + +#[contractimpl] +impl MyContract { + pub fn dead_code_fn(env: Env) -> u64 { + return 42; + let x = 10; // dead code + } +} +"#; + let rule = SorobanDeadCodeRule::default(); + let contract = SorobanParser::parse_contract(source, "test.rs").unwrap(); + let violations = rule.apply(&contract); + + assert!(violations + .iter() + .any(|v| v.variable_name == "dead_code_fn" && v.rule_name == "soroban-dead-code")); + } + + #[test] + fn test_dead_code_after_panic() { + let source = r#" +use soroban_sdk::{contract, contractimpl, Env}; + +#[contract] +pub struct MyContract; + +#[contractimpl] +impl MyContract { + pub fn panic_fn(env: Env) { + panic!("error"); + let x = 10; // dead code + } +} +"#; + let rule = SorobanDeadCodeRule::default(); + let contract = SorobanParser::parse_contract(source, "test.rs").unwrap(); + let violations = rule.apply(&contract); + + assert!(violations + .iter() + .any(|v| v.variable_name == "panic_fn" && v.rule_name == "soroban-dead-code")); + } + + #[test] + fn test_no_dead_code_clean_function() { + let source = r#" +use soroban_sdk::{contract, contractimpl, Env}; + +#[contract] +pub struct MyContract; + +#[contractimpl] +impl MyContract { + pub fn clean_fn(env: Env) -> u64 { + let x = 42; + x + } +} +"#; + let rule = SorobanDeadCodeRule::default(); + let contract = SorobanParser::parse_contract(source, "test.rs").unwrap(); + let violations = rule.apply(&contract); + + assert!(!violations + .iter() + .any(|v| v.variable_name == "clean_fn")); + } }