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