Skip to main content

drft/rules/
containment.rs

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