Skip to main content

presolve_compiler/
component_scope.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::{ComponentNode, SemanticId};
4
5/// Immutable compiler-owned component ancestry input for Context visibility.
6///
7/// Phase G populates this graph reflexively only. Future compiler-owned
8/// composition semantics may add validated parent edges without changing the
9/// Context resolution algorithm.
10#[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    /// Constructs a canonical scope graph from pre-existing compiler-owned
37    /// parent facts. This is a testable seam for future composition lowering;
38    /// it never infers a parent relation from imports or source structure.
39    #[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    /// Returns `self` followed by canonical parents, nearest first.
56    #[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}