From 4bdc724a225c79e8eda2db369bd96ecf2b76d11b Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 26 Nov 2024 19:22:18 +0100 Subject: [PATCH] Fix scanning classes delimited by tab characters (#15169) This PR fixes an issue where multi-line candidates in Svelte files couldn't be found as reported in #15148 After digging in, the real culprit seems to be that the reproduction used tab `\t` characters instead of spaces and we only delimited explicitly on spaces. Initially I couldn't reproduce this in an integration test until we (@thecrypticace and I) realised that `\t` was being used. ## Test plan: This PR adds an integration test that fails before the fix happens. The fix itself is easy in the sense that we just use all ascii whitespace characters instead of just spaces. Fixes: #15148 --- CHANGELOG.md | 1 + crates/oxide/src/parser.rs | 106 ++++++++++++++++++++++++++++++++----- 2 files changed, 93 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcb1289fe..76dc83142 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure `.group` and `.peer` are prefixed when using the `prefix(…)` option ([#15174](https://github.com/tailwindlabs/tailwindcss/pull/15174)) - Ensure 3D transforms render correctly in Safari ([#15179](https://github.com/tailwindlabs/tailwindcss/pull/15179)) - Ensure `--spacing-*` variables take precedence over `--container-*` variables ([#15180](https://github.com/tailwindlabs/tailwindcss/pull/15180)) +- Fix scanning classes delimited by tab characters ([#15169](https://github.com/tailwindlabs/tailwindcss/pull/15169)) ## [4.0.0-beta.2] - 2024-11-22 diff --git a/crates/oxide/src/parser.rs b/crates/oxide/src/parser.rs index e49b2d21c..281774261 100644 --- a/crates/oxide/src/parser.rs +++ b/crates/oxide/src/parser.rs @@ -253,9 +253,7 @@ impl<'a> Extractor<'a> { // Reject candidates that are single camelCase words, e.g.: `useEffect` if candidate.iter().all(|c| c.is_ascii_alphanumeric()) - && candidate - .iter() - .any(|c| c.is_ascii_uppercase()) + && candidate.iter().any(|c| c.is_ascii_uppercase()) { return ValidationResult::Invalid; } @@ -570,7 +568,7 @@ impl<'a> Extractor<'a> { } }, - b' ' if !self.opts.preserve_spaces_in_arbitrary => { + c if c.is_ascii_whitespace() && !self.opts.preserve_spaces_in_arbitrary => { trace!("Arbitrary::SkipAndEndEarly\t"); if let Arbitrary::Brackets { start_idx } | Arbitrary::Parens { start_idx } = @@ -633,10 +631,8 @@ impl<'a> Extractor<'a> { match self.cursor.curr { // Enter arbitrary value mode. E.g.: `bg-[rgba(0, 0, 0)]` // ^ - b'[' if matches!( - self.cursor.prev, - b'@' | b'-' | b' ' | b':' | b'/' | b'!' | b'\0' - ) => + b'[' if matches!(self.cursor.prev, b'@' | b'-' | b':' | b'/' | b'!' | b'\0') + || self.cursor.prev.is_ascii_whitespace() => { trace!("Arbitrary::Start\t"); self.arbitrary = Arbitrary::Brackets { @@ -668,7 +664,8 @@ impl<'a> Extractor<'a> { (true, _) => ParseAction::Consume, // Looks like the end of a candidate == okay - (_, b' ' | b'\'' | b'"' | b'`') => ParseAction::Consume, + (_, b'\'' | b'"' | b'`') => ParseAction::Consume, + (_, c) if c.is_ascii_whitespace() => ParseAction::Consume, // Otherwise, not a valid character in a candidate _ => ParseAction::Skip, @@ -1542,17 +1539,98 @@ mod test { #[test] fn simple_utility_names_with_numbers_work() { + let candidates = run(r#"
"#, false); + assert_eq!(candidates, vec!["div", "class", "h2", "hz",]); + } + + #[test] + fn classes_in_an_array_without_whitespace() { let candidates = run( - r#"
"#, + "let classes = ['bg-black','hover:px-0.5','text-[13px]','[--my-var:1_/_2]','[.foo_&]:px-[0]','[.foo_&]:[color:red]']", false, ); + assert_eq!( candidates, vec![ - "div", - "class", - "h2", - "hz", + "let", + "classes", + "bg-black", + "hover:px-0.5", + "text-[13px]", + "[--my-var:1_/_2]", + "--my-var:1_/_2", + "[.foo_&]:px-[0]", + "[.foo_&]:[color:red]", + ] + ); + } + + #[test] + fn classes_in_an_array_with_spaces() { + let candidates = run( + "let classes = ['bg-black', 'hover:px-0.5', 'text-[13px]', '[--my-var:1_/_2]', '[.foo_&]:px-[0]', '[.foo_&]:[color:red]']", + false, + ); + + assert_eq!( + candidates, + vec![ + "let", + "classes", + "bg-black", + "hover:px-0.5", + "text-[13px]", + "[--my-var:1_/_2]", + "--my-var:1_/_2", + "[.foo_&]:px-[0]", + "[.foo_&]:[color:red]", + ] + ); + } + + #[test] + fn classes_in_an_array_with_tabs() { + let candidates = run( + "let classes = ['bg-black',\t'hover:px-0.5',\t'text-[13px]',\t'[--my-var:1_/_2]',\t'[.foo_&]:px-[0]',\t'[.foo_&]:[color:red]']", + false, + ); + + assert_eq!( + candidates, + vec![ + "let", + "classes", + "bg-black", + "hover:px-0.5", + "text-[13px]", + "[--my-var:1_/_2]", + "--my-var:1_/_2", + "[.foo_&]:px-[0]", + "[.foo_&]:[color:red]", + ] + ); + } + + #[test] + fn classes_in_an_array_with_newlines() { + let candidates = run( + "let classes = [\n'bg-black',\n'hover:px-0.5',\n'text-[13px]',\n'[--my-var:1_/_2]',\n'[.foo_&]:px-[0]',\n'[.foo_&]:[color:red]'\n]", + false, + ); + + assert_eq!( + candidates, + vec![ + "let", + "classes", + "bg-black", + "hover:px-0.5", + "text-[13px]", + "[--my-var:1_/_2]", + "--my-var:1_/_2", + "[.foo_&]:px-[0]", + "[.foo_&]:[color:red]", ] ); }