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