Skip to main content

omena_semantic/
lib.rs

1//! Semantic fact layer for parsed omena-css style modules.
2//!
3//! This crate lifts parser facts into selector, custom-property, Sass module,
4//! design-token, and source-evidence summaries. It is the bridge between the
5//! lossless parser substrate and query/LSP consumers that need stable semantic
6//! contracts rather than raw CST traversal.
7
8use engine_input_producers::EngineInputV2;
9use omena_cascade::{SelectorMatchVerdict, selector_context_witness_for_declaration};
10use omena_interner::{
11    intern_class_name, intern_css_ident, intern_custom_property_name, intern_keyframes_name,
12    intern_mixin_name,
13};
14use omena_parser::{
15    ParsedAnimationFactKind, ParsedCssModuleComposesEdgeKind, ParsedCssModuleComposesFactKind,
16    ParsedCssModuleValueFactKind, ParsedCst, ParsedSassModuleEdgeFactKind,
17    ParsedSassSymbolFactKind, ParsedSelectorFactKind, ParsedStyleFacts, ParsedVariableFactKind,
18    StyleDialect, facts_from_cst, parse,
19};
20use omena_syntax::{SyntaxKind, SyntaxNode, css_keyword};
21use serde::Serialize;
22use std::collections::BTreeSet;
23
24mod css_modules;
25mod css_modules_cross_file;
26mod design_tokens;
27mod evidence;
28mod layer_tree;
29mod lossless_cst;
30mod observation;
31mod sass_module_graph;
32mod selector_identity;
33mod selector_references;
34mod source_evidence;
35mod types;
36
37pub use css_modules::{
38    CssModulesSemanticCapabilitiesV0, CssModulesSemanticSummaryV0, summarize_css_modules_semantics,
39    summarize_css_modules_semantics_from_source,
40};
41pub use css_modules_cross_file::{
42    CssModulesComposesClosureEdgeV0, CssModulesComposesEdgeFactV0,
43    CssModulesCrossFileClosureCapabilitiesV0, CssModulesCrossFileClosureSummaryV0,
44    CssModulesCrossFileResolutionCapabilitiesV0, CssModulesCrossFileResolutionSummaryV0,
45    CssModulesCrossFileStyleFactsV0, CssModulesCycleV0, CssModulesIcssClosureEdgeV0,
46    CssModulesIcssExportEdgeFactV0, CssModulesIcssImportEdgeFactV0,
47    CssModulesImportEdgeResolutionV0, CssModulesValueClosureEdgeV0,
48    CssModulesValueDefinitionEdgeFactV0, CssModulesValueImportEdgeFactV0,
49    summarize_css_modules_cross_file_closure, summarize_css_modules_cross_file_resolution,
50};
51pub use design_tokens::{
52    DesignTokenCascadeRankingSignalV0, DesignTokenContextSignalV0,
53    DesignTokenExternalDeclarationCandidateScopeV0, DesignTokenRankedReferenceV0,
54    DesignTokenResolutionSignalV0, DesignTokenSemanticCapabilitiesV0, DesignTokenSemanticSummaryV0,
55    DesignTokenWorkspaceDeclarationFactV0, collect_design_token_workspace_declarations,
56    summarize_design_token_semantics,
57    summarize_design_token_semantics_with_scoped_workspace_declarations,
58    summarize_design_token_semantics_with_workspace_declarations,
59};
60pub use evidence::{
61    SemanticPromotionEvidenceItemV0, SemanticPromotionEvidenceSummaryV0,
62    summarize_semantic_promotion_evidence, summarize_semantic_promotion_evidence_with_source_input,
63};
64pub use lossless_cst::{
65    LosslessCstConsumerReadinessV0, LosslessCstContractV0, LosslessCstSpanInvariantsV0,
66    summarize_lossless_cst_contract,
67};
68pub use observation::{
69    SelectorIdentityObservationV0, SemanticCouplingBoundaryObservationV0,
70    SemanticGraphDownstreamReadinessV0, SourceEvidenceObservationV0, TheoryObservationContractV0,
71    TheoryObservationHarnessInput, TheoryObservationHarnessSummaryV0,
72    summarize_theory_observation_contract, summarize_theory_observation_harness,
73};
74pub use sass_module_graph::{
75    SassModuleConfigurableNamesResolverV0, SassModuleCycleV0,
76    SassModuleForwardConfigurationRequestV0, SassModuleGraphClosureCapabilitiesV0,
77    SassModuleGraphClosureEdgeV0, SassModuleGraphClosureSummaryV0,
78    SassModuleGraphConfigurationResolverV0, SassModuleGraphEdgeFactV0,
79    SassModuleGraphResolutionCapabilitiesV0, SassModuleGraphResolutionSummaryV0,
80    SassModuleUseConfigurationRequestV0, SassModuleVariableOverrideV0,
81    SassModuleVisibleSymbolsResolverV0, SassSymbolKeyV0, StyleImportReachabilityCapabilitiesV0,
82    StyleImportReachabilityEdgeFactV0, StyleImportReachabilityFactV0,
83    StyleImportReachabilitySummaryV0, apply_sass_forward_prefix, collect_visible_sass_symbol_keys,
84    derive_sass_forward_effective_variable_overrides, derive_sass_forward_export_prefix_at_ordinal,
85    derive_sass_module_configurable_variable_names,
86    derive_sass_module_forward_effective_variable_overrides_at_ordinal,
87    derive_sass_module_forward_variable_override_values_at_ordinal,
88    derive_sass_module_forward_variable_overrides_at_ordinal,
89    derive_sass_module_rule_variable_overrides_at_ordinal,
90    filter_sass_forward_configurable_variable_names, filter_sass_forward_exports,
91    fold_sass_symbol_name, prefix_sass_forward_exports,
92    resolve_sass_module_effective_variable_overrides, sass_forward_filter_name_matches_symbol,
93    sass_module_configuration_variables_are_valid, sass_symbol_key,
94    summarize_sass_module_configuration_signature, summarize_sass_module_graph_closure,
95    summarize_sass_module_graph_resolution, summarize_sass_module_instance_identity_key,
96    summarize_style_import_reachability, with_sass_module_rawallpaths_closure_for_test,
97};
98pub use selector_identity::{
99    SelectorCanonicalIdentityV0, SelectorIdentityEngineSummaryV0, SelectorIdentityRewriteSafetyV0,
100    summarize_selector_identity_engine,
101};
102pub use selector_references::{
103    SelectorEditableDirectReferenceSiteV0, SelectorReferenceEngineSummaryV0,
104    SelectorReferenceSiteV0, SelectorReferenceSummaryV0, summarize_selector_reference_engine,
105};
106pub use source_evidence::{
107    BindingOriginEvidenceV0, CertaintyReasonEvidenceV0, ReferenceSiteIdentityEvidenceV0,
108    SourceInputPromotionEvidenceSummaryV0, StyleModuleEdgeEvidenceV0,
109    ValueDomainExplanationEvidenceV0, summarize_source_input_evidence,
110};
111pub use types::{
112    NestedSafetyCountsV0, ParserBoundarySyntaxFactsV0, ParserByteSpanV0,
113    ParserIndexComposesFactsV0, ParserIndexCustomPropertyDeclFactV0,
114    ParserIndexCustomPropertyFactsV0, ParserIndexCustomPropertyRefFactV0,
115    ParserIndexKeyframesFactsV0, ParserIndexSassModuleUseFactV0,
116    ParserIndexSassSameFileResolutionFactsV0, ParserIndexSelectorDefinitionFactV0,
117    ParserIndexSelectorFactsV0, ParserIndexValueFactsV0, ParserIndexWrapperFactsV0,
118    ParserLosslessCstFactsV0, ParserPositionV0, ParserRangeV0, ParserSassSyntaxFactsV0,
119    StyleContainerIndexV0, StyleContextBlockV0, StyleContextIndexV0,
120    StyleContextSelectorMembershipV0, StyleCustomPropertySemanticFactsV0, StyleLayerBlockBindingV0,
121    StyleLayerIndexV0, StyleLayerOrderNodeV0, StyleLayerStatementV0, StyleSassSemanticFactsV0,
122    StyleScopeIndexV0, StyleScopeRangeV0, StyleSelectorIdentityFactsV0, StyleSemanticFactsV0,
123    Stylesheet,
124};
125
126#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
127#[serde(rename_all = "camelCase")]
128pub struct StyleSemanticBoundarySummaryV0 {
129    pub schema_version: &'static str,
130    pub language: &'static str,
131    pub parser_facts: ParserBoundarySyntaxFactsV0,
132    pub semantic_facts: StyleSemanticFactsV0,
133    pub design_token_semantics: DesignTokenSemanticSummaryV0,
134    pub selector_identity_engine: SelectorIdentityEngineSummaryV0,
135    pub promotion_evidence: SemanticPromotionEvidenceSummaryV0,
136    pub lossless_cst_contract: LosslessCstContractV0,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
140#[serde(rename_all = "camelCase")]
141pub struct StyleSemanticGraphSummaryV0 {
142    pub schema_version: &'static str,
143    pub product: &'static str,
144    pub language: &'static str,
145    pub parser_facts: ParserBoundarySyntaxFactsV0,
146    pub semantic_facts: StyleSemanticFactsV0,
147    pub css_modules_semantics: CssModulesSemanticSummaryV0,
148    pub design_token_semantics: DesignTokenSemanticSummaryV0,
149    pub selector_identity_engine: SelectorIdentityEngineSummaryV0,
150    pub selector_reference_engine: SelectorReferenceEngineSummaryV0,
151    pub source_input_evidence: SourceInputPromotionEvidenceSummaryV0,
152    pub promotion_evidence: SemanticPromotionEvidenceSummaryV0,
153    pub lossless_cst_contract: LosslessCstContractV0,
154}
155
156#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
157#[serde(rename_all = "camelCase")]
158pub struct StyleSemanticSoaTablesV0 {
159    pub schema_version: &'static str,
160    pub product: &'static str,
161    pub selector_names: SemanticNameSoaTableV0,
162    pub custom_property_names: SemanticNameSoaTableV0,
163    pub sass_names: SemanticNameSoaTableV0,
164    pub total_row_count: usize,
165    pub interned_row_count: usize,
166    pub ready_surfaces: Vec<&'static str>,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
170#[serde(rename_all = "camelCase")]
171pub struct StyleRuntimeIndexFactsV0 {
172    pub schema_version: &'static str,
173    pub product: &'static str,
174    pub style_path: String,
175    pub language: &'static str,
176    pub class_selector_names: Vec<String>,
177    pub custom_property_names: Vec<String>,
178    pub custom_property_decl_names: Vec<String>,
179    pub custom_property_ref_names: Vec<String>,
180    pub keyframe_names: Vec<String>,
181    pub animation_reference_names: Vec<String>,
182    pub ready_surfaces: Vec<&'static str>,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
186#[serde(rename_all = "camelCase")]
187pub struct SemanticNameSoaTableV0 {
188    pub table_name: &'static str,
189    pub name_kind: &'static str,
190    pub row_indices: Vec<usize>,
191    pub names: Vec<String>,
192    pub interned_row_count: usize,
193    pub unique_name_count: usize,
194}
195
196pub fn summarize_style_semantic_boundary(sheet: &Stylesheet) -> StyleSemanticBoundarySummaryV0 {
197    summarize_omena_parser_style_semantic_boundary_from_source(&sheet.path, &sheet.source)
198}
199
200pub fn summarize_style_semantic_graph(
201    sheet: &Stylesheet,
202    input: &EngineInputV2,
203) -> StyleSemanticGraphSummaryV0 {
204    summarize_style_semantic_graph_for_path(sheet, input, None)
205}
206
207pub fn summarize_style_semantic_graph_for_path(
208    sheet: &Stylesheet,
209    input: &EngineInputV2,
210    style_path: Option<&str>,
211) -> StyleSemanticGraphSummaryV0 {
212    summarize_style_semantic_graph_for_path_with_workspace_declarations(
213        sheet,
214        input,
215        style_path,
216        &[],
217    )
218}
219
220pub fn summarize_style_semantic_graph_for_path_with_workspace_declarations(
221    sheet: &Stylesheet,
222    input: &EngineInputV2,
223    style_path: Option<&str>,
224    workspace_declarations: &[DesignTokenWorkspaceDeclarationFactV0],
225) -> StyleSemanticGraphSummaryV0 {
226    let (boundary, facts) = summarize_omena_parser_style_semantic_boundary_with_facts_from_source(
227        &sheet.path,
228        &sheet.source,
229    );
230    let parser_facts = boundary.parser_facts;
231    let semantic_facts = boundary.semantic_facts;
232    let effective_style_path = style_path.or(Some(sheet.path.as_str()));
233    let design_token_semantics = summarize_design_token_semantics_with_workspace_declarations(
234        &parser_facts,
235        &semantic_facts,
236        effective_style_path,
237        workspace_declarations,
238    );
239    let css_modules_semantics = css_modules::summarize_css_modules_semantics_from_facts(&facts);
240    let selector_identity_engine =
241        summarize_selector_identity_engine(&semantic_facts.selector_identity);
242    let selector_reference_engine = summarize_selector_reference_engine(input, style_path);
243    let source_input_evidence = summarize_source_input_evidence(input);
244    let promotion_evidence = summarize_semantic_promotion_evidence_with_source_input(
245        &parser_facts,
246        &semantic_facts,
247        input,
248    );
249    let lossless_cst_contract = summarize_lossless_cst_contract(&parser_facts.lossless_cst);
250
251    StyleSemanticGraphSummaryV0 {
252        schema_version: "0",
253        product: "omena-semantic.style-semantic-graph",
254        language: boundary.language,
255        parser_facts,
256        semantic_facts,
257        css_modules_semantics,
258        design_token_semantics,
259        selector_identity_engine,
260        selector_reference_engine,
261        source_input_evidence,
262        promotion_evidence,
263        lossless_cst_contract,
264    }
265}
266
267pub fn summarize_style_semantic_graph_from_source(
268    style_path: &str,
269    style_source: &str,
270    input: &EngineInputV2,
271) -> Option<StyleSemanticGraphSummaryV0> {
272    dialect_for_style_path(style_path)?;
273    let (boundary, facts) = summarize_omena_parser_style_semantic_boundary_with_facts_from_source(
274        style_path,
275        style_source,
276    );
277    let css_modules_semantics = css_modules::summarize_css_modules_semantics_from_facts(&facts);
278    let parser_facts = boundary.parser_facts;
279    let semantic_facts = boundary.semantic_facts;
280    let selector_reference_engine = summarize_selector_reference_engine(input, Some(style_path));
281    let source_input_evidence = summarize_source_input_evidence(input);
282    let promotion_evidence = summarize_semantic_promotion_evidence_with_source_input(
283        &parser_facts,
284        &semantic_facts,
285        input,
286    );
287
288    Some(StyleSemanticGraphSummaryV0 {
289        schema_version: "0",
290        product: "omena-semantic.style-semantic-graph",
291        language: boundary.language,
292        parser_facts,
293        semantic_facts,
294        css_modules_semantics,
295        design_token_semantics: boundary.design_token_semantics,
296        selector_identity_engine: boundary.selector_identity_engine,
297        selector_reference_engine,
298        source_input_evidence,
299        promotion_evidence,
300        lossless_cst_contract: boundary.lossless_cst_contract,
301    })
302}
303
304pub fn summarize_style_semantic_facts(sheet: &Stylesheet) -> StyleSemanticFactsV0 {
305    summarize_style_semantic_boundary(sheet).semantic_facts
306}
307
308pub fn summarize_style_runtime_index_facts_from_source(
309    style_path: &str,
310    style_source: &str,
311) -> Option<StyleRuntimeIndexFactsV0> {
312    dialect_for_style_path(style_path)?;
313    let boundary =
314        summarize_omena_parser_style_semantic_boundary_from_source(style_path, style_source);
315    let custom_property_names = boundary
316        .semantic_facts
317        .custom_properties
318        .decl_names
319        .iter()
320        .chain(boundary.semantic_facts.custom_properties.ref_names.iter())
321        .cloned()
322        .collect::<BTreeSet<_>>()
323        .into_iter()
324        .collect();
325
326    Some(StyleRuntimeIndexFactsV0 {
327        schema_version: "0",
328        product: "omena-semantic.style-runtime-index-facts",
329        style_path: style_path.to_string(),
330        language: boundary.language,
331        class_selector_names: boundary.parser_facts.selectors.names,
332        custom_property_names,
333        custom_property_decl_names: boundary.semantic_facts.custom_properties.decl_names,
334        custom_property_ref_names: boundary.semantic_facts.custom_properties.ref_names,
335        keyframe_names: boundary.parser_facts.keyframes.names,
336        animation_reference_names: boundary.parser_facts.keyframes.animation_ref_names,
337        ready_surfaces: vec![
338            "semanticRuntimeIndexFacts",
339            "customPropertyRuntimeIndex",
340            "keyframeRuntimeIndex",
341        ],
342    })
343}
344
345pub fn summarize_style_semantic_soa_tables(
346    semantic_facts: &StyleSemanticFactsV0,
347    db: &dyn salsa::Database,
348) -> StyleSemanticSoaTablesV0 {
349    let selector_names = semantic_name_soa_table(
350        "selectors",
351        "className",
352        semantic_facts.selector_identity.canonical_names.as_slice(),
353        |name| intern_class_name(db, name).is_ok(),
354    );
355    let custom_property_names = semantic_name_soa_table(
356        "customProperties",
357        "customPropertyName",
358        semantic_facts.custom_properties.decl_names.as_slice(),
359        |name| intern_custom_property_name(db, name).is_ok(),
360    );
361    let mut sass_name_sources = Vec::new();
362    sass_name_sources.extend(
363        semantic_facts
364            .sass
365            .same_file_resolution
366            .resolved_variable_ref_names
367            .iter()
368            .cloned(),
369    );
370    sass_name_sources.extend(
371        semantic_facts
372            .sass
373            .same_file_resolution
374            .unresolved_variable_ref_names
375            .iter()
376            .cloned(),
377    );
378    sass_name_sources.extend(
379        semantic_facts
380            .sass
381            .same_file_resolution
382            .resolved_mixin_include_names
383            .iter()
384            .cloned(),
385    );
386    sass_name_sources.extend(
387        semantic_facts
388            .sass
389            .same_file_resolution
390            .unresolved_mixin_include_names
391            .iter()
392            .cloned(),
393    );
394    sass_name_sources.extend(
395        semantic_facts
396            .sass
397            .same_file_resolution
398            .resolved_function_call_names
399            .iter()
400            .cloned(),
401    );
402    let sass_names =
403        semantic_name_soa_table("sass", "cssIdentOrMixinName", &sass_name_sources, |name| {
404            intern_css_ident(db, name).is_ok()
405                || intern_mixin_name(db, name).is_ok()
406                || intern_keyframes_name(db, name).is_ok()
407        });
408    let total_row_count = selector_names.row_indices.len()
409        + custom_property_names.row_indices.len()
410        + sass_names.row_indices.len();
411    let interned_row_count = selector_names.interned_row_count
412        + custom_property_names.interned_row_count
413        + sass_names.interned_row_count;
414
415    StyleSemanticSoaTablesV0 {
416        schema_version: "0",
417        product: "omena-semantic.soa-tables",
418        selector_names,
419        custom_property_names,
420        sass_names,
421        total_row_count,
422        interned_row_count,
423        ready_surfaces: vec!["semanticSoaTables", "semanticSoaNameTables"],
424    }
425}
426
427fn semantic_name_soa_table(
428    table_name: &'static str,
429    name_kind: &'static str,
430    names: &[String],
431    mut intern: impl FnMut(&str) -> bool,
432) -> SemanticNameSoaTableV0 {
433    let mut unique_names = BTreeSet::new();
434    let mut interned_row_count = 0usize;
435    for name in names {
436        unique_names.insert(name.clone());
437        if intern(name) {
438            interned_row_count += 1;
439        }
440    }
441
442    SemanticNameSoaTableV0 {
443        table_name,
444        name_kind,
445        row_indices: (0..names.len()).collect(),
446        names: names.to_vec(),
447        interned_row_count,
448        unique_name_count: unique_names.len(),
449    }
450}
451
452pub fn summarize_parser_contract_facts(sheet: &Stylesheet) -> ParserBoundarySyntaxFactsV0 {
453    summarize_style_semantic_boundary(sheet).parser_facts
454}
455
456pub fn parse_style_module(path: &str, source: &str) -> Option<Stylesheet> {
457    Some(Stylesheet {
458        path: path.to_string(),
459        language: dialect_for_style_path(path)?,
460        source: source.to_string(),
461    })
462}
463
464pub fn summarize_omena_parser_style_semantic_boundary_from_source(
465    style_path: &str,
466    style_source: &str,
467) -> StyleSemanticBoundarySummaryV0 {
468    summarize_omena_parser_style_semantic_boundary_with_facts_from_source(style_path, style_source)
469        .0
470}
471
472fn summarize_omena_parser_style_semantic_boundary_with_facts_from_source(
473    style_path: &str,
474    style_source: &str,
475) -> (StyleSemanticBoundarySummaryV0, ParsedStyleFacts) {
476    let dialect = omena_parser_dialect_for_style_path(style_path);
477    let parsed = parse(style_source, dialect);
478    let facts = facts_from_cst(style_source, &parsed);
479    let cst = parsed.cst();
480    let parser_facts = summarize_omena_parser_contract_facts(
481        style_source,
482        parsed.token_count(),
483        parsed.syntax().children().count(),
484        parsed.errors().len(),
485        &facts,
486        &cst,
487    );
488    let semantic_facts =
489        summarize_omena_parser_semantic_facts(style_source, &facts, &parser_facts, &cst);
490    let design_token_semantics = summarize_design_token_semantics(&parser_facts, &semantic_facts);
491    let selector_identity_engine =
492        summarize_selector_identity_engine(&semantic_facts.selector_identity);
493    let promotion_evidence = summarize_semantic_promotion_evidence(&parser_facts, &semantic_facts);
494    let lossless_cst_contract = summarize_lossless_cst_contract(&parser_facts.lossless_cst);
495
496    (
497        StyleSemanticBoundarySummaryV0 {
498            schema_version: "0",
499            language: omena_parser_dialect_label(dialect),
500            parser_facts,
501            semantic_facts,
502            design_token_semantics,
503            selector_identity_engine,
504            promotion_evidence,
505            lossless_cst_contract,
506        },
507        facts,
508    )
509}
510
511fn summarize_omena_parser_contract_facts(
512    source: &str,
513    token_count: usize,
514    root_node_count: usize,
515    diagnostic_count: usize,
516    facts: &ParsedStyleFacts,
517    cst: &ParsedCst,
518) -> ParserBoundarySyntaxFactsV0 {
519    let (all_token_spans_within_source, all_node_spans_within_source) =
520        cst_span_bounds_within_source(cst, source.len());
521    ParserBoundarySyntaxFactsV0 {
522        lossless_cst: ParserLosslessCstFactsV0 {
523            source_byte_len: source.len(),
524            token_count,
525            root_node_count,
526            diagnostic_count,
527            all_token_spans_within_source,
528            all_node_spans_within_source,
529        },
530        selectors: summarize_omena_parser_selector_facts(source, facts),
531        values: summarize_omena_parser_value_facts(facts),
532        custom_properties: summarize_omena_parser_custom_property_facts(source, facts, cst),
533        sass: summarize_omena_parser_sass_syntax_facts(facts),
534        keyframes: summarize_omena_parser_keyframe_facts(facts),
535        composes: summarize_omena_parser_composes_facts(facts),
536        wrappers: ParserIndexWrapperFactsV0::default(),
537    }
538}
539
540fn cst_span_bounds_within_source(cst: &ParsedCst, source_byte_len: usize) -> (bool, bool) {
541    let all_token_spans_within_source = cst
542        .root()
543        .descendants_with_tokens()
544        .filter_map(|element| element.into_token())
545        .all(|token| {
546            let range = token.text_range();
547            byte_offsets_within_source(
548                u32::from(range.start()) as usize,
549                u32::from(range.end()) as usize,
550                source_byte_len,
551            )
552        });
553    let all_node_spans_within_source = std::iter::once(cst.root())
554        .chain(cst.root().descendants())
555        .all(|node| {
556            let range = node.text_range();
557            byte_offsets_within_source(
558                u32::from(range.start()) as usize,
559                u32::from(range.end()) as usize,
560                source_byte_len,
561            )
562        });
563    (all_token_spans_within_source, all_node_spans_within_source)
564}
565
566fn byte_offsets_within_source(start: usize, end: usize, source_byte_len: usize) -> bool {
567    start <= end && end <= source_byte_len
568}
569
570fn summarize_omena_parser_semantic_facts(
571    source: &str,
572    facts: &ParsedStyleFacts,
573    parser_facts: &ParserBoundarySyntaxFactsV0,
574    cst: &ParsedCst,
575) -> StyleSemanticFactsV0 {
576    let custom_properties =
577        summarize_omena_parser_custom_property_semantic_facts(&parser_facts.custom_properties);
578    let sass_same_file_resolution =
579        summarize_omena_parser_sass_same_file_resolution(&parser_facts.sass);
580    let sass_selector_resolution =
581        summarize_omena_parser_sass_selector_resolution(facts, &sass_same_file_resolution, cst);
582    StyleSemanticFactsV0 {
583        selector_identity: StyleSelectorIdentityFactsV0 {
584            canonical_names: parser_facts.selectors.names.clone(),
585            bem_suffix_safe_names: parser_facts.selectors.bem_suffix_safe_names.clone(),
586            bem_suffix_parent_names: parser_facts.selectors.bem_suffix_parent_names.clone(),
587            nested_unsafe_names: parser_facts.selectors.nested_unsafe_names.clone(),
588            nested_safety_counts: parser_facts.selectors.nested_safety_counts.clone(),
589        },
590        custom_properties,
591        sass: StyleSassSemanticFactsV0 {
592            selector_symbol_facts: Vec::new(),
593            selectors_with_resolved_variable_refs_names: sass_selector_resolution
594                .resolved_variable_ref_selectors,
595            selectors_with_unresolved_variable_refs_names: sass_selector_resolution
596                .unresolved_variable_ref_selectors,
597            selectors_with_resolved_mixin_includes_names: sass_selector_resolution
598                .resolved_mixin_include_selectors,
599            selectors_with_unresolved_mixin_includes_names: sass_selector_resolution
600                .unresolved_mixin_include_selectors,
601            selectors_with_function_calls_names: parser_facts.sass.function_call_names.clone(),
602            same_file_resolution: sass_same_file_resolution,
603        },
604        context_index: summarize_style_context_index(source, cst),
605    }
606}
607
608fn summarize_style_context_index(source: &str, cst: &ParsedCst) -> StyleContextIndexV0 {
609    let layer_statements = layer_statement_facts_from_cst(source, cst);
610    let (context_blocks, memberships) = style_context_blocks_and_memberships_from_cst(source, cst);
611    let block_layers = context_blocks
612        .iter()
613        .filter(|block| block.kind == "layer")
614        .cloned()
615        .collect::<Vec<_>>();
616    let containers = context_blocks
617        .iter()
618        .filter(|block| block.kind == "container")
619        .cloned()
620        .collect::<Vec<_>>();
621    let scopes = context_blocks
622        .iter()
623        .filter(|block| block.kind == "scope")
624        .cloned()
625        .collect::<Vec<_>>();
626    let layer_memberships = memberships
627        .iter()
628        .filter(|membership| membership.context_kind == "layer")
629        .cloned()
630        .collect::<Vec<_>>();
631    let container_memberships = memberships
632        .iter()
633        .filter(|membership| membership.context_kind == "container")
634        .cloned()
635        .collect::<Vec<_>>();
636    let scope_memberships = memberships
637        .iter()
638        .filter(|membership| membership.context_kind == "scope")
639        .cloned()
640        .collect::<Vec<_>>();
641    let scope_ranges = scopes
642        .iter()
643        .map(|scope| {
644            let parsed = parse_scope_range_prelude(scope.prelude.as_str());
645            let statically_derivable = parsed.is_some();
646            StyleScopeRangeV0 {
647                context_id: scope.id.clone(),
648                root_selector: parsed.as_ref().map(|(root, _)| root.clone()),
649                limit_selector: parsed.and_then(|(_, limit)| limit),
650                statically_derivable,
651            }
652        })
653        .collect::<Vec<_>>();
654    let layer_order = layer_tree::summarize_layer_order_from_cst(source, cst);
655
656    StyleContextIndexV0 {
657        schema_version: "0",
658        product: "omena-semantic.style-context-index",
659        layer_index: StyleLayerIndexV0 {
660            statement_layers: layer_statements,
661            anonymous_layer_block_count: block_layers
662                .iter()
663                .filter(|block| block.name.is_none())
664                .count(),
665            block_layers,
666            selector_memberships: layer_memberships,
667            named_layer_count: layer_order.order_nodes.len(),
668            order_nodes: layer_order.order_nodes,
669            block_bindings: layer_order.block_bindings,
670            unresolved_topology_count: layer_order.unresolved_topology_count,
671            topology_complete: layer_order.topology_complete,
672        },
673        container_index: StyleContainerIndexV0 {
674            named_container_count: containers
675                .iter()
676                .filter(|block| block.name.is_some())
677                .count(),
678            anonymous_container_count: containers
679                .iter()
680                .filter(|block| block.name.is_none())
681                .count(),
682            containers,
683            selector_memberships: container_memberships,
684        },
685        scope_index: StyleScopeIndexV0 {
686            scoped_selector_count: scope_memberships
687                .iter()
688                .map(|membership| membership.selector_name.as_str())
689                .collect::<BTreeSet<_>>()
690                .len(),
691            scopes,
692            unresolved_range_count: scope_ranges
693                .iter()
694                .filter(|range| !range.statically_derivable)
695                .count(),
696            ranges: scope_ranges,
697            selector_memberships: scope_memberships,
698        },
699        selector_context_count: memberships.len(),
700        ready_surfaces: vec![
701            "layerIndex",
702            "containerIndex",
703            "scopeIndex",
704            "selectorContextMembership",
705        ],
706    }
707}
708
709/// Build the canonical nested cascade-layer order from the parser CST.
710pub fn summarize_style_layer_order_from_source(
711    source: &str,
712    dialect: StyleDialect,
713) -> StyleLayerIndexV0 {
714    let parsed = parse(source, dialect);
715    let cst = parsed.cst();
716    let context = summarize_style_context_index(source, &cst);
717    context.layer_index
718}
719
720fn layer_statement_facts_from_cst(source: &str, cst: &ParsedCst) -> Vec<StyleLayerStatementV0> {
721    let mut statements = Vec::new();
722    for node in cst
723        .root()
724        .descendants()
725        .filter(|node| node.kind() == SyntaxKind::LayerRule)
726    {
727        if cst_node_has_block(node) {
728            continue;
729        }
730        let range = node.text_range();
731        let byte_span = ParserByteSpanV0 {
732            start: u32::from(range.start()) as usize,
733            end: u32::from(range.end()) as usize,
734        };
735        for layer_name in node
736            .descendants()
737            .filter(|child| child.kind() == SyntaxKind::LayerName)
738            .flat_map(|child| split_layer_names(&syntax_node_text(child)))
739        {
740            statements.push(StyleLayerStatementV0 {
741                name: layer_name,
742                source_order: statements.len(),
743                byte_span,
744                range: parser_range_for_byte_span(source, byte_span),
745            });
746        }
747    }
748    statements
749}
750
751fn style_context_blocks_and_memberships_from_cst(
752    source: &str,
753    cst: &ParsedCst,
754) -> (
755    Vec<StyleContextBlockV0>,
756    Vec<StyleContextSelectorMembershipV0>,
757) {
758    let mut context_nodes = Vec::new();
759    let mut blocks = Vec::new();
760    for node in cst
761        .root()
762        .descendants()
763        .filter(|node| cst_context_kind(node.kind()).is_some() && cst_node_has_block(node))
764    {
765        let Some(context) = style_context_block_for_cst_node(source, node, blocks.len()) else {
766            continue;
767        };
768        context_nodes.push((node, context.clone()));
769        blocks.push(context);
770    }
771
772    let mut memberships = Vec::new();
773    for rule in cst
774        .root()
775        .descendants()
776        .filter(|node| node.kind() == SyntaxKind::Rule)
777    {
778        let selector_names = class_names_from_rule_node(rule);
779        if selector_names.is_empty() {
780            continue;
781        }
782        for context in cst_context_blocks_for_rule(rule, &context_nodes) {
783            for selector_name in &selector_names {
784                memberships.push(StyleContextSelectorMembershipV0 {
785                    selector_name: selector_name.clone(),
786                    context_id: context.id.clone(),
787                    context_kind: context.kind,
788                    source_order: memberships.len(),
789                });
790            }
791        }
792    }
793
794    (blocks, memberships)
795}
796
797fn style_context_block_for_cst_node(
798    source: &str,
799    node: &SyntaxNode,
800    source_order: usize,
801) -> Option<StyleContextBlockV0> {
802    let kind = cst_context_kind(node.kind())?;
803    let prelude = cst_context_prelude(node);
804    let name = match kind {
805        "layer" => split_layer_names(&prelude).into_iter().next(),
806        "container" => container_name_from_prelude(&prelude),
807        "scope" => None,
808        _ => None,
809    };
810    let header_end = cst_node_block_open_end(node)?;
811    let byte_span = ParserByteSpanV0 {
812        start: u32::from(node.text_range().start()) as usize,
813        end: header_end,
814    };
815
816    Some(StyleContextBlockV0 {
817        id: format!("{kind}:{source_order}"),
818        kind,
819        name,
820        prelude,
821        source_order,
822        byte_span,
823        range: parser_range_for_byte_span(source, byte_span),
824    })
825}
826
827fn cst_context_kind(kind: SyntaxKind) -> Option<&'static str> {
828    match kind {
829        SyntaxKind::LayerRule => Some("layer"),
830        SyntaxKind::ContainerRule => Some("container"),
831        SyntaxKind::ScopeRule => Some("scope"),
832        _ => None,
833    }
834}
835
836fn cst_context_prelude(node: &SyntaxNode) -> String {
837    if node.kind() == SyntaxKind::LayerRule {
838        return layer_rule_prelude(node);
839    }
840    let prelude_kind = match node.kind() {
841        SyntaxKind::ContainerRule => SyntaxKind::ContainerCondition,
842        SyntaxKind::ScopeRule => SyntaxKind::ScopeRange,
843        _ => return String::new(),
844    };
845    node.descendants()
846        .find(|child| child.kind() == prelude_kind)
847        .map(|child| syntax_node_text(child).trim().to_string())
848        .unwrap_or_default()
849}
850
851fn layer_rule_prelude(node: &SyntaxNode) -> String {
852    let text = syntax_node_text(node);
853    let Some(rest) = css_keyword(text.trim_start()).strip_prefix("@layer") else {
854        return String::new();
855    };
856    rest.split(['{', ';', '\n'])
857        .next()
858        .unwrap_or_default()
859        .trim()
860        .to_string()
861}
862
863fn cst_node_has_block(node: &SyntaxNode) -> bool {
864    node.descendants_with_tokens()
865        .filter_map(|element| element.into_token())
866        .any(|token| matches!(token.kind(), SyntaxKind::LeftBrace | SyntaxKind::SassIndent))
867}
868
869fn cst_node_block_open_end(node: &SyntaxNode) -> Option<usize> {
870    node.descendants_with_tokens()
871        .filter_map(|element| element.into_token())
872        .find(|token| matches!(token.kind(), SyntaxKind::LeftBrace | SyntaxKind::SassIndent))
873        .map(|token| u32::from(token.text_range().end()) as usize)
874}
875
876fn cst_context_blocks_for_rule<'a>(
877    rule: &SyntaxNode,
878    contexts: &'a [(&'a SyntaxNode, StyleContextBlockV0)],
879) -> Vec<&'a StyleContextBlockV0> {
880    contexts
881        .iter()
882        .filter(|(node, _)| {
883            rule.text_range().start() > node.text_range().start()
884                && rule.text_range().end() < node.text_range().end()
885        })
886        .map(|(_, context)| context)
887        .collect()
888}
889
890fn class_names_from_rule_node(rule: &SyntaxNode) -> Vec<String> {
891    let mut names = BTreeSet::new();
892    for child in rule.children() {
893        if matches!(
894            child.kind(),
895            SyntaxKind::DeclarationList | SyntaxKind::RuleList | SyntaxKind::SassIndentedBlock
896        ) {
897            break;
898        }
899        for class_node in child
900            .descendants()
901            .filter(|node| node.kind() == SyntaxKind::ClassSelector)
902        {
903            if let Some(name) = class_selector_name_from_cst_node(class_node) {
904                names.insert(name);
905            }
906        }
907    }
908    names.into_iter().collect()
909}
910
911fn class_selector_name_from_cst_node(node: &SyntaxNode) -> Option<String> {
912    syntax_node_text(node)
913        .trim()
914        .strip_prefix('.')
915        .filter(|name| !name.is_empty())
916        .map(ToString::to_string)
917}
918
919fn syntax_node_text(node: &SyntaxNode) -> String {
920    node.try_resolved()
921        .map(|resolved| resolved.text().to_string())
922        .unwrap_or_default()
923}
924
925fn split_layer_names(prelude: &str) -> Vec<String> {
926    prelude
927        .split(',')
928        .filter_map(|name| {
929            let name = name.trim();
930            if name.is_empty() || name == "{" {
931                None
932            } else {
933                Some(name.to_string())
934            }
935        })
936        .collect()
937}
938
939fn container_name_from_prelude(prelude: &str) -> Option<String> {
940    let trimmed = prelude.trim();
941    if trimmed.is_empty() || trimmed.starts_with('(') || trimmed.starts_with("style(") {
942        return None;
943    }
944    let name = trimmed.split_whitespace().next().unwrap_or_default().trim();
945    if css_identifier_text_is_plain(name) {
946        Some(name.to_string())
947    } else {
948        None
949    }
950}
951
952fn parse_scope_range_prelude(prelude: &str) -> Option<(String, Option<String>)> {
953    let prelude = prelude.trim();
954    if prelude.is_empty() {
955        return Some((":scope".to_string(), None));
956    }
957    let (root, limit) = match prelude.split_once(" to ") {
958        Some((root, limit)) => (root, Some(limit)),
959        None => (prelude, None),
960    };
961    let unwrap_selector = |value: &str| {
962        value
963            .trim()
964            .strip_prefix('(')
965            .and_then(|value| value.strip_suffix(')'))
966            .map(str::trim)
967            .filter(|value| !value.is_empty())
968            .map(str::to_string)
969    };
970    let limit = match limit {
971        Some(limit) => Some(unwrap_selector(limit)?),
972        None => None,
973    };
974    Some((unwrap_selector(root)?, limit))
975}
976
977fn css_identifier_text_is_plain(value: &str) -> bool {
978    let mut chars = value.chars();
979    let Some(first) = chars.next() else {
980        return false;
981    };
982    (first.is_ascii_alphabetic() || matches!(first, '_' | '-'))
983        && chars.all(|char| char.is_ascii_alphanumeric() || matches!(char, '_' | '-'))
984}
985
986fn summarize_omena_parser_selector_facts(
987    source: &str,
988    facts: &ParsedStyleFacts,
989) -> ParserIndexSelectorFactsV0 {
990    let mut names = Vec::new();
991    let mut definition_facts = Vec::new();
992    let mut bem_suffix_parent_names = BTreeSet::new();
993    let mut bem_suffix_safe_names = BTreeSet::new();
994    let mut nested_unsafe_names = BTreeSet::new();
995    let mut source_order = 0usize;
996
997    for selector in &facts.selectors {
998        if selector.kind != ParsedSelectorFactKind::Class {
999            continue;
1000        }
1001        let byte_span = parser_byte_span_for_offsets(
1002            u32::from(selector.range.start()) as usize,
1003            u32::from(selector.range.end()) as usize,
1004        );
1005        let parent_name = bem_suffix_parent_name(selector.name.as_str());
1006        let nested_safety_kind = if let Some(parent) = parent_name.clone() {
1007            bem_suffix_parent_names.insert(parent);
1008            bem_suffix_safe_names.insert(selector.name.clone());
1009            "bemSuffixSafe"
1010        } else if selector_has_parent_ampersand_class_prefix(source, byte_span.start) {
1011            nested_unsafe_names.insert(selector.name.clone());
1012            "nestedUnsafe"
1013        } else {
1014            "flat"
1015        };
1016        names.push(selector.name.clone());
1017        definition_facts.push(ParserIndexSelectorDefinitionFactV0 {
1018            name: selector.name.clone(),
1019            source_order,
1020            byte_span,
1021            range: parser_range_for_byte_span(source, byte_span),
1022            nested_safety_kind,
1023            bem_suffix_parent_name: parent_name,
1024            under_media: false,
1025            under_supports: false,
1026            under_layer: false,
1027        });
1028        source_order += 1;
1029    }
1030
1031    names.sort();
1032    names.dedup();
1033    definition_facts.sort();
1034    let bem_suffix_safe_names = bem_suffix_safe_names.into_iter().collect::<Vec<_>>();
1035    let nested_unsafe_names = nested_unsafe_names.into_iter().collect::<Vec<_>>();
1036    ParserIndexSelectorFactsV0 {
1037        names,
1038        definition_facts,
1039        bem_suffix_parent_names: bem_suffix_parent_names.into_iter().collect(),
1040        bem_suffix_safe_names: bem_suffix_safe_names.clone(),
1041        nested_unsafe_names: nested_unsafe_names.clone(),
1042        selectors_with_value_refs_names: Vec::new(),
1043        selectors_with_animation_ref_names: Vec::new(),
1044        selectors_with_animation_name_ref_names: Vec::new(),
1045        bem_suffix_count: bem_suffix_safe_names.len(),
1046        nested_safety_counts: NestedSafetyCountsV0 {
1047            flat: source_order
1048                .saturating_sub(bem_suffix_safe_names.len())
1049                .saturating_sub(nested_unsafe_names.len()),
1050            bem_suffix_safe: bem_suffix_safe_names.len(),
1051            nested_unsafe: nested_unsafe_names.len(),
1052        },
1053    }
1054}
1055
1056fn summarize_omena_parser_value_facts(facts: &ParsedStyleFacts) -> ParserIndexValueFactsV0 {
1057    let mut decl_names = BTreeSet::new();
1058    let mut ref_names = BTreeSet::new();
1059    let mut import_sources = BTreeSet::new();
1060    for value in &facts.css_module_values {
1061        match value.kind {
1062            ParsedCssModuleValueFactKind::Definition => {
1063                decl_names.insert(value.name.clone());
1064            }
1065            ParsedCssModuleValueFactKind::Reference => {
1066                ref_names.insert(value.name.clone());
1067            }
1068            ParsedCssModuleValueFactKind::ImportSource => {
1069                import_sources.insert(value.name.clone());
1070            }
1071        }
1072    }
1073    ParserIndexValueFactsV0 {
1074        decl_names: decl_names.into_iter().collect(),
1075        import_sources: import_sources.into_iter().collect(),
1076        import_alias_count: facts.css_module_value_import_edge_count,
1077        ref_names: ref_names.clone().into_iter().collect(),
1078        local_ref_names: ref_names.into_iter().collect(),
1079        ..ParserIndexValueFactsV0::default()
1080    }
1081}
1082
1083fn summarize_omena_parser_custom_property_facts(
1084    source: &str,
1085    facts: &ParsedStyleFacts,
1086    cst: &ParsedCst,
1087) -> ParserIndexCustomPropertyFactsV0 {
1088    let mut decl_names = BTreeSet::new();
1089    let mut ref_names = BTreeSet::new();
1090    let mut decl_facts = Vec::new();
1091    let mut ref_facts = Vec::new();
1092    for variable in &facts.variables {
1093        match variable.kind {
1094            ParsedVariableFactKind::CustomPropertyDeclaration => {
1095                let byte_span = parser_byte_span_for_offsets(
1096                    u32::from(variable.range.start()) as usize,
1097                    u32::from(variable.range.end()) as usize,
1098                );
1099                decl_names.insert(variable.name.clone());
1100                let context = style_context_for_cst_offset(source, cst, byte_span.start);
1101                decl_facts.push(ParserIndexCustomPropertyDeclFactV0 {
1102                    name: variable.name.clone(),
1103                    value: declaration_value_text(source, byte_span.start),
1104                    source_order: decl_facts.len(),
1105                    byte_span,
1106                    range: parser_range_for_byte_span(source, byte_span),
1107                    selector_contexts: context.selector_contexts,
1108                    condition_context: context.condition_context,
1109                    layer_names: context.layer_names,
1110                    under_media: context.under_media,
1111                    under_supports: context.under_supports,
1112                    under_layer: context.under_layer,
1113                });
1114            }
1115            ParsedVariableFactKind::CustomPropertyReference => {
1116                let byte_offset = u32::from(variable.range.start()) as usize;
1117                let context = style_context_for_cst_offset(source, cst, byte_offset);
1118                ref_names.insert(variable.name.clone());
1119                ref_facts.push(ParserIndexCustomPropertyRefFactV0 {
1120                    name: variable.name.clone(),
1121                    source_order: ref_facts.len(),
1122                    selector_contexts: context.selector_contexts,
1123                    condition_context: context.condition_context,
1124                    layer_names: context.layer_names,
1125                    under_media: context.under_media,
1126                    under_supports: context.under_supports,
1127                    under_layer: context.under_layer,
1128                });
1129            }
1130            _ => {}
1131        }
1132    }
1133    let selectors_with_refs_names = ref_facts
1134        .iter()
1135        .flat_map(|reference| reference.selector_contexts.iter().cloned())
1136        .collect::<BTreeSet<_>>();
1137    let selectors_with_refs_under_media_names = ref_facts
1138        .iter()
1139        .filter(|reference| reference.under_media)
1140        .flat_map(|reference| reference.selector_contexts.iter().cloned())
1141        .collect::<BTreeSet<_>>();
1142    let selectors_with_refs_under_supports_names = ref_facts
1143        .iter()
1144        .filter(|reference| reference.under_supports)
1145        .flat_map(|reference| reference.selector_contexts.iter().cloned())
1146        .collect::<BTreeSet<_>>();
1147    let selectors_with_refs_under_layer_names = ref_facts
1148        .iter()
1149        .filter(|reference| reference.under_layer)
1150        .flat_map(|reference| reference.selector_contexts.iter().cloned())
1151        .collect::<BTreeSet<_>>();
1152    let decl_context_selectors = decl_facts
1153        .iter()
1154        .flat_map(|declaration| declaration.selector_contexts.iter().cloned())
1155        .collect::<BTreeSet<_>>();
1156    let decl_names_under_media = decl_facts
1157        .iter()
1158        .filter(|declaration| declaration.under_media)
1159        .map(|declaration| declaration.name.clone())
1160        .collect::<BTreeSet<_>>();
1161    let decl_names_under_supports = decl_facts
1162        .iter()
1163        .filter(|declaration| declaration.under_supports)
1164        .map(|declaration| declaration.name.clone())
1165        .collect::<BTreeSet<_>>();
1166    let decl_names_under_layer = decl_facts
1167        .iter()
1168        .filter(|declaration| declaration.under_layer)
1169        .map(|declaration| declaration.name.clone())
1170        .collect::<BTreeSet<_>>();
1171
1172    ParserIndexCustomPropertyFactsV0 {
1173        decl_names: decl_names.into_iter().collect(),
1174        decl_facts,
1175        decl_context_selectors: decl_context_selectors.into_iter().collect(),
1176        decl_names_under_media: decl_names_under_media.into_iter().collect(),
1177        decl_names_under_supports: decl_names_under_supports.into_iter().collect(),
1178        decl_names_under_layer: decl_names_under_layer.into_iter().collect(),
1179        ref_names: ref_names.into_iter().collect(),
1180        ref_facts,
1181        selectors_with_refs_names: selectors_with_refs_names.into_iter().collect(),
1182        selectors_with_refs_under_media_names: selectors_with_refs_under_media_names
1183            .into_iter()
1184            .collect(),
1185        selectors_with_refs_under_supports_names: selectors_with_refs_under_supports_names
1186            .into_iter()
1187            .collect(),
1188        selectors_with_refs_under_layer_names: selectors_with_refs_under_layer_names
1189            .into_iter()
1190            .collect(),
1191    }
1192}
1193
1194fn summarize_omena_parser_sass_syntax_facts(facts: &ParsedStyleFacts) -> ParserSassSyntaxFactsV0 {
1195    let mut variable_decl_names = BTreeSet::new();
1196    let mut variable_ref_names = BTreeSet::new();
1197    let mut mixin_decl_names = BTreeSet::new();
1198    let mut mixin_include_names = BTreeSet::new();
1199    let mut function_decl_names = BTreeSet::new();
1200    let mut function_call_names = BTreeSet::new();
1201    for symbol in &facts.sass_symbols {
1202        match symbol.kind {
1203            ParsedSassSymbolFactKind::VariableDeclaration => {
1204                variable_decl_names.insert(symbol.name.clone());
1205            }
1206            ParsedSassSymbolFactKind::VariableReference => {
1207                variable_ref_names.insert(symbol.name.clone());
1208            }
1209            ParsedSassSymbolFactKind::MixinDeclaration => {
1210                mixin_decl_names.insert(symbol.name.clone());
1211            }
1212            ParsedSassSymbolFactKind::MixinInclude => {
1213                mixin_include_names.insert(symbol.name.clone());
1214            }
1215            ParsedSassSymbolFactKind::FunctionDeclaration => {
1216                function_decl_names.insert(symbol.name.clone());
1217            }
1218            ParsedSassSymbolFactKind::FunctionCall => {
1219                function_call_names.insert(symbol.name.clone());
1220            }
1221        }
1222    }
1223    let mut module_use_sources = BTreeSet::new();
1224    let mut module_use_edges = Vec::new();
1225    let mut module_forward_sources = BTreeSet::new();
1226    let mut module_import_sources = BTreeSet::new();
1227    for edge in &facts.sass_module_edges {
1228        match edge.kind {
1229            ParsedSassModuleEdgeFactKind::Use => {
1230                module_use_sources.insert(edge.source.clone());
1231                module_use_edges.push(ParserIndexSassModuleUseFactV0 {
1232                    source: edge.source.clone(),
1233                    namespace_kind: edge.namespace_kind.unwrap_or("default"),
1234                    namespace: edge.namespace.clone(),
1235                });
1236            }
1237            ParsedSassModuleEdgeFactKind::Forward => {
1238                module_forward_sources.insert(edge.source.clone());
1239            }
1240            ParsedSassModuleEdgeFactKind::Import => {
1241                module_import_sources.insert(edge.source.clone());
1242                module_use_edges.push(ParserIndexSassModuleUseFactV0 {
1243                    source: edge.source.clone(),
1244                    namespace_kind: "wildcard",
1245                    namespace: None,
1246                });
1247            }
1248        }
1249    }
1250    ParserSassSyntaxFactsV0 {
1251        variable_decl_names: variable_decl_names.into_iter().collect(),
1252        variable_parameter_names: Vec::new(),
1253        variable_ref_names: variable_ref_names.into_iter().collect(),
1254        mixin_decl_names: mixin_decl_names.into_iter().collect(),
1255        mixin_include_names: mixin_include_names.into_iter().collect(),
1256        function_decl_names: function_decl_names.into_iter().collect(),
1257        function_call_names: function_call_names.into_iter().collect(),
1258        module_use_sources: module_use_sources.into_iter().collect(),
1259        module_use_edges,
1260        module_forward_sources: module_forward_sources.into_iter().collect(),
1261        module_import_sources: module_import_sources.into_iter().collect(),
1262    }
1263}
1264
1265fn summarize_omena_parser_keyframe_facts(facts: &ParsedStyleFacts) -> ParserIndexKeyframesFactsV0 {
1266    let mut names = BTreeSet::new();
1267    let mut animation_ref_names = BTreeSet::new();
1268    for animation in &facts.animations {
1269        match animation.kind {
1270            ParsedAnimationFactKind::KeyframesDeclaration => {
1271                names.insert(animation.name.clone());
1272            }
1273            ParsedAnimationFactKind::AnimationNameReference => {
1274                animation_ref_names.insert(animation.name.clone());
1275            }
1276        }
1277    }
1278    ParserIndexKeyframesFactsV0 {
1279        names: names.into_iter().collect(),
1280        animation_ref_names: animation_ref_names.clone().into_iter().collect(),
1281        animation_name_ref_names: animation_ref_names.into_iter().collect(),
1282        ..ParserIndexKeyframesFactsV0::default()
1283    }
1284}
1285
1286fn summarize_omena_parser_composes_facts(facts: &ParsedStyleFacts) -> ParserIndexComposesFactsV0 {
1287    let mut local_selector_names = BTreeSet::new();
1288    let mut imported_selector_names = BTreeSet::new();
1289    let mut global_selector_names = BTreeSet::new();
1290    let mut import_sources = BTreeSet::new();
1291    for edge in &facts.css_module_composes_edges {
1292        match edge.kind {
1293            ParsedCssModuleComposesEdgeKind::Local => {
1294                local_selector_names.extend(edge.target_names.iter().cloned());
1295            }
1296            ParsedCssModuleComposesEdgeKind::External => {
1297                imported_selector_names.extend(edge.target_names.iter().cloned());
1298                if let Some(source) = &edge.import_source {
1299                    import_sources.insert(source.clone());
1300                }
1301            }
1302            ParsedCssModuleComposesEdgeKind::Global => {
1303                global_selector_names.extend(edge.target_names.iter().cloned());
1304            }
1305        }
1306    }
1307    for composes in &facts.css_module_composes {
1308        if composes.kind == ParsedCssModuleComposesFactKind::ImportSource {
1309            import_sources.insert(composes.name.clone());
1310        }
1311    }
1312    let local_selector_names = local_selector_names.into_iter().collect::<Vec<_>>();
1313    let imported_selector_names = imported_selector_names.into_iter().collect::<Vec<_>>();
1314    let global_selector_names = global_selector_names.into_iter().collect::<Vec<_>>();
1315    ParserIndexComposesFactsV0 {
1316        class_name_count: local_selector_names.len()
1317            + imported_selector_names.len()
1318            + global_selector_names.len(),
1319        local_class_name_count: local_selector_names.len(),
1320        imported_class_name_count: imported_selector_names.len(),
1321        global_class_name_count: global_selector_names.len(),
1322        local_selector_names,
1323        imported_selector_names,
1324        global_selector_names,
1325        import_sources: import_sources.into_iter().collect(),
1326        ..ParserIndexComposesFactsV0::default()
1327    }
1328}
1329
1330fn summarize_omena_parser_custom_property_semantic_facts(
1331    facts: &ParserIndexCustomPropertyFactsV0,
1332) -> StyleCustomPropertySemanticFactsV0 {
1333    let mut resolved_ref_names = BTreeSet::new();
1334    let mut unresolved_ref_names = BTreeSet::new();
1335    for reference in &facts.ref_facts {
1336        if facts
1337            .decl_facts
1338            .iter()
1339            .any(|declaration| custom_property_context_matches(declaration, reference))
1340        {
1341            resolved_ref_names.insert(reference.name.clone());
1342        } else {
1343            unresolved_ref_names.insert(reference.name.clone());
1344        }
1345    }
1346    StyleCustomPropertySemanticFactsV0 {
1347        decl_names: facts.decl_names.clone(),
1348        ref_names: facts.ref_names.clone(),
1349        resolved_ref_names: resolved_ref_names.into_iter().collect(),
1350        unresolved_ref_names: unresolved_ref_names.into_iter().collect(),
1351        selectors_with_refs_names: facts.selectors_with_refs_names.clone(),
1352    }
1353}
1354
1355struct SassSelectorResolution {
1356    resolved_variable_ref_selectors: Vec<String>,
1357    unresolved_variable_ref_selectors: Vec<String>,
1358    resolved_mixin_include_selectors: Vec<String>,
1359    unresolved_mixin_include_selectors: Vec<String>,
1360}
1361
1362fn summarize_omena_parser_sass_selector_resolution(
1363    facts: &ParsedStyleFacts,
1364    resolution: &ParserIndexSassSameFileResolutionFactsV0,
1365    cst: &ParsedCst,
1366) -> SassSelectorResolution {
1367    let resolved_variables = resolution
1368        .resolved_variable_ref_names
1369        .iter()
1370        .cloned()
1371        .collect::<BTreeSet<_>>();
1372    let resolved_mixins = resolution
1373        .resolved_mixin_include_names
1374        .iter()
1375        .cloned()
1376        .collect::<BTreeSet<_>>();
1377    let mut resolved_variable_ref_selectors = BTreeSet::new();
1378    let mut unresolved_variable_ref_selectors = BTreeSet::new();
1379    let mut resolved_mixin_include_selectors = BTreeSet::new();
1380    let mut unresolved_mixin_include_selectors = BTreeSet::new();
1381
1382    for symbol in &facts.sass_symbols {
1383        match symbol.kind {
1384            ParsedSassSymbolFactKind::VariableReference => {
1385                let selector = semantic_selector_name_for_cst_offset(
1386                    cst,
1387                    u32::from(symbol.range.start()) as usize,
1388                );
1389                let Some(selector) = selector else {
1390                    continue;
1391                };
1392                if resolved_variables.contains(&symbol.name) {
1393                    resolved_variable_ref_selectors.insert(selector);
1394                } else {
1395                    unresolved_variable_ref_selectors.insert(selector);
1396                }
1397            }
1398            ParsedSassSymbolFactKind::MixinInclude => {
1399                let selector = semantic_selector_name_for_cst_offset(
1400                    cst,
1401                    u32::from(symbol.range.start()) as usize,
1402                );
1403                let Some(selector) = selector else {
1404                    continue;
1405                };
1406                if resolved_mixins.contains(&symbol.name) {
1407                    resolved_mixin_include_selectors.insert(selector);
1408                } else {
1409                    unresolved_mixin_include_selectors.insert(selector);
1410                }
1411            }
1412            _ => {}
1413        }
1414    }
1415
1416    SassSelectorResolution {
1417        resolved_variable_ref_selectors: resolved_variable_ref_selectors.into_iter().collect(),
1418        unresolved_variable_ref_selectors: unresolved_variable_ref_selectors.into_iter().collect(),
1419        resolved_mixin_include_selectors: resolved_mixin_include_selectors.into_iter().collect(),
1420        unresolved_mixin_include_selectors: unresolved_mixin_include_selectors
1421            .into_iter()
1422            .collect(),
1423    }
1424}
1425
1426fn custom_property_context_matches(
1427    declaration: &ParserIndexCustomPropertyDeclFactV0,
1428    reference: &ParserIndexCustomPropertyRefFactV0,
1429) -> bool {
1430    if declaration.name != reference.name {
1431        return false;
1432    }
1433    if declaration.under_media && !reference.under_media {
1434        return false;
1435    }
1436    if declaration.under_supports && !reference.under_supports {
1437        return false;
1438    }
1439    if declaration.under_layer && !reference.under_layer {
1440        return false;
1441    }
1442    if declaration.selector_contexts.is_empty() {
1443        return true;
1444    }
1445    declaration.selector_contexts.iter().any(|selector| {
1446        !matches!(
1447            selector_context_witness_for_declaration(selector, &reference.selector_contexts)
1448                .verdict,
1449            SelectorMatchVerdict::No
1450        )
1451    })
1452}
1453
1454fn summarize_omena_parser_sass_same_file_resolution(
1455    facts: &ParserSassSyntaxFactsV0,
1456) -> ParserIndexSassSameFileResolutionFactsV0 {
1457    let variable_targets = facts
1458        .variable_decl_names
1459        .iter()
1460        .chain(facts.variable_parameter_names.iter())
1461        .cloned()
1462        .collect::<BTreeSet<_>>();
1463    let mixin_targets = facts
1464        .mixin_decl_names
1465        .iter()
1466        .cloned()
1467        .collect::<BTreeSet<_>>();
1468    let function_targets = facts
1469        .function_decl_names
1470        .iter()
1471        .cloned()
1472        .collect::<BTreeSet<_>>();
1473
1474    ParserIndexSassSameFileResolutionFactsV0 {
1475        resolved_variable_ref_names: names_matching(&facts.variable_ref_names, &variable_targets),
1476        unresolved_variable_ref_names: names_not_matching(
1477            &facts.variable_ref_names,
1478            &variable_targets,
1479        ),
1480        resolved_mixin_include_names: names_matching(&facts.mixin_include_names, &mixin_targets),
1481        unresolved_mixin_include_names: names_not_matching(
1482            &facts.mixin_include_names,
1483            &mixin_targets,
1484        ),
1485        resolved_function_call_names: names_matching(&facts.function_call_names, &function_targets),
1486    }
1487}
1488
1489fn names_matching(names: &[String], targets: &BTreeSet<String>) -> Vec<String> {
1490    names
1491        .iter()
1492        .filter(|name| targets.contains(*name))
1493        .cloned()
1494        .collect()
1495}
1496
1497fn names_not_matching(names: &[String], targets: &BTreeSet<String>) -> Vec<String> {
1498    names
1499        .iter()
1500        .filter(|name| !targets.contains(*name))
1501        .cloned()
1502        .collect()
1503}
1504
1505fn bem_suffix_parent_name(name: &str) -> Option<String> {
1506    let marker = name.find("__").or_else(|| name.find("--"))?;
1507    (marker > 0).then(|| name[..marker].to_string())
1508}
1509
1510fn selector_has_parent_ampersand_class_prefix(source: &str, selector_start: usize) -> bool {
1511    let bytes = source.as_bytes();
1512    if selector_start >= bytes.len() {
1513        return false;
1514    }
1515    let dot_index = if bytes[selector_start] == b'.' {
1516        selector_start
1517    } else {
1518        match previous_non_whitespace_byte_index(bytes, selector_start) {
1519            Some(index) if bytes[index] == b'.' => index,
1520            _ => return false,
1521        }
1522    };
1523    matches!(
1524        previous_non_whitespace_byte_index(bytes, dot_index),
1525        Some(index) if bytes[index] == b'&'
1526    )
1527}
1528
1529#[derive(Debug, Clone, Default)]
1530struct StyleOffsetContext {
1531    selector_contexts: Vec<String>,
1532    under_media: bool,
1533    under_supports: bool,
1534    under_layer: bool,
1535    layer_names: Vec<String>,
1536    condition_context: Vec<String>,
1537}
1538
1539fn style_context_for_cst_offset(
1540    source: &str,
1541    cst: &ParsedCst,
1542    byte_offset: usize,
1543) -> StyleOffsetContext {
1544    let mut context = StyleOffsetContext::default();
1545    for node in cst
1546        .root()
1547        .descendants()
1548        .filter(|node| cst_node_contains_byte_offset(node, byte_offset))
1549    {
1550        match node.kind() {
1551            SyntaxKind::Rule => {
1552                if let Some(selector) = rule_selector_text_from_cst_node(node) {
1553                    context.selector_contexts.push(selector);
1554                }
1555            }
1556            SyntaxKind::MediaRule => {
1557                context.under_media = true;
1558                if let Some(header) = cst_at_rule_header_text(source, node) {
1559                    context.condition_context.push(header);
1560                }
1561            }
1562            SyntaxKind::SupportsRule => {
1563                context.under_supports = true;
1564                if let Some(header) = cst_at_rule_header_text(source, node) {
1565                    context.condition_context.push(header);
1566                }
1567            }
1568            SyntaxKind::LayerRule => {
1569                if cst_node_has_block(node) {
1570                    context.under_layer = true;
1571                    context
1572                        .layer_names
1573                        .extend(split_layer_names(&cst_context_prelude(node)));
1574                }
1575            }
1576            kind if cst_non_layer_condition_kind(kind) => {
1577                if let Some(header) = cst_at_rule_header_text(source, node)
1578                    && css_keyword(&header).strip_prefix("@layer").is_none()
1579                {
1580                    context.condition_context.push(header);
1581                }
1582            }
1583            _ => {}
1584        }
1585    }
1586    context
1587}
1588
1589fn declaration_value_text(source: &str, offset: usize) -> String {
1590    let span = declaration_statement_byte_span_for_offset(source, offset);
1591    let Some(statement) = source.get(span.start..span.end) else {
1592        return String::new();
1593    };
1594    let Some(colon) = statement.find(':') else {
1595        return String::new();
1596    };
1597    statement[colon + 1..]
1598        .trim()
1599        .trim_end_matches(';')
1600        .trim()
1601        .to_string()
1602}
1603
1604fn declaration_statement_byte_span_for_offset(source: &str, offset: usize) -> ParserByteSpanV0 {
1605    let start = source
1606        .get(..offset)
1607        .and_then(|before| before.rfind(['{', ';']).map(|index| index + 1))
1608        .unwrap_or(offset);
1609    let end = source
1610        .get(offset..)
1611        .and_then(|rest| {
1612            let semicolon = rest.find(';');
1613            let close = rest.find('}');
1614            match (semicolon, close) {
1615                (Some(semicolon), Some(close)) => Some(offset + semicolon.min(close)),
1616                (Some(semicolon), None) => Some(offset + semicolon + 1),
1617                (None, Some(close)) => Some(offset + close),
1618                (None, None) => None,
1619            }
1620        })
1621        .unwrap_or(source.len());
1622    ParserByteSpanV0 { start, end }
1623}
1624
1625fn semantic_selector_name_for_cst_offset(cst: &ParsedCst, byte_offset: usize) -> Option<String> {
1626    cst.root()
1627        .descendants()
1628        .filter(|node| {
1629            node.kind() == SyntaxKind::Rule && cst_node_contains_byte_offset(node, byte_offset)
1630        })
1631        .filter_map(last_class_selector_name_from_rule_node)
1632        .last()
1633}
1634
1635fn cst_node_contains_byte_offset(node: &SyntaxNode, byte_offset: usize) -> bool {
1636    let range = node.text_range();
1637    let start = u32::from(range.start()) as usize;
1638    let end = u32::from(range.end()) as usize;
1639    start <= byte_offset && byte_offset < end
1640}
1641
1642fn cst_non_layer_condition_kind(kind: SyntaxKind) -> bool {
1643    matches!(
1644        kind,
1645        SyntaxKind::ContainerRule
1646            | SyntaxKind::ScopeRule
1647            | SyntaxKind::AtRule
1648            | SyntaxKind::KeyframesRule
1649            | SyntaxKind::FontFaceRule
1650            | SyntaxKind::PageRule
1651            | SyntaxKind::StartingStyleRule
1652            | SyntaxKind::PageMarginRule
1653            | SyntaxKind::CounterStyleRule
1654            | SyntaxKind::FontPaletteValuesRule
1655            | SyntaxKind::ColorProfileRule
1656            | SyntaxKind::PositionTryRule
1657            | SyntaxKind::FontFeatureValuesRule
1658            | SyntaxKind::FontFeatureValuesStylisticRule
1659            | SyntaxKind::FontFeatureValuesStylesetRule
1660            | SyntaxKind::FontFeatureValuesCharacterVariantRule
1661            | SyntaxKind::FontFeatureValuesSwashRule
1662            | SyntaxKind::FontFeatureValuesOrnamentsRule
1663            | SyntaxKind::FontFeatureValuesAnnotationRule
1664            | SyntaxKind::FontFeatureValuesHistoricalFormsRule
1665            | SyntaxKind::ViewTransitionRule
1666            | SyntaxKind::WhenRule
1667            | SyntaxKind::ElseRule
1668            | SyntaxKind::IfRule
1669    )
1670}
1671
1672fn cst_at_rule_header_text(source: &str, node: &SyntaxNode) -> Option<String> {
1673    let start = u32::from(node.text_range().start()) as usize;
1674    let end = cst_node_block_open_start(node)?;
1675    source
1676        .get(start..end)
1677        .map(normalized_condition_header)
1678        .filter(|header| !header.is_empty())
1679}
1680
1681fn cst_node_block_open_start(node: &SyntaxNode) -> Option<usize> {
1682    node.descendants_with_tokens()
1683        .filter_map(|element| element.into_token())
1684        .find(|token| matches!(token.kind(), SyntaxKind::LeftBrace | SyntaxKind::SassIndent))
1685        .map(|token| u32::from(token.text_range().start()) as usize)
1686}
1687
1688fn rule_selector_text_from_cst_node(node: &SyntaxNode) -> Option<String> {
1689    let mut selector = String::new();
1690    for child in node.children() {
1691        if matches!(
1692            child.kind(),
1693            SyntaxKind::DeclarationList | SyntaxKind::RuleList | SyntaxKind::SassIndentedBlock
1694        ) {
1695            break;
1696        }
1697        selector.push_str(&syntax_node_text(child));
1698    }
1699    let selector = selector.trim();
1700    (!selector.is_empty()).then(|| selector.to_string())
1701}
1702
1703fn last_class_selector_name_from_rule_node(node: &SyntaxNode) -> Option<String> {
1704    let mut last = None;
1705    for child in node.children() {
1706        if matches!(
1707            child.kind(),
1708            SyntaxKind::DeclarationList | SyntaxKind::RuleList | SyntaxKind::SassIndentedBlock
1709        ) {
1710            break;
1711        }
1712        for class_node in child
1713            .descendants()
1714            .filter(|node| node.kind() == SyntaxKind::ClassSelector)
1715        {
1716            last = class_selector_name_from_cst_node(class_node);
1717        }
1718    }
1719    last
1720}
1721
1722fn normalized_condition_header(header: &str) -> String {
1723    header.split_whitespace().collect::<Vec<_>>().join(" ")
1724}
1725
1726fn previous_non_whitespace_byte_index(bytes: &[u8], before: usize) -> Option<usize> {
1727    let mut index = before.checked_sub(1)?;
1728    loop {
1729        if !bytes[index].is_ascii_whitespace() {
1730            return Some(index);
1731        }
1732        index = index.checked_sub(1)?;
1733    }
1734}
1735
1736fn parser_byte_span_for_offsets(start: usize, end: usize) -> ParserByteSpanV0 {
1737    ParserByteSpanV0 { start, end }
1738}
1739
1740fn parser_range_for_byte_span(source: &str, span: ParserByteSpanV0) -> ParserRangeV0 {
1741    ParserRangeV0 {
1742        start: parser_position_for_byte_offset(source, span.start),
1743        end: parser_position_for_byte_offset(source, span.end),
1744    }
1745}
1746
1747fn parser_position_for_byte_offset(source: &str, byte_offset: usize) -> ParserPositionV0 {
1748    // LSP positions count UTF-16 code units per line, not raw bytes. Walk chars
1749    // and accumulate `len_utf16()` so non-ASCII source produces correct columns,
1750    // matching the canonical helpers in omena-query/src/style.rs and
1751    // omena-lsp-server/src/protocol.rs (previously this used a raw byte offset,
1752    // which diverged on multi-byte characters).
1753    let clamped_offset = byte_offset.min(source.len());
1754    let mut line = 0usize;
1755    let mut character = 0usize;
1756
1757    for (index, ch) in source.char_indices() {
1758        if index >= clamped_offset {
1759            break;
1760        }
1761        if ch == '\n' {
1762            line += 1;
1763            character = 0;
1764        } else {
1765            character += ch.len_utf16();
1766        }
1767    }
1768
1769    ParserPositionV0 { line, character }
1770}
1771
1772fn dialect_for_style_path(style_path: &str) -> Option<StyleDialect> {
1773    if style_path.ends_with(".sass") {
1774        Some(StyleDialect::Sass)
1775    } else if style_path.ends_with(".scss") {
1776        Some(StyleDialect::Scss)
1777    } else if style_path.ends_with(".less") {
1778        Some(StyleDialect::Less)
1779    } else if style_path.ends_with(".css") {
1780        Some(StyleDialect::Css)
1781    } else {
1782        None
1783    }
1784}
1785
1786fn omena_parser_dialect_for_style_path(style_path: &str) -> StyleDialect {
1787    dialect_for_style_path(style_path).unwrap_or(StyleDialect::Css)
1788}
1789
1790fn omena_parser_dialect_label(dialect: StyleDialect) -> &'static str {
1791    match dialect {
1792        StyleDialect::Css => "css",
1793        StyleDialect::Scss => "scss",
1794        StyleDialect::Sass => "sass",
1795        StyleDialect::Less => "less",
1796    }
1797}
1798
1799#[cfg(test)]
1800mod tests;