debtmap 0.23.0

Code complexity and technical debt analyzer
Documentation
pub mod natspec;
pub mod security_patterns;

use std::path::Path;

use crate::analyzers::solidity::debt::natspec::detect_natspec_debt_with_signatures;
use crate::analyzers::solidity::debt::security_patterns::detect_contract_patterns_with_counts;
use crate::analyzers::solidity::extraction::{
    ExtractedContract, SolidityExtraction, extract_solidity,
};
use crate::config::SolidityLanguageConfig;
use crate::core::ast::SolidityAst;
use crate::core::{DebtItem, DebtType, FunctionMetrics, Priority};
use crate::debt::patterns::find_todos_and_fixmes_with_suppression;
use crate::debt::smells::analyze_function_smells;

pub fn detect_debt(
    path: &Path,
    threshold: u32,
    functions: &[FunctionMetrics],
    ast: &SolidityAst,
    skip_debt: bool,
    config: &SolidityLanguageConfig,
) -> Vec<DebtItem> {
    let extraction = extract_solidity(ast);
    detect_debt_with_extraction(
        path,
        threshold,
        functions,
        ast,
        &extraction,
        skip_debt,
        config,
    )
}

pub fn detect_debt_with_extraction(
    path: &Path,
    threshold: u32,
    functions: &[FunctionMetrics],
    ast: &SolidityAst,
    extraction: &SolidityExtraction<'_>,
    skip_debt: bool,
    config: &SolidityLanguageConfig,
) -> Vec<DebtItem> {
    if skip_debt {
        return Vec::new();
    }

    let mut items = if config.features.detect_complexity {
        detect_complexity_debt(path, threshold, functions)
    } else {
        advisory_debt_items_for_functions(path, functions)
    };
    items.extend(find_todos_and_fixmes_with_suppression(
        &ast.source,
        path,
        None,
    ));
    items.extend(function_smell_debt(functions));
    items.extend(detect_natspec_debt_with_signatures(
        path,
        &ast.source,
        functions,
        &extraction.function_signatures,
    ));
    items.extend(contract_level_debt(
        path,
        &ast.source,
        &extraction.contracts,
        config,
    ));
    items
}

pub fn detect_complexity_debt(
    path: &Path,
    threshold: u32,
    functions: &[FunctionMetrics],
) -> Vec<DebtItem> {
    functions
        .iter()
        .filter(|function| !function.is_test)
        .flat_map(|function| debt_for_function(path, threshold, function))
        .collect()
}

fn debt_for_function(path: &Path, threshold: u32, function: &FunctionMetrics) -> Vec<DebtItem> {
    let mut items = Vec::new();

    if function.cyclomatic > threshold || function.cognitive > threshold {
        items.push(complexity_debt(path, function, threshold));
    }

    if function.nesting > 4 {
        items.push(nesting_debt(path, function));
    }

    if function.length > 50 {
        items.push(length_debt(path, function));
    }

    items.extend(advisory_debt_items(path, function));
    items
}

fn function_smell_debt(functions: &[FunctionMetrics]) -> Vec<DebtItem> {
    functions
        .iter()
        .filter(|function| !function.is_test)
        .flat_map(|function| analyze_function_smells(function, 0))
        .map(|smell| smell.to_debt_item())
        .collect()
}

fn contract_level_debt(
    path: &Path,
    source: &str,
    contracts: &[ExtractedContract<'_>],
    config: &SolidityLanguageConfig,
) -> Vec<DebtItem> {
    contracts
        .iter()
        .flat_map(|contract| {
            detect_contract_patterns_with_counts(
                source,
                contract.advisory_function_count,
                contract.info.state_variable_count,
                config,
            )
            .into_iter()
            .filter_map(|pattern| {
                contract_advisory(path, &contract.info.name, &pattern, contract.node)
            })
        })
        .collect()
}

fn contract_advisory(
    path: &Path,
    contract_name: &str,
    pattern: &str,
    node: tree_sitter::Node,
) -> Option<DebtItem> {
    let definition = contract_advisory_definition(pattern)?;
    let line = crate::analyzers::solidity::parser::node_line(&node);

    Some(DebtItem {
        id: format!("solidity-{pattern}-{}-{line}", path.display()),
        debt_type: DebtType::CodeSmell {
            smell_type: Some(pattern.to_string()),
        },
        priority: definition.priority,
        file: path.to_path_buf(),
        line,
        column: None,
        message: format!("Contract '{contract_name}' {}", definition.message),
        context: Some(definition.context.to_string()),
    })
}

fn complexity_debt(path: &Path, function: &FunctionMetrics, threshold: u32) -> DebtItem {
    let max_complexity = function.cyclomatic.max(function.cognitive);

    DebtItem {
        id: format!(
            "solidity-high-complexity-{}-{}",
            path.display(),
            function.line
        ),
        debt_type: DebtType::Complexity {
            cyclomatic: function.cyclomatic,
            cognitive: function.cognitive,
        },
        priority: complexity_priority(max_complexity, threshold),
        file: path.to_path_buf(),
        line: function.line,
        column: None,
        message: format!(
            "Function '{}' has high complexity (cyclomatic: {}, cognitive: {})",
            function.name, function.cyclomatic, function.cognitive
        ),
        context: Some("Consider breaking this function into smaller functions.".to_string()),
    }
}

fn nesting_debt(path: &Path, function: &FunctionMetrics) -> DebtItem {
    DebtItem {
        id: format!("solidity-deep-nesting-{}-{}", path.display(), function.line),
        debt_type: DebtType::NestedLoops {
            depth: function.nesting,
            complexity_estimate: format!("O(n^{})", function.nesting),
        },
        priority: if function.nesting > 6 {
            Priority::High
        } else {
            Priority::Medium
        },
        file: path.to_path_buf(),
        line: function.line,
        column: None,
        message: format!(
            "Function '{}' has deep nesting ({} levels)",
            function.name, function.nesting
        ),
        context: Some("Consider guard clauses or extracting nested logic.".to_string()),
    }
}

fn length_debt(path: &Path, function: &FunctionMetrics) -> DebtItem {
    DebtItem {
        id: format!(
            "solidity-long-function-{}-{}",
            path.display(),
            function.line
        ),
        debt_type: DebtType::CodeSmell {
            smell_type: Some("long_function".to_string()),
        },
        priority: if function.length > 100 {
            Priority::High
        } else {
            Priority::Medium
        },
        file: path.to_path_buf(),
        line: function.line,
        column: None,
        message: format!(
            "Function '{}' is too long ({} lines)",
            function.name, function.length
        ),
        context: Some("Consider splitting this function into smaller units.".to_string()),
    }
}

fn advisory_debt_items(path: &Path, function: &FunctionMetrics) -> Vec<DebtItem> {
    function
        .detected_patterns
        .clone()
        .unwrap_or_default()
        .into_iter()
        .filter_map(|pattern| advisory_debt(path, function, &pattern))
        .collect()
}

fn advisory_debt_items_for_functions(path: &Path, functions: &[FunctionMetrics]) -> Vec<DebtItem> {
    functions
        .iter()
        .filter(|function| !function.is_test)
        .flat_map(|function| advisory_debt_items(path, function))
        .collect()
}

fn advisory_debt(path: &Path, function: &FunctionMetrics, pattern: &str) -> Option<DebtItem> {
    let definition = advisory_definition(pattern)?;

    Some(DebtItem {
        id: format!("solidity-{pattern}-{}-{}", path.display(), function.line),
        debt_type: DebtType::CodeSmell {
            smell_type: Some(pattern.to_string()),
        },
        priority: definition.priority,
        file: path.to_path_buf(),
        line: function.line,
        column: None,
        message: format!(
            "Function '{}' {} — review recommended",
            function.name, definition.message
        ),
        context: Some(definition.context.to_string()),
    })
}

struct AdvisoryDefinition {
    priority: Priority,
    message: &'static str,
    context: &'static str,
}

fn advisory_definition(pattern: &str) -> Option<AdvisoryDefinition> {
    let definition = match pattern {
        "tx-origin-usage" => AdvisoryDefinition {
            priority: Priority::High,
            message: "uses tx.origin for authorization",
            context: "Prefer msg.sender; tx.origin is vulnerable to phishing-style attacks.",
        },
        "unchecked-low-level-call" => AdvisoryDefinition {
            priority: Priority::High,
            message: "may perform an unchecked low-level call",
            context: "Check the return value or use a safe wrapper that reverts on failure.",
        },
        "delegatecall-usage" => AdvisoryDefinition {
            priority: Priority::Medium,
            message: "uses delegatecall",
            context: "Delegatecall executes foreign code in this contract's context; review carefully.",
        },
        "selfdestruct-usage" => AdvisoryDefinition {
            priority: Priority::Medium,
            message: "uses selfdestruct",
            context: "Contract destruction is irreversible; confirm this is intentional.",
        },
        "assembly-block" => AdvisoryDefinition {
            priority: Priority::Medium,
            message: "contains inline assembly",
            context: "Assembly bypasses Solidity safety checks and increases audit complexity.",
        },
        "unbounded-loop" => AdvisoryDefinition {
            priority: Priority::Medium,
            message: "contains a potentially unbounded loop",
            context: "Unbounded loops can cause gas DoS; consider pagination or caps.",
        },
        "external-call-before-state-update" => AdvisoryDefinition {
            priority: Priority::High,
            message: "may perform an external call before updating state",
            context: "Follow checks-effects-interactions; external calls before state updates increase reentrancy risk.",
        },
        "hardcoded-address" => AdvisoryDefinition {
            priority: Priority::Low,
            message: "contains a hardcoded address literal",
            context: "Hardcoded addresses reduce maintainability across deployments.",
        },
        "missing-access-control" => AdvisoryDefinition {
            priority: Priority::Medium,
            message: "is public/external without visible access control",
            context: "Confirm authorization is enforced via modifiers or explicit checks.",
        },
        "unchecked-arithmetic" => AdvisoryDefinition {
            priority: Priority::Medium,
            message: "uses an unchecked arithmetic block",
            context: "Unchecked arithmetic skips overflow checks; confirm bounds are proven elsewhere.",
        },
        "unsafe-erc20-transfer" => AdvisoryDefinition {
            priority: Priority::Medium,
            message: "may call ERC20 transfer functions without checking the result",
            context: "Use SafeERC20 wrappers or explicitly validate token transfer return values.",
        },
        "push-without-length-cap" => AdvisoryDefinition {
            priority: Priority::Medium,
            message: "pushes to a collection without an obvious length cap",
            context: "Unbounded storage growth can increase gas costs or create denial-of-service risk.",
        },
        "block-timestamp-dependency" => AdvisoryDefinition {
            priority: Priority::Medium,
            message: "depends on block.timestamp",
            context: "Timestamp-dependent logic can be miner/validator-influenced within protocol limits.",
        },
        "tx-gas-price-dependency" => AdvisoryDefinition {
            priority: Priority::Low,
            message: "depends on tx.gasprice",
            context: "Gas price assumptions are brittle across fee markets and transaction relayers.",
        },
        "encode-packed-collision" => AdvisoryDefinition {
            priority: Priority::Medium,
            message: "uses abi.encodePacked",
            context: "Packed encoding with dynamic values can create hash collision ambiguity; review typed inputs.",
        },
        "delegatecall-in-constructor" => AdvisoryDefinition {
            priority: Priority::High,
            message: "uses delegatecall during construction",
            context: "Constructor delegatecalls can create proxy initialization and storage-layout risks.",
        },
        "mutability-mismatch" => AdvisoryDefinition {
            priority: Priority::Medium,
            message: "declared mutability may not match detected effects",
            context: "Review pure/view/payable keywords against state reads, writes, and external calls.",
        },
        _ => return None,
    };

    Some(definition)
}

fn contract_advisory_definition(pattern: &str) -> Option<AdvisoryDefinition> {
    let definition = match pattern {
        "floating-pragma" => AdvisoryDefinition {
            priority: Priority::Low,
            message: "uses a floating pragma",
            context: "Pin compiler versions to reduce unexpected behavior across builds.",
        },
        "large-contract" => AdvisoryDefinition {
            priority: Priority::Medium,
            message: "is large (many functions or state variables)",
            context: "Large contracts are harder to audit and may approach deployment size limits.",
        },
        _ => return None,
    };

    Some(definition)
}

fn complexity_priority(complexity: u32, threshold: u32) -> Priority {
    if complexity > threshold * 2 {
        Priority::Critical
    } else if complexity > threshold + 5 {
        Priority::High
    } else {
        Priority::Medium
    }
}