use std::path::{Path, PathBuf};
use crate::core::calibration::{Calibration, HotspotCalibration};
use crate::core::config::Config;
use crate::core::finding::{Finding, Location};
use crate::core::severity::Severity;
use crate::observer::code::hotspot::HotspotReport;
use crate::observer::docs::hotspot::DocHotspotReport;
use crate::observer::test::hotspot::TestHotspotReport;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FeatureMeta {
pub name: &'static str,
pub version: u32,
pub kind: FeatureKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FeatureKind {
Observer,
DocsScanner,
CoverageReader,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Family {
Code,
Test,
Docs,
}
impl Family {
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::Code => "code",
Self::Test => "test",
Self::Docs => "docs",
}
}
#[must_use]
pub fn banner(self) -> &'static str {
match self {
Self::Code => "═══ Code ═══",
Self::Test => "═══ Test ═══",
Self::Docs => "═══ Docs ═══",
}
}
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Code => "Code",
Self::Test => "Test",
Self::Docs => "Docs",
}
}
#[must_use]
pub fn patch_skill(self) -> &'static str {
match self {
Self::Code => "/heal-code-patch",
Self::Test => "/heal-test-patch",
Self::Docs => "/heal-doc-patch",
}
}
#[must_use]
pub fn is_enabled(self, cfg: &Config) -> bool {
match self {
Self::Code => true,
Self::Test => cfg.features.test.enabled,
Self::Docs => cfg.features.docs.enabled,
}
}
#[must_use]
pub fn for_metric(metric: &str) -> Self {
match metric {
"doc_freshness" | "doc_drift" | "doc_coverage" | "doc_link_health" | "orphan_pages"
| "todo_density" | "doc_hotspot" => Self::Docs,
"coverage_pct" | "skip_ratio" | "test_hotspot" => Self::Test,
_ => Self::Code,
}
}
}
#[derive(Debug, Default)]
pub struct HotspotIndex {
by_path: std::collections::HashMap<PathBuf, f64>,
calibration: Option<HotspotCalibration>,
}
impl HotspotIndex {
fn from_entries(
entries: impl IntoIterator<Item = (PathBuf, f64)>,
calibration: Option<HotspotCalibration>,
) -> Self {
let mut by_path: std::collections::HashMap<PathBuf, f64> = std::collections::HashMap::new();
for (path, score) in entries {
by_path
.entry(path)
.and_modify(|v| {
if score > *v {
*v = score;
}
})
.or_insert(score);
}
Self {
by_path,
calibration,
}
}
#[must_use]
pub fn new(report: Option<&HotspotReport>, cal: &Calibration) -> Self {
Self::from_entries(
report
.into_iter()
.flat_map(|h| h.entries.iter().map(|e| (e.path.clone(), e.score))),
cal.calibration.hotspot.clone(),
)
}
#[must_use]
pub fn for_test(report: Option<&TestHotspotReport>, cal: &Calibration) -> Self {
Self::from_entries(
report
.into_iter()
.flat_map(|h| h.entries.iter().map(|e| (e.path.clone(), e.score))),
cal.calibration.test_hotspot.clone(),
)
}
#[must_use]
pub fn for_doc(report: Option<&DocHotspotReport>, cal: &Calibration) -> Self {
Self::from_entries(
report.into_iter().flat_map(|h| {
h.entries.iter().flat_map(|e| {
std::iter::once((e.doc_path.clone(), e.score))
.chain(e.src_paths.iter().map(|p| (p.clone(), e.score)))
})
}),
cal.calibration.doc_hotspot.clone(),
)
}
#[must_use]
pub fn is_hot(&self, path: &Path) -> bool {
match (&self.calibration, self.by_path.get(path)) {
(Some(c), Some(score)) => c.flag(*score),
_ => false,
}
}
#[must_use]
pub(crate) fn any_location_hot(&self, primary: &Location, locations: &[Location]) -> bool {
self.is_hot(&primary.file) || locations.iter().any(|l| self.is_hot(&l.file))
}
}
#[must_use]
pub fn decorate(mut f: Finding, severity: Severity, hotspot: &HotspotIndex) -> Finding {
f.severity = severity;
f.hotspot = hotspot.any_location_hot(&f.location, &f.locations);
f
}
pub trait Feature: Send + Sync {
fn meta(&self) -> FeatureMeta;
fn enabled(&self, cfg: &Config) -> bool;
fn family(&self) -> Family {
Family::Code
}
fn lower(
&self,
reports: &crate::observers::ObserverReports,
cfg: &Config,
cal: &Calibration,
hotspot: &HotspotIndex,
) -> Vec<Finding>;
}
pub struct FeatureRegistry {
features: Vec<Box<dyn Feature>>,
}
impl FeatureRegistry {
#[must_use]
pub fn builtin() -> Self {
use crate::observer::code::change_coupling::ChangeCouplingFeature;
use crate::observer::code::complexity::ComplexityFeature;
use crate::observer::code::duplication::DuplicationFeature;
use crate::observer::code::hotspot::HotspotFeature;
use crate::observer::code::lcom::LcomFeature;
use crate::observer::docs::coverage::DocCoverageFeature;
use crate::observer::docs::drift::DocDriftFeature;
use crate::observer::docs::freshness::DocFreshnessFeature;
use crate::observer::docs::hotspot::DocHotspotFeature;
use crate::observer::docs::link_health::DocLinkHealthFeature;
use crate::observer::docs::orphan_pages::OrphanPagesFeature;
use crate::observer::docs::todo_density::TodoDensityFeature;
use crate::observer::test::coverage::CoverageFeature;
use crate::observer::test::hotspot::TestHotspotFeature;
use crate::observer::test::skip_ratio::SkipRatioFeature;
Self {
features: vec![
Box::new(ComplexityFeature),
Box::new(DuplicationFeature),
Box::new(ChangeCouplingFeature),
Box::new(HotspotFeature),
Box::new(LcomFeature),
Box::new(DocFreshnessFeature),
Box::new(DocDriftFeature),
Box::new(DocCoverageFeature),
Box::new(DocLinkHealthFeature),
Box::new(OrphanPagesFeature),
Box::new(TodoDensityFeature),
Box::new(DocHotspotFeature),
Box::new(CoverageFeature),
Box::new(SkipRatioFeature),
Box::new(TestHotspotFeature),
],
}
}
pub fn iter(&self) -> impl Iterator<Item = &dyn Feature> {
self.features.iter().map(std::convert::AsRef::as_ref)
}
pub fn enabled<'a>(&'a self, cfg: &'a Config) -> impl Iterator<Item = &'a dyn Feature> + 'a {
self.iter().filter(move |f| f.enabled(cfg))
}
pub fn lower_all(
&self,
reports: &crate::observers::ObserverReports,
cfg: &Config,
cal: &Calibration,
) -> Vec<Finding> {
let code_hotspot = HotspotIndex::new(reports.hotspot.as_ref(), cal);
let test_hotspot = HotspotIndex::for_test(reports.test_hotspot.as_ref(), cal);
let doc_hotspot = HotspotIndex::for_doc(reports.doc_hotspot.as_ref(), cal);
let mut findings = Vec::new();
for feature in self.enabled(cfg) {
let idx = match feature.family() {
Family::Code => &code_hotspot,
Family::Test => &test_hotspot,
Family::Docs => &doc_hotspot,
};
findings.extend(feature.lower(reports, cfg, cal, idx));
}
if cfg.features.test.enabled {
tag_test_findings(&mut findings, cfg);
}
findings
}
}
fn tag_test_findings(findings: &mut [Finding], cfg: &Config) {
use crate::observer::shared::file_role::is_test_path;
use crate::observer::shared::walk::ExcludeMatcher;
let glob = if cfg.features.test.test_paths.is_empty() {
None
} else {
ExcludeMatcher::compile(Path::new(""), &cfg.features.test.test_paths).ok()
};
for f in findings.iter_mut() {
let path = &f.location.file;
let hit = match glob.as_ref() {
Some(m) => m.is_excluded(path, false),
None => is_test_path(path),
};
if hit {
f.is_test_file = true;
}
}
}
impl Default for FeatureRegistry {
fn default() -> Self {
Self::builtin()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builtin_registry_emits_one_feature_per_metric() {
let r = FeatureRegistry::builtin();
let names: Vec<&str> = r.iter().map(|f| f.meta().name).collect();
assert_eq!(
names,
vec![
"complexity",
"duplication",
"change_coupling",
"hotspot",
"lcom",
"doc_freshness",
"doc_drift",
"doc_coverage",
"doc_link_health",
"orphan_pages",
"todo_density",
"doc_hotspot",
"coverage_pct",
"skip_ratio",
"test_hotspot",
],
);
}
#[test]
fn every_code_feature_is_observer_kind() {
for f in FeatureRegistry::builtin().iter() {
let want_kind = match f.meta().name {
"doc_freshness" | "doc_drift" | "doc_coverage" | "doc_link_health"
| "orphan_pages" | "todo_density" | "doc_hotspot" => FeatureKind::DocsScanner,
"coverage_pct" => FeatureKind::CoverageReader,
_ => FeatureKind::Observer,
};
assert_eq!(
f.meta().kind,
want_kind,
"unexpected kind for {}",
f.meta().name,
);
}
}
#[test]
fn family_assignment_per_feature_name() {
for f in FeatureRegistry::builtin().iter() {
let want_family = match f.meta().name {
"doc_freshness" | "doc_drift" | "doc_coverage" | "doc_link_health"
| "orphan_pages" | "todo_density" | "doc_hotspot" => Family::Docs,
"coverage_pct" | "skip_ratio" | "test_hotspot" => Family::Test,
_ => Family::Code,
};
assert_eq!(
f.family(),
want_family,
"unexpected family for {}",
f.meta().name,
);
}
}
}