1use std::collections::{BTreeMap, BTreeSet};
4
5use crate::component_graph::render_event_handlers;
6use crate::component_graph::SerializableValue;
7use crate::{
8 ApplicationSemanticModel, ComponentInstanceId, ComponentInstanceStatus, IrStorageId,
9 SemanticId, SemanticReferenceKind, SourceProvenance, TemplateInstanceBindingId,
10 TemplateInstanceTargetId, TemplateSemanticKind,
11};
12
13pub const ORDINARY_TEMPLATE_INSTANCE_REGISTRY_VERSION: u32 = 1;
14
15#[derive(Debug, Clone, Copy, Eq, PartialEq)]
16pub enum OrdinaryTemplateTargetKind {
17 Element,
18 AttributeOrPropertyHost,
19 EventHost,
20 ConditionalBoundary,
21 ListBoundary,
22 FormControlHost,
23 FormSubmissionHost,
24}
25
26#[derive(Debug, Clone, Copy, Eq, PartialEq)]
27pub enum OrdinaryTemplateBindingKind {
28 Text,
29 Attribute,
30 Property,
31 Conditional,
32 List,
33 FormControl,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct OrdinaryTemplateInstanceTargetRecord {
38 pub target_id: TemplateInstanceTargetId,
39 pub component_instance_id: ComponentInstanceId,
40 pub component_id: SemanticId,
41 pub template_entity_id: SemanticId,
42 pub target_kind: OrdinaryTemplateTargetKind,
43 pub provenance: SourceProvenance,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct OrdinaryTemplateInstanceBindingRecord {
48 pub instance_binding_id: TemplateInstanceBindingId,
49 pub component_instance_id: ComponentInstanceId,
50 pub component_id: SemanticId,
51 pub declaration_binding_id: SemanticId,
52 pub target_id: TemplateInstanceTargetId,
53 pub binding_kind: OrdinaryTemplateBindingKind,
54 pub state_storage_ids: Vec<IrStorageId>,
55 pub computed_ids: Vec<SemanticId>,
56 pub existing_program_identity: SemanticId,
57 pub expression: Option<String>,
58 pub attribute_name: Option<String>,
59 pub provenance: SourceProvenance,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct OrdinaryTemplateInstanceEventRecord {
64 pub component_instance_id: ComponentInstanceId,
65 pub component_id: SemanticId,
66 pub target_id: TemplateInstanceTargetId,
67 pub declaration_event_id: SemanticId,
68 pub event_type: String,
69 pub handler_method_id: SemanticId,
70 pub action_batch_id: Option<SemanticId>,
71 pub arguments: Vec<SerializableValue>,
72 pub existing_event_program_identity: SemanticId,
73 pub provenance: SourceProvenance,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct OrdinaryTemplateInstanceRegistry {
78 pub version: u32,
79 pub targets: Vec<OrdinaryTemplateInstanceTargetRecord>,
80 pub bindings: Vec<OrdinaryTemplateInstanceBindingRecord>,
81 pub events: Vec<OrdinaryTemplateInstanceEventRecord>,
82}
83
84#[must_use]
85#[allow(clippy::too_many_lines)]
86pub fn build_ordinary_template_instance_registry(
87 model: &ApplicationSemanticModel,
88) -> OrdinaryTemplateInstanceRegistry {
89 let mut targets = BTreeMap::new();
90 let mut bindings = Vec::new();
91 let mut events = Vec::new();
92 for instance in model.component_instance_plan.instances.values() {
93 if !matches!(
94 instance.status,
95 ComponentInstanceStatus::Planned | ComponentInstanceStatus::StructuralTemplate
96 ) {
97 continue;
98 }
99 let template = instance.component.template();
100 let entities = model.template_entities_for(&template);
101 for entity in &entities {
102 let Some(binding_kind) = binding_kind(entity.kind) else {
103 continue;
104 };
105 let target_entity = target_entity_for_binding(entity, &entities);
106 let target_id = TemplateInstanceTargetId::for_component_instance_template_entity(
107 instance.id.clone(),
108 target_entity.id.clone(),
109 );
110 targets.entry(target_id.clone()).or_insert_with(|| {
111 OrdinaryTemplateInstanceTargetRecord {
112 target_id: target_id.clone(),
113 component_instance_id: instance.id.clone(),
114 component_id: instance.component.clone(),
115 template_entity_id: target_entity.id.clone(),
116 target_kind: target_kind(target_entity.kind),
117 provenance: target_entity.provenance.clone(),
118 }
119 });
120 let (state_storage_ids, computed_ids) = binding_dependencies(model, &entity.id);
121 bindings.push(OrdinaryTemplateInstanceBindingRecord {
122 instance_binding_id: TemplateInstanceBindingId::for_component_instance_binding(
123 instance.id.clone(),
124 entity.id.clone(),
125 ),
126 component_instance_id: instance.id.clone(),
127 component_id: instance.component.clone(),
128 declaration_binding_id: entity.id.clone(),
129 target_id,
130 binding_kind,
131 state_storage_ids,
132 computed_ids,
133 existing_program_identity: entity.id.clone(),
134 expression: entity.expression.clone(),
135 attribute_name: entity.attribute_name.clone(),
136 provenance: entity.provenance.clone(),
137 });
138 }
139 if let Some(component) = model.component(&instance.component) {
140 if let Some(render) = &component.render {
141 for event in render_event_handlers(render) {
142 let Some(event_entity) = entities.iter().find(|entity| {
143 entity.kind == TemplateSemanticKind::EventAttribute
144 && provenance_matches_span(&entity.provenance, &event.span)
145 }) else {
146 continue;
147 };
148 let target_entity = target_entity_for_event(event_entity, &entities);
149 let target_id =
150 TemplateInstanceTargetId::for_component_instance_template_entity(
151 instance.id.clone(),
152 target_entity.id.clone(),
153 );
154 let name = event
155 .handler
156 .strip_prefix("this.")
157 .unwrap_or(&event.handler);
158 if let Some(endpoint) = component.action_endpoint_id(name) {
159 let Some(action_batch_id) = model
160 .effect_trigger_plan
161 .action_batches
162 .values()
163 .find(|batch| batch.authored_action_endpoint == endpoint)
164 .map(|batch| batch.id.clone())
165 else {
166 continue;
167 };
168 targets.entry(target_id.clone()).or_insert_with(|| {
169 OrdinaryTemplateInstanceTargetRecord {
170 target_id: target_id.clone(),
171 component_instance_id: instance.id.clone(),
172 component_id: instance.component.clone(),
173 template_entity_id: target_entity.id.clone(),
174 target_kind: target_kind(target_entity.kind),
175 provenance: target_entity.provenance.clone(),
176 }
177 });
178 events.push(OrdinaryTemplateInstanceEventRecord {
179 component_instance_id: instance.id.clone(),
180 component_id: instance.component.clone(),
181 target_id,
182 declaration_event_id: event.id.clone(),
183 event_type: event.event.clone(),
184 handler_method_id: endpoint,
185 action_batch_id: Some(action_batch_id),
186 arguments: event.arguments.clone(),
187 existing_event_program_identity: event.id.clone(),
188 provenance: model
189 .provenance(&event.id)
190 .cloned()
191 .unwrap_or_else(|| event_entity.provenance.clone()),
192 });
193 }
194 }
195 }
196 }
197 for binding in model
201 .form_field_bindings
202 .values()
203 .filter(|binding| binding.component == instance.component)
204 {
205 if !model
206 .optimized_form_ir
207 .optimized
208 .instances
209 .values()
210 .any(|form| form.form == binding.form && form.component_instance == instance.id)
211 {
212 continue;
213 }
214 insert_form_target(
215 &mut targets,
216 instance,
217 binding.control_entity.clone(),
218 OrdinaryTemplateTargetKind::FormControlHost,
219 binding.provenance.clone(),
220 );
221 }
222 for host in model
223 .submission_hosts
224 .values()
225 .filter(|host| host.component == instance.component)
226 {
227 if !model
228 .optimized_form_ir
229 .optimized
230 .instances
231 .values()
232 .any(|form| form.form == host.form && form.component_instance == instance.id)
233 {
234 continue;
235 }
236 insert_form_target(
237 &mut targets,
238 instance,
239 host.owner_template_element.clone(),
240 OrdinaryTemplateTargetKind::FormSubmissionHost,
241 host.provenance.clone(),
242 );
243 }
244 }
245 let mut targets = targets.into_values().collect::<Vec<_>>();
246 targets.sort_by(|left, right| left.target_id.cmp(&right.target_id));
247 bindings.sort_by(|left, right| left.instance_binding_id.cmp(&right.instance_binding_id));
248 events.sort_by(|left, right| {
249 (&left.component_instance_id, &left.declaration_event_id)
250 .cmp(&(&right.component_instance_id, &right.declaration_event_id))
251 });
252 OrdinaryTemplateInstanceRegistry {
253 version: ORDINARY_TEMPLATE_INSTANCE_REGISTRY_VERSION,
254 targets,
255 bindings,
256 events,
257 }
258}
259
260fn binding_kind(kind: TemplateSemanticKind) -> Option<OrdinaryTemplateBindingKind> {
261 match kind {
262 TemplateSemanticKind::AttributeBinding => Some(OrdinaryTemplateBindingKind::Attribute),
263 TemplateSemanticKind::Binding => Some(OrdinaryTemplateBindingKind::Text),
264 TemplateSemanticKind::Conditional => Some(OrdinaryTemplateBindingKind::Conditional),
265 TemplateSemanticKind::List => Some(OrdinaryTemplateBindingKind::List),
266 TemplateSemanticKind::Fragment
267 | TemplateSemanticKind::Element
268 | TemplateSemanticKind::Text
269 | TemplateSemanticKind::Attribute
270 | TemplateSemanticKind::EventAttribute => None,
271 }
272}
273
274const fn target_kind(kind: TemplateSemanticKind) -> OrdinaryTemplateTargetKind {
275 match kind {
276 TemplateSemanticKind::Conditional => OrdinaryTemplateTargetKind::ConditionalBoundary,
277 TemplateSemanticKind::List => OrdinaryTemplateTargetKind::ListBoundary,
278 _ => OrdinaryTemplateTargetKind::Element,
279 }
280}
281
282fn target_entity_for_binding<'a>(
283 entity: &'a crate::TemplateSemanticEntity,
284 entities: &[&'a crate::TemplateSemanticEntity],
285) -> &'a crate::TemplateSemanticEntity {
286 match entity.kind {
287 TemplateSemanticKind::AttributeBinding => containing_element(entity, entities),
288 _ => entity,
289 }
290}
291
292fn target_entity_for_event<'a>(
293 entity: &'a crate::TemplateSemanticEntity,
294 entities: &[&'a crate::TemplateSemanticEntity],
295) -> &'a crate::TemplateSemanticEntity {
296 containing_element(entity, entities)
297}
298
299fn containing_element<'a>(
300 entity: &'a crate::TemplateSemanticEntity,
301 entities: &[&'a crate::TemplateSemanticEntity],
302) -> &'a crate::TemplateSemanticEntity {
303 entities
304 .iter()
305 .copied()
306 .filter(|candidate| {
307 candidate.kind == TemplateSemanticKind::Element
308 && provenance_contains(&candidate.provenance, &entity.provenance)
309 })
310 .min_by_key(|candidate| candidate.provenance.span.end - candidate.provenance.span.start)
311 .unwrap_or(entity)
312}
313
314fn provenance_contains(parent: &SourceProvenance, child: &SourceProvenance) -> bool {
315 parent.path == child.path
316 && parent.span.start <= child.span.start
317 && parent.span.end >= child.span.end
318}
319
320fn provenance_matches_span(
321 provenance: &SourceProvenance,
322 span: &presolve_parser::SourceSpan,
323) -> bool {
324 provenance.span.start == span.start && provenance.span.end == span.end
325}
326
327fn insert_form_target(
328 targets: &mut BTreeMap<TemplateInstanceTargetId, OrdinaryTemplateInstanceTargetRecord>,
329 instance: &crate::ComponentInstance,
330 template_entity_id: SemanticId,
331 target_kind: OrdinaryTemplateTargetKind,
332 provenance: SourceProvenance,
333) {
334 let target_id = TemplateInstanceTargetId::for_component_instance_template_entity(
335 instance.id.clone(),
336 template_entity_id.clone(),
337 );
338 targets
339 .entry(target_id.clone())
340 .or_insert(OrdinaryTemplateInstanceTargetRecord {
341 target_id,
342 component_instance_id: instance.id.clone(),
343 component_id: instance.component.clone(),
344 template_entity_id,
345 target_kind,
346 provenance,
347 });
348}
349
350fn binding_dependencies(
351 model: &ApplicationSemanticModel,
352 binding: &SemanticId,
353) -> (Vec<IrStorageId>, Vec<SemanticId>) {
354 let state_storage_ids = model
355 .references_from(binding)
356 .into_iter()
357 .filter(|reference| reference.kind == SemanticReferenceKind::TemplateState)
358 .map(|reference| IrStorageId::for_semantic_origin(&reference.target))
359 .collect::<BTreeSet<_>>()
360 .into_iter()
361 .collect();
362 let computed_ids = model
363 .references_from(binding)
364 .into_iter()
365 .filter(|reference| reference.kind == SemanticReferenceKind::TemplateComputed)
366 .map(|reference| reference.target.clone())
367 .collect();
368 (state_storage_ids, computed_ids)
369}
370
371pub fn validate_ordinary_template_instance_registry(
376 model: &ApplicationSemanticModel,
377 registry: &OrdinaryTemplateInstanceRegistry,
378) -> Result<(), String> {
379 if registry.version != ORDINARY_TEMPLATE_INSTANCE_REGISTRY_VERSION {
380 return Err("unsupported ordinary template instance registry version".to_string());
381 }
382 if registry != &build_ordinary_template_instance_registry(model) {
383 return Err(
384 "ordinary template instance registry drifted from canonical products".to_string(),
385 );
386 }
387 let mut targets = BTreeSet::new();
388 let mut bindings = BTreeSet::new();
389 for target in ®istry.targets {
390 if target.target_id
391 != TemplateInstanceTargetId::for_component_instance_template_entity(
392 target.component_instance_id.clone(),
393 target.template_entity_id.clone(),
394 )
395 || !targets.insert(target.target_id.clone())
396 {
397 return Err("ordinary template target is duplicate or malformed".to_string());
398 }
399 }
400 for binding in ®istry.bindings {
401 if binding.instance_binding_id
402 != TemplateInstanceBindingId::for_component_instance_binding(
403 binding.component_instance_id.clone(),
404 binding.declaration_binding_id.clone(),
405 )
406 || !targets.contains(&binding.target_id)
407 || !bindings.insert(binding.instance_binding_id.clone())
408 {
409 return Err("ordinary template binding is duplicate or malformed".to_string());
410 }
411 }
412 Ok(())
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418 use crate::{build_application_semantic_model_for_unit, CompilationUnit};
419
420 #[test]
421 fn projects_distinct_targets_and_bindings_for_repeated_instances() {
422 let source = presolve_parser::parse_file(
423 "src/Repeated.tsx",
424 r#"
425@component("x-child") class Child {
426 count = state(1);
427 @action()
428 increment() { this.count++; }
429 render() { return <button title={this.count} onClick={() => this.increment()}>{this.count}</button>; }
430}
431@component("x-parent") class Parent { render() { return <><Child /><Child /></>; } }
432"#,
433 );
434 let model =
435 build_application_semantic_model_for_unit(&CompilationUnit::from_parsed_files(vec![
436 source,
437 ]));
438 let registry = build_ordinary_template_instance_registry(&model);
439 let child_targets = registry
440 .targets
441 .iter()
442 .filter(|target| target.component_id.as_str().contains("x-child"))
443 .collect::<Vec<_>>();
444 assert!(child_targets.len() >= 2);
445 assert!(child_targets
446 .windows(2)
447 .any(|pair| pair[0].target_id != pair[1].target_id));
448 let child_events = registry
449 .events
450 .iter()
451 .filter(|event| event.component_id.as_str().contains("x-child"))
452 .collect::<Vec<_>>();
453 assert_eq!(child_events.len(), 2);
454 assert_ne!(child_events[0].target_id, child_events[1].target_id);
455 assert!(registry.bindings.iter().all(|binding| {
456 registry
457 .targets
458 .iter()
459 .any(|target| target.target_id == binding.target_id)
460 }));
461 assert!(validate_ordinary_template_instance_registry(&model, ®istry).is_ok());
462 }
463}