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 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}