#![cfg_attr(test, allow(dead_code))]
use crate::adapters::analyzers::architecture::compiled::CompiledArchitecture;
use crate::adapters::analyzers::architecture::forbidden_rule::{
check_forbidden_rules, CompiledForbiddenRule,
};
use crate::adapters::analyzers::architecture::layer_rule::{
check_layer_rule, LayerRuleInput, UnmatchedBehavior,
};
use crate::adapters::analyzers::architecture::{MatchLocation, ViolationKind};
use crate::adapters::shared::use_tree::gather_imports;
use std::fmt::Write;
#[derive(Debug)]
pub struct ExplainReport {
pub file: String,
pub layer: Option<String>,
pub rank: Option<usize>,
pub is_reexport: bool,
pub imports: Vec<ImportEntry>,
pub layer_violations: Vec<MatchLocation>,
pub forbidden_violations: Vec<MatchLocation>,
}
#[derive(Debug)]
pub struct ImportEntry {
pub line: usize,
pub rendered: String,
pub kind: ImportKind,
}
#[derive(Debug)]
pub enum ImportKind {
Ignored { first_segment: String },
CrateInternal {
target_segment: String,
target_layer: Option<String>,
},
ExternalCrate {
crate_name: String,
resolved_layer: Option<String>,
},
}
pub fn explain_file(path: &str, ast: &syn::File, compiled: &CompiledArchitecture) -> ExplainReport {
let is_reexport = compiled.reexport_points.is_match(path);
let (layer, rank) = classify_file_layer(path, compiled);
let imports = classify_imports(ast, compiled);
let (layer_violations, forbidden_violations) = collect_rule_hits(path, ast, compiled);
ExplainReport {
file: path.to_string(),
layer,
rank,
is_reexport,
imports,
layer_violations,
forbidden_violations,
}
}
fn classify_file_layer(
path: &str,
compiled: &CompiledArchitecture,
) -> (Option<String>, Option<usize>) {
let layer = compiled.layers.layer_for_file(path).map(str::to_string);
let rank = layer.as_deref().and_then(|l| compiled.layers.rank_of(l));
(layer, rank)
}
fn classify_imports(ast: &syn::File, compiled: &CompiledArchitecture) -> Vec<ImportEntry> {
gather_imports(ast)
.into_iter()
.map(|(segments, span)| ImportEntry {
line: span.start().line,
rendered: segments.join("::"),
kind: classify_segments(&segments, compiled),
})
.collect()
}
fn classify_segments(segments: &[String], compiled: &CompiledArchitecture) -> ImportKind {
let Some(first) = segments.first() else {
return ImportKind::Ignored {
first_segment: String::new(),
};
};
match first.as_str() {
"self" | "super" | "std" | "core" | "alloc" => ImportKind::Ignored {
first_segment: first.clone(),
},
"crate" => classify_crate_import(segments, compiled),
_ => classify_external_import(first, compiled),
}
}
fn classify_crate_import(segments: &[String], compiled: &CompiledArchitecture) -> ImportKind {
let seg = segments.get(1).cloned().unwrap_or_default();
let target_layer = compiled
.layers
.layer_for_crate_segment(&seg)
.map(str::to_string);
ImportKind::CrateInternal {
target_segment: seg,
target_layer,
}
}
fn classify_external_import(crate_name: &str, compiled: &CompiledArchitecture) -> ImportKind {
if let Some(layer) = compiled.external_exact.get(crate_name) {
return ImportKind::ExternalCrate {
crate_name: crate_name.to_string(),
resolved_layer: Some(layer.clone()),
};
}
let resolved_layer = compiled
.external_glob
.iter()
.find(|(m, _)| m.is_match(crate_name))
.map(|(_, l)| l.clone());
ImportKind::ExternalCrate {
crate_name: crate_name.to_string(),
resolved_layer,
}
}
fn collect_rule_hits(
path: &str,
ast: &syn::File,
compiled: &CompiledArchitecture,
) -> (Vec<MatchLocation>, Vec<MatchLocation>) {
let files = [(path.to_string(), ast)];
let layer_hits = run_layer_rule(&files, compiled);
let forbidden_hits = run_forbidden_rules(&files, &compiled.forbidden);
(layer_hits, forbidden_hits)
}
fn run_layer_rule(
files: &[(String, &syn::File)],
compiled: &CompiledArchitecture,
) -> Vec<MatchLocation> {
let input = LayerRuleInput {
layers: &compiled.layers,
reexport_points: &compiled.reexport_points,
unmatched_behavior: compiled.unmatched_behavior,
external_exact: &compiled.external_exact,
external_glob: &compiled.external_glob,
};
check_layer_rule(files, &input)
}
fn run_forbidden_rules(
files: &[(String, &syn::File)],
rules: &[CompiledForbiddenRule],
) -> Vec<MatchLocation> {
check_forbidden_rules(files, rules)
}
impl ExplainReport {
pub fn render(&self) -> String {
let mut out = String::new();
write_header(&mut out, self);
write_imports(&mut out, self);
write_violations(&mut out, self);
out
}
}
fn write_header(out: &mut String, r: &ExplainReport) {
let _ = writeln!(out, "═══ Architecture Explain: {} ═══", r.file);
match (&r.layer, r.rank, r.is_reexport) {
(_, _, true) => {
let _ = writeln!(out, "Status: re-export point (rules bypassed)");
}
(Some(l), Some(rank), _) => {
let _ = writeln!(out, "Layer: {l} (rank {rank})");
}
_ => {
let _ = writeln!(out, "Layer: <unmatched>");
}
}
}
fn write_imports(out: &mut String, r: &ExplainReport) {
let _ = writeln!(out, "\nImports ({}):", r.imports.len());
r.imports.iter().for_each(|i| write_import_entry(out, i));
}
fn write_import_entry(out: &mut String, i: &ImportEntry) {
let tail = render_import_tail(&i.kind);
let _ = writeln!(out, " line {}: {} — {}", i.line, i.rendered, tail);
}
fn render_import_tail(kind: &ImportKind) -> String {
match kind {
ImportKind::Ignored { first_segment } => format!("ignored ({first_segment})"),
ImportKind::CrateInternal {
target_segment,
target_layer,
} => match target_layer {
Some(l) => format!("crate::{target_segment} → layer {l}"),
None => format!("crate::{target_segment} → unresolved"),
},
ImportKind::ExternalCrate {
crate_name,
resolved_layer,
} => match resolved_layer {
Some(l) => format!("external {crate_name} → layer {l}"),
None => format!("external {crate_name} → no mapping"),
},
}
}
fn write_violations(out: &mut String, r: &ExplainReport) {
write_layer_violations(out, &r.layer_violations);
write_forbidden_violations(out, &r.forbidden_violations);
if r.layer_violations.is_empty() && r.forbidden_violations.is_empty() {
let _ = writeln!(out, "\n✓ No architecture violations.");
}
}
fn write_layer_violations(out: &mut String, hits: &[MatchLocation]) {
if hits.is_empty() {
return;
}
let _ = writeln!(out, "\nLayer violations:");
hits.iter().for_each(|h| write_layer_line(out, h));
}
fn write_layer_line(out: &mut String, h: &MatchLocation) {
if let ViolationKind::LayerViolation {
from_layer,
to_layer,
imported_path,
} = &h.kind
{
let _ = writeln!(
out,
" line {}: {from_layer} ↛ {to_layer} via {imported_path}",
h.line
);
}
}
fn write_forbidden_violations(out: &mut String, hits: &[MatchLocation]) {
if hits.is_empty() {
return;
}
let _ = writeln!(out, "\nForbidden rule violations:");
hits.iter().for_each(|h| write_forbidden_line(out, h));
}
fn write_forbidden_line(out: &mut String, h: &MatchLocation) {
if let ViolationKind::ForbiddenEdge {
reason,
imported_path,
} = &h.kind
{
let _ = writeln!(out, " line {}: {imported_path} [{reason}]", h.line);
}
}