1use std::collections::BTreeMap;
2
3use crate::{
4 boundary_compatibility, is_assignable, serialization_compatibility, ConsumerEntity, ConsumerId,
5 ContextEntity, ContextId, ContextResolution, ContextResolutionResult, ExecutionBoundary,
6 ExpressionGraph, ProviderEntity, ProviderId, SemanticType, SemanticTypeId, SemanticTypeModel,
7 SerializationCompatibility, SourceProvenance,
8};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum CompatibilityStatus {
12 Compatible,
13 Incompatible,
14 Unknown,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ContextSerializationCompatibility {
19 Serializable,
20 NonSerializable,
21 Unknown,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum ContextBindingCompatibility {
26 Compatible,
27 Incompatible,
28 Unknown,
29 Unresolved,
30 Ambiguous,
31 InvalidContextReference,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct ContextTypeRecord {
36 pub context: ContextId,
37 pub declared_type: SemanticTypeId,
38 pub normalized_type: SemanticTypeId,
39 pub default_type: Option<SemanticTypeId>,
40 pub default_compatibility: Option<CompatibilityStatus>,
41 pub serialization: ContextSerializationCompatibility,
42 pub boundary: ExecutionBoundary,
43 pub boundary_compatibility: CompatibilityStatus,
44 pub provenance: SourceProvenance,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct ProviderTypeRecord {
49 pub provider: ProviderId,
50 pub context: Option<ContextId>,
51 pub inferred_value_type: SemanticTypeId,
52 pub declared_type: SemanticTypeId,
53 pub value_to_declaration: CompatibilityStatus,
54 pub declaration_to_context: CompatibilityStatus,
55 pub serialization: ContextSerializationCompatibility,
56 pub boundary: ExecutionBoundary,
57 pub boundary_compatibility: CompatibilityStatus,
58 pub provenance: SourceProvenance,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct ConsumerTypeRecord {
63 pub consumer: ConsumerId,
64 pub context: Option<ContextId>,
65 pub requested_type: SemanticTypeId,
66 pub context_to_consumer: CompatibilityStatus,
67 pub boundary: ExecutionBoundary,
68 pub boundary_compatibility: CompatibilityStatus,
69 pub provenance: SourceProvenance,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct ContextBindingTypeRecord {
74 pub consumer: ConsumerId,
75 pub resolution: ContextResolutionResult,
76 pub provider: Option<ProviderId>,
77 pub context: Option<ContextId>,
78 pub source_type: Option<SemanticTypeId>,
79 pub context_type: Option<SemanticTypeId>,
80 pub consumer_type: SemanticTypeId,
81 pub source_to_context: CompatibilityStatus,
82 pub context_to_consumer: CompatibilityStatus,
83 pub overall: ContextBindingCompatibility,
84 pub serialization: ContextSerializationCompatibility,
85 pub boundary_compatibility: CompatibilityStatus,
86 pub provenance: SourceProvenance,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct ContextTypeProducts {
91 pub contexts: BTreeMap<ContextId, ContextTypeRecord>,
92 pub providers: BTreeMap<ProviderId, ProviderTypeRecord>,
93 pub consumers: BTreeMap<ConsumerId, ConsumerTypeRecord>,
94 pub bindings: BTreeMap<ConsumerId, ContextBindingTypeRecord>,
95}
96
97#[allow(clippy::too_many_lines)]
98#[must_use]
99pub fn collect_context_type_products(
100 contexts: &BTreeMap<ContextId, ContextEntity>,
101 providers: &BTreeMap<ProviderId, ProviderEntity>,
102 consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
103 resolutions: &BTreeMap<ConsumerId, ContextResolution>,
104 expression_graph: &ExpressionGraph,
105 semantic_types: &SemanticTypeModel,
106) -> ContextTypeProducts {
107 let context_records = contexts
108 .values()
109 .map(|context| {
110 let default_root = context
111 .default_expression
112 .as_ref()
113 .and_then(|_| expression_graph.root_for(context.id.as_semantic_id()))
114 .cloned();
115 let default_type = default_root.as_ref().map(SemanticTypeId::for_subject);
116 let default_compatibility = default_root.as_ref().map(|default| {
117 compatibility_for_ids(semantic_types, default, context.id.as_semantic_id())
118 });
119 let mut serial_types = vec![context.id.as_semantic_id()];
120 if let Some(default) = &default_root {
121 serial_types.push(default);
122 }
123 let serialization = serialization_for_ids(semantic_types, &serial_types);
124 (
125 context.id.clone(),
126 ContextTypeRecord {
127 context: context.id.clone(),
128 declared_type: context.declared_type_id.clone(),
129 normalized_type: context.declared_type_id.clone(),
130 default_type,
131 default_compatibility,
132 serialization,
133 boundary: context.execution_boundary,
134 boundary_compatibility: boundary_for_id(
135 semantic_types,
136 context.id.as_semantic_id(),
137 context.execution_boundary,
138 context.execution_boundary,
139 ),
140 provenance: context.provenance.clone(),
141 },
142 )
143 })
144 .collect::<BTreeMap<_, _>>();
145 let provider_records = providers
146 .values()
147 .map(|provider| {
148 let value_root = expression_graph
149 .root_for(provider.id.as_semantic_id())
150 .cloned();
151 let inferred_value_type = value_root.as_ref().map_or_else(
152 || SemanticTypeId::for_subject(provider.id.as_semantic_id()),
153 SemanticTypeId::for_subject,
154 );
155 let value_to_declaration = compatibility_for_ids(
156 semantic_types,
157 value_root.as_ref().unwrap_or(provider.id.as_semantic_id()),
158 provider.id.as_semantic_id(),
159 );
160 let declaration_to_context = compatibility_for_ids(
161 semantic_types,
162 provider.id.as_semantic_id(),
163 provider.context.as_semantic_id(),
164 );
165 (
166 provider.id.clone(),
167 ProviderTypeRecord {
168 provider: provider.id.clone(),
169 context: Some(provider.context.clone()),
170 inferred_value_type: inferred_value_type.clone(),
171 declared_type: provider.declared_type_id.clone(),
172 value_to_declaration,
173 declaration_to_context,
174 serialization: serialization_for_ids(
175 semantic_types,
176 &[
177 value_root.as_ref().unwrap_or(provider.id.as_semantic_id()),
178 provider.id.as_semantic_id(),
179 ],
180 ),
181 boundary: provider.execution_boundary,
182 boundary_compatibility: boundary_for_id(
183 semantic_types,
184 value_root.as_ref().unwrap_or(provider.id.as_semantic_id()),
185 provider.execution_boundary,
186 provider.execution_boundary,
187 ),
188 provenance: provider.provenance.clone(),
189 },
190 )
191 })
192 .collect::<BTreeMap<_, _>>();
193 let consumer_records = consumers
194 .values()
195 .map(|consumer| {
196 let context_to_consumer =
197 consumer
198 .context()
199 .map_or(CompatibilityStatus::Unknown, |context| {
200 compatibility_for_ids(
201 semantic_types,
202 context.as_semantic_id(),
203 consumer.id.as_semantic_id(),
204 )
205 });
206 (
207 consumer.id.clone(),
208 ConsumerTypeRecord {
209 consumer: consumer.id.clone(),
210 context: consumer.context().cloned(),
211 requested_type: consumer.requested_type_id.clone(),
212 context_to_consumer,
213 boundary: consumer.execution_boundary,
214 boundary_compatibility: boundary_for_id(
215 semantic_types,
216 consumer.id.as_semantic_id(),
217 consumer.execution_boundary,
218 consumer.execution_boundary,
219 ),
220 provenance: consumer.provenance.clone(),
221 },
222 )
223 })
224 .collect::<BTreeMap<_, _>>();
225 let bindings = consumers
226 .values()
227 .filter_map(|consumer| {
228 let resolution = resolutions.get(&consumer.id)?;
229 let consumer_record = consumer_records.get(&consumer.id)?;
230 let context_record = consumer
231 .context()
232 .and_then(|context| context_records.get(context));
233 let (provider, source_type, source_to_context, serialization, boundary) =
234 match &resolution.result {
235 ContextResolutionResult::Provider { provider, .. } => {
236 let record = provider_records.get(provider)?;
237 (
238 Some(provider.clone()),
239 Some(record.inferred_value_type.clone()),
240 combine_compatibility(
241 record.value_to_declaration,
242 record.declaration_to_context,
243 ),
244 record.serialization,
245 record.boundary_compatibility,
246 )
247 }
248 ContextResolutionResult::ContextDefault {
249 context,
250 expression,
251 } => (
252 None,
253 Some(SemanticTypeId::for_subject(expression)),
254 context_records
255 .get(context)
256 .and_then(|record| record.default_compatibility)
257 .unwrap_or(CompatibilityStatus::Unknown),
258 serialization_for_ids(semantic_types, &[expression]),
259 boundary_for_id(
260 semantic_types,
261 expression,
262 ExecutionBoundary::Client,
263 ExecutionBoundary::Client,
264 ),
265 ),
266 _ => (
267 None,
268 None,
269 CompatibilityStatus::Unknown,
270 ContextSerializationCompatibility::Unknown,
271 CompatibilityStatus::Unknown,
272 ),
273 };
274 let context_to_consumer = consumer_record.context_to_consumer;
275 let overall = binding_overall(
276 &resolution.result,
277 source_to_context,
278 context_to_consumer,
279 serialization,
280 boundary,
281 );
282 Some((
283 consumer.id.clone(),
284 ContextBindingTypeRecord {
285 consumer: consumer.id.clone(),
286 resolution: resolution.result.clone(),
287 provider,
288 context: consumer.context().cloned(),
289 source_type,
290 context_type: context_record.map(|record| record.declared_type.clone()),
291 consumer_type: consumer.requested_type_id.clone(),
292 source_to_context,
293 context_to_consumer,
294 overall,
295 serialization,
296 boundary_compatibility: boundary,
297 provenance: resolution.provenance.clone(),
298 },
299 ))
300 })
301 .collect::<BTreeMap<_, _>>();
302 ContextTypeProducts {
303 contexts: context_records,
304 providers: provider_records,
305 consumers: consumer_records,
306 bindings,
307 }
308}
309
310fn compatibility_for_ids(
311 types: &SemanticTypeModel,
312 source: &crate::SemanticId,
313 target: &crate::SemanticId,
314) -> CompatibilityStatus {
315 let Some(source) = types
316 .assignments
317 .get(source)
318 .map(|record| &record.semantic_type)
319 else {
320 return CompatibilityStatus::Unknown;
321 };
322 let Some(target) = types
323 .assignments
324 .get(target)
325 .map(|record| &record.semantic_type)
326 else {
327 return CompatibilityStatus::Unknown;
328 };
329 if matches!(source, SemanticType::Unknown) || matches!(target, SemanticType::Unknown) {
330 CompatibilityStatus::Unknown
331 } else if is_assignable(source, target) {
332 CompatibilityStatus::Compatible
333 } else {
334 CompatibilityStatus::Incompatible
335 }
336}
337
338fn serialization_for_ids(
339 types: &SemanticTypeModel,
340 ids: &[&crate::SemanticId],
341) -> ContextSerializationCompatibility {
342 let mut result = ContextSerializationCompatibility::Serializable;
343 for id in ids {
344 let Some(semantic_type) = types
345 .assignments
346 .get(*id)
347 .map(|record| &record.semantic_type)
348 else {
349 return ContextSerializationCompatibility::Unknown;
350 };
351 if matches!(semantic_type, SemanticType::Unknown) {
352 return ContextSerializationCompatibility::Unknown;
353 }
354 if serialization_compatibility(semantic_type) == SerializationCompatibility::NotSerializable
355 {
356 result = ContextSerializationCompatibility::NonSerializable;
357 }
358 }
359 result
360}
361
362fn boundary_for_id(
363 types: &SemanticTypeModel,
364 id: &crate::SemanticId,
365 source: ExecutionBoundary,
366 target: ExecutionBoundary,
367) -> CompatibilityStatus {
368 let Some(semantic_type) = types
369 .assignments
370 .get(id)
371 .map(|record| &record.semantic_type)
372 else {
373 return CompatibilityStatus::Unknown;
374 };
375 if matches!(semantic_type, SemanticType::Unknown) {
376 return CompatibilityStatus::Unknown;
377 }
378 match boundary_compatibility(semantic_type, source, target) {
379 crate::BoundaryCompatibility::Compatible => CompatibilityStatus::Compatible,
380 crate::BoundaryCompatibility::Incompatible => CompatibilityStatus::Incompatible,
381 }
382}
383
384fn combine_compatibility(
385 left: CompatibilityStatus,
386 right: CompatibilityStatus,
387) -> CompatibilityStatus {
388 if left == CompatibilityStatus::Incompatible || right == CompatibilityStatus::Incompatible {
389 CompatibilityStatus::Incompatible
390 } else if left == CompatibilityStatus::Unknown || right == CompatibilityStatus::Unknown {
391 CompatibilityStatus::Unknown
392 } else {
393 CompatibilityStatus::Compatible
394 }
395}
396
397fn binding_overall(
398 resolution: &ContextResolutionResult,
399 source_to_context: CompatibilityStatus,
400 context_to_consumer: CompatibilityStatus,
401 serialization: ContextSerializationCompatibility,
402 boundary: CompatibilityStatus,
403) -> ContextBindingCompatibility {
404 match resolution {
405 ContextResolutionResult::Unresolved => ContextBindingCompatibility::Unresolved,
406 ContextResolutionResult::Ambiguous { .. } => ContextBindingCompatibility::Ambiguous,
407 ContextResolutionResult::InvalidContextReference => {
408 ContextBindingCompatibility::InvalidContextReference
409 }
410 ContextResolutionResult::Provider { .. }
411 | ContextResolutionResult::ContextDefault { .. } => {
412 if source_to_context == CompatibilityStatus::Incompatible
413 || context_to_consumer == CompatibilityStatus::Incompatible
414 || boundary == CompatibilityStatus::Incompatible
415 || serialization == ContextSerializationCompatibility::NonSerializable
416 {
417 ContextBindingCompatibility::Incompatible
418 } else if source_to_context == CompatibilityStatus::Unknown
419 || context_to_consumer == CompatibilityStatus::Unknown
420 || boundary == CompatibilityStatus::Unknown
421 || serialization == ContextSerializationCompatibility::Unknown
422 {
423 ContextBindingCompatibility::Unknown
424 } else {
425 ContextBindingCompatibility::Compatible
426 }
427 }
428 }
429}
430
431#[cfg(test)]
432mod tests {
433 use crate::{
434 build_application_semantic_model, validate_application_semantic_model, CompatibilityStatus,
435 ConsumerId, ContextBindingCompatibility::*, ProviderId,
436 };
437
438 #[test]
439 fn retains_the_directed_compatible_provider_chain() {
440 let asm = build_application_semantic_model(&presolve_parser::parse_file(
441 "src/components.tsx",
442 r#"
443@component("x-app")
444class App extends Component {
445 @context()
446 theme!: string;
447 @provide(App.theme)
448 providedTheme: string = "dark";
449 @consume(App.theme)
450 toolbarTheme!: string;
451 render() { return <main />; }
452}
453"#,
454 ));
455 let component = &asm.components[0].id;
456 let provider = ProviderId::for_component(component, "providedTheme");
457 let consumer = ConsumerId::for_component(component, "toolbarTheme");
458 let provider_type = asm.provider_type(&provider).unwrap();
459 let binding = asm.context_binding_type(&consumer).unwrap();
460
461 assert_eq!(
462 provider_type.value_to_declaration,
463 CompatibilityStatus::Compatible
464 );
465 assert_eq!(
466 provider_type.declaration_to_context,
467 CompatibilityStatus::Compatible
468 );
469 assert_eq!(binding.context_to_consumer, CompatibilityStatus::Compatible);
470 assert_eq!(binding.overall, Compatible, "{binding:#?}");
471 assert!(asm.runtime_eligible_context_binding(&consumer));
472 assert!(validate_application_semantic_model(&asm).is_empty());
473 }
474
475 #[test]
476 fn retains_incompatible_selected_provider_without_reselecting() {
477 let asm = build_application_semantic_model(&presolve_parser::parse_file(
478 "src/components.tsx",
479 r#"
480@component("x-app")
481class App extends Component {
482 @context()
483 theme!: number;
484 @provide(App.theme)
485 providedTheme: string = "dark";
486 @consume(App.theme)
487 toolbarTheme!: number;
488 render() { return <main />; }
489}
490"#,
491 ));
492 let component = &asm.components[0].id;
493 let provider = ProviderId::for_component(component, "providedTheme");
494 let consumer = ConsumerId::for_component(component, "toolbarTheme");
495 let binding = asm.context_binding_type(&consumer).unwrap();
496
497 assert_eq!(asm.resolved_provider(&consumer), Some(&provider));
498 assert_eq!(
499 asm.provider_type(&provider).unwrap().declaration_to_context,
500 CompatibilityStatus::Incompatible
501 );
502 assert_eq!(binding.overall, Incompatible);
503 assert!(!asm.runtime_eligible_context_binding(&consumer));
504 }
505
506 #[test]
507 fn types_context_defaults_as_distinct_fallback_sources() {
508 let asm = build_application_semantic_model(&presolve_parser::parse_file(
509 "src/components.tsx",
510 r#"
511@component("x-app")
512class App extends Component {
513 @context()
514 locale: string = "en";
515 @consume(App.locale)
516 toolbarLocale!: string;
517 render() { return <main />; }
518}
519"#,
520 ));
521 let consumer = ConsumerId::for_component(&asm.components[0].id, "toolbarLocale");
522 let binding = asm.context_binding_type(&consumer).unwrap();
523
524 assert!(binding.provider.is_none());
525 assert!(binding.source_type.is_some());
526 assert_eq!(binding.overall, Compatible, "{binding:#?}");
527 }
528
529 #[test]
530 fn preserves_unresolved_and_invalid_binding_states() {
531 let unresolved = build_application_semantic_model(&presolve_parser::parse_file(
532 "src/unresolved.tsx",
533 r#"
534@component("x-app")
535class App extends Component {
536 @context()
537 locale!: string;
538 @consume(App.locale)
539 toolbarLocale!: string;
540 render() { return <main />; }
541}
542"#,
543 ));
544 let unresolved_consumer =
545 ConsumerId::for_component(&unresolved.components[0].id, "toolbarLocale");
546 assert_eq!(
547 unresolved
548 .context_binding_type(&unresolved_consumer)
549 .unwrap()
550 .overall,
551 Unresolved
552 );
553
554 let invalid = build_application_semantic_model(&presolve_parser::parse_file(
555 "src/invalid.tsx",
556 r#"
557@component("x-app")
558class App extends Component {
559 @consume(Missing.locale)
560 toolbarLocale!: string;
561 render() { return <main />; }
562}
563"#,
564 ));
565 let invalid_consumer =
566 ConsumerId::for_component(&invalid.components[0].id, "toolbarLocale");
567 assert!(invalid.context_binding_type(&invalid_consumer).is_none());
568 assert_eq!(
569 invalid
570 .context_declaration_candidates()
571 .invalid_candidates()
572 .len(),
573 1
574 );
575 }
576}