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}