Skip to content

feat(lint): add nursery rule useNamedCaptureGroup#9048

Open
ff1451 wants to merge 7 commits intobiomejs:mainfrom
ff1451:feat/prefer-named-capture-group/#8737
Open

feat(lint): add nursery rule useNamedCaptureGroup#9048
ff1451 wants to merge 7 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: de6612f

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

No actionable comments were generated in the recent review. 🎉


Walkthrough

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

Suggested reviewers

  • dyc3
  • ematipico
  • Netail
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ⚠️ Unable to check for merge conflicts: Invalid branch name format
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely describes the main change: adding a new nursery lint rule called useNamedCaptureGroup.
Description check ✅ Passed The description comprehensively explains the rule's purpose, supported patterns, testing approach, and acknowledges use of AI assistance.
Linked Issues check ✅ Passed The PR fully implements the objectives from issue #8737: new lint rule, support for regex literals and RegExp constructors, dynamic pattern handling, shadowed RegExp detection, and comprehensive tests.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the useNamedCaptureGroup lint rule; no unrelated 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
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch feat/prefer-named-capture-group/#8737
  • Post resolved changes as copyable diffs 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
⏩ 95 skipped benchmarks1


Comparing ff1451:feat/prefer-named-capture-group/#8737 (de6612f) with main (89d6384)2

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.

  2. No successful run was found on main (6edd600) during the generation of this report, so 89d6384 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

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 47 to 69
///
/// ```js
/// /(?<id>ba[rz])/;
/// ```
///
/// ```js
/// /(?:ba[rz])/;
/// ```
///
/// ```js
/// /ba[rz]/;
/// ```
///
/// ```js
/// /(?<year>[0-9]{4})-(?<month>[0-9]{2})/;
/// ```
///
/// ```js
/// new RegExp("(?<id>foo)");
/// ```
///
/// ```js
/// new RegExp(pattern);
Copy link
Member

Choose a reason for hiding this comment

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

Valid cases can stay in one single code 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’ve grouped the valid cases into a single code block.

b7bbb12

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 from fcce15a to 7caad19 Compare February 16, 2026 09:35
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

2 participants