1use std::collections::{BTreeMap, BTreeSet};
2use std::path::Path;
3
4use presolve_parser::ParsedFile;
5
6use crate::compilation_unit::CompilationUnit;
7use crate::component_composition::{analyze_component_composition, ComponentCompositionAnalysis};
8use crate::component_graph::{
9 build_component_graph_for_module, render_event_handlers, ComponentAction, ComponentDiagnostic,
10 ComponentDiagnosticSeverity, ComponentMethod, ComponentNode, ComputedExpression,
11 ComputedExpressionKind, MethodLocalVariable, RenderEventHandler, StateField,
12};
13use crate::component_initialization::{plan_component_initialization, ComponentInitializationPlan};
14use crate::component_instance::{
15 plan_component_instances, plan_component_instances_with_virtual_invocations,
16 BlockedComponentInstancePlan, ComponentInstance, ComponentInstancePlan,
17};
18use crate::component_instance_scope::{
19 build_component_instance_scope_graph, ComponentInstanceScopeGraph,
20};
21use crate::component_invocation::{collect_component_invocations, ComponentInvocationEntity};
22use crate::component_ir::{lower_component_ir, ComponentIrReport};
23use crate::component_ir_optimization::{optimize_component_ir, OptimizedComponentIrReport};
24use crate::component_scope::ComponentScopeGraph;
25use crate::composition_typing::{collect_composition_type_products, CompositionTypeProducts};
26use crate::computed_value::{collect_computed_values, ComputedDiagnosticCode, ComputedValue};
27use crate::consumer::{collect_consumer_entities, ConsumerEntity, ContextResolutionState};
28use crate::context::{collect_context_entities, ContextEntity};
29use crate::context_declaration_candidate::{
30 collect_context_declaration_candidates, ContextDeclarationCandidateRegistry,
31};
32use crate::context_dependency::{collect_context_dependency_graph, ContextDependencyGraph};
33use crate::context_evaluation::{collect_context_evaluation_plan, ContextEvaluationPlan};
34use crate::context_lifetime::{collect_context_lifetime_analysis, ContextLifetimeAnalysis};
35use crate::context_ownership::{collect_context_ownership_graph, ContextOwnershipGraph};
36use crate::context_resolution::{
37 collect_context_resolutions, ContextResolution, ContextResolutionResult,
38};
39use crate::context_typing::{
40 collect_context_type_products, ConsumerTypeRecord, ContextBindingCompatibility,
41 ContextBindingTypeRecord, ContextTypeRecord, ProviderTypeRecord,
42};
43use crate::expression_graph::{ExpressionGraph, ExpressionNode, ExpressionNodeKind};
44use crate::form::{collect_form_entities, FormEntity};
45use crate::form_binding::{
46 collect_form_field_binding_products, FormFieldBinding, FormFieldBindingCandidate,
47};
48use crate::form_field::{collect_form_field_products, FormFieldEntity};
49use crate::form_ir::{lower_form_ir, FormIrReport};
50use crate::form_ir_optimization::{optimize_form_ir, OptimizedFormIrReport};
51use crate::form_ownership::{collect_form_ownership_graph, FormOwnershipGraph};
52use crate::form_reset::{collect_reset_products, ResetProducts};
53use crate::form_serialization::{collect_serialization_products, SerializationProducts};
54use crate::form_submission::{collect_submission_products, SubmissionProducts};
55use crate::form_submission_host::{
56 collect_submission_host_products, SubmissionHost, SubmissionHostCandidate,
57};
58use crate::form_tracking::{collect_form_tracking_products, FormTrackingProducts};
59use crate::form_validation::{
60 collect_validation_graph, collect_validation_products, ValidationGraph, ValidationRule,
61 ValidationRuleCandidate,
62};
63use crate::form_validation_plan::{collect_validation_dependency_plans, ValidationDependencyPlans};
64use crate::instance_context::{collect_instance_context_registry, InstanceContextRegistry};
65use crate::intermediate_representation::{
66 IrComputedEvaluationPlan, IrReactiveCycleAnalysis, IrReactiveGraph,
67 IrReactiveTransitiveAnalysis,
68};
69use crate::provider::{collect_provider_entities, DuplicateProviderDeclaration, ProviderEntity};
70use crate::runtime_form_registry::{build_runtime_form_registry, RuntimeFormRegistry};
71use crate::semantic_id::{
72 ComponentInvocationId, ConsumerId, ContextId, FieldBindingId, FieldId, FormId, ProviderId,
73 ResourceActivationId, ResourceId, SemanticId, SemanticOwner, SlotBindingId,
74 SlotContentFragmentId, SlotId, SlotOutletId, ValidationRuleId,
75};
76use crate::semantic_provenance::SourceProvenance;
77use crate::semantic_reference::{SemanticReference, SemanticReferenceKind};
78use crate::semantic_type::{EffectStatementTypeRecord, SemanticTypeModel};
79use crate::slot::{collect_slot_entities, SlotEntity};
80use crate::slot_binding::{
81 collect_slot_bindings, collect_slot_bindings_with_virtual_invocations, SlotBinding,
82 SlotBindingRegistry,
83};
84use crate::slot_content::{collect_slot_composition, SlotContentFragment, SlotOutlet};
85use crate::template_graph::{build_template_graph, TemplateNode};
86use crate::template_semantics::{
87 build_template_semantic_entities, TemplateSemanticEntity, TemplateSemanticKind,
88 TemplateSemanticScope,
89};
90use crate::{
91 analyze_effect_reactivity, collect_effects, derive_effect_trigger_plan, lower_effect_bodies,
92 plan_effect_execution, validate_effects, Effect, EffectBody, EffectExecutionPlan,
93 EffectReactiveAnalysis, EffectStatement, EffectTriggerPlan,
94};
95
96#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct FileRouteApplicationModelErrorV1 {
100 pub code: &'static str,
101 pub message: String,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct ApplicationSemanticModel {
107 pub expression_graph: ExpressionGraph,
108 pub semantic_types: SemanticTypeModel,
109 pub components: Vec<ComponentNode>,
110 pub contexts: BTreeMap<ContextId, ContextEntity>,
111 pub providers: BTreeMap<ProviderId, ProviderEntity>,
112 pub consumers: BTreeMap<ConsumerId, ConsumerEntity>,
113 pub forms: BTreeMap<FormId, FormEntity>,
114 pub resource_endpoint_resolutions: Vec<crate::ResourceEndpointResolution>,
117 pub opaque_action_resolutions: Vec<crate::OpaqueActionResolution>,
121 pub resource_declarations: BTreeMap<ResourceId, crate::ResourceDeclaration>,
124 pub resource_activations: BTreeMap<ResourceActivationId, crate::ResourceActivation>,
125 pub form_field_declaration_candidates: Vec<crate::FormFieldDeclarationCandidate>,
126 pub form_fields: BTreeMap<FieldId, FormFieldEntity>,
127 pub form_field_binding_candidates: Vec<FormFieldBindingCandidate>,
128 pub form_field_bindings: BTreeMap<FieldBindingId, FormFieldBinding>,
129 pub form_ownership: FormOwnershipGraph,
130 pub validation_rule_candidates: Vec<ValidationRuleCandidate>,
131 pub validation_rules: BTreeMap<ValidationRuleId, ValidationRule>,
132 pub validation_graph: ValidationGraph,
133 pub validation_dependency_plans: ValidationDependencyPlans,
134 pub form_tracking: FormTrackingProducts,
135 pub submissions: SubmissionProducts,
136 pub submission_host_candidates: Vec<SubmissionHostCandidate>,
137 pub submission_hosts: BTreeMap<crate::SubmissionHostId, SubmissionHost>,
138 pub serialization: SerializationProducts,
139 pub reset: ResetProducts,
140 pub form_ir: FormIrReport,
141 pub optimized_form_ir: OptimizedFormIrReport,
142 pub runtime_forms: RuntimeFormRegistry,
143 pub slots: BTreeMap<SlotId, SlotEntity>,
144 pub component_invocations: BTreeMap<ComponentInvocationId, ComponentInvocationEntity>,
145 pub component_instance_plan: ComponentInstancePlan,
146 pub component_instance_scope: ComponentInstanceScopeGraph,
147 pub component_composition: ComponentCompositionAnalysis,
148 pub component_initialization: ComponentInitializationPlan,
149 pub component_ir: ComponentIrReport,
150 pub component_ir_optimization: OptimizedComponentIrReport,
151 pub instance_context: InstanceContextRegistry,
152 pub slot_content_fragments: BTreeMap<SlotContentFragmentId, SlotContentFragment>,
153 pub slot_outlets: BTreeMap<SlotOutletId, SlotOutlet>,
154 pub slot_bindings: SlotBindingRegistry,
155 pub composition_types: CompositionTypeProducts,
156 pub context_declaration_candidates: ContextDeclarationCandidateRegistry,
157 pub context_ownership: ContextOwnershipGraph,
158 pub context_dependency: ContextDependencyGraph,
159 pub context_lifetime: ContextLifetimeAnalysis,
160 pub context_evaluation: ContextEvaluationPlan,
161 pub component_scope: ComponentScopeGraph,
162 pub context_resolutions: BTreeMap<ConsumerId, ContextResolution>,
163 pub context_types: BTreeMap<ContextId, ContextTypeRecord>,
164 pub provider_types: BTreeMap<ProviderId, ProviderTypeRecord>,
165 pub consumer_types: BTreeMap<ConsumerId, ConsumerTypeRecord>,
166 pub context_binding_types: BTreeMap<ConsumerId, ContextBindingTypeRecord>,
167 pub duplicate_provider_declarations: Vec<DuplicateProviderDeclaration>,
168 pub computed_values: BTreeMap<SemanticId, ComputedValue>,
169 pub effects: BTreeMap<SemanticId, Effect>,
170 pub effect_reactive_analysis: BTreeMap<SemanticId, EffectReactiveAnalysis>,
171 pub effect_trigger_plan: EffectTriggerPlan,
172 pub effect_execution_plan: EffectExecutionPlan,
173 pub effect_bodies: BTreeMap<SemanticId, EffectBody>,
174 pub effect_statements: BTreeMap<SemanticId, EffectStatement>,
175 pub reactive_graph: IrReactiveGraph,
176 pub reactive_transitive_analysis: IrReactiveTransitiveAnalysis,
177 pub reactive_cycle_analysis: IrReactiveCycleAnalysis,
178 pub computed_evaluation_plan: IrComputedEvaluationPlan,
179 pub templates: Vec<TemplateNode>,
180 pub template_entities: Vec<TemplateSemanticEntity>,
181 pub diagnostics: Vec<ComponentDiagnostic>,
182 pub ownership: BTreeMap<SemanticId, SemanticOwner>,
183 pub references: Vec<SemanticReference>,
184 pub provenance: BTreeMap<SemanticId, SourceProvenance>,
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum SemanticEntity<'a> {
189 Component(&'a ComponentNode),
190 StateField(&'a StateField),
191 Method(&'a ComponentMethod),
192 Context(&'a ContextEntity),
193 Provider(&'a ProviderEntity),
194 Consumer(&'a ConsumerEntity),
195 Form(&'a FormEntity),
196 FormField(&'a FormFieldEntity),
197 FormFieldBinding(&'a FormFieldBinding),
198 ValidationRule(&'a ValidationRule),
199 Slot(&'a SlotEntity),
200 ComponentInvocation(&'a ComponentInvocationEntity),
201 ComponentInstance(&'a ComponentInstance),
202 BlockedComponentInstance(&'a BlockedComponentInstancePlan),
203 SlotContentFragment(&'a SlotContentFragment),
204 SlotOutlet(&'a SlotOutlet),
205 Computed(&'a ComputedValue),
206 Effect(&'a Effect),
207 Parameter(&'a crate::MethodParameter),
208 LocalVariable(&'a MethodLocalVariable),
209 Action(&'a ComponentAction),
210 EventHandler(&'a RenderEventHandler),
211 Template(&'a TemplateNode),
212 TemplateEntity(&'a TemplateSemanticEntity),
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub enum SemanticEntityKind {
217 Component,
218 StateField,
219 Method,
220 Context,
221 Provider,
222 Consumer,
223 Form,
224 FormField,
225 FormFieldBinding,
226 ValidationRule,
227 Slot,
228 ComponentInvocation,
229 ComponentInstance,
230 BlockedComponentInstance,
231 SlotContentFragment,
232 SlotOutlet,
233 Computed,
234 Effect,
235 Parameter,
236 LocalVariable,
237 Action,
238 EventHandler,
239 Template,
240 TemplateEntity,
241}
242
243impl SemanticEntity<'_> {
244 #[must_use]
245 pub fn kind(self) -> SemanticEntityKind {
246 match self {
247 Self::Component(_) => SemanticEntityKind::Component,
248 Self::StateField(_) => SemanticEntityKind::StateField,
249 Self::Method(_) => SemanticEntityKind::Method,
250 Self::Context(_) => SemanticEntityKind::Context,
251 Self::Provider(_) => SemanticEntityKind::Provider,
252 Self::Consumer(_) => SemanticEntityKind::Consumer,
253 Self::Form(_) => SemanticEntityKind::Form,
254 Self::FormField(_) => SemanticEntityKind::FormField,
255 Self::FormFieldBinding(_) => SemanticEntityKind::FormFieldBinding,
256 Self::ValidationRule(_) => SemanticEntityKind::ValidationRule,
257 Self::Slot(_) => SemanticEntityKind::Slot,
258 Self::ComponentInvocation(_) => SemanticEntityKind::ComponentInvocation,
259 Self::ComponentInstance(_) => SemanticEntityKind::ComponentInstance,
260 Self::BlockedComponentInstance(_) => SemanticEntityKind::BlockedComponentInstance,
261 Self::SlotContentFragment(_) => SemanticEntityKind::SlotContentFragment,
262 Self::SlotOutlet(_) => SemanticEntityKind::SlotOutlet,
263 Self::Computed(_) => SemanticEntityKind::Computed,
264 Self::Effect(_) => SemanticEntityKind::Effect,
265 Self::Parameter(_) => SemanticEntityKind::Parameter,
266 Self::LocalVariable(_) => SemanticEntityKind::LocalVariable,
267 Self::Action(_) => SemanticEntityKind::Action,
268 Self::EventHandler(_) => SemanticEntityKind::EventHandler,
269 Self::Template(_) => SemanticEntityKind::Template,
270 Self::TemplateEntity(_) => SemanticEntityKind::TemplateEntity,
271 }
272 }
273}
274
275impl ApplicationSemanticModel {
276 #[allow(clippy::too_many_lines)]
277 #[must_use]
278 pub fn entity(&self, id: &SemanticId) -> Option<SemanticEntity<'_>> {
279 for component in &self.components {
280 if component.id == *id {
281 return Some(SemanticEntity::Component(component));
282 }
283 if let Some(field) = component.state_fields.iter().find(|field| field.id == *id) {
284 return Some(SemanticEntity::StateField(field));
285 }
286 if let Some(method) = component.methods.iter().find(|method| method.id == *id) {
287 return Some(SemanticEntity::Method(method));
288 }
289 if let Some(parameter) = component
290 .methods
291 .iter()
292 .flat_map(|method| method.parameters.iter())
293 .find(|parameter| parameter.id == *id)
294 {
295 return Some(SemanticEntity::Parameter(parameter));
296 }
297 if let Some(local) = component
298 .methods
299 .iter()
300 .flat_map(|method| method.local_variables.iter())
301 .find(|local| local.id == *id)
302 {
303 return Some(SemanticEntity::LocalVariable(local));
304 }
305 if let Some(action) = component.actions.iter().find(|action| action.id == *id) {
306 return Some(SemanticEntity::Action(action));
307 }
308 if let Some(render) = &component.render {
309 if let Some(handler) = render_event_handlers(render)
310 .into_iter()
311 .find(|handler| handler.id == *id)
312 {
313 return Some(SemanticEntity::EventHandler(handler));
314 }
315 }
316 }
317
318 if let Some(computed) = self.computed_values.get(id) {
319 return Some(SemanticEntity::Computed(computed));
320 }
321 if let Some(context) = self
322 .contexts
323 .values()
324 .find(|context| context.id.as_semantic_id() == id)
325 {
326 return Some(SemanticEntity::Context(context));
327 }
328 if let Some(provider) = self
329 .providers
330 .values()
331 .find(|provider| provider.id.as_semantic_id() == id)
332 {
333 return Some(SemanticEntity::Provider(provider));
334 }
335 if let Some(consumer) = self
336 .consumers
337 .values()
338 .find(|consumer| consumer.id.as_semantic_id() == id)
339 {
340 return Some(SemanticEntity::Consumer(consumer));
341 }
342 if let Some(form) = self
343 .forms
344 .values()
345 .find(|form| form.id.as_semantic_id() == id)
346 {
347 return Some(SemanticEntity::Form(form));
348 }
349 if let Some(field) = self
350 .form_fields
351 .values()
352 .find(|field| field.id.as_semantic_id() == id)
353 {
354 return Some(SemanticEntity::FormField(field));
355 }
356 if let Some(binding) = self
357 .form_field_bindings
358 .values()
359 .find(|binding| binding.id.as_semantic_id() == id)
360 {
361 return Some(SemanticEntity::FormFieldBinding(binding));
362 }
363 if let Some(rule) = self
364 .validation_rules
365 .values()
366 .find(|rule| rule.id.as_semantic_id() == id)
367 {
368 return Some(SemanticEntity::ValidationRule(rule));
369 }
370 if let Some(slot) = self
371 .slots
372 .values()
373 .find(|slot| slot.id.as_semantic_id() == id)
374 {
375 return Some(SemanticEntity::Slot(slot));
376 }
377 if let Some(invocation) = self
378 .component_invocations
379 .values()
380 .find(|invocation| invocation.id.as_semantic_id() == id)
381 {
382 return Some(SemanticEntity::ComponentInvocation(invocation));
383 }
384 if let Some(instance) = self
385 .component_instance_plan
386 .instances
387 .values()
388 .find(|instance| instance.id.as_semantic_id() == id)
389 {
390 return Some(SemanticEntity::ComponentInstance(instance));
391 }
392 if let Some(blocked) = self
393 .component_instance_plan
394 .blocked
395 .values()
396 .find(|blocked| blocked.id.as_semantic_id() == id)
397 {
398 return Some(SemanticEntity::BlockedComponentInstance(blocked));
399 }
400 if let Some(fragment) = self
401 .slot_content_fragments
402 .values()
403 .find(|fragment| fragment.id.as_semantic_id() == id)
404 {
405 return Some(SemanticEntity::SlotContentFragment(fragment));
406 }
407 if let Some(outlet) = self
408 .slot_outlets
409 .values()
410 .find(|outlet| outlet.id.as_semantic_id() == id)
411 {
412 return Some(SemanticEntity::SlotOutlet(outlet));
413 }
414 if let Some(effect) = self.effects.get(id) {
415 return Some(SemanticEntity::Effect(effect));
416 }
417
418 if let Some(template) = self.templates.iter().find(|template| template.id == *id) {
419 return Some(SemanticEntity::Template(template));
420 }
421
422 self.template_entities
423 .iter()
424 .find(|entity| entity.id == *id)
425 .map(SemanticEntity::TemplateEntity)
426 }
427
428 #[must_use]
429 pub fn component(&self, id: &SemanticId) -> Option<&ComponentNode> {
430 self.components.iter().find(|component| component.id == *id)
431 }
432
433 #[must_use]
434 pub fn template(&self, id: &SemanticId) -> Option<&TemplateNode> {
435 self.templates.iter().find(|template| template.id == *id)
436 }
437
438 #[must_use]
439 pub fn computed_value(&self, id: &SemanticId) -> Option<&ComputedValue> {
440 self.computed_values.get(id)
441 }
442
443 #[must_use]
444 pub fn contexts(&self) -> Vec<&ContextEntity> {
445 self.contexts.values().collect()
446 }
447
448 #[must_use]
449 pub fn context(&self, id: &ContextId) -> Option<&ContextEntity> {
450 self.contexts.get(id)
451 }
452
453 #[must_use]
454 pub fn forms(&self) -> Vec<&FormEntity> {
455 self.forms.values().collect()
456 }
457
458 #[must_use]
459 pub fn form(&self, id: &FormId) -> Option<&FormEntity> {
460 self.forms.get(id)
461 }
462
463 #[must_use]
464 pub fn form_fields(&self) -> Vec<&FormFieldEntity> {
465 let mut fields = self.form_fields.values().collect::<Vec<_>>();
466 fields.sort_by(|left, right| {
467 (&left.owner_form, left.declaration_order, &left.id).cmp(&(
468 &right.owner_form,
469 right.declaration_order,
470 &right.id,
471 ))
472 });
473 fields
474 }
475
476 #[must_use]
477 pub fn form_field(&self, id: &FieldId) -> Option<&FormFieldEntity> {
478 self.form_fields.get(id)
479 }
480
481 #[must_use]
482 pub fn form_field_declaration_candidates(&self) -> &[crate::FormFieldDeclarationCandidate] {
483 &self.form_field_declaration_candidates
484 }
485
486 #[must_use]
487 pub fn form_field_binding_candidates(&self) -> &[FormFieldBindingCandidate] {
488 &self.form_field_binding_candidates
489 }
490
491 #[must_use]
492 pub fn form_field_bindings(&self) -> Vec<&FormFieldBinding> {
493 let mut bindings = self.form_field_bindings.values().collect::<Vec<_>>();
494 bindings.sort_by(|left, right| {
495 (&left.owner_template, left.authored_order, &left.id).cmp(&(
496 &right.owner_template,
497 right.authored_order,
498 &right.id,
499 ))
500 });
501 bindings
502 }
503
504 #[must_use]
505 pub fn form_field_binding(&self, id: &FieldBindingId) -> Option<&FormFieldBinding> {
506 self.form_field_bindings.get(id)
507 }
508
509 #[must_use]
510 pub const fn form_ownership(&self) -> &FormOwnershipGraph {
511 &self.form_ownership
512 }
513
514 #[must_use]
515 pub fn validation_rule_candidates(&self) -> &[ValidationRuleCandidate] {
516 &self.validation_rule_candidates
517 }
518
519 #[must_use]
520 pub fn validation_candidates_of(&self, field: &FieldId) -> Vec<&ValidationRuleCandidate> {
521 self.validation_rule_candidates
522 .iter()
523 .filter(|candidate| candidate.target_field.as_ref() == Some(field))
524 .collect()
525 }
526
527 #[must_use]
528 pub fn validation_rule(&self, id: &ValidationRuleId) -> Option<&ValidationRule> {
529 self.validation_rules.get(id)
530 }
531
532 #[must_use]
533 pub fn validation_rules(&self) -> Vec<&ValidationRule> {
534 let mut rules = self.validation_rules.values().collect::<Vec<_>>();
535 rules.sort_by(|left, right| {
536 (
537 &left.owner_form,
538 left.field_authored_order,
539 left.rule_authored_order,
540 &left.id,
541 )
542 .cmp(&(
543 &right.owner_form,
544 right.field_authored_order,
545 right.rule_authored_order,
546 &right.id,
547 ))
548 });
549 rules
550 }
551
552 #[must_use]
553 pub const fn validation_graph(&self) -> &ValidationGraph {
554 &self.validation_graph
555 }
556
557 #[must_use]
558 pub const fn validation_dependency_plans(&self) -> &ValidationDependencyPlans {
559 &self.validation_dependency_plans
560 }
561
562 #[must_use]
563 pub const fn form_tracking(&self) -> &FormTrackingProducts {
564 &self.form_tracking
565 }
566
567 #[must_use]
568 pub const fn submissions(&self) -> &SubmissionProducts {
569 &self.submissions
570 }
571
572 #[must_use]
573 pub const fn serialization(&self) -> &SerializationProducts {
574 &self.serialization
575 }
576
577 #[must_use]
578 pub const fn reset(&self) -> &ResetProducts {
579 &self.reset
580 }
581
582 #[must_use]
583 pub fn form_declaration_candidates(&self) -> Vec<&crate::FormDeclarationCandidate> {
584 let mut candidates = self
585 .components
586 .iter()
587 .flat_map(|component| component.form_declaration_candidates.iter())
588 .collect::<Vec<_>>();
589 candidates.sort_by(|left, right| {
590 (
591 left.provenance.path.as_path(),
592 left.provenance.span.start,
593 left.provenance.span.end,
594 left.id.as_str(),
595 )
596 .cmp(&(
597 right.provenance.path.as_path(),
598 right.provenance.span.start,
599 right.provenance.span.end,
600 right.id.as_str(),
601 ))
602 });
603 candidates
604 }
605
606 #[must_use]
607 pub const fn context_declaration_candidates(&self) -> &ContextDeclarationCandidateRegistry {
608 &self.context_declaration_candidates
609 }
610
611 #[must_use]
612 pub fn contexts_owned_by(&self, component: &SemanticId) -> Vec<&ContextId> {
613 self.contexts
614 .iter()
615 .filter_map(|(id, context)| {
616 (context.owner.entity_id() == Some(component)).then_some(id)
617 })
618 .collect()
619 }
620
621 #[must_use]
622 pub fn context_for_authored_field(&self, field: &SemanticId) -> Option<&ContextId> {
623 self.contexts
624 .iter()
625 .find_map(|(id, context)| (&context.authored_field == field).then_some(id))
626 }
627
628 #[must_use]
629 pub fn providers(&self) -> Vec<&ProviderEntity> {
630 self.providers.values().collect()
631 }
632
633 #[must_use]
634 pub fn provider(&self, id: &ProviderId) -> Option<&ProviderEntity> {
635 self.providers.get(id)
636 }
637
638 #[must_use]
639 pub fn providers_owned_by(&self, component: &SemanticId) -> Vec<&ProviderId> {
640 self.providers
641 .iter()
642 .filter_map(|(id, provider)| {
643 (provider.owner.entity_id() == Some(component)).then_some(id)
644 })
645 .collect()
646 }
647
648 #[must_use]
649 pub fn providers_for_context(&self, context: &ContextId) -> Vec<&ProviderId> {
650 self.providers
651 .iter()
652 .filter_map(|(id, provider)| (&provider.context == context).then_some(id))
653 .collect()
654 }
655
656 #[must_use]
657 pub fn provider_for_authored_field(&self, field: &SemanticId) -> Option<&ProviderId> {
658 self.providers
659 .iter()
660 .find_map(|(id, provider)| (&provider.authored_field == field).then_some(id))
661 }
662
663 #[must_use]
664 pub fn consumers(&self) -> Vec<&ConsumerEntity> {
665 self.consumers.values().collect()
666 }
667
668 #[must_use]
669 pub fn consumer(&self, id: &ConsumerId) -> Option<&ConsumerEntity> {
670 self.consumers.get(id)
671 }
672
673 pub(crate) fn consumer_for_semantic_id(&self, id: &SemanticId) -> Option<&ConsumerEntity> {
674 self.consumers
675 .values()
676 .find(|consumer| consumer.id.as_semantic_id() == id)
677 }
678
679 #[must_use]
680 pub fn consumers_owned_by(&self, component: &SemanticId) -> Vec<&ConsumerId> {
681 self.consumers
682 .iter()
683 .filter_map(|(id, consumer)| {
684 (consumer.owner.entity_id() == Some(component)).then_some(id)
685 })
686 .collect()
687 }
688
689 #[must_use]
690 pub fn consumers_for_context(&self, context: &ContextId) -> Vec<&ConsumerId> {
691 self.consumers
692 .iter()
693 .filter_map(|(id, consumer)| (consumer.context() == Some(context)).then_some(id))
694 .collect()
695 }
696
697 #[must_use]
698 pub fn consumer_for_authored_field(&self, field: &SemanticId) -> Option<&ConsumerId> {
699 self.consumers
700 .iter()
701 .find_map(|(id, consumer)| (&consumer.authored_field == field).then_some(id))
702 }
703
704 #[must_use]
705 pub fn slots(&self) -> Vec<&SlotEntity> {
706 self.slots.values().collect()
707 }
708
709 #[must_use]
710 pub fn slot(&self, id: &SlotId) -> Option<&SlotEntity> {
711 self.slots.get(id)
712 }
713
714 #[must_use]
715 pub fn slots_owned_by(&self, component: &SemanticId) -> Vec<&SlotId> {
716 self.slots
717 .iter()
718 .filter_map(|(id, slot)| (&slot.owner == component).then_some(id))
719 .collect()
720 }
721
722 #[must_use]
723 pub fn slot_for_authored_field(&self, field: &SemanticId) -> Option<&SlotId> {
724 self.slots
725 .iter()
726 .find_map(|(id, slot)| (&slot.authored_field == field).then_some(id))
727 }
728
729 #[must_use]
730 pub fn component_invocations(&self) -> Vec<&ComponentInvocationEntity> {
731 self.component_invocations.values().collect()
732 }
733
734 #[must_use]
735 pub fn component_invocation(
736 &self,
737 id: &ComponentInvocationId,
738 ) -> Option<&ComponentInvocationEntity> {
739 self.component_invocations.get(id)
740 }
741
742 #[must_use]
743 pub fn component_invocations_owned_by(
744 &self,
745 component: &SemanticId,
746 ) -> Vec<&ComponentInvocationId> {
747 self.component_invocations
748 .iter()
749 .filter_map(|(id, invocation)| (&invocation.owner_component == component).then_some(id))
750 .collect()
751 }
752
753 #[must_use]
754 pub fn component_instance(
755 &self,
756 id: &crate::ComponentInstanceId,
757 ) -> Option<&ComponentInstance> {
758 self.component_instance_plan.instances.get(id)
759 }
760
761 #[must_use]
762 pub fn component_build_roots(&self) -> Vec<&crate::ComponentBuildRoot> {
763 self.component_instance_plan.roots.values().collect()
764 }
765
766 #[must_use]
767 pub fn component_instances(&self) -> Vec<&ComponentInstance> {
768 self.component_instance_plan.instances.values().collect()
769 }
770
771 #[must_use]
772 pub fn blocked_component_instances(&self) -> Vec<&BlockedComponentInstancePlan> {
773 self.component_instance_plan.blocked.values().collect()
774 }
775
776 #[must_use]
777 pub fn blocked_component_instance(
778 &self,
779 id: &crate::ComponentInstanceId,
780 ) -> Option<&BlockedComponentInstancePlan> {
781 self.component_instance_plan.blocked.get(id)
782 }
783
784 #[must_use]
785 pub fn component_instances_for_definition(
786 &self,
787 component: &SemanticId,
788 ) -> Vec<&ComponentInstance> {
789 self.component_instance_plan
790 .instances
791 .values()
792 .filter(|instance| &instance.component == component)
793 .collect()
794 }
795
796 #[must_use]
797 pub const fn component_instance_scope_graph(&self) -> &ComponentInstanceScopeGraph {
798 &self.component_instance_scope
799 }
800
801 #[must_use]
802 pub const fn component_composition_analysis(&self) -> &ComponentCompositionAnalysis {
803 &self.component_composition
804 }
805
806 #[must_use]
807 pub const fn component_initialization_plan(&self) -> &ComponentInitializationPlan {
808 &self.component_initialization
809 }
810
811 #[must_use]
812 pub const fn component_ir_report(&self) -> &ComponentIrReport {
813 &self.component_ir
814 }
815
816 #[must_use]
817 pub const fn optimized_component_ir_report(&self) -> &OptimizedComponentIrReport {
818 &self.component_ir_optimization
819 }
820
821 #[must_use]
822 pub const fn instance_context_registry(&self) -> &InstanceContextRegistry {
823 &self.instance_context
824 }
825
826 #[must_use]
827 pub fn slot_content_fragments(&self) -> Vec<&SlotContentFragment> {
828 self.slot_content_fragments.values().collect()
829 }
830
831 #[must_use]
832 pub const fn slot_binding_registry(&self) -> &SlotBindingRegistry {
833 &self.slot_bindings
834 }
835
836 #[must_use]
837 pub const fn composition_type_products(&self) -> &CompositionTypeProducts {
838 &self.composition_types
839 }
840
841 #[must_use]
842 pub fn slot_bindings(&self) -> Vec<&SlotBinding> {
843 self.slot_bindings.bindings.values().collect()
844 }
845
846 #[must_use]
847 pub fn slot_binding(&self, id: &SlotBindingId) -> Option<&SlotBinding> {
848 self.slot_bindings.binding(id)
849 }
850
851 #[must_use]
852 pub fn slot_bindings_for_callee(
853 &self,
854 callee: &crate::ComponentInstanceId,
855 ) -> Vec<&SlotBinding> {
856 self.slot_bindings.for_callee(callee)
857 }
858
859 #[must_use]
860 pub fn slot_content_fragment(
861 &self,
862 id: &SlotContentFragmentId,
863 ) -> Option<&SlotContentFragment> {
864 self.slot_content_fragments.get(id)
865 }
866
867 #[must_use]
868 pub fn slot_content_fragments_for(
869 &self,
870 invocation: &ComponentInvocationId,
871 ) -> Vec<&SlotContentFragment> {
872 self.slot_content_fragments
873 .values()
874 .filter(|fragment| &fragment.invocation == invocation)
875 .collect()
876 }
877
878 #[must_use]
879 pub fn slot_outlets(&self) -> Vec<&SlotOutlet> {
880 self.slot_outlets.values().collect()
881 }
882
883 #[must_use]
884 pub fn slot_outlet(&self, id: &SlotOutletId) -> Option<&SlotOutlet> {
885 self.slot_outlets.get(id)
886 }
887
888 #[must_use]
889 pub fn slot_outlets_owned_by(&self, component: &SemanticId) -> Vec<&SlotOutletId> {
890 self.slot_outlets
891 .iter()
892 .filter_map(|(id, outlet)| (&outlet.owner_component == component).then_some(id))
893 .collect()
894 }
895
896 #[must_use]
897 pub fn context_resolution(&self, consumer: &ConsumerId) -> Option<&ContextResolution> {
898 self.context_resolutions.get(consumer)
899 }
900
901 #[must_use]
902 pub const fn context_ownership_graph(&self) -> &ContextOwnershipGraph {
903 &self.context_ownership
904 }
905
906 #[must_use]
907 pub const fn context_dependency_graph(&self) -> &ContextDependencyGraph {
908 &self.context_dependency
909 }
910
911 #[must_use]
912 pub const fn context_lifetime_analysis(&self) -> &ContextLifetimeAnalysis {
913 &self.context_lifetime
914 }
915
916 #[must_use]
917 pub const fn context_evaluation_plan(&self) -> &ContextEvaluationPlan {
918 &self.context_evaluation
919 }
920
921 #[must_use]
922 pub fn resolved_provider(&self, consumer: &ConsumerId) -> Option<&ProviderId> {
923 let ContextResolutionResult::Provider { provider, .. } =
924 &self.context_resolution(consumer)?.result
925 else {
926 return None;
927 };
928 Some(provider)
929 }
930
931 #[must_use]
932 pub fn consumers_resolved_to(&self, provider: &ProviderId) -> Vec<&ConsumerId> {
933 self.context_resolutions
934 .iter()
935 .filter_map(|(consumer, resolution)| {
936 matches!(
937 &resolution.result,
938 ContextResolutionResult::Provider {
939 provider: resolved, ..
940 } if resolved == provider
941 )
942 .then_some(consumer)
943 })
944 .collect()
945 }
946
947 #[must_use]
948 pub fn consumers_using_default(&self, context: &ContextId) -> Vec<&ConsumerId> {
949 self.context_resolutions
950 .iter()
951 .filter_map(|(consumer, resolution)| {
952 matches!(
953 &resolution.result,
954 ContextResolutionResult::ContextDefault {
955 context: resolved, ..
956 } if resolved == context
957 )
958 .then_some(consumer)
959 })
960 .collect()
961 }
962
963 #[must_use]
964 pub fn unresolved_context_consumers(&self) -> Vec<&ConsumerId> {
965 self.context_resolutions
966 .iter()
967 .filter_map(|(consumer, resolution)| {
968 matches!(resolution.result, ContextResolutionResult::Unresolved).then_some(consumer)
969 })
970 .collect()
971 }
972
973 #[must_use]
974 pub fn context_type(&self, context: &ContextId) -> Option<&ContextTypeRecord> {
975 self.context_types.get(context)
976 }
977
978 #[must_use]
979 pub fn provider_type(&self, provider: &ProviderId) -> Option<&ProviderTypeRecord> {
980 self.provider_types.get(provider)
981 }
982
983 #[must_use]
984 pub fn consumer_type(&self, consumer: &ConsumerId) -> Option<&ConsumerTypeRecord> {
985 self.consumer_types.get(consumer)
986 }
987
988 #[must_use]
989 pub fn context_binding_type(&self, consumer: &ConsumerId) -> Option<&ContextBindingTypeRecord> {
990 self.context_binding_types.get(consumer)
991 }
992
993 #[must_use]
994 pub fn runtime_eligible_context_binding(&self, consumer: &ConsumerId) -> bool {
995 self.context_binding_type(consumer)
996 .is_some_and(|binding| binding.overall == ContextBindingCompatibility::Compatible)
997 }
998
999 #[must_use]
1000 pub fn effect(&self, id: &SemanticId) -> Option<&Effect> {
1001 self.effects.get(id)
1002 }
1003
1004 #[must_use]
1005 pub fn effect_body(&self, effect: &SemanticId) -> Option<&EffectBody> {
1006 self.effect_bodies.get(effect)
1007 }
1008
1009 #[must_use]
1010 pub fn effect_reactive_analysis(&self, effect: &SemanticId) -> Option<&EffectReactiveAnalysis> {
1011 self.effect_reactive_analysis.get(effect)
1012 }
1013
1014 #[must_use]
1015 pub const fn effect_trigger_plan(&self) -> &EffectTriggerPlan {
1016 &self.effect_trigger_plan
1017 }
1018
1019 #[must_use]
1020 pub const fn effect_execution_plan(&self) -> &EffectExecutionPlan {
1021 &self.effect_execution_plan
1022 }
1023
1024 #[must_use]
1026 pub fn effect_statement_type(
1027 &self,
1028 statement: &SemanticId,
1029 ) -> Option<&EffectStatementTypeRecord> {
1030 self.semantic_types.effect_statements.get(statement)
1031 }
1032
1033 #[must_use]
1034 pub fn effect_statement(&self, id: &SemanticId) -> Option<&EffectStatement> {
1035 self.effect_statements.get(id)
1036 }
1037
1038 #[must_use]
1039 pub fn template_entity(&self, id: &SemanticId) -> Option<&TemplateSemanticEntity> {
1040 self.template_entities
1041 .iter()
1042 .find(|entity| entity.id == *id)
1043 }
1044
1045 #[must_use]
1046 pub fn template_entities_for(&self, template: &SemanticId) -> Vec<&TemplateSemanticEntity> {
1047 self.children_of(template)
1048 .into_iter()
1049 .filter_map(|id| self.template_entity(id))
1050 .collect()
1051 }
1052
1053 #[must_use]
1054 pub fn owner(&self, id: &SemanticId) -> Option<&SemanticOwner> {
1055 self.ownership.get(id)
1056 }
1057
1058 #[must_use]
1059 pub fn parent_of(&self, id: &SemanticId) -> Option<&SemanticId> {
1060 self.owner(id).and_then(SemanticOwner::entity_id)
1061 }
1062
1063 #[must_use]
1064 pub fn ancestors_of(&self, id: &SemanticId) -> Vec<&SemanticId> {
1065 let mut ancestors = Vec::new();
1066 let mut seen = BTreeSet::from([id.clone()]);
1067 let mut current = id;
1068
1069 while let Some(parent) = self.parent_of(current) {
1070 if !seen.insert(parent.clone()) {
1071 break;
1072 }
1073 ancestors.push(parent);
1074 current = parent;
1075 }
1076
1077 ancestors
1078 }
1079
1080 #[must_use]
1081 pub fn provenance(&self, id: &SemanticId) -> Option<&SourceProvenance> {
1082 self.provenance.get(id)
1083 }
1084
1085 #[must_use]
1086 pub fn expression(&self, id: &SemanticId) -> Option<&ExpressionNode> {
1087 self.expression_graph.node(id)
1088 }
1089
1090 #[must_use]
1091 pub fn expression_root(&self, owner: &SemanticId) -> Option<&SemanticId> {
1092 self.expression_graph.root_for(owner)
1093 }
1094
1095 #[must_use]
1096 pub fn expressions_for(&self, owner: &SemanticId) -> Vec<&ExpressionNode> {
1097 self.expression_graph.nodes_for(owner)
1098 }
1099
1100 #[must_use]
1101 pub fn expression_dependencies(&self, id: &SemanticId) -> Vec<&SemanticId> {
1102 self.expression_graph.dependencies_of(id)
1103 }
1104
1105 #[must_use]
1106 pub fn expression_dependents(&self, id: &SemanticId) -> Vec<&ExpressionNode> {
1107 self.expression_graph.dependents_of(id)
1108 }
1109
1110 #[must_use]
1111 pub fn expression_owner(&self, id: &SemanticId) -> Option<&SemanticId> {
1112 self.expression_graph.owner_of(id)
1113 }
1114
1115 #[must_use]
1116 pub fn expression_provenance(&self, id: &SemanticId) -> Option<&SourceProvenance> {
1117 self.expression_graph.provenance_of(id)
1118 }
1119
1120 #[must_use]
1121 pub fn semantic_type_of(&self, id: &SemanticId) -> Option<&crate::SemanticType> {
1122 self.semantic_types
1123 .assignments
1124 .get(id)
1125 .map(|assignment| &assignment.semantic_type)
1126 }
1127
1128 #[must_use]
1129 pub fn expression_type(&self, id: &SemanticId) -> Option<&crate::SemanticType> {
1130 self.expression_graph
1131 .node(id)
1132 .and_then(|_| self.semantic_type_of(id))
1133 }
1134
1135 #[must_use]
1136 pub fn type_declarations(
1137 &self,
1138 semantic_type: &crate::SemanticType,
1139 ) -> Vec<&crate::SemanticTypeAssignment> {
1140 self.semantic_types
1141 .assignments
1142 .values()
1143 .filter(|assignment| {
1144 assignment.status == crate::SemanticTypeStatus::Declared
1145 && assignment.semantic_type == *semantic_type
1146 })
1147 .collect()
1148 }
1149
1150 #[must_use]
1151 pub fn type_usages(
1152 &self,
1153 semantic_type: &crate::SemanticType,
1154 ) -> Vec<&crate::SemanticTypeAssignment> {
1155 self.semantic_types
1156 .assignments
1157 .values()
1158 .filter(|assignment| assignment.semantic_type == *semantic_type)
1159 .collect()
1160 }
1161
1162 #[must_use]
1163 pub fn serialization_compatibility_of(
1164 &self,
1165 id: &SemanticId,
1166 ) -> Option<crate::SerializationCompatibility> {
1167 self.semantic_type_of(id)
1168 .map(crate::serialization_compatibility)
1169 }
1170
1171 #[must_use]
1172 pub fn is_type_assignable(&self, source: &SemanticId, target: &SemanticId) -> Option<bool> {
1173 Some(crate::is_assignable(
1174 self.semantic_type_of(source)?,
1175 self.semantic_type_of(target)?,
1176 ))
1177 }
1178
1179 #[must_use]
1180 pub fn expressions_in_file(&self, path: &Path) -> Vec<&ExpressionNode> {
1181 self.expression_graph.nodes_in_file(path)
1182 }
1183
1184 #[must_use]
1185 pub fn expressions_at(&self, path: &Path, offset: usize) -> Vec<&ExpressionNode> {
1186 self.expression_graph.nodes_at(path, offset)
1187 }
1188
1189 #[must_use]
1190 pub fn application_roots(&self) -> Vec<&SemanticId> {
1191 self.ownership
1192 .iter()
1193 .filter_map(|(id, owner)| matches!(owner, SemanticOwner::Application).then_some(id))
1194 .collect()
1195 }
1196
1197 #[must_use]
1198 pub fn children_of(&self, owner: &SemanticId) -> Vec<&SemanticId> {
1199 self.ownership
1200 .iter()
1201 .filter_map(|(id, entity_owner)| {
1202 (entity_owner.entity_id() == Some(owner)).then_some(id)
1203 })
1204 .collect()
1205 }
1206
1207 #[must_use]
1208 pub fn descendants_of(&self, owner: &SemanticId) -> Vec<&SemanticId> {
1209 let mut descendants = Vec::new();
1210 self.collect_descendants(owner, &mut descendants);
1211 descendants
1212 }
1213
1214 #[must_use]
1215 pub fn entities_of_kind(&self, kind: SemanticEntityKind) -> Vec<&SemanticId> {
1216 self.ownership
1217 .keys()
1218 .filter(|id| self.entity(id).is_some_and(|entity| entity.kind() == kind))
1219 .collect()
1220 }
1221
1222 #[must_use]
1223 pub fn entities_in_file(&self, path: &Path) -> Vec<&SemanticId> {
1224 self.provenance
1225 .iter()
1226 .filter_map(|(id, provenance)| (provenance.path == path).then_some(id))
1227 .collect()
1228 }
1229
1230 #[must_use]
1231 pub fn entities_at(&self, path: &Path, offset: usize) -> Vec<&SemanticId> {
1232 self.provenance
1233 .iter()
1234 .filter_map(|(id, provenance)| {
1235 (provenance.path == path
1236 && provenance.span.start <= offset
1237 && offset < provenance.span.end)
1238 .then_some(id)
1239 })
1240 .collect()
1241 }
1242
1243 #[must_use]
1244 pub fn references_of_kind(&self, kind: SemanticReferenceKind) -> Vec<&SemanticReference> {
1245 let mut references = self
1246 .references
1247 .iter()
1248 .filter(|reference| reference.kind == kind)
1249 .collect::<Vec<_>>();
1250 references.sort_by(|left, right| {
1251 (left.source.as_str(), left.target.as_str())
1252 .cmp(&(right.source.as_str(), right.target.as_str()))
1253 });
1254 references
1255 }
1256
1257 #[must_use]
1258 pub fn references_in_file(&self, path: &Path) -> Vec<&SemanticReference> {
1259 let mut references = self
1260 .references
1261 .iter()
1262 .filter(|reference| reference.provenance.path == path)
1263 .collect::<Vec<_>>();
1264 references.sort_by(|left, right| {
1265 (left.source.as_str(), left.target.as_str())
1266 .cmp(&(right.source.as_str(), right.target.as_str()))
1267 });
1268 references
1269 }
1270
1271 #[must_use]
1272 pub fn references_at(&self, path: &Path, offset: usize) -> Vec<&SemanticReference> {
1273 let mut references = self
1274 .references
1275 .iter()
1276 .filter(|reference| {
1277 reference.provenance.path == path
1278 && reference.provenance.span.start <= offset
1279 && offset < reference.provenance.span.end
1280 })
1281 .collect::<Vec<_>>();
1282 references.sort_by(|left, right| {
1283 (left.source.as_str(), left.target.as_str())
1284 .cmp(&(right.source.as_str(), right.target.as_str()))
1285 });
1286 references
1287 }
1288
1289 fn collect_descendants<'a>(
1290 &'a self,
1291 owner: &SemanticId,
1292 descendants: &mut Vec<&'a SemanticId>,
1293 ) {
1294 for child in self.children_of(owner) {
1295 descendants.push(child);
1296 self.collect_descendants(child, descendants);
1297 }
1298 }
1299
1300 #[must_use]
1301 pub fn references_from(&self, id: &SemanticId) -> Vec<&SemanticReference> {
1302 self.references
1303 .iter()
1304 .filter(move |reference| reference.source == *id)
1305 .collect()
1306 }
1307
1308 #[must_use]
1309 pub fn references_to(&self, id: &SemanticId) -> Vec<&SemanticReference> {
1310 self.references
1311 .iter()
1312 .filter(move |reference| reference.target == *id)
1313 .collect()
1314 }
1315}
1316
1317#[must_use]
1318pub fn build_application_semantic_model(parsed: &ParsedFile) -> ApplicationSemanticModel {
1319 build_application_semantic_model_from_files(std::slice::from_ref(parsed))
1320}
1321
1322#[allow(clippy::too_many_lines)]
1324#[must_use]
1325pub fn build_application_semantic_model_from_component_graph(
1326 component_graph: &crate::component_graph::ComponentGraph,
1327) -> ApplicationSemanticModel {
1328 let templates = build_template_graph(component_graph).templates;
1329 let template_entities = build_template_semantic_entities(&templates);
1330 let component_invocations = collect_component_invocations(
1331 &component_graph.components,
1332 &templates,
1333 &template_entities,
1334 &[],
1335 None,
1336 &component_graph.provenance,
1337 );
1338 let component_instance_plan = plan_component_instances(
1339 &component_graph.components,
1340 &component_invocations,
1341 &template_entities,
1342 &component_graph.provenance,
1343 );
1344 let component_instance_scope = build_component_instance_scope_graph(&component_instance_plan);
1345 let component_composition = analyze_component_composition(
1346 &component_graph
1347 .components
1348 .iter()
1349 .map(|component| component.id.clone())
1350 .collect(),
1351 &component_invocations,
1352 &component_instance_plan,
1353 );
1354 let (computed_values, computed_diagnostics) = classify_computed_values(
1355 &component_graph.components,
1356 collect_computed_values(&component_graph.components, &component_graph.provenance),
1357 &component_graph.provenance,
1358 );
1359 let effects = collect_effects(&component_graph.components, &component_graph.provenance);
1360 let expression_graph =
1361 ExpressionGraph::from_components(&component_graph.components, &component_graph.provenance);
1362 let contexts = collect_context_entities(&component_graph.components, &expression_graph);
1363 let forms = collect_form_entities(&component_graph.components);
1364 let resource_endpoint_resolutions =
1365 collect_resource_endpoint_resolutions(&component_graph.components, None);
1366 let opaque_action_resolutions =
1367 collect_opaque_action_resolutions(&component_graph.components, None);
1368 let base_semantic_types = SemanticTypeModel::from_components(
1369 &component_graph.components,
1370 &component_graph.provenance,
1371 );
1372 let resource_declarations = collect_resource_declarations(
1373 &component_graph.components,
1374 &resource_endpoint_resolutions,
1375 &base_semantic_types,
1376 None,
1377 );
1378 let resource_activations =
1379 collect_resource_activations(&resource_declarations, &component_instance_plan);
1380 let form_field_products = collect_form_field_products(
1381 &component_graph.components,
1382 &forms,
1383 &base_semantic_types,
1384 None,
1385 );
1386 let form_field_declaration_candidates = form_field_products.candidates;
1387 let form_fields = form_field_products.fields;
1388 let form_field_binding_products = collect_form_field_binding_products(
1389 &component_graph.components,
1390 &templates,
1391 &forms,
1392 &form_fields,
1393 &form_field_declaration_candidates,
1394 );
1395 let form_field_binding_candidates = form_field_binding_products.candidates;
1396 let form_field_bindings = form_field_binding_products.bindings;
1397 let slots = collect_slot_entities(&component_graph.components);
1398 let slot_composition = collect_slot_composition(&templates, &component_invocations, &slots);
1399 let slot_bindings = collect_slot_bindings(
1400 &component_instance_plan,
1401 &component_invocations,
1402 &slots,
1403 &slot_composition.fragments,
1404 &slot_composition.outlets,
1405 );
1406 let (providers, duplicate_provider_declarations) = collect_provider_entities(
1407 &component_graph.components,
1408 &contexts,
1409 &expression_graph,
1410 None,
1411 );
1412 let consumers = collect_consumer_entities(&component_graph.components, &contexts, None);
1413 let instance_context = collect_instance_context_registry(
1414 &component_instance_scope,
1415 &contexts,
1416 &providers,
1417 &consumers,
1418 );
1419 let context_declaration_candidates = collect_context_declaration_candidates(
1420 &component_graph.components,
1421 &contexts,
1422 &providers,
1423 &consumers,
1424 );
1425 let component_scope = ComponentScopeGraph::reflexive(&component_graph.components);
1426 let context_resolutions = collect_context_resolutions(
1427 &consumers,
1428 &contexts,
1429 &providers,
1430 &expression_graph,
1431 &component_scope,
1432 );
1433 let mut provenance = component_graph.provenance.clone();
1434 extend_template_entity_provenance(&mut provenance, &template_entities);
1435 extend_derived_entity_provenance(
1436 &mut provenance,
1437 &contexts,
1438 &forms,
1439 &form_fields,
1440 &form_field_bindings,
1441 &providers,
1442 &consumers,
1443 &slots,
1444 &component_invocations,
1445 &slot_composition.fragments,
1446 &slot_composition.outlets,
1447 &component_instance_plan,
1448 &computed_values,
1449 &effects,
1450 );
1451 let mut ownership = collect_ownership(
1452 &component_graph.components,
1453 &contexts,
1454 &forms,
1455 &form_fields,
1456 &form_field_bindings,
1457 &providers,
1458 &consumers,
1459 &slots,
1460 &component_invocations,
1461 &slot_composition.fragments,
1462 &slot_composition.outlets,
1463 &component_instance_plan,
1464 &computed_values,
1465 &effects,
1466 &templates,
1467 &template_entities,
1468 );
1469 let context_ownership = collect_context_ownership_graph(
1470 &component_graph.components,
1471 &contexts,
1472 &providers,
1473 &consumers,
1474 &expression_graph,
1475 &provenance,
1476 );
1477 let (effect_bodies, effect_statements) =
1478 lower_effect_bodies(&component_graph.components, &effects, &expression_graph);
1479 let mut references = component_graph.references.clone();
1480 references.extend(build_computed_references(
1481 &component_graph.components,
1482 &computed_values,
1483 &resource_declarations,
1484 &expression_graph,
1485 ));
1486 references.extend(build_effect_references(
1487 &component_graph.components,
1488 &effects,
1489 &computed_values,
1490 &expression_graph,
1491 ));
1492 references.extend(build_provider_references(&providers));
1493 references.extend(build_consumer_references(&consumers));
1494 references.extend(build_context_resolution_references(&context_resolutions));
1495 extend_template_references(
1496 &mut references,
1497 &component_graph.components,
1498 &computed_values,
1499 &template_entities,
1500 &ownership,
1501 );
1502 references.extend(build_form_field_binding_references(&form_field_bindings));
1503 let form_ownership = collect_form_ownership_graph(
1504 &component_instance_plan.roots,
1505 &component_graph.components,
1506 &forms,
1507 &form_fields,
1508 &form_field_bindings,
1509 &template_entities,
1510 &ownership,
1511 &references,
1512 &provenance,
1513 );
1514 let validation_products =
1515 collect_validation_products(&component_graph.components, &forms, &form_fields);
1516 let validation_rule_candidates = validation_products.candidates;
1517 let validation_rules = validation_products.rules;
1518 extend_validation_products(
1519 &mut provenance,
1520 &mut ownership,
1521 &mut references,
1522 &validation_rules,
1523 );
1524 let validation_graph = collect_validation_graph(
1525 &component_instance_plan.roots,
1526 &form_ownership,
1527 &forms,
1528 &form_fields,
1529 &validation_rules,
1530 &validation_rule_candidates,
1531 &validation_products.cycles,
1532 &ownership,
1533 &references,
1534 );
1535 let validation_dependency_plans = collect_validation_dependency_plans(
1536 &forms,
1537 &form_fields,
1538 &validation_rules,
1539 &form_ownership,
1540 &validation_graph,
1541 );
1542 let form_tracking =
1543 collect_form_tracking_products(&forms, &form_fields, &form_field_bindings, &form_ownership);
1544 let semantic_types = finalize_semantic_types(
1545 base_semantic_types.with_resource_types(&resource_declarations),
1546 &component_graph.components,
1547 &contexts,
1548 &forms,
1549 &form_fields,
1550 &providers,
1551 &consumers,
1552 &slots,
1553 &computed_values,
1554 &effects,
1555 &effect_statements,
1556 &expression_graph,
1557 &references,
1558 &template_entities,
1559 );
1560 let context_type_products = collect_context_type_products(
1561 &contexts,
1562 &providers,
1563 &consumers,
1564 &context_resolutions,
1565 &expression_graph,
1566 &semantic_types,
1567 );
1568 let context_dependency = collect_context_dependency_graph(
1569 &component_graph.components,
1570 &contexts,
1571 &providers,
1572 &consumers,
1573 &context_resolutions,
1574 &context_type_products.contexts,
1575 &context_type_products.providers,
1576 &context_type_products.bindings,
1577 &computed_values,
1578 &expression_graph,
1579 );
1580 let context_lifetime = collect_context_lifetime_analysis(
1581 &component_graph.components,
1582 &contexts,
1583 &providers,
1584 &consumers,
1585 &computed_values,
1586 &context_ownership,
1587 &component_scope,
1588 &context_resolutions,
1589 &context_dependency,
1590 &provenance,
1591 );
1592 let effects = validate_effects(
1593 &component_graph.components,
1594 effects,
1595 &effect_statements,
1596 &semantic_types,
1597 );
1598 let (
1599 reactive_graph,
1600 reactive_transitive_analysis,
1601 reactive_cycle_analysis,
1602 computed_evaluation_plan,
1603 ) = build_computed_reactive_products(
1604 &component_graph.components,
1605 &computed_values,
1606 &effects,
1607 &resource_declarations,
1608 &references,
1609 &provenance,
1610 );
1611 let context_evaluation = collect_context_evaluation_plan(
1612 &contexts,
1613 &providers,
1614 &context_resolutions,
1615 &context_type_products.contexts,
1616 &context_type_products.providers,
1617 &context_type_products.bindings,
1618 &context_lifetime,
1619 &context_dependency,
1620 &computed_evaluation_plan,
1621 &component_scope,
1622 );
1623 let effect_reactive_analysis = analyze_effect_reactivity(
1624 &component_graph.components,
1625 &computed_values,
1626 &effects,
1627 &reactive_transitive_analysis,
1628 );
1629 let effect_trigger_plan = derive_effect_trigger_plan(
1630 &component_graph.components,
1631 &effects,
1632 &effect_reactive_analysis,
1633 &provenance,
1634 );
1635 let submissions = collect_submission_products(
1636 &component_graph.components,
1637 &forms,
1638 &form_fields,
1639 &validation_rules,
1640 &effect_trigger_plan,
1641 );
1642 let serialization = collect_serialization_products(
1643 &component_graph.components,
1644 &forms,
1645 &form_fields,
1646 &submissions.plans,
1647 );
1648 let reset = collect_reset_products(&forms, &form_fields, &form_field_bindings, &form_tracking);
1649 let form_ir = lower_form_ir(&component_instance_plan, &forms, &form_fields);
1650 let optimized_form_ir = optimize_form_ir(&form_ir);
1651 let submission_host_products = collect_submission_host_products(
1652 &templates,
1653 &component_graph.components,
1654 &forms,
1655 &form_field_bindings,
1656 &submissions.plans,
1657 &serialization.plans,
1658 &optimized_form_ir.optimized,
1659 );
1660 let runtime_forms = build_runtime_form_registry(
1661 &forms,
1662 &form_fields,
1663 &form_field_bindings,
1664 &validation_rules,
1665 &optimized_form_ir.optimized,
1666 &submissions,
1667 &serialization,
1668 &reset,
1669 &submission_host_products.hosts,
1670 );
1671 let effect_execution_plan = plan_effect_execution(
1672 &computed_values,
1673 &effects,
1674 &effect_reactive_analysis,
1675 &effect_trigger_plan,
1676 &reactive_transitive_analysis,
1677 &computed_evaluation_plan,
1678 );
1679 let mut diagnostics = component_graph.diagnostics.clone();
1680 diagnostics.extend(computed_diagnostics);
1681 extend_computed_diagnostics(
1682 &mut diagnostics,
1683 &component_graph.components,
1684 &computed_values,
1685 &resource_declarations,
1686 &expression_graph,
1687 &semantic_types,
1688 &reactive_cycle_analysis,
1689 &provenance,
1690 );
1691 let component_ids = component_graph
1692 .components
1693 .iter()
1694 .map(|component| component.id.clone())
1695 .collect::<BTreeSet<_>>();
1696 let composition_types = collect_composition_type_products(
1697 &component_ids,
1698 &component_invocations,
1699 &slot_bindings,
1700 &slots,
1701 &slot_composition.fragments,
1702 &slot_composition.outlets,
1703 &instance_context,
1704 &context_type_products.bindings,
1705 &context_type_products.contexts,
1706 &context_type_products.providers,
1707 &context_lifetime,
1708 &ownership,
1709 &references,
1710 );
1711 let component_initialization = plan_component_initialization(
1712 &component_instance_plan,
1713 &slot_bindings,
1714 &composition_types,
1715 &instance_context,
1716 );
1717 let mut model = ApplicationSemanticModel {
1718 expression_graph,
1719 semantic_types,
1720 components: components_with_resolved_form_field_candidates(
1721 &component_graph.components,
1722 &form_field_declaration_candidates,
1723 ),
1724 contexts,
1725 providers,
1726 consumers,
1727 forms,
1728 resource_endpoint_resolutions,
1729 opaque_action_resolutions,
1730 resource_declarations,
1731 resource_activations,
1732 form_field_declaration_candidates,
1733 form_fields,
1734 form_field_binding_candidates,
1735 form_field_bindings,
1736 form_ownership,
1737 validation_rule_candidates,
1738 validation_rules,
1739 validation_graph,
1740 validation_dependency_plans,
1741 form_tracking,
1742 submissions,
1743 submission_host_candidates: submission_host_products.candidates,
1744 submission_hosts: submission_host_products.hosts,
1745 serialization,
1746 reset,
1747 form_ir,
1748 optimized_form_ir,
1749 runtime_forms,
1750 slots,
1751 component_invocations,
1752 component_instance_plan,
1753 component_instance_scope,
1754 component_composition,
1755 component_initialization,
1756 component_ir: ComponentIrReport::default(),
1757 component_ir_optimization: OptimizedComponentIrReport::default(),
1758 instance_context,
1759 slot_content_fragments: slot_composition.fragments,
1760 slot_outlets: slot_composition.outlets,
1761 slot_bindings,
1762 composition_types,
1763 context_declaration_candidates,
1764 context_ownership,
1765 context_dependency,
1766 context_lifetime,
1767 context_evaluation,
1768 component_scope,
1769 context_resolutions,
1770 context_types: context_type_products.contexts,
1771 provider_types: context_type_products.providers,
1772 consumer_types: context_type_products.consumers,
1773 context_binding_types: context_type_products.bindings,
1774 duplicate_provider_declarations,
1775 computed_values,
1776 effects,
1777 effect_reactive_analysis,
1778 effect_trigger_plan,
1779 effect_execution_plan,
1780 effect_bodies,
1781 effect_statements,
1782 reactive_graph,
1783 reactive_transitive_analysis,
1784 reactive_cycle_analysis,
1785 computed_evaluation_plan,
1786 templates,
1787 template_entities,
1788 diagnostics,
1789 ownership,
1790 references,
1791 provenance,
1792 };
1793 model
1794 .diagnostics
1795 .extend(crate::collect_effect_diagnostics(&model));
1796 model
1797 .diagnostics
1798 .extend(crate::collect_context_diagnostics(&model));
1799 model
1800 .diagnostics
1801 .extend(crate::collect_form_diagnostics(&model));
1802 model.component_ir = lower_component_ir(&model);
1803 model.component_ir_optimization = optimize_component_ir(&model.component_ir);
1804 model
1805 .diagnostics
1806 .extend(crate::collect_component_diagnostics(&model));
1807 model
1808}
1809
1810#[must_use]
1811pub fn build_application_semantic_model_for_unit(
1812 unit: &CompilationUnit,
1813) -> ApplicationSemanticModel {
1814 build_application_semantic_model_for_unit_with_packages(
1815 unit,
1816 &crate::semantic_package::SemanticPackageResolutionTable::default(),
1817 )
1818}
1819
1820#[must_use]
1822pub fn build_application_semantic_model_for_unit_with_packages(
1823 unit: &CompilationUnit,
1824 packages: &crate::semantic_package::SemanticPackageResolutionTable,
1825) -> ApplicationSemanticModel {
1826 let symbols = crate::build_symbol_table(unit);
1827 let modules = crate::build_module_graph(unit);
1828 let bindings =
1829 crate::binding_table::build_binding_table_with_packages(unit, &symbols, &modules, packages);
1830 build_application_semantic_model_from_files_with_bindings(unit.files(), Some(&bindings))
1831}
1832
1833pub fn build_file_route_application_semantic_model_for_unit_with_packages(
1842 unit: &CompilationUnit,
1843 packages: &crate::semantic_package::SemanticPackageResolutionTable,
1844) -> Result<ApplicationSemanticModel, FileRouteApplicationModelErrorV1> {
1845 let symbols = crate::build_symbol_table(unit);
1846 let modules = crate::build_module_graph(unit);
1847 let bindings =
1848 crate::binding_table::build_binding_table_with_packages(unit, &symbols, &modules, packages);
1849 build_application_semantic_model_from_files_with_bindings_mode(
1850 unit.files(),
1851 Some(&bindings),
1852 ApplicationAssemblyMode::FileRoutes,
1853 )
1854}
1855
1856pub fn build_file_route_application_semantic_model_for_route_with_packages(
1865 unit: &CompilationUnit,
1866 packages: &crate::semantic_package::SemanticPackageResolutionTable,
1867 route_component: &SemanticId,
1868) -> Result<ApplicationSemanticModel, FileRouteApplicationModelErrorV1> {
1869 let symbols = crate::build_symbol_table(unit);
1870 let modules = crate::build_module_graph(unit);
1871 let bindings =
1872 crate::binding_table::build_binding_table_with_packages(unit, &symbols, &modules, packages);
1873 build_application_semantic_model_from_files_with_bindings_mode(
1874 unit.files(),
1875 Some(&bindings),
1876 ApplicationAssemblyMode::FileRoute(route_component.clone()),
1877 )
1878}
1879
1880fn build_application_semantic_model_from_files(files: &[ParsedFile]) -> ApplicationSemanticModel {
1881 build_application_semantic_model_from_files_with_bindings(files, None)
1882}
1883
1884#[allow(clippy::too_many_lines)]
1885fn build_application_semantic_model_from_files_with_bindings(
1886 files: &[ParsedFile],
1887 bindings: Option<&crate::BindingTable>,
1888) -> ApplicationSemanticModel {
1889 build_application_semantic_model_from_files_with_bindings_mode(
1890 files,
1891 bindings,
1892 ApplicationAssemblyMode::Authored,
1893 )
1894 .expect("authored application assembly cannot derive file-route diagnostics")
1895}
1896
1897#[derive(Debug, Clone, PartialEq, Eq)]
1898enum ApplicationAssemblyMode {
1899 Authored,
1900 FileRoutes,
1901 FileRoute(SemanticId),
1902}
1903
1904#[allow(clippy::too_many_lines)]
1905fn build_application_semantic_model_from_files_with_bindings_mode(
1906 files: &[ParsedFile],
1907 bindings: Option<&crate::BindingTable>,
1908 mode: ApplicationAssemblyMode,
1909) -> Result<ApplicationSemanticModel, FileRouteApplicationModelErrorV1> {
1910 let (mut components, mut templates, mut diagnostics, mut references) =
1911 (Vec::new(), Vec::new(), Vec::new(), Vec::new());
1912 let (mut provenance, mut template_entities, mut type_aliases) =
1913 (BTreeMap::new(), Vec::new(), Vec::new());
1914 for parsed in files {
1915 let component_graph = build_component_graph_for_module(parsed);
1916 let template_graph = build_template_graph(&component_graph);
1917 let file_template_entities = build_template_semantic_entities(&template_graph.templates);
1918
1919 components.extend(component_graph.components);
1920 templates.extend(template_graph.templates);
1921 diagnostics.extend(component_graph.diagnostics);
1922 references.extend(component_graph.references);
1923 provenance.extend(component_graph.provenance);
1924 extend_template_entity_provenance(&mut provenance, &file_template_entities);
1925 template_entities.extend(file_template_entities);
1926 type_aliases.extend(
1927 parsed
1928 .type_aliases
1929 .iter()
1930 .cloned()
1931 .map(|alias| (parsed.path.clone(), alias)),
1932 );
1933 }
1934
1935 resolve_builtin_pure_calls(&mut components);
1936
1937 if let Some(bindings) = bindings {
1938 diagnostics.extend(bindings.diagnostics.iter().map(|diagnostic| {
1939 ComponentDiagnostic::error(
1940 diagnostic.code.clone(),
1941 format!("{}: {}", diagnostic.module.display(), diagnostic.message),
1942 )
1943 }));
1944 resolve_semantic_package_pure_calls(&mut components, bindings);
1945 }
1946
1947 let resource_endpoint_resolutions =
1948 collect_resource_endpoint_resolutions(&components, bindings);
1949 diagnostics.extend(collect_resource_lowering_diagnostics(
1950 &resource_endpoint_resolutions,
1951 ));
1952 let opaque_action_resolutions = collect_opaque_action_resolutions(&components, bindings);
1953 diagnostics.extend(collect_opaque_action_lowering_diagnostics(
1954 &opaque_action_resolutions,
1955 ));
1956
1957 let mut component_invocations = collect_component_invocations(
1958 &components,
1959 &templates,
1960 &template_entities,
1961 files,
1962 bindings,
1963 &provenance,
1964 );
1965 let virtual_invocations = match mode {
1966 ApplicationAssemblyMode::Authored => BTreeMap::new(),
1967 ApplicationAssemblyMode::FileRoutes | ApplicationAssemblyMode::FileRoute(_) => {
1968 let mut graph = crate::build_validated_file_route_graph_from_components_v1(&components)
1969 .map_err(|error| FileRouteApplicationModelErrorV1 {
1970 code: error.code,
1971 message: error.message,
1972 })?;
1973 if let ApplicationAssemblyMode::FileRoute(selected) = &mode {
1974 graph.routes.retain(|route| route.component == *selected);
1975 if graph.routes.is_empty() {
1976 return Err(FileRouteApplicationModelErrorV1 {
1977 code: "PSROUTE1014_FILE_ROUTE_COMPONENT_UNRESOLVED",
1978 message: format!("component `{selected}` is not a conventional file route"),
1979 });
1980 }
1981 }
1982 let plan = crate::build_layout_composition_plan_from_components_v1(&components, &graph)
1983 .map_err(|error| FileRouteApplicationModelErrorV1 {
1984 code: error.code,
1985 message: error.message,
1986 })?;
1987 crate::layout_composition_virtual_invocations_from_provenance_v1(&plan, &provenance)
1988 }
1989 };
1990 let component_instance_plan = plan_component_instances_with_virtual_invocations(
1991 &components,
1992 &component_invocations,
1993 &virtual_invocations,
1994 &template_entities,
1995 &provenance,
1996 );
1997 component_invocations.extend(virtual_invocations.clone());
1998 let component_instance_scope = build_component_instance_scope_graph(&component_instance_plan);
1999 let component_composition = analyze_component_composition(
2000 &components
2001 .iter()
2002 .map(|component| component.id.clone())
2003 .collect(),
2004 &component_invocations,
2005 &component_instance_plan,
2006 );
2007
2008 let (computed_values, computed_diagnostics) = classify_computed_values(
2009 &components,
2010 collect_computed_values(&components, &provenance),
2011 &provenance,
2012 );
2013 let effects = collect_effects(&components, &provenance);
2014 let expression_graph = ExpressionGraph::from_components(&components, &provenance);
2015 let contexts = collect_context_entities(&components, &expression_graph);
2016 let forms = collect_form_entities(&components);
2017 let base_semantic_types = SemanticTypeModel::from_components_with_aliases_and_bindings(
2018 &components,
2019 &provenance,
2020 &type_aliases,
2021 bindings,
2022 );
2023 let resource_declarations = collect_resource_declarations(
2024 &components,
2025 &resource_endpoint_resolutions,
2026 &base_semantic_types,
2027 bindings,
2028 );
2029 let resource_activations =
2030 collect_resource_activations(&resource_declarations, &component_instance_plan);
2031 let form_field_products =
2032 collect_form_field_products(&components, &forms, &base_semantic_types, bindings);
2033 let form_field_declaration_candidates = form_field_products.candidates;
2034 let form_fields = form_field_products.fields;
2035 let form_field_binding_products = collect_form_field_binding_products(
2036 &components,
2037 &templates,
2038 &forms,
2039 &form_fields,
2040 &form_field_declaration_candidates,
2041 );
2042 let form_field_binding_candidates = form_field_binding_products.candidates;
2043 let form_field_bindings = form_field_binding_products.bindings;
2044 let slots = collect_slot_entities(&components);
2045 let slot_composition = collect_slot_composition(&templates, &component_invocations, &slots);
2046 let slot_bindings = collect_slot_bindings_with_virtual_invocations(
2047 &component_instance_plan,
2048 &component_invocations,
2049 &virtual_invocations,
2050 &slots,
2051 &slot_composition.fragments,
2052 &slot_composition.outlets,
2053 );
2054 let (providers, duplicate_provider_declarations) =
2055 collect_provider_entities(&components, &contexts, &expression_graph, bindings);
2056 let consumers = collect_consumer_entities(&components, &contexts, bindings);
2057 let instance_context = collect_instance_context_registry(
2058 &component_instance_scope,
2059 &contexts,
2060 &providers,
2061 &consumers,
2062 );
2063 let context_declaration_candidates =
2064 collect_context_declaration_candidates(&components, &contexts, &providers, &consumers);
2065 let component_scope = ComponentScopeGraph::reflexive(&components);
2066 let context_resolutions = collect_context_resolutions(
2067 &consumers,
2068 &contexts,
2069 &providers,
2070 &expression_graph,
2071 &component_scope,
2072 );
2073 diagnostics.extend(computed_diagnostics);
2074 extend_derived_entity_provenance(
2075 &mut provenance,
2076 &contexts,
2077 &forms,
2078 &form_fields,
2079 &form_field_bindings,
2080 &providers,
2081 &consumers,
2082 &slots,
2083 &component_invocations,
2084 &slot_composition.fragments,
2085 &slot_composition.outlets,
2086 &component_instance_plan,
2087 &computed_values,
2088 &effects,
2089 );
2090 let mut ownership = collect_ownership(
2091 &components,
2092 &contexts,
2093 &forms,
2094 &form_fields,
2095 &form_field_bindings,
2096 &providers,
2097 &consumers,
2098 &slots,
2099 &component_invocations,
2100 &slot_composition.fragments,
2101 &slot_composition.outlets,
2102 &component_instance_plan,
2103 &computed_values,
2104 &effects,
2105 &templates,
2106 &template_entities,
2107 );
2108 let context_ownership = collect_context_ownership_graph(
2109 &components,
2110 &contexts,
2111 &providers,
2112 &consumers,
2113 &expression_graph,
2114 &provenance,
2115 );
2116 let (effect_bodies, effect_statements) =
2117 lower_effect_bodies(&components, &effects, &expression_graph);
2118 references.extend(build_computed_references(
2119 &components,
2120 &computed_values,
2121 &resource_declarations,
2122 &expression_graph,
2123 ));
2124 references.extend(build_effect_references(
2125 &components,
2126 &effects,
2127 &computed_values,
2128 &expression_graph,
2129 ));
2130 references.extend(build_provider_references(&providers));
2131 references.extend(build_consumer_references(&consumers));
2132 references.extend(build_context_resolution_references(&context_resolutions));
2133 extend_template_references(
2134 &mut references,
2135 &components,
2136 &computed_values,
2137 &template_entities,
2138 &ownership,
2139 );
2140 references.extend(build_form_field_binding_references(&form_field_bindings));
2141 let form_ownership = collect_form_ownership_graph(
2142 &component_instance_plan.roots,
2143 &components,
2144 &forms,
2145 &form_fields,
2146 &form_field_bindings,
2147 &template_entities,
2148 &ownership,
2149 &references,
2150 &provenance,
2151 );
2152 let validation_products = collect_validation_products(&components, &forms, &form_fields);
2153 let validation_rule_candidates = validation_products.candidates;
2154 let validation_rules = validation_products.rules;
2155 extend_validation_products(
2156 &mut provenance,
2157 &mut ownership,
2158 &mut references,
2159 &validation_rules,
2160 );
2161 let validation_graph = collect_validation_graph(
2162 &component_instance_plan.roots,
2163 &form_ownership,
2164 &forms,
2165 &form_fields,
2166 &validation_rules,
2167 &validation_rule_candidates,
2168 &validation_products.cycles,
2169 &ownership,
2170 &references,
2171 );
2172 let validation_dependency_plans = collect_validation_dependency_plans(
2173 &forms,
2174 &form_fields,
2175 &validation_rules,
2176 &form_ownership,
2177 &validation_graph,
2178 );
2179 let form_tracking =
2180 collect_form_tracking_products(&forms, &form_fields, &form_field_bindings, &form_ownership);
2181 let semantic_types = finalize_semantic_types(
2182 base_semantic_types.with_resource_types(&resource_declarations),
2183 &components,
2184 &contexts,
2185 &forms,
2186 &form_fields,
2187 &providers,
2188 &consumers,
2189 &slots,
2190 &computed_values,
2191 &effects,
2192 &effect_statements,
2193 &expression_graph,
2194 &references,
2195 &template_entities,
2196 );
2197 let context_type_products = collect_context_type_products(
2198 &contexts,
2199 &providers,
2200 &consumers,
2201 &context_resolutions,
2202 &expression_graph,
2203 &semantic_types,
2204 );
2205 let context_dependency = collect_context_dependency_graph(
2206 &components,
2207 &contexts,
2208 &providers,
2209 &consumers,
2210 &context_resolutions,
2211 &context_type_products.contexts,
2212 &context_type_products.providers,
2213 &context_type_products.bindings,
2214 &computed_values,
2215 &expression_graph,
2216 );
2217 let context_lifetime = collect_context_lifetime_analysis(
2218 &components,
2219 &contexts,
2220 &providers,
2221 &consumers,
2222 &computed_values,
2223 &context_ownership,
2224 &component_scope,
2225 &context_resolutions,
2226 &context_dependency,
2227 &provenance,
2228 );
2229 let effects = validate_effects(&components, effects, &effect_statements, &semantic_types);
2230 let (
2231 reactive_graph,
2232 reactive_transitive_analysis,
2233 reactive_cycle_analysis,
2234 computed_evaluation_plan,
2235 ) = build_computed_reactive_products(
2236 &components,
2237 &computed_values,
2238 &effects,
2239 &resource_declarations,
2240 &references,
2241 &provenance,
2242 );
2243 let context_evaluation = collect_context_evaluation_plan(
2244 &contexts,
2245 &providers,
2246 &context_resolutions,
2247 &context_type_products.contexts,
2248 &context_type_products.providers,
2249 &context_type_products.bindings,
2250 &context_lifetime,
2251 &context_dependency,
2252 &computed_evaluation_plan,
2253 &component_scope,
2254 );
2255 let effect_reactive_analysis = analyze_effect_reactivity(
2256 &components,
2257 &computed_values,
2258 &effects,
2259 &reactive_transitive_analysis,
2260 );
2261 let effect_trigger_plan = derive_effect_trigger_plan(
2262 &components,
2263 &effects,
2264 &effect_reactive_analysis,
2265 &provenance,
2266 );
2267 let submissions = collect_submission_products(
2268 &components,
2269 &forms,
2270 &form_fields,
2271 &validation_rules,
2272 &effect_trigger_plan,
2273 );
2274 let serialization =
2275 collect_serialization_products(&components, &forms, &form_fields, &submissions.plans);
2276 let reset = collect_reset_products(&forms, &form_fields, &form_field_bindings, &form_tracking);
2277 let form_ir = lower_form_ir(&component_instance_plan, &forms, &form_fields);
2278 let optimized_form_ir = optimize_form_ir(&form_ir);
2279 let submission_host_products = collect_submission_host_products(
2280 &templates,
2281 &components,
2282 &forms,
2283 &form_field_bindings,
2284 &submissions.plans,
2285 &serialization.plans,
2286 &optimized_form_ir.optimized,
2287 );
2288 let runtime_forms = build_runtime_form_registry(
2289 &forms,
2290 &form_fields,
2291 &form_field_bindings,
2292 &validation_rules,
2293 &optimized_form_ir.optimized,
2294 &submissions,
2295 &serialization,
2296 &reset,
2297 &submission_host_products.hosts,
2298 );
2299 let effect_execution_plan = plan_effect_execution(
2300 &computed_values,
2301 &effects,
2302 &effect_reactive_analysis,
2303 &effect_trigger_plan,
2304 &reactive_transitive_analysis,
2305 &computed_evaluation_plan,
2306 );
2307 extend_computed_diagnostics(
2308 &mut diagnostics,
2309 &components,
2310 &computed_values,
2311 &resource_declarations,
2312 &expression_graph,
2313 &semantic_types,
2314 &reactive_cycle_analysis,
2315 &provenance,
2316 );
2317 let component_ids = components
2318 .iter()
2319 .map(|component| component.id.clone())
2320 .collect::<BTreeSet<_>>();
2321 let composition_types = collect_composition_type_products(
2322 &component_ids,
2323 &component_invocations,
2324 &slot_bindings,
2325 &slots,
2326 &slot_composition.fragments,
2327 &slot_composition.outlets,
2328 &instance_context,
2329 &context_type_products.bindings,
2330 &context_type_products.contexts,
2331 &context_type_products.providers,
2332 &context_lifetime,
2333 &ownership,
2334 &references,
2335 );
2336 let component_initialization = plan_component_initialization(
2337 &component_instance_plan,
2338 &slot_bindings,
2339 &composition_types,
2340 &instance_context,
2341 );
2342 let components = components_with_resolved_form_field_candidates(
2343 &components,
2344 &form_field_declaration_candidates,
2345 );
2346 let mut model = ApplicationSemanticModel {
2347 expression_graph,
2348 semantic_types,
2349 components,
2350 contexts,
2351 providers,
2352 consumers,
2353 forms,
2354 resource_endpoint_resolutions,
2355 opaque_action_resolutions,
2356 resource_declarations,
2357 resource_activations,
2358 form_field_declaration_candidates,
2359 form_fields,
2360 form_field_binding_candidates,
2361 form_field_bindings,
2362 form_ownership,
2363 validation_rule_candidates,
2364 validation_rules,
2365 validation_graph,
2366 validation_dependency_plans,
2367 form_tracking,
2368 submissions,
2369 submission_host_candidates: submission_host_products.candidates,
2370 submission_hosts: submission_host_products.hosts,
2371 serialization,
2372 reset,
2373 form_ir,
2374 optimized_form_ir,
2375 runtime_forms,
2376 slots,
2377 component_invocations,
2378 component_instance_plan,
2379 component_instance_scope,
2380 component_composition,
2381 component_initialization,
2382 component_ir: ComponentIrReport::default(),
2383 component_ir_optimization: OptimizedComponentIrReport::default(),
2384 instance_context,
2385 slot_content_fragments: slot_composition.fragments,
2386 slot_outlets: slot_composition.outlets,
2387 slot_bindings,
2388 composition_types,
2389 context_declaration_candidates,
2390 context_ownership,
2391 context_dependency,
2392 context_lifetime,
2393 context_evaluation,
2394 component_scope,
2395 context_resolutions,
2396 context_types: context_type_products.contexts,
2397 provider_types: context_type_products.providers,
2398 consumer_types: context_type_products.consumers,
2399 context_binding_types: context_type_products.bindings,
2400 duplicate_provider_declarations,
2401 computed_values,
2402 effects,
2403 effect_reactive_analysis,
2404 effect_trigger_plan,
2405 effect_execution_plan,
2406 effect_bodies,
2407 effect_statements,
2408 reactive_graph,
2409 reactive_transitive_analysis,
2410 reactive_cycle_analysis,
2411 computed_evaluation_plan,
2412 templates,
2413 template_entities,
2414 diagnostics,
2415 ownership,
2416 references,
2417 provenance,
2418 };
2419 model
2420 .diagnostics
2421 .extend(crate::collect_effect_diagnostics(&model));
2422 model
2423 .diagnostics
2424 .extend(crate::collect_context_diagnostics(&model));
2425 model
2426 .diagnostics
2427 .extend(crate::collect_form_diagnostics(&model));
2428 model.component_ir = lower_component_ir(&model);
2429 model.component_ir_optimization = optimize_component_ir(&model.component_ir);
2430 model
2431 .diagnostics
2432 .extend(crate::collect_component_diagnostics(&model));
2433 Ok(model)
2434}
2435
2436fn resolve_builtin_pure_calls(components: &mut [ComponentNode]) {
2437 for component in components {
2438 for method in component
2439 .methods
2440 .iter_mut()
2441 .filter(|method| method.is_computed())
2442 {
2443 if let Some(expression) = method.computed_expression.as_mut() {
2444 resolve_builtin_pure_call(expression);
2445 }
2446 }
2447 }
2448}
2449
2450fn resolve_builtin_pure_call(expression: &mut ComputedExpression) {
2451 match &mut expression.kind {
2452 ComputedExpressionKind::Call { callee, arguments } => {
2453 for argument in arguments.iter_mut() {
2454 resolve_builtin_pure_call(argument);
2455 }
2456 let operation = match (callee.as_str(), arguments.len()) {
2457 ("Math.abs", 1) => Some(crate::component_graph::BuiltinPureOperation::MathAbs),
2458 ("Math.floor", 1) => Some(crate::component_graph::BuiltinPureOperation::MathFloor),
2459 ("Math.ceil", 1) => Some(crate::component_graph::BuiltinPureOperation::MathCeil),
2460 ("Math.round", 1) => Some(crate::component_graph::BuiltinPureOperation::MathRound),
2461 ("Math.min", 2) => Some(crate::component_graph::BuiltinPureOperation::MathMin),
2462 ("Math.max", 2) => Some(crate::component_graph::BuiltinPureOperation::MathMax),
2463 _ => None,
2464 };
2465 if let Some(operation) = operation {
2466 expression.kind = ComputedExpressionKind::BuiltinPureCall {
2467 operation,
2468 arguments: std::mem::take(arguments),
2469 };
2470 }
2471 }
2472 ComputedExpressionKind::BuiltinPureCall { arguments, .. }
2473 | ComputedExpressionKind::SemanticPackagePureCall { arguments, .. } => {
2474 for argument in arguments {
2475 resolve_builtin_pure_call(argument);
2476 }
2477 }
2478 ComputedExpressionKind::Template { expressions, .. } => {
2479 for expression in expressions {
2480 resolve_builtin_pure_call(expression);
2481 }
2482 }
2483 ComputedExpressionKind::MemberAccess { object, .. }
2484 | ComputedExpressionKind::Unary {
2485 operand: object, ..
2486 } => {
2487 resolve_builtin_pure_call(object);
2488 }
2489 ComputedExpressionKind::IndexAccess { object, index } => {
2490 resolve_builtin_pure_call(object);
2491 resolve_builtin_pure_call(index);
2492 }
2493 ComputedExpressionKind::Conditional {
2494 condition,
2495 when_true,
2496 when_false,
2497 } => {
2498 resolve_builtin_pure_call(condition);
2499 resolve_builtin_pure_call(when_true);
2500 resolve_builtin_pure_call(when_false);
2501 }
2502 ComputedExpressionKind::Arithmetic { left, right, .. }
2503 | ComputedExpressionKind::Comparison { left, right, .. }
2504 | ComputedExpressionKind::Logical { left, right, .. }
2505 | ComputedExpressionKind::NullishCoalescing { left, right } => {
2506 resolve_builtin_pure_call(left);
2507 resolve_builtin_pure_call(right);
2508 }
2509 ComputedExpressionKind::Literal(_) | ComputedExpressionKind::ThisMember(_) => {}
2510 }
2511}
2512
2513fn components_with_resolved_form_field_candidates(
2514 components: &[ComponentNode],
2515 candidates: &[crate::FormFieldDeclarationCandidate],
2516) -> Vec<ComponentNode> {
2517 let candidates = candidates
2518 .iter()
2519 .map(|candidate| (candidate.id.clone(), candidate))
2520 .collect::<BTreeMap<_, _>>();
2521 let mut components = components.to_vec();
2522 for component in &mut components {
2523 for candidate in &mut component.form_field_declaration_candidates {
2524 if let Some(resolved) = candidates.get(&candidate.id) {
2525 *candidate = (*resolved).clone();
2526 }
2527 }
2528 }
2529 components
2530}
2531
2532fn extend_template_entity_provenance(
2533 provenance: &mut BTreeMap<SemanticId, SourceProvenance>,
2534 template_entities: &[TemplateSemanticEntity],
2535) {
2536 provenance.extend(
2537 template_entities
2538 .iter()
2539 .map(|entity| (entity.id.clone(), entity.provenance.clone())),
2540 );
2541}
2542
2543fn extend_validation_products(
2544 provenance: &mut BTreeMap<SemanticId, SourceProvenance>,
2545 ownership: &mut BTreeMap<SemanticId, SemanticOwner>,
2546 references: &mut Vec<SemanticReference>,
2547 rules: &BTreeMap<ValidationRuleId, ValidationRule>,
2548) {
2549 for rule in rules.values() {
2550 provenance.insert(rule.id.as_semantic_id().clone(), rule.provenance.clone());
2551 ownership.insert(
2552 rule.id.as_semantic_id().clone(),
2553 SemanticOwner::entity(rule.target_field.as_semantic_id().clone()),
2554 );
2555 if let Some(dependency) = &rule.dependency {
2556 references.push(SemanticReference {
2557 kind: SemanticReferenceKind::ValidationRuleField,
2558 source: rule.id.as_semantic_id().clone(),
2559 target: dependency.as_semantic_id().clone(),
2560 provenance: rule
2561 .argument_provenance
2562 .clone()
2563 .unwrap_or_else(|| rule.decorator_provenance.clone()),
2564 });
2565 }
2566 }
2567 references.sort_by(|left, right| {
2568 (left.source.as_str(), left.kind, left.target.as_str()).cmp(&(
2569 right.source.as_str(),
2570 right.kind,
2571 right.target.as_str(),
2572 ))
2573 });
2574 references.dedup();
2575}
2576
2577#[allow(clippy::too_many_arguments)]
2578fn extend_derived_entity_provenance(
2579 provenance: &mut BTreeMap<SemanticId, SourceProvenance>,
2580 contexts: &BTreeMap<ContextId, ContextEntity>,
2581 forms: &BTreeMap<FormId, FormEntity>,
2582 form_fields: &BTreeMap<FieldId, FormFieldEntity>,
2583 form_field_bindings: &BTreeMap<FieldBindingId, FormFieldBinding>,
2584 providers: &BTreeMap<ProviderId, ProviderEntity>,
2585 consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
2586 slots: &BTreeMap<SlotId, SlotEntity>,
2587 component_invocations: &BTreeMap<ComponentInvocationId, ComponentInvocationEntity>,
2588 slot_content_fragments: &BTreeMap<SlotContentFragmentId, SlotContentFragment>,
2589 slot_outlets: &BTreeMap<SlotOutletId, SlotOutlet>,
2590 component_instance_plan: &ComponentInstancePlan,
2591 computed_values: &BTreeMap<SemanticId, ComputedValue>,
2592 effects: &BTreeMap<SemanticId, Effect>,
2593) {
2594 provenance.extend(contexts.values().map(|context| {
2595 (
2596 context.id.as_semantic_id().clone(),
2597 context.provenance.clone(),
2598 )
2599 }));
2600 provenance.extend(
2601 forms
2602 .values()
2603 .map(|form| (form.id.as_semantic_id().clone(), form.provenance.clone())),
2604 );
2605 provenance.extend(
2606 form_fields
2607 .values()
2608 .map(|field| (field.id.as_semantic_id().clone(), field.provenance.clone())),
2609 );
2610 provenance.extend(form_field_bindings.values().map(|binding| {
2611 (
2612 binding.id.as_semantic_id().clone(),
2613 binding.provenance.clone(),
2614 )
2615 }));
2616 provenance.extend(providers.values().map(|provider| {
2617 (
2618 provider.id.as_semantic_id().clone(),
2619 provider.provenance.clone(),
2620 )
2621 }));
2622 provenance.extend(consumers.values().map(|consumer| {
2623 (
2624 consumer.id.as_semantic_id().clone(),
2625 consumer.provenance.clone(),
2626 )
2627 }));
2628 provenance.extend(
2629 slots
2630 .values()
2631 .map(|slot| (slot.id.as_semantic_id().clone(), slot.provenance.clone())),
2632 );
2633 provenance.extend(component_invocations.values().map(|invocation| {
2634 (
2635 invocation.id.as_semantic_id().clone(),
2636 invocation.provenance.clone(),
2637 )
2638 }));
2639 provenance.extend(slot_content_fragments.values().map(|fragment| {
2640 (
2641 fragment.id.as_semantic_id().clone(),
2642 fragment.provenance.clone(),
2643 )
2644 }));
2645 provenance.extend(slot_outlets.values().map(|outlet| {
2646 (
2647 outlet.id.as_semantic_id().clone(),
2648 outlet.provenance.clone(),
2649 )
2650 }));
2651 provenance.extend(component_instance_plan.instances.values().map(|instance| {
2652 (
2653 instance.id.as_semantic_id().clone(),
2654 instance.provenance.clone(),
2655 )
2656 }));
2657 provenance.extend(component_instance_plan.blocked.values().map(|blocked| {
2658 (
2659 blocked.id.as_semantic_id().clone(),
2660 blocked.provenance.clone(),
2661 )
2662 }));
2663 provenance.extend(
2664 computed_values
2665 .iter()
2666 .map(|(id, computed)| (id.clone(), computed.provenance.clone())),
2667 );
2668 provenance.extend(
2669 effects
2670 .iter()
2671 .map(|(id, effect)| (id.clone(), effect.provenance.clone())),
2672 );
2673}
2674
2675#[allow(clippy::too_many_arguments)]
2676fn finalize_semantic_types(
2677 semantic_types: SemanticTypeModel,
2678 components: &[ComponentNode],
2679 contexts: &BTreeMap<ContextId, ContextEntity>,
2680 forms: &BTreeMap<FormId, FormEntity>,
2681 form_fields: &BTreeMap<FieldId, FormFieldEntity>,
2682 providers: &BTreeMap<ProviderId, ProviderEntity>,
2683 consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
2684 slots: &BTreeMap<SlotId, SlotEntity>,
2685 computed_values: &BTreeMap<SemanticId, ComputedValue>,
2686 effects: &BTreeMap<SemanticId, Effect>,
2687 effect_statements: &BTreeMap<SemanticId, EffectStatement>,
2688 expression_graph: &ExpressionGraph,
2689 references: &[SemanticReference],
2690 template_entities: &[TemplateSemanticEntity],
2691) -> SemanticTypeModel {
2692 semantic_types
2693 .with_slot_types(slots)
2694 .with_form_types(forms)
2695 .with_form_field_types(form_fields)
2696 .with_context_types(contexts)
2697 .with_provider_types(providers)
2698 .with_consumer_types(consumers)
2699 .with_expression_types(expression_graph, components, contexts, providers)
2700 .with_computed_value_types(components, computed_values, expression_graph, references)
2701 .with_context_source_expression_types(components, contexts, providers, expression_graph)
2702 .with_effect_statement_types(components, effects, effect_statements, expression_graph)
2703 .with_template_binding_types(template_entities, references)
2704 .normalized()
2705}
2706
2707#[allow(clippy::too_many_arguments)]
2708fn extend_computed_diagnostics(
2709 diagnostics: &mut Vec<ComponentDiagnostic>,
2710 components: &[ComponentNode],
2711 computed_values: &BTreeMap<SemanticId, ComputedValue>,
2712 resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
2713 expression_graph: &ExpressionGraph,
2714 semantic_types: &SemanticTypeModel,
2715 reactive_cycle_analysis: &IrReactiveCycleAnalysis,
2716 provenance: &BTreeMap<SemanticId, SourceProvenance>,
2717) {
2718 diagnostics.extend(collect_computed_cycle_diagnostics(
2719 reactive_cycle_analysis,
2720 computed_values,
2721 ));
2722 diagnostics.extend(collect_computed_semantic_diagnostics(
2723 components,
2724 computed_values,
2725 resource_declarations,
2726 expression_graph,
2727 semantic_types,
2728 provenance,
2729 ));
2730}
2731
2732#[allow(clippy::too_many_lines)]
2733fn classify_computed_values(
2734 components: &[ComponentNode],
2735 mut computed_values: BTreeMap<SemanticId, ComputedValue>,
2736 provenance: &BTreeMap<SemanticId, SourceProvenance>,
2737) -> (
2738 BTreeMap<SemanticId, ComputedValue>,
2739 Vec<ComponentDiagnostic>,
2740) {
2741 let mut diagnostics = Vec::new();
2742
2743 for computed in computed_values.values_mut() {
2744 let Some(component_id) = computed.owner.entity_id() else {
2745 continue;
2746 };
2747 let Some(component) = components
2748 .iter()
2749 .find(|component| component.id == *component_id)
2750 else {
2751 continue;
2752 };
2753 let Some(method) = component
2754 .methods
2755 .iter()
2756 .find(|method| method.id == computed.method)
2757 else {
2758 continue;
2759 };
2760 let mut violations = Vec::new();
2761
2762 if method.is_async {
2763 violations.push(crate::ComputedPurityViolation {
2764 kind: crate::ComputedPurityViolationKind::Async,
2765 provenance: computed.provenance.clone(),
2766 });
2767 }
2768 if method
2769 .decorators
2770 .iter()
2771 .any(|decorator| decorator == "action")
2772 {
2773 violations.push(crate::ComputedPurityViolation {
2774 kind: crate::ComputedPurityViolationKind::Action,
2775 provenance: computed.provenance.clone(),
2776 });
2777 }
2778 if method
2779 .decorators
2780 .iter()
2781 .any(|decorator| decorator == "effect")
2782 {
2783 violations.push(crate::ComputedPurityViolation {
2784 kind: crate::ComputedPurityViolationKind::Effect,
2785 provenance: computed.provenance.clone(),
2786 });
2787 }
2788 for action in component
2789 .actions
2790 .iter()
2791 .filter(|action| action.method == method.name)
2792 {
2793 violations.push(crate::ComputedPurityViolation {
2794 kind: crate::ComputedPurityViolationKind::StateMutation,
2795 provenance: provenance
2796 .get(&action.id)
2797 .expect("computed state update should have canonical provenance")
2798 .clone(),
2799 });
2800 }
2801 for call in &method.calls {
2802 if method
2803 .computed_expression
2804 .as_ref()
2805 .is_some_and(|expression| {
2806 contains_semantic_package_pure_call(expression, &call.callee)
2807 || contains_builtin_pure_call(expression, &call.callee)
2808 })
2809 {
2810 continue;
2811 }
2812 let kind = computed_call_purity_kind(component, &call.callee);
2813 violations.push(crate::ComputedPurityViolation {
2814 kind,
2815 provenance: SourceProvenance::new(&computed.provenance.path, call.span),
2816 });
2817 }
2818
2819 violations.sort_by(|left, right| {
2820 (
2821 left.kind,
2822 left.provenance.path.as_path(),
2823 left.provenance.span.start,
2824 left.provenance.span.end,
2825 )
2826 .cmp(&(
2827 right.kind,
2828 right.provenance.path.as_path(),
2829 right.provenance.span.start,
2830 right.provenance.span.end,
2831 ))
2832 });
2833 violations
2834 .dedup_by(|left, right| left.kind == right.kind && left.provenance == right.provenance);
2835 computed.purity = if violations.is_empty() {
2836 crate::ComputedPurity::Pure
2837 } else {
2838 crate::ComputedPurity::Impure
2839 };
2840 computed.purity_violations = violations;
2841 diagnostics.extend(computed.purity_violations.iter().map(|violation| {
2842 ComponentDiagnostic {
2843 severity: ComponentDiagnosticSeverity::Error,
2844 effect_id: None,
2845 statement_id: None,
2846 context_declaration_candidate_id: None,
2847 context_id: None,
2848 provider_id: None,
2849 consumer_id: None,
2850 slot_id: None,
2851 invocation_id: None,
2852 component_instance_id: None,
2853 slot_binding_id: None,
2854 structural_region_id: None,
2855 component_id: None,
2856 provider_instance_id: None,
2857 consumer_instance_id: None,
2858 secondary_labels: Vec::new(),
2859 code: ComputedDiagnosticCode::PurityViolation.as_str().to_string(),
2860 message: format!(
2861 "computed getter `{}` is impure: {}",
2862 computed.name,
2863 violation.kind.description()
2864 ),
2865 provenance: Some(violation.provenance.clone()),
2866 }
2867 }));
2868 }
2869
2870 (computed_values, diagnostics)
2871}
2872
2873fn resolve_semantic_package_pure_calls(
2874 components: &mut [ComponentNode],
2875 bindings: &crate::BindingTable,
2876) {
2877 for component in components {
2878 for method in component
2879 .methods
2880 .iter_mut()
2881 .filter(|method| method.is_computed())
2882 {
2883 let Some(expression) = method.computed_expression.as_mut() else {
2884 continue;
2885 };
2886 resolve_semantic_package_pure_call(expression, &component.module_path, bindings);
2887 }
2888 }
2889}
2890
2891fn collect_resource_endpoint_resolutions(
2892 components: &[ComponentNode],
2893 bindings: Option<&crate::BindingTable>,
2894) -> Vec<crate::ResourceEndpointResolution> {
2895 let mut resolutions = Vec::new();
2896 for component in components {
2897 for resource in &component.resource_declaration_candidates {
2898 let outcome = match resource.endpoint_designator.as_deref() {
2899 None => crate::ResourceEndpointResolutionOutcome::MissingDesignator,
2900 Some(designator) => {
2901 let Some(bindings) = bindings else {
2902 resolutions.push(crate::ResourceEndpointResolution {
2903 owner_component: resource.owner_component.clone(),
2904 field: resource.field.clone(),
2905 endpoint_designator: resource.endpoint_designator.clone(),
2906 outcome: crate::ResourceEndpointResolutionOutcome::UnboundDesignator {
2907 designator: designator.to_string(),
2908 },
2909 provenance: resource.provenance.clone(),
2910 });
2911 continue;
2912 };
2913 let Some(binding) = bindings.resolve_import(&component.module_path, designator)
2914 else {
2915 resolutions.push(crate::ResourceEndpointResolution {
2916 owner_component: resource.owner_component.clone(),
2917 field: resource.field.clone(),
2918 endpoint_designator: resource.endpoint_designator.clone(),
2919 outcome: crate::ResourceEndpointResolutionOutcome::UnboundDesignator {
2920 designator: designator.to_string(),
2921 },
2922 provenance: resource.provenance.clone(),
2923 });
2924 continue;
2925 };
2926 match &binding.target {
2927 crate::ImportBindingTarget::SemanticPackage {
2928 package,
2929 version,
2930 integrity,
2931 export,
2932 kind: crate::SemanticPackageKind::Resource,
2933 type_signature,
2934 runtime_module,
2935 resume_policy,
2936 resource_endpoint: Some(endpoint),
2937 ..
2938 } => crate::ResourceEndpointResolutionOutcome::Resolved(
2939 crate::ResourceEndpointBinding {
2940 local_name: binding.local_name.clone(),
2941 package: package.clone(),
2942 version: version.clone(),
2943 integrity: integrity.clone(),
2944 export: export.clone(),
2945 type_signature: type_signature.clone(),
2946 runtime_module: runtime_module.clone(),
2947 resume_policy: resume_policy.clone(),
2948 endpoint: endpoint.clone(),
2949 },
2950 ),
2951 crate::ImportBindingTarget::SemanticPackage { kind, .. } => {
2952 crate::ResourceEndpointResolutionOutcome::NonResourceBinding {
2953 designator: designator.to_string(),
2954 kind: kind.clone(),
2955 }
2956 }
2957 _ => crate::ResourceEndpointResolutionOutcome::NonSemanticPackageBinding {
2958 designator: designator.to_string(),
2959 },
2960 }
2961 }
2962 };
2963 resolutions.push(crate::ResourceEndpointResolution {
2964 owner_component: resource.owner_component.clone(),
2965 field: resource.field.clone(),
2966 endpoint_designator: resource.endpoint_designator.clone(),
2967 outcome,
2968 provenance: resource.provenance.clone(),
2969 });
2970 }
2971 }
2972 resolutions
2973}
2974
2975fn collect_opaque_action_resolutions(
2976 components: &[ComponentNode],
2977 bindings: Option<&crate::BindingTable>,
2978) -> Vec<crate::OpaqueActionResolution> {
2979 let mut resolutions = Vec::new();
2980 for component in components {
2981 for fact in component
2982 .opaque_action_facts
2983 .iter()
2984 .filter(|fact| crate::component_graph::is_valid_opaque_action_fact(fact))
2985 {
2986 let (Some(owner_component), Some(method), Some(package_specifier), Some(export_name)) = (
2987 fact.owner_component.as_ref(),
2988 fact.method.as_ref(),
2989 fact.package.as_ref(),
2990 fact.export.as_ref(),
2991 ) else {
2992 continue;
2993 };
2994 let outcome = bindings
2995 .and_then(|bindings| bindings.module(&component.module_path))
2996 .and_then(|module| {
2997 module.imports.values().find(|binding| {
2998 binding.source_module == *package_specifier
2999 && binding.imported_name == *export_name
3000 })
3001 })
3002 .map_or_else(
3003 || crate::OpaqueActionResolutionOutcome::UnboundImport {
3004 package: package_specifier.clone(),
3005 export: export_name.clone(),
3006 },
3007 |binding| match &binding.target {
3008 crate::ImportBindingTarget::SemanticPackage {
3009 package,
3010 version,
3011 integrity,
3012 export,
3013 kind: crate::SemanticPackageKind::Opaque,
3014 type_signature,
3015 runtime_module,
3016 resume_policy,
3017 opaque_terminal: Some(terminal),
3018 ..
3019 } => crate::OpaqueActionResolutionOutcome::Resolved(
3020 crate::OpaqueTerminalBinding {
3021 local_name: binding.local_name.clone(),
3022 package: package.clone(),
3023 version: version.clone(),
3024 integrity: integrity.clone(),
3025 export: export.clone(),
3026 type_signature: type_signature.clone(),
3027 runtime_module: runtime_module.clone(),
3028 resume_policy: resume_policy.clone(),
3029 terminal: terminal.clone(),
3030 },
3031 ),
3032 crate::ImportBindingTarget::SemanticPackage { kind, .. } => {
3033 crate::OpaqueActionResolutionOutcome::NonOpaqueBinding {
3034 package: package_specifier.clone(),
3035 export: export_name.clone(),
3036 kind: kind.clone(),
3037 }
3038 }
3039 _ => crate::OpaqueActionResolutionOutcome::UnboundImport {
3040 package: package_specifier.clone(),
3041 export: export_name.clone(),
3042 },
3043 },
3044 );
3045 resolutions.push(crate::OpaqueActionResolution {
3046 activation: fact.id.clone(),
3047 owner_component: owner_component.clone(),
3048 method: method.clone(),
3049 method_name: fact.method_name.clone(),
3050 package_specifier: package_specifier.clone(),
3051 export_name: export_name.clone(),
3052 outcome,
3053 provenance: fact.provenance.clone(),
3054 });
3055 }
3056 }
3057 resolutions.sort_by(|left, right| left.activation.cmp(&right.activation));
3058 resolutions
3059}
3060
3061fn collect_opaque_action_lowering_diagnostics(
3062 resolutions: &[crate::OpaqueActionResolution],
3063) -> Vec<ComponentDiagnostic> {
3064 resolutions
3065 .iter()
3066 .filter_map(|resolution| {
3067 let message = match &resolution.outcome {
3068 crate::OpaqueActionResolutionOutcome::Resolved(_) => return None,
3069 crate::OpaqueActionResolutionOutcome::UnboundImport { package, export } => format!(
3070 "opaque Action `{}` requires an imported opaque semantic-package export `{package}` / `{export}`",
3071 resolution.method_name
3072 ),
3073 crate::OpaqueActionResolutionOutcome::NonOpaqueBinding {
3074 package,
3075 export,
3076 kind,
3077 } => format!(
3078 "opaque Action `{}` selects `{package}` / `{export}` with package kind {kind:?}, not opaque",
3079 resolution.method_name
3080 ),
3081 };
3082 let mut diagnostic = ComponentDiagnostic::error("PSC1131", message);
3083 diagnostic.provenance = Some(resolution.provenance.clone());
3084 Some(diagnostic)
3085 })
3086 .collect()
3087}
3088
3089fn collect_resource_declarations(
3090 components: &[ComponentNode],
3091 resolutions: &[crate::ResourceEndpointResolution],
3092 semantic_types: &SemanticTypeModel,
3093 bindings: Option<&crate::BindingTable>,
3094) -> BTreeMap<ResourceId, crate::ResourceDeclaration> {
3095 let mut declarations = BTreeMap::new();
3096 for component in components {
3097 for resource in &component.resource_declaration_candidates {
3098 let Some(resolution) = resolutions.iter().find(|resolution| {
3099 resolution.owner_component == resource.owner_component
3100 && resolution.field == resource.field
3101 }) else {
3102 continue;
3103 };
3104 let crate::ResourceEndpointResolutionOutcome::Resolved(endpoint) = &resolution.outcome
3105 else {
3106 continue;
3107 };
3108 let Some(declared_type) = resource.declared_type.as_ref() else {
3109 continue;
3110 };
3111 let Some(resolved_type) = semantic_types.resolve_declared_type(declared_type, bindings)
3112 else {
3113 continue;
3114 };
3115 let crate::SemanticType::Resource(resource_type) = resolved_type.semantic_type else {
3116 continue;
3117 };
3118 let execution_boundary = match endpoint.endpoint.execution_boundary {
3119 crate::SemanticPackageResourceExecutionBoundary::Client => {
3120 crate::ResourceExecutionBoundary::Client
3121 }
3122 crate::SemanticPackageResourceExecutionBoundary::Server => {
3123 crate::ResourceExecutionBoundary::Server
3124 }
3125 crate::SemanticPackageResourceExecutionBoundary::Shared => {
3126 crate::ResourceExecutionBoundary::Shared
3127 }
3128 };
3129 let key = format!("{}:{}", component.id.as_str(), resource.field);
3130 let declaration = crate::ResourceDeclaration::new(
3131 resource.owner_component.clone(),
3132 resource.field.clone(),
3133 key,
3134 *resource_type.data,
3135 *resource_type.error,
3136 execution_boundary,
3137 BTreeSet::new(),
3138 crate::ResourceRetryPolicy::ExplicitOnly,
3139 crate::ResourceInvalidationPolicy::ExplicitOnly,
3140 resource.provenance.clone(),
3141 );
3142 let Ok(declaration) = declaration else {
3143 continue;
3144 };
3145 declarations.insert(declaration.id.clone(), declaration);
3146 }
3147 }
3148 declarations
3149}
3150
3151fn collect_resource_lowering_diagnostics(
3152 resolutions: &[crate::ResourceEndpointResolution],
3153) -> Vec<ComponentDiagnostic> {
3154 resolutions
3155 .iter()
3156 .filter(|resolution| {
3157 !matches!(
3158 resolution.outcome,
3159 crate::ResourceEndpointResolutionOutcome::Resolved(_)
3160 )
3161 })
3162 .map(|resolution| {
3163 let message = match &resolution.outcome {
3164 crate::ResourceEndpointResolutionOutcome::MissingDesignator => format!(
3165 "resource declaration `{}` requires one imported semantic-package resource endpoint designator",
3166 resolution.field
3167 ),
3168 crate::ResourceEndpointResolutionOutcome::UnboundDesignator { designator } => format!(
3169 "resource declaration `{}` designator `{designator}` does not resolve to an imported semantic-package endpoint",
3170 resolution.field
3171 ),
3172 crate::ResourceEndpointResolutionOutcome::NonSemanticPackageBinding { designator } => format!(
3173 "resource declaration `{}` designator `{designator}` must bind a semantic-package resource export",
3174 resolution.field
3175 ),
3176 crate::ResourceEndpointResolutionOutcome::NonResourceBinding { designator, kind } => format!(
3177 "resource declaration `{}` designator `{designator}` resolves to package kind {kind:?}, not resource",
3178 resolution.field
3179 ),
3180 crate::ResourceEndpointResolutionOutcome::Resolved(_) => unreachable!(
3181 "resolved resource endpoints are valid N6-C13 lowering inputs"
3182 ),
3183 };
3184 let mut diagnostic = ComponentDiagnostic::error("PSC1128", message);
3185 diagnostic.provenance = Some(resolution.provenance.clone());
3186 diagnostic
3187 })
3188 .collect()
3189}
3190
3191fn collect_resource_activations(
3192 declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3193 instances: &ComponentInstancePlan,
3194) -> BTreeMap<ResourceActivationId, crate::ResourceActivation> {
3195 let mut activations = BTreeMap::new();
3196 for instance in instances.instances.values() {
3197 for declaration in declarations
3198 .values()
3199 .filter(|declaration| declaration.owner_component == instance.component)
3200 {
3201 let activation = declaration.activation_for(instance.id.clone());
3202 activations.insert(activation.id.clone(), activation);
3203 }
3204 }
3205 activations
3206}
3207
3208fn resolve_semantic_package_pure_call(
3209 expression: &mut ComputedExpression,
3210 module_path: &Path,
3211 bindings: &crate::BindingTable,
3212) {
3213 match &mut expression.kind {
3214 ComputedExpressionKind::Call { callee, arguments } => {
3215 for argument in arguments.iter_mut() {
3216 resolve_semantic_package_pure_call(argument, module_path, bindings);
3217 }
3218 let Some(binding) = bindings.resolve_import(module_path, callee) else {
3219 let operation = match (callee.as_str(), arguments.len()) {
3220 ("Math.abs", 1) => Some(crate::component_graph::BuiltinPureOperation::MathAbs),
3221 ("Math.floor", 1) => {
3222 Some(crate::component_graph::BuiltinPureOperation::MathFloor)
3223 }
3224 ("Math.ceil", 1) => {
3225 Some(crate::component_graph::BuiltinPureOperation::MathCeil)
3226 }
3227 ("Math.round", 1) => {
3228 Some(crate::component_graph::BuiltinPureOperation::MathRound)
3229 }
3230 ("Math.min", 2) => Some(crate::component_graph::BuiltinPureOperation::MathMin),
3231 ("Math.max", 2) => Some(crate::component_graph::BuiltinPureOperation::MathMax),
3232 _ => None,
3233 };
3234 if let Some(operation) = operation {
3235 expression.kind = ComputedExpressionKind::BuiltinPureCall {
3236 operation,
3237 arguments: std::mem::take(arguments),
3238 };
3239 }
3240 return;
3241 };
3242 let crate::ImportBindingTarget::SemanticPackage {
3243 package,
3244 version,
3245 integrity,
3246 export,
3247 kind: crate::semantic_package::SemanticPackageKind::Pure,
3248 runtime_module,
3249 resume_policy,
3250 pure_operation: Some(operation),
3251 ..
3252 } = &binding.target
3253 else {
3254 return;
3255 };
3256 expression.kind = ComputedExpressionKind::SemanticPackagePureCall {
3257 local_name: callee.clone(),
3258 package: package.clone(),
3259 version: version.clone(),
3260 integrity: integrity.clone(),
3261 export: export.clone(),
3262 runtime_module: runtime_module.clone(),
3263 resume_policy: resume_policy.clone(),
3264 operation: *operation,
3265 arguments: std::mem::take(arguments),
3266 };
3267 }
3268 ComputedExpressionKind::BuiltinPureCall { arguments, .. } => {
3269 for argument in arguments {
3270 resolve_semantic_package_pure_call(argument, module_path, bindings);
3271 }
3272 }
3273 ComputedExpressionKind::SemanticPackagePureCall { arguments, .. } => {
3274 for argument in arguments {
3275 resolve_semantic_package_pure_call(argument, module_path, bindings);
3276 }
3277 }
3278 ComputedExpressionKind::Template { expressions, .. } => {
3279 for expression in expressions {
3280 resolve_semantic_package_pure_call(expression, module_path, bindings);
3281 }
3282 }
3283 ComputedExpressionKind::MemberAccess { object, .. }
3284 | ComputedExpressionKind::Unary {
3285 operand: object, ..
3286 } => {
3287 resolve_semantic_package_pure_call(object, module_path, bindings);
3288 }
3289 ComputedExpressionKind::IndexAccess { object, index } => {
3290 resolve_semantic_package_pure_call(object, module_path, bindings);
3291 resolve_semantic_package_pure_call(index, module_path, bindings);
3292 }
3293 ComputedExpressionKind::Conditional {
3294 condition,
3295 when_true,
3296 when_false,
3297 } => {
3298 resolve_semantic_package_pure_call(condition, module_path, bindings);
3299 resolve_semantic_package_pure_call(when_true, module_path, bindings);
3300 resolve_semantic_package_pure_call(when_false, module_path, bindings);
3301 }
3302 ComputedExpressionKind::Arithmetic { left, right, .. }
3303 | ComputedExpressionKind::Comparison { left, right, .. }
3304 | ComputedExpressionKind::Logical { left, right, .. }
3305 | ComputedExpressionKind::NullishCoalescing { left, right } => {
3306 resolve_semantic_package_pure_call(left, module_path, bindings);
3307 resolve_semantic_package_pure_call(right, module_path, bindings);
3308 }
3309 ComputedExpressionKind::Literal(_) | ComputedExpressionKind::ThisMember(_) => {}
3310 }
3311}
3312
3313fn contains_semantic_package_pure_call(expression: &ComputedExpression, callee: &str) -> bool {
3314 match &expression.kind {
3315 ComputedExpressionKind::SemanticPackagePureCall {
3316 local_name,
3317 arguments,
3318 ..
3319 } => {
3320 local_name == callee
3321 || arguments
3322 .iter()
3323 .any(|argument| contains_semantic_package_pure_call(argument, callee))
3324 }
3325 ComputedExpressionKind::Call { arguments, .. } => arguments
3326 .iter()
3327 .any(|argument| contains_semantic_package_pure_call(argument, callee)),
3328 ComputedExpressionKind::BuiltinPureCall { arguments, .. } => arguments
3329 .iter()
3330 .any(|argument| contains_semantic_package_pure_call(argument, callee)),
3331 ComputedExpressionKind::Template { expressions, .. } => expressions
3332 .iter()
3333 .any(|expression| contains_semantic_package_pure_call(expression, callee)),
3334 ComputedExpressionKind::MemberAccess { object, .. }
3335 | ComputedExpressionKind::Unary {
3336 operand: object, ..
3337 } => contains_semantic_package_pure_call(object, callee),
3338 ComputedExpressionKind::IndexAccess { object, index } => {
3339 contains_semantic_package_pure_call(object, callee)
3340 || contains_semantic_package_pure_call(index, callee)
3341 }
3342 ComputedExpressionKind::Conditional {
3343 condition,
3344 when_true,
3345 when_false,
3346 } => {
3347 contains_semantic_package_pure_call(condition, callee)
3348 || contains_semantic_package_pure_call(when_true, callee)
3349 || contains_semantic_package_pure_call(when_false, callee)
3350 }
3351 ComputedExpressionKind::Arithmetic { left, right, .. }
3352 | ComputedExpressionKind::Comparison { left, right, .. }
3353 | ComputedExpressionKind::Logical { left, right, .. }
3354 | ComputedExpressionKind::NullishCoalescing { left, right } => {
3355 contains_semantic_package_pure_call(left, callee)
3356 || contains_semantic_package_pure_call(right, callee)
3357 }
3358 ComputedExpressionKind::Literal(_) | ComputedExpressionKind::ThisMember(_) => false,
3359 }
3360}
3361
3362fn contains_builtin_pure_call(expression: &ComputedExpression, callee: &str) -> bool {
3363 match &expression.kind {
3364 ComputedExpressionKind::BuiltinPureCall {
3365 operation,
3366 arguments,
3367 } => {
3368 let operation_callee = match operation {
3369 crate::component_graph::BuiltinPureOperation::MathAbs => "Math.abs",
3370 crate::component_graph::BuiltinPureOperation::MathFloor => "Math.floor",
3371 crate::component_graph::BuiltinPureOperation::MathCeil => "Math.ceil",
3372 crate::component_graph::BuiltinPureOperation::MathRound => "Math.round",
3373 crate::component_graph::BuiltinPureOperation::MathMin => "Math.min",
3374 crate::component_graph::BuiltinPureOperation::MathMax => "Math.max",
3375 };
3376 operation_callee == callee
3377 || arguments
3378 .iter()
3379 .any(|argument| contains_builtin_pure_call(argument, callee))
3380 }
3381 ComputedExpressionKind::Call { arguments, .. }
3382 | ComputedExpressionKind::SemanticPackagePureCall { arguments, .. } => arguments
3383 .iter()
3384 .any(|argument| contains_builtin_pure_call(argument, callee)),
3385 ComputedExpressionKind::Template { expressions, .. } => expressions
3386 .iter()
3387 .any(|expression| contains_builtin_pure_call(expression, callee)),
3388 ComputedExpressionKind::MemberAccess { object, .. }
3389 | ComputedExpressionKind::Unary {
3390 operand: object, ..
3391 } => contains_builtin_pure_call(object, callee),
3392 ComputedExpressionKind::IndexAccess { object, index } => {
3393 contains_builtin_pure_call(object, callee) || contains_builtin_pure_call(index, callee)
3394 }
3395 ComputedExpressionKind::Conditional {
3396 condition,
3397 when_true,
3398 when_false,
3399 } => {
3400 contains_builtin_pure_call(condition, callee)
3401 || contains_builtin_pure_call(when_true, callee)
3402 || contains_builtin_pure_call(when_false, callee)
3403 }
3404 ComputedExpressionKind::Arithmetic { left, right, .. }
3405 | ComputedExpressionKind::Comparison { left, right, .. }
3406 | ComputedExpressionKind::Logical { left, right, .. }
3407 | ComputedExpressionKind::NullishCoalescing { left, right } => {
3408 contains_builtin_pure_call(left, callee) || contains_builtin_pure_call(right, callee)
3409 }
3410 ComputedExpressionKind::Literal(_) | ComputedExpressionKind::ThisMember(_) => false,
3411 }
3412}
3413
3414fn collect_computed_cycle_diagnostics(
3415 analysis: &IrReactiveCycleAnalysis,
3416 computed_values: &BTreeMap<SemanticId, ComputedValue>,
3417) -> Vec<ComponentDiagnostic> {
3418 analysis
3419 .cycles
3420 .iter()
3421 .filter_map(|cycle| {
3422 let first = cycle.nodes.first()?;
3423 let computed = computed_values
3424 .values()
3425 .find(|computed| computed.id.as_str() == first)?;
3426 Some(ComponentDiagnostic {
3427 severity: ComponentDiagnosticSeverity::Error,
3428 effect_id: None,
3429 statement_id: None,
3430 context_declaration_candidate_id: None,
3431 context_id: None,
3432 provider_id: None,
3433 consumer_id: None,
3434 slot_id: None,
3435 invocation_id: None,
3436 component_instance_id: None,
3437 slot_binding_id: None,
3438 structural_region_id: None,
3439 component_id: None,
3440 provider_instance_id: None,
3441 consumer_instance_id: None,
3442 secondary_labels: Vec::new(),
3443 code: ComputedDiagnosticCode::DependencyCycle.as_str().to_string(),
3444 message: format!(
3445 "computed dependency cycle detected among: {}",
3446 cycle.nodes.join(", ")
3447 ),
3448 provenance: Some(computed.provenance.clone()),
3449 })
3450 })
3451 .collect()
3452}
3453
3454fn collect_computed_semantic_diagnostics(
3455 components: &[ComponentNode],
3456 computed_values: &BTreeMap<SemanticId, ComputedValue>,
3457 resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3458 expression_graph: &ExpressionGraph,
3459 semantic_types: &SemanticTypeModel,
3460 provenance: &BTreeMap<SemanticId, SourceProvenance>,
3461) -> Vec<ComponentDiagnostic> {
3462 let mut diagnostics = collect_invalid_computed_declaration_diagnostics(components, provenance);
3463 diagnostics.extend(collect_computed_body_and_read_diagnostics(
3464 components,
3465 computed_values,
3466 resource_declarations,
3467 expression_graph,
3468 ));
3469 diagnostics.extend(collect_computed_type_diagnostics(
3470 computed_values,
3471 semantic_types,
3472 ));
3473 diagnostics.extend(collect_computed_conditional_diagnostics(
3474 components,
3475 expression_graph,
3476 semantic_types,
3477 ));
3478 sort_computed_diagnostics(&mut diagnostics);
3479 diagnostics
3480}
3481
3482fn collect_computed_conditional_diagnostics(
3483 components: &[ComponentNode],
3484 expression_graph: &ExpressionGraph,
3485 semantic_types: &SemanticTypeModel,
3486) -> Vec<ComponentDiagnostic> {
3487 expression_graph
3488 .nodes
3489 .values()
3490 .filter_map(|node| {
3491 let crate::ExpressionNodeKind::Conditional { condition, .. } = &node.kind else {
3492 return None;
3493 };
3494 let condition_type = semantic_types
3495 .assignments
3496 .get(condition)
3497 .map(|assignment| assignment.semantic_type.clone())
3498 .or_else(|| match &expression_graph.node(condition)?.kind {
3499 crate::ExpressionNodeKind::ThisMember { name } => components
3500 .iter()
3501 .find(|component| {
3502 component
3503 .methods
3504 .iter()
3505 .any(|method| component.id.computed(&method.name) == node.owner)
3506 })
3507 .and_then(|component| {
3508 component
3509 .state_fields
3510 .iter()
3511 .find(|field| field.name == *name)
3512 })
3513 .and_then(|field| semantic_types.assignments.get(&field.id))
3514 .map(|assignment| assignment.semantic_type.clone()),
3515 _ => None,
3516 })
3517 .unwrap_or(crate::SemanticType::Unknown);
3518 if is_boolean_computed_condition(&condition_type) {
3519 None
3520 } else {
3521 Some(ComponentDiagnostic {
3522 severity: ComponentDiagnosticSeverity::Error,
3523 effect_id: None,
3524 statement_id: None,
3525 context_declaration_candidate_id: None,
3526 context_id: None,
3527 provider_id: None,
3528 consumer_id: None,
3529 slot_id: None,
3530 invocation_id: None,
3531 component_instance_id: None,
3532 slot_binding_id: None,
3533 structural_region_id: None,
3534 component_id: None,
3535 provider_instance_id: None,
3536 consumer_instance_id: None,
3537 secondary_labels: Vec::new(),
3538 code: crate::TypeDiagnosticCode::InvalidCondition
3539 .as_str()
3540 .to_string(),
3541 message: format!(
3542 "computed conditional requires boolean, but has {}",
3543 crate::semantic_type_text(&condition_type)
3544 ),
3545 provenance: Some(node.provenance.clone()),
3546 })
3547 }
3548 })
3549 .collect()
3550}
3551
3552fn is_boolean_computed_condition(semantic_type: &crate::SemanticType) -> bool {
3553 match semantic_type {
3554 crate::SemanticType::Boolean | crate::SemanticType::BooleanLiteral(_) => true,
3555 crate::SemanticType::Union(members) => members.iter().all(is_boolean_computed_condition),
3556 _ => false,
3557 }
3558}
3559
3560fn collect_invalid_computed_declaration_diagnostics(
3561 components: &[ComponentNode],
3562 provenance: &BTreeMap<SemanticId, SourceProvenance>,
3563) -> Vec<ComponentDiagnostic> {
3564 components
3565 .iter()
3566 .flat_map(|component| &component.methods)
3567 .filter(|method| {
3568 method
3569 .decorators
3570 .iter()
3571 .any(|decorator| decorator == "computed")
3572 && !method.is_getter
3573 })
3574 .map(|method| ComponentDiagnostic {
3575 severity: ComponentDiagnosticSeverity::Error,
3576 effect_id: None,
3577 statement_id: None,
3578 context_declaration_candidate_id: None,
3579 context_id: None,
3580 provider_id: None,
3581 consumer_id: None,
3582 slot_id: None,
3583 invocation_id: None,
3584 component_instance_id: None,
3585 slot_binding_id: None,
3586 structural_region_id: None,
3587 component_id: None,
3588 provider_instance_id: None,
3589 consumer_instance_id: None,
3590 secondary_labels: Vec::new(),
3591 code: ComputedDiagnosticCode::InvalidDeclaration
3592 .as_str()
3593 .to_string(),
3594 message: format!(
3595 "@computed() declaration `{}` must decorate a getter",
3596 method.name
3597 ),
3598 provenance: Some(
3599 provenance
3600 .get(&method.id)
3601 .expect("component methods should have canonical provenance")
3602 .clone(),
3603 ),
3604 })
3605 .collect()
3606}
3607
3608fn collect_computed_body_and_read_diagnostics(
3609 components: &[ComponentNode],
3610 computed_values: &BTreeMap<SemanticId, ComputedValue>,
3611 resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3612 expression_graph: &ExpressionGraph,
3613) -> Vec<ComponentDiagnostic> {
3614 let mut diagnostics = Vec::new();
3615
3616 for computed in computed_values.values() {
3617 let component_id = computed
3618 .owner
3619 .entity_id()
3620 .expect("computed values should have component owners");
3621 let component = components
3622 .iter()
3623 .find(|component| component.id == *component_id)
3624 .expect("computed values should have owning components");
3625 let method = component
3626 .methods
3627 .iter()
3628 .find(|method| method.id == computed.method)
3629 .expect("computed values should have authored methods");
3630
3631 if method.computed_expression.is_none() {
3632 diagnostics.push(ComponentDiagnostic {
3633 severity: ComponentDiagnosticSeverity::Error,
3634 effect_id: None,
3635 statement_id: None,
3636 context_declaration_candidate_id: None,
3637 context_id: None,
3638 provider_id: None,
3639 consumer_id: None,
3640 slot_id: None,
3641 invocation_id: None,
3642 component_instance_id: None,
3643 slot_binding_id: None,
3644 structural_region_id: None,
3645 component_id: None,
3646 provider_instance_id: None,
3647 consumer_instance_id: None,
3648 secondary_labels: Vec::new(),
3649 code: ComputedDiagnosticCode::UnsupportedBody.as_str().to_string(),
3650 message: format!(
3651 "computed getter `{}` has an unsupported body",
3652 computed.name
3653 ),
3654 provenance: Some(computed.provenance.clone()),
3655 });
3656 }
3657
3658 diagnostics.extend(
3659 expression_graph
3660 .nodes_for(&computed.id)
3661 .into_iter()
3662 .filter_map(|node| {
3663 unresolved_computed_read_diagnostic(
3664 component,
3665 computed_values,
3666 resource_declarations,
3667 computed,
3668 node,
3669 )
3670 }),
3671 );
3672 diagnostics.extend(computed_resource_projection_diagnostics(
3673 component,
3674 computed,
3675 resource_declarations,
3676 expression_graph,
3677 ));
3678 }
3679
3680 diagnostics
3681}
3682
3683fn unresolved_computed_read_diagnostic(
3684 component: &ComponentNode,
3685 computed_values: &BTreeMap<SemanticId, ComputedValue>,
3686 resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3687 computed: &ComputedValue,
3688 node: &ExpressionNode,
3689) -> Option<ComponentDiagnostic> {
3690 let ExpressionNodeKind::ThisMember { name } = &node.kind else {
3691 return None;
3692 };
3693 let resolved = component
3694 .state_fields
3695 .iter()
3696 .any(|field| field.name == *name)
3697 || computed_values.contains_key(&component.id.computed(name))
3698 || resource_declarations.contains_key(&ResourceId::for_owner(&component.id, name));
3699 (!resolved).then(|| ComponentDiagnostic {
3700 severity: ComponentDiagnosticSeverity::Error,
3701 effect_id: None,
3702 statement_id: None,
3703 context_declaration_candidate_id: None,
3704 context_id: None,
3705 provider_id: None,
3706 consumer_id: None,
3707 slot_id: None,
3708 invocation_id: None,
3709 component_instance_id: None,
3710 slot_binding_id: None,
3711 structural_region_id: None,
3712 component_id: None,
3713 provider_instance_id: None,
3714 consumer_instance_id: None,
3715 secondary_labels: Vec::new(),
3716 code: ComputedDiagnosticCode::UnresolvedRead.as_str().to_string(),
3717 message: format!(
3718 "computed getter `{}` reads unresolved member `this.{name}`",
3719 computed.name
3720 ),
3721 provenance: Some(node.provenance.clone()),
3722 })
3723}
3724
3725fn computed_resource_projection_diagnostics(
3726 component: &ComponentNode,
3727 computed: &ComputedValue,
3728 resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3729 expression_graph: &ExpressionGraph,
3730) -> Vec<ComponentDiagnostic> {
3731 let nodes = expression_graph.nodes_for(&computed.id);
3732 nodes
3733 .iter()
3734 .filter_map(|node| {
3735 let ExpressionNodeKind::ThisMember { name } = &node.kind else {
3736 return None;
3737 };
3738 resource_declarations
3739 .contains_key(&ResourceId::for_owner(&component.id, name))
3740 .then_some((node, name))
3741 })
3742 .filter(|(node, _)| {
3743 !nodes.iter().any(|candidate| {
3744 is_direct_resource_projection(candidate, &nodes)
3745 && matches!(
3746 &candidate.kind,
3747 ExpressionNodeKind::MemberAccess { object, .. } if object == &node.id
3748 )
3749 })
3750 })
3751 .map(|(node, name)| ComponentDiagnostic {
3752 severity: ComponentDiagnosticSeverity::Error,
3753 effect_id: None,
3754 statement_id: None,
3755 context_declaration_candidate_id: None,
3756 context_id: None,
3757 provider_id: None,
3758 consumer_id: None,
3759 slot_id: None,
3760 invocation_id: None,
3761 component_instance_id: None,
3762 slot_binding_id: None,
3763 structural_region_id: None,
3764 component_id: None,
3765 provider_instance_id: None,
3766 consumer_instance_id: None,
3767 secondary_labels: Vec::new(),
3768 code: ComputedDiagnosticCode::UnsupportedBody.as_str().to_string(),
3769 message: format!(
3770 "computed getter `{}` may only directly project `this.{name}.data`, `this.{name}.error`, or `this.{name}.state`",
3771 computed.name
3772 ),
3773 provenance: Some(node.provenance.clone()),
3774 })
3775 .collect()
3776}
3777
3778fn is_direct_resource_projection(node: &ExpressionNode, nodes: &[&ExpressionNode]) -> bool {
3779 let ExpressionNodeKind::MemberAccess {
3780 property,
3781 optional: false,
3782 ..
3783 } = &node.kind
3784 else {
3785 return false;
3786 };
3787 matches!(property.as_str(), "data" | "error" | "state")
3788 && !nodes.iter().any(|candidate| {
3789 matches!(
3790 &candidate.kind,
3791 ExpressionNodeKind::MemberAccess { object, .. }
3792 | ExpressionNodeKind::IndexAccess { object, .. }
3793 if object == &node.id
3794 )
3795 })
3796}
3797
3798fn collect_computed_type_diagnostics(
3799 computed_values: &BTreeMap<SemanticId, ComputedValue>,
3800 semantic_types: &SemanticTypeModel,
3801) -> Vec<ComponentDiagnostic> {
3802 let mut diagnostics = Vec::new();
3803
3804 for computed_type in semantic_types.computed_values.values() {
3805 let computed = computed_values
3806 .get(&computed_type.computed)
3807 .expect("computed type records should have computed entities");
3808 if computed_type.declared_return_compatible == Some(false) {
3809 let declared = computed_type
3810 .declared_return_type
3811 .as_ref()
3812 .expect("incompatible computed return types should be declared");
3813 diagnostics.push(ComponentDiagnostic {
3814 severity: ComponentDiagnosticSeverity::Error,
3815 effect_id: None,
3816 statement_id: None,
3817 context_declaration_candidate_id: None,
3818 context_id: None,
3819 provider_id: None,
3820 consumer_id: None,
3821 slot_id: None,
3822 invocation_id: None,
3823 component_instance_id: None,
3824 slot_binding_id: None,
3825 structural_region_id: None,
3826 component_id: None,
3827 provider_instance_id: None,
3828 consumer_instance_id: None,
3829 secondary_labels: Vec::new(),
3830 code: ComputedDiagnosticCode::TypeMismatch.as_str().to_string(),
3831 message: format!(
3832 "computed getter `{}` returns `{}` but declares `{}`",
3833 computed.name,
3834 crate::semantic_type_text(&computed_type.semantic_type),
3835 crate::semantic_type_text(declared)
3836 ),
3837 provenance: Some(computed_type.provenance.clone()),
3838 });
3839 }
3840 if computed_type.serialization == crate::SerializationCompatibility::NotSerializable {
3841 diagnostics.push(ComponentDiagnostic {
3842 severity: ComponentDiagnosticSeverity::Error,
3843 effect_id: None,
3844 statement_id: None,
3845 context_declaration_candidate_id: None,
3846 context_id: None,
3847 provider_id: None,
3848 consumer_id: None,
3849 slot_id: None,
3850 invocation_id: None,
3851 component_instance_id: None,
3852 slot_binding_id: None,
3853 structural_region_id: None,
3854 component_id: None,
3855 provider_instance_id: None,
3856 consumer_instance_id: None,
3857 secondary_labels: Vec::new(),
3858 code: ComputedDiagnosticCode::SerializationViolation
3859 .as_str()
3860 .to_string(),
3861 message: format!(
3862 "computed getter `{}` has a non-serializable result",
3863 computed.name
3864 ),
3865 provenance: Some(computed_type.provenance.clone()),
3866 });
3867 }
3868 }
3869
3870 diagnostics
3871}
3872
3873fn sort_computed_diagnostics(diagnostics: &mut [ComponentDiagnostic]) {
3874 diagnostics.sort_by(|left, right| {
3875 (
3876 left.code.as_str(),
3877 left.provenance.as_ref().map(|provenance| &provenance.path),
3878 left.provenance
3879 .as_ref()
3880 .map(|provenance| provenance.span.start),
3881 left.provenance
3882 .as_ref()
3883 .map(|provenance| provenance.span.end),
3884 left.message.as_str(),
3885 )
3886 .cmp(&(
3887 right.code.as_str(),
3888 right.provenance.as_ref().map(|provenance| &provenance.path),
3889 right
3890 .provenance
3891 .as_ref()
3892 .map(|provenance| provenance.span.start),
3893 right
3894 .provenance
3895 .as_ref()
3896 .map(|provenance| provenance.span.end),
3897 right.message.as_str(),
3898 ))
3899 });
3900}
3901
3902fn build_computed_reactive_products(
3903 components: &[ComponentNode],
3904 computed_values: &BTreeMap<SemanticId, ComputedValue>,
3905 effects: &BTreeMap<SemanticId, Effect>,
3906 resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3907 references: &[SemanticReference],
3908 provenance: &BTreeMap<SemanticId, SourceProvenance>,
3909) -> (
3910 IrReactiveGraph,
3911 IrReactiveTransitiveAnalysis,
3912 IrReactiveCycleAnalysis,
3913 IrComputedEvaluationPlan,
3914) {
3915 let graph = crate::build_reactive_graph(
3916 components,
3917 computed_values,
3918 effects,
3919 resource_declarations,
3920 references,
3921 provenance,
3922 );
3923 let transitive_analysis = crate::analyze_reactive_transitive_graph(&graph);
3924 let cycle_analysis = crate::analyze_reactive_cycles(&graph);
3925 let evaluation_plan = crate::plan_computed_evaluation(&graph);
3926 (graph, transitive_analysis, cycle_analysis, evaluation_plan)
3927}
3928
3929fn extend_template_references(
3930 references: &mut Vec<SemanticReference>,
3931 components: &[ComponentNode],
3932 computed_values: &BTreeMap<SemanticId, ComputedValue>,
3933 template_entities: &[TemplateSemanticEntity],
3934 ownership: &BTreeMap<SemanticId, SemanticOwner>,
3935) {
3936 references.extend(build_template_state_references(
3937 components,
3938 template_entities,
3939 ownership,
3940 ));
3941 references.extend(build_template_computed_references(
3942 components,
3943 computed_values,
3944 template_entities,
3945 ownership,
3946 ));
3947 references.extend(build_template_event_references(
3948 components,
3949 template_entities,
3950 ownership,
3951 ));
3952 references.extend(build_template_local_references(
3953 components,
3954 template_entities,
3955 ownership,
3956 ));
3957}
3958
3959fn build_form_field_binding_references(
3960 bindings: &BTreeMap<FieldBindingId, FormFieldBinding>,
3961) -> Vec<SemanticReference> {
3962 bindings
3963 .values()
3964 .flat_map(|binding| {
3965 [
3966 SemanticReference {
3967 kind: SemanticReferenceKind::FieldBindingField,
3968 source: binding.id.as_semantic_id().clone(),
3969 target: binding.field.as_semantic_id().clone(),
3970 provenance: binding.expression_provenance.clone(),
3971 },
3972 SemanticReference {
3973 kind: SemanticReferenceKind::FieldBindingForm,
3974 source: binding.id.as_semantic_id().clone(),
3975 target: binding.form.as_semantic_id().clone(),
3976 provenance: binding.expression_provenance.clone(),
3977 },
3978 ]
3979 })
3980 .collect()
3981}
3982
3983fn computed_call_purity_kind(
3984 component: &ComponentNode,
3985 callee: &str,
3986) -> crate::ComputedPurityViolationKind {
3987 if matches!(callee, "resource" | "this.resource") {
3988 return crate::ComputedPurityViolationKind::Resource;
3989 }
3990 if matches!(
3991 callee,
3992 "Math.random"
3993 | "Date.now"
3994 | "performance.now"
3995 | "crypto.randomUUID"
3996 | "crypto.getRandomValues"
3997 ) {
3998 return crate::ComputedPurityViolationKind::NondeterministicOperation;
3999 }
4000 if callee.starts_with("console.")
4001 || matches!(
4002 callee,
4003 "fetch" | "setTimeout" | "setInterval" | "queueMicrotask"
4004 )
4005 {
4006 return crate::ComputedPurityViolationKind::Effect;
4007 }
4008 if let Some(name) = callee.strip_prefix("this.") {
4009 if let Some(method) = component.methods.iter().find(|method| method.name == name) {
4010 if method.is_action()
4011 || method
4012 .decorators
4013 .iter()
4014 .any(|decorator| decorator == "action")
4015 {
4016 return crate::ComputedPurityViolationKind::Action;
4017 }
4018 if method
4019 .decorators
4020 .iter()
4021 .any(|decorator| decorator == "effect")
4022 {
4023 return crate::ComputedPurityViolationKind::Effect;
4024 }
4025 if method
4026 .decorators
4027 .iter()
4028 .any(|decorator| decorator == "resource")
4029 {
4030 return crate::ComputedPurityViolationKind::Resource;
4031 }
4032 }
4033 }
4034 crate::ComputedPurityViolationKind::ArbitraryMethodCall
4035}
4036
4037fn build_computed_references(
4038 components: &[ComponentNode],
4039 computed_values: &BTreeMap<SemanticId, ComputedValue>,
4040 resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
4041 expression_graph: &ExpressionGraph,
4042) -> Vec<SemanticReference> {
4043 let mut references = BTreeMap::new();
4044
4045 for computed in computed_values.values() {
4046 let Some(component_id) = computed.owner.entity_id() else {
4047 continue;
4048 };
4049 let Some(component) = components
4050 .iter()
4051 .find(|component| component.id == *component_id)
4052 else {
4053 continue;
4054 };
4055
4056 let nodes = expression_graph.nodes_for(&computed.id);
4057 let direct_resource_projection_objects = nodes
4058 .iter()
4059 .filter(|node| is_direct_resource_projection(node, &nodes))
4060 .filter_map(|node| match &node.kind {
4061 crate::ExpressionNodeKind::MemberAccess { object, .. } => Some(object.clone()),
4062 _ => None,
4063 })
4064 .collect::<BTreeSet<_>>();
4065
4066 for node in nodes {
4067 let crate::ExpressionNodeKind::ThisMember { name } = &node.kind else {
4068 continue;
4069 };
4070 let reference = if let Some(field) = component
4071 .state_fields
4072 .iter()
4073 .find(|field| field.name == *name)
4074 {
4075 SemanticReference {
4076 kind: SemanticReferenceKind::ComputedState,
4077 source: computed.id.clone(),
4078 target: field.id.clone(),
4079 provenance: computed.provenance.clone(),
4080 }
4081 } else if let Some(target) = computed_values.get(&component.id.computed(name)) {
4082 SemanticReference {
4083 kind: SemanticReferenceKind::ComputedComputed,
4084 source: computed.id.clone(),
4085 target: target.id.clone(),
4086 provenance: computed.provenance.clone(),
4087 }
4088 } else if direct_resource_projection_objects.contains(&node.id) {
4089 let target = ResourceId::for_owner(&component.id, name);
4090 let Some(declaration) = resource_declarations.get(&target) else {
4091 continue;
4092 };
4093 SemanticReference {
4094 kind: SemanticReferenceKind::ComputedResource,
4095 source: computed.id.clone(),
4096 target: declaration.id.as_semantic_id().clone(),
4097 provenance: node.provenance.clone(),
4098 }
4099 } else {
4100 continue;
4101 };
4102 references.insert(
4103 (reference.source.clone(), reference.target.clone()),
4104 reference,
4105 );
4106 }
4107 }
4108
4109 references.into_values().collect()
4110}
4111
4112fn build_effect_references(
4113 components: &[ComponentNode],
4114 effects: &BTreeMap<SemanticId, Effect>,
4115 computed_values: &BTreeMap<SemanticId, ComputedValue>,
4116 expression_graph: &ExpressionGraph,
4117) -> Vec<SemanticReference> {
4118 let mut references = Vec::new();
4119 for effect in effects.values() {
4120 let Some(component_id) = effect.owner.entity_id() else {
4121 continue;
4122 };
4123 let Some(component) = components
4124 .iter()
4125 .find(|component| component.id == *component_id)
4126 else {
4127 continue;
4128 };
4129 for node in expression_graph.nodes_for(&effect.id) {
4130 let crate::ExpressionNodeKind::ThisMember { name } = &node.kind else {
4131 continue;
4132 };
4133 let reference = if let Some(field) = component
4134 .state_fields
4135 .iter()
4136 .find(|field| field.name == *name)
4137 {
4138 SemanticReference {
4139 kind: SemanticReferenceKind::EffectState,
4140 source: effect.id.clone(),
4141 target: field.id.clone(),
4142 provenance: node.provenance.clone(),
4143 }
4144 } else if let Some(computed) = computed_values.get(&component.id.computed(name)) {
4145 SemanticReference {
4146 kind: SemanticReferenceKind::EffectComputed,
4147 source: effect.id.clone(),
4148 target: computed.id.clone(),
4149 provenance: node.provenance.clone(),
4150 }
4151 } else {
4152 continue;
4153 };
4154 references.push(reference);
4155 }
4156 }
4157 references.sort_by(|left, right| {
4158 (
4159 left.source.as_str(),
4160 left.target.as_str(),
4161 left.provenance.span.start,
4162 )
4163 .cmp(&(
4164 right.source.as_str(),
4165 right.target.as_str(),
4166 right.provenance.span.start,
4167 ))
4168 });
4169 references.dedup_by(|left, right| {
4170 left.kind == right.kind && left.source == right.source && left.target == right.target
4171 });
4172 references
4173}
4174
4175fn build_provider_references(
4176 providers: &BTreeMap<ProviderId, ProviderEntity>,
4177) -> Vec<SemanticReference> {
4178 providers
4179 .values()
4180 .map(|provider| SemanticReference {
4181 kind: SemanticReferenceKind::ProvidesContext,
4182 source: provider.id.as_semantic_id().clone(),
4183 target: provider.context.as_semantic_id().clone(),
4184 provenance: provider.provenance.clone(),
4185 })
4186 .collect()
4187}
4188
4189fn build_consumer_references(
4190 consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
4191) -> Vec<SemanticReference> {
4192 consumers
4193 .values()
4194 .filter_map(|consumer| {
4195 let ContextResolutionState::Resolved(context) = &consumer.context_resolution else {
4196 return None;
4197 };
4198 Some(SemanticReference {
4199 kind: SemanticReferenceKind::ConsumesContext,
4200 source: consumer.id.as_semantic_id().clone(),
4201 target: context.as_semantic_id().clone(),
4202 provenance: consumer.provenance.clone(),
4203 })
4204 })
4205 .collect()
4206}
4207
4208fn build_context_resolution_references(
4209 resolutions: &BTreeMap<ConsumerId, ContextResolution>,
4210) -> Vec<SemanticReference> {
4211 resolutions
4212 .values()
4213 .filter_map(|resolution| {
4214 let ContextResolutionResult::Provider { provider, .. } = &resolution.result else {
4215 return None;
4216 };
4217 Some(SemanticReference {
4218 kind: SemanticReferenceKind::ResolvesToProvider,
4219 source: resolution.consumer.as_semantic_id().clone(),
4220 target: provider.as_semantic_id().clone(),
4221 provenance: resolution.provenance.clone(),
4222 })
4223 })
4224 .collect()
4225}
4226
4227fn build_template_state_references(
4228 components: &[ComponentNode],
4229 template_entities: &[TemplateSemanticEntity],
4230 ownership: &BTreeMap<SemanticId, SemanticOwner>,
4231) -> Vec<SemanticReference> {
4232 template_entities
4233 .iter()
4234 .filter(|entity| {
4235 matches!(
4236 entity.kind,
4237 TemplateSemanticKind::Binding
4238 | TemplateSemanticKind::AttributeBinding
4239 | TemplateSemanticKind::Conditional
4240 | TemplateSemanticKind::List
4241 )
4242 })
4243 .filter_map(|entity| {
4244 let field_name = entity.expression.as_deref().and_then(this_member_name)?;
4245 let component = template_entity_component(components, ownership, entity)?;
4246 let field = component
4247 .state_fields
4248 .iter()
4249 .find(|field| field.name == field_name)?;
4250
4251 Some(SemanticReference {
4252 kind: SemanticReferenceKind::TemplateState,
4253 source: entity.id.clone(),
4254 target: field.id.clone(),
4255 provenance: entity.provenance.clone(),
4256 })
4257 })
4258 .collect()
4259}
4260
4261fn build_template_computed_references(
4262 components: &[ComponentNode],
4263 computed_values: &BTreeMap<SemanticId, ComputedValue>,
4264 template_entities: &[TemplateSemanticEntity],
4265 ownership: &BTreeMap<SemanticId, SemanticOwner>,
4266) -> Vec<SemanticReference> {
4267 template_entities
4268 .iter()
4269 .filter(|entity| {
4270 matches!(
4271 entity.kind,
4272 TemplateSemanticKind::Binding
4273 | TemplateSemanticKind::AttributeBinding
4274 | TemplateSemanticKind::Conditional
4275 | TemplateSemanticKind::List
4276 )
4277 })
4278 .filter_map(|entity| {
4279 let computed_name = entity.expression.as_deref().and_then(this_member_name)?;
4280 let component = template_entity_component(components, ownership, entity)?;
4281 if component
4282 .state_fields
4283 .iter()
4284 .any(|field| field.name == computed_name)
4285 {
4286 return None;
4287 }
4288 let computed = computed_values.get(&component.id.computed(computed_name))?;
4289
4290 Some(SemanticReference {
4291 kind: SemanticReferenceKind::TemplateComputed,
4292 source: entity.id.clone(),
4293 target: computed.id.clone(),
4294 provenance: entity.provenance.clone(),
4295 })
4296 })
4297 .collect()
4298}
4299
4300fn build_template_event_references(
4301 components: &[ComponentNode],
4302 template_entities: &[TemplateSemanticEntity],
4303 ownership: &BTreeMap<SemanticId, SemanticOwner>,
4304) -> Vec<SemanticReference> {
4305 template_entities
4306 .iter()
4307 .filter(|entity| entity.kind == TemplateSemanticKind::EventAttribute)
4308 .filter_map(|entity| {
4309 let method_name = entity.expression.as_deref().and_then(this_member_name)?;
4310 let component = template_entity_component(components, ownership, entity)?;
4311 let method = component
4312 .methods
4313 .iter()
4314 .find(|method| method.name == method_name)?;
4315
4316 Some(SemanticReference {
4317 kind: SemanticReferenceKind::EventMethod,
4318 source: entity.id.clone(),
4319 target: method.id.clone(),
4320 provenance: entity.provenance.clone(),
4321 })
4322 })
4323 .collect()
4324}
4325
4326fn build_template_local_references(
4327 components: &[ComponentNode],
4328 template_entities: &[TemplateSemanticEntity],
4329 ownership: &BTreeMap<SemanticId, SemanticOwner>,
4330) -> Vec<SemanticReference> {
4331 let mut references = template_entities
4332 .iter()
4333 .filter(|entity| {
4334 matches!(
4335 entity.kind,
4336 TemplateSemanticKind::Binding | TemplateSemanticKind::AttributeBinding
4337 ) && entity.scope == TemplateSemanticScope::Render
4338 })
4339 .filter_map(|entity| {
4340 let name = entity.expression.as_deref()?;
4341 let component = template_entity_component(components, ownership, entity)?;
4342 let render = component
4343 .methods
4344 .iter()
4345 .find(|method| method.name == "render")?;
4346 let local = unique_local_variable(render, name)?;
4347
4348 Some(SemanticReference {
4349 kind: SemanticReferenceKind::TemplateLocal,
4350 source: entity.id.clone(),
4351 target: local.id.clone(),
4352 provenance: entity.provenance.clone(),
4353 })
4354 })
4355 .collect::<Vec<_>>();
4356 references.sort_by(|left, right| {
4357 (left.source.as_str(), left.target.as_str())
4358 .cmp(&(right.source.as_str(), right.target.as_str()))
4359 });
4360 references
4361}
4362
4363fn unique_local_variable<'a>(
4364 method: &'a ComponentMethod,
4365 name: &str,
4366) -> Option<&'a MethodLocalVariable> {
4367 let mut locals = method
4368 .local_variables
4369 .iter()
4370 .filter(|local| local.name == name);
4371 let local = locals.next()?;
4372 locals.next().is_none().then_some(local)
4373}
4374
4375fn template_entity_component<'a>(
4376 components: &'a [ComponentNode],
4377 ownership: &BTreeMap<SemanticId, SemanticOwner>,
4378 entity: &TemplateSemanticEntity,
4379) -> Option<&'a ComponentNode> {
4380 let template_id = ownership.get(&entity.id)?.entity_id()?;
4381 let component_id = ownership.get(template_id)?.entity_id()?;
4382
4383 components
4384 .iter()
4385 .find(|component| component.id == *component_id)
4386}
4387
4388fn this_member_name(expression: &str) -> Option<&str> {
4389 expression.strip_prefix("this.").filter(|name| {
4390 !name.is_empty()
4391 && name
4392 .bytes()
4393 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
4394 })
4395}
4396
4397#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
4398fn collect_ownership(
4399 components: &[ComponentNode],
4400 contexts: &BTreeMap<ContextId, ContextEntity>,
4401 forms: &BTreeMap<FormId, FormEntity>,
4402 form_fields: &BTreeMap<FieldId, FormFieldEntity>,
4403 form_field_bindings: &BTreeMap<FieldBindingId, FormFieldBinding>,
4404 providers: &BTreeMap<ProviderId, ProviderEntity>,
4405 consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
4406 slots: &BTreeMap<SlotId, SlotEntity>,
4407 component_invocations: &BTreeMap<ComponentInvocationId, ComponentInvocationEntity>,
4408 slot_content_fragments: &BTreeMap<SlotContentFragmentId, SlotContentFragment>,
4409 slot_outlets: &BTreeMap<SlotOutletId, SlotOutlet>,
4410 component_instance_plan: &ComponentInstancePlan,
4411 computed_values: &BTreeMap<SemanticId, ComputedValue>,
4412 effects: &BTreeMap<SemanticId, Effect>,
4413 templates: &[TemplateNode],
4414 template_entities: &[TemplateSemanticEntity],
4415) -> BTreeMap<SemanticId, SemanticOwner> {
4416 let mut ownership = BTreeMap::new();
4417
4418 for component in components {
4419 ownership.insert(component.id.clone(), SemanticOwner::Application);
4420
4421 for field in &component.state_fields {
4422 ownership.insert(
4423 field.id.clone(),
4424 SemanticOwner::entity(component.id.clone()),
4425 );
4426 }
4427 for method in &component.methods {
4428 ownership.insert(
4429 method.id.clone(),
4430 SemanticOwner::entity(component.id.clone()),
4431 );
4432 for parameter in &method.parameters {
4433 ownership.insert(
4434 parameter.id.clone(),
4435 SemanticOwner::entity(method.id.clone()),
4436 );
4437 }
4438 for local in &method.local_variables {
4439 ownership.insert(local.id.clone(), SemanticOwner::entity(method.id.clone()));
4440 }
4441 }
4442 for computed in computed_values
4443 .values()
4444 .filter(|computed| computed.owner.entity_id() == Some(&component.id))
4445 {
4446 ownership.insert(computed.id.clone(), computed.owner.clone());
4447 }
4448 for context in contexts
4449 .values()
4450 .filter(|context| context.owner.entity_id() == Some(&component.id))
4451 {
4452 ownership.insert(context.id.as_semantic_id().clone(), context.owner.clone());
4453 }
4454 for form in forms
4455 .values()
4456 .filter(|form| form.owner.entity_id() == Some(&component.id))
4457 {
4458 ownership.insert(form.id.as_semantic_id().clone(), form.owner.clone());
4459 }
4460 for field in form_fields
4461 .values()
4462 .filter(|field| field.owner_component == component.id)
4463 {
4464 ownership.insert(
4465 field.id.as_semantic_id().clone(),
4466 SemanticOwner::entity(field.owner_form.as_semantic_id().clone()),
4467 );
4468 }
4469 for provider in providers
4470 .values()
4471 .filter(|provider| provider.owner.entity_id() == Some(&component.id))
4472 {
4473 ownership.insert(provider.id.as_semantic_id().clone(), provider.owner.clone());
4474 }
4475 for consumer in consumers
4476 .values()
4477 .filter(|consumer| consumer.owner.entity_id() == Some(&component.id))
4478 {
4479 ownership.insert(consumer.id.as_semantic_id().clone(), consumer.owner.clone());
4480 }
4481 for slot in slots.values().filter(|slot| slot.owner == component.id) {
4482 ownership.insert(slot.id.as_semantic_id().clone(), slot.semantic_owner());
4483 }
4484 for invocation in component_invocations
4485 .values()
4486 .filter(|invocation| invocation.owner_component == component.id)
4487 {
4488 ownership.insert(
4489 invocation.id.as_semantic_id().clone(),
4490 SemanticOwner::entity(component.id.clone()),
4491 );
4492 }
4493 for fragment in slot_content_fragments
4494 .values()
4495 .filter(|fragment| fragment.owner_component == component.id)
4496 {
4497 ownership.insert(
4498 fragment.id.as_semantic_id().clone(),
4499 SemanticOwner::entity(component.id.clone()),
4500 );
4501 }
4502 for outlet in slot_outlets
4503 .values()
4504 .filter(|outlet| outlet.owner_component == component.id)
4505 {
4506 ownership.insert(
4507 outlet.id.as_semantic_id().clone(),
4508 SemanticOwner::entity(component.id.clone()),
4509 );
4510 }
4511 for effect in effects
4512 .values()
4513 .filter(|effect| effect.owner.entity_id() == Some(&component.id))
4514 {
4515 ownership.insert(effect.id.clone(), effect.owner.clone());
4516 }
4517 for action in &component.actions {
4518 ownership.insert(
4519 action.id.clone(),
4520 SemanticOwner::entity(component.id.method(&action.method)),
4521 );
4522 }
4523 if let Some(render) = &component.render {
4524 for handler in render_event_handlers(render) {
4525 ownership.insert(
4526 handler.id.clone(),
4527 SemanticOwner::entity(component.id.template()),
4528 );
4529 }
4530 }
4531 }
4532
4533 for instance in component_instance_plan.instances.values() {
4534 ownership.insert(
4535 instance.id.as_semantic_id().clone(),
4536 instance
4537 .parent_instance
4538 .as_ref()
4539 .map_or(SemanticOwner::Application, |parent| {
4540 SemanticOwner::entity(parent.as_semantic_id().clone())
4541 }),
4542 );
4543 }
4544 for blocked in component_instance_plan.blocked.values() {
4545 ownership.insert(
4546 blocked.id.as_semantic_id().clone(),
4547 SemanticOwner::entity(blocked.parent_instance.as_semantic_id().clone()),
4548 );
4549 }
4550
4551 for template in templates {
4552 let component = components
4553 .iter()
4554 .find(|component| component.id.template() == template.id)
4555 .expect("template graph should only contain component templates");
4556 ownership.insert(
4557 template.id.clone(),
4558 SemanticOwner::entity(component.id.clone()),
4559 );
4560 }
4561
4562 for binding in form_field_bindings.values() {
4563 ownership.insert(
4564 binding.id.as_semantic_id().clone(),
4565 SemanticOwner::entity(binding.control_entity.clone()),
4566 );
4567 }
4568
4569 for entity in template_entities {
4570 ownership.insert(entity.id.clone(), entity.owner.clone());
4571 }
4572
4573 ownership
4574}
4575
4576#[cfg(test)]
4577mod tests {
4578 use super::{
4579 build_application_semantic_model, build_application_semantic_model_for_unit,
4580 build_file_route_application_semantic_model_for_unit_with_packages, collect_ownership,
4581 };
4582 use crate::{
4583 build_component_graph_for_module, build_template_graph, build_template_semantic_entities,
4584 CompilationUnit, ComponentInstancePlan, SemanticEntity, SemanticEntityKind, SemanticOwner,
4585 SemanticReferenceKind, TemplateSemanticKind,
4586 };
4587
4588 #[test]
4589 fn lowers_supported_primitive_state_annotations_into_canonical_types() {
4590 let parsed = presolve_parser::parse_file(
4591 "src/TypedState.tsx",
4592 r#"
4593@component("x-typed-state")
4594class TypedState extends Component {
4595 count: number = state(0);
4596}
4597"#,
4598 );
4599
4600 let asm = build_application_semantic_model(&parsed);
4601
4602 let field = &asm.components[0].state_fields[0];
4603 let assignment = asm
4604 .semantic_types
4605 .assignments
4606 .get(&field.id)
4607 .expect("canonical declared type");
4608
4609 assert_eq!(assignment.semantic_type, crate::SemanticType::Number);
4610 assert_eq!(assignment.status, crate::SemanticTypeStatus::Declared);
4611 assert_eq!(
4612 assignment.provenance,
4613 field.declared_type.as_ref().unwrap().provenance
4614 );
4615 }
4616
4617 #[test]
4618 fn queries_canonical_type_information_from_the_asm() {
4619 let parsed = presolve_parser::parse_file(
4620 "src/TypeQueries.tsx",
4621 r#"
4622@component("x-type-queries")
4623class TypeQueries extends Component {
4624 count: number = state(0);
4625 label = state("Presolve");
4626}
4627"#,
4628 );
4629 let asm = build_application_semantic_model(&parsed);
4630 let count = &asm.components[0].state_fields[0];
4631 let label = &asm.components[0].state_fields[1];
4632
4633 assert_eq!(
4634 asm.semantic_type_of(&count.id),
4635 Some(&crate::SemanticType::Number)
4636 );
4637 assert_eq!(asm.type_declarations(&crate::SemanticType::Number).len(), 1);
4638 assert_eq!(asm.type_usages(&crate::SemanticType::String).len(), 1);
4639 assert_eq!(
4640 asm.serialization_compatibility_of(&count.id),
4641 Some(crate::SerializationCompatibility::Serializable)
4642 );
4643 assert_eq!(asm.is_type_assignable(&label.id, &count.id), Some(false));
4644 }
4645
4646 #[test]
4647 fn lowers_typed_method_parameters_into_canonical_entities() {
4648 let parsed = presolve_parser::parse_file(
4649 "src/Parameters.tsx",
4650 r#"
4651@component("x-parameters")
4652class Parameters extends Component {
4653 save(title: string, retries?: number) {}
4654}
4655"#,
4656 );
4657
4658 let asm = build_application_semantic_model(&parsed);
4659 let method = &asm.components[0].methods[0];
4660 let parameters = &method.parameters;
4661
4662 assert_eq!(parameters.len(), 2);
4663 assert_eq!(
4664 asm.semantic_types.assignments[¶meters[0].id].semantic_type,
4665 crate::SemanticType::String
4666 );
4667 assert_eq!(
4668 asm.semantic_types.assignments[¶meters[1].id].semantic_type,
4669 crate::SemanticType::Number
4670 );
4671 assert!(parameters.iter().all(|parameter| {
4672 asm.owner(¶meter.id) == Some(&SemanticOwner::entity(method.id.clone()))
4673 && asm
4674 .entity(¶meter.id)
4675 .is_some_and(|entity| entity.kind() == SemanticEntityKind::Parameter)
4676 }));
4677 }
4678
4679 #[test]
4680 fn lowers_declared_and_inferred_method_return_types() {
4681 let parsed = presolve_parser::parse_file(
4682 "src/Returns.tsx",
4683 r#"
4684@component("x-returns")
4685class Returns extends Component {
4686 declared(): string { return "Presolve"; }
4687 inferred() { return 1; }
4688}
4689"#,
4690 );
4691
4692 let asm = build_application_semantic_model(&parsed);
4693 let methods = &asm.components[0].methods;
4694 let declared = &methods[0];
4695 let inferred = &methods[1];
4696
4697 assert_eq!(
4698 asm.semantic_types.assignments[&declared.id].semantic_type,
4699 crate::SemanticType::String
4700 );
4701 assert_eq!(
4702 asm.semantic_types.assignments[&declared.id].status,
4703 crate::SemanticTypeStatus::Declared
4704 );
4705 assert_eq!(
4706 asm.semantic_types.assignments[&inferred.id].semantic_type,
4707 crate::SemanticType::Number
4708 );
4709 assert_eq!(
4710 asm.semantic_types.assignments[&inferred.id].status,
4711 crate::SemanticTypeStatus::Inferred
4712 );
4713 }
4714
4715 #[test]
4716 fn establishes_typed_computed_getter_contracts() {
4717 let parsed = presolve_parser::parse_file(
4718 "src/Computed.tsx",
4719 r#"
4720@component("x-computed")
4721class Computed extends Component {
4722 @computed()
4723 get remainingCount(): number { return 1; }
4724
4725 render() { return <p />; }
4726}
4727"#,
4728 );
4729
4730 let asm = build_application_semantic_model(&parsed);
4731 let method = &asm.components[0].methods[0];
4732 let computed_id = asm.components[0].id.computed("remainingCount");
4733 let computed = asm
4734 .semantic_types
4735 .computed_values
4736 .get(&computed_id)
4737 .expect("computed type contract");
4738 let computed_entity = asm
4739 .computed_value(&computed_id)
4740 .expect("first-class computed entity");
4741
4742 assert!(method.is_getter);
4743 assert!(method.is_computed());
4744 assert_eq!(
4745 computed.semantic_type,
4746 crate::SemanticType::NumberLiteral("1".to_string())
4747 );
4748 assert_eq!(
4749 computed.declared_return_type,
4750 Some(crate::SemanticType::Number)
4751 );
4752 assert_eq!(computed.declared_return_compatible, Some(true));
4753 assert_eq!(computed_entity.method, method.id);
4754 assert_eq!(
4755 computed_entity.owner,
4756 crate::SemanticOwner::entity(asm.components[0].id.clone())
4757 );
4758 assert_eq!(
4759 computed_entity.cache_policy,
4760 crate::ComputedCachePolicy::Memoized
4761 );
4762 assert_eq!(computed_entity.purity, crate::ComputedPurity::Pure);
4763 assert!(computed_entity.purity_violations.is_empty());
4764 assert_eq!(
4765 computed_entity.execution_boundary,
4766 crate::ExecutionBoundary::Client
4767 );
4768 assert_eq!(
4769 asm.entity(&computed_id),
4770 Some(SemanticEntity::Computed(computed_entity))
4771 );
4772 assert_eq!(
4773 asm.owner(&computed_id),
4774 Some(&crate::SemanticOwner::entity(asm.components[0].id.clone()))
4775 );
4776 assert_eq!(asm.provenance(&computed_id), asm.provenance(&method.id));
4777 assert_eq!(
4778 asm.entities_of_kind(SemanticEntityKind::Computed),
4779 vec![&computed_id]
4780 );
4781 }
4782
4783 #[test]
4784 fn resolves_computed_reads_to_canonical_state_and_computed_references() {
4785 let parsed = presolve_parser::parse_file(
4786 "src/ComputedReads.tsx",
4787 r#"
4788@component("x-computed-reads")
4789class ComputedReads extends Component {
4790 count = state(1);
4791 profile = state({ hidden: false });
4792
4793 @computed()
4794 get doubled() { return this.count * 2; }
4795
4796 @computed()
4797 get visible() { return this.doubled + this.count + this.count; }
4798
4799 @computed()
4800 get profileHidden() { return this.profile.hidden; }
4801
4802 @computed()
4803 get unresolved() { return this.missing; }
4804}
4805"#,
4806 );
4807 let asm = build_application_semantic_model(&parsed);
4808 let component = &asm.components[0];
4809 let count = component.id.state_field("count");
4810 let profile = component.id.state_field("profile");
4811 let doubled = component.id.computed("doubled");
4812 let visible = component.id.computed("visible");
4813 let profile_hidden = component.id.computed("profileHidden");
4814 let unresolved = component.id.computed("unresolved");
4815
4816 let state_references = asm.references_of_kind(SemanticReferenceKind::ComputedState);
4817 assert_eq!(state_references.len(), 3);
4818 assert!(state_references
4819 .iter()
4820 .any(|reference| reference.source == doubled && reference.target == count));
4821 assert!(state_references
4822 .iter()
4823 .any(|reference| reference.source == visible && reference.target == count));
4824 assert!(state_references.iter().any(|reference| {
4825 reference.source == profile_hidden && reference.target == profile
4826 }));
4827
4828 let computed_references = asm.references_of_kind(SemanticReferenceKind::ComputedComputed);
4829 assert_eq!(computed_references.len(), 1);
4830 assert_eq!(computed_references[0].source, visible);
4831 assert_eq!(computed_references[0].target, doubled);
4832 assert!(!asm
4833 .references
4834 .iter()
4835 .any(|reference| reference.source == unresolved));
4836 assert!(asm
4837 .references
4838 .iter()
4839 .all(|reference| { asm.provenance(&reference.source) == Some(&reference.provenance) }));
4840 }
4841
4842 #[test]
4843 fn populates_reactive_graph_from_direct_computed_reads() {
4844 let parsed = presolve_parser::parse_file(
4845 "src/ComputedReactiveGraph.tsx",
4846 r#"
4847@component("x-computed-reactive-graph")
4848class ComputedReactiveGraph extends Component {
4849 count = state(1);
4850
4851 @computed()
4852 get doubled() { return this.count * 2; }
4853
4854 @computed()
4855 get label() { return this.doubled + 1; }
4856}
4857"#,
4858 );
4859 let asm = build_application_semantic_model(&parsed);
4860 let component = &asm.components[0];
4861 let count = component.id.state_field("count");
4862 let doubled = component.id.computed("doubled");
4863 let label = component.id.computed("label");
4864 let graph = &asm.reactive_graph;
4865
4866 assert_eq!(graph.nodes.len(), 3);
4867 assert_eq!(
4868 graph.nodes[count.as_str()].kind,
4869 crate::IrReactiveNodeKind::State
4870 );
4871 assert_eq!(
4872 graph.nodes[doubled.as_str()].kind,
4873 crate::IrReactiveNodeKind::Computed
4874 );
4875 assert_eq!(
4876 graph.nodes[label.as_str()].kind,
4877 crate::IrReactiveNodeKind::Computed
4878 );
4879
4880 let doubled_dependencies = graph.computed_dependencies(doubled.as_str());
4881 assert_eq!(doubled_dependencies.len(), 1);
4882 assert_eq!(doubled_dependencies[0].target, count.as_str());
4883 let label_dependencies = graph.computed_dependencies(label.as_str());
4884 assert_eq!(label_dependencies.len(), 1);
4885 assert_eq!(label_dependencies[0].target, doubled.as_str());
4886
4887 let count_invalidations = graph.invalidations_from(count.as_str());
4888 assert_eq!(count_invalidations.len(), 1);
4889 assert_eq!(count_invalidations[0].target, doubled.as_str());
4890 let doubled_invalidations = graph.invalidations_from(doubled.as_str());
4891 assert_eq!(doubled_invalidations.len(), 1);
4892 assert_eq!(doubled_invalidations[0].target, label.as_str());
4893 assert!(graph
4894 .invalidations_from(count.as_str())
4895 .iter()
4896 .all(|edge| { edge.target != label.as_str() }));
4897 }
4898
4899 #[test]
4900 fn computes_deterministic_transitive_reactive_dependencies_and_dependents() {
4901 let parsed = presolve_parser::parse_file(
4902 "src/ComputedTransitiveReactiveGraph.tsx",
4903 r#"
4904@component("x-computed-transitive-reactive-graph")
4905class ComputedTransitiveReactiveGraph extends Component {
4906 count = state(1);
4907
4908 @computed()
4909 get doubled() { return this.count * 2; }
4910
4911 @computed()
4912 get label() { return this.doubled + 1; }
4913}
4914"#,
4915 );
4916 let asm = build_application_semantic_model(&parsed);
4917 let component = &asm.components[0];
4918 let count = component.id.state_field("count");
4919 let doubled = component.id.computed("doubled");
4920 let label = component.id.computed("label");
4921 let analysis = &asm.reactive_transitive_analysis;
4922
4923 assert_eq!(analysis.dependencies.len(), 3);
4924 assert_eq!(analysis.dependents.len(), 3);
4925 assert_eq!(
4926 analysis.dependencies_of(count.as_str()),
4927 Vec::<String>::new()
4928 );
4929 assert_eq!(
4930 analysis.dependencies_of(doubled.as_str()),
4931 vec![count.as_str().to_string()]
4932 );
4933 assert_eq!(
4934 analysis.dependencies_of(label.as_str()),
4935 vec![doubled.as_str().to_string(), count.as_str().to_string()]
4936 );
4937 assert_eq!(
4938 analysis.dependents_of(count.as_str()),
4939 vec![doubled.as_str().to_string(), label.as_str().to_string()]
4940 );
4941 assert_eq!(
4942 analysis.dependents_of(doubled.as_str()),
4943 vec![label.as_str().to_string()]
4944 );
4945 assert_eq!(analysis.dependents_of(label.as_str()), Vec::<String>::new());
4946 }
4947
4948 #[test]
4949 fn detects_computed_dependency_cycles_with_stable_diagnostics() {
4950 let parsed = presolve_parser::parse_file(
4951 "src/ComputedDependencyCycles.tsx",
4952 r#"
4953@component("x-computed-dependency-cycles")
4954class ComputedDependencyCycles extends Component {
4955 @computed()
4956 get alpha() { return this.beta; }
4957
4958 @computed()
4959 get beta() { return this.alpha; }
4960
4961 @computed()
4962 get selfLoop() { return this.selfLoop; }
4963}
4964"#,
4965 );
4966 let asm = build_application_semantic_model(&parsed);
4967 let component = &asm.components[0];
4968 let alpha = component.id.computed("alpha");
4969 let beta = component.id.computed("beta");
4970 let self_loop = component.id.computed("selfLoop");
4971
4972 assert_eq!(
4973 asm.reactive_cycle_analysis.cycles,
4974 vec![
4975 crate::IrReactiveCycle {
4976 nodes: vec![alpha.as_str().to_string(), beta.as_str().to_string()],
4977 },
4978 crate::IrReactiveCycle {
4979 nodes: vec![self_loop.as_str().to_string()],
4980 },
4981 ]
4982 );
4983 let diagnostics = asm
4984 .diagnostics
4985 .iter()
4986 .filter(|diagnostic| diagnostic.code == "PSC1035")
4987 .collect::<Vec<_>>();
4988 assert_eq!(diagnostics.len(), 2);
4989 assert_eq!(
4990 diagnostics[0].message,
4991 format!("computed dependency cycle detected among: {alpha}, {beta}")
4992 );
4993 assert_eq!(
4994 diagnostics[0].provenance,
4995 Some(
4996 asm.computed_value(&alpha)
4997 .expect("alpha computed value")
4998 .provenance
4999 .clone()
5000 )
5001 );
5002 assert_eq!(
5003 diagnostics[1].message,
5004 format!("computed dependency cycle detected among: {self_loop}")
5005 );
5006 assert_eq!(
5007 diagnostics[1].provenance,
5008 Some(
5009 asm.computed_value(&self_loop)
5010 .expect("self-loop computed value")
5011 .provenance
5012 .clone()
5013 )
5014 );
5015 }
5016
5017 #[test]
5018 fn plans_deterministic_computed_evaluation_order_and_batches() {
5019 let parsed = presolve_parser::parse_file(
5020 "src/ComputedEvaluationPlan.tsx",
5021 r#"
5022@component("x-computed-evaluation-plan")
5023class ComputedEvaluationPlan extends Component {
5024 count = state(1);
5025 other = state(2);
5026
5027 @computed()
5028 get alpha() { return this.count; }
5029
5030 @computed()
5031 get beta() { return this.other; }
5032
5033 @computed()
5034 get gamma() { return this.alpha; }
5035}
5036"#,
5037 );
5038 let asm = build_application_semantic_model(&parsed);
5039 let component = &asm.components[0];
5040 let alpha = component.id.computed("alpha");
5041 let beta = component.id.computed("beta");
5042 let gamma = component.id.computed("gamma");
5043
5044 assert_eq!(
5045 asm.computed_evaluation_plan.evaluation_order,
5046 vec![
5047 alpha.as_str().to_string(),
5048 beta.as_str().to_string(),
5049 gamma.as_str().to_string(),
5050 ]
5051 );
5052 assert_eq!(
5053 asm.computed_evaluation_plan.update_batches,
5054 vec![
5055 vec![alpha.as_str().to_string(), beta.as_str().to_string()],
5056 vec![gamma.as_str().to_string()],
5057 ]
5058 );
5059 assert!(asm.computed_evaluation_plan.unplanned.is_empty());
5060 }
5061
5062 #[test]
5063 fn leaves_cyclic_computed_values_unplanned() {
5064 let parsed = presolve_parser::parse_file(
5065 "src/CyclicComputedEvaluationPlan.tsx",
5066 r#"
5067@component("x-cyclic-computed-evaluation-plan")
5068class CyclicComputedEvaluationPlan extends Component {
5069 @computed()
5070 get alpha() { return this.beta; }
5071
5072 @computed()
5073 get beta() { return this.alpha; }
5074}
5075"#,
5076 );
5077 let asm = build_application_semantic_model(&parsed);
5078 let component = &asm.components[0];
5079 let alpha = component.id.computed("alpha");
5080 let beta = component.id.computed("beta");
5081
5082 assert!(asm.computed_evaluation_plan.evaluation_order.is_empty());
5083 assert!(asm.computed_evaluation_plan.update_batches.is_empty());
5084 assert_eq!(
5085 asm.computed_evaluation_plan.unplanned,
5086 vec![alpha.as_str().to_string(), beta.as_str().to_string()]
5087 );
5088 }
5089
5090 #[test]
5091 fn assigns_inferred_computed_types_and_validates_declared_returns() {
5092 let parsed = presolve_parser::parse_file(
5093 "src/ComputedTypes.tsx",
5094 r#"
5095@component("x-computed-types")
5096class ComputedTypes extends Component {
5097 count: number = state(1);
5098 profile = state({ label: "Presolve" });
5099
5100 @computed()
5101 get doubled(): number { return this.count * 2; }
5102
5103 @computed()
5104 get chained() { return this.doubled + 1; }
5105
5106 @computed()
5107 get label(): string { return this.profile.label; }
5108
5109 @computed()
5110 get invalid(): number { return "wrong"; }
5111
5112 @computed()
5113 get unresolved() { return this.missing; }
5114}
5115"#,
5116 );
5117 let asm = build_application_semantic_model(&parsed);
5118 let component = &asm.components[0];
5119 let doubled = component.id.computed("doubled");
5120 let chained = component.id.computed("chained");
5121 let label = component.id.computed("label");
5122 let invalid = component.id.computed("invalid");
5123 let unresolved = component.id.computed("unresolved");
5124
5125 let types = &asm.semantic_types.computed_values;
5126 assert_eq!(types[&doubled].semantic_type, crate::SemanticType::Number);
5127 assert_eq!(types[&chained].semantic_type, crate::SemanticType::Number);
5128 assert_eq!(types[&label].semantic_type, crate::SemanticType::String);
5129 assert_eq!(
5130 types[&invalid].semantic_type,
5131 crate::SemanticType::StringLiteral("wrong".to_string())
5132 );
5133 assert_eq!(types[&invalid].declared_return_compatible, Some(false));
5134 assert_eq!(types[&doubled].declared_return_compatible, Some(true));
5135 assert_eq!(
5136 types[&unresolved].semantic_type,
5137 crate::SemanticType::Unknown
5138 );
5139 assert_eq!(
5140 types[&unresolved].serialization,
5141 crate::SerializationCompatibility::NotSerializable
5142 );
5143 assert_eq!(
5144 types[&doubled].boundary_compatibility,
5145 crate::BoundaryCompatibility::Compatible
5146 );
5147 assert_eq!(
5148 types[&doubled].execution_boundary,
5149 crate::ExecutionBoundary::Client
5150 );
5151 assert_eq!(
5152 asm.semantic_type_of(&doubled),
5153 Some(&crate::SemanticType::Number)
5154 );
5155 assert_eq!(
5156 asm.serialization_compatibility_of(&chained),
5157 Some(crate::SerializationCompatibility::Serializable)
5158 );
5159 }
5160
5161 #[test]
5162 fn reports_stable_diagnostics_for_computed_semantics() {
5163 let parsed = presolve_parser::parse_file(
5164 "src/ComputedDiagnostics.tsx",
5165 r#"
5166@component("x-computed-diagnostics")
5167class ComputedDiagnostics extends Component {
5168 count = state(1);
5169
5170 @computed()
5171 invalidDeclaration() { return 1; }
5172
5173 @computed()
5174 get unsupportedBody() {
5175 const value = this.count;
5176 return value;
5177 }
5178
5179 @computed()
5180 get unresolvedRead() { return this.missing; }
5181
5182 @computed()
5183 get mismatch(): number { return "wrong"; }
5184
5185 @computed()
5186 get impure() { return helper(); }
5187
5188 @computed()
5189 get cycleA() { return this.cycleB; }
5190
5191 @computed()
5192 get cycleB() { return this.cycleA; }
5193
5194 render() { return <div />; }
5195}
5196"#,
5197 );
5198
5199 let asm = build_application_semantic_model(&parsed);
5200 let repeated = build_application_semantic_model(&parsed);
5201 assert_eq!(asm.diagnostics, repeated.diagnostics);
5202
5203 let component_graph = crate::build_component_graph(&parsed);
5204 let asm_from_component_graph =
5205 super::build_application_semantic_model_from_component_graph(&component_graph);
5206
5207 let codes = asm
5208 .diagnostics
5209 .iter()
5210 .map(|diagnostic| diagnostic.code.as_str())
5211 .collect::<std::collections::BTreeSet<_>>();
5212 assert_eq!(
5213 codes,
5214 std::collections::BTreeSet::from([
5215 "PSC1034", "PSC1035", "PSC1036", "PSC1037", "PSC1038", "PSC1039", "PSC1040",
5216 ])
5217 );
5218 assert_eq!(
5219 asm_from_component_graph
5220 .diagnostics
5221 .iter()
5222 .map(|diagnostic| diagnostic.code.as_str())
5223 .collect::<std::collections::BTreeSet<_>>(),
5224 codes
5225 );
5226 assert!(asm
5227 .diagnostics
5228 .iter()
5229 .all(|diagnostic| diagnostic.provenance.is_some()));
5230 assert!(asm.diagnostics.iter().any(|diagnostic| {
5231 diagnostic.code == crate::ComputedDiagnosticCode::InvalidDeclaration.as_str()
5232 && diagnostic.message
5233 == "@computed() declaration `invalidDeclaration` must decorate a getter"
5234 }));
5235 assert!(asm.diagnostics.iter().any(|diagnostic| {
5236 diagnostic.code == crate::ComputedDiagnosticCode::UnsupportedBody.as_str()
5237 && diagnostic.message == "computed getter `unsupportedBody` has an unsupported body"
5238 }));
5239 assert!(asm.diagnostics.iter().any(|diagnostic| {
5240 diagnostic.code == crate::ComputedDiagnosticCode::UnresolvedRead.as_str()
5241 && diagnostic.message
5242 == "computed getter `unresolvedRead` reads unresolved member `this.missing`"
5243 }));
5244 assert!(asm.diagnostics.iter().any(|diagnostic| {
5245 diagnostic.code == crate::ComputedDiagnosticCode::TypeMismatch.as_str()
5246 && diagnostic.message
5247 == "computed getter `mismatch` returns `\"wrong\"` but declares `number`"
5248 }));
5249 assert!(asm.diagnostics.iter().any(|diagnostic| {
5250 diagnostic.code == crate::ComputedDiagnosticCode::SerializationViolation.as_str()
5251 && diagnostic.message
5252 == "computed getter `unresolvedRead` has a non-serializable result"
5253 }));
5254 }
5255
5256 #[test]
5257 fn phase_e_audit_preserves_canonical_computed_products() {
5258 let path = "fixtures/0047-computed-diamond/input/ComputedDiamond.tsx";
5259 let parsed = presolve_parser::parse_file(
5260 path,
5261 include_str!("../../../fixtures/0047-computed-diamond/input/ComputedDiamond.tsx"),
5262 );
5263 let asm = build_application_semantic_model(&parsed);
5264 let component_graph = crate::build_component_graph(&parsed);
5265 let asm_from_component_graph =
5266 super::build_application_semantic_model_from_component_graph(&component_graph);
5267 let component = &asm.components[0];
5268 let count = component.id.state_field("count");
5269 let doubled = component.id.computed("doubled");
5270 let tripled = component.id.computed("tripled");
5271 let total = component.id.computed("total");
5272 let expected_owner = crate::SemanticOwner::entity(component.id.clone());
5273
5274 assert!(crate::validate_application_semantic_model(&asm).is_empty());
5275 assert!(crate::validate_application_semantic_model(&asm_from_component_graph).is_empty());
5276 for computed in [&doubled, &tripled, &total] {
5277 assert!(matches!(
5278 asm.entity(computed),
5279 Some(crate::SemanticEntity::Computed(_))
5280 ));
5281 assert_eq!(asm.owner(computed), Some(&expected_owner));
5282 assert_eq!(
5283 asm.semantic_types
5284 .computed_values
5285 .get(computed)
5286 .map(|computed_type| &computed_type.computed),
5287 Some(computed)
5288 );
5289 }
5290
5291 let reactive_references = asm
5292 .references
5293 .iter()
5294 .filter(|reference| {
5295 matches!(
5296 reference.kind,
5297 crate::SemanticReferenceKind::ComputedState
5298 | crate::SemanticReferenceKind::ComputedComputed
5299 )
5300 })
5301 .collect::<Vec<_>>();
5302 assert_eq!(reactive_references.len(), 4);
5303 for reference in reactive_references {
5304 assert!(asm.reactive_graph.edges.iter().any(|edge| {
5305 edge.kind == crate::IrReactiveEdgeKind::Reads
5306 && edge.source == reference.source.as_str()
5307 && edge.target == reference.target.as_str()
5308 && edge.provenance == reference.provenance
5309 }));
5310 assert!(asm.reactive_graph.edges.iter().any(|edge| {
5311 edge.kind == crate::IrReactiveEdgeKind::Invalidates
5312 && edge.source == reference.target.as_str()
5313 && edge.target == reference.source.as_str()
5314 && edge.provenance == reference.provenance
5315 }));
5316 }
5317 assert!(asm.reactive_graph.nodes.contains_key(count.as_str()));
5318 assert_eq!(
5319 asm.computed_evaluation_plan,
5320 crate::plan_computed_evaluation(&asm.reactive_graph)
5321 );
5322 assert_eq!(
5323 asm.computed_evaluation_plan.evaluation_order,
5324 vec![
5325 doubled.as_str().to_string(),
5326 tripled.as_str().to_string(),
5327 total.as_str().to_string(),
5328 ]
5329 );
5330
5331 let ir = crate::lower_components_to_ir(&asm);
5332 assert!(crate::validate_intermediate_representation(&ir).is_empty());
5333 let evaluations = ir
5334 .modules
5335 .iter()
5336 .flat_map(|module| &module.computed_evaluations)
5337 .map(|evaluation| evaluation.computed.clone())
5338 .collect::<Vec<_>>();
5339 assert_eq!(evaluations, vec![doubled, tripled, total]);
5340 }
5341
5342 #[test]
5343 fn classifies_computed_purity_and_reports_unsupported_behavior() {
5344 let mut parsed = presolve_parser::parse_file(
5345 "src/ComputedPurity.tsx",
5346 r#"
5347@component("x-computed-purity")
5348class ComputedPurity extends Component {
5349 count = state(1);
5350
5351 @action()
5352 save() {}
5353
5354 @effect()
5355 sync() {}
5356
5357 @computed()
5358 get pure() { return this.count; }
5359
5360 @computed()
5361 get mutates() { this.count++; return 1; }
5362
5363 @computed()
5364 get actionCall() { return this.save(); }
5365
5366 @computed()
5367 get effectCall() { console.log("effect"); return 1; }
5368
5369 @computed()
5370 get resourceCall() { return resource(); }
5371
5372 @computed()
5373 get arbitraryCall() { return helper(); }
5374
5375 @computed()
5376 get random() { return Math.random(); }
5377
5378 @computed()
5379 get asyncValue() { return 1; }
5380}
5381"#,
5382 );
5383 parsed.classes[0]
5384 .methods
5385 .iter_mut()
5386 .find(|method| method.name == "asyncValue")
5387 .expect("async test getter")
5388 .is_async = true;
5389
5390 let asm = build_application_semantic_model(&parsed);
5391 let component = &asm.components[0];
5392 let pure = component.id.computed("pure");
5393 let mutates = component.id.computed("mutates");
5394 let action_call = component.id.computed("actionCall");
5395 let effect_call = component.id.computed("effectCall");
5396 let resource_call = component.id.computed("resourceCall");
5397 let arbitrary_call = component.id.computed("arbitraryCall");
5398 let random = component.id.computed("random");
5399 let async_value = component.id.computed("asyncValue");
5400
5401 assert_eq!(
5402 asm.computed_value(&pure).expect("pure value").purity,
5403 crate::ComputedPurity::Pure
5404 );
5405 for (computed, kind) in [
5406 (mutates, crate::ComputedPurityViolationKind::StateMutation),
5407 (action_call, crate::ComputedPurityViolationKind::Action),
5408 (effect_call, crate::ComputedPurityViolationKind::Effect),
5409 (resource_call, crate::ComputedPurityViolationKind::Resource),
5410 (
5411 arbitrary_call,
5412 crate::ComputedPurityViolationKind::ArbitraryMethodCall,
5413 ),
5414 (
5415 random,
5416 crate::ComputedPurityViolationKind::NondeterministicOperation,
5417 ),
5418 (async_value, crate::ComputedPurityViolationKind::Async),
5419 ] {
5420 let value = asm.computed_value(&computed).expect("computed value");
5421 assert_eq!(value.purity, crate::ComputedPurity::Impure);
5422 assert!(value
5423 .purity_violations
5424 .iter()
5425 .any(|violation| violation.kind == kind));
5426 }
5427 let diagnostics = asm
5428 .diagnostics
5429 .iter()
5430 .filter(|diagnostic| diagnostic.code == "PSC1034")
5431 .collect::<Vec<_>>();
5432 assert_eq!(diagnostics.len(), 7);
5433 assert!(diagnostics
5434 .iter()
5435 .all(|diagnostic| diagnostic.provenance.is_some()));
5436 }
5437
5438 #[test]
5439 fn establishes_typed_action_input_and_output_contracts() {
5440 let parsed = presolve_parser::parse_file(
5441 "src/Action.tsx",
5442 r#"
5443type ActionResult = number;
5444
5445@component("x-action")
5446class Action extends Component {
5447 @action()
5448 async addTodo(input: string): ActionResult { return 1; }
5449}
5450"#,
5451 );
5452
5453 let asm = build_application_semantic_model(&parsed);
5454 let method = &asm.components[0].methods[0];
5455 let signature = asm
5456 .semantic_types
5457 .action_signatures
5458 .get(&method.id)
5459 .expect("action signature");
5460
5461 assert!(method.is_action());
5462 assert!(signature.is_async);
5463 assert_eq!(signature.input.len(), 1);
5464 assert_eq!(signature.input[0].1, crate::SemanticType::String);
5465 assert_eq!(signature.output, Some(crate::SemanticType::Number));
5466 }
5467
5468 #[test]
5469 fn lowers_supported_literal_state_annotations_into_canonical_types() {
5470 let parsed = presolve_parser::parse_file(
5471 "src/LiteralTypes.tsx",
5472 r#"
5473@component("x-literal-types")
5474class LiteralTypes extends Component {
5475 filter: "all" = state("all");
5476 step: 42 = state(42);
5477 enabled: true = state(true);
5478}
5479"#,
5480 );
5481
5482 let asm = build_application_semantic_model(&parsed);
5483 let types = asm.components[0]
5484 .state_fields
5485 .iter()
5486 .map(|field| {
5487 (
5488 field.name.as_str(),
5489 &asm.semantic_types.assignments[&field.id].semantic_type,
5490 )
5491 })
5492 .collect::<Vec<_>>();
5493
5494 assert_eq!(
5495 types,
5496 vec![
5497 (
5498 "filter",
5499 &crate::SemanticType::StringLiteral("all".to_string())
5500 ),
5501 (
5502 "step",
5503 &crate::SemanticType::NumberLiteral("42".to_string())
5504 ),
5505 ("enabled", &crate::SemanticType::BooleanLiteral(true)),
5506 ]
5507 );
5508 }
5509
5510 #[test]
5511 fn lowers_array_and_tuple_state_annotations_into_canonical_types() {
5512 let parsed = presolve_parser::parse_file(
5513 "src/CollectionTypes.tsx",
5514 r#"
5515@component("x-collection-types")
5516class CollectionTypes extends Component {
5517 names: string[] = state([]);
5518 todos: Todo[] = state([]);
5519 pair: [string, number] = state(["Presolve", 1]);
5520}
5521"#,
5522 );
5523
5524 let asm = build_application_semantic_model(&parsed);
5525 let types = asm.components[0]
5526 .state_fields
5527 .iter()
5528 .map(|field| {
5529 (
5530 field.name.as_str(),
5531 &asm.semantic_types.assignments[&field.id].semantic_type,
5532 )
5533 })
5534 .collect::<Vec<_>>();
5535
5536 assert_eq!(
5537 types,
5538 vec![
5539 (
5540 "names",
5541 &crate::SemanticType::Array(Box::new(crate::SemanticType::String)),
5542 ),
5543 (
5544 "todos",
5545 &crate::SemanticType::Array(Box::new(crate::SemanticType::Unknown)),
5546 ),
5547 (
5548 "pair",
5549 &crate::SemanticType::Tuple(vec![
5550 crate::SemanticType::String,
5551 crate::SemanticType::Number,
5552 ]),
5553 ),
5554 ]
5555 );
5556 }
5557
5558 #[test]
5559 fn lowers_structural_object_state_annotations_into_canonical_types() {
5560 let parsed = presolve_parser::parse_file(
5561 "src/ObjectTypes.tsx",
5562 r#"
5563@component("x-object-types")
5564class ObjectTypes extends Component {
5565 todo: { id: string; title: string; completed: boolean } = state({ id: "1", title: "Presolve", completed: false });
5566}
5567"#,
5568 );
5569
5570 let asm = build_application_semantic_model(&parsed);
5571 let field = &asm.components[0].state_fields[0];
5572 let assignment = &asm.semantic_types.assignments[&field.id];
5573 let crate::SemanticType::Object(object) = &assignment.semantic_type else {
5574 panic!("expected structural object type");
5575 };
5576
5577 assert_eq!(
5578 object.properties,
5579 std::collections::BTreeMap::from([
5580 ("completed".to_string(), crate::SemanticType::Boolean),
5581 ("id".to_string(), crate::SemanticType::String),
5582 ("title".to_string(), crate::SemanticType::String),
5583 ])
5584 );
5585 }
5586
5587 #[test]
5588 fn lowers_union_and_nullable_state_annotations_into_canonical_types() {
5589 let parsed = presolve_parser::parse_file(
5590 "src/UnionTypes.tsx",
5591 r#"
5592@component("x-union-types")
5593class UnionTypes extends Component {
5594 filter: "all" | "active" | "completed" = state("all");
5595 user: { id: string } | null = state(null);
5596}
5597"#,
5598 );
5599
5600 let asm = build_application_semantic_model(&parsed);
5601 let fields = &asm.components[0].state_fields;
5602 assert_eq!(
5603 asm.semantic_types.assignments[&fields[0].id].semantic_type,
5604 crate::SemanticType::Union(vec![
5605 crate::SemanticType::StringLiteral("active".to_string()),
5606 crate::SemanticType::StringLiteral("all".to_string()),
5607 crate::SemanticType::StringLiteral("completed".to_string()),
5608 ])
5609 );
5610 assert_eq!(
5611 asm.semantic_types.assignments[&fields[1].id].semantic_type,
5612 crate::SemanticType::Union(vec![
5613 crate::SemanticType::Null,
5614 crate::SemanticType::Object(crate::ObjectType {
5615 properties: std::collections::BTreeMap::from([(
5616 "id".to_string(),
5617 crate::SemanticType::String,
5618 )]),
5619 }),
5620 ])
5621 );
5622 }
5623
5624 #[test]
5625 fn resolves_local_type_aliases_with_canonical_alias_identity() {
5626 let parsed = presolve_parser::parse_file(
5627 "src/Aliases.tsx",
5628 r#"
5629type TodoId = string;
5630type Filter = "all" | "active" | "completed";
5631
5632@component("x-aliases")
5633class Aliases extends Component {
5634 id: TodoId = state("todo-1");
5635 filter: Filter = state("all");
5636}
5637"#,
5638 );
5639
5640 let asm = build_application_semantic_model(&parsed);
5641 let fields = &asm.components[0].state_fields;
5642 let todo_id = asm
5643 .semantic_types
5644 .aliases
5645 .values()
5646 .find(|alias| alias.name == "TodoId")
5647 .expect("TodoId alias");
5648 let filter = asm
5649 .semantic_types
5650 .aliases
5651 .values()
5652 .find(|alias| alias.name == "Filter")
5653 .expect("Filter alias");
5654
5655 assert_eq!(todo_id.semantic_type, crate::SemanticType::String);
5656 assert_eq!(
5657 filter.semantic_type,
5658 crate::SemanticType::Union(vec![
5659 crate::SemanticType::StringLiteral("active".to_string()),
5660 crate::SemanticType::StringLiteral("all".to_string()),
5661 crate::SemanticType::StringLiteral("completed".to_string()),
5662 ])
5663 );
5664 assert_eq!(
5665 asm.semantic_types.assignments[&fields[0].id].origin,
5666 todo_id.id
5667 );
5668 assert_eq!(
5669 asm.semantic_types.assignments[&fields[1].id].origin,
5670 filter.id
5671 );
5672 }
5673
5674 #[test]
5675 fn resolves_imported_type_aliases_through_named_reexports() {
5676 let unit = CompilationUnit::parse_sources([
5677 (
5678 "src/types.ts",
5679 r#"export type Filter = "all" | "active" | "completed";"#,
5680 ),
5681 ("src/index.ts", r#"export { Filter } from "./types";"#),
5682 (
5683 "src/App.tsx",
5684 r#"
5685import { Filter } from "./index";
5686
5687@component("x-app")
5688class App extends Component {
5689 filter: Filter = state("all");
5690}
5691"#,
5692 ),
5693 ]);
5694
5695 let asm = build_application_semantic_model_for_unit(&unit);
5696 let field = &asm
5697 .components
5698 .iter()
5699 .find(|component| component.class_name == "App")
5700 .expect("App component")
5701 .state_fields[0];
5702 let assignment = &asm.semantic_types.assignments[&field.id];
5703
5704 assert_eq!(
5705 assignment.semantic_type,
5706 crate::SemanticType::Union(vec![
5707 crate::SemanticType::StringLiteral("active".to_string()),
5708 crate::SemanticType::StringLiteral("all".to_string()),
5709 crate::SemanticType::StringLiteral("completed".to_string()),
5710 ])
5711 );
5712 assert_eq!(
5713 assignment.origin,
5714 crate::SemanticId::type_alias_in_module("src/types.ts", "Filter")
5715 );
5716 }
5717
5718 #[test]
5719 fn infers_state_types_from_direct_serializable_initializers() {
5720 let parsed = presolve_parser::parse_file(
5721 "src/InferredState.tsx",
5722 r#"
5723@component("x-inferred-state")
5724class InferredState extends Component {
5725 count = state(0);
5726 todos = state([]);
5727 tags = state(["Presolve"]);
5728 todo = state({ id: "1", completed: false });
5729}
5730"#,
5731 );
5732
5733 let asm = build_application_semantic_model(&parsed);
5734 let fields = &asm.components[0].state_fields;
5735 let types = fields
5736 .iter()
5737 .map(|field| {
5738 let assignment = &asm.semantic_types.assignments[&field.id];
5739 assert_eq!(assignment.status, crate::SemanticTypeStatus::Inferred);
5740 (field.name.as_str(), assignment.semantic_type.clone())
5741 })
5742 .collect::<Vec<_>>();
5743
5744 assert_eq!(
5745 types,
5746 vec![
5747 ("count", crate::SemanticType::Number),
5748 (
5749 "todos",
5750 crate::SemanticType::Array(Box::new(crate::SemanticType::Unknown)),
5751 ),
5752 (
5753 "tags",
5754 crate::SemanticType::Array(Box::new(crate::SemanticType::String)),
5755 ),
5756 (
5757 "todo",
5758 crate::SemanticType::Object(crate::ObjectType {
5759 properties: std::collections::BTreeMap::from([
5760 ("completed".to_string(), crate::SemanticType::Boolean),
5761 ("id".to_string(), crate::SemanticType::String),
5762 ]),
5763 }),
5764 ),
5765 ]
5766 );
5767 }
5768
5769 #[test]
5770 fn propagates_canonical_types_to_expression_graph_nodes() {
5771 let parsed = presolve_parser::parse_file(
5772 "src/ExpressionTypes.tsx",
5773 r#"
5774@component("x-expression-types")
5775class ExpressionTypes extends Component {
5776 total = state((1 + 2) * 3);
5777 ready = state(1 < 2);
5778}
5779"#,
5780 );
5781
5782 let asm = build_application_semantic_model(&parsed);
5783 let total = &asm.components[0].state_fields[0];
5784 let ready = &asm.components[0].state_fields[1];
5785 let total_root = asm
5786 .expression_root(&total.id)
5787 .expect("total expression root");
5788 let ready_root = asm
5789 .expression_root(&ready.id)
5790 .expect("ready expression root");
5791
5792 assert_eq!(
5793 asm.semantic_types.assignments[total_root].semantic_type,
5794 crate::SemanticType::Number
5795 );
5796 assert_eq!(
5797 asm.semantic_types.assignments[ready_root].semantic_type,
5798 crate::SemanticType::Boolean
5799 );
5800 assert!(asm
5801 .expression_dependencies(total_root)
5802 .iter()
5803 .all(|id| asm.semantic_types.assignments.contains_key(*id)));
5804 }
5805
5806 #[test]
5807 fn derives_component_ownership_without_legacy_owner_fields() {
5808 let parsed = presolve_parser::parse_file(
5809 "src/Counter.tsx",
5810 r#"
5811@component("x-counter")
5812class Counter extends Component {
5813 count = state(0);
5814
5815 increment() {
5816 this.count++;
5817 }
5818
5819 render() {
5820 return <button onClick={() => this.increment()}>{this.count}</button>;
5821 }
5822}
5823"#,
5824 );
5825 let mut component_graph = build_component_graph_for_module(&parsed);
5826 let mut templates = build_template_graph(&component_graph).templates;
5827 let template_entities = build_template_semantic_entities(&templates);
5828 let component = component_graph
5829 .components
5830 .first_mut()
5831 .expect("component graph should contain Counter");
5832 let component_id = component.id.clone();
5833 let method_id = component.methods[0].id.clone();
5834 let action_id = component.actions[0].id.clone();
5835 let state_id = component.state_fields[0].id.clone();
5836 let event_id = component
5837 .render
5838 .as_ref()
5839 .expect("Counter should render")
5840 .event_handlers[0]
5841 .id
5842 .clone();
5843
5844 component.owner = SemanticOwner::entity(component_id.clone());
5845 component.state_fields[0].owner = SemanticOwner::Application;
5846 component.methods[0].owner = SemanticOwner::Application;
5847 component.actions[0].owner = SemanticOwner::Application;
5848 component
5849 .render
5850 .as_mut()
5851 .expect("Counter should render")
5852 .event_handlers[0]
5853 .owner = SemanticOwner::Application;
5854 templates[0].owner = SemanticOwner::Application;
5855
5856 let ownership = collect_ownership(
5857 &component_graph.components,
5858 &std::collections::BTreeMap::new(),
5859 &std::collections::BTreeMap::new(),
5860 &std::collections::BTreeMap::new(),
5861 &std::collections::BTreeMap::new(),
5862 &std::collections::BTreeMap::new(),
5863 &std::collections::BTreeMap::new(),
5864 &std::collections::BTreeMap::new(),
5865 &std::collections::BTreeMap::new(),
5866 &std::collections::BTreeMap::new(),
5867 &std::collections::BTreeMap::new(),
5868 &ComponentInstancePlan::default(),
5869 &std::collections::BTreeMap::new(),
5870 &std::collections::BTreeMap::new(),
5871 &templates,
5872 &template_entities,
5873 );
5874 assert_eq!(ownership[&component_id], SemanticOwner::Application);
5875 assert_eq!(
5876 ownership[&state_id],
5877 SemanticOwner::entity(component_id.clone())
5878 );
5879 assert_eq!(
5880 ownership[&method_id],
5881 SemanticOwner::entity(component_id.clone())
5882 );
5883 assert_eq!(ownership[&action_id], SemanticOwner::entity(method_id));
5884 assert_eq!(
5885 ownership[&event_id],
5886 SemanticOwner::entity(component_id.clone().template())
5887 );
5888 assert_eq!(
5889 ownership[&templates[0].id],
5890 SemanticOwner::entity(component_id)
5891 );
5892 }
5893
5894 #[test]
5895 #[allow(clippy::too_many_lines)]
5896 fn traverses_application_ownership_in_semantic_id_order() {
5897 let parsed = presolve_parser::parse_file(
5898 "src/Counter.tsx",
5899 r#"
5900@component("x-counter")
5901class Counter extends Component {
5902 count = state(0);
5903
5904 increment() {
5905 this.count++;
5906 }
5907
5908 render() {
5909 return <button onClick={this.increment}>{this.count}</button>;
5910 }
5911}
5912"#,
5913 );
5914
5915 let asm = build_application_semantic_model(&parsed);
5916 let component = &asm.components[0];
5917 let roots = asm.application_roots();
5918
5919 assert_eq!(
5920 roots,
5921 vec![
5922 &component.id,
5923 asm.component_instance_plan
5924 .instances
5925 .values()
5926 .find(|instance| instance.depth == 0)
5927 .expect("build root instance")
5928 .id
5929 .as_semantic_id(),
5930 ]
5931 );
5932 assert_eq!(
5933 asm.children_of(&component.id),
5934 vec![
5935 &component.methods[0].id,
5936 &component.methods[1].id,
5937 &component.state_fields[0].id,
5938 &asm.templates[0].id,
5939 ]
5940 );
5941 assert_eq!(
5942 asm.children_of(&component.methods[0].id),
5943 vec![&component.actions[0].id]
5944 );
5945 assert_eq!(asm.parent_of(&component.id), None);
5946 assert_eq!(
5947 asm.parent_of(&component.actions[0].id),
5948 Some(&component.methods[0].id)
5949 );
5950 assert_eq!(
5951 asm.ancestors_of(&component.actions[0].id),
5952 vec![&component.methods[0].id, &component.id]
5953 );
5954 let descendants = asm.descendants_of(&component.id);
5955 assert_eq!(descendants[0], &component.methods[0].id);
5956 assert_eq!(descendants[1], &component.actions[0].id);
5957 assert_eq!(descendants[2], &component.methods[1].id);
5958 assert_eq!(
5959 descendants.len(),
5960 asm.ownership.len() - asm.application_roots().len()
5961 );
5962 assert_eq!(
5963 asm.entities_of_kind(SemanticEntityKind::Method),
5964 vec![&component.methods[0].id, &component.methods[1].id]
5965 );
5966 assert_eq!(
5967 asm.entities_of_kind(SemanticEntityKind::Action),
5968 vec![&component.actions[0].id]
5969 );
5970 assert_eq!(
5971 asm.entities_of_kind(SemanticEntityKind::StateField),
5972 vec![&component.state_fields[0].id]
5973 );
5974 let state_id = &component.state_fields[0].id;
5975 let state_provenance = asm.provenance(state_id).expect("state provenance");
5976 assert_eq!(
5977 asm.entities_in_file(state_provenance.path.as_path()).len(),
5978 asm.ownership.len()
5979 );
5980 let at_state =
5981 asm.entities_at(state_provenance.path.as_path(), state_provenance.span.start);
5982 assert!(at_state.contains(&state_id));
5983 assert!(at_state.iter().all(|id| {
5984 let provenance = asm.provenance(id).expect("entity provenance");
5985 provenance.span.start <= state_provenance.span.start
5986 && state_provenance.span.start < provenance.span.end
5987 }));
5988 let action_references = asm.references_of_kind(SemanticReferenceKind::ActionState);
5989 assert_eq!(action_references.len(), 1);
5990 assert_eq!(action_references[0].source, component.actions[0].id);
5991 assert_eq!(action_references[0].target, component.state_fields[0].id);
5992 let action_provenance = &action_references[0].provenance;
5993 assert_eq!(
5994 asm.references_in_file(action_provenance.path.as_path())
5995 .len(),
5996 asm.references.len()
5997 );
5998 let at_action = asm.references_at(
5999 action_provenance.path.as_path(),
6000 action_provenance.span.start,
6001 );
6002 assert!(at_action.iter().any(|reference| {
6003 reference.source == component.actions[0].id
6004 && reference.target == component.state_fields[0].id
6005 }));
6006 }
6007
6008 #[test]
6009 fn resolves_template_state_dependencies() {
6010 let parsed = presolve_parser::parse_file(
6011 "src/Panel.tsx",
6012 r#"
6013@component("x-panel")
6014class Panel extends Component {
6015 enabled = state(true);
6016
6017 render() {
6018 return <section hidden={this.enabled}>{this.enabled}{this.enabled ? <span>On</span> : <span>Off</span>}</section>;
6019 }
6020}
6021"#,
6022 );
6023
6024 let asm = build_application_semantic_model(&parsed);
6025 let component = &asm.components[0];
6026 let state = &component.state_fields[0];
6027 let state_references = asm.references_to(&state.id);
6028
6029 assert_eq!(state_references.len(), 3);
6030 assert!(state_references.iter().all(|reference| {
6031 reference.kind == SemanticReferenceKind::TemplateState
6032 && reference.target == state.id
6033 && reference.provenance.path.as_path() == std::path::Path::new("src/Panel.tsx")
6034 }));
6035 assert!(state_references.iter().any(|reference| {
6036 asm.template_entity(&reference.source)
6037 .is_some_and(|entity| entity.kind == TemplateSemanticKind::Binding)
6038 }));
6039 assert!(state_references.iter().any(|reference| {
6040 asm.template_entity(&reference.source)
6041 .is_some_and(|entity| entity.kind == TemplateSemanticKind::AttributeBinding)
6042 }));
6043 assert!(state_references.iter().any(|reference| {
6044 asm.template_entity(&reference.source)
6045 .is_some_and(|entity| entity.kind == TemplateSemanticKind::Conditional)
6046 }));
6047 }
6048
6049 #[test]
6050 fn resolves_template_bindings_to_canonical_computed_entities() {
6051 let parsed = presolve_parser::parse_file(
6052 "src/ComputedTemplate.tsx",
6053 r#"
6054@component("x-computed-template")
6055class ComputedTemplate extends Component {
6056 @computed()
6057 get label(): string { return "Ready"; }
6058
6059 @computed()
6060 get visible(): boolean { return true; }
6061
6062 render() {
6063 return <section title={this.label}>{this.label}{this.visible ? <span>Visible</span> : <span>Hidden</span>}</section>;
6064 }
6065}
6066"#,
6067 );
6068
6069 let asm = build_application_semantic_model(&parsed);
6070 let component = &asm.components[0];
6071 let label = component.id.computed("label");
6072 let visible = component.id.computed("visible");
6073 let references = asm.references_of_kind(SemanticReferenceKind::TemplateComputed);
6074
6075 assert_eq!(references.len(), 3);
6076 assert_eq!(
6077 references
6078 .iter()
6079 .filter(|reference| reference.target == label)
6080 .count(),
6081 2
6082 );
6083 assert!(references.iter().any(|reference| {
6084 reference.target == visible
6085 && asm
6086 .template_entity(&reference.source)
6087 .is_some_and(|entity| entity.kind == TemplateSemanticKind::Conditional)
6088 }));
6089 assert!(references.iter().all(|reference| {
6090 asm.provenance(&reference.source) == Some(&reference.provenance)
6091 && asm.semantic_type_of(&reference.source)
6092 == asm.semantic_type_of(&reference.target)
6093 }));
6094 assert_eq!(
6095 asm.references_of_kind(SemanticReferenceKind::TemplateState)
6096 .into_iter()
6097 .filter(|reference| reference.target == label || reference.target == visible)
6098 .count(),
6099 0
6100 );
6101
6102 let graph = crate::build_semantic_graph(&asm);
6103 assert_eq!(
6104 graph
6105 .edges
6106 .iter()
6107 .filter(|edge| edge.kind == crate::SemanticGraphEdgeKind::TemplateComputed)
6108 .count(),
6109 3
6110 );
6111 }
6112
6113 #[test]
6114 fn navigates_template_entities_from_canonical_ownership() {
6115 let parsed = presolve_parser::parse_file(
6116 "src/Panel.tsx",
6117 r#"
6118@component("x-panel")
6119class Panel extends Component {
6120 count = state(0);
6121
6122 render() {
6123 return <section>{this.count}</section>;
6124 }
6125}
6126"#,
6127 );
6128 let mut asm = build_application_semantic_model(&parsed);
6129 let template_id = asm.templates[0].id.clone();
6130 let expected = asm.template_entities_for(&template_id).len();
6131
6132 for entity in &mut asm.template_entities {
6133 entity.owner = SemanticOwner::Application;
6134 }
6135
6136 assert_eq!(asm.template_entities_for(&template_id).len(), expected);
6137 }
6138
6139 #[test]
6140 fn leaves_member_expressions_unresolved_without_expression_evaluation() {
6141 let parsed = presolve_parser::parse_file(
6142 "src/Panel.tsx",
6143 r#"
6144@component("x-panel")
6145class Panel extends Component {
6146 enabled = state(true);
6147
6148 render() {
6149 return <section data-value={this.enabled.value}>Panel</section>;
6150 }
6151}
6152"#,
6153 );
6154
6155 let asm = build_application_semantic_model(&parsed);
6156 assert_eq!(
6157 asm.references
6158 .iter()
6159 .filter(|reference| reference.kind == SemanticReferenceKind::TemplateState)
6160 .count(),
6161 0
6162 );
6163 }
6164
6165 #[test]
6166 fn resolves_keyed_list_iterable_to_component_state() {
6167 let parsed = presolve_parser::parse_file(
6168 "src/KeyedList.tsx",
6169 r#"
6170@component("x-keyed-list")
6171class KeyedList extends Component {
6172 items = state([]);
6173
6174 render() {
6175 return <ul>{this.items.map((item) => <li key={item}>{item}</li>)}</ul>;
6176 }
6177}
6178"#,
6179 );
6180
6181 let asm = build_application_semantic_model(&parsed);
6182 let component = &asm.components[0];
6183 let state = &component.state_fields[0];
6184 let reference = asm
6185 .references_to(&state.id)
6186 .into_iter()
6187 .find(|reference| reference.kind == SemanticReferenceKind::TemplateState)
6188 .expect("list iterable state reference");
6189 let list = asm
6190 .template_entity(&reference.source)
6191 .expect("list semantic entity");
6192
6193 assert_eq!(list.kind, TemplateSemanticKind::List);
6194 assert_eq!(list.expression.as_deref(), Some("this.items"));
6195 assert_eq!(reference.provenance, list.provenance);
6196 }
6197
6198 #[test]
6199 fn resolves_template_event_attribute_to_component_method() {
6200 let parsed = presolve_parser::parse_file(
6201 "src/Counter.tsx",
6202 r#"
6203@component("x-counter")
6204class Counter extends Component {
6205 count = state(0);
6206
6207 increment() {
6208 this.count += 1;
6209 }
6210
6211 render() {
6212 return <button onClick={() => this.increment()}>Count</button>;
6213 }
6214}
6215"#,
6216 );
6217
6218 let asm = build_application_semantic_model(&parsed);
6219 let component = &asm.components[0];
6220 let method = component
6221 .methods
6222 .iter()
6223 .find(|method| method.name == "increment")
6224 .expect("increment method");
6225 let reference = asm
6226 .references_to(&method.id)
6227 .into_iter()
6228 .find(|reference| {
6229 reference.kind == SemanticReferenceKind::EventMethod
6230 && asm
6231 .template_entity(&reference.source)
6232 .is_some_and(|entity| entity.kind == TemplateSemanticKind::EventAttribute)
6233 })
6234 .expect("template event method reference");
6235
6236 assert_eq!(
6237 reference.provenance.path,
6238 std::path::Path::new("src/Counter.tsx")
6239 );
6240 assert_eq!(reference.provenance.span.line, 11);
6241 }
6242
6243 #[test]
6244 fn resolves_render_template_bindings_to_unique_method_locals() {
6245 let parsed = presolve_parser::parse_file(
6246 "src/LocalResolution.tsx",
6247 r#"
6248@component("x-local-resolution")
6249class LocalResolution extends Component {
6250 render() {
6251 const title = "Presolve";
6252 return <output title={title}>{title}</output>;
6253 }
6254}
6255"#,
6256 );
6257
6258 let asm = build_application_semantic_model(&parsed);
6259 let component = &asm.components[0];
6260 let render = component
6261 .methods
6262 .iter()
6263 .find(|method| method.name == "render")
6264 .expect("render method");
6265 let local = &render.local_variables[0];
6266 let references = asm.references_of_kind(SemanticReferenceKind::TemplateLocal);
6267 let assignment = asm
6268 .semantic_types
6269 .assignments
6270 .get(&local.id)
6271 .expect("canonical inferred local type");
6272
6273 assert_eq!(
6274 asm.owner(&local.id),
6275 Some(&SemanticOwner::entity(render.id.clone()))
6276 );
6277 assert_eq!(
6278 asm.entities_of_kind(SemanticEntityKind::LocalVariable),
6279 vec![&local.id]
6280 );
6281 assert_eq!(references.len(), 2);
6282 assert!(references.iter().all(|reference| {
6283 reference.target == local.id
6284 && reference.provenance.path == std::path::Path::new("src/LocalResolution.tsx")
6285 }));
6286 assert_eq!(assignment.semantic_type, crate::SemanticType::String);
6287 assert_eq!(assignment.status, crate::SemanticTypeStatus::Inferred);
6288 assert_eq!(assignment.provenance.span, local.span);
6289
6290 let semantic_graph = crate::build_semantic_graph(&asm);
6291 assert!(semantic_graph.nodes.iter().any(|node| {
6292 node.kind == crate::SemanticGraphNodeKind::LocalVariable && node.id == local.id
6293 }));
6294 assert_eq!(
6295 semantic_graph
6296 .edges
6297 .iter()
6298 .filter(|edge| {
6299 edge.kind == crate::SemanticGraphEdgeKind::TemplateLocal
6300 && edge.target == local.id
6301 })
6302 .count(),
6303 2
6304 );
6305
6306 let template_graph = build_template_graph(&build_component_graph_for_module(&parsed));
6307 assert_eq!(
6308 crate::generate_static_html(&template_graph),
6309 "<output data-presolve-node=\"n0\" title=\"Presolve\" data-presolve-bindings=\"title\"><!-- presolve-binding:n2:title -->Presolve</output>\n"
6310 );
6311 }
6312
6313 #[test]
6314 fn queries_canonical_expression_graphs_by_dependency_provenance_and_owner() {
6315 let parsed = presolve_parser::parse_file(
6316 "src/ExpressionQueries.tsx",
6317 r#"
6318@component("x-expression-queries")
6319class ExpressionQueries extends Component {
6320 total = state((1 + 2) * 3);
6321}
6322"#,
6323 );
6324
6325 let asm = build_application_semantic_model(&parsed);
6326 let field = &asm.components[0].state_fields[0];
6327 let root = asm.expression_root(&field.id).expect("expression root");
6328 let left = field.id.expression("root.0");
6329 let right = field.id.expression("root.1");
6330
6331 assert_eq!(asm.expression(root).map(|node| &node.id), Some(root));
6332 assert_eq!(asm.expression_owner(root), Some(&field.id));
6333 assert_eq!(asm.expression_dependencies(root), vec![&left, &right]);
6334 assert_eq!(
6335 asm.expression_dependents(&left)
6336 .into_iter()
6337 .map(|node| &node.id)
6338 .collect::<Vec<_>>(),
6339 vec![root]
6340 );
6341 assert_eq!(asm.expressions_for(&field.id).len(), 5);
6342
6343 let provenance = asm
6344 .expression_provenance(root)
6345 .expect("expression provenance");
6346 assert_eq!(
6347 provenance.path,
6348 std::path::Path::new("src/ExpressionQueries.tsx")
6349 );
6350 assert_eq!(asm.expressions_in_file(&provenance.path).len(), 5);
6351 assert_eq!(
6352 asm.expressions_at(&provenance.path, provenance.span.start)
6353 .into_iter()
6354 .map(|node| &node.id)
6355 .collect::<Vec<_>>(),
6356 vec![root]
6357 );
6358 }
6359
6360 #[test]
6361 fn file_route_model_composes_layout_default_slots_into_canonical_instances() {
6362 let unit = CompilationUnit::parse_sources([
6363 (
6364 "app/layout.tsx",
6365 r#"
6366@component()
6367class AppLayout extends Component {
6368 @slot() children!: SlotContent;
6369 render() { return <main><slot /></main>; }
6370}
6371"#,
6372 ),
6373 (
6374 "app/routes/index.tsx",
6375 r#"
6376@component()
6377class Home extends Component {
6378 render() { return <article>Home</article>; }
6379}
6380"#,
6381 ),
6382 ]);
6383 let asm = build_file_route_application_semantic_model_for_unit_with_packages(
6384 &unit,
6385 &crate::SemanticPackageResolutionTable::default(),
6386 )
6387 .expect("valid conventional layout");
6388
6389 assert_eq!(asm.component_instance_plan.roots.len(), 1);
6390 let binding = asm
6391 .slot_bindings
6392 .bindings
6393 .values()
6394 .find(|binding| binding.direct_child_instance.is_some())
6395 .expect("compiler-issued layout Slot binding");
6396 assert_eq!(binding.status, crate::SlotBindingStatus::Bound);
6397 assert!(binding.content_fragment.is_none());
6398 assert!(crate::validate_application_semantic_model(&asm).is_empty());
6399
6400 let html = crate::generate_ordinary_instance_html(&asm);
6401 assert!(html.contains("<main"));
6402 assert!(html.contains("<article"));
6403 assert!(html.contains("Home"));
6404 }
6405
6406 #[test]
6407 fn retains_route_loader_source_facts_before_server_capability_resolution() {
6408 let asm = build_application_semantic_model(&presolve_parser::parse_file(
6409 "app/routes/posts/[slug].tsx",
6410 r#"
6411@component()
6412class Post {
6413 @loader("loadPost") post!: Resource<Post, NotFound>;
6414 render() { return <article />; }
6415}
6416"#,
6417 ));
6418
6419 let loader = &asm.components[0].route_loader_declaration_candidates[0];
6420 assert_eq!(loader.field, "post");
6421 assert!(loader.decorator_invoked);
6422 assert_eq!(loader.decorator_argument_count, 1);
6423 assert_eq!(loader.endpoint_designator.as_deref(), Some("loadPost"));
6424 assert_eq!(
6425 loader
6426 .declared_type
6427 .as_ref()
6428 .map(|type_| type_.text.as_str()),
6429 Some("Resource<Post, NotFound>")
6430 );
6431 assert!(asm
6432 .diagnostics
6433 .iter()
6434 .any(|diagnostic| diagnostic.code == "PSC1132"));
6435 }
6436
6437 #[test]
6438 fn retains_server_action_source_facts_before_route_capability_resolution() {
6439 let asm = build_application_semantic_model(&presolve_parser::parse_file(
6440 "app/routes/posts/[slug].tsx",
6441 r#"
6442@component()
6443class Post {
6444 @serverAction("savePost") save(): void {}
6445 render() { return <form />; }
6446}
6447"#,
6448 ));
6449
6450 let action = &asm.components[0].server_action_facts[0];
6451 assert_eq!(action.method_name, "save");
6452 assert!(action.invoked);
6453 assert_eq!(action.argument_count, 1);
6454 assert_eq!(action.endpoint_designator.as_deref(), Some("savePost"));
6455 assert!(!action.is_action);
6456 assert!(!action.action_invoked);
6457 assert!(!action.has_body_effects);
6458 assert!(asm
6459 .diagnostics
6460 .iter()
6461 .any(|diagnostic| diagnostic.code == "PSC1133"));
6462 }
6463}