1use super::*;
2use omena_parser::{
3 ParsedSassIncludeFact, ParsedSelectorFact, ParsedStyleFacts, ParsedVariableFact,
4};
5use omena_syntax::{
6 css_keyword,
7 ident::{CanonicalClassKeyV0, ClassNameV0, is_ascii_word_continue, is_css_name_continue},
8};
9use std::cell::RefCell;
10use std::cmp::Ordering;
11use std::path::{Path, PathBuf};
12
13mod cascade_position;
14mod code_actions;
15mod completion;
16mod cross_file_hypergraph;
17mod cross_file_summary;
18mod diagnostic_suppressions;
19mod diagnostics;
20mod dynamic_classname;
21mod insights;
22mod module_interface;
23mod origin_inputs;
24mod parser_facade;
25mod registered_property_values;
26#[cfg(feature = "salsa-memo")]
27mod salsa_memo;
28mod sass;
29mod source_refs;
30mod stylesheet_evaluation;
31mod substrate;
32mod transform;
33
34fn canonical_class_key(name: &str) -> CanonicalClassKeyV0 {
35 ClassNameV0::new(name).canonical_key()
36}
37
38#[cfg(test)]
39pub(crate) use cascade_checker::cascade_declarations_collect_probe;
40pub use cascade_position::*;
41pub use code_actions::*;
42pub use completion::*;
43#[cfg(feature = "hypergraph-monotone-fact-propagation")]
44pub use cross_file_hypergraph::*;
45use cross_file_summary::summarize_omena_query_cross_file_summary;
46#[cfg(any(test, feature = "test-support"))]
47pub use cross_file_summary::{
48 read_workspace_cross_file_summary_direct_recompute_count_for_test,
49 read_workspace_cross_file_summary_internal_compute_count_for_test,
50 reset_workspace_cross_file_summary_direct_recompute_count_for_test,
51 reset_workspace_cross_file_summary_internal_compute_count_for_test,
52};
53pub use cross_file_summary::{
54 summarize_omena_query_categorical_design_system_cross_project_summary,
55 summarize_omena_query_m4_axis_c_readiness,
56 summarize_omena_query_source_selector_reference_cross_file_summary,
57 summarize_omena_query_source_selector_reference_cross_file_summary_with_resolution_inputs,
58 summarize_omena_query_workspace_cross_file_summary,
59 summarize_omena_query_workspace_cross_file_summary_with_resolution_inputs,
60};
61#[cfg(test)]
62pub(crate) use diagnostics::collect_omena_query_visible_sass_symbol_keys_for_workspace_file;
63pub use diagnostics::*;
64pub use dynamic_classname::*;
65pub use insights::*;
66use module_interface::{
67 EmittedClassNameIndexV0, summarize_css_modules_interface_bundle_from_projections,
68};
69pub use module_interface::{
70 OmenaQueryCssModuleClassExportV0, OmenaQueryCssModuleClassReferenceV0,
71 OmenaQueryCssModuleIcssExportV0, OmenaQueryCssModuleInterfaceV0,
72 OmenaQueryCssModulesInterfaceBundleV0, OmenaQueryCssModulesInterfaceSummaryViewV0,
73 render_omena_query_css_module_typescript_declaration,
74 render_omena_query_css_modules_interface_json,
75 summarize_omena_query_css_modules_interface_summary_view,
76};
77pub use origin_inputs::*;
78#[cfg(test)]
79pub(crate) use parser_facade::style_facts_collect_probe;
80pub use parser_facade::{
81 OmenaQueryStyleFrameRefreshFactsV0, OmenaQueryStyleFrameRefreshParseCacheV0,
82 summarize_omena_query_omena_parser_css_modules_intermediate,
83 summarize_omena_query_omena_parser_lex, summarize_omena_query_omena_parser_style_facts,
84 summarize_omena_query_sass_module_source_edges, summarize_omena_query_style_document,
85 summarize_omena_query_style_frame_refresh_facts_with_reuse,
86};
87use parser_facade::{
88 collect_omena_query_omena_parser_style_facts_raw,
89 collect_omena_query_style_facts_with_icss_values_raw, omena_parser_dialect_for_style_path,
90 omena_parser_style_dialect_label, omena_query_sass_symbol_fact_kind_is_declaration,
91 omena_query_sass_symbol_fact_kind_is_reference,
92 summarize_omena_query_omena_parser_style_facts_from_facts,
93};
94#[cfg(feature = "salsa-memo")]
95use parser_facade::{
96 collect_omena_query_style_facts_with_icss_values_from_parse,
97 parse_omena_query_omena_parser_style_source,
98};
99pub use registered_property_values::*;
100#[cfg(feature = "salsa-memo")]
101pub use salsa_memo::*;
102pub use sass::*;
103pub use source_refs::*;
104pub use substrate::*;
105#[cfg(test)]
106pub(crate) use transform::LINKED_FALLBACK_EXACT_TOKEN_REASON;
107pub use transform::*;
108
109mod cascade_checker;
110
111#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
112#[serde(rename_all = "camelCase")]
113pub struct OmenaQueryCascadeSectionOutcomeV0 {
114 pub schema_version: &'static str,
115 pub product: &'static str,
116 pub selector: String,
117 pub property: String,
118 pub winning_value: String,
119}
120
121#[deprecated(
126 since = "0.4.0",
127 note = "use OmenaQueryCascadeSectionOutcomeV0; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
128)]
129#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
130#[serde(rename_all = "camelCase")]
131pub struct OmenaQueryCascadeSiteOutcomeV0 {
132 pub schema_version: &'static str,
133 pub product: &'static str,
134 pub selector: String,
135 pub property: String,
136 pub winning_value: String,
137}
138
139#[allow(deprecated)]
145pub fn summarize_omena_query_cascade_section_outcomes_from_source(
146 source: &str,
147) -> Vec<OmenaQueryCascadeSectionOutcomeV0> {
148 let mut outcomes = cascade_checker::collect_query_replica_ensemble_site_outcomes(source)
149 .into_iter()
150 .filter_map(|property_outcome| {
151 let omena_cascade::CascadeOutcome::Definite { winner, .. } = property_outcome.outcome
152 else {
153 return None;
154 };
155 let winning_value = match winner.value {
156 omena_cascade::CascadeValue::Literal(value) => value,
157 _ => winner.id,
158 };
159 Some(OmenaQueryCascadeSectionOutcomeV0 {
160 schema_version: "0",
161 product: "omena-query.cascade-section-outcome",
162 selector: property_outcome.site.element_selector,
163 property: property_outcome.site.property,
164 winning_value,
165 })
166 })
167 .collect::<Vec<_>>();
168 outcomes.sort_by(|left, right| {
169 left.selector
170 .cmp(&right.selector)
171 .then_with(|| left.property.cmp(&right.property))
172 .then_with(|| left.winning_value.cmp(&right.winning_value))
173 });
174 outcomes
175}
176
177#[allow(deprecated)]
178#[deprecated(
179 since = "0.4.0",
180 note = "use summarize_omena_query_cascade_section_outcomes_from_source; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
181)]
182pub fn summarize_omena_query_cascade_site_outcomes_from_source(
183 source: &str,
184) -> Vec<OmenaQueryCascadeSiteOutcomeV0> {
185 summarize_omena_query_cascade_section_outcomes_from_source(source)
186 .into_iter()
187 .map(|outcome| OmenaQueryCascadeSiteOutcomeV0 {
188 schema_version: outcome.schema_version,
189 product: "omena-query.cascade-site-outcome",
190 selector: outcome.selector,
191 property: outcome.property,
192 winning_value: outcome.winning_value,
193 })
194 .collect()
195}
196
197#[cfg(test)]
198mod cascade_section_outcome_tests {
199 use std::fmt::Write as _;
200
201 use super::*;
202 use sha2::{Digest, Sha256};
203
204 fn sha256_hex(bytes: &[u8]) -> String {
205 let mut output = String::with_capacity(64);
206 for byte in Sha256::digest(bytes) {
207 let _ = write!(&mut output, "{byte:02x}");
208 }
209 output
210 }
211
212 #[deprecated(
213 since = "0.4.0",
214 note = "legacy wire regression helper owned by omena-query maintainers; removal is not before 1.0 and requires downstream migration plus zero audited non-compatibility uses"
215 )]
216 #[allow(deprecated)]
217 fn compatibility_outcomes_serialized_v0(source: &str) -> Result<String, serde_json::Error> {
218 serde_json::to_string(&summarize_omena_query_cascade_site_outcomes_from_source(
219 source,
220 ))
221 }
222
223 #[test]
224 fn cascade_section_projection_uses_the_product_source_order_winner() {
225 let outcomes = summarize_omena_query_cascade_section_outcomes_from_source(
226 ".card { color: red; } .card { color: blue; }",
227 );
228
229 assert_eq!(outcomes.len(), 1);
230 assert_eq!(outcomes[0].product, "omena-query.cascade-section-outcome");
231 assert_eq!(outcomes[0].selector, ".card");
232 assert_eq!(outcomes[0].property, "color");
233 assert_eq!(outcomes[0].winning_value, "blue");
234 }
235
236 #[test]
237 #[allow(deprecated)]
238 fn compatibility_projection_preserves_exact_serialized_bytes() -> Result<(), serde_json::Error>
239 {
240 let serialized =
241 compatibility_outcomes_serialized_v0(".card { color: red; } .card { color: blue; }")?;
242 assert_eq!(
243 sha256_hex(serialized.as_bytes()),
244 "061a1cadf92641c82fe69480884202bfebc1ec4f51693dccbbf8430cc76b890c"
245 );
246 Ok(())
247 }
248}
249
250pub fn summarize_omena_query_style_semantic_graph_from_source(
251 style_path: &str,
252 style_source: &str,
253 input: &EngineInputV2,
254) -> Option<StyleSemanticGraphSummaryV0> {
255 summarize_omena_bridge_style_semantic_graph_from_source(style_path, style_source, input)
256}
257
258pub fn read_omena_query_style_context_index(
259 style_path: &str,
260 style_source: &str,
261 input: &EngineInputV2,
262) -> Option<OmenaQueryStyleContextIndexV0> {
263 let graph =
264 summarize_omena_query_style_semantic_graph_from_source(style_path, style_source, input)?;
265 Some(OmenaQueryStyleContextIndexV0 {
266 schema_version: "0",
267 product: "omena-query.style-context-index",
268 style_path: style_path.to_string(),
269 language: graph.language,
270 context_index_source: graph.semantic_facts.context_index.product,
271 context_index: graph.semantic_facts.context_index,
272 })
273}
274
275pub fn summarize_omena_query_style_hover_candidates(
276 style_path: &str,
277 style_source: &str,
278) -> Option<OmenaQueryStyleHoverCandidatesV0> {
279 let dialect = omena_parser_dialect_for_style_path(style_path);
280 let facts = collect_omena_query_omena_parser_style_facts_raw(style_source, dialect);
281 let mut seen = BTreeSet::new();
282 let mut candidates = Vec::new();
283 collect_style_selector_hover_candidates_from_omena_parser_facts(
284 style_source,
285 facts.selectors.as_slice(),
286 &mut seen,
287 &mut candidates,
288 );
289 collect_custom_property_hover_candidates_from_omena_parser_facts(
290 style_source,
291 facts.variables.as_slice(),
292 &mut seen,
293 &mut candidates,
294 );
295 collect_sass_symbol_hover_candidates_from_omena_parser_facts(
296 style_source,
297 facts.sass_symbols.as_slice(),
298 &mut seen,
299 &mut candidates,
300 );
301 collect_sass_partial_evaluator_selector_candidates_from_omena_parser_facts(
302 style_source,
303 facts.sass_includes.as_slice(),
304 &mut seen,
305 &mut candidates,
306 );
307 candidates.sort();
308 Some(OmenaQueryStyleHoverCandidatesV0 {
309 schema_version: "0",
310 product: "omena-query.style-hover-candidates",
311 language: omena_parser_style_dialect_label(dialect),
312 candidates,
313 })
314}
315
316pub fn summarize_omena_query_custom_property_occurrence_index(
317 style_sources: &[OmenaQueryStyleSourceInputV0],
318) -> OmenaQueryCustomPropertyOccurrenceIndexV0 {
319 let mut occurrences = Vec::new();
320 for style in style_sources {
321 let dialect = omena_parser_dialect_for_style_path(style.style_path.as_str());
322 let facts =
323 collect_omena_query_omena_parser_style_facts_raw(style.style_source.as_str(), dialect);
324 for fact in facts.variables {
325 let kind = match fact.kind {
326 ParsedVariableFactKind::CustomPropertyDeclaration => "customPropertyDeclaration",
327 ParsedVariableFactKind::CustomPropertyReference => "customPropertyReference",
328 _ => continue,
329 };
330 let byte_span = ParserByteSpanV0 {
331 start: u32::from(fact.range.start()) as usize,
332 end: u32::from(fact.range.end()) as usize,
333 };
334 occurrences.push(OmenaQueryCustomPropertyOccurrenceV0 {
335 uri: style.style_path.clone(),
336 name: fact.name,
337 range: parser_range_for_byte_span(style.style_source.as_str(), byte_span),
338 byte_span,
339 kind,
340 has_fallback: fact.has_fallback,
341 source: "omenaParserVariableFacts",
342 });
343 }
344 }
345 occurrences.sort();
346 occurrences.dedup();
347 OmenaQueryCustomPropertyOccurrenceIndexV0 {
348 schema_version: "0",
349 product: "omena-query.custom-property-occurrence-index",
350 occurrence_count: occurrences.len(),
351 occurrences,
352 ready_surfaces: vec!["customPropertyOccurrenceIndex", "customPropertyMigration"],
353 }
354}
355
356pub fn summarize_omena_query_style_hover_render_parts(
357 source: &str,
358 kind: &str,
359 name: &str,
360 position: ParserPositionV0,
361) -> OmenaQueryStyleHoverRenderPartsV0 {
362 summarize_omena_query_style_hover_render_parts_with_branch_scope(
363 source, kind, name, position, None, None,
364 )
365}
366
367pub fn summarize_omena_query_style_hover_render_parts_for_hover_position(
368 source: &str,
369 kind: &str,
370 name: &str,
371 position: ParserPositionV0,
372) -> OmenaQueryStyleHoverRenderPartsV0 {
373 let branch_scope = (kind == "selector")
374 .then(|| selector_hover_branch_scope_at_position(source, name, position))
375 .flatten();
376 summarize_omena_query_style_hover_render_parts_with_branch_scope(
377 source,
378 kind,
379 name,
380 position,
381 branch_scope,
382 None,
383 )
384}
385
386fn summarize_omena_query_style_hover_render_parts_with_branch_scope(
387 source: &str,
388 kind: &str,
389 name: &str,
390 position: ParserPositionV0,
391 selector_branch_scope: Option<HoverCascadeBranchScope>,
392 precollected_target_declarations: Option<&[cascade_checker::QueryCheckerCascadeDeclaration]>,
393) -> OmenaQueryStyleHoverRenderPartsV0 {
394 let mut parts = OmenaQueryStyleHoverRenderPartsV0 {
395 schema_version: "0",
396 product: "omena-query.style-hover-render-parts",
397 snippet: String::new(),
398 value: None,
399 signature: None,
400 property_value_narrowings: Vec::new(),
401 render_source: "lineSnippet",
402 };
403
404 match kind {
405 "selector" => {
406 parts.snippet = rule_snippet_around_position(source, position).unwrap_or_else(|| {
407 parts.render_source = "selectorFallback";
408 format!(".{name} {{ ... }}")
409 });
410 if parts.render_source != "selectorFallback" {
411 parts.render_source = "ruleSnippet";
412 }
413 parts.property_value_narrowings = match precollected_target_declarations {
414 Some(declarations) => selector_property_value_narrowings_from_declarations(
415 declarations,
416 name,
417 selector_branch_scope.as_ref(),
418 ),
419 None => selector_property_value_narrowings_for_hover(
420 source,
421 name,
422 selector_branch_scope.as_ref(),
423 ),
424 };
425 }
426 "customPropertyReference" | "customPropertyDeclaration" => {
427 parts.snippet = line_snippet_at_position(source, position).unwrap_or_default();
428 }
429 kind if is_sass_symbol_candidate_kind(kind) => {
430 parts.snippet = line_snippet_at_position(source, position).unwrap_or_default();
431 if sass_symbol_kind_from_candidate_kind(kind) == Some("variable")
432 && is_sass_symbol_declaration_kind(kind)
433 {
434 parts.value = sass_variable_value_from_declaration_line(parts.snippet.as_str());
435 } else if matches!(
436 sass_symbol_kind_from_candidate_kind(kind),
437 Some("mixin" | "function")
438 ) && is_sass_symbol_declaration_kind(kind)
439 && let Some((signature, snippet)) =
440 sass_callable_definition_render_parts(source, position)
441 {
442 parts.signature = Some(signature);
443 parts.snippet = snippet;
444 parts.render_source = "callableBlockSnippet";
445 }
446 }
447 _ => {
448 parts.snippet = name.to_string();
449 parts.render_source = "candidateNameFallback";
450 }
451 }
452
453 parts
454}
455
456pub fn summarize_omena_query_style_hover_render_parts_for_workspace_file(
457 target_style_path: &str,
458 style_sources: &[OmenaQueryStyleSourceInputV0],
459 package_manifests: &[OmenaQueryStylePackageManifestV0],
460 resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
461 kind: &str,
462 name: &str,
463 position: ParserPositionV0,
464) -> Option<OmenaQueryStyleHoverRenderPartsV0> {
465 let target = style_sources
466 .iter()
467 .find(|source| source.style_path == target_style_path)?;
468 let mut parts =
469 summarize_omena_query_style_hover_render_parts(&target.style_source, kind, name, position);
470 if kind == "selector" {
471 let module_graph_narrowings = selector_property_value_narrowings_for_hover_module_graph(
472 target_style_path,
473 style_sources,
474 package_manifests,
475 resolution_inputs.bundler_path_mappings.as_slice(),
476 resolution_inputs.tsconfig_path_mappings.as_slice(),
477 name,
478 None,
479 );
480 if !module_graph_narrowings.is_empty() {
481 parts.property_value_narrowings = module_graph_narrowings;
482 }
483 }
484 Some(parts)
485}
486
487pub fn summarize_omena_query_style_hover_render_parts_for_workspace_file_hover_position(
488 target_style_path: &str,
489 style_sources: &[OmenaQueryStyleSourceInputV0],
490 package_manifests: &[OmenaQueryStylePackageManifestV0],
491 resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
492 kind: &str,
493 name: &str,
494 position: ParserPositionV0,
495) -> Option<OmenaQueryStyleHoverRenderPartsV0> {
496 let target = style_sources
497 .iter()
498 .find(|source| source.style_path == target_style_path)?;
499 let branch_scope = (kind == "selector")
500 .then(|| selector_hover_branch_scope_at_position(&target.style_source, name, position))
501 .flatten();
502 let mut parts = summarize_omena_query_style_hover_render_parts_with_branch_scope(
503 &target.style_source,
504 kind,
505 name,
506 position,
507 branch_scope.clone(),
508 None,
509 );
510 if kind == "selector" {
511 let module_graph_narrowings = selector_property_value_narrowings_for_hover_module_graph(
512 target_style_path,
513 style_sources,
514 package_manifests,
515 resolution_inputs.bundler_path_mappings.as_slice(),
516 resolution_inputs.tsconfig_path_mappings.as_slice(),
517 name,
518 branch_scope.as_ref(),
519 );
520 if !module_graph_narrowings.is_empty() {
521 parts.property_value_narrowings = module_graph_narrowings;
522 }
523 }
524 Some(parts)
525}
526
527pub fn summarize_omena_query_style_hover_render_parts_for_workspace_file_with_substrate(
533 target_style_path: &str,
534 style_sources: &[OmenaQueryStyleSourceInputV0],
535 substrate: &OmenaQueryStyleCascadeNarrowingSubstrateV0,
536 kind: &str,
537 name: &str,
538 position: ParserPositionV0,
539) -> Option<OmenaQueryStyleHoverRenderPartsV0> {
540 let target = style_sources
541 .iter()
542 .find(|source| source.style_path == target_style_path)?;
543 summarize_omena_query_style_hover_render_parts_for_target_with_substrate(
544 target_style_path,
545 &target.style_source,
546 substrate,
547 kind,
548 name,
549 position,
550 None,
552 )
553}
554
555pub fn summarize_omena_query_style_hover_render_parts_for_workspace_file_hover_position_with_substrate(
558 target_style_path: &str,
559 style_sources: &[OmenaQueryStyleSourceInputV0],
560 substrate: &OmenaQueryStyleCascadeNarrowingSubstrateV0,
561 kind: &str,
562 name: &str,
563 position: ParserPositionV0,
564) -> Option<OmenaQueryStyleHoverRenderPartsV0> {
565 let target = style_sources
566 .iter()
567 .find(|source| source.style_path == target_style_path)?;
568 let branch_scope = (kind == "selector")
569 .then(|| selector_hover_branch_scope_at_position(&target.style_source, name, position))
570 .flatten();
571 summarize_omena_query_style_hover_render_parts_for_target_with_substrate(
572 target_style_path,
573 &target.style_source,
574 substrate,
575 kind,
576 name,
577 position,
578 branch_scope,
579 )
580}
581
582fn summarize_omena_query_style_hover_render_parts_for_target_with_substrate(
583 target_style_path: &str,
584 target_style_source: &str,
585 substrate: &OmenaQueryStyleCascadeNarrowingSubstrateV0,
586 kind: &str,
587 name: &str,
588 position: ParserPositionV0,
589 branch_scope: Option<HoverCascadeBranchScope>,
590) -> Option<OmenaQueryStyleHoverRenderPartsV0> {
591 let mut parts = summarize_omena_query_style_hover_render_parts_with_branch_scope(
592 target_style_source,
593 kind,
594 name,
595 position,
596 branch_scope.clone(),
597 substrate.declarations_for_style_path(target_style_path),
598 );
599 if kind == "selector" {
600 let module_graph_narrowings =
601 selector_property_value_narrowings_for_hover_module_graph_with_substrate(
602 target_style_path,
603 substrate,
604 name,
605 branch_scope.as_ref(),
606 );
607 if !module_graph_narrowings.is_empty() {
608 parts.property_value_narrowings = module_graph_narrowings;
609 }
610 }
611 Some(parts)
612}
613
614#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
615struct HoverCascadeBranchScope {
616 condition_context: Vec<String>,
617 layer_name: Option<String>,
618 layer_order: Option<i32>,
619}
620
621#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
622struct HoverCascadeBranchMatch {
623 span_len: usize,
624 scope: HoverCascadeBranchScope,
625}
626
627type SelectorPropertyBranchKey = (String, Vec<String>, Option<String>, Option<i32>);
628
629#[derive(Debug, Clone, PartialEq, Eq)]
636pub struct OmenaQueryStyleCascadeNarrowingSubstrateV0 {
637 entries: Vec<StyleCascadeNarrowingSubstrateEntry>,
638 resolution: OmenaQuerySassModuleCrossFileResolutionV0,
639}
640
641#[derive(Debug, Clone, PartialEq, Eq)]
642struct StyleCascadeNarrowingSubstrateEntry {
643 style_path: String,
644 facts: OmenaQueryOmenaParserStyleFactsV0,
645 declarations: Vec<cascade_checker::QueryCheckerCascadeDeclaration>,
646}
647
648impl OmenaQueryStyleCascadeNarrowingSubstrateV0 {
649 fn declarations_for_style_path(
650 &self,
651 style_path: &str,
652 ) -> Option<&[cascade_checker::QueryCheckerCascadeDeclaration]> {
653 self.entries
654 .iter()
655 .find(|entry| entry.style_path == style_path)
656 .map(|entry| entry.declarations.as_slice())
657 }
658
659 pub(crate) fn visible_sass_symbol_keys_for_workspace_file(
660 &self,
661 target_style_path: &str,
662 package_manifests: &[OmenaQueryStylePackageManifestV0],
663 external_sifs: &[OmenaQueryExternalSifInputV0],
664 resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
665 ) -> BTreeSet<diagnostics::SassSymbolKey> {
666 let facts_by_path = self
667 .entries
668 .iter()
669 .map(|entry| (entry.style_path.as_str(), &entry.facts))
670 .collect::<BTreeMap<_, _>>();
671 diagnostics::collect_visible_sass_symbol_keys(
672 target_style_path,
673 &facts_by_path,
674 &self.resolution,
675 diagnostics::OmenaQueryExternalSifResolutionContext {
676 package_manifests,
677 bundler_path_mappings: resolution_inputs.bundler_path_mappings.as_slice(),
678 tsconfig_path_mappings: resolution_inputs.tsconfig_path_mappings.as_slice(),
679 external_sifs,
680 },
681 )
682 }
683}
684
685pub fn collect_omena_query_style_cascade_narrowing_substrate(
686 style_sources: &[OmenaQueryStyleSourceInputV0],
687 package_manifests: &[OmenaQueryStylePackageManifestV0],
688 resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
689) -> OmenaQueryStyleCascadeNarrowingSubstrateV0 {
690 collect_omena_query_style_cascade_narrowing_substrate_with_external_sifs(
691 style_sources,
692 package_manifests,
693 &[],
694 resolution_inputs,
695 )
696}
697
698pub fn collect_omena_query_style_cascade_narrowing_substrate_with_external_sifs(
699 style_sources: &[OmenaQueryStyleSourceInputV0],
700 package_manifests: &[OmenaQueryStylePackageManifestV0],
701 external_sifs: &[OmenaQueryExternalSifInputV0],
702 resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
703) -> OmenaQueryStyleCascadeNarrowingSubstrateV0 {
704 #[cfg(feature = "salsa-memo")]
705 {
706 let mut host = OmenaQueryStyleMemoHostV0::new();
707 if let Some(selector) = host.workspace_revision_selector(
708 style_sources,
709 &[],
710 package_manifests,
711 external_sifs,
712 resolution_inputs,
713 ) {
714 return selector.style_cascade_narrowing_substrate();
715 }
716 }
717
718 let style_source_refs = style_sources
719 .iter()
720 .map(|source| (source.style_path.as_str(), source.style_source.as_str()))
721 .collect::<Vec<_>>();
722 let style_fact_entries = collect_omena_query_style_fact_entries(style_source_refs.as_slice());
723 let mut resolution = summarize_sass_module_cross_file_resolution(
724 &style_fact_entries,
725 package_manifests,
726 resolution_inputs.bundler_path_mappings.as_slice(),
727 resolution_inputs.tsconfig_path_mappings.as_slice(),
728 );
729 diagnostics::promote_sif_backed_external_edges(
730 &mut resolution,
731 diagnostics::OmenaQueryExternalSifResolutionContext {
732 package_manifests,
733 bundler_path_mappings: resolution_inputs.bundler_path_mappings.as_slice(),
734 tsconfig_path_mappings: resolution_inputs.tsconfig_path_mappings.as_slice(),
735 external_sifs,
736 },
737 );
738 let entries = style_sources
739 .iter()
740 .filter_map(|source| {
741 let facts = style_fact_entries
742 .iter()
743 .find(|entry| entry.style_path == source.style_path)
744 .map(|entry| entry.facts.clone())?;
745 Some(StyleCascadeNarrowingSubstrateEntry {
746 style_path: source.style_path.clone(),
747 facts,
748 declarations: cascade_checker::collect_query_checker_cascade_declarations(
749 source.style_source.as_str(),
750 ),
751 })
752 })
753 .collect();
754 OmenaQueryStyleCascadeNarrowingSubstrateV0 {
755 entries,
756 resolution,
757 }
758}
759
760fn selector_property_value_narrowings_for_hover(
761 source: &str,
762 name: &str,
763 hovered_branch_scope: Option<&HoverCascadeBranchScope>,
764) -> Vec<AbstractPropertyValueNarrowingV0> {
765 let declarations = cascade_checker::collect_query_checker_cascade_declarations(source);
766 selector_property_value_narrowings_from_declarations(
767 declarations.as_slice(),
768 name,
769 hovered_branch_scope,
770 )
771}
772
773fn selector_property_value_narrowings_from_declarations(
774 declarations: &[cascade_checker::QueryCheckerCascadeDeclaration],
775 name: &str,
776 hovered_branch_scope: Option<&HoverCascadeBranchScope>,
777) -> Vec<AbstractPropertyValueNarrowingV0> {
778 let selector = format!(".{name}");
779 let matching_declarations = declarations
780 .iter()
781 .filter(|declaration| declaration.input.selector.as_str() == selector)
782 .collect::<Vec<_>>();
783 let mut branch_keys = matching_declarations
784 .iter()
785 .map(|declaration| {
786 (
787 declaration.input.property.clone(),
788 declaration.input.condition_context.clone(),
789 declaration.input.layer_name.clone(),
790 declaration.input.layer_order,
791 )
792 })
793 .collect::<BTreeSet<_>>()
794 .into_iter()
795 .filter(|(_, condition_context, _, _)| {
796 cascade_checker::query_condition_context_static_supports_pruning_evidence(
797 condition_context.as_slice(),
798 hovered_branch_scope.map(|scope| scope.condition_context.as_slice()),
799 )
800 .is_none_or(|evidence| !evidence.pruned)
801 })
802 .collect::<Vec<_>>();
803 branch_keys.sort();
804 if let Some(hovered_branch_scope) = hovered_branch_scope {
805 let filtered_branch_keys =
806 filter_hovered_branch_keys(branch_keys.as_slice(), hovered_branch_scope);
807 if !filtered_branch_keys.is_empty() {
808 branch_keys = filtered_branch_keys;
809 }
810 }
811
812 branch_keys
813 .into_iter()
814 .map(
815 |(property_name, condition_context, layer_name, layer_order)| {
816 let property_candidates = matching_declarations
817 .iter()
818 .filter(|declaration| declaration.input.property == property_name)
819 .map(|declaration| AbstractPropertyValueCandidateV0 {
820 property_name: declaration.input.property.clone(),
821 value: declaration.input.value.clone(),
822 pseudo_state: None,
823 condition_context: declaration.input.condition_context.clone(),
824 layer_name: declaration.input.layer_name.clone(),
825 layer_order: declaration.input.layer_order,
826 source_order: Some(declaration.input.source_order),
827 important: declaration.input.important,
828 same_selector_ordering: true,
829 })
830 .collect::<Vec<_>>();
831 narrow_abstract_property_value_for_cascade_branch(
832 property_name.as_str(),
833 None,
834 condition_context.as_slice(),
835 layer_name.as_deref(),
836 layer_order,
837 true,
838 property_candidates.as_slice(),
839 )
840 },
841 )
842 .collect()
843}
844
845fn selector_property_value_narrowings_for_hover_module_graph(
846 target_style_path: &str,
847 style_sources: &[OmenaQueryStyleSourceInputV0],
848 package_manifests: &[OmenaQueryStylePackageManifestV0],
849 bundler_path_mappings: &[OmenaResolverBundlerPathAliasMappingV0],
850 tsconfig_path_mappings: &[OmenaResolverTsconfigPathMappingV0],
851 name: &str,
852 hovered_branch_scope: Option<&HoverCascadeBranchScope>,
853) -> Vec<AbstractPropertyValueNarrowingV0> {
854 let style_source_refs = style_sources
855 .iter()
856 .map(|source| (source.style_path.as_str(), source.style_source.as_str()))
857 .collect::<Vec<_>>();
858 let style_fact_entries = collect_omena_query_style_fact_entries(style_source_refs.as_slice());
859 let resolution = summarize_sass_module_cross_file_resolution(
860 &style_fact_entries,
861 package_manifests,
862 bundler_path_mappings,
863 tsconfig_path_mappings,
864 );
865 let reachable_style_paths = diagnostics::collect_sass_module_graph_reachable_style_paths(
866 target_style_path,
867 &resolution,
868 );
869 if reachable_style_paths.len() <= 1 {
870 return Vec::new();
871 }
872
873 let selector = format!(".{name}");
874 let collected_declarations = style_sources
875 .iter()
876 .filter(|source| reachable_style_paths.contains(source.style_path.as_str()))
877 .flat_map(|source| {
878 cascade_checker::collect_query_checker_cascade_declarations(
879 source.style_source.as_str(),
880 )
881 })
882 .filter(|declaration| declaration.input.selector.as_str() == selector)
883 .collect::<Vec<_>>();
884 let matching_declarations = collected_declarations.iter().collect::<Vec<_>>();
885 module_graph_narrowings_from_matching_declarations(
886 matching_declarations.as_slice(),
887 hovered_branch_scope,
888 )
889}
890
891fn selector_property_value_narrowings_for_hover_module_graph_with_substrate(
892 target_style_path: &str,
893 substrate: &OmenaQueryStyleCascadeNarrowingSubstrateV0,
894 name: &str,
895 hovered_branch_scope: Option<&HoverCascadeBranchScope>,
896) -> Vec<AbstractPropertyValueNarrowingV0> {
897 let reachable_style_paths = diagnostics::collect_sass_module_graph_reachable_style_paths(
898 target_style_path,
899 &substrate.resolution,
900 );
901 if reachable_style_paths.len() <= 1 {
902 return Vec::new();
903 }
904
905 let selector = format!(".{name}");
906 let matching_declarations = substrate
907 .entries
908 .iter()
909 .filter(|entry| reachable_style_paths.contains(entry.style_path.as_str()))
910 .flat_map(|entry| entry.declarations.iter())
911 .filter(|declaration| declaration.input.selector.as_str() == selector)
912 .collect::<Vec<_>>();
913 module_graph_narrowings_from_matching_declarations(
914 matching_declarations.as_slice(),
915 hovered_branch_scope,
916 )
917}
918
919fn module_graph_narrowings_from_matching_declarations(
920 matching_declarations: &[&cascade_checker::QueryCheckerCascadeDeclaration],
921 hovered_branch_scope: Option<&HoverCascadeBranchScope>,
922) -> Vec<AbstractPropertyValueNarrowingV0> {
923 if matching_declarations.is_empty() {
924 return Vec::new();
925 }
926
927 let mut branch_keys = matching_declarations
928 .iter()
929 .map(|declaration| {
930 (
931 declaration.input.property.clone(),
932 declaration.input.condition_context.clone(),
933 declaration.input.layer_name.clone(),
934 declaration.input.layer_order,
935 )
936 })
937 .collect::<BTreeSet<_>>()
938 .into_iter()
939 .filter(|(_, condition_context, _, _)| {
940 cascade_checker::query_condition_context_static_supports_pruning_evidence(
941 condition_context.as_slice(),
942 hovered_branch_scope.map(|scope| scope.condition_context.as_slice()),
943 )
944 .is_none_or(|evidence| !evidence.pruned)
945 })
946 .collect::<Vec<_>>();
947 branch_keys.sort();
948 if let Some(hovered_branch_scope) = hovered_branch_scope {
949 let filtered_branch_keys =
950 filter_hovered_branch_keys(branch_keys.as_slice(), hovered_branch_scope);
951 if !filtered_branch_keys.is_empty() {
952 branch_keys = filtered_branch_keys;
953 }
954 }
955
956 branch_keys
957 .into_iter()
958 .map(
959 |(property_name, condition_context, layer_name, layer_order)| {
960 let property_candidates = matching_declarations
961 .iter()
962 .filter(|declaration| declaration.input.property == property_name)
963 .map(|declaration| AbstractPropertyValueCandidateV0 {
964 property_name: declaration.input.property.clone(),
965 value: declaration.input.value.clone(),
966 pseudo_state: None,
967 condition_context: declaration.input.condition_context.clone(),
968 layer_name: declaration.input.layer_name.clone(),
969 layer_order: declaration.input.layer_order,
970 source_order: Some(declaration.input.source_order),
971 important: declaration.input.important,
972 same_selector_ordering: false,
973 })
974 .collect::<Vec<_>>();
975 let mut narrowed = narrow_abstract_property_value_for_cascade_branch(
976 property_name.as_str(),
977 None,
978 condition_context.as_slice(),
979 layer_name.as_deref(),
980 layer_order,
981 true,
982 property_candidates.as_slice(),
983 );
984 narrowed.stylesheet_scope = "moduleGraph";
985 narrowed
986 },
987 )
988 .collect()
989}
990
991fn filter_hovered_branch_keys(
992 branch_keys: &[SelectorPropertyBranchKey],
993 hovered_branch_scope: &HoverCascadeBranchScope,
994) -> Vec<SelectorPropertyBranchKey> {
995 branch_keys
996 .iter()
997 .filter(|(_, condition_context, layer_name, layer_order)| {
998 condition_context == &hovered_branch_scope.condition_context
999 && layer_name == &hovered_branch_scope.layer_name
1000 && layer_order == &hovered_branch_scope.layer_order
1001 })
1002 .cloned()
1003 .collect()
1004}
1005
1006fn selector_hover_branch_scope_at_position(
1007 source: &str,
1008 name: &str,
1009 position: ParserPositionV0,
1010) -> Option<HoverCascadeBranchScope> {
1011 let offset = byte_offset_for_parser_position(source, position)?;
1012 let selector = format!(".{name}");
1013 let mut layer_orders = BTreeMap::new();
1014 let mut next_layer_order = 0i32;
1015 let mut matches = Vec::new();
1016 collect_hover_selector_branch_scopes(
1017 source,
1018 0,
1019 source.len(),
1020 None,
1021 Vec::new(),
1022 None,
1023 None,
1024 &mut layer_orders,
1025 &mut next_layer_order,
1026 selector.as_str(),
1027 offset,
1028 &mut matches,
1029 );
1030 matches.sort();
1031 matches.into_iter().next().map(|matched| matched.scope)
1032}
1033
1034#[allow(clippy::too_many_arguments)]
1035fn collect_hover_selector_branch_scopes(
1036 source: &str,
1037 start: usize,
1038 end: usize,
1039 parent_selector: Option<String>,
1040 condition_context: Vec<String>,
1041 layer_name: Option<String>,
1042 layer_order: Option<i32>,
1043 layer_orders: &mut BTreeMap<String, i32>,
1044 next_layer_order: &mut i32,
1045 target_selector: &str,
1046 hover_offset: usize,
1047 matches: &mut Vec<HoverCascadeBranchMatch>,
1048) {
1049 let mut index = start;
1050 while let Some(open_index) = find_hover_style_top_level_byte(source, index, end, b'{') {
1051 let Some(close_index) = matching_style_block_end(source, open_index, b'{', b'}') else {
1052 break;
1053 };
1054 if close_index > end {
1055 break;
1056 }
1057 let prelude_start = hover_style_prelude_start(source, start, open_index);
1058 let prelude = source[prelude_start..open_index].trim();
1059 let body_start = open_index + 1;
1060
1061 if let Some(layer) = hover_layer_name_from_prelude(prelude) {
1062 let order = *layer_orders.entry(layer.clone()).or_insert_with(|| {
1063 let order = *next_layer_order;
1064 *next_layer_order += 1;
1065 order
1066 });
1067 collect_hover_selector_branch_scopes(
1068 source,
1069 body_start,
1070 close_index,
1071 parent_selector.clone(),
1072 condition_context.clone(),
1073 Some(layer),
1074 Some(order),
1075 layer_orders,
1076 next_layer_order,
1077 target_selector,
1078 hover_offset,
1079 matches,
1080 );
1081 } else if prelude.starts_with('@') {
1082 let mut nested_condition_context = condition_context.clone();
1083 nested_condition_context.push(normalize_hover_condition_prelude(prelude));
1084 collect_hover_selector_branch_scopes(
1085 source,
1086 body_start,
1087 close_index,
1088 parent_selector.clone(),
1089 nested_condition_context,
1090 layer_name.clone(),
1091 layer_order,
1092 layer_orders,
1093 next_layer_order,
1094 target_selector,
1095 hover_offset,
1096 matches,
1097 );
1098 } else if !prelude.is_empty() {
1099 let canonical_members = split_hover_selector_list(prelude)
1100 .into_iter()
1101 .map(|member| canonical_hover_selector(parent_selector.as_deref(), member.as_str()))
1102 .collect::<Vec<_>>();
1103 if canonical_members
1104 .iter()
1105 .any(|member| member == target_selector)
1106 && hover_offset >= prelude_start
1107 && hover_offset <= close_index
1108 {
1109 matches.push(HoverCascadeBranchMatch {
1110 span_len: close_index.saturating_sub(prelude_start),
1111 scope: HoverCascadeBranchScope {
1112 condition_context: condition_context.clone(),
1113 layer_name: layer_name.clone(),
1114 layer_order,
1115 },
1116 });
1117 }
1118 for canonical_selector in canonical_members {
1119 collect_hover_selector_branch_scopes(
1120 source,
1121 body_start,
1122 close_index,
1123 Some(canonical_selector),
1124 condition_context.clone(),
1125 layer_name.clone(),
1126 layer_order,
1127 layer_orders,
1128 next_layer_order,
1129 target_selector,
1130 hover_offset,
1131 matches,
1132 );
1133 }
1134 }
1135
1136 index = close_index + 1;
1137 }
1138}
1139
1140fn find_hover_style_top_level_byte(
1141 source: &str,
1142 start: usize,
1143 end: usize,
1144 needle: u8,
1145) -> Option<usize> {
1146 let mut index = start;
1147 let mut quote: Option<u8> = None;
1148 let mut paren_depth = 0usize;
1149 while index < end {
1150 let byte = source.as_bytes().get(index).copied()?;
1151 if let Some(quote_byte) = quote {
1152 if byte == b'\\' {
1153 index = advance_style_escaped_char(source, index, end);
1154 } else if byte == quote_byte {
1155 quote = None;
1156 index = advance_style_scan_cursor(source, index, end);
1157 } else {
1158 index = advance_style_scan_cursor(source, index, end);
1159 }
1160 continue;
1161 }
1162 if source[index..end].starts_with("/*")
1163 && let Some(close_offset) = source[index + 2..end].find("*/")
1164 {
1165 index += close_offset + 4;
1166 continue;
1167 }
1168 if byte == needle && paren_depth == 0 {
1169 return Some(index);
1170 }
1171 match byte {
1172 b'"' | b'\'' | b'`' => {
1173 quote = Some(byte);
1174 index = advance_style_scan_cursor(source, index, end);
1175 }
1176 b'(' => {
1177 paren_depth += 1;
1178 index = advance_style_scan_cursor(source, index, end);
1179 }
1180 b')' => {
1181 paren_depth = paren_depth.saturating_sub(1);
1182 index = advance_style_scan_cursor(source, index, end);
1183 }
1184 _ => index = advance_style_scan_cursor(source, index, end),
1185 }
1186 }
1187 None
1188}
1189
1190fn hover_style_prelude_start(source: &str, search_start: usize, open_index: usize) -> usize {
1191 source[search_start..open_index]
1192 .rfind(['{', '}', ';'])
1193 .map(|offset| search_start + offset + 1)
1194 .unwrap_or(search_start)
1195}
1196
1197fn hover_layer_name_from_prelude(prelude: &str) -> Option<String> {
1198 let rest = css_keyword(prelude.trim_start())
1199 .strip_prefix("@layer")?
1200 .trim();
1201 let name = rest
1202 .split(|ch: char| ch.is_ascii_whitespace() || matches!(ch, ',' | '{' | ';'))
1203 .next()
1204 .unwrap_or_default()
1205 .trim_matches(['"', '\'']);
1206 if name.is_empty() {
1207 Some("(anonymous-layer)".to_string())
1208 } else {
1209 Some(name.to_string())
1210 }
1211}
1212
1213fn normalize_hover_condition_prelude(prelude: &str) -> String {
1214 prelude.split_whitespace().collect::<Vec<_>>().join(" ")
1215}
1216
1217fn split_hover_selector_list(prelude: &str) -> Vec<String> {
1218 let mut members = split_top_level_style_segments(prelude, 0, prelude.len(), b',')
1219 .into_iter()
1220 .filter_map(|(start, end)| {
1221 let member = prelude[start..end].trim();
1222 (!member.is_empty()).then(|| member.to_string())
1223 })
1224 .collect::<Vec<_>>();
1225 if members.is_empty() {
1226 members.push(prelude.trim().to_string());
1227 }
1228 members
1229}
1230
1231fn canonical_hover_selector(parent_selector: Option<&str>, selector: &str) -> String {
1232 let selector = selector.trim();
1233 match parent_selector {
1234 Some(parent_selector) if selector.contains('&') => selector.replace('&', parent_selector),
1235 Some(parent_selector) => format!("{parent_selector} {selector}"),
1236 None => selector.to_string(),
1237 }
1238}
1239
1240fn source_reference_text_selector_name(source: &str, span: ParserByteSpanV0) -> Option<String> {
1241 let text = source.get(span.start..span.end)?;
1242 if text.is_empty() {
1243 return None;
1244 }
1245 text.chars()
1246 .all(is_css_name_continue)
1247 .then(|| text.to_string())
1248}
1249
1250pub fn summarize_omena_query_style_semantic_graph_batch_from_sources<'a>(
1251 styles: impl IntoIterator<Item = (&'a str, &'a str)>,
1252 input: &EngineInputV2,
1253) -> OmenaQueryStyleSemanticGraphBatchOutputV0 {
1254 summarize_omena_query_style_semantic_graph_batch_from_sources_with_package_manifests(
1255 styles,
1256 input,
1257 &[],
1258 )
1259}
1260
1261pub fn summarize_omena_query_style_semantic_graph_batch_from_sources_with_package_manifests<'a>(
1262 styles: impl IntoIterator<Item = (&'a str, &'a str)>,
1263 input: &EngineInputV2,
1264 package_manifests: &[OmenaQueryStylePackageManifestV0],
1265) -> OmenaQueryStyleSemanticGraphBatchOutputV0 {
1266 let resolution_inputs = OmenaQueryStyleResolutionInputsV0 {
1267 package_manifests: package_manifests.to_vec(),
1268 ..OmenaQueryStyleResolutionInputsV0::default()
1269 };
1270 summarize_omena_query_style_semantic_graph_batch_from_sources_with_resolution_inputs(
1271 styles,
1272 input,
1273 package_manifests,
1274 &resolution_inputs,
1275 )
1276}
1277
1278pub fn summarize_omena_query_style_semantic_graph_batch_from_sources_with_resolution_inputs<'a>(
1279 styles: impl IntoIterator<Item = (&'a str, &'a str)>,
1280 input: &EngineInputV2,
1281 package_manifests: &[OmenaQueryStylePackageManifestV0],
1282 resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
1283) -> OmenaQueryStyleSemanticGraphBatchOutputV0 {
1284 let style_sources = styles
1285 .into_iter()
1286 .map(|(style_path, style_source)| OmenaQueryStyleSourceInputV0 {
1287 style_path: style_path.to_string(),
1288 style_source: style_source.to_string(),
1289 })
1290 .collect::<Vec<_>>();
1291 let style_source_refs = style_sources
1292 .iter()
1293 .map(|source| (source.style_path.as_str(), source.style_source.as_str()))
1294 .collect::<Vec<_>>();
1295
1296 #[cfg(feature = "salsa-memo")]
1297 {
1298 let mut host = OmenaQueryStyleMemoHostV0::new();
1299 if let Some(selector) = host.workspace_revision_selector(
1300 style_sources.as_slice(),
1301 &[],
1302 package_manifests,
1303 &[],
1304 resolution_inputs,
1305 ) {
1306 return selector.style_semantic_graph_batch(input, package_manifests);
1307 }
1308 }
1309
1310 let style_fact_entries = collect_omena_query_style_fact_entries(style_source_refs.as_slice());
1311 let css_modules_resolution = summarize_css_modules_cross_file_resolution_with_resolution_inputs(
1312 &style_fact_entries,
1313 package_manifests,
1314 resolution_inputs,
1315 );
1316 let sass_module_resolution = summarize_sass_module_cross_file_resolution(
1317 &style_fact_entries,
1318 package_manifests,
1319 &[],
1320 &[],
1321 );
1322 let cross_file_summary = summarize_omena_query_cross_file_summary(
1323 &style_fact_entries,
1324 &css_modules_resolution,
1325 &sass_module_resolution,
1326 );
1327 summarize_omena_query_style_semantic_graph_batch_from_committed_parts(
1328 style_sources.as_slice(),
1329 input,
1330 package_manifests,
1331 resolution_inputs,
1332 OmenaQueryStyleSemanticGraphCommittedParts {
1333 style_fact_entries: style_fact_entries.as_slice(),
1334 cross_file_summary,
1335 css_modules_resolution,
1336 sass_module_resolution,
1337 },
1338 )
1339}
1340
1341pub(in crate::style) struct OmenaQueryStyleSemanticGraphCommittedParts<'a> {
1342 pub style_fact_entries: &'a [OmenaQueryStyleFactEntry],
1343 pub cross_file_summary: OmenaQueryCrossFileSummaryV0,
1344 pub css_modules_resolution: OmenaQueryCssModulesCrossFileResolutionV0,
1345 pub sass_module_resolution: OmenaQuerySassModuleCrossFileResolutionV0,
1346}
1347
1348pub(in crate::style) fn summarize_omena_query_style_semantic_graph_batch_from_committed_parts(
1349 style_sources: &[OmenaQueryStyleSourceInputV0],
1350 input: &EngineInputV2,
1351 package_manifests: &[OmenaQueryStylePackageManifestV0],
1352 resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
1353 committed_parts: OmenaQueryStyleSemanticGraphCommittedParts<'_>,
1354) -> OmenaQueryStyleSemanticGraphBatchOutputV0 {
1355 let OmenaQueryStyleSemanticGraphCommittedParts {
1356 style_fact_entries,
1357 cross_file_summary,
1358 css_modules_resolution,
1359 sass_module_resolution,
1360 } = committed_parts;
1361 let workspace_declarations = style_fact_entries
1362 .iter()
1363 .flat_map(|entry| {
1364 collect_omena_bridge_design_token_workspace_declarations_from_source(
1365 entry.style_path.as_str(),
1366 entry.style_source.as_str(),
1367 )
1368 })
1369 .collect::<Vec<_>>();
1370 let graphs = style_sources
1371 .iter()
1372 .map(|source| OmenaQueryStyleSemanticGraphBatchEntryV0 {
1373 style_path: source.style_path.clone(),
1374 graph: {
1375 let import_reachable_declarations =
1376 filter_import_reachable_design_token_workspace_declarations(
1377 source.style_path.as_str(),
1378 style_fact_entries,
1379 &workspace_declarations,
1380 package_manifests,
1381 resolution_inputs.bundler_path_mappings.as_slice(),
1382 resolution_inputs.tsconfig_path_mappings.as_slice(),
1383 resolution_inputs.disk_style_path_identities.as_slice(),
1384 );
1385 summarize_omena_bridge_style_semantic_graph_from_source_with_scoped_workspace_declarations(
1386 source.style_path.as_str(),
1387 source.style_source.as_str(),
1388 input,
1389 &import_reachable_declarations,
1390 DesignTokenExternalDeclarationCandidateScopeV0::CrossFileImportGraph,
1391 )
1392 },
1393 })
1394 .collect::<Vec<_>>();
1395
1396 OmenaQueryStyleSemanticGraphBatchOutputV0 {
1397 schema_version: "0",
1398 product: "omena-semantic.style-semantic-graph-batch",
1399 cross_file_summary,
1400 css_modules_resolution,
1401 sass_module_resolution,
1402 graphs,
1403 }
1404}
1405
1406#[derive(Default)]
1411struct OmenaQueryStyleParserMaterializationV0(
1412 Option<std::panic::AssertUnwindSafe<std::sync::Arc<omena_parser::ParseResult>>>,
1413);
1414
1415impl Clone for OmenaQueryStyleParserMaterializationV0 {
1416 fn clone(&self) -> Self {
1417 Self(
1418 self.0
1419 .as_ref()
1420 .map(|parsed| std::panic::AssertUnwindSafe(std::sync::Arc::clone(&parsed.0))),
1421 )
1422 }
1423}
1424
1425impl std::fmt::Debug for OmenaQueryStyleParserMaterializationV0 {
1426 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1427 formatter
1428 .debug_struct("OmenaQueryStyleParserMaterializationV0")
1429 .field("present", &self.0.is_some())
1430 .finish()
1431 }
1432}
1433
1434impl PartialEq for OmenaQueryStyleParserMaterializationV0 {
1435 fn eq(&self, _other: &Self) -> bool {
1436 true
1437 }
1438}
1439
1440impl Eq for OmenaQueryStyleParserMaterializationV0 {}
1441
1442#[derive(Debug, Clone, PartialEq, Eq)]
1443struct OmenaQueryStyleFactEntry {
1444 style_path: String,
1445 style_source: String,
1446 facts: OmenaQueryOmenaParserStyleFactsV0,
1447 icss_export_values: BTreeMap<String, String>,
1448 semantic_runtime_index: Option<omena_semantic::StyleRuntimeIndexFactsV0>,
1449 sass_module_public_variable_names: BTreeSet<String>,
1450 sass_module_public_mixin_names: BTreeSet<String>,
1451 sass_module_public_function_names: BTreeSet<String>,
1452 parser_materialization: OmenaQueryStyleParserMaterializationV0,
1453}
1454
1455impl OmenaQueryStyleFactEntry {
1456 #[cfg(feature = "salsa-memo")]
1457 fn with_parser_materialization(mut self, parsed: omena_parser::ParseResult) -> Self {
1458 self.parser_materialization = OmenaQueryStyleParserMaterializationV0(Some(
1459 std::panic::AssertUnwindSafe(std::sync::Arc::new(parsed)),
1460 ));
1461 self
1462 }
1463
1464 #[cfg(feature = "salsa-memo")]
1465 fn parser_materialization(&self) -> Option<&omena_parser::ParseResult> {
1466 self.parser_materialization
1467 .0
1468 .as_ref()
1469 .map(|parsed| parsed.0.as_ref())
1470 }
1471
1472 #[cfg(all(test, feature = "salsa-memo"))]
1473 fn parser_materialization_weak(&self) -> Option<std::sync::Weak<omena_parser::ParseResult>> {
1474 self.parser_materialization
1475 .0
1476 .as_ref()
1477 .map(|parsed| std::sync::Arc::downgrade(&parsed.0))
1478 }
1479}
1480
1481#[derive(Debug, Clone, PartialEq, Eq)]
1482pub struct OmenaQueryModuleInterfaceProjectionV0 {
1483 pub style_path: String,
1484 pub style_selector_definitions: Vec<OmenaQueryStyleSelectorDefinitionV0>,
1485 pub css_modules_style_facts: omena_semantic::CssModulesCrossFileStyleFactsV0,
1486 pub custom_property_decl_names: BTreeSet<String>,
1487 pub custom_property_ref_names: Vec<String>,
1488 pub style_dependency_sources: Vec<String>,
1489 pub sass_module_edges: Vec<OmenaQuerySassModuleEdgeFactV0>,
1490 pub sass_module_configurable_variable_names: BTreeSet<String>,
1491 pub sass_module_rule_configurations: Vec<OmenaQuerySassModuleRuleConfigurationSurfaceV0>,
1492}
1493
1494#[derive(Debug, Clone, PartialEq, Eq)]
1501#[non_exhaustive]
1502pub struct OmenaQueryModuleInterfaceChangeProjectionV0 {
1503 pub module_interface: OmenaQueryModuleInterfaceProjectionV0,
1504 pub sass_module_public_variable_names: BTreeSet<String>,
1505 pub sass_module_public_mixin_names: BTreeSet<String>,
1506 pub sass_module_public_function_names: BTreeSet<String>,
1507}
1508
1509#[derive(Debug, Clone, PartialEq, Eq)]
1510pub struct OmenaQuerySassModuleRuleConfigurationSurfaceV0 {
1511 pub edge_kind: &'static str,
1512 pub rule_ordinal: usize,
1513 pub variable_overrides: BTreeMap<String, String>,
1514 pub forward_variable_overrides: BTreeMap<String, omena_semantic::SassModuleVariableOverrideV0>,
1515}
1516
1517pub fn summarize_omena_query_sass_module_cross_file_resolution_for_workspace(
1518 style_sources: &[OmenaQueryStyleSourceInputV0],
1519 package_manifests: &[OmenaQueryStylePackageManifestV0],
1520 bundler_path_mappings: &[OmenaResolverBundlerPathAliasMappingV0],
1521 tsconfig_path_mappings: &[OmenaResolverTsconfigPathMappingV0],
1522) -> OmenaQuerySassModuleCrossFileResolutionV0 {
1523 #[cfg(feature = "salsa-memo")]
1524 {
1525 let mut host = OmenaQueryStyleMemoHostV0::new();
1526 let resolution_inputs = OmenaQueryStyleResolutionInputsV0 {
1527 package_manifests: package_manifests.to_vec(),
1528 tsconfig_path_mappings: tsconfig_path_mappings.to_vec(),
1529 bundler_path_mappings: bundler_path_mappings.to_vec(),
1530 ..OmenaQueryStyleResolutionInputsV0::default()
1531 };
1532 if let Some(selector) = host.workspace_revision_selector(
1533 style_sources,
1534 &[],
1535 package_manifests,
1536 &[],
1537 &resolution_inputs,
1538 ) {
1539 return selector.sass_module_cross_file_resolution().clone();
1540 }
1541 }
1542
1543 #[cfg(any(test, feature = "test-support"))]
1544 record_sass_module_resolution_direct_recompute_for_test();
1545
1546 let style_source_refs = style_sources
1547 .iter()
1548 .map(|source| (source.style_path.as_str(), source.style_source.as_str()))
1549 .collect::<Vec<_>>();
1550 let style_fact_entries = collect_omena_query_style_fact_entries(style_source_refs.as_slice());
1551 summarize_sass_module_cross_file_resolution(
1552 &style_fact_entries,
1553 package_manifests,
1554 bundler_path_mappings,
1555 tsconfig_path_mappings,
1556 )
1557}
1558
1559fn collect_omena_query_style_fact_entries(
1560 style_sources: &[(&str, &str)],
1561) -> Vec<OmenaQueryStyleFactEntry> {
1562 style_sources
1563 .iter()
1564 .map(|(style_path, style_source)| {
1565 collect_omena_query_style_fact_entry(style_path, style_source)
1566 })
1567 .collect()
1568}
1569
1570fn collect_omena_query_style_fact_entry(
1571 style_path: &str,
1572 style_source: &str,
1573) -> OmenaQueryStyleFactEntry {
1574 let dialect = omena_parser_dialect_for_style_path(style_path);
1575 let (raw_facts, icss_export_values) =
1576 collect_omena_query_style_facts_with_icss_values_raw(style_source, dialect);
1577 collect_omena_query_style_fact_entry_from_raw(
1578 style_path,
1579 style_source,
1580 dialect,
1581 raw_facts,
1582 icss_export_values,
1583 )
1584}
1585
1586fn collect_omena_query_style_fact_entry_from_raw(
1587 style_path: &str,
1588 style_source: &str,
1589 dialect: OmenaParserStyleDialect,
1590 raw_facts: ParsedStyleFacts,
1591 icss_export_values: BTreeMap<String, String>,
1592) -> OmenaQueryStyleFactEntry {
1593 let (
1594 sass_module_public_variable_names,
1595 sass_module_public_mixin_names,
1596 sass_module_public_function_names,
1597 ) = sass_module_public_member_names_from_parser_facts(&raw_facts);
1598 let facts = summarize_omena_query_omena_parser_style_facts_from_facts(raw_facts, dialect);
1599 let semantic_runtime_index = semantic_runtime_index_from_query_style_facts(style_path, &facts);
1600 OmenaQueryStyleFactEntry {
1601 style_path: style_path.to_string(),
1602 style_source: style_source.to_string(),
1603 semantic_runtime_index,
1604 facts,
1605 icss_export_values,
1606 sass_module_public_variable_names,
1607 sass_module_public_mixin_names,
1608 sass_module_public_function_names,
1609 parser_materialization: OmenaQueryStyleParserMaterializationV0::default(),
1610 }
1611}
1612
1613pub fn summarize_omena_query_module_interface_projection(
1618 style_path: &str,
1619 style_source: &str,
1620) -> OmenaQueryModuleInterfaceProjectionV0 {
1621 module_interface_projection_for_query(&collect_omena_query_style_fact_entry(
1622 style_path,
1623 style_source,
1624 ))
1625}
1626
1627pub fn summarize_omena_query_module_interface_change_projection(
1630 style_path: &str,
1631 style_source: &str,
1632) -> OmenaQueryModuleInterfaceChangeProjectionV0 {
1633 module_interface_change_projection_for_query(&collect_omena_query_style_fact_entry(
1634 style_path,
1635 style_source,
1636 ))
1637}
1638
1639pub fn summarize_omena_query_css_modules_interface_bundle(
1640 style_sources: &[OmenaQueryStyleSourceInputV0],
1641 package_manifests: &[OmenaQueryStylePackageManifestV0],
1642) -> OmenaQueryCssModulesInterfaceBundleV0 {
1643 match summarize_omena_query_css_modules_interface_bundle_inner(
1644 |module_instance| Ok::<_, std::convert::Infallible>(module_instance.clone()),
1645 style_sources,
1646 package_manifests,
1647 ) {
1648 Ok(bundle) => bundle,
1649 Err(unreachable) => match unreachable {},
1650 }
1651}
1652
1653pub fn summarize_omena_query_css_modules_interface_bundle_with_module_identity_root(
1654 workspace_root: &str,
1655 style_sources: &[OmenaQueryStyleSourceInputV0],
1656 package_manifests: &[OmenaQueryStylePackageManifestV0],
1657) -> Result<OmenaQueryCssModulesInterfaceBundleV0, String> {
1658 summarize_omena_query_css_modules_interface_bundle_inner(
1659 |module_instance| {
1660 transform::module_instance_key_relative_to_root(module_instance, workspace_root)
1661 },
1662 style_sources,
1663 package_manifests,
1664 )
1665}
1666
1667fn summarize_omena_query_css_modules_interface_bundle_inner<E>(
1668 mut token_module_instance: impl FnMut(
1669 &omena_parser::ModuleInstanceKeyV0,
1670 ) -> Result<omena_parser::ModuleInstanceKeyV0, E>,
1671 style_sources: &[OmenaQueryStyleSourceInputV0],
1672 package_manifests: &[OmenaQueryStylePackageManifestV0],
1673) -> Result<OmenaQueryCssModulesInterfaceBundleV0, E> {
1674 let style_source_refs = style_sources
1675 .iter()
1676 .map(|source| (source.style_path.as_str(), source.style_source.as_str()))
1677 .collect::<Vec<_>>();
1678 let entries = collect_omena_query_style_fact_entries(style_source_refs.as_slice());
1679 let projections = entries
1680 .iter()
1681 .map(module_interface_projection_for_query)
1682 .collect::<Vec<_>>();
1683 let icss_export_values_by_path = entries
1684 .iter()
1685 .map(|entry| (entry.style_path.clone(), entry.icss_export_values.clone()))
1686 .collect::<BTreeMap<_, _>>();
1687 let mut emitted_class_names = EmittedClassNameIndexV0::new();
1688 for entry in &entries {
1689 let module_instance =
1690 omena_parser::ModuleInstanceKeyV0::unconfigured(omena_parser::ModuleIdV0::new(
1691 crate::types::normalize_omena_query_style_path(entry.style_path.as_str()),
1692 ));
1693 let token_module_instance = token_module_instance(&module_instance)?;
1694 for rewrite in
1695 transform::derive_class_name_rewrites_for_module_instance(entry, &token_module_instance)
1696 {
1697 emitted_class_names.insert(
1698 (entry.style_path.clone(), rewrite.original_name),
1699 rewrite.rewritten_name,
1700 );
1701 }
1702 }
1703 let resolution = summarize_css_modules_cross_file_resolution(&entries, package_manifests);
1704 Ok(summarize_css_modules_interface_bundle_from_projections(
1705 &projections,
1706 &resolution,
1707 &icss_export_values_by_path,
1708 &emitted_class_names,
1709 ))
1710}
1711
1712fn module_interface_projection_for_query(
1713 entry: &OmenaQueryStyleFactEntry,
1714) -> OmenaQueryModuleInterfaceProjectionV0 {
1715 OmenaQueryModuleInterfaceProjectionV0 {
1716 style_path: entry.style_path.clone(),
1717 style_selector_definitions: style_selector_definitions_for_query(entry),
1718 css_modules_style_facts: css_modules_cross_file_style_fact_for_query(entry),
1719 custom_property_decl_names: custom_property_decl_names_for_query(entry),
1720 custom_property_ref_names: custom_property_ref_names_for_query(entry),
1721 style_dependency_sources: collect_style_module_dependency_sources_from_facts(&entry.facts),
1722 sass_module_edges: entry.facts.sass_module_edges.clone(),
1723 sass_module_configurable_variable_names: sass_module_configurable_variable_names_for_query(
1724 entry,
1725 ),
1726 sass_module_rule_configurations: sass_module_rule_configuration_surfaces_for_query(entry),
1727 }
1728}
1729
1730fn module_interface_change_projection_for_query(
1731 entry: &OmenaQueryStyleFactEntry,
1732) -> OmenaQueryModuleInterfaceChangeProjectionV0 {
1733 OmenaQueryModuleInterfaceChangeProjectionV0 {
1734 module_interface: module_interface_projection_for_query(entry),
1735 sass_module_public_variable_names: entry.sass_module_public_variable_names.clone(),
1736 sass_module_public_mixin_names: entry.sass_module_public_mixin_names.clone(),
1737 sass_module_public_function_names: entry.sass_module_public_function_names.clone(),
1738 }
1739}
1740
1741fn sass_module_public_member_names_from_parser_facts(
1742 facts: &ParsedStyleFacts,
1743) -> (BTreeSet<String>, BTreeSet<String>, BTreeSet<String>) {
1744 let variable_names = facts
1745 .variables
1746 .iter()
1747 .filter(|fact| fact.kind == ParsedVariableFactKind::ScssDeclaration && fact.is_top_level)
1748 .filter_map(|fact| canonical_public_sass_member_name(fact.name.as_str()))
1749 .collect();
1750 let mixin_names = facts
1751 .sass_symbols
1752 .iter()
1753 .filter(|fact| fact.kind == ParsedSassSymbolFactKind::MixinDeclaration && fact.is_top_level)
1754 .filter_map(|fact| canonical_public_sass_member_name(fact.name.as_str()))
1755 .collect();
1756 let function_names = facts
1757 .sass_symbols
1758 .iter()
1759 .filter(|fact| {
1760 fact.kind == ParsedSassSymbolFactKind::FunctionDeclaration && fact.is_top_level
1761 })
1762 .filter_map(|fact| canonical_public_sass_member_name(fact.name.as_str()))
1763 .collect();
1764 (variable_names, mixin_names, function_names)
1765}
1766
1767fn canonical_public_sass_member_name(name: &str) -> Option<String> {
1768 let name = name.trim().strip_prefix('$').unwrap_or_else(|| name.trim());
1769 (!name.is_empty() && !name.starts_with('-') && !name.starts_with('_'))
1770 .then(|| name.replace('_', "-"))
1771}
1772
1773fn style_selector_definitions_for_query(
1774 entry: &OmenaQueryStyleFactEntry,
1775) -> Vec<OmenaQueryStyleSelectorDefinitionV0> {
1776 let Some(candidates) = summarize_omena_query_style_hover_candidates(
1777 entry.style_path.as_str(),
1778 entry.style_source.as_str(),
1779 ) else {
1780 return Vec::new();
1781 };
1782 let mut definitions = candidates
1783 .candidates
1784 .into_iter()
1785 .filter_map(|candidate| {
1786 (candidate.kind == "selector").then(|| OmenaQueryStyleSelectorDefinitionV0 {
1787 uri: entry.style_path.clone(),
1788 name: candidate.name,
1789 range: candidate.range,
1790 })
1791 })
1792 .collect::<Vec<_>>();
1793 definitions.sort_by_key(|definition| {
1794 (
1795 definition.uri.clone(),
1796 definition.range.start.line,
1797 definition.range.start.character,
1798 definition.name.clone(),
1799 )
1800 });
1801 definitions.dedup_by(|left, right| {
1802 left.uri == right.uri && left.name == right.name && left.range == right.range
1803 });
1804 definitions
1805}
1806
1807fn custom_property_decl_names_for_query(entry: &OmenaQueryStyleFactEntry) -> BTreeSet<String> {
1808 entry
1809 .semantic_runtime_index
1810 .as_ref()
1811 .map(|index| index.custom_property_decl_names.iter().cloned().collect())
1812 .unwrap_or_else(|| {
1813 entry
1814 .facts
1815 .custom_property_decl_names
1816 .iter()
1817 .cloned()
1818 .collect()
1819 })
1820}
1821
1822fn custom_property_ref_names_for_query(entry: &OmenaQueryStyleFactEntry) -> Vec<String> {
1823 entry
1824 .semantic_runtime_index
1825 .as_ref()
1826 .map(|index| index.custom_property_ref_names.clone())
1827 .unwrap_or_else(|| entry.facts.custom_property_ref_names.clone())
1828}
1829
1830fn sass_module_configurable_variable_names_for_query(
1831 entry: &OmenaQueryStyleFactEntry,
1832) -> BTreeSet<String> {
1833 #[cfg(test)]
1834 CONFIGURABLE_NAMES_DERIVATIONS.with(|count| count.set(count.get() + 1));
1835 stylesheet_evaluation::derive_static_scss_stylesheet_module_configurable_variable_names(
1836 &entry.style_source,
1837 )
1838}
1839
1840fn sass_module_rule_configuration_surfaces_for_query(
1841 entry: &OmenaQueryStyleFactEntry,
1842) -> Vec<OmenaQuerySassModuleRuleConfigurationSurfaceV0> {
1843 let mut surfaces = Vec::new();
1844 let mut sass_use_rule_ordinal = 0usize;
1845 let mut sass_forward_rule_ordinal = 0usize;
1846 for edge in &entry.facts.sass_module_edges {
1847 match edge.kind {
1848 "sassUse" => {
1849 surfaces.push(OmenaQuerySassModuleRuleConfigurationSurfaceV0 {
1850 edge_kind: edge.kind,
1851 rule_ordinal: sass_use_rule_ordinal,
1852 variable_overrides:
1853 omena_semantic::derive_sass_module_rule_variable_overrides_at_ordinal(
1854 entry.style_source.as_str(),
1855 "@use",
1856 sass_use_rule_ordinal,
1857 ),
1858 forward_variable_overrides: BTreeMap::new(),
1859 });
1860 sass_use_rule_ordinal += 1;
1861 }
1862 "sassForward" => {
1863 let forward_variable_overrides =
1864 omena_semantic::derive_sass_module_forward_variable_overrides_at_ordinal(
1865 entry.style_source.as_str(),
1866 sass_forward_rule_ordinal,
1867 );
1868 surfaces.push(OmenaQuerySassModuleRuleConfigurationSurfaceV0 {
1869 edge_kind: edge.kind,
1870 rule_ordinal: sass_forward_rule_ordinal,
1871 variable_overrides: forward_variable_overrides
1872 .iter()
1873 .map(|(name, override_entry)| (name.clone(), override_entry.value.clone()))
1874 .collect(),
1875 forward_variable_overrides,
1876 });
1877 sass_forward_rule_ordinal += 1;
1878 }
1879 _ => {}
1880 }
1881 }
1882 surfaces
1883}
1884
1885fn semantic_runtime_index_from_query_style_facts(
1886 style_path: &str,
1887 facts: &OmenaQueryOmenaParserStyleFactsV0,
1888) -> Option<omena_semantic::StyleRuntimeIndexFactsV0> {
1889 let language = semantic_runtime_index_language_for_style_path(style_path)?;
1890 Some(omena_semantic::StyleRuntimeIndexFactsV0 {
1891 schema_version: "0",
1892 product: "omena-semantic.style-runtime-index-facts",
1893 style_path: style_path.to_string(),
1894 language,
1895 class_selector_names: facts.class_selector_names.clone(),
1896 custom_property_names: facts.custom_property_names.clone(),
1897 custom_property_decl_names: facts.custom_property_decl_names.clone(),
1898 custom_property_ref_names: facts.custom_property_ref_names.clone(),
1899 keyframe_names: facts.keyframe_names.clone(),
1900 animation_reference_names: facts.animation_reference_names.clone(),
1901 ready_surfaces: vec![
1902 "semanticRuntimeIndexFacts",
1903 "customPropertyRuntimeIndex",
1904 "keyframeRuntimeIndex",
1905 ],
1906 })
1907}
1908
1909fn semantic_runtime_index_language_for_style_path(style_path: &str) -> Option<&'static str> {
1910 if style_path.ends_with(".module.css") || style_path.ends_with(".css") {
1911 Some("css")
1912 } else if style_path.ends_with(".module.scss") || style_path.ends_with(".scss") {
1913 Some("scss")
1914 } else if style_path.ends_with(".module.sass") || style_path.ends_with(".sass") {
1915 Some("sass")
1916 } else if style_path.ends_with(".module.less") || style_path.ends_with(".less") {
1917 Some("less")
1918 } else {
1919 None
1920 }
1921}
1922
1923#[cfg(any(test, feature = "test-support"))]
1924thread_local! {
1925 static SASS_MODULE_RESOLUTION_DIRECT_RECOMPUTES: std::cell::Cell<u64> =
1926 const { std::cell::Cell::new(0) };
1927 static SASS_MODULE_RESOLUTION_INTERNAL_COMPUTES: std::cell::Cell<u64> =
1928 const { std::cell::Cell::new(0) };
1929}
1930
1931#[cfg(any(test, feature = "test-support"))]
1932pub fn reset_sass_module_resolution_direct_recompute_count_for_test() {
1933 SASS_MODULE_RESOLUTION_DIRECT_RECOMPUTES.with(|count| count.set(0));
1934}
1935
1936#[cfg(any(test, feature = "test-support"))]
1937pub fn reset_sass_module_resolution_internal_compute_count_for_test() {
1938 SASS_MODULE_RESOLUTION_INTERNAL_COMPUTES.with(|count| count.set(0));
1939}
1940
1941#[cfg(any(test, feature = "test-support"))]
1942pub fn read_sass_module_resolution_direct_recompute_count_for_test() -> u64 {
1943 SASS_MODULE_RESOLUTION_DIRECT_RECOMPUTES.with(|count| count.get())
1944}
1945
1946#[cfg(any(test, feature = "test-support"))]
1947pub fn read_sass_module_resolution_internal_compute_count_for_test() -> u64 {
1948 SASS_MODULE_RESOLUTION_INTERNAL_COMPUTES.with(|count| count.get())
1949}
1950
1951#[cfg(any(test, feature = "test-support"))]
1952fn record_sass_module_resolution_direct_recompute_for_test() {
1953 SASS_MODULE_RESOLUTION_DIRECT_RECOMPUTES.with(|count| {
1954 count.set(count.get() + 1);
1955 });
1956}
1957
1958#[cfg(any(test, feature = "test-support"))]
1959fn record_sass_module_resolution_internal_compute_for_test() {
1960 SASS_MODULE_RESOLUTION_INTERNAL_COMPUTES.with(|count| {
1961 count.set(count.get() + 1);
1962 });
1963}
1964
1965fn collect_load_path_roots(available_style_paths: &BTreeSet<&str>) -> Vec<String> {
1971 let mut roots = BTreeSet::new();
1972 for path in available_style_paths {
1973 let mut current = *path;
1974 while let Some(parent_end) = current.rfind('/') {
1977 if parent_end == 0 {
1978 roots.insert("/".to_string());
1980 break;
1981 }
1982 let parent = ¤t[..parent_end];
1983 if !roots.insert(parent.to_string()) {
1984 break;
1986 }
1987 current = parent;
1988 }
1989 }
1990 roots.into_iter().collect()
1991}
1992
1993fn summarize_sass_module_cross_file_resolution(
1994 style_fact_entries: &[OmenaQueryStyleFactEntry],
1995 package_manifests: &[OmenaQueryStylePackageManifestV0],
1996 bundler_path_mappings: &[OmenaResolverBundlerPathAliasMappingV0],
1997 tsconfig_path_mappings: &[OmenaResolverTsconfigPathMappingV0],
1998) -> OmenaQuerySassModuleCrossFileResolutionV0 {
1999 #[cfg(any(test, feature = "test-support"))]
2000 record_sass_module_resolution_internal_compute_for_test();
2001
2002 let available_style_paths = style_fact_entries
2003 .iter()
2004 .map(|entry| entry.style_path.as_str())
2005 .collect::<BTreeSet<_>>();
2006 let resolver_available_style_paths = style_fact_entries
2007 .iter()
2008 .flat_map(|entry| {
2009 [
2010 entry.style_path.clone(),
2011 resolver_style_path(entry.style_path.as_str()),
2012 ]
2013 })
2014 .collect::<BTreeSet<_>>();
2015 let resolver_available_style_path_refs = resolver_available_style_paths
2016 .iter()
2017 .map(String::as_str)
2018 .collect::<BTreeSet<_>>();
2019 let load_path_roots = collect_load_path_roots(&resolver_available_style_path_refs);
2025 let load_path_root_refs = load_path_roots
2026 .iter()
2027 .map(String::as_str)
2028 .collect::<Vec<_>>();
2029 let resolver_package_manifests = package_manifests
2030 .iter()
2031 .map(|manifest| OmenaResolverStylePackageManifestV0 {
2032 package_json_path: manifest.package_json_path.clone(),
2033 package_json_source: manifest.package_json_source.clone(),
2034 })
2035 .collect::<Vec<_>>();
2036 let source_by_path = style_fact_entries
2037 .iter()
2038 .map(|entry| (entry.style_path.clone(), entry.style_source.clone()))
2039 .collect::<BTreeMap<_, _>>();
2040 let mut edges = Vec::new();
2041
2042 for entry in style_fact_entries {
2043 let mut sass_use_rule_ordinal = 0usize;
2044 let mut sass_forward_rule_ordinal = 0usize;
2045 for edge in &entry.facts.sass_module_edges {
2046 let rule_ordinal = match edge.kind {
2047 "sassUse" => {
2048 let rule_ordinal = sass_use_rule_ordinal;
2049 sass_use_rule_ordinal += 1;
2050 rule_ordinal
2051 }
2052 "sassForward" => {
2053 let rule_ordinal = sass_forward_rule_ordinal;
2054 sass_forward_rule_ordinal += 1;
2055 rule_ordinal
2056 }
2057 _ => 0,
2058 };
2059 let resolution = summarize_omena_resolver_style_module_resolution_with_load_path_roots(
2060 resolver_style_path(entry.style_path.as_str()).as_str(),
2061 edge.source.as_str(),
2062 &resolver_available_style_path_refs,
2063 &resolver_package_manifests,
2064 bundler_path_mappings,
2065 tsconfig_path_mappings,
2066 &load_path_root_refs,
2067 );
2068 let status = if resolution.resolution_kind == "externalIgnored" {
2069 "external"
2070 } else if resolution.resolved_style_path.is_some() {
2071 "resolved"
2072 } else {
2073 "unresolved"
2074 };
2075 let resolved_style_path =
2076 resolution
2077 .resolved_style_path
2078 .and_then(|resolved_style_path| {
2079 canonical_available_style_path(
2080 resolved_style_path.as_str(),
2081 &available_style_paths,
2082 )
2083 .or(Some(resolved_style_path))
2084 });
2085 let symlink_chain_link_count = resolution.symlink_chain.link_count;
2086 let symlink_chain_links = resolution
2087 .symlink_chain
2088 .links
2089 .into_iter()
2090 .map(|link| OmenaQuerySymlinkChainLinkV0 {
2091 link_path: link.link_path,
2092 target_path: link.target_path,
2093 target_was_absolute: link.target_was_absolute,
2094 })
2095 .collect::<Vec<_>>();
2096 let configuration_evidence =
2097 transform::derive_static_scss_module_resolution_configuration_evidence(
2098 entry.style_source.as_str(),
2099 edge.kind,
2100 rule_ordinal,
2101 resolved_style_path.as_deref(),
2102 );
2103 let invalid_configuration_variable_names =
2104 resolved_style_path
2105 .as_deref()
2106 .and_then(|target_path| {
2107 source_by_path.get(target_path).map(|target_source| {
2108 let configurable_names = transform::derive_static_scss_module_configurable_variable_names_for_resolution(
2109 target_path,
2110 target_source,
2111 &available_style_paths,
2112 &source_by_path,
2113 package_manifests,
2114 bundler_path_mappings,
2115 tsconfig_path_mappings,
2116 );
2117 configuration_evidence
2118 .configuration_variable_names
2119 .iter()
2120 .filter(|name| !configurable_names.contains(*name))
2121 .cloned()
2122 .collect::<Vec<_>>()
2123 })
2124 })
2125 .unwrap_or_default();
2126 edges.push(OmenaQuerySassModuleEdgeResolutionV0 {
2127 from_style_path: entry.style_path.clone(),
2128 edge_kind: edge.kind,
2129 source: edge.source.clone(),
2130 rule_ordinal,
2131 namespace_kind: edge.namespace_kind,
2132 namespace: edge.namespace.clone(),
2133 forward_prefix: edge.forward_prefix.clone(),
2134 visibility_filter_kind: edge.visibility_filter_kind,
2135 visibility_filter_names: edge.visibility_filter_names.clone(),
2136 resolved_style_path,
2137 status,
2138 resolution_kind: resolution.resolution_kind,
2139 candidate_count: resolution.candidate_count,
2140 symlink_chain_link_count,
2141 symlink_chain_links,
2142 configuration_signature: configuration_evidence.configuration_signature,
2143 configuration_variable_count: configuration_evidence.configuration_variable_count,
2144 invalid_configuration_variable_names,
2145 module_instance_identity_key: configuration_evidence.module_instance_identity_key,
2146 });
2147 }
2148 }
2149
2150 edges.sort_by_key(|edge| {
2151 (
2152 edge.from_style_path.clone(),
2153 edge.edge_kind,
2154 edge.rule_ordinal,
2155 edge.source.clone(),
2156 )
2157 });
2158 let configurable_names_memo: RefCell<BTreeMap<String, BTreeSet<String>>> =
2159 RefCell::new(BTreeMap::new());
2160 let semantic_edges = sass_module_graph_edge_facts_for_query(&edges);
2161 let semantic_resolution = omena_semantic::summarize_sass_module_graph_resolution(
2162 style_fact_entries.len(),
2163 semantic_edges.as_slice(),
2164 &QuerySassModuleGraphConfigurationResolver {
2165 source_by_path: &source_by_path,
2166 available_style_paths: &available_style_paths,
2167 package_manifests,
2168 bundler_path_mappings,
2169 tsconfig_path_mappings,
2170 configurable_names_memo: &configurable_names_memo,
2171 },
2172 );
2173 let graph_closure_edges = semantic_resolution
2174 .graph_closure_edges
2175 .into_iter()
2176 .map(|edge| OmenaQuerySassModuleGraphClosureEdgeV0 {
2177 from_style_path: edge.from_style_path,
2178 target_style_path: edge.target_style_path,
2179 edge_kind: edge.edge_kind,
2180 depth: edge.depth,
2181 path: edge.path,
2182 namespace_kind: edge.namespace_kind,
2183 namespace: edge.namespace,
2184 forward_prefix: edge.forward_prefix,
2185 visibility_filter_kind: edge.visibility_filter_kind,
2186 visibility_filter_names: edge.visibility_filter_names,
2187 configuration_signature: edge.configuration_signature,
2188 configuration_variable_count: edge.configuration_variable_count,
2189 invalid_configuration_variable_names: edge.invalid_configuration_variable_names,
2190 module_instance_identity_key: edge.module_instance_identity_key,
2191 })
2192 .collect::<Vec<_>>();
2193 let cycles = semantic_resolution
2194 .cycles
2195 .into_iter()
2196 .map(|cycle| OmenaQuerySassModuleCycleV0 { path: cycle.path })
2197 .collect::<Vec<_>>();
2198 let symlink_chain_edge_count = edges
2199 .iter()
2200 .filter(|edge| edge.symlink_chain_link_count > 0)
2201 .count();
2202 let symlink_chain_link_count = edges.iter().map(|edge| edge.symlink_chain_link_count).sum();
2203
2204 OmenaQuerySassModuleCrossFileResolutionV0 {
2205 schema_version: "0",
2206 product: "omena-query.sass-module-cross-file-resolution",
2207 status: "moduleGraphClosureResolved",
2208 resolution_scope: "batchModuleGraph",
2209 style_count: semantic_resolution.style_count,
2210 module_edge_count: semantic_resolution.module_edge_count,
2211 resolved_module_edge_count: semantic_resolution.resolved_module_edge_count,
2212 unresolved_module_edge_count: semantic_resolution.unresolved_module_edge_count,
2213 external_module_edge_count: semantic_resolution.external_module_edge_count,
2214 symlink_chain_edge_count,
2215 symlink_chain_link_count,
2216 configured_module_instance_count: semantic_resolution.configured_module_instance_count,
2217 edges,
2218 graph_closure_edge_count: semantic_resolution.graph_closure_edge_count,
2219 cycle_count: semantic_resolution.cycle_count,
2220 visibility_filter_count: semantic_resolution.visibility_filter_count,
2221 graph_closure_edges,
2222 cycles,
2223 capabilities: OmenaQuerySassModuleCrossFileResolutionCapabilitiesV0 {
2224 omena_parser_module_edge_consumption_ready: true,
2225 resolver_backed_source_resolution_ready: true,
2226 package_manifest_resolution_ready: true,
2227 external_module_filtering_ready: true,
2228 graph_closure_ready: true,
2229 cycle_detection_ready: true,
2230 namespace_show_hide_filter_ready: true,
2231 configured_module_instance_identity_ready: true,
2232 symlink_chain_metadata_ready: true,
2233 },
2234 next_priorities: Vec::new(),
2235 }
2236}
2237
2238#[cfg_attr(not(feature = "salsa-memo"), allow(dead_code))]
2239#[allow(clippy::too_many_arguments)]
2240fn summarize_sass_module_edge_resolutions_for_module_interface(
2241 projection: &OmenaQueryModuleInterfaceProjectionV0,
2242 available_style_paths: &BTreeSet<&str>,
2243 resolver_available_style_path_refs: &BTreeSet<&str>,
2244 package_manifests: &[OmenaQueryStylePackageManifestV0],
2245 bundler_path_mappings: &[OmenaResolverBundlerPathAliasMappingV0],
2246 tsconfig_path_mappings: &[OmenaResolverTsconfigPathMappingV0],
2247 resolver_identity_index: Option<&OmenaResolverStyleModuleConfirmationIdentityIndexV0>,
2248 mut configurable_names_for_target: impl FnMut(&str) -> BTreeSet<String>,
2249) -> Vec<OmenaQuerySassModuleEdgeResolutionV0> {
2250 let load_path_roots = collect_load_path_roots(resolver_available_style_path_refs);
2251 let load_path_root_refs = load_path_roots
2252 .iter()
2253 .map(String::as_str)
2254 .collect::<Vec<_>>();
2255 let resolver_package_manifests = package_manifests
2256 .iter()
2257 .map(|manifest| OmenaResolverStylePackageManifestV0 {
2258 package_json_path: manifest.package_json_path.clone(),
2259 package_json_source: manifest.package_json_source.clone(),
2260 })
2261 .collect::<Vec<_>>();
2262 let mut edges = Vec::new();
2263 let mut sass_use_rule_ordinal = 0usize;
2264 let mut sass_forward_rule_ordinal = 0usize;
2265 for edge in &projection.sass_module_edges {
2266 let rule_ordinal = match edge.kind {
2267 "sassUse" => {
2268 let rule_ordinal = sass_use_rule_ordinal;
2269 sass_use_rule_ordinal += 1;
2270 rule_ordinal
2271 }
2272 "sassForward" => {
2273 let rule_ordinal = sass_forward_rule_ordinal;
2274 sass_forward_rule_ordinal += 1;
2275 rule_ordinal
2276 }
2277 _ => 0,
2278 };
2279 let resolution = summarize_omena_resolver_style_module_resolution_with_confirmation_inputs(
2280 resolver_style_path(projection.style_path.as_str()).as_str(),
2281 edge.source.as_str(),
2282 resolver_available_style_path_refs,
2283 &[],
2284 &resolver_package_manifests,
2285 bundler_path_mappings,
2286 tsconfig_path_mappings,
2287 &load_path_root_refs,
2288 OmenaResolverStyleModuleConfirmationOptionsV0 {
2289 identity_index: resolver_identity_index,
2290 ..OmenaResolverStyleModuleConfirmationOptionsV0::default()
2291 },
2292 );
2293 let status = if resolution.resolution_kind == "externalIgnored" {
2294 "external"
2295 } else if resolution.resolved_style_path.is_some() {
2296 "resolved"
2297 } else {
2298 "unresolved"
2299 };
2300 let resolved_style_path = resolution
2301 .resolved_style_path
2302 .and_then(|resolved_style_path| {
2303 canonical_available_style_path(resolved_style_path.as_str(), available_style_paths)
2304 .or(Some(resolved_style_path))
2305 });
2306 let symlink_chain_link_count = resolution.symlink_chain.link_count;
2307 let symlink_chain_links = resolution
2308 .symlink_chain
2309 .links
2310 .into_iter()
2311 .map(|link| OmenaQuerySymlinkChainLinkV0 {
2312 link_path: link.link_path,
2313 target_path: link.target_path,
2314 target_was_absolute: link.target_was_absolute,
2315 })
2316 .collect::<Vec<_>>();
2317 let variable_overrides =
2318 sass_module_rule_variable_overrides_from_interface(projection, edge.kind, rule_ordinal);
2319 let invalid_configuration_variable_names = resolved_style_path
2320 .as_deref()
2321 .filter(|_| !variable_overrides.is_empty())
2322 .map(|target_path| {
2323 let configurable_names = configurable_names_for_target(target_path);
2324 variable_overrides
2325 .keys()
2326 .filter(|name| !configurable_names.contains(*name))
2327 .cloned()
2328 .collect::<Vec<_>>()
2329 })
2330 .unwrap_or_default();
2331 let module_instance_identity_key = match edge.kind {
2332 "sassUse" | "sassForward" => resolved_style_path.as_deref().map(|target_path| {
2333 omena_semantic::summarize_sass_module_instance_identity_key(
2334 target_path,
2335 &variable_overrides,
2336 )
2337 }),
2338 _ => None,
2339 };
2340 edges.push(OmenaQuerySassModuleEdgeResolutionV0 {
2341 from_style_path: projection.style_path.clone(),
2342 edge_kind: edge.kind,
2343 source: edge.source.clone(),
2344 rule_ordinal,
2345 namespace_kind: edge.namespace_kind,
2346 namespace: edge.namespace.clone(),
2347 forward_prefix: edge.forward_prefix.clone(),
2348 visibility_filter_kind: edge.visibility_filter_kind,
2349 visibility_filter_names: edge.visibility_filter_names.clone(),
2350 resolved_style_path,
2351 status,
2352 resolution_kind: resolution.resolution_kind,
2353 candidate_count: resolution.candidate_count,
2354 symlink_chain_link_count,
2355 symlink_chain_links,
2356 configuration_signature: omena_semantic::summarize_sass_module_configuration_signature(
2357 &variable_overrides,
2358 ),
2359 configuration_variable_count: variable_overrides.len(),
2360 invalid_configuration_variable_names,
2361 module_instance_identity_key,
2362 });
2363 }
2364 edges.sort_by_key(|edge| {
2365 (
2366 edge.from_style_path.clone(),
2367 edge.edge_kind,
2368 edge.rule_ordinal,
2369 edge.source.clone(),
2370 )
2371 });
2372 edges
2373}
2374
2375#[cfg_attr(not(feature = "salsa-memo"), allow(dead_code))]
2376fn summarize_sass_module_cross_file_resolution_from_module_interfaces_and_edges(
2377 module_interfaces: &[OmenaQueryModuleInterfaceProjectionV0],
2378 edges: Vec<OmenaQuerySassModuleEdgeResolutionV0>,
2379 configurable_names_by_path: &BTreeMap<String, BTreeSet<String>>,
2380) -> OmenaQuerySassModuleCrossFileResolutionV0 {
2381 let module_interface_by_path = module_interfaces
2382 .iter()
2383 .map(|projection| (projection.style_path.clone(), projection))
2384 .collect::<BTreeMap<_, _>>();
2385 let semantic_edges = sass_module_graph_edge_facts_for_query(&edges);
2386 let semantic_resolution = omena_semantic::summarize_sass_module_graph_resolution(
2387 module_interfaces.len(),
2388 semantic_edges.as_slice(),
2389 &ModuleInterfaceSassModuleGraphConfigurationResolver {
2390 module_interface_by_path: &module_interface_by_path,
2391 configurable_names_by_path,
2392 },
2393 );
2394 let graph_closure_edges = semantic_resolution
2395 .graph_closure_edges
2396 .into_iter()
2397 .map(|edge| OmenaQuerySassModuleGraphClosureEdgeV0 {
2398 from_style_path: edge.from_style_path,
2399 target_style_path: edge.target_style_path,
2400 edge_kind: edge.edge_kind,
2401 depth: edge.depth,
2402 path: edge.path,
2403 namespace_kind: edge.namespace_kind,
2404 namespace: edge.namespace,
2405 forward_prefix: edge.forward_prefix,
2406 visibility_filter_kind: edge.visibility_filter_kind,
2407 visibility_filter_names: edge.visibility_filter_names,
2408 configuration_signature: edge.configuration_signature,
2409 configuration_variable_count: edge.configuration_variable_count,
2410 invalid_configuration_variable_names: edge.invalid_configuration_variable_names,
2411 module_instance_identity_key: edge.module_instance_identity_key,
2412 })
2413 .collect::<Vec<_>>();
2414 let cycles = semantic_resolution
2415 .cycles
2416 .into_iter()
2417 .map(|cycle| OmenaQuerySassModuleCycleV0 { path: cycle.path })
2418 .collect::<Vec<_>>();
2419 let symlink_chain_edge_count = edges
2420 .iter()
2421 .filter(|edge| edge.symlink_chain_link_count > 0)
2422 .count();
2423 let symlink_chain_link_count = edges.iter().map(|edge| edge.symlink_chain_link_count).sum();
2424
2425 OmenaQuerySassModuleCrossFileResolutionV0 {
2426 schema_version: "0",
2427 product: "omena-query.sass-module-cross-file-resolution",
2428 status: "moduleGraphClosureResolved",
2429 resolution_scope: "batchModuleGraph",
2430 style_count: semantic_resolution.style_count,
2431 module_edge_count: semantic_resolution.module_edge_count,
2432 resolved_module_edge_count: semantic_resolution.resolved_module_edge_count,
2433 unresolved_module_edge_count: semantic_resolution.unresolved_module_edge_count,
2434 external_module_edge_count: semantic_resolution.external_module_edge_count,
2435 symlink_chain_edge_count,
2436 symlink_chain_link_count,
2437 configured_module_instance_count: semantic_resolution.configured_module_instance_count,
2438 edges,
2439 graph_closure_edge_count: semantic_resolution.graph_closure_edge_count,
2440 cycle_count: semantic_resolution.cycle_count,
2441 visibility_filter_count: semantic_resolution.visibility_filter_count,
2442 graph_closure_edges,
2443 cycles,
2444 capabilities: OmenaQuerySassModuleCrossFileResolutionCapabilitiesV0 {
2445 omena_parser_module_edge_consumption_ready: true,
2446 resolver_backed_source_resolution_ready: true,
2447 package_manifest_resolution_ready: true,
2448 external_module_filtering_ready: true,
2449 graph_closure_ready: true,
2450 cycle_detection_ready: true,
2451 namespace_show_hide_filter_ready: true,
2452 configured_module_instance_identity_ready: true,
2453 symlink_chain_metadata_ready: true,
2454 },
2455 next_priorities: Vec::new(),
2456 }
2457}
2458
2459fn canonical_available_style_path(
2460 candidate: &str,
2461 available_style_paths: &BTreeSet<&str>,
2462) -> Option<String> {
2463 if available_style_paths.contains(candidate) {
2464 return Some(candidate.to_string());
2465 }
2466 let candidate_path = style_path_equivalence_key(candidate)?;
2467 available_style_paths
2468 .iter()
2469 .find(|available| {
2470 style_path_equivalence_key(available).as_deref() == Some(candidate_path.as_path())
2471 })
2472 .map(|available| (*available).to_string())
2473}
2474
2475#[cfg_attr(not(feature = "salsa-memo"), allow(dead_code))]
2476fn sass_module_rule_variable_overrides_from_interface(
2477 projection: &OmenaQueryModuleInterfaceProjectionV0,
2478 edge_kind: &'static str,
2479 rule_ordinal: usize,
2480) -> BTreeMap<String, String> {
2481 projection
2482 .sass_module_rule_configurations
2483 .iter()
2484 .find(|surface| surface.edge_kind == edge_kind && surface.rule_ordinal == rule_ordinal)
2485 .map(|surface| surface.variable_overrides.clone())
2486 .unwrap_or_default()
2487}
2488
2489#[cfg_attr(not(feature = "salsa-memo"), allow(dead_code))]
2490fn sass_module_forward_variable_overrides_from_interface(
2491 projection: &OmenaQueryModuleInterfaceProjectionV0,
2492 rule_ordinal: usize,
2493) -> BTreeMap<String, omena_semantic::SassModuleVariableOverrideV0> {
2494 sass_module_forward_variable_overrides_from_rule_configurations(
2495 projection.sass_module_rule_configurations.as_slice(),
2496 rule_ordinal,
2497 )
2498}
2499
2500#[cfg_attr(not(feature = "salsa-memo"), allow(dead_code))]
2501fn sass_module_forward_variable_overrides_from_rule_configurations(
2502 rule_configurations: &[OmenaQuerySassModuleRuleConfigurationSurfaceV0],
2503 rule_ordinal: usize,
2504) -> BTreeMap<String, omena_semantic::SassModuleVariableOverrideV0> {
2505 rule_configurations
2506 .iter()
2507 .find(|surface| surface.edge_kind == "sassForward" && surface.rule_ordinal == rule_ordinal)
2508 .map(|surface| surface.forward_variable_overrides.clone())
2509 .unwrap_or_default()
2510}
2511
2512#[derive(Debug, Clone, Copy)]
2513#[cfg_attr(not(feature = "salsa-memo"), allow(dead_code))]
2514struct ModuleInterfaceSassModuleGraphConfigurationResolver<'a> {
2515 module_interface_by_path: &'a BTreeMap<String, &'a OmenaQueryModuleInterfaceProjectionV0>,
2516 configurable_names_by_path: &'a BTreeMap<String, BTreeSet<String>>,
2517}
2518
2519impl omena_semantic::SassModuleGraphConfigurationResolverV0
2520 for ModuleInterfaceSassModuleGraphConfigurationResolver<'_>
2521{
2522 fn use_variable_overrides(
2523 &self,
2524 request: omena_semantic::SassModuleUseConfigurationRequestV0<'_>,
2525 ) -> BTreeMap<String, String> {
2526 self.module_interface_by_path
2527 .get(request.from_style_path)
2528 .map(|projection| {
2529 sass_module_rule_variable_overrides_from_interface(
2530 projection,
2531 "sassUse",
2532 request.rule_ordinal,
2533 )
2534 })
2535 .unwrap_or_default()
2536 }
2537
2538 fn forward_effective_variable_overrides(
2539 &self,
2540 request: omena_semantic::SassModuleForwardConfigurationRequestV0<'_>,
2541 ) -> BTreeMap<String, String> {
2542 let Some(projection) = self.module_interface_by_path.get(request.from_style_path) else {
2543 return BTreeMap::new();
2544 };
2545 let explicit_variable_overrides =
2546 sass_module_forward_variable_overrides_from_interface(projection, request.rule_ordinal);
2547 omena_semantic::derive_sass_forward_effective_variable_overrides(
2548 &explicit_variable_overrides,
2549 request.inherited_variable_overrides,
2550 request.forward_prefix,
2551 request.visibility_filter_kind,
2552 request.visibility_filter_names,
2553 request.configurable_names,
2554 )
2555 }
2556
2557 fn configurable_names(&self, target_style_path: &str) -> BTreeSet<String> {
2558 self.configurable_names_by_path
2559 .get(target_style_path)
2560 .cloned()
2561 .unwrap_or_default()
2562 }
2563}
2564
2565fn style_path_equivalence_key(path_or_uri: &str) -> Option<PathBuf> {
2566 let path = path_or_uri.strip_prefix("file://").unwrap_or(path_or_uri);
2567 Some(Path::new(path).components().collect())
2568}
2569
2570fn resolver_style_path(path_or_uri: &str) -> String {
2571 path_or_uri
2572 .strip_prefix("file://")
2573 .unwrap_or(path_or_uri)
2574 .to_string()
2575}
2576
2577#[derive(Debug, Clone, Copy)]
2578struct QuerySassModuleGraphConfigurationResolver<'a> {
2579 source_by_path: &'a BTreeMap<String, String>,
2580 available_style_paths: &'a BTreeSet<&'a str>,
2581 package_manifests: &'a [OmenaQueryStylePackageManifestV0],
2582 bundler_path_mappings: &'a [OmenaResolverBundlerPathAliasMappingV0],
2583 tsconfig_path_mappings: &'a [OmenaResolverTsconfigPathMappingV0],
2584 configurable_names_memo: &'a RefCell<BTreeMap<String, BTreeSet<String>>>,
2585}
2586
2587impl omena_semantic::SassModuleGraphConfigurationResolverV0
2588 for QuerySassModuleGraphConfigurationResolver<'_>
2589{
2590 fn use_variable_overrides(
2591 &self,
2592 request: omena_semantic::SassModuleUseConfigurationRequestV0<'_>,
2593 ) -> BTreeMap<String, String> {
2594 let Some(style_source) = self.source_by_path.get(request.from_style_path) else {
2595 return BTreeMap::new();
2596 };
2597 omena_semantic::derive_sass_module_rule_variable_overrides_at_ordinal(
2598 style_source,
2599 "@use",
2600 request.rule_ordinal,
2601 )
2602 }
2603
2604 fn forward_effective_variable_overrides(
2605 &self,
2606 request: omena_semantic::SassModuleForwardConfigurationRequestV0<'_>,
2607 ) -> BTreeMap<String, String> {
2608 let Some(style_source) = self.source_by_path.get(request.from_style_path) else {
2609 return BTreeMap::new();
2610 };
2611 omena_semantic::derive_sass_module_forward_effective_variable_overrides_at_ordinal(
2612 style_source,
2613 request.rule_ordinal,
2614 request.inherited_variable_overrides,
2615 request.forward_prefix,
2616 request.visibility_filter_kind,
2617 request.visibility_filter_names,
2618 request.configurable_names,
2619 )
2620 }
2621
2622 fn configurable_names(&self, target_style_path: &str) -> BTreeSet<String> {
2623 memoized_configurable_names(target_style_path, self)
2624 }
2625}
2626
2627fn sass_module_graph_edge_facts_for_query(
2628 edges: &[OmenaQuerySassModuleEdgeResolutionV0],
2629) -> Vec<omena_semantic::SassModuleGraphEdgeFactV0> {
2630 edges
2631 .iter()
2632 .map(|edge| omena_semantic::SassModuleGraphEdgeFactV0 {
2633 from_style_path: edge.from_style_path.clone(),
2634 edge_kind: edge.edge_kind,
2635 source: edge.source.clone(),
2636 rule_ordinal: edge.rule_ordinal,
2637 namespace_kind: edge.namespace_kind,
2638 namespace: edge.namespace.clone(),
2639 forward_prefix: edge.forward_prefix.clone(),
2640 visibility_filter_kind: edge.visibility_filter_kind,
2641 visibility_filter_names: edge.visibility_filter_names.clone(),
2642 resolved_style_path: edge.resolved_style_path.clone(),
2643 status: edge.status,
2644 configuration_signature: edge.configuration_signature.clone(),
2645 configuration_variable_count: edge.configuration_variable_count,
2646 invalid_configuration_variable_names: edge.invalid_configuration_variable_names.clone(),
2647 module_instance_identity_key: edge.module_instance_identity_key.clone(),
2648 })
2649 .collect()
2650}
2651
2652#[cfg(test)]
2658thread_local! {
2659 static CONFIGURABLE_NAMES_DERIVATIONS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
2660}
2661
2662#[cfg(test)]
2663pub(crate) fn reset_configurable_names_derivation_count() {
2664 CONFIGURABLE_NAMES_DERIVATIONS.with(|count| count.set(0));
2665}
2666
2667#[cfg(test)]
2668pub(crate) fn configurable_names_derivation_count() -> u64 {
2669 CONFIGURABLE_NAMES_DERIVATIONS.with(|count| count.get())
2670}
2671
2672#[cfg(test)]
2673pub(crate) fn with_rawallpaths_closure<R>(body: impl FnOnce() -> R) -> R {
2674 omena_semantic::with_sass_module_rawallpaths_closure_for_test(body)
2675}
2676
2677fn memoized_configurable_names(
2678 target_style_path: &str,
2679 context: &QuerySassModuleGraphConfigurationResolver<'_>,
2680) -> BTreeSet<String> {
2681 {
2682 let cache = context.configurable_names_memo.borrow();
2683 if let Some(cached) = cache.get(target_style_path) {
2684 return cached.clone();
2685 }
2686 }
2687 let computed = context
2688 .source_by_path
2689 .get(target_style_path)
2690 .map(|target_source| {
2691 #[cfg(test)]
2692 CONFIGURABLE_NAMES_DERIVATIONS.with(|count| count.set(count.get() + 1));
2693 transform::derive_static_scss_module_configurable_variable_names_for_resolution(
2694 target_style_path,
2695 target_source,
2696 context.available_style_paths,
2697 context.source_by_path,
2698 context.package_manifests,
2699 context.bundler_path_mappings,
2700 context.tsconfig_path_mappings,
2701 )
2702 })
2703 .unwrap_or_default();
2704 context
2705 .configurable_names_memo
2706 .borrow_mut()
2707 .insert(target_style_path.to_string(), computed.clone());
2708 computed
2709}
2710
2711fn summarize_css_modules_cross_file_resolution(
2712 style_fact_entries: &[OmenaQueryStyleFactEntry],
2713 package_manifests: &[OmenaQueryStylePackageManifestV0],
2714) -> OmenaQueryCssModulesCrossFileResolutionV0 {
2715 summarize_css_modules_cross_file_resolution_with_resolution_inputs(
2716 style_fact_entries,
2717 package_manifests,
2718 &OmenaQueryStyleResolutionInputsV0::default(),
2719 )
2720}
2721
2722fn summarize_css_modules_cross_file_resolution_with_resolution_inputs(
2723 style_fact_entries: &[OmenaQueryStyleFactEntry],
2724 package_manifests: &[OmenaQueryStylePackageManifestV0],
2725 resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
2726) -> OmenaQueryCssModulesCrossFileResolutionV0 {
2727 let semantic_facts = css_modules_cross_file_style_facts_for_query(style_fact_entries);
2728 let style_import_edges = style_import_reachability_edges_for_query(
2729 style_fact_entries,
2730 package_manifests,
2731 resolution_inputs,
2732 );
2733 summarize_css_modules_cross_file_resolution_from_semantic_inputs(
2734 semantic_facts.as_slice(),
2735 style_import_edges.as_slice(),
2736 package_manifests,
2737 )
2738}
2739
2740#[cfg_attr(not(feature = "salsa-memo"), allow(dead_code))]
2741fn summarize_css_modules_cross_file_resolution_from_module_interfaces_and_import_edges(
2742 module_interfaces: &[OmenaQueryModuleInterfaceProjectionV0],
2743 package_manifests: &[OmenaQueryStylePackageManifestV0],
2744 edges: Vec<OmenaQueryCssModulesImportEdgeResolutionV0>,
2745) -> OmenaQueryCssModulesCrossFileResolutionV0 {
2746 let semantic_facts = module_interfaces
2747 .iter()
2748 .map(|projection| projection.css_modules_style_facts.clone())
2749 .collect::<Vec<_>>();
2750 summarize_css_modules_cross_file_resolution_from_semantic_facts_and_import_edges(
2751 semantic_facts,
2752 package_manifests,
2753 edges,
2754 )
2755}
2756
2757#[cfg_attr(not(feature = "salsa-memo"), allow(dead_code))]
2758fn summarize_css_modules_cross_file_resolution_from_module_interfaces_and_pre_resolved_import_edges(
2759 module_interfaces: &[OmenaQueryModuleInterfaceProjectionV0],
2760 package_manifests: &[OmenaQueryStylePackageManifestV0],
2761 edges: Vec<OmenaQueryCssModulesImportEdgeResolutionV0>,
2762) -> OmenaQueryCssModulesCrossFileResolutionV0 {
2763 let mut semantic_facts = module_interfaces
2764 .iter()
2765 .map(|projection| projection.css_modules_style_facts.clone())
2766 .collect::<Vec<_>>();
2767 let resolved_sources = edges
2768 .iter()
2769 .filter_map(|edge| {
2770 edge.resolved_style_path.as_ref().map(|resolved| {
2771 (
2772 (
2773 edge.from_style_path.as_str(),
2774 edge.import_kind,
2775 edge.source.as_str(),
2776 ),
2777 resolved.as_str(),
2778 )
2779 })
2780 })
2781 .collect::<BTreeMap<_, _>>();
2782 for facts in &mut semantic_facts {
2783 for edge in &mut facts.css_module_composes_edges {
2784 let Some(source) = edge.import_source.as_deref() else {
2785 continue;
2786 };
2787 if let Some(resolved) =
2788 resolved_sources.get(&(facts.style_path.as_str(), "composes", source))
2789 {
2790 edge.import_source = Some((*resolved).to_string());
2791 }
2792 }
2793 for edge in &mut facts.css_module_value_import_edges {
2794 if let Some(resolved) = resolved_sources.get(&(
2795 facts.style_path.as_str(),
2796 "value",
2797 edge.import_source.as_str(),
2798 )) {
2799 edge.import_source = (*resolved).to_string();
2800 }
2801 }
2802 for edge in &mut facts.icss_import_edges {
2803 if let Some(resolved) = resolved_sources.get(&(
2804 facts.style_path.as_str(),
2805 "icss",
2806 edge.import_source.as_str(),
2807 )) {
2808 edge.import_source = (*resolved).to_string();
2809 }
2810 }
2811 }
2812 summarize_css_modules_cross_file_resolution_from_semantic_facts_and_import_edges(
2813 semantic_facts,
2814 package_manifests,
2815 edges,
2816 )
2817}
2818
2819fn summarize_css_modules_cross_file_resolution_from_semantic_facts_and_import_edges(
2820 semantic_facts: Vec<omena_semantic::CssModulesCrossFileStyleFactsV0>,
2821 package_manifests: &[OmenaQueryStylePackageManifestV0],
2822 edges: Vec<OmenaQueryCssModulesImportEdgeResolutionV0>,
2823) -> OmenaQueryCssModulesCrossFileResolutionV0 {
2824 let semantic_package_manifests = semantic_package_manifests_for_query(package_manifests);
2825 let closure_summary = omena_semantic::summarize_css_modules_cross_file_closure(
2826 semantic_facts.as_slice(),
2827 semantic_package_manifests.as_slice(),
2828 );
2829 let composes_closure_edges = closure_summary
2830 .composes_closure_edges
2831 .into_iter()
2832 .map(|edge| OmenaQueryCssModulesComposesClosureEdgeV0 {
2833 from_style_path: edge.from_style_path,
2834 owner_selector_name: edge.owner_selector_name,
2835 target_style_path: edge.target_style_path,
2836 target_selector_name: edge.target_selector_name,
2837 depth: edge.depth,
2838 path: edge.path,
2839 })
2840 .collect::<Vec<_>>();
2841 let value_closure_edges = closure_summary
2842 .value_closure_edges
2843 .into_iter()
2844 .map(|edge| OmenaQueryCssModulesValueClosureEdgeV0 {
2845 from_style_path: edge.from_style_path,
2846 value_name: edge.value_name,
2847 target_style_path: edge.target_style_path,
2848 target_value_name: edge.target_value_name,
2849 depth: edge.depth,
2850 path: edge.path,
2851 })
2852 .collect::<Vec<_>>();
2853 let icss_closure_edges = closure_summary
2854 .icss_closure_edges
2855 .into_iter()
2856 .map(|edge| OmenaQueryCssModulesIcssClosureEdgeV0 {
2857 from_style_path: edge.from_style_path,
2858 name: edge.name,
2859 target_style_path: edge.target_style_path,
2860 target_name: edge.target_name,
2861 depth: edge.depth,
2862 path: edge.path,
2863 })
2864 .collect::<Vec<_>>();
2865 let cycles = closure_summary
2866 .cycles
2867 .into_iter()
2868 .map(|cycle| OmenaQueryCssModulesCycleV0 {
2869 kind: cycle.kind,
2870 path: cycle.path,
2871 })
2872 .collect::<Vec<_>>();
2873
2874 css_modules_cross_file_resolution_from_query_parts(
2875 semantic_facts.len(),
2876 edges,
2877 OmenaQueryCssModulesClosurePartsV0 {
2878 composes_closure_edge_count: closure_summary.composes_closure_edge_count,
2879 value_closure_edge_count: closure_summary.value_closure_edge_count,
2880 icss_closure_edge_count: closure_summary.icss_closure_edge_count,
2881 composes_cycle_count: closure_summary.composes_cycle_count,
2882 value_cycle_count: closure_summary.value_cycle_count,
2883 icss_cycle_count: closure_summary.icss_cycle_count,
2884 composes_closure_edges,
2885 value_closure_edges,
2886 icss_closure_edges,
2887 cycles,
2888 },
2889 )
2890}
2891
2892#[cfg_attr(not(feature = "salsa-memo"), allow(dead_code))]
2893fn summarize_css_modules_import_edge_resolutions_for_module_interface(
2894 origin: &OmenaQueryModuleInterfaceProjectionV0,
2895 target_interfaces: &[OmenaQueryModuleInterfaceProjectionV0],
2896 available_style_paths: &BTreeSet<&str>,
2897 style_import_edges: &[omena_semantic::StyleImportReachabilityEdgeFactV0],
2898 package_manifests: &[OmenaQueryStylePackageManifestV0],
2899 resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
2900 resolver_identity_index: Option<&OmenaResolverStyleModuleConfirmationIdentityIndexV0>,
2901) -> Vec<OmenaQueryCssModulesImportEdgeResolutionV0> {
2902 let mut facts_by_path = target_interfaces
2903 .iter()
2904 .map(|projection| {
2905 (
2906 projection.style_path.as_str(),
2907 &projection.css_modules_style_facts,
2908 )
2909 })
2910 .collect::<BTreeMap<_, _>>();
2911 facts_by_path.insert(origin.style_path.as_str(), &origin.css_modules_style_facts);
2912 let reachable =
2913 css_modules_import_reachability_for_origin(origin.style_path.as_str(), style_import_edges);
2914 let mut edges = Vec::new();
2915
2916 for edge in &origin.css_modules_style_facts.css_module_composes_edges {
2917 let Some(source) = edge.import_source.as_deref() else {
2918 continue;
2919 };
2920 edges.push(resolve_css_modules_import_edge_for_query(
2921 origin.style_path.as_str(),
2922 "composes",
2923 source,
2924 edge.target_names.as_slice(),
2925 available_style_paths,
2926 &facts_by_path,
2927 &reachable,
2928 package_manifests,
2929 resolution_inputs,
2930 resolver_identity_index,
2931 |target| target.class_selector_names.as_slice(),
2932 ));
2933 }
2934
2935 for edge in &origin.css_modules_style_facts.css_module_value_import_edges {
2936 edges.push(resolve_css_modules_import_edge_for_query(
2937 origin.style_path.as_str(),
2938 "value",
2939 edge.import_source.as_str(),
2940 std::slice::from_ref(&edge.remote_name),
2941 available_style_paths,
2942 &facts_by_path,
2943 &reachable,
2944 package_manifests,
2945 resolution_inputs,
2946 resolver_identity_index,
2947 |target| target.css_module_value_definition_names.as_slice(),
2948 ));
2949 }
2950
2951 for edge in &origin.css_modules_style_facts.icss_import_edges {
2952 edges.push(resolve_css_modules_import_edge_for_query(
2953 origin.style_path.as_str(),
2954 "icss",
2955 edge.import_source.as_str(),
2956 std::slice::from_ref(&edge.remote_name),
2957 available_style_paths,
2958 &facts_by_path,
2959 &reachable,
2960 package_manifests,
2961 resolution_inputs,
2962 resolver_identity_index,
2963 |target| target.icss_export_names.as_slice(),
2964 ));
2965 }
2966
2967 edges.sort_by_key(|edge| {
2968 (
2969 edge.from_style_path.clone(),
2970 edge.import_kind,
2971 edge.source.clone(),
2972 )
2973 });
2974 edges
2975}
2976
2977fn summarize_css_modules_cross_file_resolution_from_semantic_inputs(
2978 semantic_facts: &[omena_semantic::CssModulesCrossFileStyleFactsV0],
2979 style_import_edges: &[omena_semantic::StyleImportReachabilityEdgeFactV0],
2980 package_manifests: &[OmenaQueryStylePackageManifestV0],
2981) -> OmenaQueryCssModulesCrossFileResolutionV0 {
2982 let semantic_package_manifests = semantic_package_manifests_for_query(package_manifests);
2983 let semantic_resolution = omena_semantic::summarize_css_modules_cross_file_resolution(
2984 semantic_facts,
2985 style_import_edges,
2986 semantic_package_manifests.as_slice(),
2987 );
2988 let composes_closure_edges = semantic_resolution
2989 .composes_closure_edges
2990 .into_iter()
2991 .map(|edge| OmenaQueryCssModulesComposesClosureEdgeV0 {
2992 from_style_path: edge.from_style_path,
2993 owner_selector_name: edge.owner_selector_name,
2994 target_style_path: edge.target_style_path,
2995 target_selector_name: edge.target_selector_name,
2996 depth: edge.depth,
2997 path: edge.path,
2998 })
2999 .collect::<Vec<_>>();
3000 let value_closure_edges = semantic_resolution
3001 .value_closure_edges
3002 .into_iter()
3003 .map(|edge| OmenaQueryCssModulesValueClosureEdgeV0 {
3004 from_style_path: edge.from_style_path,
3005 value_name: edge.value_name,
3006 target_style_path: edge.target_style_path,
3007 target_value_name: edge.target_value_name,
3008 depth: edge.depth,
3009 path: edge.path,
3010 })
3011 .collect::<Vec<_>>();
3012 let icss_closure_edges = semantic_resolution
3013 .icss_closure_edges
3014 .into_iter()
3015 .map(|edge| OmenaQueryCssModulesIcssClosureEdgeV0 {
3016 from_style_path: edge.from_style_path,
3017 name: edge.name,
3018 target_style_path: edge.target_style_path,
3019 target_name: edge.target_name,
3020 depth: edge.depth,
3021 path: edge.path,
3022 })
3023 .collect::<Vec<_>>();
3024 let edges = semantic_resolution
3025 .edges
3026 .into_iter()
3027 .map(|edge| OmenaQueryCssModulesImportEdgeResolutionV0 {
3028 from_style_path: edge.from_style_path,
3029 import_kind: edge.import_kind,
3030 source: edge.source,
3031 resolved_style_path: edge.resolved_style_path,
3032 status: edge.status,
3033 import_graph_distance: edge.import_graph_distance,
3034 import_graph_order: edge.import_graph_order,
3035 imported_names: edge.imported_names,
3036 exported_names: edge.exported_names,
3037 matched_names: edge.matched_names,
3038 })
3039 .collect::<Vec<_>>();
3040 let cycles = semantic_resolution
3041 .cycles
3042 .into_iter()
3043 .map(|cycle| OmenaQueryCssModulesCycleV0 {
3044 kind: cycle.kind,
3045 path: cycle.path,
3046 })
3047 .collect::<Vec<_>>();
3048
3049 OmenaQueryCssModulesCrossFileResolutionV0 {
3050 schema_version: "0",
3051 product: "omena-query.css-modules-cross-file-resolution",
3052 status: "semanticLayerOwnedResolutionAdapter",
3053 resolution_scope: "batchImportGraph",
3054 style_count: semantic_resolution.style_count,
3055 import_edge_count: semantic_resolution.import_edge_count,
3056 resolved_import_edge_count: semantic_resolution.resolved_import_edge_count,
3057 unresolved_import_edge_count: semantic_resolution.unresolved_import_edge_count,
3058 matched_name_count: semantic_resolution.matched_name_count,
3059 edges,
3060 composes_closure_edge_count: composes_closure_edges.len(),
3061 value_closure_edge_count: value_closure_edges.len(),
3062 icss_closure_edge_count: icss_closure_edges.len(),
3063 composes_cycle_count: semantic_resolution.composes_cycle_count,
3064 value_cycle_count: semantic_resolution.value_cycle_count,
3065 icss_cycle_count: semantic_resolution.icss_cycle_count,
3066 composes_closure_edges,
3067 value_closure_edges,
3068 icss_closure_edges,
3069 cycles,
3070 capabilities: OmenaQueryCssModulesCrossFileResolutionCapabilitiesV0 {
3071 semantic_layer_owned: semantic_resolution.capabilities.semantic_layer_owned,
3072 import_source_resolution_ready: semantic_resolution
3073 .capabilities
3074 .import_source_resolution_ready,
3075 cross_file_resolution_ready: true,
3076 composes_closure_ready: semantic_resolution.capabilities.transitive_closure_ready,
3077 composes_name_match_ready: semantic_resolution.capabilities.composes_name_match_ready,
3078 value_name_match_ready: semantic_resolution.capabilities.value_name_match_ready,
3079 icss_name_match_ready: semantic_resolution.capabilities.icss_name_match_ready,
3080 transitive_closure_ready: semantic_resolution.capabilities.transitive_closure_ready,
3081 value_graph_closure_ready: semantic_resolution.capabilities.value_graph_closure_ready,
3082 icss_export_import_closure_ready: semantic_resolution
3083 .capabilities
3084 .icss_export_import_closure_ready,
3085 cycle_detection_ready: semantic_resolution.capabilities.cycle_detection_ready,
3086 },
3087 next_priorities: vec![],
3088 }
3089}
3090
3091#[derive(Debug, Clone, PartialEq, Eq)]
3092#[cfg_attr(not(feature = "salsa-memo"), allow(dead_code))]
3093struct OmenaQueryCssModulesClosurePartsV0 {
3094 composes_closure_edge_count: usize,
3095 value_closure_edge_count: usize,
3096 icss_closure_edge_count: usize,
3097 composes_cycle_count: usize,
3098 value_cycle_count: usize,
3099 icss_cycle_count: usize,
3100 composes_closure_edges: Vec<OmenaQueryCssModulesComposesClosureEdgeV0>,
3101 value_closure_edges: Vec<OmenaQueryCssModulesValueClosureEdgeV0>,
3102 icss_closure_edges: Vec<OmenaQueryCssModulesIcssClosureEdgeV0>,
3103 cycles: Vec<OmenaQueryCssModulesCycleV0>,
3104}
3105
3106#[cfg_attr(not(feature = "salsa-memo"), allow(dead_code))]
3107fn css_modules_cross_file_resolution_from_query_parts(
3108 style_count: usize,
3109 edges: Vec<OmenaQueryCssModulesImportEdgeResolutionV0>,
3110 closure: OmenaQueryCssModulesClosurePartsV0,
3111) -> OmenaQueryCssModulesCrossFileResolutionV0 {
3112 let resolved_import_edge_count = edges
3113 .iter()
3114 .filter(|edge| edge.resolved_style_path.is_some())
3115 .count();
3116 let matched_name_count = edges
3117 .iter()
3118 .map(|edge| edge.matched_names.len())
3119 .sum::<usize>();
3120
3121 OmenaQueryCssModulesCrossFileResolutionV0 {
3122 schema_version: "0",
3123 product: "omena-query.css-modules-cross-file-resolution",
3124 status: "semanticLayerOwnedResolutionAdapter",
3125 resolution_scope: "batchImportGraph",
3126 style_count,
3127 import_edge_count: edges.len(),
3128 resolved_import_edge_count,
3129 unresolved_import_edge_count: edges.len() - resolved_import_edge_count,
3130 matched_name_count,
3131 edges,
3132 composes_closure_edge_count: closure.composes_closure_edge_count,
3133 value_closure_edge_count: closure.value_closure_edge_count,
3134 icss_closure_edge_count: closure.icss_closure_edge_count,
3135 composes_cycle_count: closure.composes_cycle_count,
3136 value_cycle_count: closure.value_cycle_count,
3137 icss_cycle_count: closure.icss_cycle_count,
3138 composes_closure_edges: closure.composes_closure_edges,
3139 value_closure_edges: closure.value_closure_edges,
3140 icss_closure_edges: closure.icss_closure_edges,
3141 cycles: closure.cycles,
3142 capabilities: OmenaQueryCssModulesCrossFileResolutionCapabilitiesV0 {
3143 semantic_layer_owned: true,
3144 import_source_resolution_ready: true,
3145 cross_file_resolution_ready: true,
3146 composes_closure_ready: true,
3147 composes_name_match_ready: true,
3148 value_name_match_ready: true,
3149 icss_name_match_ready: true,
3150 transitive_closure_ready: true,
3151 value_graph_closure_ready: true,
3152 icss_export_import_closure_ready: true,
3153 cycle_detection_ready: true,
3154 },
3155 next_priorities: vec![],
3156 }
3157}
3158
3159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3160#[cfg_attr(not(feature = "salsa-memo"), allow(dead_code))]
3161struct CssModulesImportReachabilityForQuery {
3162 distance: usize,
3163 order: usize,
3164}
3165
3166#[cfg_attr(not(feature = "salsa-memo"), allow(dead_code))]
3167fn css_modules_import_reachability_for_origin(
3168 origin_style_path: &str,
3169 style_import_edges: &[omena_semantic::StyleImportReachabilityEdgeFactV0],
3170) -> BTreeMap<String, CssModulesImportReachabilityForQuery> {
3171 omena_semantic::summarize_style_import_reachability(origin_style_path, style_import_edges)
3172 .reachable_style_paths
3173 .into_iter()
3174 .map(|fact| {
3175 (
3176 fact.style_path,
3177 CssModulesImportReachabilityForQuery {
3178 distance: fact.distance,
3179 order: fact.order,
3180 },
3181 )
3182 })
3183 .collect()
3184}
3185
3186#[allow(clippy::too_many_arguments)]
3187#[cfg_attr(not(feature = "salsa-memo"), allow(dead_code))]
3188fn resolve_css_modules_import_edge_for_query(
3189 from_style_path: &str,
3190 import_kind: &'static str,
3191 source: &str,
3192 imported_names: &[String],
3193 available_style_paths: &BTreeSet<&str>,
3194 facts_by_path: &BTreeMap<&str, &omena_semantic::CssModulesCrossFileStyleFactsV0>,
3195 reachable: &BTreeMap<String, CssModulesImportReachabilityForQuery>,
3196 package_manifests: &[OmenaQueryStylePackageManifestV0],
3197 resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
3198 resolver_identity_index: Option<&OmenaResolverStyleModuleConfirmationIdentityIndexV0>,
3199 exported_names_for_kind: fn(&omena_semantic::CssModulesCrossFileStyleFactsV0) -> &[String],
3200) -> OmenaQueryCssModulesImportEdgeResolutionV0 {
3201 let resolved_style_path = resolve_style_module_source_with_resolution_inputs_and_identity_index(
3202 from_style_path,
3203 source,
3204 available_style_paths,
3205 package_manifests,
3206 resolution_inputs,
3207 resolver_identity_index,
3208 );
3209 let reachability = resolved_style_path
3210 .as_ref()
3211 .and_then(|style_path| reachable.get(style_path));
3212 let exported_names = resolved_style_path
3213 .as_deref()
3214 .and_then(|style_path| facts_by_path.get(style_path))
3215 .map(|facts| exported_names_for_kind(facts).to_vec())
3216 .unwrap_or_default();
3217 let imported_names = sorted_unique_query_strings(imported_names);
3218 let matched_names =
3219 sorted_query_name_intersection(imported_names.as_slice(), exported_names.as_slice());
3220 let status = if resolved_style_path.is_none() {
3221 "unresolvedSource"
3222 } else if imported_names.is_empty() {
3223 "resolvedSource"
3224 } else if matched_names.is_empty() {
3225 "resolvedSourceNoNameMatch"
3226 } else {
3227 "resolved"
3228 };
3229
3230 OmenaQueryCssModulesImportEdgeResolutionV0 {
3231 from_style_path: from_style_path.to_string(),
3232 import_kind,
3233 source: source.to_string(),
3234 resolved_style_path,
3235 status,
3236 import_graph_distance: reachability.map(|reachability| reachability.distance),
3237 import_graph_order: reachability.map(|reachability| reachability.order),
3238 imported_names,
3239 exported_names,
3240 matched_names,
3241 }
3242}
3243
3244#[cfg_attr(not(feature = "salsa-memo"), allow(dead_code))]
3245fn sorted_unique_query_strings(values: &[String]) -> Vec<String> {
3246 values
3247 .iter()
3248 .cloned()
3249 .collect::<BTreeSet<_>>()
3250 .into_iter()
3251 .collect()
3252}
3253
3254#[cfg_attr(not(feature = "salsa-memo"), allow(dead_code))]
3255fn sorted_query_name_intersection(left: &[String], right: &[String]) -> Vec<String> {
3256 let right = right.iter().map(String::as_str).collect::<BTreeSet<_>>();
3257 left.iter()
3258 .filter(|name| right.contains(name.as_str()))
3259 .cloned()
3260 .collect::<BTreeSet<_>>()
3261 .into_iter()
3262 .collect()
3263}
3264
3265fn css_modules_cross_file_style_facts_for_query(
3266 style_fact_entries: &[OmenaQueryStyleFactEntry],
3267) -> Vec<omena_semantic::CssModulesCrossFileStyleFactsV0> {
3268 style_fact_entries
3269 .iter()
3270 .map(css_modules_cross_file_style_fact_for_query)
3271 .collect()
3272}
3273
3274fn css_modules_cross_file_style_fact_for_query(
3275 entry: &OmenaQueryStyleFactEntry,
3276) -> omena_semantic::CssModulesCrossFileStyleFactsV0 {
3277 omena_semantic::CssModulesCrossFileStyleFactsV0 {
3278 style_path: entry.style_path.clone(),
3279 class_selector_names: entry.facts.class_selector_names.clone(),
3280 css_module_value_definition_names: entry.facts.css_module_value_definition_names.clone(),
3281 css_module_value_import_edges: entry
3282 .facts
3283 .css_module_value_import_edges
3284 .iter()
3285 .map(|edge| omena_semantic::CssModulesValueImportEdgeFactV0 {
3286 remote_name: edge.remote_name.clone(),
3287 local_name: edge.local_name.clone(),
3288 import_source: edge.import_source.clone(),
3289 })
3290 .collect(),
3291 css_module_value_definition_edges: entry
3292 .facts
3293 .css_module_value_definition_edges
3294 .iter()
3295 .map(|edge| omena_semantic::CssModulesValueDefinitionEdgeFactV0 {
3296 definition_name: edge.definition_name.clone(),
3297 reference_names: edge.reference_names.clone(),
3298 })
3299 .collect(),
3300 css_module_composes_edges: entry
3301 .facts
3302 .css_module_composes_edges
3303 .iter()
3304 .map(|edge| omena_semantic::CssModulesComposesEdgeFactV0 {
3305 kind: edge.kind,
3306 owner_selector_names: edge.owner_selector_names.clone(),
3307 target_names: edge.target_names.clone(),
3308 import_source: edge.import_source.clone(),
3309 })
3310 .collect(),
3311 icss_export_names: entry.facts.icss_export_names.clone(),
3312 icss_import_edges: entry
3313 .facts
3314 .icss_import_edges
3315 .iter()
3316 .map(|edge| omena_semantic::CssModulesIcssImportEdgeFactV0 {
3317 local_name: edge.local_name.clone(),
3318 remote_name: edge.remote_name.clone(),
3319 import_source: edge.import_source.clone(),
3320 })
3321 .collect(),
3322 icss_export_edges: entry
3323 .facts
3324 .icss_export_edges
3325 .iter()
3326 .map(|edge| omena_semantic::CssModulesIcssExportEdgeFactV0 {
3327 export_name: edge.export_name.clone(),
3328 reference_names: edge.reference_names.clone(),
3329 })
3330 .collect(),
3331 }
3332}
3333
3334fn semantic_package_manifests_for_query(
3335 package_manifests: &[OmenaQueryStylePackageManifestV0],
3336) -> Vec<OmenaResolverStylePackageManifestV0> {
3337 package_manifests
3338 .iter()
3339 .map(|manifest| OmenaResolverStylePackageManifestV0 {
3340 package_json_path: manifest.package_json_path.clone(),
3341 package_json_source: manifest.package_json_source.clone(),
3342 })
3343 .collect()
3344}
3345
3346fn style_import_reachability_edges_for_query(
3347 style_fact_entries: &[OmenaQueryStyleFactEntry],
3348 package_manifests: &[OmenaQueryStylePackageManifestV0],
3349 resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
3350) -> Vec<omena_semantic::StyleImportReachabilityEdgeFactV0> {
3351 let available_style_paths = style_fact_entries
3352 .iter()
3353 .map(|entry| entry.style_path.as_str())
3354 .collect::<BTreeSet<_>>();
3355 let mut edges = Vec::new();
3356 for entry in style_fact_entries {
3357 let targets = collect_style_module_dependency_sources_from_facts(&entry.facts)
3358 .into_iter()
3359 .filter_map(|source| {
3360 resolve_style_module_source_with_resolution_inputs(
3361 entry.style_path.as_str(),
3362 &source,
3363 &available_style_paths,
3364 package_manifests,
3365 resolution_inputs,
3366 )
3367 })
3368 .collect::<BTreeSet<_>>();
3369 for target in targets {
3370 edges.push(omena_semantic::StyleImportReachabilityEdgeFactV0 {
3371 from_style_path: entry.style_path.clone(),
3372 target_style_path: target,
3373 });
3374 }
3375 }
3376 edges
3377}
3378
3379#[derive(Debug, Clone)]
3380struct CssModulesComposesNode {
3381 style_path: String,
3382 selector_name: String,
3383 selector_key: omena_syntax::ident::CanonicalClassKeyV0,
3384}
3385
3386impl CssModulesComposesNode {
3387 fn new(style_path: impl Into<String>, selector_name: impl Into<String>) -> Self {
3388 let selector_name = selector_name.into();
3389 Self {
3390 style_path: style_path.into(),
3391 selector_key: canonical_class_key(&selector_name),
3392 selector_name,
3393 }
3394 }
3395}
3396
3397impl PartialEq for CssModulesComposesNode {
3398 fn eq(&self, other: &Self) -> bool {
3399 self.style_path == other.style_path && self.selector_key == other.selector_key
3400 }
3401}
3402
3403impl Eq for CssModulesComposesNode {}
3404
3405impl PartialOrd for CssModulesComposesNode {
3406 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
3407 Some(self.cmp(other))
3408 }
3409}
3410
3411impl Ord for CssModulesComposesNode {
3412 fn cmp(&self, other: &Self) -> Ordering {
3413 (&self.style_path, &self.selector_key).cmp(&(&other.style_path, &other.selector_key))
3414 }
3415}
3416
3417fn collect_css_modules_composes_adjacency(
3418 facts_by_path: &BTreeMap<&str, OmenaQueryOmenaParserStyleFactsV0>,
3419 available_style_paths: &BTreeSet<&str>,
3420 package_manifests: &[OmenaQueryStylePackageManifestV0],
3421) -> BTreeMap<CssModulesComposesNode, BTreeSet<CssModulesComposesNode>> {
3422 collect_css_modules_composes_adjacency_with_path_mappings(
3423 facts_by_path,
3424 available_style_paths,
3425 package_manifests,
3426 &[],
3427 &[],
3428 &[],
3429 )
3430}
3431
3432fn collect_css_modules_composes_adjacency_with_path_mappings(
3433 facts_by_path: &BTreeMap<&str, OmenaQueryOmenaParserStyleFactsV0>,
3434 available_style_paths: &BTreeSet<&str>,
3435 package_manifests: &[OmenaQueryStylePackageManifestV0],
3436 bundler_path_mappings: &[OmenaResolverBundlerPathAliasMappingV0],
3437 tsconfig_path_mappings: &[OmenaResolverTsconfigPathMappingV0],
3438 disk_style_path_identities: &[OmenaResolverStyleModuleDiskCandidateIdentityV0],
3439) -> BTreeMap<CssModulesComposesNode, BTreeSet<CssModulesComposesNode>> {
3440 let mut graph = BTreeMap::new();
3441 for (style_path, facts) in facts_by_path {
3442 let class_names = facts
3443 .class_selector_names
3444 .iter()
3445 .map(|name| canonical_class_key(name))
3446 .collect::<BTreeSet<_>>();
3447 for edge in &facts.css_module_composes_edges {
3448 if edge.kind == "global" {
3449 continue;
3450 }
3451 let target_style_path = if edge.kind == "external" {
3452 edge.import_source.as_deref().and_then(|source| {
3453 resolve_style_module_source_with_path_mappings(
3454 style_path,
3455 source,
3456 available_style_paths,
3457 package_manifests,
3458 bundler_path_mappings,
3459 tsconfig_path_mappings,
3460 disk_style_path_identities,
3461 )
3462 })
3463 } else {
3464 Some((*style_path).to_string())
3465 };
3466 let Some(target_style_path) = target_style_path else {
3467 continue;
3468 };
3469 let target_class_names = if target_style_path == *style_path {
3470 class_names.clone()
3471 } else {
3472 facts_by_path
3473 .get(target_style_path.as_str())
3474 .map(|facts| {
3475 facts
3476 .class_selector_names
3477 .iter()
3478 .map(|name| canonical_class_key(name))
3479 .collect::<BTreeSet<_>>()
3480 })
3481 .unwrap_or_default()
3482 };
3483 for owner_selector_name in &edge.owner_selector_names {
3484 if !class_names.contains(&canonical_class_key(owner_selector_name)) {
3485 continue;
3486 }
3487 let owner =
3488 CssModulesComposesNode::new((*style_path).to_string(), owner_selector_name);
3489 for target_selector_name in &edge.target_names {
3490 if !target_class_names.contains(&canonical_class_key(target_selector_name)) {
3491 continue;
3492 }
3493 graph
3494 .entry(owner.clone())
3495 .or_insert_with(BTreeSet::new)
3496 .insert(CssModulesComposesNode::new(
3497 target_style_path.clone(),
3498 target_selector_name,
3499 ));
3500 }
3501 }
3502 }
3503 }
3504 graph
3505}
3506
3507fn filter_import_reachable_design_token_workspace_declarations(
3508 target_style_path: &str,
3509 style_fact_entries: &[OmenaQueryStyleFactEntry],
3510 workspace_declarations: &[DesignTokenWorkspaceDeclarationFactV0],
3511 package_manifests: &[OmenaQueryStylePackageManifestV0],
3512 bundler_path_mappings: &[OmenaResolverBundlerPathAliasMappingV0],
3513 tsconfig_path_mappings: &[OmenaResolverTsconfigPathMappingV0],
3514 disk_style_path_identities: &[OmenaResolverStyleModuleDiskCandidateIdentityV0],
3515) -> Vec<DesignTokenWorkspaceDeclarationFactV0> {
3516 let reachable_style_paths = collect_import_reachable_style_path_metadata(
3517 target_style_path,
3518 style_fact_entries,
3519 package_manifests,
3520 bundler_path_mappings,
3521 tsconfig_path_mappings,
3522 disk_style_path_identities,
3523 );
3524 workspace_declarations
3525 .iter()
3526 .filter_map(|declaration| {
3527 if declaration.file_path == target_style_path {
3528 return Some(declaration.clone());
3529 }
3530 let reachability = reachable_style_paths.get(declaration.file_path.as_str())?;
3531 let mut declaration = declaration.clone();
3532 declaration.import_graph_distance = Some(reachability.distance);
3533 declaration.import_graph_order = Some(reachability.order);
3534 Some(declaration)
3535 })
3536 .collect()
3537}
3538
3539#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3540struct ImportReachability {
3541 distance: usize,
3542 order: usize,
3543}
3544
3545fn collect_import_reachable_style_path_metadata(
3546 target_style_path: &str,
3547 style_fact_entries: &[OmenaQueryStyleFactEntry],
3548 package_manifests: &[OmenaQueryStylePackageManifestV0],
3549 bundler_path_mappings: &[OmenaResolverBundlerPathAliasMappingV0],
3550 tsconfig_path_mappings: &[OmenaResolverTsconfigPathMappingV0],
3551 disk_style_path_identities: &[OmenaResolverStyleModuleDiskCandidateIdentityV0],
3552) -> BTreeMap<String, ImportReachability> {
3553 let available_style_paths = style_fact_entries
3554 .iter()
3555 .map(|entry| entry.style_path.as_str())
3556 .collect::<BTreeSet<_>>();
3557 let mut edges = Vec::new();
3558 for entry in style_fact_entries {
3559 let targets = collect_style_module_dependency_sources_from_facts(&entry.facts)
3560 .into_iter()
3561 .filter_map(|source| {
3562 resolve_style_module_source_with_path_mappings(
3563 entry.style_path.as_str(),
3564 &source,
3565 &available_style_paths,
3566 package_manifests,
3567 bundler_path_mappings,
3568 tsconfig_path_mappings,
3569 disk_style_path_identities,
3570 )
3571 })
3572 .collect::<BTreeSet<_>>();
3573 for target in targets {
3574 edges.push(omena_semantic::StyleImportReachabilityEdgeFactV0 {
3575 from_style_path: entry.style_path.clone(),
3576 target_style_path: target,
3577 });
3578 }
3579 }
3580
3581 omena_semantic::summarize_style_import_reachability(target_style_path, edges.as_slice())
3582 .reachable_style_paths
3583 .into_iter()
3584 .map(|fact| {
3585 (
3586 fact.style_path,
3587 ImportReachability {
3588 distance: fact.distance,
3589 order: fact.order,
3590 },
3591 )
3592 })
3593 .collect()
3594}
3595
3596fn collect_style_module_dependency_sources_from_facts(
3597 facts: &OmenaQueryOmenaParserStyleFactsV0,
3598) -> Vec<String> {
3599 let mut sources = facts
3600 .sass_module_edges
3601 .iter()
3602 .map(|edge| edge.source.clone())
3603 .collect::<Vec<_>>();
3604 sources.extend(
3605 facts
3606 .css_module_value_import_edges
3607 .iter()
3608 .map(|edge| edge.import_source.clone()),
3609 );
3610 sources.extend(
3611 facts
3612 .css_module_composes_edges
3613 .iter()
3614 .filter_map(|edge| edge.import_source.clone()),
3615 );
3616 sources.extend(
3617 facts
3618 .icss_import_edges
3619 .iter()
3620 .map(|edge| edge.import_source.clone()),
3621 );
3622 sources.sort();
3623 sources.dedup();
3624 sources
3625}
3626
3627fn resolve_style_module_source_with_resolution_inputs(
3628 from_style_path: &str,
3629 source: &str,
3630 available_style_paths: &BTreeSet<&str>,
3631 package_manifests: &[OmenaQueryStylePackageManifestV0],
3632 resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
3633) -> Option<String> {
3634 resolve_style_module_source_with_resolution_inputs_and_identity_index(
3635 from_style_path,
3636 source,
3637 available_style_paths,
3638 package_manifests,
3639 resolution_inputs,
3640 None,
3641 )
3642}
3643
3644fn resolve_style_module_source_with_resolution_inputs_and_identity_index(
3645 from_style_path: &str,
3646 source: &str,
3647 available_style_paths: &BTreeSet<&str>,
3648 package_manifests: &[OmenaQueryStylePackageManifestV0],
3649 resolution_inputs: &OmenaQueryStyleResolutionInputsV0,
3650 identity_index: Option<&OmenaResolverStyleModuleConfirmationIdentityIndexV0>,
3651) -> Option<String> {
3652 resolve_style_module_source_with_path_mappings_and_identity_index(
3653 from_style_path,
3654 source,
3655 available_style_paths,
3656 package_manifests,
3657 resolution_inputs.bundler_path_mappings.as_slice(),
3658 resolution_inputs.tsconfig_path_mappings.as_slice(),
3659 resolution_inputs.disk_style_path_identities.as_slice(),
3660 identity_index,
3661 )
3662}
3663
3664fn resolve_style_module_source_with_path_mappings(
3667 from_style_path: &str,
3668 source: &str,
3669 available_style_paths: &BTreeSet<&str>,
3670 package_manifests: &[OmenaQueryStylePackageManifestV0],
3671 bundler_path_mappings: &[OmenaResolverBundlerPathAliasMappingV0],
3672 tsconfig_path_mappings: &[OmenaResolverTsconfigPathMappingV0],
3673 disk_style_path_identities: &[OmenaResolverStyleModuleDiskCandidateIdentityV0],
3674) -> Option<String> {
3675 resolve_style_module_source_with_path_mappings_and_identity_index(
3676 from_style_path,
3677 source,
3678 available_style_paths,
3679 package_manifests,
3680 bundler_path_mappings,
3681 tsconfig_path_mappings,
3682 disk_style_path_identities,
3683 None,
3684 )
3685}
3686
3687#[allow(clippy::too_many_arguments)]
3688fn resolve_style_module_source_with_path_mappings_and_identity_index(
3689 from_style_path: &str,
3690 source: &str,
3691 available_style_paths: &BTreeSet<&str>,
3692 package_manifests: &[OmenaQueryStylePackageManifestV0],
3693 bundler_path_mappings: &[OmenaResolverBundlerPathAliasMappingV0],
3694 tsconfig_path_mappings: &[OmenaResolverTsconfigPathMappingV0],
3695 disk_style_path_identities: &[OmenaResolverStyleModuleDiskCandidateIdentityV0],
3696 identity_index: Option<&OmenaResolverStyleModuleConfirmationIdentityIndexV0>,
3697) -> Option<String> {
3698 let load_path_roots = collect_load_path_roots(available_style_paths);
3699 let load_path_root_refs = load_path_roots
3700 .iter()
3701 .map(String::as_str)
3702 .collect::<Vec<_>>();
3703 let resolver_package_manifests = package_manifests
3704 .iter()
3705 .map(|manifest| OmenaResolverStylePackageManifestV0 {
3706 package_json_path: manifest.package_json_path.clone(),
3707 package_json_source: manifest.package_json_source.clone(),
3708 })
3709 .collect::<Vec<_>>();
3710 summarize_omena_resolver_style_module_resolution_with_confirmation_inputs(
3711 from_style_path,
3712 source,
3713 available_style_paths,
3714 disk_style_path_identities,
3715 &resolver_package_manifests,
3716 bundler_path_mappings,
3717 tsconfig_path_mappings,
3718 load_path_root_refs.as_slice(),
3719 OmenaResolverStyleModuleConfirmationOptionsV0 {
3720 allow_disk_confirmation: true,
3721 identity_index,
3722 ..OmenaResolverStyleModuleConfirmationOptionsV0::default()
3723 },
3724 )
3725 .resolved_style_path
3726}
3727
3728fn collect_style_selector_hover_candidates_from_omena_parser_facts(
3729 source: &str,
3730 definition_facts: &[ParsedSelectorFact],
3731 seen: &mut BTreeSet<(usize, usize, String)>,
3732 candidates: &mut Vec<OmenaQueryStyleHoverCandidateV0>,
3733) {
3734 for fact in definition_facts {
3735 if fact.kind != ParsedSelectorFactKind::Class {
3736 continue;
3737 }
3738 let start: u32 = fact.range.start().into();
3739 let end: u32 = fact.range.end().into();
3740 let byte_span = ParserByteSpanV0 {
3741 start: start as usize,
3742 end: end as usize,
3743 };
3744 if seen.insert((byte_span.start, byte_span.end, fact.name.clone())) {
3745 candidates.push(OmenaQueryStyleHoverCandidateV0 {
3746 kind: "selector",
3747 name: fact.name.clone(),
3748 range: parser_range_for_byte_span(source, byte_span),
3749 source: "omenaParserSelectorFacts",
3750 namespace: None,
3751 });
3752 }
3753 }
3754}
3755
3756fn collect_custom_property_hover_candidates_from_omena_parser_facts(
3757 source: &str,
3758 variable_facts: &[ParsedVariableFact],
3759 seen: &mut BTreeSet<(usize, usize, String)>,
3760 candidates: &mut Vec<OmenaQueryStyleHoverCandidateV0>,
3761) {
3762 for fact in variable_facts {
3763 let kind = match fact.kind {
3764 ParsedVariableFactKind::CustomPropertyDeclaration => "customPropertyDeclaration",
3765 ParsedVariableFactKind::CustomPropertyReference => "customPropertyReference",
3766 _ => continue,
3767 };
3768 let start: u32 = fact.range.start().into();
3769 let end: u32 = fact.range.end().into();
3770 let byte_span = ParserByteSpanV0 {
3771 start: start as usize,
3772 end: end as usize,
3773 };
3774 if seen.insert((byte_span.start, byte_span.end, fact.name.clone())) {
3775 candidates.push(OmenaQueryStyleHoverCandidateV0 {
3776 kind,
3777 name: fact.name.clone(),
3778 range: parser_range_for_byte_span(source, byte_span),
3779 source: "omenaParserVariableFacts",
3780 namespace: None,
3781 });
3782 }
3783 }
3784}
3785
3786fn collect_sass_symbol_hover_candidates_from_omena_parser_facts(
3787 source: &str,
3788 symbol_facts: &[omena_parser::ParsedSassSymbolFact],
3789 seen: &mut BTreeSet<(usize, usize, String)>,
3790 candidates: &mut Vec<OmenaQueryStyleHoverCandidateV0>,
3791) {
3792 for fact in symbol_facts {
3793 let kind = match fact.kind {
3794 ParsedSassSymbolFactKind::VariableDeclaration
3795 | ParsedSassSymbolFactKind::MixinDeclaration
3796 | ParsedSassSymbolFactKind::FunctionDeclaration => {
3797 sass_symbol_declaration_candidate_kind(fact.symbol_kind)
3798 }
3799 ParsedSassSymbolFactKind::VariableReference
3800 | ParsedSassSymbolFactKind::MixinInclude
3801 | ParsedSassSymbolFactKind::FunctionCall => {
3802 sass_symbol_reference_candidate_kind(fact.symbol_kind, fact.role)
3803 }
3804 };
3805 let start: u32 = fact.range.start().into();
3806 let end: u32 = fact.range.end().into();
3807 let byte_span = ParserByteSpanV0 {
3808 start: start as usize,
3809 end: end as usize,
3810 };
3811 if seen.insert((
3812 byte_span.start,
3813 byte_span.end,
3814 format!(
3815 "{}:{}:{}",
3816 fact.symbol_kind,
3817 fact.namespace.as_deref().unwrap_or_default(),
3818 fact.name
3819 ),
3820 )) {
3821 candidates.push(OmenaQueryStyleHoverCandidateV0 {
3822 kind,
3823 name: fact.name.clone(),
3824 range: parser_range_for_byte_span(source, byte_span),
3825 source: "omenaParserSassSymbolFacts",
3826 namespace: fact.namespace.clone(),
3827 });
3828 }
3829 }
3830}
3831
3832fn collect_sass_partial_evaluator_selector_candidates_from_omena_parser_facts(
3833 source: &str,
3834 includes: &[ParsedSassIncludeFact],
3835 seen: &mut BTreeSet<(usize, usize, String)>,
3836 candidates: &mut Vec<OmenaQueryStyleHoverCandidateV0>,
3837) {
3838 for include in includes {
3839 let start: u32 = include.range.start().into();
3840 let end: u32 = include.range.end().into();
3841 let range_span = ParserByteSpanV0 {
3842 start: start as usize,
3843 end: end as usize,
3844 };
3845 for selector_name in infer_sass_include_generated_selector_names(&include.params) {
3846 if seen.insert((range_span.start, range_span.end, selector_name.clone())) {
3847 candidates.push(OmenaQueryStyleHoverCandidateV0 {
3848 kind: "selector",
3849 name: selector_name,
3850 range: parser_range_for_byte_span(source, range_span),
3851 source: "sassPartialEvaluatorGeneratedSelectors",
3852 namespace: None,
3853 });
3854 }
3855 }
3856 }
3857}
3858
3859fn infer_sass_include_generated_selector_names(params: &str) -> Vec<String> {
3860 let Some(prefix) = sass_named_argument_string_value(params, "prefix") else {
3861 return Vec::new();
3862 };
3863 if prefix.is_empty() || !prefix.chars().all(is_css_name_continue) {
3864 return Vec::new();
3865 }
3866 let mut selectors = sass_first_map_string_keys(params)
3867 .into_iter()
3868 .filter(|key| !key.is_empty() && key.chars().all(is_css_name_continue))
3869 .map(|key| format!("{prefix}-{key}"))
3870 .collect::<Vec<_>>();
3871 selectors.sort();
3872 selectors.dedup();
3873 selectors
3874}
3875
3876fn sass_named_argument_string_value(params: &str, name: &str) -> Option<String> {
3877 let needle = format!("${name}");
3878 let mut cursor = 0usize;
3879 while let Some(relative_match) = params[cursor..].find(needle.as_str()) {
3880 let name_start = cursor + relative_match;
3881 let name_end = name_start + needle.len();
3882 if !sass_identifier_boundary(params, name_start, name_end) {
3883 cursor = name_end;
3884 continue;
3885 }
3886 let colon_offset = skip_ascii_whitespace(params, name_end);
3887 if params.as_bytes().get(colon_offset) != Some(&b':') {
3888 cursor = name_end;
3889 continue;
3890 }
3891 let value_start = skip_ascii_whitespace(params, colon_offset + 1);
3892 return sass_string_literal_value(params, value_start).map(|(value, _)| value);
3893 }
3894 None
3895}
3896
3897fn sass_first_map_string_keys(params: &str) -> Vec<String> {
3898 let mut cursor = 0usize;
3899 while cursor < params.len() {
3900 let Some(open_relative) = params[cursor..].find('(') else {
3901 break;
3902 };
3903 let open = cursor + open_relative;
3904 let Some(close) = matching_style_block_end(params, open, b'(', b')') else {
3905 break;
3906 };
3907 let keys = sass_map_string_keys(params, open + 1, close);
3908 if !keys.is_empty() {
3909 return keys;
3910 }
3911 cursor = open + 1;
3912 }
3913 Vec::new()
3914}
3915
3916fn sass_map_string_keys(params: &str, start: usize, end: usize) -> Vec<String> {
3917 split_top_level_style_segments(params, start, end, b',')
3918 .into_iter()
3919 .filter_map(|(entry_start, entry_end)| {
3920 let key_start = skip_ascii_whitespace(params, entry_start);
3921 let (key, key_end) = sass_string_literal_value(params, key_start)?;
3922 let colon_offset = skip_ascii_whitespace(params, key_end);
3923 (colon_offset < entry_end && params.as_bytes().get(colon_offset) == Some(&b':'))
3924 .then_some(key)
3925 })
3926 .collect()
3927}
3928
3929fn sass_string_literal_value(source: &str, quote_offset: usize) -> Option<(String, usize)> {
3930 let quote = source.as_bytes().get(quote_offset).copied()?;
3931 if !matches!(quote, b'\'' | b'"') {
3932 return None;
3933 }
3934 let literal_end = skip_style_string_literal(source, quote_offset, source.len())?;
3935 let value_end = literal_end.saturating_sub(1);
3936 source
3937 .get(quote_offset + 1..value_end)
3938 .map(|value| (value.to_string(), literal_end))
3939}
3940
3941fn sass_identifier_boundary(source: &str, start: usize, end: usize) -> bool {
3942 let before = source
3943 .get(..start)
3944 .and_then(|prefix| prefix.chars().next_back())
3945 .is_none_or(|ch| !is_ascii_word_continue(ch) && ch != '$');
3946 let after = source
3947 .get(end..)
3948 .and_then(|suffix| suffix.chars().next())
3949 .is_none_or(|ch| !is_ascii_word_continue(ch));
3950 before && after
3951}
3952
3953fn sass_symbol_declaration_candidate_kind(symbol_kind: &str) -> &'static str {
3954 match symbol_kind {
3955 "variable" => "sassVariableDeclaration",
3956 "mixin" => "sassMixinDeclaration",
3957 "function" => "sassFunctionDeclaration",
3958 _ => "sassSymbolDeclaration",
3959 }
3960}
3961
3962fn is_sass_symbol_candidate_kind(kind: &str) -> bool {
3963 sass_symbol_kind_from_candidate_kind(kind).is_some()
3964}
3965
3966fn is_sass_symbol_declaration_kind(kind: &str) -> bool {
3967 matches!(
3968 kind,
3969 "sassVariableDeclaration"
3970 | "sassMixinDeclaration"
3971 | "sassFunctionDeclaration"
3972 | "sassSymbolDeclaration"
3973 )
3974}
3975
3976fn sass_symbol_kind_from_candidate_kind(kind: &str) -> Option<&'static str> {
3977 match kind {
3978 "sassVariableDeclaration" | "sassVariableReference" => Some("variable"),
3979 "sassMixinDeclaration" | "sassMixinInclude" | "sassMixinReference" => Some("mixin"),
3980 "sassFunctionDeclaration" | "sassFunctionCall" | "sassFunctionReference" => {
3981 Some("function")
3982 }
3983 "sassSymbolDeclaration" | "sassSymbolReference" => Some("symbol"),
3984 _ => None,
3985 }
3986}
3987
3988fn sass_symbol_reference_candidate_kind(symbol_kind: &str, role: &str) -> &'static str {
3989 match (symbol_kind, role) {
3990 ("variable", _) => "sassVariableReference",
3991 ("mixin", "include") => "sassMixinInclude",
3992 ("function", "call") => "sassFunctionCall",
3993 ("mixin", _) => "sassMixinReference",
3994 ("function", _) => "sassFunctionReference",
3995 _ => "sassSymbolReference",
3996 }
3997}
3998
3999fn sass_variable_value_from_declaration_line(line: &str) -> Option<String> {
4000 let (_, value) = line.split_once(':')?;
4001 let value = value
4002 .trim()
4003 .trim_end_matches(';')
4004 .trim()
4005 .trim_end_matches("!default")
4006 .trim();
4007 (!value.is_empty()).then(|| value.to_string())
4008}
4009
4010fn sass_callable_definition_render_parts(
4011 source: &str,
4012 position: ParserPositionV0,
4013) -> Option<(String, String)> {
4014 let line_start = byte_offset_for_parser_position(
4015 source,
4016 ParserPositionV0 {
4017 line: position.line,
4018 character: 0,
4019 },
4020 )?;
4021 let open_brace = source[line_start..].find('{')? + line_start;
4022 let close_brace = matching_style_block_end(source, open_brace, b'{', b'}')?;
4023 let signature = source[line_start..open_brace].trim().to_string();
4024 let body = source[open_brace + 1..close_brace].trim();
4025 if signature.is_empty() || body.is_empty() {
4026 return None;
4027 }
4028 Some((
4029 signature,
4030 trim_hover_snippet(dedent_hover_body(body).as_str()),
4031 ))
4032}
4033
4034fn dedent_hover_body(body: &str) -> String {
4040 fn leading_whitespace_bytes(line: &str) -> usize {
4041 line.len() - line.trim_start().len()
4042 }
4043 let common = body
4044 .lines()
4045 .skip(1)
4046 .filter(|line| !line.trim().is_empty())
4047 .map(leading_whitespace_bytes)
4048 .min()
4049 .unwrap_or(0);
4050 if common == 0 {
4051 return body.to_string();
4052 }
4053 let mut lines = body.lines();
4054 let mut dedented = lines.next().unwrap_or_default().to_string();
4055 for line in lines {
4056 dedented.push('\n');
4057 let mut stripped = 0usize;
4058 for (offset, character) in line.char_indices() {
4059 if stripped >= common || !character.is_whitespace() {
4060 dedented.push_str(&line[offset..]);
4061 break;
4062 }
4063 stripped += character.len_utf8();
4064 }
4065 }
4066 dedented
4067}
4068
4069fn rule_snippet_around_position(source: &str, position: ParserPositionV0) -> Option<String> {
4070 let line_start = byte_offset_for_parser_position(
4071 source,
4072 ParserPositionV0 {
4073 line: position.line,
4074 character: 0,
4075 },
4076 )?;
4077 let open_brace = source[line_start..].find('{')? + line_start;
4078 let mut depth = 0usize;
4079 let mut cursor = open_brace;
4080 while cursor < source.len() {
4081 match source.as_bytes().get(cursor).copied()? {
4082 b'{' => depth += 1,
4083 b'}' => {
4084 depth = depth.saturating_sub(1);
4085 if depth == 0 {
4086 let snippet = source[line_start..=cursor].trim();
4087 return Some(trim_hover_snippet(snippet));
4088 }
4089 }
4090 _ => {}
4091 }
4092 cursor = advance_style_scan_cursor(source, cursor, source.len());
4093 }
4094 None
4095}
4096
4097fn line_snippet_at_position(source: &str, position: ParserPositionV0) -> Option<String> {
4098 let line_start = byte_offset_for_parser_position(
4099 source,
4100 ParserPositionV0 {
4101 line: position.line,
4102 character: 0,
4103 },
4104 )?;
4105 let line_end = source[line_start..]
4106 .find('\n')
4107 .map(|offset| line_start + offset)
4108 .unwrap_or(source.len());
4109 Some(source[line_start..line_end].trim().to_string())
4110}
4111
4112fn style_completion_context_at_position(
4113 source: &str,
4114 position: ParserPositionV0,
4115) -> Option<(&'static str, Option<String>)> {
4116 let cursor = byte_offset_for_parser_position(source, position)?;
4117 let line_start = byte_offset_for_parser_position(
4118 source,
4119 ParserPositionV0 {
4120 line: position.line,
4121 character: 0,
4122 },
4123 )?;
4124 let line_prefix = source.get(line_start..cursor)?;
4125 if let Some(var_start) = line_prefix.rfind("var(") {
4126 let var_prefix = &line_prefix[var_start + "var(".len()..];
4127 if !var_prefix.contains(')') {
4128 let prefix = var_prefix
4129 .rsplit(|ch: char| ch == ',' || ch.is_ascii_whitespace())
4130 .next()
4131 .unwrap_or_default();
4132 let prefix = (!prefix.is_empty()).then(|| prefix.to_string());
4133 return Some(("styleCustomPropertyReference", prefix));
4134 }
4135 }
4136 if let Some(prefix) = sass_variable_completion_prefix(line_prefix) {
4137 return Some(("sassVariableReference", Some(prefix)));
4138 }
4139 if let Some(prefix) = sass_mixin_completion_prefix(line_prefix) {
4140 return Some(("sassMixinReference", prefix));
4141 }
4142 if let Some(prefix) = sass_member_completion_prefix(line_prefix) {
4143 return Some(("sassMemberReference", Some(prefix)));
4144 }
4145
4146 Some(("styleDocument", None))
4147}
4148
4149fn sass_variable_completion_prefix(line_prefix: &str) -> Option<String> {
4150 let token = sass_completion_trailing_token(line_prefix)?;
4151 let dollar_offset = token.rfind('$')?;
4152 let suffix = token.get(dollar_offset + 1..)?;
4153 if !suffix.chars().all(is_sass_completion_identifier_continue) {
4154 return None;
4155 }
4156 let prefix = token.get(..)?;
4157 (!prefix.is_empty()).then(|| prefix.to_string())
4158}
4159
4160fn sass_mixin_completion_prefix(line_prefix: &str) -> Option<Option<String>> {
4161 let include_offset = line_prefix.rfind("@include")?;
4162 let after_include = line_prefix.get(include_offset + "@include".len()..)?;
4163 if after_include.contains(';') || after_include.contains('{') || after_include.contains('}') {
4164 return None;
4165 }
4166 let token = sass_completion_trailing_token(after_include.trim_start())?;
4167 if token.contains('$') || !token.chars().all(is_sass_completion_member_continue) {
4168 return None;
4169 }
4170 Some((!token.is_empty()).then(|| token.to_string()))
4171}
4172
4173fn sass_member_completion_prefix(line_prefix: &str) -> Option<String> {
4174 let token = sass_completion_trailing_token(line_prefix)?;
4175 if token.starts_with('.') || token.contains('$') || !token.contains('.') {
4176 return None;
4177 }
4178 if !token.chars().all(is_sass_completion_member_continue) {
4179 return None;
4180 }
4181 let (namespace, _) = token.split_once('.')?;
4182 (!namespace.is_empty()).then(|| token.to_string())
4183}
4184
4185fn sass_completion_trailing_token(text: &str) -> Option<&str> {
4186 text.rsplit(|ch: char| {
4187 ch.is_ascii_whitespace()
4188 || matches!(ch, ':' | ';' | '{' | '}' | '(' | ')' | ',' | '[' | ']')
4189 })
4190 .next()
4191 .filter(|token| !token.is_empty())
4192}
4193
4194fn is_sass_completion_identifier_continue(ch: char) -> bool {
4195 is_ascii_word_continue(ch)
4196}
4197
4198fn is_sass_completion_member_continue(ch: char) -> bool {
4199 is_ascii_word_continue(ch) || ch == '.' || ch == '$'
4200}
4201
4202fn trim_hover_snippet(snippet: &str) -> String {
4203 const MAX_SNIPPET_LEN: usize = 1200;
4204 if snippet.len() <= MAX_SNIPPET_LEN {
4205 return snippet.to_string();
4206 }
4207 let end = char_boundary_floor(snippet, MAX_SNIPPET_LEN);
4208 format!("{}...", snippet[..end].trim_end())
4209}
4210
4211fn parser_range_for_byte_span(source: &str, span: ParserByteSpanV0) -> ParserRangeV0 {
4212 ParserRangeV0 {
4213 start: parser_position_for_byte_offset(source, span.start),
4214 end: parser_position_for_byte_offset(source, span.end),
4215 }
4216}
4217
4218fn push_omena_query_ready_surface(ready_surfaces: &mut Vec<&'static str>, surface: &'static str) {
4219 if !ready_surfaces.contains(&surface) {
4220 ready_surfaces.push(surface);
4221 }
4222}
4223
4224fn end_of_source_range(source: &str) -> ParserRangeV0 {
4225 let position = parser_position_for_byte_offset(source, source.len());
4226 ParserRangeV0 {
4227 start: position,
4228 end: position,
4229 }
4230}
4231
4232fn parser_position_for_byte_offset(source: &str, offset: usize) -> ParserPositionV0 {
4233 let clamped_offset = offset.min(source.len());
4234 let mut line = 0usize;
4235 let mut character = 0usize;
4236
4237 for (byte_index, ch) in source.char_indices() {
4238 if byte_index >= clamped_offset {
4239 break;
4240 }
4241 if ch == '\n' {
4242 line += 1;
4243 character = 0;
4244 } else {
4245 character += ch.len_utf16();
4246 }
4247 }
4248
4249 ParserPositionV0 { line, character }
4250}
4251
4252fn byte_offset_for_parser_position(source: &str, position: ParserPositionV0) -> Option<usize> {
4253 let mut current_line = 0usize;
4254 let mut current_character = 0usize;
4255
4256 if position.line == 0 && position.character == 0 {
4257 return Some(0);
4258 }
4259
4260 for (byte_index, ch) in source.char_indices() {
4261 if current_line == position.line && current_character == position.character {
4262 return Some(byte_index);
4263 }
4264 if ch == '\n' {
4265 current_line += 1;
4266 current_character = 0;
4267 if current_line == position.line && position.character == 0 {
4268 return Some(byte_index + ch.len_utf8());
4269 }
4270 } else if current_line == position.line {
4271 current_character += ch.len_utf16();
4272 }
4273 }
4274
4275 (current_line == position.line && current_character == position.character)
4276 .then_some(source.len())
4277}
4278
4279fn skip_ascii_whitespace(source: &str, mut offset: usize) -> usize {
4280 while source
4281 .as_bytes()
4282 .get(offset)
4283 .is_some_and(u8::is_ascii_whitespace)
4284 {
4285 offset += 1;
4286 }
4287 offset
4288}
4289
4290fn matching_style_block_end(
4291 source: &str,
4292 open_offset: usize,
4293 open: u8,
4294 close: u8,
4295) -> Option<usize> {
4296 if source.as_bytes().get(open_offset) != Some(&open) {
4297 return None;
4298 }
4299 let mut cursor = advance_style_scan_cursor(source, open_offset, source.len());
4300 let mut depth = 1usize;
4301 while cursor < source.len() {
4302 match source.as_bytes().get(cursor).copied()? {
4303 b'\'' | b'"' | b'`' => {
4304 cursor = skip_style_string_literal(source, cursor, source.len())?;
4305 }
4306 byte if byte == open => {
4307 depth += 1;
4308 cursor = advance_style_scan_cursor(source, cursor, source.len());
4309 }
4310 byte if byte == close => {
4311 depth -= 1;
4312 if depth == 0 {
4313 return Some(cursor);
4314 }
4315 cursor = advance_style_scan_cursor(source, cursor, source.len());
4316 }
4317 _ => cursor = advance_style_scan_cursor(source, cursor, source.len()),
4318 }
4319 }
4320 None
4321}
4322
4323fn split_top_level_style_segments(
4324 source: &str,
4325 start: usize,
4326 end: usize,
4327 delimiter: u8,
4328) -> Vec<(usize, usize)> {
4329 let mut segments = Vec::new();
4330 let end = char_boundary_floor(source, end);
4331 let mut segment_start = char_boundary_ceil(source, start).min(end);
4332 let mut cursor = segment_start;
4333 let mut depth = 0usize;
4334 while cursor < end {
4335 match source.as_bytes().get(cursor).copied() {
4336 Some(b'\'' | b'"' | b'`') => {
4337 cursor = skip_style_string_literal(source, cursor, end).unwrap_or(end);
4338 }
4339 Some(b'(' | b'[' | b'{') => {
4340 depth += 1;
4341 cursor = advance_style_scan_cursor(source, cursor, end);
4342 }
4343 Some(b')' | b']' | b'}') => {
4344 depth = depth.saturating_sub(1);
4345 cursor = advance_style_scan_cursor(source, cursor, end);
4346 }
4347 Some(byte) if byte == delimiter && depth == 0 => {
4348 segments.push((segment_start, cursor));
4349 cursor = advance_style_scan_cursor(source, cursor, end);
4350 segment_start = cursor;
4351 }
4352 Some(_) => cursor = advance_style_scan_cursor(source, cursor, end),
4353 None => break,
4354 }
4355 }
4356 if segment_start <= end {
4357 segments.push((segment_start, end));
4358 }
4359 segments
4360}
4361
4362fn skip_style_string_literal(source: &str, quote_offset: usize, limit: usize) -> Option<usize> {
4363 let quote = source.as_bytes().get(quote_offset).copied()?;
4364 let limit = char_boundary_floor(source, limit);
4365 let mut cursor = quote_offset + 1;
4366 while cursor < limit {
4367 let byte = source.as_bytes().get(cursor).copied()?;
4368 if byte == b'\\' {
4369 cursor = advance_style_escaped_char(source, cursor, limit);
4370 continue;
4371 }
4372 if byte == quote {
4373 return Some(cursor + 1);
4374 }
4375 cursor = advance_style_scan_cursor(source, cursor, limit);
4376 }
4377 None
4378}
4379
4380fn advance_style_escaped_char(source: &str, slash_offset: usize, limit: usize) -> usize {
4381 let after_slash = advance_style_scan_cursor(source, slash_offset, limit);
4382 advance_style_scan_cursor(source, after_slash, limit)
4383}
4384
4385fn advance_style_scan_cursor(source: &str, cursor: usize, limit: usize) -> usize {
4386 let cursor = char_boundary_ceil(source, cursor);
4387 let limit = char_boundary_floor(source, limit);
4388 if cursor >= limit {
4389 return limit;
4390 }
4391 char_boundary_ceil(source, cursor + 1).min(limit)
4392}
4393
4394fn char_boundary_floor(source: &str, index: usize) -> usize {
4395 let mut index = index.min(source.len());
4396 while index > 0 && !source.is_char_boundary(index) {
4397 index -= 1;
4398 }
4399 index
4400}
4401
4402fn char_boundary_ceil(source: &str, index: usize) -> usize {
4403 let mut index = index.min(source.len());
4404 while index < source.len() && !source.is_char_boundary(index) {
4405 index += 1;
4406 }
4407 index
4408}
4409
4410fn is_sass_builtin_module_source(source: &str) -> bool {
4411 source.starts_with("sass:")
4412}
4413
4414fn format_query_sass_symbol_label(symbol_kind: &str, name: &str) -> String {
4415 match symbol_kind {
4416 "variable" => format!("Sass variable '${name}'"),
4417 "mixin" => format!("Sass mixin '@mixin {name}'"),
4418 "function" => format!("Sass function '{name}()'"),
4419 _ => format!("Sass symbol '{name}'"),
4420 }
4421}
4422
4423#[cfg(test)]
4424mod runtime_index_tests {
4425 use super::*;
4426
4427 #[test]
4428 fn semantic_runtime_index_from_query_facts_matches_source_parser() {
4429 let style_path = "/workspace/src/App.module.scss";
4430 let style_source = r#"
4431@keyframes fade { to { opacity: 1; } }
4432.card {
4433 --brand: red;
4434 color: var(--brand);
4435 animation: fade 1s;
4436}
4437"#;
4438 let facts = summarize_omena_query_omena_parser_style_facts(
4439 style_source,
4440 omena_parser_dialect_for_style_path(style_path),
4441 );
4442
4443 assert_eq!(
4444 semantic_runtime_index_from_query_style_facts(style_path, &facts),
4445 omena_semantic::summarize_style_runtime_index_facts_from_source(
4446 style_path,
4447 style_source,
4448 ),
4449 );
4450 }
4451}