1use 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
709pub 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 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;