Skip to main content

linguini_analyzer/
reference.rs

1use crate::{Diagnostic, NamedSpan};
2use linguini_syntax::Span;
3use std::collections::{BTreeMap, BTreeSet};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct ReferenceNode {
7    pub name: String,
8    pub references: Vec<NamedSpan>,
9    pub span: Span,
10}
11
12impl ReferenceNode {
13    pub fn new(name: impl Into<String>, references: Vec<NamedSpan>, span: Span) -> Self {
14        Self {
15            name: name.into(),
16            references,
17            span,
18        }
19    }
20}
21
22pub fn detect_reference_cycles(nodes: &[ReferenceNode]) -> Vec<Diagnostic> {
23    let graph: BTreeMap<_, _> = nodes
24        .iter()
25        .map(|node| (node.name.as_str(), node.references.as_slice()))
26        .collect();
27    let spans: BTreeMap<_, _> = nodes
28        .iter()
29        .map(|node| (node.name.as_str(), node.span))
30        .collect();
31    let mut diagnostics = Vec::new();
32    let mut reported = BTreeSet::new();
33
34    for node in nodes {
35        let mut path = Vec::new();
36        visit_cycle(
37            node.name.as_str(),
38            node.name.as_str(),
39            &graph,
40            &spans,
41            &mut path,
42            &mut reported,
43            &mut diagnostics,
44        );
45    }
46
47    diagnostics
48}
49
50fn visit_cycle(
51    start: &str,
52    current: &str,
53    graph: &BTreeMap<&str, &[NamedSpan]>,
54    spans: &BTreeMap<&str, Span>,
55    path: &mut Vec<String>,
56    reported: &mut BTreeSet<String>,
57    diagnostics: &mut Vec<Diagnostic>,
58) {
59    if path.iter().any(|item| item == current) {
60        if current == start && reported.insert(start.to_owned()) {
61            let mut cycle = path.clone();
62            cycle.push(start.to_owned());
63            let span = spans.get(start).copied().unwrap_or(Span::new(0, 0));
64            diagnostics.push(Diagnostic::error(
65                format!("cyclic reference `{}`", cycle.join(" -> ")),
66                span,
67            ));
68        }
69        return;
70    }
71
72    path.push(current.to_owned());
73    if let Some(references) = graph.get(current) {
74        for reference in *references {
75            if graph.contains_key(reference.name.as_str()) {
76                visit_cycle(
77                    start,
78                    reference.name.as_str(),
79                    graph,
80                    spans,
81                    path,
82                    reported,
83                    diagnostics,
84                );
85            }
86        }
87    }
88    path.pop();
89}