Skip to content

Conversation

@mldangelo
Copy link

@mldangelo mldangelo commented Dec 30, 2025

Summary

This PR ports 2 lint rules 1 lint rule from eslint-plugin-better-tailwindcss to Biome's nursery group.

Biome Rule Source
noDuplicateTailwindClasses no-duplicate-classes
noUnnecessaryTailwindWhitespace no-unnecessary-whitespaceRemoved: redundant with useSortedClasses

The rule reuses the existing UseSortedClassesOptions from useSortedClasses, so it works with the same attributes and functions configuration.

Related Issues

Test Plan

  • Added spec tests for valid and invalid cases
  • All tests pass: cargo test -p biome_js_analyze -- no_duplicate_tailwind
  • Clippy passes with 0 warnings

Future Work

If maintainers are open to it, I'd like to continue porting rules from eslint-plugin-better-tailwindcss. Here's the proposed roadmap:

Next Priority: High-Value Ports

Biome Rule ESLint Source Description
noConflictingTailwindClasses no-conflicting-classes Detects conflicting utilities like p-2 p-4 where only the last one applies
noDeprecatedTailwindClasses no-deprecated-classes Flags deprecated Tailwind classes

Additional Ports

Biome Rule ESLint Source
useConsistentTailwindImportantPosition enforce-consistent-important-position
noRestrictedTailwindClasses no-restricted-classes
useTailwindShorthandClasses enforce-shorthand-classes

I'm happy to split these into separate PRs for easier review.

Context

I'm a maintainer of promptfoo and have been using Biome to enforce our Tailwind rules. Happy to help expand Tailwind support in Biome!

AI Assistance Disclosure

This PR was developed with assistance from Claude Code.

mldangelo and others added 3 commits December 29, 2025 22:29
Add new nursery lint rules for Tailwind CSS utility classes:

- `noDuplicateTailwindClasses`: Detects and removes duplicate CSS utility classes
- `noUnnecessaryTailwindWhitespace`: Removes extra whitespace in class strings
- `noEmptyTailwindArbitraryValue`: Flags empty arbitrary values like `w-[]`
- `noInvalidTailwindVariantCombination`: Detects conflicting variants
- `useTailwindLogicalProperties`: Suggests logical properties (`ms-*` instead of `ml-*`)
- `useTailwindColorOpacityModifier`: Suggests opacity modifiers (`text-red-500/50`)

All rules reuse existing infrastructure from `useSortedClasses` and require no
changes to analyzer configuration or core infrastructure.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Added changeset for the 6 new Tailwind CSS lint rules
- Fixed clippy::manual_strip warning in useTailwindLogicalProperties

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Remove 4 new rules that weren't ported from eslint-plugin-better-tailwindcss:
- noEmptyTailwindArbitraryValue
- noInvalidTailwindVariantCombination
- useTailwindColorOpacityModifier
- useTailwindLogicalProperties

Keep only the 2 rules that are direct ports:
- noDuplicateTailwindClasses (from no-duplicate-classes)
- noUnnecessaryTailwindWhitespace (from no-unnecessary-whitespace)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@changeset-bot
Copy link

changeset-bot bot commented Dec 30, 2025

🦋 Changeset detected

Latest commit: 8fd7703

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 A-Diagnostic Area: diagnostocis labels Dec 30, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 30, 2025

Walkthrough

This PR introduces a new lint rule noDuplicateTailwindClasses that detects and removes duplicate Tailwind CSS utility classes in JSX attributes and utility function calls. The implementation includes the rule logic with automatic fixing capabilities, test cases covering valid and invalid scenarios, and updates to the RuleSource enum to support ESLint Better Tailwindcss rules with corresponding documentation URLs.

Possibly related PRs

  • docs: rules.rs cleanup #8044: Modifies the same RuleSource enum and its associated methods for handling new rule-source variants in crates/biome_analyze/src/rule.rs
  • docs: extra rule sources #7522: Updates the same RuleSource enum match arms by adding rule-source variants and updating related methods

Suggested labels

L-Tailwind

