Skip to main content

drft/rules/
directory_edge.rs

1use 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                // Skip URIs
19                if crate::graph::is_uri(&edge.target) {
20                    return None;
21                }
22
23                // If target is in the graph as a known node, skip
24                if graph.nodes.contains_key(&edge.target) {
25                    return None;
26                }
27
28                // Check target properties set during graph building
29                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}