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