Skip to main content

omena_parser/facts/
mod.rs

1//! Aggregated parser fact surface.
2//!
3//! This module re-exports the syntax-derived fact records that are safe for
4//! query, bridge, LSP, and transform consumers to share.
5
6mod animations;
7mod at_rules;
8mod css_modules;
9mod icss;
10mod sass;
11mod selectors;
12mod variables;
13
14use cstree::syntax::SyntaxNode;
15use omena_syntax::{StyleDialect, SyntaxKind};
16
17use crate::{DialectExtension, ParseResult, Parser, Token, tokenize};
18
19pub(crate) use animations::collect_animation_facts_from_cst;
20pub use animations::{ParsedAnimationFact, ParsedAnimationFactKind};
21pub use at_rules::ParsedAtRuleFact;
22pub(crate) use at_rules::collect_at_rule_facts_from_cst;
23pub use css_modules::{
24    ParsedCssModuleComposesEdgeFact, ParsedCssModuleComposesEdgeKind, ParsedCssModuleComposesFact,
25    ParsedCssModuleComposesFactKind, ParsedCssModuleValueDefinitionEdgeFact,
26    ParsedCssModuleValueFact, ParsedCssModuleValueFactKind, ParsedCssModuleValueImportEdgeFact,
27};
28pub(crate) use css_modules::{
29    collect_css_module_composes_edge_facts_from_cst, collect_css_module_composes_facts_from_cst,
30    collect_css_module_value_definition_edge_facts_from_cst,
31    collect_css_module_value_definition_edge_names, collect_css_module_value_facts_from_cst,
32    collect_css_module_value_import_edge_facts_from_cst,
33    css_module_value_reference_token_can_be_name, css_module_value_source_name,
34    css_module_value_statement_end, declaration_colon_index,
35};
36pub use icss::{
37    ParsedIcssExportEdgeFact, ParsedIcssFact, ParsedIcssFactKind, ParsedIcssImportEdgeFact,
38    collect_icss_export_values_from_cst,
39};
40pub(crate) use icss::{
41    collect_icss_export_edge_facts_from_cst, collect_icss_facts_from_cst,
42    collect_icss_import_edge_facts_from_cst,
43};
44pub use sass::{
45    ParsedExtendTargetFact, ParsedExtendTargetFactKind, ParsedSassCallableParameterFact,
46    ParsedSassCallableSignatureFact, ParsedSassIncludeFact, ParsedSassModuleEdgeFact,
47    ParsedSassModuleEdgeFactKind, ParsedSassPlaceholderDefinitionFact, ParsedSassSymbolFact,
48    ParsedSassSymbolFactKind,
49};
50pub(crate) use sass::{
51    collect_extend_target_facts_from_cst, collect_sass_include_facts_from_cst,
52    collect_sass_module_edge_facts_from_cst, collect_sass_placeholder_definition_facts_from_cst,
53    collect_sass_symbol_facts_from_cst,
54};
55pub use selectors::{ParsedSelectorFact, ParsedSelectorFactKind};
56pub(crate) use selectors::{
57    SelectorBranch, collect_class_selector_names_from_header, collect_selector_facts_from_cst,
58    css_module_block_scope_marker_in_header, css_module_header_is_global_only,
59    resolve_selector_header, split_selector_groups,
60};
61pub use variables::{ParsedVariableFact, ParsedVariableFactKind};
62pub(crate) use variables::{collect_variable_facts_from_cst, scss_variable_token_is_declaration};
63
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct ParsedStyleFacts {
66    pub product: &'static str,
67    pub dialect: StyleDialect,
68    pub selector_count: usize,
69    pub selectors: Vec<ParsedSelectorFact>,
70    pub variable_count: usize,
71    pub variables: Vec<ParsedVariableFact>,
72    pub sass_symbol_count: usize,
73    pub sass_symbols: Vec<ParsedSassSymbolFact>,
74    pub sass_include_count: usize,
75    pub sass_includes: Vec<ParsedSassIncludeFact>,
76    pub sass_module_edge_count: usize,
77    pub sass_module_edges: Vec<ParsedSassModuleEdgeFact>,
78    pub sass_placeholder_definition_count: usize,
79    pub sass_placeholder_definitions: Vec<ParsedSassPlaceholderDefinitionFact>,
80    pub extend_target_count: usize,
81    pub extend_targets: Vec<ParsedExtendTargetFact>,
82    pub animation_count: usize,
83    pub animations: Vec<ParsedAnimationFact>,
84    pub css_module_value_count: usize,
85    pub css_module_values: Vec<ParsedCssModuleValueFact>,
86    pub css_module_value_import_edge_count: usize,
87    pub css_module_value_import_edges: Vec<ParsedCssModuleValueImportEdgeFact>,
88    pub css_module_value_definition_edge_count: usize,
89    pub css_module_value_definition_edges: Vec<ParsedCssModuleValueDefinitionEdgeFact>,
90    pub css_module_composes_count: usize,
91    pub css_module_composes: Vec<ParsedCssModuleComposesFact>,
92    pub css_module_composes_edge_count: usize,
93    pub css_module_composes_edges: Vec<ParsedCssModuleComposesEdgeFact>,
94    pub icss_count: usize,
95    pub icss: Vec<ParsedIcssFact>,
96    pub icss_import_edge_count: usize,
97    pub icss_import_edges: Vec<ParsedIcssImportEdgeFact>,
98    pub icss_export_edge_count: usize,
99    pub icss_export_edges: Vec<ParsedIcssExportEdgeFact>,
100    pub at_rule_count: usize,
101    pub at_rules: Vec<ParsedAtRuleFact>,
102    pub error_count: usize,
103}
104
105struct ProductFacts(ParsedStyleFacts);
106
107impl From<ParsedStyleFacts> for ProductFacts {
108    fn from(mut projected: ParsedStyleFacts) -> Self {
109        if !matches!(projected.dialect, StyleDialect::Scss | StyleDialect::Sass) {
110            clear_fact_category(
111                &mut projected.sass_symbol_count,
112                &mut projected.sass_symbols,
113            );
114            clear_fact_category(
115                &mut projected.sass_module_edge_count,
116                &mut projected.sass_module_edges,
117            );
118            clear_fact_category(
119                &mut projected.sass_placeholder_definition_count,
120                &mut projected.sass_placeholder_definitions,
121            );
122        }
123
124        clear_fact_category(
125            &mut projected.sass_include_count,
126            &mut projected.sass_includes,
127        );
128        clear_fact_category(
129            &mut projected.extend_target_count,
130            &mut projected.extend_targets,
131        );
132        clear_fact_category(&mut projected.icss_count, &mut projected.icss);
133        clear_fact_category(
134            &mut projected.icss_import_edge_count,
135            &mut projected.icss_import_edges,
136        );
137        clear_fact_category(
138            &mut projected.icss_export_edge_count,
139            &mut projected.icss_export_edges,
140        );
141        clear_fact_category(&mut projected.at_rule_count, &mut projected.at_rules);
142
143        Self(projected)
144    }
145}
146
147impl From<ProductFacts> for ParsedStyleFacts {
148    fn from(facts: ProductFacts) -> Self {
149        facts.0
150    }
151}
152
153fn clear_fact_category<T>(count: &mut usize, facts: &mut Vec<T>) {
154    *count = 0;
155    facts.clear();
156}
157
158pub fn collect_style_facts_with_extension(
159    text: &str,
160    extension: &impl DialectExtension,
161) -> ParsedStyleFacts {
162    let (tokens, lex_errors) = tokenize(text, extension);
163    let token_count = tokens.len();
164    let mut parser = Parser::new(tokens.clone(), lex_errors, extension.dialect());
165    crate::record_omena_parser_parse_materialization(token_count);
166    let (green, interner) = parser.parse();
167    let errors = parser.into_errors();
168    let parsed = ParseResult::new(green, interner, errors, token_count, extension.dialect());
169    facts_from_cst(text, &parsed)
170}
171
172pub fn facts_from_cst(text: &str, parsed: &ParseResult) -> ParsedStyleFacts {
173    let selectors = collect_selector_facts_from_cst(text, parsed);
174    let variables = collect_variable_facts_from_cst(text, parsed);
175    let sass_symbols = collect_sass_symbol_facts_from_cst(text, parsed);
176    let sass_includes = collect_sass_include_facts_from_cst(text, parsed);
177    let sass_module_edges = collect_sass_module_edge_facts_from_cst(text, parsed);
178    let has_placeholder_definition = selectors
179        .iter()
180        .any(|selector| selector.kind == ParsedSelectorFactKind::Placeholder);
181    let sass_placeholder_definitions = if has_placeholder_definition {
182        collect_sass_placeholder_definition_facts_from_cst(text, parsed)
183    } else {
184        Vec::new()
185    };
186    let extend_targets = collect_extend_target_facts_from_cst(text, parsed);
187    let animations = collect_animation_facts_from_cst(text, parsed);
188    let css_module_values = collect_css_module_value_facts_from_cst(text, parsed);
189    let css_module_value_import_edges =
190        collect_css_module_value_import_edge_facts_from_cst(text, parsed);
191    let css_module_value_definition_edges =
192        collect_css_module_value_definition_edge_facts_from_cst(text, parsed);
193    let css_module_composes = collect_css_module_composes_facts_from_cst(text, parsed);
194    let css_module_composes_edges = collect_css_module_composes_edge_facts_from_cst(text, parsed);
195    let icss = collect_icss_facts_from_cst(text, parsed);
196    let icss_import_edges = collect_icss_import_edge_facts_from_cst(text, parsed);
197    let icss_export_edges = collect_icss_export_edge_facts_from_cst(text, parsed);
198    let at_rules = collect_at_rule_facts_from_cst(text, parsed);
199
200    ParsedStyleFacts {
201        product: "omena-parser.style-facts",
202        dialect: parsed.dialect(),
203        selector_count: selectors.len(),
204        selectors,
205        variable_count: variables.len(),
206        variables,
207        sass_symbol_count: sass_symbols.len(),
208        sass_symbols,
209        sass_include_count: sass_includes.len(),
210        sass_includes,
211        sass_module_edge_count: sass_module_edges.len(),
212        sass_module_edges,
213        sass_placeholder_definition_count: sass_placeholder_definitions.len(),
214        sass_placeholder_definitions,
215        extend_target_count: extend_targets.len(),
216        extend_targets,
217        animation_count: animations.len(),
218        animations,
219        css_module_value_count: css_module_values.len(),
220        css_module_values,
221        css_module_value_import_edge_count: css_module_value_import_edges.len(),
222        css_module_value_import_edges,
223        css_module_value_definition_edge_count: css_module_value_definition_edges.len(),
224        css_module_value_definition_edges,
225        css_module_composes_count: css_module_composes.len(),
226        css_module_composes,
227        css_module_composes_edge_count: css_module_composes_edges.len(),
228        css_module_composes_edges,
229        icss_count: icss.len(),
230        icss,
231        icss_import_edge_count: icss_import_edges.len(),
232        icss_import_edges,
233        icss_export_edge_count: icss_export_edges.len(),
234        icss_export_edges,
235        at_rule_count: at_rules.len(),
236        at_rules,
237        error_count: parsed.errors().len(),
238    }
239}
240
241pub(crate) fn product_facts_from_cst(text: &str, parsed: &ParseResult) -> ParsedStyleFacts {
242    ProductFacts::from(facts_from_cst(text, parsed)).into()
243}
244
245#[cfg(test)]
246mod product_facts_authority_tests;
247
248pub(crate) fn tokens_from_syntax_node<'text>(
249    text: &'text str,
250    parsed: &ParseResult,
251    node: &SyntaxNode<SyntaxKind>,
252) -> Vec<Token<'text>> {
253    let node_range = node.text_range();
254    let tokens = parsed.syntax_token_views();
255    let start_index = tokens.partition_point(|token| token.range.start() < node_range.start());
256    let end_index = tokens[start_index..]
257        .partition_point(|token| token.range.start() < node_range.end())
258        + start_index;
259    tokens[start_index..end_index]
260        .iter()
261        .filter(|token| token.range.end() <= node_range.end())
262        .map(|token| {
263            let range = token.range;
264            let start = u32::from(range.start()) as usize;
265            let end = u32::from(range.end()) as usize;
266            Token {
267                kind: token.kind,
268                text: text.get(start..end).unwrap_or_default(),
269                range,
270            }
271        })
272        .collect()
273}
274
275pub(crate) fn syntax_node_is_top_level(node: &SyntaxNode<SyntaxKind>) -> bool {
276    node.parent().is_some_and(|parent| {
277        matches!(
278            parent.kind(),
279            SyntaxKind::Stylesheet | SyntaxKind::ScssStylesheet | SyntaxKind::LessStylesheet
280        )
281    })
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287    use crate::{StyleDialect, parse};
288
289    fn tokens_from_syntax_node_linear<'text>(
290        text: &'text str,
291        parsed: &ParseResult,
292        node: &SyntaxNode<SyntaxKind>,
293    ) -> Vec<Token<'text>> {
294        let node_range = node.text_range();
295        parsed
296            .syntax_token_views()
297            .iter()
298            .filter(|token| token.range.start() >= node_range.start())
299            .filter(|token| token.range.end() <= node_range.end())
300            .map(|token| {
301                let range = token.range;
302                let start = u32::from(range.start()) as usize;
303                let end = u32::from(range.end()) as usize;
304                Token {
305                    kind: token.kind,
306                    text: text.get(start..end).unwrap_or_default(),
307                    range,
308                }
309            })
310            .collect()
311    }
312
313    #[test]
314    fn tokens_from_syntax_node_matches_linear_scan_order() {
315        let text = r#"@use "./tokens" as t;
316:export { exported: local; }
317.button, :global(.card) {
318  --gap: 1rem;
319  color: var(--brand);
320  &__icon { composes: icon from "./icons.module.css"; }
321}
322@media (width >= 1px) {
323  .button--primary { color: t.$brand; }
324}"#;
325        let parsed = parse(text, StyleDialect::Scss);
326        let syntax = parsed.syntax();
327
328        for node in syntax.descendants() {
329            assert_eq!(
330                tokens_from_syntax_node(text, &parsed, node),
331                tokens_from_syntax_node_linear(text, &parsed, node),
332                "token slice drift for {:?} at {:?}",
333                node.kind(),
334                node.text_range()
335            );
336        }
337    }
338}