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