Skip to content

Conversation

@mldangelo
Copy link

@mldangelo mldangelo commented Dec 30, 2025

Summary

Adds the noDuplicateClasses assist action to detect and remove duplicate CSS classes.

For JSX files: Supports class, className attributes and utility functions like clsx, cn, cva.

For HTML files: Checks class attributes. This is the first assist action for HTML.

Inspired by eslint-plugin-better-tailwindcss/no-duplicate-classes.

// Before
<div class="flex p-4 flex" />;

// After
<div class="flex p-4" />;

Test Plan

Snapshot tests for valid/invalid cases:

  • crates/biome_js_analyze/tests/specs/source/noDuplicateClasses/ (JSX)
  • crates/biome_html_analyze/tests/specs/source/noDuplicateClasses/ (HTML)

All tests passing.

Docs

Documentation included in rustdoc comments within the rule implementations.


This PR was written primarily by Claude Code.

@changeset-bot
Copy link

changeset-bot bot commented Dec 30, 2025

🦋 Changeset detected

Latest commit: 0083e9a

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

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

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 source rule NoDuplicateClasses that detects and removes duplicate CSS classes in JSX and HTML class attributes and utility function calls (clsx, cn, cva). The change includes making the any_class_string_like module public, adding support for ESLint Better Tailwindcss rules via a new RuleSource variant with URL mapping, creating the rule implementation with duplicate detection and safe fix capability, defining rule options through a type alias, adding comprehensive test coverage for valid and invalid scenarios, and updating documentation for callout blocks.

Possibly related PRs

Suggested labels

A-Project

Suggested reviewers

  • ematipico
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding a new noDuplicateClasses assist action to detect and remove duplicate CSS classes.
Description check ✅ Passed The PR description clearly describes the noDuplicateClasses assist action, its scope (JSX and HTML), supported attributes/utilities, and includes a concrete before/after example.

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


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.

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.

@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

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.

@mldangelo mldangelo changed the title feat(linter): add 2 Tailwind CSS utility class lint rules feat(linter): add noDuplicateTailwindClasses nursery rule Dec 30, 2025
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)

145-149: Curious <Emphasis> placement.

The <Emphasis> tag wraps just the "es" pluralisation suffix, which would render the "es" as bold/highlighted. Consider moving the emphasis to duplicates_str or removing it entirely for cleaner output.

🔎 Suggested fix
 markup! {
-    "Duplicate CSS utility class"<Emphasis>{
-        if state.duplicates.len() > 1 { "es" } else { "" }
-    }</Emphasis>" detected: "{duplicates_str}
+    "Duplicate CSS utility class"{
+        if state.duplicates.len() > 1 { "es" } else { "" }
+    }" detected: "{duplicates_str}
 },
📜 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 73152a5 and fff0dc7.

