Skip to content

qual:allow(dry, duplicate) is reported as ORPHAN_SUPPRESSION when it actually suppresses a duplicate group — no way to acknowledge intentional duplication #36

@hansjorg

Description

@hansjorg

When a set of functions form a DUPLICATE group and I annotate each of
them with // qual:allow(dry, duplicate) reason: "..." to mark the
duplication as intended, rustqual:

  1. suppresses the DUPLICATE findings (good — DRY score recovers), but then
  2. re-reports every marker as ORPHAN_SUPPRESSION ("stale"), and
  3. emits two ORPHAN_SUPPRESSION findings per marker.

The net effect is that the total finding count goes up, not down
(4 → 7 in the repro below), so there is no way to acknowledge intentional
duplication and reach a clean report: without the markers you get the
DUPLICATE findings; with them you get even more ORPHAN_SUPPRESSION
findings. The same happens for qual:allow(dry, fragment).

This blocks a common, deliberate pattern: one named test per scenario
(a readable, table-free style where each #[test] covers a single case
and a failure names the exact case). Such tests are exact/near duplicates
by design, and there's currently no clean way to tell rustqual so.

Environment

  • rustqual 1.5.0 (installed from crates.io)
  • rustc 1.95.0 (59807616e 2026-04-14), x86_64-unknown-linux-gnu

Reproduction

A self-contained project can be found here. The only file that matters is
src/lib.rsrustqual runs as static analysis on the single file, so
no build is required.

Cargo.toml:

[package]
name = "rustqual-orphan-repro"
version = "0.1.0"
edition = "2021"

[dependencies]

src/lib.rs:

//! Three deliberately-parallel `#[test]` functions (one named test per
//! scenario) with identical bodies, each marked as intended with
//! `// qual:allow(dry, duplicate)`.

pub fn step(x: u32, y: u32) -> u32 {
    x.wrapping_add(y).wrapping_mul(3)
}

#[cfg(test)]
mod tests {
    use super::*;

    // qual:allow(dry, duplicate) reason: "one named test per scenario, parallel by design"
    #[test]
    fn scenario_alpha() {
        let a = step(1, 2);
        let b = step(a, 3);
        let c = step(b, 4);
        let d = step(c, 5);
        let e = step(d, 6);
        let f = step(e, 7);
        assert!(f > 0);
        assert_ne!(f, 1);
    }

    // qual:allow(dry, duplicate) reason: "one named test per scenario, parallel by design"
    #[test]
    fn scenario_alpha() {
        let a = step(1, 2);
        let b = step(a, 3);
        let c = step(b, 4);
        let d = step(c, 5);
        let e = step(d, 6);
        let f = step(e, 7);
        assert!(f > 0);
        assert_ne!(f, 1);
    }

    // qual:allow(dry, duplicate) reason: "one named test per scenario, parallel by design"
    #[test]
    fn scenario_beta() {
        let a = step(1, 2);
        let b = step(a, 3);
        let c = step(b, 4);
        let d = step(c, 5);
        let e = step(d, 6);
        let f = step(e, 7);
        assert!(f > 0);
        assert_ne!(f, 1);
    }

    // qual:allow(dry, duplicate) reason: "one named test per scenario, parallel by design"
    #[test]
    fn scenario_gamma() {
        let a = step(1, 2);
        let b = step(a, 3);
        let c = step(b, 4);
        let d = step(c, 5);
        let e = step(d, 6);
        let f = step(e, 7);
        assert!(f > 0);
        assert_ne!(f, 1);
    }
}

Steps

# 1. Baseline — remove the three `// qual:allow(...)` lines, then:
rustqual src/lib.rs

# 2. As written (markers present):
rustqual src/lib.rs

Expected

With each member of the duplicate group carrying a valid
// qual:allow(dry, duplicate) reason: "...", the DUPLICATE findings
are acknowledged and no finding remains for them — a clean report
(modulo unrelated findings). The markers should not be considered
orphaned, because each one is covering a real DUPLICATE finding on its
own function.

Actual

Baseline (markers removed) — 4 findings, the duplicate group is reported:

═══ 4 Findings ═══
  DEAD_CODE  testonly step  in step
  DUPLICATE  exact  in scenario_alpha
  DUPLICATE  exact  in scenario_beta
  DUPLICATE  exact  in scenario_gamma

With the three markers — 7 findings; the DUPLICATEs are gone but
each marker is now ORPHAN_SUPPRESSION, twice:

  ~ All allows:   3 (qual:allow + #[allow])
  ⚠ Suppression ratio exceeds configured maximum
═══ 7 Findings ═══
  DEAD_CODE  testonly step  in step
  ORPHAN_SUPPRESSION  stale qual:allow(dry, duplicate) — one named test per scenario, parallel by design
  ORPHAN_SUPPRESSION  stale qual:allow(dry, duplicate) — one named test per scenario, parallel by design
  ORPHAN_SUPPRESSION  stale qual:allow(dry, duplicate) — one named test per scenario, parallel by design
  ORPHAN_SUPPRESSION  stale qual:allow(dry, duplicate) — one named test per scenario, parallel by design
  ORPHAN_SUPPRESSION  stale qual:allow(dry, duplicate) — one named test per scenario, parallel by design
  ORPHAN_SUPPRESSION  stale qual:allow(dry, duplicate) — one named test per scenario, parallel by design

Warning: 3 suppression(s) found (75.0% of functions, max: 5.0%)

So 3 markers → 6 ORPHAN_SUPPRESSION findings (two per marker), and
the total went from 4 → 7.

Additional observations

  • Two orphan findings per marker. Three qual:allow markers produce
    six ORPHAN_SUPPRESSION entries — looks like a double count.
  • No placement works. A file/module-level marker is reported as
    orphaned too; marking only some members of the group suppresses that
    subset's findings but still reports those markers as orphaned. There is
    no placement that both suppresses the duplicate and avoids the orphan.
  • Same behavior for // qual:allow(dry, fragment).

Probable root cause

The orphan check appears to evaluate each marker against the
post-suppression finding set. Because a DUPLICATE group is only
cleared once all its members are suppressed, the group collapses to
"no duplicate finding exists" — and then every marker that contributed to
clearing it is judged to be "covering nothing" and flagged orphaned. The
orphan determination for a group-relationship finding (duplicate /
fragment) probably needs to run against the pre-suppression findings,
or treat a marker that participated in clearing a group as non-orphan.

Impact

Intentional, idiomatic "one named test per scenario" suites cannot reach a
clean rustqual report: the duplication is real and deliberate, but the
mechanism intended to acknowledge it (qual:allow(dry, duplicate)) makes
the report strictly worse. The only workaround is to collapse the named
tests into a single table-driven test, which sacrifices per-case failure
attribution.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions