rustqual 1.5.0

Comprehensive Rust code quality analyzer — seven dimensions: IOSP, Complexity, DRY, SRP, Coupling, Test Quality, Architecture
Documentation
use super::make_parsed;
use crate::adapters::analyzers::coupling::*;

/// Find module index by name in graph.modules.
fn idx(graph: &ModuleGraph, name: &str) -> usize {
    graph
        .modules
        .iter()
        .position(|m| m == name)
        .unwrap_or_else(|| panic!("module '{name}' not found in graph"))
}

/// Whether the module graph built from `files` has a forward edge `from → to`.
fn has_module_edge(files: &[(&str, &str)], from: &str, to: &str) -> bool {
    let parsed = make_parsed(files.to_vec());
    let graph = graph::build_module_graph(&parsed);
    graph.forward[idx(&graph, from)].contains(&idx(&graph, to))
}

/// `(label, files, from_module, to_module)` for a build-graph edge case.
type EdgeCase = (
    &'static str,
    &'static [(&'static str, &'static str)],
    &'static str,
    &'static str,
);

// `file_to_module` lives in `adapters::shared::file_to_module`; its
// tests are in `adapters::shared::tests::file_to_module`.

// ── build_module_graph tests ────────────────────────────────────

#[test]
fn test_build_graph_no_deps() {
    let parsed = make_parsed(vec![
        ("main.rs", "fn main() {}"),
        ("config.rs", "pub struct Config;"),
    ]);
    let graph = graph::build_module_graph(&parsed);
    assert_eq!(graph.modules.len(), 2);
    assert!(graph.forward.iter().all(|adj| adj.is_empty()));
}

#[test]
fn test_build_graph_simple_dep() {
    let parsed = make_parsed(vec![
        ("main.rs", "use crate::config::Config; fn main() {}"),
        ("config.rs", "pub struct Config;"),
    ]);
    let graph = graph::build_module_graph(&parsed);
    let main_idx = idx(&graph, "main");
    let config_idx = idx(&graph, "config");
    assert!(graph.forward[main_idx].contains(&config_idx));
    assert!(graph.forward[config_idx].is_empty());
}

#[test]
fn test_build_graph_self_dep_skipped() {
    let parsed = make_parsed(vec![
        (
            "analyzer/mod.rs",
            "use crate::analyzer::types::Foo; fn f() {}",
        ),
        ("analyzer/types.rs", "pub struct Foo;"),
    ]);
    let graph = graph::build_module_graph(&parsed);
    let analyzer_idx = idx(&graph, "analyzer");
    assert!(
        graph.forward[analyzer_idx].is_empty(),
        "Self-dependencies should be skipped"
    );
}

#[test]
fn test_build_graph_group_use() {
    let parsed = make_parsed(vec![
        (
            "main.rs",
            "use crate::{config::Config, pipeline::run}; fn main() {}",
        ),
        ("config.rs", "pub struct Config;"),
        ("pipeline.rs", "pub fn run() {}"),
    ]);
    let graph = graph::build_module_graph(&parsed);
    let main_idx = idx(&graph, "main");
    assert_eq!(graph.forward[main_idx].len(), 2);
}

#[test]
fn test_build_graph_external_dep_ignored() {
    // `serde::config` collides with the local `config` module on its second
    // segment. Only a `crate::`-prefixed path may create an edge, so the
    // external import must still be ignored: a non-crate root short-circuits
    // the dependency, length alone is not enough.
    let parsed = make_parsed(vec![
        (
            "main.rs",
            "use std::collections::HashMap; use serde::config::Opt; fn main() {}",
        ),
        ("config.rs", "pub struct Config;"),
    ]);
    let graph = graph::build_module_graph(&parsed);
    let main_idx = idx(&graph, "main");
    assert!(
        graph.forward[main_idx].is_empty(),
        "External dependencies must be ignored even when a path segment \
         collides with a local module name"
    );
}

#[test]
fn build_graph_records_use_edges_across_forms() {
    // A `use crate::X::…` records a `from → X` edge whether it's spread across
    // files of the same module, a glob import, or a renamed import.
    // (label, files, from, to)
    let cases: &[EdgeCase] = &[
        (
            "multiple files of the same module",
            &[
                (
                    "config/mod.rs",
                    "use crate::analyzer::Foo; pub mod sections;",
                ),
                ("config/sections.rs", "pub struct Defaults;"),
                ("analyzer.rs", "pub struct Foo;"),
            ],
            "config",
            "analyzer",
        ),
        (
            "glob use",
            &[
                ("main.rs", "use crate::analyzer::*; fn main() {}"),
                ("analyzer.rs", "pub fn analyze() {}"),
            ],
            "main",
            "analyzer",
        ),
        (
            "renamed use",
            &[
                ("main.rs", "use crate::config::Config as Cfg; fn main() {}"),
                ("config.rs", "pub struct Config;"),
            ],
            "main",
            "config",
        ),
    ];
    for (label, files, from, to) in cases {
        assert!(
            has_module_edge(files, from, to),
            "case {label}: expected a {from} → {to} edge"
        );
    }
}

