Skip to content

feat(lint): add nursery rule useNamedCaptureGroup#9048

Merged
Netail merged 11 commits intobiomejs:mainfrom
ff1451:feat/prefer-named-capture-group/#8737
Feb 27, 2026
Merged

feat(lint): add nursery rule useNamedCaptureGroup#9048
Netail merged 11 commits intobiomejs:mainfrom
ff1451:feat/prefer-named-capture-group/#8737

Conversation

@ff1451
Copy link
Contributor

@ff1451 ff1451 commented Feb 13, 2026

I used Claude Code to assist with generating test cases and documentation.

Summary

Added the nursery rule useNamedCaptureGroup, which enforces using named capture groups ((?<name>...)) in regular expressions instead of numbered ones ((...)).

This rule corresponds to ESLint's prefer-named-capture-group.

Supports:

  • Regex literals: /(foo)/
  • new RegExp("(foo)") / RegExp("(foo)") constructor calls
  • Dynamic patterns (e.g., new RegExp(pattern)) are safely skipped
  • Shadowed RegExp identifiers are correctly ignored via semantic analysis

Closes #8737

Test Plan

  • just test-lintrule useNamedCaptureGroup — all spec tests pass
  • Invalid cases: regex literals with unnamed groups, new RegExp(...), RegExp(...) constructor calls
  • Valid cases: named groups, non-capturing groups, lookahead/lookbehind, escaped parentheses, character classes, dynamic patterns, shadowed RegExp

Docs

Documentation is included as rustdoc examples in the rule implementation with expect_diagnostic annotations.

@changeset-bot
Copy link

changeset-bot bot commented Feb 13, 2026

🦋 Changeset detected

Latest commit: 4174fcb

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

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

coderabbitai bot commented Feb 13, 2026

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a new nursery lint rule UseNamedCaptureGroup for the Biome JS analyser that detects unnamed capture groups in regex literals and RegExp constructor/call usages, and emits diagnostics encouraging named capture groups. Includes precise per-group ranges for simple string-literal patterns when possible, registration metadata, unit tests (valid and invalid cases), a new UseNamedCaptureGroupOptions type, and a changelog entry.

Suggested reviewers

  • ematipico
  • dyc3
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding a new nursery lint rule for useNamedCaptureGroup.
Description check ✅ Passed The description is well-detailed and clearly relates to the changeset, explaining the rule's purpose, supported patterns, and test coverage.
Linked Issues check ✅ Passed The PR fully implements the linked issue #8737 objectives: the rule enforces named capture groups, detects patterns in literals and constructor calls, safely skips dynamic patterns, ignores shadowed RegExp identifiers, and includes comprehensive tests.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the useNamedCaptureGroup rule. No out-of-scope modifications detected.

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

✨ Finishing Touches
🧪 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.

@codspeed-hq
Copy link

codspeed-hq bot commented Feb 13, 2026

Merging this PR will not alter performance

✅ 58 untouched benchmarks
⏩ 156 skipped benchmarks1


Comparing ff1451:feat/prefer-named-capture-group/#8737 (4174fcb) with main (1f2fe2e)

Open in CodSpeed

Footnotes

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

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

🤖 Fix all issues with AI agents
In `@crates/biome_js_analyze/src/lint/nursery/use_named_capture_group.rs`:
- Around line 229-249: The constructor path currently returns a single
diagnostic covering the whole expression in run_regexp_constructor (return
Box::new([node.range()])), which differs from the literal path that emits one
diagnostic per unnamed group; either (A) adjust run_regexp_constructor to mirror
the literal handling by parsing pattern_text (from extract_pattern_from_args) to
find each unnamed group span, convert those spans into TextRange values relative
to the argument/string literal and return a Box<[TextRange]> containing one
range per unnamed group (use
parse_regexp_node/extract_pattern_from_args/has_unnamed_capture_groups as hooks
to locate the pattern and compute offsets), or (B) if single-diagnostic behavior
is intentional, update the rule metadata where the rule is declared (change
.same() to .inspired() at the rule setup referenced near line 76) so the
reported behavior is documented as differing from ESLint; pick one and implement
accordingly.
🧹 Nitpick comments (2)
crates/biome_js_analyze/src/lint/nursery/use_named_capture_group.rs (2)

85-173: Helper functions should be placed below the impl Rule block.

Per project convention, all helper functions, structs, and enums must be placed below the impl Rule block (the only exception being node union declarations used in the Query type). find_unnamed_capture_groups, has_unnamed_capture_groups, is_regexp_object, parse_regexp_node, and extract_pattern_from_args should all be moved below the impl Rule for UseNamedCaptureGroup block.

Based on learnings: "In crates/biome_analyze/**/*.rs rule files, all helper functions, structs, and enums must be placed below the impl Rule block."


175-208: Semantic<AnyJsExpression> as the query type is broad.

This means run is invoked for every expression node in the file. The early match + Default::default() exit is cheap, so this isn't a blocker — just something to be aware of if profiling shows overhead. A narrower query via declare_node_union! combining JsRegexLiteralExpression, JsNewExpression, and JsCallExpression could reduce invocations.

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.

Nice work! Left some comments

