Skip to main content

presolve_compiler/
context_diagnostics.rs

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/// Projects only retained Context declaration candidates into the frozen G18
13/// catalog.  This module deliberately has no parser or source-text dependency.
14#[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}