Skip to main content

drft/rules/
orphan_node.rs

1use 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}