debtmap 0.21.0

Code complexity and technical debt analyzer
Documentation
use super::token::{SolidityEntropyToken, extract_tokens_recursive};
use crate::analyzers::solidity::parser::node_text;
use crate::complexity::entropy_analysis::{calculate_repetition_score, calculate_weighted_entropy};
use crate::complexity::entropy_core::{
    EntropyConfig, EntropyScore, LanguageEntropyAnalyzer, PatternMetrics,
};
use sha2::{Digest, Sha256};
use std::collections::HashSet;
use tree_sitter::Node;

pub struct SolidityEntropyAnalyzer<'a> {
    #[allow(dead_code)]
    source: &'a str,
}

impl<'a> SolidityEntropyAnalyzer<'a> {
    #[allow(dead_code)]
    pub fn new(source: &'a str) -> Self {
        Self { source }
    }
}

impl LanguageEntropyAnalyzer for SolidityEntropyAnalyzer<'_> {
    type AstNode = Node<'static>;
    type Token = SolidityEntropyToken;

    fn extract_tokens(&self, _node: &Self::AstNode) -> Vec<Self::Token> {
        Vec::new()
    }

    fn detect_patterns(&self, _node: &Self::AstNode) -> PatternMetrics {
        PatternMetrics::new()
    }

    fn calculate_branch_similarity(&self, _node: &Self::AstNode) -> f64 {
        0.0
    }

    fn analyze_structure(&self, _node: &Self::AstNode) -> (usize, u32) {
        (0, 0)
    }

    fn generate_cache_key(&self, _node: &Self::AstNode) -> String {
        String::new()
    }
}

pub fn calculate_entropy(node: Node, source: &str, config: &EntropyConfig) -> EntropyScore {
    let tokens = extract_tokens_recursive(node, source);
    let token_entropy = token_entropy(&tokens);
    let token_repetition = token_repetition(&tokens);
    let pattern_repetition = token_repetition.max(guard_repetition(node, source));
    let branch_similarity = 0.0;
    let (unique_variables, max_nesting) = analyze_structure(node, source);
    let effective_complexity = calculate_effective_complexity(
        token_entropy,
        pattern_repetition,
        branch_similarity,
        config,
    );

    EntropyScore {
        token_entropy,
        pattern_repetition,
        branch_similarity,
        effective_complexity,
        unique_variables,
        max_nesting,
        dampening_applied: 1.0,
    }
}

pub fn generate_cache_key(node: Node, source: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(node_text(&node, source).as_bytes());
    format!("{:x}", hasher.finalize())
}

fn token_entropy(tokens: &[SolidityEntropyToken]) -> f64 {
    if tokens.is_empty() {
        0.0
    } else {
        calculate_weighted_entropy(tokens)
    }
}

fn token_repetition(tokens: &[SolidityEntropyToken]) -> f64 {
    if tokens.is_empty() {
        0.0
    } else {
        calculate_repetition_score(tokens)
    }
}

fn calculate_effective_complexity(
    entropy: f64,
    patterns: f64,
    similarity: f64,
    config: &EntropyConfig,
) -> f64 {
    let pattern_factor = 1.0 - (patterns * config.pattern_weight);
    let similarity_factor = 1.0 - (similarity * config.similarity_weight);
    let adjusted = entropy * pattern_factor * similarity_factor;

    if adjusted < config.base_threshold {
        adjusted * 0.5
    } else {
        adjusted
    }
}

fn guard_repetition(node: Node, source: &str) -> f64 {
    let mut guard_calls = 0;
    let mut total_calls = 0;
    collect_call_counts(node, source, &mut guard_calls, &mut total_calls);

    if total_calls == 0 {
        0.0
    } else {
        guard_calls as f64 / total_calls as f64
    }
}

fn collect_call_counts(node: Node, source: &str, guard_calls: &mut usize, total_calls: &mut usize) {
    if node.kind() == "call_expression" {
        *total_calls += 1;
        if is_guard_call(node, source) {
            *guard_calls += 1;
        }
    }

    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        collect_call_counts(child, source, guard_calls, total_calls);
    }
}

fn is_guard_call(node: Node, source: &str) -> bool {
    node.child_by_field_name("function")
        .map(|function| {
            matches!(
                node_text(&function, source),
                "require" | "assert" | "revert"
            )
        })
        .unwrap_or(false)
}

fn analyze_structure(node: Node, source: &str) -> (usize, u32) {
    let mut identifiers = HashSet::new();
    let max_nesting = collect_structure(node, source, 0, &mut identifiers);
    (identifiers.len(), max_nesting)
}

fn collect_structure(
    node: Node,
    source: &str,
    current_depth: u32,
    identifiers: &mut HashSet<String>,
) -> u32 {
    let depth = if is_nesting_node(node) {
        current_depth + 1
    } else {
        current_depth
    };

    if node.kind() == "identifier" {
        identifiers.insert(node_text(&node, source).to_string());
    }

    let mut max_depth = depth;
    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        max_depth = max_depth.max(collect_structure(child, source, depth, identifiers));
    }
    max_depth
}

fn is_nesting_node(node: Node) -> bool {
    matches!(
        node.kind(),
        "if_statement" | "for_statement" | "while_statement" | "do_while_statement"
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::analyzers::solidity::parser::parse_source;
    use std::path::Path;

    fn body_for_first_function(tree: &tree_sitter::Tree) -> Node<'_> {
        find_first_function_body(tree.root_node()).expect("function body")
    }

    fn find_first_function_body(node: Node) -> Option<Node> {
        if node.kind() == "function_definition" {
            return node.child_by_field_name("body");
        }

        let mut cursor = node.walk();
        for child in node.children(&mut cursor) {
            if let Some(body) = find_first_function_body(child) {
                return Some(body);
            }
        }
        None
    }

    #[test]
    fn test_repetitive_require_chain_has_pattern_repetition() {
        let source = r#"pragma solidity 0.8.20;
contract C {
    function validate(address a, address b, address c) public pure {
        require(a != address(0));
        require(b != address(0));
        require(c != address(0));
    }
}
"#;
        let ast = parse_source(source, Path::new("C.sol")).unwrap();
        let body = body_for_first_function(&ast.tree);
        let score = calculate_entropy(body, source, &EntropyConfig::default());

        assert!(score.pattern_repetition > 0.5);
        assert!(score.token_entropy >= 0.0 && score.token_entropy <= 1.0);
    }

    #[test]
    fn test_nested_control_flow_tracks_nesting() {
        let source = r#"pragma solidity 0.8.20;
contract C {
    function run(uint256 x) public pure returns (uint256) {
        if (x > 10) {
            while (x > 1) {
                x--;
            }
        }
        return x;
    }
}
"#;
        let ast = parse_source(source, Path::new("C.sol")).unwrap();
        let body = body_for_first_function(&ast.tree);
        let score = calculate_entropy(body, source, &EntropyConfig::default());

        assert!(score.max_nesting >= 2);
    }
}