From b492187c493d0b02e3a5e57f501c3882c9b01a6f Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 28 Jan 2025 17:19:31 +0100 Subject: [PATCH] Fix Oxide scanner bugs (#15974) Fixes #15632 Fixes #15740 This PR fixes a number of Oxide scanner bugs reported over various channels, specifically: - When using the Svelte class shorthand split over various lines, we weren't extracting class names properly: ```svelte
``` - We now extract classes when using the class shortcut in Angular: ```html
``` - We now validate parentheses within arbitrary candidates so that we don't consume invalid arbitrary candidates anymore which allows us to parse the following case properly: ```js const classes = [wrapper("bg-red-500")] ``` ## Test plan Added unit tests --------- Co-authored-by: Robin Malfait Co-authored-by: Jordan Pittman --- CHANGELOG.md | 3 ++ crates/oxide/src/lib.rs | 9 ++++- crates/oxide/src/parser.rs | 73 +++++++++++++++++++++++++++++++---- crates/oxide/tests/scanner.rs | 21 +++++++++- 4 files changed, 97 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cd3ea0ad8eb..067afcbab4f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Disable bare value suggestions when not using the `--spacing` variable ([#15857](https://github.com/tailwindlabs/tailwindcss/pull/15857)) - Ensure suggested classes are properly sorted ([#15857](https://github.com/tailwindlabs/tailwindcss/pull/15857)) - Don’t look at ignore files outside initialized repos ([#15941](https://github.com/tailwindlabs/tailwindcss/pull/15941)) +- Find utilities when using the Svelte class shorthand syntax across multiple lines ([#15974](https://github.com/tailwindlabs/tailwindcss/pull/15974)) +- Find utilities when using the Angular class shorthand syntax ([#15974](https://github.com/tailwindlabs/tailwindcss/pull/15974)) +- Find utilities when using functions inside arrays ([#15974](https://github.com/tailwindlabs/tailwindcss/pull/15974)) - _Upgrade_: Ensure JavaScript config files on different drives are correctly migrated ([#15927](https://github.com/tailwindlabs/tailwindcss/pull/15927)) ## [4.0.0] - 2025-01-21 diff --git a/crates/oxide/src/lib.rs b/crates/oxide/src/lib.rs index 4631cabc843c..557e87ef5f78 100644 --- a/crates/oxide/src/lib.rs +++ b/crates/oxide/src/lib.rs @@ -447,7 +447,14 @@ fn read_changed_content(c: ChangedContent) -> Option> { }; match extension { - Some("svelte") => Some(content.replace(" class:", " ")), + // Angular class shorthand + Some("html") => Some(content.replace("[class.", "[")), + Some("svelte") => Some( + content + .replace(" class:", " ") + .replace("\tclass:", " ") + .replace("\nclass:", " "), + ), _ => Some(content), } } diff --git a/crates/oxide/src/parser.rs b/crates/oxide/src/parser.rs index d6d2df1433c3..12791ae780ac 100644 --- a/crates/oxide/src/parser.rs +++ b/crates/oxide/src/parser.rs @@ -334,6 +334,26 @@ impl<'a> Extractor<'a> { return ValidationResult::Restart; } + // Only allow parentheses for the shorthand arbitrary custom properties syntax + if let Some(index) = utility.find(b"(") { + let mut skip_parens_check = false; + let start_brace_index = utility.find(b"["); + let end_brace_index = utility.find(b"]"); + + match (start_brace_index, end_brace_index) { + (Some(start_brace_index), Some(end_brace_index)) => { + if start_brace_index < index && end_brace_index > index { + skip_parens_check = true; + } + } + _ => {} + } + + if !skip_parens_check && !utility[index + 1..].starts_with(b"--") { + return ValidationResult::Restart; + } + } + // Pluck out the part that we are interested in. let utility = &utility[offset..(utility.len() - offset_end)]; @@ -911,9 +931,6 @@ impl<'a> Extractor<'a> { fn generate_slices(&mut self, candidate: &'a [u8]) -> ParseAction<'a> { match self.without_surrounding() { Bracketing::None => ParseAction::SingleCandidate(candidate), - Bracketing::Included(sliceable) if sliceable == candidate => { - ParseAction::SingleCandidate(candidate) - } Bracketing::Included(sliceable) | Bracketing::Wrapped(sliceable) => { if candidate == sliceable { ParseAction::SingleCandidate(candidate) @@ -1117,7 +1134,7 @@ mod test { assert_eq!(candidates, vec!["something"]); let candidates = run(" [feature(slice_as_chunks)]", false); - assert_eq!(candidates, vec!["feature(slice_as_chunks)"]); + assert_eq!(candidates, vec!["feature", "slice_as_chunks"]); let candidates = run("![feature(slice_as_chunks)]", false); assert!(candidates.is_empty()); @@ -1213,9 +1230,8 @@ mod test { #[test] fn ignores_arbitrary_property_ish_things() { - // FIXME: () are only valid in an arbitrary let candidates = run(" [feature(slice_as_chunks)]", false); - assert_eq!(candidates, vec!["feature(slice_as_chunks)",]); + assert_eq!(candidates, vec!["feature", "slice_as_chunks",]); } #[test] @@ -1637,7 +1653,6 @@ mod test { #[test] fn arbitrary_properties_are_not_picked_up_after_an_escape() { - _please_trace(); let candidates = run( r#" @@ -1648,4 +1663,48 @@ mod test { assert_eq!(candidates, vec!["!code", "a"]); } + + #[test] + fn test_find_candidates_in_braces_inside_brackets() { + let candidates = run( + r#" + const classes = [wrapper("bg-red-500")] + "#, + false, + ); + + assert_eq!( + candidates, + vec!["const", "classes", "wrapper", "bg-red-500"] + ); + } + + #[test] + fn test_is_valid_candidate_string() { + assert_eq!( + Extractor::is_valid_candidate_string(b"foo"), + ValidationResult::Valid + ); + assert_eq!( + Extractor::is_valid_candidate_string(b"foo-(--color-red-500)"), + ValidationResult::Valid + ); + assert_eq!( + Extractor::is_valid_candidate_string(b"bg-[url(foo)]"), + ValidationResult::Valid + ); + assert_eq!( + Extractor::is_valid_candidate_string(b"group-foo/(--bar)"), + ValidationResult::Valid + ); + + assert_eq!( + Extractor::is_valid_candidate_string(b"foo(\"bg-red-500\")"), + ValidationResult::Restart + ); + assert_eq!( + Extractor::is_valid_candidate_string(b"foo-("), + ValidationResult::Restart + ); + } } diff --git a/crates/oxide/tests/scanner.rs b/crates/oxide/tests/scanner.rs index e2bfdf37e558..e14bfc7316f7 100644 --- a/crates/oxide/tests/scanner.rs +++ b/crates/oxide/tests/scanner.rs @@ -323,14 +323,33 @@ mod scanner { ("foo.jpg", "xl:font-bold"), // A file that is ignored ("foo.html", "lg:font-bold"), + // An Angular file using the class shorthand syntax + ( + "index.angular.html", + "
", + ), // A svelte file with `class:foo="bar"` syntax ("index.svelte", "
"), + ("index2.svelte", ""), + ("index3.svelte", ""), + ("index4.svelte", ""), ]) .1; assert_eq!( candidates, - vec!["condition", "div", "font-bold", "md:flex", "px-4"] + vec![ + "bool", + "condition", + "div", + "font-bold", + "md:flex", + "px-4", + "px-5", + "px-6", + "px-7", + "underline" + ] ); }