1use std::collections::BTreeMap;
2
3use crate::{
4 ComponentNode, ComponentScopeGraph, ComputedValue, ConsumerEntity, ConsumerId,
5 ContextDependencyEdgeKind, ContextDependencyGraph, ContextDependencyNodeId, ContextEntity,
6 ContextId, ContextOwnershipGraph, ContextResolution, ContextResolutionResult, ProviderEntity,
7 ProviderId, SemanticId, SourceProvenance,
8};
9
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
11pub struct ContextLifetimeId {
12 pub component: SemanticId,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
16pub enum ContextLifetimeEntityId {
17 Context(ContextId),
18 Provider(ProviderId),
19 Consumer(ConsumerId),
20 ContextDefault(ContextId),
21 State(SemanticId),
22 Computed(SemanticId),
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum LifetimeCompatibilityStatus {
27 Compatible,
28 Incompatible,
29 Unknown,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum ContextBindingLifetimeStatus {
34 Compatible,
35 Incompatible,
36 Unknown,
37 Unresolved,
38 Ambiguous,
39 InvalidContextReference,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct ContextEntityLifetimeRecord {
44 pub entity: ContextLifetimeEntityId,
45 pub owner_component: SemanticId,
46 pub lifetime: ContextLifetimeId,
47 pub provenance: SourceProvenance,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct ContextDependencyLifetimeRecord {
52 pub dependent: ContextDependencyNodeId,
53 pub dependency: ContextDependencyNodeId,
54 pub dependent_owner: Option<SemanticId>,
55 pub dependency_owner: Option<SemanticId>,
56 pub compatibility: LifetimeCompatibilityStatus,
57 pub provenance: SourceProvenance,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct ProviderLifetimeRecord {
62 pub provider: ProviderId,
63 pub owner_component: SemanticId,
64 pub lifetime: ContextLifetimeId,
65 pub compatibility: LifetimeCompatibilityStatus,
66 pub provenance: SourceProvenance,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct ContextDefaultLifetimeRecord {
71 pub context: ContextId,
72 pub owner_component: SemanticId,
73 pub lifetime: ContextLifetimeId,
74 pub compatibility: LifetimeCompatibilityStatus,
75 pub provenance: SourceProvenance,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum ContextBindingLifetimeSource {
80 Provider(ProviderId),
81 ContextDefault(ContextId),
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct ContextBindingLifetimeRecord {
86 pub consumer: ConsumerId,
87 pub consumer_owner: Option<SemanticId>,
88 pub resolution: ContextResolutionResult,
89 pub source: Option<ContextBindingLifetimeSource>,
90 pub source_owner: Option<SemanticId>,
91 pub compatibility: ContextBindingLifetimeStatus,
92 pub distance: Option<u32>,
93 pub provenance: SourceProvenance,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct ContextLifetimeAnalysis {
99 pub entities: BTreeMap<ContextLifetimeEntityId, ContextEntityLifetimeRecord>,
100 pub provider_lifetimes: BTreeMap<ProviderId, ProviderLifetimeRecord>,
101 pub context_default_lifetimes: BTreeMap<ContextId, ContextDefaultLifetimeRecord>,
102 pub dependency_lifetimes: Vec<ContextDependencyLifetimeRecord>,
103 pub binding_lifetimes: BTreeMap<ConsumerId, ContextBindingLifetimeRecord>,
104 dependency_index: BTreeMap<ContextDependencyNodeId, Vec<ContextDependencyLifetimeRecord>>,
105}
106
107impl ContextLifetimeAnalysis {
108 #[must_use]
109 pub fn context_entity_lifetime(
110 &self,
111 entity: &ContextLifetimeEntityId,
112 ) -> Option<&ContextEntityLifetimeRecord> {
113 self.entities.get(entity)
114 }
115
116 #[must_use]
117 pub fn provider_lifetime(&self, provider: &ProviderId) -> Option<&ProviderLifetimeRecord> {
118 self.provider_lifetimes.get(provider)
119 }
120
121 #[must_use]
122 pub fn context_default_lifetime(
123 &self,
124 context: &ContextId,
125 ) -> Option<&ContextDefaultLifetimeRecord> {
126 self.context_default_lifetimes.get(context)
127 }
128
129 #[must_use]
130 pub fn context_binding_lifetime(
131 &self,
132 consumer: &ConsumerId,
133 ) -> Option<&ContextBindingLifetimeRecord> {
134 self.binding_lifetimes.get(consumer)
135 }
136
137 #[must_use]
138 pub fn lifetime_dependencies_of(
139 &self,
140 node: &ContextDependencyNodeId,
141 ) -> &[ContextDependencyLifetimeRecord] {
142 self.dependency_index.get(node).map_or(&[], Vec::as_slice)
143 }
144
145 #[must_use]
146 pub fn runtime_lifetime_eligible(&self, consumer: &ConsumerId) -> bool {
147 self.context_binding_lifetime(consumer)
148 .is_some_and(|record| record.compatibility == ContextBindingLifetimeStatus::Compatible)
149 }
150}
151
152#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
155#[must_use]
156pub fn collect_context_lifetime_analysis(
157 components: &[ComponentNode],
158 contexts: &BTreeMap<ContextId, ContextEntity>,
159 providers: &BTreeMap<ProviderId, ProviderEntity>,
160 consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
161 computed_values: &BTreeMap<SemanticId, ComputedValue>,
162 ownership: &ContextOwnershipGraph,
163 scope: &ComponentScopeGraph,
164 resolutions: &BTreeMap<ConsumerId, ContextResolution>,
165 dependencies: &ContextDependencyGraph,
166 provenance: &BTreeMap<SemanticId, SourceProvenance>,
167) -> ContextLifetimeAnalysis {
168 let mut entities = BTreeMap::new();
169 for context in contexts.values() {
170 insert_entity_lifetime(
171 &mut entities,
172 ContextLifetimeEntityId::Context(context.id.clone()),
173 ownership.owner_of_context(&context.id).cloned(),
174 context.provenance.clone(),
175 );
176 if let Some(default) = &context.default_expression {
177 let default_provenance = default_provenance(context, dependencies, provenance, default);
178 insert_entity_lifetime(
179 &mut entities,
180 ContextLifetimeEntityId::ContextDefault(context.id.clone()),
181 ownership.owner_of_context(&context.id).cloned(),
182 default_provenance,
183 );
184 }
185 }
186 for provider in providers.values() {
187 insert_entity_lifetime(
188 &mut entities,
189 ContextLifetimeEntityId::Provider(provider.id.clone()),
190 ownership.owner_of_provider(&provider.id).cloned(),
191 provider.provenance.clone(),
192 );
193 }
194 for consumer in consumers.values() {
195 insert_entity_lifetime(
196 &mut entities,
197 ContextLifetimeEntityId::Consumer(consumer.id.clone()),
198 ownership.owner_of_consumer(&consumer.id).cloned(),
199 consumer.provenance.clone(),
200 );
201 }
202 for node in &dependencies.nodes {
203 match &node.id {
204 ContextDependencyNodeId::State(id) => {
205 if let Some(provenance) = provenance.get(id).cloned() {
206 insert_entity_lifetime(
207 &mut entities,
208 ContextLifetimeEntityId::State(id.clone()),
209 state_owner(components, id),
210 provenance,
211 );
212 }
213 }
214 ContextDependencyNodeId::Computed(id) => {
215 if let Some(computed) = computed_values.get(id) {
216 insert_entity_lifetime(
217 &mut entities,
218 ContextLifetimeEntityId::Computed(id.clone()),
219 computed.owner.entity_id().cloned(),
220 computed.provenance.clone(),
221 );
222 }
223 }
224 _ => {}
225 }
226 }
227
228 let mut dependency_lifetimes = dependencies
229 .edges
230 .iter()
231 .filter(|edge| {
232 matches!(
233 edge.kind,
234 ContextDependencyEdgeKind::ProviderReadsState
235 | ContextDependencyEdgeKind::ProviderReadsComputed
236 | ContextDependencyEdgeKind::ContextDefaultReadsState
237 | ContextDependencyEdgeKind::ContextDefaultReadsComputed
238 )
239 })
240 .map(|edge| {
241 let dependent_owner = lifetime_owner_for_node(&entities, &edge.dependent);
242 let dependency_owner = lifetime_owner_for_node(&entities, &edge.dependency);
243 ContextDependencyLifetimeRecord {
244 dependent: edge.dependent.clone(),
245 dependency: edge.dependency.clone(),
246 dependent_owner: dependent_owner.clone(),
247 dependency_owner: dependency_owner.clone(),
248 compatibility: compare_owners(scope, dependency_owner, dependent_owner),
249 provenance: edge.provenance.clone(),
250 }
251 })
252 .collect::<Vec<_>>();
253 dependency_lifetimes.sort_by(|left, right| {
254 (&left.dependent, &left.dependency).cmp(&(&right.dependent, &right.dependency))
255 });
256
257 let provider_lifetimes = providers
258 .values()
259 .filter_map(|provider| {
260 let owner = ownership.owner_of_provider(&provider.id)?.clone();
261 let status = aggregate_lifetime(dependency_lifetimes.iter().filter(|record| {
262 record.dependent == ContextDependencyNodeId::Provider(provider.id.clone())
263 }));
264 Some((
265 provider.id.clone(),
266 ProviderLifetimeRecord {
267 provider: provider.id.clone(),
268 owner_component: owner.clone(),
269 lifetime: ContextLifetimeId { component: owner },
270 compatibility: status,
271 provenance: provider.provenance.clone(),
272 },
273 ))
274 })
275 .collect::<BTreeMap<_, _>>();
276 let context_default_lifetimes = contexts
277 .values()
278 .filter_map(|context| {
279 let default = context.default_expression.as_ref()?;
280 let owner = ownership.owner_of_context(&context.id)?.clone();
281 let status = aggregate_lifetime(dependency_lifetimes.iter().filter(|record| {
282 record.dependent == ContextDependencyNodeId::ContextDefault(context.id.clone())
283 }));
284 Some((
285 context.id.clone(),
286 ContextDefaultLifetimeRecord {
287 context: context.id.clone(),
288 owner_component: owner.clone(),
289 lifetime: ContextLifetimeId { component: owner },
290 compatibility: status,
291 provenance: default_provenance(context, dependencies, provenance, default),
292 },
293 ))
294 })
295 .collect::<BTreeMap<_, _>>();
296 let binding_lifetimes = consumers
297 .values()
298 .filter_map(|consumer| {
299 let resolution = resolutions.get(&consumer.id)?;
300 let consumer_owner = ownership.owner_of_consumer(&consumer.id).cloned();
301 let record = binding_lifetime_record(
302 consumer,
303 resolution,
304 consumer_owner,
305 &provider_lifetimes,
306 &context_default_lifetimes,
307 scope,
308 );
309 Some((consumer.id.clone(), record))
310 })
311 .collect::<BTreeMap<_, _>>();
312 let mut dependency_index = BTreeMap::new();
313 for record in &dependency_lifetimes {
314 dependency_index
315 .entry(record.dependent.clone())
316 .or_insert_with(Vec::new)
317 .push(record.clone());
318 }
319 ContextLifetimeAnalysis {
320 entities,
321 provider_lifetimes,
322 context_default_lifetimes,
323 dependency_lifetimes,
324 binding_lifetimes,
325 dependency_index,
326 }
327}
328
329fn default_provenance(
330 context: &ContextEntity,
331 dependencies: &ContextDependencyGraph,
332 provenance: &BTreeMap<SemanticId, SourceProvenance>,
333 default: &SemanticId,
334) -> SourceProvenance {
335 dependencies
336 .edges
337 .iter()
338 .find(|edge| {
339 edge.kind == ContextDependencyEdgeKind::ContextDefaultSuppliesContext
340 && edge.dependent == ContextDependencyNodeId::ContextDefault(context.id.clone())
341 })
342 .map_or_else(
343 || {
344 provenance
345 .get(default)
346 .cloned()
347 .unwrap_or_else(|| context.provenance.clone())
348 },
349 |edge| edge.provenance.clone(),
350 )
351}
352
353fn insert_entity_lifetime(
354 entities: &mut BTreeMap<ContextLifetimeEntityId, ContextEntityLifetimeRecord>,
355 entity: ContextLifetimeEntityId,
356 owner_component: Option<SemanticId>,
357 provenance: SourceProvenance,
358) {
359 let Some(owner_component) = owner_component else {
360 return;
361 };
362 entities.insert(
363 entity.clone(),
364 ContextEntityLifetimeRecord {
365 entity,
366 lifetime: ContextLifetimeId {
367 component: owner_component.clone(),
368 },
369 owner_component,
370 provenance,
371 },
372 );
373}
374
375fn state_owner(components: &[ComponentNode], state: &SemanticId) -> Option<SemanticId> {
376 components.iter().find_map(|component| {
377 component
378 .state_fields
379 .iter()
380 .any(|field| field.id == *state)
381 .then_some(component.id.clone())
382 })
383}
384
385fn lifetime_owner_for_node(
386 entities: &BTreeMap<ContextLifetimeEntityId, ContextEntityLifetimeRecord>,
387 node: &ContextDependencyNodeId,
388) -> Option<SemanticId> {
389 let entity = match node {
390 ContextDependencyNodeId::State(id) => ContextLifetimeEntityId::State(id.clone()),
391 ContextDependencyNodeId::Computed(id) => ContextLifetimeEntityId::Computed(id.clone()),
392 ContextDependencyNodeId::Context(id) => ContextLifetimeEntityId::Context(id.clone()),
393 ContextDependencyNodeId::ContextDefault(id) => {
394 ContextLifetimeEntityId::ContextDefault(id.clone())
395 }
396 ContextDependencyNodeId::Provider(id) => ContextLifetimeEntityId::Provider(id.clone()),
397 ContextDependencyNodeId::Consumer(id) => ContextLifetimeEntityId::Consumer(id.clone()),
398 };
399 entities
400 .get(&entity)
401 .map(|record| record.owner_component.clone())
402}
403
404fn compare_owners(
405 scope: &ComponentScopeGraph,
406 source: Option<SemanticId>,
407 dependent: Option<SemanticId>,
408) -> LifetimeCompatibilityStatus {
409 let (Some(source), Some(dependent)) = (source, dependent) else {
410 return LifetimeCompatibilityStatus::Unknown;
411 };
412 if !scope.components.contains(&source) || !scope.components.contains(&dependent) {
413 return LifetimeCompatibilityStatus::Unknown;
414 }
415 if scope.ancestor_chain(&dependent).contains(&source) {
416 LifetimeCompatibilityStatus::Compatible
417 } else {
418 LifetimeCompatibilityStatus::Incompatible
419 }
420}
421
422fn aggregate_lifetime<'a>(
423 records: impl Iterator<Item = &'a ContextDependencyLifetimeRecord>,
424) -> LifetimeCompatibilityStatus {
425 let mut status = LifetimeCompatibilityStatus::Compatible;
426 for record in records {
427 match record.compatibility {
428 LifetimeCompatibilityStatus::Incompatible => {
429 return LifetimeCompatibilityStatus::Incompatible
430 }
431 LifetimeCompatibilityStatus::Unknown => status = LifetimeCompatibilityStatus::Unknown,
432 LifetimeCompatibilityStatus::Compatible => {}
433 }
434 }
435 status
436}
437
438fn binding_lifetime_record(
439 consumer: &ConsumerEntity,
440 resolution: &ContextResolution,
441 consumer_owner: Option<SemanticId>,
442 providers: &BTreeMap<ProviderId, ProviderLifetimeRecord>,
443 defaults: &BTreeMap<ContextId, ContextDefaultLifetimeRecord>,
444 scope: &ComponentScopeGraph,
445) -> ContextBindingLifetimeRecord {
446 let (source, source_owner, source_status, distance, compatibility) = match &resolution.result {
447 ContextResolutionResult::Provider {
448 provider, distance, ..
449 } => {
450 let source = providers.get(provider);
451 let owner = source.map(|record| record.owner_component.clone());
452 let scope_status = compare_owners(scope, owner.clone(), consumer_owner.clone());
453 (
454 Some(ContextBindingLifetimeSource::Provider(provider.clone())),
455 owner,
456 source.map(|record| record.compatibility),
457 Some(*distance),
458 combine_binding_status(scope_status, source.map(|record| record.compatibility)),
459 )
460 }
461 ContextResolutionResult::ContextDefault { context, .. } => {
462 let source = defaults.get(context);
463 let owner = source.map(|record| record.owner_component.clone());
464 let scope_status = compare_owners(scope, owner.clone(), consumer_owner.clone());
465 let distance = owner.as_ref().and_then(|owner| {
466 consumer_owner.as_ref().and_then(|consumer_owner| {
467 scope
468 .ancestor_chain(consumer_owner)
469 .iter()
470 .position(|candidate| candidate == owner)
471 .and_then(|index| u32::try_from(index).ok())
472 })
473 });
474 (
475 Some(ContextBindingLifetimeSource::ContextDefault(
476 context.clone(),
477 )),
478 owner,
479 source.map(|record| record.compatibility),
480 distance,
481 combine_binding_status(scope_status, source.map(|record| record.compatibility)),
482 )
483 }
484 ContextResolutionResult::Unresolved => (
485 None,
486 None,
487 None,
488 None,
489 ContextBindingLifetimeStatus::Unresolved,
490 ),
491 ContextResolutionResult::Ambiguous { .. } => (
492 None,
493 None,
494 None,
495 None,
496 ContextBindingLifetimeStatus::Ambiguous,
497 ),
498 ContextResolutionResult::InvalidContextReference => (
499 None,
500 None,
501 None,
502 None,
503 ContextBindingLifetimeStatus::InvalidContextReference,
504 ),
505 };
506 let _ = source_status;
507 ContextBindingLifetimeRecord {
508 consumer: consumer.id.clone(),
509 consumer_owner,
510 resolution: resolution.result.clone(),
511 source,
512 source_owner,
513 compatibility,
514 distance,
515 provenance: consumer.context_designator.provenance.clone(),
516 }
517}
518
519fn combine_binding_status(
520 scope_status: LifetimeCompatibilityStatus,
521 source_status: Option<LifetimeCompatibilityStatus>,
522) -> ContextBindingLifetimeStatus {
523 let source_status = source_status.unwrap_or(LifetimeCompatibilityStatus::Unknown);
524 if scope_status == LifetimeCompatibilityStatus::Incompatible
525 || source_status == LifetimeCompatibilityStatus::Incompatible
526 {
527 ContextBindingLifetimeStatus::Incompatible
528 } else if scope_status == LifetimeCompatibilityStatus::Unknown
529 || source_status == LifetimeCompatibilityStatus::Unknown
530 {
531 ContextBindingLifetimeStatus::Unknown
532 } else {
533 ContextBindingLifetimeStatus::Compatible
534 }
535}
536
537#[cfg(test)]
538mod tests {
539 use std::collections::BTreeMap;
540
541 use super::compare_owners;
542 use crate::{
543 build_application_semantic_model, build_component_graph, ComponentScopeGraph, ConsumerId,
544 ContextBindingLifetimeStatus, ContextDependencyNodeId, LifetimeCompatibilityStatus,
545 ProviderId,
546 };
547
548 #[test]
549 fn retains_type_incompatible_but_lifetime_compatible_selected_bindings() {
550 let asm = build_application_semantic_model(&presolve_parser::parse_file(
551 "src/app.tsx",
552 r#"
553@component("x-app")
554class App extends Component {
555 selected = state("dark");
556 @context()
557 theme!: number;
558 @provide(App.theme)
559 providedTheme: string = this.selected;
560 @consume(App.theme)
561 themeValue!: number;
562 render() { return <main />; }
563}
564"#,
565 ));
566 let component = &asm.components[0].id;
567 let provider = ProviderId::for_component(component, "providedTheme");
568 let consumer = ConsumerId::for_component(component, "themeValue");
569 let analysis = asm.context_lifetime_analysis();
570
571 assert_eq!(
572 analysis.provider_lifetime(&provider).unwrap().compatibility,
573 LifetimeCompatibilityStatus::Compatible
574 );
575 assert_eq!(
576 analysis
577 .lifetime_dependencies_of(&ContextDependencyNodeId::Provider(provider.clone()))
578 .first()
579 .unwrap()
580 .compatibility,
581 LifetimeCompatibilityStatus::Compatible
582 );
583 assert_eq!(
584 analysis
585 .context_binding_lifetime(&consumer)
586 .unwrap()
587 .compatibility,
588 ContextBindingLifetimeStatus::Compatible
589 );
590 assert!(analysis.runtime_lifetime_eligible(&consumer));
591 }
592
593 #[test]
594 fn retains_default_and_unresolved_binding_statuses() {
595 let asm = build_application_semantic_model(&presolve_parser::parse_file(
596 "src/app.tsx",
597 r#"
598@component("x-app")
599class App extends Component {
600 @context()
601 locale: string = "en";
602 @context()
603 mode!: string;
604 @consume(App.locale)
605 selectedLocale!: string;
606 render() { return <main />; }
607}
608@component("x-toolbar")
609class Toolbar extends Component {
610 @consume(App.mode)
611 mode!: string;
612 render() { return <main />; }
613}
614"#,
615 ));
616 let selected = ConsumerId::for_component(&asm.components[0].id, "selectedLocale");
617 let unresolved = ConsumerId::for_component(&asm.components[1].id, "mode");
618 let analysis = asm.context_lifetime_analysis();
619
620 assert_eq!(
621 analysis
622 .context_binding_lifetime(&selected)
623 .unwrap()
624 .compatibility,
625 ContextBindingLifetimeStatus::Compatible
626 );
627 assert_eq!(
628 analysis
629 .context_binding_lifetime(&unresolved)
630 .unwrap()
631 .compatibility,
632 ContextBindingLifetimeStatus::Unresolved
633 );
634 }
635
636 #[test]
637 fn uses_only_canonical_scope_ancestry_for_outlives() {
638 let parsed = presolve_parser::parse_file(
639 "src/components.tsx",
640 r#"
641@component("x-parent")
642class Parent extends Component { render() { return <main />; } }
643@component("x-child")
644class Child extends Component { render() { return <main />; } }
645"#,
646 );
647 let components = build_component_graph(&parsed).components;
648 let parent = components[0].id.clone();
649 let child = components[1].id.clone();
650 let scope = ComponentScopeGraph::with_parent_relations(
651 &components,
652 BTreeMap::from([(child.clone(), parent.clone())]),
653 );
654
655 assert_eq!(
656 compare_owners(&scope, Some(parent.clone()), Some(child.clone())),
657 LifetimeCompatibilityStatus::Compatible
658 );
659 assert_eq!(
660 compare_owners(&scope, Some(child), Some(parent)),
661 LifetimeCompatibilityStatus::Incompatible
662 );
663 }
664}