1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::{
4 CompatibilityStatus, ComponentNode, ComputedValue, ConsumerEntity, ConsumerId,
5 ContextBindingCompatibility, ContextBindingTypeRecord, ContextEntity, ContextId,
6 ContextResolution, ContextResolutionResult, ContextTypeRecord, ExpressionGraph,
7 ExpressionNodeKind, ProviderEntity, ProviderId, ProviderTypeRecord, SemanticId,
8 SourceProvenance,
9};
10
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
13pub enum ContextDependencyNodeId {
14 State(SemanticId),
15 Computed(SemanticId),
16 Context(ContextId),
17 ContextDefault(ContextId),
18 Provider(ProviderId),
19 Consumer(ConsumerId),
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
23pub enum ContextDependencyNodeKind {
24 State,
25 Computed,
26 Context,
27 ContextDefault,
28 Provider,
29 Consumer,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
33pub struct ContextDependencyNode {
34 pub id: ContextDependencyNodeId,
35 pub kind: ContextDependencyNodeKind,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
39pub enum ContextDependencyEdgeKind {
40 ProviderReadsState,
41 ProviderReadsComputed,
42 ContextDefaultReadsState,
43 ContextDefaultReadsComputed,
44 ProviderSuppliesContext,
45 ContextDefaultSuppliesContext,
46 ConsumerReadsProvider,
47 ConsumerReadsContextDefault,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum ContextDependencyCompatibility {
52 Compatible,
53 Incompatible,
54 Unknown,
55 NotApplicable,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct ContextDependencyEdge {
62 pub dependent: ContextDependencyNodeId,
63 pub dependency: ContextDependencyNodeId,
64 pub kind: ContextDependencyEdgeKind,
65 pub compatibility: ContextDependencyCompatibility,
66 pub provenance: SourceProvenance,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct ContextDependencyGraph {
72 pub nodes: Vec<ContextDependencyNode>,
73 pub edges: Vec<ContextDependencyEdge>,
74 dependencies_by_node: BTreeMap<ContextDependencyNodeId, Vec<ContextDependencyNodeId>>,
75 dependents_by_node: BTreeMap<ContextDependencyNodeId, Vec<ContextDependencyNodeId>>,
76 provider_value_dependencies: BTreeMap<ProviderId, Vec<ContextDependencyNodeId>>,
77 context_default_dependencies: BTreeMap<ContextId, Vec<ContextDependencyNodeId>>,
78 consumer_binding_dependencies: BTreeMap<ConsumerId, ContextDependencyNodeId>,
79 consumers_by_provider: BTreeMap<ProviderId, Vec<ConsumerId>>,
80 consumers_by_default: BTreeMap<ContextId, Vec<ConsumerId>>,
81}
82
83impl ContextDependencyGraph {
84 #[must_use]
85 pub fn direct_dependencies(
86 &self,
87 node: &ContextDependencyNodeId,
88 ) -> &[ContextDependencyNodeId] {
89 self.dependencies_by_node
90 .get(node)
91 .map_or(&[], Vec::as_slice)
92 }
93
94 #[must_use]
95 pub fn direct_dependents(&self, node: &ContextDependencyNodeId) -> &[ContextDependencyNodeId] {
96 self.dependents_by_node.get(node).map_or(&[], Vec::as_slice)
97 }
98
99 #[must_use]
100 pub fn provider_value_dependencies(&self, provider: &ProviderId) -> &[ContextDependencyNodeId] {
101 self.provider_value_dependencies
102 .get(provider)
103 .map_or(&[], Vec::as_slice)
104 }
105
106 #[must_use]
107 pub fn context_default_dependencies(&self, context: &ContextId) -> &[ContextDependencyNodeId] {
108 self.context_default_dependencies
109 .get(context)
110 .map_or(&[], Vec::as_slice)
111 }
112
113 #[must_use]
114 pub fn consumer_binding_dependency(
115 &self,
116 consumer: &ConsumerId,
117 ) -> Option<&ContextDependencyNodeId> {
118 self.consumer_binding_dependencies.get(consumer)
119 }
120
121 #[must_use]
122 pub fn consumers_bound_to_provider(&self, provider: &ProviderId) -> &[ConsumerId] {
123 self.consumers_by_provider
124 .get(provider)
125 .map_or(&[], Vec::as_slice)
126 }
127
128 #[must_use]
129 pub fn consumers_bound_to_default(&self, context: &ContextId) -> &[ConsumerId] {
130 self.consumers_by_default
131 .get(context)
132 .map_or(&[], Vec::as_slice)
133 }
134}
135
136#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
138#[must_use]
139pub fn collect_context_dependency_graph(
140 components: &[ComponentNode],
141 contexts: &BTreeMap<ContextId, ContextEntity>,
142 providers: &BTreeMap<ProviderId, ProviderEntity>,
143 consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
144 resolutions: &BTreeMap<ConsumerId, ContextResolution>,
145 context_types: &BTreeMap<ContextId, ContextTypeRecord>,
146 provider_types: &BTreeMap<ProviderId, ProviderTypeRecord>,
147 binding_types: &BTreeMap<ConsumerId, ContextBindingTypeRecord>,
148 computed_values: &BTreeMap<SemanticId, ComputedValue>,
149 expression_graph: &ExpressionGraph,
150) -> ContextDependencyGraph {
151 let mut nodes = contexts
152 .keys()
153 .cloned()
154 .map(|id| ContextDependencyNode {
155 id: ContextDependencyNodeId::Context(id),
156 kind: ContextDependencyNodeKind::Context,
157 })
158 .chain(contexts.values().filter_map(|context| {
159 context
160 .default_expression
161 .as_ref()
162 .map(|_| ContextDependencyNode {
163 id: ContextDependencyNodeId::ContextDefault(context.id.clone()),
164 kind: ContextDependencyNodeKind::ContextDefault,
165 })
166 }))
167 .chain(providers.keys().cloned().map(|id| ContextDependencyNode {
168 id: ContextDependencyNodeId::Provider(id),
169 kind: ContextDependencyNodeKind::Provider,
170 }))
171 .chain(consumers.keys().cloned().map(|id| ContextDependencyNode {
172 id: ContextDependencyNodeId::Consumer(id),
173 kind: ContextDependencyNodeKind::Consumer,
174 }))
175 .collect::<Vec<_>>();
176 let mut edges = Vec::new();
177
178 for context in contexts.values() {
179 let Some(default) = &context.default_expression else {
180 continue;
181 };
182 let default_node = ContextDependencyNodeId::ContextDefault(context.id.clone());
183 edges.push(ContextDependencyEdge {
184 dependent: default_node.clone(),
185 dependency: ContextDependencyNodeId::Context(context.id.clone()),
186 kind: ContextDependencyEdgeKind::ContextDefaultSuppliesContext,
187 compatibility: context_types
188 .get(&context.id)
189 .and_then(|record| record.default_compatibility)
190 .map_or(
191 ContextDependencyCompatibility::Unknown,
192 compatibility_from_status,
193 ),
194 provenance: expression_graph.node(default).map_or_else(
195 || context.provenance.clone(),
196 |node| node.provenance.clone(),
197 ),
198 });
199 edges.extend(expression_read_edges(
200 &default_node,
201 context.owner.entity_id(),
202 expression_graph,
203 components,
204 computed_values,
205 ContextDependencyEdgeKind::ContextDefaultReadsState,
206 ContextDependencyEdgeKind::ContextDefaultReadsComputed,
207 ));
208 }
209 for provider in providers.values() {
210 let provider_node = ContextDependencyNodeId::Provider(provider.id.clone());
211 edges.push(ContextDependencyEdge {
212 dependent: provider_node.clone(),
213 dependency: ContextDependencyNodeId::Context(provider.context.clone()),
214 kind: ContextDependencyEdgeKind::ProviderSuppliesContext,
215 compatibility: provider_types
216 .get(&provider.id)
217 .map_or(ContextDependencyCompatibility::Unknown, |record| {
218 compatibility_from_status(record.declaration_to_context)
219 }),
220 provenance: provider.context_designator.provenance.clone(),
221 });
222 edges.extend(expression_read_edges(
223 &provider_node,
224 provider.owner.entity_id(),
225 expression_graph,
226 components,
227 computed_values,
228 ContextDependencyEdgeKind::ProviderReadsState,
229 ContextDependencyEdgeKind::ProviderReadsComputed,
230 ));
231 }
232 for consumer in consumers.values() {
233 let Some(resolution) = resolutions.get(&consumer.id) else {
234 continue;
235 };
236 let dependent = ContextDependencyNodeId::Consumer(consumer.id.clone());
237 let compatibility = binding_types
238 .get(&consumer.id)
239 .map_or(ContextDependencyCompatibility::Unknown, |binding| {
240 compatibility_from_binding(binding.overall)
241 });
242 let edge = match &resolution.result {
243 ContextResolutionResult::Provider { provider, .. } => Some(ContextDependencyEdge {
244 dependent,
245 dependency: ContextDependencyNodeId::Provider(provider.clone()),
246 kind: ContextDependencyEdgeKind::ConsumerReadsProvider,
247 compatibility,
248 provenance: consumer.context_designator.provenance.clone(),
249 }),
250 ContextResolutionResult::ContextDefault { context, .. } => {
251 Some(ContextDependencyEdge {
252 dependent,
253 dependency: ContextDependencyNodeId::ContextDefault(context.clone()),
254 kind: ContextDependencyEdgeKind::ConsumerReadsContextDefault,
255 compatibility,
256 provenance: consumer.context_designator.provenance.clone(),
257 })
258 }
259 ContextResolutionResult::Unresolved
260 | ContextResolutionResult::Ambiguous { .. }
261 | ContextResolutionResult::InvalidContextReference => None,
262 };
263 if let Some(edge) = edge {
264 edges.push(edge);
265 }
266 }
267
268 edges.sort_by(|left, right| {
269 (
270 &left.dependent,
271 left.kind,
272 &left.dependency,
273 left.provenance.span.start,
274 )
275 .cmp(&(
276 &right.dependent,
277 right.kind,
278 &right.dependency,
279 right.provenance.span.start,
280 ))
281 });
282 edges.dedup_by(|left, right| {
283 left.dependent == right.dependent
284 && left.kind == right.kind
285 && left.dependency == right.dependency
286 });
287
288 let referenced_values = edges
289 .iter()
290 .filter_map(|edge| match &edge.dependency {
291 ContextDependencyNodeId::State(id) => Some(ContextDependencyNode {
292 id: ContextDependencyNodeId::State(id.clone()),
293 kind: ContextDependencyNodeKind::State,
294 }),
295 ContextDependencyNodeId::Computed(id) => Some(ContextDependencyNode {
296 id: ContextDependencyNodeId::Computed(id.clone()),
297 kind: ContextDependencyNodeKind::Computed,
298 }),
299 _ => None,
300 })
301 .collect::<BTreeSet<_>>();
302 nodes.extend(referenced_values);
303 nodes.sort_by(|left, right| left.id.cmp(&right.id));
304 nodes.dedup_by(|left, right| left.id == right.id);
305
306 let mut dependencies_by_node = BTreeMap::new();
307 let mut dependents_by_node = BTreeMap::new();
308 let mut provider_value_dependencies = BTreeMap::new();
309 let mut context_default_dependencies = BTreeMap::new();
310 let mut consumer_binding_dependencies = BTreeMap::new();
311 let mut consumers_by_provider = BTreeMap::new();
312 let mut consumers_by_default = BTreeMap::new();
313 for edge in &edges {
314 dependencies_by_node
315 .entry(edge.dependent.clone())
316 .or_insert_with(Vec::new)
317 .push(edge.dependency.clone());
318 dependents_by_node
319 .entry(edge.dependency.clone())
320 .or_insert_with(Vec::new)
321 .push(edge.dependent.clone());
322 match (&edge.dependent, &edge.dependency) {
323 (ContextDependencyNodeId::Provider(provider), dependency)
324 if matches!(
325 edge.kind,
326 ContextDependencyEdgeKind::ProviderReadsState
327 | ContextDependencyEdgeKind::ProviderReadsComputed
328 ) =>
329 {
330 provider_value_dependencies
331 .entry(provider.clone())
332 .or_insert_with(Vec::new)
333 .push(dependency.clone());
334 }
335 (ContextDependencyNodeId::ContextDefault(context), dependency)
336 if matches!(
337 edge.kind,
338 ContextDependencyEdgeKind::ContextDefaultReadsState
339 | ContextDependencyEdgeKind::ContextDefaultReadsComputed
340 ) =>
341 {
342 context_default_dependencies
343 .entry(context.clone())
344 .or_insert_with(Vec::new)
345 .push(dependency.clone());
346 }
347 (
348 ContextDependencyNodeId::Consumer(consumer),
349 ContextDependencyNodeId::Provider(provider),
350 ) => {
351 consumer_binding_dependencies.insert(consumer.clone(), edge.dependency.clone());
352 consumers_by_provider
353 .entry(provider.clone())
354 .or_insert_with(Vec::new)
355 .push(consumer.clone());
356 }
357 (
358 ContextDependencyNodeId::Consumer(consumer),
359 ContextDependencyNodeId::ContextDefault(context),
360 ) => {
361 consumer_binding_dependencies.insert(consumer.clone(), edge.dependency.clone());
362 consumers_by_default
363 .entry(context.clone())
364 .or_insert_with(Vec::new)
365 .push(consumer.clone());
366 }
367 _ => {}
368 }
369 }
370 for dependencies in dependencies_by_node.values_mut() {
371 dependencies.sort();
372 dependencies.dedup();
373 }
374 for dependents in dependents_by_node.values_mut() {
375 dependents.sort();
376 dependents.dedup();
377 }
378 for dependencies in provider_value_dependencies.values_mut() {
379 dependencies.sort();
380 dependencies.dedup();
381 }
382 for dependencies in context_default_dependencies.values_mut() {
383 dependencies.sort();
384 dependencies.dedup();
385 }
386 for consumers in consumers_by_provider.values_mut() {
387 consumers.sort();
388 consumers.dedup();
389 }
390 for consumers in consumers_by_default.values_mut() {
391 consumers.sort();
392 consumers.dedup();
393 }
394 ContextDependencyGraph {
395 nodes,
396 edges,
397 dependencies_by_node,
398 dependents_by_node,
399 provider_value_dependencies,
400 context_default_dependencies,
401 consumer_binding_dependencies,
402 consumers_by_provider,
403 consumers_by_default,
404 }
405}
406
407#[allow(clippy::too_many_arguments)]
408fn expression_read_edges(
409 dependent: &ContextDependencyNodeId,
410 owner: Option<&SemanticId>,
411 expression_graph: &ExpressionGraph,
412 components: &[ComponentNode],
413 computed_values: &BTreeMap<SemanticId, ComputedValue>,
414 state_kind: ContextDependencyEdgeKind,
415 computed_kind: ContextDependencyEdgeKind,
416) -> Vec<ContextDependencyEdge> {
417 let Some(owner) = owner else {
418 return Vec::new();
419 };
420 let Some(component) = components.iter().find(|component| component.id == *owner) else {
421 return Vec::new();
422 };
423 expression_graph
424 .nodes_for(match dependent {
425 ContextDependencyNodeId::Provider(provider) => provider.as_semantic_id(),
426 ContextDependencyNodeId::ContextDefault(context) => context.as_semantic_id(),
427 _ => return Vec::new(),
428 })
429 .into_iter()
430 .filter_map(|node| {
431 let ExpressionNodeKind::ThisMember { name } = &node.kind else {
432 return None;
433 };
434 let (dependency, kind) = if let Some(state) = component
435 .state_fields
436 .iter()
437 .find(|field| field.name == *name)
438 {
439 (ContextDependencyNodeId::State(state.id.clone()), state_kind)
440 } else {
441 let computed = computed_values.get(&component.id.computed(name))?;
442 (
443 ContextDependencyNodeId::Computed(computed.id.clone()),
444 computed_kind,
445 )
446 };
447 Some(ContextDependencyEdge {
448 dependent: dependent.clone(),
449 dependency,
450 kind,
451 compatibility: ContextDependencyCompatibility::NotApplicable,
452 provenance: node.provenance.clone(),
453 })
454 })
455 .collect()
456}
457
458fn compatibility_from_status(status: CompatibilityStatus) -> ContextDependencyCompatibility {
459 match status {
460 CompatibilityStatus::Compatible => ContextDependencyCompatibility::Compatible,
461 CompatibilityStatus::Incompatible => ContextDependencyCompatibility::Incompatible,
462 CompatibilityStatus::Unknown => ContextDependencyCompatibility::Unknown,
463 }
464}
465
466fn compatibility_from_binding(
467 status: ContextBindingCompatibility,
468) -> ContextDependencyCompatibility {
469 match status {
470 ContextBindingCompatibility::Compatible => ContextDependencyCompatibility::Compatible,
471 ContextBindingCompatibility::Incompatible => ContextDependencyCompatibility::Incompatible,
472 ContextBindingCompatibility::Unknown => ContextDependencyCompatibility::Unknown,
473 ContextBindingCompatibility::Unresolved
474 | ContextBindingCompatibility::Ambiguous
475 | ContextBindingCompatibility::InvalidContextReference => {
476 ContextDependencyCompatibility::NotApplicable
477 }
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 use crate::{
484 build_application_semantic_model, build_application_semantic_model_for_unit,
485 validate_application_semantic_model, CompilationUnit, ConsumerId,
486 ContextDependencyCompatibility, ContextDependencyEdgeKind, ContextDependencyNodeId,
487 ProviderId,
488 };
489
490 #[test]
491 fn projects_provider_reads_and_exact_selected_provider_bindings() {
492 let unit = CompilationUnit::parse_sources([
493 (
494 "src/app-shell.tsx",
495 r#"
496@component("x-app-shell")
497class AppShell extends Component {
498 @context()
499 theme!: string;
500 @context()
501 mode!: string;
502 render() { return <main />; }
503}
504export { AppShell };
505"#,
506 ),
507 (
508 "src/theme-boundary.tsx",
509 r#"
510import { AppShell } from "./app-shell";
511@component("x-theme-boundary")
512class ThemeBoundary extends Component {
513 selected = state("dark");
514 @computed()
515 get derivedTheme(): string { return this.selected; }
516 @provide(AppShell.theme)
517 providedTheme: string = this.selected;
518 @provide(AppShell.mode)
519 providedMode: string = this.derivedTheme;
520 @consume(AppShell.theme)
521 theme!: string;
522 render() { return <main />; }
523}
524"#,
525 ),
526 ]);
527 let asm = build_application_semantic_model_for_unit(&unit);
528 let component = &asm.components[1].id;
529 let provider = ProviderId::for_component(component, "providedTheme");
530 let computed_provider = ProviderId::for_component(component, "providedMode");
531 let consumer = ConsumerId::for_component(component, "theme");
532 let state = ContextDependencyNodeId::State(component.state_field("selected"));
533 let graph = asm.context_dependency_graph();
534
535 assert_eq!(graph.provider_value_dependencies(&provider), &[state]);
536 assert_eq!(
537 graph.provider_value_dependencies(&computed_provider),
538 &[ContextDependencyNodeId::Computed(
539 component.computed("derivedTheme")
540 )]
541 );
542 assert_eq!(
543 graph.consumer_binding_dependency(&consumer),
544 Some(&ContextDependencyNodeId::Provider(provider.clone()))
545 );
546 assert_eq!(graph.consumers_bound_to_provider(&provider), &[consumer]);
547 assert!(graph.edges.iter().any(|edge| {
548 edge.kind == ContextDependencyEdgeKind::ProviderSuppliesContext
549 && edge.compatibility == ContextDependencyCompatibility::Compatible
550 }));
551 }
552
553 #[test]
554 fn retains_default_and_unresolved_consumer_topology_without_hidden_providers() {
555 let asm = build_application_semantic_model(&presolve_parser::parse_file(
556 "src/app.tsx",
557 r#"
558@component("x-app")
559class App extends Component {
560 @context()
561 locale: string = "en";
562 @context()
563 mode!: string;
564 @consume(App.locale)
565 selectedLocale!: string;
566 render() { return <main />; }
567}
568@component("x-toolbar")
569class Toolbar extends Component {
570 @consume(App.mode)
571 mode!: string;
572 render() { return <main />; }
573}
574"#,
575 ));
576 let app = &asm.components[0].id;
577 let toolbar = &asm.components[1].id;
578 let context = asm.contexts()[0].id.clone();
579 let default = ContextDependencyNodeId::ContextDefault(context.clone());
580 let selected = ConsumerId::for_component(app, "selectedLocale");
581 let unresolved = ConsumerId::for_component(toolbar, "mode");
582 let graph = asm.context_dependency_graph();
583
584 assert_eq!(graph.consumer_binding_dependency(&selected), Some(&default));
585 assert_eq!(graph.consumers_bound_to_default(&context), &[selected]);
586 assert_eq!(graph.consumer_binding_dependency(&unresolved), None);
587 assert!(graph.edges.iter().any(|edge| {
588 edge.kind == ContextDependencyEdgeKind::ContextDefaultSuppliesContext
589 && edge.dependent == default
590 }));
591 assert!(validate_application_semantic_model(&asm).is_empty());
592 }
593
594 #[test]
595 fn preserves_incompatible_selected_provider_and_deterministic_ordering() {
596 let parsed = presolve_parser::parse_file(
597 "src/app.tsx",
598 r#"
599@component("x-app")
600class App extends Component {
601 @context()
602 theme!: number;
603 @provide(App.theme)
604 providedTheme: string = "dark";
605 @consume(App.theme)
606 themeValue!: number;
607 render() { return <main />; }
608}
609"#,
610 );
611 let first = build_application_semantic_model(&parsed);
612 let second = build_application_semantic_model(&parsed);
613 let component = &first.components[0].id;
614 let consumer = ConsumerId::for_component(component, "themeValue");
615 let provider = ProviderId::for_component(component, "providedTheme");
616 let edge = first
617 .context_dependency_graph()
618 .edges
619 .iter()
620 .find(|edge| edge.kind == ContextDependencyEdgeKind::ConsumerReadsProvider)
621 .unwrap();
622
623 assert_eq!(first.context_dependency, second.context_dependency);
624 assert_eq!(edge.dependent, ContextDependencyNodeId::Consumer(consumer));
625 assert_eq!(edge.dependency, ContextDependencyNodeId::Provider(provider));
626 assert_eq!(
627 edge.compatibility,
628 ContextDependencyCompatibility::Incompatible
629 );
630 }
631}