drft/rules/
encapsulation_violation.rs1use 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}