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
15#[cfg(test)]
16use cstree::syntax::SyntaxNode;
17use cstree::{
18    green::GreenNode,
19    text::{TextRange, TextSize},
20    util::NodeOrToken,
21};
22use omena_syntax::{StyleDialect, SyntaxKind};
23
24use crate::{DialectExtension, ParseResult, Parser, Token, tokenize};
25
26pub(crate) const STYLE_FACT_FAMILIES: &[&str] = &[
27    "selectors",
28    "variables",
29    "sass-symbols",
30    "sass-includes",
31    "sass-module-edges",
32    "sass-placeholder-definitions",
33    "extend-targets",
34    "animations",
35    "css-module-values",
36    "css-module-value-import-edges",
37    "css-module-value-definition-edges",
38    "css-module-composes",
39    "css-module-composes-edges",
40    "icss",
41    "icss-import-edges",
42    "icss-export-edges",
43    "at-rules",
44    "emission-selectors",
45];
46
47/// Shared event index for parser fact handlers.
48///
49/// Construction performs the only CST descendant walk. Fact-family handlers
50/// consume the resulting node events and the single source-token view instead
51/// of rematerializing or retraversing the tree independently.
52pub(crate) struct StyleFactSink<'text> {
53    text: &'text str,
54    dialect: StyleDialect,
55    error_count: usize,
56    tokens: Vec<Token<'text>>,
57    nodes: Vec<StyleFactNodeEvent>,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub(crate) struct StyleFactNodeEvent {
62    pub(crate) index: usize,
63    pub(crate) kind: SyntaxKind,
64    pub(crate) range: TextRange,
65    pub(crate) parent: Option<usize>,
66    pub(crate) is_top_level: bool,
67}
68
69impl<'text> StyleFactSink<'text> {
70    pub(crate) fn from_cst(text: &'text str, parsed: &ParseResult) -> Self {
71        crate::record_omena_parser_fact_collection_traversal("style-fact-sink");
72        crate::record_omena_parser_fact_collection_registrations(STYLE_FACT_FAMILIES);
73        let mut tokens = Vec::with_capacity(parsed.token_count());
74        let mut nodes = Vec::new();
75        collect_style_fact_events_from_green(
76            parsed.green(),
77            TextSize::from(0),
78            None,
79            text,
80            &mut tokens,
81            &mut nodes,
82        );
83        Self {
84            text,
85            dialect: parsed.dialect(),
86            error_count: parsed.errors().len(),
87            tokens,
88            nodes,
89        }
90    }
91
92    pub(crate) fn text(&self) -> &'text str {
93        self.text
94    }
95
96    pub(crate) fn dialect(&self) -> StyleDialect {
97        self.dialect
98    }
99
100    pub(crate) fn error_count(&self) -> usize {
101        self.error_count
102    }
103
104    pub(crate) fn tokens(&self) -> &[Token<'text>] {
105        self.tokens.as_slice()
106    }
107
108    pub(crate) fn nodes(&self) -> impl Iterator<Item = &StyleFactNodeEvent> {
109        self.nodes.iter()
110    }
111
112    pub(crate) fn node_tokens(&self, node: &StyleFactNodeEvent) -> &[Token<'text>] {
113        let node_range = node.range;
114        let start = self
115            .tokens
116            .partition_point(|token| token.range.start() < node_range.start());
117        let end = self.tokens[start..]
118            .partition_point(|token| token.range.start() < node_range.end())
119            + start;
120        &self.tokens[start..end]
121    }
122
123    pub(crate) fn has_token_kind(&self, kind: SyntaxKind) -> bool {
124        self.tokens.iter().any(|token| token.kind == kind)
125    }
126
127    pub(crate) fn ancestors_inclusive(
128        &self,
129        node: &StyleFactNodeEvent,
130    ) -> Vec<&StyleFactNodeEvent> {
131        let mut ancestors = Vec::new();
132        let mut current = Some(node.index);
133        while let Some(index) = current {
134            let ancestor = &self.nodes[index];
135            ancestors.push(ancestor);
136            current = ancestor.parent;
137        }
138        ancestors
139    }
140
141    pub(crate) fn has_intervening_ancestor_kind(
142        &self,
143        node: &StyleFactNodeEvent,
144        stop: &StyleFactNodeEvent,
145        kinds: &[SyntaxKind],
146    ) -> bool {
147        let mut current = node.parent;
148        while let Some(index) = current {
149            if index == stop.index {
150                return false;
151            }
152            let ancestor = &self.nodes[index];
153            if kinds.contains(&ancestor.kind) {
154                return true;
155            }
156            current = ancestor.parent;
157        }
158        false
159    }
160}
161
162fn collect_style_fact_events_from_green<'text>(
163    node: &GreenNode,
164    start: TextSize,
165    parent: Option<usize>,
166    text: &'text str,
167    tokens: &mut Vec<Token<'text>>,
168    nodes: &mut Vec<StyleFactNodeEvent>,
169) {
170    let mut offset = start;
171    for child in node.children() {
172        match child {
173            NodeOrToken::Node(child_node) => {
174                let kind =
175                    SyntaxKind::from_raw_kind(child_node.kind().0).unwrap_or(SyntaxKind::Unknown);
176                let index = nodes.len();
177                let is_top_level = parent.is_some_and(|parent| {
178                    matches!(
179                        nodes[parent].kind,
180                        SyntaxKind::Stylesheet
181                            | SyntaxKind::ScssStylesheet
182                            | SyntaxKind::LessStylesheet
183                    )
184                });
185                nodes.push(StyleFactNodeEvent {
186                    index,
187                    kind,
188                    range: TextRange::at(offset, child_node.text_len()),
189                    parent,
190                    is_top_level,
191                });
192                collect_style_fact_events_from_green(
193                    child_node,
194                    offset,
195                    Some(index),
196                    text,
197                    tokens,
198                    nodes,
199                );
200                offset += child_node.text_len();
201            }
202            NodeOrToken::Token(token) => {
203                let kind = SyntaxKind::from_raw_kind(token.kind().0).unwrap_or(SyntaxKind::Unknown);
204                let range = TextRange::at(offset, token.text_len());
205                let token_start = u32::from(range.start()) as usize;
206                let token_end = u32::from(range.end()) as usize;
207                tokens.push(Token {
208                    kind,
209                    text: text.get(token_start..token_end).unwrap_or_default(),
210                    range,
211                });
212                offset += token.text_len();
213            }
214        }
215    }
216}
217
218pub(crate) use animations::collect_animation_facts_from_sink;
219pub use animations::{ParsedAnimationFact, ParsedAnimationFactKind};
220pub use at_rules::ParsedAtRuleFact;
221pub(crate) use at_rules::collect_at_rule_facts_from_sink;
222pub use css_modules::{
223    ParsedCssModuleComposesEdgeFact, ParsedCssModuleComposesEdgeKind, ParsedCssModuleComposesFact,
224    ParsedCssModuleComposesFactKind, ParsedCssModuleValueDefinitionEdgeFact,
225    ParsedCssModuleValueFact, ParsedCssModuleValueFactKind, ParsedCssModuleValueImportEdgeFact,
226};
227pub(crate) use css_modules::{
228    collect_css_module_composes_edge_facts_from_sink, collect_css_module_composes_facts_from_sink,
229    collect_css_module_value_definition_edge_facts_from_sink,
230    collect_css_module_value_definition_edge_names, collect_css_module_value_facts_from_sink,
231    collect_css_module_value_import_edge_facts_from_sink,
232    css_module_value_reference_token_can_be_name, css_module_value_source_name,
233    css_module_value_statement_end, declaration_colon_index,
234};
235pub(crate) use emission_selectors::collect_emission_selector_facts_from_sink;
236pub use emission_selectors::{
237    ParsedEmissionSelectorFactKindV0, ParsedEmissionSelectorFactV0, ParsedEmissionSelectorFactsV0,
238    collect_emission_selector_facts_from_cst,
239};
240pub use icss::{
241    ParsedIcssExportEdgeFact, ParsedIcssFact, ParsedIcssFactKind, ParsedIcssImportEdgeFact,
242    collect_icss_export_values_from_cst,
243};
244pub(crate) use icss::{
245    collect_icss_export_edge_facts_from_sink, collect_icss_facts_from_sink,
246    collect_icss_import_edge_facts_from_sink,
247};
248pub use sass::{
249    ParsedExtendTargetFact, ParsedExtendTargetFactKind, ParsedSassCallableParameterFact,
250    ParsedSassCallableSignatureFact, ParsedSassIncludeFact, ParsedSassModuleEdgeFact,
251    ParsedSassModuleEdgeFactKind, ParsedSassPlaceholderDefinitionFact, ParsedSassSymbolFact,
252    ParsedSassSymbolFactKind,
253};
254pub(crate) use sass::{
255    collect_extend_target_facts_from_sink, collect_sass_include_facts_from_sink,
256    collect_sass_module_edge_facts_from_sink, collect_sass_placeholder_definition_facts_from_sink,
257    collect_sass_symbol_facts_from_sink,
258};
259pub use selectors::{ParsedSelectorFact, ParsedSelectorFactKind};
260pub(crate) use selectors::{
261    SelectorBranch, collect_class_selector_names_from_header, collect_selector_facts_from_sink,
262    css_module_block_scope_marker_in_header, css_module_header_is_global_only,
263    resolve_selector_header, split_selector_groups,
264};
265pub use variables::{ParsedVariableFact, ParsedVariableFactKind, ParsedVariableFactNameV0};
266pub(crate) use variables::{collect_variable_facts_from_sink, scss_variable_token_is_declaration};
267
268#[derive(Debug, Clone, PartialEq, Eq)]
269pub struct ParsedStyleFacts {
270    pub product: &'static str,
271    pub dialect: StyleDialect,
272    pub selector_count: usize,
273    pub selectors: Vec<ParsedSelectorFact>,
274    pub variable_count: usize,
275    pub variables: Vec<ParsedVariableFact>,
276    pub sass_symbol_count: usize,
277    pub sass_symbols: Vec<ParsedSassSymbolFact>,
278    pub sass_include_count: usize,
279    pub sass_includes: Vec<ParsedSassIncludeFact>,
280    pub sass_module_edge_count: usize,
281    pub sass_module_edges: Vec<ParsedSassModuleEdgeFact>,
282    pub sass_placeholder_definition_count: usize,
283    pub sass_placeholder_definitions: Vec<ParsedSassPlaceholderDefinitionFact>,
284    pub extend_target_count: usize,
285    pub extend_targets: Vec<ParsedExtendTargetFact>,
286    pub animation_count: usize,
287    pub animations: Vec<ParsedAnimationFact>,
288    pub css_module_value_count: usize,
289    pub css_module_values: Vec<ParsedCssModuleValueFact>,
290    pub css_module_value_import_edge_count: usize,
291    pub css_module_value_import_edges: Vec<ParsedCssModuleValueImportEdgeFact>,
292    pub css_module_value_definition_edge_count: usize,
293    pub css_module_value_definition_edges: Vec<ParsedCssModuleValueDefinitionEdgeFact>,
294    pub css_module_composes_count: usize,
295    pub css_module_composes: Vec<ParsedCssModuleComposesFact>,
296    pub css_module_composes_edge_count: usize,
297    pub css_module_composes_edges: Vec<ParsedCssModuleComposesEdgeFact>,
298    pub icss_count: usize,
299    pub icss: Vec<ParsedIcssFact>,
300    pub icss_import_edge_count: usize,
301    pub icss_import_edges: Vec<ParsedIcssImportEdgeFact>,
302    pub icss_export_edge_count: usize,
303    pub icss_export_edges: Vec<ParsedIcssExportEdgeFact>,
304    pub at_rule_count: usize,
305    pub at_rules: Vec<ParsedAtRuleFact>,
306    pub error_count: usize,
307}
308
309#[derive(Debug, Clone, PartialEq, Eq)]
310#[non_exhaustive]
311pub struct ParsedStyleFactCollectionV0 {
312    pub facts: ParsedStyleFacts,
313    pub emission_selectors: ParsedEmissionSelectorFactsV0,
314}
315
316struct ProductFacts(ParsedStyleFacts);
317
318impl From<ParsedStyleFacts> for ProductFacts {
319    fn from(facts: ParsedStyleFacts) -> Self {
320        let ParsedStyleFacts {
321            product,
322            dialect,
323            selector_count,
324            selectors,
325            variable_count,
326            variables,
327            sass_symbol_count,
328            sass_symbols,
329            sass_include_count: _,
330            sass_includes: _,
331            sass_module_edge_count,
332            sass_module_edges,
333            sass_placeholder_definition_count,
334            sass_placeholder_definitions,
335            extend_target_count: _,
336            extend_targets: _,
337            animation_count,
338            animations,
339            css_module_value_count,
340            css_module_values,
341            css_module_value_import_edge_count,
342            css_module_value_import_edges,
343            css_module_value_definition_edge_count,
344            css_module_value_definition_edges,
345            css_module_composes_count,
346            css_module_composes,
347            css_module_composes_edge_count,
348            css_module_composes_edges,
349            icss_count: _,
350            icss: _,
351            icss_import_edge_count: _,
352            icss_import_edges: _,
353            icss_export_edge_count: _,
354            icss_export_edges: _,
355            at_rule_count: _,
356            at_rules: _,
357            error_count,
358        } = facts;
359        let include_sass_declarations = matches!(dialect, StyleDialect::Scss | StyleDialect::Sass);
360        let (
361            sass_symbol_count,
362            sass_symbols,
363            sass_module_edge_count,
364            sass_module_edges,
365            sass_placeholder_definition_count,
366            sass_placeholder_definitions,
367        ) = if include_sass_declarations {
368            (
369                sass_symbol_count,
370                sass_symbols,
371                sass_module_edge_count,
372                sass_module_edges,
373                sass_placeholder_definition_count,
374                sass_placeholder_definitions,
375            )
376        } else {
377            (0, Vec::new(), 0, Vec::new(), 0, Vec::new())
378        };
379
380        Self(ParsedStyleFacts {
381            product,
382            dialect,
383            selector_count,
384            selectors,
385            variable_count,
386            variables,
387            sass_symbol_count,
388            sass_symbols,
389            sass_include_count: 0,
390            sass_includes: Vec::new(),
391            sass_module_edge_count,
392            sass_module_edges,
393            sass_placeholder_definition_count,
394            sass_placeholder_definitions,
395            extend_target_count: 0,
396            extend_targets: Vec::new(),
397            animation_count,
398            animations,
399            css_module_value_count,
400            css_module_values,
401            css_module_value_import_edge_count,
402            css_module_value_import_edges,
403            css_module_value_definition_edge_count,
404            css_module_value_definition_edges,
405            css_module_composes_count,
406            css_module_composes,
407            css_module_composes_edge_count,
408            css_module_composes_edges,
409            icss_count: 0,
410            icss: Vec::new(),
411            icss_import_edge_count: 0,
412            icss_import_edges: Vec::new(),
413            icss_export_edge_count: 0,
414            icss_export_edges: Vec::new(),
415            at_rule_count: 0,
416            at_rules: Vec::new(),
417            error_count,
418        })
419    }
420}
421
422impl From<ProductFacts> for ParsedStyleFacts {
423    fn from(facts: ProductFacts) -> Self {
424        facts.0
425    }
426}
427
428pub fn collect_style_facts_with_extension(
429    text: &str,
430    extension: &impl DialectExtension,
431) -> ParsedStyleFacts {
432    let parsed = parse_style_fact_source(text, extension);
433    facts_from_cst(text, &parsed)
434}
435
436pub fn collect_style_fact_collection_with_extension(
437    text: &str,
438    extension: &impl DialectExtension,
439) -> ParsedStyleFactCollectionV0 {
440    let parsed = parse_style_fact_source(text, extension);
441    let sink = StyleFactSink::from_cst(text, &parsed);
442    ParsedStyleFactCollectionV0 {
443        facts: facts_from_sink(&sink),
444        emission_selectors: collect_emission_selector_facts_from_sink(&sink),
445    }
446}
447
448fn parse_style_fact_source(text: &str, extension: &impl DialectExtension) -> ParseResult {
449    let (tokens, lex_errors) = tokenize(text, extension);
450    let token_count = tokens.len();
451    let mut parser = Parser::new(tokens, lex_errors, extension.dialect());
452    crate::record_omena_parser_parse_materialization(token_count);
453    let (green, interner) = parser.parse();
454    let errors = parser.into_errors();
455    ParseResult::new(green, interner, errors, token_count, extension.dialect())
456}
457
458pub fn facts_from_cst(text: &str, parsed: &ParseResult) -> ParsedStyleFacts {
459    let sink = StyleFactSink::from_cst(text, parsed);
460    facts_from_sink(&sink)
461}
462
463fn facts_from_sink(sink: &StyleFactSink<'_>) -> ParsedStyleFacts {
464    let selectors = collect_selector_facts_from_sink(sink);
465    let variables = collect_variable_facts_from_sink(sink);
466    let sass_symbols = collect_sass_symbol_facts_from_sink(sink);
467    let sass_includes = collect_sass_include_facts_from_sink(sink);
468    let sass_module_edges = collect_sass_module_edge_facts_from_sink(sink);
469    let sass_placeholder_definitions = collect_sass_placeholder_definition_facts_from_sink(sink);
470    let extend_targets = collect_extend_target_facts_from_sink(sink);
471    let animations = collect_animation_facts_from_sink(sink);
472    let css_module_values = collect_css_module_value_facts_from_sink(sink);
473    let css_module_value_import_edges = collect_css_module_value_import_edge_facts_from_sink(sink);
474    let css_module_value_definition_edges =
475        collect_css_module_value_definition_edge_facts_from_sink(sink);
476    let css_module_composes = collect_css_module_composes_facts_from_sink(sink);
477    let css_module_composes_edges = collect_css_module_composes_edge_facts_from_sink(sink);
478    let icss = collect_icss_facts_from_sink(sink);
479    let icss_import_edges = collect_icss_import_edge_facts_from_sink(sink);
480    let icss_export_edges = collect_icss_export_edge_facts_from_sink(sink);
481    let at_rules = collect_at_rule_facts_from_sink(sink);
482
483    ParsedStyleFacts {
484        product: "omena-parser.style-facts",
485        dialect: sink.dialect(),
486        selector_count: selectors.len(),
487        selectors,
488        variable_count: variables.len(),
489        variables,
490        sass_symbol_count: sass_symbols.len(),
491        sass_symbols,
492        sass_include_count: sass_includes.len(),
493        sass_includes,
494        sass_module_edge_count: sass_module_edges.len(),
495        sass_module_edges,
496        sass_placeholder_definition_count: sass_placeholder_definitions.len(),
497        sass_placeholder_definitions,
498        extend_target_count: extend_targets.len(),
499        extend_targets,
500        animation_count: animations.len(),
501        animations,
502        css_module_value_count: css_module_values.len(),
503        css_module_values,
504        css_module_value_import_edge_count: css_module_value_import_edges.len(),
505        css_module_value_import_edges,
506        css_module_value_definition_edge_count: css_module_value_definition_edges.len(),
507        css_module_value_definition_edges,
508        css_module_composes_count: css_module_composes.len(),
509        css_module_composes,
510        css_module_composes_edge_count: css_module_composes_edges.len(),
511        css_module_composes_edges,
512        icss_count: icss.len(),
513        icss,
514        icss_import_edge_count: icss_import_edges.len(),
515        icss_import_edges,
516        icss_export_edge_count: icss_export_edges.len(),
517        icss_export_edges,
518        at_rule_count: at_rules.len(),
519        at_rules,
520        error_count: sink.error_count(),
521    }
522}
523
524pub(crate) fn product_facts_from_cst(text: &str, parsed: &ParseResult) -> ParsedStyleFacts {
525    ProductFacts::from(facts_from_cst(text, parsed)).into()
526}
527
528#[cfg(test)]
529mod product_facts_authority_tests;
530
531#[cfg(test)]
532pub(crate) fn tokens_from_syntax_node<'text>(
533    text: &'text str,
534    parsed: &ParseResult,
535    node: &SyntaxNode<SyntaxKind>,
536) -> Vec<Token<'text>> {
537    let node_range = node.text_range();
538    let tokens = parsed.syntax_token_views();
539    let start_index = tokens.partition_point(|token| token.range.start() < node_range.start());
540    let end_index = tokens[start_index..]
541        .partition_point(|token| token.range.start() < node_range.end())
542        + start_index;
543    tokens[start_index..end_index]
544        .iter()
545        .filter(|token| token.range.end() <= node_range.end())
546        .map(|token| {
547            let range = token.range;
548            let start = u32::from(range.start()) as usize;
549            let end = u32::from(range.end()) as usize;
550            Token {
551                kind: token.kind,
552                text: text.get(start..end).unwrap_or_default(),
553                range,
554            }
555        })
556        .collect()
557}
558
559#[cfg(test)]
560mod tests {
561    use super::*;
562    use crate::{StyleDialect, parse};
563
564    const FULL_FACT_IDENTITY_FIXTURE: &str = r#"@use "./tokens" as t;
565@forward "./theme";
566@import "./legacy";
567$brand: red !default;
568@mixin paint($tone: red) { @content; color: $tone; }
569@function scale($value) { @return $value; }
570@value spacing: 1rem;
571@value gap: spacing;
572@value remoteTone as tone from "./tokens.css";
573:import("./dep.css") { localName: remoteName; }
574:export { exported: localName; }
575%shared { color: $brand; }
576#app, div[data-state="open"]::before { --accent: blue; }
577.button {
578  @include paint($brand) { color: scale(1); }
579  @extend %shared !optional;
580  composes: base global(shell) from "./base.css";
581  animation: pulse 1s ease;
582  padding: gap;
583}
584@keyframes pulse { from { opacity: 0; } to { opacity: 1; } }
585"#;
586
587    fn tokens_from_syntax_node_linear<'text>(
588        text: &'text str,
589        parsed: &ParseResult,
590        node: &SyntaxNode<SyntaxKind>,
591    ) -> Vec<Token<'text>> {
592        let node_range = node.text_range();
593        parsed
594            .syntax_token_views()
595            .iter()
596            .filter(|token| token.range.start() >= node_range.start())
597            .filter(|token| token.range.end() <= node_range.end())
598            .map(|token| {
599                let range = token.range;
600                let start = u32::from(range.start()) as usize;
601                let end = u32::from(range.end()) as usize;
602                Token {
603                    kind: token.kind,
604                    text: text.get(start..end).unwrap_or_default(),
605                    range,
606                }
607            })
608            .collect()
609    }
610
611    #[test]
612    fn tokens_from_syntax_node_matches_linear_scan_order() {
613        let text = r#"@use "./tokens" as t;
614:export { exported: local; }
615.button, :global(.card) {
616  --gap: 1rem;
617  color: var(--brand);
618  &__icon { composes: icon from "./icons.module.css"; }
619}
620@media (width >= 1px) {
621  .button--primary { color: t.$brand; }
622}"#;
623        let parsed = parse(text, StyleDialect::Scss);
624        let syntax = parsed.syntax();
625
626        for node in syntax.descendants() {
627            assert_eq!(
628                tokens_from_syntax_node(text, &parsed, node),
629                tokens_from_syntax_node_linear(text, &parsed, node),
630                "token slice drift for {:?} at {:?}",
631                node.kind(),
632                node.text_range()
633            );
634        }
635    }
636
637    #[test]
638    fn combined_fact_sink_preserves_the_full_pre_change_fact_bytes() {
639        let (collection, instrumentation) =
640            crate::instrumentation::with_omena_parser_fact_collection_instrumentation(|| {
641                crate::collect_style_fact_collection(FULL_FACT_IDENTITY_FIXTURE, StyleDialect::Scss)
642            });
643
644        assert_eq!(instrumentation.traversal_entry_count, 1);
645        assert_eq!(instrumentation.families, ["style-fact-sink"]);
646        assert_eq!(instrumentation.registered_family_count, 18);
647        assert_eq!(instrumentation.registered_families, STYLE_FACT_FAMILIES);
648
649        let facts = &collection.facts;
650        assert!(
651            facts.selector_count > 0,
652            "selectors handler produced no rows"
653        );
654        assert!(
655            facts.variable_count > 0,
656            "variables handler produced no rows"
657        );
658        assert!(
659            facts.sass_symbol_count > 0,
660            "sass-symbols handler produced no rows"
661        );
662        assert!(
663            facts.sass_include_count > 0,
664            "sass-includes handler produced no rows"
665        );
666        assert!(
667            facts.sass_module_edge_count > 0,
668            "sass-module-edges handler produced no rows"
669        );
670        assert!(
671            facts.sass_placeholder_definition_count > 0,
672            "sass-placeholder-definitions handler produced no rows"
673        );
674        assert!(
675            facts.extend_target_count > 0,
676            "extend-targets handler produced no rows"
677        );
678        assert!(
679            facts.animation_count > 0,
680            "animations handler produced no rows"
681        );
682        assert!(
683            facts.css_module_value_count > 0,
684            "css-module-values handler produced no rows"
685        );
686        assert!(
687            facts.css_module_value_import_edge_count > 0,
688            "css-module-value-import-edges handler produced no rows"
689        );
690        assert!(
691            facts.css_module_value_definition_edge_count > 0,
692            "css-module-value-definition-edges handler produced no rows"
693        );
694        assert!(
695            facts.css_module_composes_count > 0,
696            "css-module-composes handler produced no rows"
697        );
698        assert!(
699            facts.css_module_composes_edge_count > 0,
700            "css-module-composes-edges handler produced no rows"
701        );
702        assert!(facts.icss_count > 0, "icss handler produced no rows");
703        assert!(
704            facts.icss_import_edge_count > 0,
705            "icss-import-edges handler produced no rows"
706        );
707        assert!(
708            facts.icss_export_edge_count > 0,
709            "icss-export-edges handler produced no rows"
710        );
711        assert!(facts.at_rule_count > 0, "at-rules handler produced no rows");
712        assert!(
713            !collection.emission_selectors.selectors.is_empty(),
714            "emission-selectors handler produced no rows"
715        );
716
717        let bytes = format!("{collection:#?}");
718        let fingerprint = bytes.bytes().fold(0xcbf29ce484222325_u64, |hash, byte| {
719            (hash ^ u64::from(byte)).wrapping_mul(0x100000001b3)
720        });
721        assert_eq!(fingerprint, 0x60bac74d4ed97b4c);
722    }
723}