mruby-regexp: fix MatchData#begin / #end for a group name and an out-of-range index - #7048
Conversation
…out-of-range index
`matchdata_begin()` and `matchdata_end()` took their argument with
`mrb_get_args(mrb, "i", &idx)`, so a group name was a `TypeError` before the
method body ran, and an index that named no group returned `nil` instead of
raising.
```ruby
md = /(?<_x>a)/.match("a")
md.begin(:_x) # CRuby: 0, mruby: TypeError (Symbol cannot be converted to Integer)
md.begin("_x") # CRuby: 0, mruby: TypeError (String cannot be converted to Integer)
md.begin(:zz) # CRuby: IndexError (undefined group name reference: zz)
# mruby: TypeError (Symbol cannot be converted to Integer)
md = /(a)(b)/.match("ab")
md.begin(3) # CRuby: IndexError (index 3 out of matches), mruby: nil
md.begin(-1) # CRuby: IndexError (index -1 out of matches), mruby: nil
md.end(-1) # CRuby: IndexError (index -1 out of matches), mruby: nil
```
`begin` and `end` return an offset, and `nil` is not one, so an argument they
cannot use is an error rather than a missing result. That is stricter than
`MatchData#[]` on purpose: `[]` has `nil` to return for a group that did not
participate and reuses it for an index out of range, while `begin` has no such
value to return.
Both now take their argument as `"o"`. A String or Symbol resolves through the
pattern's named-capture table, and an Integer outside `0...num_captures`,
negative included, raises `IndexError`. A group that exists but did not
participate still returns `nil`:
```ruby
/(a)|(b)/.match("a").begin(2) # both: nil
```
The name lookup is the loop `matchdata_aref()` already ran, so it moves into a
shared `matchdata_name_to_group()`, together with the `RE_NAME_LEN_FITS()` bound
that keeps its `memcmp()` from being handed a length larger than what was
measured, and with the `IndexError` all three methods want. Nothing about
`MatchData#[]` changes: its negative-index rule and its `nil` for an index out
of range are its own and stay where they are.
`__byte_begin` and `__byte_end` stay on `"i"`. They are private helpers for
`String#gsub` and `#split`, are only ever called with the literal `0`, and
should not pay for the lookup.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthrough
ChangesMatchData named capture access
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested labels: 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 |
MatchData#beginand#endtake their argument withmrb_get_args(mrb, "i", &idx),so a group name is a
TypeErrorbefore the method body runs, and an index that namesno group returns
nilinstead of raising.Both halves matter for the same reason:
beginandendreturn an offset, andnilis not one. Arithmetic on the result of a mistyped index fails somewhere else entirely,
while CRuby stops at the call.
What changes
matchdata_begin()andmatchdata_end()take their argument as"o"and resolve itbefore reading the capture:
IndexErrorwithundefined group name reference: NAMEwhen it is not there0...num_captures, negative included, raisesIndexErrorwithindex N out of matchesThis is deliberately stricter than
MatchData#[], and CRuby is stricter here too:[]returnsnilfor an index out of range because a group that did not participateis also
nil, whereasbeginhas no such value to return.[]'s rules are untouched,including the negative index it normalizes and the
nilit returns out of range.A group that exists but did not participate is still
nil, which is the one resultthat must not become a raise:
The name lookup was the loop
matchdata_aref()already ran. It moves into a sharedmatchdata_name_to_group()and gainsbeginandendas callers. TheRE_NAME_LEN_FITS()bound travels with it rather than staying behind: it is what keepsthe
(uint32_t)name_lencast in the loop lossless, sobeginandendget theover-long name rejected for free. The
IndexErrorfor an unknown name moves with it aswell, since all three methods want the same message.
__byte_beginand__byte_endstay on"i"and keep returningnil. They are private,are called only as
md.__byte_begin(0)fromString#gsuband#splitinmrblib/string_regexp.rb, and should not pay for the lookup on every iteration.The bindings do not change:
beginandendwere alreadyMRB_ARGS_REQ(1), which iscorrect for an argument that is now an object rather than an integer.
Verification
Before:
After, matching CRuby 4.0.6 on every line of the table above:
Tests
mrbgems/mruby-regexp/test/regexp.rbgains three blocks: the name cases beside theexisting
MatchData#begin / #endassertion, the out-of-range cases including thenon-participating group that must stay
nil, and the over-long name beside theMatchData#[]regression test for the same bound, now that the lookup is shared.rake testpasses: 1970 assertions, 0 failures, no new compiler warnings.Summary by CodeRabbit
New Features
MatchData#beginandMatchData#endnow support named capture groups using strings or symbols.Bug Fixes
IndexError.nil.