drft/rules/
symlink_edge.rs1use 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 if crate::graph::is_uri(&edge.target) {
20 return None;
21 }
22
23 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 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 graph.add_edge(Edge {
82 source: "setup.md".into(),
83 target: "/shared/setup.md".into(),
84 link: None,
85 parser: "filesystem".into(),
86 });
87 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}