Skip to main content

drft/rules/
directed_cycle.rs

1use crate::diagnostic::Diagnostic;
2use crate::rules::{Rule, RuleContext};
3
4pub struct DirectedCycleRule;
5
6impl Rule for DirectedCycleRule {
7    fn name(&self) -> &str {
8        "directed-cycle"
9    }
10
11    fn evaluate(&self, ctx: &RuleContext) -> Vec<Diagnostic> {
12        let result = &ctx.graph.scc;
13
14        result
15            .sccs
16            .iter()
17            .map(|scc| {
18                let mut path = scc.members.clone();
19                if let Some(first) = path.first().cloned() {
20                    path.push(first);
21                }
22
23                let fix = format!(
24                    "circular dependency \u{2014} review whether one of these links can be removed or the content restructured: {}",
25                    scc.members.join(" \u{2192} ")
26                );
27
28                Diagnostic {
29                    rule: "directed-cycle".into(),
30                    message: "cycle detected".into(),
31                    path: Some(path),
32                    fix: Some(fix),
33                    ..Default::default()
34                }
35            })
36            .collect()
37    }
38}
39
40#[cfg(test)]
41mod tests {
42    use super::*;
43    use crate::analyses::EnrichedGraph;
44    use crate::config::Config;
45    use crate::graph::Graph;
46    use crate::graph::test_helpers::{make_edge, make_node};
47    use crate::rules::RuleContext;
48
49    fn make_enriched(graph: Graph) -> EnrichedGraph {
50        crate::analyses::enrich_graph(graph, std::path::Path::new("."), &Config::defaults(), None)
51    }
52
53    #[test]
54    fn detects_simple_cycle() {
55        let mut graph = Graph::new();
56        graph.add_node(make_node("a.md"));
57        graph.add_node(make_node("b.md"));
58        graph.add_node(make_node("c.md"));
59        graph.add_edge(make_edge("a.md", "b.md"));
60        graph.add_edge(make_edge("b.md", "c.md"));
61        graph.add_edge(make_edge("c.md", "a.md"));
62
63        let enriched = make_enriched(graph);
64        let ctx = RuleContext {
65            graph: &enriched,
66            options: None,
67        };
68        let diagnostics = DirectedCycleRule.evaluate(&ctx);
69        assert_eq!(diagnostics.len(), 1);
70        assert_eq!(diagnostics[0].rule, "directed-cycle");
71
72        let path = diagnostics[0].path.as_ref().unwrap();
73        assert_eq!(path.first(), path.last());
74        assert!(path.contains(&"a.md".to_string()));
75        assert!(path.contains(&"b.md".to_string()));
76        assert!(path.contains(&"c.md".to_string()));
77    }
78
79    #[test]
80    fn no_cycle_in_dag() {
81        let mut graph = Graph::new();
82        graph.add_node(make_node("a.md"));
83        graph.add_node(make_node("b.md"));
84        graph.add_node(make_node("c.md"));
85        graph.add_edge(make_edge("a.md", "b.md"));
86        graph.add_edge(make_edge("b.md", "c.md"));
87
88        let enriched = make_enriched(graph);
89        let ctx = RuleContext {
90            graph: &enriched,
91            options: None,
92        };
93        let diagnostics = DirectedCycleRule.evaluate(&ctx);
94        assert!(diagnostics.is_empty());
95    }
96
97    #[test]
98    fn ignores_broken_link_edges() {
99        let mut graph = Graph::new();
100        graph.add_node(make_node("a.md"));
101        graph.add_edge(make_edge("a.md", "missing.md"));
102
103        let enriched = make_enriched(graph);
104        let ctx = RuleContext {
105            graph: &enriched,
106            options: None,
107        };
108        let diagnostics = DirectedCycleRule.evaluate(&ctx);
109        assert!(diagnostics.is_empty());
110    }
111}