📒 Files selected for processing (1)
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.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_duplicate_tailwind_classes.rs
🧠 Learnings (11)
📓 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/**/*.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
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>`
📚 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/**/*.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_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_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_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
📚 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_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_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/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
🧬 Code graph analysis (1)
crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs (2)
crates/biome_js_factory/src/make.rs (3)
  • js_string_literal (24-31)
  • js_string_literal_single_quotes (34-41)
  • js_template_chunk (63-70)
crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs (3)
  • run (172-189)
  • diagnostic (191-209)
  • action (211-261)
🔇 Additional comments (4)
crates/biome_js_analyze/src/lint/nursery/no_duplicate_tailwind_classes.rs (4)

15-76: Well-documented rule declaration.

Good examples, proper attribution via sources, and the options documentation is helpful for users already familiar with useSortedClasses.


78-85: LGTM!

State struct is well-documented with appropriate types.


93-130: Logic is sound.

The duplicate detection correctly preserves first occurrences whilst collecting duplicates. The implicit whitespace normalisation via split_whitespace + join is a nice touch.


157-213: Action implementation handles all node types correctly.

Quote style preservation is properly implemented for each variant, and the fallback to preferred_quote / preferred_jsx_quote is appropriate. Consistent with the existing useSortedClasses pattern.

@mldangelo
Copy link
Author

mldangelo commented Dec 30, 2025

Ready for re-review

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/tests/specs/nursery/noDuplicateTailwindClasses/valid.jsx (1)

1-19: Solid test coverage for valid cases.

The test cases appropriately cover the essential scenarios: no duplicates, similar-but-different classes (e.g., p-4 vs px-4), empty strings, single classes, and both quote styles. This should prevent false positives.

Optional: Consider adding function call test cases

Since the PR objectives mention the rule reuses UseSortedClassesOptions (supporting functions configuration), you might consider adding valid test cases for utility functions:

// Valid: function calls with no duplicates
clsx("flex", "p-4");
cn("text-red-500", "bg-white");

This would ensure the rule handles function calls correctly when configured. However, if these are covered in the invalid test file or are intentionally omitted, feel free to disregard.

📜 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 fff0dc7 and d0ff300.

⛔ 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/noDuplicateTailwindClasses/valid.jsx.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (2)
  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateTailwindClasses/invalid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateTailwindClasses/valid.jsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateTailwindClasses/invalid.jsx
🧰 Additional context used
🧠 Learnings (3)
📓 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/**/*.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
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
📚 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/noDuplicateTailwindClasses/valid.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/noDuplicateTailwindClasses/valid.jsx

Copy link
Contributor

@dyc3 dyc3 left a comment

Choose a reason for hiding this comment

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

You should an HTML rule for this as well!

Copy link
Contributor

Choose a reason for hiding this comment

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

This is currently a lint rule. Would it make more sense for this to be an assist rule? @ematipico

Copy link
Author

Choose a reason for hiding this comment

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

Copied from #8623 (comment)

I believe this should remain a lint rule because:

  • Duplicate classes are objectively problematic (redundant, potential merge conflict artifacts)
  • There's no valid use case for intentional duplicates
  • The fix is always safe (removing exact duplicates doesn't change semantics)

Copy link
Member

Choose a reason for hiding this comment

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

All these reasons are good reasons why it should be an assist action 🤣

Copy link
Author

Choose a reason for hiding this comment

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

@ematipico Great, I will convert it to assist.

I noticed HTML doesn't have assist infrastructure yet (no assist/ directory exists). I looked at how CSS and JSON set theirs up and it seems straightforward to replicate.

Should I:

  1. Create HTML assist infrastructure and put both rules there, or
  2. Just convert JS to assist and leave HTML as lint for now?

Happy to take either approach.

Copy link
Author

Choose a reason for hiding this comment

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

@dyc3 @ematipico I updated the PR to make the js rule an assist rule and kept html as a lint. Hope this is ok.

Copy link
Author

Choose a reason for hiding this comment

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

I've now converted both HTML and JSX to assist rules. Created the HTML assist infrastructure following the CSS/JS pattern:

@ematipico @Netail Ready for re-review when you have a chance! Thank you!

@mldangelo mldangelo force-pushed the pr/tailwind-simple-rules branch from d0ff300 to f426f29 Compare December 31, 2025 03:42
@github-actions github-actions bot added the L-HTML Language: HTML and super languages label Dec 31, 2025
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 (2)
crates/biome_js_analyze/tests/specs/nursery/noDuplicateClasses/valid.jsx (1)

1-19: Consider adding valid template literal test cases.

The invalid tests include template literals with duplicates. Adding corresponding valid cases would improve coverage:

// Valid: template literals without duplicates
<div className={`flex p-4 ${condition}`} />;
<div className={`${prefix} p-4 m-2 ${suffix}`} />;
crates/biome_html_analyze/src/lint/nursery/no_duplicate_classes.rs (1)

8-8: Consider using FxHashSet for consistency.

The JS version of this rule uses rustc_hash::FxHashSet, which is faster for small string keys. For consistency and performance:

-use std::collections::HashSet;
+use rustc_hash::FxHashSet;

Then update the usages on lines 93-94 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 d0ff300 and f426f29.

⛔ Files ignored due to path filters (7)
  • crates/biome_diagnostics_categories/src/categories.rs is excluded by !**/categories.rs and included by **
  • crates/biome_html_analyze/src/lint/nursery.rs is excluded by !**/nursery.rs and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noDuplicateClasses/invalid.html.snap is excluded by !**/*.snap and included by **
  • crates/biome_html_analyze/tests/specs/nursery/noDuplicateClasses/valid.html.snap is excluded by !**/*.snap 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/noDuplicateClasses/invalid.jsx.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateClasses/valid.jsx.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (9)
  • .changeset/no-duplicate-classes-rule.md
  • crates/biome_analyze/src/rule.rs
  • crates/biome_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_html_analyze/tests/specs/nursery/noDuplicateClasses/invalid.html
  • crates/biome_html_analyze/tests/specs/nursery/noDuplicateClasses/valid.html
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs
  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateClasses/invalid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateClasses/valid.jsx
✅ Files skipped from review due to trivial changes (1)
  • crates/biome_html_analyze/tests/specs/nursery/noDuplicateClasses/invalid.html
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/biome_analyze/src/rule.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/use_sorted_classes.rs
  • crates/biome_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_classes.rs
🧠 Learnings (24)
📚 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:

  • .changeset/no-duplicate-classes-rule.md
  • crates/biome_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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/use_sorted_classes.rs
  • crates/biome_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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/use_sorted_classes.rs
  • crates/biome_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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 : 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/**/*.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/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_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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/use_sorted_classes.rs
  • crates/biome_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_classes.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
📚 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_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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/**/*invalid* : Create test files prefixed with `invalid` for code that should trigger the rule

Applied to files:

  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateClasses/invalid.jsx
  • crates/biome_html_analyze/tests/specs/nursery/noDuplicateClasses/valid.html
  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateClasses/valid.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/**/*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/noDuplicateClasses/invalid.jsx
  • crates/biome_html_analyze/tests/specs/nursery/noDuplicateClasses/valid.html
  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateClasses/valid.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 : Set `language` to `jsx`, `ts`, or `tsx` for rules that only apply to specific JavaScript dialects

Applied to files:

  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateClasses/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/noDuplicateClasses/invalid.jsx
  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateClasses/valid.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 `RuleSource::Eslint(...).inspired()` when implementing a rule inspired by but with different behavior than an ESLint rule

Applied to files:

  • crates/biome_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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 : The first paragraph of rule documentation must be a single line describing what the rule does

Applied to files:

  • crates/biome_html_analyze/src/lint/nursery/no_duplicate_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_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_classes.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_html_analyze/tests/specs/nursery/noDuplicateClasses/valid.html
📚 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_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: Run `just l` to lint analyzer rule code

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_classes.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: For new lint rules in changesets, show an example of invalid case in inline code or code block

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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_js_analyze/src/lint/nursery/no_duplicate_classes.rs
🧬 Code graph analysis (3)
crates/biome_js_analyze/tests/specs/nursery/noDuplicateClasses/invalid.jsx (1)
crates/biome_js_analyze/tests/specs/nursery/noUndeclaredEnvVars/validDynamicAccess.js (1)
  • prefix (8-8)
crates/biome_html_analyze/src/lint/nursery/no_duplicate_classes.rs (2)
crates/biome_analyze/src/rule.rs (2)
  • sources (642-645)
  • same (255-260)
crates/biome_html_factory/src/make.rs (1)
  • html_string_literal (10-17)
crates/biome_js_analyze/src/lint/nursery/no_duplicate_classes.rs (3)
crates/biome_js_factory/src/make.rs (3)
  • js_string_literal (24-31)
  • js_string_literal_single_quotes (34-41)
  • js_template_chunk (63-70)
crates/biome_analyze/src/rule.rs (2)
  • same (255-260)
  • recommended (627-630)
crates/biome_html_analyze/src/lint/nursery/no_duplicate_classes.rs (4)
  • run (69-119)
  • diagnostic (121-144)
  • state (123-127)
  • action (146-175)
🔇 Additional comments (7)
crates/biome_js_analyze/src/lint/nursery/use_sorted_classes.rs (1)

1-1: LGTM!

Good call making any_class_string_like public to allow reuse by the new NoDuplicateClasses rule, avoiding code duplication.

crates/biome_html_analyze/tests/specs/nursery/noDuplicateClasses/valid.html (1)

1-16: LGTM!

Good coverage of valid HTML class attribute scenarios. The test cases appropriately mirror the JSX tests.

.changeset/no-duplicate-classes-rule.md (1)

1-12: LGTM!

Clear and informative changeset entry. Good attribution to the source plugin.

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

1-23: LGTM!

Comprehensive test coverage for invalid cases. Good inclusion of edge cases like arbitrary values, complex variant classes, and template literals.

crates/biome_html_analyze/src/lint/nursery/no_duplicate_classes.rs (1)

12-49: LGTM!

Well-documented rule with clear examples. The version: "next" and sources with .same() are correctly configured per guidelines.

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

200-228: LGTM!

Thoughtful handling of template literal boundaries. The logic correctly preserves spaces when adjacent to template expressions, ensuring fixes like ${prefix} p-4 p-4 ${suffix}${prefix} p-4 ${suffix} maintain proper spacing.


16-77: LGTM!

Excellent documentation with clear examples. Good reuse of UseSortedClassesOptions for consistent configuration across related rules.

@mldangelo
Copy link
Author

mldangelo commented Dec 31, 2025

Thanks for the feedback @dyc3!

Addressing your points:

  1. HTML rule added - Created noDuplicateClasses for HTML that checks class attributes. The implementation follows existing HTML rule patterns in the codebase.
  2. Renamed from noDuplicateTailwindClasses to noDuplicateClasses - You're right that the detection logic isn't Tailwind-specific (it's just split_whitespace + duplicate detection). The rule now has a more accurate, general-purpose name.
  3. Lint vs Assist - I believe this should remain a lint rule because:
    • Duplicate classes are objectively problematic (redundant, potential merge conflict artifacts)
    • There's no valid use case for intentional duplicates
    • The fix is always safe (removing exact duplicates doesn't change semantics)

This contrasts with useSortedClasses which is more subjective (sorting preference).

Finally, the JS rule shares options with useSortedClasses to control which attributes (class, className, custom) and functions (clsx, cn, cva, etc.) are checked. The HTML rule only checks class attributes since that's the standard HTML attribute.

@mldangelo mldangelo changed the title feat(linter): add noDuplicateTailwindClasses nursery rule feat(linter): add noDuplicateClasses nursery rule for JS and HTML Dec 31, 2025
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 (2)
crates/biome_html_analyze/src/lint/nursery/no_duplicate_classes.rs (2)

93-93: Consider using FxHashSet for consistency with the JS rule.

The JS rule (line 106-107) uses rustc_hash::FxHashSet, which is typically faster for small sets. Using std::collections::HashSet here works fine, but FxHashSet would be more consistent across the codebase.

Suggested change
-use std::collections::HashSet;
+use rustc_hash::FxHashSet;

And in run():

-        let mut seen: HashSet<&str> = HashSet::default();
-        let mut duplicate_set: HashSet<&str> = HashSet::default();
+        let mut seen: FxHashSet<&str> = FxHashSet::default();
+        let mut duplicate_set: FxHashSet<&str> = FxHashSet::default();

175-183: Helpful utility for single-quoted HTML strings.

Clean helper function. Consider adding a brief rustdoc comment explaining when this is needed (i.e., when make::html_string_literal defaults to double quotes).

📜 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 f426f29 and a971cb5.

⛔ Files ignored due to path filters (1)
  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateClasses/valid.jsx.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (3)
  • crates/biome_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/tests/specs/nursery/noDuplicateClasses/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_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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/**/*.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/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/noDuplicateClasses/valid.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/noDuplicateClasses/valid.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/noDuplicateClasses/valid.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/noDuplicateClasses/valid.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/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_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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_html_analyze/src/lint/nursery/no_duplicate_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_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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 : The first paragraph of rule documentation must be a single line describing what the rule does

Applied to files:

  • crates/biome_html_analyze/src/lint/nursery/no_duplicate_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_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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_duplicate_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_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: Run `just l` to lint analyzer rule code

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_classes.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: For new lint rules in changesets, show an example of invalid case in inline code or code block

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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_js_analyze/src/lint/nursery/no_duplicate_classes.rs
📚 Learning: 2025-11-21T01:10:53.059Z
Learnt from: dyc3
Repo: biomejs/biome PR: 8171
File: crates/biome_js_analyze/src/lint/nursery/no_leaked_render.rs:125-137
Timestamp: 2025-11-21T01:10:53.059Z
Learning: In the Biome codebase, each lint rule has its own options type declaration (e.g., `type Options = RuleNameOptions`) as part of the codegen process, even if the options struct is empty or unused. This is standard practice and should not be flagged as an issue.

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_classes.rs
🧬 Code graph analysis (1)
crates/biome_js_analyze/src/lint/nursery/no_duplicate_classes.rs (1)
crates/biome_html_analyze/src/lint/nursery/no_duplicate_classes.rs (4)
  • run (69-119)
  • diagnostic (121-141)
  • state (123-127)
  • action (143-172)
🔇 Additional comments (14)
crates/biome_html_analyze/src/lint/nursery/no_duplicate_classes.rs (6)

1-11: LGTM!

Imports are well-organised and minimal for the rule's requirements.


12-50: Well-documented rule declaration.

The rustdoc is clear with good examples. Rule metadata is correct: nursery placement, version: "next", FixKind::Safe, and proper RuleSource attribution with .same(). As per coding guidelines, inline rustdoc is the right approach here.


52-61: Clear state struct with inline documentation.

The rustdoc comments on each field are helpful. One minor observation: the is_single_quote field is specific to the HTML rule (the JS rule doesn't need it since it handles quote detection differently in action()).


69-119: Solid deduplication logic.

The algorithm correctly preserves insertion order for the deduplicated result whilst tracking duplicates separately. Sorting duplicates (line 112) ensures deterministic diagnostic output across runs.


121-141: Clean diagnostic implementation.

The pluralisation logic is now handled cleanly with a pre-formatted message string, addressing the previous review feedback about the <Emphasis> wrapper.


143-172: Quote preservation in auto-fix looks correct.

The action properly detects single vs double quotes and creates the replacement token accordingly. Using the helper function for single quotes keeps the code tidy.

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

1-23: Good test coverage for valid cases.

The test file covers a solid range of scenarios: no duplicates, similar-but-different classes (e.g., p-4 px-4 py-4), empty/single class, quote styles, and template literals. This aligns with the learnings for creating valid-prefixed test files.

crates/biome_js_analyze/src/lint/nursery/no_duplicate_classes.rs (7)

1-15: Well-structured imports.

Using FxHashSet from rustc_hash is the right choice for performance. Reusing UseSortedClassesOptions from the existing useSortedClasses rule is excellent for configuration consistency.


16-77: Comprehensive rule documentation.

Excellent rustdoc with clear examples for both invalid and valid cases. The options section nicely references the useSortedClasses rule, making it clear to users that configuration is shared. Rule metadata is correct with .same() for the ESLint source.


79-86: Leaner state struct than the HTML counterpart.

Sensibly omits is_single_quote since the JS rule detects quote style directly from the node in action().


94-132: Clean run implementation leveraging shared infrastructure.

The should_visit check (line 98) properly delegates to the options, ensuring this rule respects the same attribute/function configuration as useSortedClasses. The deduplication algorithm mirrors the HTML rule correctly.


134-154: Diagnostic matches HTML rule.

Pluralisation and formatting are consistent with the HTML implementation. Good to see the same diagnostic style across both rules.


197-225: Careful handling of template chunk boundaries.

The logic to preserve leading/trailing spaces when adjacent to template expressions is thoughtful. This ensures \${prefix} foo bar ${suffix}`` doesn't lose the spaces that separate the interpolated values from the class names.

One edge case to consider: if deduplicated is empty (all classes were duplicates of each other), you'd end up with just a space. However, since duplicate_set.is_empty() returns early on line 120, there's always at least one class in deduplicated_parts, so this is safe.


160-196: Quote style preservation across node types.

Each match arm correctly detects the original quote style and uses the appropriate factory function. The fallback to preferred_quote() / preferred_jsx_quote() when the token is unavailable is a sensible default.

@ematipico
Copy link
Member

@mldangelo from now on, can you please keep our issue template and stick to it? We aren't interested in future works, past works, etc.

If you use AI, please make sure to at least do so as long as it aligns with our templates. Or write stuff yourself.

Please update the template with the information we require.

@mldangelo
Copy link
Author

@mldangelo from now on, can you please keep our issue template and stick to it? We aren't interested in future works, past works, etc.

If you use AI, please make sure to at least do so as long as it aligns with our templates. Or write stuff yourself.

Please update the template with the information we require.

@ematipico appreciate the feedback! I removed references to other things I would like to port (is there a good place to discuss these or should I just open PRs?). I've also updated the description to follow the template. Let me know if there's anything else I should adjust.

@github-actions github-actions bot added A-CLI Area: CLI A-Project Area: project labels Dec 31, 2025
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

📜 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 a971cb5 and 8779fb8.

⛔ Files ignored due to path filters (4)
  • crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs is excluded by !**/migrate/eslint_any_rule_to_biome.rs and included by **
  • crates/biome_configuration/src/analyzer/linter/rules.rs is excluded by !**/rules.rs and included by **
  • packages/@biomejs/backend-jsonrpc/src/workspace.ts is excluded by !**/backend-jsonrpc/src/workspace.ts and included by **
  • packages/@biomejs/biome/configuration_schema.json is excluded by !**/configuration_schema.json and included by **
📒 Files selected for processing (4)
  • crates/biome_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_rule_options/src/lib.rs
  • crates/biome_rule_options/src/no_duplicate_classes.rs
✅ Files skipped from review due to trivial changes (1)
  • crates/biome_rule_options/src/lib.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_rule_options/src/no_duplicate_classes.rs
  • crates/biome_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_classes.rs
🧠 Learnings (26)
📓 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/**/*.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/**/biome_rule_options/lib/**/*.rs : Use `deny_unknown_fields` in serde derive macro for rule options

Applied to files:

  • crates/biome_rule_options/src/no_duplicate_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/**/biome_rule_options/lib/**/*.rs : Wrap rule options fields in `Option<>` to properly track set and unset options during merge

Applied to files:

  • crates/biome_rule_options/src/no_duplicate_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/**/biome_rule_options/lib/**/*.rs : Rule options struct must derive `Deserializable`, `Serialize`, `Deserialize`, and optionally `JsonSchema`

Applied to files:

  • crates/biome_rule_options/src/no_duplicate_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/**/biome_rule_options/lib/**/*.rs : Use `rename_all = "camelCase"` in serde derive macro for rule options

Applied to files:

  • crates/biome_rule_options/src/no_duplicate_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/**/biome_rule_options/lib/**/*.rs : Implement `Merge` trait for rule options to support configuration inheritance

Applied to files:

  • crates/biome_rule_options/src/no_duplicate_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/**/biome_rule_options/lib/**/*.rs : Rule options must be placed inside the `biome_rule_options` crate

Applied to files:

  • crates/biome_rule_options/src/no_duplicate_classes.rs
📚 Learning: 2025-11-21T01:10:53.059Z
Learnt from: dyc3
Repo: biomejs/biome PR: 8171
File: crates/biome_js_analyze/src/lint/nursery/no_leaked_render.rs:125-137
Timestamp: 2025-11-21T01:10:53.059Z
Learning: In the Biome codebase, each lint rule has its own options type declaration (e.g., `type Options = RuleNameOptions`) as part of the codegen process, even if the options struct is empty or unused. This is standard practice and should not be flagged as an issue.

Applied to files:

  • crates/biome_rule_options/src/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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 : Rule documentation must include `## Options` section if the rule has options

