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