1use std::collections::{BTreeMap, BTreeSet};
2use std::path::{Path, PathBuf};
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 None,
1653 None,
1654 );
1655 let serialization = collect_serialization_products(
1656 &component_graph.components,
1657 &forms,
1658 &form_fields,
1659 &submissions.plans,
1660 );
1661 let reset = collect_reset_products(&forms, &form_fields, &form_field_bindings, &form_tracking);
1662 let form_ir = lower_form_ir(&component_instance_plan, &forms, &form_fields);
1663 let optimized_form_ir = optimize_form_ir(&form_ir);
1664 let submission_host_products = collect_submission_host_products(
1665 &templates,
1666 &component_graph.components,
1667 &forms,
1668 &form_field_bindings,
1669 &submissions.plans,
1670 &serialization.plans,
1671 &optimized_form_ir.optimized,
1672 );
1673 let runtime_forms = build_runtime_form_registry(
1674 &forms,
1675 &form_fields,
1676 &form_field_bindings,
1677 &validation_rules,
1678 &optimized_form_ir.optimized,
1679 &submissions,
1680 &serialization,
1681 &reset,
1682 &submission_host_products.hosts,
1683 );
1684 let effect_execution_plan = plan_effect_execution(
1685 &computed_values,
1686 &effects,
1687 &effect_reactive_analysis,
1688 &effect_trigger_plan,
1689 &reactive_transitive_analysis,
1690 &computed_evaluation_plan,
1691 );
1692 let mut diagnostics = component_graph.diagnostics.clone();
1693 diagnostics.extend(computed_diagnostics);
1694 extend_computed_diagnostics(
1695 &mut diagnostics,
1696 &component_graph.components,
1697 &computed_values,
1698 &resource_declarations,
1699 &expression_graph,
1700 &semantic_types,
1701 &reactive_cycle_analysis,
1702 &provenance,
1703 );
1704 let component_ids = component_graph
1705 .components
1706 .iter()
1707 .map(|component| component.id.clone())
1708 .collect::<BTreeSet<_>>();
1709 let composition_types = collect_composition_type_products(
1710 &component_ids,
1711 &component_invocations,
1712 &slot_bindings,
1713 &slots,
1714 &slot_composition.fragments,
1715 &slot_composition.outlets,
1716 &instance_context,
1717 &context_type_products.bindings,
1718 &context_type_products.contexts,
1719 &context_type_products.providers,
1720 &context_lifetime,
1721 &ownership,
1722 &references,
1723 );
1724 let component_initialization = plan_component_initialization(
1725 &component_instance_plan,
1726 &slot_bindings,
1727 &composition_types,
1728 &instance_context,
1729 );
1730 let mut model = ApplicationSemanticModel {
1731 expression_graph,
1732 semantic_types,
1733 components: components_with_resolved_form_field_candidates(
1734 &component_graph.components,
1735 &form_field_declaration_candidates,
1736 ),
1737 contexts,
1738 providers,
1739 consumers,
1740 forms,
1741 resource_endpoint_resolutions,
1742 opaque_action_resolutions,
1743 resource_declarations,
1744 resource_activations,
1745 form_field_declaration_candidates,
1746 form_fields,
1747 form_field_binding_candidates,
1748 form_field_bindings,
1749 form_ownership,
1750 validation_rule_candidates,
1751 validation_rules,
1752 validation_graph,
1753 validation_dependency_plans,
1754 form_tracking,
1755 submissions,
1756 submission_host_candidates: submission_host_products.candidates,
1757 submission_hosts: submission_host_products.hosts,
1758 serialization,
1759 reset,
1760 form_ir,
1761 optimized_form_ir,
1762 runtime_forms,
1763 slots,
1764 component_invocations,
1765 component_instance_plan,
1766 component_instance_scope,
1767 component_composition,
1768 component_initialization,
1769 component_ir: ComponentIrReport::default(),
1770 component_ir_optimization: OptimizedComponentIrReport::default(),
1771 instance_context,
1772 slot_content_fragments: slot_composition.fragments,
1773 slot_outlets: slot_composition.outlets,
1774 slot_bindings,
1775 composition_types,
1776 context_declaration_candidates,
1777 context_ownership,
1778 context_dependency,
1779 context_lifetime,
1780 context_evaluation,
1781 component_scope,
1782 context_resolutions,
1783 context_types: context_type_products.contexts,
1784 provider_types: context_type_products.providers,
1785 consumer_types: context_type_products.consumers,
1786 context_binding_types: context_type_products.bindings,
1787 duplicate_provider_declarations,
1788 computed_values,
1789 effects,
1790 effect_reactive_analysis,
1791 effect_trigger_plan,
1792 effect_execution_plan,
1793 effect_bodies,
1794 effect_statements,
1795 reactive_graph,
1796 reactive_transitive_analysis,
1797 reactive_cycle_analysis,
1798 computed_evaluation_plan,
1799 templates,
1800 template_entities,
1801 diagnostics,
1802 ownership,
1803 references,
1804 provenance,
1805 };
1806 model
1807 .diagnostics
1808 .extend(crate::collect_effect_diagnostics(&model));
1809 model
1810 .diagnostics
1811 .extend(crate::collect_context_diagnostics(&model));
1812 model
1813 .diagnostics
1814 .extend(crate::collect_form_diagnostics(&model));
1815 model.component_ir = lower_component_ir(&model);
1816 model.component_ir_optimization = optimize_component_ir(&model.component_ir);
1817 model
1818 .diagnostics
1819 .extend(crate::collect_component_diagnostics(&model));
1820 model
1821}
1822
1823#[must_use]
1824pub fn build_application_semantic_model_for_unit(
1825 unit: &CompilationUnit,
1826) -> ApplicationSemanticModel {
1827 build_application_semantic_model_for_unit_with_packages(
1828 unit,
1829 &crate::semantic_package::SemanticPackageResolutionTable::default(),
1830 )
1831}
1832
1833#[must_use]
1835pub fn build_application_semantic_model_for_unit_with_packages(
1836 unit: &CompilationUnit,
1837 packages: &crate::semantic_package::SemanticPackageResolutionTable,
1838) -> ApplicationSemanticModel {
1839 let symbols = crate::build_symbol_table(unit);
1840 let modules = crate::build_module_graph(unit);
1841 let bindings =
1842 crate::binding_table::build_binding_table_with_packages(unit, &symbols, &modules, packages);
1843 build_application_semantic_model_from_files_with_bindings(unit.files(), Some(&bindings))
1844}
1845
1846pub fn build_file_route_application_semantic_model_for_unit_with_packages(
1855 unit: &CompilationUnit,
1856 packages: &crate::semantic_package::SemanticPackageResolutionTable,
1857) -> Result<ApplicationSemanticModel, FileRouteApplicationModelErrorV1> {
1858 let symbols = crate::build_symbol_table(unit);
1859 let modules = crate::build_module_graph(unit);
1860 let bindings =
1861 crate::binding_table::build_binding_table_with_packages(unit, &symbols, &modules, packages);
1862 build_application_semantic_model_from_files_with_bindings_mode(
1863 unit.files(),
1864 Some(&bindings),
1865 ApplicationAssemblyMode::FileRoutes,
1866 )
1867}
1868
1869pub fn build_file_route_application_semantic_model_for_unit_with_packages_and_v2_authoring(
1873 unit: &CompilationUnit,
1874 packages: &crate::semantic_package::SemanticPackageResolutionTable,
1875 v2_authoring: &BTreeMap<std::path::PathBuf, crate::CanonicalAuthoredSemanticModelV1>,
1876) -> Result<ApplicationSemanticModel, FileRouteApplicationModelErrorV1> {
1877 let symbols = crate::build_symbol_table(unit);
1878 let modules = crate::build_module_graph(unit);
1879 let mut bindings =
1880 crate::binding_table::build_binding_table_with_packages(unit, &symbols, &modules, packages);
1881 retain_standard_schema_runtime_bindings(&mut bindings, unit, &modules, v2_authoring);
1882 build_application_semantic_model_from_files_with_bindings_mode_and_v2(
1883 unit.files(),
1884 Some(&bindings),
1885 ApplicationAssemblyMode::FileRoutes,
1886 Some(v2_authoring),
1887 )
1888}
1889
1890fn retain_standard_schema_runtime_bindings(
1891 bindings: &mut crate::BindingTable,
1892 unit: &CompilationUnit,
1893 modules: &crate::ModuleGraph,
1894 v2_authoring: &BTreeMap<std::path::PathBuf, crate::CanonicalAuthoredSemanticModelV1>,
1895) {
1896 let mut imports = BTreeSet::new();
1897 let mut specifiers = BTreeSet::new();
1898 let mut exports = BTreeSet::new();
1899 for model in v2_authoring.values() {
1900 let Some(parsed) = unit
1901 .files()
1902 .iter()
1903 .find(|parsed| parsed.path == model.source_path)
1904 else {
1905 continue;
1906 };
1907 for evidence in model
1908 .declarations
1909 .iter()
1910 .filter_map(|declaration| declaration.derived_evidence.as_ref())
1911 {
1912 let crate::DerivedAuthoredEvidenceV2::StandardSchemaValidation {
1913 module_specifier,
1914 export_name,
1915 declaration_modules,
1916 ..
1917 } = evidence
1918 else {
1919 continue;
1920 };
1921 let Some(import) = parsed
1922 .imports
1923 .iter()
1924 .find(|import| import.source == *module_specifier)
1925 else {
1926 continue;
1927 };
1928 if let Some(local) = import
1929 .specifiers
1930 .iter()
1931 .find(|specifier| specifier.imported == *export_name)
1932 .map(|specifier| specifier.local.clone())
1933 {
1934 imports.insert((model.source_path.clone(), local));
1935 specifiers.insert((model.source_path.clone(), module_specifier.clone()));
1936 }
1937 if let Some(crate::ModuleTarget::Resolved(target)) = modules
1938 .edges
1939 .iter()
1940 .find(|edge| {
1941 edge.kind == crate::ModuleEdgeKind::Import
1942 && edge.source == model.source_path
1943 && edge.specifier == *module_specifier
1944 })
1945 .map(|edge| &edge.target)
1946 {
1947 exports.insert((target.clone(), export_name.clone()));
1948 }
1949 for target in declaration_modules {
1950 if unit
1951 .files()
1952 .iter()
1953 .any(|file| file.path == Path::new(target))
1954 {
1955 exports.insert((PathBuf::from(target), export_name.clone()));
1956 }
1957 }
1958 }
1959 }
1960 bindings.diagnostics.retain(|diagnostic| {
1961 !((diagnostic.code == "PSBIND1001"
1962 && exports.contains(&(diagnostic.module.clone(), diagnostic.name.clone())))
1963 || (diagnostic.code == "PSBIND1003"
1964 && imports.contains(&(diagnostic.module.clone(), diagnostic.name.clone())))
1965 || (matches!(diagnostic.code.as_str(), "PSBIND1002" | "PSBIND1009")
1966 && specifiers.contains(&(diagnostic.module.clone(), diagnostic.name.clone()))))
1967 });
1968}
1969
1970pub fn build_file_route_application_semantic_model_for_route_with_packages(
1979 unit: &CompilationUnit,
1980 packages: &crate::semantic_package::SemanticPackageResolutionTable,
1981 route_component: &SemanticId,
1982) -> Result<ApplicationSemanticModel, FileRouteApplicationModelErrorV1> {
1983 let symbols = crate::build_symbol_table(unit);
1984 let modules = crate::build_module_graph(unit);
1985 let bindings =
1986 crate::binding_table::build_binding_table_with_packages(unit, &symbols, &modules, packages);
1987 build_application_semantic_model_from_files_with_bindings_mode(
1988 unit.files(),
1989 Some(&bindings),
1990 ApplicationAssemblyMode::FileRoute(route_component.clone()),
1991 )
1992}
1993
1994pub fn build_file_route_application_semantic_model_for_route_with_packages_and_v2_authoring(
1997 unit: &CompilationUnit,
1998 packages: &crate::semantic_package::SemanticPackageResolutionTable,
1999 route_component: &SemanticId,
2000 v2_authoring: &BTreeMap<std::path::PathBuf, crate::CanonicalAuthoredSemanticModelV1>,
2001) -> Result<ApplicationSemanticModel, FileRouteApplicationModelErrorV1> {
2002 let symbols = crate::build_symbol_table(unit);
2003 let modules = crate::build_module_graph(unit);
2004 let mut bindings =
2005 crate::binding_table::build_binding_table_with_packages(unit, &symbols, &modules, packages);
2006 retain_standard_schema_runtime_bindings(&mut bindings, unit, &modules, v2_authoring);
2007 build_application_semantic_model_from_files_with_bindings_mode_and_v2(
2008 unit.files(),
2009 Some(&bindings),
2010 ApplicationAssemblyMode::FileRoute(route_component.clone()),
2011 Some(v2_authoring),
2012 )
2013}
2014
2015fn build_application_semantic_model_from_files(files: &[ParsedFile]) -> ApplicationSemanticModel {
2016 build_application_semantic_model_from_files_with_bindings(files, None)
2017}
2018
2019#[allow(clippy::too_many_lines)]
2020fn build_application_semantic_model_from_files_with_bindings(
2021 files: &[ParsedFile],
2022 bindings: Option<&crate::BindingTable>,
2023) -> ApplicationSemanticModel {
2024 build_application_semantic_model_from_files_with_bindings_mode(
2025 files,
2026 bindings,
2027 ApplicationAssemblyMode::Authored,
2028 )
2029 .expect("authored application assembly cannot derive file-route diagnostics")
2030}
2031
2032#[derive(Debug, Clone, PartialEq, Eq)]
2033enum ApplicationAssemblyMode {
2034 Authored,
2035 FileRoutes,
2036 FileRoute(SemanticId),
2037}
2038
2039#[allow(clippy::too_many_lines)]
2040fn build_application_semantic_model_from_files_with_bindings_mode(
2041 files: &[ParsedFile],
2042 bindings: Option<&crate::BindingTable>,
2043 mode: ApplicationAssemblyMode,
2044) -> Result<ApplicationSemanticModel, FileRouteApplicationModelErrorV1> {
2045 build_application_semantic_model_from_files_with_bindings_mode_and_v2(
2046 files, bindings, mode, None,
2047 )
2048}
2049
2050#[allow(clippy::too_many_lines)]
2051fn build_application_semantic_model_from_files_with_bindings_mode_and_v2(
2052 files: &[ParsedFile],
2053 bindings: Option<&crate::BindingTable>,
2054 mode: ApplicationAssemblyMode,
2055 v2_authoring: Option<&BTreeMap<std::path::PathBuf, crate::CanonicalAuthoredSemanticModelV1>>,
2056) -> Result<ApplicationSemanticModel, FileRouteApplicationModelErrorV1> {
2057 let (mut components, mut templates, mut diagnostics, mut references) =
2058 (Vec::new(), Vec::new(), Vec::new(), Vec::new());
2059 let (mut provenance, mut template_entities, mut type_aliases) =
2060 (BTreeMap::new(), Vec::new(), Vec::new());
2061 for parsed in files {
2062 let component_graph = v2_authoring
2063 .and_then(|models| models.get(&parsed.path))
2064 .map_or_else(
2065 || build_component_graph_for_module(parsed),
2066 |model| build_v2_component_graph_for_module(parsed, model),
2067 );
2068 let template_graph = build_template_graph(&component_graph);
2069 let file_template_entities = build_template_semantic_entities(&template_graph.templates);
2070
2071 components.extend(component_graph.components);
2072 templates.extend(template_graph.templates);
2073 diagnostics.extend(component_graph.diagnostics);
2074 references.extend(component_graph.references);
2075 provenance.extend(component_graph.provenance);
2076 extend_template_entity_provenance(&mut provenance, &file_template_entities);
2077 template_entities.extend(file_template_entities);
2078 type_aliases.extend(
2079 parsed
2080 .type_aliases
2081 .iter()
2082 .cloned()
2083 .map(|alias| (parsed.path.clone(), alias)),
2084 );
2085 }
2086
2087 resolve_builtin_pure_calls(&mut components);
2088
2089 if let Some(bindings) = bindings {
2090 diagnostics.extend(bindings.diagnostics.iter().map(|diagnostic| {
2091 ComponentDiagnostic::error(
2092 diagnostic.code.clone(),
2093 format!("{}: {}", diagnostic.module.display(), diagnostic.message),
2094 )
2095 }));
2096 resolve_semantic_package_pure_calls(&mut components, bindings);
2097 }
2098
2099 let resource_endpoint_resolutions =
2100 collect_resource_endpoint_resolutions(&components, bindings);
2101 diagnostics.extend(collect_resource_lowering_diagnostics(
2102 &resource_endpoint_resolutions,
2103 ));
2104 let opaque_action_resolutions = collect_opaque_action_resolutions(&components, bindings);
2105 diagnostics.extend(collect_opaque_action_lowering_diagnostics(
2106 &opaque_action_resolutions,
2107 ));
2108
2109 let mut component_invocations = collect_component_invocations(
2110 &components,
2111 &templates,
2112 &template_entities,
2113 files,
2114 bindings,
2115 &provenance,
2116 );
2117 let virtual_invocations = match mode {
2118 ApplicationAssemblyMode::Authored => BTreeMap::new(),
2119 ApplicationAssemblyMode::FileRoutes | ApplicationAssemblyMode::FileRoute(_) => {
2120 let mut graph = crate::build_validated_file_route_graph_from_components_v1(&components)
2121 .map_err(|error| FileRouteApplicationModelErrorV1 {
2122 code: error.code,
2123 message: error.message,
2124 })?;
2125 if let ApplicationAssemblyMode::FileRoute(selected) = &mode {
2126 graph.routes.retain(|route| route.component == *selected);
2127 if graph.routes.is_empty() {
2128 return Err(FileRouteApplicationModelErrorV1 {
2129 code: "PSROUTE1014_FILE_ROUTE_COMPONENT_UNRESOLVED",
2130 message: format!("component `{selected}` is not a conventional file route"),
2131 });
2132 }
2133 }
2134 let plan = crate::build_layout_composition_plan_from_components_v1(&components, &graph)
2135 .map_err(|error| FileRouteApplicationModelErrorV1 {
2136 code: error.code,
2137 message: error.message,
2138 })?;
2139 crate::layout_composition_virtual_invocations_from_provenance_v1(&plan, &provenance)
2140 }
2141 };
2142 let mut component_instance_plan = plan_component_instances_with_virtual_invocations(
2143 &components,
2144 &component_invocations,
2145 &virtual_invocations,
2146 &template_entities,
2147 &provenance,
2148 );
2149 if let ApplicationAssemblyMode::FileRoute(selected) = &mode {
2150 let selected_roots = component_instance_plan
2151 .instances
2152 .values()
2153 .filter(|instance| instance.component == *selected)
2154 .map(|instance| instance.owner_root.clone())
2155 .collect::<BTreeSet<_>>();
2156 component_instance_plan
2157 .roots
2158 .retain(|root, _| selected_roots.contains(root));
2159 component_instance_plan
2160 .instances
2161 .retain(|_, instance| selected_roots.contains(&instance.owner_root));
2162 component_instance_plan
2163 .blocked
2164 .retain(|_, instance| selected_roots.contains(&instance.owner_root));
2165 }
2166 component_invocations.extend(virtual_invocations.clone());
2167 let component_instance_scope = build_component_instance_scope_graph(&component_instance_plan);
2168 let component_composition = analyze_component_composition(
2169 &components
2170 .iter()
2171 .map(|component| component.id.clone())
2172 .collect(),
2173 &component_invocations,
2174 &component_instance_plan,
2175 );
2176
2177 let (computed_values, computed_diagnostics) = classify_computed_values(
2178 &components,
2179 collect_computed_values(&components, &provenance),
2180 &provenance,
2181 );
2182 let effects = collect_effects(&components, &provenance);
2183 let expression_graph = ExpressionGraph::from_components(&components, &provenance);
2184 let contexts = collect_context_entities(&components, &expression_graph);
2185 let forms = collect_form_entities(&components);
2186 let base_semantic_types = SemanticTypeModel::from_components_with_aliases_and_bindings(
2187 &components,
2188 &provenance,
2189 &type_aliases,
2190 bindings,
2191 );
2192 let resource_declarations = collect_resource_declarations(
2193 &components,
2194 &resource_endpoint_resolutions,
2195 &base_semantic_types,
2196 bindings,
2197 );
2198 let resource_activations =
2199 collect_resource_activations(&resource_declarations, &component_instance_plan);
2200 let form_field_products =
2201 collect_form_field_products(&components, &forms, &base_semantic_types, bindings);
2202 let form_field_declaration_candidates = form_field_products.candidates;
2203 let form_fields = form_field_products.fields;
2204 let form_field_binding_products = collect_form_field_binding_products(
2205 &components,
2206 &templates,
2207 &forms,
2208 &form_fields,
2209 &form_field_declaration_candidates,
2210 );
2211 let form_field_binding_candidates = form_field_binding_products.candidates;
2212 let form_field_bindings = form_field_binding_products.bindings;
2213 let slots = collect_slot_entities(&components);
2214 let slot_composition = collect_slot_composition(&templates, &component_invocations, &slots);
2215 let slot_bindings = collect_slot_bindings_with_virtual_invocations(
2216 &component_instance_plan,
2217 &component_invocations,
2218 &virtual_invocations,
2219 &slots,
2220 &slot_composition.fragments,
2221 &slot_composition.outlets,
2222 );
2223 let (providers, duplicate_provider_declarations) =
2224 collect_provider_entities(&components, &contexts, &expression_graph, bindings);
2225 let consumers = collect_consumer_entities(&components, &contexts, bindings);
2226 let instance_context = collect_instance_context_registry(
2227 &component_instance_scope,
2228 &contexts,
2229 &providers,
2230 &consumers,
2231 );
2232 let context_declaration_candidates =
2233 collect_context_declaration_candidates(&components, &contexts, &providers, &consumers);
2234 let component_scope = ComponentScopeGraph::reflexive(&components);
2235 let context_resolutions = collect_context_resolutions(
2236 &consumers,
2237 &contexts,
2238 &providers,
2239 &expression_graph,
2240 &component_scope,
2241 );
2242 diagnostics.extend(computed_diagnostics);
2243 extend_derived_entity_provenance(
2244 &mut provenance,
2245 &contexts,
2246 &forms,
2247 &form_fields,
2248 &form_field_bindings,
2249 &providers,
2250 &consumers,
2251 &slots,
2252 &component_invocations,
2253 &slot_composition.fragments,
2254 &slot_composition.outlets,
2255 &component_instance_plan,
2256 &computed_values,
2257 &effects,
2258 );
2259 let mut ownership = collect_ownership(
2260 &components,
2261 &contexts,
2262 &forms,
2263 &form_fields,
2264 &form_field_bindings,
2265 &providers,
2266 &consumers,
2267 &slots,
2268 &component_invocations,
2269 &slot_composition.fragments,
2270 &slot_composition.outlets,
2271 &component_instance_plan,
2272 &computed_values,
2273 &effects,
2274 &templates,
2275 &template_entities,
2276 );
2277 let context_ownership = collect_context_ownership_graph(
2278 &components,
2279 &contexts,
2280 &providers,
2281 &consumers,
2282 &expression_graph,
2283 &provenance,
2284 );
2285 let (effect_bodies, effect_statements) =
2286 lower_effect_bodies(&components, &effects, &expression_graph);
2287 references.extend(build_computed_references(
2288 &components,
2289 &computed_values,
2290 &resource_declarations,
2291 &expression_graph,
2292 ));
2293 references.extend(build_effect_references(
2294 &components,
2295 &effects,
2296 &computed_values,
2297 &expression_graph,
2298 ));
2299 references.extend(build_provider_references(&providers));
2300 references.extend(build_consumer_references(&consumers));
2301 references.extend(build_context_resolution_references(&context_resolutions));
2302 extend_template_references(
2303 &mut references,
2304 &components,
2305 &computed_values,
2306 &template_entities,
2307 &ownership,
2308 );
2309 references.extend(build_form_field_binding_references(&form_field_bindings));
2310 let form_ownership = collect_form_ownership_graph(
2311 &component_instance_plan.roots,
2312 &components,
2313 &forms,
2314 &form_fields,
2315 &form_field_bindings,
2316 &template_entities,
2317 &ownership,
2318 &references,
2319 &provenance,
2320 );
2321 let validation_products = collect_validation_products(&components, &forms, &form_fields);
2322 let validation_rule_candidates = validation_products.candidates;
2323 let validation_rules = validation_products.rules;
2324 extend_validation_products(
2325 &mut provenance,
2326 &mut ownership,
2327 &mut references,
2328 &validation_rules,
2329 );
2330 let validation_graph = collect_validation_graph(
2331 &component_instance_plan.roots,
2332 &form_ownership,
2333 &forms,
2334 &form_fields,
2335 &validation_rules,
2336 &validation_rule_candidates,
2337 &validation_products.cycles,
2338 &ownership,
2339 &references,
2340 );
2341 let validation_dependency_plans = collect_validation_dependency_plans(
2342 &forms,
2343 &form_fields,
2344 &validation_rules,
2345 &form_ownership,
2346 &validation_graph,
2347 );
2348 let form_tracking =
2349 collect_form_tracking_products(&forms, &form_fields, &form_field_bindings, &form_ownership);
2350 let semantic_types = finalize_semantic_types(
2351 base_semantic_types.with_resource_types(&resource_declarations),
2352 &components,
2353 &contexts,
2354 &forms,
2355 &form_fields,
2356 &providers,
2357 &consumers,
2358 &slots,
2359 &computed_values,
2360 &effects,
2361 &effect_statements,
2362 &expression_graph,
2363 &references,
2364 &template_entities,
2365 );
2366 let context_type_products = collect_context_type_products(
2367 &contexts,
2368 &providers,
2369 &consumers,
2370 &context_resolutions,
2371 &expression_graph,
2372 &semantic_types,
2373 );
2374 let context_dependency = collect_context_dependency_graph(
2375 &components,
2376 &contexts,
2377 &providers,
2378 &consumers,
2379 &context_resolutions,
2380 &context_type_products.contexts,
2381 &context_type_products.providers,
2382 &context_type_products.bindings,
2383 &computed_values,
2384 &expression_graph,
2385 );
2386 let context_lifetime = collect_context_lifetime_analysis(
2387 &components,
2388 &contexts,
2389 &providers,
2390 &consumers,
2391 &computed_values,
2392 &context_ownership,
2393 &component_scope,
2394 &context_resolutions,
2395 &context_dependency,
2396 &provenance,
2397 );
2398 let effects = validate_effects(&components, effects, &effect_statements, &semantic_types);
2399 let (
2400 reactive_graph,
2401 reactive_transitive_analysis,
2402 reactive_cycle_analysis,
2403 computed_evaluation_plan,
2404 ) = build_computed_reactive_products(
2405 &components,
2406 &computed_values,
2407 &effects,
2408 &resource_declarations,
2409 &references,
2410 &provenance,
2411 );
2412 let context_evaluation = collect_context_evaluation_plan(
2413 &contexts,
2414 &providers,
2415 &context_resolutions,
2416 &context_type_products.contexts,
2417 &context_type_products.providers,
2418 &context_type_products.bindings,
2419 &context_lifetime,
2420 &context_dependency,
2421 &computed_evaluation_plan,
2422 &component_scope,
2423 );
2424 let effect_reactive_analysis = analyze_effect_reactivity(
2425 &components,
2426 &computed_values,
2427 &effects,
2428 &reactive_transitive_analysis,
2429 );
2430 let effect_trigger_plan = derive_effect_trigger_plan(
2431 &components,
2432 &effects,
2433 &effect_reactive_analysis,
2434 &provenance,
2435 );
2436 let submissions = collect_submission_products(
2437 &components,
2438 &forms,
2439 &form_fields,
2440 &validation_rules,
2441 &effect_trigger_plan,
2442 bindings,
2443 None,
2444 );
2445 let serialization =
2446 collect_serialization_products(&components, &forms, &form_fields, &submissions.plans);
2447 let reset = collect_reset_products(&forms, &form_fields, &form_field_bindings, &form_tracking);
2448 let form_ir = lower_form_ir(&component_instance_plan, &forms, &form_fields);
2449 let optimized_form_ir = optimize_form_ir(&form_ir);
2450 let submission_host_products = collect_submission_host_products(
2451 &templates,
2452 &components,
2453 &forms,
2454 &form_field_bindings,
2455 &submissions.plans,
2456 &serialization.plans,
2457 &optimized_form_ir.optimized,
2458 );
2459 let runtime_forms = build_runtime_form_registry(
2460 &forms,
2461 &form_fields,
2462 &form_field_bindings,
2463 &validation_rules,
2464 &optimized_form_ir.optimized,
2465 &submissions,
2466 &serialization,
2467 &reset,
2468 &submission_host_products.hosts,
2469 );
2470 let effect_execution_plan = plan_effect_execution(
2471 &computed_values,
2472 &effects,
2473 &effect_reactive_analysis,
2474 &effect_trigger_plan,
2475 &reactive_transitive_analysis,
2476 &computed_evaluation_plan,
2477 );
2478 extend_computed_diagnostics(
2479 &mut diagnostics,
2480 &components,
2481 &computed_values,
2482 &resource_declarations,
2483 &expression_graph,
2484 &semantic_types,
2485 &reactive_cycle_analysis,
2486 &provenance,
2487 );
2488 let component_ids = components
2489 .iter()
2490 .map(|component| component.id.clone())
2491 .collect::<BTreeSet<_>>();
2492 let composition_types = collect_composition_type_products(
2493 &component_ids,
2494 &component_invocations,
2495 &slot_bindings,
2496 &slots,
2497 &slot_composition.fragments,
2498 &slot_composition.outlets,
2499 &instance_context,
2500 &context_type_products.bindings,
2501 &context_type_products.contexts,
2502 &context_type_products.providers,
2503 &context_lifetime,
2504 &ownership,
2505 &references,
2506 );
2507 let component_initialization = plan_component_initialization(
2508 &component_instance_plan,
2509 &slot_bindings,
2510 &composition_types,
2511 &instance_context,
2512 );
2513 let components = components_with_resolved_form_field_candidates(
2514 &components,
2515 &form_field_declaration_candidates,
2516 );
2517 let mut model = ApplicationSemanticModel {
2518 expression_graph,
2519 semantic_types,
2520 components,
2521 contexts,
2522 providers,
2523 consumers,
2524 forms,
2525 resource_endpoint_resolutions,
2526 opaque_action_resolutions,
2527 resource_declarations,
2528 resource_activations,
2529 form_field_declaration_candidates,
2530 form_fields,
2531 form_field_binding_candidates,
2532 form_field_bindings,
2533 form_ownership,
2534 validation_rule_candidates,
2535 validation_rules,
2536 validation_graph,
2537 validation_dependency_plans,
2538 form_tracking,
2539 submissions,
2540 submission_host_candidates: submission_host_products.candidates,
2541 submission_hosts: submission_host_products.hosts,
2542 serialization,
2543 reset,
2544 form_ir,
2545 optimized_form_ir,
2546 runtime_forms,
2547 slots,
2548 component_invocations,
2549 component_instance_plan,
2550 component_instance_scope,
2551 component_composition,
2552 component_initialization,
2553 component_ir: ComponentIrReport::default(),
2554 component_ir_optimization: OptimizedComponentIrReport::default(),
2555 instance_context,
2556 slot_content_fragments: slot_composition.fragments,
2557 slot_outlets: slot_composition.outlets,
2558 slot_bindings,
2559 composition_types,
2560 context_declaration_candidates,
2561 context_ownership,
2562 context_dependency,
2563 context_lifetime,
2564 context_evaluation,
2565 component_scope,
2566 context_resolutions,
2567 context_types: context_type_products.contexts,
2568 provider_types: context_type_products.providers,
2569 consumer_types: context_type_products.consumers,
2570 context_binding_types: context_type_products.bindings,
2571 duplicate_provider_declarations,
2572 computed_values,
2573 effects,
2574 effect_reactive_analysis,
2575 effect_trigger_plan,
2576 effect_execution_plan,
2577 effect_bodies,
2578 effect_statements,
2579 reactive_graph,
2580 reactive_transitive_analysis,
2581 reactive_cycle_analysis,
2582 computed_evaluation_plan,
2583 templates,
2584 template_entities,
2585 diagnostics,
2586 ownership,
2587 references,
2588 provenance,
2589 };
2590 model
2591 .diagnostics
2592 .extend(crate::collect_effect_diagnostics(&model));
2593 model
2594 .diagnostics
2595 .extend(crate::collect_context_diagnostics(&model));
2596 model
2597 .diagnostics
2598 .extend(crate::collect_form_diagnostics(&model));
2599 model.component_ir = lower_component_ir(&model);
2600 model.component_ir_optimization = optimize_component_ir(&model.component_ir);
2601 model
2602 .diagnostics
2603 .extend(crate::collect_component_diagnostics(&model));
2604 Ok(model)
2605}
2606
2607fn resolve_builtin_pure_calls(components: &mut [ComponentNode]) {
2608 for component in components {
2609 for method in component
2610 .methods
2611 .iter_mut()
2612 .filter(|method| method.is_computed())
2613 {
2614 if let Some(expression) = method.computed_expression.as_mut() {
2615 resolve_builtin_pure_call(expression);
2616 }
2617 }
2618 }
2619}
2620
2621fn resolve_builtin_pure_call(expression: &mut ComputedExpression) {
2622 match &mut expression.kind {
2623 ComputedExpressionKind::Call { callee, arguments } => {
2624 for argument in arguments.iter_mut() {
2625 resolve_builtin_pure_call(argument);
2626 }
2627 let operation = match (callee.as_str(), arguments.len()) {
2628 ("Math.abs", 1) => Some(crate::component_graph::BuiltinPureOperation::MathAbs),
2629 ("Math.floor", 1) => Some(crate::component_graph::BuiltinPureOperation::MathFloor),
2630 ("Math.ceil", 1) => Some(crate::component_graph::BuiltinPureOperation::MathCeil),
2631 ("Math.round", 1) => Some(crate::component_graph::BuiltinPureOperation::MathRound),
2632 ("Math.min", 2) => Some(crate::component_graph::BuiltinPureOperation::MathMin),
2633 ("Math.max", 2) => Some(crate::component_graph::BuiltinPureOperation::MathMax),
2634 _ => None,
2635 };
2636 if let Some(operation) = operation {
2637 expression.kind = ComputedExpressionKind::BuiltinPureCall {
2638 operation,
2639 arguments: std::mem::take(arguments),
2640 };
2641 }
2642 }
2643 ComputedExpressionKind::BuiltinPureCall { arguments, .. }
2644 | ComputedExpressionKind::SemanticPackagePureCall { arguments, .. } => {
2645 for argument in arguments {
2646 resolve_builtin_pure_call(argument);
2647 }
2648 }
2649 ComputedExpressionKind::Template { expressions, .. } => {
2650 for expression in expressions {
2651 resolve_builtin_pure_call(expression);
2652 }
2653 }
2654 ComputedExpressionKind::MemberAccess { object, .. }
2655 | ComputedExpressionKind::Unary {
2656 operand: object, ..
2657 } => {
2658 resolve_builtin_pure_call(object);
2659 }
2660 ComputedExpressionKind::IndexAccess { object, index } => {
2661 resolve_builtin_pure_call(object);
2662 resolve_builtin_pure_call(index);
2663 }
2664 ComputedExpressionKind::Conditional {
2665 condition,
2666 when_true,
2667 when_false,
2668 } => {
2669 resolve_builtin_pure_call(condition);
2670 resolve_builtin_pure_call(when_true);
2671 resolve_builtin_pure_call(when_false);
2672 }
2673 ComputedExpressionKind::Arithmetic { left, right, .. }
2674 | ComputedExpressionKind::Comparison { left, right, .. }
2675 | ComputedExpressionKind::Logical { left, right, .. }
2676 | ComputedExpressionKind::NullishCoalescing { left, right } => {
2677 resolve_builtin_pure_call(left);
2678 resolve_builtin_pure_call(right);
2679 }
2680 ComputedExpressionKind::Literal(_) | ComputedExpressionKind::ThisMember(_) => {}
2681 }
2682}
2683
2684fn components_with_resolved_form_field_candidates(
2685 components: &[ComponentNode],
2686 candidates: &[crate::FormFieldDeclarationCandidate],
2687) -> Vec<ComponentNode> {
2688 let candidates = candidates
2689 .iter()
2690 .map(|candidate| (candidate.id.clone(), candidate))
2691 .collect::<BTreeMap<_, _>>();
2692 let mut components = components.to_vec();
2693 for component in &mut components {
2694 for candidate in &mut component.form_field_declaration_candidates {
2695 if let Some(resolved) = candidates.get(&candidate.id) {
2696 *candidate = (*resolved).clone();
2697 }
2698 }
2699 }
2700 components
2701}
2702
2703fn extend_template_entity_provenance(
2704 provenance: &mut BTreeMap<SemanticId, SourceProvenance>,
2705 template_entities: &[TemplateSemanticEntity],
2706) {
2707 provenance.extend(
2708 template_entities
2709 .iter()
2710 .map(|entity| (entity.id.clone(), entity.provenance.clone())),
2711 );
2712}
2713
2714fn extend_validation_products(
2715 provenance: &mut BTreeMap<SemanticId, SourceProvenance>,
2716 ownership: &mut BTreeMap<SemanticId, SemanticOwner>,
2717 references: &mut Vec<SemanticReference>,
2718 rules: &BTreeMap<ValidationRuleId, ValidationRule>,
2719) {
2720 for rule in rules.values() {
2721 provenance.insert(rule.id.as_semantic_id().clone(), rule.provenance.clone());
2722 ownership.insert(
2723 rule.id.as_semantic_id().clone(),
2724 SemanticOwner::entity(rule.target_field.as_semantic_id().clone()),
2725 );
2726 if let Some(dependency) = &rule.dependency {
2727 references.push(SemanticReference {
2728 kind: SemanticReferenceKind::ValidationRuleField,
2729 source: rule.id.as_semantic_id().clone(),
2730 target: dependency.as_semantic_id().clone(),
2731 provenance: rule
2732 .argument_provenance
2733 .clone()
2734 .unwrap_or_else(|| rule.decorator_provenance.clone()),
2735 });
2736 }
2737 }
2738 references.sort_by(|left, right| {
2739 (left.source.as_str(), left.kind, left.target.as_str()).cmp(&(
2740 right.source.as_str(),
2741 right.kind,
2742 right.target.as_str(),
2743 ))
2744 });
2745 references.dedup();
2746}
2747
2748#[allow(clippy::too_many_arguments)]
2749fn extend_derived_entity_provenance(
2750 provenance: &mut BTreeMap<SemanticId, SourceProvenance>,
2751 contexts: &BTreeMap<ContextId, ContextEntity>,
2752 forms: &BTreeMap<FormId, FormEntity>,
2753 form_fields: &BTreeMap<FieldId, FormFieldEntity>,
2754 form_field_bindings: &BTreeMap<FieldBindingId, FormFieldBinding>,
2755 providers: &BTreeMap<ProviderId, ProviderEntity>,
2756 consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
2757 slots: &BTreeMap<SlotId, SlotEntity>,
2758 component_invocations: &BTreeMap<ComponentInvocationId, ComponentInvocationEntity>,
2759 slot_content_fragments: &BTreeMap<SlotContentFragmentId, SlotContentFragment>,
2760 slot_outlets: &BTreeMap<SlotOutletId, SlotOutlet>,
2761 component_instance_plan: &ComponentInstancePlan,
2762 computed_values: &BTreeMap<SemanticId, ComputedValue>,
2763 effects: &BTreeMap<SemanticId, Effect>,
2764) {
2765 provenance.extend(contexts.values().map(|context| {
2766 (
2767 context.id.as_semantic_id().clone(),
2768 context.provenance.clone(),
2769 )
2770 }));
2771 provenance.extend(
2772 forms
2773 .values()
2774 .map(|form| (form.id.as_semantic_id().clone(), form.provenance.clone())),
2775 );
2776 provenance.extend(
2777 form_fields
2778 .values()
2779 .map(|field| (field.id.as_semantic_id().clone(), field.provenance.clone())),
2780 );
2781 provenance.extend(form_field_bindings.values().map(|binding| {
2782 (
2783 binding.id.as_semantic_id().clone(),
2784 binding.provenance.clone(),
2785 )
2786 }));
2787 provenance.extend(providers.values().map(|provider| {
2788 (
2789 provider.id.as_semantic_id().clone(),
2790 provider.provenance.clone(),
2791 )
2792 }));
2793 provenance.extend(consumers.values().map(|consumer| {
2794 (
2795 consumer.id.as_semantic_id().clone(),
2796 consumer.provenance.clone(),
2797 )
2798 }));
2799 provenance.extend(
2800 slots
2801 .values()
2802 .map(|slot| (slot.id.as_semantic_id().clone(), slot.provenance.clone())),
2803 );
2804 provenance.extend(component_invocations.values().map(|invocation| {
2805 (
2806 invocation.id.as_semantic_id().clone(),
2807 invocation.provenance.clone(),
2808 )
2809 }));
2810 provenance.extend(slot_content_fragments.values().map(|fragment| {
2811 (
2812 fragment.id.as_semantic_id().clone(),
2813 fragment.provenance.clone(),
2814 )
2815 }));
2816 provenance.extend(slot_outlets.values().map(|outlet| {
2817 (
2818 outlet.id.as_semantic_id().clone(),
2819 outlet.provenance.clone(),
2820 )
2821 }));
2822 provenance.extend(component_instance_plan.instances.values().map(|instance| {
2823 (
2824 instance.id.as_semantic_id().clone(),
2825 instance.provenance.clone(),
2826 )
2827 }));
2828 provenance.extend(component_instance_plan.blocked.values().map(|blocked| {
2829 (
2830 blocked.id.as_semantic_id().clone(),
2831 blocked.provenance.clone(),
2832 )
2833 }));
2834 provenance.extend(
2835 computed_values
2836 .iter()
2837 .map(|(id, computed)| (id.clone(), computed.provenance.clone())),
2838 );
2839 provenance.extend(
2840 effects
2841 .iter()
2842 .map(|(id, effect)| (id.clone(), effect.provenance.clone())),
2843 );
2844}
2845
2846#[allow(clippy::too_many_arguments)]
2847fn finalize_semantic_types(
2848 semantic_types: SemanticTypeModel,
2849 components: &[ComponentNode],
2850 contexts: &BTreeMap<ContextId, ContextEntity>,
2851 forms: &BTreeMap<FormId, FormEntity>,
2852 form_fields: &BTreeMap<FieldId, FormFieldEntity>,
2853 providers: &BTreeMap<ProviderId, ProviderEntity>,
2854 consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
2855 slots: &BTreeMap<SlotId, SlotEntity>,
2856 computed_values: &BTreeMap<SemanticId, ComputedValue>,
2857 effects: &BTreeMap<SemanticId, Effect>,
2858 effect_statements: &BTreeMap<SemanticId, EffectStatement>,
2859 expression_graph: &ExpressionGraph,
2860 references: &[SemanticReference],
2861 template_entities: &[TemplateSemanticEntity],
2862) -> SemanticTypeModel {
2863 semantic_types
2864 .with_slot_types(slots)
2865 .with_form_types(forms)
2866 .with_form_field_types(form_fields)
2867 .with_context_types(contexts)
2868 .with_provider_types(providers)
2869 .with_consumer_types(consumers)
2870 .with_expression_types(expression_graph, components, contexts, providers)
2871 .with_computed_value_types(components, computed_values, expression_graph, references)
2872 .with_context_source_expression_types(components, contexts, providers, expression_graph)
2873 .with_effect_statement_types(components, effects, effect_statements, expression_graph)
2874 .with_template_binding_types(template_entities, references)
2875 .normalized()
2876}
2877
2878#[allow(clippy::too_many_arguments)]
2879fn extend_computed_diagnostics(
2880 diagnostics: &mut Vec<ComponentDiagnostic>,
2881 components: &[ComponentNode],
2882 computed_values: &BTreeMap<SemanticId, ComputedValue>,
2883 resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
2884 expression_graph: &ExpressionGraph,
2885 semantic_types: &SemanticTypeModel,
2886 reactive_cycle_analysis: &IrReactiveCycleAnalysis,
2887 provenance: &BTreeMap<SemanticId, SourceProvenance>,
2888) {
2889 diagnostics.extend(collect_computed_cycle_diagnostics(
2890 reactive_cycle_analysis,
2891 computed_values,
2892 ));
2893 diagnostics.extend(collect_computed_semantic_diagnostics(
2894 components,
2895 computed_values,
2896 resource_declarations,
2897 expression_graph,
2898 semantic_types,
2899 provenance,
2900 ));
2901}
2902
2903#[allow(clippy::too_many_lines)]
2904fn classify_computed_values(
2905 components: &[ComponentNode],
2906 mut computed_values: BTreeMap<SemanticId, ComputedValue>,
2907 provenance: &BTreeMap<SemanticId, SourceProvenance>,
2908) -> (
2909 BTreeMap<SemanticId, ComputedValue>,
2910 Vec<ComponentDiagnostic>,
2911) {
2912 let mut diagnostics = Vec::new();
2913
2914 for computed in computed_values.values_mut() {
2915 let Some(component_id) = computed.owner.entity_id() else {
2916 continue;
2917 };
2918 let Some(component) = components
2919 .iter()
2920 .find(|component| component.id == *component_id)
2921 else {
2922 continue;
2923 };
2924 let Some(method) = component
2925 .methods
2926 .iter()
2927 .find(|method| method.id == computed.method)
2928 else {
2929 continue;
2930 };
2931 let mut violations = Vec::new();
2932
2933 if method.is_async {
2934 violations.push(crate::ComputedPurityViolation {
2935 kind: crate::ComputedPurityViolationKind::Async,
2936 provenance: computed.provenance.clone(),
2937 });
2938 }
2939 if method
2940 .decorators
2941 .iter()
2942 .any(|decorator| decorator == "action")
2943 {
2944 violations.push(crate::ComputedPurityViolation {
2945 kind: crate::ComputedPurityViolationKind::Action,
2946 provenance: computed.provenance.clone(),
2947 });
2948 }
2949 if method
2950 .decorators
2951 .iter()
2952 .any(|decorator| decorator == "effect")
2953 {
2954 violations.push(crate::ComputedPurityViolation {
2955 kind: crate::ComputedPurityViolationKind::Effect,
2956 provenance: computed.provenance.clone(),
2957 });
2958 }
2959 for action in component
2960 .actions
2961 .iter()
2962 .filter(|action| action.method == method.name)
2963 {
2964 violations.push(crate::ComputedPurityViolation {
2965 kind: crate::ComputedPurityViolationKind::StateMutation,
2966 provenance: provenance
2967 .get(&action.id)
2968 .expect("computed state update should have canonical provenance")
2969 .clone(),
2970 });
2971 }
2972 for call in &method.calls {
2973 if method
2974 .computed_expression
2975 .as_ref()
2976 .is_some_and(|expression| {
2977 contains_semantic_package_pure_call(expression, &call.callee)
2978 || contains_builtin_pure_call(expression, &call.callee)
2979 })
2980 {
2981 continue;
2982 }
2983 let kind = computed_call_purity_kind(component, &call.callee);
2984 violations.push(crate::ComputedPurityViolation {
2985 kind,
2986 provenance: SourceProvenance::new(&computed.provenance.path, call.span),
2987 });
2988 }
2989
2990 violations.sort_by(|left, right| {
2991 (
2992 left.kind,
2993 left.provenance.path.as_path(),
2994 left.provenance.span.start,
2995 left.provenance.span.end,
2996 )
2997 .cmp(&(
2998 right.kind,
2999 right.provenance.path.as_path(),
3000 right.provenance.span.start,
3001 right.provenance.span.end,
3002 ))
3003 });
3004 violations
3005 .dedup_by(|left, right| left.kind == right.kind && left.provenance == right.provenance);
3006 computed.purity = if violations.is_empty() {
3007 crate::ComputedPurity::Pure
3008 } else {
3009 crate::ComputedPurity::Impure
3010 };
3011 computed.purity_violations = violations;
3012 diagnostics.extend(computed.purity_violations.iter().map(|violation| {
3013 ComponentDiagnostic {
3014 severity: ComponentDiagnosticSeverity::Error,
3015 effect_id: None,
3016 statement_id: None,
3017 context_declaration_candidate_id: None,
3018 context_id: None,
3019 provider_id: None,
3020 consumer_id: None,
3021 slot_id: None,
3022 invocation_id: None,
3023 component_instance_id: None,
3024 slot_binding_id: None,
3025 structural_region_id: None,
3026 component_id: None,
3027 provider_instance_id: None,
3028 consumer_instance_id: None,
3029 secondary_labels: Vec::new(),
3030 code: ComputedDiagnosticCode::PurityViolation.as_str().to_string(),
3031 message: format!(
3032 "computed getter `{}` is impure: {}",
3033 computed.name,
3034 violation.kind.description()
3035 ),
3036 provenance: Some(violation.provenance.clone()),
3037 }
3038 }));
3039 }
3040
3041 (computed_values, diagnostics)
3042}
3043
3044fn resolve_semantic_package_pure_calls(
3045 components: &mut [ComponentNode],
3046 bindings: &crate::BindingTable,
3047) {
3048 for component in components {
3049 for method in component
3050 .methods
3051 .iter_mut()
3052 .filter(|method| method.is_computed())
3053 {
3054 let Some(expression) = method.computed_expression.as_mut() else {
3055 continue;
3056 };
3057 resolve_semantic_package_pure_call(expression, &component.module_path, bindings);
3058 }
3059 }
3060}
3061
3062fn collect_resource_endpoint_resolutions(
3063 components: &[ComponentNode],
3064 bindings: Option<&crate::BindingTable>,
3065) -> Vec<crate::ResourceEndpointResolution> {
3066 let mut resolutions = Vec::new();
3067 for component in components {
3068 for resource in &component.resource_declaration_candidates {
3069 let outcome = match resource.endpoint_designator.as_deref() {
3070 None => crate::ResourceEndpointResolutionOutcome::MissingDesignator,
3071 Some(designator) => {
3072 let Some(bindings) = bindings else {
3073 resolutions.push(crate::ResourceEndpointResolution {
3074 owner_component: resource.owner_component.clone(),
3075 field: resource.field.clone(),
3076 endpoint_designator: resource.endpoint_designator.clone(),
3077 outcome: crate::ResourceEndpointResolutionOutcome::UnboundDesignator {
3078 designator: designator.to_string(),
3079 },
3080 provenance: resource.provenance.clone(),
3081 });
3082 continue;
3083 };
3084 let Some(binding) = bindings.resolve_import(&component.module_path, designator)
3085 else {
3086 resolutions.push(crate::ResourceEndpointResolution {
3087 owner_component: resource.owner_component.clone(),
3088 field: resource.field.clone(),
3089 endpoint_designator: resource.endpoint_designator.clone(),
3090 outcome: crate::ResourceEndpointResolutionOutcome::UnboundDesignator {
3091 designator: designator.to_string(),
3092 },
3093 provenance: resource.provenance.clone(),
3094 });
3095 continue;
3096 };
3097 match &binding.target {
3098 crate::ImportBindingTarget::SemanticPackage {
3099 package,
3100 version,
3101 integrity,
3102 export,
3103 kind: crate::SemanticPackageKind::Resource,
3104 type_signature,
3105 runtime_module,
3106 resume_policy,
3107 resource_endpoint: Some(endpoint),
3108 ..
3109 } => crate::ResourceEndpointResolutionOutcome::Resolved(
3110 crate::ResourceEndpointBinding {
3111 local_name: binding.local_name.clone(),
3112 package: package.clone(),
3113 version: version.clone(),
3114 integrity: integrity.clone(),
3115 export: export.clone(),
3116 type_signature: type_signature.clone(),
3117 runtime_module: runtime_module.clone(),
3118 resume_policy: resume_policy.clone(),
3119 endpoint: endpoint.clone(),
3120 },
3121 ),
3122 crate::ImportBindingTarget::SemanticPackage { kind, .. } => {
3123 crate::ResourceEndpointResolutionOutcome::NonResourceBinding {
3124 designator: designator.to_string(),
3125 kind: kind.clone(),
3126 }
3127 }
3128 _ => crate::ResourceEndpointResolutionOutcome::NonSemanticPackageBinding {
3129 designator: designator.to_string(),
3130 },
3131 }
3132 }
3133 };
3134 resolutions.push(crate::ResourceEndpointResolution {
3135 owner_component: resource.owner_component.clone(),
3136 field: resource.field.clone(),
3137 endpoint_designator: resource.endpoint_designator.clone(),
3138 outcome,
3139 provenance: resource.provenance.clone(),
3140 });
3141 }
3142 }
3143 resolutions
3144}
3145
3146fn collect_opaque_action_resolutions(
3147 components: &[ComponentNode],
3148 bindings: Option<&crate::BindingTable>,
3149) -> Vec<crate::OpaqueActionResolution> {
3150 let mut resolutions = Vec::new();
3151 for component in components {
3152 for fact in component
3153 .opaque_action_facts
3154 .iter()
3155 .filter(|fact| crate::component_graph::is_valid_opaque_action_fact(fact))
3156 {
3157 let (Some(owner_component), Some(method), Some(package_specifier), Some(export_name)) = (
3158 fact.owner_component.as_ref(),
3159 fact.method.as_ref(),
3160 fact.package.as_ref(),
3161 fact.export.as_ref(),
3162 ) else {
3163 continue;
3164 };
3165 let outcome = bindings
3166 .and_then(|bindings| bindings.module(&component.module_path))
3167 .and_then(|module| {
3168 module.imports.values().find(|binding| {
3169 binding.source_module == *package_specifier
3170 && binding.imported_name == *export_name
3171 })
3172 })
3173 .map_or_else(
3174 || crate::OpaqueActionResolutionOutcome::UnboundImport {
3175 package: package_specifier.clone(),
3176 export: export_name.clone(),
3177 },
3178 |binding| match &binding.target {
3179 crate::ImportBindingTarget::SemanticPackage {
3180 package,
3181 version,
3182 integrity,
3183 export,
3184 kind: crate::SemanticPackageKind::Opaque,
3185 type_signature,
3186 runtime_module,
3187 resume_policy,
3188 opaque_terminal: Some(terminal),
3189 ..
3190 } => crate::OpaqueActionResolutionOutcome::Resolved(
3191 crate::OpaqueTerminalBinding {
3192 local_name: binding.local_name.clone(),
3193 package: package.clone(),
3194 version: version.clone(),
3195 integrity: integrity.clone(),
3196 export: export.clone(),
3197 type_signature: type_signature.clone(),
3198 runtime_module: runtime_module.clone(),
3199 resume_policy: resume_policy.clone(),
3200 terminal: terminal.clone(),
3201 },
3202 ),
3203 crate::ImportBindingTarget::SemanticPackage { kind, .. } => {
3204 crate::OpaqueActionResolutionOutcome::NonOpaqueBinding {
3205 package: package_specifier.clone(),
3206 export: export_name.clone(),
3207 kind: kind.clone(),
3208 }
3209 }
3210 _ => crate::OpaqueActionResolutionOutcome::UnboundImport {
3211 package: package_specifier.clone(),
3212 export: export_name.clone(),
3213 },
3214 },
3215 );
3216 resolutions.push(crate::OpaqueActionResolution {
3217 activation: fact.id.clone(),
3218 owner_component: owner_component.clone(),
3219 method: method.clone(),
3220 method_name: fact.method_name.clone(),
3221 package_specifier: package_specifier.clone(),
3222 export_name: export_name.clone(),
3223 outcome,
3224 provenance: fact.provenance.clone(),
3225 });
3226 }
3227 }
3228 resolutions.sort_by(|left, right| left.activation.cmp(&right.activation));
3229 resolutions
3230}
3231
3232fn collect_opaque_action_lowering_diagnostics(
3233 resolutions: &[crate::OpaqueActionResolution],
3234) -> Vec<ComponentDiagnostic> {
3235 resolutions
3236 .iter()
3237 .filter_map(|resolution| {
3238 let message = match &resolution.outcome {
3239 crate::OpaqueActionResolutionOutcome::Resolved(_) => return None,
3240 crate::OpaqueActionResolutionOutcome::UnboundImport { package, export } => format!(
3241 "opaque Action `{}` requires an imported opaque semantic-package export `{package}` / `{export}`",
3242 resolution.method_name
3243 ),
3244 crate::OpaqueActionResolutionOutcome::NonOpaqueBinding {
3245 package,
3246 export,
3247 kind,
3248 } => format!(
3249 "opaque Action `{}` selects `{package}` / `{export}` with package kind {kind:?}, not opaque",
3250 resolution.method_name
3251 ),
3252 };
3253 let mut diagnostic = ComponentDiagnostic::error("PSC1131", message);
3254 diagnostic.provenance = Some(resolution.provenance.clone());
3255 Some(diagnostic)
3256 })
3257 .collect()
3258}
3259
3260fn collect_resource_declarations(
3261 components: &[ComponentNode],
3262 resolutions: &[crate::ResourceEndpointResolution],
3263 semantic_types: &SemanticTypeModel,
3264 bindings: Option<&crate::BindingTable>,
3265) -> BTreeMap<ResourceId, crate::ResourceDeclaration> {
3266 let mut declarations = BTreeMap::new();
3267 for component in components {
3268 for resource in &component.resource_declaration_candidates {
3269 let Some(resolution) = resolutions.iter().find(|resolution| {
3270 resolution.owner_component == resource.owner_component
3271 && resolution.field == resource.field
3272 }) else {
3273 continue;
3274 };
3275 let crate::ResourceEndpointResolutionOutcome::Resolved(endpoint) = &resolution.outcome
3276 else {
3277 continue;
3278 };
3279 let Some(declared_type) = resource.declared_type.as_ref() else {
3280 continue;
3281 };
3282 let Some(resolved_type) = semantic_types.resolve_declared_type(declared_type, bindings)
3283 else {
3284 continue;
3285 };
3286 let crate::SemanticType::Resource(resource_type) = resolved_type.semantic_type else {
3287 continue;
3288 };
3289 let execution_boundary = match endpoint.endpoint.execution_boundary {
3290 crate::SemanticPackageResourceExecutionBoundary::Client => {
3291 crate::ResourceExecutionBoundary::Client
3292 }
3293 crate::SemanticPackageResourceExecutionBoundary::Server => {
3294 crate::ResourceExecutionBoundary::Server
3295 }
3296 crate::SemanticPackageResourceExecutionBoundary::Shared => {
3297 crate::ResourceExecutionBoundary::Shared
3298 }
3299 };
3300 let key = format!("{}:{}", component.id.as_str(), resource.field);
3301 let declaration = crate::ResourceDeclaration::new(
3302 resource.owner_component.clone(),
3303 resource.field.clone(),
3304 key,
3305 *resource_type.data,
3306 *resource_type.error,
3307 execution_boundary,
3308 BTreeSet::new(),
3309 crate::ResourceRetryPolicy::ExplicitOnly,
3310 crate::ResourceInvalidationPolicy::ExplicitOnly,
3311 resource.provenance.clone(),
3312 );
3313 let Ok(declaration) = declaration else {
3314 continue;
3315 };
3316 declarations.insert(declaration.id.clone(), declaration);
3317 }
3318 }
3319 declarations
3320}
3321
3322fn collect_resource_lowering_diagnostics(
3323 resolutions: &[crate::ResourceEndpointResolution],
3324) -> Vec<ComponentDiagnostic> {
3325 resolutions
3326 .iter()
3327 .filter(|resolution| {
3328 !matches!(
3329 resolution.outcome,
3330 crate::ResourceEndpointResolutionOutcome::Resolved(_)
3331 )
3332 })
3333 .map(|resolution| {
3334 let message = match &resolution.outcome {
3335 crate::ResourceEndpointResolutionOutcome::MissingDesignator => format!(
3336 "resource declaration `{}` requires one imported semantic-package resource endpoint designator",
3337 resolution.field
3338 ),
3339 crate::ResourceEndpointResolutionOutcome::UnboundDesignator { designator } => format!(
3340 "resource declaration `{}` designator `{designator}` does not resolve to an imported semantic-package endpoint",
3341 resolution.field
3342 ),
3343 crate::ResourceEndpointResolutionOutcome::NonSemanticPackageBinding { designator } => format!(
3344 "resource declaration `{}` designator `{designator}` must bind a semantic-package resource export",
3345 resolution.field
3346 ),
3347 crate::ResourceEndpointResolutionOutcome::NonResourceBinding { designator, kind } => format!(
3348 "resource declaration `{}` designator `{designator}` resolves to package kind {kind:?}, not resource",
3349 resolution.field
3350 ),
3351 crate::ResourceEndpointResolutionOutcome::Resolved(_) => unreachable!(
3352 "resolved resource endpoints are valid N6-C13 lowering inputs"
3353 ),
3354 };
3355 let mut diagnostic = ComponentDiagnostic::error("PSC1128", message);
3356 diagnostic.provenance = Some(resolution.provenance.clone());
3357 diagnostic
3358 })
3359 .collect()
3360}
3361
3362fn collect_resource_activations(
3363 declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3364 instances: &ComponentInstancePlan,
3365) -> BTreeMap<ResourceActivationId, crate::ResourceActivation> {
3366 let mut activations = BTreeMap::new();
3367 for instance in instances.instances.values() {
3368 for declaration in declarations
3369 .values()
3370 .filter(|declaration| declaration.owner_component == instance.component)
3371 {
3372 let activation = declaration.activation_for(instance.id.clone());
3373 activations.insert(activation.id.clone(), activation);
3374 }
3375 }
3376 activations
3377}
3378
3379fn resolve_semantic_package_pure_call(
3380 expression: &mut ComputedExpression,
3381 module_path: &Path,
3382 bindings: &crate::BindingTable,
3383) {
3384 match &mut expression.kind {
3385 ComputedExpressionKind::Call { callee, arguments } => {
3386 for argument in arguments.iter_mut() {
3387 resolve_semantic_package_pure_call(argument, module_path, bindings);
3388 }
3389 let Some(binding) = bindings.resolve_import(module_path, callee) else {
3390 let operation = match (callee.as_str(), arguments.len()) {
3391 ("Math.abs", 1) => Some(crate::component_graph::BuiltinPureOperation::MathAbs),
3392 ("Math.floor", 1) => {
3393 Some(crate::component_graph::BuiltinPureOperation::MathFloor)
3394 }
3395 ("Math.ceil", 1) => {
3396 Some(crate::component_graph::BuiltinPureOperation::MathCeil)
3397 }
3398 ("Math.round", 1) => {
3399 Some(crate::component_graph::BuiltinPureOperation::MathRound)
3400 }
3401 ("Math.min", 2) => Some(crate::component_graph::BuiltinPureOperation::MathMin),
3402 ("Math.max", 2) => Some(crate::component_graph::BuiltinPureOperation::MathMax),
3403 _ => None,
3404 };
3405 if let Some(operation) = operation {
3406 expression.kind = ComputedExpressionKind::BuiltinPureCall {
3407 operation,
3408 arguments: std::mem::take(arguments),
3409 };
3410 }
3411 return;
3412 };
3413 let crate::ImportBindingTarget::SemanticPackage {
3414 package,
3415 version,
3416 integrity,
3417 export,
3418 kind: crate::semantic_package::SemanticPackageKind::Pure,
3419 runtime_module,
3420 resume_policy,
3421 pure_operation: Some(operation),
3422 ..
3423 } = &binding.target
3424 else {
3425 return;
3426 };
3427 expression.kind = ComputedExpressionKind::SemanticPackagePureCall {
3428 local_name: callee.clone(),
3429 package: package.clone(),
3430 version: version.clone(),
3431 integrity: integrity.clone(),
3432 export: export.clone(),
3433 runtime_module: runtime_module.clone(),
3434 resume_policy: resume_policy.clone(),
3435 operation: *operation,
3436 arguments: std::mem::take(arguments),
3437 };
3438 }
3439 ComputedExpressionKind::BuiltinPureCall { arguments, .. } => {
3440 for argument in arguments {
3441 resolve_semantic_package_pure_call(argument, module_path, bindings);
3442 }
3443 }
3444 ComputedExpressionKind::SemanticPackagePureCall { arguments, .. } => {
3445 for argument in arguments {
3446 resolve_semantic_package_pure_call(argument, module_path, bindings);
3447 }
3448 }
3449 ComputedExpressionKind::Template { expressions, .. } => {
3450 for expression in expressions {
3451 resolve_semantic_package_pure_call(expression, module_path, bindings);
3452 }
3453 }
3454 ComputedExpressionKind::MemberAccess { object, .. }
3455 | ComputedExpressionKind::Unary {
3456 operand: object, ..
3457 } => {
3458 resolve_semantic_package_pure_call(object, module_path, bindings);
3459 }
3460 ComputedExpressionKind::IndexAccess { object, index } => {
3461 resolve_semantic_package_pure_call(object, module_path, bindings);
3462 resolve_semantic_package_pure_call(index, module_path, bindings);
3463 }
3464 ComputedExpressionKind::Conditional {
3465 condition,
3466 when_true,
3467 when_false,
3468 } => {
3469 resolve_semantic_package_pure_call(condition, module_path, bindings);
3470 resolve_semantic_package_pure_call(when_true, module_path, bindings);
3471 resolve_semantic_package_pure_call(when_false, module_path, bindings);
3472 }
3473 ComputedExpressionKind::Arithmetic { left, right, .. }
3474 | ComputedExpressionKind::Comparison { left, right, .. }
3475 | ComputedExpressionKind::Logical { left, right, .. }
3476 | ComputedExpressionKind::NullishCoalescing { left, right } => {
3477 resolve_semantic_package_pure_call(left, module_path, bindings);
3478 resolve_semantic_package_pure_call(right, module_path, bindings);
3479 }
3480 ComputedExpressionKind::Literal(_) | ComputedExpressionKind::ThisMember(_) => {}
3481 }
3482}
3483
3484fn contains_semantic_package_pure_call(expression: &ComputedExpression, callee: &str) -> bool {
3485 match &expression.kind {
3486 ComputedExpressionKind::SemanticPackagePureCall {
3487 local_name,
3488 arguments,
3489 ..
3490 } => {
3491 local_name == callee
3492 || arguments
3493 .iter()
3494 .any(|argument| contains_semantic_package_pure_call(argument, callee))
3495 }
3496 ComputedExpressionKind::Call { arguments, .. } => arguments
3497 .iter()
3498 .any(|argument| contains_semantic_package_pure_call(argument, callee)),
3499 ComputedExpressionKind::BuiltinPureCall { arguments, .. } => arguments
3500 .iter()
3501 .any(|argument| contains_semantic_package_pure_call(argument, callee)),
3502 ComputedExpressionKind::Template { expressions, .. } => expressions
3503 .iter()
3504 .any(|expression| contains_semantic_package_pure_call(expression, callee)),
3505 ComputedExpressionKind::MemberAccess { object, .. }
3506 | ComputedExpressionKind::Unary {
3507 operand: object, ..
3508 } => contains_semantic_package_pure_call(object, callee),
3509 ComputedExpressionKind::IndexAccess { object, index } => {
3510 contains_semantic_package_pure_call(object, callee)
3511 || contains_semantic_package_pure_call(index, callee)
3512 }
3513 ComputedExpressionKind::Conditional {
3514 condition,
3515 when_true,
3516 when_false,
3517 } => {
3518 contains_semantic_package_pure_call(condition, callee)
3519 || contains_semantic_package_pure_call(when_true, callee)
3520 || contains_semantic_package_pure_call(when_false, callee)
3521 }
3522 ComputedExpressionKind::Arithmetic { left, right, .. }
3523 | ComputedExpressionKind::Comparison { left, right, .. }
3524 | ComputedExpressionKind::Logical { left, right, .. }
3525 | ComputedExpressionKind::NullishCoalescing { left, right } => {
3526 contains_semantic_package_pure_call(left, callee)
3527 || contains_semantic_package_pure_call(right, callee)
3528 }
3529 ComputedExpressionKind::Literal(_) | ComputedExpressionKind::ThisMember(_) => false,
3530 }
3531}
3532
3533fn contains_builtin_pure_call(expression: &ComputedExpression, callee: &str) -> bool {
3534 match &expression.kind {
3535 ComputedExpressionKind::BuiltinPureCall {
3536 operation,
3537 arguments,
3538 } => {
3539 let operation_callee = match operation {
3540 crate::component_graph::BuiltinPureOperation::MathAbs => "Math.abs",
3541 crate::component_graph::BuiltinPureOperation::MathFloor => "Math.floor",
3542 crate::component_graph::BuiltinPureOperation::MathCeil => "Math.ceil",
3543 crate::component_graph::BuiltinPureOperation::MathRound => "Math.round",
3544 crate::component_graph::BuiltinPureOperation::MathMin => "Math.min",
3545 crate::component_graph::BuiltinPureOperation::MathMax => "Math.max",
3546 };
3547 operation_callee == callee
3548 || arguments
3549 .iter()
3550 .any(|argument| contains_builtin_pure_call(argument, callee))
3551 }
3552 ComputedExpressionKind::Call { arguments, .. }
3553 | ComputedExpressionKind::SemanticPackagePureCall { arguments, .. } => arguments
3554 .iter()
3555 .any(|argument| contains_builtin_pure_call(argument, callee)),
3556 ComputedExpressionKind::Template { expressions, .. } => expressions
3557 .iter()
3558 .any(|expression| contains_builtin_pure_call(expression, callee)),
3559 ComputedExpressionKind::MemberAccess { object, .. }
3560 | ComputedExpressionKind::Unary {
3561 operand: object, ..
3562 } => contains_builtin_pure_call(object, callee),
3563 ComputedExpressionKind::IndexAccess { object, index } => {
3564 contains_builtin_pure_call(object, callee) || contains_builtin_pure_call(index, callee)
3565 }
3566 ComputedExpressionKind::Conditional {
3567 condition,
3568 when_true,
3569 when_false,
3570 } => {
3571 contains_builtin_pure_call(condition, callee)
3572 || contains_builtin_pure_call(when_true, callee)
3573 || contains_builtin_pure_call(when_false, callee)
3574 }
3575 ComputedExpressionKind::Arithmetic { left, right, .. }
3576 | ComputedExpressionKind::Comparison { left, right, .. }
3577 | ComputedExpressionKind::Logical { left, right, .. }
3578 | ComputedExpressionKind::NullishCoalescing { left, right } => {
3579 contains_builtin_pure_call(left, callee) || contains_builtin_pure_call(right, callee)
3580 }
3581 ComputedExpressionKind::Literal(_) | ComputedExpressionKind::ThisMember(_) => false,
3582 }
3583}
3584
3585fn collect_computed_cycle_diagnostics(
3586 analysis: &IrReactiveCycleAnalysis,
3587 computed_values: &BTreeMap<SemanticId, ComputedValue>,
3588) -> Vec<ComponentDiagnostic> {
3589 analysis
3590 .cycles
3591 .iter()
3592 .filter_map(|cycle| {
3593 let first = cycle.nodes.first()?;
3594 let computed = computed_values
3595 .values()
3596 .find(|computed| computed.id.as_str() == first)?;
3597 Some(ComponentDiagnostic {
3598 severity: ComponentDiagnosticSeverity::Error,
3599 effect_id: None,
3600 statement_id: None,
3601 context_declaration_candidate_id: None,
3602 context_id: None,
3603 provider_id: None,
3604 consumer_id: None,
3605 slot_id: None,
3606 invocation_id: None,
3607 component_instance_id: None,
3608 slot_binding_id: None,
3609 structural_region_id: None,
3610 component_id: None,
3611 provider_instance_id: None,
3612 consumer_instance_id: None,
3613 secondary_labels: Vec::new(),
3614 code: ComputedDiagnosticCode::DependencyCycle.as_str().to_string(),
3615 message: format!(
3616 "computed dependency cycle detected among: {}",
3617 cycle.nodes.join(", ")
3618 ),
3619 provenance: Some(computed.provenance.clone()),
3620 })
3621 })
3622 .collect()
3623}
3624
3625fn collect_computed_semantic_diagnostics(
3626 components: &[ComponentNode],
3627 computed_values: &BTreeMap<SemanticId, ComputedValue>,
3628 resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3629 expression_graph: &ExpressionGraph,
3630 semantic_types: &SemanticTypeModel,
3631 provenance: &BTreeMap<SemanticId, SourceProvenance>,
3632) -> Vec<ComponentDiagnostic> {
3633 let mut diagnostics = collect_invalid_computed_declaration_diagnostics(components, provenance);
3634 diagnostics.extend(collect_computed_body_and_read_diagnostics(
3635 components,
3636 computed_values,
3637 resource_declarations,
3638 expression_graph,
3639 ));
3640 diagnostics.extend(collect_computed_type_diagnostics(
3641 computed_values,
3642 semantic_types,
3643 ));
3644 diagnostics.extend(collect_computed_conditional_diagnostics(
3645 components,
3646 expression_graph,
3647 semantic_types,
3648 ));
3649 sort_computed_diagnostics(&mut diagnostics);
3650 diagnostics
3651}
3652
3653fn collect_computed_conditional_diagnostics(
3654 components: &[ComponentNode],
3655 expression_graph: &ExpressionGraph,
3656 semantic_types: &SemanticTypeModel,
3657) -> Vec<ComponentDiagnostic> {
3658 expression_graph
3659 .nodes
3660 .values()
3661 .filter_map(|node| {
3662 let crate::ExpressionNodeKind::Conditional { condition, .. } = &node.kind else {
3663 return None;
3664 };
3665 let condition_type = semantic_types
3666 .assignments
3667 .get(condition)
3668 .map(|assignment| assignment.semantic_type.clone())
3669 .or_else(|| match &expression_graph.node(condition)?.kind {
3670 crate::ExpressionNodeKind::ThisMember { name } => components
3671 .iter()
3672 .find(|component| {
3673 component
3674 .methods
3675 .iter()
3676 .any(|method| component.id.computed(&method.name) == node.owner)
3677 })
3678 .and_then(|component| {
3679 component
3680 .state_fields
3681 .iter()
3682 .find(|field| field.name == *name)
3683 })
3684 .and_then(|field| semantic_types.assignments.get(&field.id))
3685 .map(|assignment| assignment.semantic_type.clone()),
3686 _ => None,
3687 })
3688 .unwrap_or(crate::SemanticType::Unknown);
3689 if is_boolean_computed_condition(&condition_type) {
3690 None
3691 } else {
3692 Some(ComponentDiagnostic {
3693 severity: ComponentDiagnosticSeverity::Error,
3694 effect_id: None,
3695 statement_id: None,
3696 context_declaration_candidate_id: None,
3697 context_id: None,
3698 provider_id: None,
3699 consumer_id: None,
3700 slot_id: None,
3701 invocation_id: None,
3702 component_instance_id: None,
3703 slot_binding_id: None,
3704 structural_region_id: None,
3705 component_id: None,
3706 provider_instance_id: None,
3707 consumer_instance_id: None,
3708 secondary_labels: Vec::new(),
3709 code: crate::TypeDiagnosticCode::InvalidCondition
3710 .as_str()
3711 .to_string(),
3712 message: format!(
3713 "computed conditional requires boolean, but has {}",
3714 crate::semantic_type_text(&condition_type)
3715 ),
3716 provenance: Some(node.provenance.clone()),
3717 })
3718 }
3719 })
3720 .collect()
3721}
3722
3723fn is_boolean_computed_condition(semantic_type: &crate::SemanticType) -> bool {
3724 match semantic_type {
3725 crate::SemanticType::Boolean | crate::SemanticType::BooleanLiteral(_) => true,
3726 crate::SemanticType::Union(members) => members.iter().all(is_boolean_computed_condition),
3727 _ => false,
3728 }
3729}
3730
3731fn collect_invalid_computed_declaration_diagnostics(
3732 components: &[ComponentNode],
3733 provenance: &BTreeMap<SemanticId, SourceProvenance>,
3734) -> Vec<ComponentDiagnostic> {
3735 components
3736 .iter()
3737 .flat_map(|component| &component.methods)
3738 .filter(|method| {
3739 method
3740 .decorators
3741 .iter()
3742 .any(|decorator| decorator == "computed")
3743 && !method.is_getter
3744 })
3745 .map(|method| ComponentDiagnostic {
3746 severity: ComponentDiagnosticSeverity::Error,
3747 effect_id: None,
3748 statement_id: None,
3749 context_declaration_candidate_id: None,
3750 context_id: None,
3751 provider_id: None,
3752 consumer_id: None,
3753 slot_id: None,
3754 invocation_id: None,
3755 component_instance_id: None,
3756 slot_binding_id: None,
3757 structural_region_id: None,
3758 component_id: None,
3759 provider_instance_id: None,
3760 consumer_instance_id: None,
3761 secondary_labels: Vec::new(),
3762 code: ComputedDiagnosticCode::InvalidDeclaration
3763 .as_str()
3764 .to_string(),
3765 message: format!(
3766 "@computed() declaration `{}` must decorate a getter",
3767 method.name
3768 ),
3769 provenance: Some(
3770 provenance
3771 .get(&method.id)
3772 .expect("component methods should have canonical provenance")
3773 .clone(),
3774 ),
3775 })
3776 .collect()
3777}
3778
3779fn collect_computed_body_and_read_diagnostics(
3780 components: &[ComponentNode],
3781 computed_values: &BTreeMap<SemanticId, ComputedValue>,
3782 resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3783 expression_graph: &ExpressionGraph,
3784) -> Vec<ComponentDiagnostic> {
3785 let mut diagnostics = Vec::new();
3786
3787 for computed in computed_values.values() {
3788 let component_id = computed
3789 .owner
3790 .entity_id()
3791 .expect("computed values should have component owners");
3792 let component = components
3793 .iter()
3794 .find(|component| component.id == *component_id)
3795 .expect("computed values should have owning components");
3796 let method = component
3797 .methods
3798 .iter()
3799 .find(|method| method.id == computed.method)
3800 .expect("computed values should have authored methods");
3801
3802 if method.computed_expression.is_none() {
3803 diagnostics.push(ComponentDiagnostic {
3804 severity: ComponentDiagnosticSeverity::Error,
3805 effect_id: None,
3806 statement_id: None,
3807 context_declaration_candidate_id: None,
3808 context_id: None,
3809 provider_id: None,
3810 consumer_id: None,
3811 slot_id: None,
3812 invocation_id: None,
3813 component_instance_id: None,
3814 slot_binding_id: None,
3815 structural_region_id: None,
3816 component_id: None,
3817 provider_instance_id: None,
3818 consumer_instance_id: None,
3819 secondary_labels: Vec::new(),
3820 code: ComputedDiagnosticCode::UnsupportedBody.as_str().to_string(),
3821 message: format!(
3822 "computed getter `{}` has an unsupported body",
3823 computed.name
3824 ),
3825 provenance: Some(computed.provenance.clone()),
3826 });
3827 }
3828
3829 diagnostics.extend(
3830 expression_graph
3831 .nodes_for(&computed.id)
3832 .into_iter()
3833 .filter_map(|node| {
3834 unresolved_computed_read_diagnostic(
3835 component,
3836 computed_values,
3837 resource_declarations,
3838 computed,
3839 node,
3840 )
3841 }),
3842 );
3843 diagnostics.extend(computed_resource_projection_diagnostics(
3844 component,
3845 computed,
3846 resource_declarations,
3847 expression_graph,
3848 ));
3849 }
3850
3851 diagnostics
3852}
3853
3854fn unresolved_computed_read_diagnostic(
3855 component: &ComponentNode,
3856 computed_values: &BTreeMap<SemanticId, ComputedValue>,
3857 resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3858 computed: &ComputedValue,
3859 node: &ExpressionNode,
3860) -> Option<ComponentDiagnostic> {
3861 let ExpressionNodeKind::ThisMember { name } = &node.kind else {
3862 return None;
3863 };
3864 let resolved = component
3865 .state_fields
3866 .iter()
3867 .any(|field| field.name == *name)
3868 || computed_values.contains_key(&component.id.computed(name))
3869 || resource_declarations.contains_key(&ResourceId::for_owner(&component.id, name));
3870 (!resolved).then(|| ComponentDiagnostic {
3871 severity: ComponentDiagnosticSeverity::Error,
3872 effect_id: None,
3873 statement_id: None,
3874 context_declaration_candidate_id: None,
3875 context_id: None,
3876 provider_id: None,
3877 consumer_id: None,
3878 slot_id: None,
3879 invocation_id: None,
3880 component_instance_id: None,
3881 slot_binding_id: None,
3882 structural_region_id: None,
3883 component_id: None,
3884 provider_instance_id: None,
3885 consumer_instance_id: None,
3886 secondary_labels: Vec::new(),
3887 code: ComputedDiagnosticCode::UnresolvedRead.as_str().to_string(),
3888 message: format!(
3889 "computed getter `{}` reads unresolved member `this.{name}`",
3890 computed.name
3891 ),
3892 provenance: Some(node.provenance.clone()),
3893 })
3894}
3895
3896fn computed_resource_projection_diagnostics(
3897 component: &ComponentNode,
3898 computed: &ComputedValue,
3899 resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3900 expression_graph: &ExpressionGraph,
3901) -> Vec<ComponentDiagnostic> {
3902 let nodes = expression_graph.nodes_for(&computed.id);
3903 nodes
3904 .iter()
3905 .filter_map(|node| {
3906 let ExpressionNodeKind::ThisMember { name } = &node.kind else {
3907 return None;
3908 };
3909 resource_declarations
3910 .contains_key(&ResourceId::for_owner(&component.id, name))
3911 .then_some((node, name))
3912 })
3913 .filter(|(node, _)| {
3914 !nodes.iter().any(|candidate| {
3915 is_direct_resource_projection(candidate, &nodes)
3916 && matches!(
3917 &candidate.kind,
3918 ExpressionNodeKind::MemberAccess { object, .. } if object == &node.id
3919 )
3920 })
3921 })
3922 .map(|(node, name)| ComponentDiagnostic {
3923 severity: ComponentDiagnosticSeverity::Error,
3924 effect_id: None,
3925 statement_id: None,
3926 context_declaration_candidate_id: None,
3927 context_id: None,
3928 provider_id: None,
3929 consumer_id: None,
3930 slot_id: None,
3931 invocation_id: None,
3932 component_instance_id: None,
3933 slot_binding_id: None,
3934 structural_region_id: None,
3935 component_id: None,
3936 provider_instance_id: None,
3937 consumer_instance_id: None,
3938 secondary_labels: Vec::new(),
3939 code: ComputedDiagnosticCode::UnsupportedBody.as_str().to_string(),
3940 message: format!(
3941 "computed getter `{}` may only directly project `this.{name}.data`, `this.{name}.error`, or `this.{name}.state`",
3942 computed.name
3943 ),
3944 provenance: Some(node.provenance.clone()),
3945 })
3946 .collect()
3947}
3948
3949fn is_direct_resource_projection(node: &ExpressionNode, nodes: &[&ExpressionNode]) -> bool {
3950 let ExpressionNodeKind::MemberAccess {
3951 property,
3952 optional: false,
3953 ..
3954 } = &node.kind
3955 else {
3956 return false;
3957 };
3958 matches!(property.as_str(), "data" | "error" | "state")
3959 && !nodes.iter().any(|candidate| {
3960 matches!(
3961 &candidate.kind,
3962 ExpressionNodeKind::MemberAccess { object, .. }
3963 | ExpressionNodeKind::IndexAccess { object, .. }
3964 if object == &node.id
3965 )
3966 })
3967}
3968
3969fn collect_computed_type_diagnostics(
3970 computed_values: &BTreeMap<SemanticId, ComputedValue>,
3971 semantic_types: &SemanticTypeModel,
3972) -> Vec<ComponentDiagnostic> {
3973 let mut diagnostics = Vec::new();
3974
3975 for computed_type in semantic_types.computed_values.values() {
3976 let computed = computed_values
3977 .get(&computed_type.computed)
3978 .expect("computed type records should have computed entities");
3979 if computed_type.declared_return_compatible == Some(false) {
3980 let declared = computed_type
3981 .declared_return_type
3982 .as_ref()
3983 .expect("incompatible computed return types should be declared");
3984 diagnostics.push(ComponentDiagnostic {
3985 severity: ComponentDiagnosticSeverity::Error,
3986 effect_id: None,
3987 statement_id: None,
3988 context_declaration_candidate_id: None,
3989 context_id: None,
3990 provider_id: None,
3991 consumer_id: None,
3992 slot_id: None,
3993 invocation_id: None,
3994 component_instance_id: None,
3995 slot_binding_id: None,
3996 structural_region_id: None,
3997 component_id: None,
3998 provider_instance_id: None,
3999 consumer_instance_id: None,
4000 secondary_labels: Vec::new(),
4001 code: ComputedDiagnosticCode::TypeMismatch.as_str().to_string(),
4002 message: format!(
4003 "computed getter `{}` returns `{}` but declares `{}`",
4004 computed.name,
4005 crate::semantic_type_text(&computed_type.semantic_type),
4006 crate::semantic_type_text(declared)
4007 ),
4008 provenance: Some(computed_type.provenance.clone()),
4009 });
4010 }
4011 if computed_type.serialization == crate::SerializationCompatibility::NotSerializable {
4012 diagnostics.push(ComponentDiagnostic {
4013 severity: ComponentDiagnosticSeverity::Error,
4014 effect_id: None,
4015 statement_id: None,
4016 context_declaration_candidate_id: None,
4017 context_id: None,
4018 provider_id: None,
4019 consumer_id: None,
4020 slot_id: None,
4021 invocation_id: None,
4022 component_instance_id: None,
4023 slot_binding_id: None,
4024 structural_region_id: None,
4025 component_id: None,
4026 provider_instance_id: None,
4027 consumer_instance_id: None,
4028 secondary_labels: Vec::new(),
4029 code: ComputedDiagnosticCode::SerializationViolation
4030 .as_str()
4031 .to_string(),
4032 message: format!(
4033 "computed getter `{}` has a non-serializable result",
4034 computed.name
4035 ),
4036 provenance: Some(computed_type.provenance.clone()),
4037 });
4038 }
4039 }
4040
4041 diagnostics
4042}
4043
4044fn sort_computed_diagnostics(diagnostics: &mut [ComponentDiagnostic]) {
4045 diagnostics.sort_by(|left, right| {
4046 (
4047 left.code.as_str(),
4048 left.provenance.as_ref().map(|provenance| &provenance.path),
4049 left.provenance
4050 .as_ref()
4051 .map(|provenance| provenance.span.start),
4052 left.provenance
4053 .as_ref()
4054 .map(|provenance| provenance.span.end),
4055 left.message.as_str(),
4056 )
4057 .cmp(&(
4058 right.code.as_str(),
4059 right.provenance.as_ref().map(|provenance| &provenance.path),
4060 right
4061 .provenance
4062 .as_ref()
4063 .map(|provenance| provenance.span.start),
4064 right
4065 .provenance
4066 .as_ref()
4067 .map(|provenance| provenance.span.end),
4068 right.message.as_str(),
4069 ))
4070 });
4071}
4072
4073fn build_computed_reactive_products(
4074 components: &[ComponentNode],
4075 computed_values: &BTreeMap<SemanticId, ComputedValue>,
4076 effects: &BTreeMap<SemanticId, Effect>,
4077 resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
4078 references: &[SemanticReference],
4079 provenance: &BTreeMap<SemanticId, SourceProvenance>,
4080) -> (
4081 IrReactiveGraph,
4082 IrReactiveTransitiveAnalysis,
4083 IrReactiveCycleAnalysis,
4084 IrComputedEvaluationPlan,
4085) {
4086 let graph = crate::build_reactive_graph(
4087 components,
4088 computed_values,
4089 effects,
4090 resource_declarations,
4091 references,
4092 provenance,
4093 );
4094 let transitive_analysis = crate::analyze_reactive_transitive_graph(&graph);
4095 let cycle_analysis = crate::analyze_reactive_cycles(&graph);
4096 let evaluation_plan = crate::plan_computed_evaluation(&graph);
4097 (graph, transitive_analysis, cycle_analysis, evaluation_plan)
4098}
4099
4100fn extend_template_references(
4101 references: &mut Vec<SemanticReference>,
4102 components: &[ComponentNode],
4103 computed_values: &BTreeMap<SemanticId, ComputedValue>,
4104 template_entities: &[TemplateSemanticEntity],
4105 ownership: &BTreeMap<SemanticId, SemanticOwner>,
4106) {
4107 references.extend(build_template_state_references(
4108 components,
4109 template_entities,
4110 ownership,
4111 ));
4112 references.extend(build_template_computed_references(
4113 components,
4114 computed_values,
4115 template_entities,
4116 ownership,
4117 ));
4118 references.extend(build_template_event_references(
4119 components,
4120 template_entities,
4121 ownership,
4122 ));
4123 references.extend(build_template_local_references(
4124 components,
4125 template_entities,
4126 ownership,
4127 ));
4128}
4129
4130fn build_form_field_binding_references(
4131 bindings: &BTreeMap<FieldBindingId, FormFieldBinding>,
4132) -> Vec<SemanticReference> {
4133 bindings
4134 .values()
4135 .flat_map(|binding| {
4136 [
4137 SemanticReference {
4138 kind: SemanticReferenceKind::FieldBindingField,
4139 source: binding.id.as_semantic_id().clone(),
4140 target: binding.field.as_semantic_id().clone(),
4141 provenance: binding.expression_provenance.clone(),
4142 },
4143 SemanticReference {
4144 kind: SemanticReferenceKind::FieldBindingForm,
4145 source: binding.id.as_semantic_id().clone(),
4146 target: binding.form.as_semantic_id().clone(),
4147 provenance: binding.expression_provenance.clone(),
4148 },
4149 ]
4150 })
4151 .collect()
4152}
4153
4154fn computed_call_purity_kind(
4155 component: &ComponentNode,
4156 callee: &str,
4157) -> crate::ComputedPurityViolationKind {
4158 if matches!(callee, "resource" | "this.resource") {
4159 return crate::ComputedPurityViolationKind::Resource;
4160 }
4161 if matches!(
4162 callee,
4163 "Math.random"
4164 | "Date.now"
4165 | "performance.now"
4166 | "crypto.randomUUID"
4167 | "crypto.getRandomValues"
4168 ) {
4169 return crate::ComputedPurityViolationKind::NondeterministicOperation;
4170 }
4171 if callee.starts_with("console.")
4172 || matches!(
4173 callee,
4174 "fetch" | "setTimeout" | "setInterval" | "queueMicrotask"
4175 )
4176 {
4177 return crate::ComputedPurityViolationKind::Effect;
4178 }
4179 if let Some(name) = callee.strip_prefix("this.") {
4180 if let Some(method) = component.methods.iter().find(|method| method.name == name) {
4181 if method.is_action()
4182 || method
4183 .decorators
4184 .iter()
4185 .any(|decorator| decorator == "action")
4186 {
4187 return crate::ComputedPurityViolationKind::Action;
4188 }
4189 if method
4190 .decorators
4191 .iter()
4192 .any(|decorator| decorator == "effect")
4193 {
4194 return crate::ComputedPurityViolationKind::Effect;
4195 }
4196 if method
4197 .decorators
4198 .iter()
4199 .any(|decorator| decorator == "resource")
4200 {
4201 return crate::ComputedPurityViolationKind::Resource;
4202 }
4203 }
4204 }
4205 crate::ComputedPurityViolationKind::ArbitraryMethodCall
4206}
4207
4208fn build_computed_references(
4209 components: &[ComponentNode],
4210 computed_values: &BTreeMap<SemanticId, ComputedValue>,
4211 resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
4212 expression_graph: &ExpressionGraph,
4213) -> Vec<SemanticReference> {
4214 let mut references = BTreeMap::new();
4215
4216 for computed in computed_values.values() {
4217 let Some(component_id) = computed.owner.entity_id() else {
4218 continue;
4219 };
4220 let Some(component) = components
4221 .iter()
4222 .find(|component| component.id == *component_id)
4223 else {
4224 continue;
4225 };
4226
4227 let nodes = expression_graph.nodes_for(&computed.id);
4228 let direct_resource_projection_objects = nodes
4229 .iter()
4230 .filter(|node| is_direct_resource_projection(node, &nodes))
4231 .filter_map(|node| match &node.kind {
4232 crate::ExpressionNodeKind::MemberAccess { object, .. } => Some(object.clone()),
4233 _ => None,
4234 })
4235 .collect::<BTreeSet<_>>();
4236
4237 for node in nodes {
4238 let crate::ExpressionNodeKind::ThisMember { name } = &node.kind else {
4239 continue;
4240 };
4241 let reference = if let Some(field) = component
4242 .state_fields
4243 .iter()
4244 .find(|field| field.name == *name)
4245 {
4246 SemanticReference {
4247 kind: SemanticReferenceKind::ComputedState,
4248 source: computed.id.clone(),
4249 target: field.id.clone(),
4250 provenance: computed.provenance.clone(),
4251 }
4252 } else if let Some(target) = computed_values.get(&component.id.computed(name)) {
4253 SemanticReference {
4254 kind: SemanticReferenceKind::ComputedComputed,
4255 source: computed.id.clone(),
4256 target: target.id.clone(),
4257 provenance: computed.provenance.clone(),
4258 }
4259 } else if direct_resource_projection_objects.contains(&node.id) {
4260 let target = ResourceId::for_owner(&component.id, name);
4261 let Some(declaration) = resource_declarations.get(&target) else {
4262 continue;
4263 };
4264 SemanticReference {
4265 kind: SemanticReferenceKind::ComputedResource,
4266 source: computed.id.clone(),
4267 target: declaration.id.as_semantic_id().clone(),
4268 provenance: node.provenance.clone(),
4269 }
4270 } else {
4271 continue;
4272 };
4273 references.insert(
4274 (reference.source.clone(), reference.target.clone()),
4275 reference,
4276 );
4277 }
4278 }
4279
4280 references.into_values().collect()
4281}
4282
4283fn build_effect_references(
4284 components: &[ComponentNode],
4285 effects: &BTreeMap<SemanticId, Effect>,
4286 computed_values: &BTreeMap<SemanticId, ComputedValue>,
4287 expression_graph: &ExpressionGraph,
4288) -> Vec<SemanticReference> {
4289 let mut references = Vec::new();
4290 for effect in effects.values() {
4291 let Some(component_id) = effect.owner.entity_id() else {
4292 continue;
4293 };
4294 let Some(component) = components
4295 .iter()
4296 .find(|component| component.id == *component_id)
4297 else {
4298 continue;
4299 };
4300 for node in expression_graph.nodes_for(&effect.id) {
4301 let crate::ExpressionNodeKind::ThisMember { name } = &node.kind else {
4302 continue;
4303 };
4304 let reference = if let Some(field) = component
4305 .state_fields
4306 .iter()
4307 .find(|field| field.name == *name)
4308 {
4309 SemanticReference {
4310 kind: SemanticReferenceKind::EffectState,
4311 source: effect.id.clone(),
4312 target: field.id.clone(),
4313 provenance: node.provenance.clone(),
4314 }
4315 } else if let Some(computed) = computed_values.get(&component.id.computed(name)) {
4316 SemanticReference {
4317 kind: SemanticReferenceKind::EffectComputed,
4318 source: effect.id.clone(),
4319 target: computed.id.clone(),
4320 provenance: node.provenance.clone(),
4321 }
4322 } else {
4323 continue;
4324 };
4325 references.push(reference);
4326 }
4327 }
4328 references.sort_by(|left, right| {
4329 (
4330 left.source.as_str(),
4331 left.target.as_str(),
4332 left.provenance.span.start,
4333 )
4334 .cmp(&(
4335 right.source.as_str(),
4336 right.target.as_str(),
4337 right.provenance.span.start,
4338 ))
4339 });
4340 references.dedup_by(|left, right| {
4341 left.kind == right.kind && left.source == right.source && left.target == right.target
4342 });
4343 references
4344}
4345
4346fn build_provider_references(
4347 providers: &BTreeMap<ProviderId, ProviderEntity>,
4348) -> Vec<SemanticReference> {
4349 providers
4350 .values()
4351 .map(|provider| SemanticReference {
4352 kind: SemanticReferenceKind::ProvidesContext,
4353 source: provider.id.as_semantic_id().clone(),
4354 target: provider.context.as_semantic_id().clone(),
4355 provenance: provider.provenance.clone(),
4356 })
4357 .collect()
4358}
4359
4360fn build_consumer_references(
4361 consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
4362) -> Vec<SemanticReference> {
4363 consumers
4364 .values()
4365 .filter_map(|consumer| {
4366 let ContextResolutionState::Resolved(context) = &consumer.context_resolution else {
4367 return None;
4368 };
4369 Some(SemanticReference {
4370 kind: SemanticReferenceKind::ConsumesContext,
4371 source: consumer.id.as_semantic_id().clone(),
4372 target: context.as_semantic_id().clone(),
4373 provenance: consumer.provenance.clone(),
4374 })
4375 })
4376 .collect()
4377}
4378
4379fn build_context_resolution_references(
4380 resolutions: &BTreeMap<ConsumerId, ContextResolution>,
4381) -> Vec<SemanticReference> {
4382 resolutions
4383 .values()
4384 .filter_map(|resolution| {
4385 let ContextResolutionResult::Provider { provider, .. } = &resolution.result else {
4386 return None;
4387 };
4388 Some(SemanticReference {
4389 kind: SemanticReferenceKind::ResolvesToProvider,
4390 source: resolution.consumer.as_semantic_id().clone(),
4391 target: provider.as_semantic_id().clone(),
4392 provenance: resolution.provenance.clone(),
4393 })
4394 })
4395 .collect()
4396}
4397
4398fn build_template_state_references(
4399 components: &[ComponentNode],
4400 template_entities: &[TemplateSemanticEntity],
4401 ownership: &BTreeMap<SemanticId, SemanticOwner>,
4402) -> Vec<SemanticReference> {
4403 template_entities
4404 .iter()
4405 .filter(|entity| {
4406 matches!(
4407 entity.kind,
4408 TemplateSemanticKind::Binding
4409 | TemplateSemanticKind::AttributeBinding
4410 | TemplateSemanticKind::Conditional
4411 | TemplateSemanticKind::List
4412 )
4413 })
4414 .filter_map(|entity| {
4415 let field_name = entity.expression.as_deref().and_then(this_member_name)?;
4416 let component = template_entity_component(components, ownership, entity)?;
4417 let field = component
4418 .state_fields
4419 .iter()
4420 .find(|field| field.name == field_name)?;
4421
4422 Some(SemanticReference {
4423 kind: SemanticReferenceKind::TemplateState,
4424 source: entity.id.clone(),
4425 target: field.id.clone(),
4426 provenance: entity.provenance.clone(),
4427 })
4428 })
4429 .collect()
4430}
4431
4432fn build_template_computed_references(
4433 components: &[ComponentNode],
4434 computed_values: &BTreeMap<SemanticId, ComputedValue>,
4435 template_entities: &[TemplateSemanticEntity],
4436 ownership: &BTreeMap<SemanticId, SemanticOwner>,
4437) -> Vec<SemanticReference> {
4438 template_entities
4439 .iter()
4440 .filter(|entity| {
4441 matches!(
4442 entity.kind,
4443 TemplateSemanticKind::Binding
4444 | TemplateSemanticKind::AttributeBinding
4445 | TemplateSemanticKind::Conditional
4446 | TemplateSemanticKind::List
4447 )
4448 })
4449 .filter_map(|entity| {
4450 let computed_name = entity.expression.as_deref().and_then(this_member_name)?;
4451 let component = template_entity_component(components, ownership, entity)?;
4452 if component
4453 .state_fields
4454 .iter()
4455 .any(|field| field.name == computed_name)
4456 {
4457 return None;
4458 }
4459 let computed = computed_values.get(&component.id.computed(computed_name))?;
4460
4461 Some(SemanticReference {
4462 kind: SemanticReferenceKind::TemplateComputed,
4463 source: entity.id.clone(),
4464 target: computed.id.clone(),
4465 provenance: entity.provenance.clone(),
4466 })
4467 })
4468 .collect()
4469}
4470
4471fn build_template_event_references(
4472 components: &[ComponentNode],
4473 template_entities: &[TemplateSemanticEntity],
4474 ownership: &BTreeMap<SemanticId, SemanticOwner>,
4475) -> Vec<SemanticReference> {
4476 template_entities
4477 .iter()
4478 .filter(|entity| entity.kind == TemplateSemanticKind::EventAttribute)
4479 .filter_map(|entity| {
4480 let method_name = entity.expression.as_deref().and_then(this_member_name)?;
4481 let component = template_entity_component(components, ownership, entity)?;
4482 let method = component
4483 .methods
4484 .iter()
4485 .find(|method| method.name == method_name)?;
4486
4487 Some(SemanticReference {
4488 kind: SemanticReferenceKind::EventMethod,
4489 source: entity.id.clone(),
4490 target: method.id.clone(),
4491 provenance: entity.provenance.clone(),
4492 })
4493 })
4494 .collect()
4495}
4496
4497fn build_template_local_references(
4498 components: &[ComponentNode],
4499 template_entities: &[TemplateSemanticEntity],
4500 ownership: &BTreeMap<SemanticId, SemanticOwner>,
4501) -> Vec<SemanticReference> {
4502 let mut references = template_entities
4503 .iter()
4504 .filter(|entity| {
4505 matches!(
4506 entity.kind,
4507 TemplateSemanticKind::Binding | TemplateSemanticKind::AttributeBinding
4508 ) && entity.scope == TemplateSemanticScope::Render
4509 })
4510 .filter_map(|entity| {
4511 let name = entity.expression.as_deref()?;
4512 let component = template_entity_component(components, ownership, entity)?;
4513 let render = component
4514 .methods
4515 .iter()
4516 .find(|method| method.name == "render")?;
4517 let local = unique_local_variable(render, name)?;
4518
4519 Some(SemanticReference {
4520 kind: SemanticReferenceKind::TemplateLocal,
4521 source: entity.id.clone(),
4522 target: local.id.clone(),
4523 provenance: entity.provenance.clone(),
4524 })
4525 })
4526 .collect::<Vec<_>>();
4527 references.sort_by(|left, right| {
4528 (left.source.as_str(), left.target.as_str())
4529 .cmp(&(right.source.as_str(), right.target.as_str()))
4530 });
4531 references
4532}
4533
4534fn unique_local_variable<'a>(
4535 method: &'a ComponentMethod,
4536 name: &str,
4537) -> Option<&'a MethodLocalVariable> {
4538 let mut locals = method
4539 .local_variables
4540 .iter()
4541 .filter(|local| local.name == name);
4542 let local = locals.next()?;
4543 locals.next().is_none().then_some(local)
4544}
4545
4546fn template_entity_component<'a>(
4547 components: &'a [ComponentNode],
4548 ownership: &BTreeMap<SemanticId, SemanticOwner>,
4549 entity: &TemplateSemanticEntity,
4550) -> Option<&'a ComponentNode> {
4551 let template_id = ownership.get(&entity.id)?.entity_id()?;
4552 let component_id = ownership.get(template_id)?.entity_id()?;
4553
4554 components
4555 .iter()
4556 .find(|component| component.id == *component_id)
4557}
4558
4559fn this_member_name(expression: &str) -> Option<&str> {
4560 expression.strip_prefix("this.").filter(|name| {
4561 !name.is_empty()
4562 && name
4563 .bytes()
4564 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
4565 })
4566}
4567
4568#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
4569fn collect_ownership(
4570 components: &[ComponentNode],
4571 contexts: &BTreeMap<ContextId, ContextEntity>,
4572 forms: &BTreeMap<FormId, FormEntity>,
4573 form_fields: &BTreeMap<FieldId, FormFieldEntity>,
4574 form_field_bindings: &BTreeMap<FieldBindingId, FormFieldBinding>,
4575 providers: &BTreeMap<ProviderId, ProviderEntity>,
4576 consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
4577 slots: &BTreeMap<SlotId, SlotEntity>,
4578 component_invocations: &BTreeMap<ComponentInvocationId, ComponentInvocationEntity>,
4579 slot_content_fragments: &BTreeMap<SlotContentFragmentId, SlotContentFragment>,
4580 slot_outlets: &BTreeMap<SlotOutletId, SlotOutlet>,
4581 component_instance_plan: &ComponentInstancePlan,
4582 computed_values: &BTreeMap<SemanticId, ComputedValue>,
4583 effects: &BTreeMap<SemanticId, Effect>,
4584 templates: &[TemplateNode],
4585 template_entities: &[TemplateSemanticEntity],
4586) -> BTreeMap<SemanticId, SemanticOwner> {
4587 let mut ownership = BTreeMap::new();
4588
4589 for component in components {
4590 ownership.insert(component.id.clone(), SemanticOwner::Application);
4591
4592 for field in &component.state_fields {
4593 ownership.insert(
4594 field.id.clone(),
4595 SemanticOwner::entity(component.id.clone()),
4596 );
4597 }
4598 for method in &component.methods {
4599 ownership.insert(
4600 method.id.clone(),
4601 SemanticOwner::entity(component.id.clone()),
4602 );
4603 for parameter in &method.parameters {
4604 ownership.insert(
4605 parameter.id.clone(),
4606 SemanticOwner::entity(method.id.clone()),
4607 );
4608 }
4609 for local in &method.local_variables {
4610 ownership.insert(local.id.clone(), SemanticOwner::entity(method.id.clone()));
4611 }
4612 }
4613 for computed in computed_values
4614 .values()
4615 .filter(|computed| computed.owner.entity_id() == Some(&component.id))
4616 {
4617 ownership.insert(computed.id.clone(), computed.owner.clone());
4618 }
4619 for context in contexts
4620 .values()
4621 .filter(|context| context.owner.entity_id() == Some(&component.id))
4622 {
4623 ownership.insert(context.id.as_semantic_id().clone(), context.owner.clone());
4624 }
4625 for form in forms
4626 .values()
4627 .filter(|form| form.owner.entity_id() == Some(&component.id))
4628 {
4629 ownership.insert(form.id.as_semantic_id().clone(), form.owner.clone());
4630 }
4631 for field in form_fields
4632 .values()
4633 .filter(|field| field.owner_component == component.id)
4634 {
4635 ownership.insert(
4636 field.id.as_semantic_id().clone(),
4637 SemanticOwner::entity(field.owner_form.as_semantic_id().clone()),
4638 );
4639 }
4640 for provider in providers
4641 .values()
4642 .filter(|provider| provider.owner.entity_id() == Some(&component.id))
4643 {
4644 ownership.insert(provider.id.as_semantic_id().clone(), provider.owner.clone());
4645 }
4646 for consumer in consumers
4647 .values()
4648 .filter(|consumer| consumer.owner.entity_id() == Some(&component.id))
4649 {
4650 ownership.insert(consumer.id.as_semantic_id().clone(), consumer.owner.clone());
4651 }
4652 for slot in slots.values().filter(|slot| slot.owner == component.id) {
4653 ownership.insert(slot.id.as_semantic_id().clone(), slot.semantic_owner());
4654 }
4655 for invocation in component_invocations
4656 .values()
4657 .filter(|invocation| invocation.owner_component == component.id)
4658 {
4659 ownership.insert(
4660 invocation.id.as_semantic_id().clone(),
4661 SemanticOwner::entity(component.id.clone()),
4662 );
4663 }
4664 for fragment in slot_content_fragments
4665 .values()
4666 .filter(|fragment| fragment.owner_component == component.id)
4667 {
4668 ownership.insert(
4669 fragment.id.as_semantic_id().clone(),
4670 SemanticOwner::entity(component.id.clone()),
4671 );
4672 }
4673 for outlet in slot_outlets
4674 .values()
4675 .filter(|outlet| outlet.owner_component == component.id)
4676 {
4677 ownership.insert(
4678 outlet.id.as_semantic_id().clone(),
4679 SemanticOwner::entity(component.id.clone()),
4680 );
4681 }
4682 for effect in effects
4683 .values()
4684 .filter(|effect| effect.owner.entity_id() == Some(&component.id))
4685 {
4686 ownership.insert(effect.id.clone(), effect.owner.clone());
4687 }
4688 for endpoint in &component.action_endpoints {
4689 ownership.insert(endpoint.id.clone(), endpoint.owner.clone());
4690 }
4691 for action in &component.actions {
4692 let owner = component
4695 .methods
4696 .iter()
4697 .find(|method| method.name == action.method)
4698 .map(|method| SemanticOwner::entity(method.id.clone()))
4699 .unwrap_or_else(|| action.owner.clone());
4700 ownership.insert(action.id.clone(), owner);
4701 }
4702 if let Some(render) = &component.render {
4703 for handler in render_event_handlers(render) {
4704 ownership.insert(
4705 handler.id.clone(),
4706 SemanticOwner::entity(component.id.template()),
4707 );
4708 }
4709 }
4710 }
4711
4712 for instance in component_instance_plan.instances.values() {
4713 ownership.insert(
4714 instance.id.as_semantic_id().clone(),
4715 instance
4716 .parent_instance
4717 .as_ref()
4718 .map_or(SemanticOwner::Application, |parent| {
4719 SemanticOwner::entity(parent.as_semantic_id().clone())
4720 }),
4721 );
4722 }
4723 for blocked in component_instance_plan.blocked.values() {
4724 ownership.insert(
4725 blocked.id.as_semantic_id().clone(),
4726 SemanticOwner::entity(blocked.parent_instance.as_semantic_id().clone()),
4727 );
4728 }
4729
4730 for template in templates {
4731 let component = components
4732 .iter()
4733 .find(|component| component.id.template() == template.id)
4734 .expect("template graph should only contain component templates");
4735 ownership.insert(
4736 template.id.clone(),
4737 SemanticOwner::entity(component.id.clone()),
4738 );
4739 }
4740
4741 for binding in form_field_bindings.values() {
4742 ownership.insert(
4743 binding.id.as_semantic_id().clone(),
4744 SemanticOwner::entity(binding.control_entity.clone()),
4745 );
4746 }
4747
4748 for entity in template_entities {
4749 ownership.insert(entity.id.clone(), entity.owner.clone());
4750 }
4751
4752 ownership
4753}
4754
4755#[cfg(test)]
4756mod tests {
4757 use std::collections::BTreeMap;
4758
4759 use super::{
4760 build_application_semantic_model, build_application_semantic_model_for_unit,
4761 build_file_route_application_semantic_model_for_unit_with_packages,
4762 build_file_route_application_semantic_model_for_unit_with_packages_and_v2_authoring,
4763 collect_ownership,
4764 };
4765 use crate::{
4766 build_component_graph_for_module, build_template_graph, build_template_manifest_from_asm,
4767 build_template_semantic_entities, build_v2_component_graph_for_module,
4768 CanonicalAuthoredDeclarationKindV1, CanonicalAuthoredDeclarationV1,
4769 CanonicalAuthoredSemanticModelV1, CompilationUnit, ComponentInstancePlan, SemanticEntity,
4770 SemanticEntityKind, SemanticOwner, SemanticReferenceKind, TemplateSemanticKind,
4771 };
4772
4773 #[test]
4774 fn file_route_assembly_projects_explicit_canonical_v2_components_without_decorators() {
4775 let unit = CompilationUnit::parse_sources([(
4776 "app/routes/index.tsx",
4777 "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>; } }",
4778 )]);
4779 let model = CanonicalAuthoredSemanticModelV1 {
4780 schema_version: crate::CANONICAL_AUTHORED_SEMANTICS_SCHEMA_VERSION,
4781 source_path: "app/routes/index.tsx".into(),
4782 declarations: vec![
4783 CanonicalAuthoredDeclarationV1 {
4784 kind: CanonicalAuthoredDeclarationKindV1::Component,
4785 subject: "Home".into(),
4786 source: crate::AuthoredSourceRangeV1 {
4787 start: 37,
4788 end: 106,
4789 line: 1,
4790 column: 38,
4791 },
4792 intrinsic_identity: None,
4793 derived_evidence: None,
4794 },
4795 CanonicalAuthoredDeclarationV1 {
4796 kind: CanonicalAuthoredDeclarationKindV1::State,
4797 subject: "Home.count".into(),
4798 source: crate::AuthoredSourceRangeV1 {
4799 start: 83,
4800 end: 99,
4801 line: 1,
4802 column: 84,
4803 },
4804 intrinsic_identity: None,
4805 derived_evidence: None,
4806 },
4807 CanonicalAuthoredDeclarationV1 {
4808 kind: CanonicalAuthoredDeclarationKindV1::Action,
4809 subject: "Home.increment".into(),
4810 source: crate::AuthoredSourceRangeV1 {
4811 start: 100,
4812 end: 148,
4813 line: 1,
4814 column: 101,
4815 },
4816 intrinsic_identity: None,
4817 derived_evidence: None,
4818 },
4819 ],
4820 };
4821 let asm =
4822 build_file_route_application_semantic_model_for_unit_with_packages_and_v2_authoring(
4823 &unit,
4824 &crate::SemanticPackageResolutionTable::default(),
4825 &BTreeMap::from([("app/routes/index.tsx".into(), model)]),
4826 )
4827 .expect("canonical V2 route assembly");
4828 assert_eq!(asm.components.len(), 1);
4829 assert_eq!(asm.components[0].class_name, "Home");
4830 assert_eq!(asm.components[0].state_fields[0].name, "count");
4831 assert!(asm.components[0].methods.is_empty());
4832 let endpoint = &asm.components[0].action_endpoints[0];
4833 assert_eq!(endpoint.name, "increment");
4834 assert_eq!(
4835 endpoint.id,
4836 asm.components[0].id.action_endpoint("increment")
4837 );
4838 assert_eq!(
4839 asm.components[0].actions[0].owner,
4840 SemanticOwner::entity(endpoint.id.clone())
4841 );
4842 let batch = asm
4843 .effect_trigger_plan
4844 .action_batches
4845 .values()
4846 .find(|batch| batch.authored_action_endpoint == endpoint.id)
4847 .expect("V2 field endpoint action batch");
4848 let manifest = build_template_manifest_from_asm(&asm);
4849 assert_eq!(
4850 manifest.components[0].template.events[0]
4851 .method_id
4852 .as_deref(),
4853 Some(endpoint.id.as_str())
4854 );
4855 assert_eq!(
4856 manifest.components[0].template.events[0]
4857 .action_batch_id
4858 .as_deref(),
4859 Some(batch.id.as_str())
4860 );
4861 assert!(asm
4862 .diagnostics
4863 .iter()
4864 .all(|diagnostic| diagnostic.code != "PSC1001"));
4865 }
4866
4867 #[test]
4868 fn file_route_assembly_projects_canonical_v2_form_into_existing_form_products() {
4869 let unit = CompilationUnit::parse_sources([(
4870 "app/routes/contact.tsx",
4871 "import { Component, defineForm, field, required, state } from \"presolve\"; export class Contact extends Component { contact = defineForm({ serialization: \"form-data\", fields: { email: field({ initial: \"\", validate: [required()] }) }, submit: async ({ value, signal }) => { this.submitted += 1; } }); submitted = state(0); render() { return <form form={this.contact}><input bind:value={this.contact.fields.email} /><button type=\"submit\">Send</button></form>; } }",
4872 )]);
4873 let parsed = &unit.files()[0];
4874 let class = &parsed.classes[0];
4875 let property = &class.properties[0];
4876 let field = &property
4877 .form_definition_shape
4878 .as_ref()
4879 .expect("static Form shape")
4880 .fields[0];
4881 let validation = &field.validations[0];
4882 let state_property = &class.properties[1];
4883 let model = CanonicalAuthoredSemanticModelV1 {
4884 schema_version: crate::CANONICAL_AUTHORED_SEMANTICS_SCHEMA_VERSION,
4885 source_path: parsed.path.clone(),
4886 declarations: vec![
4887 CanonicalAuthoredDeclarationV1 {
4888 kind: CanonicalAuthoredDeclarationKindV1::Component,
4889 subject: "Contact".into(),
4890 source: crate::AuthoredSourceRangeV1 {
4891 start: class.span.start,
4892 end: class.span.end,
4893 line: class.span.line,
4894 column: class.span.column,
4895 },
4896 intrinsic_identity: None,
4897 derived_evidence: None,
4898 },
4899 CanonicalAuthoredDeclarationV1 {
4900 kind: CanonicalAuthoredDeclarationKindV1::FormField,
4901 subject: "Contact.contact.email".into(),
4902 source: crate::AuthoredSourceRangeV1 {
4903 start: field.declaration_span.start,
4904 end: field.declaration_span.end,
4905 line: field.declaration_span.line,
4906 column: field.declaration_span.column,
4907 },
4908 intrinsic_identity: None,
4909 derived_evidence: None,
4910 },
4911 CanonicalAuthoredDeclarationV1 {
4912 kind: CanonicalAuthoredDeclarationKindV1::Form,
4913 subject: "Contact.contact".into(),
4914 source: crate::AuthoredSourceRangeV1 {
4915 start: property.span.start,
4916 end: property.span.end,
4917 line: property.span.line,
4918 column: property.span.column,
4919 },
4920 intrinsic_identity: None,
4921 derived_evidence: None,
4922 },
4923 CanonicalAuthoredDeclarationV1 {
4924 kind: CanonicalAuthoredDeclarationKindV1::Validation,
4925 subject: "Contact.contact.email.validation.0".into(),
4926 source: crate::AuthoredSourceRangeV1 {
4927 start: validation.span.start,
4928 end: validation.span.end,
4929 line: validation.span.line,
4930 column: validation.span.column,
4931 },
4932 intrinsic_identity: Some(crate::ResolvedIntrinsicIdentityV1 {
4933 name: "required".into(),
4934 flags: 32,
4935 declaration_modules: vec!["presolve".into()],
4936 }),
4937 derived_evidence: None,
4938 },
4939 CanonicalAuthoredDeclarationV1 {
4940 kind: CanonicalAuthoredDeclarationKindV1::State,
4941 subject: "Contact.submitted".into(),
4942 source: crate::AuthoredSourceRangeV1 {
4943 start: state_property.span.start,
4944 end: state_property.span.end,
4945 line: state_property.span.line,
4946 column: state_property.span.column,
4947 },
4948 intrinsic_identity: None,
4949 derived_evidence: None,
4950 },
4951 ],
4952 };
4953 let asm =
4954 build_file_route_application_semantic_model_for_unit_with_packages_and_v2_authoring(
4955 &unit,
4956 &crate::SemanticPackageResolutionTable::default(),
4957 &BTreeMap::from([("app/routes/contact.tsx".into(), model)]),
4958 )
4959 .expect("canonical V2 Form route assembly");
4960 assert_eq!(asm.forms().len(), 1);
4961 assert_eq!(asm.forms()[0].name, "contact");
4962 assert_eq!(asm.form_declaration_candidates().len(), 1);
4963 assert_eq!(asm.form_fields().len(), 1);
4964 assert_eq!(asm.form_fields()[0].name, "email");
4965 assert_eq!(asm.form_fields()[0].path, ["email"]);
4966 assert_eq!(asm.form_field_bindings().len(), 1);
4967 assert_eq!(
4968 asm.form_field_bindings()[0].channel,
4969 crate::FormControlChannel::Value
4970 );
4971 assert_eq!(asm.validation_rules().len(), 1);
4972 assert_eq!(
4973 asm.validation_rules()[0].kind,
4974 crate::ValidationRuleKind::Required
4975 );
4976 assert_eq!(asm.submissions.plans.len(), 1);
4977 assert_eq!(asm.submission_hosts.len(), 1);
4978 assert_eq!(
4979 asm.serialization
4980 .plans
4981 .values()
4982 .next()
4983 .expect("Form serialization plan")
4984 .format,
4985 crate::FormSerializationFormat::FormData
4986 );
4987 assert_eq!(
4988 asm.form_declaration_candidates()[0].status,
4989 crate::FormDeclarationStatus::Valid
4990 );
4991 }
4992
4993 #[test]
4994 fn file_route_v2_slot_fields_bind_caller_content_through_the_existing_slot_model() {
4995 let unit = CompilationUnit::parse_sources([
4996 (
4997 "app/components/Panel.tsx",
4998 "import { Component, slot, type SlotContent } from \"presolve\"; export class Panel extends Component { children: SlotContent = slot(); render() { return <section><slot /></section>; } }",
4999 ),
5000 (
5001 "app/routes/index.tsx",
5002 "import { Component } from \"presolve\"; import { Panel } from \"../components/Panel\"; export class Index extends Component { render() { return <main><Panel><button>Projected</button></Panel></main>; } }",
5003 ),
5004 ]);
5005 let models = unit
5006 .files()
5007 .iter()
5008 .map(|parsed| {
5009 let component_sites = crate::component_inheritance_sites_v1(parsed);
5010 let components = crate::lower_component_inheritance_v1(
5011 parsed,
5012 component_sites
5013 .into_iter()
5014 .map(|site| crate::ResolvedComponentInheritanceV1 {
5015 heritage_source: site.heritage_source,
5016 component_identity: crate::ResolvedIntrinsicIdentityV1 {
5017 name: "Component".into(),
5018 flags: 32,
5019 declaration_modules: vec!["presolve".into()],
5020 },
5021 }),
5022 )
5023 .expect("V2 component authority")
5024 .model;
5025 let slots = crate::slot_field_sites_v1(parsed, &components).expect("V2 Slot sites");
5026 let lowering = crate::lower_v2_authoring_v1(
5027 parsed,
5028 crate::V2AuthoringResolutionsV1 {
5029 components: crate::component_inheritance_sites_v1(parsed)
5030 .into_iter()
5031 .map(|site| crate::ResolvedComponentInheritanceV1 {
5032 heritage_source: site.heritage_source,
5033 component_identity: crate::ResolvedIntrinsicIdentityV1 {
5034 name: "Component".into(),
5035 flags: 32,
5036 declaration_modules: vec!["presolve".into()],
5037 },
5038 })
5039 .collect(),
5040 states: Vec::new(),
5041 actions: Vec::new(),
5042 effects: Vec::new(),
5043 slots: slots
5044 .into_iter()
5045 .map(|site| crate::ResolvedSlotFieldV1 {
5046 callee_source: site.callee_source,
5047 slot_identity: crate::ResolvedIntrinsicIdentityV1 {
5048 name: "slot".into(),
5049 flags: 32,
5050 declaration_modules: vec!["presolve".into()],
5051 },
5052 })
5053 .collect(),
5054 forms: Vec::new(),
5055 form_fields: Vec::new(),
5056 validations: Vec::new(),
5057 },
5058 )
5059 .expect("V2 Slot lowering");
5060 (parsed.path.clone(), lowering.model)
5061 })
5062 .collect::<BTreeMap<_, _>>();
5063 let asm =
5064 build_file_route_application_semantic_model_for_unit_with_packages_and_v2_authoring(
5065 &unit,
5066 &crate::SemanticPackageResolutionTable::default(),
5067 &models,
5068 )
5069 .expect("V2 Slot route assembly");
5070 assert_eq!(asm.slots().len(), 1);
5071 assert_eq!(asm.slot_content_fragments().len(), 1);
5072 assert_eq!(asm.slot_binding_registry().bindings.len(), 1);
5073 }
5074
5075 #[test]
5076 fn canonical_v2_action_rejects_unsupported_handler_before_publication() {
5077 let parsed = presolve_parser::parse_file(
5078 "app/routes/index.tsx",
5079 "import { Component, state, action } from \"presolve\"; export class Home extends Component { count = state(0); increment = action(() => { unrelated(); }); render() { return <main>Home</main>; } }",
5080 );
5081 let model = CanonicalAuthoredSemanticModelV1 {
5082 schema_version: crate::CANONICAL_AUTHORED_SEMANTICS_SCHEMA_VERSION,
5083 source_path: parsed.path.clone(),
5084 declarations: vec![
5085 CanonicalAuthoredDeclarationV1 {
5086 kind: CanonicalAuthoredDeclarationKindV1::Component,
5087 subject: "Home".into(),
5088 source: crate::AuthoredSourceRangeV1 {
5089 start: 0,
5090 end: parsed.syntax.source.len(),
5091 line: 1,
5092 column: 1,
5093 },
5094 intrinsic_identity: None,
5095 derived_evidence: None,
5096 },
5097 CanonicalAuthoredDeclarationV1 {
5098 kind: CanonicalAuthoredDeclarationKindV1::State,
5099 subject: "Home.count".into(),
5100 source: crate::AuthoredSourceRangeV1 {
5101 start: 0,
5102 end: 1,
5103 line: 1,
5104 column: 1,
5105 },
5106 intrinsic_identity: None,
5107 derived_evidence: None,
5108 },
5109 CanonicalAuthoredDeclarationV1 {
5110 kind: CanonicalAuthoredDeclarationKindV1::Action,
5111 subject: "Home.increment".into(),
5112 source: crate::AuthoredSourceRangeV1 {
5113 start: 0,
5114 end: 1,
5115 line: 1,
5116 column: 1,
5117 },
5118 intrinsic_identity: None,
5119 derived_evidence: None,
5120 },
5121 ],
5122 };
5123 let graph = build_v2_component_graph_for_module(&parsed, &model);
5124 assert!(graph.components[0].action_endpoints.is_empty());
5125 assert!(graph
5126 .diagnostics
5127 .iter()
5128 .any(|diagnostic| diagnostic.code == "PSV2A1004"));
5129 }
5130
5131 #[test]
5132 fn canonical_v2_action_projects_typed_parameter_ordinals_and_rejects_bad_events() {
5133 let parsed = presolve_parser::parse_file(
5134 "app/routes/index.tsx",
5135 "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>; } }",
5136 );
5137 let model = CanonicalAuthoredSemanticModelV1 {
5138 schema_version: crate::CANONICAL_AUTHORED_SEMANTICS_SCHEMA_VERSION,
5139 source_path: parsed.path.clone(),
5140 declarations: vec![
5141 CanonicalAuthoredDeclarationV1 {
5142 kind: CanonicalAuthoredDeclarationKindV1::Component,
5143 subject: "Home".into(),
5144 source: crate::AuthoredSourceRangeV1 {
5145 start: 0,
5146 end: parsed.syntax.source.len(),
5147 line: 1,
5148 column: 1,
5149 },
5150 intrinsic_identity: None,
5151 derived_evidence: None,
5152 },
5153 CanonicalAuthoredDeclarationV1 {
5154 kind: CanonicalAuthoredDeclarationKindV1::State,
5155 subject: "Home.count".into(),
5156 source: crate::AuthoredSourceRangeV1 {
5157 start: 0,
5158 end: 1,
5159 line: 1,
5160 column: 1,
5161 },
5162 intrinsic_identity: None,
5163 derived_evidence: None,
5164 },
5165 CanonicalAuthoredDeclarationV1 {
5166 kind: CanonicalAuthoredDeclarationKindV1::Action,
5167 subject: "Home.setExact".into(),
5168 source: crate::AuthoredSourceRangeV1 {
5169 start: 0,
5170 end: 1,
5171 line: 1,
5172 column: 1,
5173 },
5174 intrinsic_identity: None,
5175 derived_evidence: None,
5176 },
5177 CanonicalAuthoredDeclarationV1 {
5178 kind: CanonicalAuthoredDeclarationKindV1::Action,
5179 subject: "Home.setLocal".into(),
5180 source: crate::AuthoredSourceRangeV1 {
5181 start: 0,
5182 end: 1,
5183 line: 1,
5184 column: 1,
5185 },
5186 intrinsic_identity: None,
5187 derived_evidence: None,
5188 },
5189 ],
5190 };
5191 let graph = build_v2_component_graph_for_module(&parsed, &model);
5192 assert_eq!(graph.components[0].actions.len(), 2);
5193 assert!(matches!(
5194 graph.components[0].actions[0].operation,
5195 crate::StateOperation::AssignParameter(ref ordinal) if ordinal == "0"
5196 ));
5197 assert!(matches!(
5198 graph.components[0].actions[1].operation,
5199 crate::StateOperation::Assign(crate::SerializableValue::Number(ref value)) if value == "23"
5200 ));
5201 assert!(graph
5202 .diagnostics
5203 .iter()
5204 .any(|diagnostic| diagnostic.code == "PSV2A1006"));
5205 }
5206
5207 #[test]
5208 fn lowers_supported_primitive_state_annotations_into_canonical_types() {
5209 let parsed = presolve_parser::parse_file(
5210 "src/TypedState.tsx",
5211 r#"
5212@component("x-typed-state")
5213class TypedState extends Component {
5214 count: number = state(0);
5215}
5216"#,
5217 );
5218
5219 let asm = build_application_semantic_model(&parsed);
5220
5221 let field = &asm.components[0].state_fields[0];
5222 let assignment = asm
5223 .semantic_types
5224 .assignments
5225 .get(&field.id)
5226 .expect("canonical declared type");
5227
5228 assert_eq!(assignment.semantic_type, crate::SemanticType::Number);
5229 assert_eq!(assignment.status, crate::SemanticTypeStatus::Declared);
5230 assert_eq!(
5231 assignment.provenance,
5232 field.declared_type.as_ref().unwrap().provenance
5233 );
5234 }
5235
5236 #[test]
5237 fn queries_canonical_type_information_from_the_asm() {
5238 let parsed = presolve_parser::parse_file(
5239 "src/TypeQueries.tsx",
5240 r#"
5241@component("x-type-queries")
5242class TypeQueries extends Component {
5243 count: number = state(0);
5244 label = state("Presolve");
5245}
5246"#,
5247 );
5248 let asm = build_application_semantic_model(&parsed);
5249 let count = &asm.components[0].state_fields[0];
5250 let label = &asm.components[0].state_fields[1];
5251
5252 assert_eq!(
5253 asm.semantic_type_of(&count.id),
5254 Some(&crate::SemanticType::Number)
5255 );
5256 assert_eq!(asm.type_declarations(&crate::SemanticType::Number).len(), 1);
5257 assert_eq!(asm.type_usages(&crate::SemanticType::String).len(), 1);
5258 assert_eq!(
5259 asm.serialization_compatibility_of(&count.id),
5260 Some(crate::SerializationCompatibility::Serializable)
5261 );
5262 assert_eq!(asm.is_type_assignable(&label.id, &count.id), Some(false));
5263 }
5264
5265 #[test]
5266 fn lowers_typed_method_parameters_into_canonical_entities() {
5267 let parsed = presolve_parser::parse_file(
5268 "src/Parameters.tsx",
5269 r#"
5270@component("x-parameters")
5271class Parameters extends Component {
5272 save(title: string, retries?: number) {}
5273}
5274"#,
5275 );
5276
5277 let asm = build_application_semantic_model(&parsed);
5278 let method = &asm.components[0].methods[0];
5279 let parameters = &method.parameters;
5280
5281 assert_eq!(parameters.len(), 2);
5282 assert_eq!(
5283 asm.semantic_types.assignments[¶meters[0].id].semantic_type,
5284 crate::SemanticType::String
5285 );
5286 assert_eq!(
5287 asm.semantic_types.assignments[¶meters[1].id].semantic_type,
5288 crate::SemanticType::Number
5289 );
5290 assert!(parameters.iter().all(|parameter| {
5291 asm.owner(¶meter.id) == Some(&SemanticOwner::entity(method.id.clone()))
5292 && asm
5293 .entity(¶meter.id)
5294 .is_some_and(|entity| entity.kind() == SemanticEntityKind::Parameter)
5295 }));
5296 }
5297
5298 #[test]
5299 fn lowers_declared_and_inferred_method_return_types() {
5300 let parsed = presolve_parser::parse_file(
5301 "src/Returns.tsx",
5302 r#"
5303@component("x-returns")
5304class Returns extends Component {
5305 declared(): string { return "Presolve"; }
5306 inferred() { return 1; }
5307}
5308"#,
5309 );
5310
5311 let asm = build_application_semantic_model(&parsed);
5312 let methods = &asm.components[0].methods;
5313 let declared = &methods[0];
5314 let inferred = &methods[1];
5315
5316 assert_eq!(
5317 asm.semantic_types.assignments[&declared.id].semantic_type,
5318 crate::SemanticType::String
5319 );
5320 assert_eq!(
5321 asm.semantic_types.assignments[&declared.id].status,
5322 crate::SemanticTypeStatus::Declared
5323 );
5324 assert_eq!(
5325 asm.semantic_types.assignments[&inferred.id].semantic_type,
5326 crate::SemanticType::Number
5327 );
5328 assert_eq!(
5329 asm.semantic_types.assignments[&inferred.id].status,
5330 crate::SemanticTypeStatus::Inferred
5331 );
5332 }
5333
5334 #[test]
5335 fn establishes_typed_computed_getter_contracts() {
5336 let parsed = presolve_parser::parse_file(
5337 "src/Computed.tsx",
5338 r#"
5339@component("x-computed")
5340class Computed extends Component {
5341 @computed()
5342 get remainingCount(): number { return 1; }
5343
5344 render() { return <p />; }
5345}
5346"#,
5347 );
5348
5349 let asm = build_application_semantic_model(&parsed);
5350 let method = &asm.components[0].methods[0];
5351 let computed_id = asm.components[0].id.computed("remainingCount");
5352 let computed = asm
5353 .semantic_types
5354 .computed_values
5355 .get(&computed_id)
5356 .expect("computed type contract");
5357 let computed_entity = asm
5358 .computed_value(&computed_id)
5359 .expect("first-class computed entity");
5360
5361 assert!(method.is_getter);
5362 assert!(method.is_computed());
5363 assert_eq!(
5364 computed.semantic_type,
5365 crate::SemanticType::NumberLiteral("1".to_string())
5366 );
5367 assert_eq!(
5368 computed.declared_return_type,
5369 Some(crate::SemanticType::Number)
5370 );
5371 assert_eq!(computed.declared_return_compatible, Some(true));
5372 assert_eq!(computed_entity.method, method.id);
5373 assert_eq!(
5374 computed_entity.owner,
5375 crate::SemanticOwner::entity(asm.components[0].id.clone())
5376 );
5377 assert_eq!(
5378 computed_entity.cache_policy,
5379 crate::ComputedCachePolicy::Memoized
5380 );
5381 assert_eq!(computed_entity.purity, crate::ComputedPurity::Pure);
5382 assert!(computed_entity.purity_violations.is_empty());
5383 assert_eq!(
5384 computed_entity.execution_boundary,
5385 crate::ExecutionBoundary::Client
5386 );
5387 assert_eq!(
5388 asm.entity(&computed_id),
5389 Some(SemanticEntity::Computed(computed_entity))
5390 );
5391 assert_eq!(
5392 asm.owner(&computed_id),
5393 Some(&crate::SemanticOwner::entity(asm.components[0].id.clone()))
5394 );
5395 assert_eq!(asm.provenance(&computed_id), asm.provenance(&method.id));
5396 assert_eq!(
5397 asm.entities_of_kind(SemanticEntityKind::Computed),
5398 vec![&computed_id]
5399 );
5400 }
5401
5402 #[test]
5403 fn resolves_computed_reads_to_canonical_state_and_computed_references() {
5404 let parsed = presolve_parser::parse_file(
5405 "src/ComputedReads.tsx",
5406 r#"
5407@component("x-computed-reads")
5408class ComputedReads extends Component {
5409 count = state(1);
5410 profile = state({ hidden: false });
5411
5412 @computed()
5413 get doubled() { return this.count * 2; }
5414
5415 @computed()
5416 get visible() { return this.doubled + this.count + this.count; }
5417
5418 @computed()
5419 get profileHidden() { return this.profile.hidden; }
5420
5421 @computed()
5422 get unresolved() { return this.missing; }
5423}
5424"#,
5425 );
5426 let asm = build_application_semantic_model(&parsed);
5427 let component = &asm.components[0];
5428 let count = component.id.state_field("count");
5429 let profile = component.id.state_field("profile");
5430 let doubled = component.id.computed("doubled");
5431 let visible = component.id.computed("visible");
5432 let profile_hidden = component.id.computed("profileHidden");
5433 let unresolved = component.id.computed("unresolved");
5434
5435 let state_references = asm.references_of_kind(SemanticReferenceKind::ComputedState);
5436 assert_eq!(state_references.len(), 3);
5437 assert!(state_references
5438 .iter()
5439 .any(|reference| reference.source == doubled && reference.target == count));
5440 assert!(state_references
5441 .iter()
5442 .any(|reference| reference.source == visible && reference.target == count));
5443 assert!(state_references.iter().any(|reference| {
5444 reference.source == profile_hidden && reference.target == profile
5445 }));
5446
5447 let computed_references = asm.references_of_kind(SemanticReferenceKind::ComputedComputed);
5448 assert_eq!(computed_references.len(), 1);
5449 assert_eq!(computed_references[0].source, visible);
5450 assert_eq!(computed_references[0].target, doubled);
5451 assert!(!asm
5452 .references
5453 .iter()
5454 .any(|reference| reference.source == unresolved));
5455 assert!(asm
5456 .references
5457 .iter()
5458 .all(|reference| { asm.provenance(&reference.source) == Some(&reference.provenance) }));
5459 }
5460
5461 #[test]
5462 fn populates_reactive_graph_from_direct_computed_reads() {
5463 let parsed = presolve_parser::parse_file(
5464 "src/ComputedReactiveGraph.tsx",
5465 r#"
5466@component("x-computed-reactive-graph")
5467class ComputedReactiveGraph extends Component {
5468 count = state(1);
5469
5470 @computed()
5471 get doubled() { return this.count * 2; }
5472
5473 @computed()
5474 get label() { return this.doubled + 1; }
5475}
5476"#,
5477 );
5478 let asm = build_application_semantic_model(&parsed);
5479 let component = &asm.components[0];
5480 let count = component.id.state_field("count");
5481 let doubled = component.id.computed("doubled");
5482 let label = component.id.computed("label");
5483 let graph = &asm.reactive_graph;
5484
5485 assert_eq!(graph.nodes.len(), 3);
5486 assert_eq!(
5487 graph.nodes[count.as_str()].kind,
5488 crate::IrReactiveNodeKind::State
5489 );
5490 assert_eq!(
5491 graph.nodes[doubled.as_str()].kind,
5492 crate::IrReactiveNodeKind::Computed
5493 );
5494 assert_eq!(
5495 graph.nodes[label.as_str()].kind,
5496 crate::IrReactiveNodeKind::Computed
5497 );
5498
5499 let doubled_dependencies = graph.computed_dependencies(doubled.as_str());
5500 assert_eq!(doubled_dependencies.len(), 1);
5501 assert_eq!(doubled_dependencies[0].target, count.as_str());
5502 let label_dependencies = graph.computed_dependencies(label.as_str());
5503 assert_eq!(label_dependencies.len(), 1);
5504 assert_eq!(label_dependencies[0].target, doubled.as_str());
5505
5506 let count_invalidations = graph.invalidations_from(count.as_str());
5507 assert_eq!(count_invalidations.len(), 1);
5508 assert_eq!(count_invalidations[0].target, doubled.as_str());
5509 let doubled_invalidations = graph.invalidations_from(doubled.as_str());
5510 assert_eq!(doubled_invalidations.len(), 1);
5511 assert_eq!(doubled_invalidations[0].target, label.as_str());
5512 assert!(graph
5513 .invalidations_from(count.as_str())
5514 .iter()
5515 .all(|edge| { edge.target != label.as_str() }));
5516 }
5517
5518 #[test]
5519 fn computes_deterministic_transitive_reactive_dependencies_and_dependents() {
5520 let parsed = presolve_parser::parse_file(
5521 "src/ComputedTransitiveReactiveGraph.tsx",
5522 r#"
5523@component("x-computed-transitive-reactive-graph")
5524class ComputedTransitiveReactiveGraph extends Component {
5525 count = state(1);
5526
5527 @computed()
5528 get doubled() { return this.count * 2; }
5529
5530 @computed()
5531 get label() { return this.doubled + 1; }
5532}
5533"#,
5534 );
5535 let asm = build_application_semantic_model(&parsed);
5536 let component = &asm.components[0];
5537 let count = component.id.state_field("count");
5538 let doubled = component.id.computed("doubled");
5539 let label = component.id.computed("label");
5540 let analysis = &asm.reactive_transitive_analysis;
5541
5542 assert_eq!(analysis.dependencies.len(), 3);
5543 assert_eq!(analysis.dependents.len(), 3);
5544 assert_eq!(
5545 analysis.dependencies_of(count.as_str()),
5546 Vec::<String>::new()
5547 );
5548 assert_eq!(
5549 analysis.dependencies_of(doubled.as_str()),
5550 vec![count.as_str().to_string()]
5551 );
5552 assert_eq!(
5553 analysis.dependencies_of(label.as_str()),
5554 vec![doubled.as_str().to_string(), count.as_str().to_string()]
5555 );
5556 assert_eq!(
5557 analysis.dependents_of(count.as_str()),
5558 vec![doubled.as_str().to_string(), label.as_str().to_string()]
5559 );
5560 assert_eq!(
5561 analysis.dependents_of(doubled.as_str()),
5562 vec![label.as_str().to_string()]
5563 );
5564 assert_eq!(analysis.dependents_of(label.as_str()), Vec::<String>::new());
5565 }
5566
5567 #[test]
5568 fn detects_computed_dependency_cycles_with_stable_diagnostics() {
5569 let parsed = presolve_parser::parse_file(
5570 "src/ComputedDependencyCycles.tsx",
5571 r#"
5572@component("x-computed-dependency-cycles")
5573class ComputedDependencyCycles extends Component {
5574 @computed()
5575 get alpha() { return this.beta; }
5576
5577 @computed()
5578 get beta() { return this.alpha; }
5579
5580 @computed()
5581 get selfLoop() { return this.selfLoop; }
5582}
5583"#,
5584 );
5585 let asm = build_application_semantic_model(&parsed);
5586 let component = &asm.components[0];
5587 let alpha = component.id.computed("alpha");
5588 let beta = component.id.computed("beta");
5589 let self_loop = component.id.computed("selfLoop");
5590
5591 assert_eq!(
5592 asm.reactive_cycle_analysis.cycles,
5593 vec![
5594 crate::IrReactiveCycle {
5595 nodes: vec![alpha.as_str().to_string(), beta.as_str().to_string()],
5596 },
5597 crate::IrReactiveCycle {
5598 nodes: vec![self_loop.as_str().to_string()],
5599 },
5600 ]
5601 );
5602 let diagnostics = asm
5603 .diagnostics
5604 .iter()
5605 .filter(|diagnostic| diagnostic.code == "PSC1035")
5606 .collect::<Vec<_>>();
5607 assert_eq!(diagnostics.len(), 2);
5608 assert_eq!(
5609 diagnostics[0].message,
5610 format!("computed dependency cycle detected among: {alpha}, {beta}")
5611 );
5612 assert_eq!(
5613 diagnostics[0].provenance,
5614 Some(
5615 asm.computed_value(&alpha)
5616 .expect("alpha computed value")
5617 .provenance
5618 .clone()
5619 )
5620 );
5621 assert_eq!(
5622 diagnostics[1].message,
5623 format!("computed dependency cycle detected among: {self_loop}")
5624 );
5625 assert_eq!(
5626 diagnostics[1].provenance,
5627 Some(
5628 asm.computed_value(&self_loop)
5629 .expect("self-loop computed value")
5630 .provenance
5631 .clone()
5632 )
5633 );
5634 }
5635
5636 #[test]
5637 fn plans_deterministic_computed_evaluation_order_and_batches() {
5638 let parsed = presolve_parser::parse_file(
5639 "src/ComputedEvaluationPlan.tsx",
5640 r#"
5641@component("x-computed-evaluation-plan")
5642class ComputedEvaluationPlan extends Component {
5643 count = state(1);
5644 other = state(2);
5645
5646 @computed()
5647 get alpha() { return this.count; }
5648
5649 @computed()
5650 get beta() { return this.other; }
5651
5652 @computed()
5653 get gamma() { return this.alpha; }
5654}
5655"#,
5656 );
5657 let asm = build_application_semantic_model(&parsed);
5658 let component = &asm.components[0];
5659 let alpha = component.id.computed("alpha");
5660 let beta = component.id.computed("beta");
5661 let gamma = component.id.computed("gamma");
5662
5663 assert_eq!(
5664 asm.computed_evaluation_plan.evaluation_order,
5665 vec![
5666 alpha.as_str().to_string(),
5667 beta.as_str().to_string(),
5668 gamma.as_str().to_string(),
5669 ]
5670 );
5671 assert_eq!(
5672 asm.computed_evaluation_plan.update_batches,
5673 vec![
5674 vec![alpha.as_str().to_string(), beta.as_str().to_string()],
5675 vec![gamma.as_str().to_string()],
5676 ]
5677 );
5678 assert!(asm.computed_evaluation_plan.unplanned.is_empty());
5679 }
5680
5681 #[test]
5682 fn leaves_cyclic_computed_values_unplanned() {
5683 let parsed = presolve_parser::parse_file(
5684 "src/CyclicComputedEvaluationPlan.tsx",
5685 r#"
5686@component("x-cyclic-computed-evaluation-plan")
5687class CyclicComputedEvaluationPlan extends Component {
5688 @computed()
5689 get alpha() { return this.beta; }
5690
5691 @computed()
5692 get beta() { return this.alpha; }
5693}
5694"#,
5695 );
5696 let asm = build_application_semantic_model(&parsed);
5697 let component = &asm.components[0];
5698 let alpha = component.id.computed("alpha");
5699 let beta = component.id.computed("beta");
5700
5701 assert!(asm.computed_evaluation_plan.evaluation_order.is_empty());
5702 assert!(asm.computed_evaluation_plan.update_batches.is_empty());
5703 assert_eq!(
5704 asm.computed_evaluation_plan.unplanned,
5705 vec![alpha.as_str().to_string(), beta.as_str().to_string()]
5706 );
5707 }
5708
5709 #[test]
5710 fn assigns_inferred_computed_types_and_validates_declared_returns() {
5711 let parsed = presolve_parser::parse_file(
5712 "src/ComputedTypes.tsx",
5713 r#"
5714@component("x-computed-types")
5715class ComputedTypes extends Component {
5716 count: number = state(1);
5717 profile = state({ label: "Presolve" });
5718
5719 @computed()
5720 get doubled(): number { return this.count * 2; }
5721
5722 @computed()
5723 get chained() { return this.doubled + 1; }
5724
5725 @computed()
5726 get label(): string { return this.profile.label; }
5727
5728 @computed()
5729 get invalid(): number { return "wrong"; }
5730
5731 @computed()
5732 get unresolved() { return this.missing; }
5733}
5734"#,
5735 );
5736 let asm = build_application_semantic_model(&parsed);
5737 let component = &asm.components[0];
5738 let doubled = component.id.computed("doubled");
5739 let chained = component.id.computed("chained");
5740 let label = component.id.computed("label");
5741 let invalid = component.id.computed("invalid");
5742 let unresolved = component.id.computed("unresolved");
5743
5744 let types = &asm.semantic_types.computed_values;
5745 assert_eq!(types[&doubled].semantic_type, crate::SemanticType::Number);
5746 assert_eq!(types[&chained].semantic_type, crate::SemanticType::Number);
5747 assert_eq!(types[&label].semantic_type, crate::SemanticType::String);
5748 assert_eq!(
5749 types[&invalid].semantic_type,
5750 crate::SemanticType::StringLiteral("wrong".to_string())
5751 );
5752 assert_eq!(types[&invalid].declared_return_compatible, Some(false));
5753 assert_eq!(types[&doubled].declared_return_compatible, Some(true));
5754 assert_eq!(
5755 types[&unresolved].semantic_type,
5756 crate::SemanticType::Unknown
5757 );
5758 assert_eq!(
5759 types[&unresolved].serialization,
5760 crate::SerializationCompatibility::NotSerializable
5761 );
5762 assert_eq!(
5763 types[&doubled].boundary_compatibility,
5764 crate::BoundaryCompatibility::Compatible
5765 );
5766 assert_eq!(
5767 types[&doubled].execution_boundary,
5768 crate::ExecutionBoundary::Client
5769 );
5770 assert_eq!(
5771 asm.semantic_type_of(&doubled),
5772 Some(&crate::SemanticType::Number)
5773 );
5774 assert_eq!(
5775 asm.serialization_compatibility_of(&chained),
5776 Some(crate::SerializationCompatibility::Serializable)
5777 );
5778 }
5779
5780 #[test]
5781 fn reports_stable_diagnostics_for_computed_semantics() {
5782 let parsed = presolve_parser::parse_file(
5783 "src/ComputedDiagnostics.tsx",
5784 r#"
5785@component("x-computed-diagnostics")
5786class ComputedDiagnostics extends Component {
5787 count = state(1);
5788
5789 @computed()
5790 invalidDeclaration() { return 1; }
5791
5792 @computed()
5793 get unsupportedBody() {
5794 const value = this.count;
5795 return value;
5796 }
5797
5798 @computed()
5799 get unresolvedRead() { return this.missing; }
5800
5801 @computed()
5802 get mismatch(): number { return "wrong"; }
5803
5804 @computed()
5805 get impure() { return helper(); }
5806
5807 @computed()
5808 get cycleA() { return this.cycleB; }
5809
5810 @computed()
5811 get cycleB() { return this.cycleA; }
5812
5813 render() { return <div />; }
5814}
5815"#,
5816 );
5817
5818 let asm = build_application_semantic_model(&parsed);
5819 let repeated = build_application_semantic_model(&parsed);
5820 assert_eq!(asm.diagnostics, repeated.diagnostics);
5821
5822 let component_graph = crate::build_component_graph(&parsed);
5823 let asm_from_component_graph =
5824 super::build_application_semantic_model_from_component_graph(&component_graph);
5825
5826 let codes = asm
5827 .diagnostics
5828 .iter()
5829 .map(|diagnostic| diagnostic.code.as_str())
5830 .collect::<std::collections::BTreeSet<_>>();
5831 assert_eq!(
5832 codes,
5833 std::collections::BTreeSet::from([
5834 "PSC1034", "PSC1035", "PSC1036", "PSC1037", "PSC1038", "PSC1039", "PSC1040",
5835 ])
5836 );
5837 assert_eq!(
5838 asm_from_component_graph
5839 .diagnostics
5840 .iter()
5841 .map(|diagnostic| diagnostic.code.as_str())
5842 .collect::<std::collections::BTreeSet<_>>(),
5843 codes
5844 );
5845 assert!(asm
5846 .diagnostics
5847 .iter()
5848 .all(|diagnostic| diagnostic.provenance.is_some()));
5849 assert!(asm.diagnostics.iter().any(|diagnostic| {
5850 diagnostic.code == crate::ComputedDiagnosticCode::InvalidDeclaration.as_str()
5851 && diagnostic.message
5852 == "@computed() declaration `invalidDeclaration` must decorate a getter"
5853 }));
5854 assert!(asm.diagnostics.iter().any(|diagnostic| {
5855 diagnostic.code == crate::ComputedDiagnosticCode::UnsupportedBody.as_str()
5856 && diagnostic.message == "computed getter `unsupportedBody` has an unsupported body"
5857 }));
5858 assert!(asm.diagnostics.iter().any(|diagnostic| {
5859 diagnostic.code == crate::ComputedDiagnosticCode::UnresolvedRead.as_str()
5860 && diagnostic.message
5861 == "computed getter `unresolvedRead` reads unresolved member `this.missing`"
5862 }));
5863 assert!(asm.diagnostics.iter().any(|diagnostic| {
5864 diagnostic.code == crate::ComputedDiagnosticCode::TypeMismatch.as_str()
5865 && diagnostic.message
5866 == "computed getter `mismatch` returns `\"wrong\"` but declares `number`"
5867 }));
5868 assert!(asm.diagnostics.iter().any(|diagnostic| {
5869 diagnostic.code == crate::ComputedDiagnosticCode::SerializationViolation.as_str()
5870 && diagnostic.message
5871 == "computed getter `unresolvedRead` has a non-serializable result"
5872 }));
5873 }
5874
5875 #[test]
5876 fn phase_e_audit_preserves_canonical_computed_products() {
5877 let path = "fixtures/0047-computed-diamond/input/ComputedDiamond.tsx";
5878 let parsed = presolve_parser::parse_file(
5879 path,
5880 include_str!("../../../fixtures/0047-computed-diamond/input/ComputedDiamond.tsx"),
5881 );
5882 let asm = build_application_semantic_model(&parsed);
5883 let component_graph = crate::build_component_graph(&parsed);
5884 let asm_from_component_graph =
5885 super::build_application_semantic_model_from_component_graph(&component_graph);
5886 let component = &asm.components[0];
5887 let count = component.id.state_field("count");
5888 let doubled = component.id.computed("doubled");
5889 let tripled = component.id.computed("tripled");
5890 let total = component.id.computed("total");
5891 let expected_owner = crate::SemanticOwner::entity(component.id.clone());
5892
5893 assert!(crate::validate_application_semantic_model(&asm).is_empty());
5894 assert!(crate::validate_application_semantic_model(&asm_from_component_graph).is_empty());
5895 for computed in [&doubled, &tripled, &total] {
5896 assert!(matches!(
5897 asm.entity(computed),
5898 Some(crate::SemanticEntity::Computed(_))
5899 ));
5900 assert_eq!(asm.owner(computed), Some(&expected_owner));
5901 assert_eq!(
5902 asm.semantic_types
5903 .computed_values
5904 .get(computed)
5905 .map(|computed_type| &computed_type.computed),
5906 Some(computed)
5907 );
5908 }
5909
5910 let reactive_references = asm
5911 .references
5912 .iter()
5913 .filter(|reference| {
5914 matches!(
5915 reference.kind,
5916 crate::SemanticReferenceKind::ComputedState
5917 | crate::SemanticReferenceKind::ComputedComputed
5918 )
5919 })
5920 .collect::<Vec<_>>();
5921 assert_eq!(reactive_references.len(), 4);
5922 for reference in reactive_references {
5923 assert!(asm.reactive_graph.edges.iter().any(|edge| {
5924 edge.kind == crate::IrReactiveEdgeKind::Reads
5925 && edge.source == reference.source.as_str()
5926 && edge.target == reference.target.as_str()
5927 && edge.provenance == reference.provenance
5928 }));
5929 assert!(asm.reactive_graph.edges.iter().any(|edge| {
5930 edge.kind == crate::IrReactiveEdgeKind::Invalidates
5931 && edge.source == reference.target.as_str()
5932 && edge.target == reference.source.as_str()
5933 && edge.provenance == reference.provenance
5934 }));
5935 }
5936 assert!(asm.reactive_graph.nodes.contains_key(count.as_str()));
5937 assert_eq!(
5938 asm.computed_evaluation_plan,
5939 crate::plan_computed_evaluation(&asm.reactive_graph)
5940 );
5941 assert_eq!(
5942 asm.computed_evaluation_plan.evaluation_order,
5943 vec![
5944 doubled.as_str().to_string(),
5945 tripled.as_str().to_string(),
5946 total.as_str().to_string(),
5947 ]
5948 );
5949
5950 let ir = crate::lower_components_to_ir(&asm);
5951 assert!(crate::validate_intermediate_representation(&ir).is_empty());
5952 let evaluations = ir
5953 .modules
5954 .iter()
5955 .flat_map(|module| &module.computed_evaluations)
5956 .map(|evaluation| evaluation.computed.clone())
5957 .collect::<Vec<_>>();
5958 assert_eq!(evaluations, vec![doubled, tripled, total]);
5959 }
5960
5961 #[test]
5962 fn classifies_computed_purity_and_reports_unsupported_behavior() {
5963 let mut parsed = presolve_parser::parse_file(
5964 "src/ComputedPurity.tsx",
5965 r#"
5966@component("x-computed-purity")
5967class ComputedPurity extends Component {
5968 count = state(1);
5969
5970 @action()
5971 save() {}
5972
5973 @effect()
5974 sync() {}
5975
5976 @computed()
5977 get pure() { return this.count; }
5978
5979 @computed()
5980 get mutates() { this.count++; return 1; }
5981
5982 @computed()
5983 get actionCall() { return this.save(); }
5984
5985 @computed()
5986 get effectCall() { console.log("effect"); return 1; }
5987
5988 @computed()
5989 get resourceCall() { return resource(); }
5990
5991 @computed()
5992 get arbitraryCall() { return helper(); }
5993
5994 @computed()
5995 get random() { return Math.random(); }
5996
5997 @computed()
5998 get asyncValue() { return 1; }
5999}
6000"#,
6001 );
6002 parsed.classes[0]
6003 .methods
6004 .iter_mut()
6005 .find(|method| method.name == "asyncValue")
6006 .expect("async test getter")
6007 .is_async = true;
6008
6009 let asm = build_application_semantic_model(&parsed);
6010 let component = &asm.components[0];
6011 let pure = component.id.computed("pure");
6012 let mutates = component.id.computed("mutates");
6013 let action_call = component.id.computed("actionCall");
6014 let effect_call = component.id.computed("effectCall");
6015 let resource_call = component.id.computed("resourceCall");
6016 let arbitrary_call = component.id.computed("arbitraryCall");
6017 let random = component.id.computed("random");
6018 let async_value = component.id.computed("asyncValue");
6019
6020 assert_eq!(
6021 asm.computed_value(&pure).expect("pure value").purity,
6022 crate::ComputedPurity::Pure
6023 );
6024 for (computed, kind) in [
6025 (mutates, crate::ComputedPurityViolationKind::StateMutation),
6026 (action_call, crate::ComputedPurityViolationKind::Action),
6027 (effect_call, crate::ComputedPurityViolationKind::Effect),
6028 (resource_call, crate::ComputedPurityViolationKind::Resource),
6029 (
6030 arbitrary_call,
6031 crate::ComputedPurityViolationKind::ArbitraryMethodCall,
6032 ),
6033 (
6034 random,
6035 crate::ComputedPurityViolationKind::NondeterministicOperation,
6036 ),
6037 (async_value, crate::ComputedPurityViolationKind::Async),
6038 ] {
6039 let value = asm.computed_value(&computed).expect("computed value");
6040 assert_eq!(value.purity, crate::ComputedPurity::Impure);
6041 assert!(value
6042 .purity_violations
6043 .iter()
6044 .any(|violation| violation.kind == kind));
6045 }
6046 let diagnostics = asm
6047 .diagnostics
6048 .iter()
6049 .filter(|diagnostic| diagnostic.code == "PSC1034")
6050 .collect::<Vec<_>>();
6051 assert_eq!(diagnostics.len(), 7);
6052 assert!(diagnostics
6053 .iter()
6054 .all(|diagnostic| diagnostic.provenance.is_some()));
6055 }
6056
6057 #[test]
6058 fn establishes_typed_action_input_and_output_contracts() {
6059 let parsed = presolve_parser::parse_file(
6060 "src/Action.tsx",
6061 r#"
6062type ActionResult = number;
6063
6064@component("x-action")
6065class Action extends Component {
6066 @action()
6067 async addTodo(input: string): ActionResult { return 1; }
6068}
6069"#,
6070 );
6071
6072 let asm = build_application_semantic_model(&parsed);
6073 let method = &asm.components[0].methods[0];
6074 let signature = asm
6075 .semantic_types
6076 .action_signatures
6077 .get(&method.id)
6078 .expect("action signature");
6079
6080 assert!(method.is_action());
6081 assert!(signature.is_async);
6082 assert_eq!(signature.input.len(), 1);
6083 assert_eq!(signature.input[0].1, crate::SemanticType::String);
6084 assert_eq!(signature.output, Some(crate::SemanticType::Number));
6085 }
6086
6087 #[test]
6088 fn lowers_supported_literal_state_annotations_into_canonical_types() {
6089 let parsed = presolve_parser::parse_file(
6090 "src/LiteralTypes.tsx",
6091 r#"
6092@component("x-literal-types")
6093class LiteralTypes extends Component {
6094 filter: "all" = state("all");
6095 step: 42 = state(42);
6096 enabled: true = state(true);
6097}
6098"#,
6099 );
6100
6101 let asm = build_application_semantic_model(&parsed);
6102 let types = asm.components[0]
6103 .state_fields
6104 .iter()
6105 .map(|field| {
6106 (
6107 field.name.as_str(),
6108 &asm.semantic_types.assignments[&field.id].semantic_type,
6109 )
6110 })
6111 .collect::<Vec<_>>();
6112
6113 assert_eq!(
6114 types,
6115 vec![
6116 (
6117 "filter",
6118 &crate::SemanticType::StringLiteral("all".to_string())
6119 ),
6120 (
6121 "step",
6122 &crate::SemanticType::NumberLiteral("42".to_string())
6123 ),
6124 ("enabled", &crate::SemanticType::BooleanLiteral(true)),
6125 ]
6126 );
6127 }
6128
6129 #[test]
6130 fn lowers_array_and_tuple_state_annotations_into_canonical_types() {
6131 let parsed = presolve_parser::parse_file(
6132 "src/CollectionTypes.tsx",
6133 r#"
6134@component("x-collection-types")
6135class CollectionTypes extends Component {
6136 names: string[] = state([]);
6137 todos: Todo[] = state([]);
6138 pair: [string, number] = state(["Presolve", 1]);
6139}
6140"#,
6141 );
6142
6143 let asm = build_application_semantic_model(&parsed);
6144 let types = asm.components[0]
6145 .state_fields
6146 .iter()
6147 .map(|field| {
6148 (
6149 field.name.as_str(),
6150 &asm.semantic_types.assignments[&field.id].semantic_type,
6151 )
6152 })
6153 .collect::<Vec<_>>();
6154
6155 assert_eq!(
6156 types,
6157 vec![
6158 (
6159 "names",
6160 &crate::SemanticType::Array(Box::new(crate::SemanticType::String)),
6161 ),
6162 (
6163 "todos",
6164 &crate::SemanticType::Array(Box::new(crate::SemanticType::Unknown)),
6165 ),
6166 (
6167 "pair",
6168 &crate::SemanticType::Tuple(vec![
6169 crate::SemanticType::String,
6170 crate::SemanticType::Number,
6171 ]),
6172 ),
6173 ]
6174 );
6175 }
6176
6177 #[test]
6178 fn lowers_structural_object_state_annotations_into_canonical_types() {
6179 let parsed = presolve_parser::parse_file(
6180 "src/ObjectTypes.tsx",
6181 r#"
6182@component("x-object-types")
6183class ObjectTypes extends Component {
6184 todo: { id: string; title: string; completed: boolean } = state({ id: "1", title: "Presolve", completed: false });
6185}
6186"#,
6187 );
6188
6189 let asm = build_application_semantic_model(&parsed);
6190 let field = &asm.components[0].state_fields[0];
6191 let assignment = &asm.semantic_types.assignments[&field.id];
6192 let crate::SemanticType::Object(object) = &assignment.semantic_type else {
6193 panic!("expected structural object type");
6194 };
6195
6196 assert_eq!(
6197 object.properties,
6198 std::collections::BTreeMap::from([
6199 ("completed".to_string(), crate::SemanticType::Boolean),
6200 ("id".to_string(), crate::SemanticType::String),
6201 ("title".to_string(), crate::SemanticType::String),
6202 ])
6203 );
6204 }
6205
6206 #[test]
6207 fn lowers_union_and_nullable_state_annotations_into_canonical_types() {
6208 let parsed = presolve_parser::parse_file(
6209 "src/UnionTypes.tsx",
6210 r#"
6211@component("x-union-types")
6212class UnionTypes extends Component {
6213 filter: "all" | "active" | "completed" = state("all");
6214 user: { id: string } | null = state(null);
6215}
6216"#,
6217 );
6218
6219 let asm = build_application_semantic_model(&parsed);
6220 let fields = &asm.components[0].state_fields;
6221 assert_eq!(
6222 asm.semantic_types.assignments[&fields[0].id].semantic_type,
6223 crate::SemanticType::Union(vec![
6224 crate::SemanticType::StringLiteral("active".to_string()),
6225 crate::SemanticType::StringLiteral("all".to_string()),
6226 crate::SemanticType::StringLiteral("completed".to_string()),
6227 ])
6228 );
6229 assert_eq!(
6230 asm.semantic_types.assignments[&fields[1].id].semantic_type,
6231 crate::SemanticType::Union(vec![
6232 crate::SemanticType::Null,
6233 crate::SemanticType::Object(crate::ObjectType {
6234 properties: std::collections::BTreeMap::from([(
6235 "id".to_string(),
6236 crate::SemanticType::String,
6237 )]),
6238 }),
6239 ])
6240 );
6241 }
6242
6243 #[test]
6244 fn resolves_local_type_aliases_with_canonical_alias_identity() {
6245 let parsed = presolve_parser::parse_file(
6246 "src/Aliases.tsx",
6247 r#"
6248type TodoId = string;
6249type Filter = "all" | "active" | "completed";
6250
6251@component("x-aliases")
6252class Aliases extends Component {
6253 id: TodoId = state("todo-1");
6254 filter: Filter = state("all");
6255}
6256"#,
6257 );
6258
6259 let asm = build_application_semantic_model(&parsed);
6260 let fields = &asm.components[0].state_fields;
6261 let todo_id = asm
6262 .semantic_types
6263 .aliases
6264 .values()
6265 .find(|alias| alias.name == "TodoId")
6266 .expect("TodoId alias");
6267 let filter = asm
6268 .semantic_types
6269 .aliases
6270 .values()
6271 .find(|alias| alias.name == "Filter")
6272 .expect("Filter alias");
6273
6274 assert_eq!(todo_id.semantic_type, crate::SemanticType::String);
6275 assert_eq!(
6276 filter.semantic_type,
6277 crate::SemanticType::Union(vec![
6278 crate::SemanticType::StringLiteral("active".to_string()),
6279 crate::SemanticType::StringLiteral("all".to_string()),
6280 crate::SemanticType::StringLiteral("completed".to_string()),
6281 ])
6282 );
6283 assert_eq!(
6284 asm.semantic_types.assignments[&fields[0].id].origin,
6285 todo_id.id
6286 );
6287 assert_eq!(
6288 asm.semantic_types.assignments[&fields[1].id].origin,
6289 filter.id
6290 );
6291 }
6292
6293 #[test]
6294 fn resolves_imported_type_aliases_through_named_reexports() {
6295 let unit = CompilationUnit::parse_sources([
6296 (
6297 "src/types.ts",
6298 r#"export type Filter = "all" | "active" | "completed";"#,
6299 ),
6300 ("src/index.ts", r#"export { Filter } from "./types";"#),
6301 (
6302 "src/App.tsx",
6303 r#"
6304import { Filter } from "./index";
6305
6306@component("x-app")
6307class App extends Component {
6308 filter: Filter = state("all");
6309}
6310"#,
6311 ),
6312 ]);
6313
6314 let asm = build_application_semantic_model_for_unit(&unit);
6315 let field = &asm
6316 .components
6317 .iter()
6318 .find(|component| component.class_name == "App")
6319 .expect("App component")
6320 .state_fields[0];
6321 let assignment = &asm.semantic_types.assignments[&field.id];
6322
6323 assert_eq!(
6324 assignment.semantic_type,
6325 crate::SemanticType::Union(vec![
6326 crate::SemanticType::StringLiteral("active".to_string()),
6327 crate::SemanticType::StringLiteral("all".to_string()),
6328 crate::SemanticType::StringLiteral("completed".to_string()),
6329 ])
6330 );
6331 assert_eq!(
6332 assignment.origin,
6333 crate::SemanticId::type_alias_in_module("src/types.ts", "Filter")
6334 );
6335 }
6336
6337 #[test]
6338 fn infers_state_types_from_direct_serializable_initializers() {
6339 let parsed = presolve_parser::parse_file(
6340 "src/InferredState.tsx",
6341 r#"
6342@component("x-inferred-state")
6343class InferredState extends Component {
6344 count = state(0);
6345 todos = state([]);
6346 tags = state(["Presolve"]);
6347 todo = state({ id: "1", completed: false });
6348}
6349"#,
6350 );
6351
6352 let asm = build_application_semantic_model(&parsed);
6353 let fields = &asm.components[0].state_fields;
6354 let types = fields
6355 .iter()
6356 .map(|field| {
6357 let assignment = &asm.semantic_types.assignments[&field.id];
6358 assert_eq!(assignment.status, crate::SemanticTypeStatus::Inferred);
6359 (field.name.as_str(), assignment.semantic_type.clone())
6360 })
6361 .collect::<Vec<_>>();
6362
6363 assert_eq!(
6364 types,
6365 vec![
6366 ("count", crate::SemanticType::Number),
6367 (
6368 "todos",
6369 crate::SemanticType::Array(Box::new(crate::SemanticType::Unknown)),
6370 ),
6371 (
6372 "tags",
6373 crate::SemanticType::Array(Box::new(crate::SemanticType::String)),
6374 ),
6375 (
6376 "todo",
6377 crate::SemanticType::Object(crate::ObjectType {
6378 properties: std::collections::BTreeMap::from([
6379 ("completed".to_string(), crate::SemanticType::Boolean),
6380 ("id".to_string(), crate::SemanticType::String),
6381 ]),
6382 }),
6383 ),
6384 ]
6385 );
6386 }
6387
6388 #[test]
6389 fn propagates_canonical_types_to_expression_graph_nodes() {
6390 let parsed = presolve_parser::parse_file(
6391 "src/ExpressionTypes.tsx",
6392 r#"
6393@component("x-expression-types")
6394class ExpressionTypes extends Component {
6395 total = state((1 + 2) * 3);
6396 ready = state(1 < 2);
6397}
6398"#,
6399 );
6400
6401 let asm = build_application_semantic_model(&parsed);
6402 let total = &asm.components[0].state_fields[0];
6403 let ready = &asm.components[0].state_fields[1];
6404 let total_root = asm
6405 .expression_root(&total.id)
6406 .expect("total expression root");
6407 let ready_root = asm
6408 .expression_root(&ready.id)
6409 .expect("ready expression root");
6410
6411 assert_eq!(
6412 asm.semantic_types.assignments[total_root].semantic_type,
6413 crate::SemanticType::Number
6414 );
6415 assert_eq!(
6416 asm.semantic_types.assignments[ready_root].semantic_type,
6417 crate::SemanticType::Boolean
6418 );
6419 assert!(asm
6420 .expression_dependencies(total_root)
6421 .iter()
6422 .all(|id| asm.semantic_types.assignments.contains_key(*id)));
6423 }
6424
6425 #[test]
6426 fn derives_component_ownership_without_legacy_owner_fields() {
6427 let parsed = presolve_parser::parse_file(
6428 "src/Counter.tsx",
6429 r#"
6430@component("x-counter")
6431class Counter extends Component {
6432 count = state(0);
6433
6434 increment() {
6435 this.count++;
6436 }
6437
6438 render() {
6439 return <button onClick={() => this.increment()}>{this.count}</button>;
6440 }
6441}
6442"#,
6443 );
6444 let mut component_graph = build_component_graph_for_module(&parsed);
6445 let mut templates = build_template_graph(&component_graph).templates;
6446 let template_entities = build_template_semantic_entities(&templates);
6447 let component = component_graph
6448 .components
6449 .first_mut()
6450 .expect("component graph should contain Counter");
6451 let component_id = component.id.clone();
6452 let method_id = component.methods[0].id.clone();
6453 let action_id = component.actions[0].id.clone();
6454 let state_id = component.state_fields[0].id.clone();
6455 let event_id = component
6456 .render
6457 .as_ref()
6458 .expect("Counter should render")
6459 .event_handlers[0]
6460 .id
6461 .clone();
6462
6463 component.owner = SemanticOwner::entity(component_id.clone());
6464 component.state_fields[0].owner = SemanticOwner::Application;
6465 component.methods[0].owner = SemanticOwner::Application;
6466 component.actions[0].owner = SemanticOwner::Application;
6467 component
6468 .render
6469 .as_mut()
6470 .expect("Counter should render")
6471 .event_handlers[0]
6472 .owner = SemanticOwner::Application;
6473 templates[0].owner = SemanticOwner::Application;
6474
6475 let ownership = collect_ownership(
6476 &component_graph.components,
6477 &std::collections::BTreeMap::new(),
6478 &std::collections::BTreeMap::new(),
6479 &std::collections::BTreeMap::new(),
6480 &std::collections::BTreeMap::new(),
6481 &std::collections::BTreeMap::new(),
6482 &std::collections::BTreeMap::new(),
6483 &std::collections::BTreeMap::new(),
6484 &std::collections::BTreeMap::new(),
6485 &std::collections::BTreeMap::new(),
6486 &std::collections::BTreeMap::new(),
6487 &ComponentInstancePlan::default(),
6488 &std::collections::BTreeMap::new(),
6489 &std::collections::BTreeMap::new(),
6490 &templates,
6491 &template_entities,
6492 );
6493 assert_eq!(ownership[&component_id], SemanticOwner::Application);
6494 assert_eq!(
6495 ownership[&state_id],
6496 SemanticOwner::entity(component_id.clone())
6497 );
6498 assert_eq!(
6499 ownership[&method_id],
6500 SemanticOwner::entity(component_id.clone())
6501 );
6502 assert_eq!(ownership[&action_id], SemanticOwner::entity(method_id));
6503 assert_eq!(
6504 ownership[&event_id],
6505 SemanticOwner::entity(component_id.clone().template())
6506 );
6507 assert_eq!(
6508 ownership[&templates[0].id],
6509 SemanticOwner::entity(component_id)
6510 );
6511 }
6512
6513 #[test]
6514 #[allow(clippy::too_many_lines)]
6515 fn traverses_application_ownership_in_semantic_id_order() {
6516 let parsed = presolve_parser::parse_file(
6517 "src/Counter.tsx",
6518 r#"
6519@component("x-counter")
6520class Counter extends Component {
6521 count = state(0);
6522
6523 increment() {
6524 this.count++;
6525 }
6526
6527 render() {
6528 return <button onClick={this.increment}>{this.count}</button>;
6529 }
6530}
6531"#,
6532 );
6533
6534 let asm = build_application_semantic_model(&parsed);
6535 let component = &asm.components[0];
6536 let roots = asm.application_roots();
6537
6538 assert_eq!(
6539 roots,
6540 vec![
6541 &component.id,
6542 asm.component_instance_plan
6543 .instances
6544 .values()
6545 .find(|instance| instance.depth == 0)
6546 .expect("build root instance")
6547 .id
6548 .as_semantic_id(),
6549 ]
6550 );
6551 assert_eq!(
6552 asm.children_of(&component.id),
6553 vec![
6554 &component.methods[0].id,
6555 &component.methods[1].id,
6556 &component.state_fields[0].id,
6557 &asm.templates[0].id,
6558 ]
6559 );
6560 assert_eq!(
6561 asm.children_of(&component.methods[0].id),
6562 vec![&component.actions[0].id]
6563 );
6564 assert_eq!(asm.parent_of(&component.id), None);
6565 assert_eq!(
6566 asm.parent_of(&component.actions[0].id),
6567 Some(&component.methods[0].id)
6568 );
6569 assert_eq!(
6570 asm.ancestors_of(&component.actions[0].id),
6571 vec![&component.methods[0].id, &component.id]
6572 );
6573 let descendants = asm.descendants_of(&component.id);
6574 assert_eq!(descendants[0], &component.methods[0].id);
6575 assert_eq!(descendants[1], &component.actions[0].id);
6576 assert_eq!(descendants[2], &component.methods[1].id);
6577 assert_eq!(
6578 descendants.len(),
6579 asm.ownership.len() - asm.application_roots().len()
6580 );
6581 assert_eq!(
6582 asm.entities_of_kind(SemanticEntityKind::Method),
6583 vec![&component.methods[0].id, &component.methods[1].id]
6584 );
6585 assert_eq!(
6586 asm.entities_of_kind(SemanticEntityKind::Action),
6587 vec![&component.actions[0].id]
6588 );
6589 assert_eq!(
6590 asm.entities_of_kind(SemanticEntityKind::StateField),
6591 vec![&component.state_fields[0].id]
6592 );
6593 let state_id = &component.state_fields[0].id;
6594 let state_provenance = asm.provenance(state_id).expect("state provenance");
6595 assert_eq!(
6596 asm.entities_in_file(state_provenance.path.as_path()).len(),
6597 asm.ownership.len()
6598 );
6599 let at_state =
6600 asm.entities_at(state_provenance.path.as_path(), state_provenance.span.start);
6601 assert!(at_state.contains(&state_id));
6602 assert!(at_state.iter().all(|id| {
6603 let provenance = asm.provenance(id).expect("entity provenance");
6604 provenance.span.start <= state_provenance.span.start
6605 && state_provenance.span.start < provenance.span.end
6606 }));
6607 let action_references = asm.references_of_kind(SemanticReferenceKind::ActionState);
6608 assert_eq!(action_references.len(), 1);
6609 assert_eq!(action_references[0].source, component.actions[0].id);
6610 assert_eq!(action_references[0].target, component.state_fields[0].id);
6611 let action_provenance = &action_references[0].provenance;
6612 assert_eq!(
6613 asm.references_in_file(action_provenance.path.as_path())
6614 .len(),
6615 asm.references.len()
6616 );
6617 let at_action = asm.references_at(
6618 action_provenance.path.as_path(),
6619 action_provenance.span.start,
6620 );
6621 assert!(at_action.iter().any(|reference| {
6622 reference.source == component.actions[0].id
6623 && reference.target == component.state_fields[0].id
6624 }));
6625 }
6626
6627 #[test]
6628 fn resolves_template_state_dependencies() {
6629 let parsed = presolve_parser::parse_file(
6630 "src/Panel.tsx",
6631 r#"
6632@component("x-panel")
6633class Panel extends Component {
6634 enabled = state(true);
6635
6636 render() {
6637 return <section hidden={this.enabled}>{this.enabled}{this.enabled ? <span>On</span> : <span>Off</span>}</section>;
6638 }
6639}
6640"#,
6641 );
6642
6643 let asm = build_application_semantic_model(&parsed);
6644 let component = &asm.components[0];
6645 let state = &component.state_fields[0];
6646 let state_references = asm.references_to(&state.id);
6647
6648 assert_eq!(state_references.len(), 3);
6649 assert!(state_references.iter().all(|reference| {
6650 reference.kind == SemanticReferenceKind::TemplateState
6651 && reference.target == state.id
6652 && reference.provenance.path.as_path() == std::path::Path::new("src/Panel.tsx")
6653 }));
6654 assert!(state_references.iter().any(|reference| {
6655 asm.template_entity(&reference.source)
6656 .is_some_and(|entity| entity.kind == TemplateSemanticKind::Binding)
6657 }));
6658 assert!(state_references.iter().any(|reference| {
6659 asm.template_entity(&reference.source)
6660 .is_some_and(|entity| entity.kind == TemplateSemanticKind::AttributeBinding)
6661 }));
6662 assert!(state_references.iter().any(|reference| {
6663 asm.template_entity(&reference.source)
6664 .is_some_and(|entity| entity.kind == TemplateSemanticKind::Conditional)
6665 }));
6666 }
6667
6668 #[test]
6669 fn resolves_template_bindings_to_canonical_computed_entities() {
6670 let parsed = presolve_parser::parse_file(
6671 "src/ComputedTemplate.tsx",
6672 r#"
6673@component("x-computed-template")
6674class ComputedTemplate extends Component {
6675 @computed()
6676 get label(): string { return "Ready"; }
6677
6678 @computed()
6679 get visible(): boolean { return true; }
6680
6681 render() {
6682 return <section title={this.label}>{this.label}{this.visible ? <span>Visible</span> : <span>Hidden</span>}</section>;
6683 }
6684}
6685"#,
6686 );
6687
6688 let asm = build_application_semantic_model(&parsed);
6689 let component = &asm.components[0];
6690 let label = component.id.computed("label");
6691 let visible = component.id.computed("visible");
6692 let references = asm.references_of_kind(SemanticReferenceKind::TemplateComputed);
6693
6694 assert_eq!(references.len(), 3);
6695 assert_eq!(
6696 references
6697 .iter()
6698 .filter(|reference| reference.target == label)
6699 .count(),
6700 2
6701 );
6702 assert!(references.iter().any(|reference| {
6703 reference.target == visible
6704 && asm
6705 .template_entity(&reference.source)
6706 .is_some_and(|entity| entity.kind == TemplateSemanticKind::Conditional)
6707 }));
6708 assert!(references.iter().all(|reference| {
6709 asm.provenance(&reference.source) == Some(&reference.provenance)
6710 && asm.semantic_type_of(&reference.source)
6711 == asm.semantic_type_of(&reference.target)
6712 }));
6713 assert_eq!(
6714 asm.references_of_kind(SemanticReferenceKind::TemplateState)
6715 .into_iter()
6716 .filter(|reference| reference.target == label || reference.target == visible)
6717 .count(),
6718 0
6719 );
6720
6721 let graph = crate::build_semantic_graph(&asm);
6722 assert_eq!(
6723 graph
6724 .edges
6725 .iter()
6726 .filter(|edge| edge.kind == crate::SemanticGraphEdgeKind::TemplateComputed)
6727 .count(),
6728 3
6729 );
6730 }
6731
6732 #[test]
6733 fn navigates_template_entities_from_canonical_ownership() {
6734 let parsed = presolve_parser::parse_file(
6735 "src/Panel.tsx",
6736 r#"
6737@component("x-panel")
6738class Panel extends Component {
6739 count = state(0);
6740
6741 render() {
6742 return <section>{this.count}</section>;
6743 }
6744}
6745"#,
6746 );
6747 let mut asm = build_application_semantic_model(&parsed);
6748 let template_id = asm.templates[0].id.clone();
6749 let expected = asm.template_entities_for(&template_id).len();
6750
6751 for entity in &mut asm.template_entities {
6752 entity.owner = SemanticOwner::Application;
6753 }
6754
6755 assert_eq!(asm.template_entities_for(&template_id).len(), expected);
6756 }
6757
6758 #[test]
6759 fn leaves_member_expressions_unresolved_without_expression_evaluation() {
6760 let parsed = presolve_parser::parse_file(
6761 "src/Panel.tsx",
6762 r#"
6763@component("x-panel")
6764class Panel extends Component {
6765 enabled = state(true);
6766
6767 render() {
6768 return <section data-value={this.enabled.value}>Panel</section>;
6769 }
6770}
6771"#,
6772 );
6773
6774 let asm = build_application_semantic_model(&parsed);
6775 assert_eq!(
6776 asm.references
6777 .iter()
6778 .filter(|reference| reference.kind == SemanticReferenceKind::TemplateState)
6779 .count(),
6780 0
6781 );
6782 }
6783
6784 #[test]
6785 fn resolves_keyed_list_iterable_to_component_state() {
6786 let parsed = presolve_parser::parse_file(
6787 "src/KeyedList.tsx",
6788 r#"
6789@component("x-keyed-list")
6790class KeyedList extends Component {
6791 items = state([]);
6792
6793 render() {
6794 return <ul>{this.items.map((item) => <li key={item}>{item}</li>)}</ul>;
6795 }
6796}
6797"#,
6798 );
6799
6800 let asm = build_application_semantic_model(&parsed);
6801 let component = &asm.components[0];
6802 let state = &component.state_fields[0];
6803 let reference = asm
6804 .references_to(&state.id)
6805 .into_iter()
6806 .find(|reference| reference.kind == SemanticReferenceKind::TemplateState)
6807 .expect("list iterable state reference");
6808 let list = asm
6809 .template_entity(&reference.source)
6810 .expect("list semantic entity");
6811
6812 assert_eq!(list.kind, TemplateSemanticKind::List);
6813 assert_eq!(list.expression.as_deref(), Some("this.items"));
6814 assert_eq!(reference.provenance, list.provenance);
6815 }
6816
6817 #[test]
6818 fn resolves_template_event_attribute_to_component_method() {
6819 let parsed = presolve_parser::parse_file(
6820 "src/Counter.tsx",
6821 r#"
6822@component("x-counter")
6823class Counter extends Component {
6824 count = state(0);
6825
6826 increment() {
6827 this.count += 1;
6828 }
6829
6830 render() {
6831 return <button onClick={() => this.increment()}>Count</button>;
6832 }
6833}
6834"#,
6835 );
6836
6837 let asm = build_application_semantic_model(&parsed);
6838 let component = &asm.components[0];
6839 let method = component
6840 .methods
6841 .iter()
6842 .find(|method| method.name == "increment")
6843 .expect("increment method");
6844 let reference = asm
6845 .references_to(&method.id)
6846 .into_iter()
6847 .find(|reference| {
6848 reference.kind == SemanticReferenceKind::EventMethod
6849 && asm
6850 .template_entity(&reference.source)
6851 .is_some_and(|entity| entity.kind == TemplateSemanticKind::EventAttribute)
6852 })
6853 .expect("template event method reference");
6854
6855 assert_eq!(
6856 reference.provenance.path,
6857 std::path::Path::new("src/Counter.tsx")
6858 );
6859 assert_eq!(reference.provenance.span.line, 11);
6860 }
6861
6862 #[test]
6863 fn resolves_render_template_bindings_to_unique_method_locals() {
6864 let parsed = presolve_parser::parse_file(
6865 "src/LocalResolution.tsx",
6866 r#"
6867@component("x-local-resolution")
6868class LocalResolution extends Component {
6869 render() {
6870 const title = "Presolve";
6871 return <output title={title}>{title}</output>;
6872 }
6873}
6874"#,
6875 );
6876
6877 let asm = build_application_semantic_model(&parsed);
6878 let component = &asm.components[0];
6879 let render = component
6880 .methods
6881 .iter()
6882 .find(|method| method.name == "render")
6883 .expect("render method");
6884 let local = &render.local_variables[0];
6885 let references = asm.references_of_kind(SemanticReferenceKind::TemplateLocal);
6886 let assignment = asm
6887 .semantic_types
6888 .assignments
6889 .get(&local.id)
6890 .expect("canonical inferred local type");
6891
6892 assert_eq!(
6893 asm.owner(&local.id),
6894 Some(&SemanticOwner::entity(render.id.clone()))
6895 );
6896 assert_eq!(
6897 asm.entities_of_kind(SemanticEntityKind::LocalVariable),
6898 vec![&local.id]
6899 );
6900 assert_eq!(references.len(), 2);
6901 assert!(references.iter().all(|reference| {
6902 reference.target == local.id
6903 && reference.provenance.path == std::path::Path::new("src/LocalResolution.tsx")
6904 }));
6905 assert_eq!(assignment.semantic_type, crate::SemanticType::String);
6906 assert_eq!(assignment.status, crate::SemanticTypeStatus::Inferred);
6907 assert_eq!(assignment.provenance.span, local.span);
6908
6909 let semantic_graph = crate::build_semantic_graph(&asm);
6910 assert!(semantic_graph.nodes.iter().any(|node| {
6911 node.kind == crate::SemanticGraphNodeKind::LocalVariable && node.id == local.id
6912 }));
6913 assert_eq!(
6914 semantic_graph
6915 .edges
6916 .iter()
6917 .filter(|edge| {
6918 edge.kind == crate::SemanticGraphEdgeKind::TemplateLocal
6919 && edge.target == local.id
6920 })
6921 .count(),
6922 2
6923 );
6924
6925 let template_graph = build_template_graph(&build_component_graph_for_module(&parsed));
6926 assert_eq!(
6927 crate::generate_static_html(&template_graph),
6928 "<output data-presolve-node=\"n0\" title=\"Presolve\" data-presolve-bindings=\"title\"><!-- presolve-binding:n2:title -->Presolve</output>\n"
6929 );
6930 }
6931
6932 #[test]
6933 fn queries_canonical_expression_graphs_by_dependency_provenance_and_owner() {
6934 let parsed = presolve_parser::parse_file(
6935 "src/ExpressionQueries.tsx",
6936 r#"
6937@component("x-expression-queries")
6938class ExpressionQueries extends Component {
6939 total = state((1 + 2) * 3);
6940}
6941"#,
6942 );
6943
6944 let asm = build_application_semantic_model(&parsed);
6945 let field = &asm.components[0].state_fields[0];
6946 let root = asm.expression_root(&field.id).expect("expression root");
6947 let left = field.id.expression("root.0");
6948 let right = field.id.expression("root.1");
6949
6950 assert_eq!(asm.expression(root).map(|node| &node.id), Some(root));
6951 assert_eq!(asm.expression_owner(root), Some(&field.id));
6952 assert_eq!(asm.expression_dependencies(root), vec![&left, &right]);
6953 assert_eq!(
6954 asm.expression_dependents(&left)
6955 .into_iter()
6956 .map(|node| &node.id)
6957 .collect::<Vec<_>>(),
6958 vec![root]
6959 );
6960 assert_eq!(asm.expressions_for(&field.id).len(), 5);
6961
6962 let provenance = asm
6963 .expression_provenance(root)
6964 .expect("expression provenance");
6965 assert_eq!(
6966 provenance.path,
6967 std::path::Path::new("src/ExpressionQueries.tsx")
6968 );
6969 assert_eq!(asm.expressions_in_file(&provenance.path).len(), 5);
6970 assert_eq!(
6971 asm.expressions_at(&provenance.path, provenance.span.start)
6972 .into_iter()
6973 .map(|node| &node.id)
6974 .collect::<Vec<_>>(),
6975 vec![root]
6976 );
6977 }
6978
6979 #[test]
6980 fn file_route_model_composes_layout_default_slots_into_canonical_instances() {
6981 let unit = CompilationUnit::parse_sources([
6982 (
6983 "app/layout.tsx",
6984 r#"
6985@component()
6986class AppLayout extends Component {
6987 @slot() children!: SlotContent;
6988 render() { return <main><slot /></main>; }
6989}
6990"#,
6991 ),
6992 (
6993 "app/routes/index.tsx",
6994 r#"
6995@component()
6996class Home extends Component {
6997 render() { return <article>Home</article>; }
6998}
6999"#,
7000 ),
7001 ]);
7002 let asm = build_file_route_application_semantic_model_for_unit_with_packages(
7003 &unit,
7004 &crate::SemanticPackageResolutionTable::default(),
7005 )
7006 .expect("valid conventional layout");
7007
7008 assert_eq!(asm.component_instance_plan.roots.len(), 1);
7009 let binding = asm
7010 .slot_bindings
7011 .bindings
7012 .values()
7013 .find(|binding| binding.direct_child_instance.is_some())
7014 .expect("compiler-issued layout Slot binding");
7015 assert_eq!(binding.status, crate::SlotBindingStatus::Bound);
7016 assert!(binding.content_fragment.is_none());
7017 assert!(crate::validate_application_semantic_model(&asm).is_empty());
7018
7019 let html = crate::generate_ordinary_instance_html(&asm);
7020 assert!(html.contains("<main"));
7021 assert!(html.contains("<article"));
7022 assert!(html.contains("Home"));
7023 }
7024
7025 #[test]
7026 fn selected_file_route_excludes_sibling_instances_and_runtime_targets() {
7027 let unit = CompilationUnit::parse_sources([
7028 (
7029 "app/layout.tsx",
7030 r#"
7031@component()
7032class AppLayout extends Component {
7033 @slot() children!: SlotContent;
7034 render() { return <main><slot /></main>; }
7035}
7036"#,
7037 ),
7038 (
7039 "app/routes/index.tsx",
7040 r#"
7041@component()
7042class Home extends Component {
7043 count = state(0);
7044 @action() increment() { this.count += 1; }
7045 render() { return <button onClick={this.increment}>{this.count}</button>; }
7046}
7047"#,
7048 ),
7049 (
7050 "app/routes/about.tsx",
7051 r#"
7052@component()
7053class About extends Component {
7054 open = state(false);
7055 @action() toggle() { this.open = !this.open; }
7056 render() { return <button onClick={this.toggle}>{this.open}</button>; }
7057}
7058"#,
7059 ),
7060 ]);
7061 let packages = crate::SemanticPackageResolutionTable::default();
7062 let all_routes =
7063 build_file_route_application_semantic_model_for_unit_with_packages(&unit, &packages)
7064 .expect("valid file routes");
7065 let home = all_routes
7066 .components
7067 .iter()
7068 .find(|component| component.module_path == std::path::Path::new("app/routes/index.tsx"))
7069 .expect("home route")
7070 .id
7071 .clone();
7072 let about = all_routes
7073 .components
7074 .iter()
7075 .find(|component| component.module_path == std::path::Path::new("app/routes/about.tsx"))
7076 .expect("about route")
7077 .id
7078 .clone();
7079
7080 let selected = crate::build_file_route_application_semantic_model_for_route_with_packages(
7081 &unit, &packages, &home,
7082 )
7083 .expect("selected route model");
7084 assert_eq!(selected.component_instance_plan.roots.len(), 1);
7085 assert!(selected
7086 .component_instance_plan
7087 .instances
7088 .values()
7089 .any(|instance| instance.component == home));
7090 assert!(!selected
7091 .component_instance_plan
7092 .instances
7093 .values()
7094 .any(|instance| instance.component == about));
7095
7096 let artifact =
7097 crate::build_runtime_component_artifact(&selected, &selected.component_ir_optimization);
7098 assert!(artifact
7099 .ordinary_template_events
7100 .iter()
7101 .all(|event| event.component_id == home.to_string()));
7102 assert!(artifact
7103 .ordinary_template_targets
7104 .iter()
7105 .all(|target| target.component_id != about.to_string()));
7106 }
7107
7108 #[test]
7109 fn retains_route_loader_source_facts_before_server_capability_resolution() {
7110 let asm = build_application_semantic_model(&presolve_parser::parse_file(
7111 "app/routes/posts/[slug].tsx",
7112 r#"
7113@component()
7114class Post {
7115 @loader("loadPost") post!: Resource<Post, NotFound>;
7116 render() { return <article />; }
7117}
7118"#,
7119 ));
7120
7121 let loader = &asm.components[0].route_loader_declaration_candidates[0];
7122 assert_eq!(loader.field, "post");
7123 assert!(loader.decorator_invoked);
7124 assert_eq!(loader.decorator_argument_count, 1);
7125 assert_eq!(loader.endpoint_designator.as_deref(), Some("loadPost"));
7126 assert_eq!(
7127 loader
7128 .declared_type
7129 .as_ref()
7130 .map(|type_| type_.text.as_str()),
7131 Some("Resource<Post, NotFound>")
7132 );
7133 assert!(asm
7134 .diagnostics
7135 .iter()
7136 .any(|diagnostic| diagnostic.code == "PSC1132"));
7137 }
7138
7139 #[test]
7140 fn retains_server_action_source_facts_before_route_capability_resolution() {
7141 let asm = build_application_semantic_model(&presolve_parser::parse_file(
7142 "app/routes/posts/[slug].tsx",
7143 r#"
7144@component()
7145class Post {
7146 @serverAction("savePost") save(): void {}
7147 render() { return <form />; }
7148}
7149"#,
7150 ));
7151
7152 let action = &asm.components[0].server_action_facts[0];
7153 assert_eq!(action.method_name, "save");
7154 assert!(action.invoked);
7155 assert_eq!(action.argument_count, 1);
7156 assert_eq!(action.endpoint_designator.as_deref(), Some("savePost"));
7157 assert!(!action.is_action);
7158 assert!(!action.action_invoked);
7159 assert!(!action.has_body_effects);
7160 assert!(asm
7161 .diagnostics
7162 .iter()
7163 .any(|diagnostic| diagnostic.code == "PSC1133"));
7164 }
7165}