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 if the target node is a symlink
24                let target_node = graph.nodes.get(&edge.target)?;
25                if target_node.node_type != Some(crate::graph::NodeType::Symlink) {
26                    return None;
27                }
28
29                // Find the filesystem edge from this node to get the symlink target
30                let symlink_target = graph.forward.get(&edge.target)
31                    .and_then(|indices| indices.iter()
32                        .find_map(|&idx| {
33                            let fs_edge = &graph.edges[idx];
34                            if fs_edge.parser == "filesystem" { Some(fs_edge.target.clone()) } else { None }
35                        }));
36
37                let resolved = symlink_target.as_deref().unwrap_or("unknown");
38                Some(Diagnostic {
39                    rule: "symlink-edge".into(),
40                    message: format!("target is a symlink to {resolved}"),
41                    source: Some(edge.source.clone()),
42                    target: Some(edge.target.clone()),
43                    fix: Some(format!(
44                        "{} is a symlink to {resolved} \u{2014} consider linking to the actual file directly in {}",
45                        edge.target, edge.source
46                    )),
47                    ..Default::default()
48                })
49            })
50            .collect()
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use crate::graph::test_helpers::{make_enriched, make_node};
58    use crate::graph::{Edge, Graph, Node, NodeType};
59    use crate::rules::RuleContext;
60    use std::collections::HashMap;
61
62    #[test]
63    fn detects_symlink_target() {
64        let mut graph = Graph::new();
65        graph.add_node(make_node("index.md"));
66        graph.add_node(Node {
67            path: "setup.md".into(),
68            node_type: Some(NodeType::Symlink),
69            included: true,
70            hash: None,
71            metadata: HashMap::new(),
72        });
73        graph.add_node(Node {
74            path: "/shared/setup.md".into(),
75            node_type: Some(NodeType::File),
76            included: false,
77            hash: None,
78            metadata: HashMap::new(),
79        });
80        // Filesystem edge from symlink to its target
81        graph.add_edge(Edge {
82            source: "setup.md".into(),
83            target: "/shared/setup.md".into(),
84            link: None,
85            parser: "filesystem".into(),
86        });
87        // Content edge from index.md to the symlink
88        graph.add_edge(Edge {
89            source: "index.md".into(),
90            target: "setup.md".into(),
91            link: None,
92            parser: "markdown".into(),
93        });
94
95        let enriched = make_enriched(graph);
96        let ctx = RuleContext {
97            graph: &enriched,
98            options: None,
99        };
100        let diagnostics = SymlinkEdgeRule.evaluate(&ctx);
101        assert_eq!(diagnostics.len(), 1);
102        assert_eq!(diagnostics[0].rule, "symlink-edge");
103        assert!(diagnostics[0].message.contains("symlink"));
104    }
105
106    #[test]
107    fn no_diagnostic_for_regular_file() {
108        let mut graph = Graph::new();
109        graph.add_node(make_node("index.md"));
110        graph.add_node(Node {
111            path: "setup.md".into(),
112            node_type: Some(NodeType::File),
113            included: false,
114            hash: None,
115            metadata: HashMap::new(),
116        });
117        graph.add_edge(Edge {
118            source: "index.md".into(),
119            target: "setup.md".into(),
120            link: None,
121            parser: "markdown".into(),
122        });
123
124        let enriched = make_enriched(graph);
125        let ctx = RuleContext {
126            graph: &enriched,
127            options: None,
128        };
129        let diagnostics = SymlinkEdgeRule.evaluate(&ctx);
130        assert!(diagnostics.is_empty());
131    }
132}