Skip to main content

drft/rules/
layer_violation.rs

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