mruby-regexp: reject non-Regexp patterns in String#match and #match? - #6994
Conversation
`String#match` and `#match?` converted a `String` argument to a `Regexp` and handed `self` straight to anything else, so a pattern of the wrong type reached a method call it does not respond to. The failure named the argument as the receiver, which says nothing about the pattern being wrong. ```ruby "abc".match(:b) # CRuby: TypeError, mruby: NoMethodError for :b "abc".match(nil) # CRuby: TypeError, mruby: NoMethodError for nil ``` Resolve the pattern in one helper so both methods reject the same set, and follow CRuby in naming `nil`, `true` and `false` by value and everything else by class. The check runs before `pos` is inspected, matching CRuby's order. The helper is private, so the gem does not grow `String`'s public API with a method only `match` and `match?` call. ```ruby "abc".match(:b) # TypeError: wrong argument type Symbol (expected Regexp) "abc".match(nil, 99) # TypeError on the pattern, not an error about pos ``` CRuby also accepts an object that defines `to_str`, converting it through `rb_check_string_type` before the type check. mruby has no implicit `String` conversion in core, so `"a" + obj` and `"abc".index(obj)` already raise `TypeError` for such an object; honouring `to_str` in `match` alone would make the gem more permissive than the core it sits on. Rejecting `to_str` objects therefore stays an intentional difference from CRuby. `String#=~` deliberately keeps its current behaviour. CRuby's `rb_str_match` special-cases `String` only and dispatches everything else as `other =~ self`. ```ruby "abc" =~ 1 # CRuby: NoMethodError, mruby: NoMethodError "abc" =~ nil # CRuby: nil, mruby: NoMethodError ``` A blanket type check would turn both into `TypeError`, losing the case mruby already matches and freezing the other in a state it can never grow out of. `Object#=~` was removed in Ruby 3.2, so `nil` is the only receiver left to dispatch to, and the NoMethodError becomes CRuby's `nil` the moment core gains `NilClass#=~`. That is a core fix rather than one for this gem. This becomes more urgent once `Symbol#match` exists (mruby#6993). `Symbol#match` and `#match?` delegate to `to_s`, so without the check the receiver and the argument swap places and the call quietly succeeds. ```ruby "abc".match(:b) # without the check: :b.to_s.match("abc") -> nil # with the check: TypeError ```
📝 WalkthroughWalkthrough
ChangesString pattern validation
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Caller
participant StringMatch
participant RegexpMatchPattern
participant RegexpOperation
Caller->>StringMatch: call match or match?
StringMatch->>RegexpMatchPattern: validate pattern
RegexpMatchPattern-->>StringMatch: return Regexp or raise TypeError
StringMatch->>RegexpOperation: perform matching
RegexpOperation-->>Caller: return match result
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 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.
Actionable comments posted: 2
🤖 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/mrblib/string_regexp.rb`:
- Around line 15-16: The pattern type checks in __match_pattern currently
dispatch is_a? on unsupported objects; replace them with non-dispatching class
predicates such as Regexp === re and String === re so unsupported patterns raise
TypeError rather than reaching a missing match method. Add a regression test
using an object that overrides is_a? and verify the required TypeError.
In `@mrbgems/mruby-regexp/test/regexp.rb`:
- Around line 596-597: Update the precedence tests around String#match to pass
an invalid pos such as Object.new, then assert the TypeError message identifies
the invalid pattern rather than position validation; add the equivalent case for
String#match? to verify pattern validation takes precedence there as well.
🪄 Autofix (Beta)
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: d398674f-f063-4dd4-8c71-d060774a5efe
📒 Files selected for processing (2)
mrbgems/mruby-regexp/mrblib/string_regexp.rbmrbgems/mruby-regexp/test/regexp.rb
`__match_pattern` asked `re.is_a?(Regexp)`, so an object overriding `is_a?` could pose as a Regexp, reach `.match(self, pos)` and raise NoMethodError where CRuby raises TypeError. ```ruby class Liar def is_a?(klass) = true end "abc".match(Liar.new) #=> CRuby: TypeError (wrong argument type Liar (expected Regexp)) #=> mruby: NoMethodError (undefined method 'match' for an instance of Liar) ``` `Module#===` is implemented in C and does not dispatch back to the argument, so the check now goes through `Regexp === re` and `String === re`.
The test called `"abc".match(nil, 99)`, but `99` is a valid position, so the TypeError it asserted came from the pattern whether or not pos is validated first. Pass `Object.new` as pos and assert the pattern's message, which only holds if the pattern is rejected first, and cover `String#match?` too.
CRuby resolves a pattern through `rb_check_string_type`, so an object that defines `to_str` is converted and used as the pattern. ```ruby class Pat; def to_str; "b"; end; end "abc".match(Pat.new) # CRuby: #<MatchData "b">, mruby: TypeError ``` mruby has no implicit String conversion in core: its same-named `mrb_check_string_type` only checks the type, and `"a" + obj`, `"a" << obj` and `"abc".index(obj)` all reject that same object. Honouring `to_str` in `String#match` alone would make the gem more permissive than the core it sits on, so the argument is named by class like any other. Cover it so the difference reads as a decision rather than an oversight.
…` test The pattern check went from `is_a?` to `Module#===` so that an argument overriding `is_a?` cannot pose as a Regexp. Both forms accept a subclass of the tested class, and that acceptance is the part a future rewrite of the check could silently drop, so exercise it directly.
The check resolving the pattern given to `String#match` and `#match?` lived in a private `String#__match_pattern` written in Ruby. That placed it where the argument could steer it: the type name in the message came from `re.class`, so an argument redefining `class` chose its own name, and a `String` subclass defining a method named `__match_pattern` took over the check and widened what `match` and `match?` accept. Move it to `Regexp.__match_pattern` in C. The kind test and the class name are read from the object without dispatching, and `String` carries no helper for a subclass to redefine.
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/regexp.c`:
- Line 1137: Remove the public class-method registration of __match_pattern in
the regexp initialization code around regexp_match_pattern, and route pattern
resolution through a C-only mechanism instead. If the method must remain
exposed, add coverage proving direct Regexp.__match_pattern redefinition cannot
alter the resolver, including subclass behavior.
🪄 Autofix (Beta)
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: 06f78069-f25b-4f03-9f7c-e3c199af7b89
📒 Files selected for processing (3)
mrbgems/mruby-regexp/mrblib/string_regexp.rbmrbgems/mruby-regexp/src/regexp.cmrbgems/mruby-regexp/test/regexp.rb
🚧 Files skipped from review as they are similar to previous changes (1)
- mrbgems/mruby-regexp/mrblib/string_regexp.rb
String#matchand#match?converted aStringargument to aRegexpand handed
selfstraight to anything else, so a pattern of the wrongtype reached a method call it does not respond to. The failure named the
argument as the receiver, which says nothing about the pattern being
wrong.
Resolve the pattern in one helper so both methods reject the same set,
and follow CRuby in naming
nil,trueandfalseby value andeverything else by class. The check runs before
posis inspected,matching CRuby's order.
The helper is
Regexp.__match_pattern, written in C, so nothing about thecheck dispatches back to the argument. In Ruby it would, in two ways.
is_a?is a normal method call, so an argument that overrides it can poseas a
Regexp, reach.match(self, pos)and raiseNoMethodErrorwhereCRuby raises
TypeError. Reading the type name fromre.classlikewiselets the argument choose the name the message reports. The C helper reads
the kind and the class name off the object without dispatching.
Putting it on
Regexprather thanStringalso keeps it out of reach ofthe receiver:
Stringcarries no helper for a subclass to redefine, andString's public API does not grow a method onlymatchandmatch?call.
The kind test accepts a subclass of the tested class, and that acceptance
is the part a future rewrite of the check could silently drop, so the
tests exercise a
Regexpand aStringsubclass directly.CRuby also accepts an object that defines
to_str, converting it throughrb_check_string_typebefore the type check. mruby has no implicitStringconversion in core, so"a" + objand"abc".index(obj)already raise
TypeErrorfor such an object; honouringto_strinmatchalone would make the gem more permissive than the core it sitson. Rejecting
to_strobjects therefore stays an intentionaldifference from CRuby.
String#=~deliberately keeps its current behaviour. CRuby'srb_str_matchspecial-casesStringonly and dispatches everything elseas
other =~ self.A blanket type check would turn both into
TypeError, losing the casemruby already matches and freezing the other in a state it can never grow
out of.
Object#=~was removed in Ruby 3.2, sonilis the onlyreceiver left to dispatch to, and the NoMethodError becomes CRuby's
nilthe moment core gains
NilClass#=~. That is a core fix rather than onefor this gem.
This becomes more urgent once
Symbol#matchexists (#6993).Symbol#matchand#match?delegate toto_s, so without the check thereceiver and the argument swap places and the call quietly succeeds.
A Symbol receiver goes the same way, since
Symbol#matchdelegates toString#matchand inherits whatever check it has.Both receivers are handled by the one helper this PR adds, so #6993 needs
no code of its own for them; only its tests for these rows have to wait
until the check exists.
#6993 touches none of the files this PR changes, so the two merge in either
order without a conflict. The order still matters for behaviour, though.
Merging #6993 first defines
Symbol#match, which gives"abc".match(:b)areceiver to dispatch to: it resolves to
:b.to_s.match("abc")and quietlyreturns nil where it used to raise NoMethodError. Merging this one first
closes that window, and #6993 needs no change either way.
rake testpasses.Summary by CodeRabbit
String#matchandString#match?.TypeErrormessages.