1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
//! Class-selector names of a CSS Modules stylesheet, read from the lightningcss
//! AST. `crate::css` locates each name in the source.
use lightningcss::rules::CssRule;
use lightningcss::selector::{Component, PseudoClass, Selector, SelectorList};
use lightningcss::stylesheet::{ParserOptions, StyleSheet};
use rustc_hash::FxHashSet;
use crate::css::blank_bare_css_module_scope_keywords;
/// Collect the authoritative set of class-selector names from a CSS source by
/// parsing it into a real AST (lightningcss). Returns `None` on a
/// catastrophic parse failure (Sass syntax that is not standard CSS), in which
/// case the caller falls back to the regex scanner. With `error_recovery` on,
/// individual malformed rules are recovered silently and contribute a partial
/// set rather than triggering the fallback, so a broken rule drops only its own
/// classes (a conservative miss) instead of returning `None`.
///
/// This is the source of truth for which `.token` occurrences are genuine class
/// selectors. It natively excludes `@layer foo.bar` layer names, `@import ...
/// layer(theme.button)` layer references, `@keyframes` step selectors, id and
/// element selectors, and the contents of comments / strings / `url()`, which
/// the older regex-only scanner had to approximate with a stack of masking
/// passes. Classes nested inside `:is()` / `:where()` / `:not()` / `:has()` /
/// `:any()` / `::slotted()` / `:host()` / `:nth-child(... of ...)` are
/// collected too, matching the regex scanner's "every `.class` token" behavior.
/// A source that nests brackets too deep for the parser stack returns `None`.
pub fn lightningcss_class_set(source: &str) -> Option<FxHashSet<String>> {
crate::parse_style_bounded(source, || class_set_unbounded(source)).flatten()
}
fn class_set_unbounded(source: &str) -> Option<FxHashSet<String>> {
// lightningcss rejects the bare `:global` / `:local` forms, and error
// recovery then drops the full rule with its local classes. Blank the bare
// keywords so the rule parses. The scope scan in
// `mask_css_module_global_scopes` removes the global classes later.
let source = blank_bare_css_module_scope_keywords(source);
let options = ParserOptions {
// Recover from individual malformed rules so a single bad rule does not
// discard class names from the rest of the file.
error_recovery: true,
// These files are CSS Modules, so parse standard CSS syntax in CSS Modules
// mode. That makes the `:local()` / `:global()` pseudo-classes parse as
// real selectors rather than erroring. Classes in `:local()` are
// collected, classes in `:global()` are not. Renaming is a print-time
// concern, so the AST class names stay the original author-written names.
css_modules: Some(lightningcss::css_modules::Config::default()),
..ParserOptions::default()
};
let stylesheet = StyleSheet::parse(&source, options).ok()?;
let mut classes = FxHashSet::default();
collect_classes_from_rules(&stylesheet.rules.0, &mut classes);
Some(classes)
}
/// Recursively collect class-selector names from a list of CSS rules, descending
/// into every grouping rule (`@media`, `@supports`, `@container`, `@layer {}`,
/// `@document`, `@starting-style`, `@scope`, nested style rules) so a class
/// declared anywhere contributes to the set.
fn collect_classes_from_rules(rules: &[CssRule<'_>], classes: &mut FxHashSet<String>) {
for rule in rules {
match rule {
CssRule::Style(style) => {
collect_classes_from_selector_list(&style.selectors, classes);
collect_classes_from_rules(&style.rules.0, classes);
}
CssRule::Media(rule) => collect_classes_from_rules(&rule.rules.0, classes),
CssRule::Supports(rule) => collect_classes_from_rules(&rule.rules.0, classes),
CssRule::Container(rule) => collect_classes_from_rules(&rule.rules.0, classes),
CssRule::LayerBlock(rule) => collect_classes_from_rules(&rule.rules.0, classes),
CssRule::MozDocument(rule) => collect_classes_from_rules(&rule.rules.0, classes),
CssRule::StartingStyle(rule) => collect_classes_from_rules(&rule.rules.0, classes),
CssRule::Nesting(rule) => {
collect_classes_from_selector_list(&rule.style.selectors, classes);
collect_classes_from_rules(&rule.style.rules.0, classes);
}
CssRule::Scope(rule) => {
if let Some(scope_start) = &rule.scope_start {
collect_classes_from_selector_list(scope_start, classes);
}
if let Some(scope_end) = &rule.scope_end {
collect_classes_from_selector_list(scope_end, classes);
}
collect_classes_from_rules(&rule.rules.0, classes);
}
_ => {}
}
}
}
fn collect_classes_from_selector_list(list: &SelectorList<'_>, classes: &mut FxHashSet<String>) {
for selector in &list.0 {
collect_classes_from_selector(selector, classes);
}
}
fn collect_classes_from_selector(selector: &Selector<'_>, classes: &mut FxHashSet<String>) {
for component in selector.iter_raw_match_order() {
match component {
Component::Class(name) => {
classes.insert(name.0.to_string());
}
Component::Is(list)
| Component::Where(list)
| Component::Has(list)
| Component::Negation(list)
| Component::Any(_, list) => {
for nested in list.as_ref() {
collect_classes_from_selector(nested, classes);
}
}
Component::Slotted(nested) | Component::Host(Some(nested)) => {
collect_classes_from_selector(nested, classes);
}
Component::NthOf(data) => {
for nested in data.selectors() {
collect_classes_from_selector(nested, classes);
}
}
// CSS Modules `:local(.foo)` wraps a real local selector. A
// `:global(.foo)` class is not in the class map, so it is skipped.
Component::NonTSPseudoClass(PseudoClass::Local { selector }) => {
collect_classes_from_selector(selector, classes);
}
_ => {}
}
}
}