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