rustqual 1.6.0

Comprehensive Rust code quality analyzer — seven dimensions: IOSP, Complexity, DRY, SRP, Coupling, Test Quality, Architecture
Documentation
//! Unit tests for the `--explain` diagnostic renderer.
//!
//! Each test builds a minimal `CompiledArchitecture`, parses a fixture file,
//! and asserts that `render_explain` returns text containing the expected
//! structural markers. Formatting details (spaces, punctuation) are not
//! asserted to keep the tests resilient to cosmetic tweaks.

use crate::adapters::analyzers::architecture::compiled::CompiledArchitecture;
use crate::adapters::analyzers::architecture::explain::{
    explain_file, ExplainReport, ImportEntry, ImportKind,
};
use crate::adapters::analyzers::architecture::forbidden_rule::CompiledForbiddenRule;
use crate::adapters::analyzers::architecture::layer_rule::{LayerDefinitions, UnmatchedBehavior};
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
use std::collections::HashMap;

fn matcher(pattern: &str) -> GlobMatcher {
    Glob::new(pattern).expect("valid glob").compile_matcher()
}

fn globset(patterns: &[&str]) -> GlobSet {
    let mut b = GlobSetBuilder::new();
    for p in patterns {
        b.add(Glob::new(p).expect("valid glob"));
    }
    b.build().expect("valid glob set")
}

fn minimal_compiled() -> CompiledArchitecture {
    CompiledArchitecture {
        layers: LayerDefinitions::new(
            vec![
                "domain".to_string(),
                "port".to_string(),
                "application".to_string(),
                "adapter".to_string(),
            ],
            vec![
                ("domain".to_string(), globset(&["src/domain/**"])),
                ("port".to_string(), globset(&["src/ports/**"])),
                ("application".to_string(), globset(&["src/app/**"])),
                ("adapter".to_string(), globset(&["src/adapters/**"])),
            ],
        ),
        reexport_points: globset(&["src/lib.rs"]),
        unmatched_behavior: UnmatchedBehavior::CompositionRoot,
        external_exact: HashMap::new(),
        external_glob: Vec::new(),
        forbidden: Vec::new(),
        trait_contracts: Vec::new(),
        call_parity: None,
    }
}

fn parse_file(src: &str) -> syn::File {
    syn::parse_str(src).expect("parse")
}

/// `minimal_compiled()` plus one `src/domain/** → src/adapters/**` forbidden
/// rule, then explain a domain file that imports an adapter — the fixture
/// shared by the forbidden-violation tests.
fn explain_domain_importing_adapter() -> ExplainReport {
    let mut compiled = minimal_compiled();
    compiled.forbidden.push(CompiledForbiddenRule {
        from: matcher("src/domain/**"),
        to: matcher("src/adapters/**"),
        except: globset(&[]),
        reason: "no outward imports".to_string(),
    });
    let ast = parse_file("use crate::adapters::X;");
    explain_file("src/domain/bad.rs", &ast, &compiled)
}

// ── file layer classification ──────────────────────────────────────────

#[test]
fn reports_assigned_layer() {
    let ast = parse_file("fn f() {}");
    let report = explain_file("src/domain/foo.rs", &ast, &minimal_compiled());
    assert_eq!(report.layer.as_deref(), Some("domain"));
    assert_eq!(report.rank, Some(0));
    assert!(!report.is_reexport);
}

#[test]
fn reports_reexport_point() {
    let ast = parse_file("pub use crate::adapters::X;");
    let report = explain_file("src/lib.rs", &ast, &minimal_compiled());
    assert!(report.is_reexport);
    assert!(report.layer_violations.is_empty());
    assert!(report.forbidden_violations.is_empty());
}

#[test]
fn reports_unmatched_file() {
    let ast = parse_file("fn f() {}");
    let report = explain_file("src/misc/foo.rs", &ast, &minimal_compiled());
    assert!(report.layer.is_none());
    assert!(!report.is_reexport);
}

// ── import classification ──────────────────────────────────────────────

#[test]
fn classifies_crate_import_resolved() {
    let ast = parse_file("use crate::adapters::foo::Bar;");
    let report = explain_file("src/adapters/mod.rs", &ast, &minimal_compiled());
    let imports = &report.imports;
    assert_eq!(imports.len(), 1);
    match &imports[0].kind {
        ImportKind::CrateInternal { target_layer, .. } => {
            assert_eq!(target_layer.as_deref(), Some("adapter"));
        }
        other => panic!("expected CrateInternal, got {other:?}"),
    }
}

#[test]
fn classifies_stdlib_imports_as_ignored() {
    let ast = parse_file("use std::collections::HashMap; use core::fmt;");
    let report = explain_file("src/domain/foo.rs", &ast, &minimal_compiled());
    assert!(report
        .imports
        .iter()
        .all(|i| matches!(i.kind, ImportKind::Ignored { .. })));
}

