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