Skip to main content

drft/rules/
symlink_edge.rs

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