Skip to main content

omena_query/style/
source_refs.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::path::{Component, PathBuf};
3
4use super::dynamic_classname::{
5    OMENA_QUERY_WORKSPACE_DYNAMIC_CLASSNAME_CONTEXT_DEPTH,
6    harvest_omena_query_dynamic_classname_m_tier_diagnostics,
7};
8use super::*;
9
10pub enum OmenaWorkspaceMonikerInput<'a> {
11    CssModuleSelector {
12        target_style_uri: Option<&'a str>,
13        selector_name: &'a str,
14    },
15    CssCustomProperty {
16        workspace_folder_uri: Option<&'a str>,
17        name: &'a str,
18    },
19    SassSymbol {
20        definition_uri: &'a str,
21        family: &'a str,
22        name: &'a str,
23    },
24    SassUnresolvedSymbol {
25        workspace_folder_uri: Option<&'a str>,
26        family: &'a str,
27        namespace: Option<&'a str>,
28        name: &'a str,
29    },
30}
31
32pub fn omena_workspace_moniker(input: OmenaWorkspaceMonikerInput<'_>) -> String {
33    match input {
34        OmenaWorkspaceMonikerInput::CssModuleSelector {
35            target_style_uri,
36            selector_name,
37        } => {
38            let target = target_style_uri.unwrap_or("*");
39            format!("css-module-selector:{target}#.{selector_name}")
40        }
41        OmenaWorkspaceMonikerInput::CssCustomProperty {
42            workspace_folder_uri,
43            name,
44        } => {
45            let scope = workspace_folder_uri.unwrap_or("global");
46            format!("css-custom-property:{scope}#{name}")
47        }
48        OmenaWorkspaceMonikerInput::SassSymbol {
49            definition_uri,
50            family,
51            name,
52        } => format!("sass-symbol:{definition_uri}#{family}:{name}"),
53        OmenaWorkspaceMonikerInput::SassUnresolvedSymbol {
54            workspace_folder_uri,
55            family,
56            namespace,
57            name,
58        } => {
59            let scope = workspace_folder_uri.unwrap_or("global");
60            let namespace = namespace.unwrap_or("*");
61            format!("sass-symbol-unresolved:{scope}#{family}:{namespace}:{name}")
62        }
63    }
64}
65
66pub fn summarize_omena_query_refs_for_class(
67    selector_name: &str,
68    target_style_uri: Option<&str>,
69    include_declaration: bool,
70    definitions: &[OmenaQueryStyleSelectorDefinitionV0],
71    references: &[OmenaQuerySourceSelectorReferenceCandidateV0],
72) -> OmenaQueryRefsForClassV0 {
73    let mut locations = Vec::new();
74
75    if include_declaration {
76        locations.extend(
77            definitions
78                .iter()
79                .filter(|definition| class_names_match(definition.name.as_str(), selector_name))
80                .filter(|definition| {
81                    target_style_uri.is_none_or(|target_uri| {
82                        file_uri_equivalent(target_uri, definition.uri.as_str())
83                    })
84                })
85                .map(|definition| OmenaQueryReferenceLocationV0 {
86                    uri: definition.uri.clone(),
87                    range: definition.range,
88                    name: definition.name.clone(),
89                    role: "definition",
90                    source: "omenaQueryStyleSelectorDefinitions",
91                }),
92        );
93    }
94
95    for reference in references {
96        let reference_candidate = OmenaQuerySourceSelectorCandidateV0 {
97            kind: reference.kind,
98            name: reference.name.clone(),
99            range: reference.range,
100            source: reference.source,
101            target_style_uri: reference.target_style_uri.clone(),
102        };
103        if !source_selector_candidate_matches_target_uri(&reference_candidate, target_style_uri) {
104            continue;
105        }
106        let selector_names = resolve_omena_query_source_candidate_selector_names(
107            &reference_candidate,
108            definitions,
109            target_style_uri,
110        );
111        if selector_names
112            .iter()
113            .any(|name| class_names_match(name.as_str(), selector_name))
114        {
115            locations.push(OmenaQueryReferenceLocationV0 {
116                uri: reference.uri.clone(),
117                range: reference.range,
118                name: selector_name.to_string(),
119                role: "reference",
120                source: "omenaQuerySourceSelectorReferences",
121            });
122        }
123    }
124
125    locations.sort_by_key(|location| {
126        (
127            reference_location_role_rank(location.role),
128            location.uri.clone(),
129            location.range.start.line,
130            location.range.start.character,
131        )
132    });
133    locations.dedup_by(|left, right| left.uri == right.uri && left.range == right.range);
134
135    OmenaQueryRefsForClassV0 {
136        schema_version: "0",
137        product: "omena-query.refs-for-class",
138        selector_name: selector_name.to_string(),
139        target_style_uri: target_style_uri.map(ToString::to_string),
140        include_declaration,
141        location_count: locations.len(),
142        locations,
143        ready_surfaces: vec!["refsForClass", "workspaceWideSelectorReferences"],
144    }
145}
146
147pub fn summarize_omena_query_rename_plan(
148    selector_name: &str,
149    new_name: &str,
150    target_style_uri: Option<&str>,
151    definitions: &[OmenaQueryStyleSelectorDefinitionV0],
152    references: &[OmenaQuerySourceSelectorReferenceEditTargetV0],
153) -> OmenaQueryRenamePlanV0 {
154    let edits = resolve_omena_query_selector_rename_edits(
155        selector_name,
156        new_name,
157        target_style_uri,
158        definitions,
159        references,
160    );
161    OmenaQueryRenamePlanV0 {
162        schema_version: "0",
163        product: "omena-query.rename-plan",
164        selector_name: selector_name.to_string(),
165        new_name: new_name.to_string(),
166        target_style_uri: target_style_uri.map(ToString::to_string),
167        edit_count: edits.len(),
168        edits,
169        ready_surfaces: vec!["renamePlan", "workspaceWideSelectorRename"],
170    }
171}
172
173pub fn summarize_omena_query_source_selector_occurrence_index(
174    definitions: &[OmenaQueryStyleSelectorDefinitionV0],
175    references: &[OmenaQuerySourceSelectorReferenceCandidateV0],
176) -> OmenaQuerySourceSelectorOccurrenceIndexV0 {
177    let mut occurrences = Vec::new();
178    for reference in references {
179        let reference_candidate = OmenaQuerySourceSelectorCandidateV0 {
180            kind: reference.kind,
181            name: reference.name.clone(),
182            range: reference.range,
183            source: reference.source,
184            target_style_uri: reference.target_style_uri.clone(),
185        };
186        for selector_name in resolve_omena_query_source_candidate_selector_names(
187            &reference_candidate,
188            definitions,
189            reference.target_style_uri.as_deref(),
190        ) {
191            let moniker = source_selector_occurrence_moniker(
192                selector_name.as_str(),
193                reference.target_style_uri.as_deref(),
194            );
195            occurrences.push(OmenaQuerySourceSelectorOccurrenceV0 {
196                moniker,
197                uri: reference.uri.clone(),
198                selector_name: selector_name.clone(),
199                range: reference.range,
200                kind: workspace_occurrence_kind_from_source_reference_kind(reference.kind)
201                    .unwrap_or(OmenaWorkspaceOccurrenceKindV0::SourceSelectorReference),
202                role: OmenaWorkspaceOccurrenceRoleV0::Reference,
203                source: source_reference_occurrence_surface(reference.projection_surface()),
204                target_style_uri: reference.target_style_uri.clone(),
205                rename_target: reference.kind == "sourceSelectorReference"
206                    && reference.name == selector_name,
207            });
208        }
209    }
210
211    occurrences.sort();
212    occurrences.dedup();
213    let moniker_count = occurrences
214        .iter()
215        .map(|occurrence| occurrence.moniker.as_str())
216        .collect::<BTreeSet<_>>()
217        .len();
218    let workspace_index = summarize_omena_query_workspace_occurrence_index_from_source_occurrences(
219        occurrences.as_slice(),
220        vec![
221            "workspaceOccurrenceIndex",
222            "sourceSelectorOccurrenceIndex",
223            "workspaceWideSelectorReferences",
224            "workspaceWideSelectorRename",
225        ],
226    );
227    OmenaQuerySourceSelectorOccurrenceIndexV0 {
228        schema_version: "0",
229        product: "omena-query.source-selector-occurrence-index",
230        moniker_count,
231        occurrence_count: occurrences.len(),
232        workspace_index,
233        occurrences,
234        ready_surfaces: vec![
235            "sourceSelectorOccurrenceIndex",
236            "workspaceWideSelectorReferences",
237            "workspaceWideSelectorRename",
238        ],
239    }
240}
241
242fn source_reference_occurrence_surface(
243    surface: OmenaQuerySourceSelectorReferenceSurfaceV0,
244) -> OmenaWorkspaceOccurrenceSurfaceV0 {
245    match surface {
246        OmenaQuerySourceSelectorReferenceSurfaceV0::OmenaQuerySourceSyntaxIndex
247        | OmenaQuerySourceSelectorReferenceSurfaceV0::OmenaTsgoTypeFactProjection => {
248            // The published workspace V0 groups source-side projections together.
249            OmenaWorkspaceOccurrenceSurfaceV0::OmenaQuerySourceSyntaxIndex
250        }
251    }
252}
253
254pub fn summarize_omena_query_refs_for_class_from_occurrence_index(
255    selector_name: &str,
256    target_style_uri: Option<&str>,
257    include_declaration: bool,
258    definitions: &[OmenaQueryStyleSelectorDefinitionV0],
259    occurrence_index: &OmenaQuerySourceSelectorOccurrenceIndexV0,
260) -> OmenaQueryRefsForClassV0 {
261    let mut locations = Vec::new();
262
263    if include_declaration {
264        locations.extend(
265            definitions
266                .iter()
267                .filter(|definition| class_names_match(definition.name.as_str(), selector_name))
268                .filter(|definition| {
269                    target_style_uri.is_none_or(|target_uri| {
270                        file_uri_equivalent(target_uri, definition.uri.as_str())
271                    })
272                })
273                .map(|definition| OmenaQueryReferenceLocationV0 {
274                    uri: definition.uri.clone(),
275                    range: definition.range,
276                    name: definition.name.clone(),
277                    role: "definition",
278                    source: "omenaQueryStyleSelectorDefinitions",
279                }),
280        );
281    }
282
283    locations.extend(
284        source_selector_occurrences_for_query(occurrence_index, selector_name, target_style_uri)
285            .into_iter()
286            .map(|occurrence| OmenaQueryReferenceLocationV0 {
287                uri: occurrence.uri.clone(),
288                range: occurrence.range,
289                name: occurrence.selector_name.clone(),
290                role: occurrence.role.as_str(),
291                source: "omenaQuerySourceSelectorOccurrenceIndex",
292            }),
293    );
294
295    locations.sort_by_key(|location| {
296        (
297            reference_location_role_rank(location.role),
298            location.uri.clone(),
299            location.range.start.line,
300            location.range.start.character,
301        )
302    });
303    locations.dedup_by(|left, right| left.uri == right.uri && left.range == right.range);
304
305    OmenaQueryRefsForClassV0 {
306        schema_version: "0",
307        product: "omena-query.refs-for-class",
308        selector_name: selector_name.to_string(),
309        target_style_uri: target_style_uri.map(ToString::to_string),
310        include_declaration,
311        location_count: locations.len(),
312        locations,
313        ready_surfaces: vec![
314            "refsForClass",
315            "workspaceWideSelectorReferences",
316            "sourceSelectorOccurrenceIndex",
317        ],
318    }
319}
320
321pub fn summarize_omena_query_rename_plan_from_occurrence_index(
322    selector_name: &str,
323    new_name: &str,
324    target_style_uri: Option<&str>,
325    definitions: &[OmenaQueryStyleSelectorDefinitionV0],
326    occurrence_index: &OmenaQuerySourceSelectorOccurrenceIndexV0,
327) -> OmenaQueryRenamePlanV0 {
328    let references =
329        source_selector_occurrences_for_query(occurrence_index, selector_name, target_style_uri)
330            .into_iter()
331            .filter(|occurrence| occurrence.rename_target)
332            .map(|occurrence| OmenaQuerySourceSelectorReferenceEditTargetV0 {
333                uri: occurrence.uri.clone(),
334                name: occurrence.selector_name.clone(),
335                range: occurrence.range,
336                target_style_uri: occurrence.target_style_uri.clone(),
337            })
338            .collect::<Vec<_>>();
339    let mut plan = summarize_omena_query_rename_plan(
340        selector_name,
341        new_name,
342        target_style_uri,
343        definitions,
344        references.as_slice(),
345    );
346    plan.ready_surfaces.push("sourceSelectorOccurrenceIndex");
347    plan
348}
349
350pub fn occurrences_for_monikers<'a>(
351    index: &'a OmenaWorkspaceOccurrenceIndexV0,
352    monikers: &BTreeSet<String>,
353) -> Vec<&'a OmenaWorkspaceOccurrenceV0> {
354    monikers
355        .iter()
356        .filter_map(|moniker| index.by_moniker.get(moniker.as_str()))
357        .flat_map(|occurrences| occurrences.iter())
358        .collect()
359}
360
361pub fn summarize_omena_query_refs_for_workspace_class(
362    selector_name: &str,
363    target_style_uri: Option<&str>,
364    include_declaration: bool,
365    style_sources: &[OmenaQueryStyleSourceInputV0],
366    source_documents: &[OmenaQuerySourceDocumentInputV0],
367    package_manifests: &[OmenaQueryStylePackageManifestV0],
368) -> OmenaQueryRefsForClassV0 {
369    summarize_omena_query_refs_for_workspace_class_with_resolution_inputs(
370        selector_name,
371        target_style_uri,
372        include_declaration,
373        style_sources,
374        source_documents,
375        package_manifests,
376        &OmenaQueryStyleResolutionInputsV0::default(),
377    )
378}
379
380#[allow(clippy::too_many_arguments)]
381pub fn summarize_omena_query_refs_for_workspace_class_with_resolution_inputs(
382    selector_name: &str,
383    target_style_uri: Option<&str>,
384    include_declaration: bool,
385    style_sources: &[OmenaQueryStyleSourceInputV0],
386    source_documents: &[OmenaQuerySourceDocumentInputV0],
387    package_manifests: &[OmenaQueryStylePackageManifestV0],
388    resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
389) -> OmenaQueryRefsForClassV0 {
390    let definitions = summarize_omena_query_style_selector_definitions(style_sources);
391    let references = collect_omena_query_source_selector_reference_candidates(
392        style_sources,
393        source_documents,
394        package_manifests,
395        resolution_inputs,
396    );
397    summarize_omena_query_refs_for_class(
398        selector_name,
399        target_style_uri,
400        include_declaration,
401        definitions.as_slice(),
402        references.as_slice(),
403    )
404}
405
406pub fn summarize_omena_query_rename_plan_for_workspace_class(
407    selector_name: &str,
408    new_name: &str,
409    target_style_uri: Option<&str>,
410    style_sources: &[OmenaQueryStyleSourceInputV0],
411    source_documents: &[OmenaQuerySourceDocumentInputV0],
412    package_manifests: &[OmenaQueryStylePackageManifestV0],
413) -> OmenaQueryRenamePlanV0 {
414    summarize_omena_query_rename_plan_for_workspace_class_with_resolution_inputs(
415        selector_name,
416        new_name,
417        target_style_uri,
418        style_sources,
419        source_documents,
420        package_manifests,
421        &OmenaQueryStyleResolutionInputsV0::default(),
422    )
423}
424
425#[allow(clippy::too_many_arguments)]
426pub fn summarize_omena_query_rename_plan_for_workspace_class_with_resolution_inputs(
427    selector_name: &str,
428    new_name: &str,
429    target_style_uri: Option<&str>,
430    style_sources: &[OmenaQueryStyleSourceInputV0],
431    source_documents: &[OmenaQuerySourceDocumentInputV0],
432    package_manifests: &[OmenaQueryStylePackageManifestV0],
433    resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
434) -> OmenaQueryRenamePlanV0 {
435    let definitions = summarize_omena_query_style_selector_definitions(style_sources);
436    let references = collect_omena_query_source_selector_reference_edit_targets(
437        style_sources,
438        source_documents,
439        package_manifests,
440        resolution_inputs,
441    );
442    summarize_omena_query_rename_plan(
443        selector_name,
444        new_name,
445        target_style_uri,
446        definitions.as_slice(),
447        references.as_slice(),
448    )
449}
450
451pub fn summarize_omena_query_missing_selector_diagnostic(
452    target_style_uri: &str,
453    target_style_source: &str,
454    selector_name: &str,
455    source_reference_range: ParserRangeV0,
456) -> OmenaQuerySourceDiagnosticV0 {
457    let insertion_range = end_of_source_range(target_style_source);
458    let has_existing_style_content = !target_style_source.trim().is_empty();
459    OmenaQuerySourceDiagnosticV0 {
460        code: "missingSelector",
461        severity: "warning",
462        provenance: omena_query_evidence_graph_provenance![
463            "omena-query.source-syntax-index",
464            "omena-query.style-selector-definitions",
465        ],
466        range: source_reference_range,
467        message: format!(
468            "CSS Module selector '.{selector_name}' not found in indexed style tokens."
469        ),
470        precision: Some(source_diagnostic_precision(
471            "classValueResolution",
472            "sourceSyntaxIndex",
473            "perSourceReference",
474        )),
475        suggestion: None,
476        create_selector: Some(OmenaQueryCreateSelectorActionV0 {
477            uri: target_style_uri.to_string(),
478            range: insertion_range,
479            new_text: if has_existing_style_content {
480                format!("\n\n.{selector_name} {{\n}}\n")
481            } else {
482                format!(".{selector_name} {{\n}}\n")
483            },
484            selector_name: selector_name.to_string(),
485        }),
486    }
487}
488
489/// Two-tier reference universe, tier two: the name failed the bound CSS
490/// Module's export set but resolves in the GLOBAL class universe (class
491/// selectors of indexed non-module stylesheets). At runtime a bind-style
492/// lookup falls through to the literal class name, which the global
493/// stylesheet styles — so this is not a missing selector; it is a scoping
494/// fact worth disclosing (the emitted class is literal and unscoped).
495pub fn summarize_omena_query_global_class_fallthrough_diagnostic(
496    selector_name: &str,
497    global_definition_uri: &str,
498    target_style_uri: &str,
499    target_style_source: &str,
500    source_reference_range: ParserRangeV0,
501) -> OmenaQuerySourceDiagnosticV0 {
502    let global_file_label = global_definition_uri
503        .rsplit('/')
504        .next()
505        .filter(|label| !label.is_empty())
506        .unwrap_or(global_definition_uri);
507    // LSP-provided URIs percent-encode non-ASCII filenames; decode after the
508    // split so the user-facing message shows the readable name.
509    let global_file =
510        percent_decode_uri_path(global_file_label).unwrap_or_else(|| global_file_label.to_string());
511    // The one action that changes the scoping fact: adding the selector to
512    // the bound module makes the reference module-scoped again. Reuses the
513    // missing-selector machinery so the edit shape cannot drift.
514    let create_selector = summarize_omena_query_missing_selector_diagnostic(
515        target_style_uri,
516        target_style_source,
517        selector_name,
518        source_reference_range,
519    )
520    .create_selector;
521    OmenaQuerySourceDiagnosticV0 {
522        code: "globalClassFallthrough",
523        severity: "hint",
524        provenance: omena_query_evidence_graph_provenance![
525            "omena-query.source-syntax-index",
526            "omena-query.style-selector-definitions",
527        ],
528        range: source_reference_range,
529        message: format!(
530            "'.{selector_name}' is not exported by the bound CSS Module; it resolves to the global stylesheet '{global_file}' and is emitted as a literal, unscoped class name."
531        ),
532        precision: Some(source_diagnostic_precision(
533            "classValueResolution",
534            "globalClassUniverse",
535            "perSourceReference",
536        )),
537        suggestion: None,
538        create_selector,
539    }
540}
541
542pub fn summarize_omena_query_source_diagnostics_for_file(
543    source_uri: &str,
544    candidates: &[OmenaQuerySourceMissingSelectorDiagnosticCandidateV0],
545) -> OmenaQuerySourceDiagnosticsForFileV0 {
546    let mut diagnostics = candidates
547        .iter()
548        .map(|candidate| {
549            summarize_omena_query_missing_selector_diagnostic(
550                candidate.target_style_uri.as_str(),
551                candidate.target_style_source.as_str(),
552                candidate.selector_name.as_str(),
553                candidate.source_reference_range,
554            )
555        })
556        .collect::<Vec<_>>();
557    apply_omena_query_checker_product_gate_to_source_diagnostics(&mut diagnostics);
558    OmenaQuerySourceDiagnosticsForFileV0 {
559        schema_version: "0",
560        product: "omena-query.diagnostics-for-file",
561        file_uri: source_uri.to_string(),
562        file_kind: "source",
563        diagnostic_count: diagnostics.len(),
564        diagnostics,
565        ready_surfaces: vec![
566            "sourceMissingSelectorDiagnostics",
567            "crossLanguageDiagnostics",
568            "checkerProductDiagnosticGate",
569        ],
570    }
571}
572
573pub fn summarize_omena_query_source_diagnostics_for_workspace_file(
574    source_path: &str,
575    source_source: &str,
576    style_sources: &[OmenaQueryStyleSourceInputV0],
577    package_manifests: &[OmenaQueryStylePackageManifestV0],
578) -> OmenaQuerySourceDiagnosticsForFileV0 {
579    summarize_omena_query_source_diagnostics_for_workspace_file_with_resolution_inputs(
580        source_path,
581        source_source,
582        style_sources,
583        package_manifests,
584        &OmenaQueryStyleResolutionInputsV0::default(),
585    )
586}
587
588pub fn summarize_omena_query_source_diagnostics_for_workspace_file_with_resolution_inputs(
589    source_path: &str,
590    source_source: &str,
591    style_sources: &[OmenaQueryStyleSourceInputV0],
592    package_manifests: &[OmenaQueryStylePackageManifestV0],
593    resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
594) -> OmenaQuerySourceDiagnosticsForFileV0 {
595    summarize_omena_query_source_diagnostics_for_workspace_file_with_resolution_inputs_and_context_depth(
596        source_path,
597        source_source,
598        style_sources,
599        package_manifests,
600        resolution_inputs,
601        OMENA_QUERY_WORKSPACE_DYNAMIC_CLASSNAME_CONTEXT_DEPTH,
602    )
603}
604
605fn summarize_omena_query_source_diagnostics_for_workspace_file_with_resolution_inputs_and_context_depth(
606    source_path: &str,
607    source_source: &str,
608    style_sources: &[OmenaQueryStyleSourceInputV0],
609    package_manifests: &[OmenaQueryStylePackageManifestV0],
610    resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
611    max_context_depth: usize,
612) -> OmenaQuerySourceDiagnosticsForFileV0 {
613    let available_style_paths = style_sources
614        .iter()
615        .map(|source| source.style_path.as_str())
616        .collect::<BTreeSet<_>>();
617    let definitions = summarize_omena_query_style_selector_definitions(style_sources);
618    let imports = summarize_omena_query_source_import_declarations_for_source_language(
619        source_path,
620        source_source,
621        None,
622    );
623    let mut imported_style_bindings = Vec::new();
624    let mut classnames_bind_bindings = Vec::new();
625    let mut diagnostics = Vec::new();
626
627    for import in imports.imports {
628        if import.specifier == "classnames/bind" {
629            classnames_bind_bindings.push(import.binding);
630            continue;
631        }
632
633        if !is_query_source_style_module_specifier(import.specifier.as_str()) {
634            continue;
635        }
636
637        match resolve_style_module_source_with_path_mappings(
638            source_path,
639            import.specifier.as_str(),
640            &available_style_paths,
641            package_manifests,
642            resolution_inputs.bundler_path_mappings.as_slice(),
643            resolution_inputs.tsconfig_path_mappings.as_slice(),
644            resolution_inputs.disk_style_path_identities.as_slice(),
645        ) {
646            Some(style_path) => {
647                imported_style_bindings.push(OmenaQuerySourceImportedStyleBindingV0 {
648                    binding: import.binding,
649                    style_uri: style_path,
650                })
651            }
652            None => diagnostics.push(OmenaQuerySourceDiagnosticV0 {
653                code: "missing-module",
654                severity: "warning",
655                provenance: omena_query_evidence_graph_provenance![
656                    "omena-query.source-import-declarations",
657                    "omena-resolver.style-module-resolution",
658                ],
659                range: parser_range_for_byte_span(source_source, import.specifier_byte_span),
660                message: if resolution_inputs.disk_style_path_identities.is_empty() {
661                    format!(
662                        "Cannot resolve CSS Module '{}' from the provided workspace inputs.",
663                        import.specifier
664                    )
665                } else {
666                    format!(
667                        "Cannot resolve CSS Module '{}'. The file does not exist.",
668                        import.specifier
669                    )
670                },
671                precision: Some(source_diagnostic_precision(
672                    "styleModuleResolution",
673                    "sourceImportResolution",
674                    "perImportSpecifier",
675                )),
676                suggestion: None,
677                create_selector: None,
678            }),
679        }
680    }
681
682    let index = summarize_omena_query_source_syntax_index_for_source_language(
683        source_path,
684        source_source,
685        None,
686        imported_style_bindings,
687        classnames_bind_bindings,
688    );
689    summarize_omena_query_source_diagnostics_from_syntax_index(
690        source_path,
691        source_source,
692        &index,
693        OmenaQuerySourceDiagnosticsWorkspaceFacts {
694            definitions: definitions.as_slice(),
695            style_sources,
696        },
697        diagnostics,
698        OmenaQuerySourceDiagnosticsAssemblyOptions {
699            max_context_depth,
700            ready_surfaces: vec![
701                "sourceMissingModuleDiagnostics",
702                "sourceMissingSelectorDiagnostics",
703                "sourceResolvedClassDiagnostics",
704                "crossLanguageDiagnostics",
705                "checkerProductDiagnosticGate",
706            ],
707            include_dynamic_classname_m_tier: true,
708        },
709    )
710}
711
712/// Workspace source diagnostics with an explicit call-string bound `k` for the
713/// harvested dynamic-className M-tier flow. The default LSP entry pins
714/// `k = OMENA_QUERY_WORKSPACE_DYNAMIC_CLASSNAME_CONTEXT_DEPTH`; this variant
715/// exposes `k` so the context-sensitivity of the harvested k-CFA flow is
716/// observable (a context-insensitive `k = 0` run joins call sites that share a
717/// callee binding and emits a different M-tier diagnostic set).
718pub fn summarize_omena_query_source_diagnostics_for_workspace_file_with_context_depth(
719    source_path: &str,
720    source_source: &str,
721    style_sources: &[OmenaQueryStyleSourceInputV0],
722    package_manifests: &[OmenaQueryStylePackageManifestV0],
723    max_context_depth: usize,
724) -> OmenaQuerySourceDiagnosticsForFileV0 {
725    summarize_omena_query_source_diagnostics_for_workspace_file_with_resolution_inputs_and_context_depth(
726        source_path,
727        source_source,
728        style_sources,
729        package_manifests,
730        &OmenaQueryStyleResolutionInputsV0::default(),
731        max_context_depth,
732    )
733}
734
735pub fn summarize_omena_query_source_diagnostics_for_workspace_file_with_source_syntax_index(
736    source_path: &str,
737    source_source: &str,
738    source_syntax_index: &OmenaQuerySourceSyntaxIndexV0,
739    style_sources: &[OmenaQueryStyleSourceInputV0],
740) -> OmenaQuerySourceDiagnosticsForFileV0 {
741    summarize_omena_query_source_diagnostics_for_workspace_file_with_source_syntax_index_and_context_depth(
742        source_path,
743        source_source,
744        source_syntax_index,
745        style_sources,
746        OMENA_QUERY_WORKSPACE_DYNAMIC_CLASSNAME_CONTEXT_DEPTH,
747    )
748}
749
750pub fn summarize_omena_query_source_diagnostics_for_workspace_file_with_source_syntax_index_and_definitions(
751    source_path: &str,
752    source_source: &str,
753    source_syntax_index: &OmenaQuerySourceSyntaxIndexV0,
754    definitions: &[OmenaQueryStyleSelectorDefinitionV0],
755    style_sources: &[OmenaQueryStyleSourceInputV0],
756) -> OmenaQuerySourceDiagnosticsForFileV0 {
757    summarize_omena_query_source_diagnostics_from_syntax_index(
758        source_path,
759        source_source,
760        source_syntax_index,
761        OmenaQuerySourceDiagnosticsWorkspaceFacts {
762            definitions,
763            style_sources,
764        },
765        Vec::new(),
766        OmenaQuerySourceDiagnosticsAssemblyOptions {
767            max_context_depth: OMENA_QUERY_WORKSPACE_DYNAMIC_CLASSNAME_CONTEXT_DEPTH,
768            ready_surfaces: vec![
769                "sourceIndexedSyntaxDiagnostics",
770                "sourceMissingSelectorDiagnostics",
771                "sourceResolvedClassDiagnostics",
772                "crossLanguageDiagnostics",
773                "checkerProductDiagnosticGate",
774            ],
775            include_dynamic_classname_m_tier: true,
776        },
777    )
778}
779
780pub fn summarize_omena_query_source_baseline_diagnostics_for_workspace_file_with_source_syntax_index_and_definitions(
781    source_path: &str,
782    source_source: &str,
783    source_syntax_index: &OmenaQuerySourceSyntaxIndexV0,
784    definitions: &[OmenaQueryStyleSelectorDefinitionV0],
785    style_sources: &[OmenaQueryStyleSourceInputV0],
786) -> OmenaQuerySourceDiagnosticsForFileV0 {
787    summarize_omena_query_source_diagnostics_from_syntax_index(
788        source_path,
789        source_source,
790        source_syntax_index,
791        OmenaQuerySourceDiagnosticsWorkspaceFacts {
792            definitions,
793            style_sources,
794        },
795        Vec::new(),
796        OmenaQuerySourceDiagnosticsAssemblyOptions {
797            max_context_depth: OMENA_QUERY_WORKSPACE_DYNAMIC_CLASSNAME_CONTEXT_DEPTH,
798            ready_surfaces: vec![
799                "sourceIndexedSyntaxDiagnostics",
800                "sourceMissingSelectorDiagnostics",
801                "sourceBaselineDiagnostics",
802                "crossLanguageDiagnostics",
803                "checkerProductDiagnosticGate",
804            ],
805            include_dynamic_classname_m_tier: false,
806        },
807    )
808}
809
810pub fn summarize_omena_query_source_diagnostics_for_workspace_file_with_source_syntax_index_and_context_depth(
811    source_path: &str,
812    source_source: &str,
813    source_syntax_index: &OmenaQuerySourceSyntaxIndexV0,
814    style_sources: &[OmenaQueryStyleSourceInputV0],
815    max_context_depth: usize,
816) -> OmenaQuerySourceDiagnosticsForFileV0 {
817    let definitions = summarize_omena_query_style_selector_definitions(style_sources);
818    summarize_omena_query_source_diagnostics_from_syntax_index(
819        source_path,
820        source_source,
821        source_syntax_index,
822        OmenaQuerySourceDiagnosticsWorkspaceFacts {
823            definitions: definitions.as_slice(),
824            style_sources,
825        },
826        Vec::new(),
827        OmenaQuerySourceDiagnosticsAssemblyOptions {
828            max_context_depth,
829            ready_surfaces: vec![
830                "sourceIndexedSyntaxDiagnostics",
831                "sourceMissingSelectorDiagnostics",
832                "sourceResolvedClassDiagnostics",
833                "crossLanguageDiagnostics",
834                "checkerProductDiagnosticGate",
835            ],
836            include_dynamic_classname_m_tier: true,
837        },
838    )
839}
840
841struct OmenaQuerySourceDiagnosticsWorkspaceFacts<'a> {
842    definitions: &'a [OmenaQueryStyleSelectorDefinitionV0],
843    style_sources: &'a [OmenaQueryStyleSourceInputV0],
844}
845
846struct OmenaQuerySourceDiagnosticsAssemblyOptions {
847    max_context_depth: usize,
848    ready_surfaces: Vec<&'static str>,
849    include_dynamic_classname_m_tier: bool,
850}
851
852fn summarize_omena_query_source_diagnostics_from_syntax_index(
853    source_path: &str,
854    source_source: &str,
855    index: &OmenaQuerySourceSyntaxIndexV0,
856    workspace_facts: OmenaQuerySourceDiagnosticsWorkspaceFacts<'_>,
857    mut diagnostics: Vec<OmenaQuerySourceDiagnosticV0>,
858    options: OmenaQuerySourceDiagnosticsAssemblyOptions,
859) -> OmenaQuerySourceDiagnosticsForFileV0 {
860    let style_sources_by_path = workspace_facts
861        .style_sources
862        .iter()
863        .map(|source| (source.style_path.as_str(), source.style_source.as_str()))
864        .collect::<BTreeMap<_, _>>();
865    diagnostics.extend(summarize_omena_query_domain_class_reference_diagnostics(
866        source_source,
867        index,
868    ));
869    diagnostics.extend(
870        summarize_omena_query_type_fact_provider_unavailable_diagnostics(source_source, index),
871    );
872
873    if !index.imported_style_bindings.is_empty() {
874        if options.include_dynamic_classname_m_tier {
875            // Harvest dynamic-className call sites (template-interpolation projections)
876            // from the same syntax index and route them through the real k-limited
877            // (k-CFA) M-tier flow gate, so the LSP-consumed default path emits the
878            // context-sensitive no-unknown-dynamic-class / no-impossible-selector
879            // diagnostics without an external producer.
880            //
881            // `no-unknown-dynamic-class` is module-scoped: a target carrying a
882            // resolved `target_style_uri` is matched against ONLY that module's
883            // selectors (`selector_universe_by_uri`), so a `btn-` prefix is not
884            // cross-attributed to a `btn-*` selector in a different imported module.
885            // Targets with no resolved binding fall back to the union universe.
886            let selector_universe = workspace_facts
887                .definitions
888                .iter()
889                .map(|definition| definition.name.clone())
890                .collect::<BTreeSet<_>>()
891                .into_iter()
892                .collect::<Vec<_>>();
893            let mut selector_universe_by_uri: BTreeMap<String, Vec<String>> = BTreeMap::new();
894            for definition in workspace_facts.definitions {
895                selector_universe_by_uri
896                    .entry(definition.uri.clone())
897                    .or_default()
898                    .push(definition.name.clone());
899            }
900            for names in selector_universe_by_uri.values_mut() {
901                names.sort();
902                names.dedup();
903            }
904            diagnostics.extend(harvest_omena_query_dynamic_classname_m_tier_diagnostics(
905                source_path,
906                source_source,
907                index,
908                selector_universe.as_slice(),
909                &selector_universe_by_uri,
910                options.max_context_depth,
911            ));
912        }
913
914        for reference in &index.selector_references {
915            let Some(target_style_uri) = reference.target_style_uri.as_deref() else {
916                continue;
917            };
918            let target_style_is_known =
919                workspace_facts.definitions.iter().any(|definition| {
920                    file_uri_equivalent(definition.uri.as_str(), target_style_uri)
921                }) || style_sources_by_path
922                    .keys()
923                    .any(|style_uri| file_uri_equivalent(style_uri, target_style_uri));
924            if !target_style_is_known {
925                continue;
926            }
927            let Some(selector_name) = reference.selector_name.clone().or_else(|| {
928                source_reference_text_selector_name(source_source, reference.byte_span)
929            }) else {
930                continue;
931            };
932            let candidate = OmenaQuerySourceSelectorCandidateV0 {
933                kind: match reference.match_kind {
934                    OmenaQuerySourceSelectorReferenceMatchKindV0::Exact => {
935                        "sourceSelectorReference"
936                    }
937                    OmenaQuerySourceSelectorReferenceMatchKindV0::Prefix => {
938                        "sourceSelectorPrefixReference"
939                    }
940                },
941                name: selector_name.clone(),
942                range: parser_range_for_byte_span(source_source, reference.byte_span),
943                source: "omenaQuerySourceSyntaxIndex",
944                target_style_uri: Some(target_style_uri.to_string()),
945            };
946            if !resolve_omena_query_style_selector_definitions_for_source_candidate(
947                &candidate,
948                workspace_facts.definitions,
949            )
950            .is_empty()
951            {
952                continue;
953            }
954            let target_style_source = style_sources_by_path
955                .get(target_style_uri)
956                .copied()
957                .or_else(|| {
958                    style_sources_by_path
959                        .iter()
960                        .find(|(style_uri, _)| file_uri_equivalent(style_uri, target_style_uri))
961                        .map(|(_, source)| *source)
962                });
963            let value_domain_size = index
964                .selector_references
965                .iter()
966                .filter(|candidate| {
967                    candidate.byte_span == reference.byte_span
968                        && candidate.target_style_uri == reference.target_style_uri
969                        && candidate.match_kind == reference.match_kind
970                })
971                .filter_map(|candidate| candidate.selector_name.as_deref())
972                .collect::<BTreeSet<_>>()
973                .len();
974            diagnostics.push(
975                summarize_omena_query_unresolved_source_reference_diagnostic(
976                    source_source,
977                    reference,
978                    selector_name.as_str(),
979                    target_style_source,
980                    workspace_facts.definitions,
981                    value_domain_size,
982                ),
983            );
984        }
985    }
986
987    diagnostics.sort_by_key(|diagnostic| {
988        (
989            diagnostic.range.start.line,
990            diagnostic.range.start.character,
991            diagnostic.code,
992            diagnostic.message.clone(),
993        )
994    });
995    diagnostics.dedup_by(|left, right| {
996        left.code == right.code && left.range == right.range && left.message == right.message
997    });
998    apply_omena_query_checker_product_gate_to_source_diagnostics(&mut diagnostics);
999
1000    OmenaQuerySourceDiagnosticsForFileV0 {
1001        schema_version: "0",
1002        product: "omena-query.diagnostics-for-file",
1003        file_uri: source_path.to_string(),
1004        file_kind: "source",
1005        diagnostic_count: diagnostics.len(),
1006        diagnostics,
1007        ready_surfaces: options.ready_surfaces,
1008    }
1009}
1010
1011/// Undecidability disclosures, split by WHOSE property the cause is.
1012///
1013/// `unresolvable` is a property of the CODE (the value's type is an open
1014/// string, so no finite class-name domain exists — no type checker can
1015/// enumerate it): disclosed per site at HINT severity, with the one action
1016/// that lifts it (narrow to a string-literal union). Every other reason is
1017/// a property of the SESSION (the provider is missing or broken): stamping
1018/// every dynamic site with the tool's own outage is noise, so those
1019/// collapse to ONE disclosure per file at the first affected site.
1020fn summarize_omena_query_type_fact_provider_unavailable_diagnostics(
1021    source: &str,
1022    index: &OmenaQuerySourceSyntaxIndexV0,
1023) -> Vec<OmenaQuerySourceDiagnosticV0> {
1024    let provenance = || {
1025        omena_query_evidence_graph_provenance![
1026            "omena-query.source-syntax-index",
1027            "omena-tsgo-client.provider-capabilities",
1028            OMENA_QUERY_TSGO_PROVIDER_UNAVAILABLE_PROVENANCE,
1029        ]
1030    };
1031    let precision = || {
1032        Some(source_diagnostic_precision(
1033            OMENA_QUERY_TYPE_ORACLE_UNKNOWN_VALUE_DOMAIN,
1034            "typeOracleProviderUnavailable",
1035            "perTypeFactTarget",
1036        ))
1037    };
1038    let mut diagnostics = Vec::new();
1039    let mut session_facts = Vec::new();
1040    for fact in index
1041        .type_fact_provider_unavailable
1042        .iter()
1043        .filter(|fact| fact.provider_id == "tsgo")
1044    {
1045        if fact.reason == "unresolvable" {
1046            diagnostics.push(OmenaQuerySourceDiagnosticV0 {
1047                code: "unknownClassValueDomain",
1048                severity: "hint",
1049                provenance: provenance(),
1050                range: parser_range_for_byte_span(source, fact.byte_span),
1051                message: "This class value has an open string type, so its class names cannot be checked here.".to_string(),
1052                precision: precision(),
1053                suggestion: Some(
1054                    "Narrow the value's type to a string-literal union (for example 'primary' | 'danger') to enable class checking at this site.".to_string(),
1055                ),
1056                create_selector: None,
1057            });
1058        } else {
1059            session_facts.push(fact);
1060        }
1061    }
1062    if let Some(first) = session_facts.first() {
1063        let cause = match first.reason {
1064            "projectMiss" => "tsgo could not find a project for this source",
1065            "noTransport" => "no tsgo provider transport is available",
1066            "processUnavailable" => "the tsgo provider process could not start",
1067            _ => "the tsgo provider request failed",
1068        };
1069        let site_count = session_facts.len();
1070        diagnostics.push(OmenaQuerySourceDiagnosticV0 {
1071            code: "unknownClassValueDomain",
1072            severity: "warning",
1073            provenance: provenance(),
1074            range: parser_range_for_byte_span(source, first.byte_span),
1075            message: format!(
1076                "CSS Module class value domain is unknown because {cause}. Dynamic class values in this file ({site_count} site{}) are not checked until the provider is available.",
1077                if site_count == 1 { "" } else { "s" }
1078            ),
1079            precision: precision(),
1080            suggestion: None,
1081            create_selector: None,
1082        });
1083    }
1084    diagnostics
1085}
1086
1087fn summarize_omena_query_domain_class_reference_diagnostics(
1088    source: &str,
1089    index: &OmenaQuerySourceSyntaxIndexV0,
1090) -> Vec<OmenaQuerySourceDiagnosticV0> {
1091    let mut diagnostics = Vec::new();
1092    for reference in &index.domain_class_references {
1093        let Some(option_name) = reference.option_name.as_ref() else {
1094            continue;
1095        };
1096        let Some(universe) = index.class_value_universes.iter().find(|universe| {
1097            universe.plugin_id == reference.plugin_id
1098                && universe.domain == reference.domain
1099                && universe.owner_name == reference.owner_name
1100        }) else {
1101            continue;
1102        };
1103        let Some(axis) = universe
1104            .axes
1105            .iter()
1106            .find(|axis| axis.axis_name == reference.axis_name)
1107        else {
1108            continue;
1109        };
1110        if axis.values.iter().any(|value| value == option_name) {
1111            continue;
1112        }
1113        diagnostics.push(OmenaQuerySourceDiagnosticV0 {
1114            code: "missingClassValueOption",
1115            severity: "warning",
1116            provenance: omena_query_evidence_graph_provenance![
1117                "omena-bridge.class-value-universe-provider",
1118                "omena-query.source-domain-class-references",
1119            ],
1120            range: parser_range_for_byte_span(source, reference.byte_span),
1121            message: format!(
1122                "Class value option '{}' is not defined for {}.{}.",
1123                option_name, reference.owner_name, reference.axis_name
1124            ),
1125            precision: Some(source_diagnostic_precision(
1126                "classValueUniverse",
1127                "sourceDomainReference",
1128                "perDomainAxis",
1129            )),
1130            suggestion: None,
1131            create_selector: None,
1132        });
1133    }
1134    diagnostics
1135}
1136
1137pub(super) fn summarize_omena_query_style_selector_definitions(
1138    style_sources: &[OmenaQueryStyleSourceInputV0],
1139) -> Vec<OmenaQueryStyleSelectorDefinitionV0> {
1140    let mut definitions = Vec::new();
1141    for source in style_sources {
1142        let Some(candidates) = summarize_omena_query_style_hover_candidates(
1143            source.style_path.as_str(),
1144            source.style_source.as_str(),
1145        ) else {
1146            continue;
1147        };
1148        definitions.extend(candidates.candidates.into_iter().filter_map(|candidate| {
1149            (candidate.kind == "selector").then(|| OmenaQueryStyleSelectorDefinitionV0 {
1150                uri: source.style_path.clone(),
1151                name: candidate.name,
1152                range: candidate.range,
1153            })
1154        }));
1155    }
1156    definitions.sort_by_key(|definition| {
1157        (
1158            definition.uri.clone(),
1159            definition.range.start.line,
1160            definition.range.start.character,
1161            definition.name.clone(),
1162        )
1163    });
1164    definitions.dedup();
1165    definitions
1166}
1167
1168fn collect_omena_query_source_selector_reference_candidates(
1169    style_sources: &[OmenaQueryStyleSourceInputV0],
1170    source_documents: &[OmenaQuerySourceDocumentInputV0],
1171    package_manifests: &[OmenaQueryStylePackageManifestV0],
1172    resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
1173) -> Vec<OmenaQuerySourceSelectorReferenceCandidateV0> {
1174    collect_omena_query_source_selector_references_with_resolution_inputs(
1175        style_sources,
1176        source_documents,
1177        package_manifests,
1178        resolution_inputs,
1179    )
1180    .into_iter()
1181    .map(|reference| reference.candidate)
1182    .collect()
1183}
1184
1185fn collect_omena_query_source_selector_reference_edit_targets(
1186    style_sources: &[OmenaQueryStyleSourceInputV0],
1187    source_documents: &[OmenaQuerySourceDocumentInputV0],
1188    package_manifests: &[OmenaQueryStylePackageManifestV0],
1189    resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
1190) -> Vec<OmenaQuerySourceSelectorReferenceEditTargetV0> {
1191    collect_omena_query_source_selector_references_with_resolution_inputs(
1192        style_sources,
1193        source_documents,
1194        package_manifests,
1195        resolution_inputs,
1196    )
1197    .into_iter()
1198    .filter_map(|reference| {
1199        reference
1200            .is_exact
1201            .then_some(OmenaQuerySourceSelectorReferenceEditTargetV0 {
1202                uri: reference.candidate.uri,
1203                name: reference.candidate.name,
1204                range: reference.candidate.range,
1205                target_style_uri: reference.candidate.target_style_uri,
1206            })
1207    })
1208    .collect()
1209}
1210
1211pub(super) fn collect_omena_query_source_selector_references_with_resolution_inputs(
1212    style_sources: &[OmenaQueryStyleSourceInputV0],
1213    source_documents: &[OmenaQuerySourceDocumentInputV0],
1214    package_manifests: &[OmenaQueryStylePackageManifestV0],
1215    resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
1216) -> Vec<OmenaQueryWorkspaceSourceReferenceCandidateV0> {
1217    let available_style_paths = style_sources
1218        .iter()
1219        .map(|source| source.style_path.as_str())
1220        .collect::<BTreeSet<_>>();
1221    let mut references = Vec::new();
1222
1223    for document in source_documents {
1224        let Some(mut index) = source_selector_reference_index_for_document(
1225            document,
1226            &available_style_paths,
1227            package_manifests,
1228            resolution_inputs,
1229        ) else {
1230            continue;
1231        };
1232        canonicalize_omena_query_source_selector_references(&mut index.selector_references);
1233
1234        for reference in index.selector_references {
1235            let Some(name) = reference.selector_name.clone().or_else(|| {
1236                source_reference_text_selector_name(&document.source_source, reference.byte_span)
1237            }) else {
1238                continue;
1239            };
1240            let is_exact = matches!(
1241                reference.match_kind,
1242                OmenaQuerySourceSelectorReferenceMatchKindV0::Exact
1243            );
1244            references.push(OmenaQueryWorkspaceSourceReferenceCandidateV0 {
1245                is_exact,
1246                candidate: OmenaQuerySourceSelectorReferenceCandidateV0 {
1247                    uri: document.source_path.clone(),
1248                    kind: if is_exact {
1249                        "sourceSelectorReference"
1250                    } else {
1251                        "sourceSelectorPrefixReference"
1252                    },
1253                    name,
1254                    range: parser_range_for_byte_span(&document.source_source, reference.byte_span),
1255                    source: reference.surface.as_str(),
1256                    target_style_uri: reference.target_style_uri,
1257                },
1258            });
1259        }
1260    }
1261
1262    references.sort_by_key(|reference| {
1263        (
1264            reference.candidate.uri.clone(),
1265            reference.candidate.range.start.line,
1266            reference.candidate.range.start.character,
1267            reference.candidate.name.clone(),
1268        )
1269    });
1270    references.dedup_by(|left, right| {
1271        left.candidate.uri == right.candidate.uri
1272            && left.candidate.range == right.candidate.range
1273            && left.candidate.name == right.candidate.name
1274            && left.candidate.target_style_uri == right.candidate.target_style_uri
1275    });
1276    references
1277}
1278
1279fn source_selector_reference_index_for_document(
1280    document: &OmenaQuerySourceDocumentInputV0,
1281    available_style_paths: &BTreeSet<&str>,
1282    package_manifests: &[OmenaQueryStylePackageManifestV0],
1283    resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
1284) -> Option<OmenaQuerySourceSyntaxIndexV0> {
1285    if let Some(index) = document.source_syntax_index.clone()
1286        && (!index.imported_style_bindings.is_empty()
1287            || index
1288                .selector_references
1289                .iter()
1290                .any(|reference| reference.target_style_uri.is_some()))
1291    {
1292        return Some(index);
1293    }
1294
1295    let imports = summarize_omena_query_source_import_declarations_for_source_language(
1296        document.source_path.as_str(),
1297        &document.source_source,
1298        None,
1299    );
1300    let mut imported_style_bindings = Vec::new();
1301    let mut classnames_bind_bindings = Vec::new();
1302
1303    for import in imports.imports {
1304        if import.specifier == "classnames/bind" {
1305            classnames_bind_bindings.push(import.binding);
1306            continue;
1307        }
1308        let Some(style_uri) = resolve_style_module_source_with_path_mappings(
1309            &document.source_path,
1310            &import.specifier,
1311            available_style_paths,
1312            package_manifests,
1313            resolution_inputs.bundler_path_mappings.as_slice(),
1314            resolution_inputs.tsconfig_path_mappings.as_slice(),
1315            resolution_inputs.disk_style_path_identities.as_slice(),
1316        ) else {
1317            continue;
1318        };
1319        imported_style_bindings.push(OmenaQuerySourceImportedStyleBindingV0 {
1320            binding: import.binding,
1321            style_uri,
1322        });
1323    }
1324
1325    if imported_style_bindings.is_empty() {
1326        return None;
1327    }
1328
1329    Some(
1330        summarize_omena_query_source_syntax_index_for_source_language(
1331            document.source_path.as_str(),
1332            &document.source_source,
1333            None,
1334            imported_style_bindings,
1335            classnames_bind_bindings,
1336        ),
1337    )
1338}
1339
1340#[derive(Debug, Clone, PartialEq, Eq)]
1341pub(super) struct OmenaQueryWorkspaceSourceReferenceCandidateV0 {
1342    pub(super) is_exact: bool,
1343    pub(super) candidate: OmenaQuerySourceSelectorReferenceCandidateV0,
1344}
1345
1346fn summarize_omena_query_unresolved_source_reference_diagnostic(
1347    source: &str,
1348    reference: &OmenaQuerySourceSelectorReferenceFactV0,
1349    selector_name: &str,
1350    target_style_source: Option<&str>,
1351    definitions: &[OmenaQueryStyleSelectorDefinitionV0],
1352    value_domain_size: usize,
1353) -> OmenaQuerySourceDiagnosticV0 {
1354    let range = parser_range_for_byte_span(source, reference.byte_span);
1355    let reference_text = source
1356        .get(reference.byte_span.start..reference.byte_span.end)
1357        .unwrap_or_default()
1358        .trim_matches(['"', '\'', '`']);
1359    let code = match reference.match_kind {
1360        OmenaQuerySourceSelectorReferenceMatchKindV0::Exact if reference_text == selector_name => {
1361            "missingStaticClass"
1362        }
1363        OmenaQuerySourceSelectorReferenceMatchKindV0::Exact => "missingResolvedClassValues",
1364        OmenaQuerySourceSelectorReferenceMatchKindV0::Prefix if reference_text == selector_name => {
1365            "missingTemplatePrefix"
1366        }
1367        OmenaQuerySourceSelectorReferenceMatchKindV0::Prefix => "missingResolvedClassDomain",
1368    };
1369    let create_selector = reference
1370        .target_style_uri
1371        .as_deref()
1372        .zip(target_style_source)
1373        .filter(|_| {
1374            matches!(
1375                reference.match_kind,
1376                OmenaQuerySourceSelectorReferenceMatchKindV0::Exact
1377            )
1378        })
1379        .and_then(|(target_style_uri, target_style_source)| {
1380            summarize_omena_query_missing_selector_diagnostic(
1381                target_style_uri,
1382                target_style_source,
1383                selector_name,
1384                range,
1385            )
1386            .create_selector
1387        });
1388    let suggestion = if code == "missingStaticClass" {
1389        reference
1390            .target_style_uri
1391            .as_deref()
1392            .and_then(|target_style_uri| {
1393                closest_selector_name(
1394                    selector_name,
1395                    definitions
1396                        .iter()
1397                        .filter(|definition| {
1398                            file_uri_equivalent(definition.uri.as_str(), target_style_uri)
1399                        })
1400                        .map(|definition| definition.name.as_str()),
1401                    3,
1402                )
1403            })
1404    } else {
1405        None
1406    };
1407
1408    OmenaQuerySourceDiagnosticV0 {
1409        code,
1410        severity: "warning",
1411        provenance: omena_query_evidence_graph_provenance![
1412            "omena-query.source-syntax-index",
1413            "omena-query.style-selector-definitions",
1414        ],
1415        range,
1416        message: query_source_diagnostic_message(
1417            code,
1418            selector_name,
1419            suggestion.as_deref(),
1420            value_domain_size,
1421        ),
1422        precision: Some(source_diagnostic_precision(
1423            "classValueResolution",
1424            "sourceSelectorReference",
1425            match code {
1426                "missingResolvedClassValues" | "missingResolvedClassDomain" => {
1427                    "resolvedClassValueDomain"
1428                }
1429                _ => "perSourceReference",
1430            },
1431        )),
1432        suggestion,
1433        create_selector,
1434    }
1435}
1436
1437fn query_source_diagnostic_message(
1438    code: &str,
1439    selector_name: &str,
1440    suggestion: Option<&str>,
1441    value_domain_size: usize,
1442) -> String {
1443    match code {
1444        "missingStaticClass" => {
1445            let hint = suggestion
1446                .map(|suggestion| format!(" Did you mean '{suggestion}'?"))
1447                .unwrap_or_default();
1448            format!("Class '.{selector_name}' not found in target CSS Module.{hint}")
1449        }
1450        "missingTemplatePrefix" => {
1451            format!("No class starting with '{selector_name}' found in target CSS Module.")
1452        }
1453        "missingResolvedClassValues" => {
1454            format!(
1455                "Missing class for possible value: '{selector_name}'. Analysis reason: analysis preserved multiple finite candidate values. Analysis shape: bounded finite ({value_domain_size})."
1456            )
1457        }
1458        "missingResolvedClassDomain" => {
1459            format!("No class matched resolved prefix '{selector_name}'.")
1460        }
1461        _ => "Source diagnostic reported by omena-query.".to_string(),
1462    }
1463}
1464
1465fn closest_selector_name<'a>(
1466    target: &str,
1467    candidates: impl IntoIterator<Item = &'a str>,
1468    max_distance: usize,
1469) -> Option<String> {
1470    let mut best = None::<(&'a str, usize)>;
1471    for candidate in candidates {
1472        let current_bound = best
1473            .map(|(_, distance)| distance.saturating_sub(1))
1474            .unwrap_or(max_distance);
1475        let distance = bounded_levenshtein_distance(target, candidate, current_bound);
1476        if distance <= max_distance
1477            && best.is_none_or(|(_, best_distance)| distance < best_distance)
1478        {
1479            best = Some((candidate, distance));
1480        }
1481    }
1482    best.map(|(candidate, _)| candidate.to_string())
1483}
1484
1485fn bounded_levenshtein_distance(left: &str, right: &str, max_distance: usize) -> usize {
1486    if left == right {
1487        return 0;
1488    }
1489    if left.is_empty() {
1490        return right.chars().count();
1491    }
1492    if right.is_empty() {
1493        return left.chars().count();
1494    }
1495
1496    let left_chars = left.chars().collect::<Vec<_>>();
1497    let right_chars = right.chars().collect::<Vec<_>>();
1498    if left_chars.len().abs_diff(right_chars.len()) > max_distance {
1499        return max_distance + 1;
1500    }
1501
1502    let mut previous = (0..=right_chars.len()).collect::<Vec<_>>();
1503    let mut current = vec![0; right_chars.len() + 1];
1504    for (left_index, left_char) in left_chars.iter().enumerate() {
1505        current[0] = left_index + 1;
1506        let mut row_min = current[0];
1507        for (right_index, right_char) in right_chars.iter().enumerate() {
1508            let cost = usize::from(left_char != right_char);
1509            let value = (current[right_index] + 1)
1510                .min(previous[right_index + 1] + 1)
1511                .min(previous[right_index] + cost);
1512            current[right_index + 1] = value;
1513            row_min = row_min.min(value);
1514        }
1515        if row_min > max_distance {
1516            return max_distance + 1;
1517        }
1518        previous.copy_from_slice(&current);
1519    }
1520    previous[right_chars.len()]
1521}
1522
1523fn is_query_source_style_module_specifier(specifier: &str) -> bool {
1524    specifier.contains(".module.")
1525        || specifier.ends_with(".css")
1526        || specifier.ends_with(".scss")
1527        || specifier.ends_with(".sass")
1528        || specifier.ends_with(".less")
1529}
1530
1531pub fn resolve_omena_query_source_provider_candidates(
1532    source_candidates: Vec<OmenaQuerySourceSelectorCandidateV0>,
1533    definitions: &[OmenaQueryStyleSelectorDefinitionV0],
1534) -> OmenaQuerySourceProviderCandidateResolutionV0 {
1535    if definitions.is_empty() {
1536        return OmenaQuerySourceProviderCandidateResolutionV0 {
1537            schema_version: "0",
1538            product: "omena-query.source-provider-candidate-resolution",
1539            matched: Vec::new(),
1540            unresolved: Vec::new(),
1541        };
1542    }
1543
1544    let (mut matched, mut unresolved): (Vec<_>, Vec<_>) =
1545        source_candidates.into_iter().partition(|candidate| {
1546            definitions.iter().any(|definition| {
1547                source_selector_candidate_matches_definition(candidate, definition)
1548            })
1549        });
1550    matched.sort();
1551    unresolved.sort();
1552    OmenaQuerySourceProviderCandidateResolutionV0 {
1553        schema_version: "0",
1554        product: "omena-query.source-provider-candidate-resolution",
1555        matched,
1556        unresolved,
1557    }
1558}
1559
1560pub fn resolve_omena_query_style_selector_definitions_for_source_candidate(
1561    candidate: &OmenaQuerySourceSelectorCandidateV0,
1562    definitions: &[OmenaQueryStyleSelectorDefinitionV0],
1563) -> Vec<OmenaQueryStyleSelectorDefinitionV0> {
1564    let mut matched = definitions
1565        .iter()
1566        .filter(|definition| source_selector_candidate_matches_definition(candidate, definition))
1567        .cloned()
1568        .collect::<Vec<_>>();
1569    matched.sort_by_key(|definition| {
1570        (
1571            definition.uri.clone(),
1572            definition.range.start.line,
1573            definition.range.start.character,
1574            definition.name.clone(),
1575        )
1576    });
1577    matched.dedup();
1578    matched
1579}
1580
1581pub fn resolve_omena_query_source_candidate_selector_names(
1582    candidate: &OmenaQuerySourceSelectorCandidateV0,
1583    definitions: &[OmenaQueryStyleSelectorDefinitionV0],
1584    target_style_uri: Option<&str>,
1585) -> Vec<String> {
1586    if candidate.kind != "sourceSelectorPrefixReference" {
1587        return vec![candidate.name.clone()];
1588    }
1589
1590    let mut names = definitions
1591        .iter()
1592        .filter(|definition| source_selector_candidate_matches_definition(candidate, definition))
1593        .filter(|definition| {
1594            candidate
1595                .target_style_uri
1596                .as_deref()
1597                .or(target_style_uri)
1598                .is_none_or(|target_uri| file_uri_equivalent(target_uri, definition.uri.as_str()))
1599        })
1600        .map(|definition| definition.name.clone())
1601        .collect::<Vec<_>>();
1602    names.sort();
1603    names.dedup();
1604    names
1605}
1606
1607pub fn resolve_omena_query_selector_rename_edits(
1608    selector_name: &str,
1609    new_name: &str,
1610    target_style_uri: Option<&str>,
1611    definitions: &[OmenaQueryStyleSelectorDefinitionV0],
1612    references: &[OmenaQuerySourceSelectorReferenceEditTargetV0],
1613) -> Vec<OmenaQueryWorkspaceTextEditV0> {
1614    let replacement = new_name.trim_start_matches('.');
1615    if replacement.is_empty() {
1616        return Vec::new();
1617    }
1618
1619    let mut edits = definitions
1620        .iter()
1621        .filter(|definition| class_names_match(definition.name.as_str(), selector_name))
1622        .filter(|definition| {
1623            target_style_uri
1624                .is_none_or(|target_uri| file_uri_equivalent(target_uri, definition.uri.as_str()))
1625        })
1626        .map(|definition| OmenaQueryWorkspaceTextEditV0 {
1627            uri: definition.uri.clone(),
1628            range: definition.range,
1629            new_text: replacement.to_string(),
1630        })
1631        .chain(
1632            references
1633                .iter()
1634                .filter(|reference| class_names_match(reference.name.as_str(), selector_name))
1635                .filter(|reference| {
1636                    source_reference_matches_target_style(reference, target_style_uri)
1637                })
1638                .map(|reference| OmenaQueryWorkspaceTextEditV0 {
1639                    uri: reference.uri.clone(),
1640                    range: reference.range,
1641                    new_text: replacement.to_string(),
1642                }),
1643        )
1644        .collect::<Vec<_>>();
1645    edits.sort_by_key(|edit| {
1646        (
1647            edit.uri.clone(),
1648            edit.range.start.line,
1649            edit.range.start.character,
1650            edit.range.end.line,
1651            edit.range.end.character,
1652        )
1653    });
1654    edits
1655}
1656
1657fn source_selector_candidate_matches_definition(
1658    candidate: &OmenaQuerySourceSelectorCandidateV0,
1659    definition: &OmenaQueryStyleSelectorDefinitionV0,
1660) -> bool {
1661    let selector_matches = if candidate.kind == "sourceSelectorPrefixReference" {
1662        ClassNameV0::new(definition.name.as_str())
1663            .decoded()
1664            .starts_with(ClassNameV0::new(candidate.name.as_str()).decoded())
1665    } else {
1666        canonical_class_key(definition.name.as_str())
1667            == canonical_class_key(candidate.name.as_str())
1668    };
1669    selector_matches
1670        && candidate
1671            .target_style_uri
1672            .as_deref()
1673            .is_none_or(|target_uri| file_uri_equivalent(target_uri, definition.uri.as_str()))
1674}
1675
1676fn source_reference_matches_target_style(
1677    reference: &OmenaQuerySourceSelectorReferenceEditTargetV0,
1678    target_style_uri: Option<&str>,
1679) -> bool {
1680    target_style_uri.is_none_or(|target_uri| {
1681        reference
1682            .target_style_uri
1683            .as_deref()
1684            .is_none_or(|candidate_target_uri| {
1685                file_uri_equivalent(candidate_target_uri, target_uri)
1686            })
1687    })
1688}
1689
1690fn source_selector_occurrences_for_query(
1691    occurrence_index: &OmenaQuerySourceSelectorOccurrenceIndexV0,
1692    selector_name: &str,
1693    target_style_uri: Option<&str>,
1694) -> Vec<OmenaQuerySourceSelectorOccurrenceV0> {
1695    let matching_monikers = occurrence_index
1696        .occurrences
1697        .iter()
1698        .filter(|occurrence| class_names_match(occurrence.selector_name.as_str(), selector_name))
1699        .filter(|occurrence| {
1700            target_style_uri.is_none_or(|target_uri| {
1701                occurrence
1702                    .target_style_uri
1703                    .as_deref()
1704                    .is_some_and(|candidate_uri| file_uri_equivalent(candidate_uri, target_uri))
1705            })
1706        })
1707        .map(|occurrence| occurrence.moniker.clone())
1708        .collect::<BTreeSet<_>>();
1709    occurrences_for_monikers(&occurrence_index.workspace_index, &matching_monikers)
1710        .into_iter()
1711        .filter_map(source_selector_occurrence_from_workspace_occurrence)
1712        .collect()
1713}
1714
1715pub fn summarize_omena_query_workspace_occurrence_index_from_source_occurrences(
1716    occurrences: &[OmenaQuerySourceSelectorOccurrenceV0],
1717    ready_surfaces: Vec<&'static str>,
1718) -> OmenaWorkspaceOccurrenceIndexV0 {
1719    let occurrences = occurrences
1720        .iter()
1721        .map(workspace_occurrence_from_source_occurrence)
1722        .collect::<Vec<_>>();
1723    summarize_omena_query_workspace_occurrence_index_from_occurrences(
1724        occurrences.as_slice(),
1725        ready_surfaces,
1726    )
1727}
1728
1729pub fn summarize_omena_query_workspace_occurrence_index_from_occurrences(
1730    occurrences: &[OmenaWorkspaceOccurrenceV0],
1731    ready_surfaces: Vec<&'static str>,
1732) -> OmenaWorkspaceOccurrenceIndexV0 {
1733    let mut by_moniker: BTreeMap<String, Vec<OmenaWorkspaceOccurrenceV0>> = BTreeMap::new();
1734    let mut by_file: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
1735    for occurrence in occurrences {
1736        let workspace_occurrence = occurrence.clone();
1737        by_file
1738            .entry(workspace_occurrence.uri.clone())
1739            .or_default()
1740            .insert(workspace_occurrence.moniker.clone());
1741        by_moniker
1742            .entry(workspace_occurrence.moniker.clone())
1743            .or_default()
1744            .push(workspace_occurrence);
1745    }
1746    for occurrences in by_moniker.values_mut() {
1747        occurrences.sort();
1748        occurrences.dedup();
1749    }
1750    let by_file = by_file
1751        .into_iter()
1752        .map(|(uri, monikers)| (uri, monikers.into_iter().collect()))
1753        .collect::<BTreeMap<_, _>>();
1754    let moniker_count = by_moniker.len();
1755    let occurrence_count = by_moniker.values().map(Vec::len).sum();
1756    OmenaWorkspaceOccurrenceIndexV0 {
1757        schema_version: "0",
1758        product: "omena-query.workspace-occurrence-index",
1759        moniker_count,
1760        occurrence_count,
1761        by_moniker,
1762        by_file,
1763        ready_surfaces,
1764    }
1765}
1766
1767fn workspace_occurrence_from_source_occurrence(
1768    occurrence: &OmenaQuerySourceSelectorOccurrenceV0,
1769) -> OmenaWorkspaceOccurrenceV0 {
1770    OmenaWorkspaceOccurrenceV0 {
1771        moniker: occurrence.moniker.clone(),
1772        uri: occurrence.uri.clone(),
1773        name: occurrence.selector_name.clone(),
1774        range: occurrence.range,
1775        kind: occurrence.kind,
1776        role: occurrence.role,
1777        surface: occurrence.source,
1778        family: Some(OmenaWorkspaceOccurrenceFamilyV0::CssModuleSelector),
1779        namespace: None,
1780        target_style_uri: occurrence.target_style_uri.clone(),
1781        rename_target: occurrence.rename_target,
1782    }
1783}
1784
1785fn source_selector_occurrence_from_workspace_occurrence(
1786    occurrence: &OmenaWorkspaceOccurrenceV0,
1787) -> Option<OmenaQuerySourceSelectorOccurrenceV0> {
1788    (occurrence.family == Some(OmenaWorkspaceOccurrenceFamilyV0::CssModuleSelector)).then(|| {
1789        OmenaQuerySourceSelectorOccurrenceV0 {
1790            moniker: occurrence.moniker.clone(),
1791            uri: occurrence.uri.clone(),
1792            selector_name: occurrence.name.clone(),
1793            range: occurrence.range,
1794            kind: occurrence.kind,
1795            role: occurrence.role,
1796            source: occurrence.surface,
1797            target_style_uri: occurrence.target_style_uri.clone(),
1798            rename_target: occurrence.rename_target,
1799        }
1800    })
1801}
1802
1803fn workspace_occurrence_kind_from_source_reference_kind(
1804    kind: &str,
1805) -> Option<OmenaWorkspaceOccurrenceKindV0> {
1806    match kind {
1807        "sourceSelectorReference" => Some(OmenaWorkspaceOccurrenceKindV0::SourceSelectorReference),
1808        "sourceSelectorPrefixReference" => {
1809            Some(OmenaWorkspaceOccurrenceKindV0::SourceSelectorPrefixReference)
1810        }
1811        _ => None,
1812    }
1813}
1814
1815fn source_selector_candidate_matches_target_uri(
1816    candidate: &OmenaQuerySourceSelectorCandidateV0,
1817    target_style_uri: Option<&str>,
1818) -> bool {
1819    target_style_uri.is_none_or(|target_uri| {
1820        candidate
1821            .target_style_uri
1822            .as_deref()
1823            .is_none_or(|candidate_target_uri| {
1824                file_uri_equivalent(candidate_target_uri, target_uri)
1825            })
1826    })
1827}
1828
1829fn source_selector_occurrence_moniker(
1830    selector_name: &str,
1831    target_style_uri: Option<&str>,
1832) -> String {
1833    omena_workspace_moniker(OmenaWorkspaceMonikerInput::CssModuleSelector {
1834        target_style_uri,
1835        selector_name,
1836    })
1837}
1838
1839fn class_names_match(left: &str, right: &str) -> bool {
1840    ClassNameV0::new(left).same_as(&ClassNameV0::new(right))
1841}
1842
1843fn reference_location_role_rank(role: &str) -> u8 {
1844    match role {
1845        "definition" => 0,
1846        "reference" => 1,
1847        _ => 2,
1848    }
1849}
1850
1851fn file_uri_equivalent(left: &str, right: &str) -> bool {
1852    if left == right {
1853        return true;
1854    }
1855    match (
1856        file_uri_to_normalized_path(left),
1857        file_uri_to_normalized_path(right),
1858    ) {
1859        (Some(left_path), Some(right_path)) => left_path == right_path,
1860        _ => false,
1861    }
1862}
1863
1864fn file_uri_to_normalized_path(uri: &str) -> Option<PathBuf> {
1865    let raw_path = uri.strip_prefix("file://")?;
1866    Some(normalize_path(PathBuf::from(percent_decode_uri_path(
1867        raw_path,
1868    )?)))
1869}
1870
1871fn percent_decode_uri_path(raw_path: &str) -> Option<String> {
1872    let bytes = raw_path.as_bytes();
1873    let mut decoded = Vec::with_capacity(bytes.len());
1874    let mut index = 0usize;
1875    while index < bytes.len() {
1876        if bytes[index] == b'%' {
1877            let high = bytes.get(index + 1).and_then(|byte| hex_value(*byte))?;
1878            let low = bytes.get(index + 2).and_then(|byte| hex_value(*byte))?;
1879            decoded.push((high << 4) | low);
1880            index += 3;
1881        } else {
1882            decoded.push(bytes[index]);
1883            index += 1;
1884        }
1885    }
1886    String::from_utf8(decoded).ok()
1887}
1888
1889fn hex_value(byte: u8) -> Option<u8> {
1890    match byte {
1891        b'0'..=b'9' => Some(byte - b'0'),
1892        b'a'..=b'f' => Some(byte - b'a' + 10),
1893        b'A'..=b'F' => Some(byte - b'A' + 10),
1894        _ => None,
1895    }
1896}
1897
1898fn normalize_path(path: PathBuf) -> PathBuf {
1899    let mut normalized = PathBuf::new();
1900    for component in path.components() {
1901        match component {
1902            Component::CurDir => {}
1903            Component::ParentDir => {
1904                normalized.pop();
1905            }
1906            Component::Normal(_) | Component::RootDir | Component::Prefix(_) => {
1907                normalized.push(component.as_os_str());
1908            }
1909        }
1910    }
1911    normalized
1912}
1913
1914#[cfg(test)]
1915mod global_class_fallthrough_label_tests {
1916    use super::summarize_omena_query_global_class_fallthrough_diagnostic;
1917    use crate::ParserRangeV0;
1918
1919    #[test]
1920    fn message_shows_decoded_non_ascii_global_filename() {
1921        let diagnostic = summarize_omena_query_global_class_fallthrough_diagnostic(
1922            "chip",
1923            "file:///ws/%EC%83%98%ED%94%8C%EB%B0%B0%EB%84%88.css",
1924            "file:///ws/App.module.css",
1925            ".root {}\n",
1926            ParserRangeV0::default(),
1927        );
1928        assert!(
1929            diagnostic.message.contains("샘플배너.css"),
1930            "{}",
1931            diagnostic.message
1932        );
1933        assert!(
1934            !diagnostic.message.contains("%EC"),
1935            "{}",
1936            diagnostic.message
1937        );
1938    }
1939}
1940
1941#[cfg(test)]
1942mod class_reference_identity_tests {
1943    use super::*;
1944
1945    #[test]
1946    fn references_join_cross_spelling_names_in_direct_and_indexed_paths() {
1947        let style_uri = "file:///workspace/App.module.css";
1948        let source_uri = "file:///workspace/App.tsx";
1949        let definitions = vec![OmenaQueryStyleSelectorDefinitionV0 {
1950            uri: style_uri.to_string(),
1951            name: r"\62 tn".to_string(),
1952            range: ParserRangeV0::default(),
1953        }];
1954        let references = vec![OmenaQuerySourceSelectorReferenceCandidateV0 {
1955            uri: source_uri.to_string(),
1956            kind: "sourceSelectorReference",
1957            name: "btn".to_string(),
1958            range: ParserRangeV0::default(),
1959            source: "omenaQuerySourceSyntaxIndex",
1960            target_style_uri: Some(style_uri.to_string()),
1961        }];
1962
1963        let direct = summarize_omena_query_refs_for_class(
1964            r"\62 tn",
1965            Some(style_uri),
1966            true,
1967            definitions.as_slice(),
1968            references.as_slice(),
1969        );
1970        let occurrence_index = summarize_omena_query_source_selector_occurrence_index(
1971            definitions.as_slice(),
1972            references.as_slice(),
1973        );
1974        let indexed = summarize_omena_query_refs_for_class_from_occurrence_index(
1975            r"\62 tn",
1976            Some(style_uri),
1977            true,
1978            definitions.as_slice(),
1979            &occurrence_index,
1980        );
1981
1982        for summary in [direct, indexed] {
1983            assert_eq!(summary.location_count, 2);
1984            assert!(
1985                summary
1986                    .locations
1987                    .iter()
1988                    .any(|location| { location.role == "reference" && location.uri == source_uri })
1989            );
1990        }
1991    }
1992}