Ensure } and { are valid boundary characters (#17001)

This commit is contained in:
Robin Malfait 2025-03-06 20:40:03 +01:00 committed by GitHub
parent 85c6e04f44
commit 57e91a671a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 112 additions and 49 deletions

View File

@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ensure utilities are sorted based on their actual property order ([#16995](https://github.com/tailwindlabs/tailwindcss/pull/16995))
- Ensure strings in Pug and Slim templates are handled correctly ([#17000](https://github.com/tailwindlabs/tailwindcss/pull/17000))
- Ensure `}` and `{` are valid boundary characters when extracting candidates ([#17001](https://github.com/tailwindlabs/tailwindcss/pull/17001))
## [4.0.11] - 2025-03-06

View File

@ -191,7 +191,7 @@ fn is_valid_common_boundary(c: &u8) -> bool {
/// A candidate must be preceded by any of these characters.
#[inline(always)]
fn is_valid_before_boundary(c: &u8) -> bool {
is_valid_common_boundary(c) || matches!(c, b'.')
is_valid_common_boundary(c) || matches!(c, b'.' | b'}')
}
/// A candidate must be followed by any of these characters.
@ -200,8 +200,8 @@ fn is_valid_before_boundary(c: &u8) -> bool {
/// E.g.: `<div class:flex="bool">` Svelte
/// ^
#[inline(always)]
fn is_valid_after_boundary(c: &u8) -> bool {
is_valid_common_boundary(c) || matches!(c, b'}' | b']' | b'=')
pub fn is_valid_after_boundary(c: &u8) -> bool {
is_valid_common_boundary(c) || matches!(c, b'}' | b']' | b'=' | b'{')
}
#[inline(always)]
@ -316,13 +316,16 @@ mod tests {
//
// HTML
// Inside a class (on its own)
(r#"<div class="{}"></div>"#, vec![]),
(r#"<div class="{}"></div>"#, vec!["class"]),
// Inside a class (first)
(r#"<div class="{} foo"></div>"#, vec!["foo"]),
(r#"<div class="{} foo"></div>"#, vec!["class", "foo"]),
// Inside a class (second)
(r#"<div class="foo {}"></div>"#, vec!["foo"]),
(r#"<div class="foo {}"></div>"#, vec!["class", "foo"]),
// Inside a class (surrounded)
(r#"<div class="foo {} bar"></div>"#, vec!["foo", "bar"]),
(
r#"<div class="foo {} bar"></div>"#,
vec!["class", "foo", "bar"],
),
// --------------------------
//
// JavaScript

View File

@ -248,6 +248,22 @@ mod tests {
assert_eq!(actual, expected);
}
fn assert_extract_candidates_contains(input: &str, expected: Vec<&str>) {
let actual = extract_sorted_candidates(input);
let mut missing = vec![];
for item in &expected {
if !actual.contains(item) {
missing.push(item);
}
}
if !missing.is_empty() {
dbg!(&actual, &missing);
panic!("Missing some items");
}
}
fn assert_extract_sorted_css_variables(input: &str, expected: Vec<&str>) {
let actual = extract_sorted_css_variables(input);
@ -311,6 +327,7 @@ mod tests {
(
r#"<div class="flex items-center px-2.5 bg-[#0088cc] text-(--my-color)"></div>"#,
vec![
"class",
"flex",
"items-center",
"px-2.5",
@ -363,7 +380,7 @@ mod tests {
("{ underline: true }", vec!["underline", "true"]),
(
r#" <CheckIcon className={clsx('h-4 w-4', { invisible: index !== 0 })} />"#,
vec!["h-4", "w-4", "invisible", "index"],
vec!["className", "h-4", "w-4", "invisible", "index"],
),
// You can have variants but in a string. Vue example.
(
@ -480,13 +497,16 @@ mod tests {
//
// HTML
// Inside a class (on its own)
(r#"<div class="{}"></div>"#, vec![]),
(r#"<div class="{}"></div>"#, vec!["class"]),
// Inside a class (first)
(r#"<div class="{} foo"></div>"#, vec!["foo"]),
(r#"<div class="{} foo"></div>"#, vec!["class", "foo"]),
// Inside a class (second)
(r#"<div class="foo {}"></div>"#, vec!["foo"]),
(r#"<div class="foo {}"></div>"#, vec!["class", "foo"]),
// Inside a class (surrounded)
(r#"<div class="foo {} bar"></div>"#, vec!["foo", "bar"]),
(
r#"<div class="foo {} bar"></div>"#,
vec!["class", "foo", "bar"],
),
// --------------------------
//
// JavaScript
@ -590,7 +610,7 @@ mod tests {
// Quoted attribute
(
r#"input(type="checkbox" class="px-2.5")"#,
vec!["checkbox", "px-2.5"],
vec!["checkbox", "class", "px-2.5"],
),
] {
assert_extract_sorted_candidates(&pre_process_input(input, "pug"), expected);
@ -611,7 +631,7 @@ mod tests {
vec!["bg-blue-100", "2xl:bg-red-100"],
),
// Quoted attribute
(r#"div class="px-2.5""#, vec!["div", "px-2.5"]),
(r#"div class="px-2.5""#, vec!["div", "class", "px-2.5"]),
] {
assert_extract_sorted_candidates(&pre_process_input(input, "slim"), expected);
}
@ -831,6 +851,25 @@ mod tests {
&pre_process_input(r#"<div class:px-4='condition'></div>"#, "svelte"),
vec!["class", "px-4", "condition"],
);
assert_extract_sorted_candidates(
&pre_process_input(r#"<div class:flex='condition'></div>"#, "svelte"),
vec!["class", "flex", "condition"],
);
}
// https://github.com/tailwindlabs/tailwindcss/issues/16999
#[test]
fn test_twig_syntax() {
assert_extract_candidates_contains(
r#"<div class="flex items-center mx-4{% if session.isValid %}{% else %} h-4{% endif %}"></div>"#,
vec!["flex", "items-center", "mx-4", "h-4"],
);
// With touching both `}` and `{`
assert_extract_candidates_contains(
r#"<div class="{% if true %}flex{% else %}block{% endif %}">"#,
vec!["flex", "block"],
);
}
// https://github.com/tailwindlabs/tailwindcss/issues/16982
@ -839,6 +878,7 @@ mod tests {
assert_extract_sorted_candidates(
r#"<div class="@md:flex @max-md:flex @-[36rem]:flex @[36rem]:flex"></div>"#,
vec![
"class",
"@md:flex",
"@max-md:flex",
"@-[36rem]:flex",
@ -852,7 +892,7 @@ mod tests {
fn test_classes_containing_number_followed_by_dash_or_underscore() {
assert_extract_sorted_candidates(
r#"<div class="text-Title1_Strong"></div>"#,
vec!["text-Title1_Strong"],
vec!["class", "text-Title1_Strong"],
);
}
@ -861,7 +901,11 @@ mod tests {
fn test_arbitrary_variable_with_data_type() {
assert_extract_sorted_candidates(
r#"<div class="bg-(length:--my-length) bg-[color:var(--my-color)]"></div>"#,
vec!["bg-(length:--my-length)", "bg-[color:var(--my-color)]"],
vec![
"class",
"bg-(length:--my-length)",
"bg-[color:var(--my-color)]",
],
);
}

View File

@ -1,6 +1,7 @@
use crate::cursor;
use crate::extractor::arbitrary_value_machine::ArbitraryValueMachine;
use crate::extractor::arbitrary_variable_machine::ArbitraryVariableMachine;
use crate::extractor::candidate_machine::is_valid_after_boundary;
use crate::extractor::machine::{Machine, MachineState};
use classification_macros::ClassifyBytes;
@ -120,19 +121,22 @@ impl Machine for NamedUtilityMachine {
// E.g.: `:div="{ flex: true }"` (JavaScript object syntax)
// ^
Class::AlphaLower | Class::AlphaUpper => {
match cursor.next.into() {
Class::Quote
| Class::Whitespace
| Class::CloseBracket
| Class::Dot
| Class::Colon
| Class::End
| Class::Slash
| Class::Exclamation => return self.done(self.start_pos, cursor),
// Still valid characters
_ => cursor.advance(),
if is_valid_after_boundary(&cursor.next) || {
// Or any of these characters
//
// - `:`, because of JS object keys
// - `/`, because of modifiers
// - `!`, because of important
matches!(
cursor.next.into(),
Class::Colon | Class::Slash | Class::Exclamation
)
} {
return self.done(self.start_pos, cursor);
}
// Still valid characters
cursor.advance()
}
Class::Dash => match cursor.next.into() {
@ -213,14 +217,20 @@ impl Machine for NamedUtilityMachine {
// ^
// E.g.: `:div="{ flex: true }"` (JavaScript object syntax)
// ^
Class::Quote
| Class::Whitespace
| Class::CloseBracket
| Class::Dot
| Class::Colon
| Class::End
| Class::Slash
| Class::Exclamation => return self.done(self.start_pos, cursor),
_ if is_valid_after_boundary(&cursor.next) || {
// Or any of these characters
//
// - `:`, because of JS object keys
// - `/`, because of modifiers
// - `!`, because of important
matches!(
cursor.next.into(),
Class::Colon | Class::Slash | Class::Exclamation
)
} =>
{
return self.done(self.start_pos, cursor)
}
// Everything else is invalid
_ => return self.restart(),
@ -454,15 +464,15 @@ mod tests {
//
// HTML
// Inside a class (on its own)
(r#"<div class="{}"></div>"#, vec!["div"]),
(r#"<div class="{}"></div>"#, vec!["div", "class"]),
// Inside a class (first)
(r#"<div class="{} foo"></div>"#, vec!["div", "foo"]),
(r#"<div class="{} foo"></div>"#, vec!["div", "class", "foo"]),
// Inside a class (second)
(r#"<div class="foo {}"></div>"#, vec!["div", "foo"]),
(r#"<div class="foo {}"></div>"#, vec!["div", "class", "foo"]),
// Inside a class (surrounded)
(
r#"<div class="foo {} bar"></div>"#,
vec!["div", "foo", "bar"],
vec!["div", "class", "foo", "bar"],
),
// --------------------------
//
@ -475,7 +485,10 @@ mod tests {
vec!["let", "classes", "true"],
),
// Inside an object (no spaces, key)
(r#"let classes = {'{}':true};"#, vec!["let", "classes"]),
(
r#"let classes = {'{}':true};"#,
vec!["let", "classes", "true"],
),
// Inside an object (value)
(
r#"let classes = { primary: '{}' };"#,

View File

@ -266,8 +266,6 @@ mod tests {
"bg-(--my-color) flex px-(--my-padding)",
vec!["bg-(--my-color)", "flex", "px-(--my-padding)"],
),
// Pug syntax
(".flex.bg-red-500", vec!["flex", "bg-red-500"]),
// --------------------------------------------------------
// Exceptions:
@ -293,15 +291,15 @@ mod tests {
//
// HTML
// Inside a class (on its own)
(r#"<div class="{}"></div>"#, vec!["div"]),
(r#"<div class="{}"></div>"#, vec!["div", "class"]),
// Inside a class (first)
(r#"<div class="{} foo"></div>"#, vec!["div", "foo"]),
(r#"<div class="{} foo"></div>"#, vec!["div", "class", "foo"]),
// Inside a class (second)
(r#"<div class="foo {}"></div>"#, vec!["div", "foo"]),
(r#"<div class="foo {}"></div>"#, vec!["div", "class", "foo"]),
// Inside a class (surrounded)
(
r#"<div class="foo {} bar"></div>"#,
vec!["div", "foo", "bar"],
vec!["div", "class", "foo", "bar"],
),
// --------------------------
//
@ -314,7 +312,10 @@ mod tests {
vec!["let", "classes", "true"],
),
// Inside an object (no spaces, key)
(r#"let classes = {'{}':true};"#, vec!["let", "classes"]),
(
r#"let classes = {'{}':true};"#,
vec!["let", "classes", "true"],
),
// Inside an object (value)
(
r#"let classes = { primary: '{}' };"#,

View File

@ -3,7 +3,6 @@ use crate::scanner::allowed_paths::resolve_paths;
use crate::scanner::detect_sources::DetectSources;
use bexpand::Expression;
use bstr::ByteSlice;
use extractor::string_machine::StringMachine;
use extractor::{Extracted, Extractor};
use fast_glob::glob_match;
use fxhash::{FxHashMap, FxHashSet};
@ -541,6 +540,7 @@ mod tests {
(
r#"<div class="!tw__flex sm:!tw__block tw__bg-gradient-to-t flex tw:[color:red] group-[]:tw__flex"#,
vec![
("class".to_string(), 5),
("!tw__flex".to_string(), 12),
("sm:!tw__block".to_string(), 22),
("tw__bg-gradient-to-t".to_string(), 36),
@ -553,6 +553,7 @@ mod tests {
(
r#"<div class="tw:flex! tw:sm:block! tw:bg-linear-to-t flex tw:[color:red] tw:in-[.tw\:group]:flex"></div>"#,
vec![
("class".to_string(), 5),
("tw:flex!".to_string(), 12),
("tw:sm:block!".to_string(), 21),
("tw:bg-linear-to-t".to_string(), 34),