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::hotspot::HotspotReport;
#[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, Default)]
pub struct HotspotIndex {
by_path: std::collections::HashMap<PathBuf, f64>,
calibration: Option<HotspotCalibration>,
}
impl HotspotIndex {
#[must_use]
pub fn new(report: Option<&HotspotReport>, cal: &Calibration) -> Self {
let by_path = report
.map(|h| {
h.entries
.iter()
.map(|e| (e.path.clone(), e.score))
.collect()
})
.unwrap_or_default();
Self {
by_path,
calibration: cal.calibration.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 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::change_coupling::ChangeCouplingFeature;
use crate::observer::complexity::ComplexityFeature;
use crate::observer::duplication::DuplicationFeature;
use crate::observer::hotspot::HotspotFeature;
use crate::observer::lcom::LcomFeature;
Self {
features: vec![
Box::new(ComplexityFeature),
Box::new(DuplicationFeature),
Box::new(ChangeCouplingFeature),
Box::new(HotspotFeature),
Box::new(LcomFeature),
],
}
}
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 hotspot = HotspotIndex::new(reports.hotspot.as_ref(), cal);
let mut findings = Vec::new();
for feature in self.enabled(cfg) {
findings.extend(feature.lower(reports, cfg, cal, &hotspot));
}
findings
}
}
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",
],
);
}
#[test]
fn every_builtin_is_observer_kind() {
for f in FeatureRegistry::builtin().iter() {
assert_eq!(
f.meta().kind,
FeatureKind::Observer,
"unexpected kind for {}",
f.meta().name,
);
}
}
}