// ── compute_coupling_metrics tests ──────────────────────────────

#[test]
fn test_metrics_empty() {
    let graph = ModuleGraph {
        modules: vec![],
        forward: vec![],
    };
    let metrics = metrics::compute_coupling_metrics(&graph);
    assert!(metrics.is_empty());
}

#[test]
fn test_metrics_simple_dep() {
    // A → B
    let graph = ModuleGraph {
        modules: vec!["a".into(), "b".into()],
        forward: vec![vec![1], vec![]],
    };
    let metrics = metrics::compute_coupling_metrics(&graph);
    // A: Ca=0, Ce=1
    assert_eq!(metrics[0].afferent, 0);
    assert_eq!(metrics[0].efferent, 1);
    // B: Ca=1, Ce=0
    assert_eq!(metrics[1].afferent, 1);
    assert_eq!(metrics[1].efferent, 0);
}

#[test]
fn test_metrics_instability_formula() {
    // A → B, A → C (Ce=2)
    let graph = ModuleGraph {
        modules: vec!["a".into(), "b".into(), "c".into()],
        forward: vec![vec![1, 2], vec![], vec![]],
    };
    let metrics = metrics::compute_coupling_metrics(&graph);
    // A: Ca=0, Ce=2, I = 2/(0+2) = 1.0
    assert!((metrics[0].instability - 1.0).abs() < f64::EPSILON);
    // B: Ca=1, Ce=0, I = 0/(1+0) = 0.0
    assert!((metrics[1].instability).abs() < f64::EPSILON);
}

#[test]
fn test_metrics_isolated_module() {
    let graph = ModuleGraph {
        modules: vec!["isolated".into()],
        forward: vec![vec![]],
    };
    let metrics = metrics::compute_coupling_metrics(&graph);
    assert_eq!(metrics[0].afferent, 0);
    assert_eq!(metrics[0].efferent, 0);
    assert!((metrics[0].instability).abs() < f64::EPSILON);
}

// ── detect_cycles tests ─────────────────────────────────────────

#[test]
fn test_cycles_empty_graph() {
    let graph = ModuleGraph {
        modules: vec![],
        forward: vec![],
    };
    let cycles = cycles::detect_cycles(&graph);
    assert!(cycles.is_empty());
}

#[test]
fn test_cycles_no_cycles() {
    // A → B → C (linear, no cycles)
    let graph = ModuleGraph {
        modules: vec!["a".into(), "b".into(), "c".into()],
        forward: vec![vec![1], vec![2], vec![]],
    };
    let cycles = cycles::detect_cycles(&graph);
    assert!(cycles.is_empty());
}

#[test]
fn test_cycles_simple_cycle() {
    // A → B → A
    let graph = ModuleGraph {
        modules: vec!["a".into(), "b".into()],
        forward: vec![vec![1], vec![0]],
    };
    let cycles = cycles::detect_cycles(&graph);
    assert_eq!(cycles.len(), 1);
    assert!(cycles[0].modules.contains(&"a".to_string()));
    assert!(cycles[0].modules.contains(&"b".to_string()));
}

#[test]
fn test_cycles_complex_cycle() {
    // A → B → C → A (3-node cycle)
    let graph = ModuleGraph {
        modules: vec!["a".into(), "b".into(), "c".into()],
        forward: vec![vec![1], vec![2], vec![0]],
    };
    let cycles = cycles::detect_cycles(&graph);
    assert_eq!(cycles.len(), 1);
    assert_eq!(cycles[0].modules.len(), 3);
}

#[test]
fn test_cycles_self_loop_not_counted() {
    // A → A (self-loop, not a meaningful cycle)
    let graph = ModuleGraph {
        modules: vec!["a".into()],
        forward: vec![vec![0]],
    };
    let cycles = cycles::detect_cycles(&graph);
    assert!(
        cycles.is_empty(),
        "Self-loops should not be reported as cycles"
    );
}

#[test]
fn test_cycles_two_independent_cycles() {
    // A ↔ B, C ↔ D (two separate cycles)
    let graph = ModuleGraph {
        modules: vec!["a".into(), "b".into(), "c".into(), "d".into()],
        forward: vec![vec![1], vec![0], vec![3], vec![2]],
    };
    let cycles = cycles::detect_cycles(&graph);
    assert_eq!(cycles.len(), 2);
}