use std::collections::BTreeMap;
use std::io::Write;
use std::path::PathBuf;
use alint_core::{FixReport, FixStatus, Level, Report, RuleResult, Violation};
use crate::style::{self, GlyphSet, HumanOptions, write_hyperlink};
pub fn write_human(report: &Report, w: &mut dyn Write, opts: HumanOptions) -> std::io::Result<()> {
if opts.compact {
return write_human_compact(report, w, &opts);
}
if report.failing_rules() == 0 {
let s = style::SUCCESS;
let passing = report.passing_rules();
writeln!(
w,
"{s}{} All {passing} rule(s) passed.{s:#}",
opts.glyphs.success,
)?;
return Ok(());
}
let mut by_bucket: BTreeMap<Option<PathBuf>, Vec<(&RuleResult, &Violation)>> = BTreeMap::new();
for result in &report.results {
if result.passed() {
continue;
}
for violation in &result.violations {
by_bucket
.entry(violation.path.clone())
.or_default()
.push((result, violation));
}
}
let width = opts.effective_width();
let mut first_bucket = true;
for (bucket, items) in &by_bucket {
if !first_bucket {
writeln!(w)?;
}
first_bucket = false;
let label = bucket.as_ref().map_or_else(
|| "Repository-level".to_string(),
|p| p.display().to_string(),
);
write_section_header(w, &label, width, &opts.glyphs)?;
for (result, violation) in items {
write_violation(w, result, violation, &opts)?;
}
}
writeln!(w)?;
write_summary(w, report, &opts.glyphs)?;
Ok(())
}
fn write_section_header(
w: &mut dyn Write,
label: &str,
width: usize,
glyphs: &GlyphSet,
) -> std::io::Result<()> {
let lead = format!("{r}{r}{r} {label} ", r = glyphs.rule);
let used = lead.chars().count();
let tail_cols = width.saturating_sub(used);
let tail: String = glyphs.rule.repeat(tail_cols);
let s = style::DIM;
writeln!(w, "{s}{lead}{tail}{s:#}")?;
Ok(())
}
fn write_violation(
w: &mut dyn Write,
result: &RuleResult,
violation: &Violation,
opts: &HumanOptions,
) -> std::io::Result<()> {
let (sigil, level_style, level_name) = level_presentation(result.level, &opts.glyphs);
let rule_style = style::RULE_ID;
if result.is_fixable {
let fix = style::FIXABLE;
writeln!(
w,
" {level_style}{sigil} {level_name}{level_style:#} {rule_style}{}{rule_style:#} {fix}fixable{fix:#}",
result.rule_id,
)?;
} else {
writeln!(
w,
" {level_style}{sigil} {level_name}{level_style:#} {rule_style}{}{rule_style:#}",
result.rule_id,
)?;
}
let dim = style::DIM;
match (violation.line, violation.column) {
(Some(line), Some(col)) => {
writeln!(
w,
"{MSG_INDENT}{dim}{line}:{col}{dim:#} {}",
violation.message
)?;
}
(Some(line), None) => {
writeln!(
w,
"{MSG_INDENT}{dim}line {line}{dim:#} {}",
violation.message
)?;
}
_ => {
writeln!(w, "{MSG_INDENT}{}", violation.message)?;
}
}
if let Some(url) = &result.policy_url {
let docs = style::DOCS;
write!(w, "{MSG_INDENT}{dim}docs:{dim:#} {docs}")?;
write_hyperlink(w, url, url, opts.hyperlinks)?;
writeln!(w, "{docs:#}")?;
}
Ok(())
}
fn write_summary(w: &mut dyn Write, report: &Report, glyphs: &GlyphSet) -> std::io::Result<()> {
let mut errors = 0usize;
let mut warnings = 0usize;
let mut infos = 0usize;
let mut fixable_violations = 0usize;
for r in &report.results {
if r.passed() {
continue;
}
let count = r.violations.len();
if r.is_fixable {
fixable_violations += count;
}
match r.level {
Level::Error => errors += count,
Level::Warning => warnings += count,
Level::Info => infos += count,
Level::Off => {} }
}
let total = errors + warnings + infos;
let failing = report.failing_rules();
let passing = report.passing_rules();
let dim = style::DIM;
let plural = if total == 1 { "" } else { "s" };
writeln!(w, "{dim}Summary ({total} violation{plural}):{dim:#}")?;
let mut parts: Vec<String> = Vec::new();
if errors > 0 {
let s = style::ERROR;
parts.push(format!(
"{s}{} {errors} error{e}{s:#}",
glyphs.error,
e = if errors == 1 { "" } else { "s" }
));
}
if warnings > 0 {
let s = style::WARNING;
parts.push(format!(
"{s}{} {warnings} warning{e}{s:#}",
glyphs.warning,
e = if warnings == 1 { "" } else { "s" }
));
}
if infos > 0 {
let s = style::INFO;
parts.push(format!("{s}{} {infos} info{s:#}", glyphs.info));
}
writeln!(w, " {}", parts.join(" "))?;
let bullet = glyphs.bullet;
let fixable_tag = if fixable_violations > 0 {
let fix = style::FIXABLE;
format!(" {dim}{bullet}{dim:#} {fix}{fixable_violations} auto-fixable{fix:#}")
} else {
String::new()
};
writeln!(
w,
" {passing} passing {dim}{bullet}{dim:#} {failing} failing{fixable_tag}",
)?;
if fixable_violations > 0 {
writeln!(w)?;
let fix = style::FIXABLE;
writeln!(
w,
" {arrow} run {fix}`alint fix`{fix:#} to resolve {fixable_violations} fixable violation{p}.",
arrow = glyphs.arrow,
p = if fixable_violations == 1 { "" } else { "s" }
)?;
}
Ok(())
}
fn write_human_compact(
report: &Report,
w: &mut dyn Write,
opts: &HumanOptions,
) -> std::io::Result<()> {
let mut errors = 0usize;
let mut warnings = 0usize;
let mut infos = 0usize;
let mut fixable = 0usize;
for result in &report.results {
if result.passed() {
continue;
}
for v in &result.violations {
let path = v
.path
.as_ref()
.map_or_else(|| "<repo>".to_string(), |p| p.display().to_string());
let line = v.line.unwrap_or(0);
let col = v.column.unwrap_or(0);
let (level_style, level_name) = match result.level {
Level::Error => {
errors += 1;
(style::ERROR, "error")
}
Level::Warning => {
warnings += 1;
(style::WARNING, "warning")
}
Level::Info => {
infos += 1;
(style::INFO, "info")
}
Level::Off => (style::DIM, "off"), };
if result.is_fixable {
fixable += 1;
}
let rule_style = style::RULE_ID;
let fix_tag = if result.is_fixable {
let fix = style::FIXABLE;
format!(" {fix}[fixable]{fix:#}")
} else {
String::new()
};
writeln!(
w,
"{path}:{line}:{col}: {level_style}{level_name}{level_style:#}: {rule_style}{}{rule_style:#}: {}{fix_tag}",
result.rule_id, v.message,
)?;
}
}
if errors == 0 && warnings == 0 && infos == 0 {
let s = style::SUCCESS;
writeln!(w, "{s}{} all rules passed.{s:#}", opts.glyphs.success)?;
return Ok(());
}
let mut parts: Vec<String> = Vec::new();
if errors > 0 {
let s = style::ERROR;
parts.push(format!(
"{s}{errors} error{p}{s:#}",
p = if errors == 1 { "" } else { "s" }
));
}
if warnings > 0 {
let s = style::WARNING;
parts.push(format!(
"{s}{warnings} warning{p}{s:#}",
p = if warnings == 1 { "" } else { "s" }
));
}
if infos > 0 {
let s = style::INFO;
parts.push(format!("{s}{infos} info{s:#}"));
}
let mut line = parts.join(", ");
if fixable > 0 {
use std::fmt::Write as _;
let fix = style::FIXABLE;
write!(line, "; {fix}{fixable} auto-fixable{fix:#}").ok();
}
writeln!(w, "{line}.")?;
Ok(())
}
pub fn write_fix_human(
report: &FixReport,
w: &mut dyn Write,
opts: HumanOptions,
) -> std::io::Result<()> {
let dim = style::DIM;
for rule in &report.results {
let (level_style, level_name) = match rule.level {
Level::Error => (style::ERROR, "error"),
Level::Warning => (style::WARNING, "warning"),
Level::Info => (style::INFO, "info"),
Level::Off => (style::DIM, "off"),
};
let rule_style = style::RULE_ID;
writeln!(
w,
"{level_style}{level_name}{level_style:#} {rule_style}[{}]{rule_style:#}:",
rule.rule_id
)?;
for item in &rule.items {
let path = item
.violation
.path
.as_ref()
.map(|p| format!("{} — ", p.display()))
.unwrap_or_default();
match &item.status {
FixStatus::Applied(summary) => {
let s = style::SUCCESS;
writeln!(w, " {s}{} {path}{summary}{s:#}", opts.glyphs.success)?;
}
FixStatus::Skipped(reason) => {
writeln!(
w,
" {dim}{} {path}{} (skipped: {reason}){dim:#}",
opts.glyphs.bullet, item.violation.message
)?;
}
FixStatus::Unfixable => {
writeln!(
w,
" {dim}{} {path}{} (no fixer){dim:#}",
opts.glyphs.bullet, item.violation.message
)?;
}
}
}
}
let applied = report.applied();
let skipped = report.skipped();
let unfixable = report.unfixable();
let ok = style::SUCCESS;
writeln!(
w,
"\n{ok}{applied} applied{ok:#}, {skipped} skipped, {unfixable} unfixable."
)?;
Ok(())
}
const MSG_INDENT: &str = " ";
fn level_presentation(
level: Level,
glyphs: &GlyphSet,
) -> (&'static str, anstyle::Style, &'static str) {
match level {
Level::Error => (glyphs.error, style::ERROR, "error "),
Level::Warning => (glyphs.warning, style::WARNING, "warning"),
Level::Info => (glyphs.info, style::INFO, "info "),
Level::Off => (glyphs.bullet, style::DIM, "off "),
}
}