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