1use 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#[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#[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}