use std::{collections::HashMap, env, io::IsTerminal, time::Duration};
use colored::*;
use crate::{
config::{Config, RuleLevel},
rules::{Category, Violation},
};
pub fn report_violations(
violations: &[Violation],
verbose: bool,
config: &Config,
duration: Duration,
) {
let use_hyperlinks = std::io::stdout().is_terminal();
if violations.is_empty() {
if verbose {
println!("{}", "No violations found!".green().bold());
}
return;
}
println!(
"{}",
format!("Found {} violation(s):", violations.len())
.red()
.bold()
);
println!();
for violation in violations {
let file_link = create_clickable_link(
&violation.file,
violation.line,
violation.column,
&config.editor_url,
use_hyperlinks,
);
println!("{}", file_link);
if let Some(ref snippet) = violation.snippet {
for line in snippet.lines() {
println!(" {}", line.dimmed());
}
}
let level_label = match violation.level {
RuleLevel::Error => "error:".red().bold(),
RuleLevel::Warn => "warning:".yellow().bold(),
RuleLevel::Ignore => {
unreachable!("Ignored violations should not be reported")
}
};
println!(" {} {}", level_label, violation.message);
println!(" {} {}", "rule:".blue(), violation.rule);
println!();
}
println!(
"{} violation(s) in {}",
violations.len().to_string().yellow().bold(),
format_duration(duration).dimmed(),
);
}
pub fn report_summary(
violations: &[Violation],
fixable: &HashMap<&str, bool>,
rule_timings: &[(&str, Duration)],
duration: Duration,
) {
if violations.is_empty() {
println!("{}", "No violations found!".green().bold());
return;
}
let mut counts: HashMap<&str, usize> = HashMap::new();
let mut levels: HashMap<&str, RuleLevel> = HashMap::new();
let mut categories: HashMap<&str, Category> = HashMap::new();
for v in violations {
*counts.entry(v.rule).or_insert(0) += 1;
levels.entry(v.rule).or_insert(v.level);
categories.entry(v.rule).or_insert(v.category);
}
let timings: HashMap<&str, Duration> = rule_timings.iter().copied().collect();
let mut rows: Vec<(&str, usize, RuleLevel, Category, bool, String)> = counts
.iter()
.map(|(&rule, &count)| {
let time_str = timings
.get(rule)
.map(|d| format_duration(*d))
.unwrap_or_default();
(
rule,
count,
*levels.get(rule).unwrap(),
*categories.get(rule).unwrap(),
*fixable.get(rule).unwrap_or(&false),
time_str,
)
})
.collect();
rows.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
let count_w = rows
.iter()
.map(|(_, c, ..)| c.to_string().len())
.max()
.unwrap_or(0)
.max("Count".len());
let rule_w = rows
.iter()
.map(|(r, ..)| r.len())
.max()
.unwrap_or(0)
.max("Rule".len());
let level_w = "Level".len();
let fix_w = "Autofix".len();
let cat_w = rows
.iter()
.map(|(_, _, _, cat, ..)| cat.to_string().len())
.max()
.unwrap_or(0)
.max("Category".len());
let time_w = rows
.iter()
.map(|(.., t)| t.len())
.max()
.unwrap_or(0)
.max("Time".len());
let top = format!(
"┌{:─<cw$}┬{:─<rw$}┬{:─<lw$}┬{:─<fw$}┬{:─<aw$}┬{:─<tw$}┐",
"",
"",
"",
"",
"",
"",
cw = count_w + 2,
rw = rule_w + 2,
lw = level_w + 2,
fw = fix_w + 2,
aw = cat_w + 2,
tw = time_w + 2,
);
let sep = format!(
"├{:─<cw$}┼{:─<rw$}┼{:─<lw$}┼{:─<fw$}┼{:─<aw$}┼{:─<tw$}┤",
"",
"",
"",
"",
"",
"",
cw = count_w + 2,
rw = rule_w + 2,
lw = level_w + 2,
fw = fix_w + 2,
aw = cat_w + 2,
tw = time_w + 2,
);
let bot = format!(
"└{:─<cw$}┴{:─<rw$}┴{:─<lw$}┴{:─<fw$}┴{:─<aw$}┴{:─<tw$}┘",
"",
"",
"",
"",
"",
"",
cw = count_w + 2,
rw = rule_w + 2,
lw = level_w + 2,
fw = fix_w + 2,
aw = cat_w + 2,
tw = time_w + 2,
);
println!("{}", top);
println!(
"│ {:<cw$} │ {:<rw$} │ {:<lw$} │ {:<fw$} │ {:<aw$} │ {:<tw$} │",
"Count".bold(),
"Rule".bold(),
"Level".bold(),
"Autofix".bold(),
"Category".bold(),
"Time".bold(),
cw = count_w,
rw = rule_w,
lw = level_w,
fw = fix_w,
aw = cat_w,
tw = time_w,
);
println!("{}", sep);
for (rule, count, level, category, is_fixable, time_str) in &rows {
let count_str = count.to_string().yellow().to_string();
let rule_str = rule.cyan().to_string();
let (level_str, level_len) = match level {
RuleLevel::Error => ("error".red().to_string(), 5),
RuleLevel::Warn => ("warn".yellow().to_string(), 4),
RuleLevel::Ignore => {
unreachable!("Ignored violations should not be in summary")
}
};
let fix_str = if *is_fixable {
"Yes".green().to_string()
} else {
"No".dimmed().to_string()
};
let cat_display = category.to_string();
let cat_str = cat_display.magenta().to_string();
let time_colored = time_str.dimmed().to_string();
let count_pad = count_w - count.to_string().len();
let rule_pad = rule_w - rule.len();
let level_pad = level_w - level_len;
let fix_pad = fix_w - if *is_fixable { 3 } else { 2 };
let cat_pad = cat_w - cat_display.len();
let time_pad = time_w - time_str.len();
println!(
"│ {}{} │ {}{} │ {}{} │ {}{} │ {}{} │ {}{} │",
count_str,
" ".repeat(count_pad),
rule_str,
" ".repeat(rule_pad),
level_str,
" ".repeat(level_pad),
fix_str,
" ".repeat(fix_pad),
cat_str,
" ".repeat(cat_pad),
time_colored,
" ".repeat(time_pad),
);
}
println!("{}", bot);
let total_count: usize = counts.values().sum();
println!(
" {} violation(s) across {} rule(s) in {}",
total_count.to_string().yellow().bold(),
rows.len().to_string().yellow().bold(),
format_duration(duration).dimmed(),
);
}
pub fn format_duration(d: Duration) -> String {
let secs = d.as_secs_f64();
if secs < 1.0 {
format!("{:.0}ms", secs * 1000.0)
} else {
format!("{:.2}s", secs)
}
}
fn create_clickable_link(
file_path: &std::path::Path,
line: usize,
column: usize,
editor_url_template: &Option<String>,
use_hyperlinks: bool,
) -> String {
let abs_path = if file_path.is_absolute() {
file_path
} else {
match env::current_dir() {
Ok(cwd) => &cwd.join(file_path),
Err(_) => file_path,
}
};
let location = format!("{}:{}:{}", abs_path.display(), line, column);
if !use_hyperlinks {
return location;
}
let file_url = if let Some(template) = editor_url_template {
template
.replace("{path}", &abs_path.display().to_string())
.replace("{line}", &line.to_string())
.replace("{column}", &column.to_string())
} else {
format!("file://{}", abs_path.display())
};
let hyperlink = format!(
"\x1b]8;;{}\x1b\\{}\x1b]8;;\x1b\\",
file_url,
location.cyan()
);
hyperlink
}