drft/rules/
directory_edge.rs1use crate::diagnostic::Diagnostic;
2use crate::rules::{Rule, RuleContext};
3
4pub struct DirectoryEdgeRule;
5
6impl Rule for DirectoryEdgeRule {
7 fn name(&self) -> &str {
8 "directory-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 if graph.nodes.contains_key(&edge.target) {
25 return None;
26 }
27
28 if graph.target_properties.get(&edge.target).is_some_and(|p| p.is_directory) {
30 Some(Diagnostic {
31 rule: "directory-edge".into(),
32 message: "links to directory, not file".into(),
33 source: Some(edge.source.clone()),
34 target: Some(edge.target.clone()),
35 fix: Some(format!(
36 "{}/ is a directory \u{2014} link to the specific file (e.g., {}/README.md)",
37 edge.target.trim_end_matches('/'),
38 edge.target.trim_end_matches('/')
39 )),
40 ..Default::default()
41 })
42 } else {
43 None
44 }
45 })
46 .collect()
47 }
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53 use crate::analyses::EnrichedGraph;
54 use crate::config::Config;
55 use crate::graph::{Edge, Graph, Node, NodeType, TargetProperties};
56 use crate::rules::RuleContext;
57 use std::collections::HashMap;
58
59 fn make_enriched(graph: Graph) -> EnrichedGraph {
60 crate::analyses::enrich_graph(graph, std::path::Path::new("."), &Config::defaults(), None)
61 }
62
63 #[test]
64 fn detects_directory_link() {
65 let mut graph = Graph::new();
66 graph.add_node(Node {
67 path: "index.md".into(),
68 node_type: NodeType::File,
69 hash: None,
70 graph: None,
71 metadata: HashMap::new(),
72 });
73 graph.target_properties.insert(
74 "guides".into(),
75 TargetProperties {
76 is_symlink: false,
77 is_directory: true,
78 symlink_target: None,
79 },
80 );
81 graph.add_edge(Edge {
82 source: "index.md".into(),
83 target: "guides".into(),
84 link: None,
85 parser: "markdown".into(),
86 });
87
88 let enriched = make_enriched(graph);
89 let ctx = RuleContext {
90 graph: &enriched,
91 options: None,
92 };
93 let diagnostics = DirectoryEdgeRule.evaluate(&ctx);
94 assert_eq!(diagnostics.len(), 1);
95 assert_eq!(diagnostics[0].rule, "directory-edge");
96 assert_eq!(diagnostics[0].target.as_deref(), Some("guides"));
97 }
98
99 #[test]
100 fn no_diagnostic_for_file_link() {
101 let mut graph = Graph::new();
102 graph.add_node(Node {
103 path: "index.md".into(),
104 node_type: NodeType::File,
105 hash: None,
106 graph: None,
107 metadata: HashMap::new(),
108 });
109 graph.add_node(Node {
110 path: "setup.md".into(),
111 node_type: NodeType::File,
112 hash: None,
113 graph: None,
114 metadata: HashMap::new(),
115 });
116 graph.add_edge(Edge {
117 source: "index.md".into(),
118 target: "setup.md".into(),
119 link: None,
120 parser: "markdown".into(),
121 });
122
123 let enriched = make_enriched(graph);
124 let ctx = RuleContext {
125 graph: &enriched,
126 options: None,
127 };
128 let diagnostics = DirectoryEdgeRule.evaluate(&ctx);
129 assert!(diagnostics.is_empty());
130 }
131}