Suggested reviewers

  • dyc3
  • ematipico
  • Netail

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title refers to adding 2 Tailwind CSS rules, but the changeset and code only implement noDuplicateTailwindClasses; noUnnecessaryTailwindWhitespace was removed per maintainer feedback. Update the title to reflect the single rule being added: 'feat(linter): add noDuplicateTailwindClasses lint rule' or similar.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description clearly details the porting of noDuplicateTailwindClasses from eslint-plugin-better-tailwindcss, with test plan, related issues, and future roadmap.

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
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 (3)
crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs (1)

66-72: Consider adding issue_number for tracking.

Per guidelines, nursery rules benefit from an issue_number field linking to the tracking issue. The PR objectives mention #6502 as the request for duplicate class detection.

🔎 Suggested addition
     pub NoDuplicateTailwindClasses {
         version: "next",
         name: "noDuplicateTailwindClasses",
         language: "jsx",
         recommended: false,
         fix_kind: FixKind::Safe,
+        issue_number: Some("6502"),
     }
crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs (2)

53-59: Consider adding issue_number for tracking.

Similar to the duplicate classes rule, adding an issue_number helps track the feature request. The PR objectives reference #1274 as the Tailwind CSS support tracking issue.

🔎 Suggested addition
     pub NoUnnecessaryTailwindWhitespace {
         version: "next",
         name: "noUnnecessaryTailwindWhitespace",
         language: "jsx",
         recommended: false,
         fix_kind: FixKind::Safe,
+        issue_number: Some("1274"),
     }

79-86: Detection doesn't explicitly cover tabs or mixed whitespace.

Line 82's contains(" ") only detects double spaces. Tabs or mixed whitespace (e.g., "flex\tp-4" or "flex \t p-4") will still be normalised by split_whitespace(), but the detection check and diagnostic won't explicitly mention them. The rule works correctly due to the guard at line 92, but the diagnostic message might not be fully accurate for tab-heavy inputs.

If you'd like more precise detection:

🔎 Optional enhancement
-        let has_multiple_spaces = value_str.contains("  ");
+        // Check for multiple consecutive whitespace characters (spaces, tabs, newlines)
+        let has_multiple_whitespace = value_str
+            .chars()
+            .zip(value_str.chars().skip(1))
+            .any(|(a, b)| a.is_whitespace() && b.is_whitespace());

And update the diagnostic message at line 111-112 accordingly.

📜 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 7c85bf0 and dfcda2d.

⛔ Files ignored due to path filters (6)
  • crates/biome_diagnostics_categories/src/categories.rs is excluded by !**/categories.rs and included by **
  • crates/biome_js_analyze/src/lint/nursery.rs is excluded by !**/nursery.rs and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateTailwindClasses/invalid.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateTailwindClasses/valid.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noUnnecessaryTailwindWhitespace/invalid.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noUnnecessaryTailwindWhitespace/valid.jsx.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (8)
  • .changeset/tailwind-utility-class-rules.md
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
  • crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs
  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateTailwindClasses/invalid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateTailwindClasses/valid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noUnnecessaryTailwindWhitespace/invalid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noUnnecessaryTailwindWhitespace/valid.jsx
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.rs: Use inline rustdoc documentation for rules, assists, and their options
Use the dbg!() macro for debugging output in Rust tests and code
Use doc tests (doctest) format with code blocks in rustdoc comments; ensure assertions pass in tests

Files:

  • crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
