drft/rules/
directory_link.rs1use crate::diagnostic::Diagnostic;
2use crate::rules::{Rule, RuleContext};
3
4pub struct DirectoryLinkRule;
5
6impl Rule for DirectoryLinkRule {
7 fn name(&self) -> &str {
8 "directory-link"
9 }
10
11 fn evaluate(&self, ctx: &RuleContext) -> Vec<Diagnostic> {
12 let graph = ctx.graph;
13 let root = ctx.root;
14
15 graph
16 .edges
17 .iter()
18 .filter_map(|edge| {
19 if edge.target.starts_with("http://") || edge.target.starts_with("https://") {
21 return None;
22 }
23
24 if graph.nodes.contains_key(&edge.target) {
26 return None;
27 }
28
29 let target_path = root.join(&edge.target);
31 if target_path.is_dir() {
32 Some(Diagnostic {
33 rule: "directory-link".into(),
34 message: "links to directory, not file".into(),
35 source: Some(edge.source.clone()),
36 target: Some(edge.target.clone()),
37 fix: Some(format!(
38 "{}/ is a directory \u{2014} link to the specific file (e.g., {}/README.md)",
39 edge.target.trim_end_matches('/'),
40 edge.target.trim_end_matches('/')
41 )),
42 ..Default::default()
43 })
44 } else {
45 None
46 }
47 })
48 .collect()
49 }
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55 use crate::config::Config;
56 use crate::graph::{Edge, EdgeType, Graph, Node, NodeType};
57 use crate::rules::RuleContext;
58 use std::fs;
59 use std::path::Path;
60 use tempfile::TempDir;
61
62 fn make_ctx<'a>(graph: &'a Graph, root: &'a Path, config: &'a Config) -> RuleContext<'a> {
63 RuleContext {
64 graph,
65 root,
66 config,
67 lockfile: None,
68 }
69 }
70
71 #[test]
72 fn detects_directory_link() {
73 let dir = TempDir::new().unwrap();
74 fs::write(dir.path().join("index.md"), "").unwrap();
75 let guides = dir.path().join("guides");
76 fs::create_dir(&guides).unwrap();
77 fs::write(guides.join("README.md"), "").unwrap();
78
79 let mut graph = Graph::new();
80 graph.add_node(Node {
81 path: "index.md".into(),
82 node_type: NodeType::Source,
83 hash: None,
84 graph: None,
85 });
86 graph.add_edge(Edge {
87 source: "index.md".into(),
88 target: "guides".into(),
89 edge_type: EdgeType::new("markdown", "inline"),
90 synthetic: false,
91 });
92
93 let config = Config::defaults();
94 let ctx = make_ctx(&graph, dir.path(), &config);
95 let diagnostics = DirectoryLinkRule.evaluate(&ctx);
96 assert_eq!(diagnostics.len(), 1);
97 assert_eq!(diagnostics[0].rule, "directory-link");
98 assert_eq!(diagnostics[0].target.as_deref(), Some("guides"));
99 }
100
101 #[test]
102 fn no_diagnostic_for_file_link() {
103 let dir = TempDir::new().unwrap();
104 fs::write(dir.path().join("index.md"), "").unwrap();
105 fs::write(dir.path().join("setup.md"), "").unwrap();
106
107 let mut graph = Graph::new();
108 graph.add_node(Node {
109 path: "index.md".into(),
110 node_type: NodeType::Source,
111 hash: None,
112 graph: None,
113 });
114 graph.add_node(Node {
115 path: "setup.md".into(),
116 node_type: NodeType::Source,
117 hash: None,
118 graph: None,
119 });
120 graph.add_edge(Edge {
121 source: "index.md".into(),
122 target: "setup.md".into(),
123 edge_type: EdgeType::new("markdown", "inline"),
124 synthetic: false,
125 });
126
127 let config = Config::defaults();
128 let ctx = make_ctx(&graph, dir.path(), &config);
129 let diagnostics = DirectoryLinkRule.evaluate(&ctx);
130 assert!(diagnostics.is_empty());
131 }
132}