Applied to files:

  • crates/biome_rule_options/src/no_duplicate_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 `options` code block property for rule-specific configuration snippets in documentation

Applied to files:

  • crates/biome_rule_options/src/no_duplicate_classes.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 : No module may copy or clone data from another module in the module graph, not even behind an `Arc`

Applied to files:

  • crates/biome_rule_options/src/no_duplicate_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_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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 : The first paragraph of rule documentation must be a single line describing what the rule does

Applied to files:

  • crates/biome_html_analyze/src/lint/nursery/no_duplicate_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_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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_html_analyze/src/lint/nursery/no_duplicate_classes.rs
  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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_html_analyze/src/lint/nursery/no_duplicate_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_duplicate_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_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: Run `just l` to lint analyzer rule code

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_classes.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: For new lint rules in changesets, show an example of invalid case in inline code or code block

Applied to files:

  • crates/biome_js_analyze/src/lint/nursery/no_duplicate_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_js_analyze/src/lint/nursery/no_duplicate_classes.rs
🧬 Code graph analysis (1)
crates/biome_js_analyze/src/lint/nursery/no_duplicate_classes.rs (2)
crates/biome_js_factory/src/make.rs (3)
  • js_string_literal (24-31)
  • js_string_literal_single_quotes (34-41)
  • js_template_chunk (63-70)
