use std::{
collections::{BTreeMap, HashSet},
path::{Path, PathBuf},
time::Duration,
};
use crate::diagnostic::{Diagnostic, Severity};
use crate::sarif;
use crate::{args, rules::rule::Rules};
use crate::{checker::CheckFileResult, config::Config};
fn display_settings(path: &Path, config: &Config, rules: &Rules) {
println!("Settings for file: {}", path.display());
println!(" {config:?}");
let rules_names = rules
.enabled
.iter()
.map(|r| r.name())
.collect::<Vec<&str>>()
.join(", ");
println!(
" Rules enabled: {}",
if rules_names.is_empty() {
"<none>"
} else {
&rules_names
}
);
}
fn display_diagnostics_human(result: &[CheckFileResult], args: &args::CheckArgs) {
let mut diags: Vec<&Diagnostic> = result.iter().flat_map(|x| &x.diagnostics).collect();
match args.sort {
args::CheckSort::Line => {
diags.sort_by_cached_key(|diag| {
(
diag.path.clone(),
diag.lines
.iter()
.map(|l| l.line_number)
.collect::<Vec<usize>>(),
)
});
}
args::CheckSort::Message => {
diags.sort_by_cached_key(|diag| {
(
diag.lines
.first()
.map_or_else(String::new, |line| line.message.clone()),
diag.path.clone(),
diag.lines
.iter()
.map(|l| l.line_number)
.collect::<Vec<usize>>(),
)
});
}
args::CheckSort::Rule => {
diags.sort_by_cached_key(|diag| {
(
diag.rule,
diag.path.clone(),
diag.lines
.iter()
.map(|l| l.line_number)
.collect::<Vec<usize>>(),
)
});
}
}
for diag in diags {
println!("{diag}");
}
}
fn display_rule_stats(result: &[CheckFileResult]) {
let mut count_rule_errors = BTreeMap::<&str, usize>::new();
for rule in result.iter().flat_map(|x| &x.diagnostics).map(|r| r.rule) {
*count_rule_errors.entry(rule).or_insert(0) += 1;
}
let mut items: Vec<_> = count_rule_errors.iter().collect();
if items.is_empty() {
println!("No errors found.");
return;
}
items.sort_by(|a, b| b.1.cmp(a.1));
println!("Errors by rule:");
for (rule, count) in items {
println!(" {rule}: {count}");
}
}
fn display_file_stats(file_errors: &[(PathBuf, usize, usize, usize)]) {
for (filename, info, warnings, errors) in file_errors {
if errors + warnings + info == 0 {
println!("{}: all OK!", filename.display());
} else {
println!(
"{}: {} problems ({} errors, {} warnings, {} info)",
filename.display(),
errors + warnings + info,
errors,
warnings,
info,
);
}
}
}
fn display_diagnostics_json(result: &[CheckFileResult], _args: &args::CheckArgs) {
let diags: Vec<&Diagnostic> = result.iter().flat_map(|x| &x.diagnostics).collect();
println!("{}", serde_json::to_string(&diags).unwrap_or_default());
}
fn display_diagnostics_sarif(result: &[CheckFileResult]) {
let sarif_log = sarif::build_sarif(result);
println!("{}", serde_json::to_string(&sarif_log).unwrap_or_default());
}
fn display_misspelled_words(result: &[CheckFileResult], _args: &args::CheckArgs) {
let hash_misspelled_words: HashSet<_> = result
.iter()
.flat_map(|x| &x.diagnostics)
.flat_map(|d| d.misspelled_words.iter())
.collect::<HashSet<_>>();
let mut misspelled_words = hash_misspelled_words.iter().copied().collect::<Vec<_>>();
misspelled_words.sort_unstable();
for word in misspelled_words {
println!("{word}");
}
}
pub fn display_result(
result: &[CheckFileResult],
args: &args::CheckArgs,
elapsed: &Duration,
) -> i32 {
let mut files_checked = 0;
let mut files_with_errors = 0;
let mut count_info = 0;
let mut count_warnings = 0;
let mut count_errors = 0;
let mut file_errors: Vec<(PathBuf, usize, usize, usize)> = Vec::new();
for file in result {
if args.show_settings && !args.quiet {
display_settings(file.path.as_path(), &file.config, &file.rules);
}
let mut count_file_info = 0;
let mut count_file_warnings = 0;
let mut count_file_errors = 0;
files_checked += 1;
if !file.diagnostics.is_empty() {
files_with_errors += 1;
for diag in &file.diagnostics {
match diag.severity {
Severity::Info => {
count_info += 1;
count_file_info += 1;
}
Severity::Warning => {
count_warnings += 1;
count_file_warnings += 1;
}
Severity::Error => {
count_errors += 1;
count_file_errors += 1;
}
}
}
}
if args.file_stats {
file_errors.push((
file.path.clone(),
count_file_info,
count_file_warnings,
count_file_errors,
));
}
}
if !args.quiet {
match args.output {
args::CheckOutputFormat::Human => {
if !args.no_errors {
display_diagnostics_human(result, args);
}
if args.rule_stats {
display_rule_stats(result);
}
if args.file_stats {
file_errors.sort();
display_file_stats(&file_errors);
}
}
args::CheckOutputFormat::Json => {
if !args.no_errors {
display_diagnostics_json(result, args);
}
}
args::CheckOutputFormat::Sarif => {
if !args.no_errors {
display_diagnostics_sarif(result);
}
}
args::CheckOutputFormat::Misspelled => {
if !args.no_errors {
display_misspelled_words(result, args);
}
}
}
}
if files_with_errors == 0 {
if !args.quiet && args.output == args::CheckOutputFormat::Human {
if files_checked > 0 {
println!("{files_checked} files checked: all OK! [{elapsed:?}]");
} else {
println!("No files checked [{elapsed:?}]");
}
}
0
} else {
if !args.quiet && args.output == args::CheckOutputFormat::Human {
println!(
"{files_checked} files checked: \
{} problems \
in {files_with_errors} files \
({count_errors} errors, \
{count_warnings} warnings, \
{count_info} info) \
[{elapsed:?}]",
count_errors + count_warnings + count_info
);
}
if args.output == args::CheckOutputFormat::Misspelled {
return 0;
}
1
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diagnostic::Diagnostic;
fn default_check_args() -> args::CheckArgs {
args::CheckArgs {
files: vec![],
show_settings: false,
config: None,
no_config: false,
fuzzy: false,
noqa: false,
obsolete: false,
select: None,
ignore: None,
path_msgfmt: None,
path_dicts: None,
path_words: None,
lang_id: None,
langs: None,
short_factor: None,
long_factor: None,
severity: vec![],
punc_ignore_ellipsis: false,
no_errors: false,
sort: args::CheckSort::default(),
rule_stats: false,
file_stats: false,
output: args::CheckOutputFormat::default(),
quiet: false,
}
}
fn diag(rule: &'static str, severity: Severity) -> Diagnostic {
Diagnostic::new(Path::new("test.po"), rule, severity, "msg".to_string())
}
fn file_result(path: &str, diagnostics: Vec<Diagnostic>) -> CheckFileResult {
CheckFileResult {
path: PathBuf::from(path),
diagnostics,
..CheckFileResult::default()
}
}
#[test]
fn test_display_result_no_files_returns_zero() {
let args = default_check_args();
let code = display_result(&[], &args, &Duration::from_millis(0));
assert_eq!(code, 0);
}
#[test]
fn test_display_result_all_clean_returns_zero() {
let args = default_check_args();
let result = vec![file_result("a.po", vec![]), file_result("b.po", vec![])];
let code = display_result(&result, &args, &Duration::from_millis(0));
assert_eq!(code, 0);
}
#[test]
fn test_display_result_info_diagnostic_returns_one() {
let args = default_check_args();
let result = vec![file_result("a.po", vec![diag("brackets", Severity::Info)])];
let code = display_result(&result, &args, &Duration::from_millis(0));
assert_eq!(code, 1);
}
#[test]
fn test_display_result_warning_diagnostic_returns_one() {
let args = default_check_args();
let result = vec![file_result("a.po", vec![diag("blank", Severity::Warning)])];
let code = display_result(&result, &args, &Duration::from_millis(0));
assert_eq!(code, 1);
}
#[test]
fn test_display_result_error_diagnostic_returns_one() {
let args = default_check_args();
let result = vec![file_result("a.po", vec![diag("escapes", Severity::Error)])];
let code = display_result(&result, &args, &Duration::from_millis(0));
assert_eq!(code, 1);
}
#[test]
fn test_display_result_misspelled_mode_returns_zero_even_with_diags() {
let mut args = default_check_args();
args.output = args::CheckOutputFormat::Misspelled;
let result = vec![file_result(
"a.po",
vec![diag("spelling-str", Severity::Info)],
)];
let code = display_result(&result, &args, &Duration::from_millis(0));
assert_eq!(code, 0);
}
#[test]
fn test_display_result_misspelled_mode_no_diags_returns_zero() {
let mut args = default_check_args();
args.output = args::CheckOutputFormat::Misspelled;
let result = vec![file_result("a.po", vec![])];
let code = display_result(&result, &args, &Duration::from_millis(0));
assert_eq!(code, 0);
}
#[test]
fn test_display_result_quiet_with_errors_still_returns_one() {
let mut args = default_check_args();
args.quiet = true;
let result = vec![file_result("a.po", vec![diag("escapes", Severity::Error)])];
let code = display_result(&result, &args, &Duration::from_millis(0));
assert_eq!(code, 1);
}
#[test]
fn test_display_result_no_errors_flag_does_not_change_exit_code() {
let mut args = default_check_args();
args.no_errors = true;
let result = vec![file_result("a.po", vec![diag("blank", Severity::Warning)])];
let code = display_result(&result, &args, &Duration::from_millis(0));
assert_eq!(code, 1);
}
#[test]
fn test_display_result_json_output_returns_one_on_errors() {
let mut args = default_check_args();
args.output = args::CheckOutputFormat::Json;
let result = vec![file_result("a.po", vec![diag("escapes", Severity::Error)])];
let code = display_result(&result, &args, &Duration::from_millis(0));
assert_eq!(code, 1);
}
#[test]
fn test_display_result_sarif_output_returns_one_on_errors() {
let mut args = default_check_args();
args.output = args::CheckOutputFormat::Sarif;
let result = vec![file_result("a.po", vec![diag("escapes", Severity::Error)])];
let code = display_result(&result, &args, &Duration::from_millis(0));
assert_eq!(code, 1);
}
#[test]
fn test_display_result_with_rule_and_file_stats_flags() {
let mut args = default_check_args();
args.rule_stats = true;
args.file_stats = true;
let result = vec![
file_result(
"a.po",
vec![
diag("blank", Severity::Warning),
diag("escapes", Severity::Error),
],
),
file_result("b.po", vec![diag("brackets", Severity::Info)]),
];
let code = display_result(&result, &args, &Duration::from_millis(0));
assert_eq!(code, 1);
}
#[test]
fn test_display_result_mixed_severities_returns_one() {
let args = default_check_args();
let result = vec![file_result(
"a.po",
vec![
diag("brackets", Severity::Info),
diag("blank", Severity::Warning),
diag("escapes", Severity::Error),
],
)];
let code = display_result(&result, &args, &Duration::from_millis(0));
assert_eq!(code, 1);
}
}