drft/rules/
containment.rs1use crate::analyses::Analysis;
2use crate::analyses::AnalysisContext;
3use crate::analyses::graph_boundaries::GraphBoundaries;
4use crate::diagnostic::Diagnostic;
5use crate::rules::{Rule, RuleContext};
6
7pub struct ContainmentRule;
8
9impl Rule for ContainmentRule {
10 fn name(&self) -> &str {
11 "containment"
12 }
13
14 fn evaluate(&self, ctx: &RuleContext) -> Vec<Diagnostic> {
15 let analysis_ctx = AnalysisContext {
16 graph: ctx.graph,
17 root: ctx.root,
18 config: ctx.config,
19 lockfile: ctx.lockfile,
20 };
21 let result = GraphBoundaries.run(&analysis_ctx);
22
23 if !result.sealed {
24 return vec![];
25 }
26
27 result
28 .escapes
29 .iter()
30 .map(|e| Diagnostic {
31 rule: "containment".into(),
32 message: "links outside graph boundary".into(),
33 source: Some(e.source.clone()),
34 target: Some(e.target.clone()),
35 fix: Some(format!(
36 "link reaches outside the graph \u{2014} move {} into the graph or remove the link from {}",
37 e.target, e.source
38 )),
39 ..Default::default()
40 })
41 .collect()
42 }
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48 use crate::config::Config;
49 use crate::graph::{Edge, EdgeType, Graph, Node, NodeType};
50 use crate::rules::RuleContext;
51 use std::fs;
52 use std::path::Path;
53 use tempfile::TempDir;
54
55 fn make_ctx<'a>(graph: &'a Graph, root: &'a Path, config: &'a Config) -> RuleContext<'a> {
56 RuleContext {
57 graph,
58 root,
59 config,
60 lockfile: None,
61 }
62 }
63
64 #[test]
65 fn detects_escape() {
66 let dir = TempDir::new().unwrap();
67 fs::write(dir.path().join("drft.lock"), "lockfile_version = 1\n").unwrap();
68
69 let mut graph = Graph::new();
70 graph.add_node(Node {
71 path: "index.md".into(),
72 node_type: NodeType::Source,
73 hash: None,
74 graph: None,
75 });
76 graph.add_edge(Edge {
77 source: "index.md".into(),
78 target: "../README.md".into(),
79 edge_type: EdgeType::new("markdown", "inline"),
80 synthetic: false,
81 });
82
83 let config = Config::defaults();
84 let ctx = make_ctx(&graph, dir.path(), &config);
85 let diagnostics = ContainmentRule.evaluate(&ctx);
86 assert_eq!(diagnostics.len(), 1);
87 assert_eq!(diagnostics[0].rule, "containment");
88 assert_eq!(diagnostics[0].target.as_deref(), Some("../README.md"));
89 }
90
91 #[test]
92 fn detects_deep_escape() {
93 let dir = TempDir::new().unwrap();
94 fs::write(dir.path().join("drft.lock"), "lockfile_version = 1\n").unwrap();
95
96 let mut graph = Graph::new();
97 graph.add_node(Node {
98 path: "index.md".into(),
99 node_type: NodeType::Source,
100 hash: None,
101 graph: None,
102 });
103 graph.add_edge(Edge {
104 source: "index.md".into(),
105 target: "../../other.md".into(),
106 edge_type: EdgeType::new("markdown", "inline"),
107 synthetic: false,
108 });
109
110 let config = Config::defaults();
111 let ctx = make_ctx(&graph, dir.path(), &config);
112 let diagnostics = ContainmentRule.evaluate(&ctx);
113 assert_eq!(diagnostics.len(), 1);
114 }
115
116 #[test]
117 fn no_violation_for_internal_link() {
118 let dir = TempDir::new().unwrap();
119 fs::write(dir.path().join("drft.lock"), "lockfile_version = 1\n").unwrap();
120
121 let mut graph = Graph::new();
122 graph.add_node(Node {
123 path: "index.md".into(),
124 node_type: NodeType::Source,
125 hash: None,
126 graph: None,
127 });
128 graph.add_edge(Edge {
129 source: "index.md".into(),
130 target: "setup.md".into(),
131 edge_type: EdgeType::new("markdown", "inline"),
132 synthetic: false,
133 });
134
135 let config = Config::defaults();
136 let ctx = make_ctx(&graph, dir.path(), &config);
137 let diagnostics = ContainmentRule.evaluate(&ctx);
138 assert!(diagnostics.is_empty());
139 }
140
141 #[test]
142 fn vacuous_without_lockfile() {
143 let dir = TempDir::new().unwrap();
144
145 let mut graph = Graph::new();
146 graph.add_edge(Edge {
147 source: "index.md".into(),
148 target: "../escape.md".into(),
149 edge_type: EdgeType::new("markdown", "inline"),
150 synthetic: false,
151 });
152
153 let config = Config::defaults();
154 let ctx = make_ctx(&graph, dir.path(), &config);
155 let diagnostics = ContainmentRule.evaluate(&ctx);
156 assert!(
157 diagnostics.is_empty(),
158 "no lockfile means no boundary to enforce"
159 );
160 }
161}