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