linguini_analyzer/
reference.rs1use 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}