Skip to main content

drft/rules/
encapsulation_violation.rs

1use crate::diagnostic::Diagnostic;
2use crate::rules::{Rule, RuleContext};
3
4pub struct EncapsulationViolationRule;
5
6impl Rule for EncapsulationViolationRule {
7    fn name(&self) -> &str {
8        "encapsulation-violation"
9    }
10
11    fn evaluate(&self, ctx: &RuleContext) -> Vec<Diagnostic> {
12        let result = &ctx.graph.graph_boundaries;
13
14        result
15            .encapsulation_violations
16            .iter()
17            .map(|v| Diagnostic {
18                rule: "encapsulation-violation".into(),
19                message: format!("not in {} interface", v.graph),
20                source: Some(v.source.clone()),
21                target: Some(v.target.clone()),
22                fix: Some(format!(
23                    "{} is not exposed by the {} interface \u{2014} either add it to the interface or remove the link from {}",
24                    v.target, v.graph, v.source
25                )),
26                ..Default::default()
27            })
28            .collect()
29    }
30}
31
32#[cfg(test)]
33mod tests {
34    use super::*;
35    use crate::graph::test_helpers::make_enriched_with_root;
36    use crate::graph::{Edge, Graph, Node, NodeType};
37    use crate::lockfile::{Lockfile, LockfileInterface, LockfileNode, write_lockfile};
38    use crate::rules::RuleContext;
39    use std::collections::{BTreeMap, HashMap};
40    use std::fs;
41    use tempfile::TempDir;
42
43    fn setup_sealed_child(dir: &std::path::Path) {
44        let research = dir.join("research");
45        fs::create_dir_all(&research).unwrap();
46        fs::write(research.join("overview.md"), "# Overview").unwrap();
47        fs::write(research.join("internal.md"), "# Internal").unwrap();
48
49        let mut nodes = BTreeMap::new();
50        nodes.insert(
51            "overview.md".into(),
52            LockfileNode {
53                node_type: NodeType::File,
54                hash: Some("b3:aaa".into()),
55                graph: None,
56            },
57        );
58        nodes.insert(
59            "internal.md".into(),
60            LockfileNode {
61                node_type: NodeType::File,
62                hash: Some("b3:bbb".into()),
63                graph: None,
64            },
65        );
66
67        let lockfile = Lockfile {
68            lockfile_version: 2,
69            interface: Some(LockfileInterface {
70                files: vec!["overview.md".into()],
71            }),
72            nodes,
73        };
74        write_lockfile(&research, &lockfile).unwrap();
75    }
76
77    #[test]
78    fn no_violation_for_interface_file() {
79        let dir = TempDir::new().unwrap();
80        setup_sealed_child(dir.path());
81
82        let mut graph = Graph::new();
83        graph.add_node(Node {
84            path: "index.md".into(),
85            node_type: NodeType::File,
86            hash: None,
87            graph: Some(".".into()),
88            is_graph: false,
89            metadata: HashMap::new(),
90            included: true,
91        });
92        graph.add_node(Node {
93            path: "research".into(),
94            node_type: NodeType::Directory,
95            hash: None,
96            graph: Some(".".into()),
97            is_graph: true,
98            metadata: HashMap::new(),
99            included: false,
100        });
101        graph.add_node(Node {
102            path: "research/overview.md".into(),
103            node_type: NodeType::File,
104            hash: None,
105            graph: Some("research".into()),
106            is_graph: false,
107            metadata: HashMap::new(),
108            included: false,
109        });
110        graph.add_edge(Edge {
111            source: "index.md".into(),
112            target: "research/overview.md".into(),
113            link: None,
114            parser: "markdown".into(),
115        });
116        graph.add_edge(Edge {
117            source: "research/overview.md".into(),
118            target: "research".into(),
119            link: None,
120            parser: "markdown".into(),
121        });
122
123        let enriched = make_enriched_with_root(graph, dir.path());
124        let ctx = RuleContext {
125            graph: &enriched,
126            options: None,
127        };
128        let diagnostics = EncapsulationViolationRule.evaluate(&ctx);
129        assert!(diagnostics.is_empty());
130    }
131
132    #[test]
133    fn violation_for_non_interface_file() {
134        let dir = TempDir::new().unwrap();
135        setup_sealed_child(dir.path());
136
137        let mut graph = Graph::new();
138        graph.add_node(Node {
139            path: "index.md".into(),
140            node_type: NodeType::File,
141            hash: None,
142            graph: Some(".".into()),
143            is_graph: false,
144            metadata: HashMap::new(),
145            included: true,
146        });
147        graph.add_node(Node {
148            path: "research".into(),
149            node_type: NodeType::Directory,
150            hash: None,
151            graph: Some(".".into()),
152            is_graph: true,
153            metadata: HashMap::new(),
154            included: false,
155        });
156        graph.add_node(Node {
157            path: "research/internal.md".into(),
158            node_type: NodeType::File,
159            hash: None,
160            graph: Some("research".into()),
161            is_graph: false,
162            metadata: HashMap::new(),
163            included: false,
164        });
165        graph.add_edge(Edge {
166            source: "index.md".into(),
167            target: "research/internal.md".into(),
168            link: None,
169            parser: "markdown".into(),
170        });
171
172        let enriched = make_enriched_with_root(graph, dir.path());
173        let ctx = RuleContext {
174            graph: &enriched,
175            options: None,
176        };
177        let diagnostics = EncapsulationViolationRule.evaluate(&ctx);
178        assert_eq!(diagnostics.len(), 1);
179        assert_eq!(diagnostics[0].rule, "encapsulation-violation");
180        assert_eq!(
181            diagnostics[0].target.as_deref(),
182            Some("research/internal.md")
183        );
184    }
185}