Skip to main content

drft/rules/
fragmentation.rs

1use crate::analyses::Analysis;
2use crate::analyses::AnalysisContext;
3use crate::analyses::connected_components::ConnectedComponents;
4use crate::diagnostic::Diagnostic;
5use crate::rules::{Rule, RuleContext};
6
7pub struct FragmentationRule;
8
9impl Rule for FragmentationRule {
10    fn name(&self) -> &str {
11        "fragmentation"
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 = ConnectedComponents.run(&analysis_ctx);
22
23        if result.component_count <= 1 {
24            return Vec::new();
25        }
26
27        result
28            .components
29            .iter()
30            .skip(1)
31            .map(|c| {
32                let members = c.members.join(", ");
33                Diagnostic {
34                    rule: "fragmentation".into(),
35                    message: format!("disconnected component ({} nodes)", c.members.len()),
36                    node: Some(members.clone()),
37                    fix: Some(format!(
38                        "these nodes are disconnected from the main graph: {members} \u{2014} add links to connect them"
39                    )),
40                    ..Default::default()
41                }
42            })
43            .collect()
44    }
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50    use crate::config::Config;
51    use crate::graph::Graph;
52    use crate::graph::test_helpers::{make_edge, make_node};
53    use crate::rules::RuleContext;
54    use std::path::Path;
55
56    fn make_ctx<'a>(graph: &'a Graph, config: &'a Config) -> RuleContext<'a> {
57        RuleContext {
58            graph,
59            root: Path::new("."),
60            config,
61            lockfile: None,
62        }
63    }
64
65    #[test]
66    fn no_diagnostic_when_connected() {
67        let mut graph = Graph::new();
68        graph.add_node(make_node("a.md"));
69        graph.add_node(make_node("b.md"));
70        graph.add_edge(make_edge("a.md", "b.md"));
71
72        let config = Config::defaults();
73        let diagnostics = FragmentationRule.evaluate(&make_ctx(&graph, &config));
74        assert!(diagnostics.is_empty());
75    }
76
77    #[test]
78    fn detects_fragmentation() {
79        let mut graph = Graph::new();
80        graph.add_node(make_node("a.md"));
81        graph.add_node(make_node("b.md"));
82        graph.add_node(make_node("c.md"));
83        graph.add_edge(make_edge("a.md", "b.md"));
84
85        let config = Config::defaults();
86        let diagnostics = FragmentationRule.evaluate(&make_ctx(&graph, &config));
87        assert_eq!(diagnostics.len(), 1);
88        assert_eq!(diagnostics[0].rule, "fragmentation");
89        assert!(diagnostics[0].message.contains("disconnected component"));
90    }
91
92    #[test]
93    fn no_diagnostic_for_empty_graph() {
94        let graph = Graph::new();
95        let config = Config::defaults();
96        let diagnostics = FragmentationRule.evaluate(&make_ctx(&graph, &config));
97        assert!(diagnostics.is_empty());
98    }
99}