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 = ¶ms[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(¶ms[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}