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