mruby-regexp: stop capturing plain groups once a pattern has a named group - #7057
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe regexp compiler detects named groups before parsing, makes unnamed groups non-capturing in named-group patterns, rejects numeric backreferences, and shares POSIX bracket parsing. Tests and documentation cover these semantics. ChangesNamed group semantics
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Pattern
participant ExtendedModePreprocessor
participant NamedGroupScanner
participant RegexpCompiler
participant BackreferenceParser
Pattern->>ExtendedModePreprocessor: preprocess extended-mode syntax
ExtendedModePreprocessor->>NamedGroupScanner: provide preprocessed pattern
NamedGroupScanner->>RegexpCompiler: report named-group mode
RegexpCompiler->>RegexpCompiler: demote unnamed groups
RegexpCompiler->>BackreferenceParser: compile backreferences
BackreferenceParser-->>RegexpCompiler: reject numeric forms in named-group patterns
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
mrbgems/mruby-regexp/test/regexp.rb (1)
1132-1166: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTest a plain group before the named group.
The pre-scan is required when a plain group appears before the named-group declaration. The current mixed-group case puts the named group first. Add a reverse-order case to protect the pre-scan behavior.
Proposed test
assert_equal ["ab", "a"], md.to_a assert_equal ["a"], md.captures assert_nil md[2] assert_raise_with_message(IndexError, "index 2 out of matches") { md.begin(2) } assert_equal "a", md[:a] + + md = /(a)(?<b>b)/.match("ab") + assert_equal 2, md.size + assert_equal ["ab", "b"], md.to_a + assert_equal "b", md[:b]🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mrbgems/mruby-regexp/test/regexp.rb` around lines 1132 - 1166, Add a reverse-order mixed-group assertion to the Regexp test: match a pattern where the plain capturing group appears before the named group, such as `(b)(?<a>a)`, and verify the plain group is demoted while the named capture remains available. Place it alongside the existing mixed-group case in the `Regexp - a named group makes plain groups non-capturing` test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@mrbgems/mruby-regexp/test/regexp.rb`:
- Around line 1132-1166: Add a reverse-order mixed-group assertion to the Regexp
test: match a pattern where the plain capturing group appears before the named
group, such as `(b)(?<a>a)`, and verify the plain group is demoted while the
named capture remains available. Place it alongside the existing mixed-group
case in the `Regexp - a named group makes plain groups non-capturing` test.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: d4304cc2-fef7-4093-97ed-0b3038f44135
📒 Files selected for processing (2)
mrbgems/mruby-regexp/src/re_compile.cmrbgems/mruby-regexp/test/regexp.rb
2baa944 to
36349ba
Compare
|
This needs a rebase. Six of the regexp changes landed together just now, up to 1f3f28b, and they touch For the record on what went in, so you can see what your branch is landing on top of:
I verified the six together against CRuby before merging: sixteen rows, all agreeing, and the suite clean under ASan and UBSan. Nothing in that set is aimed at what this pull request changes, so I expect the rebase to be mechanical, mostly in the test file. If it turns out not to be, say so and I will look at the interaction rather than have you work around it. |
36349ba to
49e7896
Compare
|
Rebased onto 1f3f28b, now at 49e7896. One conflict, and not in the test file: Three things followed from that rename rather than from the conflict itself:
The one interaction worth checking was #7055 against the pre-scan: a /(?# (?<a>x )(b)/.match("b").to_a # => ["b", "b"], as in CRubyI also took CodeRabbit's suggestion and added the reverse order, where the plain group precedes the declaration that demotes it. That is the case the pre-scan exists for, and the branch had only been covering the other order: md = /(a)(?<b>b)/.match("ab")
md.size # => 2
md.captures # => ["b"]
md[1] # => "b"
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@mrbgems/mruby-regexp/src/re_compile.c`:
- Around line 1203-1209: Update skip_posix_bracket to validate the remaining
distance from q to end before evaluating the second-character terminator check;
only read q[1] when at least two characters remain, while preserving the
existing successful `:]` return and NULL behavior for truncated brackets.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 2b4cea8b-f621-4f09-98d3-5c222e787c4c
📒 Files selected for processing (3)
mrbgems/mruby-regexp/README.mdmrbgems/mruby-regexp/src/re_compile.cmrbgems/mruby-regexp/test/regexp.rb
🚧 Files skipped from review as they are similar to previous changes (1)
- mrbgems/mruby-regexp/README.md
The scan that copies `[:name:]` through `preprocess_pattern()` walks `q` to the bracket's `:` and then tests `q + 1 < end` before reading `q[1]`. For a bracket the pattern truncates, as in `/[[:alpha/x`, the walk stops with `q == end`, and `q + 1` then forms a pointer past the one-past-the-end position. ISO C leaves that undefined whether or not anything reads through it, and here nothing does: the `&&` stops at the comparison. Compare `end - q >= 2` instead, which is how the `(?#` test a few lines below already spells the same question. No behaviour change. A truncated bracket still falls through to the parser, which reports the unterminated class, and the two spellings that reach the stopped-at-`end` case are pinned as tests.
The preprocessing pass has to know that a POSIX bracket's `]` does not end a character class, because `compile_charclass()` consumes `[:name:]` as a unit. A second scan over the same bytes is about to need the same test, so lift it into a helper rather than write it twice. No behaviour change. The helper returns the position just past the bracket's closing `]`, or `NULL` when the bracket is malformed, which is the same fall-through the inline code had.
…group
CRuby turns on Onigmo's `ONIG_OPTION_DONT_CAPTURE_GROUP` for any pattern that
declares at least one named group: a plain `(...)` then groups without
capturing, and a numbered backreference is a compile error. This gem captured
both kinds side by side, so a pattern that mixes them had a different number
of groups in the two implementations and every numbered accessor disagreed.
```ruby
md = /(?<a>a)(b)/.match("ab")
md.size # CRuby: 2, mruby: 3
md.to_a # CRuby: ["ab", "a"], mruby: ["ab", "a", "b"]
md.captures # CRuby: ["a"], mruby: ["a", "b"]
md[2] # CRuby: nil, mruby: "b"
md.begin(2) # CRuby: IndexError (index 2 out of matches), mruby: 1
"ab" =~ /(?<a>a)(b)/
$2 # CRuby: nil, mruby: "b"
$+ # CRuby: "a", mruby: "b"
"ab".sub(/(?<a>a)(b)/, '[\2]') # CRuby: "[]", mruby: "[b]"
Regexp.new("(a)(?<b>b)\\1")
```
Named access already agreed in every case. `md[:a]`, `Regexp#named_captures`,
`Regexp#names` and `MatchData#names` were never affected; only the numbered
side differed.
Whether a plain group captures depends on a named group that may appear later
in the pattern, so `mrb_re_compile()` now pre-scans for `(?<` before it starts
allocating group numbers. The scan runs on the same bytes the parser will
read, after `preprocess_pattern()` has run, so free-spacing, `#` comments and
`(?#...)` comment groups are already gone. It skips escape pairs and character
classes, which keeps `/\(?/` and `/[(?<]/` from being false positives, and it
excludes `(?<=` and `(?<!`, which open a lookbehind rather than define a
group. `(?'name'...)` is not a spelling this
gem accepts, so `(?<` is the only form to look for. A truncated `(?<` still
raises from the parser, as `Regexp.new("(?<")` asserts.
`compile_atom()` then demotes a plain group to non-capturing and rejects a
numbered backreference in both its `\1` and its `\k<1>` / `\k<-1>` spellings.
The `\k` branch matters as much as the `\1` one: once plain groups stop
consuming numbers, its absolute bound and its relative `num_captures - n`
would resolve to a different group instead of erroring.
Everything downstream already derives from `pat->num_captures`, which is now
smaller for an affected pattern, so no accessor needed an edit.
The README gains a Named Captures section stating the rule, and the `\k<name>`
row its Pattern Syntax list was missing, since that is what a named pattern
has to use in place of a numbered backreference.
49e7896 to
fb83b7f
Compare
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@mrbgems/mruby-regexp/test/regexp.rb`:
- Around line 1315-1320: Add a narrowly scoped RuboCop disable directive around
the assert_nil $2 assertion in the regexp test, specifically suppressing
Lint/OutOfRangeRegexpRef while preserving the intentional nil assertion and
keeping lint checks enabled for surrounding code.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 445da97d-4289-407d-b9ed-0200525d51a7
📒 Files selected for processing (2)
mrbgems/mruby-regexp/src/re_compile.cmrbgems/mruby-regexp/test/regexp.rb
🚧 Files skipped from review as they are similar to previous changes (1)
- mrbgems/mruby-regexp/src/re_compile.c
In CRuby, a single named group turns every plain
(...)in the same pattern into anon-capturing group and makes a numbered backreference a compile error.
mruby-regexpcaptures both kinds side by side, so a pattern that mixes them has a different number of
groups in the two implementations, and every numbered accessor disagrees.
Named access agrees in every case already:
md[:a],md["a"],Regexp#named_captures,Regexp#namesandMatchData#namesall report the same thing. Only the numbered sidediffers.
The rule is Onigmo's
ONIG_OPTION_DONT_CAPTURE_GROUP, which CRuby enables for a patternthat declares at least one named group. It is not a corner case:
(...)used purely forgrouping or alternation is common, and code carried over from CRuby silently sees its
capture numbers shift by however many plain groups the pattern has.
What this changes
Whether a plain group captures depends on a named group that may appear later in the
pattern, so
mrb_re_compile()pre-scans for(?<before it starts allocating groupnumbers. The scan reads the same bytes the parser will read, after the
/xstrip, sofree-spacing and comments are already gone. Three details it has to get right, each
checked against the parser and pinned by a test:
/\(?/and/[(?<]/are not falsepositives. That skipping is shared with
strip_extended()for the[:name:]case,which the first commit lifts into
skip_posix_bracket().(?<=and(?<!, which open a lookbehind rather than define a group.\k<name>is a reference, not a definition.(?'name'...)is not a spelling this gemaccepts at all, so
(?<is the only form to look for.(?<still raises from the parser, asRegexp.new("(?<")asserts. Thepre-scan counts those bytes as a named group, which is harmless, but it is not the
thing that decides the error.
compile_atom()then demotes a plain group to non-capturing, and rejects a numberedbackreference in both its
\1and its\k<1>/\k<-1>spellings. The\kbranchmatters as much as the
\1one: once plain groups stop consuming numbers, its absolutebound and its relative
num_captures - nwould resolve to a different group instead oferroring.
The only representation that changes is
pat->num_captures, which shrinks for anaffected pattern. Every numbered accessor already derives from it, so
regexp.candre_exec.cneed no edit, and that is what makes the$2,$+,subandmd.begin(2)rows above agree, down to the
IndexErrormessage.The gem README gains a Named Captures section stating the rule, and the
\k<name>row its Pattern Syntax list was missing.Alternative considered
Documenting the difference instead. mruby's behaviour was a superset: every pattern CRuby
accepts still matched the same text, and the only patterns that behaved differently were
the ones CRuby refuses to compile or whose numbered groups it discards. Keeping it costs
nothing at compile time. There is a precedent for pinning a deliberate gap as a test
rather than closing it (599856d, for the
to_strpattern ofString#match). Whatargues the other way here is that this difference is silent and changes results rather
than raising, so I went with following CRuby. Happy to turn it into a documented
difference instead if you prefer.
Out of scope
Two adjacent gaps on the same patterns are left alone, since neither follows from the
capture-numbering rule:
\1in a replacement string when the pattern has namedcaptures, even though
md[1]still answers. After this change"ab".sub(/(?<a>a)b/, '[\1]')is
"[a]"here against"[]"in CRuby.\k<name>in a replacement string is not supported by this gem; it stays literal.Testing
rake testpasses. No existing test mixed a named group with a plain capturing group, sonothing in the suite had to change. New coverage in
mrbgems/mruby-regexp/test/regexp.rbtakes the table above row by row, plus the lookbehind, escaped, character-class,
[:alpha:],/xand truncated(?<cases, and the three rejected backreferencespellings with their messages.
Summary by CodeRabbit
Bug Fixes
Documentation
Tests