Skip to content

feat: port rule prefer-named-capture-group - #1829

Open
swwind wants to merge 5 commits into
mainfrom
feat/port-rule-prefer-named-capture-group-20260821
Open

feat: port rule prefer-named-capture-group#1829
swwind wants to merge 5 commits into
mainfrom
feat/port-rule-prefer-named-capture-group-20260821

Conversation

@swwind

@swwind swwind commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Port the prefer-named-capture-group rule from ESLint to rslint.

Enforces using named capture groups instead of numbered capture groups in regular expressions, in regex literals and RegExp()/new RegExp() calls with statically-determinable patterns.

Related Links

Checklist

  • Tests updated (or not required).
  • Documentation updated (or not required).

swwind added 3 commits August 21, 2026 14:39
- read \c outside a character class as Annex B's literal backslash when no
  control letter follows, and reject it under u/v
- fold a regex literal passed to RegExp() to its own source text, so both the
  call and the literal are reported
- reject modifier-group headers with a repeated flag, an overlapping add/remove
  pair, or two empty sets, and accept an empty removal set
- reject \q{...} outside a character class under u/v
…re-group scanner

Differential-tested against regexpp over ~3.4k pattern/flag combinations.

- read the closed u/v escape grammar strictly instead of falling back to an
  identity escape, and resolve u/v backreferences against the groups found
- read a class-body \c against ClassControlLetter, so \c with no control
  letter no longer swallows the character after it
- reject \q outside a character class, and under u inside one
- reject a quantifier with no operand, a second quantifier on one atom, and a
  quantifier on an assertion (lookahead only outside u/v)
- reject a group name that isn't an IdentifierName
- reject a stray ] or } under u/v
@swwind
swwind marked this pull request as ready for review August 21, 2026 07:57
Copilot AI lite review requested due to automatic review settings August 21, 2026 07:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@swwind
swwind requested review from elecmonkey and a lite review from Copilot August 21, 2026 08:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread internal/utils/regex_capture_groups.go
Comment thread internal/utils/regex_capture_groups.go
Comment thread internal/utils/regex_capture_groups.go Outdated
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 25, 2026

Copy link
Copy Markdown

Deploying rslint with  Cloudflare Pages  Cloudflare Pages

Latest commit: ccb33dd
Status:🚫  Build failed.

View logs

@swwind
swwind requested a review from elecmonkey August 25, 2026 13:15
classesValid := true
if !IterateRegexCharacterClasses(pattern, flags, func(start, end int) {
elements, _, parsed := ParseRegexCharacterClassWithEnd(pattern, start, end, flags)
if !parsed {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] Treat failed character-class parsing as invalid

For new RegExp('(a)' + '[a-\\d]', 'u'), ParseRegexCharacterClassWithEnd fails here, but the callback returns without clearing classesValid. ESLint 10.8.1 therefore emits no rule diagnostic, while Rslint reports (a). Failed class parsing—and strict u/v class escapes such as [\\a]—must reject the entire pattern.

for n < len(rest) && isHex(rest[n]) {
n++
}
if n > 1 && n < len(rest) && rest[n] == '}' {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] Reject out-of-range Unicode code point escapes

This branch accepts any non-empty hexadecimal \u{...} escape without checking that its value is at most 0x10FFFF. For new RegExp('(a)' + '\u{110000}', 'u'), ESLint 10.8.1 rejects the pattern and emits no rule diagnostic, while Rslint reports (a).

if pos != len(pattern) {
return nil, false
}
if flags.UV() && !unicodeBackrefsResolve(pattern, flags, groups) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] Validate named references outside Unicode mode

Named backreferences must resolve whenever the pattern contains named captures, even without u or v. With new RegExp('(?<x>a)' + '\k<y>(b)'), regexpp rejects the unresolved y reference and ESLint emits nothing, but this flags.UV() gate skips validation and Rslint reports (b).

continue
case c == '\\':
// A `\u` escape spells the code point out; its value isn't checked.
if i+1 >= len(name) || name[i+1] != 'u' {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] Support braced Unicode escapes in capture names

A capture name may contain a braced Unicode escape under Unicode mode. ESLint 10.8.1 reports (b) for new RegExp('(?<\\u{61}>a)' + '(b)', 'u'), but this scanner advances only past \u, rejects the following {, and suppresses the diagnostic entirely.


switch callee.Kind {
case ast.KindIdentifier:
if callee.AsIdentifier().Text != "RegExp" || utils.IsShadowed(callee, "RegExp") {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] Stop tracking a reassigned RegExp global

ESLint's ReferenceTracker suppresses all references when the global binding has any write, but this check only detects declarations. Consequently RegExp = custom; RegExp('(a)' + '') produces no ESLint diagnostic while Rslint reports (a) as though the call still targeted the built-in constructor.

return isKnownGlobalObject(ctx, access.Expression)
case ast.KindBinaryExpression:
binary := callee.AsBinaryExpression()
if binary == nil || binary.OperatorToken == nil || binary.OperatorToken.Kind != ast.KindCommaToken {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] Trace all ReferenceTracker pass-through expressions

The callee matcher only follows comma expressions, while ReferenceTracker also propagates references through conditional and logical expressions. ESLint reports prefer-named-capture-group for (true ? RegExp : other)('(a)' + '') and (RegExp || other)('(a)' + ''), but Rslint emits no diagnostic; wrapped global objects such as (0, globalThis).RegExp(...) are missed for the same reason.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants