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