Skip to main content

omena_semantic/
sass_module_graph.rs

1//! Sass module graph closure, configuration, and visibility helpers for semantic consumers.
2
3use 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}