Skip to main content

presolve_compiler/
component_diagnostics.rs

1use std::collections::BTreeSet;
2
3use crate::component_graph::MethodSemanticRole;
4use crate::{
5    ApplicationSemanticModel, AuthoredDeclarationKind, ComponentDiagnostic,
6    ComponentDiagnosticSeverity, ComponentInvocationResolutionStatus, CompositionCompatibility,
7    DiagnosticSecondaryLabel, InstanceContextResolutionStatus, SemanticId, SlotBindingStatus,
8    SlotContentFragmentViolation, SlotDeclarationViolation, SlotOutletViolation, SourceProvenance,
9};
10
11/// Executable H19 contract metadata. Tests use this table to prove that the
12/// reserved catalog stays complete and that every code has an explicit
13/// authority, evidence, identity, deduplication, suppression, and projection
14/// contract.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct ComponentDiagnosticContract {
17    pub code: &'static str,
18    pub message: &'static str,
19    pub authority: &'static str,
20    pub primary_role: &'static str,
21    pub identities: &'static str,
22    pub secondary_roles: &'static str,
23    pub deduplication: &'static str,
24    pub suppression: &'static str,
25    pub projection: &'static str,
26}
27
28pub const COMPONENT_DIAGNOSTIC_CONTRACTS: [ComponentDiagnosticContract; 16] = [
29    contract((
30        "PSC1068",
31        "Invalid slot declaration.",
32        "H1 slot declaration candidates",
33        "invalid authored slot declaration",
34        "component_id",
35        "related slot declarations",
36        "candidate identity",
37        "none",
38        "shared diagnostic envelope",
39    )),
40    contract((
41        "PSC1069",
42        "Invalid component invocation.",
43        "H2 invocation resolution",
44        "component invocation",
45        "component_id, invocation_id",
46        "candidate definitions",
47        "invocation_id",
48        "inheritance",
49        "shared diagnostic envelope",
50    )),
51    contract((
52        "PSC1070",
53        "Unresolved component symbol.",
54        "H2 invocation resolution",
55        "component invocation symbol",
56        "component_id, invocation_id",
57        "none",
58        "invocation_id",
59        "inheritance",
60        "shared diagnostic envelope",
61    )),
62    contract((
63        "PSC1071",
64        "Component composition cycle.",
65        "H9 composition analysis",
66        "cycle invocation",
67        "component_id, invocation_id",
68        "cycle edges",
69        "cycle invocation_id",
70        "inheritance and unresolved invocation",
71        "shared diagnostic envelope",
72    )),
73    contract((
74        "PSC1072",
75        "Component inheritance is unsupported.",
76        "H1 normalized component heritage",
77        "component base class",
78        "component_id",
79        "none",
80        "component_id and base",
81        "none",
82        "shared diagnostic envelope",
83    )),
84    contract((
85        "PSC1073",
86        "Inherited semantic declaration is unsupported.",
87        "H1 normalized heritage and semantic declarations",
88        "component base class",
89        "component_id",
90        "inherited semantic declarations",
91        "component_id and inherited declaration",
92        "generic inheritance",
93        "shared diagnostic envelope",
94    )),
95    contract((
96        "PSC1074",
97        "Unknown slot.",
98        "H3 slot fragment/outlet registry",
99        "unknown supplied slot",
100        "component_id, invocation_id",
101        "none",
102        "fragment identity",
103        "invocation",
104        "shared diagnostic envelope",
105    )),
106    contract((
107        "PSC1075",
108        "Duplicate slot content.",
109        "H3 slot fragment registry",
110        "duplicate supplied content",
111        "component_id, invocation_id, slot_id",
112        "duplicate fragments",
113        "fragment identity",
114        "invocation",
115        "shared diagnostic envelope",
116    )),
117    contract((
118        "PSC1076",
119        "Duplicate slot outlet.",
120        "H3 slot outlet registry",
121        "duplicate callee outlet",
122        "component_id, slot_id",
123        "other outlets",
124        "slot_id and outlet provenance",
125        "inheritance",
126        "shared diagnostic envelope",
127    )),
128    contract((
129        "PSC1077",
130        "Missing slot outlet.",
131        "H7 slot binding registry",
132        "instance slot binding",
133        "component_id, invocation_id, component_instance_id, slot_binding_id, slot_id",
134        "slot declaration",
135        "slot_binding_id",
136        "invocation and invalid slot",
137        "shared diagnostic envelope",
138    )),
139    contract((
140        "PSC1078",
141        "Invalid slot content ownership.",
142        "H7 binding and H8 ownership typing",
143        "instance slot binding",
144        "component_id, invocation_id, component_instance_id, slot_binding_id, slot_id",
145        "content and outlet owners",
146        "slot_binding_id",
147        "invocation and invalid slot",
148        "shared diagnostic envelope",
149    )),
150    contract((
151        "PSC1079",
152        "Slot type or boundary incompatibility.",
153        "H8 composition typing",
154        "instance slot binding",
155        "component_id, invocation_id, component_instance_id, slot_binding_id, slot_id",
156        "declared slot type",
157        "slot_binding_id",
158        "invocation and invalid slot",
159        "shared diagnostic envelope",
160    )),
161    contract((
162        "PSC1080",
163        "Component instance planning failure.",
164        "H4 blocked instance plan",
165        "blocked component instance",
166        "component_id, invocation_id, component_instance_id",
167        "parent instance",
168        "component_instance_id",
169        "invocation, cycle, and slot",
170        "shared diagnostic envelope",
171    )),
172    contract((
173        "PSC1081",
174        "Instance-aware Context binding unavailable.",
175        "H6 instance Context and H8 typing",
176        "consumer instance binding",
177        "component_id, component_instance_id, provider_instance_id, consumer_instance_id",
178        "candidate Provider instances",
179        "consumer_instance_id",
180        "component and slot failures",
181        "shared diagnostic envelope",
182    )),
183    contract((
184        "PSC1082",
185        "Structural region cannot be planned.",
186        "H4 blocked structural instance plan",
187        "structural region",
188        "component_id, invocation_id, component_instance_id, structural_region_id",
189        "parent instance",
190        "component_instance_id and structural_region_id",
191        "invocation, cycle, slot, and Context",
192        "shared diagnostic envelope",
193    )),
194    contract((
195        "PSC1083",
196        "Component/slot source cannot be lowered.",
197        "H10 initialization exclusions and H11 lowering",
198        "blocked component or slot source",
199        "component_id, invocation_id, component_instance_id, slot_binding_id",
200        "none",
201        "typed blocked source identity",
202        "all earlier subject failures",
203        "shared diagnostic envelope",
204    )),
205];
206
207const fn contract(
208    details: (
209        &'static str,
210        &'static str,
211        &'static str,
212        &'static str,
213        &'static str,
214        &'static str,
215        &'static str,
216        &'static str,
217        &'static str,
218    ),
219) -> ComponentDiagnosticContract {
220    let (
221        code,
222        message,
223        authority,
224        primary_role,
225        identities,
226        secondary_roles,
227        deduplication,
228        suppression,
229        projection,
230    ) = details;
231    ComponentDiagnosticContract {
232        code,
233        message,
234        authority,
235        primary_role,
236        identities,
237        secondary_roles,
238        deduplication,
239        suppression,
240        projection,
241    }
242}
243
244/// Project H19 diagnostics only from immutable H1-H17 products retained in the
245/// ASM. This function has no parser, source-text, CLI, or runtime dependency.
246#[must_use]
247pub fn collect_component_diagnostics(model: &ApplicationSemanticModel) -> Vec<ComponentDiagnostic> {
248    let mut diagnostics = Vec::new();
249    collect_declarations_and_inheritance(model, &mut diagnostics);
250    collect_invocations_and_cycles(model, &mut diagnostics);
251    collect_slot_composition(model, &mut diagnostics);
252    collect_slot_bindings(model, &mut diagnostics);
253    collect_instance_context(model, &mut diagnostics);
254    collect_planning_and_lowering(model, &mut diagnostics);
255    suppress_derivative_cascades(&mut diagnostics);
256    canonicalize(&mut diagnostics);
257    diagnostics
258}
259
260fn collect_declarations_and_inheritance(
261    model: &ApplicationSemanticModel,
262    diagnostics: &mut Vec<ComponentDiagnostic>,
263) {
264    for component in &model.components {
265        for candidate in &component.slot_declaration_candidates {
266            let Some(violation) = candidate.violations.first() else {
267                continue;
268            };
269            let primary = slot_declaration_primary(candidate, violation);
270            let mut diagnostic = diagnostic("PSC1068", primary, Some(component.id.clone()));
271            diagnostic.secondary_labels = component
272                .slot_declaration_candidates
273                .iter()
274                .filter(|other| {
275                    other.id != candidate.id && other.field_name == candidate.field_name
276                })
277                .map(|other| label(other.provenance.clone(), "Related Slot declaration."))
278                .collect();
279            diagnostics.push(diagnostic);
280        }
281
282        let Some(heritage) = &component.heritage else {
283            continue;
284        };
285        if heritage.base == "Component" {
286            continue;
287        }
288        let inherited = inherited_semantic_provenances(model, component, &heritage.base);
289        if inherited.is_empty() {
290            diagnostics.push(diagnostic(
291                "PSC1072",
292                heritage.provenance.clone(),
293                Some(component.id.clone()),
294            ));
295        } else {
296            for provenance in inherited {
297                let mut item = diagnostic(
298                    "PSC1073",
299                    heritage.provenance.clone(),
300                    Some(component.id.clone()),
301                );
302                item.secondary_labels
303                    .push(label(provenance, "Inherited semantic declaration is here."));
304                diagnostics.push(item);
305            }
306        }
307    }
308}
309
310fn inherited_semantic_provenances(
311    model: &ApplicationSemanticModel,
312    component: &crate::ComponentNode,
313    base: &str,
314) -> Vec<SourceProvenance> {
315    let component_path = model.provenance(&component.id).map(|item| &item.path);
316    let Some(base_component) = model.components.iter().find(|candidate| {
317        candidate.class_name == base
318            && model.provenance(&candidate.id).map(|item| &item.path) == component_path
319    }) else {
320        return Vec::new();
321    };
322    let mut result = Vec::new();
323    result.extend(
324        base_component
325            .state_fields
326            .iter()
327            .filter_map(|item| model.provenance(&item.id))
328            .cloned(),
329    );
330    result.extend(
331        base_component
332            .context_declaration_candidates
333            .iter()
334            .map(|item| item.provenance.clone()),
335    );
336    result.extend(
337        base_component
338            .slot_declaration_candidates
339            .iter()
340            .map(|item| item.provenance.clone()),
341    );
342    result.extend(
343        base_component
344            .methods
345            .iter()
346            .filter(|method| method.semantic_role != MethodSemanticRole::Standard)
347            .filter_map(|method| model.provenance(&method.id))
348            .cloned(),
349    );
350    result.sort_by(provenance_order);
351    result.dedup();
352    result
353}
354
355fn collect_invocations_and_cycles(
356    model: &ApplicationSemanticModel,
357    diagnostics: &mut Vec<ComponentDiagnostic>,
358) {
359    for invocation in model.component_invocations.values() {
360        let code = match invocation.status {
361            ComponentInvocationResolutionStatus::Resolved => continue,
362            ComponentInvocationResolutionStatus::UnresolvedSymbol => "PSC1070",
363            ComponentInvocationResolutionStatus::ResolvedNonComponent
364            | ComponentInvocationResolutionStatus::Ambiguous
365            | ComponentInvocationResolutionStatus::UnsupportedDynamicTarget => "PSC1069",
366        };
367        let mut item = diagnostic(
368            code,
369            invocation.provenance.clone(),
370            Some(invocation.owner_component.clone()),
371        );
372        item.invocation_id = Some(invocation.id.clone());
373        diagnostics.push(item);
374    }
375    for cycle in &model.component_composition.cycles {
376        for invocation_id in &cycle.invocations {
377            let Some(invocation) = model.component_invocations.get(invocation_id) else {
378                continue;
379            };
380            let mut item = diagnostic(
381                "PSC1071",
382                invocation.provenance.clone(),
383                Some(invocation.owner_component.clone()),
384            );
385            item.invocation_id = Some(invocation.id.clone());
386            item.secondary_labels = cycle
387                .invocations
388                .iter()
389                .filter(|other| *other != invocation_id)
390                .filter_map(|other| model.component_invocations.get(other))
391                .map(|other| label(other.provenance.clone(), "Composition cycle edge."))
392                .collect();
393            diagnostics.push(item);
394        }
395    }
396}
397
398fn collect_slot_composition(
399    model: &ApplicationSemanticModel,
400    diagnostics: &mut Vec<ComponentDiagnostic>,
401) {
402    for fragment in model.slot_content_fragments.values() {
403        let code = if fragment
404            .violations
405            .contains(&SlotContentFragmentViolation::DuplicateFragment)
406        {
407            Some("PSC1075")
408        } else if fragment.violations.iter().any(|item| {
409            matches!(
410                item,
411                SlotContentFragmentViolation::MissingSlotDeclaration
412                    | SlotContentFragmentViolation::UnsupportedDynamicSlotName
413                    | SlotContentFragmentViolation::InvalidNestedWrapper
414                    | SlotContentFragmentViolation::InvalidWrapperForm
415            )
416        }) {
417            Some("PSC1074")
418        } else {
419            None
420        };
421        let Some(code) = code else { continue };
422        let mut item = diagnostic(
423            code,
424            fragment.provenance.clone(),
425            Some(fragment.owner_component.clone()),
426        );
427        item.invocation_id = Some(fragment.invocation.clone());
428        item.slot_id.clone_from(&fragment.slot);
429        item.secondary_labels = fragment
430            .secondary_provenances
431            .iter()
432            .cloned()
433            .map(|provenance| label(provenance, "Related Slot content."))
434            .collect();
435        diagnostics.push(item);
436    }
437    let mut emitted_outlet_groups = BTreeSet::new();
438    for outlet in model.slot_outlets.values().filter(|outlet| {
439        outlet
440            .violations
441            .contains(&SlotOutletViolation::DuplicateOutlet)
442    }) {
443        let group = (
444            outlet.owner_component.clone(),
445            outlet.requested_slot_name.clone(),
446        );
447        if !emitted_outlet_groups.insert(group) {
448            continue;
449        }
450        let mut item = diagnostic(
451            "PSC1076",
452            outlet.provenance.clone(),
453            Some(outlet.owner_component.clone()),
454        );
455        item.slot_id.clone_from(&outlet.slot);
456        item.secondary_labels = model
457            .slot_outlets
458            .values()
459            .filter(|other| {
460                other.id != outlet.id
461                    && other.owner_component == outlet.owner_component
462                    && other.requested_slot_name == outlet.requested_slot_name
463            })
464            .map(|other| label(other.provenance.clone(), "Other Slot outlet."))
465            .collect();
466        diagnostics.push(item);
467    }
468}
469
470fn collect_slot_bindings(
471    model: &ApplicationSemanticModel,
472    diagnostics: &mut Vec<ComponentDiagnostic>,
473) {
474    for binding in model.slot_bindings.bindings.values() {
475        let code = match binding.status {
476            SlotBindingStatus::MissingOutlet => Some("PSC1077"),
477            SlotBindingStatus::InvalidOwnership => Some("PSC1078"),
478            _ => None,
479        };
480        if let Some(code) = code {
481            diagnostics.push(binding_diagnostic(model, binding, code));
482        }
483        if model
484            .composition_types
485            .slot_bindings
486            .get(&binding.id)
487            .is_some_and(|record| {
488                record.type_compatibility == CompositionCompatibility::Incompatible
489            })
490        {
491            diagnostics.push(binding_diagnostic(model, binding, "PSC1079"));
492        }
493    }
494}
495
496fn binding_diagnostic(
497    model: &ApplicationSemanticModel,
498    binding: &crate::SlotBinding,
499    code: &str,
500) -> ComponentDiagnostic {
501    let component = model
502        .component_instance_plan
503        .instances
504        .get(&binding.callee_instance)
505        .map(|item| item.component.clone());
506    let mut item = diagnostic(code, binding.provenance.clone(), component);
507    item.invocation_id = Some(binding.invocation.clone());
508    item.component_instance_id = Some(binding.callee_instance.clone());
509    item.slot_binding_id = Some(binding.id.clone());
510    item.slot_id.clone_from(&binding.slot);
511    if let Some(slot) = binding.slot.as_ref().and_then(|id| model.slot(id)) {
512        item.secondary_labels
513            .push(label(slot.provenance.clone(), "Slot declaration."));
514    }
515    if code == "PSC1078" {
516        if let Some(fragment) = binding
517            .content_fragment
518            .as_ref()
519            .and_then(|id| model.slot_content_fragments.get(id))
520        {
521            item.secondary_labels.push(label(
522                fragment.provenance.clone(),
523                "Caller-owned Slot content.",
524            ));
525        }
526        if let Some(outlet) = binding
527            .outlet
528            .as_ref()
529            .and_then(|id| model.slot_outlets.get(id))
530        {
531            item.secondary_labels.push(label(
532                outlet.provenance.clone(),
533                "Callee-owned Slot outlet.",
534            ));
535        }
536    }
537    item
538}
539
540fn collect_instance_context(
541    model: &ApplicationSemanticModel,
542    diagnostics: &mut Vec<ComponentDiagnostic>,
543) {
544    for resolution in model.instance_context.resolutions.values() {
545        let incompatible = model
546            .composition_types
547            .instance_context_bindings
548            .get(&resolution.consumer_instance)
549            .is_some_and(|record| record.overall != CompositionCompatibility::Compatible);
550        if !incompatible
551            && matches!(
552                resolution.status,
553                InstanceContextResolutionStatus::ProviderSelected
554                    | InstanceContextResolutionStatus::ContextDefaultSelected
555            )
556        {
557            continue;
558        }
559        let component = model
560            .instance_context
561            .consumer_instances
562            .get(&resolution.consumer_instance)
563            .map(|item| item.component.clone());
564        let mut item = diagnostic("PSC1081", resolution.provenance.clone(), component);
565        item.context_id.clone_from(&resolution.context);
566        item.provider_id = resolution
567            .provider_instance
568            .as_ref()
569            .map(|id| id.provider.clone());
570        item.consumer_id = Some(resolution.consumer_instance.consumer.clone());
571        item.component_instance_id = Some(resolution.consumer_instance.component_instance.clone());
572        item.provider_instance_id
573            .clone_from(&resolution.provider_instance);
574        item.consumer_instance_id = Some(resolution.consumer_instance.clone());
575        item.secondary_labels = resolution
576            .candidate_provider_instances
577            .iter()
578            .filter_map(|id| model.instance_context.provider_instances.get(id))
579            .map(|record| label(record.provenance.clone(), "Candidate Provider instance."))
580            .collect();
581        diagnostics.push(item);
582    }
583}
584
585fn collect_planning_and_lowering(
586    model: &ApplicationSemanticModel,
587    diagnostics: &mut Vec<ComponentDiagnostic>,
588) {
589    let blocked_ids = model
590        .component_initialization
591        .blocked_instances
592        .iter()
593        .collect::<BTreeSet<_>>();
594    for blocked in model.component_instance_plan.blocked.values() {
595        let code = if blocked.structural_region.is_some() {
596            "PSC1082"
597        } else {
598            "PSC1080"
599        };
600        let component = blocked.target_component.clone().or_else(|| {
601            model
602                .component_invocations
603                .get(&blocked.invocation)
604                .map(|item| item.owner_component.clone())
605        });
606        let mut item = diagnostic(code, blocked.provenance.clone(), component.clone());
607        item.invocation_id = Some(blocked.invocation.clone());
608        item.component_instance_id = Some(blocked.id.clone());
609        item.structural_region_id
610            .clone_from(&blocked.structural_region);
611        if let Some(parent) = model
612            .component_instance_plan
613            .instances
614            .get(&blocked.parent_instance)
615        {
616            item.secondary_labels
617                .push(label(parent.provenance.clone(), "Planned parent instance."));
618        }
619        diagnostics.push(item);
620        if blocked_ids.contains(&blocked.id) {
621            let mut lowering = diagnostic("PSC1083", blocked.provenance.clone(), component);
622            lowering.invocation_id = Some(blocked.invocation.clone());
623            lowering.component_instance_id = Some(blocked.id.clone());
624            lowering
625                .structural_region_id
626                .clone_from(&blocked.structural_region);
627            diagnostics.push(lowering);
628        }
629    }
630    for binding in model.slot_bindings.bindings.values().filter(|binding| {
631        !matches!(
632            binding.status,
633            SlotBindingStatus::Bound | SlotBindingStatus::Empty
634        )
635    }) {
636        let mut item = binding_diagnostic(model, binding, "PSC1083");
637        item.secondary_labels.clear();
638        diagnostics.push(item);
639    }
640}
641
642fn suppress_derivative_cascades(diagnostics: &mut Vec<ComponentDiagnostic>) {
643    let invalid_components = diagnostics
644        .iter()
645        .filter(|item| matches!(item.code.as_str(), "PSC1072" | "PSC1073"))
646        .filter_map(|item| item.component_id.clone())
647        .collect::<BTreeSet<_>>();
648    let unresolved_invocations = diagnostics
649        .iter()
650        .filter(|item| matches!(item.code.as_str(), "PSC1069" | "PSC1070"))
651        .filter_map(|item| item.invocation_id.clone())
652        .collect::<BTreeSet<_>>();
653    let cycle_invocations = diagnostics
654        .iter()
655        .filter(|item| item.code == "PSC1071")
656        .filter_map(|item| item.invocation_id.clone())
657        .collect::<BTreeSet<_>>();
658    let invalid_bindings = diagnostics
659        .iter()
660        .filter(|item| {
661            matches!(
662                item.code.as_str(),
663                "PSC1074" | "PSC1075" | "PSC1076" | "PSC1077" | "PSC1078" | "PSC1079"
664            )
665        })
666        .filter_map(|item| item.slot_binding_id.clone())
667        .collect::<BTreeSet<_>>();
668    let invalid_slot_invocations = diagnostics
669        .iter()
670        .filter(|item| matches!(item.code.as_str(), "PSC1074" | "PSC1075"))
671        .filter_map(|item| item.invocation_id.clone())
672        .collect::<BTreeSet<_>>();
673    let context_instances = diagnostics
674        .iter()
675        .filter(|item| item.code == "PSC1081")
676        .filter_map(|item| item.component_instance_id.clone())
677        .collect::<BTreeSet<_>>();
678    let planning_failures = diagnostics
679        .iter()
680        .filter(|item| matches!(item.code.as_str(), "PSC1080" | "PSC1082"))
681        .filter_map(|item| item.component_instance_id.clone())
682        .collect::<BTreeSet<_>>();
683    diagnostics.retain(|item| {
684        let rank = suppression_rank(&item.code);
685        let invalid_component = rank > 1
686            && item
687                .component_id
688                .as_ref()
689                .is_some_and(|id| invalid_components.contains(id));
690        let unresolved_invocation = rank > 2
691            && item
692                .invocation_id
693                .as_ref()
694                .is_some_and(|id| unresolved_invocations.contains(id));
695        let cyclic_invocation = rank > 3
696            && item
697                .invocation_id
698                .as_ref()
699                .is_some_and(|id| cycle_invocations.contains(id));
700        let invalid_binding = rank > 4
701            && item
702                .slot_binding_id
703                .as_ref()
704                .is_some_and(|id| invalid_bindings.contains(id));
705        let invalid_slot_invocation = rank > 4
706            && item
707                .invocation_id
708                .as_ref()
709                .is_some_and(|id| invalid_slot_invocations.contains(id));
710        let unavailable_context = rank > 5
711            && item
712                .component_instance_id
713                .as_ref()
714                .is_some_and(|id| context_instances.contains(id));
715        let failed_planning = item.code == "PSC1083"
716            && item
717                .component_instance_id
718                .as_ref()
719                .is_some_and(|id| planning_failures.contains(id));
720        !(invalid_component
721            || unresolved_invocation
722            || cyclic_invocation
723            || invalid_binding
724            || invalid_slot_invocation
725            || unavailable_context
726            || failed_planning)
727    });
728}
729
730const fn suppression_rank(code: &str) -> u8 {
731    match code.as_bytes() {
732        b"PSC1068" | b"PSC1072" | b"PSC1073" => 1,
733        b"PSC1069" | b"PSC1070" => 2,
734        b"PSC1071" => 3,
735        b"PSC1074" | b"PSC1075" | b"PSC1076" | b"PSC1077" | b"PSC1078" | b"PSC1079" => 4,
736        b"PSC1081" => 5,
737        _ => 6,
738    }
739}
740
741fn canonicalize(diagnostics: &mut Vec<ComponentDiagnostic>) {
742    for item in diagnostics.iter_mut() {
743        item.secondary_labels.sort_by_key(secondary_order);
744        item.secondary_labels.dedup();
745        if let Some(primary) = &item.provenance {
746            item.secondary_labels
747                .retain(|label| &label.provenance != primary);
748        }
749    }
750    diagnostics.sort_by(diagnostic_order);
751    diagnostics.dedup();
752}
753
754fn diagnostic_order(left: &ComponentDiagnostic, right: &ComponentDiagnostic) -> std::cmp::Ordering {
755    diagnostic_key(left).cmp(&diagnostic_key(right))
756}
757
758fn diagnostic_key(item: &ComponentDiagnostic) -> (String, String, usize, usize, String) {
759    let provenance = item.provenance.as_ref();
760    (
761        item.code.clone(),
762        provenance
763            .map(|item| item.path.display().to_string())
764            .unwrap_or_default(),
765        provenance.map_or(0, |item| item.span.start),
766        provenance.map_or(0, |item| item.span.end),
767        identity_key(item),
768    )
769}
770
771fn identity_key(item: &ComponentDiagnostic) -> String {
772    format!(
773        "{:?}|{:?}|{:?}|{:?}|{:?}|{:?}|{:?}|{:?}",
774        item.component_id,
775        item.slot_id,
776        item.invocation_id,
777        item.component_instance_id,
778        item.slot_binding_id,
779        item.structural_region_id,
780        item.provider_instance_id,
781        item.consumer_instance_id
782    )
783}
784
785fn secondary_order(label: &DiagnosticSecondaryLabel) -> (String, usize, usize, String) {
786    (
787        label.provenance.path.display().to_string(),
788        label.provenance.span.start,
789        label.provenance.span.end,
790        label.message.clone(),
791    )
792}
793
794fn provenance_order(left: &SourceProvenance, right: &SourceProvenance) -> std::cmp::Ordering {
795    (left.path.as_path(), left.span.start, left.span.end).cmp(&(
796        right.path.as_path(),
797        right.span.start,
798        right.span.end,
799    ))
800}
801
802fn slot_declaration_primary(
803    candidate: &crate::AuthoredSlotDeclarationCandidate,
804    violation: &SlotDeclarationViolation,
805) -> SourceProvenance {
806    match violation {
807        SlotDeclarationViolation::StaticDeclarationUnsupported => {
808            candidate.static_modifier_provenance.as_ref()
809        }
810        SlotDeclarationViolation::ForbiddenInitializer => candidate.initializer_provenance.as_ref(),
811        SlotDeclarationViolation::InvalidDeclarationKind {
812            actual:
813                AuthoredDeclarationKind::Method
814                | AuthoredDeclarationKind::Getter
815                | AuthoredDeclarationKind::Parameter,
816        } => Some(&candidate.provenance),
817        _ => Some(&candidate.decorator_provenance),
818    }
819    .unwrap_or(&candidate.provenance)
820    .clone()
821}
822
823fn diagnostic(
824    code: &str,
825    provenance: SourceProvenance,
826    component_id: Option<SemanticId>,
827) -> ComponentDiagnostic {
828    let message = COMPONENT_DIAGNOSTIC_CONTRACTS
829        .iter()
830        .find(|item| item.code == code)
831        .expect("H19 code has a contract")
832        .message;
833    ComponentDiagnostic {
834        code: code.to_string(),
835        severity: ComponentDiagnosticSeverity::Error,
836        message: message.to_string(),
837        provenance: Some(provenance),
838        effect_id: None,
839        statement_id: None,
840        context_declaration_candidate_id: None,
841        context_id: None,
842        provider_id: None,
843        consumer_id: None,
844        slot_id: None,
845        invocation_id: None,
846        component_instance_id: None,
847        slot_binding_id: None,
848        structural_region_id: None,
849        component_id,
850        provider_instance_id: None,
851        consumer_instance_id: None,
852        secondary_labels: Vec::new(),
853    }
854}
855
856fn label(provenance: SourceProvenance, message: &str) -> DiagnosticSecondaryLabel {
857    DiagnosticSecondaryLabel {
858        provenance,
859        message: message.to_string(),
860    }
861}
862
863#[cfg(test)]
864mod tests {
865    use std::collections::BTreeSet;
866
867    use super::{collect_component_diagnostics, COMPONENT_DIAGNOSTIC_CONTRACTS};
868    use crate::{
869        build_application_semantic_model, validate_application_semantic_model,
870        BlockedComponentInstancePlan, BlockedComponentInstanceReason, ComponentInstanceStatus,
871        ComponentInvocationId, ComponentInvocationResolutionStatus, CompositionCompatibility,
872        SemanticId, SlotBindingStatus,
873    };
874
875    #[test]
876    fn contract_table_covers_the_complete_reserved_catalog() {
877        let codes = COMPONENT_DIAGNOSTIC_CONTRACTS
878            .iter()
879            .map(|item| item.code)
880            .collect::<Vec<_>>();
881        assert_eq!(
882            codes,
883            (1068..=1083)
884                .map(|code| format!("PSC{code}"))
885                .collect::<Vec<_>>()
886        );
887        for item in COMPONENT_DIAGNOSTIC_CONTRACTS {
888            assert!(item.message.ends_with('.'));
889            assert!(!item.authority.is_empty());
890            assert!(!item.primary_role.is_empty());
891            assert!(!item.identities.is_empty());
892            assert!(!item.secondary_roles.is_empty());
893            assert!(!item.deduplication.is_empty());
894            assert!(!item.suppression.is_empty());
895            assert_eq!(item.projection, "shared diagnostic envelope");
896        }
897    }
898
899    #[test]
900    fn projects_declaration_invocation_cycle_slot_and_context_diagnostics_deterministically() {
901        let source = r#"
902class Base { @slot() inherited!: SlotContent; render() { return <div />; } }
903@component("x-card") class Card extends Component {
904  @slot("bad") bad!: SlotContent;
905  @slot() children!: SlotContent;
906  render() { return <article><slot /><slot /></article>; }
907}
908@component("x-cycle") class Cycle extends Component { render() { return <Cycle />; } }
909@component("x-page") class Page extends Base {
910  render() { return <main />; }
911}
912@component("x-app") class App extends Component {
913  render() { return <main><Missing /><Card><template slot="unknown"><b /></template></Card></main>; }
914}
915"#;
916        let model =
917            build_application_semantic_model(&presolve_parser::parse_file("src/H19.tsx", source));
918        let first = collect_component_diagnostics(&model);
919        let second = collect_component_diagnostics(&model);
920        assert_eq!(first, second);
921        let codes = first
922            .iter()
923            .map(|item| item.code.as_str())
924            .collect::<BTreeSet<_>>();
925        for code in [
926            "PSC1068", "PSC1070", "PSC1071", "PSC1073", "PSC1074", "PSC1076",
927        ] {
928            assert!(codes.contains(code), "missing {code}: {first:#?}");
929        }
930        assert!(first
931            .iter()
932            .all(|item| item.severity == crate::ComponentDiagnosticSeverity::Error));
933        let validation = validate_application_semantic_model(&model);
934        assert!(validation.is_empty(), "{validation:#?}");
935    }
936
937    #[test]
938    fn nearby_valid_component_composition_emits_no_h19_diagnostic() {
939        let model = build_application_semantic_model(&presolve_parser::parse_file(
940            "src/ValidH19.tsx",
941            r#"@component("x-card") class Card extends Component { @slot() children!: SlotContent; render() { return <article><slot /></article>; } } @component("x-page") class Page extends Component { render() { return <Card><p /></Card>; } }"#,
942        ));
943        assert!(collect_component_diagnostics(&model).is_empty());
944    }
945
946    #[test]
947    #[allow(clippy::too_many_lines)]
948    fn every_code_has_canonical_identity_provenance_label_order_and_deduplication() {
949        for contract in COMPONENT_DIAGNOSTIC_CONTRACTS {
950            let item = example_diagnostic(contract.code);
951            assert_eq!(item.code, contract.code);
952            assert_eq!(item.message, contract.message);
953            assert_eq!(item.severity, crate::ComponentDiagnosticSeverity::Error);
954            assert!(item.provenance.is_some(), "{} primary", contract.code);
955            assert_required_identities(&item, contract.identities);
956            let mut labels = item.secondary_labels.clone();
957            labels.sort_by(|left, right| {
958                super::secondary_order(left).cmp(&super::secondary_order(right))
959            });
960            labels.dedup();
961            assert_eq!(labels, item.secondary_labels, "{} labels", contract.code);
962            assert!(item
963                .secondary_labels
964                .iter()
965                .all(|label| Some(&label.provenance) != item.provenance.as_ref()));
966        }
967    }
968
969    #[test]
970    fn suppression_removes_only_derivative_planning_and_lowering_failures() {
971        let unresolved = model(
972            r#"@component("x-page") class Page extends Component { render() { return <main><Missing /></main>; } }"#,
973        );
974        let diagnostics = collect_component_diagnostics(&unresolved);
975        assert!(diagnostics.iter().any(|item| item.code == "PSC1070"));
976        assert!(!diagnostics
977            .iter()
978            .any(|item| matches!(item.code.as_str(), "PSC1080" | "PSC1082" | "PSC1083")));
979
980        let independent = model(
981            r#"@component("x-a") class A extends Component { @slot("bad") invalid!: SlotContent; render() { return <B />; } } @component("x-b") class B extends Component { render() { return <A />; } }"#,
982        );
983        let codes = collect_component_diagnostics(&independent)
984            .into_iter()
985            .map(|item| item.code)
986            .collect::<BTreeSet<_>>();
987        assert!(codes.contains("PSC1068"));
988        assert!(codes.contains("PSC1071"));
989    }
990
991    #[test]
992    fn validation_rejects_every_noncanonical_component_diagnostic_mutation() {
993        let mut unknown_identity = model(
994            r#"@component("x-page") class Page extends Component { render() { return <Missing />; } }"#,
995        );
996        let index = unknown_identity
997            .diagnostics
998            .iter()
999            .position(|item| item.code == "PSC1070")
1000            .expect("component diagnostic");
1001        unknown_identity.diagnostics[index].component_id =
1002            Some(SemanticId::component(Some("x-fabricated"), "Fabricated"));
1003        unknown_identity.diagnostics[index].invocation_id =
1004            Some(ComponentInvocationId::for_template_entity(
1005                &SemanticId::component(Some("x-fabricated"), "Fabricated"),
1006                "missing",
1007            ));
1008        assert!(validate_application_semantic_model(&unknown_identity)
1009            .iter()
1010            .any(|item| item.code == "PSASM1201"));
1011
1012        let mut cross_owner = model(
1013            r#"@component("x-a") class A extends Component { render() { return <Missing />; } } @component("x-b") class B extends Component { render() { return <main />; } }"#,
1014        );
1015        let other = cross_owner
1016            .components
1017            .iter()
1018            .find(|item| item.element_name.as_deref() == Some("x-b"))
1019            .unwrap()
1020            .id
1021            .clone();
1022        let index = cross_owner
1023            .diagnostics
1024            .iter()
1025            .position(|item| item.code == "PSC1070")
1026            .unwrap();
1027        cross_owner.diagnostics[index].component_id = Some(other);
1028        assert!(validate_application_semantic_model(&cross_owner)
1029            .iter()
1030            .any(|item| item.code == "PSASM1201"));
1031
1032        let mut bad_provenance = model(
1033            r#"@component("x-page") class Page extends Component { render() { return <Missing />; } }"#,
1034        );
1035        let index = bad_provenance
1036            .diagnostics
1037            .iter()
1038            .position(|item| item.code == "PSC1070")
1039            .unwrap();
1040        bad_provenance.diagnostics[index]
1041            .provenance
1042            .as_mut()
1043            .unwrap()
1044            .span
1045            .start += 1;
1046        assert!(validate_application_semantic_model(&bad_provenance)
1047            .iter()
1048            .any(|item| item.code == "PSASM1201"));
1049
1050        let mut bad_labels = model(
1051            r#"@component("x-card") class Card extends Component { @slot() children!: SlotContent; render() { return <main><slot /><slot /></main>; } }"#,
1052        );
1053        let index = bad_labels
1054            .diagnostics
1055            .iter()
1056            .position(|item| item.code == "PSC1076")
1057            .unwrap();
1058        let duplicate = bad_labels.diagnostics[index].secondary_labels[0].clone();
1059        bad_labels.diagnostics[index]
1060            .secondary_labels
1061            .push(duplicate);
1062        assert!(validate_application_semantic_model(&bad_labels)
1063            .iter()
1064            .any(|item| item.code == "PSASM1201"));
1065
1066        let mut primary_repeated = model(
1067            r#"@component("x-card") class Card extends Component { @slot() children!: SlotContent; render() { return <main><slot /><slot /></main>; } }"#,
1068        );
1069        let index = primary_repeated
1070            .diagnostics
1071            .iter()
1072            .position(|item| item.code == "PSC1076")
1073            .unwrap();
1074        primary_repeated.diagnostics[index].secondary_labels[0].provenance = primary_repeated
1075            .diagnostics[index]
1076            .provenance
1077            .clone()
1078            .unwrap();
1079        assert!(validate_application_semantic_model(&primary_repeated)
1080            .iter()
1081            .any(|item| item.code == "PSASM1201"));
1082
1083        let mut unsupported = model(
1084            r#"@component("x-page") class Page extends Component { render() { return <Missing />; } }"#,
1085        );
1086        let mut fabricated = unsupported
1087            .diagnostics
1088            .iter()
1089            .find(|item| item.code == "PSC1070")
1090            .unwrap()
1091            .clone();
1092        fabricated.code = "PSC1083".to_string();
1093        fabricated.message = "Component/slot source cannot be lowered.".to_string();
1094        unsupported.diagnostics.push(fabricated);
1095        assert!(validate_application_semantic_model(&unsupported)
1096            .iter()
1097            .any(|item| item.code == "PSASM1201"));
1098    }
1099
1100    fn example_model(code: &str) -> crate::ApplicationSemanticModel {
1101        match code {
1102            "PSC1068" => model(
1103                r#"@component("x-page") class Page extends Component { @slot("bad") value!: SlotContent; render() { return <main />; } }"#,
1104            ),
1105            "PSC1069" => model(
1106                r#"@component("x-page") class Page extends Component { render() { return <Registry.Card />; } }"#,
1107            ),
1108            "PSC1070" => model(
1109                r#"@component("x-page") class Page extends Component { render() { return <Missing />; } }"#,
1110            ),
1111            "PSC1071" => model(
1112                r#"@component("x-a") class A extends Component { render() { return <B />; } } @component("x-b") class B extends Component { render() { return <A />; } }"#,
1113            ),
1114            "PSC1072" => model(
1115                r#"class Base { render() { return <div />; } } @component("x-page") class Page extends Base { render() { return <main />; } }"#,
1116            ),
1117            "PSC1073" => model(
1118                r#"class Base { value = state(0); render() { return <div />; } } @component("x-page") class Page extends Base { render() { return <main />; } }"#,
1119            ),
1120            "PSC1074" => slot_model(r#"<Card><template slot="missing"><b /></template></Card>"#),
1121            "PSC1075" => slot_model(
1122                r#"<Card><template slot="header"><b /></template><template slot="header"><i /></template></Card>"#,
1123            ),
1124            "PSC1076" => model(
1125                r#"@component("x-card") class Card extends Component { @slot() children!: SlotContent; render() { return <article><slot /><slot /></article>; } }"#,
1126            ),
1127            "PSC1077" => slot_model(r#"<Card><template slot="header"><b /></template></Card>"#),
1128            "PSC1078" | "PSC1079" | "PSC1083" => {
1129                slot_model(r#"<Card><template slot="header"><b /></template></Card>"#)
1130            }
1131            "PSC1080" => model(
1132                r#"@component("x-page") class Page extends Component { render() { return <Missing />; } } @component("x-card") class Card extends Component { render() { return <div />; } }"#,
1133            ),
1134            "PSC1081" => model(
1135                r#"@component("x-theme") class Theme extends Component { @context() color!: string; render() { return <div />; } } @component("x-leaf") class Leaf extends Component { @consume(Theme.color) color!: number; render() { return <span />; } } @component("x-card") class Card extends Component { @provide(Theme.color) color: string = "blue"; render() { return <Leaf />; } } @component("x-page") class Page extends Component { render() { return <Card />; } }"#,
1136            ),
1137            "PSC1082" => model(
1138                r#"@component("x-card") class Card extends Component { render() { return <div />; } } @component("x-page") class Page extends Component { visible = state(true); render() { return <main>{this.visible ? <Card /> : <span />}</main>; } }"#,
1139            ),
1140            _ => unreachable!(),
1141        }
1142    }
1143
1144    fn example_diagnostic(code: &str) -> crate::ComponentDiagnostic {
1145        let mut model = example_model(code);
1146
1147        match code {
1148            "PSC1078" => {
1149                model
1150                    .slot_bindings
1151                    .bindings
1152                    .values_mut()
1153                    .next()
1154                    .unwrap()
1155                    .status = SlotBindingStatus::InvalidOwnership;
1156            }
1157            "PSC1079" => {
1158                let record = model
1159                    .composition_types
1160                    .slot_bindings
1161                    .values_mut()
1162                    .next()
1163                    .unwrap();
1164                record.type_compatibility = CompositionCompatibility::Incompatible;
1165                record.overall = CompositionCompatibility::Incompatible;
1166            }
1167            "PSC1080" => {
1168                let target = model
1169                    .components
1170                    .iter()
1171                    .find(|item| item.element_name.as_deref() == Some("x-card"))
1172                    .unwrap()
1173                    .id
1174                    .clone();
1175                let invocation_id = model
1176                    .component_instance_plan
1177                    .blocked
1178                    .values()
1179                    .next()
1180                    .unwrap()
1181                    .invocation
1182                    .clone();
1183                let invocation = model.component_invocations.get_mut(&invocation_id).unwrap();
1184                invocation.status = ComponentInvocationResolutionStatus::Resolved;
1185                invocation.target_component = Some(target.clone());
1186                model
1187                    .component_instance_plan
1188                    .blocked
1189                    .values_mut()
1190                    .next()
1191                    .unwrap()
1192                    .target_component = Some(target);
1193            }
1194            "PSC1082" => {
1195                let id = model
1196                    .component_instance_plan
1197                    .instances
1198                    .values()
1199                    .find(|item| item.status == ComponentInstanceStatus::StructuralTemplate)
1200                    .unwrap()
1201                    .id
1202                    .clone();
1203                let instance = model.component_instance_plan.instances.remove(&id).unwrap();
1204                model.component_instance_plan.blocked.insert(
1205                    id.clone(),
1206                    BlockedComponentInstancePlan {
1207                        id,
1208                        invocation: instance.invocation.unwrap(),
1209                        parent_instance: instance.parent_instance.unwrap(),
1210                        owner_root: instance.owner_root,
1211                        target_component: Some(instance.component),
1212                        structural_region: instance.structural_region,
1213                        depth: instance.depth,
1214                        reason: BlockedComponentInstanceReason::InvalidParentPlan,
1215                        provenance: instance.provenance,
1216                    },
1217                );
1218            }
1219            "PSC1083" => {
1220                model
1221                    .slot_bindings
1222                    .bindings
1223                    .values_mut()
1224                    .next()
1225                    .unwrap()
1226                    .status = SlotBindingStatus::BlockedInvocation;
1227            }
1228            _ => {}
1229        }
1230        collect_component_diagnostics(&model)
1231            .into_iter()
1232            .find(|item| item.code == code)
1233            .unwrap_or_else(|| panic!("missing {code}"))
1234    }
1235
1236    fn model(source: &str) -> crate::ApplicationSemanticModel {
1237        build_application_semantic_model(&presolve_parser::parse_file(
1238            "src/DiagnosticMatrix.tsx",
1239            source,
1240        ))
1241    }
1242
1243    fn slot_model(invocation: &str) -> crate::ApplicationSemanticModel {
1244        model(&format!(
1245            r#"@component("x-card") class Card extends Component {{ @slot() children!: SlotContent; @slot() header!: SlotContent; render() {{ return <article><slot /></article>; }} }} @component("x-page") class Page extends Component {{ render() {{ return <main>{invocation}</main>; }} }}"#
1246        ))
1247    }
1248
1249    fn assert_required_identities(item: &crate::ComponentDiagnostic, identities: &str) {
1250        if identities.contains("component_id") {
1251            assert!(item.component_id.is_some(), "{} component", item.code);
1252        }
1253        if identities.contains("slot_id") {
1254            assert!(item.slot_id.is_some(), "{} slot", item.code);
1255        }
1256        if identities.contains("invocation_id") {
1257            assert!(item.invocation_id.is_some(), "{} invocation", item.code);
1258        }
1259        if identities.contains("component_instance_id") {
1260            assert!(
1261                item.component_instance_id.is_some(),
1262                "{} instance",
1263                item.code
1264            );
1265        }
1266        if identities.contains("slot_binding_id") {
1267            assert!(item.slot_binding_id.is_some(), "{} binding", item.code);
1268        }
1269        if identities.contains("structural_region_id") {
1270            assert!(item.structural_region_id.is_some(), "{} region", item.code);
1271        }
1272        if identities.contains("provider_instance_id") {
1273            assert!(
1274                item.provider_instance_id.is_some(),
1275                "{} provider instance",
1276                item.code
1277            );
1278        }
1279        if identities.contains("consumer_instance_id") {
1280            assert!(
1281                item.consumer_instance_id.is_some(),
1282                "{} consumer instance",
1283                item.code
1284            );
1285        }
1286    }
1287}