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