🧠 Learnings (22)
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/lint/nursery/**/*.rs : Place new rules inside the `nursery` group during development

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
  • .changeset/tailwind-utility-class-rules.md
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Assist rules should detect refactoring opportunities and emit code action signals

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Check if a variable is global using the semantic model to avoid false positives

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/lint/nursery/**/*.rs : Add `issue_number` field to `declare_lint_rule!` macro for work-in-progress rules

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Use helper functions like `map`, `filter`, and `and_then` to avoid deep indentation

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Specify `fix_kind: FixKind::Safe` in `declare_lint_rule!` for safe code actions

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/lint/**/*.rs : Lint rules should perform static analysis of source code to detect invalid or error-prone patterns and emit diagnostics with proposed fixes

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.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 : Use `TypeReference` instead of `Arc` for types that reference other types to avoid stale cache issues when modules are replaced

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Set `version` field to `next` in `declare_lint_rule!` macro

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/biome_rule_options/lib/**/*.rs : Use `rename_all = "camelCase"` in serde derive macro for rule options

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs
📚 Learning: 2025-12-22T09:26:56.943Z
Learnt from: ematipico
Repo: biomejs/biome PR: 8537
File: crates/biome_js_analyze/src/lint/nursery/no_leaked_render.rs:167-210
Timestamp: 2025-12-22T09:26:56.943Z
Learning: When defining lint rules (declare_lint_rule!), only specify fix_kind if the rule implements an action(...) function. Rules that only emit diagnostics without a code fix should omit fix_kind. This applies to all Rust lint rule definitions under crates/.../src/lint (e.g., crates/biome_js_analyze/src/lint/...).

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Use `RuleSource::Eslint(...).same()` when implementing a rule that matches the behavior of an ESLint rule

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Use `RuleSource::Eslint(...).inspired()` when implementing a rule inspired by but with different behavior than an ESLint rule

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Use `domains` field in `declare_lint_rule!` to tag rules that belong to specific concepts like testing or frameworks

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Add `sources` field with `RuleSource` to cite ESLint or other rules that inspired the implementation

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/tests/specs/**/*valid* : Create test files prefixed with `valid` for code that should not trigger the rule

Applied to files:

  • crates/biome_js_analyze/tests/specs/nursery/noUnnecessaryTailwindWhitespace/valid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateTailwindClasses/valid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateTailwindClasses/invalid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noUnnecessaryTailwindWhitespace/invalid.jsx
📚 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_js_analyze/tests/specs/nursery/noUnnecessaryTailwindWhitespace/valid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateTailwindClasses/invalid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noUnnecessaryTailwindWhitespace/invalid.jsx
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/tests/specs/**/*invalid* : Create test files prefixed with `invalid` for code that should trigger the rule

Applied to files:

  • crates/biome_js_analyze/tests/specs/nursery/noUnnecessaryTailwindWhitespace/valid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateTailwindClasses/valid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateTailwindClasses/invalid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noUnnecessaryTailwindWhitespace/invalid.jsx
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/tests/specs/**/*.jsonc : Use `.jsonc` files to contain arrays of code snippet strings for snapshot tests

Applied to files:

  • crates/biome_js_analyze/tests/specs/nursery/noUnnecessaryTailwindWhitespace/valid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateTailwindClasses/invalid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noUnnecessaryTailwindWhitespace/invalid.jsx
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Use `declare_lint_rule!` macro to declare analyzer rule types and implement the RuleMeta trait

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
📚 Learning: 2025-12-21T21:15:03.796Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: CONTRIBUTING.md:0-0
Timestamp: 2025-12-21T21:15:03.796Z
Learning: Changesets should describe user-facing changes only; internal refactoring without behavior changes does not require a changeset

Applied to files:

  • .changeset/tailwind-utility-class-rules.md
📚 Learning: 2025-12-21T21:15:03.796Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: CONTRIBUTING.md:0-0
Timestamp: 2025-12-21T21:15:03.796Z
Learning: For new nursery rules, send PRs to the maintenance branch `main`

Applied to files:

  • .changeset/tailwind-utility-class-rules.md
🔇 Additional comments (9)
crates/biome_js_analyze/tests/specs/nursery/noDuplicateTailwindClasses/valid.jsx (1)

1-16: LGTM!

Good coverage of valid cases: non-duplicates, variant-differentiated classes, similar-but-different utilities, empty strings, and single-class scenarios. Based on learnings, the valid prefix correctly indicates these should not trigger diagnostics.

crates/biome_js_analyze/tests/specs/nursery/noUnnecessaryTailwindWhitespace/invalid.jsx (1)

1-18: LGTM!

Comprehensive invalid test cases covering leading/trailing whitespace, multiple consecutive spaces, combined scenarios, and multiline strings with newlines and tabs. Good coverage of the className attribute variant too.

crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs (1)

1-1: Visibility change looks appropriate.

Making any_class_string_like public enables the new Tailwind rules to reuse AnyClassStringLike. Since all consumers are within the same crate (biome_js_analyze), this is a reasonable approach. If external crate access becomes a concern later, consider using pub(crate) instead.

crates/biome_js_analyze/tests/specs/nursery/noUnnecessaryTailwindWhitespace/valid.jsx (1)

1-12: LGTM!

Straightforward valid cases with properly formatted whitespace. Good inclusion of the empty class and single class edge cases.

crates/biome_js_analyze/tests/specs/nursery/noDuplicateTailwindClasses/invalid.jsx (1)

1-16: LGTM!

Solid invalid test coverage: exact duplicates, non-adjacent duplicates, variant-prefixed duplicates, multiple duplicates per string, and arbitrary value duplicates. Well structured!

.changeset/tailwind-utility-class-rules.md (1)

1-9: LGTM!

Changeset correctly documents the two new nursery rules with appropriate links. Patch version bump is suitable for new nursery rules.

crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs (2)

106-122: Whitespace normalisation occurs as a side effect.

Using split_whitespace() and join(" ") means this rule will also normalise whitespace when removing duplicates. This is generally fine, but users may see whitespace fixes from this rule even when noUnnecessaryTailwindWhitespace is disabled. Consider documenting this in the rustdoc if intentional.


155-207: LGTM!

The action implementation correctly handles all AnyClassStringLike variants and preserves quote styles. Good consistency with the existing useSortedClasses rule.

crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs (1)

131-183: LGTM!

Action implementation correctly handles all AnyClassStringLike variants with proper quote style preservation. Consistent with other Tailwind rules in this PR.

