Skip to content

feat(lint): add useClassNameHelper nursery rule#9064

Open
PaulRBerg wants to merge 2 commits intobiomejs:nextfrom
PaulRBerg:feat/no-class-name-template-literal
Open

feat(lint): add useClassNameHelper nursery rule#9064
PaulRBerg wants to merge 2 commits intobiomejs:nextfrom
PaulRBerg:feat/no-class-name-template-literal

Conversation

@PaulRBerg
Copy link
Contributor

This PR was created with AI assistance (Codex).

Summary

  • Add a new nursery lint rule, useClassNameHelper, to flag interpolated template literals in class attributes.
  • Detect interpolated templates across direct and nested class attribute expressions, and update diagnostics to focus on extraction/merging/tooling reliability (not only IntelliSense).
  • Add configurable attributes/helperFunctions options, test fixtures and snapshots, and a changeset that references discussion #8652.

Test Plan

  • INSTA_UPDATE=always cargo test -p biome_js_analyze --test spec_tests use_class_name_helper
  • just gen-rules
  • just gen-configuration
  • just f
  • just l

Docs

  • Rule rustdoc includes examples and option documentation.
  • No separate website PR included.

- Add useClassNameHelper nursery rule with configurable attributes and helper names
- Flag interpolated template literals in class attributes, including nested expressions
- Add fixtures/snapshots and changeset linked to discussion biomejs#8652
@changeset-bot
Copy link

changeset-bot bot commented Feb 14, 2026

🦋 Changeset detected

Latest commit: a06ce5c

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-Project Area: project A-Linter Area: linter L-JavaScript Language: JavaScript and super languages A-Diagnostic Area: diagnostocis labels Feb 14, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 14, 2026

Walkthrough

This PR introduces a new lint rule UseClassNameHelper that detects interpolated template literals within JSX className and class attributes. The implementation includes a configurable rule with defaults for target attributes and helper functions, comprehensive test coverage with valid and invalid patterns, an options module with configuration methods, and accompanying documentation and changeset entry.

Suggested labels

A-Project, A-Linter, L-JavaScript, A-Diagnostic

Suggested reviewers

  • ematipico
  • dyc3
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a new nursery lint rule called useClassNameHelper.
Description check ✅ Passed The description directly relates to the changeset, clearly outlining what the rule does, its configuration options, test plan, and documentation approach.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into next

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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: 2

🤖 Fix all issues with AI agents
In `@crates/biome_js_analyze/src/lint/nursery/use_class_name_helper.rs`:
- Around line 121-133: The helper find_interpolated_template currently searches
the entire subtree (using expression.syntax().descendants()) which can produce
false positives on deeply nested template literals; restrict the search to only
the immediate/semantically relevant positions instead of all descendants — for
example limit traversal depth or only inspect direct value positions such as
call arguments, JSX expression root, ternary branches, or binary/conditional
operands; modify find_interpolated_template (and its use of
JsTemplateExpression::cast and descendants) to walk only those targeted children
of the initial expression (or stop after a fixed depth) so templates inside
unrelated nested constructs (e.g., computed property accesses) are not flagged.

In `@crates/biome_rule_options/src/use_class_name_helper.rs`:
- Around line 20-25: has_attribute currently treats Some([]) as disabling the
rule because as_deref().map_or_else only uses DEFAULT_ATTRIBUTES when attributes
is None; update has_attribute so an empty attributes list falls back to
DEFAULT_ATTRIBUTES (or explicitly document otherwise). Concretely, modify the
logic in has_attribute (and keep using DEFAULT_ATTRIBUTES and contains_name) to
check if attributes.is_empty() and in that case call
DEFAULT_ATTRIBUTES.contains(&name), otherwise call contains_name(attributes,
name).
🧹 Nitpick comments (2)
.changeset/use-class-name-helper-rule.md (1)

5-5: Add an inline example of an invalid case to the changeset.

Per project conventions, changeset descriptions for new lint rules should include an example of the code pattern the rule flags. Something like:

`<div className={`px-4 ${isActive ? "a" : "b"}`} />`

would help end users quickly understand what the rule catches. Based on learnings: "For new lint rules in changesets, show an example of invalid case in inline code or code block."

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

109-119: get_attribute_name only handles simple JSX names, not namespaced ones.

as_jsx_name() won't match namespaced attributes like aria-class or data-class. This is fine for the current defaults (className, class), but if a user configures a namespaced attribute name it would silently never match. A small doc comment or a note in the Options section would save future confusion.

Comment on lines +121 to +133
fn find_interpolated_template(expression: &AnyJsExpression) -> Option<JsTemplateExpression> {
if let AnyJsExpression::JsTemplateExpression(template) = expression
&& !template.is_constant()
{
return Some(template.clone());
}

expression
.syntax()
.descendants()
.filter_map(JsTemplateExpression::cast)
.find(|template| !template.is_constant())
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

descendants() may produce false positives on deeply nested, unrelated templates.

find_interpolated_template searches the entire subtree of the attribute expression. This means a template literal nested inside, say, a computed property access or a callback will also trigger a diagnostic:

<div className={getClass(obj[`key_${i}`])} />
// flagged, but the template is for property access, not class construction

For a nursery rule this is likely acceptable, but worth noting for promotion. If you'd like to tighten it, you could limit the traversal depth or only walk "direct value" positions (call arguments, ternary branches, etc.).

🤖 Prompt for AI Agents
In `@crates/biome_js_analyze/src/lint/nursery/use_class_name_helper.rs` around
lines 121 - 133, The helper find_interpolated_template currently searches the
entire subtree (using expression.syntax().descendants()) which can produce false
positives on deeply nested template literals; restrict the search to only the
immediate/semantically relevant positions instead of all descendants — for
example limit traversal depth or only inspect direct value positions such as
call arguments, JSX expression root, ternary branches, or binary/conditional
operands; modify find_interpolated_template (and its use of
JsTemplateExpression::cast and descendants) to walk only those targeted children
of the initial expression (or stop after a fixed depth) so templates inside
unrelated nested constructs (e.g., computed property accesses) are not flagged.

Comment on lines +20 to +25
pub fn has_attribute(&self, name: &str) -> bool {
self.attributes.as_deref().map_or_else(
|| DEFAULT_ATTRIBUTES.contains(&name),
|attributes| contains_name(attributes, name),
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Empty attributes array silently disables the rule.

If a user sets "attributes": [], has_attribute will never match, effectively making the rule a no-op. This differs from the helper_functions path which explicitly falls back to defaults on empty. Consider whether they should behave consistently — either both fall back to defaults on empty, or document that an empty attributes array disables the rule.

🤖 Prompt for AI Agents
In `@crates/biome_rule_options/src/use_class_name_helper.rs` around lines 20 - 25,
has_attribute currently treats Some([]) as disabling the rule because
as_deref().map_or_else only uses DEFAULT_ATTRIBUTES when attributes is None;
update has_attribute so an empty attributes list falls back to
DEFAULT_ATTRIBUTES (or explicitly document otherwise). Concretely, modify the
logic in has_attribute (and keep using DEFAULT_ATTRIBUTES and contains_name) to
check if attributes.is_empty() and in that case call
DEFAULT_ATTRIBUTES.contains(&name), otherwise call contains_name(attributes,
name).

@codspeed-hq
Copy link

codspeed-hq bot commented Feb 14, 2026

Merging this PR will not alter performance

✅ 58 untouched benchmarks
⏩ 95 skipped benchmarks1


Comparing PaulRBerg:feat/no-class-name-template-literal (a06ce5c) with next (c047e86)

Open in CodSpeed

Footnotes

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

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant