drft/rules/
indirect_link.rs1use crate::diagnostic::Diagnostic;
2use crate::rules::{Rule, RuleContext};
3
4pub struct IndirectLinkRule;
5
6impl Rule for IndirectLinkRule {
7 fn name(&self) -> &str {
8 "indirect-link"
9 }
10
11 fn evaluate(&self, ctx: &RuleContext) -> Vec<Diagnostic> {
12 let graph = ctx.graph;
13 let root = ctx.root;
14
15 graph
16 .edges
17 .iter()
18 .filter_map(|edge| {
19 if edge.target.starts_with("http://") || edge.target.starts_with("https://") {
21 return None;
22 }
23
24 let target_path = root.join(&edge.target);
26 if target_path.is_symlink() {
27 let resolved = std::fs::read_link(&target_path)
28 .map(|p| p.to_string_lossy().to_string())
29 .unwrap_or_else(|_| "unknown".to_string());
30 Some(Diagnostic {
31 rule: "indirect-link".into(),
32 message: format!("target is a symlink to {resolved}"),
33 source: Some(edge.source.clone()),
34 target: Some(edge.target.clone()),
35 fix: Some(format!(
36 "{} is a symlink to {resolved} \u{2014} consider linking to the actual file directly in {}",
37 edge.target, edge.source
38 )),
39 ..Default::default()
40 })
41 } else {
42 None
43 }
44 })
45 .collect()
46 }
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52 use crate::config::Config;
53 use crate::graph::{Edge, EdgeType, Graph, Node, NodeType};
54 use crate::rules::RuleContext;
55 use std::fs;
56 use std::os::unix::fs::symlink;
57 use std::path::Path;
58 use tempfile::TempDir;
59
60 fn make_ctx<'a>(graph: &'a Graph, root: &'a Path, config: &'a Config) -> RuleContext<'a> {
61 RuleContext {
62 graph,
63 root,
64 config,
65 lockfile: None,
66 }
67 }
68
69 #[test]
70 fn detects_symlink_target() {
71 let dir = TempDir::new().unwrap();
72 let shared = dir.path().join("shared");
73 fs::create_dir(&shared).unwrap();
74 fs::write(shared.join("setup.md"), "# Setup").unwrap();
75 symlink(shared.join("setup.md"), dir.path().join("setup.md")).unwrap();
76
77 let mut graph = Graph::new();
78 graph.add_node(Node {
79 path: "index.md".into(),
80 node_type: NodeType::Source,
81 hash: None,
82 graph: None,
83 });
84 graph.add_edge(Edge {
85 source: "index.md".into(),
86 target: "setup.md".into(),
87 edge_type: EdgeType::new("markdown", "inline"),
88 synthetic: false,
89 });
90
91 let config = Config::defaults();
92 let ctx = make_ctx(&graph, dir.path(), &config);
93 let diagnostics = IndirectLinkRule.evaluate(&ctx);
94 assert_eq!(diagnostics.len(), 1);
95 assert_eq!(diagnostics[0].rule, "indirect-link");
96 assert!(diagnostics[0].message.contains("symlink"));
97 }
98
99 #[test]
100 fn no_diagnostic_for_regular_file() {
101 let dir = TempDir::new().unwrap();
102 fs::write(dir.path().join("setup.md"), "# Setup").unwrap();
103
104 let mut graph = Graph::new();
105 graph.add_node(Node {
106 path: "index.md".into(),
107 node_type: NodeType::Source,
108 hash: None,
109 graph: None,
110 });
111 graph.add_edge(Edge {
112 source: "index.md".into(),
113 target: "setup.md".into(),
114 edge_type: EdgeType::new("markdown", "inline"),
115 synthetic: false,
116 });
117
118 let config = Config::defaults();
119 let ctx = make_ctx(&graph, dir.path(), &config);
120 let diagnostics = IndirectLinkRule.evaluate(&ctx);
121 assert!(diagnostics.is_empty());
122 }
123}