use std::collections::HashSet;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::rules::pattern::{PatternCandidate, PatternCandidateId, RustPattern};
pub const SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BaselinePatternCandidate {
pub id: PatternCandidateId,
pub pattern: RustPattern,
pub krate: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatternBaseline {
pub schema_version: u32,
pub judge_version: String,
pub candidates: Vec<BaselinePatternCandidate>,
}
impl PatternBaseline {
pub fn new(candidates: &[PatternCandidate]) -> Self {
Self {
schema_version: SCHEMA_VERSION,
judge_version: env!("CARGO_PKG_VERSION").to_string(),
candidates: candidates
.iter()
.map(|candidate| BaselinePatternCandidate {
id: candidate.id.clone(),
pattern: candidate.pattern,
krate: candidate.scope.krate.clone(),
})
.collect(),
}
}
}
#[derive(Debug)]
pub enum PatternBaselineError {
Io(PathBuf, std::io::Error),
Serialize(serde_json::Error),
Deserialize(PathBuf, serde_json::Error),
UnsupportedSchemaVersion {
path: PathBuf,
found: Option<u64>,
},
}
impl std::fmt::Display for PatternBaselineError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let error = match self {
Self::Io(path, err) => crate::baseline::StoreErrorRef::Io(path, err),
Self::Serialize(err) => crate::baseline::StoreErrorRef::Serialize(err),
Self::Deserialize(path, err) => crate::baseline::StoreErrorRef::Deserialize(path, err),
Self::UnsupportedSchemaVersion { path, found } => {
crate::baseline::StoreErrorRef::UnsupportedSchemaVersion {
path,
found: *found,
}
}
};
crate::baseline::fmt_store_error(
f,
"pattern baseline",
SCHEMA_VERSION,
"cargo judge patterns --save-pattern-baseline",
error,
)
}
}
impl std::error::Error for PatternBaselineError {}
pub fn save(path: &Path, baseline: &PatternBaseline) -> Result<(), PatternBaselineError> {
crate::baseline::write_json_pretty(
path,
baseline,
PatternBaselineError::Io,
PatternBaselineError::Serialize,
)
}
pub fn load(path: &Path) -> Result<PatternBaseline, PatternBaselineError> {
let value = crate::baseline::read_json_value(
path,
PatternBaselineError::Io,
PatternBaselineError::Deserialize,
)?;
let found = crate::baseline::schema_version_of(&value);
match found {
Some(version) if version == u64::from(SCHEMA_VERSION) => {
crate::baseline::deserialize_value(path, value, PatternBaselineError::Deserialize)
}
_ => Err(PatternBaselineError::UnsupportedSchemaVersion {
path: path.to_path_buf(),
found,
}),
}
}
#[derive(Debug, Clone, Serialize)]
pub struct PatternDelta {
pub new: Vec<PatternCandidate>,
pub resolved: Vec<BaselinePatternCandidate>,
pub unchanged_count: usize,
}
pub fn diff_patterns(current: &[PatternCandidate], baseline: &PatternBaseline) -> PatternDelta {
let known_ids: HashSet<&str> = baseline
.candidates
.iter()
.map(|candidate| candidate.id.as_str())
.collect();
let mut new = Vec::new();
let mut unchanged_count = 0;
for candidate in current {
if known_ids.contains(candidate.id.as_str()) {
unchanged_count += 1;
} else {
new.push(candidate.clone());
}
}
let current_ids: HashSet<&str> = current
.iter()
.map(|candidate| candidate.id.as_str())
.collect();
let resolved = baseline
.candidates
.iter()
.filter(|candidate| !current_ids.contains(candidate.id.as_str()))
.cloned()
.collect();
PatternDelta {
new,
resolved,
unchanged_count,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rules::pattern::{CodeScope, CorroboratedEvidence, Evidence};
fn candidate_id(raw: &str) -> PatternCandidateId {
serde_json::from_value(serde_json::json!(raw)).unwrap()
}
fn candidate(id: &str, pattern: RustPattern, krate: &str) -> PatternCandidate {
PatternCandidate {
id: candidate_id(id),
pattern,
scope: CodeScope {
krate: krate.to_string(),
modules: Vec::new(),
},
evidence: CorroboratedEvidence {
primary: Evidence {
description: "primary".to_string(),
locations: Vec::new(),
},
independent: Evidence {
description: "independent".to_string(),
locations: Vec::new(),
},
additional: Vec::new(),
},
preconditions: Vec::new(),
contraindications: Vec::new(),
migration: Vec::new(),
related_findings: Vec::new(),
}
}
fn baseline_with(candidates: &[PatternCandidate]) -> PatternBaseline {
PatternBaseline::new(candidates)
}
#[test]
fn save_and_load_round_trips() {
let dir = crate::test_util::TempDir::new("pattern-baseline-round-trip");
let path = dir.join(".judge/baseline-patterns.json");
let baseline = baseline_with(&[candidate("a", RustPattern::Builder, "fixture")]);
save(&path, &baseline).unwrap();
let loaded = load(&path).unwrap();
assert_eq!(loaded.judge_version, baseline.judge_version);
assert_eq!(loaded.candidates.len(), 1);
assert_eq!(loaded.candidates[0].id.as_str(), "a");
assert_eq!(loaded.candidates[0].pattern, RustPattern::Builder);
assert_eq!(loaded.candidates[0].krate, "fixture");
}
#[test]
fn baseline_with_future_schema_version_is_rejected() {
let dir = crate::test_util::TempDir::new("pattern-baseline-future-schema");
let path = dir.join("baseline.json");
std::fs::write(
&path,
r#"{
"schema_version": 99,
"judge_version": "9.9.9",
"candidates": []
}"#,
)
.unwrap();
let err = load(&path).unwrap_err();
assert!(matches!(
err,
PatternBaselineError::UnsupportedSchemaVersion {
found: Some(99),
..
}
));
}
#[test]
fn known_candidate_is_unchanged_not_new() {
let baseline = baseline_with(&[candidate("a", RustPattern::Builder, "fixture")]);
let current = [candidate("a", RustPattern::Builder, "fixture")];
let delta = diff_patterns(¤t, &baseline);
assert_eq!(delta.unchanged_count, 1);
assert!(delta.new.is_empty());
assert!(delta.resolved.is_empty());
}
#[test]
fn candidate_only_in_current_is_new() {
let baseline = baseline_with(&[]);
let current = [candidate("a", RustPattern::Builder, "fixture")];
let delta = diff_patterns(¤t, &baseline);
assert_eq!(delta.new.len(), 1);
assert_eq!(delta.new[0].id.as_str(), "a");
assert_eq!(delta.unchanged_count, 0);
}
#[test]
fn candidate_only_in_baseline_is_resolved() {
let baseline = baseline_with(&[candidate("gone", RustPattern::Builder, "fixture")]);
let delta = diff_patterns(&[], &baseline);
assert_eq!(delta.resolved.len(), 1);
assert_eq!(delta.resolved[0].id.as_str(), "gone");
}
}