crates/biome_html_analyze/src/lint/nursery/no_duplicate_classes.rs (4)
  • run (69-119)
  • diagnostic (121-143)
  • state (123-127)
  • action (145-174)
🔇 Additional comments (5)
crates/biome_html_analyze/src/lint/nursery/no_duplicate_classes.rs (2)

69-119: Deduplication logic is sound.

The use of HashSet for tracking seen and duplicate classes is appropriate, and the logic correctly preserves the first occurrence of each class whilst identifying duplicates.


145-185: Quote-style preservation works correctly.

The fix logic properly detects and maintains the original quote style (single vs double) when constructing the replacement token.

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

88-92: Good reuse of UseSortedClassesOptions.

Sharing configuration with useSortedClasses ensures consistent handling of attributes and functions options across related rules.


199-227: Template chunk space preservation is correct.

The logic to preserve leading/trailing spaces when a template chunk is adjacent to template expressions (e.g. `${var} flex flex`) is necessary and correctly implemented. Without this, the fix would produce `${var}flex` instead of `${var} flex`.

crates/biome_rule_options/src/no_duplicate_classes.rs (1)

1-6: This struct is generated as part of Biome's standard lint rule codegen pattern.

Even empty or unused options structs are expected to exist in biome_rule_options/ for each rule, whether or not the corresponding rule actually references them. This is intentional and not an issue.

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.

