presolve_compiler/
component_scope.rs1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::{ComponentNode, SemanticId};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct ComponentScopeGraph {
12 pub components: Vec<SemanticId>,
13 pub parent_by_component: BTreeMap<SemanticId, SemanticId>,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct ComponentScopeDiagnostic {
18 pub message: String,
19}
20
21impl ComponentScopeGraph {
22 #[must_use]
23 pub fn reflexive(components: &[ComponentNode]) -> Self {
24 let mut component_ids = components
25 .iter()
26 .map(|component| component.id.clone())
27 .collect::<Vec<_>>();
28 component_ids.sort();
29 component_ids.dedup();
30 Self {
31 components: component_ids,
32 parent_by_component: BTreeMap::new(),
33 }
34 }
35
36 #[must_use]
40 pub fn with_parent_relations(
41 components: &[ComponentNode],
42 parent_by_component: BTreeMap<SemanticId, SemanticId>,
43 ) -> Self {
44 Self {
45 components: Self::reflexive(components).components,
46 parent_by_component,
47 }
48 }
49
50 #[must_use]
51 pub fn parent_component(&self, component: &SemanticId) -> Option<&SemanticId> {
52 self.parent_by_component.get(component)
53 }
54
55 #[must_use]
57 pub fn ancestor_chain(&self, component: &SemanticId) -> Vec<SemanticId> {
58 if !self.components.contains(component) {
59 return Vec::new();
60 }
61
62 let mut chain = vec![component.clone()];
63 let mut seen = BTreeSet::from([component.clone()]);
64 let mut current = component;
65 while let Some(parent) = self.parent_component(current) {
66 if !seen.insert(parent.clone()) {
67 break;
68 }
69 chain.push(parent.clone());
70 current = parent;
71 }
72 chain
73 }
74
75 #[must_use]
76 pub fn diagnostics(&self) -> Vec<ComponentScopeDiagnostic> {
77 let component_ids = self.components.iter().collect::<BTreeSet<_>>();
78 let mut diagnostics = Vec::new();
79
80 for (child, parent) in &self.parent_by_component {
81 if !component_ids.contains(child) || !component_ids.contains(parent) {
82 diagnostics.push(ComponentScopeDiagnostic {
83 message: format!(
84 "component scope relation `{child}` -> `{parent}` references a missing component"
85 ),
86 });
87 }
88 }
89
90 for component in &self.components {
91 let mut seen = BTreeSet::from([component.clone()]);
92 let mut current = component;
93 while let Some(parent) = self.parent_component(current) {
94 if !seen.insert(parent.clone()) {
95 diagnostics.push(ComponentScopeDiagnostic {
96 message: format!("component scope graph contains a cycle at `{component}`"),
97 });
98 break;
99 }
100 current = parent;
101 }
102 }
103 diagnostics.sort_by(|left, right| left.message.cmp(&right.message));
104 diagnostics.dedup();
105 diagnostics
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use std::collections::BTreeMap;
112
113 use super::ComponentScopeGraph;
114 use crate::build_component_graph;
115
116 #[test]
117 fn preserves_explicit_parent_edges_without_inferring_any() {
118 let parsed = presolve_parser::parse_file(
119 "src/components.tsx",
120 r#"
121@component("x-parent")
122class Parent extends Component { render() { return <main />; } }
123@component("x-child")
124class Child extends Component { render() { return <main />; } }
125"#,
126 );
127 let components = build_component_graph(&parsed).components;
128 let parent = components[0].id.clone();
129 let child = components[1].id.clone();
130 let scope = ComponentScopeGraph::with_parent_relations(
131 &components,
132 BTreeMap::from([(child.clone(), parent.clone())]),
133 );
134
135 assert_eq!(scope.ancestor_chain(&child), vec![child, parent]);
136 assert!(scope.diagnostics().is_empty());
137 }
138
139 #[test]
140 fn reports_component_scope_cycles() {
141 let parsed = presolve_parser::parse_file(
142 "src/components.tsx",
143 r#"
144@component("x-first")
145class First extends Component { render() { return <main />; } }
146@component("x-second")
147class Second extends Component { render() { return <main />; } }
148"#,
149 );
150 let components = build_component_graph(&parsed).components;
151 let first = components[0].id.clone();
152 let second = components[1].id.clone();
153 let scope = ComponentScopeGraph::with_parent_relations(
154 &components,
155 BTreeMap::from([(first.clone(), second.clone()), (second, first)]),
156 );
157
158 assert!(!scope.diagnostics().is_empty());
159 }
160}