Skip to main content

drft/rules/
fragility.rs

1use crate::analyses::Analysis;
2use crate::analyses::AnalysisContext;
3use crate::analyses::bridges::Bridges;
4use crate::diagnostic::Diagnostic;
5use crate::rules::{Rule, RuleContext};
6
7pub struct FragilityRule;
8
9impl Rule for FragilityRule {
10    fn name(&self) -> &str {
11        "fragility"
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 = Bridges.run(&analysis_ctx);
22        let mut diagnostics = Vec::new();
23
24        for vertex in &result.cut_vertices {
25            diagnostics.push(Diagnostic {
26                rule: "fragility".into(),
27                message: "cut vertex".into(),
28                node: Some(vertex.clone()),
29                fix: Some(format!(
30                    "{vertex} is a single point of failure \u{2014} removing it disconnects the graph. Consider adding alternative paths."
31                )),
32                ..Default::default()
33            });
34        }
35
36        for bridge in &result.bridges {
37            diagnostics.push(Diagnostic {
38                rule: "fragility".into(),
39                message: "bridge edge".into(),
40                source: Some(bridge.source.clone()),
41                target: Some(bridge.target.clone()),
42                fix: Some(format!(
43                    "{} \u{2194} {} is the only connection between two parts of the graph \u{2014} consider adding alternative paths",
44                    bridge.source, bridge.target
45                )),
46                ..Default::default()
47            });
48        }
49
50        diagnostics
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use crate::config::Config;
58    use crate::graph::Graph;
59    use crate::graph::test_helpers::{make_edge, make_node};
60    use crate::rules::RuleContext;
61    use std::path::Path;
62
63    fn make_ctx<'a>(graph: &'a Graph, config: &'a Config) -> RuleContext<'a> {
64        RuleContext {
65            graph,
66            root: Path::new("."),
67            config,
68            lockfile: None,
69        }
70    }
71
72    #[test]
73    fn no_fragility_in_cycle() {
74        let mut graph = Graph::new();
75        graph.add_node(make_node("a.md"));
76        graph.add_node(make_node("b.md"));
77        graph.add_node(make_node("c.md"));
78        graph.add_edge(make_edge("a.md", "b.md"));
79        graph.add_edge(make_edge("b.md", "c.md"));
80        graph.add_edge(make_edge("c.md", "a.md"));
81
82        let config = Config::defaults();
83        let diagnostics = FragilityRule.evaluate(&make_ctx(&graph, &config));
84        assert!(diagnostics.is_empty());
85    }
86
87    #[test]
88    fn detects_cut_vertex_and_bridge() {
89        let mut graph = Graph::new();
90        graph.add_node(make_node("a.md"));
91        graph.add_node(make_node("b.md"));
92        graph.add_node(make_node("c.md"));
93        graph.add_edge(make_edge("a.md", "b.md"));
94        graph.add_edge(make_edge("b.md", "c.md"));
95
96        let config = Config::defaults();
97        let diagnostics = FragilityRule.evaluate(&make_ctx(&graph, &config));
98        let cut_vertices: Vec<_> = diagnostics
99            .iter()
100            .filter(|d| d.message == "cut vertex")
101            .collect();
102        let bridges: Vec<_> = diagnostics
103            .iter()
104            .filter(|d| d.message == "bridge edge")
105            .collect();
106        assert_eq!(cut_vertices.len(), 1);
107        assert_eq!(bridges.len(), 2);
108    }
109}