The rules/actions require more work:

  • diagnostics don't follow our standards
  • newlines aren't handled
  • sorting can break people's code

@mldangelo
Copy link
Author

The rules/actions require more work:

  • diagnostics don't follow our standards
  • newlines aren't handled
  • sorting can break people's code

@ematipico

I believe I addressed all three points:

  1. Diagnostics: Rewrote to follow rule pillars
  2. Newlines: Added tests for \n and \t, documented whitespace collapsing behavior
  3. Sorting: Added comment clarifying sorting only affects diagnostic message ordering, not fix output

Please let me know if you'd like to see anything else and happy new year!

mldangelo and others added 7 commits January 13, 2026 00:48
- 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]>
- 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]>
Replace O(n) linear search with FxHashSet for tracking which
duplicates have already been recorded. This improves worst-case
performance from O(n²) to O(n) for class strings with many duplicates.

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

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Make JsLiteralMemberName consistent with JsStringLiteralExpression
and JsxString by checking the original token's quote style before
falling back to ctx.preferred_quote().

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

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add test cases for single-quoted JSX strings to verify quote style
preservation in the code fix.

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

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Address maintainer feedback:
- Rename from noDuplicateTailwindClasses to noDuplicateClasses (more general)
- Add HTML rule for detecting duplicate classes in HTML files
- The logic works for any CSS classes, not just Tailwind utilities

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

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Sort duplicate class names for deterministic diagnostic messages
- Preserve template literal boundary spaces when fixing duplicates
- Add test cases for template literals with expressions
@mldangelo mldangelo marked this pull request as draft January 13, 2026 08:51
mldangelo and others added 15 commits January 13, 2026 00:54
- Clean up pluralization logic by using pre-formatted message string
- Add valid template literal test cases for better coverage
- Update changeset to use "new nursery rule" wording and add example
- Remove "Inspired by" line from changeset
- Store HtmlString in state to avoid repeating operations in action()
- Use FxHashSet for consistency with JS rule
- Add comment explaining sorting is for deterministic diagnostics only
- Improve diagnostics to follow rule pillars
- Add tests for newlines and tabs
- Document whitespace collapsing behavior
Convert the JS noDuplicateClasses rule from a lint rule (lint/nursery)
to an assist action (assist/source) per maintainer feedback.

