Freeze all instances of Regexp, except subclass instances

* Instances of a subclass of Regexp are not frozen for compatibility.
* [Feature #8948]
* Use a less confusing example in test_regexp2,
  the ivar was named @encoding but had no effect on Regexp#encoding.
This commit is contained in:
Benoit Daloze committed 2026-03-25 10:00:03 +01:00
1 parent be659a0ef2
commit cb03bf43eb
16 files changed
+175 -57
Notes: git 2026-03-25 09:00:32 +00:00

No files matched your search

+7
View File
@@ -42,6 +42,12 @@ Note: We're only listing outstanding class updates.
* `Array#pack` accepts a new format `R` and `r` for unpacking unsigned
and signed LEB128 encoded integers. [[Feature #21785]]
* Regexp
* All instances of `Regexp` are now frozen, not just literals.
Subclasses of `Regexp` are not frozen for compatibility.
[[Feature #8948]]
* Set
* A deprecated behavior, `Set#to_set`, `Range#to_set`, and
@@ -140,6 +146,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable.
## JIT
[Feature #6012]: https://bugs.ruby-lang.org/issues/6012
[Feature #8948]: https://bugs.ruby-lang.org/issues/8948
[Feature #15330]: https://bugs.ruby-lang.org/issues/15330
[Feature #21390]: https://bugs.ruby-lang.org/issues/21390
[Feature #21785]: https://bugs.ruby-lang.org/issues/21785
+1
View File
@@ -8862,6 +8862,7 @@ marshal.$(OBJEXT): $(top_srcdir)/internal/hash.h
marshal.$(OBJEXT): $(top_srcdir)/internal/imemo.h
marshal.$(OBJEXT): $(top_srcdir)/internal/numeric.h
marshal.$(OBJEXT): $(top_srcdir)/internal/object.h
marshal.$(OBJEXT): $(top_srcdir)/internal/re.h
marshal.$(OBJEXT): $(top_srcdir)/internal/sanitizers.h
marshal.$(OBJEXT): $(top_srcdir)/internal/serial.h
marshal.$(OBJEXT): $(top_srcdir)/internal/set_table.h
+1
View File
@@ -12,6 +12,7 @@
#include "ruby/ruby.h" /* for VALUE */
/* re.c */
VALUE rb_reg_s_alloc(VALUE klass);
VALUE rb_reg_compile(VALUE str, int options, const char *sourcefile, int sourceline);
VALUE rb_reg_check_preprocess(VALUE);
long rb_reg_search0(VALUE, VALUE, long, int, int, VALUE *);
+14 -7
View File
@@ -30,6 +30,7 @@
#include "internal/hash.h"
#include "internal/numeric.h"
#include "internal/object.h"
#include "internal/re.h"
#include "internal/struct.h"
#include "internal/symbol.h"
#include "internal/util.h"
@@ -1731,7 +1732,10 @@ r_ivar_encoding(VALUE obj, struct load_arg *arg, VALUE sym, VALUE val)
int idx = sym2encidx(sym, val);
if (idx >= 0) {
if (rb_enc_capable(obj)) {
rb_enc_associate_index(obj, idx);
// Check if needed to avoid rb_check_frozen() check for Regexps
if (rb_enc_get_index(obj) != idx) {
rb_enc_associate_index(obj, idx);
}
}
else {
rb_raise(rb_eArgError, "%"PRIsVALUE" is not enc_capable", obj);
@@ -1854,17 +1858,17 @@ append_extmod(VALUE obj, VALUE extmod)
override_ivar_error(type, str); \
} while (0)
static VALUE r_object_for(struct load_arg *arg, bool partial, int *ivp, VALUE extmod, int type);
static VALUE r_object_for(struct load_arg *arg, bool partial, int *ivp, VALUE klass, VALUE extmod, int type);
static VALUE
r_object0(struct load_arg *arg, bool partial, int *ivp, VALUE extmod)
{
int type = r_byte(arg);
return r_object_for(arg, partial, ivp, extmod, type);
return r_object_for(arg, partial, ivp, 0, extmod, type);
}
static VALUE
r_object_for(struct load_arg *arg, bool partial, int *ivp, VALUE extmod, int type)
r_object_for(struct load_arg *arg, bool partial, int *ivp, VALUE klass, VALUE extmod, int type)
{
VALUE (*hash_new_with_size)(st_index_t) = rb_hash_new_with_size;
VALUE v = Qnil;
@@ -1940,12 +1944,12 @@ r_object_for(struct load_arg *arg, bool partial, int *ivp, VALUE extmod, int typ
}
type = r_byte(arg);
if ((c == rb_cHash) &&
/* Hack for compare_by_identify */
/* Hack for compare_by_identity */
(type == TYPE_HASH || type == TYPE_HASH_DEF)) {
hash_new_with_size = rb_ident_hash_new_with_size;
goto type_hash;
}
v = r_object_for(arg, partial, 0, extmod, type);
v = r_object_for(arg, partial, 0, c, extmod, type);
if (RB_SPECIAL_CONST_P(v) || RB_TYPE_P(v, T_OBJECT) || RB_TYPE_P(v, T_CLASS)) {
goto format_error;
}
@@ -2082,7 +2086,10 @@ r_object_for(struct load_arg *arg, bool partial, int *ivp, VALUE extmod, int typ
}
rb_str_set_len(str, dst - ptr);
}
VALUE regexp = rb_reg_new_str(str, options);
if (!klass) {
klass = rb_cRegexp;
}
VALUE regexp = rb_reg_init_str(rb_reg_s_alloc(klass), str, options);
r_copy_ivar(regexp, str);
v = r_entry0(regexp, idx, arg);
+11 -5
View File
@@ -3368,6 +3368,9 @@ rb_reg_initialize(VALUE obj, const char *s, long len, rb_encoding *enc,
options & ARG_REG_OPTION_MASK, err,
sourcefile, sourceline);
if (!re->ptr) return -1;
if (RBASIC_CLASS(obj) == rb_cRegexp) {
OBJ_FREEZE(obj);
}
RB_GC_GUARD(unescaped);
return 0;
}
@@ -3407,7 +3410,7 @@ rb_reg_initialize_str(VALUE obj, VALUE str, int options, onig_errmsg_buffer err,
return ret;
}
static VALUE
VALUE
rb_reg_s_alloc(VALUE klass)
{
NEWOBJ_OF(re, struct RRegexp, klass, T_REGEXP | (RGENGC_WB_PROTECTED_REGEXP ? FL_WB_PROTECTED : 0), sizeof(struct RRegexp), 0);
@@ -3460,9 +3463,7 @@ rb_reg_init_str_enc(VALUE re, VALUE s, rb_encoding *enc, int options)
VALUE
rb_reg_new_ary(VALUE ary, int opt)
{
VALUE re = rb_reg_new_str(rb_reg_preprocess_dregexp(ary, opt), opt);
rb_obj_freeze(re);
return re;
return rb_reg_new_str(rb_reg_preprocess_dregexp(ary, opt), opt);
}
VALUE
@@ -3496,7 +3497,6 @@ rb_reg_compile(VALUE str, int options, const char *sourcefile, int sourceline)
rb_set_errinfo(rb_reg_error_desc(str, options, err));
return Qnil;
}
rb_obj_freeze(re);
return re;
}
@@ -4033,6 +4033,9 @@ reg_copy(VALUE copy, VALUE orig)
RREGEXP_PTR(copy)->timelimit = RREGEXP_PTR(orig)->timelimit;
rb_enc_copy(copy, orig);
FL_SET_RAW(copy, FL_TEST_RAW(orig, KCODE_FIXED|REG_ENCODING_NONE));
if (RBASIC_CLASS(copy) == rb_cRegexp) {
OBJ_FREEZE(copy);
}
return copy;
}
@@ -4115,6 +4118,9 @@ rb_reg_initialize_m(int argc, VALUE *argv, VALUE self)
}
set_timeout(&RREGEXP_PTR(self)->timelimit, args.timeout);
if (RBASIC_CLASS(self) == rb_cRegexp) {
OBJ_FREEZE(self);
}
return self;
}
+17 -5
View File
@@ -473,14 +473,26 @@ describe "Marshal.dump" do
Marshal.dump(//im).should == "\x04\bI/\x00\x05\x06:\x06EF"
end
it "dumps a Regexp with instance variables" do
o = Regexp.new("")
it "dumps a Regexp subclass with instance variables" do
o = UserRegexp.new("")
o.instance_variable_set(:@ivar, :ivar)
Marshal.dump(o).should == "\x04\bI/\x00\x00\a:\x06EF:\n@ivar:\tivar"
Marshal.dump(o).should == "\x04\bIC:\x0FUserRegexp/\x00\x00\a:\x06EF:\n@ivar:\tivar"
end
it "dumps an extended Regexp" do
Marshal.dump(Regexp.new("").extend(Meths)).should == "\x04\bIe:\nMeths/\x00\x00\x06:\x06EF"
it "dumps an extended Regexp subclass" do
Marshal.dump(UserRegexp.new("").extend(Meths)).should == "\x04\bIe:\nMethsC:\x0FUserRegexp/\x00\x00\x06:\x06EF"
end
ruby_version_is ""..."4.1" do
it "dumps a Regexp with instance variables" do
o = Regexp.new("")
o.instance_variable_set(:@ivar, :ivar)
Marshal.dump(o).should == "\x04\bI/\x00\x00\a:\x06EF:\n@ivar:\tivar"
end
it "dumps an extended Regexp" do
Marshal.dump(Regexp.new("").extend(Meths)).should == "\x04\bIe:\nMeths/\x00\x00\x06:\x06EF"
end
end
it "dumps a Regexp subclass" do
+43 -14
View File
@@ -901,14 +901,52 @@ describe :marshal_load, shared: true do
end
describe "for a Regexp" do
it "loads an extended Regexp" do
obj = /[a-z]/.dup.extend(Meths, MethsMore)
new_obj = Marshal.send(@method, "\004\be:\nMethse:\016MethsMore/\n[a-z]\000")
ruby_version_is "4.1" do
it "raises FrozenError for an extended Regexp" do
-> {
Marshal.send(@method, "\004\be:\nMethse:\016MethsMore/\n[a-z]\000")
}.should raise_error(FrozenError)
end
it "raises FrozenError when regexp has instance variables" do
-> {
Marshal.send(@method, "\x04\bI/\nhello\x00\a:\x06EF:\x11@regexp_ivar[\x06i/")
}.should raise_error(FrozenError)
end
end
ruby_version_is ""..."4.1" do
it "loads an extended Regexp" do
obj = /[a-z]/.dup.extend(Meths, MethsMore)
new_obj = Marshal.send(@method, "\004\be:\nMethse:\016MethsMore/\n[a-z]\000")
new_obj.should == obj
new_obj_metaclass_ancestors = class << new_obj; ancestors; end
new_obj_metaclass_ancestors[@num_self_class, 3].should ==
[Meths, MethsMore, Regexp]
end
it "restore the regexp instance variables" do
obj = Regexp.new("hello")
obj.instance_variable_set(:@regexp_ivar, [42])
new_obj = Marshal.send(@method, "\x04\bI/\nhello\x00\a:\x06EF:\x11@regexp_ivar[\x06i/")
new_obj.instance_variables.should == [:@regexp_ivar]
new_obj.instance_variable_get(:@regexp_ivar).should == [42]
end
end
it "loads a Regexp subclass instance variables" do
obj = UserRegexp.new('abc')
obj.instance_variable_set(:@noise, 'much')
new_obj = Marshal.send(@method, Marshal.dump(obj))
new_obj.should == obj
new_obj.instance_variable_get(:@noise).should == 'much'
new_obj_metaclass_ancestors = class << new_obj; ancestors; end
new_obj_metaclass_ancestors[@num_self_class, 3].should ==
[Meths, MethsMore, Regexp]
new_obj_metaclass_ancestors[@num_self_class, 2].should ==
[UserRegexp, Regexp]
end
it "loads a Regexp subclass instance variables when it is extended with a module" do
@@ -924,15 +962,6 @@ describe :marshal_load, shared: true do
[Meths, UserRegexp, Regexp]
end
it "restore the regexp instance variables" do
obj = Regexp.new("hello")
obj.instance_variable_set(:@regexp_ivar, [42])
new_obj = Marshal.send(@method, "\x04\bI/\nhello\x00\a:\x06EF:\x11@regexp_ivar[\x06i/")
new_obj.instance_variables.should == [:@regexp_ivar]
new_obj.instance_variable_get(:@regexp_ivar).should == [42]
end
it "preserves Regexp encoding" do
source_object = Regexp.new("a".encode("utf-32le"))
regexp = Marshal.send(@method, Marshal.dump(source_object))
+16 -2
View File
@@ -9,7 +9,21 @@ describe "Regexp#initialize" do
-> { //.send(:initialize, "") }.should raise_error(FrozenError)
end
it "raises a TypeError on an initialized non-literal Regexp" do
-> { Regexp.new("").send(:initialize, "") }.should raise_error(TypeError)
ruby_version_is "4.1" do
it "raises a FrozenError on an initialized non-literal Regexp" do
regexp = Regexp.new("")
-> { regexp.send(:initialize, "") }.should raise_error(FrozenError)
end
end
ruby_version_is ""..."4.1" do
it "raises a TypeError on an initialized non-literal Regexp" do
-> { Regexp.new("").send(:initialize, "") }.should raise_error(TypeError)
end
end
it "raises a TypeError on an initialized non-literal Regexp subclass" do
r = Class.new(Regexp).new("")
-> { r.send(:initialize, "") }.should raise_error(TypeError)
end
end
+6
View File
@@ -5,6 +5,12 @@ describe :regexp_new, shared: true do
Regexp.send(@method, '').is_a?(Regexp).should == true
end
ruby_version_is "4.1" do
it "is frozen" do
Regexp.send(@method, '').should.frozen?
end
end
it "works by default for subclasses with overridden #initialize" do
class RegexpSpecsSubclass < Regexp
def initialize(*args)
+11 -3
View File
@@ -137,9 +137,17 @@ describe "String#match" do
end
it "calls match on the regular expression" do
regexp = /./.dup
regexp.should_receive(:match).and_return(:foo)
'hello'.match(regexp).should == :foo
# Can't use regexp.should_receive(:match).and_return(:foo) since regexps are frozen
ScratchPad.clear
regexp = Class.new(Regexp) {
def match(*args)
ScratchPad.record [:match, *args]
super(*args)
end
}.new('.')
'hello'.match(regexp)
ScratchPad.recorded.should == [:match, 'hello']
end
end
+34 -9
View File
@@ -489,12 +489,20 @@ describe "C-API Encoding function" do
@s.rb_enc_copy("string", @obj).encoding.should == Encoding::US_ASCII
end
it "raises a RuntimeError if the second argument is a Symbol" do
it "raises a RuntimeError if the first argument is a Symbol" do
-> { @s.rb_enc_copy(:symbol, @obj) }.should raise_error(RuntimeError)
end
it "sets the encoding of a Regexp to that of the second argument" do
@s.rb_enc_copy(/regexp/.dup, @obj).encoding.should == Encoding::US_ASCII
ruby_version_is "4.1" do
it "raises a FrozenError if the first argument is a Regexp" do
-> { @s.rb_enc_copy(/regexp/.dup, @obj) }.should raise_error(FrozenError)
end
end
ruby_version_is ""..."4.1" do
it "sets the encoding of a Regexp to that of the second argument" do
@s.rb_enc_copy(/regexp/.dup, @obj).encoding.should == Encoding::US_ASCII
end
end
end
@@ -544,8 +552,16 @@ describe "C-API Encoding function" do
-> { @s.rb_enc_associate(:symbol, "US-ASCII") }.should raise_error(RuntimeError)
end
it "sets the encoding of a Regexp to the encoding" do
@s.rb_enc_associate(/regexp/.dup, "BINARY").encoding.should == Encoding::BINARY
ruby_version_is "4.1" do
it "raises a FrozenError if the argument is a Regexp" do
-> { @s.rb_enc_associate(/regexp/.dup, "BINARY") }.should raise_error(FrozenError)
end
end
ruby_version_is ""..."4.1" do
it "sets the encoding of a Regexp to the encoding" do
@s.rb_enc_associate(/regexp/.dup, "BINARY").encoding.should == Encoding::BINARY
end
end
it "sets the encoding of a String to a default when the encoding is NULL" do
@@ -560,10 +576,19 @@ describe "C-API Encoding function" do
enc.should == Encoding::BINARY
end
it "sets the encoding of a Regexp to the encoding" do
index = @s.rb_enc_find_index("UTF-8")
enc = @s.rb_enc_associate_index(/regexp/.dup, index).encoding
enc.should == Encoding::UTF_8
ruby_version_is "4.1" do
it "raises a FrozenError if the argument is a Regexp" do
index = @s.rb_enc_find_index("UTF-8")
-> { @s.rb_enc_associate_index(/regexp/.dup, index) }.should raise_error(FrozenError)
end
end
ruby_version_is ""..."4.1" do
it "sets the encoding of a Regexp to the encoding" do
index = @s.rb_enc_find_index("UTF-8")
enc = @s.rb_enc_associate_index(/regexp/.dup, index).encoding
enc.should == Encoding::UTF_8
end
end
it "sets the encoding of a Symbol to the encoding" do
+5 -5
View File
@@ -69,11 +69,11 @@ class TestEnumerable < Test::Unit::TestCase
assert_equal(['z', 42, nil], [:a, 'b', 'z', :c, 42, nil].grep_v(/[a-d]/), bug17030)
assert_equal('match', $1, bug17030)
regexp = Regexp.new('x')
assert_equal([], @obj.grep(regexp), bug17030) # sanity check
def regexp.===(other)
true
end
regexp = Class.new(Regexp) {
def ===(other)
true
end
}.new('x')
assert_equal([1, 2, 3, 1, 2], @obj.grep(regexp), bug17030)
o = Object.new
+2 -2
View File
@@ -314,7 +314,7 @@ class TestMarshal < Test::Unit::TestCase
def test_regexp2
assert_equal(/\\u/, Marshal.load("\004\b/\b\\\\u\000"))
assert_equal(/u/, Marshal.load("\004\b/\a\\u\000"))
assert_equal(/u/, Marshal.load("\004\bI/\a\\u\000\006:\016@encoding\"\vEUC-JP"))
assert_raise(FrozenError) { Marshal.load("\x04\bI/\x06u\x00\a:\x06EF:\t@fooi/") }
bug2109 = '[ruby-core:25625]'
a = "\x82\xa0".force_encoding(Encoding::Windows_31J)
@@ -988,7 +988,7 @@ class TestMarshal < Test::Unit::TestCase
end
def test_proc_returned_object_are_not_frozen
source = ["foo", {}, /foo/, 1..2]
source = ["foo", {}, 1..2]
objects = Marshal.load(encode(source), ->(o) { o.dup }, freeze: true)
assert_equal source, objects
refute_predicate objects, :frozen?
+1 -1
View File
@@ -975,7 +975,7 @@ class TestRegexp < Test::Unit::TestCase
def test_dup
assert_equal(//, //.dup)
assert_raise(TypeError) { //.dup.instance_eval { initialize_copy(nil) } }
assert_raise(FrozenError) { //.dup.instance_eval { initialize_copy(/a/) } }
end
def test_regsub
+3 -2
View File
@@ -2757,8 +2757,9 @@ CODE
def test_match_method
assert_equal("bar", S("foobarbaz").match(/bar/).to_s)
o = Regexp.new('foo')
def o.match(x, y, z); x + y + z; end
o = Class.new(Regexp) {
def match(x, y, z) = x + y + z
}.new('foo')
assert_equal("foobarbaz", S("foo").match(o, "bar", "baz"))
x = nil
S("foo").match(o, "bar", "baz") {|y| x = y }
+3 -2
View File
@@ -417,8 +417,9 @@ class TestSymbol < Test::Unit::TestCase
def test_match_method
assert_equal("bar", :"foobarbaz".match(/bar/).to_s)
o = Regexp.new('foo')
def o.match(x, y, z); x + y + z; end
o = Class.new(Regexp) {
def match(x, y, z) = x + y + z
}.new('foo')
assert_equal("foobarbaz", :"foo".match(o, "bar", "baz"))
x = nil
:"foo".match(o, "bar", "baz") {|y| x = y }