Skip to content

feat(useSortedClasses): scaffold v4 parser-based sort module#10291

Open
jiwon79 wants to merge 23 commits into
biomejs:mainfrom
jiwon79:feat/tailwind-v4-utility-sort
Open

feat(useSortedClasses): scaffold v4 parser-based sort module#10291
jiwon79 wants to merge 23 commits into
biomejs:mainfrom
jiwon79:feat/tailwind-v4-utility-sort

Conversation

@jiwon79
Copy link
Copy Markdown
Contributor

@jiwon79 jiwon79 commented May 7, 2026

Claude Code assist to writing code. I decided all about it and review it

Summary

Related to #1274.

Sort Module

Adds an internal sort module sort_v4 that using biome_tailwind_parser instead of the existing class lexer.

Current logic cover only pure utilities and negative value. (e.g. flex, bg-red-500, -mt-2)
Out of scope (each tagged with a // TODO: comment in sort_v4.rs and it will be covered on next PR):

  • Variants (hover:, sm:, [&:hover]:)
  • Important suffix (flex!)
  • Modifiers (/50)
  • Bracketed arbitrary values (p-[10px], bg-[#abc])
  • CSS variable values (bg-(--my-color))
  • Arbitrary CSS ([mask:none])

Codegen

Tailwind v4 sort standard is complied css property from classname unlike v3 standard is only tailwind classname. A new TS codegen extracted the v4 preset, which is the information needed to match tailwind class to css-properties. (PROPERTY_ORDER, STATIC_UTILITIES, FUNCTIONAL_UTILITIES with Branch list, theme-namespace key sets)

The 17 v4 value-type predicates (is_color, is_length, is_ratio, …) are ported 1:1 from Tailwind's infer-data-type.ts into predicates.rs, each with a source-link comment.

Test Plan

  • crates/biome_js_analyze/tests/sort_v4_test.rs runs every entry in tests/sort_v4/cases.jsonc
  • Unit test in predicates.rs cover Color / Length / Ratio / Position / BgSize / LineWidth / Vector

Docs

jiwon79 added 10 commits May 6, 2026 12:55
Generates `tailwind_preset_v4.rs` by probing a v4 design system at codegen
time (884 static + 311 functional utilities, 354 property-order entries,
18 theme namespace key sets). The Rust file is currently unwired; value
predicates and sort logic land next.
Emits each `FunctionalEntry` on multiple lines (one branch per line) and
orders branches by resolve precedence (NamedKeyword → Named → NamedTyped
→ Typed → Arbitrary) so the generated table reads top-to-bottom in the
order the runtime lookup will try.
…rated preset

Drops the `use ValueType::*; use ThemeNamespace::*;` re-exports; both
enums share variant names (e.g. `Color`), so glob-importing them caused
ambiguous-name + type-mismatch errors. The generator now emits fully
qualified `ValueType::<X>` and `ThemeNamespace::<X>` paths.
Adds `predicates.rs` with 17 CSS value-type checks (`is_color`,
`is_length`, `is_percentage`, ...) ported 1:1 from Tailwind v4.2.2.
Uses `LazyLock<Regex>` for the seven anchored numeric patterns,
`phf::Set` for the 169 named CSS colors, a custom `Segments` iterator
backed by `SmallVec` for top-level CSS splitting (no heap alloc on
hot paths), and a 32-byte stack buffer for case-insensitive named-color
lookup. Each predicate carries a doc-link to the matching Tailwind
function. Wires up `predicates` and `tailwind_preset_v4` in the rule's
mod tree and adds `phf` to the crate's workspace dependencies.

Inline tests cover each predicate (16 cases, all passing).
Adds sort_v4::sort_class_list(&TwRoot) -> String as a parser-based
passthrough stub, paired with a jsonc fixture and insta::glob! runner.
The real v4 sort algorithm will replace the body in a follow-up.
…enericName

- Branch::Typed → Branch::ArbitraryTyped to make the bracketed dispatch / fallback
  pair (ArbitraryTyped / Arbitrary) read as a parallel family.
- Drop ValueType::FamilyName and ::GenericName: their probes always dedupe
  against the Arbitrary fallback for `font-` (both route to font-family),
  so the variants and predicates were unreachable.
- Replace #![allow(dead_code)] with #[expect(dead_code, reason = ...)] to
  satisfy clippy::allow_attributes; tighten &'static str redundancy.
- Replace free `classify` with `SortKey::from_candidate(&AnyTwFullCandidate)`
  so the Bogus and Full arms collapse into one method and the caller no
  longer rebuilds a SortKey from a tuple.
- Replace `resolve_value -> Option<(u16, u8)>` with
  `resolve_branch -> Option<SortKey>`, threading `registration_idx`
  through so the helper returns a complete sort key.
- Pre-size the `Vec` and the output `String` so neither reallocates
  during a single sort pass.
- Use `.contains` and `usize::from(pool_idx)` to drop the `as` cast and
  the redundant `iter().any(...)`.
- Extend the fixture with font / w / aspect bare-value cases that
  exercise Named, NamedKeyword, and NamedTyped(Number/Ratio).
…sts/

Match Biome's convention of keeping integration tests under `tests/`.
The runner now lives in `tests/sort_v4_test.rs` and reads fixtures from
`tests/sort_v4/`, with snapshots emitted flat next to the fixture
(`cases.jsonc` → `cases.snap`). The inline `#[cfg(test)]` block under
`src/` is removed.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 7, 2026

🦋 Changeset detected

Latest commit: ccbccc5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
Name Type
@biomejs/biome Patch
@biomejs/cli-win32-x64 Patch
@biomejs/cli-win32-arm64 Patch
@biomejs/cli-darwin-x64 Patch
@biomejs/cli-darwin-arm64 Patch
@biomejs/cli-linux-x64 Patch
@biomejs/cli-linux-arm64 Patch
@biomejs/cli-linux-x64-musl Patch
@biomejs/cli-linux-arm64-musl Patch
@biomejs/wasm-web Patch
@biomejs/wasm-bundler Patch
@biomejs/wasm-nodejs Patch
@biomejs/backend-jsonrpc Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added A-Linter Area: linter L-JavaScript Language: JavaScript and super languages labels May 7, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR adds Tailwind v4 support: a TypeScript pipeline that extracts v4 property order, theme keys and utilities and renders generated Rust preset source; Rust-side adds nesting-aware CSS value predicates, a v4 sort implementation computing stable SortKeys (unknowns preserved at front), Cargo wiring for new parser/syntax crates, and snapshot-driven tests with fixtures.

Suggested labels

A-Parser, A-Tooling, L-CSS

Suggested reviewers

  • ematipico
  • dyc3
  • Netail
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly summarises the main change: scaffolding a v4 parser-based sort module for useSortedClasses, which aligns with the substantial additions across Rust and TypeScript codegen.
Description check ✅ Passed The description is well-related to the changeset, covering the sort module scope, out-of-scope items (marked TODO), codegen extraction, value-type predicates, and test coverage. It also discloses AI assistance as required.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
crates/biome_js_analyze/tests/sort_v4_test.rs (1)

13-15: 💤 Low value

Consider surfacing parse errors in test output

parse_tailwind parse errors are silently ignored — the sorted output could be partially or fully derived from an error-recovered tree. Even just including parsed.diagnostics() in the rendered snapshot would make future debugging much easier.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/biome_js_analyze/tests/sort_v4_test.rs` around lines 13 - 15, The test
currently ignores parse errors from parse_tailwind and may assert against an
error-recovered tree; change the test to include parse diagnostics in the
snapshot output by appending parsed.diagnostics() (or formatted diagnostics) to
the rendered string alongside the existing parsed.tree()/sorted result, so when
calling parse_tailwind and sort_class_list you render something like the input,
parsed.diagnostics(), and sorted to make parse failures visible during test
failures.
packages/tailwindcss-config-analyzer/src/v4/extract-property-order.ts (1)

13-13: 💤 Low value

Anchor fragility across Tailwind versions

"padding","padding-inline" is positional — if Tailwind ever reorders or renames these adjacent entries, the extractor silently fails with the "not found" error rather than a graceful degradation message. Consider documenting the expected Tailwind version range (or adding a version check) so the failure is obviously actionable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/tailwindcss-config-analyzer/src/v4/extract-property-order.ts` at
line 13, The hardcoded ANCHOR constant ("padding","padding-inline") is fragile
across Tailwind versions; change the extractor to first try a tolerant search
for the anchor by locating "padding" and then searching nearby for
"padding-inline" (or accept either key independently), fall back to a clear,
actionable error that includes the detected Tailwind version or a documented
expected version range, and update any code that references ANCHOR (the ANCHOR
constant and the extraction logic in extract-property-order.ts) to use the new
tolerant lookup and explicit version-check/fallback message.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/predicates.rs`:
- Around line 469-490: The is_bg_size function currently ignores invalid
comma-separated segments (e.g., "cover, foo") instead of failing; update the
loop in is_bg_size so that any invalid segment causes an immediate false return:
for each comma-segment (in function is_bg_size), trim and if it's "cover" or
"contain" increment count; otherwise collect tokens into SmallVec as currently
done, and if tokens.len() is not 1 or 2 return false; then if the tokens are all
valid (auto, is_length, or is_percentage) increment count else return false;
keep final return as count > 0.

In `@packages/tailwindcss-config-analyzer/package.json`:
- Line 15: The README/header comment in generate-tailwind-preset-v4.ts and the
package.json script are inconsistent: package.json defines the npm script
"execute:v4" but the file instructs users to run "gen:tailwind-preset-v4";
update one to match the other — either rename the script in package.json to
"gen:tailwind-preset-v4" (keeping the command "tsx
src/v4/generate-tailwind-preset-v4.ts") or change the comment in
generate-tailwind-preset-v4.ts to instruct running "pnpm --filter
tailwindcss-config-analyzer execute:v4", and ensure the chosen name is used
consistently across docs and scripts.

---

Nitpick comments:
In `@crates/biome_js_analyze/tests/sort_v4_test.rs`:
- Around line 13-15: The test currently ignores parse errors from parse_tailwind
and may assert against an error-recovered tree; change the test to include parse
diagnostics in the snapshot output by appending parsed.diagnostics() (or
formatted diagnostics) to the rendered string alongside the existing
parsed.tree()/sorted result, so when calling parse_tailwind and sort_class_list
you render something like the input, parsed.diagnostics(), and sorted to make
parse failures visible during test failures.

In `@packages/tailwindcss-config-analyzer/src/v4/extract-property-order.ts`:
- Line 13: The hardcoded ANCHOR constant ("padding","padding-inline") is fragile
across Tailwind versions; change the extractor to first try a tolerant search
for the anchor by locating "padding" and then searching nearby for
"padding-inline" (or accept either key independently), fall back to a clear,
actionable error that includes the detected Tailwind version or a documented
expected version range, and update any code that references ANCHOR (the ANCHOR
constant and the extraction logic in extract-property-order.ts) to use the new
tolerant lookup and explicit version-check/fallback message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 680776e2-f235-47d8-b40f-787bbb6ff128

📥 Commits

Reviewing files that changed from the base of the PR and between df96a57 and bf5d27f.

⛔ Files ignored due to path filters (3)
  • Cargo.lock is excluded by !**/*.lock and included by **
  • crates/biome_js_analyze/tests/sort_v4/cases.snap is excluded by !**/*.snap and included by **
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml and included by **
📒 Files selected for processing (17)
  • crates/biome_js_analyze/Cargo.toml
  • crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/predicates.rs
  • crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/sort_v4.rs
  • crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/tailwind_preset_v4.rs
  • crates/biome_js_analyze/tests/sort_v4/cases.jsonc
  • crates/biome_js_analyze/tests/sort_v4_test.rs
  • packages/tailwindcss-config-analyzer/package.json
  • packages/tailwindcss-config-analyzer/src/v4/css-helpers.ts
  • packages/tailwindcss-config-analyzer/src/v4/extract-property-order.ts
  • packages/tailwindcss-config-analyzer/src/v4/extract-theme-keys.ts
  • packages/tailwindcss-config-analyzer/src/v4/extract-utilities.ts
  • packages/tailwindcss-config-analyzer/src/v4/generate-tailwind-preset-v4.ts
  • packages/tailwindcss-config-analyzer/src/v4/probe-samples.ts
  • packages/tailwindcss-config-analyzer/src/v4/render-rust.ts
  • packages/tailwindcss-config-analyzer/src/v4/theme-namespaces.ts
  • packages/tailwindcss-config-analyzer/src/v4/value-types.ts

Comment thread crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/predicates.rs Outdated
Comment thread packages/tailwindcss-config-analyzer/package.json
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 7, 2026

Merging this PR will not alter performance

✅ 249 untouched benchmarks


Comparing jiwon79:feat/tailwind-v4-utility-sort (ccbccc5) with main (9dd3271)

Open in CodSpeed

jiwon79 added 2 commits May 7, 2026 17:29
…g-size note

- Rename the codegen instructions in generate-tailwind-preset-v4.ts and
  the generated tailwind_preset_v4.rs header to use the actual script
  name `execute:v4`, matching `package.json`.
- Note in `is_bg_size` that the partial-valid acceptance is intentional
  and mirrors Tailwind v4's `isBackgroundSize`, to forestall reviews that
  treat it as a bug.
Copy link
Copy Markdown
Contributor

@dyc3 dyc3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking much better overall. Nice job wrangling the agent to avoid heap allocations.

I'm seeing some things that we probably need to improve the parser for. If you could help me extract those requirements, I can work on the parser to get it up to snuff

Comment thread crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/predicates.rs Outdated
Comment on lines +1441 to +1458
"-inset" => FunctionalEntry {
registration_idx: 0,
branches: &[
Named(ThemeNamespace::Spacing, 4, 1),
NamedTyped(ValueType::Number, 4, 1),
NamedTyped(ValueType::Ratio, 4, 1),
Arbitrary(4, 1),
],
},
"inset" => FunctionalEntry {
registration_idx: 1,
branches: &[
Named(ThemeNamespace::Spacing, 4, 1),
NamedTyped(ValueType::Number, 4, 1),
NamedTyped(ValueType::Ratio, 4, 1),
Arbitrary(4, 1),
],
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

notice how these entries are effectively the same. tailwind doesn't really parse the negative - at the front of candidates. our tailwind parser does.

for example: -inset and inset to be 2 different classes. however, our tailwind parser considers them to be the same inset class, and detects the - as a negative candidate.

Copy link
Copy Markdown
Contributor Author

@jiwon79 jiwon79 May 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two reasons why negative values ​​are assigned separately.

  1. It allows distinguishing between classes that can be negative and those that cannot. By differentiating that bg-red-500 compiles while -bg-red-500 does not, uncompiled items can be moved to the front during sorting.
  2. Most negative classes share the same branch as positive classes, but some differ. The z class does not accept a NamedKeyword (e.g., auto) when negative. bg-linear has an Arbitray branch when positive but not when negative, and like z, it does not have a NamedKeyword too.
    "-z" => FunctionalEntry {
        registration_idx: 22,
        branches: &[
            NamedTyped(ValueType::Number, 16, 1),
            Arbitrary(16, 1),
        ],
    },
    "z" => FunctionalEntry {
        registration_idx: 23,
        branches: &[
            NamedKeyword(0, 16, 1),
            NamedTyped(ValueType::Number, 16, 1),
            Arbitrary(16, 1),
        ],
    },

    "-bg-linear" => FunctionalEntry {
        registration_idx: 200,
        branches: &[
            NamedTyped(ValueType::Number, 196, 3),
            NamedTyped(ValueType::Ratio, 196, 3),
            ArbitraryTyped(ValueType::Angle, 196, 2),
        ],
    },
    "bg-linear" => FunctionalEntry {
        registration_idx: 201,
        branches: &[
            NamedKeyword(12, 196, 3),
            NamedTyped(ValueType::Number, 196, 3),
            NamedTyped(ValueType::Ratio, 196, 3),
            Arbitrary(196, 2),
        ],
    },

Out of 311 Functional Utilities, 70 are negative, and among them, only 10 have a different branch from the positive ones. Therefore, I could omit the duplicates and list only the different classes separately. What do you think is the best way to handle this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my main concern is that you have to do string allocations to look up entries in this map, because you have to concatenate the - and the base name.

deduplicating the entries sounds like it would also avoid that problem. so yeah, sounds good.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I change preset structure to nested. This structure help to avoid string allocation and deduplicate 7b5d63d

Comment thread crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/sort_v4.rs Outdated
Comment thread crates/biome_js_analyze/tests/sort_v4/cases.snap
Comment on lines +66 to +78
const MATH_FUNCTIONS: &[&str] = &[
"calc(", "min(", "max(", "clamp(", "mod(", "rem(", "sin(", "cos(", "tan(", "asin(", "acos(",
"atan(", "atan2(", "pow(", "sqrt(", "hypot(", "log(", "exp(", "round(",
];

/// Mirrors <https://github.com/tailwindlabs/tailwindcss/blob/v4.2.2/packages/tailwindcss/src/utils/math-operators.ts#L41>
/// (`hasMathFn`).
fn has_math_fn(input: &str) -> bool {
if !input.contains('(') {
return false;
}
MATH_FUNCTIONS.iter().any(|f| input.contains(f))
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to me, this indicates a deficiency in the tailwind parser. what's the context in which these can be used? probably arbitrary values?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this function is used by inferring data type at arbitrary value. This is because the CSS-property changes depending on the type of the arbitrary value in some class (e.g. bg-[#abc] -> background-color: #abc / bg-[10px] -> background-size: 10px / bg-[25%] -> background-position: 25%).

+) Since there are often arbitrary fallbacks anyway, it seems like syntax accuracy wasn't a major concern.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've opened #10299 to have the parser handle these values more precisely

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will follow up to simplify logic after the parser change merges!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its merged

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, exactly what I had in mind.
However I think treating everything as a modifier seems better than adding a Radio token Because Is is possible where Number/Number is treated as a modifier, not a ratio in some case.

text-{font-size}/{line-height} is the case. Default font-size key is string(e.g. lg, xl), but when the user defines a numeral key such as --text-12:12px, then user write class text-12/4. In this case, 12/4 is not ratio token. It is font-size: 12px; line-heigt:1rem

Therefore how about not adding ratio token, just treating modifier? If NamedTyped branch has ratio case, determine toke is valid by check modifier and value like tailwind.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oof yeah i was kinda on the fence about the ratio type. I agree. I think we'll need it eventually (because it is a distinct type in tailwind's semantics), but for now I'll get rid of it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, If the PR merged, I really will remove all about predicate code!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merged 👍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I deleted predicate code and update code to using parsed value

Comment thread crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/predicates.rs Outdated
Comment thread crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/predicates.rs Outdated
@jiwon79 jiwon79 marked this pull request as draft May 7, 2026 14:07
…cates

Replace the eight `LazyLock<Regex>` statics with byte-level scanners
(number / percentage / ratio / length / angle / vector) and prefix-array
matchers (gradient / image / color functions). Behaviour stays a 1:1
mirror of Tailwind v4.2.2's `infer-data-type` patterns; the existing 30
unit tests cover the edge cases. This addresses the reviewer note that
regex usage is discouraged in this crate's hot paths.
jiwon79 added 5 commits May 8, 2026 19:54
Match what IDE folding (rust-analyzer / VSCode) recognises so the
predicate file's logical sections collapse cleanly. Address review nit
on predicates.rs section markers.
Cover the three internal contracts that the snapshot fixture cannot
isolate:
- compare ordering rules (Unknown < Known, property_idx asc,
  property_count desc, registration_idx asc, equal-key invariance).
- resolve_branch dispatch (first match wins, ArbitraryTyped/Arbitrary
  skipped for bare values, registration_idx threading, no-match path).
- sort_class_list edge cases (empty input, whitespace-only).

Tests are grouped under collapsible region/endregion markers.
Looking up `-<basename>` by reconstructing the dashed key would force a
heap allocation per token; merging negatives into the positive entry
keeps the lookup at a single alloc-free phf hit.

Functional side gains an `Option<Negative>`:
- `None`: utility rejects negation (`bg`, `flex`, ...).
- `Some(SameBranches { registration_idx })`: 60 of 70 cases — the
  negative form reuses the positive's branch list and only needs its
  own sort tie-break index.
- `Some(Distinct { registration_idx, branches })`: the 10 cases that
  diverge structurally (`-z`/`-order`/... drop the `auto`/`first`
  NamedKeyword; `-bg-linear` keeps only `ArbitraryTyped(Angle)`).

Static side gains `negative_registration_idx: Option<u16>` for the
sign-paired statics (`inset-px`/`-inset-px`, ...).
…ndidates

Drop the early-skip on `negative_token` and route through the new
preset metadata:
- Functional candidates match `entry.negative` to pick the correct
  (registration_idx, branches) slot, falling back to `Unknown` when
  the utility rejects negation (`-bg-red-500`, `-text-lg`) or when the
  Distinct branch list no longer accepts the value (`-z-auto`).
- Static candidates use `entry.negative_registration_idx` to keep the
  negative form's own tie-break key. `None` reduces to `Unknown` —
  exercised by `-flex` (Tailwind never registers it).

Fixture grows by 7 cases that intentionally jumble input order so the
new sort paths are observable in the snapshot.
@jiwon79 jiwon79 marked this pull request as ready for review May 8, 2026 13:14
Comment thread crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/predicates.rs Outdated
Comment thread crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/predicates.rs Outdated
Comment thread crates/biome_js_analyze/src/lint/nursery/use_sorted_classes/predicates.rs Outdated
Comment on lines +66 to +78
const MATH_FUNCTIONS: &[&str] = &[
"calc(", "min(", "max(", "clamp(", "mod(", "rem(", "sin(", "cos(", "tan(", "asin(", "acos(",
"atan(", "atan2(", "pow(", "sqrt(", "hypot(", "log(", "exp(", "round(",
];

/// Mirrors <https://github.com/tailwindlabs/tailwindcss/blob/v4.2.2/packages/tailwindcss/src/utils/math-operators.ts#L41>
/// (`hasMathFn`).
fn has_math_fn(input: &str) -> bool {
if !input.contains('(') {
return false;
}
MATH_FUNCTIONS.iter().any(|f| input.contains(f))
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I thought that's what you needed. Can you elaborate what you mean by predicates?

@jiwon79 jiwon79 force-pushed the feat/tailwind-v4-utility-sort branch from d87b4a6 to 2584190 Compare May 10, 2026 07:12
NamedTyped only carries Number/Percentage/Ratio in the preset, so
delete the 12 dormant Color/Length/Angle/Image/etc. predicates and
their helpers. ValueType::matches now dispatches only those three
and returns false for the rest. ArbitraryTyped/Arbitrary branches
stay in the preset for the follow-up PR that wires them up via the
parser nodes from biomejs#10299.
@jiwon79 jiwon79 requested a review from dyc3 May 10, 2026 07:46
jiwon79 added 2 commits May 11, 2026 22:10
Routes sort_v4 through the new TwNumberValue / TwPercentageValue nodes
from biomejs#10332 instead of scanning value text in predicates.rs. Ratio is
synthesized from value + numeric modifier when the entry has a Ratio
branch (mirrors Tailwind's supportsFractions). All hot-path matching
stays allocation-free.
@github-actions github-actions Bot added A-CLI Area: CLI A-Project Area: project A-Parser Area: parser A-Formatter Area: formatter A-Tooling Area: internal tools L-CSS Language: CSS and super languages A-Diagnostic Area: diagnostocis L-HTML Language: HTML and super languages L-Grit Language: GritQL L-Tailwind Language: Tailwind CSS labels May 11, 2026
@jiwon79 jiwon79 force-pushed the feat/tailwind-v4-utility-sort branch from ccbccc5 to 84d2dec Compare May 11, 2026 15:26
@github-actions github-actions Bot removed A-Project Area: project A-Parser Area: parser A-Formatter Area: formatter A-Tooling Area: internal tools L-CSS Language: CSS and super languages A-Diagnostic Area: diagnostocis L-HTML Language: HTML and super languages L-Grit Language: GritQL L-Tailwind Language: Tailwind CSS labels May 11, 2026
@jiwon79
Copy link
Copy Markdown
Contributor Author

jiwon79 commented May 12, 2026

Can I get a review? @dyc3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-CLI Area: CLI A-Linter Area: linter L-JavaScript Language: JavaScript and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants