Skip to main content

drft/rules/
stale.rs

1use crate::diagnostic::Diagnostic;
2use crate::rules::{Rule, RuleContext};
3
4pub struct StaleRule;
5
6impl Rule for StaleRule {
7    fn name(&self) -> &str {
8        "stale"
9    }
10
11    fn evaluate(&self, ctx: &RuleContext) -> Vec<Diagnostic> {
12        let result = &ctx.graph.change_propagation;
13
14        if !result.has_lockfile {
15            return vec![];
16        }
17
18        let mut diagnostics = Vec::new();
19
20        for change in &result.directly_changed {
21            diagnostics.push(Diagnostic {
22                rule: "stale".into(),
23                message: "content changed".into(),
24                node: Some(change.node.clone()),
25                fix: Some(format!(
26                    "{} has been modified since the last lock \u{2014} review its dependents, then run drft lock",
27                    change.node
28                )),
29                ..Default::default()
30            });
31        }
32
33        for stale in &result.transitively_stale {
34            diagnostics.push(Diagnostic {
35                rule: "stale".into(),
36                message: "stale via".into(),
37                node: Some(stale.node.clone()),
38                via: Some(stale.via.clone()),
39                fix: Some(format!(
40                    "{} has changed \u{2014} review {} to ensure it still accurately reflects {}, then run drft lock",
41                    stale.via, stale.node, stale.via
42                )),
43                ..Default::default()
44            });
45        }
46
47        diagnostics.sort_by(|a, b| a.node.cmp(&b.node));
48        diagnostics
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use crate::config::Config;
56    use crate::graph::test_helpers::make_edge;
57    use crate::graph::{Graph, Node, hash_bytes};
58    use crate::lockfile::{Lockfile, write_lockfile};
59    use crate::rules::RuleContext;
60    use std::collections::HashMap;
61    use std::fs;
62    use tempfile::TempDir;
63
64    fn setup_locked_dir() -> TempDir {
65        let dir = TempDir::new().unwrap();
66        fs::write(dir.path().join("index.md"), "[setup](setup.md)").unwrap();
67        fs::write(dir.path().join("setup.md"), "# Setup").unwrap();
68
69        let mut graph = Graph::new();
70        let index_hash = hash_bytes(b"[setup](setup.md)");
71        let setup_hash = hash_bytes(b"# Setup");
72
73        graph.add_node(Node {
74            path: "index.md".into(),
75            node_type: Some(crate::graph::NodeType::File),
76            included: true,
77            hash: Some(index_hash),
78            metadata: HashMap::new(),
79        });
80        graph.add_node(Node {
81            path: "setup.md".into(),
82            node_type: Some(crate::graph::NodeType::File),
83            included: true,
84            hash: Some(setup_hash),
85            metadata: HashMap::new(),
86        });
87        graph.add_edge(make_edge("index.md", "setup.md"));
88
89        let lockfile = Lockfile::from_graph(&graph);
90        write_lockfile(dir.path(), &lockfile).unwrap();
91        dir
92    }
93
94    #[test]
95    fn no_staleness_when_unchanged() {
96        let dir = setup_locked_dir();
97        let config = Config::defaults();
98        let lockfile = crate::lockfile::read_lockfile(dir.path()).ok().flatten();
99        let enriched = crate::analyses::enrich(dir.path(), &config, lockfile.as_ref()).unwrap();
100        let ctx = RuleContext {
101            graph: &enriched,
102            options: None,
103        };
104        let diagnostics = StaleRule.evaluate(&ctx);
105        assert!(diagnostics.is_empty());
106    }
107
108    #[test]
109    fn detects_direct_and_transitive_staleness() {
110        let dir = setup_locked_dir();
111        fs::write(dir.path().join("setup.md"), "# Setup (edited)").unwrap();
112
113        let config = Config::defaults();
114        let lockfile = crate::lockfile::read_lockfile(dir.path()).ok().flatten();
115        let enriched = crate::analyses::enrich(dir.path(), &config, lockfile.as_ref()).unwrap();
116        let ctx = RuleContext {
117            graph: &enriched,
118            options: None,
119        };
120        let diagnostics = StaleRule.evaluate(&ctx);
121        assert_eq!(diagnostics.len(), 2);
122
123        let direct = diagnostics
124            .iter()
125            .find(|d| d.message == "content changed")
126            .unwrap();
127        assert_eq!(direct.node.as_deref(), Some("setup.md"));
128        assert!(direct.via.is_none());
129
130        let transitive = diagnostics
131            .iter()
132            .find(|d| d.message == "stale via")
133            .unwrap();
134        assert_eq!(transitive.node.as_deref(), Some("index.md"));
135        assert_eq!(transitive.via.as_deref(), Some("setup.md"));
136    }
137
138    #[test]
139    fn skips_when_no_lockfile() {
140        let dir = TempDir::new().unwrap();
141        fs::write(dir.path().join("dummy.md"), "").unwrap();
142        let config = Config::defaults();
143        let enriched = crate::analyses::enrich(dir.path(), &config, None).unwrap();
144        let ctx = RuleContext {
145            graph: &enriched,
146            options: None,
147        };
148        let diagnostics = StaleRule.evaluate(&ctx);
149        assert!(diagnostics.is_empty());
150    }
151
152    #[test]
153    fn deleted_file_causes_staleness() {
154        let dir = setup_locked_dir();
155        fs::remove_file(dir.path().join("setup.md")).unwrap();
156
157        let config = Config::defaults();
158        let lockfile = crate::lockfile::read_lockfile(dir.path()).ok().flatten();
159        let enriched = crate::analyses::enrich(dir.path(), &config, lockfile.as_ref()).unwrap();
160        let ctx = RuleContext {
161            graph: &enriched,
162            options: None,
163        };
164        let diagnostics = StaleRule.evaluate(&ctx);
165        assert!(!diagnostics.is_empty());
166
167        let direct = diagnostics
168            .iter()
169            .find(|d| d.message == "content changed")
170            .unwrap();
171        assert_eq!(direct.node.as_deref(), Some("setup.md"));
172    }
173}