drft/rules/
directed_cycle.rs1use 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}