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