Skip to content

Commit 28392e8

Browse files
committed
extract used CSS variables from CSS files
We will track CSS files while traversing the folder structure, but don't extract any normal candidates from these CSS files. We will also not include these files into any of the returned globs. We will just run the CSS extractor on these CSS files, and every time we find a CSS variable, we will verify whether it was used or not. For now, "using", just means if it is used inside of `var(…)`.
1 parent 6286f26 commit 28392e8

File tree

3 files changed

+125
-2
lines changed

3 files changed

+125
-2
lines changed

crates/oxide/src/extractor/mod.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::cursor;
22
use crate::extractor::machine::Span;
3+
use bstr::ByteSlice;
34
use candidate_machine::CandidateMachine;
45
use css_variable_machine::CssVariableMachine;
56
use machine::{Machine, MachineState};
@@ -139,6 +140,41 @@ impl<'a> Extractor<'a> {
139140

140141
extracted
141142
}
143+
144+
pub fn extract_css_variables_from_css_files(&mut self) -> Vec<Extracted<'a>> {
145+
let mut extracted = Vec::with_capacity(100);
146+
147+
let len = self.cursor.input.len();
148+
149+
let cursor = &mut self.cursor.clone();
150+
while cursor.pos < len {
151+
if cursor.curr.is_ascii_whitespace() {
152+
cursor.advance();
153+
continue;
154+
}
155+
156+
if let MachineState::Done(span) = self.css_variable_machine.next(cursor) {
157+
// We are only interested in variables that are used, not defined. Therefore we
158+
// need to ensure that the variable is prefixed with `var(`.
159+
if span.start < 4 {
160+
cursor.advance();
161+
continue;
162+
}
163+
164+
let slice_before = Span::new(span.start - 4, span.start - 1);
165+
if !slice_before.slice(self.cursor.input).starts_with(b"var(") {
166+
cursor.advance();
167+
continue;
168+
}
169+
170+
extracted.push(Extracted::CssVariable(span.slice(self.cursor.input)));
171+
}
172+
173+
cursor.advance();
174+
}
175+
176+
extracted
177+
}
142178
}
143179

144180
// Extract sub-candidates from a given range.

crates/oxide/src/scanner/mod.rs

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,11 +215,25 @@ impl Scanner {
215215
fn extract_candidates(&mut self) -> Vec<String> {
216216
let changed_content = self.changed_content.drain(..).collect::<Vec<_>>();
217217

218-
let candidates = parse_all_blobs(read_all_files(changed_content));
218+
// Extract all candidates from the changed content
219+
let mut new_candidates = parse_all_blobs(read_all_files(changed_content));
220+
221+
// Extract all CSS variables from the CSS files
222+
let css_files = self.css_files.drain(..).collect::<Vec<_>>();
223+
if !css_files.is_empty() {
224+
let css_variables = extract_css_variables(read_all_files(
225+
css_files
226+
.into_iter()
227+
.map(|file| ChangedContent::File(file, "css".into()))
228+
.collect(),
229+
));
230+
231+
new_candidates.extend(css_variables);
232+
}
219233

220234
// Only compute the new candidates and ignore the ones we already have. This is for
221235
// subsequent calls to prevent serializing the entire set of candidates every time.
222-
let mut new_candidates = candidates
236+
let mut new_candidates = new_candidates
223237
.into_par_iter()
224238
.filter(|candidate| !self.candidates.contains(candidate))
225239
.collect::<Vec<_>>();
@@ -411,6 +425,44 @@ fn read_all_files(changed_content: Vec<ChangedContent>) -> Vec<Vec<u8>> {
411425
.collect()
412426
}
413427

428+
#[tracing::instrument(skip_all)]
429+
fn extract_css_variables(blobs: Vec<Vec<u8>>) -> Vec<String> {
430+
let mut result: Vec<_> = blobs
431+
.par_iter()
432+
.flat_map(|blob| blob.par_split(|x| *x == b'\n'))
433+
.filter_map(|blob| {
434+
if blob.is_empty() {
435+
return None;
436+
}
437+
438+
let extracted =
439+
crate::extractor::Extractor::new(blob).extract_css_variables_from_css_files();
440+
if extracted.is_empty() {
441+
return None;
442+
}
443+
444+
Some(FxHashSet::from_iter(extracted.into_iter().map(
445+
|x| match x {
446+
Extracted::CssVariable(bytes) => bytes,
447+
_ => &[],
448+
},
449+
)))
450+
})
451+
.reduce(Default::default, |mut a, b| {
452+
a.extend(b);
453+
a
454+
})
455+
.into_iter()
456+
.map(|s| unsafe { String::from_utf8_unchecked(s.to_vec()) })
457+
.collect();
458+
459+
// SAFETY: Unstable sort is faster and in this scenario it's also safe because we are
460+
// guaranteed to have unique candidates.
461+
result.par_sort_unstable();
462+
463+
result
464+
}
465+
414466
#[tracing::instrument(skip_all)]
415467
fn parse_all_blobs(blobs: Vec<Vec<u8>>) -> Vec<String> {
416468
let mut result: Vec<_> = blobs

crates/oxide/tests/scanner.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1735,4 +1735,39 @@ mod scanner {
17351735

17361736
assert_eq!(candidates, vec!["content-['abcd/xyz.html']"]);
17371737
}
1738+
1739+
#[test]
1740+
fn test_extract_used_css_variables_from_css() {
1741+
let dir = tempdir().unwrap().into_path();
1742+
create_files_in(
1743+
&dir,
1744+
&[
1745+
(
1746+
"src/index.css",
1747+
r#"
1748+
@theme {
1749+
--color-red: #ff0000; /* Not used, so don't extract */
1750+
--color-green: #00ff00; /* Not used, so don't extract */
1751+
}
1752+
1753+
.button {
1754+
color: var(--color-red); /* Used, so extract */
1755+
}
1756+
"#,
1757+
),
1758+
("src/used-at-start.css", "var(--color-used-at-start)"),
1759+
// Here to verify that we don't crash when trying to find `var(` in front of the
1760+
// variable.
1761+
("src/defined-at-start.css", "--color-defined-at-start: red;"),
1762+
],
1763+
);
1764+
1765+
let mut scanner = Scanner::new(vec![public_source_entry_from_pattern(
1766+
dir.clone(),
1767+
"@source './'",
1768+
)]);
1769+
let candidates = scanner.scan();
1770+
1771+
assert_eq!(candidates, vec!["--color-red", "--color-used-at-start"]);
1772+
}
17381773
}

0 commit comments

Comments
 (0)