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