rustqual 1.4.2

Comprehensive Rust code quality analyzer — seven dimensions: IOSP, Complexity, DRY, SRP, Coupling, Test Quality, Architecture
Documentation
//! Tests for the shared DRY projection (`split_dry_findings`) — the
//! reporter-agnostic step that text and HTML reporters consume.
//!
//! Round 15 P2 (Codex): `build_repeated_match_groups` deduped by
//! `enum_name` alone, collapsing two distinct repeated-match groups
//! over the same enum into one bucket entry. The JSON reporter
//! builds its own groups (with the correct
//! `(enum_name, sorted locations)` key, see round 13), but text and
//! HTML went through the shared projection and silently lost the
//! second group. Reporter parity regressed once more. These tests
//! lock the dedup contract at the projection layer so every consumer
//! benefits.

use crate::adapters::report::projections::dry::split_dry_findings;
use crate::domain::findings::{
    DryFinding, DryFindingDetails, DryFindingKind, RepeatedMatchParticipant,
};
use crate::domain::{Dimension, Finding, Severity};

fn dry_common(line: usize) -> Finding {
    Finding {
        file: "lib.rs".into(),
        line,
        dimension: Dimension::Dry,
        rule_id: "dry/repeated_match".into(),
        message: "repeated match".into(),
        severity: Severity::Medium,
        ..Default::default()
    }
}

fn participant(name: &str, line: usize, arms: usize) -> RepeatedMatchParticipant {
    RepeatedMatchParticipant {
        function_name: name.into(),
        file: "lib.rs".into(),
        line,
        arm_count: arms,
    }
}

fn repeated_match(line: usize, participants: Vec<RepeatedMatchParticipant>) -> DryFinding {
    DryFinding {
        common: dry_common(line),
        kind: DryFindingKind::RepeatedMatch,
        details: DryFindingDetails::RepeatedMatch {
            enum_name: "MyEnum".into(),
            participants,
        },
    }
}

#[test]
fn split_dry_findings_keeps_distinct_repeated_match_groups_over_same_enum() {
    let group_a = vec![participant("fa1", 10, 4), participant("fa2", 20, 4)];
    let group_b = vec![participant("fb1", 50, 3), participant("fb2", 60, 3)];
    let findings = vec![repeated_match(10, group_a), repeated_match(50, group_b)];

    let buckets = split_dry_findings(&findings);

    assert_eq!(
        buckets.repeated_match_groups.len(),
        2,
        "two distinct repeated-match groups over the same enum must NOT \
         collapse via enum-name-only dedup; got {} groups",
        buckets.repeated_match_groups.len(),
    );
    let participant_lines: Vec<usize> = buckets
        .repeated_match_groups
        .iter()
        .flat_map(|g| g.participants.iter().map(|p| p.line))
        .collect();
    assert!(
        participant_lines.contains(&10) && participant_lines.contains(&50),
        "both groups' participants must survive projection (expected \
         lines 10 and 50); got {participant_lines:?}",
    );
}

#[test]
fn split_dry_findings_collapses_duplicate_repeated_match_group_emissions() {
    // The analyzer emits one finding per participant for repeated-match
    // groups too; dedup by (enum_name, sorted locations) must collapse
    // those into a single bucket entry.
    let group = vec![participant("fa1", 10, 4), participant("fa2", 20, 4)];
    let findings = vec![repeated_match(10, group.clone()), repeated_match(20, group)];

    let buckets = split_dry_findings(&findings);

    assert_eq!(
        buckets.repeated_match_groups.len(),
        1,
        "two findings describing the same participant set must collapse \
         to one bucket entry; got {} groups",
        buckets.repeated_match_groups.len(),
    );
}

fn dead_code_finding(name: &str, suppressed: bool) -> DryFinding {
    let mut common = dry_common(1);
    common.suppressed = suppressed;
    DryFinding {
        common,
        kind: DryFindingKind::DeadCodeUncalled,
        details: DryFindingDetails::DeadCode {
            qualified_name: name.into(),
            suggestion: Some("remove".into()),
        },
    }
}

#[test]
fn dead_code_boilerplate_wildcard_buckets_built_and_drop_suppressed() {
    let findings = vec![
        dead_code_finding("dead_fn", false),
        dead_code_finding("suppressed_dead", true),
        DryFinding {
            common: dry_common(2),
            kind: DryFindingKind::Boilerplate,
            details: DryFindingDetails::Boilerplate {
                pattern_id: "BP-001".into(),
                struct_name: Some("Foo".into()),
                suggestion: "derive".into(),
            },
        },
        DryFinding {
            common: dry_common(3),
            kind: DryFindingKind::Wildcard,
            details: DryFindingDetails::Wildcard {
                module_path: "foo::*".into(),
            },
        },
    ];
    let b = split_dry_findings(&findings);
    assert_eq!(
        b.dead_code.len(),
        1,
        "suppressed dead-code dropped (`!suppressed`)"
    );
    assert_eq!(b.dead_code[0].qualified_name, "dead_fn");
    assert_eq!(b.boilerplate.len(), 1);
    assert_eq!(b.boilerplate[0].pattern_id, "BP-001");
    assert_eq!(b.wildcards.len(), 1);
    assert_eq!(b.wildcards[0].module_path, "foo::*");
}

#[test]
fn duplicate_and_fragment_groups_carry_participants() {
    use crate::domain::findings::{DuplicateParticipant, FragmentParticipant};
    let findings = vec![
        DryFinding {
            common: dry_common(1),
            kind: DryFindingKind::DuplicateExact,
            details: DryFindingDetails::Duplicate {
                participants: vec![DuplicateParticipant {
                    function_name: "dup_a".into(),
                    file: "lib.rs".into(),
                    line: 5,
                }],
                similarity: None,
            },
        },
        DryFinding {
            common: dry_common(2),
            kind: DryFindingKind::Fragment,
            details: DryFindingDetails::Fragment {
                participants: vec![FragmentParticipant {
                    function_name: "frag_a".into(),
                    file: "lib.rs".into(),
                    line: 10,
                    end_line: 14,
                }],
                statement_count: 4,
            },
        },
    ];
    let b = split_dry_findings(&findings);
    assert_eq!(b.duplicate_groups.len(), 1);
    assert!(b.duplicate_groups[0]
        .participants
        .iter()
        .any(|p| p.function_name == "dup_a"));
    assert_eq!(b.fragment_groups.len(), 1);
    assert!(b.fragment_groups[0]
        .participants
        .iter()
        .any(|p| p.function_name == "frag_a"));
}