Changes:
- Use declare_source_rule! macro instead of declare_lint_rule!
- Update diagnostic category to assist/source/noDuplicateClasses
- Use rule_action_category!() for action metadata
- Move tests from specs/nursery/ to specs/source/
- Update snapshots to reflect new category

The rule behavior remains the same: it detects and offers to remove
duplicate CSS classes in JSX class/className attributes.

Note: HTML version remains as a lint rule for now (HTML assist
infrastructure does not yet exist).
- Remove redundant sentence about helping keep class strings clean
- Use consistent absolute URL format for useSortedClasses links
- Tighten wording in Options section

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

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Address maintainer feedback from ematipico:

1. **Preserve whitespace** - Unlike useSortedClasses which is
   Tailwind-specific, this rule is general-purpose and should
   only remove duplicates without altering formatting.

2. **Remove sorting** - The duplicate list sorting was only for
   deterministic diagnostic output but adds unnecessary overhead
   since this rule runs on every class attribute.

3. **Unified behavior** - Both JS assist and HTML lint rule now
   use identical whitespace-preserving tokenization logic.

Changes:
- Replaced split_whitespace().join(" ") with position-tracking tokenizer
- Remove duplicate class while preserving surrounding whitespace
- Updated test comments to reflect new behavior
- Simplified template chunk handling (whitespace now preserved by core logic)

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

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Replace inefficient `format!("{}", s)` with `s.as_ref()`
- Remove accidentally committed plan file

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

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add missing test cases:

**JS Invalid:**
- Utility function calls: cn("flex flex"), clsx("p-4 m-2 p-4")
- Object member names: cn({ "flex flex": isActive })
- Multiple spaces: "flex  p-4  flex"
- Leading/trailing whitespace: " flex flex "
- Triple duplicate: "flex flex flex"

**JS Valid:**
- Utility functions without duplicates
- Object member names without duplicates
- Whitespace-only class (no classes to duplicate)
- Case-sensitive classes: "Flex flex" (different in CSS)

**HTML Invalid/Valid:**
- Multiple spaces, leading/trailing whitespace
- Triple duplicate
- Whitespace-only, case-sensitive

All tests verify whitespace preservation behavior.

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

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Update HTML rule to use NoDuplicateClassesOptions (type alias for
  UseSortedClassesOptions) for schema/config consistency
- Clarify changeset: JSX uses assist.source, HTML uses linter.nursery
- Regenerate configuration, schema, and TypeScript bindings

The ordering in diagnostics is deterministic via insertion-order Vec;
FxHashSet is only used for O(1) containment checks, not iteration.

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

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Apply @ematipico's suggested doc comment wording for clearer, more
action-oriented documentation.

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

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Remove rustc-hash dependency from biome_html_analyze by using the
standard library HashSet. This aligns with other HTML rules in the
codebase.
Convert the HTML noDuplicateClasses rule from a lint/nursery rule
to an assist/source action. This is the first assist action for HTML.

