use crate::adapters::analyzers::architecture::compiled::compile_architecture;
use crate::adapters::analyzers::architecture::explain::explain_file;
use crate::config::Config;
use crate::domain::rule_cards::{find_rule_card, RuleCard};
use crate::domain::{target_kind, target_names, Dimension, TargetKind};
use std::path::Path;
pub fn dispatch_explain(target: &Path, config: &Config) -> Result<(), i32> {
if target.as_os_str() == "allow" {
explain_allow();
return Ok(());
}
if let Some(text) = rule_card_text(&target.to_string_lossy()) {
print!("{text}");
return Ok(());
}
handle_explain(target, config)
}
pub(crate) fn rule_card_text(id: &str) -> Option<String> {
find_rule_card(id).map(render_rule_card)
}
pub(crate) fn explain_fallback_text(arg: &str) -> String {
let hint = target_name_hint(arg).unwrap_or_default();
format!(
"`{arg}` is not a rule id, `allow`, or a readable file.\n{hint}{}",
EXPLAIN_MODES
)
}
const EXPLAIN_MODES: &str = "\n--explain modes:\n\
\x20 rustqual --explain <RULE-ID> rule card: what it detects, why, how to fix,\n\
\x20 suppression, config knob. Rule ids appear in\n\
\x20 the findings output, e.g. --explain BP-009\n\
\x20 rustqual --explain allow the qual:allow suppression guide\n\
\x20 (grammar + per-dimension targets)\n\
\x20 rustqual --explain <file.rs> architecture-rule diagnostics for one file\n";
const ALL_DIMS: [Dimension; 7] = [
Dimension::Iosp,
Dimension::Complexity,
Dimension::Dry,
Dimension::Srp,
Dimension::Coupling,
Dimension::TestQuality,
Dimension::Architecture,
];
fn target_name_hint(arg: &str) -> Option<String> {
let dim = ALL_DIMS.iter().find(|d| {
target_names(**d)
.iter()
.any(|t| t.eq_ignore_ascii_case(arg))
})?;
Some(format!(
"Note: `{arg}` is a suppression target of the `{dim}` dimension — \
usable as // qual:allow({dim}, {arg}); see rustqual --explain allow.\n"
))
}
fn render_rule_card(card: &RuleCard) -> String {
format!(
"\n{id} — {title}\n\n Detects: {detects}\n Why: {why}\n \
Fix: {fix}\n Suppress: {suppress}\n Config: {config}\n",
id = card.id,
title = card.title,
detects = card.detects,
why = card.why,
fix = card.fix,
suppress = card.suppress,
config = card.config,
)
}
pub fn explain_allow() {
print!("{}", suppression_guide());
}
const GUIDE_HEADER: &str = "\nqual:allow — targeted suppressions\n\n\
Suppress ONE finding-kind, not a whole dimension:\n\
\x20 // qual:allow(dim, target) boolean target (takes no value)\n\
\x20 // qual:allow(dim, target=N) metric target — value REQUIRED; re-fires above N\n\
\x20 // qual:allow(dim) whole dimension (multi-kind dims reject this)\n\n\
Every targeted suppression needs a reason:\n\
\x20 // qual:allow(srp, file_length=400) reason: \"long but cohesive\"\n\n\
Valid targets per dimension:\n";
const GUIDE_FOOTER: &str =
"\nA metric pin re-fires once the value grows past it, and is flagged too-loose\n\
(orphan) when the pin sits more than [suppression].pin_headroom above the value\n\
it covers (default 10%) — so a pin can never silently mask a growing problem. A\n\
targeted marker is also orphaned when no finding of its kind exists. Fix the\n\
finding first; suppression is the last resort.\n";
pub(crate) fn suppression_guide() -> String {
let mut out = String::from(GUIDE_HEADER);
ALL_DIMS
.iter()
.for_each(|&dim| out.push_str(&dim_targets_block(dim)));
out.push_str(GUIDE_FOOTER);
out
}
fn dim_targets_block(dim: Dimension) -> String {
let metrics = targets_of_kind(dim, TargetKind::Metric);
let booleans = targets_of_kind(dim, TargetKind::Boolean);
if metrics.is_empty() && booleans.is_empty() {
return format!(" {dim}: bare allow({dim}) only (single-kind)\n");
}
let mut block = format!(" {dim}:\n");
if !metrics.is_empty() {
block.push_str(&format!(
" metric (needs =N): {}\n",
metrics.join(", ")
));
}
if !booleans.is_empty() {
block.push_str(&format!(
" boolean: {}\n",
booleans.join(", ")
));
}
block
}
fn targets_of_kind(dim: Dimension, kind: TargetKind) -> Vec<&'static str> {
target_names(dim)
.iter()
.copied()
.filter(|t| target_kind(dim, t) == Some(kind))
.collect()
}
pub fn handle_explain(target: &Path, config: &Config) -> Result<(), i32> {
let source = match std::fs::read_to_string(target) {
Ok(s) => s,
Err(e) => {
eprintln!("Error reading {}: {e}", target.display());
eprintln!("{}", explain_fallback_text(&target.to_string_lossy()));
return Err(1);
}
};
let ast: syn::File = match syn::parse_str(&source) {
Ok(a) => a,
Err(e) => {
eprintln!("Error parsing {}: {e}", target.display());
return Err(1);
}
};
let compiled = match compile_architecture(&config.architecture) {
Ok(c) => c,
Err(e) => {
eprintln!("Error compiling [architecture] config: {e}");
return Err(2);
}
};
let rel = target.to_string_lossy().replace('\\', "/");
let report = explain_file(&rel, &ast, &compiled);
print!("{}", report.render());
Ok(())
}
#[cfg(test)]
mod tests;