yoi/crates/memory/src/linter/references.rs

68 lines
2.2 KiB
Rust

//! Reference-integrity checks: `replaced_by` existence + cycle detection.
use std::collections::HashSet;
use crate::Slug;
use crate::error::LintError;
use crate::linter::ExistingRecords;
use crate::linter::LintReport;
use crate::workspace::RecordKind;
/// Validate a Decision's `replaced_by` against the existing record set.
///
/// `self_slug` is the slug of the record currently being written (None
/// only when the path was malformed and we shouldn't even reach here).
pub fn check_replaced_by(
self_slug: Option<&Slug>,
target: &Slug,
existing: &ExistingRecords,
report: &mut LintReport,
) {
// Existence: target must already be a Decision on disk.
if !existing.contains(RecordKind::Decision, target) {
report.push_error(LintError::UnknownReference {
field: "replaced_by",
kind: "decision",
slug: target.to_string(),
});
return;
}
// Cycle: walk the chain target → target.replaced_by → ... and
// ensure we never revisit `self_slug` or any node twice.
let mut visited = HashSet::new();
if let Some(s) = self_slug {
visited.insert(s.clone());
}
let mut cursor = Some(target.clone());
let mut chain: Vec<String> = Vec::new();
while let Some(node) = cursor {
if !visited.insert(node.clone()) {
chain.push(node.to_string());
report.push_error(LintError::ReplacedByCycle {
chain: chain.join(" -> "),
});
return;
}
chain.push(node.to_string());
cursor = existing.decision(&node).and_then(|m| m.replaced_by.clone());
}
}
#[cfg(test)]
mod tests {
use super::*;
// Smoke test: cycle detection terminates on a 2-node loop where the
// existing tree already contains A↔B and the new write would close
// the loop. A direct unit test against `check_replaced_by` is
// exercised by linter::tests; here we just guard the loop bound.
#[test]
fn empty_chain_terminates() {
let mut report = LintReport::default();
let existing = ExistingRecords::default();
let target = Slug::parse("foo").unwrap();
check_replaced_by(None, &target, &existing, &mut report);
assert_eq!(report.errors.len(), 1);
}
}