Skip to main content

drft/rules/
redundant_edge.rs

1use crate::analyses::Analysis;
2use crate::analyses::AnalysisContext;
3use crate::analyses::transitive_reduction::TransitiveReduction;
4use crate::diagnostic::Diagnostic;
5use crate::rules::{Rule, RuleContext};
6
7pub struct RedundantEdgeRule;
8
9impl Rule for RedundantEdgeRule {
10    fn name(&self) -> &str {
11        "redundant-edge"
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 = TransitiveReduction.run(&analysis_ctx);
22
23        result
24            .redundant_edges
25            .iter()
26            .map(|re| Diagnostic {
27                rule: "redundant-edge".into(),
28                message: "transitively redundant".into(),
29                source: Some(re.source.clone()),
30                target: Some(re.target.clone()),
31                via: Some(re.via.clone()),
32                fix: Some(format!(
33                    "{} links directly to {}, but already reaches it via {} \u{2014} remove the direct link",
34                    re.source, re.target, re.via
35                )),
36                ..Default::default()
37            })
38            .collect()
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45    use crate::config::Config;
46    use crate::graph::{Edge, EdgeType, Graph, Node, NodeType};
47    use crate::rules::RuleContext;
48    use std::path::Path;
49
50    fn make_node(path: &str) -> Node {
51        Node {
52            path: path.into(),
53            node_type: NodeType::Source,
54            hash: None,
55            graph: None,
56        }
57    }
58
59    fn make_edge(source: &str, target: &str) -> Edge {
60        Edge {
61            source: source.into(),
62            target: target.into(),
63            edge_type: EdgeType::new("markdown", "inline"),
64            synthetic: false,
65        }
66    }
67
68    fn make_ctx<'a>(graph: &'a Graph, config: &'a Config) -> RuleContext<'a> {
69        RuleContext {
70            graph,
71            root: Path::new("."),
72            config,
73            lockfile: None,
74        }
75    }
76
77    #[test]
78    fn produces_diagnostics_for_redundant_edges() {
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        graph.add_edge(make_edge("b.md", "c.md"));
85        graph.add_edge(make_edge("a.md", "c.md"));
86
87        let config = Config::defaults();
88        let diagnostics = RedundantEdgeRule.evaluate(&make_ctx(&graph, &config));
89
90        assert_eq!(diagnostics.len(), 1);
91        assert_eq!(diagnostics[0].rule, "redundant-edge");
92        assert_eq!(diagnostics[0].source.as_deref(), Some("a.md"));
93        assert_eq!(diagnostics[0].target.as_deref(), Some("c.md"));
94        assert_eq!(diagnostics[0].via.as_deref(), Some("b.md"));
95        assert_eq!(diagnostics[0].message, "transitively redundant");
96    }
97
98    #[test]
99    fn no_diagnostics_when_no_redundancy() {
100        let mut graph = Graph::new();
101        graph.add_node(make_node("a.md"));
102        graph.add_node(make_node("b.md"));
103        graph.add_node(make_node("c.md"));
104        graph.add_edge(make_edge("a.md", "b.md"));
105        graph.add_edge(make_edge("b.md", "c.md"));
106
107        let config = Config::defaults();
108        let diagnostics = RedundantEdgeRule.evaluate(&make_ctx(&graph, &config));
109        assert!(diagnostics.is_empty());
110    }
111}