Skip to content

mruby-regexp: reject non-Regexp patterns in String#match and #match? - #6994

Merged
matz merged 6 commits into
mruby:masterfrom
takumin:string-match-argument-type
Aug 2, 2026
Merged

mruby-regexp: reject non-Regexp patterns in String#match and #match?#6994
matz merged 6 commits into
mruby:masterfrom
takumin:string-match-argument-type

Conversation

@takumin

@takumin takumin commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

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.

"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.

"abc".match(:b)       # TypeError: wrong argument type Symbol (expected Regexp)
"abc".match(nil, 99)  # TypeError on the pattern, not an error about pos

The helper is Regexp.__match_pattern, written in C, so nothing about the
check 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 pose
as a Regexp, reach .match(self, pos) and raise NoMethodError where
CRuby raises TypeError. Reading the type name from re.class likewise
lets the argument choose the name the message reports. The C helper reads
the kind and the class name off the object without dispatching.

class Liar
  def is_a?(klass) = true
end

class ClassLiar
  def class = Regexp
end

"abc".match(Liar.new)
# CRuby:  TypeError (wrong argument type Liar (expected Regexp))
# is_a?:  NoMethodError (undefined method 'match' for an instance of Liar)

"abc".match(ClassLiar.new)
# CRuby:     TypeError (wrong argument type ClassLiar (expected Regexp))
# re.class:  TypeError (wrong argument type Regexp (expected Regexp))

Putting it on Regexp rather than String also keeps it out of reach of
the receiver: String carries no helper for a subclass to redefine, and
String's public API does not grow a method only match and match?
call.

class Sub < String
  private def __match_pattern(re) = Regexp.new(re.to_s)
end

Sub.new("abc").match(:abc)
# on String:  a MatchData, the subclass widened what `match` accepts
# on Regexp:  TypeError (wrong argument type Symbol (expected Regexp))

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 Regexp and a String subclass directly.

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.

"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 (#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.

"abc".match(:b)  # without the check: :b.to_s.match("abc") -> nil
                 # with the check:    TypeError

A Symbol receiver goes the same way, since Symbol#match delegates to
String#match and inherits whatever check it has.

:abc.match(:b)   # without the check: nil
                 # with the check:    TypeError
:abc.match?(:b)  # likewise false without the check
:abc.match(nil)  # NoMethodError without the check, TypeError with it

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) a
receiver to dispatch to: it resolves to :b.to_s.match("abc") and quietly
returns nil where it used to raise NoMethodError. Merging this one first
closes that window, and #6993 needs no change either way.

rake test passes.

Summary by CodeRabbit

  • Bug Fixes
    • Improved validation for String#match and String#match?.
    • String patterns continue to work, while unsupported pattern types now produce clear TypeError messages.
    • Pattern validation now occurs before position validation for more consistent behavior.
    • Fixed handling of values that incorrectly identify themselves as regular expressions.
    • Improved support for valid regular expression and string subclasses.

`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
```
@takumin
takumin requested a review from matz as a code owner August 2, 2026 15:41
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

String#match and String#match? now use Regexp.__match_pattern for strict pattern validation. The helper accepts Regexp and String values, converts strings, and raises TypeError for unsupported values. Tests cover validation order and subclasses.

Changes

String pattern validation

Layer / File(s) Summary
Native pattern validation
mrbgems/mruby-regexp/src/regexp.c
Adds and registers Regexp.__match_pattern. The helper accepts Regexp and String values, converts strings, and raises compatible TypeError messages for unsupported values.
Matching integration and coverage
mrbgems/mruby-regexp/mrblib/string_regexp.rb, mrbgems/mruby-regexp/test/regexp.rb
Routes String#match and String#match? through the helper. Tests cover invalid and deceptive arguments, validation order, subclass overrides, and valid patterns.

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
Loading

Possibly related PRs

  • mruby/mruby#6993: Both PRs modify mruby-regexp pattern handling for String-style regexp matching.

Suggested reviewers: matz, nattzn

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: rejecting non-RegExp patterns in String#match and String#match?.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between f7be5bf and 7aa5905.

📒 Files selected for processing (2)
  • mrbgems/mruby-regexp/mrblib/string_regexp.rb
  • mrbgems/mruby-regexp/test/regexp.rb

Comment thread mrbgems/mruby-regexp/mrblib/string_regexp.rb Outdated
Comment thread mrbgems/mruby-regexp/test/regexp.rb Outdated
takumin added 4 commits August 3, 2026 00:49
`__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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 9e4b11b and f525917.

📒 Files selected for processing (3)
  • mrbgems/mruby-regexp/mrblib/string_regexp.rb
  • mrbgems/mruby-regexp/src/regexp.c
  • mrbgems/mruby-regexp/test/regexp.rb
🚧 Files skipped from review as they are similar to previous changes (1)
  • mrbgems/mruby-regexp/mrblib/string_regexp.rb

Comment thread mrbgems/mruby-regexp/src/regexp.c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants