Skip to main content

omena_semantic/
css_modules_cross_file.rs

1//! CSS Modules cross-file closure and resolution summaries for semantic consumers.
2
3use std::{
4    cmp::Ordering,
5    collections::{BTreeMap, BTreeSet},
6};
7
8use omena_cross_file_summary::{
9    HypergraphClosurePath, collect_hypergraph_transitive_closure_paths,
10};
11use omena_resolver::{
12    OmenaResolverStylePackageManifestV0, resolve_omena_resolver_style_module_source,
13};
14use omena_syntax::{
15    css_keyword,
16    ident::{CanonicalClassKeyV0, ClassNameV0},
17};
18use serde::Serialize;
19
20fn canonical_class_key(name: &str) -> CanonicalClassKeyV0 {
21    ClassNameV0::new(name).canonical_key()
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
25#[serde(rename_all = "camelCase")]
26pub struct CssModulesCrossFileStyleFactsV0 {
27    pub style_path: String,
28    pub class_selector_names: Vec<String>,
29    pub css_module_value_definition_names: Vec<String>,
30    pub css_module_value_import_edges: Vec<CssModulesValueImportEdgeFactV0>,
31    pub css_module_value_definition_edges: Vec<CssModulesValueDefinitionEdgeFactV0>,
32    pub css_module_composes_edges: Vec<CssModulesComposesEdgeFactV0>,
33    pub icss_export_names: Vec<String>,
34    pub icss_import_edges: Vec<CssModulesIcssImportEdgeFactV0>,
35    pub icss_export_edges: Vec<CssModulesIcssExportEdgeFactV0>,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
39#[serde(rename_all = "camelCase")]
40pub struct CssModulesValueImportEdgeFactV0 {
41    pub remote_name: String,
42    pub local_name: String,
43    pub import_source: String,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
47#[serde(rename_all = "camelCase")]
48pub struct CssModulesValueDefinitionEdgeFactV0 {
49    pub definition_name: String,
50    pub reference_names: Vec<String>,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
54#[serde(rename_all = "camelCase")]
55pub struct CssModulesComposesEdgeFactV0 {
56    pub kind: &'static str,
57    pub owner_selector_names: Vec<String>,
58    pub target_names: Vec<String>,
59    pub import_source: Option<String>,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
63#[serde(rename_all = "camelCase")]
64pub struct CssModulesIcssImportEdgeFactV0 {
65    pub local_name: String,
66    pub remote_name: String,
67    pub import_source: String,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
71#[serde(rename_all = "camelCase")]
72pub struct CssModulesIcssExportEdgeFactV0 {
73    pub export_name: String,
74    pub reference_names: Vec<String>,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
78#[serde(rename_all = "camelCase")]
79pub struct CssModulesCrossFileClosureSummaryV0 {
80    pub schema_version: &'static str,
81    pub product: &'static str,
82    pub status: &'static str,
83    pub style_count: usize,
84    pub composes_closure_edge_count: usize,
85    pub value_closure_edge_count: usize,
86    pub icss_closure_edge_count: usize,
87    pub composes_cycle_count: usize,
88    pub value_cycle_count: usize,
89    pub icss_cycle_count: usize,
90    pub composes_closure_edges: Vec<CssModulesComposesClosureEdgeV0>,
91    pub value_closure_edges: Vec<CssModulesValueClosureEdgeV0>,
92    pub icss_closure_edges: Vec<CssModulesIcssClosureEdgeV0>,
93    pub cycles: Vec<CssModulesCycleV0>,
94    pub capabilities: CssModulesCrossFileClosureCapabilitiesV0,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
98#[serde(rename_all = "camelCase")]
99pub struct CssModulesCrossFileClosureCapabilitiesV0 {
100    pub semantic_layer_owned: bool,
101    pub composes_closure_ready: bool,
102    pub value_graph_closure_ready: bool,
103    pub icss_export_import_closure_ready: bool,
104    pub cycle_detection_ready: bool,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
108#[serde(rename_all = "camelCase")]
109pub struct CssModulesCrossFileResolutionSummaryV0 {
110    pub schema_version: &'static str,
111    pub product: &'static str,
112    pub status: &'static str,
113    pub resolution_scope: &'static str,
114    pub style_count: usize,
115    pub import_edge_count: usize,
116    pub resolved_import_edge_count: usize,
117    pub unresolved_import_edge_count: usize,
118    pub matched_name_count: usize,
119    pub edges: Vec<CssModulesImportEdgeResolutionV0>,
120    pub composes_closure_edge_count: usize,
121    pub value_closure_edge_count: usize,
122    pub icss_closure_edge_count: usize,
123    pub composes_cycle_count: usize,
124    pub value_cycle_count: usize,
125    pub icss_cycle_count: usize,
126    pub composes_closure_edges: Vec<CssModulesComposesClosureEdgeV0>,
127    pub value_closure_edges: Vec<CssModulesValueClosureEdgeV0>,
128    pub icss_closure_edges: Vec<CssModulesIcssClosureEdgeV0>,
129    pub cycles: Vec<CssModulesCycleV0>,
130    pub capabilities: CssModulesCrossFileResolutionCapabilitiesV0,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
134#[serde(rename_all = "camelCase")]
135pub struct CssModulesImportEdgeResolutionV0 {
136    pub from_style_path: String,
137    pub import_kind: &'static str,
138    pub source: String,
139    pub resolved_style_path: Option<String>,
140    pub status: &'static str,
141    pub import_graph_distance: Option<usize>,
142    pub import_graph_order: Option<usize>,
143    pub imported_names: Vec<String>,
144    pub exported_names: Vec<String>,
145    pub matched_names: Vec<String>,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
149#[serde(rename_all = "camelCase")]
150pub struct CssModulesCrossFileResolutionCapabilitiesV0 {
151    pub semantic_layer_owned: bool,
152    pub import_source_resolution_ready: bool,
153    pub composes_name_match_ready: bool,
154    pub value_name_match_ready: bool,
155    pub icss_name_match_ready: bool,
156    pub transitive_closure_ready: bool,
157    pub value_graph_closure_ready: bool,
158    pub icss_export_import_closure_ready: bool,
159    pub cycle_detection_ready: bool,
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
163#[serde(rename_all = "camelCase")]
164pub struct CssModulesComposesClosureEdgeV0 {
165    pub from_style_path: String,
166    pub owner_selector_name: String,
167    pub target_style_path: String,
168    pub target_selector_name: String,
169    pub depth: usize,
170    pub path: Vec<String>,
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
174#[serde(rename_all = "camelCase")]
175pub struct CssModulesValueClosureEdgeV0 {
176    pub from_style_path: String,
177    pub value_name: String,
178    pub target_style_path: String,
179    pub target_value_name: String,
180    pub depth: usize,
181    pub path: Vec<String>,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
185#[serde(rename_all = "camelCase")]
186pub struct CssModulesIcssClosureEdgeV0 {
187    pub from_style_path: String,
188    pub name: String,
189    pub target_style_path: String,
190    pub target_name: String,
191    pub depth: usize,
192    pub path: Vec<String>,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
196#[serde(rename_all = "camelCase")]
197pub struct CssModulesCycleV0 {
198    pub kind: &'static str,
199    pub path: Vec<String>,
200}
201
202pub fn summarize_css_modules_cross_file_closure(
203    style_facts: &[CssModulesCrossFileStyleFactsV0],
204    package_manifests: &[OmenaResolverStylePackageManifestV0],
205) -> CssModulesCrossFileClosureSummaryV0 {
206    let available_style_paths = style_facts
207        .iter()
208        .map(|entry| entry.style_path.as_str())
209        .collect::<BTreeSet<_>>();
210    let facts_by_path = style_facts
211        .iter()
212        .map(|entry| (entry.style_path.as_str(), entry))
213        .collect::<BTreeMap<_, _>>();
214    let (composes_closure_edges, cycles) = summarize_css_modules_composes_closure(
215        &facts_by_path,
216        &available_style_paths,
217        package_manifests,
218    );
219    let (value_closure_edges, value_cycles) = summarize_css_modules_value_closure(
220        &facts_by_path,
221        &available_style_paths,
222        package_manifests,
223    );
224    let (icss_closure_edges, icss_cycles) = summarize_css_modules_icss_closure(
225        &facts_by_path,
226        &available_style_paths,
227        package_manifests,
228    );
229    let composes_cycle_count = cycles.len();
230    let value_cycle_count = value_cycles.len();
231    let icss_cycle_count = icss_cycles.len();
232    let mut cycles = cycles;
233    cycles.extend(value_cycles);
234    cycles.extend(icss_cycles);
235    cycles.sort_by_key(|cycle| (cycle.kind, cycle.path.clone()));
236
237    CssModulesCrossFileClosureSummaryV0 {
238        schema_version: "0",
239        product: "omena-semantic.css-modules-cross-file-closure",
240        status: "semanticLayerOwnedClosure",
241        style_count: style_facts.len(),
242        composes_closure_edge_count: composes_closure_edges.len(),
243        value_closure_edge_count: value_closure_edges.len(),
244        icss_closure_edge_count: icss_closure_edges.len(),
245        composes_cycle_count,
246        value_cycle_count,
247        icss_cycle_count,
248        composes_closure_edges,
249        value_closure_edges,
250        icss_closure_edges,
251        cycles,
252        capabilities: CssModulesCrossFileClosureCapabilitiesV0 {
253            semantic_layer_owned: true,
254            composes_closure_ready: true,
255            value_graph_closure_ready: true,
256            icss_export_import_closure_ready: true,
257            cycle_detection_ready: true,
258        },
259    }
260}
261
262pub fn summarize_css_modules_cross_file_resolution(
263    style_facts: &[CssModulesCrossFileStyleFactsV0],
264    style_import_edges: &[crate::sass_module_graph::StyleImportReachabilityEdgeFactV0],
265    package_manifests: &[OmenaResolverStylePackageManifestV0],
266) -> CssModulesCrossFileResolutionSummaryV0 {
267    let available_style_paths = style_facts
268        .iter()
269        .map(|entry| entry.style_path.as_str())
270        .collect::<BTreeSet<_>>();
271    let facts_by_path = style_facts
272        .iter()
273        .map(|entry| (entry.style_path.as_str(), entry))
274        .collect::<BTreeMap<_, _>>();
275    let mut edges = Vec::new();
276
277    for entry in style_facts {
278        let style_path = entry.style_path.as_str();
279        let reachable =
280            collect_import_reachable_style_path_metadata(style_path, style_import_edges);
281
282        for edge in &entry.css_module_composes_edges {
283            let Some(source) = edge.import_source.as_deref() else {
284                continue;
285            };
286            edges.push(resolve_css_modules_import_edge(
287                style_path,
288                "composes",
289                source,
290                edge.target_names.as_slice(),
291                &available_style_paths,
292                &facts_by_path,
293                &reachable,
294                package_manifests,
295                |target| target.class_selector_names.as_slice(),
296            ));
297        }
298
299        for edge in &entry.css_module_value_import_edges {
300            edges.push(resolve_css_modules_import_edge(
301                style_path,
302                "value",
303                edge.import_source.as_str(),
304                std::slice::from_ref(&edge.remote_name),
305                &available_style_paths,
306                &facts_by_path,
307                &reachable,
308                package_manifests,
309                |target| target.css_module_value_definition_names.as_slice(),
310            ));
311        }
312
313        for edge in &entry.icss_import_edges {
314            edges.push(resolve_css_modules_import_edge(
315                style_path,
316                "icss",
317                edge.import_source.as_str(),
318                std::slice::from_ref(&edge.remote_name),
319                &available_style_paths,
320                &facts_by_path,
321                &reachable,
322                package_manifests,
323                |target| target.icss_export_names.as_slice(),
324            ));
325        }
326    }
327
328    edges.sort_by_key(|edge| {
329        (
330            edge.from_style_path.clone(),
331            edge.import_kind,
332            edge.source.clone(),
333        )
334    });
335    let closure_summary = summarize_css_modules_cross_file_closure(style_facts, package_manifests);
336    let resolved_import_edge_count = edges
337        .iter()
338        .filter(|edge| edge.resolved_style_path.is_some())
339        .count();
340    let matched_name_count = edges
341        .iter()
342        .map(|edge| edge.matched_names.len())
343        .sum::<usize>();
344
345    CssModulesCrossFileResolutionSummaryV0 {
346        schema_version: "0",
347        product: "omena-semantic.css-modules-cross-file-resolution",
348        status: "semanticLayerOwnedResolution",
349        resolution_scope: "batchImportGraph",
350        style_count: style_facts.len(),
351        import_edge_count: edges.len(),
352        resolved_import_edge_count,
353        unresolved_import_edge_count: edges.len() - resolved_import_edge_count,
354        matched_name_count,
355        edges,
356        composes_closure_edge_count: closure_summary.composes_closure_edge_count,
357        value_closure_edge_count: closure_summary.value_closure_edge_count,
358        icss_closure_edge_count: closure_summary.icss_closure_edge_count,
359        composes_cycle_count: closure_summary.composes_cycle_count,
360        value_cycle_count: closure_summary.value_cycle_count,
361        icss_cycle_count: closure_summary.icss_cycle_count,
362        composes_closure_edges: closure_summary.composes_closure_edges,
363        value_closure_edges: closure_summary.value_closure_edges,
364        icss_closure_edges: closure_summary.icss_closure_edges,
365        cycles: closure_summary.cycles,
366        capabilities: CssModulesCrossFileResolutionCapabilitiesV0 {
367            semantic_layer_owned: true,
368            import_source_resolution_ready: true,
369            composes_name_match_ready: true,
370            value_name_match_ready: true,
371            icss_name_match_ready: true,
372            transitive_closure_ready: true,
373            value_graph_closure_ready: true,
374            icss_export_import_closure_ready: true,
375            cycle_detection_ready: true,
376        },
377    }
378}
379
380#[derive(Debug, Clone, Copy, PartialEq, Eq)]
381struct ImportReachability {
382    distance: usize,
383    order: usize,
384}
385
386fn collect_import_reachable_style_path_metadata(
387    target_style_path: &str,
388    style_import_edges: &[crate::sass_module_graph::StyleImportReachabilityEdgeFactV0],
389) -> BTreeMap<String, ImportReachability> {
390    crate::sass_module_graph::summarize_style_import_reachability(
391        target_style_path,
392        style_import_edges,
393    )
394    .reachable_style_paths
395    .into_iter()
396    .map(|fact| {
397        (
398            fact.style_path,
399            ImportReachability {
400                distance: fact.distance,
401                order: fact.order,
402            },
403        )
404    })
405    .collect()
406}
407
408#[allow(clippy::too_many_arguments)]
409fn resolve_css_modules_import_edge(
410    from_style_path: &str,
411    import_kind: &'static str,
412    source: &str,
413    imported_names: &[String],
414    available_style_paths: &BTreeSet<&str>,
415    facts_by_path: &BTreeMap<&str, &CssModulesCrossFileStyleFactsV0>,
416    reachable: &BTreeMap<String, ImportReachability>,
417    package_manifests: &[OmenaResolverStylePackageManifestV0],
418    exported_names_for_kind: fn(&CssModulesCrossFileStyleFactsV0) -> &[String],
419) -> CssModulesImportEdgeResolutionV0 {
420    let resolved_style_path = resolve_omena_resolver_style_module_source(
421        from_style_path,
422        source,
423        available_style_paths,
424        package_manifests,
425    );
426    let reachability = resolved_style_path
427        .as_ref()
428        .and_then(|style_path| reachable.get(style_path));
429    let exported_names = resolved_style_path
430        .as_deref()
431        .and_then(|style_path| facts_by_path.get(style_path))
432        .map(|facts| exported_names_for_kind(facts).to_vec())
433        .unwrap_or_default();
434    let imported_names = sorted_unique_strings(imported_names);
435    let matched_names = if css_keyword(import_kind).equals("composes") {
436        sorted_class_name_intersection(imported_names.as_slice(), exported_names.as_slice())
437    } else {
438        sorted_name_intersection(imported_names.as_slice(), exported_names.as_slice())
439    };
440    let status = if resolved_style_path.is_none() {
441        "unresolvedSource"
442    } else if imported_names.is_empty() {
443        "resolvedSource"
444    } else if matched_names.is_empty() {
445        "resolvedSourceNoNameMatch"
446    } else {
447        "resolved"
448    };
449
450    CssModulesImportEdgeResolutionV0 {
451        from_style_path: from_style_path.to_string(),
452        import_kind,
453        source: source.to_string(),
454        resolved_style_path,
455        status,
456        import_graph_distance: reachability.map(|reachability| reachability.distance),
457        import_graph_order: reachability.map(|reachability| reachability.order),
458        imported_names,
459        exported_names,
460        matched_names,
461    }
462}
463
464fn sorted_unique_strings(values: &[String]) -> Vec<String> {
465    values
466        .iter()
467        .cloned()
468        .collect::<BTreeSet<_>>()
469        .into_iter()
470        .collect()
471}
472
473fn sorted_name_intersection(left: &[String], right: &[String]) -> Vec<String> {
474    let right = right.iter().map(String::as_str).collect::<BTreeSet<_>>();
475    left.iter()
476        .filter(|name| right.contains(name.as_str()))
477        .cloned()
478        .collect::<BTreeSet<_>>()
479        .into_iter()
480        .collect()
481}
482
483fn sorted_class_name_intersection(left: &[String], right: &[String]) -> Vec<String> {
484    let right = right
485        .iter()
486        .map(|name| canonical_class_key(name))
487        .collect::<BTreeSet<_>>();
488    left.iter()
489        .filter(|name| right.contains(&canonical_class_key(name)))
490        .cloned()
491        .collect::<BTreeSet<_>>()
492        .into_iter()
493        .collect()
494}
495
496#[derive(Debug, Clone)]
497struct CssModulesComposesNode {
498    style_path: String,
499    selector_name: String,
500    selector_key: CanonicalClassKeyV0,
501}
502
503impl CssModulesComposesNode {
504    fn new(style_path: impl Into<String>, selector_name: impl Into<String>) -> Self {
505        let selector_name = selector_name.into();
506        Self {
507            style_path: style_path.into(),
508            selector_key: canonical_class_key(&selector_name),
509            selector_name,
510        }
511    }
512}
513
514impl PartialEq for CssModulesComposesNode {
515    fn eq(&self, other: &Self) -> bool {
516        self.style_path == other.style_path && self.selector_key == other.selector_key
517    }
518}
519
520impl Eq for CssModulesComposesNode {}
521
522impl PartialOrd for CssModulesComposesNode {
523    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
524        Some(self.cmp(other))
525    }
526}
527
528impl Ord for CssModulesComposesNode {
529    fn cmp(&self, other: &Self) -> Ordering {
530        (&self.style_path, &self.selector_key).cmp(&(&other.style_path, &other.selector_key))
531    }
532}
533
534fn summarize_css_modules_composes_closure(
535    facts_by_path: &BTreeMap<&str, &CssModulesCrossFileStyleFactsV0>,
536    available_style_paths: &BTreeSet<&str>,
537    package_manifests: &[OmenaResolverStylePackageManifestV0],
538) -> (Vec<CssModulesComposesClosureEdgeV0>, Vec<CssModulesCycleV0>) {
539    let graph = collect_css_modules_composes_adjacency(
540        facts_by_path,
541        available_style_paths,
542        package_manifests,
543    );
544    let (closure_paths, cycle_paths) =
545        collect_hypergraph_transitive_closure_paths(&graph, css_modules_composes_node_label);
546    let mut closure_edges = closure_paths
547        .into_iter()
548        .map(
549            |HypergraphClosurePath {
550                 origin,
551                 target,
552                 depth,
553                 path_labels,
554             }| CssModulesComposesClosureEdgeV0 {
555                from_style_path: origin.style_path,
556                owner_selector_name: origin.selector_name,
557                target_style_path: target.style_path,
558                target_selector_name: target.selector_name,
559                depth,
560                path: path_labels,
561            },
562        )
563        .collect::<Vec<_>>();
564    let mut cycles = cycle_paths
565        .into_iter()
566        .map(|path| CssModulesCycleV0 {
567            kind: "composes",
568            path,
569        })
570        .collect::<Vec<_>>();
571
572    closure_edges.sort_by_key(|edge| {
573        (
574            edge.from_style_path.clone(),
575            edge.owner_selector_name.clone(),
576            edge.depth,
577            edge.target_style_path.clone(),
578            edge.target_selector_name.clone(),
579        )
580    });
581    cycles.sort_by_key(|cycle| cycle.path.clone());
582    (closure_edges, cycles)
583}
584
585fn collect_css_modules_composes_adjacency(
586    facts_by_path: &BTreeMap<&str, &CssModulesCrossFileStyleFactsV0>,
587    available_style_paths: &BTreeSet<&str>,
588    package_manifests: &[OmenaResolverStylePackageManifestV0],
589) -> BTreeMap<CssModulesComposesNode, BTreeSet<CssModulesComposesNode>> {
590    let mut graph = BTreeMap::new();
591    for (style_path, facts) in facts_by_path {
592        let class_names = facts
593            .class_selector_names
594            .iter()
595            .map(|name| canonical_class_key(name))
596            .collect::<BTreeSet<_>>();
597        for edge in &facts.css_module_composes_edges {
598            if edge.kind == "global" {
599                continue;
600            }
601            let target_style_path = if edge.kind == "external" {
602                edge.import_source.as_deref().and_then(|source| {
603                    resolve_omena_resolver_style_module_source(
604                        style_path,
605                        source,
606                        available_style_paths,
607                        package_manifests,
608                    )
609                })
610            } else {
611                Some((*style_path).to_string())
612            };
613            let Some(target_style_path) = target_style_path else {
614                continue;
615            };
616            let target_class_names = if target_style_path == *style_path {
617                class_names.clone()
618            } else {
619                facts_by_path
620                    .get(target_style_path.as_str())
621                    .map(|facts| {
622                        facts
623                            .class_selector_names
624                            .iter()
625                            .map(|name| canonical_class_key(name))
626                            .collect::<BTreeSet<_>>()
627                    })
628                    .unwrap_or_default()
629            };
630            for owner_selector_name in &edge.owner_selector_names {
631                if !class_names.contains(&canonical_class_key(owner_selector_name)) {
632                    continue;
633                }
634                let owner =
635                    CssModulesComposesNode::new((*style_path).to_string(), owner_selector_name);
636                for target_selector_name in &edge.target_names {
637                    if !target_class_names.contains(&canonical_class_key(target_selector_name)) {
638                        continue;
639                    }
640                    graph
641                        .entry(owner.clone())
642                        .or_insert_with(BTreeSet::new)
643                        .insert(CssModulesComposesNode::new(
644                            target_style_path.clone(),
645                            target_selector_name,
646                        ));
647                }
648            }
649        }
650    }
651    graph
652}
653
654fn css_modules_composes_node_label(node: &CssModulesComposesNode) -> String {
655    format!("{}#{}", node.style_path, node.selector_name)
656}
657
658#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
659struct CssModulesValueNode {
660    style_path: String,
661    value_name: String,
662}
663
664fn summarize_css_modules_value_closure(
665    facts_by_path: &BTreeMap<&str, &CssModulesCrossFileStyleFactsV0>,
666    available_style_paths: &BTreeSet<&str>,
667    package_manifests: &[OmenaResolverStylePackageManifestV0],
668) -> (Vec<CssModulesValueClosureEdgeV0>, Vec<CssModulesCycleV0>) {
669    let graph = collect_css_modules_value_adjacency(
670        facts_by_path,
671        available_style_paths,
672        package_manifests,
673    );
674    let (closure_paths, cycle_paths) =
675        collect_hypergraph_transitive_closure_paths(&graph, css_modules_value_node_label);
676    let mut closure_edges = closure_paths
677        .into_iter()
678        .map(
679            |HypergraphClosurePath {
680                 origin,
681                 target,
682                 depth,
683                 path_labels,
684             }| CssModulesValueClosureEdgeV0 {
685                from_style_path: origin.style_path,
686                value_name: origin.value_name,
687                target_style_path: target.style_path,
688                target_value_name: target.value_name,
689                depth,
690                path: path_labels,
691            },
692        )
693        .collect::<Vec<_>>();
694    let mut cycles = cycle_paths
695        .into_iter()
696        .map(|path| CssModulesCycleV0 {
697            kind: "value",
698            path,
699        })
700        .collect::<Vec<_>>();
701
702    closure_edges.sort_by_key(|edge| {
703        (
704            edge.from_style_path.clone(),
705            edge.value_name.clone(),
706            edge.depth,
707            edge.target_style_path.clone(),
708            edge.target_value_name.clone(),
709        )
710    });
711    cycles.sort_by_key(|cycle| cycle.path.clone());
712    (closure_edges, cycles)
713}
714
715fn collect_css_modules_value_adjacency(
716    facts_by_path: &BTreeMap<&str, &CssModulesCrossFileStyleFactsV0>,
717    available_style_paths: &BTreeSet<&str>,
718    package_manifests: &[OmenaResolverStylePackageManifestV0],
719) -> BTreeMap<CssModulesValueNode, BTreeSet<CssModulesValueNode>> {
720    let mut graph = BTreeMap::new();
721    for (style_path, facts) in facts_by_path {
722        let local_value_names = facts
723            .css_module_value_definition_names
724            .iter()
725            .chain(
726                facts
727                    .css_module_value_import_edges
728                    .iter()
729                    .map(|edge| &edge.local_name),
730            )
731            .map(String::as_str)
732            .collect::<BTreeSet<_>>();
733        for edge in &facts.css_module_value_definition_edges {
734            if !local_value_names.contains(edge.definition_name.as_str()) {
735                continue;
736            }
737            let owner = CssModulesValueNode {
738                style_path: (*style_path).to_string(),
739                value_name: edge.definition_name.clone(),
740            };
741            for reference_name in &edge.reference_names {
742                if !local_value_names.contains(reference_name.as_str()) {
743                    continue;
744                }
745                graph
746                    .entry(owner.clone())
747                    .or_insert_with(BTreeSet::new)
748                    .insert(CssModulesValueNode {
749                        style_path: (*style_path).to_string(),
750                        value_name: reference_name.clone(),
751                    });
752            }
753        }
754
755        for edge in &facts.css_module_value_import_edges {
756            let Some(target_style_path) = resolve_omena_resolver_style_module_source(
757                style_path,
758                edge.import_source.as_str(),
759                available_style_paths,
760                package_manifests,
761            ) else {
762                continue;
763            };
764            let Some(target_facts) = facts_by_path.get(target_style_path.as_str()) else {
765                continue;
766            };
767            if !target_facts
768                .css_module_value_definition_names
769                .iter()
770                .any(|name| name == &edge.remote_name)
771            {
772                continue;
773            }
774            graph
775                .entry(CssModulesValueNode {
776                    style_path: (*style_path).to_string(),
777                    value_name: edge.local_name.clone(),
778                })
779                .or_insert_with(BTreeSet::new)
780                .insert(CssModulesValueNode {
781                    style_path: target_style_path,
782                    value_name: edge.remote_name.clone(),
783                });
784        }
785    }
786    graph
787}
788
789fn css_modules_value_node_label(node: &CssModulesValueNode) -> String {
790    format!("{}#{}", node.style_path, node.value_name)
791}
792
793#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
794struct CssModulesIcssNode {
795    style_path: String,
796    name: String,
797}
798
799fn summarize_css_modules_icss_closure(
800    facts_by_path: &BTreeMap<&str, &CssModulesCrossFileStyleFactsV0>,
801    available_style_paths: &BTreeSet<&str>,
802    package_manifests: &[OmenaResolverStylePackageManifestV0],
803) -> (Vec<CssModulesIcssClosureEdgeV0>, Vec<CssModulesCycleV0>) {
804    let graph =
805        collect_css_modules_icss_adjacency(facts_by_path, available_style_paths, package_manifests);
806    let (closure_paths, cycle_paths) =
807        collect_hypergraph_transitive_closure_paths(&graph, css_modules_icss_node_label);
808    let mut closure_edges = closure_paths
809        .into_iter()
810        .map(
811            |HypergraphClosurePath {
812                 origin,
813                 target,
814                 depth,
815                 path_labels,
816             }| CssModulesIcssClosureEdgeV0 {
817                from_style_path: origin.style_path,
818                name: origin.name,
819                target_style_path: target.style_path,
820                target_name: target.name,
821                depth,
822                path: path_labels,
823            },
824        )
825        .collect::<Vec<_>>();
826    let mut cycles = cycle_paths
827        .into_iter()
828        .map(|path| CssModulesCycleV0 { kind: "icss", path })
829        .collect::<Vec<_>>();
830
831    closure_edges.sort_by_key(|edge| {
832        (
833            edge.from_style_path.clone(),
834            edge.name.clone(),
835            edge.depth,
836            edge.target_style_path.clone(),
837            edge.target_name.clone(),
838        )
839    });
840    cycles.sort_by_key(|cycle| cycle.path.clone());
841    (closure_edges, cycles)
842}
843
844fn collect_css_modules_icss_adjacency(
845    facts_by_path: &BTreeMap<&str, &CssModulesCrossFileStyleFactsV0>,
846    available_style_paths: &BTreeSet<&str>,
847    package_manifests: &[OmenaResolverStylePackageManifestV0],
848) -> BTreeMap<CssModulesIcssNode, BTreeSet<CssModulesIcssNode>> {
849    let mut graph = BTreeMap::new();
850    for (style_path, facts) in facts_by_path {
851        let local_names = facts
852            .icss_export_names
853            .iter()
854            .chain(facts.icss_import_edges.iter().map(|edge| &edge.local_name))
855            .map(String::as_str)
856            .collect::<BTreeSet<_>>();
857        for edge in &facts.icss_export_edges {
858            if !local_names.contains(edge.export_name.as_str()) {
859                continue;
860            }
861            let owner = CssModulesIcssNode {
862                style_path: (*style_path).to_string(),
863                name: edge.export_name.clone(),
864            };
865            for reference_name in &edge.reference_names {
866                if !local_names.contains(reference_name.as_str()) {
867                    continue;
868                }
869                graph
870                    .entry(owner.clone())
871                    .or_insert_with(BTreeSet::new)
872                    .insert(CssModulesIcssNode {
873                        style_path: (*style_path).to_string(),
874                        name: reference_name.clone(),
875                    });
876            }
877        }
878
879        for edge in &facts.icss_import_edges {
880            let Some(target_style_path) = resolve_omena_resolver_style_module_source(
881                style_path,
882                edge.import_source.as_str(),
883                available_style_paths,
884                package_manifests,
885            ) else {
886                continue;
887            };
888            let Some(target_facts) = facts_by_path.get(target_style_path.as_str()) else {
889                continue;
890            };
891            if !target_facts
892                .icss_export_names
893                .iter()
894                .any(|name| name == &edge.remote_name)
895            {
896                continue;
897            }
898            graph
899                .entry(CssModulesIcssNode {
900                    style_path: (*style_path).to_string(),
901                    name: edge.local_name.clone(),
902                })
903                .or_insert_with(BTreeSet::new)
904                .insert(CssModulesIcssNode {
905                    style_path: target_style_path,
906                    name: edge.remote_name.clone(),
907                });
908        }
909    }
910    graph
911}
912
913fn css_modules_icss_node_label(node: &CssModulesIcssNode) -> String {
914    format!("{}#{}", node.style_path, node.name)
915}