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 });
60 graph.add_node(Node {
61 path: "../README.md".into(),
62 node_type: NodeType::External,
63 hash: None,
64 graph: Some("..".into()),
65 is_graph: false,
66 metadata: HashMap::new(),
67 });
68 graph.add_edge(Edge {
69 source: "index.md".into(),
70 target: "../README.md".into(),
71 link: None,
72 parser: "markdown".into(),
73 });
74
75 let enriched = make_enriched_with_root(graph, dir.path());
76 let ctx = RuleContext {
77 graph: &enriched,
78 options: None,
79 };
80 let diagnostics = BoundaryViolationRule.evaluate(&ctx);
81 assert_eq!(diagnostics.len(), 1);
82 assert_eq!(diagnostics[0].rule, "boundary-violation");
83 assert_eq!(diagnostics[0].target.as_deref(), Some("../README.md"));
84 }
85
86 #[test]
87 fn detects_deep_escape() {
88 let dir = TempDir::new().unwrap();
89 fs::write(dir.path().join("drft.lock"), "lockfile_version = 1\n").unwrap();
90
91 let mut graph = Graph::new();
92 graph.add_node(Node {
93 path: "index.md".into(),
94 node_type: NodeType::File,
95 hash: None,
96 graph: Some(".".into()),
97 is_graph: false,
98 metadata: HashMap::new(),
99 });
100 graph.add_node(Node {
101 path: "../../other.md".into(),
102 node_type: NodeType::External,
103 hash: None,
104 graph: Some("..".into()),
105 is_graph: false,
106 metadata: HashMap::new(),
107 });
108 graph.add_edge(Edge {
109 source: "index.md".into(),
110 target: "../../other.md".into(),
111 link: None,
112 parser: "markdown".into(),
113 });
114
115 let enriched = make_enriched_with_root(graph, dir.path());
116 let ctx = RuleContext {
117 graph: &enriched,
118 options: None,
119 };
120 let diagnostics = BoundaryViolationRule.evaluate(&ctx);
121 assert_eq!(diagnostics.len(), 1);
122 }
123
124 #[test]
125 fn no_violation_for_internal_link() {
126 let dir = TempDir::new().unwrap();
127 fs::write(dir.path().join("drft.lock"), "lockfile_version = 1\n").unwrap();
128
129 let mut graph = Graph::new();
130 graph.add_node(Node {
131 path: "index.md".into(),
132 node_type: NodeType::File,
133 hash: None,
134 graph: None,
135 is_graph: false,
136 metadata: HashMap::new(),
137 });
138 graph.add_edge(Edge {
139 source: "index.md".into(),
140 target: "setup.md".into(),
141 link: None,
142 parser: "markdown".into(),
143 });
144
145 let enriched = make_enriched_with_root(graph, dir.path());
146 let ctx = RuleContext {
147 graph: &enriched,
148 options: None,
149 };
150 let diagnostics = BoundaryViolationRule.evaluate(&ctx);
151 assert!(diagnostics.is_empty());
152 }
153
154 #[test]
155 fn vacuous_without_lockfile() {
156 let dir = TempDir::new().unwrap();
157
158 let mut graph = Graph::new();
159 graph.add_edge(Edge {
160 source: "index.md".into(),
161 target: "../escape.md".into(),
162 link: None,
163 parser: "markdown".into(),
164 });
165
166 let enriched = make_enriched_with_root(graph, dir.path());
167 let ctx = RuleContext {
168 graph: &enriched,
169 options: None,
170 };
171 let diagnostics = BoundaryViolationRule.evaluate(&ctx);
172 assert!(
173 diagnostics.is_empty(),
174 "no lockfile means no boundary to enforce"
175 );
176 }
177}