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::graph::Graph;
92 use crate::graph::test_helpers::{make_edge, make_enriched, make_node};
93 use crate::rules::RuleContext;
94
95 #[test]
96 fn no_violation_in_clean_hierarchy() {
97 let mut graph = Graph::new();
98 graph.add_node(make_node("a.md"));
99 graph.add_node(make_node("b.md"));
100 graph.add_node(make_node("c.md"));
101 graph.add_edge(make_edge("a.md", "b.md"));
102 graph.add_edge(make_edge("b.md", "c.md"));
103
104 let enriched = make_enriched(graph);
105 let ctx = RuleContext {
106 graph: &enriched,
107 options: None,
108 };
109 let diagnostics = LayerViolationRule.evaluate(&ctx);
110 assert!(diagnostics.is_empty());
111 }
112
113 #[test]
114 fn detects_upward_link() {
115 let mut graph = Graph::new();
116 graph.add_node(make_node("a.md"));
117 graph.add_node(make_node("b.md"));
118 graph.add_node(make_node("c.md"));
119 graph.add_node(make_node("d.md"));
120 graph.add_edge(make_edge("a.md", "b.md"));
121 graph.add_edge(make_edge("b.md", "c.md"));
122 graph.add_edge(make_edge("c.md", "d.md"));
123 graph.add_edge(make_edge("d.md", "a.md"));
124
125 let enriched = make_enriched(graph);
126 let ctx = RuleContext {
127 graph: &enriched,
128 options: None,
129 };
130 let diagnostics = LayerViolationRule.evaluate(&ctx);
131 assert!(diagnostics.is_empty());
132 }
133
134 #[test]
135 fn detects_skip_layer() {
136 let mut graph = Graph::new();
137 graph.add_node(make_node("a.md"));
138 graph.add_node(make_node("b.md"));
139 graph.add_node(make_node("c.md"));
140 graph.add_edge(make_edge("a.md", "b.md"));
141 graph.add_edge(make_edge("b.md", "c.md"));
142 graph.add_edge(make_edge("a.md", "c.md"));
143
144 let enriched = make_enriched(graph);
145 let ctx = RuleContext {
146 graph: &enriched,
147 options: None,
148 };
149 let diagnostics = LayerViolationRule.evaluate(&ctx);
150 assert_eq!(diagnostics.len(), 1);
151 assert!(diagnostics[0].message.contains("skip-layer"));
152 }
153
154 #[test]
155 fn skips_cyclic_nodes() {
156 let mut graph = Graph::new();
157 graph.add_node(make_node("a.md"));
158 graph.add_node(make_node("b.md"));
159 graph.add_edge(make_edge("a.md", "b.md"));
160 graph.add_edge(make_edge("b.md", "a.md"));
161
162 let enriched = make_enriched(graph);
163 let ctx = RuleContext {
164 graph: &enriched,
165 options: None,
166 };
167 let diagnostics = LayerViolationRule.evaluate(&ctx);
168 assert!(diagnostics.is_empty());
169 }
170
171 #[test]
172 fn same_layer_link_is_not_violation() {
173 let mut graph = Graph::new();
174 graph.add_node(make_node("a.md"));
175 graph.add_node(make_node("b.md"));
176 graph.add_node(make_node("c.md"));
177 graph.add_edge(make_edge("a.md", "b.md"));
178 graph.add_edge(make_edge("a.md", "c.md"));
179
180 let enriched = make_enriched(graph);
181 let ctx = RuleContext {
182 graph: &enriched,
183 options: None,
184 };
185 let diagnostics = LayerViolationRule.evaluate(&ctx);
186 assert!(diagnostics.is_empty());
187 }
188}