brokk-bifrost-cpp 0.9.2

C++ language knowledge for brokk-bifrost: declarations and macro-sentinel recovery, include-graph visibility, out-of-line member identity reconciliation, and usage-graph resolution
Documentation
//! C++ test-file recognition and test-assertion smell detection.
//!
//! `analyzer/cpp/tests.rs` in `brokk-bifrost-analysis` keeps the empty
//! `impl TestDetectionProvider for CppAnalyzer` and the analyzer-bound fixture
//! suites; everything here is a pure function of the file's source text.
//!
//! C++ recognizes the four framework macro families lexically rather than from
//! the parse tree: `TEST`/`TEST_F`/`TEST_P`/`TYPED_TEST` (gtest),
//! `TEST_CASE`/`SCENARIO` (Catch2), `BOOST_AUTO_TEST_CASE` and `TEST_METHOD`
//! (MSTest) all expand from macros the grammar cannot see through.

use brokk_bifrost_core::analyzer::ProjectFile;
use brokk_bifrost_core::analyzer::model::{TestAssertionSmell, TestAssertionWeights};
use regex::Regex;
use std::sync::LazyLock;

static CPP_GTEST_TEST_START_RE: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r#"(?s)\b(?:TEST|TEST_F|TEST_P|TYPED_TEST)\b\s*(?:/\*.*?\*/\s*)?\([^)]*\)\s*\{"#)
        .expect("valid regex")
});
static CPP_CATCH2_TEST_START_RE: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r#"(?s)\b(?:TEST_CASE|SCENARIO)\b\s*\([^)]*\)\s*\{"#).expect("valid regex")
});
static CPP_BOOST_TEST_START_RE: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r#"(?s)\bBOOST_AUTO_TEST_CASE\b\s*\([^)]*\)\s*\{"#).expect("valid regex")
});
static CPP_MSTEST_METHOD_START_RE: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r#"(?s)\bTEST_METHOD\b\s*\([^)]*\)\s*\{"#).expect("valid regex"));
static CPP_ASSERT_TRUTH_RE: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r#"(?:EXPECT|ASSERT)_(?P<matcher>TRUE|FALSE)\s*\((?P<arg>[^)\n]+)\)"#)
        .expect("valid regex")
});
static CPP_ASSERT_EQUALITY_RE: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(
        r#"(?:EXPECT|ASSERT)_(?P<matcher>EQ|NE)\s*\((?P<left>[^,\n]+?)\s*,\s*(?P<right>[^)\n]+)\)"#,
    )
    .expect("valid regex")
});

#[derive(Clone)]
struct CppAssertionSignal {
    kind: String,
    score: i32,
    shallow: bool,
    reason: String,
    excerpt: String,
    start_byte: usize,
}

pub fn detect_cpp_test_assertion_smells(
    file: &ProjectFile,
    source: &str,
    weights: &TestAssertionWeights,
) -> Vec<TestAssertionSmell> {
    let mut findings = Vec::new();
    for regex in [
        &*CPP_GTEST_TEST_START_RE,
        &*CPP_CATCH2_TEST_START_RE,
        &*CPP_BOOST_TEST_START_RE,
        &*CPP_MSTEST_METHOD_START_RE,
    ] {
        for whole in regex.find_iter(source) {
            let Some((body, body_start)) = extract_braced_body(source, whole.end() - 1) else {
                continue;
            };
            analyze_cpp_test_case(file, body, body_start, weights, &mut findings);
        }
    }
    findings
}

fn analyze_cpp_test_case(
    file: &ProjectFile,
    body: &str,
    start_byte: usize,
    weights: &TestAssertionWeights,
    out: &mut Vec<TestAssertionSmell>,
) {
    let assertions = collect_cpp_assertions(body, weights);
    let assertion_count = assertions.len() as i32;
    let symbol = file.to_string();

    if assertion_count == 0 {
        out.push(TestAssertionSmell {
            file: file.clone(),
            enclosing_fq_name: symbol,
            assertion_kind: "no-assertions".to_string(),
            score: weights.no_assertion_weight,
            assertion_count: 0,
            reasons: vec!["no-assertions".to_string()],
            excerpt: compact_cpp_excerpt(body),
            start_byte,
        });
        return;
    }

    for assertion in &assertions {
        if assertion.score <= 0 {
            continue;
        }
        out.push(TestAssertionSmell {
            file: file.clone(),
            enclosing_fq_name: symbol.clone(),
            assertion_kind: assertion.kind.clone(),
            score: assertion.score,
            assertion_count,
            reasons: vec![assertion.reason.clone()],
            excerpt: assertion.excerpt.clone(),
            start_byte: start_byte + assertion.start_byte,
        });
    }

    if assertions.iter().all(|assertion| assertion.shallow) {
        out.push(TestAssertionSmell {
            file: file.clone(),
            enclosing_fq_name: symbol,
            assertion_kind: "shallow-assertions-only".to_string(),
            score: weights.shallow_assertion_only_weight,
            assertion_count,
            reasons: vec!["shallow-assertions-only".to_string()],
            excerpt: compact_cpp_excerpt(body),
            start_byte,
        });
    }
}

