Skip to content

Commit 25f7ccf

Browse files
authored
Fix class extraction for Rails' strict locals (tailwindlabs#19525)
Fixes: tailwindlabs#19481 This PR improves the Ruby extractor to better handle strict locals. We recently introduced skipping comments in the Ruby extractor (PR tailwindlabs#19243 for tailwindlabs#19239) by ignoring comments that start with `#` until the end of the line. Strict locals are implemented like this: ```ruby <%# locals: (css: "text-amber-600") %> ``` Notice the `#` after the `<%`, we considered this a comment and ignored it. This PR changes that behavior slightly where we skip comments that are preceded by `%`. This means that `<%# anything here _will_ be scanned %>`. This should solve the strict locals case, and normal comments will still be skipped. We can be more strict in the future if needed, but I think that this should be a good solution for both scenarios. ### Test plan 1. Added a test to ensure we extract candidates in strict locals 2. Added a regression test for issue tailwindlabs#19239 where we introduced skipping comments in the Ruby extractor 3. Other existing tests are still passing We can also verify the extracted candidates: (it's subtle, but you can see that the class is being extracted now) <img width="1187" height="1376" alt="image" src="https://github.com/user-attachments/assets/74bbfd79-9db4-4a5b-bd8d-25f1565c6bfd" />
1 parent d5beb95 commit 25f7ccf

3 files changed

Lines changed: 75 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Allow whitespace around `@source inline()` argument ([#19461](https://github.com/tailwindlabs/tailwindcss/pull/19461))
1414
- CLI: Emit comment when source maps are saved to files ([#19447](https://github.com/tailwindlabs/tailwindcss/pull/19447))
1515
- Detect utilities when containing capital letters followed by numbers ([#19465](https://github.com/tailwindlabs/tailwindcss/pull/19465))
16+
- Fix class extraction for Rails' strict locals ([#19525](https://github.com/tailwindlabs/tailwindcss/pull/19525))
1617

1718
### Added
1819

crates/oxide/src/extractor/pre_processors/pre_processor.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,33 @@ pub trait PreProcessor: Sized + Default {
2525
}
2626

2727
#[cfg(test)]
28-
fn test_extract_contains(input: &str, items: Vec<&str>) {
28+
fn test_extract_exact(input: &str, expected: Vec<&str>) {
29+
use crate::extractor::{Extracted, Extractor};
30+
31+
let input = input.as_bytes();
32+
33+
let processor = Self::default();
34+
let transformed = processor.process(input);
35+
36+
let extracted = Extractor::new(&transformed).extract();
37+
38+
// Extract all candidates and css variables.
39+
let candidates = extracted
40+
.iter()
41+
.filter_map(|x| match x {
42+
Extracted::Candidate(bytes) => std::str::from_utf8(bytes).ok(),
43+
Extracted::CssVariable(bytes) => std::str::from_utf8(bytes).ok(),
44+
})
45+
.collect::<Vec<_>>();
46+
47+
if candidates != expected {
48+
dbg!(&candidates, &expected);
49+
panic!("Extracted candidates do not match expected candidates");
50+
}
51+
}
52+
53+
#[cfg(test)]
54+
fn test_extract_contains(input: &str, expected: Vec<&str>) {
2955
use crate::extractor::{Extracted, Extractor};
3056

3157
let input = input.as_bytes();
@@ -46,7 +72,7 @@ pub trait PreProcessor: Sized + Default {
4672

4773
// Ensure all items are present in the candidates.
4874
let mut missing = vec![];
49-
for item in &items {
75+
for item in &expected {
5076
if !candidates.contains(item) {
5177
missing.push(item);
5278
}

crates/oxide/src/extractor/pre_processors/ruby.rs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,11 @@ impl PreProcessor for Ruby {
119119
}
120120

121121
// Replace comments in Ruby files
122-
b'#' => {
122+
//
123+
// Except for strict locals, these are defined in a `<%# locals: … %>`. Checking if
124+
// the comment is preceded by a `%` should be enough without having to perform more
125+
// parsing logic. Worst case we _do_ scan a few comments.
126+
b'#' if !matches!(cursor.prev, b'%') => {
123127
result[cursor.pos] = b' ';
124128
cursor.advance();
125129

@@ -382,4 +386,45 @@ mod tests {
382386
"#;
383387
Ruby::test_extract_contains(input, vec!["z-1", "z-2", "z-3"]);
384388
}
389+
390+
// https://github.com/tailwindlabs/tailwindcss/issues/19239
391+
#[test]
392+
fn test_skip_comments() {
393+
let input = r#"
394+
# From activerecord-8.1.1/lib/active_record/errors.rb:147
395+
# Rails uses RDoc cross-reference syntax in inline documentation:
396+
# {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!]
397+
"#;
398+
399+
// Nothing should be extracted from comments, so expect an empty array.
400+
Ruby::test_extract_exact(input, vec![]);
401+
}
402+
403+
// https://github.com/tailwindlabs/tailwindcss/issues/19481
404+
#[test]
405+
fn test_strict_locals() {
406+
// Strict locals are defined in a `<%# locals: … %>`, but the `#` looks like a comment
407+
// which we should not ignore in this case.
408+
let input = r#"
409+
<%# locals: (css: "text-amber-600") %>
410+
<% more_css = "text-sky-500" %>
411+
412+
<p class="text-green-500">
413+
In a partial
414+
</p>
415+
416+
<p class="<%= css %>">
417+
In a partial using explicit local variables
418+
</p>
419+
420+
<p class="<%= more_css %>">
421+
In a partial using explicit local variables
422+
</p>
423+
"#;
424+
425+
Ruby::test_extract_contains(
426+
input,
427+
vec!["text-amber-600", "text-sky-500", "text-green-500"],
428+
);
429+
}
385430
}

0 commit comments

Comments
 (0)