1use std::collections::{BTreeMap, BTreeSet};
2
3use omena_parser::{
4 ParsedAnimationFactKind, ParsedCssModuleComposesEdgeKind, ParsedSassModuleEdgeFactKind,
5 ParsedSelectorFactKind,
6};
7
8use super::cascade_checker::summarize_query_cascade_checker_diagnostics;
9use super::diagnostic_suppressions::apply_omena_query_style_diagnostic_suppressions;
10use super::parser_facade::collect_omena_query_omena_parser_style_facts_raw;
11use super::*;
12
13const LSP_DIAGNOSTIC_TAG_UNNECESSARY: u8 = 1;
14const LSP_DIAGNOSTIC_TAG_DEPRECATED: u8 = 2;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum OmenaQueryExternalModuleModeV0 {
18 Ignored,
19 Sif,
20}
21
22pub fn summarize_omena_query_missing_custom_property_diagnostics(
23 style_uri: &str,
24 source: &str,
25 candidates: &[OmenaQueryStyleHoverCandidateV0],
26) -> Vec<OmenaQueryStyleDiagnosticV0> {
27 let declaration_names = candidates
28 .iter()
29 .filter(|candidate| candidate.kind == "customPropertyDeclaration")
30 .map(|candidate| candidate.name.as_str())
31 .collect::<BTreeSet<_>>();
32 if declaration_names.is_empty() {
33 return Vec::new();
34 }
35
36 let insertion_range = end_of_source_range(source);
37 candidates
38 .iter()
39 .filter(|candidate| {
40 candidate.kind == "customPropertyReference"
41 && !declaration_names.contains(candidate.name.as_str())
42 })
43 .map(|candidate| OmenaQueryStyleDiagnosticV0 {
44 code: "missingCustomProperty",
45 severity: "warning",
46 provenance: vec![
47 "omena-parser.custom-property-facts",
48 "omena-query.style-diagnostics",
49 ],
50 range: candidate.range,
51 message: format!(
52 "CSS custom property '{}' not found in indexed style tokens.",
53 candidate.name
54 ),
55 tags: Vec::new(),
56 create_custom_property: Some(OmenaQueryCreateCustomPropertyActionV0 {
57 uri: style_uri.to_string(),
58 range: insertion_range,
59 new_text: format!("\n\n:root {{\n {}: ;\n}}\n", candidate.name),
60 property_name: candidate.name.clone(),
61 }),
62 })
63 .collect()
64}
65
66pub fn summarize_omena_query_cascade_aware_style_diagnostics(
67 style_uri: &str,
68 source: &str,
69 candidates: &[OmenaQueryStyleHoverCandidateV0],
70) -> Vec<OmenaQueryStyleDiagnosticV0> {
71 let declarations_by_name = candidates
72 .iter()
73 .filter(|candidate| candidate.kind == "customPropertyDeclaration")
74 .map(|candidate| (candidate.name.as_str(), candidate.range))
75 .collect::<BTreeMap<_, _>>();
76
77 let dialect = omena_parser_dialect_for_style_path(style_uri);
78 let mut diagnostics =
79 summarize_static_css_custom_property_fixed_point_from_source(source, dialect)
80 .entries
81 .into_iter()
82 .filter(|entry| entry.guaranteed_invalid)
83 .filter_map(|entry| {
84 declarations_by_name
85 .get(entry.name.as_str())
86 .copied()
87 .map(|range| OmenaQueryStyleDiagnosticV0 {
88 code: "guaranteedInvalidCustomProperty",
89 severity: "warning",
90 provenance: vec![
91 "omena-transform-passes.custom-property-lfp",
92 "omena-query.cascade-aware-diagnostics",
93 ],
94 range,
95 message: format!(
96 "CSS custom property '{}' resolves to the guaranteed-invalid value.",
97 entry.name
98 ),
99 tags: Vec::new(),
100 create_custom_property: None,
101 })
102 })
103 .collect::<Vec<_>>();
104
105 diagnostics.extend(summarize_query_cascade_checker_diagnostics(
106 style_uri, source,
107 ));
108
109 diagnostics
110}
111
112pub fn summarize_omena_query_missing_keyframes_diagnostics(
113 style_uri: &str,
114 source: &str,
115) -> Vec<OmenaQueryStyleDiagnosticV0> {
116 let dialect = omena_parser_dialect_for_style_path(style_uri);
117 let facts = collect_omena_query_omena_parser_style_facts_raw(source, dialect);
118 let declared_keyframes = facts
119 .animations
120 .iter()
121 .filter(|animation| animation.kind == ParsedAnimationFactKind::KeyframesDeclaration)
122 .map(|animation| animation.name.clone())
123 .collect::<BTreeSet<_>>();
124 let mut emitted = BTreeSet::new();
125
126 facts
127 .animations
128 .into_iter()
129 .filter(|animation| animation.kind == ParsedAnimationFactKind::AnimationNameReference)
130 .filter(|animation| !declared_keyframes.contains(animation.name.as_str()))
131 .filter_map(|animation| {
132 let start: u32 = animation.range.start().into();
133 let end: u32 = animation.range.end().into();
134 let byte_span = ParserByteSpanV0 {
135 start: start as usize,
136 end: end as usize,
137 };
138 if !emitted.insert((animation.name.clone(), byte_span.start, byte_span.end)) {
139 return None;
140 }
141 Some((animation, parser_range_for_byte_span(source, byte_span)))
142 })
143 .map(|(animation, range)| OmenaQueryStyleDiagnosticV0 {
144 code: "missingKeyframes",
145 severity: "warning",
146 provenance: vec![
147 "omena-parser.animation-facts",
148 "omena-query.style-diagnostics",
149 ],
150 range,
151 message: format!("@keyframes '{}' not found in this file.", animation.name),
152 tags: Vec::new(),
153 create_custom_property: None,
154 })
155 .collect()
156}
157
158pub fn summarize_omena_query_missing_sass_symbol_diagnostics(
159 style_uri: &str,
160 source: &str,
161) -> Vec<OmenaQueryStyleDiagnosticV0> {
162 let dialect = omena_parser_dialect_for_style_path(style_uri);
163 let facts = collect_omena_query_omena_parser_style_facts_raw(source, dialect);
164 let mut declarations = BTreeSet::<(&'static str, Option<String>, String)>::new();
165 let mut emitted = BTreeSet::new();
166 let mut diagnostics = Vec::new();
167
168 for symbol in facts.sass_symbols {
169 let key = (
170 symbol.symbol_kind,
171 symbol.namespace.clone(),
172 symbol.name.clone(),
173 );
174 if omena_query_sass_symbol_fact_kind_is_declaration(symbol.kind) {
175 declarations.insert(key);
176 continue;
177 }
178 if !omena_query_sass_symbol_fact_kind_is_reference(symbol.kind) {
179 continue;
180 }
181 if declarations.contains(&key) {
182 continue;
183 }
184 if is_omena_query_sass_builtin_symbol_reference_resolved(
185 &facts.sass_module_edges,
186 symbol.symbol_kind,
187 symbol.namespace.as_deref(),
188 symbol.name.as_str(),
189 ) {
190 continue;
191 }
192
193 let start: u32 = symbol.range.start().into();
194 let end: u32 = symbol.range.end().into();
195 let byte_span = ParserByteSpanV0 {
196 start: start as usize,
197 end: end as usize,
198 };
199 if !emitted.insert((
200 symbol.symbol_kind,
201 symbol.namespace.clone(),
202 symbol.name.clone(),
203 byte_span.start,
204 byte_span.end,
205 )) {
206 continue;
207 }
208 diagnostics.push(OmenaQueryStyleDiagnosticV0 {
209 code: "missingSassSymbol",
210 severity: "warning",
211 provenance: vec![
212 "omena-parser.sass-symbol-facts",
213 "omena-query.style-diagnostics",
214 ],
215 range: parser_range_for_byte_span(source, byte_span),
216 message: format!(
217 "{} not found in this file.",
218 format_query_sass_symbol_label(symbol.symbol_kind, symbol.name.as_str())
219 ),
220 tags: Vec::new(),
221 create_custom_property: None,
222 });
223 }
224
225 diagnostics
226}
227
228pub fn summarize_omena_query_sass_import_deprecation_hints(
229 style_uri: &str,
230 source: &str,
231) -> Vec<OmenaQueryStyleDiagnosticV0> {
232 let dialect = omena_parser_dialect_for_style_path(style_uri);
233 if !matches!(
234 dialect,
235 OmenaParserStyleDialect::Scss | OmenaParserStyleDialect::Sass
236 ) {
237 return Vec::new();
238 }
239
240 let facts = collect_omena_query_omena_parser_style_facts_raw(source, dialect);
241 facts
242 .sass_module_edges
243 .into_iter()
244 .filter(|edge| edge.kind == ParsedSassModuleEdgeFactKind::Import)
245 .map(|edge| {
246 let start: u32 = edge.range.start().into();
247 let end: u32 = edge.range.end().into();
248 OmenaQueryStyleDiagnosticV0 {
249 code: "deprecatedSassImport",
250 severity: "information",
251 provenance: vec![
252 "omena-parser.sass-module-edges",
253 "omena-query.sass-import-deprecation-hints",
254 ],
255 range: parser_range_for_byte_span(
256 source,
257 ParserByteSpanV0 {
258 start: start as usize,
259 end: end as usize,
260 },
261 ),
262 message: "Sass @import is deprecated; prefer @use or @forward.".to_string(),
263 tags: vec![LSP_DIAGNOSTIC_TAG_DEPRECATED],
264 create_custom_property: None,
265 }
266 })
267 .collect()
268}
269
270pub fn summarize_omena_query_missing_sass_symbol_diagnostics_for_workspace(
271 target_style_path: &str,
272 style_sources: &[OmenaQueryStyleSourceInputV0],
273 package_manifests: &[OmenaQueryStylePackageManifestV0],
274) -> Vec<OmenaQueryStyleDiagnosticV0> {
275 summarize_omena_query_missing_sass_symbol_diagnostics_for_workspace_with_sifs(
276 target_style_path,
277 style_sources,
278 package_manifests,
279 &[],
280 )
281}
282
283fn summarize_omena_query_missing_sass_symbol_diagnostics_for_workspace_with_sifs(
284 target_style_path: &str,
285 style_sources: &[OmenaQueryStyleSourceInputV0],
286 package_manifests: &[OmenaQueryStylePackageManifestV0],
287 external_sifs: &[OmenaQueryExternalSifInputV0],
288) -> Vec<OmenaQueryStyleDiagnosticV0> {
289 let Some(target) = style_sources
290 .iter()
291 .find(|source| source.style_path == target_style_path)
292 else {
293 return Vec::new();
294 };
295 let style_source_refs = style_sources
296 .iter()
297 .map(|source| (source.style_path.as_str(), source.style_source.as_str()))
298 .collect::<Vec<_>>();
299 let style_fact_entries = collect_omena_query_style_fact_entries(style_source_refs.as_slice());
300 let facts_by_path = style_fact_entries
301 .iter()
302 .map(|entry| (entry.style_path.as_str(), &entry.facts))
303 .collect::<BTreeMap<_, _>>();
304 let resolution =
305 summarize_sass_module_cross_file_resolution(&style_fact_entries, package_manifests);
306 let visible_symbols = collect_visible_sass_symbol_keys(
307 target_style_path,
308 &facts_by_path,
309 &resolution,
310 external_sifs,
311 );
312 let facts = collect_omena_query_omena_parser_style_facts_raw(
313 target.style_source.as_str(),
314 omena_parser_dialect_for_style_path(target_style_path),
315 );
316 let mut emitted = BTreeSet::new();
317 let mut diagnostics = Vec::new();
318
319 for symbol in facts.sass_symbols {
320 if !omena_query_sass_symbol_fact_kind_is_reference(symbol.kind) {
321 continue;
322 }
323 let key = sass_symbol_key(
324 symbol.symbol_kind,
325 symbol.namespace.clone(),
326 symbol.name.clone(),
327 );
328 if visible_symbols.contains(&key) {
329 continue;
330 }
331 if is_omena_query_sass_builtin_symbol_reference_resolved(
332 &facts.sass_module_edges,
333 symbol.symbol_kind,
334 symbol.namespace.as_deref(),
335 symbol.name.as_str(),
336 ) {
337 continue;
338 }
339
340 let start: u32 = symbol.range.start().into();
341 let end: u32 = symbol.range.end().into();
342 let byte_span = ParserByteSpanV0 {
343 start: start as usize,
344 end: end as usize,
345 };
346 if !emitted.insert((
347 symbol.symbol_kind,
348 symbol.namespace.clone(),
349 symbol.name.clone(),
350 byte_span.start,
351 byte_span.end,
352 )) {
353 continue;
354 }
355 diagnostics.push(OmenaQueryStyleDiagnosticV0 {
356 code: "missingSassSymbol",
357 severity: "warning",
358 provenance: vec![
359 "omena-parser.sass-symbol-facts",
360 "omena-query.graph-aware-sass-diagnostics",
361 ],
362 range: parser_range_for_byte_span(target.style_source.as_str(), byte_span),
363 message: format!(
364 "{} not found in the visible Sass module graph.",
365 format_query_sass_symbol_label(symbol.symbol_kind, symbol.name.as_str())
366 ),
367 tags: Vec::new(),
368 create_custom_property: None,
369 });
370 }
371
372 diagnostics
373}
374
375pub fn summarize_omena_query_style_diagnostics_for_file(
376 style_uri: &str,
377 source: &str,
378 candidates: &[OmenaQueryStyleHoverCandidateV0],
379) -> OmenaQueryStyleDiagnosticsForFileV0 {
380 let mut diagnostics =
381 summarize_omena_query_missing_custom_property_diagnostics(style_uri, source, candidates);
382 diagnostics.extend(summarize_omena_query_cascade_aware_style_diagnostics(
383 style_uri, source, candidates,
384 ));
385 diagnostics.extend(summarize_omena_query_missing_keyframes_diagnostics(
386 style_uri, source,
387 ));
388 diagnostics.extend(summarize_omena_query_sass_import_deprecation_hints(
389 style_uri, source,
390 ));
391 diagnostics.extend(summarize_omena_query_missing_sass_symbol_diagnostics(
392 style_uri, source,
393 ));
394 let mut summary = OmenaQueryStyleDiagnosticsForFileV0 {
395 schema_version: "0",
396 product: "omena-query.diagnostics-for-file",
397 file_uri: style_uri.to_string(),
398 file_kind: "style",
399 diagnostic_count: diagnostics.len(),
400 diagnostics,
401 ready_surfaces: vec![
402 "missingCustomPropertyDiagnostics",
403 "cascadeAwareDiagnostics",
404 "missingKeyframesDiagnostics",
405 "sassImportDeprecationHints",
406 "missingSassSymbolDiagnostics",
407 ],
408 };
409 apply_omena_query_style_diagnostic_suppressions(source, &mut summary);
410 summary
411}
412
413pub fn summarize_omena_query_style_diagnostics_for_workspace_file(
414 target_style_path: &str,
415 style_sources: &[OmenaQueryStyleSourceInputV0],
416 source_documents: &[OmenaQuerySourceDocumentInputV0],
417 package_manifests: &[OmenaQueryStylePackageManifestV0],
418 classname_transform: Option<&str>,
419) -> Option<OmenaQueryStyleDiagnosticsForFileV0> {
420 summarize_omena_query_style_diagnostics_for_workspace_file_with_external_mode(
421 target_style_path,
422 style_sources,
423 source_documents,
424 package_manifests,
425 classname_transform,
426 OmenaQueryExternalModuleModeV0::Ignored,
427 )
428}
429
430pub fn summarize_omena_query_style_diagnostics_for_workspace_file_with_external_mode(
431 target_style_path: &str,
432 style_sources: &[OmenaQueryStyleSourceInputV0],
433 source_documents: &[OmenaQuerySourceDocumentInputV0],
434 package_manifests: &[OmenaQueryStylePackageManifestV0],
435 classname_transform: Option<&str>,
436 external_mode: OmenaQueryExternalModuleModeV0,
437) -> Option<OmenaQueryStyleDiagnosticsForFileV0> {
438 summarize_omena_query_style_diagnostics_for_workspace_file_with_external_mode_and_sifs(
439 target_style_path,
440 style_sources,
441 source_documents,
442 package_manifests,
443 classname_transform,
444 external_mode,
445 &[],
446 )
447}
448
449pub fn summarize_omena_query_style_diagnostics_for_workspace_file_with_external_mode_and_sifs(
450 target_style_path: &str,
451 style_sources: &[OmenaQueryStyleSourceInputV0],
452 source_documents: &[OmenaQuerySourceDocumentInputV0],
453 package_manifests: &[OmenaQueryStylePackageManifestV0],
454 classname_transform: Option<&str>,
455 external_mode: OmenaQueryExternalModuleModeV0,
456 external_sifs: &[OmenaQueryExternalSifInputV0],
457) -> Option<OmenaQueryStyleDiagnosticsForFileV0> {
458 let target = style_sources
459 .iter()
460 .find(|source| source.style_path == target_style_path)?;
461 let candidates =
462 summarize_omena_query_style_hover_candidates(target_style_path, &target.style_source)?;
463 let mut summary = summarize_omena_query_style_diagnostics_for_file(
464 target_style_path,
465 &target.style_source,
466 candidates.candidates.as_slice(),
467 );
468 summary
469 .diagnostics
470 .retain(|diagnostic| diagnostic.code != "missingSassSymbol");
471 summary.diagnostics.extend(
472 summarize_omena_query_missing_sass_symbol_diagnostics_for_workspace_with_sifs(
473 target_style_path,
474 style_sources,
475 package_manifests,
476 external_sifs,
477 ),
478 );
479 summary.diagnostics.extend(
480 summarize_omena_query_css_modules_resolution_style_diagnostics(
481 target_style_path,
482 &target.style_source,
483 style_sources,
484 package_manifests,
485 ),
486 );
487 summary
488 .diagnostics
489 .extend(summarize_omena_query_unused_selector_style_diagnostics(
490 target_style_path,
491 &target.style_source,
492 style_sources,
493 source_documents,
494 package_manifests,
495 classname_transform,
496 ));
497 summary.diagnostic_count = summary.diagnostics.len();
498 push_omena_query_ready_surface(
499 &mut summary.ready_surfaces,
500 "cssModulesComposesResolutionDiagnostics",
501 );
502 push_omena_query_ready_surface(
503 &mut summary.ready_surfaces,
504 "cssModulesValueResolutionDiagnostics",
505 );
506 push_omena_query_ready_surface(&mut summary.ready_surfaces, "unusedSelectorDiagnostics");
507 push_omena_query_ready_surface(
508 &mut summary.ready_surfaces,
509 "graphAwareSassSymbolDiagnostics",
510 );
511 if external_mode == OmenaQueryExternalModuleModeV0::Sif {
512 let top_any_external_symbol_ranges =
513 collect_omena_query_external_top_any_sass_symbol_ranges(
514 target_style_path,
515 style_sources,
516 package_manifests,
517 external_sifs,
518 );
519 summary.diagnostics.retain(|diagnostic| {
520 diagnostic.code != "missingSassSymbol"
521 || !top_any_external_symbol_ranges.contains(&diagnostic.range)
522 });
523 summary
524 .diagnostics
525 .extend(summarize_omena_query_external_sif_boundary_diagnostics(
526 target_style_path,
527 style_sources,
528 package_manifests,
529 external_sifs,
530 ));
531 push_omena_query_ready_surface(
532 &mut summary.ready_surfaces,
533 "externalSifBoundaryDiagnostics",
534 );
535 }
536 apply_omena_query_style_diagnostic_suppressions(&target.style_source, &mut summary);
537 summary.diagnostic_count = summary.diagnostics.len();
538 Some(summary)
539}
540
541fn collect_omena_query_external_top_any_sass_symbol_ranges(
542 target_style_path: &str,
543 style_sources: &[OmenaQueryStyleSourceInputV0],
544 package_manifests: &[OmenaQueryStylePackageManifestV0],
545 external_sifs: &[OmenaQueryExternalSifInputV0],
546) -> BTreeSet<ParserRangeV0> {
547 let Some(target) = style_sources
548 .iter()
549 .find(|source| source.style_path == target_style_path)
550 else {
551 return BTreeSet::new();
552 };
553 let style_source_refs = style_sources
554 .iter()
555 .map(|source| (source.style_path.as_str(), source.style_source.as_str()))
556 .collect::<Vec<_>>();
557 let style_fact_entries = collect_omena_query_style_fact_entries(style_source_refs.as_slice());
558 let resolution =
559 summarize_sass_module_cross_file_resolution(&style_fact_entries, package_manifests);
560 let top_any_namespaces = resolution
561 .edges
562 .iter()
563 .filter(|edge| edge.from_style_path == target_style_path)
564 .filter(|edge| edge.status == "external")
565 .filter(|edge| find_omena_query_external_sif(edge.source.as_str(), external_sifs).is_none())
566 .filter_map(|edge| match edge.edge_kind {
567 "sassUse"
568 if edge.namespace_kind == Some("default")
569 || edge.namespace_kind == Some("alias") =>
570 {
571 edge.namespace.clone().map(Some)
572 }
573 "sassUse" if edge.namespace_kind == Some("wildcard") => Some(None),
574 "sassImport" => Some(None),
575 _ => None,
576 })
577 .collect::<BTreeSet<_>>();
578 if top_any_namespaces.is_empty() {
579 return BTreeSet::new();
580 }
581
582 let facts = collect_omena_query_omena_parser_style_facts_raw(
583 target.style_source.as_str(),
584 omena_parser_dialect_for_style_path(target_style_path),
585 );
586 facts
587 .sass_symbols
588 .into_iter()
589 .filter(|symbol| omena_query_sass_symbol_fact_kind_is_reference(symbol.kind))
590 .filter(|symbol| top_any_namespaces.contains(&symbol.namespace))
591 .map(|symbol| {
592 let start: u32 = symbol.range.start().into();
593 let end: u32 = symbol.range.end().into();
594 parser_range_for_byte_span(
595 target.style_source.as_str(),
596 ParserByteSpanV0 {
597 start: start as usize,
598 end: end as usize,
599 },
600 )
601 })
602 .collect()
603}
604
605fn summarize_omena_query_external_sif_boundary_diagnostics(
606 target_style_path: &str,
607 style_sources: &[OmenaQueryStyleSourceInputV0],
608 package_manifests: &[OmenaQueryStylePackageManifestV0],
609 external_sifs: &[OmenaQueryExternalSifInputV0],
610) -> Vec<OmenaQueryStyleDiagnosticV0> {
611 let Some(target) = style_sources
612 .iter()
613 .find(|source| source.style_path == target_style_path)
614 else {
615 return Vec::new();
616 };
617 let style_source_refs = style_sources
618 .iter()
619 .map(|source| (source.style_path.as_str(), source.style_source.as_str()))
620 .collect::<Vec<_>>();
621 let style_fact_entries = collect_omena_query_style_fact_entries(style_source_refs.as_slice());
622 let resolution =
623 summarize_sass_module_cross_file_resolution(&style_fact_entries, package_manifests);
624 let external_sources = resolution
625 .edges
626 .iter()
627 .filter(|edge| edge.from_style_path == target_style_path)
628 .filter(|edge| edge.status == "external")
629 .filter(|edge| find_omena_query_external_sif(edge.source.as_str(), external_sifs).is_none())
630 .map(|edge| edge.source.as_str())
631 .collect::<BTreeSet<_>>();
632 if external_sources.is_empty() {
633 return Vec::new();
634 }
635
636 let facts = collect_omena_query_omena_parser_style_facts_raw(
637 target.style_source.as_str(),
638 omena_parser_dialect_for_style_path(target_style_path),
639 );
640 let mut emitted = BTreeSet::new();
641 facts
642 .sass_module_edges
643 .into_iter()
644 .filter(|edge| external_sources.contains(edge.source.as_str()))
645 .filter_map(|edge| {
646 if !emitted.insert((edge.kind, edge.source.clone())) {
647 return None;
648 }
649 let state = OmenaResolverBoundaryStateV0::missing(
650 None,
651 "SIF mode requires a local SIF artifact for this external Sass module",
652 );
653 let start: u32 = edge.range.start().into();
654 let end: u32 = edge.range.end().into();
655 Some(OmenaQueryStyleDiagnosticV0 {
656 code: "missingExternalSif",
657 severity: "warning",
658 provenance: vec![
659 "omena-resolver.boundary-state",
660 "omena-query.external-sif-boundary-diagnostics",
661 ],
662 range: parser_range_for_byte_span(
663 target.style_source.as_str(),
664 ParserByteSpanV0 {
665 start: start as usize,
666 end: end as usize,
667 },
668 ),
669 message: format!(
670 "External Sass module '{}' is {} ({}); generate or provide a SIF artifact, or use --external ignored.",
671 edge.source, state.state_name, state.top_name
672 ),
673 tags: Vec::new(),
674 create_custom_property: None,
675 })
676 })
677 .collect()
678}
679
680type SassSymbolKey = (&'static str, Option<String>, String);
681
682fn sass_symbol_key(
683 symbol_kind: &'static str,
684 namespace: Option<String>,
685 name: String,
686) -> SassSymbolKey {
687 (symbol_kind, namespace, name)
688}
689
690fn collect_visible_sass_symbol_keys(
691 target_style_path: &str,
692 facts_by_path: &BTreeMap<&str, &OmenaQueryOmenaParserStyleFactsV0>,
693 resolution: &OmenaQuerySassModuleCrossFileResolutionV0,
694 external_sifs: &[OmenaQueryExternalSifInputV0],
695) -> BTreeSet<SassSymbolKey> {
696 let mut visible = BTreeSet::new();
697 if let Some(facts) = facts_by_path.get(target_style_path) {
698 visible.extend(
699 own_sass_symbol_declaration_keys(facts)
700 .into_iter()
701 .map(|(symbol_kind, name)| sass_symbol_key(symbol_kind, None, name)),
702 );
703 }
704
705 for edge in resolution
706 .edges
707 .iter()
708 .filter(|edge| edge.from_style_path == target_style_path)
709 {
710 let exported = if let Some(module_name) = sass_builtin_module_name(edge.source.as_str()) {
711 builtin_sass_symbol_exports(module_name)
712 } else if edge.status == "resolved" {
713 let mut visiting = BTreeSet::new();
714 edge.resolved_style_path
715 .as_deref()
716 .map(|path| {
717 collect_exported_sass_symbol_keys(
718 path,
719 facts_by_path,
720 resolution,
721 external_sifs,
722 &mut visiting,
723 )
724 })
725 .unwrap_or_default()
726 } else if edge.status == "external" {
727 find_omena_query_external_sif(edge.source.as_str(), external_sifs)
728 .map(|sif| collect_sif_exported_sass_symbol_keys(&sif.sif))
729 .unwrap_or_default()
730 } else {
731 BTreeSet::new()
732 };
733
734 match edge.edge_kind {
735 "sassUse"
736 if edge.namespace_kind == Some("default")
737 || edge.namespace_kind == Some("alias") =>
738 {
739 if let Some(namespace) = edge.namespace.clone() {
740 visible.extend(exported.into_iter().map(|(symbol_kind, name)| {
741 sass_symbol_key(symbol_kind, Some(namespace.clone()), name)
742 }));
743 }
744 }
745 "sassUse" if edge.namespace_kind == Some("wildcard") => {
746 visible.extend(
747 exported
748 .into_iter()
749 .map(|(symbol_kind, name)| sass_symbol_key(symbol_kind, None, name)),
750 );
751 }
752 "sassImport" => {
753 visible.extend(
754 exported
755 .into_iter()
756 .map(|(symbol_kind, name)| sass_symbol_key(symbol_kind, None, name)),
757 );
758 }
759 _ => {}
760 }
761 }
762
763 visible
764}
765
766fn collect_exported_sass_symbol_keys(
767 style_path: &str,
768 facts_by_path: &BTreeMap<&str, &OmenaQueryOmenaParserStyleFactsV0>,
769 resolution: &OmenaQuerySassModuleCrossFileResolutionV0,
770 external_sifs: &[OmenaQueryExternalSifInputV0],
771 visiting: &mut BTreeSet<String>,
772) -> BTreeSet<(&'static str, String)> {
773 if !visiting.insert(style_path.to_string()) {
774 return BTreeSet::new();
775 }
776
777 let mut exported = facts_by_path
778 .get(style_path)
779 .map(|facts| own_sass_symbol_declaration_keys(facts))
780 .unwrap_or_default();
781
782 for edge in resolution
783 .edges
784 .iter()
785 .filter(|edge| edge.from_style_path == style_path)
786 .filter(|edge| edge.edge_kind == "sassForward" || edge.edge_kind == "sassImport")
787 {
788 let module_exports =
789 if let Some(module_name) = sass_builtin_module_name(edge.source.as_str()) {
790 builtin_sass_symbol_exports(module_name)
791 } else if edge.status == "resolved" {
792 edge.resolved_style_path
793 .as_deref()
794 .map(|path| {
795 collect_exported_sass_symbol_keys(
796 path,
797 facts_by_path,
798 resolution,
799 external_sifs,
800 visiting,
801 )
802 })
803 .unwrap_or_default()
804 } else if edge.status == "external" {
805 find_omena_query_external_sif(edge.source.as_str(), external_sifs)
806 .map(|sif| collect_sif_exported_sass_symbol_keys(&sif.sif))
807 .unwrap_or_default()
808 } else {
809 BTreeSet::new()
810 };
811
812 for (symbol_kind, name) in module_exports {
813 if !sass_forward_visibility_allows(edge, symbol_kind, name.as_str()) {
814 continue;
815 }
816 let exported_name = if edge.edge_kind == "sassForward" {
817 apply_sass_forward_prefix(edge.forward_prefix.as_deref(), name.as_str())
818 } else {
819 name
820 };
821 exported.insert((symbol_kind, exported_name));
822 }
823 }
824
825 visiting.remove(style_path);
826 exported
827}
828
829fn own_sass_symbol_declaration_keys(
830 facts: &OmenaQueryOmenaParserStyleFactsV0,
831) -> BTreeSet<(&'static str, String)> {
832 facts
833 .sass_symbol_facts
834 .iter()
835 .filter(|fact| is_omena_query_sass_symbol_declaration_kind(fact.kind))
836 .map(|fact| (fact.symbol_kind, fact.name.clone()))
837 .collect()
838}
839
840fn collect_sif_exported_sass_symbol_keys(
841 sif: &omena_sif::OmenaSifV1,
842) -> BTreeSet<(&'static str, String)> {
843 let mut exported = BTreeSet::new();
844 exported.extend(sif.exports.variables.iter().map(|variable| {
845 (
846 "variable",
847 variable.name.trim_start_matches('$').to_string(),
848 )
849 }));
850 exported.extend(
851 sif.exports
852 .mixins
853 .iter()
854 .map(|mixin| ("mixin", mixin.name.clone())),
855 );
856 exported.extend(
857 sif.exports
858 .functions
859 .iter()
860 .map(|function| ("function", function.name.clone())),
861 );
862 exported
863}
864
865fn find_omena_query_external_sif<'a>(
866 canonical_url: &str,
867 external_sifs: &'a [OmenaQueryExternalSifInputV0],
868) -> Option<&'a OmenaQueryExternalSifInputV0> {
869 external_sifs.iter().find(|input| {
870 input.canonical_url == canonical_url || input.sif.canonical_url == canonical_url
871 })
872}
873
874fn sass_forward_visibility_allows(
875 edge: &OmenaQuerySassModuleEdgeResolutionV0,
876 symbol_kind: &'static str,
877 name: &str,
878) -> bool {
879 let prefixed = apply_sass_forward_prefix(edge.forward_prefix.as_deref(), name);
880 let matches_filter = |filter_name: &String| {
881 filter_name == name
882 || filter_name == prefixed.as_str()
883 || filter_name.trim_start_matches('$') == name
884 || filter_name.trim_start_matches('$') == prefixed.as_str()
885 || (symbol_kind != "variable" && filter_name.trim_start_matches('@') == name)
886 };
887 match edge.visibility_filter_kind {
888 Some("show") => edge.visibility_filter_names.iter().any(matches_filter),
889 Some("hide") => !edge.visibility_filter_names.iter().any(matches_filter),
890 _ => true,
891 }
892}
893
894fn apply_sass_forward_prefix(prefix: Option<&str>, name: &str) -> String {
895 match prefix {
896 Some(prefix) if prefix.contains('*') => prefix.replace('*', name),
897 Some(prefix) => format!("{prefix}{name}"),
898 None => name.to_string(),
899 }
900}
901
902fn is_omena_query_sass_builtin_symbol_reference_resolved(
903 edges: &[omena_parser::ParsedSassModuleEdgeFact],
904 symbol_kind: &'static str,
905 namespace: Option<&str>,
906 name: &str,
907) -> bool {
908 edges
909 .iter()
910 .filter(|edge| edge.kind == ParsedSassModuleEdgeFactKind::Use)
911 .filter_map(|edge| {
912 sass_builtin_module_name(edge.source.as_str()).map(|module| (edge, module))
913 })
914 .any(|(edge, module)| {
915 let namespace_matches =
916 match (namespace, edge.namespace_kind, edge.namespace.as_deref()) {
917 (Some(reference_namespace), Some("default" | "alias"), Some(use_namespace)) => {
918 reference_namespace == use_namespace
919 }
920 (None, Some("wildcard"), _) => true,
921 _ => false,
922 };
923 namespace_matches && sass_builtin_module_has_symbol(module, symbol_kind, name)
924 })
925}
926
927fn sass_builtin_module_name(source: &str) -> Option<&str> {
928 source.strip_prefix("sass:")
929}
930
931fn builtin_sass_symbol_exports(module: &str) -> BTreeSet<(&'static str, String)> {
932 let mut exports = BTreeSet::new();
933 for name in sass_builtin_module_function_names(module) {
934 exports.insert(("function", (*name).to_string()));
935 }
936 for name in sass_builtin_module_mixin_names(module) {
937 exports.insert(("mixin", (*name).to_string()));
938 }
939 for name in sass_builtin_module_variable_names(module) {
940 exports.insert(("variable", (*name).to_string()));
941 }
942 exports
943}
944
945fn sass_builtin_module_has_symbol(module: &str, symbol_kind: &'static str, name: &str) -> bool {
946 match symbol_kind {
947 "function" => sass_builtin_module_function_names(module).contains(&name),
948 "mixin" => sass_builtin_module_mixin_names(module).contains(&name),
949 "variable" => sass_builtin_module_variable_names(module).contains(&name),
950 _ => false,
951 }
952}
953
954fn sass_builtin_module_function_names(module: &str) -> &'static [&'static str] {
955 match module {
956 "color" => &[
957 "adjust",
958 "alpha",
959 "blue",
960 "channel",
961 "change",
962 "complement",
963 "desaturate",
964 "fade-in",
965 "fade-out",
966 "grayscale",
967 "green",
968 "hsl",
969 "hsla",
970 "hue",
971 "ie-hex-str",
972 "invert",
973 "is-legacy",
974 "is-missing",
975 "is-powerless",
976 "lighten",
977 "lightness",
978 "mix",
979 "opacify",
980 "opacity",
981 "red",
982 "same",
983 "saturate",
984 "saturation",
985 "scale",
986 "space",
987 "to-gamut",
988 "to-space",
989 "transparentize",
990 ],
991 "math" => &[
992 "abs",
993 "acos",
994 "asin",
995 "atan",
996 "atan2",
997 "ceil",
998 "clamp",
999 "compatible",
1000 "cos",
1001 "div",
1002 "floor",
1003 "hypot",
1004 "is-unitless",
1005 "log",
1006 "max",
1007 "min",
1008 "percentage",
1009 "pow",
1010 "random",
1011 "round",
1012 "sin",
1013 "sqrt",
1014 "tan",
1015 "unit",
1016 ],
1017 "list" => &[
1018 "append",
1019 "index",
1020 "is-bracketed",
1021 "join",
1022 "length",
1023 "separator",
1024 "set-nth",
1025 "slash",
1026 "nth",
1027 "zip",
1028 ],
1029 "map" => &[
1030 "deep-merge",
1031 "deep-remove",
1032 "get",
1033 "has-key",
1034 "keys",
1035 "merge",
1036 "remove",
1037 "set",
1038 "values",
1039 ],
1040 "string" => &[
1041 "index",
1042 "insert",
1043 "length",
1044 "quote",
1045 "slice",
1046 "split",
1047 "to-lower-case",
1048 "to-upper-case",
1049 "unique-id",
1050 "unquote",
1051 ],
1052 "selector" => &[
1053 "append",
1054 "extend",
1055 "is-superselector",
1056 "nest",
1057 "parse",
1058 "replace",
1059 "simple-selectors",
1060 "unify",
1061 ],
1062 "meta" => &[
1063 "accepts-content",
1064 "calc-args",
1065 "calc-name",
1066 "call",
1067 "content-exists",
1068 "feature-exists",
1069 "function-exists",
1070 "get-function",
1071 "global-variable-exists",
1072 "inspect",
1073 "keywords",
1074 "mixin-exists",
1075 "module-functions",
1076 "module-mixins",
1077 "module-variables",
1078 "type-of",
1079 "variable-exists",
1080 ],
1081 _ => &[],
1082 }
1083}
1084
1085fn sass_builtin_module_mixin_names(module: &str) -> &'static [&'static str] {
1086 match module {
1087 "meta" => &["load-css"],
1088 _ => &[],
1089 }
1090}
1091
1092fn sass_builtin_module_variable_names(module: &str) -> &'static [&'static str] {
1093 match module {
1094 "math" => &["e", "epsilon", "max-safe-integer", "min-safe-integer", "pi"],
1095 _ => &[],
1096 }
1097}
1098
1099pub fn summarize_omena_query_css_modules_resolution_style_diagnostics(
1100 target_style_path: &str,
1101 target_source: &str,
1102 style_sources: &[OmenaQueryStyleSourceInputV0],
1103 package_manifests: &[OmenaQueryStylePackageManifestV0],
1104) -> Vec<OmenaQueryStyleDiagnosticV0> {
1105 let style_source_refs = style_sources
1106 .iter()
1107 .map(|source| (source.style_path.as_str(), source.style_source.as_str()))
1108 .collect::<Vec<_>>();
1109 let style_fact_entries = collect_omena_query_style_fact_entries(style_source_refs.as_slice());
1110 let available_style_paths = style_fact_entries
1111 .iter()
1112 .map(|entry| entry.style_path.as_str())
1113 .collect::<BTreeSet<_>>();
1114 let facts_by_path = style_fact_entries
1115 .iter()
1116 .map(|entry| (entry.style_path.as_str(), entry.facts.clone()))
1117 .collect::<BTreeMap<_, _>>();
1118 let dialect = omena_parser_dialect_for_style_path(target_style_path);
1119 let target_facts = collect_omena_query_omena_parser_style_facts_raw(target_source, dialect);
1120 let mut diagnostics = Vec::new();
1121
1122 for edge in target_facts.css_module_composes_edges {
1123 if edge.kind == ParsedCssModuleComposesEdgeKind::Global {
1124 continue;
1125 }
1126 let start: u32 = edge.range.start().into();
1127 let end: u32 = edge.range.end().into();
1128 let range = parser_range_for_byte_span(
1129 target_source,
1130 ParserByteSpanV0 {
1131 start: start as usize,
1132 end: end as usize,
1133 },
1134 );
1135 let target_style = if edge.kind == ParsedCssModuleComposesEdgeKind::External {
1136 let Some(source) = edge.import_source.as_deref() else {
1137 continue;
1138 };
1139 let Some(resolved_style_path) = resolve_style_module_source(
1140 target_style_path,
1141 source,
1142 &available_style_paths,
1143 package_manifests,
1144 ) else {
1145 diagnostics.push(OmenaQueryStyleDiagnosticV0 {
1146 code: "missingComposedModule",
1147 severity: "warning",
1148 provenance: vec![
1149 "omena-parser.css-modules-composes-facts",
1150 "omena-resolver.style-module-resolution",
1151 ],
1152 range,
1153 message: format!("Cannot resolve composed CSS Module '{}'.", source),
1154 tags: Vec::new(),
1155 create_custom_property: None,
1156 });
1157 continue;
1158 };
1159 resolved_style_path
1160 } else {
1161 target_style_path.to_string()
1162 };
1163 let target_class_names = facts_by_path
1164 .get(target_style.as_str())
1165 .map(|facts| facts.class_selector_names.as_slice())
1166 .unwrap_or(&[])
1167 .iter()
1168 .map(String::as_str)
1169 .collect::<BTreeSet<_>>();
1170
1171 for target_name in edge.target_names {
1172 if target_class_names.contains(target_name.as_str()) {
1173 continue;
1174 }
1175 let message = if let Some(source) = edge.import_source.as_deref() {
1176 format!(
1177 "Selector '.{}' not found in composed module '{}'.",
1178 target_name, source
1179 )
1180 } else {
1181 format!(
1182 "Selector '.{}' not found in this file for composes.",
1183 target_name
1184 )
1185 };
1186 diagnostics.push(OmenaQueryStyleDiagnosticV0 {
1187 code: "missingComposedSelector",
1188 severity: "warning",
1189 provenance: vec![
1190 "omena-parser.css-modules-composes-facts",
1191 "omena-query.css-modules-resolution-diagnostics",
1192 ],
1193 range,
1194 message,
1195 tags: Vec::new(),
1196 create_custom_property: None,
1197 });
1198 }
1199 }
1200
1201 let mut reported_missing_value_modules = BTreeSet::new();
1202 for edge in target_facts.css_module_value_import_edges {
1203 let start: u32 = edge.range.start().into();
1204 let end: u32 = edge.range.end().into();
1205 let range = parser_range_for_byte_span(
1206 target_source,
1207 ParserByteSpanV0 {
1208 start: start as usize,
1209 end: end as usize,
1210 },
1211 );
1212 let Some(resolved_style_path) = resolve_style_module_source(
1213 target_style_path,
1214 &edge.import_source,
1215 &available_style_paths,
1216 package_manifests,
1217 ) else {
1218 if reported_missing_value_modules.insert(edge.import_source.clone()) {
1219 diagnostics.push(OmenaQueryStyleDiagnosticV0 {
1220 code: "missingValueModule",
1221 severity: "warning",
1222 provenance: vec![
1223 "omena-parser.css-modules-value-facts",
1224 "omena-resolver.style-module-resolution",
1225 ],
1226 range,
1227 message: format!(
1228 "Cannot resolve imported @value module '{}'.",
1229 edge.import_source
1230 ),
1231 tags: Vec::new(),
1232 create_custom_property: None,
1233 });
1234 }
1235 continue;
1236 };
1237 let target_value_names = facts_by_path
1238 .get(resolved_style_path.as_str())
1239 .map(|facts| facts.css_module_value_definition_names.as_slice())
1240 .unwrap_or(&[])
1241 .iter()
1242 .map(String::as_str)
1243 .collect::<BTreeSet<_>>();
1244 if target_value_names.contains(edge.remote_name.as_str()) {
1245 continue;
1246 }
1247 let message = if edge.local_name == edge.remote_name {
1248 format!(
1249 "@value '{}' not found in '{}'.",
1250 edge.remote_name, edge.import_source
1251 )
1252 } else {
1253 format!(
1254 "@value '{}' not found in '{}' for local binding '{}'.",
1255 edge.remote_name, edge.import_source, edge.local_name
1256 )
1257 };
1258 diagnostics.push(OmenaQueryStyleDiagnosticV0 {
1259 code: "missingImportedValue",
1260 severity: "warning",
1261 provenance: vec![
1262 "omena-parser.css-modules-value-facts",
1263 "omena-query.css-modules-resolution-diagnostics",
1264 ],
1265 range,
1266 message,
1267 tags: Vec::new(),
1268 create_custom_property: None,
1269 });
1270 }
1271
1272 diagnostics
1273}
1274
1275pub fn summarize_omena_query_unused_selector_style_diagnostics(
1276 target_style_path: &str,
1277 target_source: &str,
1278 style_sources: &[OmenaQueryStyleSourceInputV0],
1279 source_documents: &[OmenaQuerySourceDocumentInputV0],
1280 package_manifests: &[OmenaQueryStylePackageManifestV0],
1281 classname_transform: Option<&str>,
1282) -> Vec<OmenaQueryStyleDiagnosticV0> {
1283 if source_documents.is_empty() {
1284 return Vec::new();
1285 }
1286
1287 let style_source_refs = style_sources
1288 .iter()
1289 .map(|source| (source.style_path.as_str(), source.style_source.as_str()))
1290 .collect::<Vec<_>>();
1291 let style_fact_entries = collect_omena_query_style_fact_entries(style_source_refs.as_slice());
1292 let available_style_paths = style_fact_entries
1293 .iter()
1294 .map(|entry| entry.style_path.as_str())
1295 .collect::<BTreeSet<_>>();
1296 let facts_by_path = style_fact_entries
1297 .iter()
1298 .map(|entry| (entry.style_path.as_str(), entry.facts.clone()))
1299 .collect::<BTreeMap<_, _>>();
1300 let aliases_by_path = collect_classname_transform_aliases(&facts_by_path, classname_transform);
1301 let (mut used_selectors, unresolved_dynamic_usage) =
1302 collect_omena_query_source_selector_usage_by_style(
1303 &available_style_paths,
1304 source_documents,
1305 package_manifests,
1306 &aliases_by_path,
1307 );
1308 if unresolved_dynamic_usage.contains(target_style_path) {
1309 return Vec::new();
1310 }
1311
1312 let composes_graph = collect_css_modules_composes_adjacency(
1313 &facts_by_path,
1314 &available_style_paths,
1315 package_manifests,
1316 );
1317 propagate_omena_query_composes_usage(&composes_graph, &mut used_selectors);
1318
1319 let dialect = omena_parser_dialect_for_style_path(target_style_path);
1320 let target_facts = collect_omena_query_omena_parser_style_facts_raw(target_source, dialect);
1321 let used_in_target = used_selectors
1322 .get(target_style_path)
1323 .cloned()
1324 .unwrap_or_default();
1325 let mut emitted = BTreeSet::new();
1326
1327 target_facts
1328 .selectors
1329 .into_iter()
1330 .filter(|selector| selector.kind == ParsedSelectorFactKind::Class)
1331 .filter(|selector| !used_in_target.contains(selector.name.as_str()))
1332 .filter_map(|selector| {
1333 let start: u32 = selector.range.start().into();
1334 let end: u32 = selector.range.end().into();
1335 if !emitted.insert(selector.name.clone()) {
1336 return None;
1337 }
1338 Some(OmenaQueryStyleDiagnosticV0 {
1339 code: "unusedSelector",
1340 severity: "hint",
1341 provenance: vec![
1342 "omena-parser.selector-facts",
1343 "omena-query.source-selector-usage",
1344 ],
1345 range: parser_range_for_byte_span(
1346 target_source,
1347 ParserByteSpanV0 {
1348 start: start as usize,
1349 end: end as usize,
1350 },
1351 ),
1352 message: format!("Selector '.{}' is declared but never used.", selector.name),
1353 tags: vec![LSP_DIAGNOSTIC_TAG_UNNECESSARY],
1354 create_custom_property: None,
1355 })
1356 })
1357 .collect()
1358}
1359
1360fn collect_omena_query_source_selector_usage_by_style(
1361 available_style_paths: &BTreeSet<&str>,
1362 source_documents: &[OmenaQuerySourceDocumentInputV0],
1363 package_manifests: &[OmenaQueryStylePackageManifestV0],
1364 aliases_by_path: &BTreeMap<String, BTreeMap<String, BTreeSet<String>>>,
1365) -> (BTreeMap<String, BTreeSet<String>>, BTreeSet<String>) {
1366 let mut used_selectors = BTreeMap::<String, BTreeSet<String>>::new();
1367 let mut unresolved_dynamic_usage = BTreeSet::<String>::new();
1368
1369 for document in source_documents {
1370 let imports = summarize_omena_query_source_import_declarations_for_source_language(
1371 document.source_path.as_str(),
1372 &document.source_source,
1373 None,
1374 );
1375 let mut imported_style_bindings = Vec::new();
1376 let mut classnames_bind_bindings = Vec::new();
1377 for import in imports.imports {
1378 if import.specifier == "classnames/bind" {
1379 classnames_bind_bindings.push(import.binding);
1380 continue;
1381 }
1382 let Some(style_path) = resolve_style_module_source(
1383 &document.source_path,
1384 &import.specifier,
1385 available_style_paths,
1386 package_manifests,
1387 ) else {
1388 continue;
1389 };
1390 imported_style_bindings.push(OmenaQuerySourceImportedStyleBindingV0 {
1391 binding: import.binding,
1392 style_uri: style_path,
1393 });
1394 }
1395 if imported_style_bindings.is_empty() {
1396 continue;
1397 }
1398
1399 let index = summarize_omena_query_source_syntax_index_for_source_language(
1400 document.source_path.as_str(),
1401 &document.source_source,
1402 None,
1403 imported_style_bindings,
1404 classnames_bind_bindings,
1405 );
1406 for reference in index.selector_references {
1407 let Some(target_style_path) = reference.target_style_uri else {
1408 continue;
1409 };
1410 let Some(selector_name) = reference.selector_name.or_else(|| {
1411 source_reference_text_selector_name(&document.source_source, reference.byte_span)
1412 }) else {
1413 unresolved_dynamic_usage.insert(target_style_path);
1414 continue;
1415 };
1416 let used_for_style = used_selectors.entry(target_style_path.clone()).or_default();
1417 if let Some(canonical_names) = aliases_by_path
1418 .get(target_style_path.as_str())
1419 .and_then(|aliases| aliases.get(selector_name.as_str()))
1420 {
1421 used_for_style.extend(canonical_names.iter().cloned());
1422 } else {
1423 used_for_style.insert(selector_name);
1424 }
1425 }
1426 }
1427
1428 (used_selectors, unresolved_dynamic_usage)
1429}
1430
1431fn collect_classname_transform_aliases(
1432 facts_by_path: &BTreeMap<&str, OmenaQueryOmenaParserStyleFactsV0>,
1433 classname_transform: Option<&str>,
1434) -> BTreeMap<String, BTreeMap<String, BTreeSet<String>>> {
1435 let mut aliases_by_path = BTreeMap::<String, BTreeMap<String, BTreeSet<String>>>::new();
1436 for (style_path, facts) in facts_by_path {
1437 let aliases = aliases_by_path
1438 .entry((*style_path).to_string())
1439 .or_default();
1440 for selector_name in &facts.class_selector_names {
1441 for alias in classname_transform_aliases(selector_name.as_str(), classname_transform) {
1442 aliases
1443 .entry(alias)
1444 .or_default()
1445 .insert(selector_name.clone());
1446 }
1447 }
1448 }
1449 aliases_by_path
1450}
1451
1452fn classname_transform_aliases(name: &str, classname_transform: Option<&str>) -> Vec<String> {
1453 match classname_transform.unwrap_or("asIs") {
1454 "camelCase" => keep_original_plus_transformed(name, to_ascii_camel_case(name)),
1455 "camelCaseOnly" => vec![to_ascii_camel_case(name)],
1456 "dashes" => keep_original_plus_transformed(name, dashes_to_ascii_camel(name)),
1457 "dashesOnly" => vec![dashes_to_ascii_camel(name)],
1458 _ => vec![name.to_string()],
1459 }
1460}
1461
1462fn keep_original_plus_transformed(name: &str, transformed: String) -> Vec<String> {
1463 if transformed == name {
1464 vec![name.to_string()]
1465 } else {
1466 vec![name.to_string(), transformed]
1467 }
1468}
1469
1470fn dashes_to_ascii_camel(name: &str) -> String {
1471 transform_ascii_separated_name(name, |byte| byte == b'-')
1472}
1473
1474fn to_ascii_camel_case(name: &str) -> String {
1475 transform_ascii_separated_name(name, |byte| byte == b'-' || byte == b'_' || byte == b' ')
1476}
1477
1478fn transform_ascii_separated_name(name: &str, is_separator: impl Fn(u8) -> bool) -> String {
1479 let mut output = String::with_capacity(name.len());
1480 let mut capitalize_next = false;
1481 for byte in name.bytes() {
1482 if is_separator(byte) {
1483 capitalize_next = true;
1484 continue;
1485 }
1486 if capitalize_next {
1487 output.push((byte as char).to_ascii_uppercase());
1488 capitalize_next = false;
1489 continue;
1490 }
1491 output.push(byte as char);
1492 }
1493 output
1494}
1495
1496fn propagate_omena_query_composes_usage(
1497 composes_graph: &BTreeMap<CssModulesComposesNode, BTreeSet<CssModulesComposesNode>>,
1498 used_selectors: &mut BTreeMap<String, BTreeSet<String>>,
1499) {
1500 let mut used_nodes = used_selectors
1501 .iter()
1502 .flat_map(|(style_path, selectors)| {
1503 selectors
1504 .iter()
1505 .map(|selector_name| CssModulesComposesNode {
1506 style_path: style_path.clone(),
1507 selector_name: selector_name.clone(),
1508 })
1509 })
1510 .collect::<BTreeSet<_>>();
1511
1512 let mut changed = true;
1513 while changed {
1514 changed = false;
1515 for (owner, targets) in composes_graph {
1516 if !used_nodes.contains(owner) {
1517 continue;
1518 }
1519 for target in targets {
1520 if used_nodes.insert(target.clone()) {
1521 used_selectors
1522 .entry(target.style_path.clone())
1523 .or_default()
1524 .insert(target.selector_name.clone());
1525 changed = true;
1526 }
1527 }
1528 }
1529 }
1530}