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}