- Move rule from lint/nursery to assist/source
- Update diagnostic category from lint/nursery to assist/source
- Add assist module infrastructure for HTML analyzer
- Move test specs from nursery to source directory
- Update changeset to minor (new feature)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
The noDuplicateClasses rule was converted to an assist action,
so the lint category entry is no longer needed.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@mldangelo mldangelo force-pushed the pr/tailwind-simple-rules branch from 35cef9b to ca9092d Compare January 13, 2026 09:03
@github-actions github-actions bot removed the A-Tooling Area: internal tools label Jan 13, 2026
- Fix duplicate header comments in generated files
- Run configuration and bindings codegen to properly register
  noDuplicateClasses as assist/source instead of lint/nursery
- Update configuration_schema.json with correct assist action URL
- Update workspace.ts with assist/source/noDuplicateClasses category

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@github-actions github-actions bot added the A-Project Area: project label Jan 13, 2026
mldangelo and others added 3 commits January 13, 2026 08:58
- Create NoDuplicateClassesOptions struct with Deref to UseSortedClassesOptions
- This generates NoDuplicateClassesConfiguration instead of reusing UseSortedClassesConfiguration
- Fixes duplicate TypeScript type definition error
- Fixes schema using lint config (warn/error) instead of assist config (on/off)
- Fix source attribution: both JS and HTML now use .inspired()
- Update doc comments to not reference lint rule

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…ointer cast

Replace the unsafe pointer cast in Deref implementation with a safe
newtype wrapper pattern: `struct NoDuplicateClassesOptions(UseSortedClassesOptions)`

This fixes undefined behavior - the previous unsafe cast relied on
identical memory layout which is not guaranteed for #[repr(Rust)] structs.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
…h options

Add test case with options.json to verify that cn, clsx, and cva
utility functions are correctly checked for duplicate classes when
the functions option is configured.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-CLI Area: CLI A-Diagnostic Area: diagnostocis A-Linter Area: linter A-Project Area: project L-HTML Language: HTML and super languages L-JavaScript Language: JavaScript and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants