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