drft/rules/
layer_violation.rs1use crate::diagnostic::Diagnostic;
2use crate::rules::{Rule, RuleContext};
3use std::collections::HashMap;
4
5pub struct LayerViolationRule;
6
7impl Rule for LayerViolationRule {
8 fn name(&self) -> &str {
9 "layer-violation"
10 }
11
12 fn evaluate(&self, ctx: &RuleContext) -> Vec<Diagnostic> {
13 let graph = &ctx.graph.graph;
14 let result = &ctx.graph.depth;
15
16 let depth_map: HashMap<&str, usize> = result
17 .nodes
18 .iter()
19 .map(|n| (n.node.as_str(), n.depth))
20 .collect();
21 let cycle_map: HashMap<&str, bool> = result
22 .nodes
23 .iter()
24 .map(|n| (n.node.as_str(), n.in_cycle))
25 .collect();
26
27 let mut diagnostics = Vec::new();
28
29 for edge in &graph.edges {
30 if !graph.is_file_node(&edge.source) || !graph.is_file_node(&edge.target) {
31 continue;
32 }
33
34 if cycle_map.get(edge.source.as_str()) == Some(&true)
35 || cycle_map.get(edge.target.as_str()) == Some(&true)
36 {
37 continue;
38 }
39
40 let Some(&src_depth) = depth_map.get(edge.source.as_str()) else {
41 continue;
42 };
43 let Some(&tgt_depth) = depth_map.get(edge.target.as_str()) else {
44 continue;
45 };
46
47 if tgt_depth < src_depth {
48 diagnostics.push(Diagnostic {
49 rule: "layer-violation".into(),
50 message: format!(
51 "upward link (depth {} \u{2192} depth {})",
52 src_depth, tgt_depth
53 ),
54 source: Some(edge.source.clone()),
55 target: Some(edge.target.clone()),
56 fix: Some(format!(
57 "{} (depth {}) links to {} (depth {}) \u{2014} this points upward in the hierarchy",
58 edge.source, src_depth, edge.target, tgt_depth
59 )),
60 ..Default::default()
61 });
62 } else if tgt_depth > src_depth + 1 {
63 diagnostics.push(Diagnostic {
64 rule: "layer-violation".into(),
65 message: format!(
66 "skip-layer link (depth {} \u{2192} depth {})",
67 src_depth, tgt_depth
68 ),
69 source: Some(edge.source.clone()),
70 target: Some(edge.target.clone()),
71 fix: Some(format!(
72 "{} (depth {}) links to {} (depth {}), skipping {} layers",
73 edge.source,
74 src_depth,
75 edge.target,
76 tgt_depth,
77 tgt_depth - src_depth - 1
78 )),
79 ..Default::default()
80 });
81 }
82 }
83
84 diagnostics
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::analyses::EnrichedGraph;
92 use crate::config::Config;
93 use crate::graph::Graph;
94 use crate::graph::test_helpers::{make_edge, make_node};
95 use crate::rules::RuleContext;
96
97 fn make_enriched(graph: Graph) -> EnrichedGraph {
98 crate::analyses::enrich_graph(graph, std::path::Path::new("."), &Config::defaults(), None)
99 }
100
101 #[test]
102 fn no_violation_in_clean_hierarchy() {
103 let mut graph = Graph::new();
104 graph.add_node(make_node("a.md"));
105 graph.add_node(make_node("b.md"));
106 graph.add_node(make_node("c.md"));
107 graph.add_edge(make_edge("a.md", "b.md"));
108 graph.add_edge(make_edge("b.md", "c.md"));
109
110 let enriched = make_enriched(graph);
111 let ctx = RuleContext {
112 graph: &enriched,
113 options: None,
114 };
115 let diagnostics = LayerViolationRule.evaluate(&ctx);
116 assert!(diagnostics.is_empty());
117 }
118
119 #[test]
120 fn detects_upward_link() {
121 let mut graph = Graph::new();
122 graph.add_node(make_node("a.md"));
123 graph.add_node(make_node("b.md"));
124 graph.add_node(make_node("c.md"));
125 graph.add_node(make_node("d.md"));
126 graph.add_edge(make_edge("a.md", "b.md"));
127 graph.add_edge(make_edge("b.md", "c.md"));
128 graph.add_edge(make_edge("c.md", "d.md"));
129 graph.add_edge(make_edge("d.md", "a.md"));
130
131 let enriched = make_enriched(graph);
132 let ctx = RuleContext {
133 graph: &enriched,
134 options: None,
135 };
136 let diagnostics = LayerViolationRule.evaluate(&ctx);
137 assert!(diagnostics.is_empty());
138 }
139
140 #[test]
141 fn detects_skip_layer() {
142 let mut graph = Graph::new();
143 graph.add_node(make_node("a.md"));
144 graph.add_node(make_node("b.md"));
145 graph.add_node(make_node("c.md"));
146 graph.add_edge(make_edge("a.md", "b.md"));
147 graph.add_edge(make_edge("b.md", "c.md"));
148 graph.add_edge(make_edge("a.md", "c.md"));
149
150 let enriched = make_enriched(graph);
151 let ctx = RuleContext {
152 graph: &enriched,
153 options: None,
154 };
155 let diagnostics = LayerViolationRule.evaluate(&ctx);
156 assert_eq!(diagnostics.len(), 1);
157 assert!(diagnostics[0].message.contains("skip-layer"));
158 }
159
160 #[test]
161 fn skips_cyclic_nodes() {
162 let mut graph = Graph::new();
163 graph.add_node(make_node("a.md"));
164 graph.add_node(make_node("b.md"));
165 graph.add_edge(make_edge("a.md", "b.md"));
166 graph.add_edge(make_edge("b.md", "a.md"));
167
168 let enriched = make_enriched(graph);
169 let ctx = RuleContext {
170 graph: &enriched,
171 options: None,
172 };
173 let diagnostics = LayerViolationRule.evaluate(&ctx);
174 assert!(diagnostics.is_empty());
175 }
176
177 #[test]
178 fn same_layer_link_is_not_violation() {
179 let mut graph = Graph::new();
180 graph.add_node(make_node("a.md"));
181 graph.add_node(make_node("b.md"));
182 graph.add_node(make_node("c.md"));
183 graph.add_edge(make_edge("a.md", "b.md"));
184 graph.add_edge(make_edge("a.md", "c.md"));
185
186 let enriched = make_enriched(graph);
187 let ctx = RuleContext {
188 graph: &enriched,
189 options: None,
190 };
191 let diagnostics = LayerViolationRule.evaluate(&ctx);
192 assert!(diagnostics.is_empty());
193 }
194}