drft/rules/
broken_link.rs1use crate::diagnostic::Diagnostic;
2use crate::graph::NodeType;
3use crate::rules::{Rule, RuleContext};
4
5pub struct BrokenLinkRule;
6
7impl Rule for BrokenLinkRule {
8 fn name(&self) -> &str {
9 "broken-link"
10 }
11
12 fn evaluate(&self, ctx: &RuleContext) -> Vec<Diagnostic> {
13 let graph = ctx.graph;
14 let root = ctx.root;
15
16 graph
17 .edges
18 .iter()
19 .filter_map(|edge| {
20 if edge.target.starts_with("http://") || edge.target.starts_with("https://") {
22 return None;
23 }
24
25 if let Some(node) = graph.nodes.get(&edge.target) {
27 if node.node_type == NodeType::Graph {
28 return None; }
30 let target_path = root.join(&edge.target);
32 if target_path.is_symlink() {
33 return None; }
35 return None; }
37
38 let target_path = root.join(&edge.target);
40
41 if target_path.is_dir() {
42 return None; }
44
45 if target_path.is_symlink() {
46 return None; }
48
49 if target_path.exists() {
50 return Some(Diagnostic {
52 rule: "broken-link".into(),
53 message: "file excluded by ignore pattern".into(),
54 source: Some(edge.source.clone()),
55 target: Some(edge.target.clone()),
56 fix: Some(format!(
57 "{} exists but is excluded by an ignore pattern \u{2014} either remove the link from {} or update the ignore config",
58 edge.target, edge.source
59 )),
60 ..Default::default()
61 });
62 }
63
64 Some(Diagnostic {
66 rule: "broken-link".into(),
67 message: "file not found".into(),
68 source: Some(edge.source.clone()),
69 target: Some(edge.target.clone()),
70 fix: Some(format!(
71 "{} does not exist \u{2014} either create it or update the link in {}",
72 edge.target, edge.source
73 )),
74 ..Default::default()
75 })
76 })
77 .collect()
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use crate::config::Config;
85 use crate::graph::{Edge, EdgeType, Graph, Node, NodeType};
86 use crate::rules::RuleContext;
87 use std::fs;
88 use std::path::Path;
89 use tempfile::TempDir;
90
91 fn make_ctx<'a>(graph: &'a Graph, root: &'a Path, config: &'a Config) -> RuleContext<'a> {
92 RuleContext {
93 graph,
94 root,
95 config,
96 lockfile: None,
97 }
98 }
99
100 #[test]
101 fn detects_broken_link() {
102 let dir = TempDir::new().unwrap();
103 fs::write(dir.path().join("index.md"), "").unwrap();
104
105 let mut graph = Graph::new();
106 graph.add_node(Node {
107 path: "index.md".into(),
108 node_type: NodeType::Source,
109 hash: None,
110 graph: None,
111 });
112 graph.add_edge(Edge {
113 source: "index.md".into(),
114 target: "gone.md".into(),
115 edge_type: EdgeType::new("markdown", "inline"),
116 synthetic: false,
117 });
118
119 let config = Config::defaults();
120 let ctx = make_ctx(&graph, dir.path(), &config);
121 let diagnostics = BrokenLinkRule.evaluate(&ctx);
122 assert_eq!(diagnostics.len(), 1);
123 assert_eq!(diagnostics[0].rule, "broken-link");
124 assert_eq!(diagnostics[0].source.as_deref(), Some("index.md"));
125 assert_eq!(diagnostics[0].target.as_deref(), Some("gone.md"));
126 }
127
128 #[test]
129 fn no_diagnostic_for_valid_link() {
130 let dir = TempDir::new().unwrap();
131 fs::write(dir.path().join("index.md"), "").unwrap();
132 fs::write(dir.path().join("setup.md"), "").unwrap();
133
134 let mut graph = Graph::new();
135 graph.add_node(Node {
136 path: "index.md".into(),
137 node_type: NodeType::Source,
138 hash: None,
139 graph: None,
140 });
141 graph.add_node(Node {
142 path: "setup.md".into(),
143 node_type: NodeType::Source,
144 hash: None,
145 graph: None,
146 });
147 graph.add_edge(Edge {
148 source: "index.md".into(),
149 target: "setup.md".into(),
150 edge_type: EdgeType::new("markdown", "inline"),
151 synthetic: false,
152 });
153
154 let config = Config::defaults();
155 let ctx = make_ctx(&graph, dir.path(), &config);
156 let diagnostics = BrokenLinkRule.evaluate(&ctx);
157 assert!(diagnostics.is_empty());
158 }
159}