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