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