- Add issue_number to noDuplicateTailwindClasses (biomejs#6502)
- Add issue_number to noUnnecessaryTailwindWhitespace (biomejs#1274)
- Improve whitespace detection to handle tabs and mixed whitespace

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
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: 2

🧹 Nitpick comments (3)
crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs (2)

144-148: Minor: Consider simpler pluralisation.

The <Emphasis> wrapper around the conditional "es" suffix is a bit unusual. Standard approach would be to conditionally include the whole word. Not blocking though — it works.


173-180: Inconsistent quote preservation for JsLiteralMemberName.

Unlike JsStringLiteralExpression (line 162-165) and JsxString (line 182-185), this branch doesn't attempt to detect the original quote style from the token. It falls back directly to ctx.preferred_quote().

Consider preserving the original quote style for consistency:

🔎 Suggested fix
 AnyClassStringLike::JsLiteralMemberName(string_literal) => {
-    let replacement = js_literal_member_name(if ctx.preferred_quote().is_double() {
+    let is_double_quote = string_literal
+        .value_token()
+        .map(|token| token.text_trimmed().starts_with('"'))
+        .unwrap_or(ctx.preferred_quote().is_double());
+    let replacement = js_literal_member_name(if is_double_quote {
         js_string_literal(deduplicated)
     } else {
         js_string_literal_single_quotes(deduplicated)
     });
     mutation.replace_node(string_literal.clone(), replacement);
 }
crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs (1)

158-165: Same quote preservation inconsistency for JsLiteralMemberName.

As noted in no_duplicate_tailwind_classes.rs, this branch doesn't detect the original quote style. Consider applying the same fix here for consistency.

Also worth noting: the action() implementations in both rules are nearly identical. A shared helper function could reduce duplication in future, but not blocking for this PR.

📜 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 dfcda2d and abeaa21.

⛔ Files ignored due to path filters (2)
  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateTailwindClasses/invalid.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noUnnecessaryTailwindWhitespace/invalid.jsx.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (2)
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.rs: Use inline rustdoc documentation for rules, assists, and their options
Use the dbg!() macro for debugging output in Rust tests and code
Use doc tests (doctest) format with code blocks in rustdoc comments; ensure assertions pass in tests

Files:

  • crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
🧠 Learnings (14)
📓 Common learnings
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/lint/nursery/**/*.rs : Add `issue_number` field to `declare_lint_rule!` macro for work-in-progress rules
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/lint/**/*.rs : Lint rules should perform static analysis of source code to detect invalid or error-prone patterns and emit diagnostics with proposed fixes
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Use `RuleSource::Eslint(...).same()` when implementing a rule that matches the behavior of an ESLint rule
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/lint/nursery/**/*.rs : Place new rules inside the `nursery` group during development
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Use `RuleSource::Eslint(...).inspired()` when implementing a rule inspired by but with different behavior than an ESLint rule
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Commit rule work with message format `feat(biome_<language>_analyze): <ruleName>`
Learnt from: CR
Repo: biomejs/biome PR: 0
File: CONTRIBUTING.md:0-0
Timestamp: 2025-12-21T21:15:03.796Z
Learning: For new nursery rules, send PRs to the maintenance branch `main`
📚 Learning: 2025-12-22T09:26:56.943Z
Learnt from: ematipico
Repo: biomejs/biome PR: 8537
File: crates/biome_js_analyze/src/lint/nursery/no_leaked_render.rs:167-210
Timestamp: 2025-12-22T09:26:56.943Z
Learning: When defining lint rules (declare_lint_rule!), only specify fix_kind if the rule implements an action(...) function. Rules that only emit diagnostics without a code fix should omit fix_kind. This applies to all Rust lint rule definitions under crates/.../src/lint (e.g., crates/biome_js_analyze/src/lint/...).

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/lint/**/*.rs : Lint rules should perform static analysis of source code to detect invalid or error-prone patterns and emit diagnostics with proposed fixes

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/lint/nursery/**/*.rs : Add `issue_number` field to `declare_lint_rule!` macro for work-in-progress rules

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Specify `fix_kind: FixKind::Safe` in `declare_lint_rule!` for safe code actions

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Use `declare_lint_rule!` macro to declare analyzer rule types and implement the RuleMeta trait

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Specify `fix_kind: FixKind::Unsafe` in `declare_lint_rule!` for unsafe code actions

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Use `RuleSource::Eslint(...).same()` when implementing a rule that matches the behavior of an ESLint rule

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/lint/nursery/**/*.rs : Place new rules inside the `nursery` group during development

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Use `RuleSource::Eslint(...).inspired()` when implementing a rule inspired by but with different behavior than an ESLint rule

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Assist rules should detect refactoring opportunities and emit code action signals

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Set `version` field to `next` in `declare_lint_rule!` macro

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Add `deprecated` field to `declare_lint_rule!` macro when deprecating a rule

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Use `domains` field in `declare_lint_rule!` to tag rules that belong to specific concepts like testing or frameworks

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
🔇 Additional comments (3)
crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs (2)

13-83: LGTM!

Rule declaration follows Biome conventions nicely. Documentation is clear, issue_number is present for nursery rules, and fix_kind correctly indicates a safe fix. Based on learnings, this is spot on.


107-117: LGTM!

Duplicate detection logic is correct. The linear search at line 110 is fine given typical duplicate counts are small.

crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs (1)

12-61: LGTM!

Rule declaration is well-structured with clear documentation and proper metadata. Based on learnings, issue_number is correctly included for nursery rules.

Comment on lines 80 to 91
// Check for unnecessary whitespace
let has_leading_whitespace = value_str.starts_with(char::is_whitespace);
let has_trailing_whitespace = value_str.ends_with(char::is_whitespace);
// Check for multiple consecutive whitespace characters (spaces, tabs, newlines)
let has_multiple_whitespace = value_str
.as_bytes()
.windows(2)
.any(|w| w[0].is_ascii_whitespace() && w[1].is_ascii_whitespace());

if !has_leading_whitespace && !has_trailing_whitespace && !has_multiple_whitespace {
return None;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Minor inconsistency in whitespace detection methods.

Lines 81-82 use char::is_whitespace (Unicode-aware), but lines 84-87 use is_ascii_whitespace() (ASCII only). This could cause edge cases with Unicode whitespace characters like non-breaking spaces between classes — they'd be normalised correctly but the diagnostic message might not mention "multiple consecutive whitespace".

For Tailwind classes this is unlikely to matter in practice, but for consistency you could use char::is_whitespace throughout:

🔎 Suggested fix
-        let has_multiple_whitespace = value_str
-            .as_bytes()
-            .windows(2)
-            .any(|w| w[0].is_ascii_whitespace() && w[1].is_ascii_whitespace());
+        let chars: Vec<char> = value_str.chars().collect();
+        let has_multiple_whitespace = chars
+            .windows(2)
+            .any(|w| w[0].is_whitespace() && w[1].is_whitespace());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check for unnecessary whitespace
let has_leading_whitespace = value_str.starts_with(char::is_whitespace);
let has_trailing_whitespace = value_str.ends_with(char::is_whitespace);
// Check for multiple consecutive whitespace characters (spaces, tabs, newlines)
let has_multiple_whitespace = value_str
.as_bytes()
.windows(2)
.any(|w| w[0].is_ascii_whitespace() && w[1].is_ascii_whitespace());
if !has_leading_whitespace && !has_trailing_whitespace && !has_multiple_whitespace {
return None;
}
// Check for unnecessary whitespace
let has_leading_whitespace = value_str.starts_with(char::is_whitespace);
let has_trailing_whitespace = value_str.ends_with(char::is_whitespace);
// Check for multiple consecutive whitespace characters (spaces, tabs, newlines)
let chars: Vec<char> = value_str.chars().collect();
let has_multiple_whitespace = chars
.windows(2)
.any(|w| w[0].is_whitespace() && w[1].is_whitespace());
if !has_leading_whitespace && !has_trailing_whitespace && !has_multiple_whitespace {
return None;
}
🤖 Prompt for AI Agents
In
crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
around lines 80-91, replace the mixed use of char::is_whitespace (Unicode-aware)
and is_ascii_whitespace (ASCII-only) when detecting consecutive whitespace:
iterate over characters (e.g., via chars() and pairing adjacent chars or
collecting into a char slice and using windows) and use char::is_whitespace for
both leading/trailing and consecutive-whitespace checks so Unicode whitespace
(e.g., NBSP) is handled consistently; update the has_multiple_whitespace logic
to operate on chars instead of raw bytes and keep the rest of the conditional
unchanged.

Comment on lines 116 to 123
// Check for multiple consecutive whitespace characters
let has_multiple_whitespace = value_str
.as_bytes()
.windows(2)
.any(|w| w[0].is_ascii_whitespace() && w[1].is_ascii_whitespace());
if has_multiple_whitespace {
issues.push("multiple consecutive whitespace characters");
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Same whitespace check inconsistency.

Lines 117-120 repeat the ASCII-only check. If you update run(), update this to match.

🤖 Prompt for AI Agents
In
crates/biome_js_analyze/src/lint/nursery/no_unnecessary_tailwind_whitespace.rs
around lines 116 to 123, the consecutive-whitespace check still uses an
ASCII-only byte-based windows check; change it to a Unicode-aware char-based
check to match the updated run() behavior — e.g., iterate over chars (zip
current and next char or use windows over char indices) and use
char::is_whitespace() for both chars, then push the same issue when any pair of
consecutive whitespace chars is found.

@ematipico
Copy link
Member

ematipico commented Dec 30, 2025

noUnnecessaryTailwindWhitespace

I really don't think we need a rule for this. We already useSortedClasses that does that

language: "jsx",
recommended: false,
fix_kind: FixKind::Safe,
issue_number: Some("6502"),
Copy link
Member

Choose a reason for hiding this comment

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

I don't think an issue_number is relevant here, but we should add a sources array with better-tailwinda

language: "jsx",
recommended: false,
fix_kind: FixKind::Safe,
issue_number: Some("1274"),
Copy link
Member

Choose a reason for hiding this comment

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

Same here

- Remove `noUnnecessaryTailwindWhitespace` rule (redundant with `useSortedClasses`)
- Remove `issue_number` from `noDuplicateTailwindClasses`
- Add `EslintBetterTailwindcss` to `RuleSource` enum
- Add `sources` attribution to `noDuplicateTailwindClasses`
- Update changeset to reflect single rule

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
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_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs (1)

175-182: Consider preserving the original quote style for consistency.

The JsLiteralMemberName case uses ctx.preferred_quote() directly without checking the original node's quote style, unlike JsStringLiteralExpression (Line 164-167) and JsxString (Line 184-187) which both inspect the original token first. This inconsistency might unnecessarily change quote styles during the fix.

🔎 Suggested refactor to preserve original quote style
 AnyClassStringLike::JsLiteralMemberName(string_literal) => {
-    let replacement = js_literal_member_name(if ctx.preferred_quote().is_double() {
+    let is_double_quote = string_literal
+        .value()
+        .ok()
+        .and_then(|v| v.text().chars().next())
+        .map(|c| c == '"')
+        .unwrap_or(ctx.preferred_quote().is_double());
+    let replacement = js_literal_member_name(if is_double_quote {
         js_string_literal(deduplicated)
     } else {
         js_string_literal_single_quotes(deduplicated)
     });
     mutation.replace_node(string_literal.clone(), replacement);
 }
📜 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 abeaa21 and 8fd7703.

⛔ Files ignored due to path filters (3)
  • crates/biome_diagnostics_categories/src/categories.rs is excluded by !**/categories.rs and included by **
  • crates/biome_js_analyze/src/lint/nursery.rs is excluded by !**/nursery.rs and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateTailwindClasses/invalid.jsx.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (3)
  • .changeset/tailwind-utility-class-rules.md
  • crates/biome_analyze/src/rule.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • .changeset/tailwind-utility-class-rules.md
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.rs: Use inline rustdoc documentation for rules, assists, and their options
Use the dbg!() macro for debugging output in Rust tests and code
Use doc tests (doctest) format with code blocks in rustdoc comments; ensure assertions pass in tests

Files:

  • crates/biome_analyze/src/rule.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
🧠 Learnings (21)
📓 Common learnings
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/lint/nursery/**/*.rs : Add `issue_number` field to `declare_lint_rule!` macro for work-in-progress rules
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Commit rule work with message format `feat(biome_<language>_analyze): <ruleName>`
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Use `RuleSource::Eslint(...).same()` when implementing a rule that matches the behavior of an ESLint rule
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Use `RuleSource::Eslint(...).inspired()` when implementing a rule inspired by but with different behavior than an ESLint rule
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Add `sources` field with `RuleSource` to cite ESLint or other rules that inspired the implementation

Applied to files:

  • crates/biome_analyze/src/rule.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Use `RuleSource::Eslint(...).inspired()` when implementing a rule inspired by but with different behavior than an ESLint rule

Applied to files:

  • crates/biome_analyze/src/rule.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Use `RuleSource::Eslint(...).same()` when implementing a rule that matches the behavior of an ESLint rule

Applied to files:

  • crates/biome_analyze/src/rule.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Use language-specific rule names if the rule is meant for a specific language only

Applied to files:

  • crates/biome_analyze/src/rule.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Implement `action` function in Rule trait to provide code actions

Applied to files:

  • crates/biome_analyze/src/rule.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Use generic rule names if the rule could potentially be implemented for multiple languages

Applied to files:

  • crates/biome_analyze/src/rule.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Assist rules should detect refactoring opportunities and emit code action signals

Applied to files:

  • crates/biome_analyze/src/rule.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Set `version` field to `next` in `declare_lint_rule!` macro

Applied to files:

  • crates/biome_analyze/src/rule.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Set `language` field in `declare_lint_rule!` macro to the language the rule primarily applies to

Applied to files:

  • crates/biome_analyze/src/rule.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Use `declare_lint_rule!` macro to declare analyzer rule types and implement the RuleMeta trait

Applied to files:

  • crates/biome_analyze/src/rule.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/lint/**/*.rs : Lint rules should check syntax according to language specification and emit error diagnostics

Applied to files:

  • crates/biome_analyze/src/rule.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/lint/**/*.rs : Lint rules should perform static analysis of source code to detect invalid or error-prone patterns and emit diagnostics with proposed fixes

Applied to files:

  • crates/biome_analyze/src/rule.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Set `language` to `jsx`, `ts`, or `tsx` for rules that only apply to specific JavaScript dialects

Applied to files:

  • crates/biome_analyze/src/rule.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Use `domains` field in `declare_lint_rule!` to tag rules that belong to specific concepts like testing or frameworks

Applied to files:

  • crates/biome_analyze/src/rule.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.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_analyze/src/rule.rs
📚 Learning: 2025-12-21T21:15:03.796Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: CONTRIBUTING.md:0-0
Timestamp: 2025-12-21T21:15:03.796Z
Learning: Applies to **/*.rs : Use inline rustdoc documentation for rules, assists, and their options

Applied to files:

  • crates/biome_analyze/src/rule.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/lint/nursery/**/*.rs : Add `issue_number` field to `declare_lint_rule!` macro for work-in-progress rules

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
📚 Learning: 2025-12-22T09:26:56.943Z
Learnt from: ematipico
Repo: biomejs/biome PR: 8537
File: crates/biome_js_analyze/src/lint/nursery/no_leaked_render.rs:167-210
Timestamp: 2025-12-22T09:26:56.943Z
Learning: When defining lint rules (declare_lint_rule!), only specify fix_kind if the rule implements an action(...) function. Rules that only emit diagnostics without a code fix should omit fix_kind. This applies to all Rust lint rule definitions under crates/.../src/lint (e.g., crates/biome_js_analyze/src/lint/...).

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/lint/nursery/**/*.rs : Place new rules inside the `nursery` group during development

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
📚 Learning: 2025-12-19T12:53:30.413Z
Learnt from: CR
Repo: biomejs/biome PR: 0
File: crates/biome_analyze/CONTRIBUTING.md:0-0
Timestamp: 2025-12-19T12:53:30.413Z
Learning: Applies to crates/biome_analyze/**/*analyze/src/**/*.rs : Specify `fix_kind: FixKind::Safe` in `declare_lint_rule!` for safe code actions

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs
🔇 Additional comments (11)
crates/biome_analyze/src/rule.rs (5)

109-110: LGTM! Enum variant follows established pattern.

The new EslintBetterTailwindcss variant is properly documented and structured consistently with other ESLint plugin variants in the codebase.


190-190: LGTM! Display implementation is consistent.

The display name follows the eslint-plugin-{name} convention used by other ESLint plugins.


276-276: LGTM! Pattern match correctly extracts rule name.

The implementation follows the established pattern for extracting rule names from source variants.


320-322: LGTM! Namespacing follows plugin conventions.

The better-tailwindcss/{rule_name} format is consistent with how other ESLint plugins are namespaced in the codebase.


366-366: The URL format is correct.

The upstream repository confirms this documentation structure exists and is accessible. Looks good to merge.

crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs (6)

1-13: LGTM!

The imports are well-organised and all appear to be used appropriately for the rule implementation.


15-76: LGTM!

The rule declaration correctly follows Biome conventions, and the sources field properly attributes the upstream ESLint rule as requested by the maintainer.


78-85: LGTM!

The state structure appropriately uses boxed types for owned data.


93-131: LGTM!

The duplicate detection logic correctly preserves first-occurrence order and efficiently tracks duplicates using FxHashSet.


133-156: LGTM!

The diagnostic message is clear, properly pluralised, and provides helpful guidance.


201-210: LGTM!

The action creation follows the standard pattern with appropriate metadata and a clear message.

@mldangelo
Copy link
Author

Thanks @ematipico and @Netail for the review feedback! I've addressed both points:

  1. Removed noUnnecessaryTailwindWhitespace - @ematipico you're right that useSortedClasses already handles whitespace normalization via split_whitespace() + join(" "). No need for a separate rule. We should think about how to document this for tailwind users.
  2. Updated noDuplicateTailwindClasses per @Netail's feedback.

The PR now contains just the single noDuplicateTailwindClasses rule ported from https://github.com/schoero/eslint-plugin-better-tailwindcss.

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants