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