1use std::collections::BTreeSet;
2
3use crate::{
4 ApplicationSemanticModel, CompatibilityStatus, ComponentDiagnostic,
5 ComponentDiagnosticSeverity, ContextBindingLifetimeStatus, ContextDeclarationCandidateKind,
6 ContextDeclarationStatus, ContextDeclarationViolation, ContextDependencyNodeId,
7 ContextResolutionResult, ContextSerializationCompatibility, ContextSourceBlockReason,
8 ContextSourcePlanEntry, ContextSourcePlanStatus, ContextValueSourceId,
9 LifetimeCompatibilityStatus, SourceProvenance,
10};
11
12#[allow(clippy::too_many_lines)]
15#[must_use]
16pub fn collect_context_diagnostics(model: &ApplicationSemanticModel) -> Vec<ComponentDiagnostic> {
17 let mut emitted_duplicate_groups = BTreeSet::new();
18 let mut diagnostics = Vec::new();
19 for candidate in model.context_declaration_candidates().invalid_candidates() {
20 let ContextDeclarationStatus::Invalid(violations) = &candidate.status else {
21 continue;
22 };
23 let primary = violations.first();
24 let Some(violation) = primary else {
25 continue;
26 };
27 let code = match violation {
28 ContextDeclarationViolation::UnresolvedContextDesignator => "PSC1055",
29 ContextDeclarationViolation::DuplicateProvider => "PSC1056",
30 _ => match candidate.authored.kind {
31 ContextDeclarationCandidateKind::Context => "PSC1052",
32 ContextDeclarationCandidateKind::Provider => "PSC1053",
33 ContextDeclarationCandidateKind::Consumer => "PSC1054",
34 },
35 };
36 if code == "PSC1056" {
37 let Some(designator) = &candidate.authored.context_designator else {
38 continue;
39 };
40 let group = (
41 candidate.authored.owner_component.clone(),
42 designator.component_symbol.clone(),
43 designator.context_member.clone(),
44 );
45 if !emitted_duplicate_groups.insert(group) {
46 continue;
47 }
48 }
49 let provenance = match violation {
50 ContextDeclarationViolation::StaticDeclarationUnsupported => {
51 candidate.authored.static_modifier_provenance.as_ref()
52 }
53 ContextDeclarationViolation::UnsupportedInitializer
54 | ContextDeclarationViolation::ForbiddenInitializer
55 | ContextDeclarationViolation::MissingInitializer => {
56 candidate.authored.initializer_provenance.as_ref()
57 }
58 ContextDeclarationViolation::ContextDesignatorUnsupported
59 | ContextDeclarationViolation::UnresolvedContextDesignator => candidate
60 .authored
61 .context_designator
62 .as_ref()
63 .map(|designator| &designator.provenance),
64 _ => Some(&candidate.authored.decorator_provenance),
65 }
66 .or(Some(&candidate.authored.provenance));
67 diagnostics.push(ComponentDiagnostic {
68 code: code.to_string(),
69 severity: ComponentDiagnosticSeverity::Error,
70 message: message(code, candidate.authored.field_name.as_deref()),
71 provenance: provenance.cloned(),
72 effect_id: None,
73 statement_id: None,
74 context_declaration_candidate_id: Some(candidate.authored.id.clone()),
75 context_id: None,
76 provider_id: None,
77 consumer_id: None,
78 slot_id: None,
79 invocation_id: None,
80 component_instance_id: None,
81 slot_binding_id: None,
82 structural_region_id: None,
83 component_id: None,
84 provider_instance_id: None,
85 consumer_instance_id: None,
86 secondary_labels: Vec::new(),
87 });
88 }
89 for resolution in model.context_resolutions.values() {
90 match &resolution.result {
91 ContextResolutionResult::Unresolved => push(
92 &mut diagnostics,
93 "PSC1057",
94 "Consumer Context binding is unresolved.",
95 &resolution.provenance,
96 ),
97 ContextResolutionResult::Ambiguous { .. } => push(
98 &mut diagnostics,
99 "PSC1058",
100 "Consumer Context binding is ambiguous.",
101 &resolution.provenance,
102 ),
103 _ => {}
104 }
105 }
106 for record in model.provider_types.values() {
107 let value_mismatch = record.value_to_declaration == CompatibilityStatus::Incompatible;
108 let context_mismatch = record.declaration_to_context == CompatibilityStatus::Incompatible;
109 if value_mismatch {
110 let provenance = model
111 .provider(&record.provider)
112 .and_then(|provider| expression_provenance(model, &provider.value_expression))
113 .unwrap_or(&record.provenance);
114 push(
115 &mut diagnostics,
116 "PSC1059",
117 "Provider value is incompatible with its declared type.",
118 provenance,
119 );
120 }
121 if context_mismatch {
122 let provenance = model
123 .provider(&record.provider)
124 .map_or(&record.provenance, |provider| {
125 &provider.declared_type.provenance
126 });
127 push(
128 &mut diagnostics,
129 "PSC1060",
130 "Provider declared type is incompatible with its Context.",
131 provenance,
132 );
133 }
134 if !value_mismatch
135 && !context_mismatch
136 && record.serialization == ContextSerializationCompatibility::NonSerializable
137 {
138 let provenance = model
139 .provider(&record.provider)
140 .and_then(|provider| expression_provenance(model, &provider.value_expression))
141 .unwrap_or(&record.provenance);
142 push(
143 &mut diagnostics,
144 "PSC1063",
145 "Context Provider source is not serializable.",
146 provenance,
147 );
148 } else if !value_mismatch
149 && !context_mismatch
150 && record.boundary_compatibility == CompatibilityStatus::Incompatible
151 {
152 let provenance = model
153 .provider(&record.provider)
154 .and_then(|provider| expression_provenance(model, &provider.value_expression))
155 .unwrap_or(&record.provenance);
156 push(
157 &mut diagnostics,
158 "PSC1064",
159 "Context Provider source crosses an incompatible boundary.",
160 provenance,
161 );
162 }
163 }
164 for record in model.context_types.values() {
165 let default_mismatch =
166 record.default_compatibility == Some(CompatibilityStatus::Incompatible);
167 if default_mismatch {
168 let provenance =
169 context_default_provenance(model, &record.context).unwrap_or(&record.provenance);
170 push(
171 &mut diagnostics,
172 "PSC1061",
173 "Context default is incompatible with its declared type.",
174 provenance,
175 );
176 }
177 if !default_mismatch
178 && record.serialization == ContextSerializationCompatibility::NonSerializable
179 {
180 let provenance = context_default_or_declared_type_provenance(model, &record.context)
181 .unwrap_or(&record.provenance);
182 push(
183 &mut diagnostics,
184 "PSC1063",
185 "Context default is not serializable.",
186 provenance,
187 );
188 } else if !default_mismatch
189 && record.boundary_compatibility == CompatibilityStatus::Incompatible
190 {
191 let provenance = context_default_or_declared_type_provenance(model, &record.context)
192 .unwrap_or(&record.provenance);
193 push(
194 &mut diagnostics,
195 "PSC1064",
196 "Context crosses an incompatible boundary.",
197 provenance,
198 );
199 }
200 }
201 for record in model.consumer_types.values() {
202 if record.context_to_consumer == CompatibilityStatus::Incompatible {
203 let provenance = model
204 .consumer(&record.consumer)
205 .map_or(&record.provenance, |consumer| {
206 &consumer.requested_type.provenance
207 });
208 push(
209 &mut diagnostics,
210 "PSC1062",
211 "Context is incompatible with the Consumer request.",
212 provenance,
213 );
214 }
215 }
216 for record in &model.context_lifetime.dependency_lifetimes {
217 if record.compatibility == LifetimeCompatibilityStatus::Incompatible {
218 push(
219 &mut diagnostics,
220 "PSC1065",
221 "Context dependency lifetime is incompatible.",
222 &record.provenance,
223 );
224 }
225 }
226 for record in model.context_lifetime.binding_lifetimes.values() {
227 if record.compatibility == ContextBindingLifetimeStatus::Incompatible {
228 push(
229 &mut diagnostics,
230 "PSC1065",
231 "Context Consumer binding lifetime is incompatible.",
232 &record.provenance,
233 );
234 }
235 }
236 for entry in model.context_evaluation.source_entries.values() {
237 let specific = matches!(
238 entry.status,
239 ContextSourcePlanStatus::BlockedType
240 | ContextSourcePlanStatus::BlockedSerialization
241 | ContextSourcePlanStatus::BlockedBoundary
242 | ContextSourcePlanStatus::BlockedLifetime
243 );
244 if !specific
245 && entry.reasons.iter().any(|reason| {
246 matches!(
247 reason,
248 ContextSourceBlockReason::MissingStateDependency(_)
249 | ContextSourceBlockReason::UnavailableComputedDependency(_)
250 )
251 })
252 {
253 let provenance = context_source_failure_provenance(model, entry, "PSC1066");
254 push(
255 &mut diagnostics,
256 "PSC1066",
257 "Context source has an unavailable dependency.",
258 provenance,
259 );
260 } else if !specific
261 && entry
262 .reasons
263 .iter()
264 .any(|reason| matches!(reason, ContextSourceBlockReason::UnsupportedExpression))
265 {
266 let provenance = context_source_failure_provenance(model, entry, "PSC1067");
267 push(
268 &mut diagnostics,
269 "PSC1067",
270 "Context source cannot be planned from an unsupported expression.",
271 provenance,
272 );
273 }
274 }
275 populate_identities(model, &mut diagnostics);
276 populate_secondary_labels(model, &mut diagnostics);
277 diagnostics.sort_by(|left, right| {
278 (
279 left.provenance
280 .as_ref()
281 .map(|value| (&value.path, value.span.start)),
282 &left.code,
283 )
284 .cmp(&(
285 right
286 .provenance
287 .as_ref()
288 .map(|value| (&value.path, value.span.start)),
289 &right.code,
290 ))
291 });
292 diagnostics
293}
294
295fn context_default_provenance<'a>(
296 model: &'a ApplicationSemanticModel,
297 context: &crate::ContextId,
298) -> Option<&'a SourceProvenance> {
299 model
300 .context(context)
301 .and_then(|context| context.default_expression.as_ref())
302 .and_then(|expression| expression_provenance(model, expression))
303}
304
305fn expression_provenance<'a>(
306 model: &'a ApplicationSemanticModel,
307 expression: &crate::SemanticId,
308) -> Option<&'a SourceProvenance> {
309 model
310 .expression_graph
311 .provenance_of(expression)
312 .or_else(|| model.provenance(expression))
313}
314
315fn context_default_or_declared_type_provenance<'a>(
316 model: &'a ApplicationSemanticModel,
317 context: &crate::ContextId,
318) -> Option<&'a SourceProvenance> {
319 context_default_provenance(model, context).or_else(|| {
320 model
321 .context(context)
322 .map(|context| &context.declared_type.provenance)
323 })
324}
325
326fn context_source_failure_provenance<'a>(
327 model: &'a ApplicationSemanticModel,
328 entry: &'a ContextSourcePlanEntry,
329 code: &str,
330) -> &'a SourceProvenance {
331 if code == "PSC1066" {
332 let dependent = match &entry.source {
333 ContextValueSourceId::Provider(provider) => {
334 ContextDependencyNodeId::Provider(provider.clone())
335 }
336 ContextValueSourceId::ContextDefault(context) => {
337 ContextDependencyNodeId::ContextDefault(context.clone())
338 }
339 };
340 if let Some(provenance) = entry.reasons.iter().find_map(|reason| {
341 let dependency = match reason {
342 ContextSourceBlockReason::MissingStateDependency(id) => {
343 ContextDependencyNodeId::State(id.clone())
344 }
345 ContextSourceBlockReason::UnavailableComputedDependency(id) => {
346 ContextDependencyNodeId::Computed(id.clone())
347 }
348 _ => return None,
349 };
350 model
351 .context_dependency
352 .edges
353 .iter()
354 .find(|edge| edge.dependent == dependent && edge.dependency == dependency)
355 .map(|edge| &edge.provenance)
356 }) {
357 return provenance;
358 }
359 }
360 if code == "PSC1067" {
361 if let Some(provenance) = expression_provenance(model, &entry.expression_root) {
362 return provenance;
363 }
364 }
365 &entry.provenance
366}
367
368#[allow(clippy::too_many_lines)]
369fn populate_secondary_labels(
370 model: &ApplicationSemanticModel,
371 diagnostics: &mut [ComponentDiagnostic],
372) {
373 for diagnostic in diagnostics {
374 if diagnostic.code == "PSC1058" {
375 let mut labels = diagnostic
376 .consumer_id
377 .as_ref()
378 .and_then(|consumer| {
379 let crate::ContextResolution {
380 result: ContextResolutionResult::Ambiguous { providers, .. },
381 ..
382 } = model.context_resolutions.get(consumer)?
383 else {
384 return None;
385 };
386 Some(
387 providers
388 .iter()
389 .filter_map(|id| model.provider(id))
390 .map(|provider| crate::DiagnosticSecondaryLabel {
391 provenance: provider.provenance.clone(),
392 message: format!("Candidate Provider `{}`.", provider.id),
393 })
394 .collect::<Vec<_>>(),
395 )
396 })
397 .unwrap_or_default();
398 labels.sort_by(|left, right| left.message.cmp(&right.message));
399 labels.dedup();
400 diagnostic.secondary_labels = labels;
401 continue;
402 }
403 let mut labels = Vec::new();
404 let context = diagnostic
405 .context_id
406 .as_ref()
407 .and_then(|id| model.context(id));
408 let provider = diagnostic
409 .provider_id
410 .as_ref()
411 .and_then(|id| model.provider(id));
412 match diagnostic.code.as_str() {
413 "PSC1057" => {
414 if let Some(context) = context {
415 labels.push(crate::DiagnosticSecondaryLabel {
416 provenance: context.provenance.clone(),
417 message: "Requested Context declaration.".to_string(),
418 });
419 }
420 }
421 "PSC1059" => {
422 if let Some(provider) = provider {
423 labels.push(crate::DiagnosticSecondaryLabel {
424 provenance: provider.declared_type.provenance.clone(),
425 message: "Provider declared type.".to_string(),
426 });
427 }
428 }
429 "PSC1060" | "PSC1061" | "PSC1062" | "PSC1063" | "PSC1064" => {
430 if let Some(context) = context {
431 labels.push(crate::DiagnosticSecondaryLabel {
432 provenance: context.declared_type.provenance.clone(),
433 message: "Context declared type.".to_string(),
434 });
435 }
436 }
437 "PSC1065" => populate_lifetime_labels(model, diagnostic, &mut labels),
438 "PSC1067" => {
439 if let Some(provider) = provider {
440 labels.push(crate::DiagnosticSecondaryLabel {
441 provenance: provider.provenance.clone(),
442 message: "Context source declaration.".to_string(),
443 });
444 } else if let Some(context) = context {
445 labels.push(crate::DiagnosticSecondaryLabel {
446 provenance: context.provenance.clone(),
447 message: "Context source declaration.".to_string(),
448 });
449 }
450 }
451 _ => {}
452 }
453 if diagnostic.code == "PSC1066" {
454 if let Some(entry) = model
455 .context_evaluation
456 .source_entries
457 .values()
458 .find(|entry| {
459 diagnostic.provenance.as_ref()
460 == Some(context_source_failure_provenance(model, entry, "PSC1066"))
461 })
462 {
463 for reason in &entry.reasons {
464 let dependency = match reason {
465 ContextSourceBlockReason::MissingStateDependency(id)
466 | ContextSourceBlockReason::UnavailableComputedDependency(id) => Some(id),
467 _ => None,
468 };
469 if let Some(provenance) = dependency.and_then(|id| model.provenance(id)) {
470 labels.push(crate::DiagnosticSecondaryLabel {
471 provenance: provenance.clone(),
472 message: "Unavailable Context dependency.".to_string(),
473 });
474 }
475 }
476 }
477 }
478 labels.retain(|label| diagnostic.provenance.as_ref() != Some(&label.provenance));
479 labels.sort_by(|left, right| {
480 (
481 &left.provenance.path,
482 left.provenance.span.start,
483 &left.message,
484 )
485 .cmp(&(
486 &right.provenance.path,
487 right.provenance.span.start,
488 &right.message,
489 ))
490 });
491 labels.dedup();
492 diagnostic.secondary_labels = labels;
493 }
494}
495
496fn populate_lifetime_labels(
497 model: &ApplicationSemanticModel,
498 diagnostic: &ComponentDiagnostic,
499 labels: &mut Vec<crate::DiagnosticSecondaryLabel>,
500) {
501 let Some(primary) = diagnostic.provenance.as_ref() else {
502 return;
503 };
504 if let Some(record) = model
505 .context_lifetime
506 .dependency_lifetimes
507 .iter()
508 .find(|record| &record.provenance == primary)
509 {
510 if let Some(provenance) = context_dependency_node_provenance(model, &record.dependency) {
511 labels.push(crate::DiagnosticSecondaryLabel {
512 provenance: provenance.clone(),
513 message: "Incompatible lifetime dependency.".to_string(),
514 });
515 }
516 return;
517 }
518 let Some(record) = diagnostic
519 .consumer_id
520 .as_ref()
521 .and_then(|consumer| model.context_lifetime.binding_lifetimes.get(consumer))
522 else {
523 return;
524 };
525 let provenance = match &record.source {
526 Some(crate::ContextBindingLifetimeSource::Provider(provider)) => {
527 model.provider(provider).map(|item| &item.provenance)
528 }
529 Some(crate::ContextBindingLifetimeSource::ContextDefault(context)) => {
530 model.context(context).map(|item| &item.provenance)
531 }
532 None => None,
533 };
534 if let Some(provenance) = provenance {
535 labels.push(crate::DiagnosticSecondaryLabel {
536 provenance: provenance.clone(),
537 message: "Selected Context source declaration.".to_string(),
538 });
539 }
540}
541
542fn context_dependency_node_provenance<'a>(
543 model: &'a ApplicationSemanticModel,
544 node: &ContextDependencyNodeId,
545) -> Option<&'a SourceProvenance> {
546 match node {
547 ContextDependencyNodeId::State(id) | ContextDependencyNodeId::Computed(id) => {
548 model.provenance(id)
549 }
550 ContextDependencyNodeId::Context(id) | ContextDependencyNodeId::ContextDefault(id) => {
551 model.context(id).map(|item| &item.provenance)
552 }
553 ContextDependencyNodeId::Provider(id) => model.provider(id).map(|item| &item.provenance),
554 ContextDependencyNodeId::Consumer(id) => model.consumer(id).map(|item| &item.provenance),
555 }
556}
557
558#[allow(clippy::too_many_lines)]
559fn populate_identities(model: &ApplicationSemanticModel, diagnostics: &mut [ComponentDiagnostic]) {
560 for diagnostic in diagnostics {
561 let Some(provenance) = diagnostic.provenance.as_ref() else {
562 continue;
563 };
564 match diagnostic.code.as_str() {
565 "PSC1057" | "PSC1058" => {
566 if let Some(resolution) = model
567 .context_resolutions
568 .values()
569 .find(|record| record.provenance == *provenance)
570 {
571 diagnostic.consumer_id = Some(resolution.consumer.clone());
572 diagnostic.context_id = resolution.context.clone();
573 }
574 }
575 "PSC1059" | "PSC1060" => {
576 if let Some(record) = model.provider_types.values().find(|record| {
577 let Some(provider) = model.provider(&record.provider) else {
578 return false;
579 };
580 let expected = if diagnostic.code == "PSC1059" {
581 expression_provenance(model, &provider.value_expression)
582 .unwrap_or(&record.provenance)
583 } else {
584 &provider.declared_type.provenance
585 };
586 expected == provenance
587 }) {
588 diagnostic.provider_id = Some(record.provider.clone());
589 diagnostic.context_id = record.context.clone();
590 }
591 }
592 "PSC1061" => {
593 if let Some(record) = model.context_types.values().find(|record| {
594 context_default_provenance(model, &record.context).unwrap_or(&record.provenance)
595 == provenance
596 }) {
597 diagnostic.context_id = Some(record.context.clone());
598 }
599 }
600 "PSC1062" => {
601 if let Some(record) = model.consumer_types.values().find(|record| {
602 model
603 .consumer(&record.consumer)
604 .map_or(&record.provenance, |consumer| {
605 &consumer.requested_type.provenance
606 })
607 == provenance
608 }) {
609 diagnostic.consumer_id = Some(record.consumer.clone());
610 diagnostic.context_id = record.context.clone();
611 }
612 }
613 "PSC1063" | "PSC1064" => {
614 if let Some(record) = model.provider_types.values().find(|record| {
615 model
616 .provider(&record.provider)
617 .and_then(|provider| {
618 expression_provenance(model, &provider.value_expression)
619 })
620 .unwrap_or(&record.provenance)
621 == provenance
622 }) {
623 diagnostic.provider_id = Some(record.provider.clone());
624 diagnostic.context_id = record.context.clone();
625 } else if let Some(record) = model.context_types.values().find(|record| {
626 context_default_or_declared_type_provenance(model, &record.context)
627 .unwrap_or(&record.provenance)
628 == provenance
629 }) {
630 diagnostic.context_id = Some(record.context.clone());
631 }
632 }
633 "PSC1065" => {
634 if let Some(record) = model
635 .context_lifetime
636 .binding_lifetimes
637 .values()
638 .find(|record| record.provenance == *provenance)
639 {
640 diagnostic.consumer_id = Some(record.consumer.clone());
641 diagnostic.context_id = model
642 .context_resolutions
643 .get(&record.consumer)
644 .and_then(|resolution| resolution.context.clone());
645 } else if let Some(record) = model
646 .context_lifetime
647 .dependency_lifetimes
648 .iter()
649 .find(|record| record.provenance == *provenance)
650 {
651 match &record.dependent {
652 ContextDependencyNodeId::Provider(provider) => {
653 diagnostic.provider_id = Some(provider.clone());
654 diagnostic.context_id =
655 model.provider(provider).map(|item| item.context.clone());
656 }
657 ContextDependencyNodeId::ContextDefault(context)
658 | ContextDependencyNodeId::Context(context) => {
659 diagnostic.context_id = Some(context.clone());
660 }
661 ContextDependencyNodeId::Consumer(consumer) => {
662 diagnostic.consumer_id = Some(consumer.clone());
663 diagnostic.context_id = model
664 .consumer(consumer)
665 .and_then(|item| item.context().cloned());
666 }
667 ContextDependencyNodeId::State(_)
668 | ContextDependencyNodeId::Computed(_) => {}
669 }
670 }
671 }
672 "PSC1066" | "PSC1067" => {
673 if let Some(entry) =
674 model
675 .context_evaluation
676 .source_entries
677 .values()
678 .find(|entry| {
679 context_source_failure_provenance(model, entry, &diagnostic.code)
680 == provenance
681 })
682 {
683 diagnostic.context_id = Some(entry.context.clone());
684 if let crate::ContextValueSourceId::Provider(provider) = &entry.source {
685 diagnostic.provider_id = Some(provider.clone());
686 }
687 }
688 }
689 _ => {}
690 }
691 }
692}
693
694fn push(
695 diagnostics: &mut Vec<ComponentDiagnostic>,
696 code: &str,
697 message: &str,
698 provenance: &crate::SourceProvenance,
699) {
700 diagnostics.push(ComponentDiagnostic {
701 code: code.to_string(),
702 severity: ComponentDiagnosticSeverity::Error,
703 message: message.to_string(),
704 provenance: Some(provenance.clone()),
705 effect_id: None,
706 statement_id: None,
707 context_declaration_candidate_id: None,
708 context_id: None,
709 provider_id: None,
710 consumer_id: None,
711 slot_id: None,
712 invocation_id: None,
713 component_instance_id: None,
714 slot_binding_id: None,
715 structural_region_id: None,
716 component_id: None,
717 provider_instance_id: None,
718 consumer_instance_id: None,
719 secondary_labels: Vec::new(),
720 });
721}
722
723fn message(code: &str, field: Option<&str>) -> String {
724 let subject = field.map_or("declaration".to_string(), |field| {
725 format!("declaration `{field}`")
726 });
727 match code {
728 "PSC1052" => format!("Invalid Context {subject}."),
729 "PSC1053" => format!("Invalid Provider {subject}."),
730 "PSC1054" => format!("Invalid Consumer {subject}."),
731 "PSC1055" => format!("Unresolved Context designator for {subject}."),
732 "PSC1056" => "Duplicate Provider declarations target the same Context.".to_string(),
733 _ => unreachable!("frozen Context diagnostic code"),
734 }
735}
736
737#[cfg(test)]
738mod tests {
739 use crate::{build_application_semantic_model, ComponentDiagnosticSeverity};
740
741 fn codes(source: &str) -> Vec<crate::ComponentDiagnostic> {
742 let model = build_application_semantic_model(&presolve_parser::parse_file(
743 "src/context-diagnostics.tsx",
744 source,
745 ));
746 assert_catalog_shapes(&model, &model.diagnostics);
747 model.diagnostics
748 }
749
750 #[allow(clippy::too_many_lines)]
751 fn assert_catalog_shapes(
752 model: &crate::ApplicationSemanticModel,
753 diagnostics: &[crate::ComponentDiagnostic],
754 ) {
755 let context_diagnostics = diagnostics
756 .iter()
757 .filter(|diagnostic| ("PSC1052"..="PSC1067").contains(&diagnostic.code.as_str()))
758 .collect::<Vec<_>>();
759 assert!(context_diagnostics.windows(2).all(|pair| {
760 let key = |diagnostic: &crate::ComponentDiagnostic| {
761 (
762 diagnostic
763 .provenance
764 .as_ref()
765 .map(|value| (value.path.clone(), value.span.start)),
766 diagnostic.code.clone(),
767 )
768 };
769 key(pair[0]) <= key(pair[1])
770 }));
771 for diagnostic in context_diagnostics {
772 assert_eq!(diagnostic.severity, ComponentDiagnosticSeverity::Error);
773 assert!(diagnostic.effect_id.is_none() && diagnostic.statement_id.is_none());
774 let primary = diagnostic.provenance.as_ref().expect("Context primary");
775 let mut expected_labels = Vec::new();
776 match diagnostic.code.as_str() {
777 "PSC1052" | "PSC1053" | "PSC1054" | "PSC1055" | "PSC1056" => {
778 let candidate_id = diagnostic
779 .context_declaration_candidate_id
780 .as_ref()
781 .expect("declaration candidate identity");
782 let candidate = model
783 .context_declaration_candidates()
784 .candidate(candidate_id)
785 .expect("retained declaration candidate");
786 let crate::ContextDeclarationStatus::Invalid(violations) = &candidate.status
787 else {
788 panic!("diagnostic candidate must be invalid");
789 };
790 let expected = match violations.first().unwrap() {
791 crate::ContextDeclarationViolation::StaticDeclarationUnsupported => {
792 candidate.authored.static_modifier_provenance.as_ref()
793 }
794 crate::ContextDeclarationViolation::UnsupportedInitializer
795 | crate::ContextDeclarationViolation::ForbiddenInitializer
796 | crate::ContextDeclarationViolation::MissingInitializer => {
797 candidate.authored.initializer_provenance.as_ref()
798 }
799 crate::ContextDeclarationViolation::ContextDesignatorUnsupported
800 | crate::ContextDeclarationViolation::UnresolvedContextDesignator => {
801 candidate
802 .authored
803 .context_designator
804 .as_ref()
805 .map(|designator| &designator.provenance)
806 }
807 _ => Some(&candidate.authored.decorator_provenance),
808 }
809 .unwrap_or(&candidate.authored.provenance);
810 assert_eq!(primary, expected);
811 assert!(
812 diagnostic.context_id.is_none()
813 && diagnostic.provider_id.is_none()
814 && diagnostic.consumer_id.is_none()
815 );
816 }
817 "PSC1057" | "PSC1058" => {
818 assert!(diagnostic.context_declaration_candidate_id.is_none());
819 assert!(diagnostic.provider_id.is_none());
820 let consumer = diagnostic.consumer_id.as_ref().expect("Consumer identity");
821 let resolution = model
822 .context_resolutions
823 .get(consumer)
824 .expect("G4 resolution");
825 assert_eq!(primary, &resolution.provenance);
826 assert_eq!(diagnostic.context_id, resolution.context);
827 if diagnostic.code == "PSC1057" {
828 if let Some(context) = diagnostic
829 .context_id
830 .as_ref()
831 .and_then(|id| model.context(id))
832 {
833 expected_labels.push(crate::DiagnosticSecondaryLabel {
834 provenance: context.provenance.clone(),
835 message: "Requested Context declaration.".to_string(),
836 });
837 }
838 } else {
839 let crate::ContextResolutionResult::Ambiguous { providers, .. } =
840 &resolution.result
841 else {
842 panic!("PSC1058 requires G4 ambiguity");
843 };
844 expected_labels.extend(providers.iter().filter_map(|id| {
845 model
846 .provider(id)
847 .map(|provider| crate::DiagnosticSecondaryLabel {
848 provenance: provider.provenance.clone(),
849 message: format!("Candidate Provider `{}`.", provider.id),
850 })
851 }));
852 expected_labels.sort_by(|left, right| left.message.cmp(&right.message));
853 expected_labels.dedup();
854 }
855 }
856 "PSC1059" | "PSC1060" => {
857 assert!(diagnostic.context_declaration_candidate_id.is_none());
858 assert!(diagnostic.consumer_id.is_none());
859 let provider_id = diagnostic.provider_id.as_ref().expect("Provider identity");
860 let provider = model.provider(provider_id).expect("Provider entity");
861 assert_eq!(diagnostic.context_id.as_ref(), Some(&provider.context));
862 if diagnostic.code == "PSC1059" {
863 assert_eq!(
864 primary,
865 super::expression_provenance(model, &provider.value_expression)
866 .unwrap()
867 );
868 expected_labels.push(crate::DiagnosticSecondaryLabel {
869 provenance: provider.declared_type.provenance.clone(),
870 message: "Provider declared type.".to_string(),
871 });
872 } else {
873 assert_eq!(primary, &provider.declared_type.provenance);
874 let context = model.context(&provider.context).unwrap();
875 expected_labels.push(crate::DiagnosticSecondaryLabel {
876 provenance: context.declared_type.provenance.clone(),
877 message: "Context declared type.".to_string(),
878 });
879 }
880 }
881 "PSC1061" => {
882 assert!(diagnostic.context_declaration_candidate_id.is_none());
883 assert!(diagnostic.provider_id.is_none() && diagnostic.consumer_id.is_none());
884 let context = model
885 .context(diagnostic.context_id.as_ref().expect("Context identity"))
886 .unwrap();
887 assert_eq!(
888 primary,
889 super::expression_provenance(
890 model,
891 context.default_expression.as_ref().unwrap(),
892 )
893 .unwrap()
894 );
895 expected_labels.push(crate::DiagnosticSecondaryLabel {
896 provenance: context.declared_type.provenance.clone(),
897 message: "Context declared type.".to_string(),
898 });
899 }
900 "PSC1062" => {
901 let consumer = model
902 .consumer(diagnostic.consumer_id.as_ref().expect("Consumer identity"))
903 .unwrap();
904 assert_eq!(primary, &consumer.requested_type.provenance);
905 assert_eq!(diagnostic.context_id.as_ref(), consumer.context());
906 let context = model.context(consumer.context().unwrap()).unwrap();
907 expected_labels.push(crate::DiagnosticSecondaryLabel {
908 provenance: context.declared_type.provenance.clone(),
909 message: "Context declared type.".to_string(),
910 });
911 }
912 "PSC1063" | "PSC1064" => {
913 let context = model
914 .context(diagnostic.context_id.as_ref().expect("Context identity"))
915 .unwrap();
916 if let Some(provider) = diagnostic
917 .provider_id
918 .as_ref()
919 .and_then(|id| model.provider(id))
920 {
921 assert_eq!(
922 primary,
923 super::expression_provenance(model, &provider.value_expression)
924 .unwrap()
925 );
926 } else if let Some(default) = &context.default_expression {
927 assert_eq!(
928 primary,
929 super::expression_provenance(model, default).unwrap()
930 );
931 } else {
932 assert_eq!(primary, &context.declared_type.provenance);
933 }
934 expected_labels.push(crate::DiagnosticSecondaryLabel {
935 provenance: context.declared_type.provenance.clone(),
936 message: "Context declared type.".to_string(),
937 });
938 }
939 "PSC1065" => {
940 let consumer = diagnostic.consumer_id.as_ref().expect("Consumer identity");
941 let record = model
942 .context_lifetime
943 .binding_lifetimes
944 .get(consumer)
945 .expect("G8 binding lifetime");
946 assert_eq!(primary, &record.provenance);
947 assert_eq!(
948 diagnostic.context_id,
949 model
950 .context_resolutions
951 .get(consumer)
952 .and_then(|resolution| resolution.context.clone())
953 );
954 if let Some(crate::ContextBindingLifetimeSource::Provider(provider)) =
955 &record.source
956 {
957 expected_labels.push(crate::DiagnosticSecondaryLabel {
958 provenance: model.provider(provider).unwrap().provenance.clone(),
959 message: "Selected Context source declaration.".to_string(),
960 });
961 }
962 }
963 "PSC1066" | "PSC1067" => {
964 let entry = model
965 .context_evaluation
966 .source_entries
967 .values()
968 .find(|entry| {
969 super::context_source_failure_provenance(model, entry, &diagnostic.code)
970 == primary
971 })
972 .expect("G9 source failure");
973 assert_eq!(diagnostic.context_id.as_ref(), Some(&entry.context));
974 match &entry.source {
975 crate::ContextValueSourceId::Provider(provider) => {
976 assert_eq!(diagnostic.provider_id.as_ref(), Some(provider));
977 if diagnostic.code == "PSC1067" {
978 expected_labels.push(crate::DiagnosticSecondaryLabel {
979 provenance: model
980 .provider(provider)
981 .unwrap()
982 .provenance
983 .clone(),
984 message: "Context source declaration.".to_string(),
985 });
986 }
987 }
988 crate::ContextValueSourceId::ContextDefault(_) => {
989 assert!(diagnostic.provider_id.is_none());
990 if diagnostic.code == "PSC1067" {
991 expected_labels.push(crate::DiagnosticSecondaryLabel {
992 provenance: model
993 .context(&entry.context)
994 .unwrap()
995 .provenance
996 .clone(),
997 message: "Context source declaration.".to_string(),
998 });
999 }
1000 }
1001 }
1002 }
1003 _ => unreachable!("stable Context diagnostic code"),
1004 }
1005 expected_labels.retain(|label| &label.provenance != primary);
1006 expected_labels.sort_by(|left, right| {
1007 (
1008 &left.provenance.path,
1009 left.provenance.span.start,
1010 &left.message,
1011 )
1012 .cmp(&(
1013 &right.provenance.path,
1014 right.provenance.span.start,
1015 &right.message,
1016 ))
1017 });
1018 expected_labels.dedup();
1019 assert_eq!(diagnostic.secondary_labels, expected_labels);
1020 }
1021 }
1022
1023 #[test]
1024 fn psc1052_invalid_context_declaration_has_candidate_identity_and_no_semantic_identity() {
1025 let diagnostics = codes(
1026 r#"
1027@component("x-app") class App extends Component {
1028 @context("invalid") theme!: string;
1029 render() { return <main />; }
1030}"#,
1031 );
1032 let diagnostic = diagnostics
1033 .iter()
1034 .find(|item| item.code == "PSC1052")
1035 .unwrap();
1036 assert_eq!(diagnostic.severity, ComponentDiagnosticSeverity::Error);
1037 assert!(diagnostic.provenance.is_some());
1038 assert!(diagnostic.context_declaration_candidate_id.is_some());
1039 assert!(
1040 diagnostic.context_id.is_none()
1041 && diagnostic.provider_id.is_none()
1042 && diagnostic.consumer_id.is_none()
1043 );
1044 assert!(diagnostic.secondary_labels.is_empty());
1045 assert!(!diagnostics.iter().any(|item| item.code == "PSC1055"));
1046 }
1047
1048 #[test]
1049 fn psc1053_and_psc1055_keep_declaration_and_designator_failures_distinct() {
1050 let diagnostics = codes(
1051 r#"
1052@component("x-app") class App extends Component {
1053 @provide(Missing.theme) value: string = "x";
1054 render() { return <main />; }
1055}"#,
1056 );
1057 let diagnostic = diagnostics
1058 .iter()
1059 .find(|item| item.code == "PSC1055")
1060 .unwrap();
1061 assert_eq!(diagnostic.severity, ComponentDiagnosticSeverity::Error);
1062 assert!(diagnostic.context_declaration_candidate_id.is_some());
1063 assert!(diagnostic.provenance.is_some());
1064 assert!(!diagnostics.iter().any(|item| item.code == "PSC1053"));
1065 }
1066
1067 #[test]
1068 fn psc1053_invalid_provider_declaration_has_only_candidate_identity() {
1069 let diagnostics = codes(
1070 r#"
1071@component("x-app") class App extends Component { @provide() value: string = "x"; render() { return <main />; } }
1072"#,
1073 );
1074 let diagnostic = diagnostics
1075 .iter()
1076 .find(|item| item.code == "PSC1053")
1077 .unwrap();
1078 assert_eq!(diagnostic.severity, ComponentDiagnosticSeverity::Error);
1079 assert!(diagnostic.context_declaration_candidate_id.is_some());
1080 assert!(
1081 diagnostic.context_id.is_none()
1082 && diagnostic.provider_id.is_none()
1083 && diagnostic.consumer_id.is_none()
1084 );
1085 assert!(!diagnostics.iter().any(|item| item.code == "PSC1055"));
1086 }
1087
1088 #[test]
1089 fn psc1056_duplicate_provider_group_has_one_deterministic_diagnostic() {
1090 let diagnostics = codes(
1091 r#"
1092@component("x-app") class App extends Component {
1093 @context() theme: string;
1094 @provide(App.theme) second: string = "b";
1095 @provide(App.theme) first: string = "a";
1096 render() { return <main />; }
1097}"#,
1098 );
1099 let matches = diagnostics
1100 .iter()
1101 .filter(|item| item.code == "PSC1056")
1102 .collect::<Vec<_>>();
1103 assert_eq!(matches.len(), 1);
1104 assert!(matches[0].context_declaration_candidate_id.is_some());
1105 assert!(matches[0].provider_id.is_none());
1106 assert!(!diagnostics.iter().any(|item| item.code == "PSC1058"));
1107 }
1108
1109 #[test]
1110 fn psc1054_invalid_consumer_declaration_suppresses_binding_diagnostics() {
1111 let diagnostics = codes(
1112 r#"
1113@component("x-app") class App extends Component {
1114 @consume(App.theme) theme: string = "x";
1115 render() { return <main />; }
1116}"#,
1117 );
1118 let diagnostic = diagnostics
1119 .iter()
1120 .find(|item| item.code == "PSC1054")
1121 .unwrap();
1122 assert!(diagnostic.context_declaration_candidate_id.is_some());
1123 assert!(!diagnostics.iter().any(|item| matches!(
1124 item.code.as_str(),
1125 "PSC1057" | "PSC1058" | "PSC1062" | "PSC1065" | "PSC1066" | "PSC1067"
1126 )));
1127 }
1128
1129 #[test]
1130 fn psc1059_and_psc1060_coexist_for_independent_provider_contract_failures() {
1131 let diagnostics = codes(
1132 r#"
1133@component("x-app") class App extends Component {
1134 @context() theme: string;
1135 @provide(App.theme) provided: boolean = "x";
1136 render() { return <main />; }
1137}"#,
1138 );
1139 for code in ["PSC1059", "PSC1060"] {
1140 let diagnostic = diagnostics.iter().find(|item| item.code == code).unwrap();
1141 assert_eq!(diagnostic.severity, ComponentDiagnosticSeverity::Error);
1142 assert!(diagnostic.provider_id.is_some() && diagnostic.context_id.is_some());
1143 assert!(diagnostic.provenance.is_some());
1144 }
1145 assert!(!diagnostics
1146 .iter()
1147 .any(|item| matches!(item.code.as_str(), "PSC1066" | "PSC1067")));
1148 }
1149
1150 #[test]
1151 fn psc1059_value_mismatch_has_provider_and_context_identities() {
1152 let diagnostics = codes(
1153 r#"
1154@component("x-app") class App extends Component { @context() theme: boolean; @provide(App.theme) provided: boolean = "x"; render() { return <main />; } }
1155"#,
1156 );
1157 let diagnostic = diagnostics
1158 .iter()
1159 .find(|item| item.code == "PSC1059")
1160 .unwrap();
1161 assert!(diagnostic.provider_id.is_some() && diagnostic.context_id.is_some());
1162 assert!(!diagnostics.iter().any(|item| item.code == "PSC1060"));
1163 }
1164
1165 #[test]
1166 fn psc1060_declaration_context_mismatch_has_provider_and_context_identities() {
1167 let diagnostics = codes(
1168 r#"
1169@component("x-app") class App extends Component { @context() theme: string; @provide(App.theme) provided: boolean = true; render() { return <main />; } }
1170"#,
1171 );
1172 let diagnostic = diagnostics
1173 .iter()
1174 .find(|item| item.code == "PSC1060")
1175 .unwrap();
1176 assert!(diagnostic.provider_id.is_some() && diagnostic.context_id.is_some());
1177 assert!(!diagnostics.iter().any(|item| item.code == "PSC1059"));
1178 }
1179
1180 #[test]
1181 fn psc1061_context_default_mismatch_has_context_identity() {
1182 let diagnostics = codes(
1183 r#"
1184@component("x-app") class App extends Component {
1185 @context() theme: number = "x";
1186 render() { return <main />; }
1187}"#,
1188 );
1189 let diagnostic = diagnostics
1190 .iter()
1191 .find(|item| item.code == "PSC1061")
1192 .unwrap();
1193 assert_eq!(diagnostic.severity, ComponentDiagnosticSeverity::Error);
1194 assert!(diagnostic.context_id.is_some() && diagnostic.provenance.is_some());
1195 assert!(!diagnostics.iter().any(|item| matches!(
1196 item.code.as_str(),
1197 "PSC1063" | "PSC1064" | "PSC1066" | "PSC1067"
1198 )));
1199 }
1200
1201 #[test]
1202 fn psc1062_context_consumer_mismatch_has_both_canonical_identities() {
1203 let diagnostics = codes(
1204 r#"
1205@component("x-app") class App extends Component {
1206 @context() theme: string;
1207 @consume(App.theme) selected!: number;
1208 render() { return <main />; }
1209}"#,
1210 );
1211 let diagnostic = diagnostics
1212 .iter()
1213 .find(|item| item.code == "PSC1062")
1214 .unwrap();
1215 assert_eq!(diagnostic.severity, ComponentDiagnosticSeverity::Error);
1216 assert!(diagnostic.context_id.is_some() && diagnostic.consumer_id.is_some());
1217 assert!(diagnostic.provenance.is_some());
1218 }
1219
1220 #[test]
1221 fn psc1057_projects_only_an_explicit_unresolved_g4_result() {
1222 let mut model = build_application_semantic_model(&presolve_parser::parse_file(
1223 "src/g4.tsx",
1224 r#"
1225@component("x-app") class App extends Component {
1226 @context() theme: string;
1227 @consume(App.theme) selected!: string;
1228 render() { return <main />; }
1229}"#,
1230 ));
1231 let consumer = model.consumers()[0].id.clone();
1232 let resolution = model.context_resolutions.get_mut(&consumer).unwrap();
1233 resolution.result = crate::ContextResolutionResult::Unresolved;
1234 let diagnostics = super::collect_context_diagnostics(&model);
1235 assert_catalog_shapes(&model, &diagnostics);
1236 let diagnostic = diagnostics
1237 .iter()
1238 .find(|item| item.code == "PSC1057")
1239 .unwrap();
1240 assert_eq!(diagnostic.severity, ComponentDiagnosticSeverity::Error);
1241 assert_eq!(diagnostic.consumer_id, Some(consumer));
1242 assert!(diagnostic.context_id.is_some());
1243 assert!(!diagnostics.iter().any(|item| matches!(
1244 item.code.as_str(),
1245 "PSC1062" | "PSC1065" | "PSC1066" | "PSC1067"
1246 )));
1247 }
1248
1249 #[test]
1250 fn psc1058_emits_one_ambiguity_diagnostic_with_sorted_provider_labels() {
1251 let mut model = build_application_semantic_model(&presolve_parser::parse_file(
1252 "src/g4-ambiguous.tsx",
1253 r#"
1254@component("x-app") class App extends Component {
1255 @context() theme: string;
1256 @provide(App.theme) first: string = "a";
1257 @consume(App.theme) selected!: string;
1258 render() { return <main />; }
1259}"#,
1260 ));
1261 let consumer = model.consumers()[0].id.clone();
1262 let first = model.providers()[0].id.clone();
1263 let mut second_entity = model.providers()[0].clone();
1264 let second = crate::ProviderId::for_component(&model.components[0].id, "second");
1265 second_entity.id = second.clone();
1266 second_entity.provenance.span.start += 1;
1267 second_entity.provenance.span.end += 1;
1268 model.provenance.insert(
1269 second.as_semantic_id().clone(),
1270 second_entity.provenance.clone(),
1271 );
1272 model.providers.insert(second.clone(), second_entity);
1273 model.context_resolutions.get_mut(&consumer).unwrap().result =
1274 crate::ContextResolutionResult::Ambiguous {
1275 providers: vec![second, first],
1276 distance: 0,
1277 };
1278 let diagnostics = super::collect_context_diagnostics(&model);
1279 assert_catalog_shapes(&model, &diagnostics);
1280 let matches = diagnostics
1281 .iter()
1282 .filter(|item| item.code == "PSC1058")
1283 .collect::<Vec<_>>();
1284 assert_eq!(matches.len(), 1);
1285 assert_eq!(matches[0].consumer_id, Some(consumer));
1286 assert!(matches[0].provider_id.is_none());
1287 assert_eq!(matches[0].secondary_labels.len(), 2);
1288 assert!(!diagnostics.iter().any(|item| matches!(
1289 item.code.as_str(),
1290 "PSC1062" | "PSC1065" | "PSC1066" | "PSC1067"
1291 )));
1292 let mut validation_model = model.clone();
1293 validation_model.diagnostics.clone_from(&diagnostics);
1294 assert!(
1295 !crate::validate_application_semantic_model(&validation_model)
1296 .iter()
1297 .any(|item| item.code == "PSASM1134")
1298 );
1299 validation_model
1300 .diagnostics
1301 .iter_mut()
1302 .find(|item| item.code == "PSC1058")
1303 .unwrap()
1304 .secondary_labels
1305 .reverse();
1306 assert!(
1307 crate::validate_application_semantic_model(&validation_model)
1308 .iter()
1309 .any(|item| item.code == "PSASM1134")
1310 );
1311 }
1312
1313 #[test]
1314 fn psc1063_serialization_suppresses_generic_planning_codes() {
1315 let mut model = build_application_semantic_model(&presolve_parser::parse_file(
1316 "src/g5-serialization.tsx",
1317 r#"@component("x-app") class App extends Component { @context() theme: string; render() { return <main />; } }"#,
1318 ));
1319 let context = model.contexts()[0].id.clone();
1320 model.context_types.get_mut(&context).unwrap().serialization =
1321 crate::ContextSerializationCompatibility::NonSerializable;
1322 let diagnostics = super::collect_context_diagnostics(&model);
1323 assert_catalog_shapes(&model, &diagnostics);
1324 assert_eq!(
1325 diagnostics
1326 .iter()
1327 .find(|item| item.code == "PSC1063")
1328 .unwrap()
1329 .context_id,
1330 Some(context)
1331 );
1332 assert!(!diagnostics
1333 .iter()
1334 .any(|item| matches!(item.code.as_str(), "PSC1066" | "PSC1067")));
1335 }
1336
1337 #[test]
1338 fn psc1064_boundary_suppresses_generic_planning_codes() {
1339 let mut model = build_application_semantic_model(&presolve_parser::parse_file(
1340 "src/g5-boundary.tsx",
1341 r#"@component("x-app") class App extends Component { @context() theme: string; render() { return <main />; } }"#,
1342 ));
1343 let context = model.contexts()[0].id.clone();
1344 model
1345 .context_types
1346 .get_mut(&context)
1347 .unwrap()
1348 .boundary_compatibility = crate::CompatibilityStatus::Incompatible;
1349 let diagnostics = super::collect_context_diagnostics(&model);
1350 assert_catalog_shapes(&model, &diagnostics);
1351 assert_eq!(
1352 diagnostics
1353 .iter()
1354 .find(|item| item.code == "PSC1064")
1355 .unwrap()
1356 .context_id,
1357 Some(context)
1358 );
1359 assert!(!diagnostics
1360 .iter()
1361 .any(|item| matches!(item.code.as_str(), "PSC1066" | "PSC1067")));
1362 }
1363
1364 fn planned_provider_model() -> crate::ApplicationSemanticModel {
1365 build_application_semantic_model(&presolve_parser::parse_file(
1366 "src/g9.tsx",
1367 r#"
1368@component("x-app") class App extends Component {
1369 @context() theme: string;
1370 @provide(App.theme) provided: string = "x";
1371 @consume(App.theme) selected!: string;
1372 render() { return <main />; }
1373}"#,
1374 ))
1375 }
1376
1377 #[test]
1378 fn psc1065_projects_explicit_g8_binding_lifetime_failure() {
1379 let mut model = planned_provider_model();
1380 let consumer = model.consumers()[0].id.clone();
1381 model
1382 .context_lifetime
1383 .binding_lifetimes
1384 .get_mut(&consumer)
1385 .unwrap()
1386 .compatibility = crate::ContextBindingLifetimeStatus::Incompatible;
1387 let diagnostics = super::collect_context_diagnostics(&model);
1388 assert_catalog_shapes(&model, &diagnostics);
1389 let diagnostic = diagnostics
1390 .iter()
1391 .find(|item| item.code == "PSC1065")
1392 .unwrap();
1393 assert_eq!(diagnostic.consumer_id, Some(consumer));
1394 assert!(diagnostic.context_id.is_some());
1395 assert!(!diagnostics
1396 .iter()
1397 .any(|item| matches!(item.code.as_str(), "PSC1066" | "PSC1067")));
1398 }
1399
1400 #[test]
1401 fn psc1066_unavailable_dependency_does_not_imply_psc1067() {
1402 let mut model = planned_provider_model();
1403 let entry = model
1404 .context_evaluation
1405 .source_entries
1406 .values_mut()
1407 .next()
1408 .unwrap();
1409 entry.status = crate::ContextSourcePlanStatus::BlockedDependency;
1410 entry.reasons = vec![
1411 crate::ContextSourceBlockReason::UnavailableComputedDependency(
1412 model.components[0].id.computed("missing"),
1413 ),
1414 ];
1415 let diagnostics = super::collect_context_diagnostics(&model);
1416 assert_catalog_shapes(&model, &diagnostics);
1417 let diagnostic = diagnostics
1418 .iter()
1419 .find(|item| item.code == "PSC1066")
1420 .unwrap();
1421 assert!(diagnostic.context_id.is_some() && diagnostic.provider_id.is_some());
1422 assert!(!diagnostics.iter().any(|item| item.code == "PSC1067"));
1423 }
1424
1425 #[test]
1426 fn psc1067_requires_explicit_retained_unsupported_expression_reason() {
1427 let mut model = planned_provider_model();
1428 let entry = model
1429 .context_evaluation
1430 .source_entries
1431 .values_mut()
1432 .next()
1433 .unwrap();
1434 entry.status = crate::ContextSourcePlanStatus::BlockedUnsupportedExpression;
1435 entry.reasons = vec![crate::ContextSourceBlockReason::UnsupportedExpression];
1436 let diagnostics = super::collect_context_diagnostics(&model);
1437 assert_catalog_shapes(&model, &diagnostics);
1438 let diagnostic = diagnostics
1439 .iter()
1440 .find(|item| item.code == "PSC1067")
1441 .unwrap();
1442 assert!(diagnostic.context_id.is_some() && diagnostic.provider_id.is_some());
1443 let mut without_reason = model.clone();
1444 without_reason
1445 .context_evaluation
1446 .source_entries
1447 .values_mut()
1448 .next()
1449 .unwrap()
1450 .reasons
1451 .clear();
1452 assert!(!super::collect_context_diagnostics(&without_reason)
1453 .iter()
1454 .any(|item| item.code == "PSC1067"));
1455 }
1456
1457 #[test]
1458 fn unused_context_source_emits_no_diagnostic() {
1459 let model = build_application_semantic_model(&presolve_parser::parse_file(
1460 "src/unused.tsx",
1461 r#"
1462@component("x-app") class App extends Component { @context() theme: string; @provide(App.theme) provided: string = "x"; render() { return <main />; } }
1463"#,
1464 ));
1465 assert!(!super::collect_context_diagnostics(&model)
1466 .iter()
1467 .any(|item| ("PSC1052"..="PSC1067").contains(&item.code.as_str())));
1468 }
1469
1470 #[test]
1471 fn validation_rejects_unknown_candidate_and_fabricated_semantic_identity() {
1472 let mut model = build_application_semantic_model(&presolve_parser::parse_file(
1473 "src/invalid-validation.tsx",
1474 r#"
1475@component("x-app") class App extends Component { @context("bad") theme: string; render() { return <main />; } }
1476"#,
1477 ));
1478 let diagnostic = model
1479 .diagnostics
1480 .iter_mut()
1481 .find(|item| item.code == "PSC1052")
1482 .unwrap();
1483 diagnostic.context_declaration_candidate_id = Some(
1484 crate::ContextDeclarationCandidateId::for_component_position(
1485 &model.components[0].id,
1486 usize::MAX,
1487 ),
1488 );
1489 diagnostic.context_id = Some(crate::ContextId::for_component(
1490 &model.components[0].id,
1491 "fabricated",
1492 ));
1493 let validation = crate::validate_application_semantic_model(&model);
1494 assert!(validation.iter().any(|item| item.code == "PSASM1135"));
1495 assert!(validation.iter().any(|item| item.code == "PSASM1136"));
1496 }
1497
1498 #[test]
1499 fn validation_rejects_wrong_provider_and_consumer_context_relationships() {
1500 let mut model = build_application_semantic_model(&presolve_parser::parse_file(
1501 "src/wrong-context.tsx",
1502 r#"
1503@component("x-app") class App extends Component {
1504 @context() first: string; @context() second: string;
1505 @provide(App.first) provided: boolean = true;
1506 @consume(App.first) selected!: number;
1507 render() { return <main />; }
1508}"#,
1509 ));
1510 let wrong = model
1511 .contexts()
1512 .iter()
1513 .find(|context| context.name == "second")
1514 .unwrap()
1515 .id
1516 .clone();
1517 model
1518 .diagnostics
1519 .iter_mut()
1520 .filter(|item| matches!(item.code.as_str(), "PSC1060" | "PSC1062"))
1521 .for_each(|item| item.context_id = Some(wrong.clone()));
1522 let validation = crate::validate_application_semantic_model(&model);
1523 assert!(validation.iter().any(|item| item.code == "PSASM1138"));
1524 assert!(validation.iter().any(|item| item.code == "PSASM1139"));
1525 }
1526
1527 #[test]
1528 fn validation_rejects_unsorted_duplicate_and_primary_repeating_labels() {
1529 let mut model = build_application_semantic_model(&presolve_parser::parse_file(
1530 "src/label-validation.tsx",
1531 r#"
1532@component("x-app") class App extends Component { @context() theme: string; @provide(App.theme) provided: boolean = true; render() { return <main />; } }
1533"#,
1534 ));
1535 let diagnostic = model
1536 .diagnostics
1537 .iter_mut()
1538 .find(|item| item.code == "PSC1060")
1539 .unwrap();
1540 let primary = diagnostic.provenance.clone().unwrap();
1541 diagnostic.secondary_labels = vec![
1542 crate::DiagnosticSecondaryLabel {
1543 provenance: primary.clone(),
1544 message: "z".to_string(),
1545 },
1546 crate::DiagnosticSecondaryLabel {
1547 provenance: primary,
1548 message: "z".to_string(),
1549 },
1550 ];
1551 let validation = crate::validate_application_semantic_model(&model);
1552 assert!(validation.iter().any(|item| item.code == "PSASM1133"));
1553 assert!(validation.iter().any(|item| item.code == "PSASM1137"));
1554 }
1555
1556 #[test]
1557 fn validation_rejects_noncanonical_context_primary_provenance() {
1558 let mut model = build_application_semantic_model(&presolve_parser::parse_file(
1559 "src/primary-validation.tsx",
1560 r#"
1561@component("x-app") class App extends Component { @context("bad") theme: string; render() { return <main />; } }
1562"#,
1563 ));
1564 let diagnostic = model
1565 .diagnostics
1566 .iter_mut()
1567 .find(|item| item.code == "PSC1052")
1568 .unwrap();
1569 diagnostic.provenance.as_mut().unwrap().span.start += 1;
1570 let validation = crate::validate_application_semantic_model(&model);
1571 assert!(validation.iter().any(|item| item.code == "PSASM1140"));
1572 }
1573
1574 #[test]
1575 fn validation_rejects_noncanonical_context_secondary_label_provenance() {
1576 let mut model = build_application_semantic_model(&presolve_parser::parse_file(
1577 "src/secondary-validation.tsx",
1578 r#"
1579@component("x-app") class App extends Component { @context() theme: string; @provide(App.theme) provided: boolean = true; render() { return <main />; } }
1580"#,
1581 ));
1582 let diagnostic = model
1583 .diagnostics
1584 .iter_mut()
1585 .find(|item| item.code == "PSC1060")
1586 .unwrap();
1587 diagnostic.secondary_labels[0].provenance.span.start += 1;
1588 let validation = crate::validate_application_semantic_model(&model);
1589 assert!(validation.iter().any(|item| item.code == "PSASM1134"));
1590 }
1591}