1use crate::analyses::Analysis;
2use crate::analyses::AnalysisContext;
3use crate::analyses::degree::Degree;
4use crate::diagnostic::Diagnostic;
5use crate::rules::{Rule, RuleContext};
6
7pub struct OrphanRule;
8
9impl Rule for OrphanRule {
10 fn name(&self) -> &str {
11 "orphan"
12 }
13
14 fn evaluate(&self, ctx: &RuleContext) -> Vec<Diagnostic> {
15 let analysis_ctx = AnalysisContext {
16 graph: ctx.graph,
17 root: ctx.root,
18 config: ctx.config,
19 lockfile: ctx.lockfile,
20 };
21 let result = Degree.run(&analysis_ctx);
22
23 result
24 .nodes
25 .iter()
26 .filter(|nd| nd.in_degree == 0)
27 .map(|nd| Diagnostic {
28 rule: "orphan".into(),
29 message: "no inbound links".into(),
30 node: Some(nd.node.clone()),
31 fix: Some(format!(
32 "{} has no inbound links — either link to it from another file or remove it",
33 nd.node
34 )),
35 ..Default::default()
36 })
37 .collect()
38 }
39}
40
41#[cfg(test)]
42mod tests {
43 use super::*;
44 use crate::config::Config;
45 use crate::graph::Graph;
46 use crate::graph::test_helpers::{make_edge, make_node};
47 use crate::rules::RuleContext;
48 use std::path::Path;
49
50 fn make_ctx<'a>(graph: &'a Graph, config: &'a Config) -> RuleContext<'a> {
51 RuleContext {
52 graph,
53 root: Path::new("."),
54 config,
55 lockfile: None,
56 }
57 }
58
59 #[test]
60 fn detects_orphan() {
61 let mut graph = Graph::new();
62 graph.add_node(make_node("index.md"));
63 graph.add_node(make_node("orphan.md"));
64 graph.add_edge(make_edge("index.md", "setup.md"));
65
66 let config = Config::defaults();
67 let diagnostics = OrphanRule.evaluate(&make_ctx(&graph, &config));
68
69 let orphan_nodes: Vec<&str> = diagnostics
70 .iter()
71 .map(|d| d.node.as_deref().unwrap())
72 .collect();
73 assert!(orphan_nodes.contains(&"orphan.md"));
74 assert!(orphan_nodes.contains(&"index.md"));
75 }
76
77 #[test]
78 fn linked_file_is_not_orphan() {
79 let mut graph = Graph::new();
80 graph.add_node(make_node("index.md"));
81 graph.add_node(make_node("setup.md"));
82 graph.add_edge(make_edge("index.md", "setup.md"));
83
84 let config = Config::defaults();
85 let diagnostics = OrphanRule.evaluate(&make_ctx(&graph, &config));
86
87 let orphan_nodes: Vec<&str> = diagnostics
88 .iter()
89 .map(|d| d.node.as_deref().unwrap())
90 .collect();
91 assert!(!orphan_nodes.contains(&"setup.md"));
92 assert!(orphan_nodes.contains(&"index.md"));
93 }
94}