Skip to main content

presolve_compiler/
structural_component.rs

1//! TypeScript-authoritative structural component and props projection.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use serde::Serialize;
6
7use crate::{semantic_type_text, ControlFlowProvenanceV1, ObjectType, SemanticId, SemanticType};
8
9pub const STRUCTURAL_COMPONENT_SCHEMA_VERSION: u32 = 1;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
12#[serde(rename_all = "snake_case")]
13pub enum ComponentInheritanceStatusV1 {
14    ResolvedPresolveComponent,
15    Unresolved,
16    UnsupportedMixin,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum ComponentPropsResolutionV1 {
21    DefaultEmpty,
22    Resolved(SemanticType),
23    UnresolvedGeneric,
24}
25
26/// Facts supplied by the TypeScript authority; source names never decide them.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct StructuralComponentFactV1 {
29    pub component: SemanticId,
30    pub inheritance: ComponentInheritanceStatusV1,
31    pub props: ComponentPropsResolutionV1,
32    pub route_root: bool,
33    pub provenance: ControlFlowProvenanceV1,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
37pub struct StructuralComponentPropV1 {
38    pub name: String,
39    pub semantic_type: String,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
43pub struct StructuralComponentRecordV1 {
44    pub component: SemanticId,
45    pub props: Vec<StructuralComponentPropV1>,
46    pub route_root: bool,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
50#[serde(rename_all = "snake_case")]
51pub enum StructuralComponentDiagnosticReasonV1 {
52    UnknownComponent,
53    DuplicateFact,
54    UnresolvedInheritance,
55    UnsupportedMixinInheritance,
56    InvalidPropsShape,
57    RouteRootUnresolvedGenericProps,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
61pub struct StructuralComponentDiagnosticV1 {
62    pub component: SemanticId,
63    pub reason: StructuralComponentDiagnosticReasonV1,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
67pub struct StructuralComponentGraphV1 {
68    pub schema_version: u32,
69    pub components: Vec<StructuralComponentRecordV1>,
70    pub diagnostics: Vec<StructuralComponentDiagnosticV1>,
71}
72
73/// Builds records only for resolved Presolve component facts.
74#[must_use]
75pub fn build_structural_component_graph_v1(
76    components: &[SemanticId],
77    facts: &[StructuralComponentFactV1],
78) -> StructuralComponentGraphV1 {
79    let known = components.iter().collect::<BTreeSet<_>>();
80    let duplicate_facts = facts
81        .iter()
82        .fold(
83            (BTreeSet::new(), BTreeSet::new()),
84            |(mut seen, mut duplicate), fact| {
85                if !seen.insert(&fact.component) {
86                    duplicate.insert(&fact.component);
87                }
88                (seen, duplicate)
89            },
90        )
91        .1;
92    let mut records = BTreeMap::new();
93    let mut diagnostics = Vec::new();
94    for fact in facts {
95        if !known.contains(&fact.component) {
96            diagnostics.push(diagnostic(
97                fact,
98                StructuralComponentDiagnosticReasonV1::UnknownComponent,
99            ));
100            continue;
101        }
102        if duplicate_facts.contains(&fact.component) {
103            diagnostics.push(diagnostic(
104                fact,
105                StructuralComponentDiagnosticReasonV1::DuplicateFact,
106            ));
107            continue;
108        }
109        match fact.inheritance {
110            ComponentInheritanceStatusV1::Unresolved => {
111                diagnostics.push(diagnostic(
112                    fact,
113                    StructuralComponentDiagnosticReasonV1::UnresolvedInheritance,
114                ));
115                continue;
116            }
117            ComponentInheritanceStatusV1::UnsupportedMixin => {
118                diagnostics.push(diagnostic(
119                    fact,
120                    StructuralComponentDiagnosticReasonV1::UnsupportedMixinInheritance,
121                ));
122                continue;
123            }
124            ComponentInheritanceStatusV1::ResolvedPresolveComponent => {}
125        }
126        let props = match &fact.props {
127            ComponentPropsResolutionV1::DefaultEmpty => Vec::new(),
128            ComponentPropsResolutionV1::Resolved(SemanticType::Object(shape)) => props(shape),
129            ComponentPropsResolutionV1::Resolved(_) => {
130                diagnostics.push(diagnostic(
131                    fact,
132                    StructuralComponentDiagnosticReasonV1::InvalidPropsShape,
133                ));
134                continue;
135            }
136            ComponentPropsResolutionV1::UnresolvedGeneric => {
137                if fact.route_root {
138                    diagnostics.push(diagnostic(
139                        fact,
140                        StructuralComponentDiagnosticReasonV1::RouteRootUnresolvedGenericProps,
141                    ));
142                }
143                continue;
144            }
145        };
146        records.insert(
147            fact.component.clone(),
148            StructuralComponentRecordV1 {
149                component: fact.component.clone(),
150                props,
151                route_root: fact.route_root,
152            },
153        );
154    }
155    diagnostics.sort_by(|left, right| {
156        (&left.component, left.reason).cmp(&(&right.component, right.reason))
157    });
158    StructuralComponentGraphV1 {
159        schema_version: STRUCTURAL_COMPONENT_SCHEMA_VERSION,
160        components: records.into_values().collect(),
161        diagnostics,
162    }
163}
164
165fn props(shape: &ObjectType) -> Vec<StructuralComponentPropV1> {
166    shape
167        .properties
168        .iter()
169        .map(|(name, semantic_type)| StructuralComponentPropV1 {
170            name: name.clone(),
171            semantic_type: semantic_type_text(semantic_type),
172        })
173        .collect()
174}
175
176fn diagnostic(
177    fact: &StructuralComponentFactV1,
178    reason: StructuralComponentDiagnosticReasonV1,
179) -> StructuralComponentDiagnosticV1 {
180    StructuralComponentDiagnosticV1 {
181        component: fact.component.clone(),
182        reason,
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    fn provenance() -> ControlFlowProvenanceV1 {
191        ControlFlowProvenanceV1 {
192            path: "src/App.tsx".into(),
193            start: 0,
194            end: 1,
195            line: 1,
196            column: 1,
197        }
198    }
199
200    #[test]
201    fn projects_explicit_props_without_injecting_children() {
202        let card = SemanticId::component(None, "Card");
203        let props = ObjectType {
204            properties: BTreeMap::from([("title".into(), SemanticType::String)]),
205        };
206        let graph = build_structural_component_graph_v1(
207            std::slice::from_ref(&card),
208            &[StructuralComponentFactV1 {
209                component: card.clone(),
210                inheritance: ComponentInheritanceStatusV1::ResolvedPresolveComponent,
211                props: ComponentPropsResolutionV1::Resolved(SemanticType::Object(props)),
212                route_root: false,
213                provenance: provenance(),
214            }],
215        );
216        assert_eq!(graph.schema_version, STRUCTURAL_COMPONENT_SCHEMA_VERSION);
217        assert_eq!(graph.components[0].props[0].name, "title");
218        assert!(!graph.components[0]
219            .props
220            .iter()
221            .any(|prop| prop.name == "children"));
222    }
223
224    #[test]
225    fn rejects_unresolved_route_generic_props_at_the_component_boundary() {
226        let page = SemanticId::component(None, "Page");
227        let graph = build_structural_component_graph_v1(
228            std::slice::from_ref(&page),
229            &[StructuralComponentFactV1 {
230                component: page.clone(),
231                inheritance: ComponentInheritanceStatusV1::ResolvedPresolveComponent,
232                props: ComponentPropsResolutionV1::UnresolvedGeneric,
233                route_root: true,
234                provenance: provenance(),
235            }],
236        );
237        assert!(graph.components.is_empty());
238        assert_eq!(
239            graph.diagnostics[0].reason,
240            StructuralComponentDiagnosticReasonV1::RouteRootUnresolvedGenericProps
241        );
242    }
243}