Skip to content

Commit

Permalink
Fix Oxide scanner bugs (#15974)
Browse files Browse the repository at this point in the history
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
   <div
     class:underline={isUnderline}>
   </div>
   ```
- We now extract classes when using the class shortcut in Angular:
   
   ```html
   <div [class.underline]=\"bool\"></div>
   ```
- 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 <[email protected]>
Co-authored-by: Jordan Pittman <[email protected]>
  • Loading branch information
3 people authored Jan 28, 2025
1 parent e02a29f commit b492187
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 9 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion crates/oxide/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,14 @@ fn read_changed_content(c: ChangedContent) -> Option<Vec<u8>> {
};

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),
}
}
Expand Down
73 changes: 66 additions & 7 deletions crates/oxide/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)];

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -1637,7 +1653,6 @@ mod test {

#[test]
fn arbitrary_properties_are_not_picked_up_after_an_escape() {
_please_trace();
let candidates = run(
r#"
<!-- [!code word:group-has-\\[a\\]\\:block] -->
Expand All @@ -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
);
}
}
21 changes: 20 additions & 1 deletion crates/oxide/tests/scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
"<div [class.underline]=\"bool\"></div>",
),
// A svelte file with `class:foo="bar"` syntax
("index.svelte", "<div class:px-4='condition'></div>"),
("index2.svelte", "<div\n\tclass:px-5='condition'></div>"),
("index3.svelte", "<div\n class:px-6='condition'></div>"),
("index4.svelte", "<div\nclass:px-7='condition'></div>"),
])
.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"
]
);
}

Expand Down

0 comments on commit b492187

Please sign in to comment.