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 method = component.methods.iter().find(|method| {
155 method.name
156 == event
157 .handler
158 .strip_prefix("this.")
159 .unwrap_or(&event.handler)
160 });
161 if let Some(method) = method {
162 let Some(action_batch_id) = model
163 .effect_trigger_plan
164 .action_batches
165 .values()
166 .find(|batch| batch.authored_action_method == method.id)
167 .map(|batch| batch.id.clone())
168 else {
169 continue;
170 };
171 targets.entry(target_id.clone()).or_insert_with(|| {
172 OrdinaryTemplateInstanceTargetRecord {
173 target_id: target_id.clone(),
174 component_instance_id: instance.id.clone(),
175 component_id: instance.component.clone(),
176 template_entity_id: target_entity.id.clone(),
177 target_kind: target_kind(target_entity.kind),
178 provenance: target_entity.provenance.clone(),
179 }
180 });
181 events.push(OrdinaryTemplateInstanceEventRecord {
182 component_instance_id: instance.id.clone(),
183 component_id: instance.component.clone(),
184 target_id,
185 declaration_event_id: event.id.clone(),
186 event_type: event.event.clone(),
187 handler_method_id: method.id.clone(),
188 action_batch_id: Some(action_batch_id),
189 arguments: event.arguments.clone(),
190 existing_event_program_identity: event.id.clone(),
191 provenance: model
192 .provenance(&event.id)
193 .cloned()
194 .unwrap_or_else(|| event_entity.provenance.clone()),
195 });
196 }
197 }
198 }
199 }
200 for binding in model
204 .form_field_bindings
205 .values()
206 .filter(|binding| binding.component == instance.component)
207 {
208 if !model
209 .optimized_form_ir
210 .optimized
211 .instances
212 .values()
213 .any(|form| form.form == binding.form && form.component_instance == instance.id)
214 {
215 continue;
216 }
217 insert_form_target(
218 &mut targets,
219 instance,
220 binding.control_entity.clone(),
221 OrdinaryTemplateTargetKind::FormControlHost,
222 binding.provenance.clone(),
223 );
224 }
225 for host in model
226 .submission_hosts
227 .values()
228 .filter(|host| host.component == instance.component)
229 {
230 if !model
231 .optimized_form_ir
232 .optimized
233 .instances
234 .values()
235 .any(|form| form.form == host.form && form.component_instance == instance.id)
236 {
237 continue;
238 }
239 insert_form_target(
240 &mut targets,
241 instance,
242 host.owner_template_element.clone(),
243 OrdinaryTemplateTargetKind::FormSubmissionHost,
244 host.provenance.clone(),
245 );
246 }
247 }
248 let mut targets = targets.into_values().collect::<Vec<_>>();
249 targets.sort_by(|left, right| left.target_id.cmp(&right.target_id));
250 bindings.sort_by(|left, right| left.instance_binding_id.cmp(&right.instance_binding_id));
251 events.sort_by(|left, right| {
252 (&left.component_instance_id, &left.declaration_event_id)
253 .cmp(&(&right.component_instance_id, &right.declaration_event_id))
254 });
255 OrdinaryTemplateInstanceRegistry {
256 version: ORDINARY_TEMPLATE_INSTANCE_REGISTRY_VERSION,
257 targets,
258 bindings,
259 events,
260 }
261}
262
263fn binding_kind(kind: TemplateSemanticKind) -> Option<OrdinaryTemplateBindingKind> {
264 match kind {
265 TemplateSemanticKind::AttributeBinding => Some(OrdinaryTemplateBindingKind::Attribute),
266 TemplateSemanticKind::Binding => Some(OrdinaryTemplateBindingKind::Text),
267 TemplateSemanticKind::Conditional => Some(OrdinaryTemplateBindingKind::Conditional),
268 TemplateSemanticKind::List => Some(OrdinaryTemplateBindingKind::List),
269 TemplateSemanticKind::Fragment
270 | TemplateSemanticKind::Element
271 | TemplateSemanticKind::Text
272 | TemplateSemanticKind::Attribute
273 | TemplateSemanticKind::EventAttribute => None,
274 }
275}
276
277const fn target_kind(kind: TemplateSemanticKind) -> OrdinaryTemplateTargetKind {
278 match kind {
279 TemplateSemanticKind::Conditional => OrdinaryTemplateTargetKind::ConditionalBoundary,
280 TemplateSemanticKind::List => OrdinaryTemplateTargetKind::ListBoundary,
281 _ => OrdinaryTemplateTargetKind::Element,
282 }
283}
284
285fn target_entity_for_binding<'a>(
286 entity: &'a crate::TemplateSemanticEntity,
287 entities: &[&'a crate::TemplateSemanticEntity],
288) -> &'a crate::TemplateSemanticEntity {
289 match entity.kind {
290 TemplateSemanticKind::AttributeBinding => containing_element(entity, entities),
291 _ => entity,
292 }
293}
294
295fn target_entity_for_event<'a>(
296 entity: &'a crate::TemplateSemanticEntity,
297 entities: &[&'a crate::TemplateSemanticEntity],
298) -> &'a crate::TemplateSemanticEntity {
299 containing_element(entity, entities)
300}
301
302fn containing_element<'a>(
303 entity: &'a crate::TemplateSemanticEntity,
304 entities: &[&'a crate::TemplateSemanticEntity],
305) -> &'a crate::TemplateSemanticEntity {
306 entities
307 .iter()
308 .copied()
309 .filter(|candidate| {
310 candidate.kind == TemplateSemanticKind::Element
311 && provenance_contains(&candidate.provenance, &entity.provenance)
312 })
313 .min_by_key(|candidate| candidate.provenance.span.end - candidate.provenance.span.start)
314 .unwrap_or(entity)
315}
316
317fn provenance_contains(parent: &SourceProvenance, child: &SourceProvenance) -> bool {
318 parent.path == child.path
319 && parent.span.start <= child.span.start
320 && parent.span.end >= child.span.end
321}
322
323fn provenance_matches_span(
324 provenance: &SourceProvenance,
325 span: &presolve_parser::SourceSpan,
326) -> bool {
327 provenance.span.start == span.start && provenance.span.end == span.end
328}
329
330fn insert_form_target(
331 targets: &mut BTreeMap<TemplateInstanceTargetId, OrdinaryTemplateInstanceTargetRecord>,
332 instance: &crate::ComponentInstance,
333 template_entity_id: SemanticId,
334 target_kind: OrdinaryTemplateTargetKind,
335 provenance: SourceProvenance,
336) {
337 let target_id = TemplateInstanceTargetId::for_component_instance_template_entity(
338 instance.id.clone(),
339 template_entity_id.clone(),
340 );
341 targets
342 .entry(target_id.clone())
343 .or_insert(OrdinaryTemplateInstanceTargetRecord {
344 target_id,
345 component_instance_id: instance.id.clone(),
346 component_id: instance.component.clone(),
347 template_entity_id,
348 target_kind,
349 provenance,
350 });
351}
352
353fn binding_dependencies(
354 model: &ApplicationSemanticModel,
355 binding: &SemanticId,
356) -> (Vec<IrStorageId>, Vec<SemanticId>) {
357 let state_storage_ids = model
358 .references_from(binding)
359 .into_iter()
360 .filter(|reference| reference.kind == SemanticReferenceKind::TemplateState)
361 .map(|reference| IrStorageId::for_semantic_origin(&reference.target))
362 .collect::<BTreeSet<_>>()
363 .into_iter()
364 .collect();
365 let computed_ids = model
366 .references_from(binding)
367 .into_iter()
368 .filter(|reference| reference.kind == SemanticReferenceKind::TemplateComputed)
369 .map(|reference| reference.target.clone())
370 .collect();
371 (state_storage_ids, computed_ids)
372}
373
374pub fn validate_ordinary_template_instance_registry(
379 model: &ApplicationSemanticModel,
380 registry: &OrdinaryTemplateInstanceRegistry,
381) -> Result<(), String> {
382 if registry.version != ORDINARY_TEMPLATE_INSTANCE_REGISTRY_VERSION {
383 return Err("unsupported ordinary template instance registry version".to_string());
384 }
385 if registry != &build_ordinary_template_instance_registry(model) {
386 return Err(
387 "ordinary template instance registry drifted from canonical products".to_string(),
388 );
389 }
390 let mut targets = BTreeSet::new();
391 let mut bindings = BTreeSet::new();
392 for target in ®istry.targets {
393 if target.target_id
394 != TemplateInstanceTargetId::for_component_instance_template_entity(
395 target.component_instance_id.clone(),
396 target.template_entity_id.clone(),
397 )
398 || !targets.insert(target.target_id.clone())
399 {
400 return Err("ordinary template target is duplicate or malformed".to_string());
401 }
402 }
403 for binding in ®istry.bindings {
404 if binding.instance_binding_id
405 != TemplateInstanceBindingId::for_component_instance_binding(
406 binding.component_instance_id.clone(),
407 binding.declaration_binding_id.clone(),
408 )
409 || !targets.contains(&binding.target_id)
410 || !bindings.insert(binding.instance_binding_id.clone())
411 {
412 return Err("ordinary template binding is duplicate or malformed".to_string());
413 }
414 }
415 Ok(())
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421 use crate::{build_application_semantic_model_for_unit, CompilationUnit};
422
423 #[test]
424 fn projects_distinct_targets_and_bindings_for_repeated_instances() {
425 let source = presolve_parser::parse_file(
426 "src/Repeated.tsx",
427 r#"
428@component("x-child") class Child {
429 count = state(1);
430 @action()
431 increment() { this.count++; }
432 render() { return <button title={this.count} onClick={() => this.increment()}>{this.count}</button>; }
433}
434@component("x-parent") class Parent { render() { return <><Child /><Child /></>; } }
435"#,
436 );
437 let model =
438 build_application_semantic_model_for_unit(&CompilationUnit::from_parsed_files(vec![
439 source,
440 ]));
441 let registry = build_ordinary_template_instance_registry(&model);
442 let child_targets = registry
443 .targets
444 .iter()
445 .filter(|target| target.component_id.as_str().contains("x-child"))
446 .collect::<Vec<_>>();
447 assert!(child_targets.len() >= 2);
448 assert!(child_targets
449 .windows(2)
450 .any(|pair| pair[0].target_id != pair[1].target_id));
451 let child_events = registry
452 .events
453 .iter()
454 .filter(|event| event.component_id.as_str().contains("x-child"))
455 .collect::<Vec<_>>();
456 assert_eq!(child_events.len(), 2);
457 assert_ne!(child_events[0].target_id, child_events[1].target_id);
458 assert!(registry.bindings.iter().all(|binding| {
459 registry
460 .targets
461 .iter()
462 .any(|target| target.target_id == binding.target_id)
463 }));
464 assert!(validate_ordinary_template_instance_registry(&model, ®istry).is_ok());
465 }
466}