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