Skip to main content

drft/rules/
cycle.rs

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