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