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