Skip to main content

drft/rules/
layer_violation.rs

1use 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}