Skip to content

mruby-regexp: fix String#sub/#gsub argument handling - #6989

Merged
matz merged 3 commits into
mruby:masterfrom
takumin:string-sub-gsub-block-precedence
Aug 2, 2026
Merged

mruby-regexp: fix String#sub/#gsub argument handling#6989
matz merged 3 commits into
mruby:masterfrom
takumin:string-sub-gsub-block-precedence

Conversation

@takumin

@takumin takumin commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

String#sub and String#gsub in mruby-regexp branched on whether a block was given rather than on how many arguments were actually passed. Two behaviours diverged from CRuby as a result.

1. The block won over the replacement argument

"abc".sub(/b/, "X") { "Y" }   #=> "aYc"  (CRuby: "aXc")
"abcb".gsub(/b/, "X") { "Y" } #=> "aYcY" (CRuby: "aXcX")

CRuby ignores the block whenever a replacement argument is present. Both methods now take a splat, so an omitted replacement can be told apart from an explicit one, and dispatch on the argument count instead.

This also lets them reject a bad argument count with the CRuby message. gsub always reports 1..2; sub reports 1..2 with a block and exactly 2 without one, matching CRuby.

2. gsub without a block deleted every match

"abcb".gsub(/b/) #=> "ac"  (CRuby: #<Enumerator: "abcb":gsub(/b/)>)

It fell through to __gsub_str with nil.to_s as the replacement. It now returns an enumerator over the matched substrings, so gsub(pattern).each { ... } performs the substitution as in CRuby.

Enumerator lives in mruby-enumerator, which mruby-regexp does not require at runtime — without it, to_enum is core Kernel#to_enum, which raises NotImplementedError, exactly as with Kernel#loop and String#each_char in mruby-string-ext. The gem declares the dependency only when the build already contains it, so no build gains Enumerator (and thus Fiber) it did not ask for. This is what lets the enumerator test run: mrbtest gives each gem's tests an mrb_open_core() state holding just its declared dependencies. When Enumerator is undefined the test skips itself. Both gems are in stdlib.gembox, so a normal build runs it.

Notes

  • "abc".gsub(/b/, nil) returns "ac" where CRuby raises TypeError. That predates this change and is left alone.
  • rake test passes (1890 OK / 0 KO / 0 Crash).
  • Checked against three build configs: default (test runs), mruby-regexp + mruby-string-ext only (test skips, neither Enumerator nor Fiber linked), and mruby-regexp + mruby-set where mruby-enumerator arrives transitively (test skips).

Summary by CodeRabbit

  • Bug Fixes
    • Improved String#sub and String#gsub argument-count validation.
    • Ensured replacement arguments take precedence over blocks.
    • Preserved blockless gsub enumeration behavior.
    • Continued support for escaping string patterns.
    • Improved handling of invalid argument combinations with clear errors.

takumin added 2 commits August 2, 2026 19:37
…ng#sub/#gsub

Both methods took `pattern, replacement = nil` and branched on whether a
block was given, so `"abc".sub(/b/, "X") { "Y" }` ran the block and
returned "aYc". CRuby ignores the block whenever a replacement argument
is present, and returns "aXc".

Switch both to a splat so the number of arguments actually given can be
told apart from an omitted replacement, and dispatch on that instead of
on the block. This also lets them reject a bad argument count with the
CRuby message: `gsub` always reports 1..2, while `sub` reports 1..2 with
a block and exactly 2 without one.
`"abcb".gsub(/b/)` fell through to `__gsub_str` with `nil.to_s` as the
replacement and returned "ac", silently deleting every match. CRuby
returns an Enumerator over the matched substrings, which also makes
`gsub(pattern).each { ... }` perform the substitution.

Enumerator lives in mruby-enumerator, which mruby-regexp does not
require at runtime (mruby-string-ext takes the same approach for
String#each_char), so declare it as a test dependency only. Both gems
are in stdlib.gembox, so a normal build has it.
@takumin
takumin requested a review from matz as a code owner August 2, 2026 10:40
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

String#sub and String#gsub now validate argument counts and replacement precedence. Blockless gsub enumeration remains supported. Tests cover substitutions, errors, and enumeration. mruby-enumerator is conditionally wired when available.

Changes

Regexp substitution behavior

Layer / File(s) Summary
Substitution argument handling
mrbgems/mruby-regexp/mrblib/string_regexp.rb
String#sub and String#gsub validate argument counts, prioritize replacement arguments over blocks, and preserve string-pattern escaping and blockless enumeration.
Substitution behavior validation
mrbgems/mruby-regexp/test/regexp.rb, mrbgems/mruby-regexp/mrbgem.rake
Tests cover replacement precedence, exact argument errors, and blockless gsub enumeration. The gem conditionally declares mruby-enumerator when it is already included in the build.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: matz

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 to String#sub and String#gsub argument handling.
✨ 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: 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/mrbgem.rake`:
- Around line 7-9: Change the mruby-enumerator declaration in the gem
specification from add_test_dependency to add_dependency so mruby-regexp’s
String#gsub implementation can access to_enum at runtime; keep the existing
mruby-enumerator dependency and core option unchanged.
🪄 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: 073cf1bb-c45e-40b3-8b28-e1a9f3090fdd

📥 Commits

Reviewing files that changed from the base of the PR and between 11ff450 and ef662c5.

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

Comment thread mrbgems/mruby-regexp/mrbgem.rake Outdated
`String#gsub` without a block returns an enumerator, but that is the only
path in this gem reaching `to_enum`. Without mruby-enumerator, `to_enum`
is core `Kernel#to_enum`, which raises NotImplementedError -- the same
deal as `Kernel#loop` and `String#each_char` in mruby-string-ext, neither
of which depends on mruby-enumerator either. A plain dependency would
drag Enumerator, and thus Fiber, into builds that never take that path.

Declare the dependency only when the build already has the gem. mrbtest
runs each gem's tests in a state holding just its declared dependencies,
so this is what lets the enumerator test run at all; without it the test
skips itself. A gem arriving only through another gem's dependency is not
visible at that point, which likewise just skips the test.
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