rustqual 1.6.0

Comprehensive Rust code quality analyzer — seven dimensions: IOSP, Complexity, DRY, SRP, Coupling, Test Quality, Architecture
Documentation
//! Tests for Check C — multi-touchpoint detection.
//!
//! Check C fires when an adapter pub-fn has more than one touchpoint
//! in the target layer — i.e. it orchestrates multiple application
//! calls itself instead of wrapping them inside a single application
//! method. Configurable severity via `single_touchpoint`.

use super::support::{
    build_workspace, empty_cfg_test, globset, run_check_c, three_layer, Workspace,
};
use crate::adapters::analyzers::architecture::compiled::CompiledCallParity;
use crate::adapters::analyzers::architecture::{MatchLocation, ViolationKind};
use crate::config::architecture::SingleTouchpointMode;
use std::collections::HashSet;

/// Run Check C with the three-layer layout and no cfg-test files — the
/// shared call across the Check-C tests.
fn run_c(ws: &Workspace, cp: &CompiledCallParity) -> Vec<MatchLocation> {
    run_check_c(ws, &three_layer(), cp, &empty_cfg_test())
}

fn make_config(mode: SingleTouchpointMode) -> CompiledCallParity {
    CompiledCallParity {
        adapters: vec!["cli".to_string(), "mcp".to_string()],
        target: "application".to_string(),
        call_depth: 3,
        exclude_targets: globset(&[]),
        transparent_wrappers: HashSet::new(),
        transparent_macros: HashSet::new(),
        promoted_attributes: HashSet::new(),
        single_touchpoint: mode,
    }
}

fn extract_c(findings: &[MatchLocation]) -> Vec<(String, Vec<String>)> {
    findings
        .iter()
        .filter_map(|f| match &f.kind {
            ViolationKind::CallParityMultiTouchpoint {
                fn_name,
                touchpoints,
                ..
            } => Some((fn_name.clone(), touchpoints.clone())),
            _ => None,
        })
        .collect()
}

// ── Single touchpoint → silent ────────────────────────────────

#[test]
fn check_c_single_touchpoint_silent() {
    let ws = build_workspace(&[
        ("src/application/session.rs", "pub fn search() {}"),
        (
            "src/cli/handlers.rs",
            r#"
            use crate::application::session::search;
            pub fn cmd_search() { search(); }
            "#,
        ),
    ]);
    let cp = make_config(SingleTouchpointMode::Warn);
    let findings = run_c(&ws, &cp);
    assert!(
        extract_c(&findings).is_empty(),
        "single-touchpoint handler should be silent, got {findings:?}"
    );
}

// ── Single dispatch on dyn Trait stays silent ─────────────────

#[test]
fn check_c_silent_for_single_trait_dispatch_with_multiple_impls() {
    // CLI handler issues exactly ONE call: `h.handle()` on `dyn Handler`.
    // The trait has THREE overriding impls in the target layer. With
    // dispatch fanout-to-impls semantics this would expose three
    // touchpoints to Check C and fire a false-positive multi-touchpoint
    // warning for what is semantically a single boundary call.
    // Anchor semantics collapses the dispatch into one capability
    // (`crate::application::handler::Handler::handle`) so Check C is
    // silent.
    let ws = build_workspace(&[
        (
            "src/application/handler.rs",
            r#"
            pub trait Handler { fn handle(&self); }
            pub struct A; impl Handler for A { fn handle(&self) {} }
            pub struct B; impl Handler for B { fn handle(&self) {} }
            pub struct C; impl Handler for C { fn handle(&self) {} }
            "#,
        ),
        (
            "src/cli/handlers.rs",
            r#"
            use crate::application::handler::Handler;
            pub fn cmd_dispatch(h: &dyn Handler) { h.handle(); }
            "#,
        ),
    ]);
    let cp = make_config(SingleTouchpointMode::Warn);
    let findings = run_c(&ws, &cp);
    assert!(
        extract_c(&findings).is_empty(),
        "single trait-dispatch call with multiple impls must collapse to one anchor (no Check C fire), got {findings:?}"
    );
}

// ── Two distinct targets from one handler (sequence or branch) → C fires ──

