use crate::finding::Reason;
use cpd_core::deadcode::Category;
#[derive(Debug, Default, Clone, Copy)]
pub struct Evidence {
pub dynamic_module: bool,
pub name_in_string: bool,
pub decorated: bool,
pub abstract_declaration: bool,
pub overrides: bool,
pub package_surface: bool,
pub public_api: bool,
pub used_only_by_tests: bool,
pub in_test_file: bool,
pub unparsed_module: bool,
pub ambiguous_name: bool,
pub wildcarded: bool,
pub name_read_as_attribute: bool,
pub path_in_string: bool,
}
fn base_score(category: Category) -> u8 {
match category {
Category::UnusedImport => 100,
Category::UnusedFile => 95,
Category::UnusedSymbol => 90,
Category::UnusedExport => 85,
Category::UnusedMember => 70,
}
}
pub fn score(category: Category, evidence: &Evidence) -> (u8, Vec<Reason>) {
let mut reasons = Vec::new();
let mut add = |on: bool, reason: Reason| {
if on {
reasons.push(reason);
}
};
add(evidence.public_api, Reason::PublicApi);
add(evidence.abstract_declaration, Reason::Abstract);
add(evidence.overrides, Reason::Overrides);
add(evidence.decorated, Reason::Decorated);
add(evidence.name_in_string, Reason::NameAppearsInString);
add(evidence.path_in_string, Reason::PathAppearsInString);
add(evidence.dynamic_module, Reason::DynamicAccess);
add(evidence.wildcarded, Reason::WildcardReExport);
add(evidence.name_read_as_attribute, Reason::NameReadAsAttribute);
add(evidence.used_only_by_tests, Reason::UsedOnlyByTests);
add(evidence.ambiguous_name, Reason::AmbiguousName);
add(evidence.package_surface, Reason::PackageSurface);
add(evidence.unparsed_module, Reason::UnparsedModule);
add(evidence.in_test_file, Reason::InTestFile);
reasons.sort_by(|a, b| {
b.penalty()
.cmp(&a.penalty())
.then(a.explain().cmp(b.explain()))
});
let deduction: u32 = reasons.iter().map(|r| u32::from(r.penalty())).sum();
let score = u32::from(base_score(category)).saturating_sub(deduction) as u8;
(score, reasons)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_finding_with_nothing_against_it_scores_its_base() {
for category in Category::ALL {
let (score, reasons) = score(*category, &Evidence::default());
assert_eq!(score, base_score(*category), "{category:?}");
assert!(reasons.is_empty(), "{category:?}");
}
}
#[test]
fn categories_are_ranked_by_how_much_inference_they_need() {
let of = |c| score(c, &Evidence::default()).0;
assert!(of(Category::UnusedImport) > of(Category::UnusedFile));
assert!(of(Category::UnusedFile) > of(Category::UnusedSymbol));
assert!(of(Category::UnusedSymbol) > of(Category::UnusedExport));
assert!(of(Category::UnusedExport) > of(Category::UnusedMember));
}
#[test]
fn each_piece_of_evidence_costs_its_penalty() {
let (with, reasons) = score(
Category::UnusedExport,
&Evidence {
dynamic_module: true,
..Evidence::default()
},
);
assert_eq!(reasons, vec![Reason::DynamicAccess]);
assert_eq!(
with,
base_score(Category::UnusedExport) - Reason::DynamicAccess.penalty()
);
}
#[test]
fn reasons_are_ordered_strongest_first() {
let (_, reasons) = score(
Category::UnusedMember,
&Evidence {
in_test_file: true,
decorated: true,
dynamic_module: true,
..Evidence::default()
},
);
assert_eq!(
reasons,
vec![Reason::Decorated, Reason::DynamicAccess, Reason::InTestFile]
);
}
#[test]
fn overwhelming_evidence_floors_at_zero_rather_than_wrapping() {
let (score, reasons) = score(
Category::UnusedMember,
&Evidence {
dynamic_module: true,
name_in_string: true,
decorated: true,
abstract_declaration: true,
overrides: true,
package_surface: true,
public_api: true,
used_only_by_tests: true,
in_test_file: true,
unparsed_module: true,
ambiguous_name: true,
wildcarded: true,
name_read_as_attribute: true,
path_in_string: true,
},
);
assert_eq!(score, 0);
assert_eq!(reasons.len(), 14);
}
#[test]
fn a_file_whose_path_is_written_in_a_string_falls_below_the_default_threshold() {
let (score, reasons) = score(
Category::UnusedFile,
&Evidence {
path_in_string: true,
..Evidence::default()
},
);
assert_eq!(score, 55, "reported on request, not by default");
assert_eq!(reasons, vec![Reason::PathAppearsInString]);
}
#[test]
fn a_published_api_export_falls_below_the_default_threshold() {
let (score, _) = score(
Category::UnusedExport,
&Evidence {
public_api: true,
..Evidence::default()
},
);
assert!(
score < crate::config::BastaConfig::default().min_confidence,
"a package's published entry must not be reported by default, got {score}"
);
}
#[test]
fn an_unused_import_survives_a_single_weak_objection() {
let (score, _) = score(
Category::UnusedImport,
&Evidence {
in_test_file: true,
..Evidence::default()
},
);
assert!(
score >= crate::config::BastaConfig::default().min_confidence,
"an unreferenced import binding is a fact, not an inference, got {score}"
);
}
}