drft-cli 0.7.0

A structural integrity checker for linked file systems
Documentation
use crate::diagnostic::Diagnostic;
use crate::rules::{Rule, RuleContext};

pub struct DirectedCycleRule;

impl Rule for DirectedCycleRule {
    fn name(&self) -> &str {
        "directed-cycle"
    }

    fn evaluate(&self, ctx: &RuleContext) -> Vec<Diagnostic> {
        let result = &ctx.graph.scc;

        result
            .sccs
            .iter()
            .map(|scc| {
                let mut path = scc.members.clone();
                if let Some(first) = path.first().cloned() {
                    path.push(first);
                }

                let fix = format!(
                    "circular dependency \u{2014} review whether one of these links can be removed or the content restructured: {}",
                    scc.members.join(" \u{2192} ")
                );

                Diagnostic {
                    rule: "directed-cycle".into(),
                    message: "cycle detected".into(),
                    path: Some(path),
                    fix: Some(fix),
                    ..Default::default()
                }
            })
            .collect()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::graph::Graph;
    use crate::graph::test_helpers::{make_edge, make_enriched, make_node};
    use crate::graph::{Edge, Node};
    use crate::rules::RuleContext;
    use std::collections::HashMap;

    #[test]
    fn detects_simple_cycle() {
        let mut graph = Graph::new();
        graph.add_node(make_node("a.md"));
        graph.add_node(make_node("b.md"));
        graph.add_node(make_node("c.md"));
        graph.add_edge(make_edge("a.md", "b.md"));
        graph.add_edge(make_edge("b.md", "c.md"));
        graph.add_edge(make_edge("c.md", "a.md"));

        let enriched = make_enriched(graph);
        let ctx = RuleContext {
            graph: &enriched,
            options: None,
        };
        let diagnostics = DirectedCycleRule.evaluate(&ctx);
        assert_eq!(diagnostics.len(), 1);
        assert_eq!(diagnostics[0].rule, "directed-cycle");

        let path = diagnostics[0].path.as_ref().unwrap();
        assert_eq!(path.first(), path.last());
        assert!(path.contains(&"a.md".to_string()));
        assert!(path.contains(&"b.md".to_string()));
        assert!(path.contains(&"c.md".to_string()));
    }

    #[test]
    fn no_cycle_in_dag() {
        let mut graph = Graph::new();
        graph.add_node(make_node("a.md"));
        graph.add_node(make_node("b.md"));
        graph.add_node(make_node("c.md"));
        graph.add_edge(make_edge("a.md", "b.md"));
        graph.add_edge(make_edge("b.md", "c.md"));

        let enriched = make_enriched(graph);
        let ctx = RuleContext {
            graph: &enriched,
            options: None,
        };
        let diagnostics = DirectedCycleRule.evaluate(&ctx);
        assert!(diagnostics.is_empty());
    }

    #[test]
    fn ignores_broken_link_edges() {
        let mut graph = Graph::new();
        graph.add_node(make_node("a.md"));
        graph.add_node(Node {
            path: "missing.md".into(),
            node_type: None,
            included: true,
            hash: None,
            metadata: HashMap::new(),
        });
        graph.add_edge(Edge {
            source: "a.md".into(),
            target: "missing.md".into(),
            link: None,
            parser: "markdown".into(),
        });

        let enriched = make_enriched(graph);
        let ctx = RuleContext {
            graph: &enriched,
            options: None,
        };
        let diagnostics = DirectedCycleRule.evaluate(&ctx);
        assert!(diagnostics.is_empty());
    }
}