#[test]
fn check_c_reports_both_touchpoints_for_sequence_and_branch() {
    // Two distinct targets reached from a single handler produce one C finding
    // listing both touchpoints — whether they are called in sequence or split
    // across the arms of an `if`/`else` (both branches are seen statically).
    let session = (
        "src/application/session.rs",
        r#"
        pub fn foo() {}
        pub fn bar() {}
        "#,
    );
    let cases: &[(&str, &str)] = &[
        ("sequence", "pub fn cmd_orchestrate() { foo(); bar(); }"),
        (
            "branch",
            "pub fn cmd_branch(cond: bool) { if cond { foo(); } else { bar(); } }",
        ),
    ];
    for (label, handler_body) in cases {
        let handler = format!("use crate::application::session::{{foo, bar}};\n{handler_body}");
        let ws = build_workspace(&[session, ("src/cli/handlers.rs", handler.as_str())]);
        let findings = run_c(&ws, &make_config(SingleTouchpointMode::Warn));
        let pairs = extract_c(&findings);
        assert_eq!(pairs.len(), 1, "case {label}: got {findings:?}");
        let mut tps = pairs[0].1.clone();
        tps.sort();
        assert_eq!(
            tps,
            vec![
                "crate::application::session::bar".to_string(),
                "crate::application::session::foo".to_string(),
            ],
            "case {label}"
        );
    }
}

// ── single_touchpoint mode dispatch (Off / Warn / Error) ─────────

#[test]
fn check_c_severity_off_skips_check() {
    // single_touchpoint = Off must produce zero findings even with a
    // multi-touchpoint handler.
    let ws = build_workspace(&[
        (
            "src/application/session.rs",
            r#"
            pub fn foo() {}
            pub fn bar() {}
            "#,
        ),
        (
            "src/cli/handlers.rs",
            r#"
            use crate::application::session::{foo, bar};
            pub fn cmd_orchestrate() { foo(); bar(); }
            "#,
        ),
    ]);
    let cp = make_config(SingleTouchpointMode::Off);
    let findings = run_c(&ws, &cp);
    assert!(
        extract_c(&findings).is_empty(),
        "Off mode should skip the check entirely, got {findings:?}"
    );
}

// ── Severity: error mode produces findings (severity tested via projection) ──

#[test]
fn check_c_severity_error_emits_finding() {
    let ws = build_workspace(&[
        (
            "src/application/session.rs",
            r#"
            pub fn foo() {}
            pub fn bar() {}
            "#,
        ),
        (
            "src/cli/handlers.rs",
            r#"
            use crate::application::session::{foo, bar};
            pub fn cmd_orchestrate() { foo(); bar(); }
            "#,
        ),
    ]);
    let cp = make_config(SingleTouchpointMode::Error);
    let findings = run_c(&ws, &cp);
    assert_eq!(extract_c(&findings).len(), 1, "got {findings:?}");
}

// ── Loop calling the same target multiple times → silent ──────

#[test]
fn check_c_loop_single_target_silent() {
    let ws = build_workspace(&[
        ("src/application/session.rs", "pub fn process(_x: u32) {}"),
        (
            "src/cli/handlers.rs",
            r#"
            use crate::application::session::process;
            pub fn cmd_batch() {
                for x in 0..10 { process(x); }
            }
            "#,
        ),
    ]);
    let cp = make_config(SingleTouchpointMode::Warn);
    let findings = run_c(&ws, &cp);
    assert!(
        extract_c(&findings).is_empty(),
        "single target hit by multiple call sites is one touchpoint, got {findings:?}"
    );
}

// ── Finding lists every touchpoint ───────────────────────────

#[test]
fn check_c_finding_lists_touchpoints() {
    let ws = build_workspace(&[
        (
            "src/application/session.rs",
            r#"
            pub fn alpha() {}
            pub fn beta() {}
            pub fn gamma() {}
            "#,
        ),
        (
            "src/cli/handlers.rs",
            r#"
            use crate::application::session::{alpha, beta, gamma};
            pub fn cmd_three() { alpha(); beta(); gamma(); }
            "#,
        ),
    ]);
    let cp = make_config(SingleTouchpointMode::Warn);
    let findings = run_c(&ws, &cp);
    let pairs = extract_c(&findings);
    assert_eq!(pairs.len(), 1, "got {findings:?}");
    assert_eq!(pairs[0].1.len(), 3);
}