presolve_compiler/
slot.rs1use std::collections::BTreeMap;
2
3use crate::{
4 ComponentNode, ExecutionBoundary, SemanticOwner, SemanticType, SlotId, SlotKind,
5 SourceProvenance,
6};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct SlotEntity {
11 pub id: SlotId,
12 pub owner: crate::SemanticId,
13 pub authored_field: crate::SemanticId,
14 pub name: String,
15 pub kind: SlotKind,
16 pub semantic_type: SemanticType,
17 pub boundary: ExecutionBoundary,
18 pub declared_type_provenance: SourceProvenance,
19 pub provenance: SourceProvenance,
20}
21
22impl SlotEntity {
23 #[must_use]
24 pub fn semantic_owner(&self) -> SemanticOwner {
25 SemanticOwner::entity(self.owner.clone())
26 }
27}
28
29#[must_use]
31pub fn collect_slot_entities(components: &[ComponentNode]) -> BTreeMap<SlotId, SlotEntity> {
32 components
33 .iter()
34 .flat_map(|component| {
35 component.slot_declarations.iter().map(move |declaration| {
36 let id = SlotId::for_component(&component.id, &declaration.name);
37 (
38 id.clone(),
39 SlotEntity {
40 id,
41 owner: component.id.clone(),
42 authored_field: declaration.authored_field.clone(),
43 name: declaration.name.clone(),
44 kind: declaration.kind,
45 semantic_type: SemanticType::SlotContent,
46 boundary: ExecutionBoundary::Client,
47 declared_type_provenance: declaration.declared_type.provenance.clone(),
48 provenance: declaration.provenance.clone(),
49 },
50 )
51 })
52 })
53 .collect()
54}
55
56#[cfg(test)]
57mod tests {
58 use crate::{
59 build_application_semantic_model, build_semantic_graph,
60 validate_application_semantic_model, AuthoredDeclarationKind, ExecutionBoundary,
61 SemanticEntityKind, SemanticOwner, SemanticType, SlotDeclarationViolation, SlotId,
62 SlotKind,
63 };
64
65 #[test]
66 fn lowers_default_and_named_slots_with_canonical_identity_and_ownership() {
67 let parsed = presolve_parser::parse_file(
68 "src/Card.tsx",
69 r#"
70@component("x-card")
71class Card extends Component {
72 @slot()
73 children!: SlotContent;
74
75 @slot()
76 header!: SlotContent;
77
78 render() { return <section />; }
79}
80"#,
81 );
82 let asm = build_application_semantic_model(&parsed);
83 let component = &asm.components[0];
84 let children = asm
85 .slot(&SlotId::for_component(&component.id, "children"))
86 .expect("default slot");
87 let header = asm
88 .slot(&SlotId::for_component(&component.id, "header"))
89 .expect("named slot");
90
91 assert_eq!(asm.slots().len(), 2);
92 assert_eq!(children.kind, SlotKind::Default);
93 assert_eq!(header.kind, SlotKind::Named);
94 assert_eq!(children.semantic_type, SemanticType::SlotContent);
95 assert_eq!(children.boundary, ExecutionBoundary::Client);
96 assert_eq!(children.owner, component.id);
97 assert_eq!(children.authored_field, component.id.slot_field("children"));
98 assert_eq!(
99 children.id.as_str(),
100 "module:src/Card.tsx/component:x-card/slot:children"
101 );
102 assert_eq!(
103 asm.owner(children.id.as_semantic_id()),
104 Some(&SemanticOwner::entity(component.id.clone()))
105 );
106 assert_eq!(
107 asm.semantic_type_of(children.id.as_semantic_id()),
108 Some(&SemanticType::SlotContent)
109 );
110 assert!(asm
111 .entity(children.id.as_semantic_id())
112 .is_some_and(|entity| { entity.kind() == SemanticEntityKind::Slot }));
113 assert!(children.provenance.span.start < children.provenance.span.end);
114 assert!(
115 children.declared_type_provenance.span.start
116 < children.declared_type_provenance.span.end
117 );
118 let diagnostics = validate_application_semantic_model(&asm);
119 assert!(diagnostics.is_empty(), "{diagnostics:#?}");
120 }
121
122 #[test]
123 fn retains_invalid_slot_field_shape_facts_without_creating_slot_ids() {
124 let parsed = presolve_parser::parse_file(
125 "src/InvalidSlots.tsx",
126 r#"
127@component("x-invalid-slots")
128class InvalidSlots extends Component {
129 @slot("header") argument!: SlotContent;
130 @slot() static staticSlot!: SlotContent;
131 @slot() missingType!;
132 @slot() wrongType!: string;
133 @slot() initialized: SlotContent = "bad";
134 @slot() notDefinite: SlotContent;
135 render() { return <main />; }
136}
137"#,
138 );
139 let asm = build_application_semantic_model(&parsed);
140 let candidates = &asm.components[0].slot_declaration_candidates;
141
142 assert!(asm.slots().is_empty());
143 assert_eq!(candidates.len(), 6);
144 assert!(candidates
145 .iter()
146 .any(
147 |candidate| candidate.violations.iter().any(|violation| matches!(
148 violation,
149 SlotDeclarationViolation::DecoratorArity { .. }
150 ))
151 ));
152 assert!(candidates
153 .iter()
154 .any(
155 |candidate| candidate.violations.iter().any(|violation| matches!(
156 violation,
157 SlotDeclarationViolation::StaticDeclarationUnsupported
158 ))
159 ));
160 assert_eq!(
161 candidates
162 .iter()
163 .filter(
164 |candidate| candidate.violations.iter().any(|violation| matches!(
165 violation,
166 SlotDeclarationViolation::InvalidDeclaredType { .. }
167 ))
168 )
169 .count(),
170 2
171 );
172 assert!(candidates.iter().any(|candidate| candidate
173 .violations
174 .iter()
175 .any(|violation| matches!(violation, SlotDeclarationViolation::ForbiddenInitializer))));
176 assert!(candidates
177 .iter()
178 .any(
179 |candidate| candidate.violations.iter().any(|violation| matches!(
180 violation,
181 SlotDeclarationViolation::DefiniteAssignmentRequired
182 ))
183 ));
184 assert!(candidates.iter().all(|candidate| {
185 !asm.slots.contains_key(&SlotId::for_component(
186 &candidate.owner_component,
187 candidate.field_name.as_deref().unwrap_or("invalid"),
188 ))
189 }));
190 }
191
192 #[test]
193 fn retains_slot_conflict_duplicate_and_invalid_declaration_kind_facts() {
194 let parsed = presolve_parser::parse_file(
195 "src/InvalidSlotKinds.tsx",
196 r#"
197@component("x-invalid-slot-kinds")
198class InvalidSlotKinds extends Component {
199 @slot() @context() conflict!: SlotContent;
200 @slot() duplicate!: SlotContent;
201 @slot() duplicate!: SlotContent;
202 @slot() method() {}
203 @slot() get accessor(): SlotContent { return this.value; }
204 attach(@slot() content: SlotContent) {}
205 render() { return <main />; }
206}
207"#,
208 );
209 let asm = build_application_semantic_model(&parsed);
210 let candidates = &asm.components[0].slot_declaration_candidates;
211
212 assert!(asm.slots().is_empty());
213 assert_eq!(candidates.len(), 6);
214 assert!(candidates.iter().any(|candidate| candidate
215 .violations
216 .contains(&SlotDeclarationViolation::ConflictingSemanticDecorators)));
217 assert_eq!(
218 candidates
219 .iter()
220 .filter(|candidate| candidate
221 .violations
222 .contains(&SlotDeclarationViolation::DuplicateSlot))
223 .count(),
224 2
225 );
226 assert!(candidates.iter().any(|candidate| {
227 candidate
228 .violations
229 .contains(&SlotDeclarationViolation::InvalidDeclarationKind {
230 actual: AuthoredDeclarationKind::Method,
231 })
232 }));
233 assert!(candidates.iter().any(|candidate| {
234 candidate
235 .violations
236 .contains(&SlotDeclarationViolation::InvalidDeclarationKind {
237 actual: AuthoredDeclarationKind::Getter,
238 })
239 }));
240 assert!(candidates.iter().any(|candidate| {
241 candidate
242 .violations
243 .contains(&SlotDeclarationViolation::InvalidDeclarationKind {
244 actual: AuthoredDeclarationKind::Parameter,
245 })
246 }));
247 }
248
249 #[test]
250 fn slot_order_is_canonical_across_components_and_fields() {
251 let parsed = presolve_parser::parse_file(
252 "src/Slots.tsx",
253 r#"
254@component("x-z") class Z extends Component {
255 @slot() zebra!: SlotContent;
256 @slot() alpha!: SlotContent;
257 render() { return <main />; }
258}
259@component("x-a") class A extends Component {
260 @slot() children!: SlotContent;
261 render() { return <main />; }
262}
263"#,
264 );
265 let asm = build_application_semantic_model(&parsed);
266 let ids = asm.slots.keys().map(SlotId::as_str).collect::<Vec<_>>();
267 let mut sorted = ids.clone();
268 sorted.sort_unstable();
269 assert_eq!(ids, sorted);
270 }
271
272 #[test]
273 fn keeps_slots_out_of_the_frozen_phase_g_semantic_graph_schema() {
274 let parsed = presolve_parser::parse_file(
275 "src/Card.tsx",
276 r#"
277@component("x-card")
278class Card extends Component {
279 @slot() children!: SlotContent;
280 render() { return <section />; }
281}
282"#,
283 );
284 let asm = build_application_semantic_model(&parsed);
285 let slot_id = asm
286 .slots()
287 .into_iter()
288 .next()
289 .expect("slot entity")
290 .id
291 .clone();
292 let graph = build_semantic_graph(&asm);
293
294 assert_eq!(graph.schema_version, 6);
295 assert!(graph
296 .nodes
297 .iter()
298 .all(|node| node.id != *slot_id.as_semantic_id()));
299 assert!(graph.edges.iter().all(|edge| {
300 edge.source != *slot_id.as_semantic_id() && edge.target != *slot_id.as_semantic_id()
301 }));
302 }
303}