use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde::Deserialize;
use super::model::{Report, Warning, WarningKind};
use crate::structure::DirStats;
use crate::units::{Cognitive, Cyclomatic, Depth, Lines, Params, Tokens};
pub struct Baseline {
known: HashSet<Key>,
sizes: Sizes,
}
type Key = (PathBuf, WarningKind, Option<String>);
type FnSizes = [usize; 6];
#[derive(Deserialize)]
struct SavedReport {
files: Vec<SavedFile>,
functions: Vec<SavedFn>,
#[serde(default)]
directories: Vec<DirStats>,
warnings: Vec<SavedWarning>,
}
#[derive(Deserialize)]
struct SavedFile {
path: PathBuf,
tokens: Tokens,
lines: Lines,
}
#[derive(Deserialize)]
struct SavedFn {
path: PathBuf,
name: String,
lines: Lines,
tokens: Tokens,
#[serde(default)]
depth: Depth,
#[serde(default)]
params: Option<Params>,
#[serde(default)]
cyclomatic: Cyclomatic,
#[serde(default)]
cognitive: Cognitive,
}
#[derive(Deserialize)]
struct SavedWarning {
path: PathBuf,
kind: WarningKind,
#[serde(default)]
subject: Option<String>,
}
#[derive(Default)]
struct Sizes {
files: HashMap<PathBuf, [usize; 3]>,
functions: HashMap<(PathBuf, String), FnSizes>,
dirs: HashMap<PathBuf, [usize; 3]>,
}
impl Baseline {
pub fn load(path: &Path) -> Result<Self> {
let text = std::fs::read_to_string(path)
.with_context(|| format!("cannot read baseline {}", path.display()))?;
let saved: SavedReport = serde_json::from_str(&text)
.with_context(|| format!("{} is not a sarah JSON report", path.display()))?;
Ok(Self::from_saved(saved))
}
fn from_saved(saved: SavedReport) -> Self {
let SavedReport {
files,
functions,
directories,
warnings,
} = saved;
let mut sizes = Sizes::default();
for f in files {
sizes.files.insert(f.path, [f.tokens.0, f.lines.0, 0]);
}
for f in functions {
let fn_sizes = [
f.lines.0,
f.tokens.0,
f.depth.0,
f.params.map_or(0, |p| p.0),
f.cyclomatic.0,
f.cognitive.0,
];
sizes.add_function((f.path, f.name), fn_sizes);
}
sizes.add_dirs(&directories);
let known = warnings
.into_iter()
.map(|w| (w.path, w.kind, w.subject))
.collect();
Self { known, sizes }
}
fn covers(&self, warning: &Warning, current: &Sizes) -> bool {
let key = (warning.path.clone(), warning.kind, warning.subject.clone());
if !self.known.contains(&key) {
return false;
}
match (current.of(warning), self.sizes.of(warning)) {
(Some(now), Some(then)) => now.iter().zip(then).all(|(n, t)| *n <= t),
_ => true,
}
}
}
impl Sizes {
fn of_report(report: &Report) -> Self {
let mut sizes = Self::default();
for f in &report.files {
sizes
.files
.insert(f.path.clone(), [f.tokens.0, f.lines.0, 0]);
}
for f in &report.functions {
let fn_sizes = [
f.lines.0,
f.tokens.0,
f.depth.0,
f.params.map_or(0, |p| p.0),
f.cyclomatic.0,
f.cognitive.0,
];
sizes.add_function((f.path.clone(), f.name.clone()), fn_sizes);
}
sizes.add_dirs(&report.directories);
sizes
}
fn add_function(&mut self, key: (PathBuf, String), sizes: FnSizes) {
let entry = self.functions.entry(key).or_default();
for (e, s) in entry.iter_mut().zip(sizes) {
*e = (*e).max(s);
}
}
fn add_dirs(&mut self, dirs: &[DirStats]) {
for d in dirs {
self.dirs
.insert(d.path.clone(), [d.files, d.dirs, d.tokens.0]);
}
}
fn of(&self, w: &Warning) -> Option<Vec<usize>> {
match w.kind {
WarningKind::LargeFile => self.files.get(&w.path).map(|s| s.to_vec()),
WarningKind::LargeFunction
| WarningKind::DeepFunction
| WarningKind::TooManyParams
| WarningKind::ComplexFunction => {
let name = w.subject.clone()?;
let sizes = self.functions.get(&(w.path.clone(), name))?;
Some(sizes.to_vec())
}
WarningKind::FolderTooManyFiles
| WarningKind::FolderTooManyDirs
| WarningKind::FolderTooManyTokens => self.dirs.get(&w.path).map(|s| s.to_vec()),
WarningKind::FolderTooDeep
| WarningKind::FolderTooFewEntries
| WarningKind::FolderMissingRequired
| WarningKind::FolderForbiddenName
| WarningKind::FolderBadName
| WarningKind::FolderSiblingModule
| WarningKind::FolderUnexpectedDir
| WarningKind::BadAllow => None,
}
}
}
impl Report {
pub fn apply_baseline(&mut self, baseline: &Baseline) {
let current = Sizes::of_report(self);
let before = self.warnings.len();
self.warnings.retain(|w| !baseline.covers(w, ¤t));
self.baselined = Some(before.saturating_sub(self.warnings.len()));
}
}
#[cfg(test)]
mod tests {
use super::*;
fn saved(json: &str) -> Baseline {
Baseline::from_saved(serde_json::from_str(json).unwrap())
}
fn warning(kind: WarningKind, subject: Option<&str>) -> Warning {
let w = Warning::new("a.rs".into(), kind, String::new());
match subject {
Some(s) => w.about(s.into()),
None => w,
}
}
#[test]
fn known_warnings_hide_until_they_grow() {
let baseline = saved(
r#"{"files":[{"path":"a.rs","tokens":100,"lines":10}],
"functions":[{"path":"a.rs","name":"f","lines":50,"tokens":9}],
"warnings":[{"path":"a.rs","kind":"large_file","message":""},
{"path":"a.rs","kind":"large_function","subject":"f","message":""}]}"#,
);
let mut current = Sizes::default();
current.files.insert("a.rs".into(), [100, 10, 0]);
current
.functions
.insert(("a.rs".into(), "f".into()), [51, 9, 0, 0, 1, 0]);
assert!(baseline.covers(&warning(WarningKind::LargeFile, None), ¤t));
assert!(!baseline.covers(&warning(WarningKind::LargeFunction, Some("f")), ¤t));
assert!(!baseline.covers(&warning(WarningKind::LargeFunction, Some("g")), ¤t));
}
}