1use 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}