Skip to main content

omena_parser/facts/
selectors.rs

1//! Parser facts for selector definitions and references.
2//!
3//! Selector facts expose the local class, id, and custom property names
4//! required by diagnostics, rename, references, and transform reachability.
5
6use cstree::text::TextRange;
7use omena_syntax::SyntaxKind;
8use std::collections::BTreeSet;
9
10use crate::{
11    ParseResult, Token, find_selector_block_after_header, is_selector_combinator_kind,
12    matching_right_paren_from_range, next_non_trivia_token_after_range,
13    next_non_trivia_token_until, previous_non_trivia_token, selector_component_can_end,
14    selector_component_can_start, skip_statement_or_unmatched_boundary, skip_trivia_tokens,
15    style_wrapper_at_rule, token_index_by_range,
16};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct ParsedSelectorFact {
20    pub kind: ParsedSelectorFactKind,
21    pub name: String,
22    pub range: TextRange,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
26pub enum ParsedSelectorFactKind {
27    Class,
28    Id,
29    Placeholder,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub(crate) struct SelectorBranch {
34    pub(crate) name: String,
35    pub(crate) range: TextRange,
36    pub(crate) bare_suffix_base: bool,
37}
38
39pub(crate) fn collect_selector_facts_from_cst(
40    text: &str,
41    parsed: &ParseResult,
42) -> Vec<ParsedSelectorFact> {
43    let mut selectors = Vec::new();
44    let mut seen = BTreeSet::new();
45    for tokens in selector_statement_tokens_from_cst(text, parsed) {
46        collect_selector_facts_in_range(
47            &tokens,
48            0,
49            tokens.len(),
50            &[],
51            None,
52            &mut seen,
53            &mut selectors,
54        );
55    }
56    selectors
57}
58
59fn selector_statement_tokens_from_cst<'text>(
60    text: &'text str,
61    parsed: &ParseResult,
62) -> Vec<Vec<Token<'text>>> {
63    vec![
64        parsed
65            .syntax_token_views()
66            .iter()
67            .map(|token| {
68                let range = token.range;
69                let start = u32::from(range.start()) as usize;
70                let end = u32::from(range.end()) as usize;
71                Token {
72                    kind: token.kind,
73                    text: text.get(start..end).unwrap_or_default(),
74                    range,
75                }
76            })
77            .collect(),
78    ]
79}
80
81fn collect_selector_facts_in_range(
82    tokens: &[Token<'_>],
83    start: usize,
84    end: usize,
85    parent_branches: &[SelectorBranch],
86    css_module_scope: Option<&'static str>,
87    seen: &mut BTreeSet<(ParsedSelectorFactKind, String, u32, u32)>,
88    selectors: &mut Vec<ParsedSelectorFact>,
89) {
90    let mut index = start;
91    while index < end {
92        index = skip_trivia_tokens(tokens, index, end);
93        if index >= end {
94            break;
95        }
96
97        if tokens[index].kind == SyntaxKind::AtKeyword {
98            let block = find_selector_block_after_header(tokens, index, end);
99            if let Some((open, close)) = block {
100                if tokens[index].text == "@nest" {
101                    if css_module_scope == Some("global") {
102                        collect_selector_facts_in_range(
103                            tokens,
104                            open + 1,
105                            close,
106                            &[],
107                            css_module_scope,
108                            seen,
109                            selectors,
110                        );
111                    } else {
112                        let branches =
113                            resolve_selector_header(tokens, index + 1, open, parent_branches);
114                        push_class_selector_facts_from_header(
115                            selectors,
116                            seen,
117                            tokens,
118                            index + 1,
119                            open,
120                        );
121                        for branch in &branches {
122                            push_selector_fact(
123                                selectors,
124                                seen,
125                                ParsedSelectorFactKind::Class,
126                                branch.name.clone(),
127                                branch.range,
128                            );
129                        }
130                        collect_selector_facts_in_range(
131                            tokens,
132                            open + 1,
133                            close,
134                            &branches,
135                            css_module_scope,
136                            seen,
137                            selectors,
138                        );
139                    }
140                } else if style_wrapper_at_rule(tokens[index].text) {
141                    collect_selector_facts_in_range(
142                        tokens,
143                        open + 1,
144                        close,
145                        parent_branches,
146                        css_module_scope,
147                        seen,
148                        selectors,
149                    );
150                }
151                index = close + 1;
152            } else {
153                index = skip_statement_or_unmatched_boundary(tokens, index, end);
154            }
155            continue;
156        }
157
158        let Some((open, close)) = find_selector_block_after_header(tokens, index, end) else {
159            index = skip_statement_or_unmatched_boundary(tokens, index, end);
160            continue;
161        };
162
163        let effective_scope = css_module_scope
164            .or_else(|| css_module_block_scope_marker_in_header(tokens, index, open));
165        if effective_scope == Some("global") {
166            collect_selector_facts_in_range(
167                tokens,
168                open + 1,
169                close,
170                &[],
171                effective_scope,
172                seen,
173                selectors,
174            );
175        } else {
176            let branches = resolve_selector_header(tokens, index, open, parent_branches);
177            push_class_selector_facts_from_header(selectors, seen, tokens, index, open);
178            for branch in &branches {
179                push_selector_fact(
180                    selectors,
181                    seen,
182                    ParsedSelectorFactKind::Class,
183                    branch.name.clone(),
184                    branch.range,
185                );
186            }
187            for id in collect_id_selector_facts_from_header(tokens, index, open)
188                .into_iter()
189                .chain(collect_local_function_id_selector_facts_from_header(
190                    tokens, index, open,
191                ))
192            {
193                push_selector_fact(selectors, seen, ParsedSelectorFactKind::Id, id.0, id.1);
194            }
195            for placeholder in collect_placeholder_selector_facts_from_header(tokens, index, open) {
196                push_selector_fact(
197                    selectors,
198                    seen,
199                    ParsedSelectorFactKind::Placeholder,
200                    placeholder.0,
201                    placeholder.1,
202                );
203            }
204
205            collect_selector_facts_in_range(
206                tokens,
207                open + 1,
208                close,
209                &branches,
210                effective_scope,
211                seen,
212                selectors,
213            );
214        }
215        index = close + 1;
216    }
217}
218
219fn push_class_selector_facts_from_header(
220    selectors: &mut Vec<ParsedSelectorFact>,
221    seen: &mut BTreeSet<(ParsedSelectorFactKind, String, u32, u32)>,
222    tokens: &[Token<'_>],
223    start: usize,
224    end: usize,
225) {
226    for (name, range) in collect_class_selector_names_from_header(tokens, start, end) {
227        push_selector_fact(selectors, seen, ParsedSelectorFactKind::Class, name, range);
228    }
229}
230
231fn push_selector_fact(
232    selectors: &mut Vec<ParsedSelectorFact>,
233    seen: &mut BTreeSet<(ParsedSelectorFactKind, String, u32, u32)>,
234    kind: ParsedSelectorFactKind,
235    name: String,
236    range: TextRange,
237) {
238    if seen.insert((
239        kind,
240        name.clone(),
241        u32::from(range.start()),
242        u32::from(range.end()),
243    )) {
244        selectors.push(ParsedSelectorFact { kind, name, range });
245    }
246}
247
248pub(crate) fn resolve_selector_header(
249    tokens: &[Token<'_>],
250    start: usize,
251    end: usize,
252    parent_branches: &[SelectorBranch],
253) -> Vec<SelectorBranch> {
254    split_selector_groups(tokens, start, end)
255        .into_iter()
256        .flat_map(|(group_start, group_end)| {
257            resolve_selector_group(tokens, group_start, group_end, parent_branches)
258        })
259        .collect()
260}
261
262fn resolve_selector_group(
263    tokens: &[Token<'_>],
264    start: usize,
265    end: usize,
266    parent_branches: &[SelectorBranch],
267) -> Vec<SelectorBranch> {
268    if let Some(mut local_names) = collect_local_function_selector_names(tokens, start, end) {
269        local_names.extend(collect_class_selector_names_from_header(tokens, start, end));
270        let bare_suffix_base = parent_branches.is_empty() && local_names.len() == 1;
271        return local_names
272            .into_iter()
273            .map(|(name, range)| SelectorBranch {
274                name,
275                range,
276                bare_suffix_base,
277            })
278            .collect();
279    }
280
281    let (tail_start, tail_end) = selector_group_tail_range(tokens, start, end);
282    let tail_start = skip_trivia_tokens(tokens, tail_start, tail_end);
283
284    if let Some((suffix, range)) = ampersand_suffix_selector(tokens, tail_start, tail_end) {
285        let bases: Vec<&SelectorBranch> = if parent_branches.is_empty() {
286            Vec::new()
287        } else {
288            parent_branches
289                .iter()
290                .filter(|parent| parent.bare_suffix_base)
291                .collect()
292        };
293        return bases
294            .into_iter()
295            .map(|parent| SelectorBranch {
296                name: format!("{}{}", parent.name, suffix),
297                range,
298                bare_suffix_base: parent.bare_suffix_base,
299            })
300            .collect();
301    }
302
303    let class_names = collect_class_selector_names_from_header(tokens, tail_start, tail_end);
304    if class_names.is_empty() {
305        return Vec::new();
306    }
307
308    let bare_suffix_base = parent_branches.is_empty()
309        && class_names.len() == 1
310        && is_bare_class_selector_group(tokens, tail_start, tail_end);
311    class_names
312        .into_iter()
313        .map(|(name, range)| SelectorBranch {
314            name,
315            range,
316            bare_suffix_base,
317        })
318        .collect()
319}
320
321fn is_bare_class_selector_group(tokens: &[Token<'_>], start: usize, end: usize) -> bool {
322    let dot_index = skip_trivia_tokens(tokens, start, end);
323    if tokens.get(dot_index).map(|token| token.kind) != Some(SyntaxKind::Dot) {
324        return false;
325    }
326    let name_index = skip_trivia_tokens(tokens, dot_index + 1, end);
327    if !tokens.get(name_index).is_some_and(|token| {
328        matches!(
329            token.kind,
330            SyntaxKind::Ident | SyntaxKind::CustomPropertyName
331        )
332    }) {
333        return false;
334    }
335    skip_trivia_tokens(tokens, name_index + 1, end) >= end
336}
337
338pub(crate) fn split_selector_groups(
339    tokens: &[Token<'_>],
340    start: usize,
341    end: usize,
342) -> Vec<(usize, usize)> {
343    let mut groups = Vec::new();
344    let mut group_start = start;
345    let mut paren_depth = 0usize;
346    let mut bracket_depth = 0usize;
347    let mut index = start;
348    while index < end {
349        match tokens[index].kind {
350            SyntaxKind::LeftParen => paren_depth += 1,
351            SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
352            SyntaxKind::LeftBracket => bracket_depth += 1,
353            SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
354            SyntaxKind::Comma if paren_depth == 0 && bracket_depth == 0 => {
355                groups.push((group_start, index));
356                group_start = index + 1;
357            }
358            _ => {}
359        }
360        index += 1;
361    }
362    groups.push((group_start, end));
363    groups
364}
365
366fn selector_group_tail_range(tokens: &[Token<'_>], start: usize, end: usize) -> (usize, usize) {
367    let mut paren_depth = 0usize;
368    let mut bracket_depth = 0usize;
369    let mut tail_start = start;
370    let mut index = start;
371    while index < end {
372        match tokens[index].kind {
373            SyntaxKind::LeftParen => paren_depth += 1,
374            SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
375            SyntaxKind::LeftBracket => bracket_depth += 1,
376            SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
377            kind if paren_depth == 0 && bracket_depth == 0 && is_selector_combinator_kind(kind) => {
378                tail_start = index + 1;
379            }
380            SyntaxKind::Whitespace if paren_depth == 0 && bracket_depth == 0 => {
381                let previous = previous_non_trivia_token(tokens, start, index);
382                let next = next_non_trivia_token_until(tokens, index + 1, end);
383                if previous.is_some_and(|token| selector_component_can_end(token.kind))
384                    && next.is_some_and(|token| selector_component_can_start(token.kind))
385                {
386                    tail_start = index + 1;
387                }
388            }
389            _ => {}
390        }
391        index += 1;
392    }
393    (tail_start, end)
394}
395
396fn ampersand_suffix_selector(
397    tokens: &[Token<'_>],
398    start: usize,
399    end: usize,
400) -> Option<(String, TextRange)> {
401    let ampersand_index = skip_trivia_tokens(tokens, start, end);
402    if tokens.get(ampersand_index)?.kind != SyntaxKind::Ampersand {
403        return None;
404    }
405    let suffix = next_non_trivia_token_until(tokens, ampersand_index + 1, end)?;
406    if matches!(
407        suffix.kind,
408        SyntaxKind::Ident | SyntaxKind::CustomPropertyName
409    ) {
410        return Some((suffix.text.to_string(), suffix.range));
411    }
412    None
413}
414
415pub(crate) fn collect_class_selector_names_from_header(
416    tokens: &[Token<'_>],
417    start: usize,
418    end: usize,
419) -> Vec<(String, TextRange)> {
420    let mut names = Vec::new();
421    let mut index = start;
422    let mut paren_depth = 0usize;
423    let mut bracket_depth = 0usize;
424    while index < end {
425        match tokens[index].kind {
426            SyntaxKind::LeftParen => paren_depth += 1,
427            SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
428            SyntaxKind::LeftBracket => bracket_depth += 1,
429            SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
430            _ => {}
431        }
432        if paren_depth == 0
433            && bracket_depth == 0
434            && tokens[index].kind == SyntaxKind::Dot
435            && let Some(name) = next_non_trivia_token_until(tokens, index + 1, end)
436            && matches!(
437                name.kind,
438                SyntaxKind::Ident | SyntaxKind::CustomPropertyName
439            )
440        {
441            names.push((name.text.to_string(), name.range));
442        }
443        index += 1;
444    }
445    names
446}
447
448fn collect_local_function_selector_names(
449    tokens: &[Token<'_>],
450    start: usize,
451    end: usize,
452) -> Option<Vec<(String, TextRange)>> {
453    let colon_index = skip_trivia_tokens(tokens, start, end);
454    if tokens.get(colon_index)?.kind != SyntaxKind::Colon {
455        return None;
456    }
457    let ident = next_non_trivia_token_until(tokens, colon_index + 1, end)?;
458    if ident.kind != SyntaxKind::Ident || ident.text != "local" {
459        return None;
460    }
461    let open_index = skip_trivia_tokens(tokens, colon_index + 2, end);
462    if tokens.get(open_index)?.kind != SyntaxKind::LeftParen {
463        return None;
464    }
465    Some(collect_class_selector_names_from_header(
466        tokens,
467        open_index + 1,
468        end.saturating_sub(1),
469    ))
470}
471
472fn collect_local_function_id_selector_facts_from_header(
473    tokens: &[Token<'_>],
474    start: usize,
475    end: usize,
476) -> Vec<(String, TextRange)> {
477    let mut ids = Vec::new();
478    let mut index = start;
479    while index < end {
480        if tokens[index].kind == SyntaxKind::Colon
481            && let Some(scope) = next_non_trivia_token_until(tokens, index + 1, end)
482            && scope.kind == SyntaxKind::Ident
483            && scope.text == "local"
484            && let Some(open) = next_non_trivia_token_after_range(tokens, scope.range, end)
485            && open.kind == SyntaxKind::LeftParen
486            && let Some(close) = matching_right_paren_from_range(tokens, open.range, end)
487        {
488            ids.extend(collect_id_selector_facts_from_header(
489                tokens,
490                token_index_by_range(tokens, open.range).map_or(index + 1, |value| value + 1),
491                close,
492            ));
493            index = close.saturating_add(1);
494            continue;
495        }
496        index += 1;
497    }
498    ids
499}
500
501pub(crate) fn css_module_block_scope_marker_in_header(
502    tokens: &[Token<'_>],
503    start: usize,
504    end: usize,
505) -> Option<&'static str> {
506    if next_non_trivia_token_until(tokens, start, end)
507        .is_some_and(|token| token.kind == SyntaxKind::AtKeyword)
508    {
509        return None;
510    }
511
512    css_module_scope_marker_after_colon(tokens, start, end)
513        .filter(|_| !css_module_scope_marker_is_function(tokens, start, end))
514}
515
516pub(crate) fn css_module_header_is_global_only(
517    tokens: &[Token<'_>],
518    start: usize,
519    end: usize,
520) -> bool {
521    if next_non_trivia_token_until(tokens, start, end)
522        .is_some_and(|token| token.kind == SyntaxKind::AtKeyword)
523    {
524        return false;
525    }
526    css_module_header_contains_scope(tokens, start, end, "global")
527        && collect_class_selector_names_from_header(tokens, start, end).is_empty()
528        && collect_local_function_selector_names(tokens, start, end)
529            .map(|names| names.is_empty())
530            .unwrap_or(true)
531}
532
533fn css_module_header_contains_scope(
534    tokens: &[Token<'_>],
535    start: usize,
536    end: usize,
537    expected_scope: &str,
538) -> bool {
539    let mut index = start;
540    while index < end {
541        if tokens[index].kind == SyntaxKind::Colon
542            && let Some(scope) = next_non_trivia_token_until(tokens, index + 1, end)
543            && scope.kind == SyntaxKind::Ident
544            && scope.text == expected_scope
545        {
546            return true;
547        }
548        index += 1;
549    }
550    false
551}
552
553fn css_module_scope_marker_after_colon(
554    tokens: &[Token<'_>],
555    start: usize,
556    end: usize,
557) -> Option<&'static str> {
558    let colon = skip_trivia_tokens(tokens, start, end);
559    if tokens.get(colon)?.kind != SyntaxKind::Colon {
560        return None;
561    }
562    let scope = next_non_trivia_token_until(tokens, colon + 1, end)?;
563    if scope.kind != SyntaxKind::Ident {
564        return None;
565    }
566    match scope.text {
567        "global" => Some("global"),
568        "local" => Some("local"),
569        _ => None,
570    }
571}
572
573fn css_module_scope_marker_is_function(tokens: &[Token<'_>], start: usize, end: usize) -> bool {
574    let colon = skip_trivia_tokens(tokens, start, end);
575    let mut index = colon + 1;
576    let Some(scope) = next_non_trivia_token_until(tokens, index, end) else {
577        return false;
578    };
579    while index < end {
580        if tokens[index].range == scope.range {
581            break;
582        }
583        index += 1;
584    }
585    let Some(next) = next_non_trivia_token_until(tokens, index + 1, end) else {
586        return false;
587    };
588    scope.kind == SyntaxKind::Ident && next.kind == SyntaxKind::LeftParen
589}
590
591fn collect_id_selector_facts_from_header(
592    tokens: &[Token<'_>],
593    start: usize,
594    end: usize,
595) -> Vec<(String, TextRange)> {
596    let mut names = Vec::new();
597    let mut index = start;
598    let mut paren_depth = 0usize;
599    let mut bracket_depth = 0usize;
600    while index < end {
601        match tokens[index].kind {
602            SyntaxKind::LeftParen => paren_depth += 1,
603            SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
604            SyntaxKind::LeftBracket => bracket_depth += 1,
605            SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
606            _ => {}
607        }
608        let token = tokens[index];
609        if paren_depth == 0 && bracket_depth == 0 && token.kind == SyntaxKind::Hash {
610            names.push((token.text.trim_start_matches('#').to_string(), token.range));
611        }
612        index += 1;
613    }
614    names
615}
616
617fn collect_placeholder_selector_facts_from_header(
618    tokens: &[Token<'_>],
619    start: usize,
620    end: usize,
621) -> Vec<(String, TextRange)> {
622    let mut names = Vec::new();
623    let mut index = start;
624    let mut paren_depth = 0usize;
625    let mut bracket_depth = 0usize;
626    while index < end {
627        match tokens[index].kind {
628            SyntaxKind::LeftParen => paren_depth += 1,
629            SyntaxKind::RightParen => paren_depth = paren_depth.saturating_sub(1),
630            SyntaxKind::LeftBracket => bracket_depth += 1,
631            SyntaxKind::RightBracket => bracket_depth = bracket_depth.saturating_sub(1),
632            _ => {}
633        }
634        let token = tokens[index];
635        if paren_depth == 0 && bracket_depth == 0 && token.kind == SyntaxKind::ScssPlaceholder {
636            names.push((token.text.trim_start_matches('%').to_string(), token.range));
637        }
638        index += 1;
639    }
640    names
641}