use crate::diagnostic::Diagnostic;
use crate::rules::{Rule, RuleContext};
pub struct StaleRule;
impl Rule for StaleRule {
fn name(&self) -> &str {
"stale"
}
fn evaluate(&self, ctx: &RuleContext) -> Vec<Diagnostic> {
let result = &ctx.graph.change_propagation;
if !result.has_lockfile {
return vec![];
}
let mut diagnostics = Vec::new();
for change in &result.directly_changed {
diagnostics.push(Diagnostic {
rule: "stale".into(),
message: "content changed".into(),
node: Some(change.node.clone()),
fix: Some(format!(
"{} has been modified since the last lock \u{2014} review its dependents, then run drft lock",
change.node
)),
..Default::default()
});
}
for stale in &result.transitively_stale {
diagnostics.push(Diagnostic {
rule: "stale".into(),
message: "stale via".into(),
node: Some(stale.node.clone()),
via: Some(stale.via.clone()),
fix: Some(format!(
"{} has changed \u{2014} review {} to ensure it still accurately reflects {}, then run drft lock",
stale.via, stale.node, stale.via
)),
..Default::default()
});
}
diagnostics.sort_by(|a, b| a.node.cmp(&b.node));
diagnostics
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use crate::graph::test_helpers::make_edge;
use crate::graph::{Graph, Node, hash_bytes};
use crate::lockfile::{Lockfile, write_lockfile};
use crate::rules::RuleContext;
use std::collections::HashMap;
use std::fs;
use tempfile::TempDir;
fn setup_locked_dir() -> TempDir {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("index.md"), "[setup](setup.md)").unwrap();
fs::write(dir.path().join("setup.md"), "# Setup").unwrap();
let mut graph = Graph::new();
let index_hash = hash_bytes(b"[setup](setup.md)");
let setup_hash = hash_bytes(b"# Setup");
graph.add_node(Node {
path: "index.md".into(),
node_type: Some(crate::graph::NodeType::File),
included: true,
hash: Some(index_hash),
metadata: HashMap::new(),
});
graph.add_node(Node {
path: "setup.md".into(),
node_type: Some(crate::graph::NodeType::File),
included: true,
hash: Some(setup_hash),
metadata: HashMap::new(),
});
graph.add_edge(make_edge("index.md", "setup.md"));
let lockfile = Lockfile::from_graph(&graph);
write_lockfile(dir.path(), &lockfile).unwrap();
dir
}
#[test]
fn no_staleness_when_unchanged() {
let dir = setup_locked_dir();
let config = Config::defaults();
let lockfile = crate::lockfile::read_lockfile(dir.path()).ok().flatten();
let enriched = crate::analyses::enrich(dir.path(), &config, lockfile.as_ref()).unwrap();
let ctx = RuleContext {
graph: &enriched,
options: None,
};
let diagnostics = StaleRule.evaluate(&ctx);
assert!(diagnostics.is_empty());
}
#[test]
fn detects_direct_and_transitive_staleness() {
let dir = setup_locked_dir();
fs::write(dir.path().join("setup.md"), "# Setup (edited)").unwrap();
let config = Config::defaults();
let lockfile = crate::lockfile::read_lockfile(dir.path()).ok().flatten();
let enriched = crate::analyses::enrich(dir.path(), &config, lockfile.as_ref()).unwrap();
let ctx = RuleContext {
graph: &enriched,
options: None,
};
let diagnostics = StaleRule.evaluate(&ctx);
assert_eq!(diagnostics.len(), 2);
let direct = diagnostics
.iter()
.find(|d| d.message == "content changed")
.unwrap();
assert_eq!(direct.node.as_deref(), Some("setup.md"));
assert!(direct.via.is_none());
let transitive = diagnostics
.iter()
.find(|d| d.message == "stale via")
.unwrap();
assert_eq!(transitive.node.as_deref(), Some("index.md"));
assert_eq!(transitive.via.as_deref(), Some("setup.md"));
}
#[test]
fn skips_when_no_lockfile() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("dummy.md"), "").unwrap();
let config = Config::defaults();
let enriched = crate::analyses::enrich(dir.path(), &config, None).unwrap();
let ctx = RuleContext {
graph: &enriched,
options: None,
};
let diagnostics = StaleRule.evaluate(&ctx);
assert!(diagnostics.is_empty());
}
#[test]
fn deleted_file_causes_staleness() {
let dir = setup_locked_dir();
fs::remove_file(dir.path().join("setup.md")).unwrap();
let config = Config::defaults();
let lockfile = crate::lockfile::read_lockfile(dir.path()).ok().flatten();
let enriched = crate::analyses::enrich(dir.path(), &config, lockfile.as_ref()).unwrap();
let ctx = RuleContext {
graph: &enriched,
options: None,
};
let diagnostics = StaleRule.evaluate(&ctx);
assert!(!diagnostics.is_empty());
let direct = diagnostics
.iter()
.find(|d| d.message == "content changed")
.unwrap();
assert_eq!(direct.node.as_deref(), Some("setup.md"));
}
}