Skip to main content

drft/rules/
directory_link.rs

1use 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                // Skip external URLs
20                if edge.target.starts_with("http://") || edge.target.starts_with("https://") {
21                    return None;
22                }
23
24                // If target is in the graph as a known node, skip
25                if graph.nodes.contains_key(&edge.target) {
26                    return None;
27                }
28
29                // Check if target is a directory on the filesystem
30                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}