use std::collections::HashMap;
use rumdl_lib::rule::{LintWarning, Severity};
#[derive(PartialEq, Eq, Hash)]
struct WarningIdentity<'a> {
rule_name: Option<&'a str>,
message: &'a str,
severity: Severity,
}
impl<'a> WarningIdentity<'a> {
fn of(warning: &'a LintWarning) -> Self {
Self {
rule_name: warning.rule_name.as_deref(),
message: &warning.message,
severity: warning.severity,
}
}
}
pub struct FixReconciliation {
fixed: Vec<bool>,
}
impl FixReconciliation {
pub fn per_warning(&self) -> &[bool] {
&self.fixed
}
pub fn fixed_count(&self) -> usize {
self.fixed.iter().filter(|&&was_fixed| was_fixed).count()
}
}
pub fn reconcile_fixed_warnings(all_warnings: &[LintWarning], remaining_warnings: &[LintWarning]) -> FixReconciliation {
let mut net_resolved: HashMap<Option<&str>, usize> = HashMap::new();
for warning in all_warnings {
*net_resolved.entry(warning.rule_name.as_deref()).or_insert(0) += 1;
}
let mut survivors: HashMap<WarningIdentity<'_>, usize> = HashMap::new();
for warning in remaining_warnings {
*survivors.entry(WarningIdentity::of(warning)).or_insert(0) += 1;
let remaining_for_rule = net_resolved.entry(warning.rule_name.as_deref()).or_insert(0);
*remaining_for_rule = remaining_for_rule.saturating_sub(1);
}
let mut claim_order: Vec<usize> = (0..all_warnings.len()).collect();
claim_order.sort_by_key(|&index| (all_warnings[index].fix.is_some(), index));
let mut unmatched: Vec<usize> = Vec::new();
for index in claim_order {
if !claim_survivor(&mut survivors, &all_warnings[index]) {
unmatched.push(index);
}
}
unmatched.sort_by_key(|&index| (all_warnings[index].fix.is_none(), index));
let mut fixed = vec![false; all_warnings.len()];
for index in unmatched {
let rule_budget = net_resolved
.entry(all_warnings[index].rule_name.as_deref())
.or_insert(0);
if *rule_budget > 0 {
*rule_budget -= 1;
fixed[index] = true;
}
}
FixReconciliation { fixed }
}
fn claim_survivor<'a>(survivors: &mut HashMap<WarningIdentity<'a>, usize>, warning: &'a LintWarning) -> bool {
match survivors.get_mut(&WarningIdentity::of(warning)) {
Some(count) if *count > 0 => {
*count -= 1;
true
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use rumdl_lib::rule::{Fix, LintWarning};
fn warning(rule: &str, line: usize, message: &str, fix: Option<Fix>) -> LintWarning {
LintWarning {
message: message.to_string(),
line,
column: 1,
end_line: line,
end_column: 2,
severity: Severity::Warning,
fix,
rule_name: Some(rule.to_string()),
}
}
fn escalated(mut warning: LintWarning) -> LintWarning {
warning.severity = Severity::Error;
warning
}
fn some_fix() -> Option<Fix> {
Some(Fix::new(0..1, String::new()))
}
fn reconcile(all: &[LintWarning], remaining: &[LintWarning]) -> Vec<bool> {
reconcile_fixed_warnings(all, remaining).per_warning().to_vec()
}
#[test]
fn a_warning_that_moved_but_survived_is_not_fixed() {
let all = vec![warning("MD052", 7, "Reference 'zz' not found", None)];
let remaining = vec![warning("MD052", 9, "Reference 'zz' not found", None)];
assert_eq!(reconcile(&all, &remaining), vec![false]);
}
#[test]
fn a_document_level_fix_is_credited_without_a_per_warning_fix() {
let all = vec![warning("MD046", 5, "Use fenced code blocks", None)];
assert_eq!(reconcile(&all, &[]), vec![true]);
}
#[test]
fn a_warning_another_rule_resolved_is_credited() {
let all = vec![
warning("MD009", 3, "Trailing spaces", some_fix()),
warning("MD013", 3, "Line length 82 exceeds 80 characters", None),
];
assert_eq!(reconcile(&all, &[]), vec![true, true]);
}
#[test]
fn a_run_that_changed_nothing_credits_nothing() {
let all = vec![
warning("MD046", 5, "Use fenced code blocks", None),
warning("MD013", 9, "Line length 82 exceeds 80 characters", None),
];
assert_eq!(reconcile(&all, &all), vec![false, false]);
}
#[test]
fn identical_warnings_are_credited_one_per_disappearance() {
let all = vec![
warning("MD009", 1, "Trailing spaces", some_fix()),
warning("MD009", 2, "Trailing spaces", some_fix()),
warning("MD009", 3, "Trailing spaces", some_fix()),
];
let remaining = vec![warning("MD009", 2, "Trailing spaces", some_fix())];
let reconciled = reconcile(&all, &remaining);
assert_eq!(reconciled.iter().filter(|&&f| f).count(), 2);
}
#[test]
fn one_of_two_equivalent_warnings_disappearing_credits_the_one_that_was_fixable() {
let all = vec![
warning("MD040", 1, "Code block missing language", None),
warning("MD040", 5, "Code block missing language", some_fix()),
];
let remaining = vec![warning("MD040", 1, "Code block missing language", None)];
assert_eq!(reconcile(&all, &remaining), vec![false, true]);
}
#[test]
fn crediting_does_not_depend_on_the_order_the_warnings_were_reported() {
let all = vec![
warning("MD040", 1, "Code block missing language", some_fix()),
warning("MD040", 5, "Code block missing language", None),
];
let remaining = vec![warning("MD040", 5, "Code block missing language", None)];
assert_eq!(reconcile(&all, &remaining), vec![true, false]);
}
#[test]
fn no_more_warnings_are_credited_than_actually_disappeared() {
let all = vec![
warning("MD009", 1, "Trailing spaces", some_fix()),
warning("MD009", 2, "Trailing spaces", some_fix()),
warning("MD009", 3, "Trailing spaces", some_fix()),
];
let remaining = vec![
warning("MD009", 1, "Trailing spaces", some_fix()),
warning("MD009", 2, "Trailing spaces", some_fix()),
];
let reconciled = reconcile(&all, &remaining);
assert_eq!(reconciled.iter().filter(|&&f| f).count(), 1);
}
#[test]
fn a_reworded_survivor_is_not_counted_as_a_disappearance() {
let all = vec![
warning("MD013", 3, "Line length 131 exceeds 80 characters", some_fix()),
warning("MD013", 5, "Line length 90 exceeds 80 characters", some_fix()),
];
let remaining = vec![warning("MD013", 5, "Line length 86 exceeds 80 characters", some_fix())];
let reconciled = reconcile(&all, &remaining);
assert_eq!(reconciled.iter().filter(|&&f| f).count(), 1);
}
#[test]
fn a_survivor_whose_message_names_a_moved_line_is_not_credited() {
let all = vec![warning("MD053", 7, "Unused link/image reference: [a] (line 7)", None)];
let remaining = vec![warning("MD053", 9, "Unused link/image reference: [a] (line 9)", None)];
assert_eq!(reconcile(&all, &remaining), vec![false]);
}
#[test]
fn a_rule_that_gained_findings_is_credited_for_none_of_them() {
let all = vec![warning("MD012", 4, "Multiple consecutive blank lines", some_fix())];
let remaining = vec![
warning("MD012", 6, "Multiple consecutive blank lines [expected: 1]", some_fix()),
warning("MD012", 9, "Multiple consecutive blank lines [expected: 1]", some_fix()),
];
assert_eq!(reconcile(&all, &remaining), vec![false]);
}
#[test]
fn warnings_differing_only_in_severity_are_not_interchangeable() {
let all = vec![
warning("MD040", 1, "Code block missing language", some_fix()),
escalated(warning("MD040", 5, "Code block missing language", some_fix())),
];
let remaining = vec![escalated(warning(
"MD040",
5,
"Code block missing language",
some_fix(),
))];
assert_eq!(reconcile(&all, &remaining), vec![true, false]);
}
#[test]
fn warnings_differing_only_in_message_are_not_interchangeable() {
let all = vec![
warning("MD075", 5, "Orphaned table row(s)", some_fix()),
warning("MD075", 9, "Table missing header/delimiter", some_fix()),
];
let remaining = vec![warning("MD075", 8, "Table missing header/delimiter", some_fix())];
assert_eq!(reconcile(&all, &remaining), vec![true, false]);
}
#[test]
fn the_survivor_decides_which_warning_is_credited_not_the_order() {
let all = vec![
warning("MD075", 5, "Table missing header/delimiter", some_fix()),
warning("MD075", 9, "Orphaned table row(s)", some_fix()),
];
let remaining = vec![warning("MD075", 4, "Table missing header/delimiter", some_fix())];
assert_eq!(reconcile(&all, &remaining), vec![false, true]);
}
#[test]
fn a_disappearance_is_credited_to_the_warning_the_run_could_act_on() {
let all = vec![
warning("MD013", 3, "Line length 90 exceeds 80 characters", None),
warning("MD013", 5, "Line length exceeds 80 characters", some_fix()),
];
let remaining = vec![warning("MD013", 3, "Line length 86 exceeds 80 characters", None)];
assert_eq!(reconcile(&all, &remaining), vec![false, true]);
}
#[test]
fn one_rule_disappearing_never_credits_another_rules_warning() {
let all = vec![
warning("MD009", 1, "Trailing spaces", some_fix()),
warning("MD052", 3, "Reference 'zz' not found", None),
warning("MD009", 5, "Trailing spaces", some_fix()),
];
let remaining = vec![warning("MD052", 3, "Reference 'zz' not found", None)];
assert_eq!(reconcile(&all, &remaining), vec![true, false, true]);
}
}