Skip to main content

engine_style_parser/
lib.rs

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