drft/rules/
dangling_edge.rs1use crate::diagnostic::Diagnostic;
2use crate::rules::{Rule, RuleContext};
3
4pub struct DanglingEdgeRule;
5
6impl Rule for DanglingEdgeRule {
7 fn name(&self) -> &str {
8 "dangling-edge"
9 }
10
11 fn evaluate(&self, ctx: &RuleContext) -> Vec<Diagnostic> {
12 let graph = &ctx.graph.graph;
13
14 graph
15 .edges
16 .iter()
17 .filter_map(|edge| {
18 if crate::graph::is_uri(&edge.target) {
20 return None;
21 }
22
23 let props = graph.target_properties.get(&edge.target);
24
25 if graph.nodes.contains_key(&edge.target) {
27 return None;
28 }
29
30 if props.is_some_and(|p| p.is_symlink) {
32 return None; }
34
35 Some(Diagnostic {
38 rule: "dangling-edge".into(),
39 message: "file not found".into(),
40 source: Some(edge.source.clone()),
41 target: Some(edge.target.clone()),
42 fix: Some(format!(
43 "{} does not exist \u{2014} either create it or update the link in {}",
44 edge.target, edge.source
45 )),
46 ..Default::default()
47 })
48 })
49 .collect()
50 }
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56 use crate::graph::test_helpers::make_enriched;
57 use crate::graph::{Edge, Graph, Node, NodeType, TargetProperties};
58 use crate::rules::RuleContext;
59 use std::collections::HashMap;
60
61 #[test]
62 fn detects_dangling_edge() {
63 let mut graph = Graph::new();
64 graph.add_node(Node {
65 path: "index.md".into(),
66 node_type: NodeType::File,
67 hash: None,
68 graph: None,
69 is_graph: false,
70 metadata: HashMap::new(),
71 });
72 graph.add_edge(Edge {
73 source: "index.md".into(),
74 target: "gone.md".into(),
75 link: None,
76 parser: "markdown".into(),
77 });
78
79 let enriched = make_enriched(graph);
80 let ctx = RuleContext {
81 graph: &enriched,
82 options: None,
83 };
84 let diagnostics = DanglingEdgeRule.evaluate(&ctx);
85 assert_eq!(diagnostics.len(), 1);
86 assert_eq!(diagnostics[0].rule, "dangling-edge");
87 assert_eq!(diagnostics[0].source.as_deref(), Some("index.md"));
88 assert_eq!(diagnostics[0].target.as_deref(), Some("gone.md"));
89 }
90
91 #[test]
92 fn no_diagnostic_for_valid_link() {
93 let mut graph = Graph::new();
94 graph.add_node(Node {
95 path: "index.md".into(),
96 node_type: NodeType::File,
97 hash: None,
98 graph: None,
99 is_graph: false,
100 metadata: HashMap::new(),
101 });
102 graph.add_node(Node {
103 path: "setup.md".into(),
104 node_type: NodeType::File,
105 hash: None,
106 graph: None,
107 is_graph: false,
108 metadata: HashMap::new(),
109 });
110 graph.add_edge(Edge {
111 source: "index.md".into(),
112 target: "setup.md".into(),
113 link: None,
114 parser: "markdown".into(),
115 });
116
117 let enriched = make_enriched(graph);
118 let ctx = RuleContext {
119 graph: &enriched,
120 options: None,
121 };
122 let diagnostics = DanglingEdgeRule.evaluate(&ctx);
123 assert!(diagnostics.is_empty());
124 }
125
126 #[test]
127 fn skips_symlink_targets() {
128 let mut graph = Graph::new();
129 graph.add_node(Node {
130 path: "index.md".into(),
131 node_type: NodeType::File,
132 hash: None,
133 graph: None,
134 is_graph: false,
135 metadata: HashMap::new(),
136 });
137 graph.target_properties.insert(
138 "linked.md".into(),
139 TargetProperties {
140 is_symlink: true,
141 is_directory: false,
142 symlink_target: Some("real.md".into()),
143 },
144 );
145 graph.add_edge(Edge {
146 source: "index.md".into(),
147 target: "linked.md".into(),
148 link: None,
149 parser: "markdown".into(),
150 });
151
152 let enriched = make_enriched(graph);
153 let ctx = RuleContext {
154 graph: &enriched,
155 options: None,
156 };
157 let diagnostics = DanglingEdgeRule.evaluate(&ctx);
158 assert!(diagnostics.is_empty());
159 }
160
161 #[test]
162 fn skips_directory_targets() {
163 let mut graph = Graph::new();
164 graph.add_node(Node {
165 path: "index.md".into(),
166 node_type: NodeType::File,
167 hash: None,
168 graph: None,
169 is_graph: false,
170 metadata: HashMap::new(),
171 });
172 graph.add_node(Node {
174 path: "guides".into(),
175 node_type: NodeType::Directory,
176 hash: None,
177 graph: None,
178 is_graph: false,
179 metadata: HashMap::new(),
180 });
181 graph.add_edge(Edge {
182 source: "index.md".into(),
183 target: "guides".into(),
184 link: None,
185 parser: "markdown".into(),
186 });
187
188 let enriched = make_enriched(graph);
189 let ctx = RuleContext {
190 graph: &enriched,
191 options: None,
192 };
193 let diagnostics = DanglingEdgeRule.evaluate(&ctx);
194 assert!(diagnostics.is_empty());
195 }
196}