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::config::Config;
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 make_enriched(graph: Graph, root: &std::path::Path) -> crate::analyses::EnrichedGraph {
44        crate::analyses::enrich_graph(graph, root, &Config::defaults(), None)
45    }
46
47    fn setup_sealed_child(dir: &std::path::Path) {
48        let research = dir.join("research");
49        fs::create_dir_all(&research).unwrap();
50        fs::write(research.join("overview.md"), "# Overview").unwrap();
51        fs::write(research.join("internal.md"), "# Internal").unwrap();
52
53        let mut nodes = BTreeMap::new();
54        nodes.insert(
55            "overview.md".into(),
56            LockfileNode {
57                node_type: NodeType::File,
58                hash: Some("b3:aaa".into()),
59                graph: None,
60            },
61        );
62        nodes.insert(
63            "internal.md".into(),
64            LockfileNode {
65                node_type: NodeType::File,
66                hash: Some("b3:bbb".into()),
67                graph: None,
68            },
69        );
70
71        let lockfile = Lockfile {
72            lockfile_version: 2,
73            interface: Some(LockfileInterface {
74                nodes: vec!["overview.md".into()],
75            }),
76            nodes,
77        };
78        write_lockfile(&research, &lockfile).unwrap();
79    }
80
81    #[test]
82    fn no_violation_for_interface_file() {
83        let dir = TempDir::new().unwrap();
84        setup_sealed_child(dir.path());
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_node(Node {
95            path: "research/".into(),
96            node_type: NodeType::Graph,
97            hash: None,
98            graph: None,
99            metadata: HashMap::new(),
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            metadata: HashMap::new(),
107        });
108        graph.add_edge(Edge {
109            source: "index.md".into(),
110            target: "research/overview.md".into(),
111            link: None,
112            parser: "markdown".into(),
113        });
114        graph.add_edge(Edge {
115            source: "research/overview.md".into(),
116            target: "research/".into(),
117            link: None,
118            parser: "markdown".into(),
119        });
120
121        let enriched = make_enriched(graph, dir.path());
122        let ctx = RuleContext {
123            graph: &enriched,
124            options: None,
125        };
126        let diagnostics = EncapsulationViolationRule.evaluate(&ctx);
127        assert!(diagnostics.is_empty());
128    }
129
130    #[test]
131    fn violation_for_non_interface_file() {
132        let dir = TempDir::new().unwrap();
133        setup_sealed_child(dir.path());
134
135        let mut graph = Graph::new();
136        graph.add_node(Node {
137            path: "index.md".into(),
138            node_type: NodeType::File,
139            hash: None,
140            graph: None,
141            metadata: HashMap::new(),
142        });
143        graph.add_node(Node {
144            path: "research/".into(),
145            node_type: NodeType::Graph,
146            hash: None,
147            graph: None,
148            metadata: HashMap::new(),
149        });
150        graph.add_edge(Edge {
151            source: "index.md".into(),
152            target: "research/internal.md".into(),
153            link: None,
154            parser: "markdown".into(),
155        });
156
157        let enriched = make_enriched(graph, dir.path());
158        let ctx = RuleContext {
159            graph: &enriched,
160            options: None,
161        };
162        let diagnostics = EncapsulationViolationRule.evaluate(&ctx);
163        assert_eq!(diagnostics.len(), 1);
164        assert_eq!(diagnostics[0].rule, "encapsulation-violation");
165        assert_eq!(
166            diagnostics[0].target.as_deref(),
167            Some("research/internal.md")
168        );
169    }
170}