Skip to main content

engine_style_parser/
lib.rs

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