Skip to main content

engine_style_parser/
lib.rs

1use serde::Serialize;
2use std::collections::{BTreeMap, BTreeSet};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum StyleLanguage {
6    Css,
7    Scss,
8    Less,
9}
10
11impl StyleLanguage {
12    pub fn from_module_path(path: &str) -> Option<Self> {
13        if path.ends_with(".module.css") {
14            Some(Self::Css)
15        } else if path.ends_with(".module.scss") {
16            Some(Self::Scss)
17        } else if path.ends_with(".module.less") {
18            Some(Self::Less)
19        } else {
20            None
21        }
22    }
23
24    fn supports_line_comments(self) -> bool {
25        matches!(self, Self::Scss | Self::Less)
26    }
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct TextSpan {
31    pub start: usize,
32    pub end: usize,
33}
34
35impl TextSpan {
36    fn new(start: usize, end: usize) -> Self {
37        Self { start, end }
38    }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum TokenKind {
43    Whitespace,
44    Ident,
45    Number,
46    String,
47    LineComment,
48    BlockComment,
49    Dot,
50    Ampersand,
51    Hash,
52    Colon,
53    Semicolon,
54    Comma,
55    At,
56    OpenBrace,
57    CloseBrace,
58    OpenParen,
59    CloseParen,
60    OpenBracket,
61    CloseBracket,
62    InterpolationStart,
63    Other,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct Token {
68    pub kind: TokenKind,
69    pub span: TextSpan,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct ParseDiagnostic {
74    pub message: String,
75    pub span: TextSpan,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum SyntaxNodeKind {
80    Rule,
81    AtRule,
82    Declaration,
83    Comment,
84    Unknown,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct SyntaxNode {
89    pub kind: SyntaxNodeKind,
90    pub span: TextSpan,
91    pub header_span: Option<TextSpan>,
92    pub payload: Option<SyntaxNodePayload>,
93    pub children: Vec<SyntaxNode>,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub enum SyntaxNodePayload {
98    Rule(RulePayload),
99    AtRule(AtRulePayload),
100    Declaration(DeclarationPayload),
101    Comment(CommentPayload),
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct RulePayload {
106    pub prelude: String,
107    pub selector_groups: Vec<SelectorGroup>,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct SelectorGroup {
112    pub raw: String,
113    pub segments: Vec<SelectorSegment>,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
117pub enum SelectorSegment {
118    ClassName(String),
119    Ampersand,
120    BemSuffix(String),
121    AmpersandSuffix(String),
122    Pseudo(String),
123    Combinator(String),
124    Other(String),
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct AtRulePayload {
129    pub kind: AtRuleKind,
130    pub name: String,
131    pub params: String,
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub enum AtRuleKind {
136    Media,
137    Supports,
138    Layer,
139    Keyframes,
140    Value,
141    AtRoot,
142    Mixin,
143    Include,
144    Function,
145    Use,
146    Forward,
147    Import,
148    Generic,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct DeclarationPayload {
153    pub property: String,
154    pub value: String,
155}
156
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct CommentPayload {
159    pub text: String,
160}
161
162#[derive(Debug, Clone, PartialEq, Eq)]
163pub struct Stylesheet {
164    pub language: StyleLanguage,
165    pub source: String,
166    pub tokens: Vec<Token>,
167    pub nodes: Vec<SyntaxNode>,
168    pub diagnostics: Vec<ParseDiagnostic>,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
172#[serde(rename_all = "camelCase")]
173pub struct ParserParityLiteSummaryV0 {
174    pub schema_version: &'static str,
175    pub language: &'static str,
176    pub selector_names: Vec<String>,
177    pub keyframes_names: Vec<String>,
178    pub value_decl_names: Vec<String>,
179    pub diagnostic_count: usize,
180    pub rule_count: usize,
181    pub declaration_count: usize,
182    pub grouped_selector_count: usize,
183    pub max_nesting_depth: usize,
184    pub at_rule_kind_counts: AtRuleKindCountsV0,
185    pub declaration_kind_counts: DeclarationKindCountsV0,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
189#[serde(rename_all = "camelCase")]
190pub struct ParserIndexSummaryV0 {
191    pub schema_version: &'static str,
192    pub language: &'static str,
193    pub selectors: ParserIndexSelectorFactsV0,
194    pub values: ParserIndexValueFactsV0,
195    pub custom_properties: ParserIndexCustomPropertyFactsV0,
196    pub sass: ParserIndexSassFactsV0,
197    pub keyframes: ParserIndexKeyframesFactsV0,
198    pub composes: ParserIndexComposesFactsV0,
199    pub wrappers: ParserIndexWrapperFactsV0,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
203#[serde(rename_all = "camelCase")]
204pub struct ParserSemanticBoundarySummaryV0 {
205    pub schema_version: &'static str,
206    pub language: &'static str,
207    pub parser_facts: ParserBoundarySyntaxFactsV0,
208    pub semantic_facts: StyleSemanticFactsV0,
209}
210
211#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
212#[serde(rename_all = "camelCase")]
213pub struct ParserBoundarySyntaxFactsV0 {
214    pub lossless_cst: ParserLosslessCstFactsV0,
215    pub selectors: ParserIndexSelectorFactsV0,
216    pub values: ParserIndexValueFactsV0,
217    pub custom_properties: ParserIndexCustomPropertyFactsV0,
218    pub sass: ParserSassSyntaxFactsV0,
219    pub keyframes: ParserIndexKeyframesFactsV0,
220    pub composes: ParserIndexComposesFactsV0,
221    pub wrappers: ParserIndexWrapperFactsV0,
222}
223
224#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
225#[serde(rename_all = "camelCase")]
226pub struct ParserLosslessCstFactsV0 {
227    pub source_byte_len: usize,
228    pub token_count: usize,
229    pub root_node_count: usize,
230    pub diagnostic_count: usize,
231    pub all_token_spans_within_source: bool,
232    pub all_node_spans_within_source: bool,
233}
234
235#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
236#[serde(rename_all = "camelCase")]
237pub struct ParserSassSyntaxFactsV0 {
238    pub variable_decl_names: Vec<String>,
239    pub variable_parameter_names: Vec<String>,
240    pub variable_ref_names: Vec<String>,
241    pub mixin_decl_names: Vec<String>,
242    pub mixin_include_names: Vec<String>,
243    pub function_decl_names: Vec<String>,
244    pub function_call_names: Vec<String>,
245    pub module_use_sources: Vec<String>,
246    pub module_use_edges: Vec<ParserIndexSassModuleUseFactV0>,
247    pub module_forward_sources: Vec<String>,
248    pub module_import_sources: Vec<String>,
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
252#[serde(rename_all = "camelCase")]
253pub struct StyleSemanticFactsV0 {
254    pub selector_identity: StyleSelectorIdentityFactsV0,
255    pub custom_properties: StyleCustomPropertySemanticFactsV0,
256    pub sass: StyleSassSemanticFactsV0,
257}
258
259#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
260#[serde(rename_all = "camelCase")]
261pub struct StyleSelectorIdentityFactsV0 {
262    pub canonical_names: Vec<String>,
263    pub bem_suffix_safe_names: Vec<String>,
264    pub bem_suffix_parent_names: Vec<String>,
265    pub nested_unsafe_names: Vec<String>,
266    pub nested_safety_counts: NestedSafetyCountsV0,
267}
268
269#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
270#[serde(rename_all = "camelCase")]
271pub struct StyleCustomPropertySemanticFactsV0 {
272    pub decl_names: Vec<String>,
273    pub ref_names: Vec<String>,
274    pub resolved_ref_names: Vec<String>,
275    pub unresolved_ref_names: Vec<String>,
276    pub selectors_with_refs_names: Vec<String>,
277}
278
279#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
280#[serde(rename_all = "camelCase")]
281pub struct StyleSassSemanticFactsV0 {
282    pub selector_symbol_facts: Vec<ParserIndexSassSelectorSymbolFactV0>,
283    pub selectors_with_resolved_variable_refs_names: Vec<String>,
284    pub selectors_with_unresolved_variable_refs_names: Vec<String>,
285    pub selectors_with_resolved_mixin_includes_names: Vec<String>,
286    pub selectors_with_unresolved_mixin_includes_names: Vec<String>,
287    pub selectors_with_function_calls_names: Vec<String>,
288    pub same_file_resolution: ParserIndexSassSameFileResolutionFactsV0,
289}
290
291#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
292#[serde(rename_all = "camelCase")]
293pub struct ParserCanonicalCandidateBundleV0 {
294    pub schema_version: &'static str,
295    pub language: &'static str,
296    pub parity_lite: ParserParityLiteSummaryV0,
297    pub css_modules_intermediate: ParserIndexSummaryV0,
298}
299
300#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
301#[serde(rename_all = "camelCase")]
302pub struct ParserEvaluatorCandidateV0 {
303    pub kind: &'static str,
304    pub selector_name: String,
305    pub nested_safety_kind: &'static str,
306    #[serde(skip_serializing_if = "Option::is_none")]
307    pub bem_suffix_parent_name: Option<String>,
308    pub under_media: bool,
309    pub under_supports: bool,
310    pub under_layer: bool,
311    pub has_value_refs: bool,
312    pub has_local_value_refs: bool,
313    pub has_imported_value_refs: bool,
314    pub has_custom_property_refs: bool,
315    pub has_animation_ref: bool,
316    pub has_animation_name_ref: bool,
317    pub has_composes: bool,
318    pub has_local_composes: bool,
319    pub has_imported_composes: bool,
320    pub has_global_composes: bool,
321}
322
323#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
324#[serde(rename_all = "camelCase")]
325pub struct ParserEvaluatorCandidatesV0 {
326    pub schema_version: &'static str,
327    pub language: &'static str,
328    pub results: Vec<ParserEvaluatorCandidateV0>,
329}
330
331#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
332#[serde(rename_all = "camelCase")]
333pub struct ParserCanonicalProducerSignalV0 {
334    pub schema_version: &'static str,
335    pub language: &'static str,
336    pub canonical_candidate: ParserCanonicalCandidateBundleV0,
337    pub evaluator_candidates: ParserEvaluatorCandidatesV0,
338    pub public_product_gate: ParserPublicProductGateSignalV0,
339}
340
341#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
342#[serde(rename_all = "camelCase")]
343pub struct ParserPublicProductGateSignalV0 {
344    pub canonical_candidate_command: &'static str,
345    pub consumer_boundary_command: &'static str,
346    pub public_product_gate_command: &'static str,
347    pub included_in_parser_lane: bool,
348    pub included_in_rust_lane_bundle: bool,
349    pub included_in_rust_release_bundle: bool,
350}
351
352#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
353#[serde(rename_all = "camelCase")]
354pub struct ParserIndexSelectorFactsV0 {
355    pub names: Vec<String>,
356    pub bem_suffix_parent_names: Vec<String>,
357    pub bem_suffix_safe_names: Vec<String>,
358    pub nested_unsafe_names: Vec<String>,
359    pub selectors_with_value_refs_names: Vec<String>,
360    pub selectors_with_animation_ref_names: Vec<String>,
361    pub selectors_with_animation_name_ref_names: Vec<String>,
362    pub bem_suffix_count: usize,
363    pub nested_safety_counts: NestedSafetyCountsV0,
364}
365
366#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
367#[serde(rename_all = "camelCase")]
368pub struct ParserIndexValueFactsV0 {
369    pub decl_names: Vec<String>,
370    pub decl_names_with_local_refs: Vec<String>,
371    pub decl_names_with_imported_refs: Vec<String>,
372    pub import_names: Vec<String>,
373    pub import_sources: Vec<String>,
374    pub import_alias_count: usize,
375    pub ref_names: Vec<String>,
376    pub local_ref_names: Vec<String>,
377    pub imported_ref_names: Vec<String>,
378    pub imported_ref_sources: Vec<String>,
379    pub declaration_ref_names: Vec<String>,
380    pub declaration_imported_ref_sources: Vec<String>,
381    pub value_decl_ref_names: Vec<String>,
382    pub value_decl_imported_ref_sources: Vec<String>,
383    pub selectors_with_refs_names: Vec<String>,
384    pub selectors_with_local_refs_names: Vec<String>,
385    pub selectors_with_imported_refs_names: Vec<String>,
386    pub selectors_with_refs_under_media_names: Vec<String>,
387    pub selectors_with_refs_under_supports_names: Vec<String>,
388    pub selectors_with_refs_under_layer_names: Vec<String>,
389    pub selectors_with_local_refs_under_media_names: Vec<String>,
390    pub selectors_with_local_refs_under_supports_names: Vec<String>,
391    pub selectors_with_local_refs_under_layer_names: Vec<String>,
392    pub selectors_with_imported_refs_under_media_names: Vec<String>,
393    pub selectors_with_imported_refs_under_supports_names: Vec<String>,
394    pub selectors_with_imported_refs_under_layer_names: Vec<String>,
395}
396
397#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
398#[serde(rename_all = "camelCase")]
399pub struct ParserIndexCustomPropertyFactsV0 {
400    pub decl_names: Vec<String>,
401    pub decl_facts: Vec<ParserIndexCustomPropertyDeclFactV0>,
402    pub decl_context_selectors: Vec<String>,
403    pub decl_names_under_media: Vec<String>,
404    pub decl_names_under_supports: Vec<String>,
405    pub decl_names_under_layer: Vec<String>,
406    pub ref_names: Vec<String>,
407    pub ref_facts: Vec<ParserIndexCustomPropertyRefFactV0>,
408    pub selectors_with_refs_names: Vec<String>,
409    pub selectors_with_refs_under_media_names: Vec<String>,
410    pub selectors_with_refs_under_supports_names: Vec<String>,
411    pub selectors_with_refs_under_layer_names: Vec<String>,
412}
413
414#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Default)]
415#[serde(rename_all = "camelCase")]
416pub struct ParserIndexCustomPropertyDeclFactV0 {
417    pub name: String,
418    pub source_order: usize,
419    pub selector_contexts: Vec<String>,
420    pub under_media: bool,
421    pub under_supports: bool,
422    pub under_layer: bool,
423}
424
425#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Default)]
426#[serde(rename_all = "camelCase")]
427pub struct ParserIndexCustomPropertyRefFactV0 {
428    pub name: String,
429    pub source_order: usize,
430    pub selector_contexts: Vec<String>,
431    pub under_media: bool,
432    pub under_supports: bool,
433    pub under_layer: bool,
434}
435
436#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
437#[serde(rename_all = "camelCase")]
438pub struct ParserIndexSassFactsV0 {
439    pub variable_decl_names: Vec<String>,
440    pub variable_parameter_names: Vec<String>,
441    pub variable_ref_names: Vec<String>,
442    pub selectors_with_variable_refs_names: Vec<String>,
443    pub selectors_with_resolved_variable_refs_names: Vec<String>,
444    pub selectors_with_unresolved_variable_refs_names: Vec<String>,
445    pub mixin_decl_names: Vec<String>,
446    pub mixin_include_names: Vec<String>,
447    pub selectors_with_mixin_includes_names: Vec<String>,
448    pub selectors_with_resolved_mixin_includes_names: Vec<String>,
449    pub selectors_with_unresolved_mixin_includes_names: Vec<String>,
450    pub function_decl_names: Vec<String>,
451    pub function_call_names: Vec<String>,
452    pub selectors_with_function_calls_names: Vec<String>,
453    pub selector_symbol_facts: Vec<ParserIndexSassSelectorSymbolFactV0>,
454    pub module_use_sources: Vec<String>,
455    pub module_use_edges: Vec<ParserIndexSassModuleUseFactV0>,
456    pub module_forward_sources: Vec<String>,
457    pub module_import_sources: Vec<String>,
458    pub same_file_resolution: ParserIndexSassSameFileResolutionFactsV0,
459}
460
461#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
462#[serde(rename_all = "camelCase")]
463pub struct ParserIndexSassModuleUseFactV0 {
464    pub source: String,
465    pub namespace_kind: &'static str,
466    pub namespace: Option<String>,
467}
468
469#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
470#[serde(rename_all = "camelCase")]
471pub struct ParserIndexSassSameFileResolutionFactsV0 {
472    pub resolved_variable_ref_names: Vec<String>,
473    pub unresolved_variable_ref_names: Vec<String>,
474    pub resolved_mixin_include_names: Vec<String>,
475    pub unresolved_mixin_include_names: Vec<String>,
476    pub resolved_function_call_names: Vec<String>,
477}
478
479#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
480#[serde(rename_all = "camelCase")]
481pub struct ParserByteSpanV0 {
482    pub start: usize,
483    pub end: usize,
484}
485
486impl From<TextSpan> for ParserByteSpanV0 {
487    fn from(span: TextSpan) -> Self {
488        Self {
489            start: span.start,
490            end: span.end,
491        }
492    }
493}
494
495#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
496#[serde(rename_all = "camelCase")]
497pub struct ParserPositionV0 {
498    pub line: usize,
499    pub character: usize,
500}
501
502#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
503#[serde(rename_all = "camelCase")]
504pub struct ParserRangeV0 {
505    pub start: ParserPositionV0,
506    pub end: ParserPositionV0,
507}
508
509#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
510#[serde(rename_all = "camelCase")]
511pub struct ParserIndexSassSelectorSymbolFactV0 {
512    pub selector_name: String,
513    pub symbol_kind: &'static str,
514    pub name: String,
515    pub role: &'static str,
516    pub resolution: &'static str,
517    pub byte_span: ParserByteSpanV0,
518    pub range: ParserRangeV0,
519}
520
521#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
522#[serde(rename_all = "camelCase")]
523pub struct ParserIndexKeyframesFactsV0 {
524    pub names: Vec<String>,
525    pub names_under_media: Vec<String>,
526    pub names_under_supports: Vec<String>,
527    pub names_under_layer: Vec<String>,
528    pub animation_ref_names: Vec<String>,
529    pub animation_name_ref_names: Vec<String>,
530    pub selectors_with_animation_ref_names: Vec<String>,
531    pub selectors_with_animation_name_ref_names: Vec<String>,
532    pub selectors_with_animation_refs_under_media_names: Vec<String>,
533    pub selectors_with_animation_refs_under_supports_names: Vec<String>,
534    pub selectors_with_animation_refs_under_layer_names: Vec<String>,
535    pub selectors_with_animation_name_refs_under_media_names: Vec<String>,
536    pub selectors_with_animation_name_refs_under_supports_names: Vec<String>,
537    pub selectors_with_animation_name_refs_under_layer_names: Vec<String>,
538}
539
540#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
541#[serde(rename_all = "camelCase")]
542pub struct ParserIndexComposesFactsV0 {
543    pub selectors_with_composes_names: Vec<String>,
544    pub selectors_with_composes_under_media_names: Vec<String>,
545    pub selectors_with_composes_under_supports_names: Vec<String>,
546    pub selectors_with_composes_under_layer_names: Vec<String>,
547    pub local_selector_names: Vec<String>,
548    pub imported_selector_names: Vec<String>,
549    pub global_selector_names: Vec<String>,
550    pub local_selector_names_under_media: Vec<String>,
551    pub local_selector_names_under_supports: Vec<String>,
552    pub local_selector_names_under_layer: Vec<String>,
553    pub imported_selector_names_under_media: Vec<String>,
554    pub imported_selector_names_under_supports: Vec<String>,
555    pub imported_selector_names_under_layer: Vec<String>,
556    pub global_selector_names_under_media: Vec<String>,
557    pub global_selector_names_under_supports: Vec<String>,
558    pub global_selector_names_under_layer: Vec<String>,
559    pub import_sources: Vec<String>,
560    pub import_sources_under_media: Vec<String>,
561    pub import_sources_under_supports: Vec<String>,
562    pub import_sources_under_layer: Vec<String>,
563    pub class_name_count: usize,
564    pub local_class_name_count: usize,
565    pub imported_class_name_count: usize,
566    pub global_class_name_count: usize,
567}
568
569#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
570#[serde(rename_all = "camelCase")]
571pub struct ParserIndexWrapperFactsV0 {
572    pub selectors_under_media_names: Vec<String>,
573    pub selectors_under_supports_names: Vec<String>,
574    pub selectors_under_layer_names: Vec<String>,
575}
576
577#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
578#[serde(rename_all = "camelCase")]
579pub struct AtRuleKindCountsV0 {
580    pub media: usize,
581    pub supports: usize,
582    pub layer: usize,
583    pub keyframes: usize,
584    pub value: usize,
585    pub at_root: usize,
586    pub generic: usize,
587}
588
589#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
590#[serde(rename_all = "camelCase")]
591pub struct DeclarationKindCountsV0 {
592    pub composes: usize,
593    pub animation: usize,
594    pub animation_name: usize,
595    pub generic: usize,
596}
597
598#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
599#[serde(rename_all = "camelCase")]
600pub struct NestedSafetyCountsV0 {
601    pub flat: usize,
602    pub bem_suffix_safe: usize,
603    pub nested_unsafe: usize,
604}
605
606#[derive(Debug, Default)]
607struct ParityLiteAcc {
608    selector_names: Vec<String>,
609    keyframes_names: Vec<String>,
610    value_decl_names: Vec<String>,
611    rule_count: usize,
612    declaration_count: usize,
613    grouped_selector_count: usize,
614    max_nesting_depth: usize,
615    at_rule_kind_counts: AtRuleKindCountsV0,
616    declaration_kind_counts: DeclarationKindCountsV0,
617}
618
619#[derive(Debug, Default)]
620struct IndexSummaryAcc {
621    selector_names: Vec<String>,
622    bem_suffix_parent_names: Vec<String>,
623    bem_suffix_safe_selector_names: Vec<String>,
624    selectors_with_composes_names: Vec<String>,
625    selectors_with_composes_under_media_names: Vec<String>,
626    selectors_with_composes_under_supports_names: Vec<String>,
627    selectors_with_composes_under_layer_names: Vec<String>,
628    local_composes_selector_names: Vec<String>,
629    imported_composes_selector_names: Vec<String>,
630    global_composes_selector_names: Vec<String>,
631    local_composes_selector_names_under_media: Vec<String>,
632    local_composes_selector_names_under_supports: Vec<String>,
633    local_composes_selector_names_under_layer: Vec<String>,
634    imported_composes_selector_names_under_media: Vec<String>,
635    imported_composes_selector_names_under_supports: Vec<String>,
636    imported_composes_selector_names_under_layer: Vec<String>,
637    global_composes_selector_names_under_media: Vec<String>,
638    global_composes_selector_names_under_supports: Vec<String>,
639    global_composes_selector_names_under_layer: Vec<String>,
640    composes_import_sources: Vec<String>,
641    composes_import_sources_under_media: Vec<String>,
642    composes_import_sources_under_supports: Vec<String>,
643    composes_import_sources_under_layer: Vec<String>,
644    keyframes_names: Vec<String>,
645    nested_unsafe_selector_names: Vec<String>,
646    value_decl_names: Vec<String>,
647    value_decl_names_with_local_refs: Vec<String>,
648    value_decl_names_with_imported_refs: Vec<String>,
649    value_import_names: Vec<String>,
650    value_import_sources: Vec<String>,
651    value_import_source_by_name: BTreeMap<String, String>,
652    value_ref_names: Vec<String>,
653    local_value_ref_names: Vec<String>,
654    imported_value_ref_names: Vec<String>,
655    imported_value_ref_sources: Vec<String>,
656    declaration_value_ref_names: Vec<String>,
657    declaration_imported_value_ref_sources: Vec<String>,
658    value_decl_ref_names: Vec<String>,
659    value_decl_imported_value_ref_sources: Vec<String>,
660    custom_property_decl_names: Vec<String>,
661    custom_property_decl_facts: Vec<ParserIndexCustomPropertyDeclFactV0>,
662    custom_property_decl_context_selectors: Vec<String>,
663    custom_property_decl_names_under_media: Vec<String>,
664    custom_property_decl_names_under_supports: Vec<String>,
665    custom_property_decl_names_under_layer: Vec<String>,
666    custom_property_ref_names: Vec<String>,
667    custom_property_ref_facts: Vec<ParserIndexCustomPropertyRefFactV0>,
668    selectors_with_custom_property_refs_names: Vec<String>,
669    selectors_with_custom_property_refs_under_media_names: Vec<String>,
670    selectors_with_custom_property_refs_under_supports_names: Vec<String>,
671    selectors_with_custom_property_refs_under_layer_names: Vec<String>,
672    sass_variable_decl_names: Vec<String>,
673    sass_variable_decl_facts: Vec<SassVariableDeclFact>,
674    sass_variable_parameter_names: Vec<String>,
675    sass_variable_ref_names: Vec<String>,
676    sass_variable_ref_facts: Vec<SassVariableRefFact>,
677    sass_selectors_with_variable_refs_names: Vec<String>,
678    sass_selectors_with_resolved_variable_refs_names: Vec<String>,
679    sass_selectors_with_unresolved_variable_refs_names: Vec<String>,
680    sass_mixin_decl_names: Vec<String>,
681    sass_mixin_include_names: Vec<String>,
682    sass_selectors_with_mixin_includes_names: Vec<String>,
683    sass_selectors_with_resolved_mixin_includes_names: Vec<String>,
684    sass_selectors_with_unresolved_mixin_includes_names: Vec<String>,
685    sass_function_decl_names: Vec<String>,
686    sass_function_call_names: Vec<String>,
687    sass_selectors_with_function_calls_names: Vec<String>,
688    sass_selector_symbol_facts: Vec<ParserIndexSassSelectorSymbolFactV0>,
689    sass_module_use_sources: Vec<String>,
690    sass_module_use_edges: Vec<ParserIndexSassModuleUseFactV0>,
691    sass_module_forward_sources: Vec<String>,
692    sass_module_import_sources: Vec<String>,
693    selectors_with_value_refs_names: Vec<String>,
694    selectors_with_local_value_refs_names: Vec<String>,
695    selectors_with_imported_value_refs_names: Vec<String>,
696    selectors_with_value_refs_under_media_names: Vec<String>,
697    selectors_with_value_refs_under_supports_names: Vec<String>,
698    selectors_with_value_refs_under_layer_names: Vec<String>,
699    selectors_with_local_value_refs_under_media_names: Vec<String>,
700    selectors_with_local_value_refs_under_supports_names: Vec<String>,
701    selectors_with_local_value_refs_under_layer_names: Vec<String>,
702    selectors_with_imported_value_refs_under_media_names: Vec<String>,
703    selectors_with_imported_value_refs_under_supports_names: Vec<String>,
704    selectors_with_imported_value_refs_under_layer_names: Vec<String>,
705    selectors_with_animation_ref_names: Vec<String>,
706    selectors_with_animation_refs_under_media_names: Vec<String>,
707    selectors_with_animation_refs_under_supports_names: Vec<String>,
708    selectors_with_animation_refs_under_layer_names: Vec<String>,
709    selectors_with_animation_name_ref_names: Vec<String>,
710    selectors_with_animation_name_refs_under_media_names: Vec<String>,
711    selectors_with_animation_name_refs_under_supports_names: Vec<String>,
712    selectors_with_animation_name_refs_under_layer_names: Vec<String>,
713    selectors_under_media_names: Vec<String>,
714    selectors_under_supports_names: Vec<String>,
715    selectors_under_layer_names: Vec<String>,
716    animation_ref_names: Vec<String>,
717    animation_name_ref_names: Vec<String>,
718    keyframes_names_under_media: Vec<String>,
719    keyframes_names_under_supports: Vec<String>,
720    keyframes_names_under_layer: Vec<String>,
721    value_import_alias_count: usize,
722    composes_class_name_count: usize,
723    local_composes_class_name_count: usize,
724    imported_composes_class_name_count: usize,
725    global_composes_class_name_count: usize,
726    bem_suffix_count: usize,
727    nested_safety_counts: NestedSafetyCountsV0,
728}
729
730#[derive(Debug, Clone, PartialEq, Eq)]
731struct ResolvedSelectorBranch {
732    name: String,
733    bare_suffix_base: bool,
734}
735
736pub fn parse_style_module(path: &str, source: &str) -> Option<Stylesheet> {
737    let language = StyleLanguage::from_module_path(path)?;
738    Some(parse_stylesheet(language, source))
739}
740
741pub fn parse_stylesheet(language: StyleLanguage, source: &str) -> Stylesheet {
742    let (tokens, mut diagnostics) = tokenize(language, source);
743    let mut parser = Parser::new(source, &tokens, &mut diagnostics);
744    let nodes = parser.parse_root();
745    Stylesheet {
746        language,
747        source: source.to_string(),
748        tokens,
749        nodes,
750        diagnostics,
751    }
752}
753
754pub fn summarize_parity_lite(sheet: &Stylesheet) -> ParserParityLiteSummaryV0 {
755    let mut acc = ParityLiteAcc::default();
756    collect_parity_names(&sheet.nodes, &mut acc);
757    acc.selector_names.sort();
758    acc.keyframes_names.sort();
759    acc.keyframes_names.dedup();
760    acc.value_decl_names.sort();
761    acc.value_decl_names.dedup();
762
763    ParserParityLiteSummaryV0 {
764        schema_version: "0",
765        language: match sheet.language {
766            StyleLanguage::Css => "css",
767            StyleLanguage::Scss => "scss",
768            StyleLanguage::Less => "less",
769        },
770        selector_names: acc.selector_names,
771        keyframes_names: acc.keyframes_names,
772        value_decl_names: acc.value_decl_names,
773        diagnostic_count: sheet.diagnostics.len(),
774        rule_count: acc.rule_count,
775        declaration_count: acc.declaration_count,
776        grouped_selector_count: acc.grouped_selector_count,
777        max_nesting_depth: acc.max_nesting_depth,
778        at_rule_kind_counts: acc.at_rule_kind_counts,
779        declaration_kind_counts: acc.declaration_kind_counts,
780    }
781}
782
783pub fn summarize_css_modules_intermediate(sheet: &Stylesheet) -> ParserIndexSummaryV0 {
784    let mut acc = IndexSummaryAcc::default();
785    collect_index_names(
786        &sheet.nodes,
787        &mut acc,
788        &[],
789        false,
790        None,
791        WrapperContext::default(),
792    );
793    let local_value_names: BTreeSet<String> = acc.value_decl_names.iter().cloned().collect();
794    let imported_value_names: BTreeSet<String> = acc.value_import_names.iter().cloned().collect();
795    let known_value_names: BTreeSet<String> = acc
796        .value_decl_names
797        .iter()
798        .chain(acc.value_import_names.iter())
799        .cloned()
800        .collect();
801    let value_ref_ctx = ValueRefContext {
802        known: &known_value_names,
803        local: &local_value_names,
804        imported: &imported_value_names,
805    };
806    let known_keyframe_names: BTreeSet<String> = acc.keyframes_names.iter().cloned().collect();
807    collect_index_refs_and_counts(&sheet.nodes, value_ref_ctx, &known_keyframe_names, &mut acc);
808    let known_sass_function_names: BTreeSet<String> =
809        acc.sass_function_decl_names.iter().cloned().collect();
810    collect_sass_ref_facts(
811        &sheet.nodes,
812        &sheet.source,
813        &known_sass_function_names,
814        &mut acc,
815    );
816    let sass_variable_decl_facts = acc.sass_variable_decl_facts.clone();
817    let sass_mixin_targets: BTreeSet<String> = acc.sass_mixin_decl_names.iter().cloned().collect();
818    let sass_ref_ctx = SassRefContext {
819        variable_decls: &sass_variable_decl_facts,
820        mixin_targets: &sass_mixin_targets,
821        function_targets: &known_sass_function_names,
822    };
823    let selector_attachment_ctx = SelectorAttachmentContext {
824        source: &sheet.source,
825        value_ref_ctx,
826        known_keyframe_names: &known_keyframe_names,
827        sass_ref_ctx,
828    };
829    collect_index_selector_attachment_facts(&sheet.nodes, selector_attachment_ctx, &mut acc, &[]);
830
831    acc.selector_names.sort();
832    acc.bem_suffix_parent_names.sort();
833    acc.bem_suffix_safe_selector_names.sort();
834    acc.selectors_with_composes_names.sort();
835    acc.selectors_with_composes_under_media_names.sort();
836    acc.selectors_with_composes_under_supports_names.sort();
837    acc.selectors_with_composes_under_layer_names.sort();
838    acc.local_composes_selector_names.sort();
839    acc.imported_composes_selector_names.sort();
840    acc.global_composes_selector_names.sort();
841    acc.local_composes_selector_names_under_media.sort();
842    acc.local_composes_selector_names_under_supports.sort();
843    acc.local_composes_selector_names_under_layer.sort();
844    acc.imported_composes_selector_names_under_media.sort();
845    acc.imported_composes_selector_names_under_supports.sort();
846    acc.imported_composes_selector_names_under_layer.sort();
847    acc.global_composes_selector_names_under_media.sort();
848    acc.global_composes_selector_names_under_supports.sort();
849    acc.global_composes_selector_names_under_layer.sort();
850    acc.composes_import_sources.sort();
851    acc.composes_import_sources_under_media.sort();
852    acc.composes_import_sources_under_supports.sort();
853    acc.composes_import_sources_under_layer.sort();
854    acc.keyframes_names.sort();
855    acc.keyframes_names.dedup();
856    acc.nested_unsafe_selector_names.sort();
857    acc.value_decl_names.sort();
858    acc.value_decl_names.dedup();
859    acc.value_decl_names_with_local_refs.sort();
860    acc.value_decl_names_with_local_refs.dedup();
861    acc.value_decl_names_with_imported_refs.sort();
862    acc.value_decl_names_with_imported_refs.dedup();
863    acc.value_import_names.sort();
864    acc.value_import_names.dedup();
865    acc.value_import_sources.sort();
866    acc.value_ref_names.sort();
867    acc.value_ref_names.dedup();
868    acc.local_value_ref_names.sort();
869    acc.local_value_ref_names.dedup();
870    acc.imported_value_ref_names.sort();
871    acc.imported_value_ref_names.dedup();
872    acc.imported_value_ref_sources.sort();
873    acc.declaration_value_ref_names.sort();
874    acc.declaration_value_ref_names.dedup();
875    acc.declaration_imported_value_ref_sources.sort();
876    acc.value_decl_ref_names.sort();
877    acc.value_decl_ref_names.dedup();
878    acc.value_decl_imported_value_ref_sources.sort();
879    acc.custom_property_decl_names.sort();
880    acc.custom_property_decl_names.dedup();
881    acc.custom_property_decl_facts.sort();
882    acc.custom_property_decl_facts.dedup();
883    acc.custom_property_decl_context_selectors.sort();
884    acc.custom_property_decl_context_selectors.dedup();
885    acc.custom_property_decl_names_under_media.sort();
886    acc.custom_property_decl_names_under_media.dedup();
887    acc.custom_property_decl_names_under_supports.sort();
888    acc.custom_property_decl_names_under_supports.dedup();
889    acc.custom_property_decl_names_under_layer.sort();
890    acc.custom_property_decl_names_under_layer.dedup();
891    acc.custom_property_ref_names.sort();
892    acc.custom_property_ref_names.dedup();
893    acc.custom_property_ref_facts.sort();
894    acc.custom_property_ref_facts.dedup();
895    acc.selectors_with_custom_property_refs_names.sort();
896    acc.selectors_with_custom_property_refs_names.dedup();
897    acc.selectors_with_custom_property_refs_under_media_names
898        .sort();
899    acc.selectors_with_custom_property_refs_under_media_names
900        .dedup();
901    acc.selectors_with_custom_property_refs_under_supports_names
902        .sort();
903    acc.selectors_with_custom_property_refs_under_supports_names
904        .dedup();
905    acc.selectors_with_custom_property_refs_under_layer_names
906        .sort();
907    acc.selectors_with_custom_property_refs_under_layer_names
908        .dedup();
909    acc.sass_variable_decl_names.sort();
910    acc.sass_variable_decl_names.dedup();
911    acc.sass_variable_parameter_names.sort();
912    acc.sass_variable_parameter_names.dedup();
913    acc.sass_variable_ref_names.sort();
914    acc.sass_variable_ref_names.dedup();
915    acc.sass_selectors_with_variable_refs_names.sort();
916    acc.sass_selectors_with_variable_refs_names.dedup();
917    acc.sass_selectors_with_resolved_variable_refs_names.sort();
918    acc.sass_selectors_with_resolved_variable_refs_names.dedup();
919    acc.sass_selectors_with_unresolved_variable_refs_names
920        .sort();
921    acc.sass_selectors_with_unresolved_variable_refs_names
922        .dedup();
923    acc.sass_mixin_decl_names.sort();
924    acc.sass_mixin_decl_names.dedup();
925    acc.sass_mixin_include_names.sort();
926    acc.sass_mixin_include_names.dedup();
927    acc.sass_selectors_with_mixin_includes_names.sort();
928    acc.sass_selectors_with_mixin_includes_names.dedup();
929    acc.sass_selectors_with_resolved_mixin_includes_names.sort();
930    acc.sass_selectors_with_resolved_mixin_includes_names
931        .dedup();
932    acc.sass_selectors_with_unresolved_mixin_includes_names
933        .sort();
934    acc.sass_selectors_with_unresolved_mixin_includes_names
935        .dedup();
936    acc.sass_function_decl_names.sort();
937    acc.sass_function_decl_names.dedup();
938    acc.sass_function_call_names.sort();
939    acc.sass_function_call_names.dedup();
940    acc.sass_selectors_with_function_calls_names.sort();
941    acc.sass_selectors_with_function_calls_names.dedup();
942    acc.sass_selector_symbol_facts.sort();
943    acc.sass_selector_symbol_facts.dedup();
944    acc.sass_module_use_sources.sort();
945    acc.sass_module_use_sources.dedup();
946    acc.sass_module_use_edges.sort();
947    acc.sass_module_use_edges.dedup();
948    acc.sass_module_forward_sources.sort();
949    acc.sass_module_forward_sources.dedup();
950    acc.sass_module_import_sources.sort();
951    acc.sass_module_import_sources.dedup();
952    acc.selectors_with_value_refs_names.sort();
953    acc.selectors_with_local_value_refs_names.sort();
954    acc.selectors_with_imported_value_refs_names.sort();
955    acc.selectors_with_value_refs_under_media_names.sort();
956    acc.selectors_with_value_refs_under_supports_names.sort();
957    acc.selectors_with_value_refs_under_layer_names.sort();
958    acc.selectors_with_local_value_refs_under_media_names.sort();
959    acc.selectors_with_local_value_refs_under_supports_names
960        .sort();
961    acc.selectors_with_local_value_refs_under_layer_names.sort();
962    acc.selectors_with_imported_value_refs_under_media_names
963        .sort();
964    acc.selectors_with_imported_value_refs_under_supports_names
965        .sort();
966    acc.selectors_with_imported_value_refs_under_layer_names
967        .sort();
968    acc.selectors_with_animation_ref_names.sort();
969    acc.selectors_with_animation_refs_under_media_names.sort();
970    acc.selectors_with_animation_refs_under_supports_names
971        .sort();
972    acc.selectors_with_animation_refs_under_layer_names.sort();
973    acc.selectors_with_animation_name_ref_names.sort();
974    acc.selectors_with_animation_name_refs_under_media_names
975        .sort();
976    acc.selectors_with_animation_name_refs_under_supports_names
977        .sort();
978    acc.selectors_with_animation_name_refs_under_layer_names
979        .sort();
980    acc.selectors_under_media_names.sort();
981    acc.selectors_under_supports_names.sort();
982    acc.selectors_under_layer_names.sort();
983    acc.animation_ref_names.sort();
984    acc.animation_ref_names.dedup();
985    acc.animation_name_ref_names.sort();
986    acc.animation_name_ref_names.dedup();
987    acc.keyframes_names_under_media.sort();
988    acc.keyframes_names_under_media.dedup();
989    acc.keyframes_names_under_supports.sort();
990    acc.keyframes_names_under_supports.dedup();
991    acc.keyframes_names_under_layer.sort();
992    acc.keyframes_names_under_layer.dedup();
993    let selectors_with_value_refs_names = acc.selectors_with_value_refs_names.clone();
994    let selectors_with_animation_ref_names = acc.selectors_with_animation_ref_names.clone();
995    let selectors_with_animation_name_ref_names =
996        acc.selectors_with_animation_name_ref_names.clone();
997    let sass_same_file_resolution = summarize_sass_same_file_resolution(&acc);
998
999    ParserIndexSummaryV0 {
1000        schema_version: "0",
1001        language: match sheet.language {
1002            StyleLanguage::Css => "css",
1003            StyleLanguage::Scss => "scss",
1004            StyleLanguage::Less => "less",
1005        },
1006        selectors: ParserIndexSelectorFactsV0 {
1007            names: acc.selector_names,
1008            bem_suffix_parent_names: acc.bem_suffix_parent_names,
1009            bem_suffix_safe_names: acc.bem_suffix_safe_selector_names,
1010            nested_unsafe_names: acc.nested_unsafe_selector_names,
1011            selectors_with_value_refs_names,
1012            selectors_with_animation_ref_names,
1013            selectors_with_animation_name_ref_names,
1014            bem_suffix_count: acc.bem_suffix_count,
1015            nested_safety_counts: acc.nested_safety_counts,
1016        },
1017        values: ParserIndexValueFactsV0 {
1018            decl_names: acc.value_decl_names,
1019            decl_names_with_local_refs: acc.value_decl_names_with_local_refs,
1020            decl_names_with_imported_refs: acc.value_decl_names_with_imported_refs,
1021            import_names: acc.value_import_names,
1022            import_sources: acc.value_import_sources,
1023            import_alias_count: acc.value_import_alias_count,
1024            ref_names: acc.value_ref_names,
1025            local_ref_names: acc.local_value_ref_names,
1026            imported_ref_names: acc.imported_value_ref_names,
1027            imported_ref_sources: acc.imported_value_ref_sources,
1028            declaration_ref_names: acc.declaration_value_ref_names,
1029            declaration_imported_ref_sources: acc.declaration_imported_value_ref_sources,
1030            value_decl_ref_names: acc.value_decl_ref_names,
1031            value_decl_imported_ref_sources: acc.value_decl_imported_value_ref_sources,
1032            selectors_with_refs_names: acc.selectors_with_value_refs_names,
1033            selectors_with_local_refs_names: acc.selectors_with_local_value_refs_names,
1034            selectors_with_imported_refs_names: acc.selectors_with_imported_value_refs_names,
1035            selectors_with_refs_under_media_names: acc.selectors_with_value_refs_under_media_names,
1036            selectors_with_refs_under_supports_names: acc
1037                .selectors_with_value_refs_under_supports_names,
1038            selectors_with_refs_under_layer_names: acc.selectors_with_value_refs_under_layer_names,
1039            selectors_with_local_refs_under_media_names: acc
1040                .selectors_with_local_value_refs_under_media_names,
1041            selectors_with_local_refs_under_supports_names: acc
1042                .selectors_with_local_value_refs_under_supports_names,
1043            selectors_with_local_refs_under_layer_names: acc
1044                .selectors_with_local_value_refs_under_layer_names,
1045            selectors_with_imported_refs_under_media_names: acc
1046                .selectors_with_imported_value_refs_under_media_names,
1047            selectors_with_imported_refs_under_supports_names: acc
1048                .selectors_with_imported_value_refs_under_supports_names,
1049            selectors_with_imported_refs_under_layer_names: acc
1050                .selectors_with_imported_value_refs_under_layer_names,
1051        },
1052        custom_properties: ParserIndexCustomPropertyFactsV0 {
1053            decl_names: acc.custom_property_decl_names,
1054            decl_facts: acc.custom_property_decl_facts,
1055            decl_context_selectors: acc.custom_property_decl_context_selectors,
1056            decl_names_under_media: acc.custom_property_decl_names_under_media,
1057            decl_names_under_supports: acc.custom_property_decl_names_under_supports,
1058            decl_names_under_layer: acc.custom_property_decl_names_under_layer,
1059            ref_names: acc.custom_property_ref_names,
1060            ref_facts: acc.custom_property_ref_facts,
1061            selectors_with_refs_names: acc.selectors_with_custom_property_refs_names,
1062            selectors_with_refs_under_media_names: acc
1063                .selectors_with_custom_property_refs_under_media_names,
1064            selectors_with_refs_under_supports_names: acc
1065                .selectors_with_custom_property_refs_under_supports_names,
1066            selectors_with_refs_under_layer_names: acc
1067                .selectors_with_custom_property_refs_under_layer_names,
1068        },
1069        sass: ParserIndexSassFactsV0 {
1070            variable_decl_names: acc.sass_variable_decl_names,
1071            variable_parameter_names: acc.sass_variable_parameter_names,
1072            variable_ref_names: acc.sass_variable_ref_names,
1073            selectors_with_variable_refs_names: acc.sass_selectors_with_variable_refs_names,
1074            selectors_with_resolved_variable_refs_names: acc
1075                .sass_selectors_with_resolved_variable_refs_names,
1076            selectors_with_unresolved_variable_refs_names: acc
1077                .sass_selectors_with_unresolved_variable_refs_names,
1078            mixin_decl_names: acc.sass_mixin_decl_names,
1079            mixin_include_names: acc.sass_mixin_include_names,
1080            selectors_with_mixin_includes_names: acc.sass_selectors_with_mixin_includes_names,
1081            selectors_with_resolved_mixin_includes_names: acc
1082                .sass_selectors_with_resolved_mixin_includes_names,
1083            selectors_with_unresolved_mixin_includes_names: acc
1084                .sass_selectors_with_unresolved_mixin_includes_names,
1085            function_decl_names: acc.sass_function_decl_names,
1086            function_call_names: acc.sass_function_call_names,
1087            selectors_with_function_calls_names: acc.sass_selectors_with_function_calls_names,
1088            selector_symbol_facts: acc.sass_selector_symbol_facts,
1089            module_use_sources: acc.sass_module_use_sources,
1090            module_use_edges: acc.sass_module_use_edges,
1091            module_forward_sources: acc.sass_module_forward_sources,
1092            module_import_sources: acc.sass_module_import_sources,
1093            same_file_resolution: sass_same_file_resolution,
1094        },
1095        keyframes: ParserIndexKeyframesFactsV0 {
1096            names: acc.keyframes_names,
1097            names_under_media: acc.keyframes_names_under_media,
1098            names_under_supports: acc.keyframes_names_under_supports,
1099            names_under_layer: acc.keyframes_names_under_layer,
1100            animation_ref_names: acc.animation_ref_names,
1101            animation_name_ref_names: acc.animation_name_ref_names,
1102            selectors_with_animation_ref_names: acc.selectors_with_animation_ref_names,
1103            selectors_with_animation_name_ref_names: acc.selectors_with_animation_name_ref_names,
1104            selectors_with_animation_refs_under_media_names: acc
1105                .selectors_with_animation_refs_under_media_names,
1106            selectors_with_animation_refs_under_supports_names: acc
1107                .selectors_with_animation_refs_under_supports_names,
1108            selectors_with_animation_refs_under_layer_names: acc
1109                .selectors_with_animation_refs_under_layer_names,
1110            selectors_with_animation_name_refs_under_media_names: acc
1111                .selectors_with_animation_name_refs_under_media_names,
1112            selectors_with_animation_name_refs_under_supports_names: acc
1113                .selectors_with_animation_name_refs_under_supports_names,
1114            selectors_with_animation_name_refs_under_layer_names: acc
1115                .selectors_with_animation_name_refs_under_layer_names,
1116        },
1117        composes: ParserIndexComposesFactsV0 {
1118            selectors_with_composes_names: acc.selectors_with_composes_names,
1119            selectors_with_composes_under_media_names: acc
1120                .selectors_with_composes_under_media_names,
1121            selectors_with_composes_under_supports_names: acc
1122                .selectors_with_composes_under_supports_names,
1123            selectors_with_composes_under_layer_names: acc
1124                .selectors_with_composes_under_layer_names,
1125            local_selector_names: acc.local_composes_selector_names,
1126            imported_selector_names: acc.imported_composes_selector_names,
1127            global_selector_names: acc.global_composes_selector_names,
1128            local_selector_names_under_media: acc.local_composes_selector_names_under_media,
1129            local_selector_names_under_supports: acc.local_composes_selector_names_under_supports,
1130            local_selector_names_under_layer: acc.local_composes_selector_names_under_layer,
1131            imported_selector_names_under_media: acc.imported_composes_selector_names_under_media,
1132            imported_selector_names_under_supports: acc
1133                .imported_composes_selector_names_under_supports,
1134            imported_selector_names_under_layer: acc.imported_composes_selector_names_under_layer,
1135            global_selector_names_under_media: acc.global_composes_selector_names_under_media,
1136            global_selector_names_under_supports: acc.global_composes_selector_names_under_supports,
1137            global_selector_names_under_layer: acc.global_composes_selector_names_under_layer,
1138            import_sources: acc.composes_import_sources,
1139            import_sources_under_media: acc.composes_import_sources_under_media,
1140            import_sources_under_supports: acc.composes_import_sources_under_supports,
1141            import_sources_under_layer: acc.composes_import_sources_under_layer,
1142            class_name_count: acc.composes_class_name_count,
1143            local_class_name_count: acc.local_composes_class_name_count,
1144            imported_class_name_count: acc.imported_composes_class_name_count,
1145            global_class_name_count: acc.global_composes_class_name_count,
1146        },
1147        wrappers: ParserIndexWrapperFactsV0 {
1148            selectors_under_media_names: acc.selectors_under_media_names,
1149            selectors_under_supports_names: acc.selectors_under_supports_names,
1150            selectors_under_layer_names: acc.selectors_under_layer_names,
1151        },
1152    }
1153}
1154
1155pub fn summarize_parser_canonical_candidate(
1156    sheet: &Stylesheet,
1157) -> ParserCanonicalCandidateBundleV0 {
1158    let parity_lite = summarize_parity_lite(sheet);
1159    let css_modules_intermediate = summarize_css_modules_intermediate(sheet);
1160
1161    ParserCanonicalCandidateBundleV0 {
1162        schema_version: "0",
1163        language: parity_lite.language,
1164        parity_lite,
1165        css_modules_intermediate,
1166    }
1167}
1168
1169pub fn summarize_parser_evaluator_candidates(sheet: &Stylesheet) -> ParserEvaluatorCandidatesV0 {
1170    let intermediate = summarize_css_modules_intermediate(sheet);
1171    let bem_suffix_safe_names: BTreeSet<&str> = intermediate
1172        .selectors
1173        .bem_suffix_safe_names
1174        .iter()
1175        .map(String::as_str)
1176        .collect();
1177    let nested_unsafe_names: BTreeSet<&str> = intermediate
1178        .selectors
1179        .nested_unsafe_names
1180        .iter()
1181        .map(String::as_str)
1182        .collect();
1183    let selectors_under_media_names: BTreeSet<&str> = intermediate
1184        .wrappers
1185        .selectors_under_media_names
1186        .iter()
1187        .map(String::as_str)
1188        .collect();
1189    let selectors_under_supports_names: BTreeSet<&str> = intermediate
1190        .wrappers
1191        .selectors_under_supports_names
1192        .iter()
1193        .map(String::as_str)
1194        .collect();
1195    let selectors_under_layer_names: BTreeSet<&str> = intermediate
1196        .wrappers
1197        .selectors_under_layer_names
1198        .iter()
1199        .map(String::as_str)
1200        .collect();
1201    let selectors_with_refs_names: BTreeSet<&str> = intermediate
1202        .values
1203        .selectors_with_refs_names
1204        .iter()
1205        .map(String::as_str)
1206        .collect();
1207    let selectors_with_local_refs_names: BTreeSet<&str> = intermediate
1208        .values
1209        .selectors_with_local_refs_names
1210        .iter()
1211        .map(String::as_str)
1212        .collect();
1213    let selectors_with_imported_refs_names: BTreeSet<&str> = intermediate
1214        .values
1215        .selectors_with_imported_refs_names
1216        .iter()
1217        .map(String::as_str)
1218        .collect();
1219    let selectors_with_custom_property_refs_names: BTreeSet<&str> = intermediate
1220        .custom_properties
1221        .selectors_with_refs_names
1222        .iter()
1223        .map(String::as_str)
1224        .collect();
1225    let selectors_with_animation_ref_names: BTreeSet<&str> = intermediate
1226        .keyframes
1227        .selectors_with_animation_ref_names
1228        .iter()
1229        .map(String::as_str)
1230        .collect();
1231    let selectors_with_animation_name_ref_names: BTreeSet<&str> = intermediate
1232        .keyframes
1233        .selectors_with_animation_name_ref_names
1234        .iter()
1235        .map(String::as_str)
1236        .collect();
1237    let selectors_with_composes_names: BTreeSet<&str> = intermediate
1238        .composes
1239        .selectors_with_composes_names
1240        .iter()
1241        .map(String::as_str)
1242        .collect();
1243    let local_selector_names: BTreeSet<&str> = intermediate
1244        .composes
1245        .local_selector_names
1246        .iter()
1247        .map(String::as_str)
1248        .collect();
1249    let imported_selector_names: BTreeSet<&str> = intermediate
1250        .composes
1251        .imported_selector_names
1252        .iter()
1253        .map(String::as_str)
1254        .collect();
1255    let global_selector_names: BTreeSet<&str> = intermediate
1256        .composes
1257        .global_selector_names
1258        .iter()
1259        .map(String::as_str)
1260        .collect();
1261
1262    let results = intermediate
1263        .selectors
1264        .names
1265        .iter()
1266        .map(|selector_name| {
1267            let selector = selector_name.as_str();
1268            let nested_safety_kind = if nested_unsafe_names.contains(selector) {
1269                "nestedUnsafe"
1270            } else if bem_suffix_safe_names.contains(selector) {
1271                "bemSuffixSafe"
1272            } else {
1273                "flat"
1274            };
1275            let bem_suffix_parent_name = if nested_safety_kind == "bemSuffixSafe" {
1276                let suffix_split_index = [selector.rfind("__"), selector.rfind("--")]
1277                    .into_iter()
1278                    .flatten()
1279                    .max();
1280                suffix_split_index.map(|index| selector[..index].to_string())
1281            } else {
1282                None
1283            };
1284
1285            ParserEvaluatorCandidateV0 {
1286                kind: "selector-index-facts",
1287                selector_name: selector_name.clone(),
1288                nested_safety_kind,
1289                bem_suffix_parent_name,
1290                under_media: selectors_under_media_names.contains(selector),
1291                under_supports: selectors_under_supports_names.contains(selector),
1292                under_layer: selectors_under_layer_names.contains(selector),
1293                has_value_refs: selectors_with_refs_names.contains(selector),
1294                has_local_value_refs: selectors_with_local_refs_names.contains(selector),
1295                has_imported_value_refs: selectors_with_imported_refs_names.contains(selector),
1296                has_custom_property_refs: selectors_with_custom_property_refs_names
1297                    .contains(selector),
1298                has_animation_ref: selectors_with_animation_ref_names.contains(selector),
1299                has_animation_name_ref: selectors_with_animation_name_ref_names.contains(selector),
1300                has_composes: selectors_with_composes_names.contains(selector),
1301                has_local_composes: local_selector_names.contains(selector),
1302                has_imported_composes: imported_selector_names.contains(selector),
1303                has_global_composes: global_selector_names.contains(selector),
1304            }
1305        })
1306        .collect();
1307
1308    ParserEvaluatorCandidatesV0 {
1309        schema_version: "0",
1310        language: intermediate.language,
1311        results,
1312    }
1313}
1314
1315pub fn summarize_parser_canonical_producer_signal(
1316    sheet: &Stylesheet,
1317) -> ParserCanonicalProducerSignalV0 {
1318    let canonical_candidate = summarize_parser_canonical_candidate(sheet);
1319    let evaluator_candidates = summarize_parser_evaluator_candidates(sheet);
1320
1321    ParserCanonicalProducerSignalV0 {
1322        schema_version: "0",
1323        language: canonical_candidate.language,
1324        canonical_candidate,
1325        evaluator_candidates,
1326        public_product_gate: ParserPublicProductGateSignalV0 {
1327            canonical_candidate_command: "pnpm check:rust-parser-canonical-candidate",
1328            consumer_boundary_command: "pnpm check:rust-parser-consumer-boundary",
1329            public_product_gate_command: "pnpm check:rust-parser-public-product",
1330            included_in_parser_lane: true,
1331            included_in_rust_lane_bundle: true,
1332            included_in_rust_release_bundle: true,
1333        },
1334    }
1335}
1336
1337pub fn summarize_index_bridge(sheet: &Stylesheet) -> ParserIndexSummaryV0 {
1338    summarize_css_modules_intermediate(sheet)
1339}
1340
1341pub fn summarize_semantic_boundary(sheet: &Stylesheet) -> ParserSemanticBoundarySummaryV0 {
1342    let index = summarize_css_modules_intermediate(sheet);
1343    let ParserIndexSummaryV0 {
1344        schema_version: _,
1345        language,
1346        selectors,
1347        values,
1348        custom_properties,
1349        sass,
1350        keyframes,
1351        composes,
1352        wrappers,
1353    } = index;
1354    let ParserIndexSelectorFactsV0 {
1355        names,
1356        bem_suffix_parent_names,
1357        bem_suffix_safe_names,
1358        nested_unsafe_names,
1359        selectors_with_value_refs_names,
1360        selectors_with_animation_ref_names,
1361        selectors_with_animation_name_ref_names,
1362        bem_suffix_count,
1363        nested_safety_counts,
1364    } = selectors;
1365    let ParserIndexSassFactsV0 {
1366        variable_decl_names,
1367        variable_parameter_names,
1368        variable_ref_names,
1369        selectors_with_variable_refs_names: _,
1370        selectors_with_resolved_variable_refs_names,
1371        selectors_with_unresolved_variable_refs_names,
1372        mixin_decl_names,
1373        mixin_include_names,
1374        selectors_with_mixin_includes_names: _,
1375        selectors_with_resolved_mixin_includes_names,
1376        selectors_with_unresolved_mixin_includes_names,
1377        function_decl_names,
1378        function_call_names,
1379        selectors_with_function_calls_names,
1380        selector_symbol_facts,
1381        module_use_sources,
1382        module_use_edges,
1383        module_forward_sources,
1384        module_import_sources,
1385        same_file_resolution,
1386    } = sass;
1387    let custom_property_semantic_facts =
1388        summarize_custom_property_semantic_facts(&custom_properties);
1389
1390    ParserSemanticBoundarySummaryV0 {
1391        schema_version: "0",
1392        language,
1393        parser_facts: ParserBoundarySyntaxFactsV0 {
1394            lossless_cst: summarize_lossless_cst(sheet),
1395            selectors: ParserIndexSelectorFactsV0 {
1396                names: names.clone(),
1397                bem_suffix_parent_names: bem_suffix_parent_names.clone(),
1398                bem_suffix_safe_names: bem_suffix_safe_names.clone(),
1399                nested_unsafe_names: nested_unsafe_names.clone(),
1400                selectors_with_value_refs_names,
1401                selectors_with_animation_ref_names,
1402                selectors_with_animation_name_ref_names,
1403                bem_suffix_count,
1404                nested_safety_counts: nested_safety_counts.clone(),
1405            },
1406            values,
1407            custom_properties,
1408            sass: ParserSassSyntaxFactsV0 {
1409                variable_decl_names,
1410                variable_parameter_names,
1411                variable_ref_names,
1412                mixin_decl_names,
1413                mixin_include_names,
1414                function_decl_names,
1415                function_call_names,
1416                module_use_sources,
1417                module_use_edges,
1418                module_forward_sources,
1419                module_import_sources,
1420            },
1421            keyframes,
1422            composes,
1423            wrappers,
1424        },
1425        semantic_facts: StyleSemanticFactsV0 {
1426            selector_identity: StyleSelectorIdentityFactsV0 {
1427                canonical_names: names,
1428                bem_suffix_safe_names,
1429                bem_suffix_parent_names,
1430                nested_unsafe_names,
1431                nested_safety_counts,
1432            },
1433            custom_properties: custom_property_semantic_facts,
1434            sass: StyleSassSemanticFactsV0 {
1435                selector_symbol_facts,
1436                selectors_with_resolved_variable_refs_names,
1437                selectors_with_unresolved_variable_refs_names,
1438                selectors_with_resolved_mixin_includes_names,
1439                selectors_with_unresolved_mixin_includes_names,
1440                selectors_with_function_calls_names,
1441                same_file_resolution,
1442            },
1443        },
1444    }
1445}
1446
1447fn summarize_custom_property_semantic_facts(
1448    facts: &ParserIndexCustomPropertyFactsV0,
1449) -> StyleCustomPropertySemanticFactsV0 {
1450    let (resolved_ref_names, unresolved_ref_names) =
1451        summarize_custom_property_resolution_names(facts);
1452
1453    StyleCustomPropertySemanticFactsV0 {
1454        decl_names: facts.decl_names.clone(),
1455        ref_names: facts.ref_names.clone(),
1456        resolved_ref_names,
1457        unresolved_ref_names,
1458        selectors_with_refs_names: facts.selectors_with_refs_names.clone(),
1459    }
1460}
1461
1462fn summarize_custom_property_resolution_names(
1463    facts: &ParserIndexCustomPropertyFactsV0,
1464) -> (Vec<String>, Vec<String>) {
1465    if !facts.decl_facts.is_empty() && !facts.ref_facts.is_empty() {
1466        let mut resolved = BTreeSet::new();
1467        let mut unresolved = BTreeSet::new();
1468        for ref_fact in &facts.ref_facts {
1469            if facts
1470                .decl_facts
1471                .iter()
1472                .any(|decl_fact| custom_property_context_matches(decl_fact, ref_fact))
1473            {
1474                resolved.insert(ref_fact.name.clone());
1475            } else {
1476                unresolved.insert(ref_fact.name.clone());
1477            }
1478        }
1479        return (
1480            resolved.into_iter().collect(),
1481            unresolved.into_iter().collect(),
1482        );
1483    }
1484
1485    let decl_names: BTreeSet<&str> = facts.decl_names.iter().map(String::as_str).collect();
1486    let resolved_ref_names = facts
1487        .ref_names
1488        .iter()
1489        .filter(|name| decl_names.contains(name.as_str()))
1490        .cloned()
1491        .collect();
1492    let unresolved_ref_names = facts
1493        .ref_names
1494        .iter()
1495        .filter(|name| !decl_names.contains(name.as_str()))
1496        .cloned()
1497        .collect();
1498    (resolved_ref_names, unresolved_ref_names)
1499}
1500
1501fn custom_property_context_matches(
1502    decl: &ParserIndexCustomPropertyDeclFactV0,
1503    reference: &ParserIndexCustomPropertyRefFactV0,
1504) -> bool {
1505    if decl.name != reference.name {
1506        return false;
1507    }
1508    if decl.under_media && !reference.under_media {
1509        return false;
1510    }
1511    if decl.under_supports && !reference.under_supports {
1512        return false;
1513    }
1514    if decl.under_layer && !reference.under_layer {
1515        return false;
1516    }
1517    if decl.selector_contexts.is_empty() {
1518        return true;
1519    }
1520    decl.selector_contexts
1521        .iter()
1522        .any(|decl_selector| custom_property_selector_context_matches(decl_selector, reference))
1523}
1524
1525fn custom_property_selector_context_matches(
1526    decl_selector: &str,
1527    reference: &ParserIndexCustomPropertyRefFactV0,
1528) -> bool {
1529    decl_selector == ":root"
1530        || reference.selector_contexts.iter().any(|ref_selector| {
1531            ref_selector == decl_selector || ref_selector.contains(decl_selector)
1532        })
1533}
1534
1535fn summarize_lossless_cst(sheet: &Stylesheet) -> ParserLosslessCstFactsV0 {
1536    let source_byte_len = sheet.source.len();
1537    ParserLosslessCstFactsV0 {
1538        source_byte_len,
1539        token_count: sheet.tokens.len(),
1540        root_node_count: sheet.nodes.len(),
1541        diagnostic_count: sheet.diagnostics.len(),
1542        all_token_spans_within_source: sheet
1543            .tokens
1544            .iter()
1545            .all(|token| is_valid_span(token.span, source_byte_len)),
1546        all_node_spans_within_source: nodes_have_valid_spans(&sheet.nodes, source_byte_len),
1547    }
1548}
1549
1550fn nodes_have_valid_spans(nodes: &[SyntaxNode], source_byte_len: usize) -> bool {
1551    nodes.iter().all(|node| {
1552        is_valid_span(node.span, source_byte_len)
1553            && node
1554                .header_span
1555                .is_none_or(|span| is_valid_span(span, source_byte_len))
1556            && nodes_have_valid_spans(&node.children, source_byte_len)
1557    })
1558}
1559
1560fn is_valid_span(span: TextSpan, source_byte_len: usize) -> bool {
1561    span.start <= span.end && span.end <= source_byte_len
1562}
1563
1564fn collect_parity_names(nodes: &[SyntaxNode], acc: &mut ParityLiteAcc) {
1565    collect_parity_names_with_parent(nodes, acc, &[], 0);
1566}
1567
1568fn collect_parity_names_with_parent(
1569    nodes: &[SyntaxNode],
1570    acc: &mut ParityLiteAcc,
1571    parent_branches: &[ResolvedSelectorBranch],
1572    depth: usize,
1573) {
1574    for node in nodes {
1575        let mut next_parent_branches = parent_branches.to_vec();
1576        let mut next_depth = depth;
1577        match &node.payload {
1578            Some(SyntaxNodePayload::Rule(rule)) => {
1579                acc.rule_count += 1;
1580                next_depth = depth + 1;
1581                acc.max_nesting_depth = acc.max_nesting_depth.max(next_depth);
1582                if rule.selector_groups.len() > 1 {
1583                    acc.grouped_selector_count += rule.selector_groups.len();
1584                }
1585                let resolved = resolve_rule_selector_branches(rule, parent_branches);
1586                if !resolved.is_empty() {
1587                    acc.selector_names
1588                        .extend(resolved.iter().map(|branch| branch.name.clone()));
1589                    next_parent_branches = resolved;
1590                }
1591            }
1592            Some(SyntaxNodePayload::AtRule(at_rule)) => {
1593                next_depth = depth + 1;
1594                acc.max_nesting_depth = acc.max_nesting_depth.max(next_depth);
1595                increment_at_rule_kind_count(&mut acc.at_rule_kind_counts, at_rule.kind);
1596                match at_rule.kind {
1597                    AtRuleKind::Keyframes if !at_rule.params.is_empty() => {
1598                        acc.keyframes_names.push(at_rule.params.clone());
1599                    }
1600                    AtRuleKind::Keyframes => {}
1601                    AtRuleKind::Value => {
1602                        if let Some((name, _)) = at_rule.params.split_once(':') {
1603                            let trimmed = name.trim();
1604                            if !trimmed.is_empty() {
1605                                acc.value_decl_names.push(trimmed.to_string());
1606                            }
1607                        }
1608                    }
1609                    _ => {}
1610                }
1611            }
1612            Some(SyntaxNodePayload::Declaration(declaration)) => {
1613                acc.declaration_count += 1;
1614                increment_declaration_kind_count(
1615                    &mut acc.declaration_kind_counts,
1616                    classify_declaration_kind(&declaration.property),
1617                );
1618            }
1619            _ => {}
1620        }
1621        collect_parity_names_with_parent(&node.children, acc, &next_parent_branches, next_depth);
1622    }
1623}
1624
1625fn increment_at_rule_kind_count(counts: &mut AtRuleKindCountsV0, kind: AtRuleKind) {
1626    match kind {
1627        AtRuleKind::Media => counts.media += 1,
1628        AtRuleKind::Supports => counts.supports += 1,
1629        AtRuleKind::Layer => counts.layer += 1,
1630        AtRuleKind::Keyframes => counts.keyframes += 1,
1631        AtRuleKind::Value => counts.value += 1,
1632        AtRuleKind::AtRoot => counts.at_root += 1,
1633        AtRuleKind::Mixin
1634        | AtRuleKind::Include
1635        | AtRuleKind::Function
1636        | AtRuleKind::Use
1637        | AtRuleKind::Forward
1638        | AtRuleKind::Import => counts.generic += 1,
1639        AtRuleKind::Generic => counts.generic += 1,
1640    }
1641}
1642
1643#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1644enum DeclarationKind {
1645    Composes,
1646    Animation,
1647    AnimationName,
1648    Generic,
1649}
1650
1651#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1652enum NestedSafetyKind {
1653    Flat,
1654    BemSuffixSafe,
1655    NestedUnsafe,
1656}
1657
1658struct RuleSelectorFacts {
1659    nested_safety: NestedSafetyKind,
1660    bem_suffix_count: usize,
1661}
1662
1663#[derive(Debug, Default)]
1664struct RuleComposesFacts {
1665    local_class_name_count: usize,
1666    imported_class_name_count: usize,
1667    global_class_name_count: usize,
1668    imported_sources: Vec<String>,
1669}
1670
1671#[derive(Debug, Default)]
1672struct RuleReferenceFacts {
1673    has_value_refs: bool,
1674    has_local_value_refs: bool,
1675    has_imported_value_refs: bool,
1676    has_animation_refs: bool,
1677    has_animation_name_refs: bool,
1678    has_custom_property_refs: bool,
1679    custom_property_ref_names: Vec<String>,
1680    has_sass_variable_refs: bool,
1681    has_resolved_sass_variable_refs: bool,
1682    has_unresolved_sass_variable_refs: bool,
1683    has_sass_mixin_includes: bool,
1684    has_resolved_sass_mixin_includes: bool,
1685    has_unresolved_sass_mixin_includes: bool,
1686    has_sass_function_calls: bool,
1687    sass_symbol_facts: Vec<RuleSassSymbolFact>,
1688}
1689
1690#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
1691struct RuleSassSymbolFact {
1692    symbol_kind: &'static str,
1693    name: String,
1694    role: &'static str,
1695    resolution: &'static str,
1696    byte_span: ParserByteSpanV0,
1697}
1698
1699#[derive(Debug, Clone, PartialEq, Eq)]
1700struct SassNameSpan {
1701    name: String,
1702    byte_span: ParserByteSpanV0,
1703}
1704
1705#[derive(Debug, Clone, PartialEq, Eq)]
1706struct SassVariableDeclFact {
1707    name: String,
1708    scope: SassVariableScope,
1709}
1710
1711#[derive(Debug, Clone, PartialEq, Eq)]
1712struct SassVariableRefFact {
1713    name: String,
1714    byte_span: ParserByteSpanV0,
1715}
1716
1717#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1718enum SassVariableScope {
1719    File,
1720    Span(TextSpan),
1721}
1722
1723#[derive(Debug, Clone, Copy, Default)]
1724struct WrapperContext {
1725    under_media: bool,
1726    under_supports: bool,
1727    under_layer: bool,
1728}
1729
1730#[derive(Debug, Clone, Copy)]
1731struct ValueRefContext<'a> {
1732    known: &'a BTreeSet<String>,
1733    local: &'a BTreeSet<String>,
1734    imported: &'a BTreeSet<String>,
1735}
1736
1737#[derive(Debug, Clone, Copy)]
1738struct SassRefContext<'a> {
1739    variable_decls: &'a [SassVariableDeclFact],
1740    mixin_targets: &'a BTreeSet<String>,
1741    function_targets: &'a BTreeSet<String>,
1742}
1743
1744#[derive(Debug, Clone, Copy)]
1745struct SelectorAttachmentContext<'a> {
1746    source: &'a str,
1747    value_ref_ctx: ValueRefContext<'a>,
1748    known_keyframe_names: &'a BTreeSet<String>,
1749    sass_ref_ctx: SassRefContext<'a>,
1750}
1751
1752#[derive(Debug, Clone, Copy)]
1753enum ValueRefOrigin {
1754    Declaration,
1755    ValueDecl,
1756}
1757
1758#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1759enum ComposesKind {
1760    Local,
1761    Imported,
1762    Global,
1763}
1764
1765#[derive(Debug)]
1766struct ComposesSpec {
1767    class_names: Vec<String>,
1768    kind: ComposesKind,
1769    from_source: Option<String>,
1770}
1771
1772fn classify_declaration_kind(property: &str) -> DeclarationKind {
1773    match property.trim().to_ascii_lowercase().as_str() {
1774        "composes" => DeclarationKind::Composes,
1775        "animation" => DeclarationKind::Animation,
1776        "animation-name" => DeclarationKind::AnimationName,
1777        _ => DeclarationKind::Generic,
1778    }
1779}
1780
1781fn increment_declaration_kind_count(counts: &mut DeclarationKindCountsV0, kind: DeclarationKind) {
1782    match kind {
1783        DeclarationKind::Composes => counts.composes += 1,
1784        DeclarationKind::Animation => counts.animation += 1,
1785        DeclarationKind::AnimationName => counts.animation_name += 1,
1786        DeclarationKind::Generic => counts.generic += 1,
1787    }
1788}
1789
1790fn classify_rule_selector_facts(
1791    rule: &RulePayload,
1792    parent_branches: &[ResolvedSelectorBranch],
1793    parent_is_grouped: bool,
1794) -> RuleSelectorFacts {
1795    let is_nested = !parent_branches.is_empty()
1796        || rule
1797            .selector_groups
1798            .iter()
1799            .any(|group| group.raw.contains('&'));
1800    if !is_nested {
1801        return RuleSelectorFacts {
1802            nested_safety: NestedSafetyKind::Flat,
1803            bem_suffix_count: 0,
1804        };
1805    }
1806
1807    let bem_suffix_safe = rule.selector_groups.len() == 1
1808        && parent_branches.len() == 1
1809        && parent_branches[0].bare_suffix_base
1810        && !parent_is_grouped
1811        && matches!(
1812            rule.selector_groups[0].segments.as_slice(),
1813            [SelectorSegment::Ampersand, SelectorSegment::BemSuffix(_)]
1814        );
1815
1816    if bem_suffix_safe {
1817        RuleSelectorFacts {
1818            nested_safety: NestedSafetyKind::BemSuffixSafe,
1819            bem_suffix_count: 1,
1820        }
1821    } else {
1822        RuleSelectorFacts {
1823            nested_safety: NestedSafetyKind::NestedUnsafe,
1824            bem_suffix_count: 0,
1825        }
1826    }
1827}
1828
1829fn increment_nested_safety_count(
1830    counts: &mut NestedSafetyCountsV0,
1831    kind: NestedSafetyKind,
1832    amount: usize,
1833) {
1834    match kind {
1835        NestedSafetyKind::Flat => counts.flat += amount,
1836        NestedSafetyKind::BemSuffixSafe => counts.bem_suffix_safe += amount,
1837        NestedSafetyKind::NestedUnsafe => counts.nested_unsafe += amount,
1838    }
1839}
1840
1841fn collect_rule_composes_facts(children: &[SyntaxNode]) -> RuleComposesFacts {
1842    let mut facts = RuleComposesFacts::default();
1843    for child in children {
1844        if let Some(SyntaxNodePayload::Declaration(declaration)) = &child.payload
1845            && classify_declaration_kind(&declaration.property) == DeclarationKind::Composes
1846            && let Some(spec) = parse_composes_spec(&declaration.value)
1847        {
1848            match spec.kind {
1849                ComposesKind::Local => facts.local_class_name_count += spec.class_names.len(),
1850                ComposesKind::Imported => {
1851                    facts.imported_class_name_count += spec.class_names.len();
1852                    if let Some(source) = spec.from_source {
1853                        facts.imported_sources.push(source);
1854                    }
1855                }
1856                ComposesKind::Global => facts.global_class_name_count += spec.class_names.len(),
1857            }
1858        }
1859    }
1860    facts
1861}
1862
1863fn declaration_value_span(source: &str, node: &SyntaxNode) -> TextSpan {
1864    let header_span = node.header_span.unwrap_or(node.span);
1865    let raw = &source[header_span.start..header_span.end];
1866    let Some(colon_index) = raw.find(':') else {
1867        return TextSpan::new(header_span.end, header_span.end);
1868    };
1869    trim_source_span(
1870        source,
1871        TextSpan::new(header_span.start + colon_index + 1, header_span.end),
1872    )
1873}
1874
1875fn at_rule_params_span(source: &str, node: &SyntaxNode, at_rule: &AtRulePayload) -> TextSpan {
1876    let header_span = node.header_span.unwrap_or(node.span);
1877    let raw = &source[header_span.start..header_span.end];
1878    let search_start = raw.find('@').map_or(0, |index| index + 1);
1879    let Some(name_index) = raw[search_start..].find(&at_rule.name) else {
1880        return TextSpan::new(header_span.end, header_span.end);
1881    };
1882    let params_start = header_span.start + search_start + name_index + at_rule.name.len();
1883    trim_source_span(source, TextSpan::new(params_start, header_span.end))
1884}
1885
1886fn trim_source_span(source: &str, span: TextSpan) -> TextSpan {
1887    let raw = &source[span.start..span.end];
1888    let trimmed_start = raw.len() - raw.trim_start().len();
1889    let trimmed_len = raw.trim().len();
1890    let start = span.start + trimmed_start;
1891    TextSpan::new(start, start + trimmed_len)
1892}
1893
1894fn source_range_for_byte_span(source: &str, span: ParserByteSpanV0) -> ParserRangeV0 {
1895    ParserRangeV0 {
1896        start: source_position_for_byte_offset(source, span.start),
1897        end: source_position_for_byte_offset(source, span.end),
1898    }
1899}
1900
1901fn source_position_for_byte_offset(source: &str, offset: usize) -> ParserPositionV0 {
1902    let clamped_offset = offset.min(source.len());
1903    let mut line = 0usize;
1904    let mut character = 0usize;
1905
1906    for (byte_index, ch) in source.char_indices() {
1907        if byte_index >= clamped_offset {
1908            break;
1909        }
1910        if ch == '\n' {
1911            line += 1;
1912            character = 0;
1913        } else {
1914            character += ch.len_utf16();
1915        }
1916    }
1917
1918    ParserPositionV0 { line, character }
1919}
1920
1921fn collect_rule_reference_facts(
1922    children: &[SyntaxNode],
1923    source: &str,
1924    value_ref_ctx: ValueRefContext<'_>,
1925    known_keyframe_names: &BTreeSet<String>,
1926    sass_ref_ctx: SassRefContext<'_>,
1927) -> RuleReferenceFacts {
1928    let mut facts = RuleReferenceFacts::default();
1929    for child in children {
1930        match &child.payload {
1931            Some(SyntaxNodePayload::Declaration(declaration)) => {
1932                let value_span = declaration_value_span(source, child);
1933                match classify_declaration_kind(&declaration.property) {
1934                    DeclarationKind::Composes => {}
1935                    DeclarationKind::Animation => {
1936                        if !find_identifier_matches(&declaration.value, known_keyframe_names)
1937                            .is_empty()
1938                        {
1939                            facts.has_animation_refs = true;
1940                        }
1941                        extend_rule_value_ref_facts(&mut facts, &declaration.value, value_ref_ctx);
1942                    }
1943                    DeclarationKind::AnimationName => {
1944                        if !find_identifier_matches(&declaration.value, known_keyframe_names)
1945                            .is_empty()
1946                        {
1947                            facts.has_animation_name_refs = true;
1948                        }
1949                        extend_rule_value_ref_facts(&mut facts, &declaration.value, value_ref_ctx);
1950                    }
1951                    DeclarationKind::Generic => {
1952                        extend_rule_value_ref_facts(&mut facts, &declaration.value, value_ref_ctx);
1953                    }
1954                }
1955                extend_rule_sass_value_ref_facts(
1956                    &mut facts,
1957                    &declaration.value,
1958                    value_span,
1959                    sass_ref_ctx,
1960                );
1961                let custom_property_refs = find_css_var_ref_names(&declaration.value);
1962                if !custom_property_refs.is_empty() {
1963                    facts.has_custom_property_refs = true;
1964                    facts.custom_property_ref_names.extend(custom_property_refs);
1965                }
1966            }
1967            Some(SyntaxNodePayload::AtRule(at_rule)) => match at_rule.kind {
1968                AtRuleKind::Mixin | AtRuleKind::Function => {}
1969                AtRuleKind::Include => {
1970                    let params_span = at_rule_params_span(source, child, at_rule);
1971                    extend_rule_sass_value_ref_facts(
1972                        &mut facts,
1973                        &at_rule.params,
1974                        params_span,
1975                        sass_ref_ctx,
1976                    );
1977                    if let Some(name_span) =
1978                        parse_sass_callable_name_with_span(&at_rule.params, params_span.start)
1979                    {
1980                        facts.has_sass_mixin_includes = true;
1981                        let resolution = if sass_ref_ctx.mixin_targets.contains(&name_span.name) {
1982                            facts.has_resolved_sass_mixin_includes = true;
1983                            "resolved"
1984                        } else {
1985                            facts.has_unresolved_sass_mixin_includes = true;
1986                            "unresolved"
1987                        };
1988                        facts.sass_symbol_facts.push(RuleSassSymbolFact {
1989                            symbol_kind: "mixin",
1990                            name: name_span.name,
1991                            role: "include",
1992                            resolution,
1993                            byte_span: name_span.byte_span,
1994                        });
1995                    }
1996                }
1997                _ => {
1998                    let params_span = at_rule_params_span(source, child, at_rule);
1999                    extend_rule_sass_value_ref_facts(
2000                        &mut facts,
2001                        &at_rule.params,
2002                        params_span,
2003                        sass_ref_ctx,
2004                    );
2005                }
2006            },
2007            _ => {}
2008        }
2009    }
2010    facts
2011}
2012
2013fn extend_rule_value_ref_facts(
2014    facts: &mut RuleReferenceFacts,
2015    value: &str,
2016    value_ref_ctx: ValueRefContext<'_>,
2017) {
2018    let value_refs = find_identifier_matches(value, value_ref_ctx.known);
2019    if !value_refs.is_empty() {
2020        facts.has_value_refs = true;
2021        facts.has_local_value_refs |= value_refs
2022            .iter()
2023            .any(|name| value_ref_ctx.local.contains(name));
2024        facts.has_imported_value_refs |= value_refs
2025            .iter()
2026            .any(|name| value_ref_ctx.imported.contains(name));
2027    }
2028}
2029
2030fn extend_rule_sass_value_ref_facts(
2031    facts: &mut RuleReferenceFacts,
2032    value: &str,
2033    value_span: TextSpan,
2034    sass_ref_ctx: SassRefContext<'_>,
2035) {
2036    let variable_refs = find_sass_variable_ref_spans(value, value_span.start);
2037    if !variable_refs.is_empty() {
2038        facts.has_sass_variable_refs = true;
2039        for name_span in variable_refs {
2040            let resolution = if resolve_sass_variable_ref(
2041                &name_span.name,
2042                name_span.byte_span,
2043                sass_ref_ctx.variable_decls,
2044            ) {
2045                facts.has_resolved_sass_variable_refs = true;
2046                "resolved"
2047            } else {
2048                facts.has_unresolved_sass_variable_refs = true;
2049                "unresolved"
2050            };
2051            facts.sass_symbol_facts.push(RuleSassSymbolFact {
2052                symbol_kind: "variable",
2053                name: name_span.name,
2054                role: "reference",
2055                resolution,
2056                byte_span: name_span.byte_span,
2057            });
2058        }
2059    }
2060
2061    let function_calls =
2062        find_sass_function_call_spans(value, value_span.start, sass_ref_ctx.function_targets);
2063    if !function_calls.is_empty() {
2064        facts.has_sass_function_calls = true;
2065        facts
2066            .sass_symbol_facts
2067            .extend(
2068                function_calls
2069                    .into_iter()
2070                    .map(|name_span| RuleSassSymbolFact {
2071                        symbol_kind: "function",
2072                        name: name_span.name,
2073                        role: "call",
2074                        resolution: "resolved",
2075                        byte_span: name_span.byte_span,
2076                    }),
2077            );
2078    }
2079}
2080
2081fn resolve_sass_variable_ref(
2082    name: &str,
2083    byte_span: ParserByteSpanV0,
2084    variable_decls: &[SassVariableDeclFact],
2085) -> bool {
2086    variable_decls
2087        .iter()
2088        .any(|decl| decl.name == name && sass_variable_scope_contains(decl.scope, byte_span))
2089}
2090
2091fn sass_variable_scope_contains(scope: SassVariableScope, byte_span: ParserByteSpanV0) -> bool {
2092    match scope {
2093        SassVariableScope::File => true,
2094        SassVariableScope::Span(scope_span) => {
2095            scope_span.start <= byte_span.start && scope_span.end >= byte_span.end
2096        }
2097    }
2098}
2099
2100fn collect_index_names(
2101    nodes: &[SyntaxNode],
2102    acc: &mut IndexSummaryAcc,
2103    parent_branches: &[ResolvedSelectorBranch],
2104    parent_is_grouped: bool,
2105    current_sass_scope: Option<TextSpan>,
2106    wrapper_ctx: WrapperContext,
2107) {
2108    for node in nodes {
2109        let mut next_parent_branches = parent_branches.to_vec();
2110        let mut next_parent_is_grouped = false;
2111        let mut split_child_branches = false;
2112        let mut next_sass_scope = current_sass_scope;
2113        let mut child_wrapper_ctx = wrapper_ctx;
2114        match &node.payload {
2115            Some(SyntaxNodePayload::Rule(rule)) => {
2116                next_sass_scope = Some(node.span);
2117                let resolved_branches = resolve_rule_selector_branches(rule, parent_branches);
2118                let custom_property_decl_names = custom_property_decl_names_in_rule(&node.children);
2119                if !custom_property_decl_names.is_empty() {
2120                    let selector_contexts = selector_contexts_for_rule(rule, &resolved_branches);
2121                    let source_order_offset = acc.custom_property_decl_facts.len();
2122                    acc.custom_property_decl_context_selectors
2123                        .extend(selector_contexts.iter().cloned());
2124                    acc.custom_property_decl_facts.extend(
2125                        custom_property_decl_names
2126                            .into_iter()
2127                            .enumerate()
2128                            .map(|(index, name)| ParserIndexCustomPropertyDeclFactV0 {
2129                                name,
2130                                source_order: source_order_offset + index,
2131                                selector_contexts: selector_contexts.clone(),
2132                                under_media: wrapper_ctx.under_media,
2133                                under_supports: wrapper_ctx.under_supports,
2134                                under_layer: wrapper_ctx.under_layer,
2135                            }),
2136                    );
2137                }
2138                if !resolved_branches.is_empty() {
2139                    let resolved: Vec<String> = resolved_branches
2140                        .iter()
2141                        .map(|branch| branch.name.clone())
2142                        .collect();
2143                    let selector_facts =
2144                        classify_rule_selector_facts(rule, parent_branches, parent_is_grouped);
2145                    acc.bem_suffix_count += selector_facts.bem_suffix_count;
2146                    increment_nested_safety_count(
2147                        &mut acc.nested_safety_counts,
2148                        selector_facts.nested_safety,
2149                        resolved.len(),
2150                    );
2151                    acc.selector_names.extend(resolved.iter().cloned());
2152                    let composes_facts = collect_rule_composes_facts(&node.children);
2153                    if composes_facts.local_class_name_count > 0
2154                        || composes_facts.imported_class_name_count > 0
2155                        || composes_facts.global_class_name_count > 0
2156                    {
2157                        acc.selectors_with_composes_names
2158                            .extend(resolved.iter().cloned());
2159                        let selector_multiplier = resolved.len();
2160                        if composes_facts.local_class_name_count > 0 {
2161                            acc.local_composes_selector_names
2162                                .extend(resolved.iter().cloned());
2163                            acc.local_composes_class_name_count +=
2164                                composes_facts.local_class_name_count * selector_multiplier;
2165                        }
2166                        if composes_facts.imported_class_name_count > 0 {
2167                            acc.imported_composes_selector_names
2168                                .extend(resolved.iter().cloned());
2169                            acc.imported_composes_class_name_count +=
2170                                composes_facts.imported_class_name_count * selector_multiplier;
2171                            acc.composes_import_sources.extend(
2172                                composes_facts.imported_sources.iter().flat_map(|source| {
2173                                    std::iter::repeat_n(source.clone(), selector_multiplier)
2174                                }),
2175                            );
2176                        }
2177                        if composes_facts.global_class_name_count > 0 {
2178                            acc.global_composes_selector_names
2179                                .extend(resolved.iter().cloned());
2180                            acc.global_composes_class_name_count +=
2181                                composes_facts.global_class_name_count * selector_multiplier;
2182                        }
2183                        acc.composes_class_name_count += (composes_facts.local_class_name_count
2184                            + composes_facts.imported_class_name_count
2185                            + composes_facts.global_class_name_count)
2186                            * selector_multiplier;
2187                    }
2188                    match selector_facts.nested_safety {
2189                        NestedSafetyKind::BemSuffixSafe => {
2190                            acc.bem_suffix_safe_selector_names
2191                                .extend(resolved.iter().cloned());
2192                            if let Some(parent) = parent_branches.first() {
2193                                acc.bem_suffix_parent_names.push(parent.name.clone());
2194                            }
2195                        }
2196                        NestedSafetyKind::NestedUnsafe => {
2197                            acc.nested_unsafe_selector_names
2198                                .extend(resolved.iter().cloned());
2199                        }
2200                        NestedSafetyKind::Flat => {}
2201                    }
2202                    next_parent_is_grouped = resolved.len() > 1;
2203                    next_parent_branches = resolved_branches;
2204                    split_child_branches = true;
2205                }
2206            }
2207            Some(SyntaxNodePayload::AtRule(at_rule)) => match at_rule.kind {
2208                AtRuleKind::Media => {
2209                    child_wrapper_ctx.under_media = true;
2210                    next_sass_scope = Some(node.span);
2211                }
2212                AtRuleKind::Supports => {
2213                    child_wrapper_ctx.under_supports = true;
2214                    next_sass_scope = Some(node.span);
2215                }
2216                AtRuleKind::Layer => {
2217                    child_wrapper_ctx.under_layer = true;
2218                    next_sass_scope = Some(node.span);
2219                }
2220                AtRuleKind::AtRoot
2221                | AtRuleKind::Mixin
2222                | AtRuleKind::Function
2223                | AtRuleKind::Generic => {
2224                    next_sass_scope = Some(node.span);
2225                }
2226                AtRuleKind::Keyframes
2227                | AtRuleKind::Value
2228                | AtRuleKind::Include
2229                | AtRuleKind::Use
2230                | AtRuleKind::Forward
2231                | AtRuleKind::Import => {}
2232            },
2233            _ => {}
2234        }
2235
2236        match &node.payload {
2237            Some(SyntaxNodePayload::AtRule(at_rule)) => match at_rule.kind {
2238                AtRuleKind::Keyframes if !at_rule.params.is_empty() => {
2239                    acc.keyframes_names.push(at_rule.params.clone());
2240                }
2241                AtRuleKind::Keyframes => {}
2242                AtRuleKind::Value => {
2243                    if let Some(import_specs) = parse_value_import_specs(&at_rule.params) {
2244                        acc.value_import_alias_count += import_specs
2245                            .iter()
2246                            .filter(|spec| spec.imported_name != spec.local_name)
2247                            .count();
2248                        acc.value_import_names
2249                            .extend(import_specs.iter().map(|spec| spec.local_name.clone()));
2250                        acc.value_import_source_by_name
2251                            .extend(import_specs.iter().filter_map(|spec| {
2252                                spec.from_source
2253                                    .as_ref()
2254                                    .map(|source| (spec.local_name.clone(), source.clone()))
2255                            }));
2256                        acc.value_import_sources
2257                            .extend(import_specs.into_iter().filter_map(|spec| spec.from_source));
2258                    } else if let Some((name, _)) = parse_local_value_decl_parts(&at_rule.params) {
2259                        acc.value_decl_names.push(name.to_string());
2260                    }
2261                }
2262                AtRuleKind::Mixin => {
2263                    if let Some(name) = parse_sass_callable_name(&at_rule.params) {
2264                        acc.sass_mixin_decl_names.push(name);
2265                    }
2266                    let parameter_names = parse_sass_parameter_names(&at_rule.params);
2267                    acc.sass_variable_decl_facts
2268                        .extend(parameter_names.iter().map(|name| SassVariableDeclFact {
2269                            name: name.clone(),
2270                            scope: SassVariableScope::Span(node.span),
2271                        }));
2272                    acc.sass_variable_parameter_names.extend(parameter_names);
2273                }
2274                AtRuleKind::Include => {
2275                    if let Some(name) = parse_sass_callable_name(&at_rule.params) {
2276                        acc.sass_mixin_include_names.push(name);
2277                    }
2278                }
2279                AtRuleKind::Function => {
2280                    if let Some(name) = parse_sass_callable_name(&at_rule.params) {
2281                        acc.sass_function_decl_names.push(name);
2282                    }
2283                    let parameter_names = parse_sass_parameter_names(&at_rule.params);
2284                    acc.sass_variable_decl_facts
2285                        .extend(parameter_names.iter().map(|name| SassVariableDeclFact {
2286                            name: name.clone(),
2287                            scope: SassVariableScope::Span(node.span),
2288                        }));
2289                    acc.sass_variable_parameter_names.extend(parameter_names);
2290                }
2291                AtRuleKind::Use => {
2292                    acc.sass_module_use_sources
2293                        .extend(parse_sass_module_sources(&at_rule.params));
2294                    acc.sass_module_use_edges
2295                        .extend(parse_sass_module_use_edges(&at_rule.params));
2296                }
2297                AtRuleKind::Forward => {
2298                    acc.sass_module_forward_sources
2299                        .extend(parse_sass_module_sources(&at_rule.params));
2300                }
2301                AtRuleKind::Import => {
2302                    acc.sass_module_import_sources
2303                        .extend(parse_sass_module_sources(&at_rule.params));
2304                    acc.sass_module_use_sources
2305                        .extend(parse_sass_module_sources(&at_rule.params));
2306                    acc.sass_module_use_edges
2307                        .extend(parse_sass_module_import_use_edges(&at_rule.params));
2308                }
2309                _ => {}
2310            },
2311            Some(SyntaxNodePayload::Declaration(declaration)) => {
2312                if is_css_custom_property_name(&declaration.property) {
2313                    acc.custom_property_decl_names
2314                        .push(declaration.property.clone());
2315                    if wrapper_ctx.under_media {
2316                        acc.custom_property_decl_names_under_media
2317                            .push(declaration.property.clone());
2318                    }
2319                    if wrapper_ctx.under_supports {
2320                        acc.custom_property_decl_names_under_supports
2321                            .push(declaration.property.clone());
2322                    }
2323                    if wrapper_ctx.under_layer {
2324                        acc.custom_property_decl_names_under_layer
2325                            .push(declaration.property.clone());
2326                    }
2327                }
2328                if let Some(name) = parse_sass_variable_decl_name(&declaration.property) {
2329                    acc.sass_variable_decl_facts.push(SassVariableDeclFact {
2330                        name: name.clone(),
2331                        scope: current_sass_scope
2332                            .map(SassVariableScope::Span)
2333                            .unwrap_or(SassVariableScope::File),
2334                    });
2335                    acc.sass_variable_decl_names.push(name);
2336                }
2337            }
2338            _ => {}
2339        }
2340        if split_child_branches {
2341            for parent_branch in &next_parent_branches {
2342                collect_index_names(
2343                    &node.children,
2344                    acc,
2345                    std::slice::from_ref(parent_branch),
2346                    next_parent_is_grouped,
2347                    next_sass_scope,
2348                    child_wrapper_ctx,
2349                );
2350            }
2351        } else {
2352            collect_index_names(
2353                &node.children,
2354                acc,
2355                &next_parent_branches,
2356                next_parent_is_grouped,
2357                next_sass_scope,
2358                child_wrapper_ctx,
2359            );
2360        }
2361    }
2362}
2363
2364fn custom_property_decl_names_in_rule(children: &[SyntaxNode]) -> Vec<String> {
2365    children
2366        .iter()
2367        .filter_map(|child| match &child.payload {
2368            Some(SyntaxNodePayload::Declaration(declaration))
2369                if is_css_custom_property_name(&declaration.property) =>
2370            {
2371                Some(declaration.property.clone())
2372            }
2373            _ => None,
2374        })
2375        .collect()
2376}
2377
2378fn selector_contexts_for_rule(
2379    rule: &RulePayload,
2380    resolved_branches: &[ResolvedSelectorBranch],
2381) -> Vec<String> {
2382    let mut contexts: Vec<String> = rule
2383        .selector_groups
2384        .iter()
2385        .map(|group| group.raw.clone())
2386        .chain(
2387            resolved_branches
2388                .iter()
2389                .map(|branch| format!(".{}", branch.name)),
2390        )
2391        .collect();
2392    contexts.sort();
2393    contexts.dedup();
2394    contexts
2395}
2396
2397fn collect_index_refs_and_counts(
2398    nodes: &[SyntaxNode],
2399    value_ref_ctx: ValueRefContext<'_>,
2400    known_keyframe_names: &BTreeSet<String>,
2401    acc: &mut IndexSummaryAcc,
2402) {
2403    for node in nodes {
2404        match &node.payload {
2405            Some(SyntaxNodePayload::Declaration(declaration)) => {
2406                acc.custom_property_ref_names
2407                    .extend(find_css_var_ref_names(&declaration.value));
2408                match classify_declaration_kind(&declaration.property) {
2409                    DeclarationKind::Composes => {}
2410                    DeclarationKind::Animation => {
2411                        acc.animation_ref_names.extend(find_identifier_matches(
2412                            &declaration.value,
2413                            known_keyframe_names,
2414                        ));
2415                        extend_value_ref_facts(
2416                            acc,
2417                            find_identifier_matches(&declaration.value, value_ref_ctx.known),
2418                            value_ref_ctx,
2419                            ValueRefOrigin::Declaration,
2420                        );
2421                    }
2422                    DeclarationKind::AnimationName => {
2423                        acc.animation_name_ref_names.extend(find_identifier_matches(
2424                            &declaration.value,
2425                            known_keyframe_names,
2426                        ));
2427                        extend_value_ref_facts(
2428                            acc,
2429                            find_identifier_matches(&declaration.value, value_ref_ctx.known),
2430                            value_ref_ctx,
2431                            ValueRefOrigin::Declaration,
2432                        );
2433                    }
2434                    DeclarationKind::Generic => {
2435                        extend_value_ref_facts(
2436                            acc,
2437                            find_identifier_matches(&declaration.value, value_ref_ctx.known),
2438                            value_ref_ctx,
2439                            ValueRefOrigin::Declaration,
2440                        );
2441                    }
2442                }
2443            }
2444            Some(SyntaxNodePayload::AtRule(at_rule)) if at_rule.kind == AtRuleKind::Value => {
2445                if let Some((name, value)) = parse_local_value_decl_parts(&at_rule.params) {
2446                    let value_refs: Vec<String> =
2447                        find_identifier_matches(value, value_ref_ctx.known)
2448                            .into_iter()
2449                            .filter(|candidate| candidate != name)
2450                            .collect();
2451                    if value_refs
2452                        .iter()
2453                        .any(|candidate| value_ref_ctx.local.contains(candidate))
2454                    {
2455                        acc.value_decl_names_with_local_refs.push(name.to_string());
2456                    }
2457                    if value_refs
2458                        .iter()
2459                        .any(|candidate| value_ref_ctx.imported.contains(candidate))
2460                    {
2461                        acc.value_decl_names_with_imported_refs
2462                            .push(name.to_string());
2463                    }
2464                    extend_value_ref_facts(
2465                        acc,
2466                        value_refs,
2467                        value_ref_ctx,
2468                        ValueRefOrigin::ValueDecl,
2469                    );
2470                }
2471            }
2472            _ => {}
2473        }
2474        collect_index_refs_and_counts(&node.children, value_ref_ctx, known_keyframe_names, acc);
2475    }
2476}
2477
2478fn collect_sass_ref_facts(
2479    nodes: &[SyntaxNode],
2480    source: &str,
2481    known_function_names: &BTreeSet<String>,
2482    acc: &mut IndexSummaryAcc,
2483) {
2484    for node in nodes {
2485        match &node.payload {
2486            Some(SyntaxNodePayload::Declaration(declaration)) => {
2487                let value_span = declaration_value_span(source, node);
2488                let variable_refs =
2489                    find_sass_variable_ref_spans(&declaration.value, value_span.start);
2490                acc.sass_variable_ref_names
2491                    .extend(variable_refs.iter().map(|span| span.name.clone()));
2492                acc.sass_variable_ref_facts
2493                    .extend(variable_refs.into_iter().map(|span| SassVariableRefFact {
2494                        name: span.name,
2495                        byte_span: span.byte_span,
2496                    }));
2497                acc.sass_function_call_names
2498                    .extend(find_sass_function_calls(
2499                        &declaration.value,
2500                        known_function_names,
2501                    ));
2502            }
2503            Some(SyntaxNodePayload::AtRule(at_rule)) => match at_rule.kind {
2504                AtRuleKind::Mixin | AtRuleKind::Function => {}
2505                _ => {
2506                    let params_span = at_rule_params_span(source, node, at_rule);
2507                    let variable_refs =
2508                        find_sass_variable_ref_spans(&at_rule.params, params_span.start);
2509                    acc.sass_variable_ref_names
2510                        .extend(variable_refs.iter().map(|span| span.name.clone()));
2511                    acc.sass_variable_ref_facts
2512                        .extend(variable_refs.into_iter().map(|span| SassVariableRefFact {
2513                            name: span.name,
2514                            byte_span: span.byte_span,
2515                        }));
2516                    acc.sass_function_call_names
2517                        .extend(find_sass_function_calls(
2518                            &at_rule.params,
2519                            known_function_names,
2520                        ));
2521                }
2522            },
2523            _ => {}
2524        }
2525        collect_sass_ref_facts(&node.children, source, known_function_names, acc);
2526    }
2527}
2528
2529fn summarize_sass_same_file_resolution(
2530    acc: &IndexSummaryAcc,
2531) -> ParserIndexSassSameFileResolutionFactsV0 {
2532    let mixin_targets: BTreeSet<&str> = acc
2533        .sass_mixin_decl_names
2534        .iter()
2535        .map(String::as_str)
2536        .collect();
2537    let function_targets: BTreeSet<&str> = acc
2538        .sass_function_decl_names
2539        .iter()
2540        .map(String::as_str)
2541        .collect();
2542
2543    let mut resolved_variable_ref_names = BTreeSet::new();
2544    let mut unresolved_variable_ref_names = BTreeSet::new();
2545    for fact in &acc.sass_variable_ref_facts {
2546        if resolve_sass_variable_ref(&fact.name, fact.byte_span, &acc.sass_variable_decl_facts) {
2547            resolved_variable_ref_names.insert(fact.name.clone());
2548        } else {
2549            unresolved_variable_ref_names.insert(fact.name.clone());
2550        }
2551    }
2552
2553    ParserIndexSassSameFileResolutionFactsV0 {
2554        resolved_variable_ref_names: resolved_variable_ref_names.into_iter().collect(),
2555        unresolved_variable_ref_names: unresolved_variable_ref_names.into_iter().collect(),
2556        resolved_mixin_include_names: names_matching(&acc.sass_mixin_include_names, &mixin_targets),
2557        unresolved_mixin_include_names: names_not_matching(
2558            &acc.sass_mixin_include_names,
2559            &mixin_targets,
2560        ),
2561        resolved_function_call_names: names_matching(
2562            &acc.sass_function_call_names,
2563            &function_targets,
2564        ),
2565    }
2566}
2567
2568fn names_matching(names: &[String], targets: &BTreeSet<&str>) -> Vec<String> {
2569    names
2570        .iter()
2571        .filter(|name| targets.contains(name.as_str()))
2572        .cloned()
2573        .collect()
2574}
2575
2576fn names_not_matching(names: &[String], targets: &BTreeSet<&str>) -> Vec<String> {
2577    names
2578        .iter()
2579        .filter(|name| !targets.contains(name.as_str()))
2580        .cloned()
2581        .collect()
2582}
2583
2584fn extend_value_ref_facts(
2585    acc: &mut IndexSummaryAcc,
2586    value_refs: Vec<String>,
2587    value_ref_ctx: ValueRefContext<'_>,
2588    origin: ValueRefOrigin,
2589) {
2590    acc.value_ref_names.extend(value_refs.iter().cloned());
2591
2592    let local_refs: Vec<String> = value_refs
2593        .iter()
2594        .filter(|name| value_ref_ctx.local.contains(*name))
2595        .cloned()
2596        .collect();
2597    acc.local_value_ref_names.extend(local_refs);
2598
2599    let imported_refs: Vec<String> = value_refs
2600        .iter()
2601        .filter(|name| value_ref_ctx.imported.contains(*name))
2602        .cloned()
2603        .collect();
2604    acc.imported_value_ref_names
2605        .extend(imported_refs.iter().cloned());
2606
2607    let imported_ref_sources: Vec<String> = imported_refs
2608        .iter()
2609        .filter_map(|name| acc.value_import_source_by_name.get(name))
2610        .cloned()
2611        .collect();
2612    acc.imported_value_ref_sources
2613        .extend(imported_ref_sources.iter().cloned());
2614
2615    match origin {
2616        ValueRefOrigin::Declaration => {
2617            acc.declaration_value_ref_names.extend(value_refs);
2618            acc.declaration_imported_value_ref_sources
2619                .extend(imported_ref_sources);
2620        }
2621        ValueRefOrigin::ValueDecl => {
2622            acc.value_decl_ref_names.extend(value_refs);
2623            acc.value_decl_imported_value_ref_sources
2624                .extend(imported_ref_sources);
2625        }
2626    }
2627}
2628
2629fn collect_index_selector_attachment_facts(
2630    nodes: &[SyntaxNode],
2631    ctx: SelectorAttachmentContext<'_>,
2632    acc: &mut IndexSummaryAcc,
2633    parent_branches: &[ResolvedSelectorBranch],
2634) {
2635    collect_index_selector_attachment_facts_with_context(
2636        nodes,
2637        ctx,
2638        acc,
2639        parent_branches,
2640        WrapperContext::default(),
2641    );
2642}
2643
2644fn collect_index_selector_attachment_facts_with_context(
2645    nodes: &[SyntaxNode],
2646    ctx: SelectorAttachmentContext<'_>,
2647    acc: &mut IndexSummaryAcc,
2648    parent_branches: &[ResolvedSelectorBranch],
2649    wrapper_ctx: WrapperContext,
2650) {
2651    for node in nodes {
2652        let mut next_parent_branches = parent_branches.to_vec();
2653        let mut split_child_branches = false;
2654        let mut child_wrapper_ctx = wrapper_ctx;
2655        if let Some(SyntaxNodePayload::Rule(rule)) = &node.payload {
2656            let resolved_branches = resolve_rule_selector_branches(rule, parent_branches);
2657            if !resolved_branches.is_empty() {
2658                let resolved: Vec<String> = resolved_branches
2659                    .iter()
2660                    .map(|branch| branch.name.clone())
2661                    .collect();
2662                let ref_facts = collect_rule_reference_facts(
2663                    &node.children,
2664                    ctx.source,
2665                    ctx.value_ref_ctx,
2666                    ctx.known_keyframe_names,
2667                    ctx.sass_ref_ctx,
2668                );
2669                if ref_facts.has_value_refs {
2670                    acc.selectors_with_value_refs_names
2671                        .extend(resolved.iter().cloned());
2672                    if wrapper_ctx.under_media {
2673                        acc.selectors_with_value_refs_under_media_names
2674                            .extend(resolved.iter().cloned());
2675                    }
2676                    if wrapper_ctx.under_supports {
2677                        acc.selectors_with_value_refs_under_supports_names
2678                            .extend(resolved.iter().cloned());
2679                    }
2680                    if wrapper_ctx.under_layer {
2681                        acc.selectors_with_value_refs_under_layer_names
2682                            .extend(resolved.iter().cloned());
2683                    }
2684                }
2685                if ref_facts.has_local_value_refs {
2686                    acc.selectors_with_local_value_refs_names
2687                        .extend(resolved.iter().cloned());
2688                    if wrapper_ctx.under_media {
2689                        acc.selectors_with_local_value_refs_under_media_names
2690                            .extend(resolved.iter().cloned());
2691                    }
2692                    if wrapper_ctx.under_supports {
2693                        acc.selectors_with_local_value_refs_under_supports_names
2694                            .extend(resolved.iter().cloned());
2695                    }
2696                    if wrapper_ctx.under_layer {
2697                        acc.selectors_with_local_value_refs_under_layer_names
2698                            .extend(resolved.iter().cloned());
2699                    }
2700                }
2701                if ref_facts.has_imported_value_refs {
2702                    acc.selectors_with_imported_value_refs_names
2703                        .extend(resolved.iter().cloned());
2704                    if wrapper_ctx.under_media {
2705                        acc.selectors_with_imported_value_refs_under_media_names
2706                            .extend(resolved.iter().cloned());
2707                    }
2708                    if wrapper_ctx.under_supports {
2709                        acc.selectors_with_imported_value_refs_under_supports_names
2710                            .extend(resolved.iter().cloned());
2711                    }
2712                    if wrapper_ctx.under_layer {
2713                        acc.selectors_with_imported_value_refs_under_layer_names
2714                            .extend(resolved.iter().cloned());
2715                    }
2716                }
2717                if ref_facts.has_animation_refs {
2718                    acc.selectors_with_animation_ref_names
2719                        .extend(resolved.iter().cloned());
2720                    if wrapper_ctx.under_media {
2721                        acc.selectors_with_animation_refs_under_media_names
2722                            .extend(resolved.iter().cloned());
2723                    }
2724                    if wrapper_ctx.under_supports {
2725                        acc.selectors_with_animation_refs_under_supports_names
2726                            .extend(resolved.iter().cloned());
2727                    }
2728                    if wrapper_ctx.under_layer {
2729                        acc.selectors_with_animation_refs_under_layer_names
2730                            .extend(resolved.iter().cloned());
2731                    }
2732                }
2733                if ref_facts.has_animation_name_refs {
2734                    acc.selectors_with_animation_name_ref_names
2735                        .extend(resolved.iter().cloned());
2736                    if wrapper_ctx.under_media {
2737                        acc.selectors_with_animation_name_refs_under_media_names
2738                            .extend(resolved.iter().cloned());
2739                    }
2740                    if wrapper_ctx.under_supports {
2741                        acc.selectors_with_animation_name_refs_under_supports_names
2742                            .extend(resolved.iter().cloned());
2743                    }
2744                    if wrapper_ctx.under_layer {
2745                        acc.selectors_with_animation_name_refs_under_layer_names
2746                            .extend(resolved.iter().cloned());
2747                    }
2748                }
2749                if ref_facts.has_custom_property_refs {
2750                    let selector_contexts = selector_contexts_for_rule(rule, &resolved_branches);
2751                    let source_order_offset = acc.custom_property_ref_facts.len();
2752                    acc.selectors_with_custom_property_refs_names
2753                        .extend(resolved.iter().cloned());
2754                    acc.custom_property_ref_facts.extend(
2755                        ref_facts
2756                            .custom_property_ref_names
2757                            .iter()
2758                            .cloned()
2759                            .enumerate()
2760                            .map(|(index, name)| ParserIndexCustomPropertyRefFactV0 {
2761                                name,
2762                                source_order: source_order_offset + index,
2763                                selector_contexts: selector_contexts.clone(),
2764                                under_media: wrapper_ctx.under_media,
2765                                under_supports: wrapper_ctx.under_supports,
2766                                under_layer: wrapper_ctx.under_layer,
2767                            }),
2768                    );
2769                    if wrapper_ctx.under_media {
2770                        acc.selectors_with_custom_property_refs_under_media_names
2771                            .extend(resolved.iter().cloned());
2772                    }
2773                    if wrapper_ctx.under_supports {
2774                        acc.selectors_with_custom_property_refs_under_supports_names
2775                            .extend(resolved.iter().cloned());
2776                    }
2777                    if wrapper_ctx.under_layer {
2778                        acc.selectors_with_custom_property_refs_under_layer_names
2779                            .extend(resolved.iter().cloned());
2780                    }
2781                }
2782                if ref_facts.has_sass_variable_refs {
2783                    acc.sass_selectors_with_variable_refs_names
2784                        .extend(resolved.iter().cloned());
2785                }
2786                if ref_facts.has_resolved_sass_variable_refs {
2787                    acc.sass_selectors_with_resolved_variable_refs_names
2788                        .extend(resolved.iter().cloned());
2789                }
2790                if ref_facts.has_unresolved_sass_variable_refs {
2791                    acc.sass_selectors_with_unresolved_variable_refs_names
2792                        .extend(resolved.iter().cloned());
2793                }
2794                if ref_facts.has_sass_mixin_includes {
2795                    acc.sass_selectors_with_mixin_includes_names
2796                        .extend(resolved.iter().cloned());
2797                }
2798                if ref_facts.has_resolved_sass_mixin_includes {
2799                    acc.sass_selectors_with_resolved_mixin_includes_names
2800                        .extend(resolved.iter().cloned());
2801                }
2802                if ref_facts.has_unresolved_sass_mixin_includes {
2803                    acc.sass_selectors_with_unresolved_mixin_includes_names
2804                        .extend(resolved.iter().cloned());
2805                }
2806                if ref_facts.has_sass_function_calls {
2807                    acc.sass_selectors_with_function_calls_names
2808                        .extend(resolved.iter().cloned());
2809                }
2810                for selector_name in &resolved {
2811                    acc.sass_selector_symbol_facts
2812                        .extend(ref_facts.sass_symbol_facts.iter().map(|fact| {
2813                            ParserIndexSassSelectorSymbolFactV0 {
2814                                selector_name: selector_name.clone(),
2815                                symbol_kind: fact.symbol_kind,
2816                                name: fact.name.clone(),
2817                                role: fact.role,
2818                                resolution: fact.resolution,
2819                                byte_span: fact.byte_span,
2820                                range: source_range_for_byte_span(ctx.source, fact.byte_span),
2821                            }
2822                        }));
2823                }
2824                let composes_facts = collect_rule_composes_facts(&node.children);
2825                if composes_facts.local_class_name_count > 0
2826                    || composes_facts.imported_class_name_count > 0
2827                    || composes_facts.global_class_name_count > 0
2828                {
2829                    if wrapper_ctx.under_media {
2830                        acc.selectors_with_composes_under_media_names
2831                            .extend(resolved.iter().cloned());
2832                    }
2833                    if wrapper_ctx.under_supports {
2834                        acc.selectors_with_composes_under_supports_names
2835                            .extend(resolved.iter().cloned());
2836                    }
2837                    if wrapper_ctx.under_layer {
2838                        acc.selectors_with_composes_under_layer_names
2839                            .extend(resolved.iter().cloned());
2840                    }
2841                }
2842                if composes_facts.local_class_name_count > 0 {
2843                    if wrapper_ctx.under_media {
2844                        acc.local_composes_selector_names_under_media
2845                            .extend(resolved.iter().cloned());
2846                    }
2847                    if wrapper_ctx.under_supports {
2848                        acc.local_composes_selector_names_under_supports
2849                            .extend(resolved.iter().cloned());
2850                    }
2851                    if wrapper_ctx.under_layer {
2852                        acc.local_composes_selector_names_under_layer
2853                            .extend(resolved.iter().cloned());
2854                    }
2855                }
2856                if composes_facts.imported_class_name_count > 0 {
2857                    if wrapper_ctx.under_media {
2858                        acc.imported_composes_selector_names_under_media
2859                            .extend(resolved.iter().cloned());
2860                        acc.composes_import_sources_under_media.extend(
2861                            composes_facts.imported_sources.iter().flat_map(|source| {
2862                                std::iter::repeat_n(source.clone(), resolved.len())
2863                            }),
2864                        );
2865                    }
2866                    if wrapper_ctx.under_supports {
2867                        acc.imported_composes_selector_names_under_supports
2868                            .extend(resolved.iter().cloned());
2869                        acc.composes_import_sources_under_supports.extend(
2870                            composes_facts.imported_sources.iter().flat_map(|source| {
2871                                std::iter::repeat_n(source.clone(), resolved.len())
2872                            }),
2873                        );
2874                    }
2875                    if wrapper_ctx.under_layer {
2876                        acc.imported_composes_selector_names_under_layer
2877                            .extend(resolved.iter().cloned());
2878                        acc.composes_import_sources_under_layer.extend(
2879                            composes_facts.imported_sources.iter().flat_map(|source| {
2880                                std::iter::repeat_n(source.clone(), resolved.len())
2881                            }),
2882                        );
2883                    }
2884                }
2885                if composes_facts.global_class_name_count > 0 {
2886                    if wrapper_ctx.under_media {
2887                        acc.global_composes_selector_names_under_media
2888                            .extend(resolved.iter().cloned());
2889                    }
2890                    if wrapper_ctx.under_supports {
2891                        acc.global_composes_selector_names_under_supports
2892                            .extend(resolved.iter().cloned());
2893                    }
2894                    if wrapper_ctx.under_layer {
2895                        acc.global_composes_selector_names_under_layer
2896                            .extend(resolved.iter().cloned());
2897                    }
2898                }
2899                if wrapper_ctx.under_media {
2900                    acc.selectors_under_media_names
2901                        .extend(resolved.iter().cloned());
2902                }
2903                if wrapper_ctx.under_supports {
2904                    acc.selectors_under_supports_names
2905                        .extend(resolved.iter().cloned());
2906                }
2907                if wrapper_ctx.under_layer {
2908                    acc.selectors_under_layer_names
2909                        .extend(resolved.iter().cloned());
2910                }
2911                next_parent_branches = resolved_branches;
2912                split_child_branches = true;
2913            }
2914        } else if let Some(SyntaxNodePayload::AtRule(at_rule)) = &node.payload {
2915            if at_rule.kind == AtRuleKind::Keyframes && !at_rule.params.is_empty() {
2916                if wrapper_ctx.under_media {
2917                    acc.keyframes_names_under_media.push(at_rule.params.clone());
2918                }
2919                if wrapper_ctx.under_supports {
2920                    acc.keyframes_names_under_supports
2921                        .push(at_rule.params.clone());
2922                }
2923                if wrapper_ctx.under_layer {
2924                    acc.keyframes_names_under_layer.push(at_rule.params.clone());
2925                }
2926            }
2927            match at_rule.kind {
2928                AtRuleKind::Media => child_wrapper_ctx.under_media = true,
2929                AtRuleKind::Supports => child_wrapper_ctx.under_supports = true,
2930                AtRuleKind::Layer => child_wrapper_ctx.under_layer = true,
2931                _ => {}
2932            }
2933        }
2934
2935        if split_child_branches {
2936            for parent_branch in &next_parent_branches {
2937                collect_index_selector_attachment_facts_with_context(
2938                    &node.children,
2939                    ctx,
2940                    acc,
2941                    std::slice::from_ref(parent_branch),
2942                    child_wrapper_ctx,
2943                );
2944            }
2945        } else {
2946            collect_index_selector_attachment_facts_with_context(
2947                &node.children,
2948                ctx,
2949                acc,
2950                &next_parent_branches,
2951                child_wrapper_ctx,
2952            );
2953        }
2954    }
2955}
2956
2957fn parse_local_value_decl_parts(params: &str) -> Option<(&str, &str)> {
2958    if params.contains(" from ") {
2959        return None;
2960    }
2961    let (name, value) = params.split_once(':')?;
2962    let trimmed_name = name.trim();
2963    let trimmed_value = value.trim();
2964    if trimmed_name.is_empty() || trimmed_value.is_empty() {
2965        return None;
2966    }
2967    Some((trimmed_name, trimmed_value))
2968}
2969
2970struct ValueImportSpec {
2971    imported_name: String,
2972    local_name: String,
2973    from_source: Option<String>,
2974}
2975
2976fn parse_value_import_specs(params: &str) -> Option<Vec<ValueImportSpec>> {
2977    let (raw_specs, raw_source) = params.split_once(" from ")?;
2978    let from_source = parse_quoted_import_source(raw_source);
2979    let mut specs = Vec::new();
2980    for raw_spec in raw_specs.split(',') {
2981        let trimmed = raw_spec.trim();
2982        if trimmed.is_empty() {
2983            continue;
2984        }
2985        let imported_name = trimmed
2986            .split_once(" as ")
2987            .map(|(imported, _)| imported.trim())
2988            .unwrap_or(trimmed);
2989        let local_name = trimmed
2990            .split_once(" as ")
2991            .map(|(_, local)| local.trim())
2992            .unwrap_or(trimmed);
2993        if !imported_name.is_empty() && !local_name.is_empty() {
2994            specs.push(ValueImportSpec {
2995                imported_name: imported_name.to_string(),
2996                local_name: local_name.to_string(),
2997                from_source: from_source.clone(),
2998            });
2999        }
3000    }
3001    (!specs.is_empty()).then_some(specs)
3002}
3003
3004fn parse_composes_spec(value: &str) -> Option<ComposesSpec> {
3005    let head = value
3006        .split_once(" from ")
3007        .map(|(left, _)| left)
3008        .unwrap_or(value);
3009    let class_names: Vec<String> = head
3010        .split_whitespace()
3011        .filter(|name| !name.is_empty())
3012        .map(ToString::to_string)
3013        .collect();
3014    if class_names.is_empty() {
3015        return None;
3016    }
3017    let from_source = value
3018        .split_once(" from ")
3019        .and_then(|(_, source)| parse_quoted_import_source(source));
3020    let kind = match value.split_once(" from ").map(|(_, source)| source.trim()) {
3021        Some("global") => ComposesKind::Global,
3022        Some(_) => ComposesKind::Imported,
3023        None => ComposesKind::Local,
3024    };
3025    Some(ComposesSpec {
3026        class_names,
3027        kind,
3028        from_source,
3029    })
3030}
3031
3032fn parse_quoted_import_source(raw: &str) -> Option<String> {
3033    let trimmed = raw.trim();
3034    if trimmed.len() < 2 {
3035        return None;
3036    }
3037    let quote = trimmed.chars().next()?;
3038    if !matches!(quote, '"' | '\'') || !trimmed.ends_with(quote) {
3039        return None;
3040    }
3041    Some(trimmed[1..trimmed.len() - 1].to_string())
3042}
3043
3044fn parse_sass_variable_decl_name(property: &str) -> Option<String> {
3045    let name = property.trim().strip_prefix('$')?;
3046    (!name.is_empty() && name.chars().all(is_sass_ident_continue)).then(|| name.to_string())
3047}
3048
3049fn parse_sass_callable_name(params: &str) -> Option<String> {
3050    parse_sass_callable_name_with_span(params, 0).map(|span| span.name)
3051}
3052
3053fn parse_sass_callable_name_with_span(params: &str, base_start: usize) -> Option<SassNameSpan> {
3054    let trimmed_start = params.find(|ch: char| !ch.is_whitespace())?;
3055    let trimmed = &params[trimmed_start..];
3056    let end = trimmed
3057        .find(|ch: char| ch.is_whitespace() || ch == '(')
3058        .unwrap_or(trimmed.len());
3059    let name = &trimmed[..end];
3060    (!name.is_empty() && name.chars().all(is_sass_ident_continue)).then(|| SassNameSpan {
3061        name: name.to_string(),
3062        byte_span: ParserByteSpanV0 {
3063            start: base_start + trimmed_start,
3064            end: base_start + trimmed_start + end,
3065        },
3066    })
3067}
3068
3069fn parse_sass_parameter_names(params: &str) -> Vec<String> {
3070    let Some(open_index) = params.find('(') else {
3071        return Vec::new();
3072    };
3073    let close_index = params[open_index + 1..]
3074        .find(')')
3075        .map(|index| open_index + 1 + index)
3076        .unwrap_or(params.len());
3077    find_sass_variable_refs(&params[open_index + 1..close_index])
3078}
3079
3080fn parse_sass_module_sources(params: &str) -> Vec<String> {
3081    let mut sources = Vec::new();
3082    let chars: Vec<char> = params.chars().collect();
3083    let mut index = 0usize;
3084
3085    while index < chars.len() {
3086        let quote = chars[index];
3087        if !matches!(quote, '"' | '\'') {
3088            index += 1;
3089            continue;
3090        }
3091        index += 1;
3092        let start = index;
3093        while index < chars.len() {
3094            if chars[index] == '\\' {
3095                index = (index + 2).min(chars.len());
3096                continue;
3097            }
3098            if chars[index] == quote {
3099                break;
3100            }
3101            index += 1;
3102        }
3103        if start < index {
3104            sources.push(chars[start..index].iter().collect());
3105        }
3106        index += usize::from(index < chars.len());
3107    }
3108
3109    sources
3110}
3111
3112fn parse_sass_module_use_edges(params: &str) -> Vec<ParserIndexSassModuleUseFactV0> {
3113    let alias = parse_sass_use_alias(params);
3114    parse_sass_module_sources(params)
3115        .into_iter()
3116        .map(|source| match alias.as_deref() {
3117            Some("*") => ParserIndexSassModuleUseFactV0 {
3118                source,
3119                namespace_kind: "wildcard",
3120                namespace: None,
3121            },
3122            Some(namespace) if is_valid_sass_namespace(namespace) => {
3123                ParserIndexSassModuleUseFactV0 {
3124                    source,
3125                    namespace_kind: "alias",
3126                    namespace: Some(namespace.to_string()),
3127                }
3128            }
3129            _ => ParserIndexSassModuleUseFactV0 {
3130                namespace: default_sass_namespace_for_source(&source),
3131                source,
3132                namespace_kind: "default",
3133            },
3134        })
3135        .collect()
3136}
3137
3138fn parse_sass_module_import_use_edges(params: &str) -> Vec<ParserIndexSassModuleUseFactV0> {
3139    parse_sass_module_sources(params)
3140        .into_iter()
3141        .map(|source| ParserIndexSassModuleUseFactV0 {
3142            source,
3143            namespace_kind: "wildcard",
3144            namespace: None,
3145        })
3146        .collect()
3147}
3148
3149fn parse_sass_use_alias(params: &str) -> Option<String> {
3150    let chars: Vec<char> = params.chars().collect();
3151    let mut tokens = Vec::new();
3152    let mut current = String::new();
3153    let mut index = 0usize;
3154    let mut quote: Option<char> = None;
3155
3156    while index < chars.len() {
3157        let ch = chars[index];
3158        if let Some(active_quote) = quote {
3159            if ch == '\\' && index + 1 < chars.len() {
3160                index += 2;
3161                continue;
3162            }
3163            if ch == active_quote {
3164                quote = None;
3165            }
3166            index += 1;
3167            continue;
3168        }
3169
3170        if ch == '"' || ch == '\'' {
3171            if !current.is_empty() {
3172                tokens.push(std::mem::take(&mut current));
3173            }
3174            quote = Some(ch);
3175            index += 1;
3176            continue;
3177        }
3178
3179        if ch.is_whitespace() || ch == ';' || ch == ',' {
3180            if !current.is_empty() {
3181                tokens.push(std::mem::take(&mut current));
3182            }
3183            index += 1;
3184            continue;
3185        }
3186
3187        current.push(ch);
3188        index += 1;
3189    }
3190
3191    if !current.is_empty() {
3192        tokens.push(current);
3193    }
3194
3195    tokens
3196        .windows(2)
3197        .find_map(|window| (window[0].eq_ignore_ascii_case("as")).then(|| window[1].clone()))
3198}
3199
3200fn default_sass_namespace_for_source(source: &str) -> Option<String> {
3201    let clean = source
3202        .split(['?', '#'])
3203        .next()
3204        .unwrap_or(source)
3205        .trim_end_matches('/');
3206    let segment = clean.rsplit('/').next().unwrap_or(clean);
3207    let package_segment = segment.rsplit(':').next().unwrap_or(segment);
3208    let stem = package_segment
3209        .rsplit_once('.')
3210        .map(|(stem, _)| stem)
3211        .unwrap_or(package_segment);
3212    let stem = stem.strip_prefix('_').unwrap_or(stem);
3213
3214    is_valid_sass_namespace(stem).then(|| stem.to_string())
3215}
3216
3217fn is_valid_sass_namespace(value: &str) -> bool {
3218    let mut chars = value.chars();
3219    let Some(first) = chars.next() else {
3220        return false;
3221    };
3222    is_sass_ident_start(first) && chars.all(is_sass_ident_continue)
3223}
3224
3225fn find_sass_variable_refs(raw: &str) -> Vec<String> {
3226    find_sass_variable_ref_spans(raw, 0)
3227        .into_iter()
3228        .map(|span| span.name)
3229        .collect()
3230}
3231
3232fn find_sass_variable_ref_spans(raw: &str, base_start: usize) -> Vec<SassNameSpan> {
3233    let mut refs = Vec::new();
3234    let mut chars = raw.char_indices().peekable();
3235    let mut quote: Option<char> = None;
3236
3237    while let Some((byte_index, ch)) = chars.next() {
3238        if let Some(active_quote) = quote {
3239            if ch == '\\' {
3240                chars.next();
3241                continue;
3242            }
3243            if ch == active_quote {
3244                quote = None;
3245            }
3246            continue;
3247        }
3248
3249        if ch == '"' || ch == '\'' {
3250            quote = Some(ch);
3251            continue;
3252        }
3253
3254        if ch == '$' {
3255            if is_sass_module_qualified_reference(raw, byte_index) {
3256                continue;
3257            }
3258            let name_start = byte_index + ch.len_utf8();
3259            let mut name_end = name_start;
3260            let mut name = String::new();
3261            while let Some(&(next_index, next_ch)) = chars.peek() {
3262                if !is_sass_ident_continue(next_ch) {
3263                    break;
3264                }
3265                name.push(next_ch);
3266                name_end = next_index + next_ch.len_utf8();
3267                chars.next();
3268            }
3269            if !name.is_empty() {
3270                refs.push(SassNameSpan {
3271                    name,
3272                    byte_span: ParserByteSpanV0 {
3273                        start: base_start + byte_index,
3274                        end: base_start + name_end,
3275                    },
3276                });
3277            }
3278        }
3279    }
3280
3281    refs
3282}
3283
3284fn find_sass_function_calls(raw: &str, known_function_names: &BTreeSet<String>) -> Vec<String> {
3285    find_sass_function_call_spans(raw, 0, known_function_names)
3286        .into_iter()
3287        .map(|span| span.name)
3288        .collect()
3289}
3290
3291fn find_sass_function_call_spans(
3292    raw: &str,
3293    base_start: usize,
3294    known_function_names: &BTreeSet<String>,
3295) -> Vec<SassNameSpan> {
3296    let mut calls = Vec::new();
3297    let mut chars = raw.char_indices().peekable();
3298    let mut quote: Option<char> = None;
3299
3300    while let Some((byte_index, ch)) = chars.next() {
3301        if let Some(active_quote) = quote {
3302            if ch == '\\' {
3303                chars.next();
3304                continue;
3305            }
3306            if ch == active_quote {
3307                quote = None;
3308            }
3309            continue;
3310        }
3311
3312        if ch == '"' || ch == '\'' {
3313            quote = Some(ch);
3314            continue;
3315        }
3316
3317        if is_sass_ident_start(ch) {
3318            if is_sass_module_qualified_reference(raw, byte_index) {
3319                continue;
3320            }
3321            let mut name = String::from(ch);
3322            let mut name_end = byte_index + ch.len_utf8();
3323            while let Some(&(next_index, next_ch)) = chars.peek() {
3324                if !is_sass_ident_continue(next_ch) {
3325                    break;
3326                }
3327                name.push(next_ch);
3328                name_end = next_index + next_ch.len_utf8();
3329                chars.next();
3330            }
3331            while let Some(&(_, next_ch)) = chars.peek() {
3332                if !next_ch.is_whitespace() {
3333                    break;
3334                }
3335                chars.next();
3336            }
3337            if matches!(chars.peek(), Some(&(_, '('))) && known_function_names.contains(&name) {
3338                calls.push(SassNameSpan {
3339                    name,
3340                    byte_span: ParserByteSpanV0 {
3341                        start: base_start + byte_index,
3342                        end: base_start + name_end,
3343                    },
3344                });
3345            }
3346        }
3347    }
3348
3349    calls
3350}
3351
3352fn is_sass_module_qualified_reference(raw: &str, start: usize) -> bool {
3353    if start <= 1 || !raw[..start].ends_with('.') {
3354        return false;
3355    }
3356    let namespace = raw[..start - 1]
3357        .rsplit(|ch: char| !is_sass_ident_continue(ch))
3358        .next()
3359        .unwrap_or("");
3360    is_valid_sass_namespace(namespace)
3361}
3362
3363fn find_identifier_matches(raw: &str, known_names: &BTreeSet<String>) -> Vec<String> {
3364    let mut matches = Vec::new();
3365    let chars: Vec<char> = raw.chars().collect();
3366    let mut index = 0usize;
3367    let mut quote: Option<char> = None;
3368    let mut identifier_start: Option<usize> = None;
3369
3370    let flush_identifier =
3371        |end: usize, identifier_start: &mut Option<usize>, matches: &mut Vec<String>| {
3372            if let Some(start) = *identifier_start {
3373                let candidate: String = chars[start..end].iter().collect();
3374                if known_names.contains(&candidate) {
3375                    matches.push(candidate);
3376                }
3377                *identifier_start = None;
3378            }
3379        };
3380
3381    while index < chars.len() {
3382        let ch = chars[index];
3383        if let Some(active_quote) = quote {
3384            if ch == '\\' && index + 1 < chars.len() {
3385                index += 2;
3386                continue;
3387            }
3388            if ch == active_quote {
3389                quote = None;
3390            }
3391            index += 1;
3392            continue;
3393        }
3394
3395        if ch == '"' || ch == '\'' {
3396            flush_identifier(index, &mut identifier_start, &mut matches);
3397            quote = Some(ch);
3398            index += 1;
3399            continue;
3400        }
3401
3402        if is_value_ident_continue(ch) {
3403            if identifier_start.is_none() {
3404                identifier_start = Some(index);
3405            }
3406            index += 1;
3407            continue;
3408        }
3409
3410        flush_identifier(index, &mut identifier_start, &mut matches);
3411        index += 1;
3412    }
3413
3414    flush_identifier(chars.len(), &mut identifier_start, &mut matches);
3415    matches
3416}
3417
3418fn is_value_ident_continue(ch: char) -> bool {
3419    ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '$')
3420}
3421
3422fn find_css_var_ref_names(raw: &str) -> Vec<String> {
3423    let mut refs = Vec::new();
3424    let mut search_start = 0usize;
3425
3426    while let Some(relative_start) = raw[search_start..].find("var(") {
3427        let function_start = search_start + relative_start;
3428        if !is_css_function_boundary(raw, function_start) {
3429            search_start = function_start + "var(".len();
3430            continue;
3431        }
3432
3433        let mut cursor = function_start + "var(".len();
3434        while cursor < raw.len() {
3435            let Some(ch) = raw[cursor..].chars().next() else {
3436                break;
3437            };
3438            if !ch.is_whitespace() {
3439                break;
3440            }
3441            cursor += ch.len_utf8();
3442        }
3443
3444        if !raw[cursor..].starts_with("--") {
3445            search_start = cursor;
3446            continue;
3447        }
3448
3449        let name_start = cursor;
3450        let mut name_end = cursor;
3451        for (relative_index, ch) in raw[name_start..].char_indices() {
3452            let absolute_index = name_start + relative_index;
3453            if relative_index == 0 {
3454                name_end = absolute_index + ch.len_utf8();
3455                continue;
3456            }
3457            if !is_css_custom_property_ident_continue(ch) {
3458                break;
3459            }
3460            name_end = absolute_index + ch.len_utf8();
3461        }
3462
3463        let candidate = &raw[name_start..name_end];
3464        if is_css_custom_property_name(candidate) {
3465            refs.push(candidate.to_string());
3466        }
3467        search_start = name_end.max(function_start + "var(".len());
3468    }
3469
3470    refs
3471}
3472
3473fn is_css_function_boundary(raw: &str, function_start: usize) -> bool {
3474    raw[..function_start]
3475        .chars()
3476        .next_back()
3477        .is_none_or(|ch| !ch.is_ascii_alphanumeric() && ch != '_')
3478}
3479
3480fn is_css_custom_property_name(name: &str) -> bool {
3481    let Some(rest) = name.strip_prefix("--") else {
3482        return false;
3483    };
3484    let mut chars = rest.chars();
3485    let Some(first) = chars.next() else {
3486        return false;
3487    };
3488    is_css_custom_property_ident_start(first) && chars.all(is_css_custom_property_ident_continue)
3489}
3490
3491fn is_css_custom_property_ident_start(ch: char) -> bool {
3492    ch == '_' || ch == '-' || ch.is_ascii_alphabetic() || !ch.is_ascii()
3493}
3494
3495fn is_css_custom_property_ident_continue(ch: char) -> bool {
3496    is_css_custom_property_ident_start(ch) || ch.is_ascii_digit()
3497}
3498
3499fn is_sass_ident_start(ch: char) -> bool {
3500    ch.is_ascii_alphabetic() || ch == '_' || ch == '-' || !ch.is_ascii()
3501}
3502
3503fn is_sass_ident_continue(ch: char) -> bool {
3504    is_sass_ident_start(ch) || ch.is_ascii_digit()
3505}
3506
3507fn extract_simple_selector_name(prelude: &str) -> Option<String> {
3508    let trimmed = prelude.trim();
3509    let rest = trimmed.strip_prefix('.')?;
3510    if rest.is_empty() {
3511        return None;
3512    }
3513    if rest.contains([' ', ',', ':', '&', '#', '[', '>', '+', '~']) {
3514        return None;
3515    }
3516    Some(rest.to_string())
3517}
3518
3519fn resolve_rule_selector_branches(
3520    rule: &RulePayload,
3521    parent_branches: &[ResolvedSelectorBranch],
3522) -> Vec<ResolvedSelectorBranch> {
3523    let mut branches = Vec::new();
3524
3525    for group in &rule.selector_groups {
3526        let resolved = extract_group_selector_branches(group, parent_branches);
3527        if !resolved.is_empty() {
3528            branches.extend(resolved);
3529        } else if let Some(local_names) = extract_local_function_selector_names(&group.raw) {
3530            branches.extend(repeat_names_for_parent_branches(
3531                local_names,
3532                parent_branches,
3533                false,
3534            ));
3535        } else if let Some(name) = extract_simple_selector_name(&group.raw) {
3536            branches.extend(repeat_names_for_parent_branches(
3537                vec![name],
3538                parent_branches,
3539                true,
3540            ));
3541        }
3542    }
3543
3544    branches
3545}
3546
3547fn repeat_names_for_parent_branches(
3548    names: Vec<String>,
3549    parent_branches: &[ResolvedSelectorBranch],
3550    bare_when_top_level_single: bool,
3551) -> Vec<ResolvedSelectorBranch> {
3552    if names.is_empty() {
3553        return Vec::new();
3554    }
3555    if parent_branches.is_empty() {
3556        let bare_suffix_base = bare_when_top_level_single && names.len() == 1;
3557        return names
3558            .into_iter()
3559            .map(|name| ResolvedSelectorBranch {
3560                name,
3561                bare_suffix_base,
3562            })
3563            .collect();
3564    }
3565    if parent_branches.len() == 1 {
3566        return names
3567            .into_iter()
3568            .map(|name| ResolvedSelectorBranch {
3569                name,
3570                bare_suffix_base: false,
3571            })
3572            .collect();
3573    }
3574    let mut repeated = Vec::with_capacity(names.len() * parent_branches.len());
3575    for _ in parent_branches {
3576        repeated.extend(names.iter().cloned().map(|name| ResolvedSelectorBranch {
3577            name,
3578            bare_suffix_base: false,
3579        }));
3580    }
3581    repeated
3582}
3583
3584fn extract_group_selector_branches(
3585    group: &SelectorGroup,
3586    parent_branches: &[ResolvedSelectorBranch],
3587) -> Vec<ResolvedSelectorBranch> {
3588    match group.segments.as_slice() {
3589        [SelectorSegment::ClassName(name)] => {
3590            repeat_names_for_parent_branches(vec![name.clone()], parent_branches, true)
3591        }
3592        [
3593            SelectorSegment::Ampersand,
3594            SelectorSegment::BemSuffix(suffix),
3595        ] => parent_branches
3596            .iter()
3597            .filter(|parent| parent.bare_suffix_base)
3598            .map(|parent| ResolvedSelectorBranch {
3599                name: format!("{}{}", parent.name, suffix),
3600                bare_suffix_base: true,
3601            })
3602            .collect(),
3603        [
3604            SelectorSegment::Ampersand,
3605            SelectorSegment::AmpersandSuffix(suffix),
3606        ] => parent_branches
3607            .iter()
3608            .filter(|parent| parent.bare_suffix_base)
3609            .map(|parent| ResolvedSelectorBranch {
3610                name: format!("{}{}", parent.name, suffix),
3611                bare_suffix_base: true,
3612            })
3613            .collect(),
3614        [SelectorSegment::Ampersand, SelectorSegment::ClassName(name)] => {
3615            repeat_names_for_parent_branches(vec![name.clone()], parent_branches, false)
3616        }
3617        segments => {
3618            let last_combinator = segments
3619                .iter()
3620                .enumerate()
3621                .rev()
3622                .find_map(|(index, segment)| {
3623                    matches!(segment, SelectorSegment::Combinator(_)).then_some(index)
3624                });
3625            let tail = last_combinator
3626                .map(|index| &segments[index + 1..])
3627                .unwrap_or(segments);
3628            if let [
3629                SelectorSegment::Ampersand,
3630                SelectorSegment::BemSuffix(suffix) | SelectorSegment::AmpersandSuffix(suffix),
3631            ] = tail
3632            {
3633                return parent_branches
3634                    .iter()
3635                    .filter(|parent| parent.bare_suffix_base)
3636                    .map(|parent| ResolvedSelectorBranch {
3637                        name: format!("{}{}", parent.name, suffix),
3638                        bare_suffix_base: false,
3639                    })
3640                    .collect();
3641            }
3642            let names: Vec<String> = tail
3643                .iter()
3644                .filter_map(|segment| match segment {
3645                    SelectorSegment::ClassName(name) => Some(name.clone()),
3646                    _ => None,
3647                })
3648                .collect();
3649            repeat_names_for_parent_branches(names, parent_branches, false)
3650        }
3651    }
3652}
3653
3654fn extract_local_function_selector_names(raw: &str) -> Option<Vec<String>> {
3655    let trimmed = raw.trim();
3656    let inner = trimmed
3657        .strip_prefix(":local(")
3658        .and_then(|rest| rest.strip_suffix(')'))?;
3659    let mut names = Vec::new();
3660    let chars: Vec<char> = inner.chars().collect();
3661    let mut index = 0usize;
3662
3663    while index < chars.len() {
3664        if chars[index] == '.' {
3665            index += 1;
3666            let start = index;
3667            while index < chars.len() && is_selector_ident_continue(chars[index]) {
3668                index += 1;
3669            }
3670            if start < index {
3671                names.push(chars[start..index].iter().collect());
3672                continue;
3673            }
3674        }
3675        index += 1;
3676    }
3677
3678    (!names.is_empty()).then_some(names)
3679}
3680
3681fn parse_selector_groups(prelude: &str) -> Vec<SelectorGroup> {
3682    let mut groups = Vec::new();
3683    let mut depth_paren = 0usize;
3684    let mut depth_bracket = 0usize;
3685    let mut start = 0usize;
3686
3687    for (index, ch) in prelude.char_indices() {
3688        match ch {
3689            '(' => depth_paren += 1,
3690            ')' => depth_paren = depth_paren.saturating_sub(1),
3691            '[' => depth_bracket += 1,
3692            ']' => depth_bracket = depth_bracket.saturating_sub(1),
3693            ',' if depth_paren == 0 && depth_bracket == 0 => {
3694                let raw = prelude[start..index].trim();
3695                if !raw.is_empty() {
3696                    groups.push(SelectorGroup {
3697                        raw: raw.to_string(),
3698                        segments: parse_selector_segments(raw),
3699                    });
3700                }
3701                start = index + ch.len_utf8();
3702            }
3703            _ => {}
3704        }
3705    }
3706
3707    let raw = prelude[start..].trim();
3708    if !raw.is_empty() {
3709        groups.push(SelectorGroup {
3710            raw: raw.to_string(),
3711            segments: parse_selector_segments(raw),
3712        });
3713    }
3714
3715    groups
3716}
3717
3718fn parse_selector_segments(raw: &str) -> Vec<SelectorSegment> {
3719    let chars: Vec<char> = raw.chars().collect();
3720    let mut index = 0usize;
3721    let mut segments = Vec::new();
3722
3723    while index < chars.len() {
3724        match chars[index] {
3725            c if c.is_whitespace() => {
3726                let start = index;
3727                while index < chars.len() && chars[index].is_whitespace() {
3728                    index += 1;
3729                }
3730                let next = chars.get(index).copied();
3731                let has_prev = segments.last().is_some();
3732                let prev_is_combinator =
3733                    matches!(segments.last(), Some(SelectorSegment::Combinator(_)));
3734                let next_starts_selector = next.is_some_and(|ch| {
3735                    matches!(ch, '.' | '&' | ':' | '#' | '*' | '[') || ch.is_ascii_alphabetic()
3736                });
3737                if start > 0 && has_prev && !prev_is_combinator && next_starts_selector {
3738                    segments.push(SelectorSegment::Combinator(" ".to_string()));
3739                }
3740            }
3741            '.' => {
3742                index += 1;
3743                let start = index;
3744                while index < chars.len() && is_selector_ident_continue(chars[index]) {
3745                    index += 1;
3746                }
3747                if start < index {
3748                    segments.push(SelectorSegment::ClassName(
3749                        chars[start..index].iter().collect(),
3750                    ));
3751                } else {
3752                    segments.push(SelectorSegment::Other(".".to_string()));
3753                }
3754            }
3755            '&' => {
3756                segments.push(SelectorSegment::Ampersand);
3757                index += 1;
3758                if index + 1 < chars.len()
3759                    && ((chars[index] == '-' && chars[index + 1] == '-')
3760                        || (chars[index] == '_' && chars[index + 1] == '_'))
3761                {
3762                    let start = index;
3763                    index += 2;
3764                    while index < chars.len() && is_selector_ident_continue(chars[index]) {
3765                        index += 1;
3766                    }
3767                    segments.push(SelectorSegment::BemSuffix(
3768                        chars[start..index].iter().collect(),
3769                    ));
3770                } else if index < chars.len()
3771                    && !starts_selector_component_after_ampersand(chars[index])
3772                {
3773                    let start = index;
3774                    while index < chars.len() && is_selector_ident_continue(chars[index]) {
3775                        index += 1;
3776                    }
3777                    if start < index {
3778                        segments.push(SelectorSegment::AmpersandSuffix(
3779                            chars[start..index].iter().collect(),
3780                        ));
3781                    }
3782                }
3783            }
3784            ':' => {
3785                index += 1;
3786                let start = index;
3787                while index < chars.len() && is_selector_ident_continue(chars[index]) {
3788                    index += 1;
3789                }
3790                segments.push(SelectorSegment::Pseudo(
3791                    chars[start..index].iter().collect(),
3792                ));
3793                if index < chars.len() && chars[index] == '(' {
3794                    let mut depth = 1usize;
3795                    index += 1;
3796                    while index < chars.len() && depth > 0 {
3797                        match chars[index] {
3798                            '(' => depth += 1,
3799                            ')' => depth = depth.saturating_sub(1),
3800                            '\'' | '"' => {
3801                                let quote = chars[index];
3802                                index += 1;
3803                                while index < chars.len() {
3804                                    if chars[index] == '\\' {
3805                                        index += 2;
3806                                        continue;
3807                                    }
3808                                    if chars[index] == quote {
3809                                        break;
3810                                    }
3811                                    index += 1;
3812                                }
3813                            }
3814                            _ => {}
3815                        }
3816                        index += 1;
3817                    }
3818                }
3819            }
3820            '>' | '+' | '~' => {
3821                segments.push(SelectorSegment::Combinator(chars[index].to_string()));
3822                index += 1;
3823            }
3824            _ => {
3825                let start = index;
3826                index += 1;
3827                while index < chars.len()
3828                    && !chars[index].is_whitespace()
3829                    && !matches!(chars[index], '.' | '&' | ':' | '>' | '+' | '~' | ',')
3830                {
3831                    index += 1;
3832                }
3833                segments.push(SelectorSegment::Other(chars[start..index].iter().collect()));
3834            }
3835        }
3836    }
3837
3838    segments
3839}
3840
3841fn is_selector_ident_continue(ch: char) -> bool {
3842    ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-') || !ch.is_ascii()
3843}
3844
3845fn starts_selector_component_after_ampersand(ch: char) -> bool {
3846    matches!(
3847        ch,
3848        ':' | '.' | '[' | '#' | '>' | '+' | '~' | ',' | '*' | ')' | '}' | ']'
3849    ) || ch.is_whitespace()
3850}
3851
3852fn tokenize(language: StyleLanguage, source: &str) -> (Vec<Token>, Vec<ParseDiagnostic>) {
3853    let mut tokens = Vec::new();
3854    let mut diagnostics = Vec::new();
3855    let bytes = source.as_bytes();
3856    let mut i = 0;
3857
3858    while i < bytes.len() {
3859        let start = i;
3860        let byte = bytes[i];
3861
3862        if byte.is_ascii_whitespace() {
3863            i += 1;
3864            while i < bytes.len() && bytes[i].is_ascii_whitespace() {
3865                i += 1;
3866            }
3867            tokens.push(Token {
3868                kind: TokenKind::Whitespace,
3869                span: TextSpan::new(start, i),
3870            });
3871            continue;
3872        }
3873
3874        if language.supports_line_comments() && byte == b'/' && bytes.get(i + 1) == Some(&b'/') {
3875            i += 2;
3876            while i < bytes.len() && bytes[i] != b'\n' {
3877                i += 1;
3878            }
3879            tokens.push(Token {
3880                kind: TokenKind::LineComment,
3881                span: TextSpan::new(start, i),
3882            });
3883            continue;
3884        }
3885
3886        if byte == b'/' && bytes.get(i + 1) == Some(&b'*') {
3887            i += 2;
3888            let mut closed = false;
3889            while i + 1 < bytes.len() {
3890                if bytes[i] == b'*' && bytes[i + 1] == b'/' {
3891                    i += 2;
3892                    closed = true;
3893                    break;
3894                }
3895                i += 1;
3896            }
3897            if !closed {
3898                i = bytes.len();
3899                diagnostics.push(ParseDiagnostic {
3900                    message: "unterminated block comment".to_string(),
3901                    span: TextSpan::new(start, i),
3902                });
3903            }
3904            tokens.push(Token {
3905                kind: TokenKind::BlockComment,
3906                span: TextSpan::new(start, i),
3907            });
3908            continue;
3909        }
3910
3911        if byte == b'"' || byte == b'\'' {
3912            let quote = byte;
3913            i += 1;
3914            let mut closed = false;
3915            while i < bytes.len() {
3916                if bytes[i] == b'\\' {
3917                    i = (i + 2).min(bytes.len());
3918                    continue;
3919                }
3920                if bytes[i] == quote {
3921                    i += 1;
3922                    closed = true;
3923                    break;
3924                }
3925                i += 1;
3926            }
3927            if !closed {
3928                diagnostics.push(ParseDiagnostic {
3929                    message: "unterminated string literal".to_string(),
3930                    span: TextSpan::new(start, i),
3931                });
3932            }
3933            tokens.push(Token {
3934                kind: TokenKind::String,
3935                span: TextSpan::new(start, i),
3936            });
3937            continue;
3938        }
3939
3940        if byte == b'#' && bytes.get(i + 1) == Some(&b'{') {
3941            i += 2;
3942            tokens.push(Token {
3943                kind: TokenKind::InterpolationStart,
3944                span: TextSpan::new(start, i),
3945            });
3946            continue;
3947        }
3948
3949        if is_ident_start(byte) {
3950            i += 1;
3951            while i < bytes.len() && is_ident_continue(bytes[i]) {
3952                i += 1;
3953            }
3954            tokens.push(Token {
3955                kind: TokenKind::Ident,
3956                span: TextSpan::new(start, i),
3957            });
3958            continue;
3959        }
3960
3961        if byte.is_ascii_digit() {
3962            i += 1;
3963            while i < bytes.len() && bytes[i].is_ascii_digit() {
3964                i += 1;
3965            }
3966            tokens.push(Token {
3967                kind: TokenKind::Number,
3968                span: TextSpan::new(start, i),
3969            });
3970            continue;
3971        }
3972
3973        let kind = match byte {
3974            b'.' => TokenKind::Dot,
3975            b'&' => TokenKind::Ampersand,
3976            b'#' => TokenKind::Hash,
3977            b':' => TokenKind::Colon,
3978            b';' => TokenKind::Semicolon,
3979            b',' => TokenKind::Comma,
3980            b'@' => TokenKind::At,
3981            b'{' => TokenKind::OpenBrace,
3982            b'}' => TokenKind::CloseBrace,
3983            b'(' => TokenKind::OpenParen,
3984            b')' => TokenKind::CloseParen,
3985            b'[' => TokenKind::OpenBracket,
3986            b']' => TokenKind::CloseBracket,
3987            _ => TokenKind::Other,
3988        };
3989        i += 1;
3990        tokens.push(Token {
3991            kind,
3992            span: TextSpan::new(start, i),
3993        });
3994    }
3995
3996    (tokens, diagnostics)
3997}
3998
3999fn is_ident_start(byte: u8) -> bool {
4000    byte.is_ascii_alphabetic() || matches!(byte, b'_' | b'-') || byte >= 0x80
4001}
4002
4003fn is_ident_continue(byte: u8) -> bool {
4004    is_ident_start(byte) || byte.is_ascii_digit()
4005}
4006
4007struct Parser<'a> {
4008    source: &'a str,
4009    tokens: &'a [Token],
4010    diagnostics: &'a mut Vec<ParseDiagnostic>,
4011    cursor: usize,
4012}
4013
4014impl<'a> Parser<'a> {
4015    fn new(
4016        source: &'a str,
4017        tokens: &'a [Token],
4018        diagnostics: &'a mut Vec<ParseDiagnostic>,
4019    ) -> Self {
4020        Self {
4021            source,
4022            tokens,
4023            diagnostics,
4024            cursor: 0,
4025        }
4026    }
4027
4028    fn parse_root(&mut self) -> Vec<SyntaxNode> {
4029        self.parse_block(false)
4030    }
4031
4032    fn parse_block(&mut self, stop_at_close_brace: bool) -> Vec<SyntaxNode> {
4033        let mut nodes = Vec::new();
4034
4035        while self.cursor < self.tokens.len() {
4036            let token = &self.tokens[self.cursor];
4037
4038            match token.kind {
4039                TokenKind::Whitespace => {
4040                    self.cursor += 1;
4041                }
4042                TokenKind::LineComment | TokenKind::BlockComment => {
4043                    nodes.push(SyntaxNode {
4044                        kind: SyntaxNodeKind::Comment,
4045                        span: token.span,
4046                        header_span: None,
4047                        payload: Some(SyntaxNodePayload::Comment(CommentPayload {
4048                            text: self.slice(token.span).to_string(),
4049                        })),
4050                        children: Vec::new(),
4051                    });
4052                    self.cursor += 1;
4053                }
4054                TokenKind::CloseBrace if stop_at_close_brace => {
4055                    self.cursor += 1;
4056                    return nodes;
4057                }
4058                TokenKind::CloseBrace => {
4059                    self.diagnostics.push(ParseDiagnostic {
4060                        message: "unexpected closing brace".to_string(),
4061                        span: token.span,
4062                    });
4063                    self.cursor += 1;
4064                }
4065                _ => nodes.push(self.parse_statement()),
4066            }
4067        }
4068
4069        if stop_at_close_brace {
4070            let end = self.tokens.last().map_or(0, |token| token.span.end);
4071            self.diagnostics.push(ParseDiagnostic {
4072                message: "unterminated block".to_string(),
4073                span: TextSpan::new(end, end),
4074            });
4075        }
4076
4077        nodes
4078    }
4079
4080    fn parse_statement(&mut self) -> SyntaxNode {
4081        let start_index = self.cursor;
4082        let mut index = self.cursor;
4083        let mut saw_at = self.tokens[index].kind == TokenKind::At;
4084        let mut saw_colon = false;
4085        let mut first_colon_index = None;
4086        let mut paren_depth = 0usize;
4087        let mut bracket_depth = 0usize;
4088
4089        while index < self.tokens.len() {
4090            let token = &self.tokens[index];
4091            match token.kind {
4092                TokenKind::OpenParen => paren_depth += 1,
4093                TokenKind::CloseParen => paren_depth = paren_depth.saturating_sub(1),
4094                TokenKind::OpenBracket => bracket_depth += 1,
4095                TokenKind::CloseBracket => bracket_depth = bracket_depth.saturating_sub(1),
4096                TokenKind::Colon if paren_depth == 0 && bracket_depth == 0 => {
4097                    saw_colon = true;
4098                    if first_colon_index.is_none() {
4099                        first_colon_index = Some(index);
4100                    }
4101                }
4102                TokenKind::At if index == start_index => saw_at = true,
4103                TokenKind::Semicolon if paren_depth == 0 && bracket_depth == 0 => {
4104                    let span = TextSpan::new(
4105                        self.tokens[start_index].span.start,
4106                        self.tokens[index].span.end,
4107                    );
4108                    self.cursor = index + 1;
4109                    return SyntaxNode {
4110                        kind: classify_statement_kind(saw_at, saw_colon),
4111                        span,
4112                        header_span: Some(TextSpan::new(
4113                            self.tokens[start_index].span.start,
4114                            self.tokens[index].span.start,
4115                        )),
4116                        payload: self.build_inline_payload(
4117                            start_index,
4118                            index,
4119                            saw_at,
4120                            first_colon_index,
4121                        ),
4122                        children: Vec::new(),
4123                    };
4124                }
4125                TokenKind::OpenBrace if paren_depth == 0 && bracket_depth == 0 => {
4126                    let header_span = TextSpan::new(
4127                        self.tokens[start_index].span.start,
4128                        self.tokens[index].span.start,
4129                    );
4130                    self.cursor = index + 1;
4131                    let children = self.parse_block(true);
4132                    let end = self
4133                        .tokens
4134                        .get(self.cursor.saturating_sub(1))
4135                        .map_or(self.tokens[index].span.end, |token| token.span.end);
4136                    return SyntaxNode {
4137                        kind: if saw_at {
4138                            SyntaxNodeKind::AtRule
4139                        } else {
4140                            SyntaxNodeKind::Rule
4141                        },
4142                        span: TextSpan::new(self.tokens[start_index].span.start, end),
4143                        header_span: Some(header_span),
4144                        payload: Some(if saw_at {
4145                            SyntaxNodePayload::AtRule(
4146                                self.build_at_rule_payload(start_index, index),
4147                            )
4148                        } else {
4149                            let prelude = self.slice_trimmed(header_span).to_string();
4150                            SyntaxNodePayload::Rule(RulePayload {
4151                                selector_groups: parse_selector_groups(&prelude),
4152                                prelude,
4153                            })
4154                        }),
4155                        children,
4156                    };
4157                }
4158                TokenKind::CloseBrace => break,
4159                _ => {}
4160            }
4161            index += 1;
4162        }
4163
4164        let end = self
4165            .tokens
4166            .get(index.saturating_sub(1))
4167            .map_or(self.tokens[start_index].span.end, |token| token.span.end);
4168        self.cursor = index.max(start_index + 1);
4169        let span = TextSpan::new(self.tokens[start_index].span.start, end);
4170        SyntaxNode {
4171            kind: classify_statement_kind(saw_at, saw_colon),
4172            span,
4173            header_span: Some(span),
4174            payload: self.build_inline_payload(start_index, index, saw_at, first_colon_index),
4175            children: Vec::new(),
4176        }
4177    }
4178
4179    fn build_inline_payload(
4180        &self,
4181        start_index: usize,
4182        end_index: usize,
4183        saw_at: bool,
4184        first_colon_index: Option<usize>,
4185    ) -> Option<SyntaxNodePayload> {
4186        if saw_at {
4187            return Some(SyntaxNodePayload::AtRule(
4188                self.build_at_rule_payload(start_index, end_index),
4189            ));
4190        }
4191
4192        let colon_index = first_colon_index?;
4193        let property_span = TextSpan::new(
4194            self.tokens[start_index].span.start,
4195            self.tokens[colon_index].span.start,
4196        );
4197        let value_start = self.tokens[colon_index].span.end;
4198        let value_end = self
4199            .tokens
4200            .get(end_index.saturating_sub(1))
4201            .map_or(value_start, |token| token.span.end);
4202        Some(SyntaxNodePayload::Declaration(DeclarationPayload {
4203            property: self.slice_trimmed(property_span).to_string(),
4204            value: self
4205                .slice_trimmed(TextSpan::new(value_start, value_end))
4206                .to_string(),
4207        }))
4208    }
4209
4210    fn build_at_rule_payload(&self, start_index: usize, end_index: usize) -> AtRulePayload {
4211        let name = self
4212            .tokens
4213            .get(start_index + 1)
4214            .map(|token| self.slice(token.span))
4215            .unwrap_or_default()
4216            .trim()
4217            .to_string();
4218        let params_start = self
4219            .tokens
4220            .get(start_index + 2)
4221            .map_or(self.tokens[start_index].span.end, |token| token.span.start);
4222        let params_end = self
4223            .tokens
4224            .get(end_index.saturating_sub(1))
4225            .map_or(params_start, |token| token.span.end);
4226        AtRulePayload {
4227            kind: classify_at_rule_kind(&name),
4228            name,
4229            params: self
4230                .slice_trimmed(TextSpan::new(params_start, params_end))
4231                .to_string(),
4232        }
4233    }
4234
4235    fn slice(&self, span: TextSpan) -> &'a str {
4236        &self.source[span.start..span.end]
4237    }
4238
4239    fn slice_trimmed(&self, span: TextSpan) -> &'a str {
4240        self.slice(span).trim()
4241    }
4242}
4243
4244fn classify_statement_kind(saw_at: bool, saw_colon: bool) -> SyntaxNodeKind {
4245    if saw_at {
4246        SyntaxNodeKind::AtRule
4247    } else if saw_colon {
4248        SyntaxNodeKind::Declaration
4249    } else {
4250        SyntaxNodeKind::Unknown
4251    }
4252}
4253
4254fn classify_at_rule_kind(name: &str) -> AtRuleKind {
4255    match name {
4256        "media" => AtRuleKind::Media,
4257        "supports" => AtRuleKind::Supports,
4258        "layer" => AtRuleKind::Layer,
4259        "keyframes" | "-webkit-keyframes" => AtRuleKind::Keyframes,
4260        "value" => AtRuleKind::Value,
4261        "at-root" => AtRuleKind::AtRoot,
4262        "mixin" => AtRuleKind::Mixin,
4263        "include" => AtRuleKind::Include,
4264        "function" => AtRuleKind::Function,
4265        "use" => AtRuleKind::Use,
4266        "forward" => AtRuleKind::Forward,
4267        "import" => AtRuleKind::Import,
4268        _ => AtRuleKind::Generic,
4269    }
4270}
4271
4272#[cfg(test)]
4273mod tests {
4274    use super::{
4275        AtRuleKind, AtRulePayload, DeclarationPayload, ParserByteSpanV0,
4276        ParserIndexSassModuleUseFactV0, ParserIndexSassSelectorSymbolFactV0, ParserPositionV0,
4277        ParserRangeV0, RulePayload, SelectorGroup, SelectorSegment, StyleLanguage, SyntaxNodeKind,
4278        SyntaxNodePayload, TextSpan, TokenKind, parse_stylesheet,
4279    };
4280
4281    fn token_texts<'a>(source: &'a str, sheet: &super::Stylesheet) -> Vec<(TokenKind, &'a str)> {
4282        sheet
4283            .tokens
4284            .iter()
4285            .map(|token| (token.kind, &source[token.span.start..token.span.end]))
4286            .collect()
4287    }
4288
4289    fn span_after(source: &str, anchor: &str, needle: &str) -> Result<ParserByteSpanV0, String> {
4290        let anchor_start = source
4291            .find(anchor)
4292            .ok_or_else(|| format!("missing anchor {anchor:?}"))?;
4293        let relative_start = source[anchor_start..]
4294            .find(needle)
4295            .ok_or_else(|| format!("missing needle {needle:?} after {anchor:?}"))?;
4296        let start = anchor_start + relative_start;
4297        Ok(ParserByteSpanV0 {
4298            start,
4299            end: start + needle.len(),
4300        })
4301    }
4302
4303    fn range_after(source: &str, anchor: &str, needle: &str) -> Result<ParserRangeV0, String> {
4304        span_after(source, anchor, needle)
4305            .map(|span| super::source_range_for_byte_span(source, span))
4306    }
4307
4308    #[test]
4309    fn detects_style_language_from_module_path() {
4310        assert_eq!(
4311            StyleLanguage::from_module_path("/x/Button.module.css"),
4312            Some(StyleLanguage::Css)
4313        );
4314        assert_eq!(
4315            StyleLanguage::from_module_path("/x/Button.module.scss"),
4316            Some(StyleLanguage::Scss)
4317        );
4318        assert_eq!(
4319            StyleLanguage::from_module_path("/x/Button.module.less"),
4320            Some(StyleLanguage::Less)
4321        );
4322        assert_eq!(StyleLanguage::from_module_path("/x/Button.css"), None);
4323    }
4324
4325    #[test]
4326    fn tokenizes_basic_css_rule() {
4327        let source = ".button { color: red; }";
4328        let sheet = parse_stylesheet(StyleLanguage::Css, source);
4329        let tokens = token_texts(source, &sheet);
4330        assert!(tokens.contains(&(TokenKind::Dot, ".")));
4331        assert!(tokens.contains(&(TokenKind::Ident, "button")));
4332        assert!(tokens.contains(&(TokenKind::OpenBrace, "{")));
4333        assert!(tokens.contains(&(TokenKind::Semicolon, ";")));
4334        assert!(sheet.diagnostics.is_empty());
4335    }
4336
4337    #[test]
4338    fn keeps_css_double_slash_as_regular_tokens() {
4339        let source = ".button { // not-a-comment\n color: red; }";
4340        let sheet = parse_stylesheet(StyleLanguage::Css, source);
4341        assert!(
4342            !sheet
4343                .tokens
4344                .iter()
4345                .any(|token| matches!(token.kind, TokenKind::LineComment))
4346        );
4347    }
4348
4349    #[test]
4350    fn parses_scss_nested_rules_and_comments() {
4351        let source = ".button {\n  // note\n  &--primary { color: red; }\n}\n";
4352        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
4353        assert_eq!(sheet.nodes.len(), 1);
4354        let root_rule = &sheet.nodes[0];
4355        assert_eq!(root_rule.kind, SyntaxNodeKind::Rule);
4356        assert_eq!(
4357            root_rule.payload,
4358            Some(SyntaxNodePayload::Rule(RulePayload {
4359                prelude: ".button".to_string(),
4360                selector_groups: vec![SelectorGroup {
4361                    raw: ".button".to_string(),
4362                    segments: vec![SelectorSegment::ClassName("button".to_string())],
4363                }],
4364            }))
4365        );
4366        assert_eq!(root_rule.children.len(), 2);
4367        assert_eq!(root_rule.children[0].kind, SyntaxNodeKind::Comment);
4368        assert_eq!(
4369            root_rule.children[0].payload,
4370            Some(SyntaxNodePayload::Comment(super::CommentPayload {
4371                text: "// note".to_string(),
4372            }))
4373        );
4374        assert_eq!(root_rule.children[1].kind, SyntaxNodeKind::Rule);
4375        assert_eq!(
4376            root_rule.children[1].payload,
4377            Some(SyntaxNodePayload::Rule(RulePayload {
4378                prelude: "&--primary".to_string(),
4379                selector_groups: vec![SelectorGroup {
4380                    raw: "&--primary".to_string(),
4381                    segments: vec![
4382                        SelectorSegment::Ampersand,
4383                        SelectorSegment::BemSuffix("--primary".to_string()),
4384                    ],
4385                }],
4386            }))
4387        );
4388        assert_eq!(
4389            root_rule.children[1].children[0].payload,
4390            Some(SyntaxNodePayload::Declaration(DeclarationPayload {
4391                property: "color".to_string(),
4392                value: "red".to_string(),
4393            }))
4394        );
4395        assert!(sheet.diagnostics.is_empty());
4396    }
4397
4398    #[test]
4399    fn parses_less_at_rule_like_variable_assignment() {
4400        let source = "@color: red;\n.button { color: @color; }";
4401        let sheet = parse_stylesheet(StyleLanguage::Less, source);
4402        assert_eq!(sheet.nodes[0].kind, SyntaxNodeKind::AtRule);
4403        assert_eq!(
4404            sheet.nodes[0].payload,
4405            Some(SyntaxNodePayload::AtRule(AtRulePayload {
4406                kind: AtRuleKind::Generic,
4407                name: "color".to_string(),
4408                params: ": red".to_string(),
4409            }))
4410        );
4411        assert_eq!(sheet.nodes[1].kind, SyntaxNodeKind::Rule);
4412    }
4413
4414    #[test]
4415    fn parses_at_rule_header_and_params() {
4416        let source = "@media screen and (min-width: 10px) { .button { color: red; } }";
4417        let sheet = parse_stylesheet(StyleLanguage::Css, source);
4418        assert_eq!(
4419            sheet.nodes[0].payload,
4420            Some(SyntaxNodePayload::AtRule(AtRulePayload {
4421                kind: AtRuleKind::Media,
4422                name: "media".to_string(),
4423                params: "screen and (min-width: 10px)".to_string(),
4424            }))
4425        );
4426    }
4427
4428    #[test]
4429    fn classifies_keyframes_and_value_at_rules() {
4430        let source = "@value brand: red;\n@keyframes fade { from { opacity: 0; } }\n";
4431        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
4432        assert_eq!(
4433            sheet.nodes[0].payload,
4434            Some(SyntaxNodePayload::AtRule(AtRulePayload {
4435                kind: AtRuleKind::Value,
4436                name: "value".to_string(),
4437                params: "brand: red".to_string(),
4438            }))
4439        );
4440        assert_eq!(
4441            sheet.nodes[1].payload,
4442            Some(SyntaxNodePayload::AtRule(AtRulePayload {
4443                kind: AtRuleKind::Keyframes,
4444                name: "keyframes".to_string(),
4445                params: "fade".to_string(),
4446            }))
4447        );
4448    }
4449
4450    #[test]
4451    fn classifies_sass_symbol_at_rules() {
4452        let source = r#"@use "./tokens" as tokens;
4453@forward "./theme";
4454@import "./legacy";
4455@mixin raised($depth) { color: red; }
4456@include raised($gap);
4457@function tone($value) { @return $value; }
4458"#;
4459        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
4460        let kinds: Vec<AtRuleKind> = sheet
4461            .nodes
4462            .iter()
4463            .filter_map(|node| match &node.payload {
4464                Some(SyntaxNodePayload::AtRule(at_rule)) => Some(at_rule.kind),
4465                _ => None,
4466            })
4467            .collect();
4468        assert_eq!(
4469            kinds,
4470            vec![
4471                AtRuleKind::Use,
4472                AtRuleKind::Forward,
4473                AtRuleKind::Import,
4474                AtRuleKind::Mixin,
4475                AtRuleKind::Include,
4476                AtRuleKind::Function,
4477            ]
4478        );
4479    }
4480
4481    #[test]
4482    fn records_unterminated_block_comment_diagnostic() {
4483        let source = "/* open";
4484        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
4485        assert_eq!(sheet.diagnostics.len(), 1);
4486        assert_eq!(sheet.diagnostics[0].message, "unterminated block comment");
4487        assert_eq!(sheet.diagnostics[0].span, TextSpan::new(0, source.len()));
4488    }
4489
4490    #[test]
4491    fn records_unterminated_block_diagnostic() {
4492        let source = ".button { color: red;";
4493        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
4494        assert_eq!(sheet.nodes.len(), 1);
4495        assert_eq!(sheet.nodes[0].kind, SyntaxNodeKind::Rule);
4496        assert!(
4497            sheet
4498                .diagnostics
4499                .iter()
4500                .any(|diagnostic| diagnostic.message == "unterminated block")
4501        );
4502    }
4503
4504    #[test]
4505    fn splits_grouped_selectors_into_groups_and_segments() {
4506        let source = ".a, .b { &--c { color: red; } }";
4507        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
4508        assert_eq!(
4509            sheet.nodes[0].payload,
4510            Some(SyntaxNodePayload::Rule(RulePayload {
4511                prelude: ".a, .b".to_string(),
4512                selector_groups: vec![
4513                    SelectorGroup {
4514                        raw: ".a".to_string(),
4515                        segments: vec![SelectorSegment::ClassName("a".to_string())],
4516                    },
4517                    SelectorGroup {
4518                        raw: ".b".to_string(),
4519                        segments: vec![SelectorSegment::ClassName("b".to_string())],
4520                    },
4521                ],
4522            }))
4523        );
4524    }
4525
4526    #[test]
4527    fn parity_summary_reconstructs_bem_suffix_names() {
4528        let source = ".card { &__icon { &--small { color: red; } } }";
4529        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
4530        let summary = super::summarize_parity_lite(&sheet);
4531        assert_eq!(
4532            summary.selector_names,
4533            vec!["card", "card__icon", "card__icon--small"]
4534        );
4535    }
4536
4537    #[test]
4538    fn parity_summary_expands_grouped_parent_bem_suffixes() {
4539        let source = ".a, .b { &--c { color: red; } }";
4540        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
4541        let summary = super::summarize_parity_lite(&sheet);
4542        assert_eq!(summary.selector_names, vec!["a", "a--c", "b", "b--c"]);
4543    }
4544
4545    #[test]
4546    fn parity_summary_expands_grouped_parent_nested_bem_suffixes() {
4547        let source = ".a, .b { &__icon { &--small { color: red; } } }";
4548        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
4549        let summary = super::summarize_parity_lite(&sheet);
4550        assert_eq!(
4551            summary.selector_names,
4552            vec![
4553                "a",
4554                "a__icon",
4555                "a__icon--small",
4556                "b",
4557                "b__icon",
4558                "b__icon--small"
4559            ]
4560        );
4561    }
4562
4563    #[test]
4564    fn parity_summary_keeps_ampersand_class_as_standalone_class() {
4565        let source = ".a, .b { &.active { color: red; } }";
4566        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
4567        let summary = super::summarize_parity_lite(&sheet);
4568        assert_eq!(summary.selector_names, vec!["a", "active", "active", "b"]);
4569    }
4570
4571    #[test]
4572    fn index_summary_matches_nested_bem_safety_matrix() {
4573        let source = ".btn { &--primary {} &__icon {} &.active {} &-legacy {} &_legacy {} & + &--paired {} }";
4574        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
4575        let summary = super::summarize_css_modules_intermediate(&sheet);
4576        assert_eq!(
4577            summary.selectors.names,
4578            vec![
4579                "active",
4580                "btn",
4581                "btn--paired",
4582                "btn--primary",
4583                "btn-legacy",
4584                "btn__icon",
4585                "btn_legacy"
4586            ]
4587        );
4588        assert_eq!(
4589            summary.selectors.bem_suffix_safe_names,
4590            vec!["btn--primary", "btn__icon"]
4591        );
4592        assert_eq!(
4593            summary.selectors.nested_unsafe_names,
4594            vec!["active", "btn--paired", "btn-legacy", "btn_legacy"]
4595        );
4596    }
4597
4598    #[test]
4599    fn index_summary_does_not_bem_expand_non_bare_parent_suffixes() {
4600        let source = ".card:hover { &--primary {} }";
4601        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
4602        let summary = super::summarize_css_modules_intermediate(&sheet);
4603        assert_eq!(summary.selectors.names, vec!["card"]);
4604        assert_eq!(summary.selectors.bem_suffix_count, 0);
4605    }
4606
4607    #[test]
4608    fn index_summary_marks_plain_nested_selectors_unsafe() {
4609        let source = ".wrapper { .inner { &--mod {} } }";
4610        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
4611        let summary = super::summarize_css_modules_intermediate(&sheet);
4612        assert_eq!(summary.selectors.names, vec!["inner", "wrapper"]);
4613        assert_eq!(summary.selectors.nested_unsafe_names, vec!["inner"]);
4614        assert_eq!(summary.selectors.nested_safety_counts.flat, 1);
4615        assert_eq!(summary.selectors.nested_safety_counts.nested_unsafe, 1);
4616    }
4617
4618    #[test]
4619    fn index_summary_allows_unsafe_suffix_to_become_deep_bem_base() {
4620        let source = ".btn { &-legacy { &--x {} } }";
4621        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
4622        let summary = super::summarize_css_modules_intermediate(&sheet);
4623        assert_eq!(
4624            summary.selectors.names,
4625            vec!["btn", "btn-legacy", "btn-legacy--x"]
4626        );
4627        assert_eq!(
4628            summary.selectors.bem_suffix_safe_names,
4629            vec!["btn-legacy--x"]
4630        );
4631        assert_eq!(summary.selectors.nested_unsafe_names, vec!["btn-legacy"]);
4632    }
4633
4634    #[test]
4635    fn parity_summary_keeps_class_from_pseudo_selector() {
4636        let source = ".btn:hover { color: red; }";
4637        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
4638        let summary = super::summarize_parity_lite(&sheet);
4639        assert_eq!(summary.selector_names, vec!["btn"]);
4640    }
4641
4642    #[test]
4643    fn parity_summary_keeps_multiple_classes_from_compound_selector() {
4644        let source = ".btn.active { color: red; }";
4645        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
4646        let summary = super::summarize_parity_lite(&sheet);
4647        assert_eq!(summary.selector_names, vec!["active", "btn"]);
4648    }
4649
4650    #[test]
4651    fn parity_summary_prefers_rightmost_class_after_combinator() {
4652        let source = ".a > .b { color: red; }";
4653        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
4654        let summary = super::summarize_parity_lite(&sheet);
4655        assert_eq!(summary.selector_names, vec!["b"]);
4656    }
4657
4658    #[test]
4659    fn parity_summary_collects_nested_layer_rule_selectors() {
4660        let source = "@layer ui { .btn:hover { color: red; } }";
4661        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
4662        let summary = super::summarize_parity_lite(&sheet);
4663        assert_eq!(summary.selector_names, vec!["btn"]);
4664    }
4665
4666    #[test]
4667    fn parity_summary_prefers_rightmost_class_after_descendant_combinator() {
4668        let source = ".a .b { color: red; }";
4669        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
4670        let summary = super::summarize_parity_lite(&sheet);
4671        assert_eq!(summary.selector_names, vec!["b"]);
4672    }
4673
4674    #[test]
4675    fn parity_summary_ignores_classes_inside_pseudo_functions() {
4676        let source = ".btn:is(.active, .primary) { color: red; }";
4677        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
4678        let summary = super::summarize_parity_lite(&sheet);
4679        assert_eq!(summary.selector_names, vec!["btn"]);
4680    }
4681
4682    #[test]
4683    fn parity_summary_ignores_global_function_classes() {
4684        let source = ":global(.foo) { color: red; }";
4685        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
4686        let summary = super::summarize_parity_lite(&sheet);
4687        assert!(summary.selector_names.is_empty());
4688    }
4689
4690    #[test]
4691    fn parity_summary_ignores_not_function_classes() {
4692        let source = ".btn:not(.disabled) { color: red; }";
4693        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
4694        let summary = super::summarize_parity_lite(&sheet);
4695        assert_eq!(summary.selector_names, vec!["btn"]);
4696    }
4697
4698    #[test]
4699    fn parity_summary_keeps_local_function_class() {
4700        let source = ":local(.foo) { color: red; }";
4701        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
4702        let summary = super::summarize_parity_lite(&sheet);
4703        assert_eq!(summary.selector_names, vec!["foo"]);
4704    }
4705
4706    #[test]
4707    fn index_summary_collects_sass_symbol_seed_facts() -> Result<(), String> {
4708        let source = r#"@use "./plain";
4709@use "./reset" as *;
4710@use "./tokens" as tokens;
4711@use "sass:color";
4712@forward "./theme";
4713@import "./legacy";
4714$gap: 1rem;
4715@mixin raised($depth) { box-shadow: 0 0 $depth black; }
4716@function tone($value) { @return $value; }
4717.btn { color: $gap; @include raised($gap); border-color: tone($gap); }
4718.ghost { color: $missing; @include absent($gap); }
4719"#;
4720        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
4721        let summary = super::summarize_css_modules_intermediate(&sheet);
4722
4723        assert_eq!(summary.sass.variable_decl_names, vec!["gap"]);
4724        assert_eq!(
4725            summary.sass.variable_parameter_names,
4726            vec!["depth", "value"]
4727        );
4728        assert_eq!(
4729            summary.sass.variable_ref_names,
4730            vec!["depth", "gap", "missing", "value"]
4731        );
4732        assert_eq!(
4733            summary.sass.selectors_with_variable_refs_names,
4734            vec!["btn", "ghost"]
4735        );
4736        assert_eq!(
4737            summary.sass.selectors_with_resolved_variable_refs_names,
4738            vec!["btn", "ghost"]
4739        );
4740        assert_eq!(
4741            summary.sass.selectors_with_unresolved_variable_refs_names,
4742            vec!["ghost"]
4743        );
4744        assert_eq!(summary.sass.mixin_decl_names, vec!["raised"]);
4745        assert_eq!(summary.sass.mixin_include_names, vec!["absent", "raised"]);
4746        assert_eq!(
4747            summary.sass.selectors_with_mixin_includes_names,
4748            vec!["btn", "ghost"]
4749        );
4750        assert_eq!(
4751            summary.sass.selectors_with_resolved_mixin_includes_names,
4752            vec!["btn"]
4753        );
4754        assert_eq!(
4755            summary.sass.selectors_with_unresolved_mixin_includes_names,
4756            vec!["ghost"]
4757        );
4758        assert_eq!(summary.sass.function_decl_names, vec!["tone"]);
4759        assert_eq!(summary.sass.function_call_names, vec!["tone"]);
4760        assert_eq!(
4761            summary.sass.selectors_with_function_calls_names,
4762            vec!["btn"]
4763        );
4764        assert_eq!(
4765            summary.sass.selector_symbol_facts,
4766            vec![
4767                ParserIndexSassSelectorSymbolFactV0 {
4768                    selector_name: "btn".to_string(),
4769                    symbol_kind: "function",
4770                    name: "tone".to_string(),
4771                    role: "call",
4772                    resolution: "resolved",
4773                    byte_span: span_after(source, "border-color:", "tone")?,
4774                    range: range_after(source, "border-color:", "tone")?,
4775                },
4776                ParserIndexSassSelectorSymbolFactV0 {
4777                    selector_name: "btn".to_string(),
4778                    symbol_kind: "mixin",
4779                    name: "raised".to_string(),
4780                    role: "include",
4781                    resolution: "resolved",
4782                    byte_span: span_after(source, "@include", "raised")?,
4783                    range: range_after(source, "@include", "raised")?,
4784                },
4785                ParserIndexSassSelectorSymbolFactV0 {
4786                    selector_name: "btn".to_string(),
4787                    symbol_kind: "variable",
4788                    name: "gap".to_string(),
4789                    role: "reference",
4790                    resolution: "resolved",
4791                    byte_span: span_after(source, ".btn { color", "$gap")?,
4792                    range: range_after(source, ".btn { color", "$gap")?,
4793                },
4794                ParserIndexSassSelectorSymbolFactV0 {
4795                    selector_name: "btn".to_string(),
4796                    symbol_kind: "variable",
4797                    name: "gap".to_string(),
4798                    role: "reference",
4799                    resolution: "resolved",
4800                    byte_span: span_after(source, "@include raised(", "$gap")?,
4801                    range: range_after(source, "@include raised(", "$gap")?,
4802                },
4803                ParserIndexSassSelectorSymbolFactV0 {
4804                    selector_name: "btn".to_string(),
4805                    symbol_kind: "variable",
4806                    name: "gap".to_string(),
4807                    role: "reference",
4808                    resolution: "resolved",
4809                    byte_span: span_after(source, "border-color: tone(", "$gap")?,
4810                    range: range_after(source, "border-color: tone(", "$gap")?,
4811                },
4812                ParserIndexSassSelectorSymbolFactV0 {
4813                    selector_name: "ghost".to_string(),
4814                    symbol_kind: "mixin",
4815                    name: "absent".to_string(),
4816                    role: "include",
4817                    resolution: "unresolved",
4818                    byte_span: span_after(source, ".ghost", "absent")?,
4819                    range: range_after(source, ".ghost", "absent")?,
4820                },
4821                ParserIndexSassSelectorSymbolFactV0 {
4822                    selector_name: "ghost".to_string(),
4823                    symbol_kind: "variable",
4824                    name: "gap".to_string(),
4825                    role: "reference",
4826                    resolution: "resolved",
4827                    byte_span: span_after(source, "absent(", "$gap")?,
4828                    range: range_after(source, "absent(", "$gap")?,
4829                },
4830                ParserIndexSassSelectorSymbolFactV0 {
4831                    selector_name: "ghost".to_string(),
4832                    symbol_kind: "variable",
4833                    name: "missing".to_string(),
4834                    role: "reference",
4835                    resolution: "unresolved",
4836                    byte_span: span_after(source, ".ghost { color", "$missing")?,
4837                    range: range_after(source, ".ghost { color", "$missing")?,
4838                },
4839            ]
4840        );
4841        assert_eq!(
4842            summary.sass.module_use_sources,
4843            vec!["./legacy", "./plain", "./reset", "./tokens", "sass:color"]
4844        );
4845        assert_eq!(
4846            summary.sass.module_use_edges,
4847            vec![
4848                ParserIndexSassModuleUseFactV0 {
4849                    source: "./legacy".to_string(),
4850                    namespace_kind: "wildcard",
4851                    namespace: None,
4852                },
4853                ParserIndexSassModuleUseFactV0 {
4854                    source: "./plain".to_string(),
4855                    namespace_kind: "default",
4856                    namespace: Some("plain".to_string()),
4857                },
4858                ParserIndexSassModuleUseFactV0 {
4859                    source: "./reset".to_string(),
4860                    namespace_kind: "wildcard",
4861                    namespace: None,
4862                },
4863                ParserIndexSassModuleUseFactV0 {
4864                    source: "./tokens".to_string(),
4865                    namespace_kind: "alias",
4866                    namespace: Some("tokens".to_string()),
4867                },
4868                ParserIndexSassModuleUseFactV0 {
4869                    source: "sass:color".to_string(),
4870                    namespace_kind: "default",
4871                    namespace: Some("color".to_string()),
4872                },
4873            ]
4874        );
4875        assert_eq!(summary.sass.module_forward_sources, vec!["./theme"]);
4876        assert_eq!(summary.sass.module_import_sources, vec!["./legacy"]);
4877        assert_eq!(
4878            summary
4879                .sass
4880                .same_file_resolution
4881                .resolved_variable_ref_names,
4882            vec!["depth", "gap", "value"]
4883        );
4884        assert_eq!(
4885            summary
4886                .sass
4887                .same_file_resolution
4888                .unresolved_variable_ref_names,
4889            vec!["missing"]
4890        );
4891        assert_eq!(
4892            summary
4893                .sass
4894                .same_file_resolution
4895                .resolved_mixin_include_names,
4896            vec!["raised"]
4897        );
4898        assert_eq!(
4899            summary
4900                .sass
4901                .same_file_resolution
4902                .unresolved_mixin_include_names,
4903            vec!["absent"]
4904        );
4905        assert_eq!(
4906            summary
4907                .sass
4908                .same_file_resolution
4909                .resolved_function_call_names,
4910            vec!["tone"]
4911        );
4912        Ok(())
4913    }
4914
4915    #[test]
4916    fn index_summary_collects_css_custom_property_seed_facts() {
4917        let source = r#":root { --color-gray-700: #767678; }
4918@media (min-width: 1px) { :root { --brand: white; } .btn { color: var(--color-gray-700); } }
4919@supports (display: grid) { @layer ui { [data-theme="dark"] { --surface: black; } .card { color: var(--missing); } } }
4920"#;
4921        let sheet = parse_stylesheet(StyleLanguage::Css, source);
4922        let summary = super::summarize_css_modules_intermediate(&sheet);
4923        let semantic_boundary = super::summarize_semantic_boundary(&sheet);
4924
4925        assert_eq!(
4926            summary.custom_properties.decl_names,
4927            vec!["--brand", "--color-gray-700", "--surface"]
4928        );
4929        assert_eq!(
4930            summary
4931                .custom_properties
4932                .decl_facts
4933                .iter()
4934                .map(|fact| (
4935                    fact.name.as_str(),
4936                    fact.source_order,
4937                    fact.selector_contexts.as_slice(),
4938                    fact.under_media,
4939                    fact.under_supports,
4940                    fact.under_layer,
4941                ))
4942                .collect::<Vec<_>>(),
4943            vec![
4944                (
4945                    "--brand",
4946                    1,
4947                    vec![":root".to_string()].as_slice(),
4948                    true,
4949                    false,
4950                    false,
4951                ),
4952                (
4953                    "--color-gray-700",
4954                    0,
4955                    vec![":root".to_string()].as_slice(),
4956                    false,
4957                    false,
4958                    false,
4959                ),
4960                (
4961                    "--surface",
4962                    2,
4963                    vec!["[data-theme=\"dark\"]".to_string()].as_slice(),
4964                    false,
4965                    true,
4966                    true,
4967                ),
4968            ]
4969        );
4970        assert_eq!(
4971            summary.custom_properties.decl_context_selectors,
4972            vec![":root", "[data-theme=\"dark\"]"]
4973        );
4974        assert_eq!(
4975            summary.custom_properties.decl_names_under_media,
4976            vec!["--brand"]
4977        );
4978        assert_eq!(
4979            summary.custom_properties.decl_names_under_supports,
4980            vec!["--surface"]
4981        );
4982        assert_eq!(
4983            summary.custom_properties.decl_names_under_layer,
4984            vec!["--surface"]
4985        );
4986        assert_eq!(
4987            summary.custom_properties.ref_names,
4988            vec!["--color-gray-700", "--missing"]
4989        );
4990        assert_eq!(
4991            summary
4992                .custom_properties
4993                .ref_facts
4994                .iter()
4995                .map(|fact| (
4996                    fact.name.as_str(),
4997                    fact.source_order,
4998                    fact.selector_contexts.as_slice(),
4999                    fact.under_media,
5000                    fact.under_supports,
5001                    fact.under_layer,
5002                ))
5003                .collect::<Vec<_>>(),
5004            vec![
5005                (
5006                    "--color-gray-700",
5007                    0,
5008                    vec![".btn".to_string()].as_slice(),
5009                    true,
5010                    false,
5011                    false,
5012                ),
5013                (
5014                    "--missing",
5015                    1,
5016                    vec![".card".to_string()].as_slice(),
5017                    false,
5018                    true,
5019                    true,
5020                ),
5021            ]
5022        );
5023        assert_eq!(
5024            summary.custom_properties.selectors_with_refs_names,
5025            vec!["btn", "card"]
5026        );
5027        assert_eq!(
5028            summary
5029                .custom_properties
5030                .selectors_with_refs_under_media_names,
5031            vec!["btn"]
5032        );
5033        assert_eq!(
5034            summary
5035                .custom_properties
5036                .selectors_with_refs_under_supports_names,
5037            vec!["card"]
5038        );
5039        assert_eq!(
5040            summary
5041                .custom_properties
5042                .selectors_with_refs_under_layer_names,
5043            vec!["card"]
5044        );
5045        assert_eq!(
5046            semantic_boundary
5047                .semantic_facts
5048                .custom_properties
5049                .resolved_ref_names,
5050            vec!["--color-gray-700"]
5051        );
5052        assert_eq!(
5053            semantic_boundary
5054                .semantic_facts
5055                .custom_properties
5056                .unresolved_ref_names,
5057            vec!["--missing"]
5058        );
5059    }
5060
5061    #[test]
5062    fn semantic_boundary_respects_custom_property_selector_context() {
5063        let source = r#".theme { --brand: #222; }
5064.button { color: var(--brand); }
5065.theme .button { border-color: var(--brand); }
5066:root { --surface: white; }
5067.card { background: var(--surface); }
5068"#;
5069        let sheet = parse_stylesheet(StyleLanguage::Css, source);
5070        let semantic_boundary = super::summarize_semantic_boundary(&sheet);
5071
5072        assert_eq!(
5073            semantic_boundary
5074                .semantic_facts
5075                .custom_properties
5076                .resolved_ref_names,
5077            vec!["--brand", "--surface"]
5078        );
5079        assert_eq!(
5080            semantic_boundary
5081                .semantic_facts
5082                .custom_properties
5083                .unresolved_ref_names,
5084            vec!["--brand"]
5085        );
5086    }
5087
5088    #[test]
5089    fn semantic_boundary_separates_parser_syntax_from_semantic_resolution() -> Result<(), String> {
5090        let source = r#"@use "./tokens" as tokens;
5091$gap: 1rem;
5092@mixin raised($depth) { box-shadow: 0 0 $depth black; }
5093.btn { color: $gap; @include raised($gap); }
5094.ghost { color: $missing; }
5095"#;
5096        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
5097        let summary = super::summarize_semantic_boundary(&sheet);
5098
5099        assert_eq!(
5100            summary.parser_facts.sass.module_use_edges,
5101            vec![ParserIndexSassModuleUseFactV0 {
5102                source: "./tokens".to_string(),
5103                namespace_kind: "alias",
5104                namespace: Some("tokens".to_string()),
5105            }]
5106        );
5107        assert_eq!(
5108            summary.parser_facts.sass.variable_ref_names,
5109            vec!["depth", "gap", "missing"]
5110        );
5111        assert_eq!(
5112            summary
5113                .semantic_facts
5114                .sass
5115                .same_file_resolution
5116                .resolved_variable_ref_names,
5117            vec!["depth", "gap"]
5118        );
5119        assert_eq!(
5120            summary
5121                .semantic_facts
5122                .sass
5123                .same_file_resolution
5124                .unresolved_variable_ref_names,
5125            vec!["missing"]
5126        );
5127        assert_eq!(
5128            summary.semantic_facts.selector_identity.canonical_names,
5129            vec!["btn", "ghost"]
5130        );
5131        assert_eq!(
5132            summary.semantic_facts.sass.selector_symbol_facts,
5133            vec![
5134                ParserIndexSassSelectorSymbolFactV0 {
5135                    selector_name: "btn".to_string(),
5136                    symbol_kind: "mixin",
5137                    name: "raised".to_string(),
5138                    role: "include",
5139                    resolution: "resolved",
5140                    byte_span: span_after(source, "@include", "raised")?,
5141                    range: range_after(source, "@include", "raised")?,
5142                },
5143                ParserIndexSassSelectorSymbolFactV0 {
5144                    selector_name: "btn".to_string(),
5145                    symbol_kind: "variable",
5146                    name: "gap".to_string(),
5147                    role: "reference",
5148                    resolution: "resolved",
5149                    byte_span: span_after(source, ".btn { color", "$gap")?,
5150                    range: range_after(source, ".btn { color", "$gap")?,
5151                },
5152                ParserIndexSassSelectorSymbolFactV0 {
5153                    selector_name: "btn".to_string(),
5154                    symbol_kind: "variable",
5155                    name: "gap".to_string(),
5156                    role: "reference",
5157                    resolution: "resolved",
5158                    byte_span: span_after(source, "@include raised(", "$gap")?,
5159                    range: range_after(source, "@include raised(", "$gap")?,
5160                },
5161                ParserIndexSassSelectorSymbolFactV0 {
5162                    selector_name: "ghost".to_string(),
5163                    symbol_kind: "variable",
5164                    name: "missing".to_string(),
5165                    role: "reference",
5166                    resolution: "unresolved",
5167                    byte_span: span_after(source, ".ghost { color", "$missing")?,
5168                    range: range_after(source, ".ghost { color", "$missing")?,
5169                },
5170            ]
5171        );
5172        Ok(())
5173    }
5174
5175    #[test]
5176    fn semantic_boundary_reports_lossless_cst_span_contract() {
5177        let source = "// heading\n.card { &--primary { color: red; } }\n";
5178        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
5179        let summary = super::summarize_semantic_boundary(&sheet);
5180
5181        assert_eq!(
5182            summary.parser_facts.lossless_cst.source_byte_len,
5183            source.len()
5184        );
5185        assert!(summary.parser_facts.lossless_cst.token_count > 0);
5186        assert_eq!(summary.parser_facts.lossless_cst.diagnostic_count, 0);
5187        assert!(
5188            summary
5189                .parser_facts
5190                .lossless_cst
5191                .all_token_spans_within_source
5192        );
5193        assert!(
5194            summary
5195                .parser_facts
5196                .lossless_cst
5197                .all_node_spans_within_source
5198        );
5199    }
5200
5201    #[test]
5202    fn index_summary_does_not_resolve_sass_variables_from_another_local_scope() -> Result<(), String>
5203    {
5204        let source = ".one { $gap: 1rem; }\n.two { color: $gap; }\n";
5205        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
5206        let summary = super::summarize_css_modules_intermediate(&sheet);
5207
5208        assert_eq!(
5209            summary.sass.selectors_with_resolved_variable_refs_names,
5210            Vec::<String>::new()
5211        );
5212        assert_eq!(
5213            summary.sass.selectors_with_unresolved_variable_refs_names,
5214            vec!["two"]
5215        );
5216        assert_eq!(
5217            summary
5218                .sass
5219                .same_file_resolution
5220                .resolved_variable_ref_names,
5221            Vec::<String>::new()
5222        );
5223        assert_eq!(
5224            summary
5225                .sass
5226                .same_file_resolution
5227                .unresolved_variable_ref_names,
5228            vec!["gap"]
5229        );
5230        assert_eq!(
5231            summary.sass.selector_symbol_facts,
5232            vec![ParserIndexSassSelectorSymbolFactV0 {
5233                selector_name: "two".to_string(),
5234                symbol_kind: "variable",
5235                name: "gap".to_string(),
5236                role: "reference",
5237                resolution: "unresolved",
5238                byte_span: span_after(source, ".two", "$gap")?,
5239                range: range_after(source, ".two", "$gap")?,
5240            }]
5241        );
5242        Ok(())
5243    }
5244
5245    #[test]
5246    fn index_summary_skips_module_qualified_sass_refs_from_same_file_resolution() {
5247        let source = r#"@use "./tokens" as tokens;
5248@mixin raised { box-shadow: none; }
5249@function tone($value) { @return $value; }
5250.button {
5251  color: tokens.$gap;
5252  @include tokens.raised;
5253  border-color: tokens.tone(tokens.$gap);
5254}
5255"#;
5256        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
5257        let summary = super::summarize_css_modules_intermediate(&sheet);
5258
5259        assert_eq!(summary.sass.variable_ref_names, vec!["value"]);
5260        assert_eq!(
5261            summary
5262                .sass
5263                .same_file_resolution
5264                .resolved_variable_ref_names,
5265            vec!["value"]
5266        );
5267        assert_eq!(
5268            summary
5269                .sass
5270                .same_file_resolution
5271                .unresolved_variable_ref_names,
5272            Vec::<String>::new()
5273        );
5274        assert_eq!(summary.sass.mixin_include_names, Vec::<String>::new());
5275        assert_eq!(summary.sass.function_call_names, Vec::<String>::new());
5276        assert_eq!(
5277            summary.sass.selectors_with_variable_refs_names,
5278            Vec::<String>::new()
5279        );
5280        assert_eq!(
5281            summary.sass.selectors_with_mixin_includes_names,
5282            Vec::<String>::new()
5283        );
5284        assert_eq!(
5285            summary.sass.selectors_with_function_calls_names,
5286            Vec::<String>::new()
5287        );
5288        assert_eq!(summary.sass.selector_symbol_facts, Vec::new());
5289        assert_eq!(
5290            summary.sass.module_use_edges,
5291            vec![ParserIndexSassModuleUseFactV0 {
5292                source: "./tokens".to_string(),
5293                namespace_kind: "alias",
5294                namespace: Some("tokens".to_string()),
5295            }]
5296        );
5297    }
5298
5299    #[test]
5300    fn index_summary_reports_sass_symbol_ranges_after_non_ascii_text() -> Result<(), String> {
5301        let source = "$gap: 1rem;\n.btn { content: \"한🙂\"; color: $gap; }\n";
5302        let sheet = parse_stylesheet(StyleLanguage::Scss, source);
5303        let summary = super::summarize_css_modules_intermediate(&sheet);
5304
5305        assert_eq!(
5306            summary.sass.selector_symbol_facts,
5307            vec![ParserIndexSassSelectorSymbolFactV0 {
5308                selector_name: "btn".to_string(),
5309                symbol_kind: "variable",
5310                name: "gap".to_string(),
5311                role: "reference",
5312                resolution: "resolved",
5313                byte_span: ParserByteSpanV0 { start: 46, end: 50 },
5314                range: ParserRangeV0 {
5315                    start: ParserPositionV0 {
5316                        line: 1,
5317                        character: 30,
5318                    },
5319                    end: ParserPositionV0 {
5320                        line: 1,
5321                        character: 34,
5322                    },
5323                },
5324            }]
5325        );
5326
5327        Ok(())
5328    }
5329}