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::graph::test_helpers::make_enriched;
52    use crate::graph::{Edge, Graph, Node, NodeType, TargetProperties};
53    use crate::rules::RuleContext;
54    use std::collections::HashMap;
55
56    #[test]
57    fn detects_symlink_target() {
58        let mut graph = Graph::new();
59        graph.add_node(Node {
60            path: "index.md".into(),
61            node_type: NodeType::File,
62            hash: None,
63            graph: None,
64            is_graph: false,
65            metadata: HashMap::new(),
66        });
67        graph.target_properties.insert(
68            "setup.md".into(),
69            TargetProperties {
70                is_symlink: true,
71                is_directory: false,
72                symlink_target: Some("/shared/setup.md".into()),
73            },
74        );
75        graph.add_edge(Edge {
76            source: "index.md".into(),
77            target: "setup.md".into(),
78            link: None,
79            parser: "markdown".into(),
80        });
81
82        let enriched = make_enriched(graph);
83        let ctx = RuleContext {
84            graph: &enriched,
85            options: None,
86        };
87        let diagnostics = SymlinkEdgeRule.evaluate(&ctx);
88        assert_eq!(diagnostics.len(), 1);
89        assert_eq!(diagnostics[0].rule, "symlink-edge");
90        assert!(diagnostics[0].message.contains("symlink"));
91    }
92
93    #[test]
94    fn no_diagnostic_for_regular_file() {
95        let mut graph = Graph::new();
96        graph.add_node(Node {
97            path: "index.md".into(),
98            node_type: NodeType::File,
99            hash: None,
100            graph: None,
101            is_graph: false,
102            metadata: HashMap::new(),
103        });
104        graph.add_edge(Edge {
105            source: "index.md".into(),
106            target: "setup.md".into(),
107            link: None,
108            parser: "markdown".into(),
109        });
110
111        let enriched = make_enriched(graph);
112        let ctx = RuleContext {
113            graph: &enriched,
114            options: None,
115        };
116        let diagnostics = SymlinkEdgeRule.evaluate(&ctx);
117        assert!(diagnostics.is_empty());
118    }
119}