fn collect_cpp_assertions(body: &str, weights: &TestAssertionWeights) -> Vec<CppAssertionSignal> {
    let mut assertions = Vec::new();

    for captures in CPP_ASSERT_TRUTH_RE.captures_iter(body) {
        let whole = captures.get(0).expect("whole match");
        let matcher = captures.name("matcher").map(|m| m.as_str()).unwrap_or("");
        let arg = normalize_cpp_expr(captures.name("arg").map(|m| m.as_str()).unwrap_or(""));
        let (kind, score, shallow) = match matcher {
            "TRUE" if arg == "true" => ("constant-truth", weights.constant_truth_weight, true),
            "FALSE" if arg == "false" => ("constant-truth", weights.constant_truth_weight, true),
            _ => ("meaningful-assertion", 0, false),
        };
        assertions.push(CppAssertionSignal {
            kind: kind.to_string(),
            score,
            shallow,
            reason: kind.to_string(),
            excerpt: compact_cpp_excerpt(whole.as_str()),
            start_byte: whole.start(),
        });
    }

    for captures in CPP_ASSERT_EQUALITY_RE.captures_iter(body) {
        let whole = captures.get(0).expect("whole match");
        let matcher = captures.name("matcher").map(|m| m.as_str()).unwrap_or("");
        let left = normalize_cpp_expr(captures.name("left").map(|m| m.as_str()).unwrap_or(""));
        let right = normalize_cpp_expr(captures.name("right").map(|m| m.as_str()).unwrap_or(""));
        let signal = if matcher == "EQ" && left == right {
            let (kind, reason, score) = if is_cpp_literal(&left) {
                (
                    "constant-equality",
                    "constant-equality",
                    weights.constant_equality_weight,
                )
            } else {
                (
                    "self-comparison",
                    "self-comparison",
                    weights.tautological_assertion_weight,
                )
            };
            CppAssertionSignal {
                kind: kind.to_string(),
                score,
                shallow: false,
                reason: reason.to_string(),
                excerpt: compact_cpp_excerpt(whole.as_str()),
                start_byte: whole.start(),
            }
        } else if matcher == "NE" && (is_cpp_null_literal(&left) || is_cpp_null_literal(&right)) {
            CppAssertionSignal {
                kind: "nullness-only".to_string(),
                score: weights.nullness_only_weight,
                shallow: true,
                reason: "nullness-only".to_string(),
                excerpt: compact_cpp_excerpt(whole.as_str()),
                start_byte: whole.start(),
            }
        } else {
            CppAssertionSignal {
                kind: "meaningful-assertion".to_string(),
                score: 0,
                shallow: false,
                reason: "meaningful-assertion".to_string(),
                excerpt: compact_cpp_excerpt(whole.as_str()),
                start_byte: whole.start(),
            }
        };
        assertions.push(signal);
    }

    assertions
}

pub fn cpp_contains_tests(source: &str) -> bool {
    [
        &*CPP_GTEST_TEST_START_RE,
        &*CPP_CATCH2_TEST_START_RE,
        &*CPP_BOOST_TEST_START_RE,
        &*CPP_MSTEST_METHOD_START_RE,
    ]
    .iter()
    .any(|regex| regex.is_match(source))
}

fn normalize_cpp_expr(expr: &str) -> String {
    expr.trim()
        .trim_end_matches(';')
        .trim_matches(|ch| matches!(ch, '(' | ')' | ' '))
        .split_whitespace()
        .collect::<Vec<_>>()
        .join(" ")
}

fn is_cpp_literal(expr: &str) -> bool {
    let trimmed = expr.trim();
    (trimmed.starts_with('"') && trimmed.ends_with('"'))
        || matches!(trimmed, "true" | "false" | "nullptr" | "NULL")
        || trimmed.parse::<i64>().is_ok()
        || trimmed.parse::<f64>().is_ok()
}

fn is_cpp_null_literal(expr: &str) -> bool {
    matches!(expr.trim(), "nullptr" | "NULL")
}

fn compact_cpp_excerpt(text: &str) -> String {
    text.split_whitespace().collect::<Vec<_>>().join(" ")
}

fn extract_braced_body(source: &str, open_brace_index: usize) -> Option<(&str, usize)> {
    let mut depth = 0usize;
    let mut body_start = None;
    for (offset, ch) in source[open_brace_index..].char_indices() {
        let absolute = open_brace_index + offset;
        match ch {
            '{' => {
                depth += 1;
                if depth == 1 {
                    body_start = Some(absolute + ch.len_utf8());
                }
            }
            '}' => {
                if depth == 1 {
                    let start = body_start?;
                    return Some((&source[start..absolute], start));
                }
                depth = depth.saturating_sub(1);
            }
            _ => {}
        }
    }
    None
}