Comment on lines 88 to 89
while i < pattern.len() {
match pattern[i] {
Copy link
Member

Choose a reason for hiding this comment

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

See if you can use as_bytes instead. Then, evaluate the removal of the counter by using an iterator

Copy link
Contributor Author

@ff1451 ff1451 Feb 13, 2026

Choose a reason for hiding this comment

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

Thanks for the suggestion!
I’ve updated the code to use as_bytes and replaced the counter-based loop with an iterator.

f599ba7

Comment on lines +177 to +182
let raw_inner = &token_text[1..token_text.len() - 1];
let inner_text = string_lit.inner_string_text().ok()?;
// If raw source and interpreted text differ, escapes are present
if raw_inner != inner_text.text() {
return None;
}
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand when this is the case. Are there tests for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry for missing the test cases earlier.

raw_inner != inner_text becomes true when the string contains escape sequences.
Without this guard find_unnamed_capture_groups would run on the raw source and produce incorrect offsets.

To clarify the behavior I added tests covering both directions

  • valid: new RegExp("\\(foo)") — using the raw source \\(foo) would incorrectly report ( as an unnamed group, while the actual regex \(foo) has no capture group.

  • invalid: new RegExp("\\d+(foo)") — since escapes are present, precise mapping is skipped but the fallback path correctly detects the unnamed group (foo) using the interpreted string \d+(foo).

fcce15a

@ff1451 ff1451 requested a review from ematipico February 13, 2026 18:31
@ff1451 ff1451 force-pushed the feat/prefer-named-capture-group/#8737 branch 2 times, most recently from de6612f to d020593 Compare February 19, 2026 14:00
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: 1

🧹 Nitpick comments (1)
crates/biome_rule_options/src/use_named_capture_group.rs (1)

3-6: Add a rustdoc comment to UseNamedCaptureGroupOptions.

Per the coding guidelines, options structs need inline rustdoc. Even if the struct is currently empty, a brief doc comment keeps things consistent with the rest of the codebase.

📝 Suggested doc comment
+/// Options for the [UseNamedCaptureGroup](https://biomejs.dev/linter/rules/use-named-capture-group) rule.
 #[derive(Default, Clone, Debug, Deserialize, Deserializable, Merge, Eq, PartialEq, Serialize)]
 #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
 #[serde(rename_all = "camelCase", deny_unknown_fields, default)]
 pub struct UseNamedCaptureGroupOptions {}

As per coding guidelines, "Use inline rustdoc documentation for rules, assists, and their options."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_rule_options/src/use_named_capture_group.rs` around lines 3 - 6,
Add an inline rustdoc comment above the UseNamedCaptureGroupOptions struct
describing its purpose (e.g., "Options for the UseNamedCaptureGroup rule.") and
any notes about default/empty options; ensure the comment follows rustdoc style
(///) and matches other options structs' phrasing so documentation and tooling
pick it up alongside the existing derives and serde attributes on
UseNamedCaptureGroupOptions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_rule_options/src/use_named_capture_group.rs`:
- Around line 1-6: Add a blank line between the import block (the two use
statements for biome_deserialize_macros and serde) and the struct declaration’s
attribute block so the #[derive(...)]/#[cfg_attr(...)] lines are separated from
the use statements; run the formatter (just f) after making this change to
ensure spacing and lint rules are applied for the UseNamedCaptureGroupOptions
struct.

---

Nitpick comments:
In `@crates/biome_rule_options/src/use_named_capture_group.rs`:
- Around line 3-6: Add an inline rustdoc comment above the
UseNamedCaptureGroupOptions struct describing its purpose (e.g., "Options for
the UseNamedCaptureGroup rule.") and any notes about default/empty options;
ensure the comment follows rustdoc style (///) and matches other options
structs' phrasing so documentation and tooling pick it up alongside the existing
derives and serde attributes on UseNamedCaptureGroupOptions.

@ff1451
Copy link
Contributor Author

ff1451 commented Feb 19, 2026

@ematipico I've resolved the conflicts and updated the PR.
Could you please take another look when you have time? Thank you!

Comment on lines 5 to 6
Added the nursery rule [`useNamedCaptureGroup`](https://biomejs.dev/linter/rules/use-named-capture-group/). The rule enforces using named capture groups in regular expressions instead
of numbered ones. It supports both regex literals and `RegExp` constructor calls.
Copy link
Member

@Netail Netail Feb 23, 2026

Choose a reason for hiding this comment

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

Interesting manual line-break, maybe force these breaks after a dot (.)

Comment on lines 1 to 2
---
"@biomejs/biome": patch
Copy link
Member

@Netail Netail Feb 23, 2026

Choose a reason for hiding this comment

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

Unexpected white-spaces after every line? Something Claude added?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you for the suggestions. While using a translation tool during editing, some trailing whitespace was unintentionally introduced. I’ve adjusted the line breaks and removed the extra whitespace. The updates have been pushed.

04605fd

@ff1451 ff1451 requested a review from Netail February 23, 2026 12:53
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.

Looks good. Just need to rebase and rerun codegen to deal with the merge conflicts.

///
/// Returns a list of byte offsets (relative to pattern start) for each
/// unnamed capture group `(` found.
fn find_unnamed_capture_groups(pattern: &str) -> Vec<u32> {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: move all helpers below the impl Rule block

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thanks i fixed it 0f54573

@ff1451 ff1451 force-pushed the feat/prefer-named-capture-group/#8737 branch from 658c7c4 to b017b25 Compare February 26, 2026 16:11
@Netail Netail merged commit 9bbdf4d into biomejs:main Feb 27, 2026
20 checks passed
@github-actions github-actions bot mentioned this pull request Feb 27, 2026
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-JavaScript Language: JavaScript and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

📎 Port prefer-named-capture-group from eslint

4 participants