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 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
492pub 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 let global_file =
513 percent_decode_uri_path(global_file_label).unwrap_or_else(|| global_file_label.to_string());
514 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
617pub 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
743pub 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 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
1052fn 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(¤t);
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}