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