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