Skip to main content

omena_query/style/
source_refs.rs

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