rustqual 1.2.4

Comprehensive Rust code quality analyzer — seven dimensions: IOSP, Complexity, DRY, SRP, Coupling, Test Quality, Architecture
Documentation
/// A violation of the Stable Dependencies Principle (SDP).
/// A stable module (low instability) depends on an unstable module (high instability).
#[derive(Debug, Clone)]
pub struct SdpViolation {
    /// The depending module (more stable).
    pub from_module: String,
    /// The depended-upon module (less stable).
    pub to_module: String,
    /// Instability of the depending module.
    pub from_instability: f64,
    /// Instability of the depended-upon module.
    pub to_instability: f64,
    /// Whether this violation is suppressed (either module has a coupling suppression).
    pub suppressed: bool,
}

/// Check the Stable Dependencies Principle: for each dependency edge A→B,
/// if A is more stable (lower instability) than B, it's a violation.
/// Operation: iteration + comparison logic, no own calls.
pub(super) fn check_sdp(
    graph: &super::ModuleGraph,
    metrics: &[super::CouplingMetrics],
) -> Vec<SdpViolation> {
    let instability: std::collections::HashMap<&str, f64> = metrics
        .iter()
        .map(|m| (m.module_name.as_str(), m.instability))
        .collect();

    let suppressed_modules: std::collections::HashSet<&str> = metrics
        .iter()
        .filter(|m| m.suppressed)
        .map(|m| m.module_name.as_str())
        .collect();

    let mut violations = Vec::new();
    for (from_idx, deps) in graph.forward.iter().enumerate() {
        let from_name = &graph.modules[from_idx];
        // Skip edges whose modules are missing from `metrics` — defaulting
        // to 0.0 would silently misclassify them as maximally stable and
        // could emit bogus violations. In practice `metrics` is built from
        // the same graph so this branch is unreachable; skipping keeps
        // the invariant explicit if the two ever drift.
        let Some(from_inst) = instability.get(from_name.as_str()).copied() else {
            continue;
        };
        for &to_idx in deps {
            let to_name = &graph.modules[to_idx];
            let Some(to_inst) = instability.get(to_name.as_str()).copied() else {
                continue;
            };
            // SDP violation: stable module depends on less stable module
            if from_inst < to_inst {
                let suppressed = suppressed_modules.contains(from_name.as_str())
                    || suppressed_modules.contains(to_name.as_str());
                violations.push(SdpViolation {
                    from_module: from_name.clone(),
                    to_module: to_name.clone(),
                    from_instability: from_inst,
                    to_instability: to_inst,
                    suppressed,
                });
            }
        }
    }
    violations
}