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        });
91        graph.add_node(Node {
92            path: "research".into(),
93            node_type: NodeType::Directory,
94            hash: None,
95            graph: Some(".".into()),
96            is_graph: true,
97            metadata: HashMap::new(),
98        });
99        graph.add_node(Node {
100            path: "research/overview.md".into(),
101            node_type: NodeType::External,
102            hash: None,
103            graph: Some("research".into()),
104            is_graph: false,
105            metadata: HashMap::new(),
106        });
107        graph.add_edge(Edge {
108            source: "index.md".into(),
109            target: "research/overview.md".into(),
110            link: None,
111            parser: "markdown".into(),
112        });
113        graph.add_edge(Edge {
114            source: "research/overview.md".into(),
115            target: "research".into(),
116            link: None,
117            parser: "markdown".into(),
118        });
119
120        let enriched = make_enriched_with_root(graph, dir.path());
121        let ctx = RuleContext {
122            graph: &enriched,
123            options: None,
124        };
125        let diagnostics = EncapsulationViolationRule.evaluate(&ctx);
126        assert!(diagnostics.is_empty());
127    }
128
129    #[test]
130    fn violation_for_non_interface_file() {
131        let dir = TempDir::new().unwrap();
132        setup_sealed_child(dir.path());
133
134        let mut graph = Graph::new();
135        graph.add_node(Node {
136            path: "index.md".into(),
137            node_type: NodeType::File,
138            hash: None,
139            graph: Some(".".into()),
140            is_graph: false,
141            metadata: HashMap::new(),
142        });
143        graph.add_node(Node {
144            path: "research".into(),
145            node_type: NodeType::Directory,
146            hash: None,
147            graph: Some(".".into()),
148            is_graph: true,
149            metadata: HashMap::new(),
150        });
151        graph.add_node(Node {
152            path: "research/internal.md".into(),
153            node_type: NodeType::External,
154            hash: None,
155            graph: Some("research".into()),
156            is_graph: false,
157            metadata: HashMap::new(),
158        });
159        graph.add_edge(Edge {
160            source: "index.md".into(),
161            target: "research/internal.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 = EncapsulationViolationRule.evaluate(&ctx);
172        assert_eq!(diagnostics.len(), 1);
173        assert_eq!(diagnostics[0].rule, "encapsulation-violation");
174        assert_eq!(
175            diagnostics[0].target.as_deref(),
176            Some("research/internal.md")
177        );
178    }
179}