1use std::collections::{BTreeMap, BTreeSet, VecDeque};
4
5use omena_cross_file_summary::{
6 HypergraphClosureMode, HypergraphClosurePath, collect_directed_graph_cycles,
7 collect_hypergraph_transitive_closure_paths,
8 collect_hypergraph_transitive_closure_paths_with_mode,
9};
10use omena_parser::{LexedToken, StyleDialect, summarize_omena_parser_style_facts};
11use omena_resolver::canonicalize_omena_resolver_style_identity_path;
12use omena_syntax::SyntaxKind;
13use serde::Serialize;
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
16#[serde(rename_all = "camelCase")]
17pub struct SassModuleGraphEdgeFactV0 {
18 pub from_style_path: String,
19 pub edge_kind: &'static str,
20 pub source: String,
21 pub rule_ordinal: usize,
22 pub namespace_kind: Option<&'static str>,
23 pub namespace: Option<String>,
24 pub forward_prefix: Option<String>,
25 pub visibility_filter_kind: Option<&'static str>,
26 pub visibility_filter_names: Vec<String>,
27 pub resolved_style_path: Option<String>,
28 pub status: &'static str,
29 pub configuration_signature: String,
30 pub configuration_variable_count: usize,
31 pub invalid_configuration_variable_names: Vec<String>,
32 pub module_instance_identity_key: Option<String>,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
36#[serde(rename_all = "camelCase")]
37pub struct SassModuleGraphClosureSummaryV0 {
38 pub schema_version: &'static str,
39 pub product: &'static str,
40 pub status: &'static str,
41 pub module_edge_count: usize,
42 pub graph_closure_edge_count: usize,
43 pub cycle_count: usize,
44 pub graph_closure_edges: Vec<SassModuleGraphClosureEdgeV0>,
45 pub cycles: Vec<SassModuleCycleV0>,
46 pub capabilities: SassModuleGraphClosureCapabilitiesV0,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
50#[serde(rename_all = "camelCase")]
51pub struct SassModuleGraphResolutionSummaryV0 {
52 pub schema_version: &'static str,
53 pub product: &'static str,
54 pub status: &'static str,
55 pub resolution_scope: &'static str,
56 pub style_count: usize,
57 pub module_edge_count: usize,
58 pub resolved_module_edge_count: usize,
59 pub unresolved_module_edge_count: usize,
60 pub external_module_edge_count: usize,
61 pub configured_module_instance_count: usize,
62 pub visibility_filter_count: usize,
63 pub edges: Vec<SassModuleGraphEdgeFactV0>,
64 pub graph_closure_edge_count: usize,
65 pub cycle_count: usize,
66 pub graph_closure_edges: Vec<SassModuleGraphClosureEdgeV0>,
67 pub cycles: Vec<SassModuleCycleV0>,
68 pub capabilities: SassModuleGraphResolutionCapabilitiesV0,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
72#[serde(rename_all = "camelCase")]
73pub struct SassModuleGraphResolutionCapabilitiesV0 {
74 pub semantic_layer_owned: bool,
75 pub edge_aggregation_ready: bool,
76 pub graph_closure_ready: bool,
77 pub cycle_detection_ready: bool,
78 pub namespace_show_hide_filter_ready: bool,
79 pub configured_module_instance_identity_ready: bool,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
83#[serde(rename_all = "camelCase")]
84pub struct SassModuleGraphClosureCapabilitiesV0 {
85 pub semantic_layer_owned: bool,
86 pub graph_closure_ready: bool,
87 pub cycle_detection_ready: bool,
88 pub namespace_show_hide_filter_ready: bool,
89 pub configured_module_instance_identity_ready: bool,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
93#[serde(rename_all = "camelCase")]
94pub struct SassModuleGraphClosureEdgeV0 {
95 pub from_style_path: String,
96 pub target_style_path: String,
97 pub edge_kind: &'static str,
98 pub depth: usize,
99 pub path: Vec<String>,
100 pub namespace_kind: Option<&'static str>,
101 pub namespace: Option<String>,
102 pub forward_prefix: Option<String>,
103 pub visibility_filter_kind: Option<&'static str>,
104 pub visibility_filter_names: Vec<String>,
105 pub configuration_signature: String,
106 pub configuration_variable_count: usize,
107 pub invalid_configuration_variable_names: Vec<String>,
108 pub module_instance_identity_key: Option<String>,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
112#[serde(rename_all = "camelCase")]
113pub struct SassModuleCycleV0 {
114 pub path: Vec<String>,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
118#[serde(rename_all = "camelCase")]
119pub struct SassSymbolKeyV0 {
120 pub symbol_kind: &'static str,
121 pub namespace: Option<String>,
122 pub name: String,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
126#[serde(rename_all = "camelCase")]
127pub struct StyleImportReachabilityEdgeFactV0 {
128 pub from_style_path: String,
129 pub target_style_path: String,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
133#[serde(rename_all = "camelCase")]
134pub struct StyleImportReachabilitySummaryV0 {
135 pub schema_version: &'static str,
136 pub product: &'static str,
137 pub status: &'static str,
138 pub target_style_path: String,
139 pub edge_count: usize,
140 pub reachable_style_path_count: usize,
141 pub reachable_style_paths: Vec<StyleImportReachabilityFactV0>,
142 pub capabilities: StyleImportReachabilityCapabilitiesV0,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
146#[serde(rename_all = "camelCase")]
147pub struct StyleImportReachabilityFactV0 {
148 pub style_path: String,
149 pub distance: usize,
150 pub order: usize,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
154#[serde(rename_all = "camelCase")]
155pub struct StyleImportReachabilityCapabilitiesV0 {
156 pub semantic_layer_owned: bool,
157 pub transitive_reachability_ready: bool,
158 pub stable_distance_ready: bool,
159 pub stable_order_ready: bool,
160}
161
162#[derive(Debug, Clone, PartialEq, Eq)]
163pub struct SassModuleVariableOverrideV0 {
164 pub value: String,
165 pub is_default: bool,
166}
167
168#[derive(Debug, Clone, Copy)]
169pub struct SassModuleUseConfigurationRequestV0<'a> {
170 pub from_style_path: &'a str,
171 pub rule_ordinal: usize,
172}
173
174#[derive(Debug, Clone, Copy)]
175pub struct SassModuleForwardConfigurationRequestV0<'a> {
176 pub from_style_path: &'a str,
177 pub target_style_path: &'a str,
178 pub rule_ordinal: usize,
179 pub inherited_variable_overrides: &'a BTreeMap<String, String>,
180 pub forward_prefix: Option<&'a str>,
181 pub visibility_filter_kind: Option<&'static str>,
182 pub visibility_filter_names: &'a [String],
183 pub configurable_names: &'a BTreeSet<String>,
184}
185
186pub trait SassModuleGraphConfigurationResolverV0 {
187 fn use_variable_overrides(
188 &self,
189 request: SassModuleUseConfigurationRequestV0<'_>,
190 ) -> BTreeMap<String, String>;
191
192 fn forward_effective_variable_overrides(
193 &self,
194 request: SassModuleForwardConfigurationRequestV0<'_>,
195 ) -> BTreeMap<String, String>;
196
197 fn configurable_names(&self, target_style_path: &str) -> BTreeSet<String>;
198}
199
200pub trait SassModuleConfigurableNamesResolverV0 {
201 fn local_configurable_names(&self, style_path: &str, style_source: &str) -> BTreeSet<String>;
202
203 fn resolve_module_source(
204 &self,
205 from_style_path: &str,
206 source: &str,
207 available_style_paths: &BTreeSet<&str>,
208 ) -> Option<String>;
209}
210
211pub trait SassModuleVisibleSymbolsResolverV0 {
212 fn own_symbol_declaration_keys(&self, style_path: &str) -> BTreeSet<(&'static str, String)>;
213
214 fn builtin_module_exports(&self, source: &str) -> Option<BTreeSet<(&'static str, String)>>;
215
216 fn external_module_exports(
217 &self,
218 edge: &SassModuleGraphEdgeFactV0,
219 ) -> BTreeSet<(&'static str, String)>;
220}
221
222pub fn collect_visible_sass_symbol_keys(
223 target_style_path: &str,
224 edges: &[SassModuleGraphEdgeFactV0],
225 resolver: &impl SassModuleVisibleSymbolsResolverV0,
226) -> BTreeSet<SassSymbolKeyV0> {
227 let mut visible = resolver
228 .own_symbol_declaration_keys(target_style_path)
229 .into_iter()
230 .map(|(symbol_kind, name)| sass_symbol_key(symbol_kind, None, name))
231 .collect::<BTreeSet<_>>();
232
233 for edge in edges
234 .iter()
235 .filter(|edge| edge.from_style_path == target_style_path)
236 {
237 let exported =
238 exported_sass_symbol_keys_for_edge(edge, edges, resolver, &mut BTreeSet::new());
239 match edge.edge_kind {
240 "sassUse"
241 if edge.namespace_kind == Some("default")
242 || edge.namespace_kind == Some("alias") =>
243 {
244 if let Some(namespace) = edge.namespace.clone() {
245 visible.extend(exported.into_iter().map(|(symbol_kind, name)| {
246 sass_symbol_key(symbol_kind, Some(namespace.clone()), name)
247 }));
248 }
249 }
250 "sassUse" if edge.namespace_kind == Some("wildcard") => {
251 visible.extend(
252 exported
253 .into_iter()
254 .map(|(symbol_kind, name)| sass_symbol_key(symbol_kind, None, name)),
255 );
256 }
257 "sassImport" => {
258 visible.extend(
259 exported
260 .into_iter()
261 .map(|(symbol_kind, name)| sass_symbol_key(symbol_kind, None, name)),
262 );
263 }
264 _ => {}
265 }
266 }
267
268 visible
269}
270
271fn collect_exported_sass_symbol_keys(
272 style_path: &str,
273 edges: &[SassModuleGraphEdgeFactV0],
274 resolver: &impl SassModuleVisibleSymbolsResolverV0,
275 visiting: &mut BTreeSet<String>,
276) -> BTreeSet<(&'static str, String)> {
277 if !visiting.insert(style_path.to_string()) {
278 return BTreeSet::new();
279 }
280
281 let mut exported = resolver.own_symbol_declaration_keys(style_path);
282 for edge in edges
283 .iter()
284 .filter(|edge| edge.from_style_path == style_path)
285 .filter(|edge| edge.edge_kind == "sassForward" || edge.edge_kind == "sassImport")
286 {
287 let module_exports = exported_sass_symbol_keys_for_edge(edge, edges, resolver, visiting);
288 for (symbol_kind, name) in module_exports {
289 if !sass_forward_visibility_allows(edge, symbol_kind, name.as_str()) {
290 continue;
291 }
292 let exported_name = if edge.edge_kind == "sassForward" {
293 apply_sass_forward_prefix(edge.forward_prefix.as_deref(), name.as_str())
294 } else {
295 name
296 };
297 exported.insert((symbol_kind, exported_name));
298 }
299 }
300
301 visiting.remove(style_path);
302 exported
303}
304
305fn exported_sass_symbol_keys_for_edge(
306 edge: &SassModuleGraphEdgeFactV0,
307 edges: &[SassModuleGraphEdgeFactV0],
308 resolver: &impl SassModuleVisibleSymbolsResolverV0,
309 visiting: &mut BTreeSet<String>,
310) -> BTreeSet<(&'static str, String)> {
311 if let Some(exports) = resolver.builtin_module_exports(edge.source.as_str()) {
312 exports
313 } else if edge.status == "resolved" {
314 edge.resolved_style_path
315 .as_deref()
316 .map(|path| collect_exported_sass_symbol_keys(path, edges, resolver, visiting))
317 .unwrap_or_default()
318 } else if edge.status == "external" {
319 resolver.external_module_exports(edge)
320 } else {
321 BTreeSet::new()
322 }
323}
324
325pub fn derive_sass_module_configurable_variable_names(
326 style_path: &str,
327 style_source: &str,
328 available_style_paths: &BTreeSet<&str>,
329 source_by_path: &BTreeMap<String, String>,
330 resolver: &impl SassModuleConfigurableNamesResolverV0,
331) -> BTreeSet<String> {
332 let mut visiting = BTreeSet::new();
333 derive_sass_module_configurable_variable_names_inner(
334 style_path,
335 style_source,
336 available_style_paths,
337 source_by_path,
338 resolver,
339 &mut visiting,
340 )
341}
342
343fn derive_sass_module_configurable_variable_names_inner(
344 style_path: &str,
345 style_source: &str,
346 available_style_paths: &BTreeSet<&str>,
347 source_by_path: &BTreeMap<String, String>,
348 resolver: &impl SassModuleConfigurableNamesResolverV0,
349 visiting: &mut BTreeSet<String>,
350) -> BTreeSet<String> {
351 let identity_path = canonicalize_omena_resolver_style_identity_path(style_path);
352 if !visiting.insert(identity_path.clone()) {
353 return BTreeSet::new();
354 }
355
356 let mut names = resolver.local_configurable_names(style_path, style_source);
357 let facts = summarize_omena_parser_style_facts(style_source, StyleDialect::Scss);
358 for (forward_rule_ordinal, edge) in facts
359 .sass_module_edges
360 .iter()
361 .filter(|edge| edge.kind == "sassForward")
362 .enumerate()
363 {
364 let Some(resolved) =
365 resolver.resolve_module_source(style_path, edge.source.as_str(), available_style_paths)
366 else {
367 continue;
368 };
369 let Some(source) = source_by_path.get(resolved.as_str()) else {
370 continue;
371 };
372 let child_names = derive_sass_module_configurable_variable_names_inner(
373 resolved.as_str(),
374 source,
375 available_style_paths,
376 source_by_path,
377 resolver,
378 visiting,
379 );
380 let non_default_forward_overrides =
381 derive_sass_module_forward_variable_overrides_at_ordinal(
382 style_source,
383 forward_rule_ordinal,
384 )
385 .into_iter()
386 .filter_map(|(name, override_entry)| (!override_entry.is_default).then_some(name))
387 .collect::<BTreeSet<_>>();
388 let child_names = child_names
389 .into_iter()
390 .filter(|name| !non_default_forward_overrides.contains(name))
391 .collect::<BTreeSet<_>>();
392 let export_prefix =
393 derive_sass_forward_export_prefix_at_ordinal(style_source, forward_rule_ordinal);
394 names.extend(filter_sass_forward_configurable_variable_names(
395 child_names,
396 export_prefix.as_deref(),
397 edge.visibility_filter_kind,
398 &edge.visibility_filter_names,
399 ));
400 }
401
402 visiting.remove(identity_path.as_str());
403 names
404}
405
406pub fn fold_sass_symbol_name(name: &str) -> String {
407 name.replace('_', "-")
408}
409
410pub fn sass_symbol_key(
411 symbol_kind: &'static str,
412 namespace: Option<String>,
413 name: String,
414) -> SassSymbolKeyV0 {
415 SassSymbolKeyV0 {
416 symbol_kind,
417 namespace,
418 name: fold_sass_symbol_name(&name),
419 }
420}
421
422fn sass_forward_visibility_allows(
423 edge: &SassModuleGraphEdgeFactV0,
424 symbol_kind: &'static str,
425 name: &str,
426) -> bool {
427 let matches_filter = |filter_name: &String| {
428 sass_forward_filter_name_matches_symbol(
429 filter_name,
430 edge.forward_prefix.as_deref(),
431 symbol_kind,
432 name,
433 )
434 };
435 match edge.visibility_filter_kind {
436 Some("show") => edge.visibility_filter_names.iter().any(matches_filter),
437 Some("hide") => !edge.visibility_filter_names.iter().any(matches_filter),
438 _ => true,
439 }
440}
441
442pub fn sass_forward_filter_name_matches_symbol(
443 filter_name: &str,
444 prefix: Option<&str>,
445 symbol_kind: &'static str,
446 name: &str,
447) -> bool {
448 let exposed_name = apply_sass_forward_prefix(prefix, name);
449 filter_name == exposed_name
450 || filter_name.trim_start_matches('$') == exposed_name
451 || (symbol_kind != "variable" && filter_name.trim_start_matches('@') == exposed_name)
452}
453
454pub fn apply_sass_forward_prefix(prefix: Option<&str>, name: &str) -> String {
455 match prefix {
456 Some(prefix) if prefix.contains('*') => prefix.replace('*', name),
457 Some(prefix) => format!("{prefix}{name}"),
458 None => name.to_string(),
459 }
460}
461
462pub fn summarize_sass_module_configuration_signature(
463 variable_overrides: &BTreeMap<String, String>,
464) -> String {
465 if variable_overrides.is_empty() {
466 return "with:none".to_string();
467 }
468 let mut key = String::from("with");
469 for (name, value) in variable_overrides {
470 key.push('|');
471 key.push_str(name.len().to_string().as_str());
472 key.push(':');
473 key.push_str(name);
474 key.push('=');
475 key.push_str(value.len().to_string().as_str());
476 key.push(':');
477 key.push_str(value);
478 }
479 key
480}
481
482pub fn summarize_sass_module_instance_identity_key(
483 style_path: &str,
484 variable_overrides: &BTreeMap<String, String>,
485) -> String {
486 let canonical_path = canonicalize_omena_resolver_style_identity_path(style_path);
487 let mut key = format!("path:{}:{canonical_path}", canonical_path.len());
488 key.push('|');
489 key.push_str(summarize_sass_module_configuration_signature(variable_overrides).as_str());
490 key
491}
492
493pub fn resolve_sass_module_effective_variable_overrides(
494 style_path: &str,
495 variable_overrides: &BTreeMap<String, String>,
496 loaded_module_overrides_by_path: &mut BTreeMap<String, BTreeMap<String, String>>,
497) -> Option<BTreeMap<String, String>> {
498 let canonical_path = canonicalize_omena_resolver_style_identity_path(style_path);
499 match loaded_module_overrides_by_path.get(canonical_path.as_str()) {
500 Some(existing_overrides) if variable_overrides.is_empty() => {
501 Some(existing_overrides.clone())
502 }
503 Some(existing_overrides) => {
504 (existing_overrides == variable_overrides).then(|| variable_overrides.clone())
505 }
506 None => {
507 loaded_module_overrides_by_path.insert(canonical_path, variable_overrides.clone());
508 Some(variable_overrides.clone())
509 }
510 }
511}
512
513pub fn sass_module_configuration_variables_are_valid(
514 variable_overrides: &BTreeMap<String, String>,
515 configurable_names: &BTreeSet<String>,
516) -> bool {
517 variable_overrides
518 .keys()
519 .all(|name| configurable_names.contains(name))
520}
521
522pub fn derive_sass_module_rule_variable_overrides_at_ordinal(
523 style_source: &str,
524 at_keyword: &str,
525 rule_ordinal: usize,
526) -> BTreeMap<String, String> {
527 sass_module_rule_source_at_ordinal(style_source, at_keyword, rule_ordinal)
528 .map(parse_sass_module_use_variable_overrides_from_rule)
529 .unwrap_or_default()
530}
531
532pub fn derive_sass_module_forward_variable_overrides_at_ordinal(
533 style_source: &str,
534 forward_rule_ordinal: usize,
535) -> BTreeMap<String, SassModuleVariableOverrideV0> {
536 sass_module_rule_source_at_ordinal(style_source, "@forward", forward_rule_ordinal)
537 .map(parse_sass_module_forward_variable_overrides_from_rule)
538 .unwrap_or_default()
539}
540
541pub fn derive_sass_module_forward_variable_override_values_at_ordinal(
542 style_source: &str,
543 forward_rule_ordinal: usize,
544) -> BTreeMap<String, String> {
545 derive_sass_module_forward_variable_overrides_at_ordinal(style_source, forward_rule_ordinal)
546 .into_iter()
547 .map(|(name, override_entry)| (name, override_entry.value))
548 .collect()
549}
550
551pub fn derive_sass_module_forward_effective_variable_overrides_at_ordinal(
552 style_source: &str,
553 forward_rule_ordinal: usize,
554 inherited_variable_overrides: &BTreeMap<String, String>,
555 export_prefix: Option<&str>,
556 visibility_filter_kind: Option<&'static str>,
557 visibility_filter_names: &[String],
558 configurable_names: &BTreeSet<String>,
559) -> BTreeMap<String, String> {
560 let explicit_variable_overrides = derive_sass_module_forward_variable_overrides_at_ordinal(
561 style_source,
562 forward_rule_ordinal,
563 );
564 derive_sass_forward_effective_variable_overrides(
565 &explicit_variable_overrides,
566 inherited_variable_overrides,
567 export_prefix,
568 visibility_filter_kind,
569 visibility_filter_names,
570 configurable_names,
571 )
572}
573
574pub fn derive_sass_forward_effective_variable_overrides(
575 explicit_variable_overrides: &BTreeMap<String, SassModuleVariableOverrideV0>,
576 inherited_variable_overrides: &BTreeMap<String, String>,
577 export_prefix: Option<&str>,
578 visibility_filter_kind: Option<&'static str>,
579 visibility_filter_names: &[String],
580 configurable_names: &BTreeSet<String>,
581) -> BTreeMap<String, String> {
582 let mut variable_overrides = explicit_variable_overrides
583 .iter()
584 .filter(|(_, override_entry)| override_entry.is_default)
585 .map(|(name, override_entry)| (name.clone(), override_entry.value.clone()))
586 .collect::<BTreeMap<_, _>>();
587 variable_overrides.extend(
588 inherited_variable_overrides
589 .iter()
590 .filter_map(|(name, value)| {
591 let internal_name = sass_forward_internal_variable_name_for_exposed_name(
592 name.as_str(),
593 export_prefix,
594 )?;
595 sass_forward_exposed_variable_is_visible(
596 name.as_str(),
597 visibility_filter_kind,
598 visibility_filter_names,
599 )
600 .then_some((internal_name, value.clone()))
601 })
602 .filter(|(name, _)| configurable_names.contains(name))
603 .collect::<BTreeMap<_, _>>(),
604 );
605 variable_overrides.extend(
606 explicit_variable_overrides
607 .iter()
608 .filter(|(_, override_entry)| !override_entry.is_default)
609 .map(|(name, override_entry)| (name.clone(), override_entry.value.clone())),
610 );
611 variable_overrides
612}
613
614pub fn filter_sass_forward_configurable_variable_names(
615 names: BTreeSet<String>,
616 prefix: Option<&str>,
617 visibility_filter_kind: Option<&'static str>,
618 visibility_filter_names: &[String],
619) -> BTreeSet<String> {
620 names
621 .into_iter()
622 .filter_map(|name| {
623 let exposed_name = prefix
624 .map(|prefix| prefix.replace('*', name.as_str()))
625 .unwrap_or(name);
626 sass_forward_exposed_variable_is_visible(
627 exposed_name.as_str(),
628 visibility_filter_kind,
629 visibility_filter_names,
630 )
631 .then(|| canonical_sass_variable_name(exposed_name.as_str()))
632 })
633 .collect()
634}
635
636pub fn filter_sass_forward_exports(
637 exports: BTreeMap<String, String>,
638 filter_kind: Option<&'static str>,
639 filter_names: &[String],
640) -> BTreeMap<String, String> {
641 match filter_kind {
642 Some("show") => exports
643 .into_iter()
644 .filter(|(name, _)| {
645 filter_names
646 .iter()
647 .any(|filter| sass_variable_names_equal(filter, name))
648 })
649 .collect(),
650 Some("hide") => exports
651 .into_iter()
652 .filter(|(name, _)| {
653 !filter_names
654 .iter()
655 .any(|filter| sass_variable_names_equal(filter, name))
656 })
657 .collect(),
658 _ => exports,
659 }
660}
661
662pub fn prefix_sass_forward_exports(
663 exports: BTreeMap<String, String>,
664 prefix: Option<&str>,
665) -> BTreeMap<String, String> {
666 let Some(prefix) = prefix else {
667 return exports;
668 };
669 exports
670 .into_iter()
671 .map(|(name, value)| (prefix.replace('*', name.as_str()), value))
672 .collect()
673}
674
675pub fn derive_sass_forward_export_prefix_at_ordinal(
676 style_source: &str,
677 forward_rule_ordinal: usize,
678) -> Option<String> {
679 sass_module_rule_source_at_ordinal(style_source, "@forward", forward_rule_ordinal)
680 .and_then(parse_sass_forward_export_prefix_from_rule)
681}
682
683fn sass_module_rule_source_at_ordinal<'a>(
684 style_source: &'a str,
685 at_keyword: &str,
686 rule_ordinal: usize,
687) -> Option<&'a str> {
688 let lexed = omena_parser::lex(style_source, StyleDialect::Scss);
689 let tokens = lexed.tokens();
690 let mut depth = 0usize;
691 let mut index = 0usize;
692 let mut current_rule_ordinal = 0usize;
693
694 while index < tokens.len() {
695 match tokens[index].kind {
696 SyntaxKind::LeftBrace => depth += 1,
697 SyntaxKind::RightBrace => depth = depth.saturating_sub(1),
698 SyntaxKind::AtKeyword
699 if depth == 0 && tokens[index].text.eq_ignore_ascii_case(at_keyword) =>
700 {
701 let Some(end_index) = sass_module_rule_semicolon(tokens, index) else {
702 index += 1;
703 continue;
704 };
705 if sass_module_rule_source_name(tokens, index + 1, end_index).is_some() {
706 if current_rule_ordinal == rule_ordinal {
707 let start = token_start(&tokens[index]);
708 let end = token_end(&tokens[end_index]);
709 return style_source.get(start..end);
710 }
711 current_rule_ordinal += 1;
712 }
713 index = end_index + 1;
714 continue;
715 }
716 _ => {}
717 }
718 index += 1;
719 }
720
721 None
722}
723
724fn sass_module_rule_semicolon(tokens: &[LexedToken], at_use_index: usize) -> Option<usize> {
725 let mut index = at_use_index + 1;
726 while index < tokens.len() {
727 match tokens[index].kind {
728 SyntaxKind::Semicolon => return Some(index),
729 SyntaxKind::LeftBrace | SyntaxKind::RightBrace => return None,
730 _ => index += 1,
731 }
732 }
733 None
734}
735
736fn sass_module_rule_source_name(
737 tokens: &[LexedToken],
738 start_index: usize,
739 end_index: usize,
740) -> Option<String> {
741 tokens[start_index..end_index]
742 .iter()
743 .find(|token| matches!(token.kind, SyntaxKind::String | SyntaxKind::Url))
744 .map(|token| token.text.trim_matches('"').trim_matches('\'').to_string())
745}
746
747fn parse_sass_module_use_variable_overrides_from_rule(
748 rule_source: &str,
749) -> BTreeMap<String, String> {
750 parse_sass_module_rule_override_content(rule_source)
751 .map(parse_sass_use_variable_override_list)
752 .unwrap_or_default()
753}
754
755fn parse_sass_module_forward_variable_overrides_from_rule(
756 rule_source: &str,
757) -> BTreeMap<String, SassModuleVariableOverrideV0> {
758 parse_sass_module_rule_override_content(rule_source)
759 .map(|content| parse_sass_variable_override_list(content, true))
760 .unwrap_or_default()
761}
762
763fn parse_sass_module_rule_override_content(rule_source: &str) -> Option<&str> {
764 let lexed = omena_parser::lex(rule_source, StyleDialect::Scss);
765 let tokens = lexed.tokens();
766 let with_index = tokens
767 .iter()
768 .position(|token| token.text.eq_ignore_ascii_case("with"))?;
769 let left_paren_index = tokens[with_index + 1..]
770 .iter()
771 .position(|token| token.kind == SyntaxKind::LeftParen)
772 .map(|offset| with_index + 1 + offset)?;
773 let right_paren_index = matching_right_paren(tokens, left_paren_index)?;
774 let start = token_end(&tokens[left_paren_index]);
775 let end = token_start(&tokens[right_paren_index]);
776 rule_source.get(start..end)
777}
778
779fn parse_sass_use_variable_override_list(content: &str) -> BTreeMap<String, String> {
780 parse_sass_variable_override_list(content, false)
781 .into_iter()
782 .map(|(name, override_entry)| (name, override_entry.value))
783 .collect()
784}
785
786fn parse_sass_variable_override_list(
787 content: &str,
788 allow_default_flag: bool,
789) -> BTreeMap<String, SassModuleVariableOverrideV0> {
790 let mut overrides = BTreeMap::new();
791 for entry in split_top_level_commas(content) {
792 if entry.trim().is_empty() {
793 continue;
794 }
795 let Some((name, value)) = parse_sass_variable_override(entry.trim(), allow_default_flag)
796 else {
797 return BTreeMap::new();
798 };
799 overrides.insert(name, value);
800 }
801 overrides
802}
803
804fn parse_sass_variable_override(
805 entry: &str,
806 allow_default_flag: bool,
807) -> Option<(String, SassModuleVariableOverrideV0)> {
808 let colon_index = top_level_colon_index(entry)?;
809 let name = entry[..colon_index].trim().strip_prefix('$')?;
810 if name.is_empty() || !name.chars().all(sass_identifier_char) {
811 return None;
812 }
813 let (value, is_default) =
814 split_sass_forward_default_flag(entry[colon_index + 1..].trim(), allow_default_flag)?;
815 if !sass_variable_override_value_is_safe(value) {
816 return None;
817 }
818 Some((
819 canonical_sass_variable_name(name),
820 SassModuleVariableOverrideV0 {
821 value: value.to_string(),
822 is_default,
823 },
824 ))
825}
826
827fn parse_sass_forward_export_prefix_from_rule(rule_source: &str) -> Option<String> {
828 let lexed = omena_parser::lex(rule_source, StyleDialect::Scss);
829 let tokens = lexed.tokens();
830 let source_index = tokens
831 .iter()
832 .position(|token| matches!(token.kind, SyntaxKind::String | SyntaxKind::Url))?;
833 let as_index = tokens[source_index + 1..]
834 .iter()
835 .position(|token| token.text.eq_ignore_ascii_case("as"))
836 .map(|offset| source_index + 1 + offset)?;
837 let prefix_start_index = tokens[as_index + 1..]
838 .iter()
839 .position(|token| token.kind != SyntaxKind::Whitespace)
840 .map(|offset| as_index + 1 + offset)?;
841 let prefix_end_index = tokens[prefix_start_index..]
842 .iter()
843 .position(|token| {
844 token.kind == SyntaxKind::Semicolon
845 || matches!(
846 token.text.to_ascii_lowercase().as_str(),
847 "show" | "hide" | "with"
848 )
849 })
850 .map(|offset| prefix_start_index + offset)
851 .unwrap_or(tokens.len());
852 let prefix_end = tokens
853 .get(prefix_end_index)
854 .map(token_start)
855 .unwrap_or(rule_source.len());
856 let prefix = rule_source
857 .get(token_start(&tokens[prefix_start_index])..prefix_end)?
858 .trim();
859 sass_forward_export_prefix_is_safe(prefix).then(|| prefix.to_string())
860}
861
862fn sass_forward_export_prefix_is_safe(prefix: &str) -> bool {
863 prefix.contains('*')
864 && prefix
865 .chars()
866 .all(|ch| sass_identifier_char(ch) || ch == '*')
867}
868
869fn sass_forward_exposed_variable_is_visible(
870 exposed_name: &str,
871 visibility_filter_kind: Option<&'static str>,
872 visibility_filter_names: &[String],
873) -> bool {
874 match visibility_filter_kind {
875 Some("show") => visibility_filter_names
876 .iter()
877 .any(|filter| sass_variable_names_equal(filter, exposed_name)),
878 Some("hide") => !visibility_filter_names
879 .iter()
880 .any(|filter| sass_variable_names_equal(filter, exposed_name)),
881 _ => true,
882 }
883}
884
885fn sass_forward_internal_variable_name_for_exposed_name(
886 exposed_name: &str,
887 export_prefix: Option<&str>,
888) -> Option<String> {
889 let exposed_name = canonical_sass_variable_name(exposed_name);
890 let Some(export_prefix) = export_prefix else {
891 return Some(exposed_name);
892 };
893 let star_offset = export_prefix.find('*')?;
894 let prefix_before_star = canonical_sass_variable_name(&export_prefix[..star_offset]);
895 let prefix_after_star =
896 canonical_sass_variable_name(&export_prefix[star_offset + '*'.len_utf8()..]);
897 let without_prefix = exposed_name.strip_prefix(prefix_before_star.as_str())?;
898 let without_suffix = if prefix_after_star.is_empty() {
899 without_prefix
900 } else {
901 without_prefix.strip_suffix(prefix_after_star.as_str())?
902 };
903 (!without_suffix.is_empty()).then(|| canonical_sass_variable_name(without_suffix))
904}
905
906fn split_sass_forward_default_flag(value: &str, allow_default_flag: bool) -> Option<(&str, bool)> {
907 if !allow_default_flag {
908 return Some((value, false));
909 }
910 let lower = value.to_ascii_lowercase();
911 let Some(before_default) = lower.strip_suffix("!default") else {
912 return Some((value, false));
913 };
914 let value_before_default = &value[..before_default.len()];
915 let stripped = value_before_default.trim_end();
916 (!stripped.is_empty()).then_some((stripped, true))
917}
918
919fn split_top_level_commas(content: &str) -> Vec<&str> {
920 let mut entries = Vec::new();
921 let mut start = 0usize;
922 let mut delimiter_stack = Vec::<char>::new();
923 let mut quote = None;
924 let mut escaped = false;
925
926 for (index, ch) in content.char_indices() {
927 if let Some(quote_ch) = quote {
928 if escaped {
929 escaped = false;
930 } else if ch == '\\' {
931 escaped = true;
932 } else if ch == quote_ch {
933 quote = None;
934 }
935 continue;
936 }
937
938 match ch {
939 '"' | '\'' => quote = Some(ch),
940 '(' | '[' => delimiter_stack.push(ch),
941 ')' if delimiter_stack.last() == Some(&'(') => {
942 delimiter_stack.pop();
943 }
944 ']' if delimiter_stack.last() == Some(&'[') => {
945 delimiter_stack.pop();
946 }
947 ',' if delimiter_stack.is_empty() => {
948 entries.push(&content[start..index]);
949 start = index + ch.len_utf8();
950 }
951 _ => {}
952 }
953 }
954 entries.push(&content[start..]);
955 entries
956}
957
958fn top_level_colon_index(content: &str) -> Option<usize> {
959 let mut delimiter_stack = Vec::<char>::new();
960 let mut quote = None;
961 let mut escaped = false;
962
963 for (index, ch) in content.char_indices() {
964 if let Some(quote_ch) = quote {
965 if escaped {
966 escaped = false;
967 } else if ch == '\\' {
968 escaped = true;
969 } else if ch == quote_ch {
970 quote = None;
971 }
972 continue;
973 }
974
975 match ch {
976 '"' | '\'' => quote = Some(ch),
977 '(' | '[' => delimiter_stack.push(ch),
978 ')' if delimiter_stack.last() == Some(&'(') => {
979 delimiter_stack.pop();
980 }
981 ']' if delimiter_stack.last() == Some(&'[') => {
982 delimiter_stack.pop();
983 }
984 ':' if delimiter_stack.is_empty() => return Some(index),
985 _ => {}
986 }
987 }
988 None
989}
990
991fn matching_right_paren(tokens: &[LexedToken], left_paren_index: usize) -> Option<usize> {
992 let mut depth = 0usize;
993 for (index, token) in tokens.iter().enumerate().skip(left_paren_index) {
994 match token.kind {
995 SyntaxKind::LeftParen => depth += 1,
996 SyntaxKind::RightParen => {
997 depth = depth.checked_sub(1)?;
998 if depth == 0 {
999 return Some(index);
1000 }
1001 }
1002 _ => {}
1003 }
1004 }
1005 None
1006}
1007
1008fn sass_variable_override_value_is_safe(value: &str) -> bool {
1009 !value.is_empty()
1010 && !value
1011 .chars()
1012 .any(|ch| matches!(ch, ';' | '{' | '}' | '!' | '$' | '@'))
1013}
1014
1015fn canonical_sass_variable_name(name: &str) -> String {
1016 name.trim()
1017 .strip_prefix('$')
1018 .unwrap_or_else(|| name.trim())
1019 .replace('_', "-")
1020}
1021
1022fn sass_variable_names_equal(left: &str, right: &str) -> bool {
1023 canonical_sass_variable_name(left) == canonical_sass_variable_name(right)
1024}
1025
1026fn sass_identifier_char(ch: char) -> bool {
1027 ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-')
1028}
1029
1030fn token_start(token: &LexedToken) -> usize {
1031 let start: u32 = token.range.start().into();
1032 start as usize
1033}
1034
1035fn token_end(token: &LexedToken) -> usize {
1036 let end: u32 = token.range.end().into();
1037 end as usize
1038}
1039
1040pub fn summarize_sass_module_graph_closure(
1041 edges: &[SassModuleGraphEdgeFactV0],
1042 configuration_resolver: &impl SassModuleGraphConfigurationResolverV0,
1043) -> SassModuleGraphClosureSummaryV0 {
1044 let (graph_closure_edges, cycles) =
1045 summarize_sass_module_graph_closure_edges(edges, configuration_resolver);
1046 SassModuleGraphClosureSummaryV0 {
1047 schema_version: "0",
1048 product: "omena-semantic.sass-module-graph-closure",
1049 status: "semanticLayerOwnedClosure",
1050 module_edge_count: edges.len(),
1051 graph_closure_edge_count: graph_closure_edges.len(),
1052 cycle_count: cycles.len(),
1053 graph_closure_edges,
1054 cycles,
1055 capabilities: SassModuleGraphClosureCapabilitiesV0 {
1056 semantic_layer_owned: true,
1057 graph_closure_ready: true,
1058 cycle_detection_ready: true,
1059 namespace_show_hide_filter_ready: true,
1060 configured_module_instance_identity_ready: true,
1061 },
1062 }
1063}
1064
1065pub fn summarize_sass_module_graph_resolution(
1066 style_count: usize,
1067 edges: &[SassModuleGraphEdgeFactV0],
1068 configuration_resolver: &impl SassModuleGraphConfigurationResolverV0,
1069) -> SassModuleGraphResolutionSummaryV0 {
1070 let closure_summary = summarize_sass_module_graph_closure(edges, configuration_resolver);
1071 let resolved_module_edge_count = edges
1072 .iter()
1073 .filter(|edge| edge.status == "resolved")
1074 .count();
1075 let external_module_edge_count = edges
1076 .iter()
1077 .filter(|edge| edge.status == "external")
1078 .count();
1079 let unresolved_module_edge_count = edges
1080 .len()
1081 .saturating_sub(resolved_module_edge_count + external_module_edge_count);
1082 let configured_module_instance_count = edges
1083 .iter()
1084 .filter(|edge| edge.module_instance_identity_key.is_some())
1085 .count();
1086 let visibility_filter_count = edges
1087 .iter()
1088 .filter(|edge| edge.visibility_filter_kind.is_some())
1089 .count();
1090
1091 SassModuleGraphResolutionSummaryV0 {
1092 schema_version: "0",
1093 product: "omena-semantic.sass-module-graph-resolution",
1094 status: "semanticLayerOwnedResolution",
1095 resolution_scope: "batchModuleGraph",
1096 style_count,
1097 module_edge_count: edges.len(),
1098 resolved_module_edge_count,
1099 unresolved_module_edge_count,
1100 external_module_edge_count,
1101 configured_module_instance_count,
1102 visibility_filter_count,
1103 edges: edges.to_vec(),
1104 graph_closure_edge_count: closure_summary.graph_closure_edge_count,
1105 cycle_count: closure_summary.cycle_count,
1106 graph_closure_edges: closure_summary.graph_closure_edges,
1107 cycles: closure_summary.cycles,
1108 capabilities: SassModuleGraphResolutionCapabilitiesV0 {
1109 semantic_layer_owned: true,
1110 edge_aggregation_ready: true,
1111 graph_closure_ready: true,
1112 cycle_detection_ready: true,
1113 namespace_show_hide_filter_ready: true,
1114 configured_module_instance_identity_ready: true,
1115 },
1116 }
1117}
1118
1119pub fn summarize_style_import_reachability(
1120 target_style_path: &str,
1121 edges: &[StyleImportReachabilityEdgeFactV0],
1122) -> StyleImportReachabilitySummaryV0 {
1123 let mut graph = BTreeMap::<String, BTreeSet<String>>::new();
1124 for edge in edges {
1125 graph
1126 .entry(edge.from_style_path.clone())
1127 .or_default()
1128 .insert(edge.target_style_path.clone());
1129 }
1130
1131 let (closure_paths, _) =
1132 collect_hypergraph_transitive_closure_paths(&graph, |style_path: &String| {
1133 style_path.clone()
1134 });
1135 let mut seen = BTreeSet::new();
1136 let mut reachable_style_paths = Vec::new();
1137 for path in closure_paths
1138 .into_iter()
1139 .filter(|path| path.origin == target_style_path)
1140 {
1141 if path.target == target_style_path || !seen.insert(path.target.clone()) {
1142 continue;
1143 }
1144 let order = reachable_style_paths.len();
1145 reachable_style_paths.push(StyleImportReachabilityFactV0 {
1146 style_path: path.target,
1147 distance: path.depth,
1148 order,
1149 });
1150 }
1151
1152 StyleImportReachabilitySummaryV0 {
1153 schema_version: "0",
1154 product: "omena-semantic.style-import-reachability",
1155 status: "semanticLayerOwnedReachability",
1156 target_style_path: target_style_path.to_string(),
1157 edge_count: edges.len(),
1158 reachable_style_path_count: reachable_style_paths.len(),
1159 reachable_style_paths,
1160 capabilities: StyleImportReachabilityCapabilitiesV0 {
1161 semantic_layer_owned: true,
1162 transitive_reachability_ready: true,
1163 stable_distance_ready: true,
1164 stable_order_ready: true,
1165 },
1166 }
1167}
1168
1169fn summarize_sass_module_graph_closure_edges(
1170 edges: &[SassModuleGraphEdgeFactV0],
1171 configuration_resolver: &impl SassModuleGraphConfigurationResolverV0,
1172) -> (Vec<SassModuleGraphClosureEdgeV0>, Vec<SassModuleCycleV0>) {
1173 let mut resolved_edges = edges
1174 .iter()
1175 .filter(|edge| edge.status == "resolved" && edge.resolved_style_path.is_some())
1176 .collect::<Vec<_>>();
1177 resolved_edges.sort_by_key(|edge| {
1178 (
1179 edge.from_style_path.clone(),
1180 edge.resolved_style_path.clone().unwrap_or_default(),
1181 edge.edge_kind,
1182 edge.rule_ordinal,
1183 edge.source.clone(),
1184 )
1185 });
1186
1187 let mut graph = BTreeMap::<String, BTreeSet<String>>::new();
1188 let mut metadata_by_step =
1189 BTreeMap::<(String, String), Vec<SassModuleGraphClosureStepMetadata>>::new();
1190 for edge in resolved_edges {
1191 let Some(target_style_path) = edge.resolved_style_path.clone() else {
1192 continue;
1193 };
1194 graph
1195 .entry(edge.from_style_path.clone())
1196 .or_default()
1197 .insert(target_style_path.clone());
1198 metadata_by_step
1199 .entry((edge.from_style_path.clone(), target_style_path))
1200 .or_default()
1201 .push(SassModuleGraphClosureStepMetadata::from(edge));
1202 }
1203
1204 let cycle_paths = collect_directed_graph_cycles(&graph);
1205
1206 if test_force_rawallpaths_closure() {
1207 let (closure_paths, _) = collect_hypergraph_transitive_closure_paths_with_mode(
1208 &graph,
1209 &mut |style_path: &String| style_path.clone(),
1210 HypergraphClosureMode::RawAllPaths,
1211 );
1212 let closure_edges = sass_module_graph_closure_edges_from_paths(
1213 closure_paths,
1214 &metadata_by_step,
1215 configuration_resolver,
1216 );
1217 return finalize_sass_module_graph_closure(closure_edges, cycle_paths);
1218 }
1219
1220 let (mut closure_edges, capped) = collect_sass_module_graph_closure_edges_via_worklist(
1221 &graph,
1222 &metadata_by_step,
1223 configuration_resolver,
1224 SASS_MODULE_CLOSURE_STATE_CAP,
1225 );
1226 if capped {
1227 let (closure_paths, _) =
1228 collect_hypergraph_transitive_closure_paths(&graph, |style_path: &String| {
1229 style_path.clone()
1230 });
1231 closure_edges = sass_module_graph_closure_edges_from_paths(
1232 closure_paths,
1233 &metadata_by_step,
1234 configuration_resolver,
1235 );
1236 }
1237 finalize_sass_module_graph_closure(closure_edges, cycle_paths)
1238}
1239
1240fn finalize_sass_module_graph_closure(
1241 mut closure_edges: Vec<SassModuleGraphClosureEdgeV0>,
1242 cycle_paths: Vec<Vec<String>>,
1243) -> (Vec<SassModuleGraphClosureEdgeV0>, Vec<SassModuleCycleV0>) {
1244 let mut cycles = cycle_paths
1245 .into_iter()
1246 .map(|path| SassModuleCycleV0 { path })
1247 .collect::<Vec<_>>();
1248 closure_edges.sort_by_key(|edge| {
1249 (
1250 edge.from_style_path.clone(),
1251 edge.depth,
1252 edge.target_style_path.clone(),
1253 edge.edge_kind,
1254 edge.configuration_signature.clone(),
1255 edge.module_instance_identity_key
1256 .clone()
1257 .unwrap_or_default(),
1258 edge.path.clone(),
1259 )
1260 });
1261 closure_edges.dedup();
1262 cycles.sort_by_key(|cycle| cycle.path.clone());
1263 (closure_edges, cycles)
1264}
1265
1266#[derive(Debug, Clone)]
1267struct SassModuleGraphClosureStepMetadata {
1268 rule_ordinal: usize,
1269 edge_kind: &'static str,
1270 namespace_kind: Option<&'static str>,
1271 namespace: Option<String>,
1272 forward_prefix: Option<String>,
1273 visibility_filter_kind: Option<&'static str>,
1274 visibility_filter_names: Vec<String>,
1275 configuration_signature: String,
1276 configuration_variable_count: usize,
1277 invalid_configuration_variable_names: Vec<String>,
1278 module_instance_identity_key: Option<String>,
1279}
1280
1281impl From<&SassModuleGraphEdgeFactV0> for SassModuleGraphClosureStepMetadata {
1282 fn from(edge: &SassModuleGraphEdgeFactV0) -> Self {
1283 Self {
1284 rule_ordinal: edge.rule_ordinal,
1285 edge_kind: edge.edge_kind,
1286 namespace_kind: edge.namespace_kind,
1287 namespace: edge.namespace.clone(),
1288 forward_prefix: edge.forward_prefix.clone(),
1289 visibility_filter_kind: edge.visibility_filter_kind,
1290 visibility_filter_names: edge.visibility_filter_names.clone(),
1291 configuration_signature: edge.configuration_signature.clone(),
1292 configuration_variable_count: edge.configuration_variable_count,
1293 invalid_configuration_variable_names: edge.invalid_configuration_variable_names.clone(),
1294 module_instance_identity_key: edge.module_instance_identity_key.clone(),
1295 }
1296 }
1297}
1298
1299const SASS_MODULE_CLOSURE_STATE_CAP: usize = 1 << 16;
1300
1301thread_local! {
1302 static FORCE_RAWALLPATHS_CLOSURE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1303}
1304
1305fn test_force_rawallpaths_closure() -> bool {
1306 FORCE_RAWALLPATHS_CLOSURE.with(std::cell::Cell::get)
1307}
1308
1309pub fn with_sass_module_rawallpaths_closure_for_test<R>(body: impl FnOnce() -> R) -> R {
1310 FORCE_RAWALLPATHS_CLOSURE.with(|cell| cell.set(true));
1311 let result = body();
1312 FORCE_RAWALLPATHS_CLOSURE.with(|cell| cell.set(false));
1313 result
1314}
1315
1316fn sass_module_graph_closure_edge(
1317 origin: &str,
1318 target: &str,
1319 depth: usize,
1320 path: Vec<String>,
1321 metadata: SassModuleGraphClosureStepMetadata,
1322) -> SassModuleGraphClosureEdgeV0 {
1323 SassModuleGraphClosureEdgeV0 {
1324 from_style_path: origin.to_string(),
1325 target_style_path: target.to_string(),
1326 edge_kind: metadata.edge_kind,
1327 depth,
1328 path,
1329 namespace_kind: metadata.namespace_kind,
1330 namespace: metadata.namespace,
1331 forward_prefix: metadata.forward_prefix,
1332 visibility_filter_kind: metadata.visibility_filter_kind,
1333 visibility_filter_names: metadata.visibility_filter_names,
1334 configuration_signature: metadata.configuration_signature,
1335 configuration_variable_count: metadata.configuration_variable_count,
1336 invalid_configuration_variable_names: metadata.invalid_configuration_variable_names,
1337 module_instance_identity_key: metadata.module_instance_identity_key,
1338 }
1339}
1340
1341fn sass_module_graph_closure_edges_from_paths(
1342 closure_paths: Vec<HypergraphClosurePath<String>>,
1343 metadata_by_step: &BTreeMap<(String, String), Vec<SassModuleGraphClosureStepMetadata>>,
1344 configuration_resolver: &impl SassModuleGraphConfigurationResolverV0,
1345) -> Vec<SassModuleGraphClosureEdgeV0> {
1346 closure_paths
1347 .into_iter()
1348 .flat_map(
1349 |HypergraphClosurePath {
1350 origin,
1351 target,
1352 depth,
1353 path_labels,
1354 }| {
1355 derive_sass_module_graph_closure_path_metadata(
1356 path_labels.as_slice(),
1357 metadata_by_step,
1358 configuration_resolver,
1359 )
1360 .into_iter()
1361 .map(move |metadata| {
1362 sass_module_graph_closure_edge(
1363 &origin,
1364 &target,
1365 depth,
1366 path_labels.clone(),
1367 metadata,
1368 )
1369 })
1370 .collect::<Vec<_>>()
1371 },
1372 )
1373 .collect()
1374}
1375
1376fn collect_sass_module_graph_closure_edges_via_worklist(
1377 graph: &BTreeMap<String, BTreeSet<String>>,
1378 metadata_by_step: &BTreeMap<(String, String), Vec<SassModuleGraphClosureStepMetadata>>,
1379 configuration_resolver: &impl SassModuleGraphConfigurationResolverV0,
1380 per_origin_state_cap: usize,
1381) -> (Vec<SassModuleGraphClosureEdgeV0>, bool) {
1382 let mut edges = Vec::new();
1383 for origin in graph.keys() {
1384 let mut visited = BTreeSet::<(String, BTreeMap<String, String>)>::new();
1385 let mut pending = VecDeque::<(String, BTreeMap<String, String>, usize, Vec<String>)>::new();
1386 visited.insert((origin.clone(), BTreeMap::new()));
1387 pending.push_back((origin.clone(), BTreeMap::new(), 0, vec![origin.clone()]));
1388 let mut state_count = 0usize;
1389 while let Some((node, inherited_overrides, depth, path)) = pending.pop_front() {
1390 state_count += 1;
1391 if state_count > per_origin_state_cap {
1392 return (edges, true);
1393 }
1394 let Some(targets) = graph.get(node.as_str()) else {
1395 continue;
1396 };
1397 for target in targets {
1398 if path.contains(target) {
1399 continue;
1400 }
1401 let Some(step_metadata) = metadata_by_step.get(&(node.clone(), target.clone()))
1402 else {
1403 continue;
1404 };
1405 for metadata in step_metadata {
1406 let variable_overrides =
1407 derive_sass_module_graph_closure_step_variable_overrides(
1408 node.as_str(),
1409 target.as_str(),
1410 metadata,
1411 &inherited_overrides,
1412 configuration_resolver,
1413 );
1414 let applied = apply_sass_module_graph_closure_step_configuration(
1415 metadata.clone(),
1416 target.as_str(),
1417 variable_overrides.clone(),
1418 configuration_resolver,
1419 );
1420 let mut edge_path = path.clone();
1421 edge_path.push(target.clone());
1422 edges.push(sass_module_graph_closure_edge(
1423 origin,
1424 target,
1425 depth + 1,
1426 edge_path.clone(),
1427 applied,
1428 ));
1429 let next_state = (target.clone(), variable_overrides);
1430 if visited.insert(next_state.clone()) {
1431 pending.push_back((target.clone(), next_state.1, depth + 1, edge_path));
1432 }
1433 }
1434 }
1435 }
1436 }
1437 (edges, false)
1438}
1439
1440fn derive_sass_module_graph_closure_path_metadata(
1441 path_labels: &[String],
1442 metadata_by_step: &BTreeMap<(String, String), Vec<SassModuleGraphClosureStepMetadata>>,
1443 configuration_resolver: &impl SassModuleGraphConfigurationResolverV0,
1444) -> Vec<SassModuleGraphClosureStepMetadata> {
1445 let mut states = vec![(BTreeMap::<String, String>::new(), None)];
1446
1447 for step in path_labels.windows(2) {
1448 let Some(from_style_path) = step.first() else {
1449 return Vec::new();
1450 };
1451 let Some(target_style_path) = step.get(1) else {
1452 return Vec::new();
1453 };
1454 let Some(step_metadata) =
1455 metadata_by_step.get(&(from_style_path.clone(), target_style_path.clone()))
1456 else {
1457 return Vec::new();
1458 };
1459 let mut next_states = Vec::new();
1460 for (inherited_variable_overrides, _) in &states {
1461 for metadata in step_metadata {
1462 let variable_overrides = derive_sass_module_graph_closure_step_variable_overrides(
1463 from_style_path,
1464 target_style_path,
1465 metadata,
1466 inherited_variable_overrides,
1467 configuration_resolver,
1468 );
1469 let applied_metadata = apply_sass_module_graph_closure_step_configuration(
1470 metadata.clone(),
1471 target_style_path,
1472 variable_overrides.clone(),
1473 configuration_resolver,
1474 );
1475 next_states.push((variable_overrides, Some(applied_metadata)));
1476 }
1477 }
1478 states = next_states;
1479 }
1480
1481 states
1482 .into_iter()
1483 .filter_map(|(_, metadata)| metadata)
1484 .collect()
1485}
1486
1487fn derive_sass_module_graph_closure_step_variable_overrides(
1488 from_style_path: &str,
1489 target_style_path: &str,
1490 metadata: &SassModuleGraphClosureStepMetadata,
1491 inherited_variable_overrides: &BTreeMap<String, String>,
1492 configuration_resolver: &impl SassModuleGraphConfigurationResolverV0,
1493) -> BTreeMap<String, String> {
1494 match metadata.edge_kind {
1495 "sassForward" => {
1496 let configurable_names = configuration_resolver.configurable_names(target_style_path);
1497 configuration_resolver.forward_effective_variable_overrides(
1498 SassModuleForwardConfigurationRequestV0 {
1499 from_style_path,
1500 target_style_path,
1501 rule_ordinal: metadata.rule_ordinal,
1502 inherited_variable_overrides,
1503 forward_prefix: metadata.forward_prefix.as_deref(),
1504 visibility_filter_kind: metadata.visibility_filter_kind,
1505 visibility_filter_names: &metadata.visibility_filter_names,
1506 configurable_names: &configurable_names,
1507 },
1508 )
1509 }
1510 "sassUse" => {
1511 configuration_resolver.use_variable_overrides(SassModuleUseConfigurationRequestV0 {
1512 from_style_path,
1513 rule_ordinal: metadata.rule_ordinal,
1514 })
1515 }
1516 _ => BTreeMap::new(),
1517 }
1518}
1519
1520fn apply_sass_module_graph_closure_step_configuration(
1521 mut metadata: SassModuleGraphClosureStepMetadata,
1522 target_style_path: &str,
1523 variable_overrides: BTreeMap<String, String>,
1524 configuration_resolver: &impl SassModuleGraphConfigurationResolverV0,
1525) -> SassModuleGraphClosureStepMetadata {
1526 let configurable_names = configuration_resolver.configurable_names(target_style_path);
1527 metadata.invalid_configuration_variable_names = variable_overrides
1528 .keys()
1529 .filter(|name| !configurable_names.contains(*name))
1530 .cloned()
1531 .collect();
1532 metadata.configuration_signature =
1533 summarize_sass_module_configuration_signature(&variable_overrides);
1534 metadata.configuration_variable_count = variable_overrides.len();
1535 metadata.module_instance_identity_key = Some(summarize_sass_module_instance_identity_key(
1536 target_style_path,
1537 &variable_overrides,
1538 ));
1539 metadata
1540}