Skip to content

refactor(parse/tailwind): use trivia again#8513

Merged
dyc3 merged 1 commit intomainfrom
dyc3/tw-parser-whitespace
Dec 19, 2025
Merged

refactor(parse/tailwind): use trivia again#8513
dyc3 merged 1 commit intomainfrom
dyc3/tw-parser-whitespace

Conversation

@dyc3
Copy link
Contributor

@dyc3 dyc3 commented Dec 19, 2025

Summary

This refactors the tailwind parser to use trivia again, instead of considering whitespace to not be trivia.

Test Plan

snapshots

Docs

@changeset-bot
Copy link

changeset-bot bot commented Dec 19, 2025

⚠️ No Changeset found

Latest commit: c242dce

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@github-actions github-actions bot added A-Parser Area: parser A-Tooling Area: internal tools L-Tailwind Language: Tailwind CSS labels Dec 19, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 19, 2025

Walkthrough

The PR restructures the Tailwind parser and lexer state: it removes multiple internal fields from TailwindLexer (position, current_kind, current_start, diagnostics, current_flags, preceding_line_break, after_newline, unicode_bom_length) and leaves source as the remaining field. TailwindTokenSource gains a had_trivia_before flag and accessor to track trivia between non‑trivia tokens; trivia handling and newline recording were adjusted. Syntax changes move CandidateList to implement ParseNodeList, add whitespace/trivia checks to disambiguate functional vs static candidates, and remove a minus‑guard in parse_named_value. The grammar for candidate lists was flattened and WHITESPACE was added to trivia classification. Tests and fixtures were updated for negative utilities.

Possibly related PRs

Suggested reviewers

  • ematipico
  • denbezrukov

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarises the main change: refactoring the Tailwind parser to use trivia again instead of treating whitespace as non-trivia.
Description check ✅ Passed The description clearly explains the motivation (reverting whitespace-as-non-trivia approach), provides test plan details (snapshots), and relates directly to the changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dyc3/tw-parser-whitespace

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

Copy link
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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/biome_tailwind_syntax/src/lib.rs (1)

83-88: Stale comment contradicts the new behaviour.

The comment on lines 84-85 states whitespace is intentionally not considered trivia, but the code now explicitly treats WHITESPACE as trivia. Please update or remove this outdated comment.

