Skip to content

Commit b492187

Browse files
philipp-spiessRobinMalfaitthecrypticace
authored
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 <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]>
1 parent e02a29f commit b492187

File tree

4 files changed

+97
-9
lines changed

4 files changed

+97
-9
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2222
- Disable bare value suggestions when not using the `--spacing` variable ([#15857](https://github.com/tailwindlabs/tailwindcss/pull/15857))
2323
- Ensure suggested classes are properly sorted ([#15857](https://github.com/tailwindlabs/tailwindcss/pull/15857))
2424
- Don’t look at ignore files outside initialized repos ([#15941](https://github.com/tailwindlabs/tailwindcss/pull/15941))
25+
- Find utilities when using the Svelte class shorthand syntax across multiple lines ([#15974](https://github.com/tailwindlabs/tailwindcss/pull/15974))
26+
- Find utilities when using the Angular class shorthand syntax ([#15974](https://github.com/tailwindlabs/tailwindcss/pull/15974))
27+
- Find utilities when using functions inside arrays ([#15974](https://github.com/tailwindlabs/tailwindcss/pull/15974))
2528
- _Upgrade_: Ensure JavaScript config files on different drives are correctly migrated ([#15927](https://github.com/tailwindlabs/tailwindcss/pull/15927))
2629

2730
## [4.0.0] - 2025-01-21

crates/oxide/src/lib.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,14 @@ fn read_changed_content(c: ChangedContent) -> Option<Vec<u8>> {
447447
};
448448

449449
match extension {
450-
Some("svelte") => Some(content.replace(" class:", " ")),
450+
// Angular class shorthand
451+
Some("html") => Some(content.replace("[class.", "[")),
452+
Some("svelte") => Some(
453+
content
454+
.replace(" class:", " ")
455+
.replace("\tclass:", " ")
456+
.replace("\nclass:", " "),
457+
),
451458
_ => Some(content),
452459
}
453460
}

crates/oxide/src/parser.rs

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,26 @@ impl<'a> Extractor<'a> {
334334
return ValidationResult::Restart;
335335
}
336336

337+
// Only allow parentheses for the shorthand arbitrary custom properties syntax
338+
if let Some(index) = utility.find(b"(") {
339+
let mut skip_parens_check = false;
340+
let start_brace_index = utility.find(b"[");
341+
let end_brace_index = utility.find(b"]");
342+
343+
match (start_brace_index, end_brace_index) {
344+
(Some(start_brace_index), Some(end_brace_index)) => {
345+
if start_brace_index < index && end_brace_index > index {
346+
skip_parens_check = true;
347+
}
348+
}
349+
_ => {}
350+
}
351+
352+
if !skip_parens_check && !utility[index + 1..].starts_with(b"--") {
353+
return ValidationResult::Restart;
354+
}
355+
}
356+
337357
// Pluck out the part that we are interested in.
338358
let utility = &utility[offset..(utility.len() - offset_end)];
339359

@@ -911,9 +931,6 @@ impl<'a> Extractor<'a> {
911931
fn generate_slices(&mut self, candidate: &'a [u8]) -> ParseAction<'a> {
912932
match self.without_surrounding() {
913933
Bracketing::None => ParseAction::SingleCandidate(candidate),
914-
Bracketing::Included(sliceable) if sliceable == candidate => {
915-
ParseAction::SingleCandidate(candidate)
916-
}
917934
Bracketing::Included(sliceable) | Bracketing::Wrapped(sliceable) => {
918935
if candidate == sliceable {
919936
ParseAction::SingleCandidate(candidate)
@@ -1117,7 +1134,7 @@ mod test {
11171134
assert_eq!(candidates, vec!["something"]);
11181135

11191136
let candidates = run(" [feature(slice_as_chunks)]", false);
1120-
assert_eq!(candidates, vec!["feature(slice_as_chunks)"]);
1137+
assert_eq!(candidates, vec!["feature", "slice_as_chunks"]);
11211138

11221139
let candidates = run("![feature(slice_as_chunks)]", false);
11231140
assert!(candidates.is_empty());
@@ -1213,9 +1230,8 @@ mod test {
12131230

12141231
#[test]
12151232
fn ignores_arbitrary_property_ish_things() {
1216-
// FIXME: () are only valid in an arbitrary
12171233
let candidates = run(" [feature(slice_as_chunks)]", false);
1218-
assert_eq!(candidates, vec!["feature(slice_as_chunks)",]);
1234+
assert_eq!(candidates, vec!["feature", "slice_as_chunks",]);
12191235
}
12201236

12211237
#[test]
@@ -1637,7 +1653,6 @@ mod test {
16371653

16381654
#[test]
16391655
fn arbitrary_properties_are_not_picked_up_after_an_escape() {
1640-
_please_trace();
16411656
let candidates = run(
16421657
r#"
16431658
<!-- [!code word:group-has-\\[a\\]\\:block] -->
@@ -1648,4 +1663,48 @@ mod test {
16481663

16491664
assert_eq!(candidates, vec!["!code", "a"]);
16501665
}
1666+
1667+
#[test]
1668+
fn test_find_candidates_in_braces_inside_brackets() {
1669+
let candidates = run(
1670+
r#"
1671+
const classes = [wrapper("bg-red-500")]
1672+
"#,
1673+
false,
1674+
);
1675+
1676+
assert_eq!(
1677+
candidates,
1678+
vec!["const", "classes", "wrapper", "bg-red-500"]
1679+
);
1680+
}
1681+
1682+
#[test]
1683+
fn test_is_valid_candidate_string() {
1684+
assert_eq!(
1685+
Extractor::is_valid_candidate_string(b"foo"),
1686+
ValidationResult::Valid
1687+
);
1688+
assert_eq!(
1689+
Extractor::is_valid_candidate_string(b"foo-(--color-red-500)"),
1690+
ValidationResult::Valid
1691+
);
1692+
assert_eq!(
1693+
Extractor::is_valid_candidate_string(b"bg-[url(foo)]"),
1694+
ValidationResult::Valid
1695+
);
1696+
assert_eq!(
1697+
Extractor::is_valid_candidate_string(b"group-foo/(--bar)"),
1698+
ValidationResult::Valid
1699+
);
1700+
1701+
assert_eq!(
1702+
Extractor::is_valid_candidate_string(b"foo(\"bg-red-500\")"),
1703+
ValidationResult::Restart
1704+
);
1705+
assert_eq!(
1706+
Extractor::is_valid_candidate_string(b"foo-("),
1707+
ValidationResult::Restart
1708+
);
1709+
}
16511710
}

crates/oxide/tests/scanner.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,14 +323,33 @@ mod scanner {
323323
("foo.jpg", "xl:font-bold"),
324324
// A file that is ignored
325325
("foo.html", "lg:font-bold"),
326+
// An Angular file using the class shorthand syntax
327+
(
328+
"index.angular.html",
329+
"<div [class.underline]=\"bool\"></div>",
330+
),
326331
// A svelte file with `class:foo="bar"` syntax
327332
("index.svelte", "<div class:px-4='condition'></div>"),
333+
("index2.svelte", "<div\n\tclass:px-5='condition'></div>"),
334+
("index3.svelte", "<div\n class:px-6='condition'></div>"),
335+
("index4.svelte", "<div\nclass:px-7='condition'></div>"),
328336
])
329337
.1;
330338

331339
assert_eq!(
332340
candidates,
333-
vec!["condition", "div", "font-bold", "md:flex", "px-4"]
341+
vec![
342+
"bool",
343+
"condition",
344+
"div",
345+
"font-bold",
346+
"md:flex",
347+
"px-4",
348+
"px-5",
349+
"px-6",
350+
"px-7",
351+
"underline"
352+
]
334353
);
335354
}
336355

0 commit comments

Comments
 (0)