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}