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