Skip to main content

drft/rules/
boundary_violation.rs

1use 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}