🔎 Suggested fix
         if value.is_trivia() {
-            // We intentionally don't consider whitespace to be trivia because it's a required part of the syntax.
-            // There must be spaces between Candidates in order for tailwind to parse them.
             match value {
                 TailwindSyntaxKind::NEWLINE => Ok(Self::Newline),
                 TailwindSyntaxKind::WHITESPACE => Ok(Self::Whitespace),
                 _ => unreachable!("Not Trivia"),
             }
🧹 Nitpick comments (1)
crates/biome_tailwind_parser/src/syntax/mod.rs (1)

113-124: Consider consolidating the two whitespace checks.

There are two separate conditions checking for whitespace between tokens:

  1. had_trivia_before() – trivia immediately before current token
  2. Checking if last_trivia position is after pos

These seem to handle slightly different cases, but it's a bit subtle. A brief inline comment explaining why both are needed would help future maintainers.

Also, minor typo: "Theres" → "There's" (lines 115 and 122).

🔎 Typo fix
     if p.source().had_trivia_before() {
         // Whitespace is not allowed in tailwind candidates
-        // Theres whitespace between these tokens, so it can't be a functional candidate
+        // There's whitespace between these tokens, so it can't be a functional candidate
         return Present(m.complete(p, TW_STATIC_CANDIDATE));
     }
     if let Some(last_trivia) = p.source().trivia_list.last()
         && pos < last_trivia.text_range().start()
     {
         // Whitespace is not allowed in tailwind candidates
-        // Theres whitespace between these tokens, so it can't be a functional candidate
+        // There's whitespace between these tokens, so it can't be a functional candidate
         return Present(m.complete(p, TW_STATIC_CANDIDATE));
     }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 16a9036 and 98f8dc3.

⛔ Files ignored due to path filters (48)
  • crates/biome_tailwind_factory/src/generated/node_factory.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_tailwind_factory/src/generated/syntax_factory.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/error/arbitrary-candidate/missing-property.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/error/arbitrary-candidate/missing-value-in-arbitrary.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/error/incomplete-arbitrary-value-0.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/error/incomplete-arbitrary-value-1.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/error/incomplete-arbitrary-value-2.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/error/incomplete-arbitrary-variant.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/error/missing-modifier-value-1.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/error/missing-modifier-value.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/error/missing-value.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/brackets/gradient.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/brackets/image-url.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/brackets/inset.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/brackets/misc-xy.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/brackets/shadow.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-0.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-1.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-2.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-3.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/gradients/precise-control.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/gradients/simple.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/2-classes.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/arbitrary-value-0.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/arbitrary-value-1.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/base-has-dash-0.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/base-has-dash-1.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/base-has-dash-2.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/basic-0.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/basic-1.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/basic-2.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/border.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/css-value.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/important.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/modifier.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/multiple-spaces.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/multiple.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/negative.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/static.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/stress/stress-1.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/stress/stress-2.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/variants/arbitrary-variant.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/variants/functional-arbirary-param.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/variants/functional-named-param.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/variants/hover.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/variants/hover_focus.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/variants/starts-with-number.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_syntax/src/generated/nodes.rs is excluded by !**/generated/**, !**/generated/** and included by **
📒 Files selected for processing (8)
  • crates/biome_tailwind_parser/src/lexer/mod.rs (0 hunks)
  • crates/biome_tailwind_parser/src/syntax/mod.rs (4 hunks)
  • crates/biome_tailwind_parser/src/syntax/value.rs (0 hunks)
  • crates/biome_tailwind_parser/src/token_source.rs (4 hunks)
  • crates/biome_tailwind_parser/tests/quick_test.rs (1 hunks)
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/2-classes.txt (1 hunks)
  • crates/biome_tailwind_syntax/src/lib.rs (2 hunks)
  • xtask/codegen/tailwind.ungram (1 hunks)
💤 Files with no reviewable changes (2)
  • crates/biome_tailwind_parser/src/syntax/value.rs
  • crates/biome_tailwind_parser/src/lexer/mod.rs
🧰 Additional context used
📓 Path-based instructions (1)
crates/**/*.rs

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Update inline rustdoc documentation for rules, assists, and their options when adding new features or changing existing features in Rust crates

Files:

  • crates/biome_tailwind_parser/tests/quick_test.rs
  • crates/biome_tailwind_syntax/src/lib.rs
  • crates/biome_tailwind_parser/src/token_source.rs
  • crates/biome_tailwind_parser/src/syntax/mod.rs
🧠 Learnings (20)
📓 Common learnings
Learnt from: ematipico
Repo: biomejs/biome PR: 7852
File: crates/biome_css_parser/src/syntax/property/mod.rs:161-168
Timestamp: 2025-10-25T07:22:18.540Z
Learning: In the Biome CSS parser, lexer token emission should not be gated behind parser options like `is_tailwind_directives_enabled()`. The lexer must emit correct tokens regardless of parser options to enable accurate diagnostics and error messages when the syntax is used incorrectly.
📚 Learning: 2025-11-24T18:05:27.810Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_js_formatter/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:05:27.810Z
Learning: Applies to crates/biome_js_formatter/**/*.rs : When formatting AST nodes, use mandatory tokens from the AST instead of hardcoding token strings (e.g., use `node.l_paren_token().format()` instead of `token("(")`)

Applied to files:

  • crates/biome_tailwind_parser/tests/quick_test.rs
  • crates/biome_tailwind_parser/src/syntax/mod.rs
📚 Learning: 2025-12-19T12:53:30.399Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.399Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Avoid string allocations by comparing against `&str` or using `TokenText`

Applied to files:

  • crates/biome_tailwind_parser/tests/quick_test.rs
📚 Learning: 2025-12-04T13:29:49.287Z
Learnt from: dyc3
Repo: biomejs/biome PR: 8291
File: crates/biome_html_formatter/tests/specs/prettier/vue/html-vue/elastic-header.html:10-10
Timestamp: 2025-12-04T13:29:49.287Z
Learning: Files under `crates/biome_html_formatter/tests/specs/prettier` are test fixtures synced from Prettier and should not receive detailed code quality reviews (e.g., HTTP vs HTTPS, formatting suggestions, etc.). These files are test data meant to validate formatter behavior and should be preserved as-is.

Applied to files:

  • crates/biome_tailwind_parser/tests/quick_test.rs
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/2-classes.txt
📚 Learning: 2025-11-24T18:06:03.545Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_parser/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:06:03.545Z
Learning: Applies to crates/biome_parser/**/src/**/*.rs : Use `p.eat(token)` for optional tokens, `p.expect(token)` for required tokens, `parse_rule(p).ok(p)` for optional nodes, and `parse_rule(p).or_add_diagnostic(p, error)` for required nodes

Applied to files:

  • crates/biome_tailwind_parser/tests/quick_test.rs
  • crates/biome_tailwind_parser/src/syntax/mod.rs
📚 Learning: 2025-11-24T18:05:42.356Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_js_type_info/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:05:42.356Z
Learning: Applies to crates/biome_js_type_info/**/*.rs : Distinguish between `TypeData::Unknown` and `TypeData::UnknownKeyword` to measure inference effectiveness versus explicit user-provided unknown types

Applied to files:

  • crates/biome_tailwind_parser/tests/quick_test.rs
  • crates/biome_tailwind_syntax/src/lib.rs
📚 Learning: 2025-11-24T18:05:20.371Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_formatter/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:05:20.371Z
Learning: Applies to crates/biome_formatter/**/biome_*_formatter/tests/language.rs : Implement `TestFormatLanguage` trait in `tests/language.rs` for the formatter's test language

Applied to files:

  • crates/biome_tailwind_parser/tests/quick_test.rs
📚 Learning: 2025-11-24T18:06:03.545Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_parser/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:06:03.545Z
Learning: Applies to crates/biome_parser/**/src/**/*.rs : Implement error recovery in list parsing using `or_recover()` to wrap unparseable tokens in a `BOGUS_*` node and consume tokens until a recovery token is found

Applied to files:

  • crates/biome_tailwind_parser/tests/quick_test.rs
  • crates/biome_tailwind_parser/src/syntax/mod.rs
📚 Learning: 2025-12-19T12:53:30.399Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.399Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Prefix line with `#` in documentation code examples sparingly; prefer concise complete snippets

Applied to files:

  • crates/biome_tailwind_parser/tests/quick_test.rs
📚 Learning: 2025-11-24T18:06:03.545Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_parser/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:06:03.545Z
Learning: Applies to crates/biome_parser/**/src/**/*.rs : Use `ConditionalParsedSyntax` for syntax that is only valid in specific contexts (e.g., strict mode, file types, language versions) and call `or_invalid_to_bogus()` to convert to a bogus node if not supported

Applied to files:

  • crates/biome_tailwind_syntax/src/lib.rs
  • crates/biome_tailwind_parser/src/syntax/mod.rs
📚 Learning: 2025-12-12T10:11:05.564Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: CONTRIBUTING.md:0-0
Timestamp: 2025-12-12T10:11:05.564Z
Learning: Applies to crates/**/*.rs : Update inline rustdoc documentation for rules, assists, and their options when adding new features or changing existing features in Rust crates

Applied to files:

  • crates/biome_tailwind_syntax/src/lib.rs
📚 Learning: 2025-11-24T18:06:03.545Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_parser/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:06:03.545Z
Learning: Applies to crates/biome_parser/**/src/**/*.rs : Implement a token source struct that wraps the lexer and implements `TokenSourceWithBufferedLexer` and `LexerWithCheckpoint` for lookahead and re-lexing capabilities

Applied to files:

  • crates/biome_tailwind_parser/src/token_source.rs
  • crates/biome_tailwind_parser/src/syntax/mod.rs
📚 Learning: 2025-11-09T12:47:46.298Z
Learnt from: ematipico
Repo: biomejs/biome PR: 8031
File: crates/biome_html_parser/src/syntax/svelte.rs:140-147
Timestamp: 2025-11-09T12:47:46.298Z
Learning: In the Biome HTML parser, `expect` and `expect_with_context` consume the current token and then lex the next token. The context parameter in `expect_with_context` controls how the next token (after the consumed one) is lexed, not the current token being consumed. For example, in Svelte parsing, after `bump_with_context(T!["{:"], HtmlLexContext::Svelte)`, the next token is already lexed in the Svelte context, so `expect(T![else])` is sufficient unless the token after `else` also needs to be lexed in a specific context.

Applied to files:

  • crates/biome_tailwind_parser/src/token_source.rs
📚 Learning: 2025-11-24T18:06:03.545Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_parser/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:06:03.545Z
Learning: Applies to crates/biome_parser/**/src/**/*.rs : Use `ParseSeparatedList` and `ParseNodeList` for parsing lists with error recovery to avoid infinite loops

Applied to files:

  • crates/biome_tailwind_parser/src/syntax/mod.rs
📚 Learning: 2025-11-24T18:06:03.545Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_parser/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:06:03.545Z
Learning: Applies to crates/biome_parser/**/src/**/*.rs : Parse rules must take a mutable reference to the parser as their only parameter and return a `ParsedSyntax`

Applied to files:

  • crates/biome_tailwind_parser/src/syntax/mod.rs
📚 Learning: 2025-11-24T18:06:03.545Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_parser/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:06:03.545Z
Learning: Applies to crates/biome_parser/**/src/**/*.rs : Parse rule functions must be prefixed with `parse_` and use the name defined in the grammar file, e.g., `parse_for_statement` or `parse_expression`

Applied to files:

  • crates/biome_tailwind_parser/src/syntax/mod.rs
📚 Learning: 2025-11-24T18:06:03.545Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_parser/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:06:03.545Z
Learning: Applies to crates/biome_parser/**/*.ungram : Nodes that represent a list must end with the postfix `List`, e.g., `HtmlAttributeList`, and lists are mandatory (not optional) but empty by default

Applied to files:

  • crates/biome_tailwind_parser/src/syntax/mod.rs
📚 Learning: 2025-11-24T18:06:03.545Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_parser/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:06:03.545Z
Learning: Applies to crates/biome_parser/**/src/**/*.rs : A parser struct must implement the `Parser` trait and save the token source, parser context, and optional parser options

Applied to files:

  • crates/biome_tailwind_parser/src/syntax/mod.rs
📚 Learning: 2025-11-24T18:06:03.545Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_parser/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:06:03.545Z
Learning: Applies to crates/biome_parser/**/src/**/*.rs : Parse rules must return `ParsedSyntax::Absent` if the rule can't predict by the next token(s) if they form the expected node, and must not progress the parser in this case

Applied to files:

  • crates/biome_tailwind_parser/src/syntax/mod.rs
📚 Learning: 2025-11-24T18:06:03.545Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_parser/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:06:03.545Z
Learning: Applies to crates/biome_parser/**/lexer/mod.rs : Implement a `Lexer` trait from `biome_parser` crate for the lexer struct that consumes characters from source code and emits tokens

Applied to files:

  • crates/biome_tailwind_parser/src/syntax/mod.rs
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Check Dependencies
  • GitHub Check: Lint project (depot-ubuntu-24.04-arm-16)
  • GitHub Check: Test (depot-windows-2022-16)
  • GitHub Check: Documentation
  • GitHub Check: Test (depot-ubuntu-24.04-arm-16)
  • GitHub Check: End-to-end tests
  • GitHub Check: Lint project (depot-windows-2022)
  • GitHub Check: Test Node.js API
  • GitHub Check: Parser conformance
  • GitHub Check: Bench (biome_tailwind_parser)
  • GitHub Check: autofix
🔇 Additional comments (6)
crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/2-classes.txt (1)

1-1: LGTM!

Good test case for validating that two classes separated by whitespace—including a negative utility—are correctly parsed.

xtask/codegen/tailwind.ungram (1)

55-55: LGTM!

The flattened AnyTwFullCandidate* structure properly reflects that whitespace between candidates is now handled as trivia rather than explicit separators.

crates/biome_tailwind_parser/tests/quick_test.rs (1)

7-7: LGTM!

Solid test input for exercising negative utilities with whitespace separation.

crates/biome_tailwind_parser/src/token_source.rs (2)

17-19: LGTM on the trivia tracking implementation.

Clean addition of the had_trivia_before state and its accessor. The reset logic in next_non_trivia_token is correctly placed.

Also applies to: 106-110


100-105: The rewind method doesn't need to restore had_trivia_before—the flag is reset at the start of every token consumption.

Since next_non_trivia_token() resets had_trivia_before = false on entry (line 67), and every code path that advances the parser (via bump_with_context()) calls this method, the flag is always reset before its state matters. The checkpoint design is sound.

crates/biome_tailwind_parser/src/syntax/mod.rs (1)

31-56: LGTM on the ParseNodeList implementation.

Clean switch from ParseSeparatedList to ParseNodeList. The recovery logic is well-structured.

@github-actions
Copy link
Contributor

Parser conformance results on

js/262

Test result main count This PR count Difference
Total 52650 52650 0
Passed 51437 51437 0
Failed 1171 1171 0
Panics 42 42 0
Coverage 97.70% 97.70% 0.00%

jsx/babel

Test result main count This PR count Difference
Total 38 38 0
Passed 37 37 0
Failed 1 1 0
Panics 0 0 0
Coverage 97.37% 97.37% 0.00%

symbols/microsoft

Test result main count This PR count Difference
Total 6339 6339 0
Passed 2110 2110 0
Failed 4229 4229 0
Panics 0 0 0
Coverage 33.29% 33.29% 0.00%

ts/babel

Test result main count This PR count Difference
Total 626 626 0
Passed 561 561 0
Failed 65 65 0
Panics 0 0 0
Coverage 89.62% 89.62% 0.00%

ts/microsoft

Test result main count This PR count Difference
Total 18846 18846 0
Passed 14090 14090 0
Failed 4755 4755 0
Panics 1 1 0
Coverage 74.76% 74.76% 0.00%

@codspeed-hq
Copy link

codspeed-hq bot commented Dec 19, 2025

CodSpeed Performance Report

Merging #8513 will improve performances by 16.27%

Comparing dyc3/tw-parser-whitespace (c242dce) with main (d64e92d)

Summary

⚡ 4 improvements
✅ 6 untouched
⏩ 145 skipped1

Benchmarks breakdown

Benchmark BASE HEAD Change
cached[extreme_stress.txt] 944.9 µs 866.8 µs +9.01%
cached[arbitrary_classes.txt] 259.1 µs 242.2 µs +6.99%
uncached[extreme_stress.txt] 1.2 ms 1.1 ms +16.27%
uncached[arbitrary_classes.txt] 331.5 µs 307.2 µs +7.9%

Footnotes

  1. 145 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Copy link
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

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

And performances 🚀

Copy link
Contributor Author

dyc3 commented Dec 19, 2025

@dyc3 dyc3 force-pushed the dyc3/tw-parser-whitespace branch from 98f8dc3 to c242dce Compare December 19, 2025 17:07
Copy link
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: 0

🧹 Nitpick comments (1)
crates/biome_tailwind_parser/src/syntax/mod.rs (1)

113-124: Clarify whether both trivia checks are needed.

Lines 113–117 check had_trivia_before() and lines 118–124 check trivia_list.last() against the captured position. Both return TW_STATIC_CANDIDATE with identical comments. If had_trivia_before() already detects trivia immediately preceding the current token, the second check may be redundant.

Could you clarify when one check would succeed but not the other? If both are truly necessary, a brief comment explaining the distinction would help future maintainers.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 98f8dc3 and c242dce.

⛔ Files ignored due to path filters (48)
  • crates/biome_tailwind_factory/src/generated/node_factory.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_tailwind_factory/src/generated/syntax_factory.rs is excluded by !**/generated/**, !**/generated/** and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/error/arbitrary-candidate/missing-property.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/error/arbitrary-candidate/missing-value-in-arbitrary.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/error/incomplete-arbitrary-value-0.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/error/incomplete-arbitrary-value-1.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/error/incomplete-arbitrary-value-2.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/error/incomplete-arbitrary-variant.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/error/missing-modifier-value-1.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/error/missing-modifier-value.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/error/missing-value.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/brackets/gradient.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/brackets/image-url.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/brackets/inset.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/brackets/misc-xy.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/brackets/shadow.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-0.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-1.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-2.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/candidates/arbitrary-candidate-3.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/gradients/precise-control.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/gradients/simple.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/2-classes.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/arbitrary-value-0.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/arbitrary-value-1.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/base-has-dash-0.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/base-has-dash-1.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/base-has-dash-2.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/basic-0.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/basic-1.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/basic-2.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/border.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/css-value.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/important.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/modifier.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/multiple-spaces.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/multiple.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/negative.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/static.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/stress/stress-1.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/stress/stress-2.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/variants/arbitrary-variant.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/variants/functional-arbirary-param.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/variants/functional-named-param.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/variants/hover.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/variants/hover_focus.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/variants/starts-with-number.txt.snap is excluded by !**/*.snap and included by **
  • crates/biome_tailwind_syntax/src/generated/nodes.rs is excluded by !**/generated/**, !**/generated/** and included by **
📒 Files selected for processing (8)
  • crates/biome_tailwind_parser/src/lexer/mod.rs (0 hunks)
  • crates/biome_tailwind_parser/src/syntax/mod.rs (4 hunks)
  • crates/biome_tailwind_parser/src/syntax/value.rs (0 hunks)
  • crates/biome_tailwind_parser/src/token_source.rs (4 hunks)
  • crates/biome_tailwind_parser/tests/quick_test.rs (1 hunks)
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/2-classes.txt (1 hunks)
  • crates/biome_tailwind_syntax/src/lib.rs (2 hunks)
  • xtask/codegen/tailwind.ungram (1 hunks)
💤 Files with no reviewable changes (2)
  • crates/biome_tailwind_parser/src/syntax/value.rs
  • crates/biome_tailwind_parser/src/lexer/mod.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • crates/biome_tailwind_parser/tests/tailwind_specs/ok/simple/2-classes.txt
  • crates/biome_tailwind_parser/tests/quick_test.rs
  • crates/biome_tailwind_syntax/src/lib.rs
  • crates/biome_tailwind_parser/src/token_source.rs
🧰 Additional context used
📓 Path-based instructions (1)
crates/**/*.rs

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Update inline rustdoc documentation for rules, assists, and their options when adding new features or changing existing features in Rust crates

Files:

  • crates/biome_tailwind_parser/src/syntax/mod.rs
🧠 Learnings (13)
📓 Common learnings
Learnt from: ematipico
Repo: biomejs/biome PR: 7852
File: crates/biome_css_parser/src/syntax/property/mod.rs:161-168
Timestamp: 2025-10-25T07:22:18.540Z
Learning: In the Biome CSS parser, lexer token emission should not be gated behind parser options like `is_tailwind_directives_enabled()`. The lexer must emit correct tokens regardless of parser options to enable accurate diagnostics and error messages when the syntax is used incorrectly.
📚 Learning: 2025-11-24T18:06:03.545Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_parser/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:06:03.545Z
Learning: Applies to crates/biome_parser/**/src/**/*.rs : Use `ParseSeparatedList` and `ParseNodeList` for parsing lists with error recovery to avoid infinite loops

Applied to files:

  • crates/biome_tailwind_parser/src/syntax/mod.rs
📚 Learning: 2025-11-24T18:06:03.545Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_parser/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:06:03.545Z
Learning: Applies to crates/biome_parser/**/src/**/*.rs : Implement error recovery in list parsing using `or_recover()` to wrap unparseable tokens in a `BOGUS_*` node and consume tokens until a recovery token is found

Applied to files:

  • crates/biome_tailwind_parser/src/syntax/mod.rs
📚 Learning: 2025-11-24T18:06:03.545Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_parser/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:06:03.545Z
Learning: Applies to crates/biome_parser/**/src/**/*.rs : Use `ConditionalParsedSyntax` for syntax that is only valid in specific contexts (e.g., strict mode, file types, language versions) and call `or_invalid_to_bogus()` to convert to a bogus node if not supported

Applied to files:

  • crates/biome_tailwind_parser/src/syntax/mod.rs
📚 Learning: 2025-11-24T18:05:27.810Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_js_formatter/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:05:27.810Z
Learning: Applies to crates/biome_js_formatter/**/*.rs : When formatting AST nodes, use mandatory tokens from the AST instead of hardcoding token strings (e.g., use `node.l_paren_token().format()` instead of `token("(")`)

Applied to files:

  • crates/biome_tailwind_parser/src/syntax/mod.rs
📚 Learning: 2025-11-24T18:06:03.545Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_parser/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:06:03.545Z
Learning: Applies to crates/biome_parser/**/*.ungram : Nodes that represent a list must end with the postfix `List`, e.g., `HtmlAttributeList`, and lists are mandatory (not optional) but empty by default

Applied to files:

  • crates/biome_tailwind_parser/src/syntax/mod.rs
📚 Learning: 2025-11-24T18:06:03.545Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_parser/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:06:03.545Z
Learning: Applies to crates/biome_parser/**/src/**/*.rs : Use `p.eat(token)` for optional tokens, `p.expect(token)` for required tokens, `parse_rule(p).ok(p)` for optional nodes, and `parse_rule(p).or_add_diagnostic(p, error)` for required nodes

Applied to files:

  • crates/biome_tailwind_parser/src/syntax/mod.rs
📚 Learning: 2025-11-24T18:06:03.545Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_parser/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:06:03.545Z
Learning: Applies to crates/biome_parser/**/src/**/*.rs : Parse rules must take a mutable reference to the parser as their only parameter and return a `ParsedSyntax`

Applied to files:

  • crates/biome_tailwind_parser/src/syntax/mod.rs
📚 Learning: 2025-11-24T18:06:03.545Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_parser/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:06:03.545Z
Learning: Applies to crates/biome_parser/**/src/**/*.rs : Parse rule functions must be prefixed with `parse_` and use the name defined in the grammar file, e.g., `parse_for_statement` or `parse_expression`

Applied to files:

  • crates/biome_tailwind_parser/src/syntax/mod.rs
📚 Learning: 2025-11-24T18:06:03.545Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_parser/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:06:03.545Z
Learning: Applies to crates/biome_parser/**/src/**/*.rs : Parse rules must return `ParsedSyntax::Absent` if the rule can't predict by the next token(s) if they form the expected node, and must not progress the parser in this case

Applied to files:

  • crates/biome_tailwind_parser/src/syntax/mod.rs
📚 Learning: 2025-11-24T18:06:03.545Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_parser/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:06:03.545Z
Learning: Applies to crates/biome_parser/**/src/**/*.rs : A parser struct must implement the `Parser` trait and save the token source, parser context, and optional parser options

Applied to files:

  • crates/biome_tailwind_parser/src/syntax/mod.rs
📚 Learning: 2025-11-24T18:06:03.545Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_parser/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:06:03.545Z
Learning: Applies to crates/biome_parser/**/src/**/*.rs : Implement a token source struct that wraps the lexer and implements `TokenSourceWithBufferedLexer` and `LexerWithCheckpoint` for lookahead and re-lexing capabilities

Applied to files:

  • crates/biome_tailwind_parser/src/syntax/mod.rs
📚 Learning: 2025-11-24T18:06:03.545Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_parser/CONTRIBUTING.md:0-0
Timestamp: 2025-11-24T18:06:03.545Z
Learning: Applies to crates/biome_parser/**/lexer/mod.rs : Implement a `Lexer` trait from `biome_parser` crate for the lexer struct that consumes characters from source code and emits tokens

Applied to files:

  • crates/biome_tailwind_parser/src/syntax/mod.rs
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Test Node.js API
  • GitHub Check: Bench (biome_tailwind_parser)
  • GitHub Check: autofix
  • GitHub Check: Parser conformance
  • GitHub Check: End-to-end tests
  • GitHub Check: Documentation
  • GitHub Check: Lint project (depot-windows-2022)
  • GitHub Check: Lint project (depot-ubuntu-24.04-arm-16)
  • GitHub Check: Test (depot-ubuntu-24.04-arm-16)
  • GitHub Check: Test (depot-windows-2022-16)
  • GitHub Check: Check Dependencies
🔇 Additional comments (3)
crates/biome_tailwind_parser/src/syntax/mod.rs (2)

6-6: LGTM!

Import updated appropriately for the new ParseNodeList usage.


31-56: Clean transition to ParseNodeList.

The switch from ParseSeparatedList to ParseNodeList is well-aligned with treating whitespace as trivia. The recovery logic remains sensible.

xtask/codegen/tailwind.ungram (1)

55-55: Grammar simplification aligns with parser changes.

Flattening the list to AnyTwFullCandidate* is consistent with treating whitespace as trivia rather than explicit separators. Clean change.

Copy link
Contributor Author

dyc3 commented Dec 19, 2025

Merge activity

  • Dec 19, 5:20 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Dec 19, 5:21 PM UTC: @dyc3 merged this pull request with Graphite.

@dyc3 dyc3 merged commit c89cfe2 into main Dec 19, 2025
16 checks passed
@dyc3 dyc3 deleted the dyc3/tw-parser-whitespace branch December 19, 2025 17:21
l0ngvh pushed a commit to l0ngvh/biome that referenced this pull request Dec 21, 2025
<!--
  IMPORTANT!!
  If you generated this PR with the help of any AI assistance, please disclose it in the PR.
  https://github.com/biomejs/biome/blob/main/CONTRIBUTING.md#ai-assistance-notice
-->

<!--
	Thanks for submitting a Pull Request! We appreciate you spending the time to work on these changes.
	Please provide enough information so that others can review your PR.
	Once created, your PR will be automatically labeled according to changed files.
	Learn more about contributing: https://github.com/biomejs/biome/blob/main/CONTRIBUTING.md
-->

## Summary

<!-- Explain the **motivation** for making this change. What existing problem does the pull request solve?-->
This refactors the tailwind parser to use trivia again, instead of considering whitespace to not be trivia.

<!-- Link any relevant issues if necessary or include a transcript of any Discord discussion. -->

<!-- If you create a user-facing change, please write a changeset: https://github.com/biomejs/biome/blob/main/CONTRIBUTING.md#writing-a-changeset (your changeset is often a good starting point for this summary as well) -->

## Test Plan

<!-- What demonstrates that your implementation is correct? -->
snapshots

## Docs

<!-- If you're submitting a new rule or action (or an option for them), the documentation is part of the code. Make sure rules and actions have example usages, and that all options are documented. -->

<!-- For other features, please submit a documentation PR to the `next` branch of our website: https://github.com/biomejs/website/. Link the PR here once it's ready. -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Parser Area: parser A-Tooling Area: internal tools L-Tailwind Language: Tailwind CSS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants