debtmap 0.21.1

Code complexity and technical debt analyzer
Documentation
use std::path::{Path, PathBuf};

use super::*;
use crate::analyzers::solidity::debt::natspec::{
    detect_natspec_debt, detect_natspec_debt_with_signatures,
};
use crate::analyzers::solidity::debt::security_patterns::{
    detect_contract_patterns, detect_contract_patterns_with_counts,
};
use crate::analyzers::solidity::debt::{detect_debt, detect_debt_with_extraction};
use crate::analyzers::solidity::extraction::extract_solidity;
use crate::analyzers::solidity::metrics::to_function_metrics;
use crate::analyzers::solidity::parser::parse_source;
use crate::analyzers::solidity::visitor::legacy_oracle::{
    count_advisory_callables, legacy_analyze_ast,
};

const FIXTURES: [(&str, &str); 5] = [
    (
        "SimpleToken.sol",
        include_str!("../../../tests/fixtures/solidity/simple/SimpleToken.sol"),
    ),
    (
        "ComplexVault.sol",
        include_str!("../../../tests/fixtures/solidity/complex/ComplexVault.sol"),
    ),
    (
        "InsecureBank.sol",
        include_str!("../../../tests/fixtures/solidity/security/InsecureBank.sol"),
    ),
    (
        "IERC20.sol",
        include_str!("../../../tests/fixtures/solidity/interfaces/IERC20.sol"),
    ),
    (
        "Token.t.sol",
        include_str!("../../../tests/fixtures/solidity/foundry/Token.t.sol"),
    ),
];

fn assert_analysis_equivalent(
    mut extracted: SolidityAnalysis,
    legacy: SolidityAnalysis,
    fixture: &str,
) {
    for (current, previous) in extracted.functions.iter_mut().zip(&legacy.functions) {
        normalize_entropy(current, previous);
    }
    assert_eq!(extracted, legacy, "analysis changed for {fixture}");
}

fn normalize_entropy(current: &mut SolidityFunction, previous: &SolidityFunction) {
    let (Some(current), Some(previous)) = (
        current.entropy_analysis.as_mut(),
        previous.entropy_analysis.as_ref(),
    ) else {
        return;
    };
    assert!((current.entropy_score - previous.entropy_score).abs() < 1e-12);
    assert!((current.pattern_repetition - previous.pattern_repetition).abs() < 1e-12);
    assert!((current.branch_similarity - previous.branch_similarity).abs() < 1e-12);
    assert!((current.dampening_factor - previous.dampening_factor).abs() < 1e-12);
    current.entropy_score = previous.entropy_score;
    current.pattern_repetition = previous.pattern_repetition;
    current.branch_similarity = previous.branch_similarity;
    current.dampening_factor = previous.dampening_factor;
}

#[test]
fn extracted_analysis_matches_legacy_visitor_for_solidity_fixtures() {
    let config = SolidityLanguageConfig::default();
    for (name, source) in FIXTURES {
        let ast = parse_source(source, Path::new(name)).unwrap();
        let extraction = extract_solidity(&ast);
        let legacy = legacy_analyze_ast(&ast, &config);
        let extracted = analyze_extracted(&ast, &config, &extraction);
        assert_analysis_equivalent(extracted, legacy, name);
    }
}

#[test]
fn extracted_debt_inputs_match_legacy_detectors() {
    let config = SolidityLanguageConfig::default();
    for (name, source) in FIXTURES {
        assert_debt_inputs_equivalent(name, source, &config);
    }
}

fn assert_debt_inputs_equivalent(name: &str, source: &str, config: &SolidityLanguageConfig) {
    let path = PathBuf::from(name);
    let ast = parse_source(source, &path).unwrap();
    let extraction = extract_solidity(&ast);
    let analysis = analyze_extracted(&ast, config, &extraction);
    let metrics = analysis
        .functions
        .iter()
        .map(to_function_metrics)
        .collect::<Vec<_>>();
    assert_eq!(
        detect_natspec_debt(&path, &ast, &metrics),
        detect_natspec_debt_with_signatures(
            &path,
            &ast.source,
            &metrics,
            &extraction.function_signatures,
        ),
        "NatSpec debt changed for {name}"
    );
    assert_contract_inputs(name, &ast, &extraction, config);
    let skip_debt = analysis.is_test_file;
    assert_eq!(
        detect_debt(&path, 10, &metrics, &ast, skip_debt, config),
        detect_debt_with_extraction(&path, 10, &metrics, &ast, &extraction, skip_debt, config,),
        "debt ordering changed for {name}"
    );
}

fn assert_contract_inputs(
    name: &str,
    ast: &SolidityAst,
    extraction: &SolidityExtraction<'_>,
    config: &SolidityLanguageConfig,
) {
    for contract in &extraction.contracts {
        let legacy = detect_contract_patterns(
            contract.node,
            &ast.source,
            count_advisory_callables(contract.node),
            config,
        );
        let extracted = detect_contract_patterns_with_counts(
            &ast.source,
            contract.advisory_function_count,
            contract.info.state_variable_count,
            config,
        );
        assert_eq!(legacy, extracted, "contract debt inputs changed for {name}");
    }
}