mruby-regexp: read the real type of String#split's limit - #7004
Conversation
📝 WalkthroughWalkthrough
ChangesString split limit validation
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related issues
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)
1081-1117: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAdd a regression assertion for the corrected
TypeErrormessage.The added cases verify only
TypeError. They do not protect the message fixed atmrbgems/mruby-regexp/mrblib/string_regexp.rbLine 143. Add an invalidto_intcase and assertno implicit conversion of Float to Integer, so the extra)cannot return without a test failure.Proposed test
assert_raise(TypeError) { "a,b,c".split(/,/, cmp.new) } + + invalid = Class.new do + def to_int; 1.5; end + end + error = assert_raise(TypeError) { "a,b,c".split(/,/, invalid.new) } + assert_equal("no implicit conversion of Float to Integer", error.message) end🤖 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 1081 - 1117, Add a regression case in the String#split limit tests using an object whose to_int returns a Float, and assert that splitting raises TypeError with the exact message “no implicit conversion of Float to Integer”. Keep the existing liar and comparator cases unchanged, and target the conversion path exercised by String#split with the regexp pattern.
🤖 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 1081-1117: Add a regression case in the String#split limit tests
using an object whose to_int returns a Float, and assert that splitting raises
TypeError with the exact message “no implicit conversion of Float to Integer”.
Keep the existing liar and comparator cases unchanged, and target the conversion
path exercised by String#split with the regexp pattern.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: e0f7d77f-826d-4901-88fb-e9a3ce48f253
📒 Files selected for processing (2)
mrbgems/mruby-regexp/mrblib/string_regexp.rbmrbgems/mruby-regexp/test/regexp.rb
15d28ac to
6d61652
Compare
String#split's limit
The limit conversion is guarded by `limit.is_a?(Integer)`, which asks the
argument for its own type. `is_a?` is redefinable, so an argument claiming
to be an Integer skips the conversion entirely and reaches the split loop
as itself.
Answering none of the operators the loop uses, the failure surfaces as a
`NoMethodError` naming an internal comparison, which says nothing about the
limit being wrong. Answering `==`, `>` and `-`, there is no error at all:
the loop runs to completion with a non-Integer limit and returns a
plausible wrong result.
```ruby
class Cmp
def is_a?(klass); true; end
def ==(other); false; end
def >(other); true; end
def -(other); 1; end
end
"a,b,c".split(/,/, Cmp.new) # CRuby: TypeError
# mruby: ["a", "b,c"]
```
`Module#===` reads the real type and cannot be redefined, the same
substitution mruby#7001 made for the pattern argument a few lines below.
The two halves of the method used to disagree about one argument. A String
pattern delegates to `__split`, which converts the limit again in C, where
the argument gets no say, so only the regexp path was reachable with an
unconverted limit. Both paths now raise the same `TypeError`.
8f85a84 dropped the `to_int` dispatch this guard used to stand in front
of, so the block behind it is now a single `__to_int` call. That call is
what a redefined `is_a?` still skips, so the guard remains the only thing
between such an argument and the loop.
Related to mruby#7003.
6d61652 to
d09f626
Compare
|
This needs a rebase, and the conflict is my doing rather than yours. I answered #7003 with option 1 and pushed it as 8f85a84, which removes the I also told you in #7003 that "the What is left after the rebase is one line: - if limit_given && !limit.is_a?(Integer)
+ if limit_given && !(Integer === limit)The other two hunks are gone with the branch that held them: the inner The fix itself I want. I checked both of your cases on current master and they both still reproduce: The second one is the one that concerns me. No exception at all, a non-Integer limit driving the loop to completion, and a plausible wrong answer coming back. That is worse than the Please keep the tests. The |
|
Merged. Please disregard my previous comment: you had already rebased before I wrote it. Your rebased commit is dated 08:29 and my comment went up at 09:40. I read a stale head SHA and a stale diff, saw the pre-rebase content, and wrote a whole comment asking you to do something you had finished over an hour earlier. I should have re-fetched before posting; there was no excuse for it, since checking is one command. The one thing the comment got right is that the rebase left exactly one line, and that is what you had already pushed: - if limit_given && !limit.is_a?(Integer)
+ if limit_given && !(Integer === limit)Verified on your head (d09f626): all three cases raise now, and the suite is green (2108 OK, 0 KO). Thank you for the extra Between #7001, #7003 and this one, redefinable guards have come up three times in a day, each with |
|
Thank you for reviewing, verifying the behavior on the rebased head, and merging the PR. I also appreciate the clarification about the stale diff. No problem at all. Your point about documenting this pattern is especially helpful. Using |
Related to #7003, which 8f85a84 already settled by dropping the
to_intdispatch. Thispull request does not close that issue. It is the follow-up 8f85a84 deliberately left in
place: the
is_a?guard standing in front of the conversion. Rebased onto that commit. Thestray-paren commit is gone, since the branch that raised that message no longer exists.
String#split'slimitargument is converted in the override this gem installs over thecore method. The conversion is guarded by
limit.is_a?(Integer), which asks the argumentfor its own type.
is_a?is redefinable, so an argument claiming to be an Integer skips theconversion entirely and reaches the split loop as itself.
What happens next depends on which operators that argument answers. With none, the failure
surfaces as a
NoMethodErrornaming an internal comparison, which says nothing about thelimit being wrong:
With
==,>and-answered, there is no error at all: the loop runs to completion witha non-Integer limit and returns a plausible wrong result.
This is the same hole #7001 closed for the pattern argument, left open one argument over.
The two halves of the same method disagree
The conversion sits ahead of the delegation to
__split, so a String ornilpattern runsthrough it too. It survives only because the core implementation converts the limit again in
C, where the argument gets no say:
Two calls to one method, one argument, two answers. The regexp path is the one with no C
conversion behind it, and it is the one that gets it wrong.
Fix
Module#===reads the real type and cannot be redefined, substituted for the oneis_a?the limit block still has:
This is the substitution #7001 made for the pattern argument a few lines below, so the two
guards in this method now read the same way. 8f85a84 collapsed the body behind this guard
to a single
__to_int, and that call is exactly what a redefinedis_a?skips.Dropping the guard altogether and writing
limit = limit.__to_int if limit_givenwouldclose the same hole with no redefinable call at all. It is not what this does: 8f85a84
kept that line deliberately, and the substitution above is the shape #7001 already
established for this method. One measured consequence of keeping it is recorded below.
What keeping the guard leaves behind
A BigInt limit is a real Integer, so it passes the guard under either spelling and never
reaches
__to_int. The regexp path then runs the loop with it, while the String pathconverts it in C and raises. Measured on a plain host build,
mruby-bigintbeing part ofmath.gemboxand so ofdefault.gembox:This is the same two-halves disagreement described above, on a different argument, and it
predates this pull request:
limit.is_a?(Integer)answers true for a BigInt exactly asInteger === limitdoes, so the change neither introduces nor closes it. Removing the guardwould close it, by sending every limit through
mrb_ensure_int_type(), but that is abehaviour change on a case no test covers, and a wider question than the redefinable guard
this pull request is about. Recorded here rather than folded in, and happy to file it
separately if it is worth fixing.
Test
Added to
mrbgems/mruby-regexp/test/regexp.rb:The two classes cover distinct pre-fix paths:
StringSplitLimitIsALiarblew up onlimit > 0,StringSplitLimitComparablereturned a wrong array with no error at all. Theyare named top-level classes rather than anonymous ones because that is how this file already
writes an
is_a?liar, inStringMatchIsALiara few hundred lines above.The
String#split with regexp limittest 8f85a84 rewrote is untouched. Therespond_to?liar it added is a different route from theis_a?one pinned here.Verified on a default host build at
8f85a8413:limit.is_a?(Integer),rake testreportsFail: String#split limit cannot pose as an Integer, so the test pins the guard ratherthan passing either way.
Comparison against CRuby 4.0.6
Run side by side on the four cases the guard governs:
split(/,/, FakeInt.new)NoMethodErrorTypeErrorTypeErrorsplit(",", FakeInt.new)TypeErrorTypeErrorTypeErrorsplit(/,/, Cmp.new)["a", "b,c"]TypeErrorTypeErrorsplit(",", Cmp.new)TypeErrorTypeErrorTypeErrorAfter the change the exception class agrees in every case and the return value agrees in
every case. What is left is wording, from
mrb_ensure_integer_type():That is the wording 8f85a84 chose deliberately for every non-Integer limit, and this
change does not touch it.
Unaffected and green: an ordinary Integer limit, a Float limit,
nil, and limits of 0, 1and -1.