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