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::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}