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