use crate::finding::{at, Finding, Severity};
use crate::instance::Outcome;
use crate::scope::{EdgeCheck, EdgeUnit, EdgeView};
use headwater_graph::declarations::Relation;
use headwater_graph::edges::VERIFIED_REVISION;
use headwater_graph::{Declarations, Target};
pub const RULE: &str = "relation.target.suspect";
const NO_HALF: &str = "the entry carries no declared half";
pub struct Suspect<'a> {
declared: Vec<&'a Relation>,
}
impl<'a> Suspect<'a> {
pub fn over(declarations: &'a Declarations) -> Self {
Suspect {
declared: declarations
.relations
.iter()
.filter(|relation| relation.created_by.as_deref() == Some("import"))
.collect(),
}
}
}
impl EdgeCheck for Suspect<'_> {
const RULE: &'static str = self::RULE;
const VERSION: u32 = 1;
const UNIT: EdgeUnit = EdgeUnit::Entry;
fn instantiates(&self, relation: &str) -> bool {
self.declared.iter().any(|known| known.name == relation)
}
fn evaluate(&self, view: &EdgeView<'_>) -> Outcome {
let Some(edge) = view.declared_half().or_else(|| view.inverse_half()) else {
return Outcome::Skipped(NO_HALF.to_string());
};
let Target::Anchor { revision, .. } = &edge.target else {
return Outcome::Passed;
};
let Some(verified) = edge
.attributes
.iter()
.find(|entry| entry.key.value == VERIFIED_REVISION)
.and_then(|entry| entry.value.value.as_scalar())
.map(|scalar| scalar.text.as_str())
else {
return Outcome::Passed;
};
let Some(current) = revision.as_deref() else {
return Outcome::Passed;
};
if verified == current {
return Outcome::Passed;
}
let (line, column) = at(Some(edge.span));
Outcome::failed_with(Finding {
rule: self::RULE,
severity: Severity::Warn,
obligation: None,
path: edge.source.path.clone(),
line,
column,
message: message(
&edge.source.id,
&edge.name,
&edge.raw_target,
verified,
current,
),
remediation: remediation(current),
patch: None,
})
}
}
fn message(id: &str, name: &str, raw: &str, verified: &str, current: &str) -> String {
format!(
"`{id}` declares `{name}: {raw}`, which was verified at revision `{verified}` and the \
snapshot now pins revision `{current}`"
)
}
fn remediation(current: &str) -> String {
format!(
"re-read the upstream item, and where the edge still holds set \
`{VERIFIED_REVISION}: {current}` on this entry"
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_finding_names_both_revisions_and_the_remedy_names_the_attribute() {
let text = message("SPEC-FIX-one", "audited_by", "12345", "7", "8");
assert!(text.contains("SPEC-FIX-one"), "{text}");
assert!(text.contains("audited_by: 12345"), "{text}");
assert!(text.contains("verified at revision `7`"), "{text}");
assert!(text.contains("pins revision `8`"), "{text}");
let remedy = remediation("8");
assert!(remedy.contains("verified_revision: 8"), "{remedy}");
assert!(!remedy.contains("headwater import"), "{remedy}");
}
#[test]
fn the_recorded_revision_and_the_pinned_one_are_never_the_same_side() {
let text = message("SPEC-FIX-one", "audited_by", "12345", "7", "8");
let verified = text.find("verified at revision").expect("it is there");
let pinned = text.find("now pins revision").expect("it is there");
assert!(verified < pinned, "{text}");
}
}