Skip to main content

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