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