linguini-analyzer 0.1.0-alpha.4

Semantic diagnostics for Linguini schema and locale files
Documentation
use crate::{Diagnostic, NamedSpan};
use linguini_syntax::Span;
use std::collections::{BTreeMap, BTreeSet};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReferenceNode {
    pub name: String,
    pub references: Vec<NamedSpan>,
    pub span: Span,
}

impl ReferenceNode {
    pub fn new(name: impl Into<String>, references: Vec<NamedSpan>, span: Span) -> Self {
        Self {
            name: name.into(),
            references,
            span,
        }
    }
}

pub fn detect_reference_cycles(nodes: &[ReferenceNode]) -> Vec<Diagnostic> {
    let graph: BTreeMap<_, _> = nodes
        .iter()
        .map(|node| (node.name.as_str(), node.references.as_slice()))
        .collect();
    let spans: BTreeMap<_, _> = nodes
        .iter()
        .map(|node| (node.name.as_str(), node.span))
        .collect();
    let mut diagnostics = Vec::new();
    let mut reported = BTreeSet::new();

    for node in nodes {
        let mut path = Vec::new();
        visit_cycle(
            node.name.as_str(),
            node.name.as_str(),
            &graph,
            &spans,
            &mut path,
            &mut reported,
            &mut diagnostics,
        );
    }

    diagnostics
}

fn visit_cycle(
    start: &str,
    current: &str,
    graph: &BTreeMap<&str, &[NamedSpan]>,
    spans: &BTreeMap<&str, Span>,
    path: &mut Vec<String>,
    reported: &mut BTreeSet<String>,
    diagnostics: &mut Vec<Diagnostic>,
) {
    if path.iter().any(|item| item == current) {
        if current == start && reported.insert(start.to_owned()) {
            let mut cycle = path.clone();
            cycle.push(start.to_owned());
            let span = spans.get(start).copied().unwrap_or(Span::new(0, 0));
            diagnostics.push(Diagnostic::error(
                format!("cyclic reference `{}`", cycle.join(" -> ")),
                span,
            ));
        }
        return;
    }

    path.push(current.to_owned());
    if let Some(references) = graph.get(current) {
        for reference in *references {
            if graph.contains_key(reference.name.as_str()) {
                visit_cycle(
                    start,
                    reference.name.as_str(),
                    graph,
                    spans,
                    path,
                    reported,
                    diagnostics,
                );
            }
        }
    }
    path.pop();
}