diff --git a/NEWS.md b/NEWS.md index ea693b710de730..7e00b3a851c0a9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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 @@ -69,7 +75,7 @@ releases. * RubyGems 4.1.0.dev * bundler 4.1.0.dev -* json 2.19.2 +* json 2.19.3 * 2.18.0 to [v2.18.1][json-v2.18.1], [v2.19.0][json-v2.19.0], [v2.19.1][json-v2.19.1], [v2.19.2][json-v2.19.2] * openssl 4.0.1 * 4.0.0 to [v4.0.1][openssl-v4.0.1] @@ -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 diff --git a/depend b/depend index 711011451f2f0d..f22dbbd8189aa0 100644 --- a/depend +++ b/depend @@ -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 diff --git a/ext/json/lib/json/version.rb b/ext/json/lib/json/version.rb index 8853ed885d2a42..e7af97d435e398 100644 --- a/ext/json/lib/json/version.rb +++ b/ext/json/lib/json/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module JSON - VERSION = '2.19.2' + VERSION = '2.19.3' end diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index b05f3f5273d71d..38f3a48ce4515a 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -770,7 +770,9 @@ NOINLINE(static) VALUE json_string_unescape(JSON_ParserState *state, JSON_Parser } raise_parse_error_at("invalid ASCII control character in string: %s", state, pe - 1); } - } else if (config->allow_invalid_escape) { + } + + if (config->allow_invalid_escape) { APPEND_CHAR(*pe); } else { raise_parse_error_at("invalid escape character in string: %s", state, pe - 1); diff --git a/gc/default/default.c b/gc/default/default.c index a3b8a5685e0a69..e577a731fdfacd 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -9532,11 +9532,6 @@ rb_gc_impl_objspace_init(void *objspace_ptr) rb_darray_make_without_gc(&objspace->heap_pages.sorted, 0); rb_darray_make_without_gc(&objspace->weak_references, 0); - // TODO: debug why on Windows Ruby crashes on boot when GC is on. -#ifdef _WIN32 - dont_gc_on(); -#endif - #if defined(INIT_HEAP_PAGE_ALLOC_USE_MMAP) /* Need to determine if we can use mmap at runtime. */ heap_page_alloc_use_mmap = INIT_HEAP_PAGE_ALLOC_USE_MMAP; diff --git a/gems/bundled_gems b/gems/bundled_gems index cc419e6b670434..f0d7da6108986c 100644 --- a/gems/bundled_gems +++ b/gems/bundled_gems @@ -16,7 +16,7 @@ net-imap 0.6.3 https://github.com/ruby/net-imap net-smtp 0.5.1 https://github.com/ruby/net-smtp matrix 0.4.3 https://github.com/ruby/matrix prime 0.1.4 https://github.com/ruby/prime -rbs 3.10.3 https://github.com/ruby/rbs +rbs 4.0.2 https://github.com/ruby/rbs typeprof 0.31.1 https://github.com/ruby/typeprof debug 1.11.1 https://github.com/ruby/debug racc 1.8.1 https://github.com/ruby/racc diff --git a/include/ruby/internal/core/rmatch.h b/include/ruby/internal/core/rmatch.h index a528c2999e988a..d48e8b1249e224 100644 --- a/include/ruby/internal/core/rmatch.h +++ b/include/ruby/internal/core/rmatch.h @@ -71,7 +71,7 @@ struct rmatch_offset { struct rb_matchext_struct { /** * "Registers" of a match. This is a quasi-opaque struct that holds - * execution result of a match. Roughly resembles `&~`. + * execution result of a match. Roughly resembles `$~`. */ struct re_registers regs; diff --git a/internal/re.h b/internal/re.h index 593e5c464fdfb3..2d2eba0dc1905c 100644 --- a/internal/re.h +++ b/internal/re.h @@ -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 *); diff --git a/lib/bundler/errors.rb b/lib/bundler/errors.rb index d8df4d6ec5c553..dff5d931288706 100644 --- a/lib/bundler/errors.rb +++ b/lib/bundler/errors.rb @@ -265,14 +265,40 @@ def message class InvalidArgumentError < BundlerError; status_code(40); end class IncorrectLockfileDependencies < BundlerError - attr_reader :spec + attr_reader :spec, :actual_dependencies, :lockfile_dependencies - def initialize(spec) + def initialize(spec, actual_dependencies = nil, lockfile_dependencies = nil) @spec = spec + @actual_dependencies = actual_dependencies + @lockfile_dependencies = lockfile_dependencies end def message - "Bundler found incorrect dependencies in the lockfile for #{spec.full_name}" + lines = ["Bundler found incorrect dependencies in the lockfile for #{spec.full_name}", ""] + + if @actual_dependencies && @lockfile_dependencies + actual_by_name = @actual_dependencies.each_with_object({}) {|d, h| h[d.name] = d } + lockfile_by_name = @lockfile_dependencies.each_with_object({}) {|d, h| h[d.name] = d } + + (actual_by_name.keys | lockfile_by_name.keys).sort.each do |name| + actual = actual_by_name[name] + lockfile = lockfile_by_name[name] + next if actual && lockfile && actual.requirement == lockfile.requirement + + if actual && lockfile + lines << " #{name}: gemspec specifies #{actual.requirement}, lockfile has #{lockfile.requirement}" + elsif actual + lines << " #{name}: gemspec specifies #{actual.requirement}, not in lockfile" + else + lines << " #{name}: not in gemspec, lockfile has #{lockfile.requirement}" + end + end + + lines << "" + end + + lines << "Please run `bundle install` to regenerate the lockfile." + lines.join("\n") end status_code(41) diff --git a/lib/bundler/lazy_specification.rb b/lib/bundler/lazy_specification.rb index 786dbcae658618..46b1e905d398e3 100644 --- a/lib/bundler/lazy_specification.rb +++ b/lib/bundler/lazy_specification.rb @@ -262,7 +262,7 @@ def validate_dependencies(spec) spec.dependencies = dependencies else if !source.is_a?(Source::Path) && spec.runtime_dependencies.sort != dependencies.sort - raise IncorrectLockfileDependencies.new(self) + raise IncorrectLockfileDependencies.new(self, spec.runtime_dependencies, dependencies) end end end diff --git a/lib/bundler/plugin/index.rb b/lib/bundler/plugin/index.rb index 94683a5e544f49..1dfb061304a466 100644 --- a/lib/bundler/plugin/index.rb +++ b/lib/bundler/plugin/index.rb @@ -163,6 +163,8 @@ def installed_in_plugin_root?(name) # @param [Pathname] index file path # @param [Boolean] is the index file global index def load_index(index_file, global = false) + base = base_for_index(global) + SharedHelpers.filesystem_access(index_file, :read) do |index_f| valid_file = index_f&.exist? && !index_f.size.zero? break unless valid_file @@ -174,8 +176,8 @@ def load_index(index_file, global = false) @commands.merge!(index["commands"]) @hooks.merge!(index["hooks"]) - @load_paths.merge!(index["load_paths"]) - @plugin_paths.merge!(index["plugin_paths"]) + @load_paths.merge!(transform_index_paths(index["load_paths"]) {|p| absolutize_path(p, base) }) + @plugin_paths.merge!(transform_index_paths(index["plugin_paths"]) {|p| absolutize_path(p, base) }) @sources.merge!(index["sources"]) unless global end end @@ -184,11 +186,13 @@ def load_index(index_file, global = false) # instance variables in YAML format. (The instance variables are supposed # to be only String key value pairs) def save_index + base = base_for_index(false) + index = { "commands" => @commands, "hooks" => @hooks, - "load_paths" => @load_paths, - "plugin_paths" => @plugin_paths, + "load_paths" => transform_index_paths(@load_paths) {|p| relativize_path(p, base) }, + "plugin_paths" => transform_index_paths(@plugin_paths) {|p| relativize_path(p, base) }, "sources" => @sources, } @@ -198,6 +202,40 @@ def save_index File.open(index_f, "w") {|f| f.puts YAMLSerializer.dump(index) } end end + + def base_for_index(global) + global ? Plugin.global_root : Plugin.root + end + + def transform_index_paths(paths) + return {} unless paths + + paths.transform_values do |value| + if value.is_a?(Array) + value.map {|path| yield path } + else + yield value + end + end + end + + def relativize_path(path, base) + pathname = Pathname.new(path) + return path unless pathname.absolute? + + base_path = Pathname.new(base) + if pathname == base_path || pathname.to_s.start_with?(base_path.to_s + File::SEPARATOR) + pathname.relative_path_from(base_path).to_s + else + path + end + end + + def absolutize_path(path, base) + pathname = Pathname.new(path) + pathname = Pathname.new(base).join(pathname) unless pathname.absolute? + pathname.to_s + end end end end diff --git a/marshal.c b/marshal.c index 199923df4df7f9..a89a9eccba3d0e 100644 --- a/marshal.c +++ b/marshal.c @@ -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); diff --git a/re.c b/re.c index a112cde3e943f7..563b1e988b3169 100644 --- a/re.c +++ b/re.c @@ -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; } diff --git a/spec/bundler/bundler/errors_spec.rb b/spec/bundler/bundler/errors_spec.rb new file mode 100644 index 00000000000000..b62d85d32bd4b1 --- /dev/null +++ b/spec/bundler/bundler/errors_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::IncorrectLockfileDependencies do + describe "#message" do + let(:spec) do + double("LazySpecification", full_name: "rubocop-1.82.0") + end + + context "without dependency details" do + subject { described_class.new(spec) } + + it "provides a basic error message" do + expect(subject.message).to include("Bundler found incorrect dependencies in the lockfile for rubocop-1.82.0") + expect(subject.message).to include("Please run `bundle install` to regenerate the lockfile.") + end + end + + context "with dependency details" do + let(:actual_dependencies) do + [ + Gem::Dependency.new("json", [">= 2.3", "< 4.0"]), + Gem::Dependency.new("parallel", ["~> 1.10"]), + Gem::Dependency.new("parser", [">= 3.3.0.2"]), + ] + end + + let(:lockfile_dependencies) do + [ + Gem::Dependency.new("json", [">= 2.3", "< 3.0"]), + Gem::Dependency.new("parallel", ["~> 1.10"]), + Gem::Dependency.new("parser", [">= 3.2.0.0"]), + ] + end + + subject { described_class.new(spec, actual_dependencies, lockfile_dependencies) } + + it "shows only mismatched dependencies" do + message = subject.message + + expect(message).to include("json: gemspec specifies") + expect(message).to include("parser: gemspec specifies") + expect(message).not_to include("parallel") + end + end + + context "when gemspec has dependencies but lockfile has none" do + let(:actual_dependencies) do + [ + Gem::Dependency.new("myrack-test", ["~> 1.0"]), + ] + end + + let(:lockfile_dependencies) { [] } + + subject { described_class.new(spec, actual_dependencies, lockfile_dependencies) } + + it "shows the dependency as not in lockfile" do + message = subject.message + + expect(message).to include("myrack-test: gemspec specifies ~> 1.0, not in lockfile") + end + end + + context "when gemspec has no dependencies but lockfile has some" do + let(:actual_dependencies) { [] } + + let(:lockfile_dependencies) do + [ + Gem::Dependency.new("unexpected", ["~> 1.0"]), + ] + end + + subject { described_class.new(spec, actual_dependencies, lockfile_dependencies) } + + it "shows the dependency as not in gemspec" do + message = subject.message + + expect(message).to include("unexpected: not in gemspec, lockfile has ~> 1.0") + end + end + end + + describe "#status_code" do + let(:spec) { double("LazySpecification", full_name: "test-1.0.0") } + subject { described_class.new(spec) } + + it "returns 41" do + expect(subject.status_code).to eq(41) + end + end +end diff --git a/spec/bundler/bundler/plugin/index_spec.rb b/spec/bundler/bundler/plugin/index_spec.rb index 565fc9b088e352..a28934269bd24a 100644 --- a/spec/bundler/bundler/plugin/index_spec.rb +++ b/spec/bundler/bundler/plugin/index_spec.rb @@ -193,4 +193,83 @@ include_examples "it cleans up" end end + + describe "relative plugin paths" do + let(:plugin_name) { "relative-plugin" } + + before do + Bundler::Plugin.reset! + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + + plugin_root = Bundler::Plugin.root + FileUtils.mkdir_p(plugin_root) + + path = plugin_root.join(plugin_name) + FileUtils.mkdir_p(path.join("lib")) + + index.register_plugin(plugin_name, path.to_s, [path.join("lib").to_s], [], [], []) + end + + it "stores plugin paths relative to the plugin root" do + require "yaml" + data = YAML.load_file(index.index_file) + + expect(data["plugin_paths"][plugin_name]).to eq(plugin_name) + expect(data["load_paths"][plugin_name]).to eq([File.join(plugin_name, "lib")]) + end + + it "expands relative paths to absolute on load" do + require "bundler/yaml_serializer" + + plugin_root = Bundler::Plugin.root + + relative_index = { + "commands" => {}, + "hooks" => {}, + "load_paths" => { plugin_name => [File.join(plugin_name, "lib")] }, + "plugin_paths" => { plugin_name => plugin_name }, + "sources" => {}, + } + + File.open(index.index_file, "w") {|f| f.puts Bundler::YAMLSerializer.dump(relative_index) } + + new_index = Index.new + expect(new_index.plugin_path(plugin_name)).to eq(plugin_root.join(plugin_name)) + expect(new_index.load_paths(plugin_name)).to eq([plugin_root.join(plugin_name, "lib").to_s]) + end + + it "keeps paths outside the plugin root as absolute" do + outside_path = tmp.join("outside", "external-plugin") + FileUtils.mkdir_p(outside_path.join("lib")) + + index.register_plugin("external-plugin", outside_path.to_s, [outside_path.join("lib").to_s], [], [], []) + + require "yaml" + data = YAML.load_file(index.index_file) + + expect(data["plugin_paths"]["external-plugin"]).to eq(outside_path.to_s) + expect(data["load_paths"]["external-plugin"]).to eq([outside_path.join("lib").to_s]) + end + + it "reads legacy index files with absolute paths" do + require "bundler/yaml_serializer" + + plugin_root = Bundler::Plugin.root + absolute_path = plugin_root.join(plugin_name).to_s + + legacy_index = { + "commands" => {}, + "hooks" => {}, + "load_paths" => { plugin_name => [File.join(absolute_path, "lib")] }, + "plugin_paths" => { plugin_name => absolute_path }, + "sources" => {}, + } + + File.open(index.index_file, "w") {|f| f.puts Bundler::YAMLSerializer.dump(legacy_index) } + + new_index = Index.new + expect(new_index.plugin_path(plugin_name)).to eq(Pathname.new(absolute_path)) + expect(new_index.load_paths(plugin_name)).to eq([File.join(absolute_path, "lib")]) + end + end end diff --git a/spec/bundler/commands/install_spec.rb b/spec/bundler/commands/install_spec.rb index ae651bf981c7f8..86079e66b4d78b 100644 --- a/spec/bundler/commands/install_spec.rb +++ b/spec/bundler/commands/install_spec.rb @@ -1658,7 +1658,8 @@ def run bundle "install", raise_on_error: false expect(exitstatus).to eq(41) - expect(err).to eq("Bundler found incorrect dependencies in the lockfile for myrack_middleware-1.0") + expect(err).to include("Bundler found incorrect dependencies in the lockfile for myrack_middleware-1.0") + expect(err).to include("myrack: gemspec specifies = 0.9.1, not in lockfile") end it "updates the lockfile when not frozen" do diff --git a/spec/bundler/install/failure_spec.rb b/spec/bundler/install/failure_spec.rb index 2c2773e8491752..32ca4554396d05 100644 --- a/spec/bundler/install/failure_spec.rb +++ b/spec/bundler/install/failure_spec.rb @@ -48,4 +48,38 @@ end end end + + context "when lockfile dependencies don't match the gemspec" do + before do + build_repo4 do + build_gem "myrack", "1.0.0" do |s| + s.add_dependency "myrack-test", "~> 1.0" + end + + build_gem "myrack-test", "1.0.0" + end + + gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + # First install to generate lockfile + bundle :install + + # Manually edit lockfile to have incorrect dependencies + lockfile_content = File.read(bundled_app_lock) + # Remove the myrack-test dependency from myrack + lockfile_content.gsub!(/^ myrack \(1\.0\.0\)\n myrack-test \(~> 1\.0\)\n/, " myrack (1.0.0)\n") + File.write(bundled_app_lock, lockfile_content) + end + + it "reports the mismatch with detailed information" do + bundle :install, raise_on_error: false, env: { "BUNDLE_FROZEN" => "true" } + + expect(err).to include("Bundler found incorrect dependencies in the lockfile for myrack-1.0.0") + expect(err).to include("myrack-test: gemspec specifies ~> 1.0, not in lockfile") + expect(err).to include("Please run `bundle install` to regenerate the lockfile.") + end + end end diff --git a/spec/bundler/lock/lockfile_spec.rb b/spec/bundler/lock/lockfile_spec.rb index dcefe9cc2a2eff..654ac02aa7d43a 100644 --- a/spec/bundler/lock/lockfile_spec.rb +++ b/spec/bundler/lock/lockfile_spec.rb @@ -1608,7 +1608,8 @@ gem "myrack_middleware" G - expect(err).to eq("Bundler found incorrect dependencies in the lockfile for myrack_middleware-1.0") + expect(err).to include("Bundler found incorrect dependencies in the lockfile for myrack_middleware-1.0") + expect(err).to include("myrack: gemspec specifies = 0.9.1, not in lockfile") expect(the_bundle).not_to include_gems "myrack_middleware 1.0" end diff --git a/spec/ruby/core/marshal/dump_spec.rb b/spec/ruby/core/marshal/dump_spec.rb index ff9b9214faffa4..0de6d8c7e65263 100644 --- a/spec/ruby/core/marshal/dump_spec.rb +++ b/spec/ruby/core/marshal/dump_spec.rb @@ -473,14 +473,26 @@ def _dump(level) 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 diff --git a/spec/ruby/core/marshal/shared/load.rb b/spec/ruby/core/marshal/shared/load.rb index 692c14cfa10adb..edc430e522a8dd 100644 --- a/spec/ruby/core/marshal/shared/load.rb +++ b/spec/ruby/core/marshal/shared/load.rb @@ -901,14 +901,52 @@ def io.binmode; raise "binmode"; end 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 @@ def io.binmode; raise "binmode"; end [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)) diff --git a/spec/ruby/core/regexp/initialize_spec.rb b/spec/ruby/core/regexp/initialize_spec.rb index dd57292242494c..aeab6d0f5ca1fe 100644 --- a/spec/ruby/core/regexp/initialize_spec.rb +++ b/spec/ruby/core/regexp/initialize_spec.rb @@ -9,7 +9,21 @@ -> { //.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 diff --git a/spec/ruby/core/regexp/shared/new.rb b/spec/ruby/core/regexp/shared/new.rb index 7b0f12d623cb7b..ba06ded756c605 100644 --- a/spec/ruby/core/regexp/shared/new.rb +++ b/spec/ruby/core/regexp/shared/new.rb @@ -5,6 +5,12 @@ 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) diff --git a/spec/ruby/core/string/match_spec.rb b/spec/ruby/core/string/match_spec.rb index 5e988f34caea80..3a906716c53b5a 100644 --- a/spec/ruby/core/string/match_spec.rb +++ b/spec/ruby/core/string/match_spec.rb @@ -137,9 +137,17 @@ def obj.method_missing(*args) "." end 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 diff --git a/spec/ruby/optional/capi/encoding_spec.rb b/spec/ruby/optional/capi/encoding_spec.rb index 260ebc88a437cc..a4a42be7561f65 100644 --- a/spec/ruby/optional/capi/encoding_spec.rb +++ b/spec/ruby/optional/capi/encoding_spec.rb @@ -489,12 +489,20 @@ @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 @@ -> { @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 @@ 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 diff --git a/test/json/json_parser_test.rb b/test/json/json_parser_test.rb index 2d2f065ecb8f2a..67d86a0b35db28 100644 --- a/test/json/json_parser_test.rb +++ b/test/json/json_parser_test.rb @@ -183,6 +183,15 @@ def test_parse_allowed_control_chars_in_string end end + def test_parse_control_char_and_backslash + backslash_and_control_char = "\\\t" + assert_raise JSON::ParserError do + JSON.parse(%("#{'a' * 30}#{backslash_and_control_char}"), allow_control_characters: true, allow_invalid_escape: false) + end + + JSON.parse(%("#{'a' * 30}#{backslash_and_control_char}"), allow_control_characters: true, allow_invalid_escape: true) + end + def test_parse_invalid_escape assert_raise JSON::ParserError do parse(%("fo\\o")) diff --git a/test/ruby/test_enum.rb b/test/ruby/test_enum.rb index 237bdc8a4d0155..32ec4f5779666c 100644 --- a/test/ruby/test_enum.rb +++ b/test/ruby/test_enum.rb @@ -69,11 +69,11 @@ def test_grep_optimization 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 diff --git a/test/ruby/test_marshal.rb b/test/ruby/test_marshal.rb index 05d3ceb60a5387..0b67cd42831fb9 100644 --- a/test/ruby/test_marshal.rb +++ b/test/ruby/test_marshal.rb @@ -314,7 +314,7 @@ def test_nonascii_class_module 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 @@ def test_return_objects_are_frozen 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? diff --git a/test/ruby/test_regexp.rb b/test/ruby/test_regexp.rb index 1873f4283de2d0..c85484d81969d2 100644 --- a/test/ruby/test_regexp.rb +++ b/test/ruby/test_regexp.rb @@ -975,7 +975,7 @@ def test_union2 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 diff --git a/test/ruby/test_string.rb b/test/ruby/test_string.rb index bc911408d6d54a..0c211bbd7f3c1e 100644 --- a/test/ruby/test_string.rb +++ b/test/ruby/test_string.rb @@ -2757,8 +2757,9 @@ def o.==(other) "" == other end 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 } diff --git a/test/ruby/test_symbol.rb b/test/ruby/test_symbol.rb index c50febf5d1c6fa..fa65dca22508b5 100644 --- a/test/ruby/test_symbol.rb +++ b/test/ruby/test_symbol.rb @@ -417,8 +417,9 @@ def o.=~(x); x + "bar"; end 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 } diff --git a/test/rubygems/test_gem_commands_yank_command.rb b/test/rubygems/test_gem_commands_yank_command.rb index 73fd1772432717..457a0e65c8a930 100644 --- a/test/rubygems/test_gem_commands_yank_command.rb +++ b/test/rubygems/test_gem_commands_yank_command.rb @@ -141,6 +141,7 @@ def test_with_webauthn_enabled_success end def test_with_webauthn_enabled_failure + pend "Flaky on TruffleRuby" if RUBY_ENGINE == "truffleruby" server = Gem::MockTCPServer.new error = Gem::WebauthnVerificationError.new("Something went wrong")