Skip to main content

omena_semantic/
css_modules.rs

1//! CSS Modules semantic summaries over parser facts.
2//!
3//! The public V0 payloads in this module describe class definitions,
4//! compositions, exported values, and capability flags that downstream query and
5//! checker layers use without reinterpreting parser-specific fact shapes.
6
7use std::collections::BTreeSet;
8
9use omena_parser::{
10    ParsedAnimationFactKind, ParsedCssModuleComposesEdgeKind, ParsedCssModuleComposesFactKind,
11    ParsedCssModuleValueFactKind, ParsedIcssFactKind, ParsedSelectorFactKind, StyleDialect,
12    collect_style_facts,
13};
14use serde::Serialize;
15
16use crate::Stylesheet;
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
19#[serde(rename_all = "camelCase")]
20pub struct CssModulesSemanticSummaryV0 {
21    pub schema_version: &'static str,
22    pub product: &'static str,
23    pub status: &'static str,
24    pub resolution_scope: &'static str,
25    pub class_export_count: usize,
26    pub class_export_names: Vec<String>,
27    pub composes_edge_seed_count: usize,
28    pub composes_local_edge_count: usize,
29    pub composes_global_edge_count: usize,
30    pub composes_external_edge_count: usize,
31    pub composes_target_names: Vec<String>,
32    pub composes_import_sources: Vec<String>,
33    pub value_edge_seed_count: usize,
34    pub value_import_edge_count: usize,
35    pub value_definition_edge_count: usize,
36    pub value_definition_names: Vec<String>,
37    pub value_reference_names: Vec<String>,
38    pub value_import_sources: Vec<String>,
39    pub icss_edge_seed_count: usize,
40    pub icss_import_edge_count: usize,
41    pub icss_export_edge_count: usize,
42    pub icss_export_names: Vec<String>,
43    pub icss_import_local_names: Vec<String>,
44    pub icss_import_remote_names: Vec<String>,
45    pub icss_import_sources: Vec<String>,
46    pub keyframe_names: Vec<String>,
47    pub animation_reference_names: Vec<String>,
48    pub capabilities: CssModulesSemanticCapabilitiesV0,
49    pub next_priorities: Vec<&'static str>,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
53#[serde(rename_all = "camelCase")]
54pub struct CssModulesSemanticCapabilitiesV0 {
55    pub parser_fact_surface_ready: bool,
56    pub per_file_symbol_summary_ready: bool,
57    pub composes_edge_seed_ready: bool,
58    pub value_edge_seed_ready: bool,
59    pub icss_edge_seed_ready: bool,
60    pub animation_edge_seed_ready: bool,
61    pub cross_file_resolution_ready: bool,
62    pub composes_closure_ready: bool,
63    pub value_graph_resolution_ready: bool,
64    pub cycle_detection_ready: bool,
65}
66
67pub fn summarize_css_modules_semantics(sheet: &Stylesheet) -> CssModulesSemanticSummaryV0 {
68    summarize_css_modules_semantics_for_source(sheet.source.as_str(), sheet.language)
69}
70
71pub fn summarize_css_modules_semantics_from_source(
72    style_path: &str,
73    style_source: &str,
74) -> Option<CssModulesSemanticSummaryV0> {
75    let dialect = dialect_for_style_path(style_path)?;
76    Some(summarize_css_modules_semantics_for_source(
77        style_source,
78        dialect,
79    ))
80}
81
82fn summarize_css_modules_semantics_for_source(
83    style_source: &str,
84    dialect: StyleDialect,
85) -> CssModulesSemanticSummaryV0 {
86    let facts = collect_style_facts(style_source, dialect);
87    let mut class_export_names = BTreeSet::new();
88    let mut composes_target_names = BTreeSet::new();
89    let mut composes_import_sources = BTreeSet::new();
90    let mut composes_local_edge_count = 0usize;
91    let mut composes_global_edge_count = 0usize;
92    let mut composes_external_edge_count = 0usize;
93    let mut value_definition_names = BTreeSet::new();
94    let mut value_reference_names = BTreeSet::new();
95    let mut value_import_sources = BTreeSet::new();
96    let mut icss_export_names = BTreeSet::new();
97    let mut icss_import_local_names = BTreeSet::new();
98    let mut icss_import_remote_names = BTreeSet::new();
99    let mut icss_import_sources = BTreeSet::new();
100    let mut keyframe_names = BTreeSet::new();
101    let mut animation_reference_names = BTreeSet::new();
102
103    for selector in facts.selectors {
104        if selector.kind == ParsedSelectorFactKind::Class {
105            class_export_names.insert(selector.name);
106        }
107    }
108
109    for composes in facts.css_module_composes {
110        match composes.kind {
111            ParsedCssModuleComposesFactKind::Target => {
112                composes_target_names.insert(composes.name);
113            }
114            ParsedCssModuleComposesFactKind::ImportSource => {
115                composes_import_sources.insert(composes.name);
116            }
117        }
118    }
119    for edge in facts.css_module_composes_edges {
120        match edge.kind {
121            ParsedCssModuleComposesEdgeKind::Local => composes_local_edge_count += 1,
122            ParsedCssModuleComposesEdgeKind::Global => composes_global_edge_count += 1,
123            ParsedCssModuleComposesEdgeKind::External => composes_external_edge_count += 1,
124        }
125    }
126
127    for value in facts.css_module_values {
128        match value.kind {
129            ParsedCssModuleValueFactKind::Definition => {
130                value_definition_names.insert(value.name);
131            }
132            ParsedCssModuleValueFactKind::Reference => {
133                value_reference_names.insert(value.name);
134            }
135            ParsedCssModuleValueFactKind::ImportSource => {
136                value_import_sources.insert(value.name);
137            }
138        }
139    }
140
141    for icss in facts.icss {
142        match icss.kind {
143            ParsedIcssFactKind::ExportName => {
144                icss_export_names.insert(icss.name);
145            }
146            ParsedIcssFactKind::ImportLocalName => {
147                icss_import_local_names.insert(icss.name);
148            }
149            ParsedIcssFactKind::ImportRemoteName => {
150                icss_import_remote_names.insert(icss.name);
151            }
152            ParsedIcssFactKind::ImportSource => {
153                icss_import_sources.insert(icss.name);
154            }
155        }
156    }
157
158    for animation in facts.animations {
159        match animation.kind {
160            ParsedAnimationFactKind::KeyframesDeclaration => {
161                keyframe_names.insert(animation.name);
162            }
163            ParsedAnimationFactKind::AnimationNameReference => {
164                animation_reference_names.insert(animation.name);
165            }
166        }
167    }
168
169    let class_export_names: Vec<_> = class_export_names.into_iter().collect();
170    let composes_target_names: Vec<_> = composes_target_names.into_iter().collect();
171    let composes_import_sources: Vec<_> = composes_import_sources.into_iter().collect();
172    let value_definition_names: Vec<_> = value_definition_names.into_iter().collect();
173    let value_reference_names: Vec<_> = value_reference_names.into_iter().collect();
174    let value_import_sources: Vec<_> = value_import_sources.into_iter().collect();
175    let icss_export_names: Vec<_> = icss_export_names.into_iter().collect();
176    let icss_import_local_names: Vec<_> = icss_import_local_names.into_iter().collect();
177    let icss_import_remote_names: Vec<_> = icss_import_remote_names.into_iter().collect();
178    let icss_import_sources: Vec<_> = icss_import_sources.into_iter().collect();
179    let keyframe_names: Vec<_> = keyframe_names.into_iter().collect();
180    let animation_reference_names: Vec<_> = animation_reference_names.into_iter().collect();
181
182    CssModulesSemanticSummaryV0 {
183        schema_version: "0",
184        product: "omena-semantic.css-modules-semantics",
185        status: "parserFactSeed",
186        resolution_scope: "perFileFactSummary",
187        class_export_count: class_export_names.len(),
188        class_export_names,
189        composes_edge_seed_count: composes_local_edge_count
190            + composes_global_edge_count
191            + composes_external_edge_count,
192        composes_local_edge_count,
193        composes_global_edge_count,
194        composes_external_edge_count,
195        composes_target_names,
196        composes_import_sources,
197        value_edge_seed_count: facts.css_module_value_import_edge_count
198            + facts.css_module_value_definition_edge_count,
199        value_import_edge_count: facts.css_module_value_import_edge_count,
200        value_definition_edge_count: facts.css_module_value_definition_edge_count,
201        value_definition_names,
202        value_reference_names,
203        value_import_sources,
204        icss_edge_seed_count: facts.icss_import_edge_count + facts.icss_export_edge_count,
205        icss_import_edge_count: facts.icss_import_edge_count,
206        icss_export_edge_count: facts.icss_export_edge_count,
207        icss_export_names,
208        icss_import_local_names,
209        icss_import_remote_names,
210        icss_import_sources,
211        keyframe_names,
212        animation_reference_names,
213        capabilities: CssModulesSemanticCapabilitiesV0 {
214            parser_fact_surface_ready: true,
215            per_file_symbol_summary_ready: true,
216            composes_edge_seed_ready: true,
217            value_edge_seed_ready: true,
218            icss_edge_seed_ready: true,
219            animation_edge_seed_ready: true,
220            cross_file_resolution_ready: false,
221            composes_closure_ready: false,
222            value_graph_resolution_ready: false,
223            cycle_detection_ready: false,
224        },
225        next_priorities: vec![
226            "crossFileComposesResolution",
227            "cssModulesValueGraphResolution",
228            "icssImportExportResolution",
229            "cycleDetection",
230        ],
231    }
232}
233
234fn dialect_for_style_path(style_path: &str) -> Option<StyleDialect> {
235    if style_path.ends_with(".module.css") || style_path.ends_with(".css") {
236        Some(StyleDialect::Css)
237    } else if style_path.ends_with(".module.scss") || style_path.ends_with(".scss") {
238        Some(StyleDialect::Scss)
239    } else if style_path.ends_with(".module.sass") || style_path.ends_with(".sass") {
240        Some(StyleDialect::Sass)
241    } else if style_path.ends_with(".module.less") || style_path.ends_with(".less") {
242        Some(StyleDialect::Less)
243    } else {
244        None
245    }
246}