#[test]
fn classifies_external_exact() {
    let mut compiled = minimal_compiled();
    compiled
        .external_exact
        .insert("tokio".to_string(), "adapter".to_string());
    let ast = parse_file("use tokio::spawn;");
    let report = explain_file("src/adapters/net.rs", &ast, &compiled);
    match &report.imports[0].kind {
        ImportKind::ExternalCrate {
            crate_name,
            resolved_layer,
            ..
        } => {
            assert_eq!(crate_name, "tokio");
            assert_eq!(resolved_layer.as_deref(), Some("adapter"));
        }
        other => panic!("expected ExternalCrate, got {other:?}"),
    }
}

#[test]
fn classifies_external_unknown_as_no_mapping() {
    let ast = parse_file("use mystery_crate::Foo;");
    let report = explain_file("src/adapters/x.rs", &ast, &minimal_compiled());
    match &report.imports[0].kind {
        ImportKind::ExternalCrate { resolved_layer, .. } => assert!(resolved_layer.is_none()),
        other => panic!("unexpected kind: {other:?}"),
    }
}

// ── violations ─────────────────────────────────────────────────────────

#[test]
fn layer_violation_surfaced() {
    let ast = parse_file("use crate::adapters::X;");
    let report = explain_file("src/domain/bad.rs", &ast, &minimal_compiled());
    assert_eq!(
        report.layer_violations.len(),
        1,
        "{:?}",
        report.layer_violations
    );
}

#[test]
fn forbidden_violation_surfaced() {
    let report = explain_domain_importing_adapter();
    assert_eq!(report.forbidden_violations.len(), 1);
}

// ── text rendering ─────────────────────────────────────────────────────

#[test]
fn render_mentions_layer_name() {
    let ast = parse_file("fn f() {}");
    let report = explain_file("src/domain/foo.rs", &ast, &minimal_compiled());
    let text = report.render();
    assert!(text.contains("domain"), "{text}");
    assert!(text.contains("src/domain/foo.rs"), "{text}");
}

#[test]
fn render_marks_reexport_point() {
    let ast = parse_file("fn f() {}");
    let report = explain_file("src/lib.rs", &ast, &minimal_compiled());
    let text = report.render();
    assert!(text.to_lowercase().contains("re-export"), "{text}");
}

#[test]
fn render_includes_violation_sections() {
    let text = explain_domain_importing_adapter().render();
    assert!(text.to_lowercase().contains("layer violation"), "{text}");
    assert!(text.to_lowercase().contains("forbidden"), "{text}");
}

use crate::adapters::analyzers::architecture::{MatchLocation, ViolationKind};

fn base_report() -> ExplainReport {
    ExplainReport {
        file: "src/x.rs".into(),
        layer: None,
        rank: None,
        is_reexport: false,
        imports: vec![],
        layer_violations: vec![],
        forbidden_violations: vec![],
    }
}

fn layer_violation() -> MatchLocation {
    MatchLocation {
        file: "src/x.rs".into(),
        line: 5,
        column: 0,
        kind: ViolationKind::LayerViolation {
            from_layer: "application".into(),
            to_layer: "domain".into(),
            imported_path: "crate::domain::Y".into(),
        },
    }
}

#[test]
fn header_shows_layer_and_rank() {
    let mut r = base_report();
    r.layer = Some("domain".into());
    r.rank = Some(2);
    let text = r.render();
    assert!(text.contains("Layer: domain (rank 2)"), "{text}");
}

#[test]
fn layer_violation_line_rendered_and_no_all_clear() {
    let mut r = base_report();
    r.layer_violations = vec![layer_violation()];
    let text = r.render();
    assert!(text.contains("Layer violations:"), "section header: {text}");
    assert!(
        text.contains("application") && text.contains("domain"),
        "from/to layers + import in the line: {text}"
    );
    assert!(text.contains("crate::domain::Y"), "imported path: {text}");
    // Violations present → the all-clear line must NOT appear (pins the `&&`).
    assert!(
        !text.contains("No architecture violations"),
        "all-clear suppressed when violations exist: {text}"
    );
}

#[test]
fn forbidden_violation_line_rendered() {
    let mut r = base_report();
    r.forbidden_violations = vec![MatchLocation {
        file: "src/x.rs".into(),
        line: 9,
        column: 0,
        kind: ViolationKind::ForbiddenEdge {
            reason: "no outward imports".into(),
            imported_path: "crate::app::Z".into(),
        },
    }];
    let text = r.render();
    assert!(text.contains("Forbidden rule violations:"), "{text}");
    assert!(
        text.contains("crate::app::Z") || text.contains("no outward imports"),
        "{text}"
    );
}

#[test]
fn import_entry_renders_tail() {
    let mut r = base_report();
    r.imports = vec![ImportEntry {
        line: 3,
        rendered: "use std::fmt;".into(),
        kind: ImportKind::Ignored {
            first_segment: "std".into(),
        },
    }];
    let text = r.render();
    assert!(text.contains("ignored (std)"), "import tail: {text}");
}