From b1b2d5e107a16f5e4c7ed60192855b1ff26ab8c0 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Wed, 25 Mar 2026 16:18:26 +0900 Subject: [PATCH 01/15] Bundle rbs-4.0.2 (#16436) * Bundle rbs-4.0.2 --- gems/bundled_gems | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From b4ad6f872065346186e31fa4eea5be216cccf957 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 26 Jan 2026 18:05:43 +0900 Subject: [PATCH 02/15] [ruby/rubygems] Implement relative path handling for plugin paths in Bundler::Plugin::Index https://github.com/ruby/rubygems/commit/dd50b93622 --- lib/bundler/plugin/index.rb | 46 +++++++++++++++++++++-- spec/bundler/bundler/plugin/index_spec.rb | 44 ++++++++++++++++++++++ 2 files changed, 86 insertions(+), 4 deletions(-) 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/spec/bundler/bundler/plugin/index_spec.rb b/spec/bundler/bundler/plugin/index_spec.rb index 565fc9b088e352..2ef4323dd344bd 100644 --- a/spec/bundler/bundler/plugin/index_spec.rb +++ b/spec/bundler/bundler/plugin/index_spec.rb @@ -193,4 +193,48 @@ 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 when the plugin root changes" do + old_index_file = index.index_file + + new_root = tmp.join("moved_plugin_root") + FileUtils.mkdir_p(new_root) + FileUtils.cp(old_index_file, new_root.join("index")) + + Bundler::Plugin.reset! + Bundler::Plugin.instance_variable_set(:@root, nil) + allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) + allow(Bundler::Plugin).to receive(:root).and_return(new_root) + allow(Bundler::Plugin).to receive(:local_root).and_return(new_root) + + new_index = Index.new + + expect(new_index.plugin_path(plugin_name)).to eq(new_root.join(plugin_name)) + expect(new_index.load_paths(plugin_name)).to eq([new_root.join(plugin_name, "lib").to_s]) + end + end end From b3cdedb8acd54dd088281537bdc9fd4b468d22b0 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 25 Mar 2026 14:56:42 +0900 Subject: [PATCH 03/15] [ruby/rubygems] Consolidate path conversion helpers and add test coverage Replace four separate path conversion methods (relativize_paths, relativize_load_paths, absolutize_paths, absolutize_load_paths) with a single transform_index_paths method that handles both String and Array values via a block. Add tests for paths outside the plugin root staying absolute and for backward compatibility with legacy index files containing absolute paths. https://github.com/ruby/rubygems/commit/5fc8452533 Co-Authored-By: Claude Opus 4.6 (1M context) --- spec/bundler/bundler/plugin/index_spec.rb | 34 +++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/spec/bundler/bundler/plugin/index_spec.rb b/spec/bundler/bundler/plugin/index_spec.rb index 2ef4323dd344bd..2c7fb09850406f 100644 --- a/spec/bundler/bundler/plugin/index_spec.rb +++ b/spec/bundler/bundler/plugin/index_spec.rb @@ -236,5 +236,39 @@ expect(new_index.plugin_path(plugin_name)).to eq(new_root.join(plugin_name)) expect(new_index.load_paths(plugin_name)).to eq([new_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_relative "../../../bundler/lib/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 From c46b911dcc3ccbab5f25c5a8af5d6cabeaac7e74 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 25 Mar 2026 14:58:15 +0900 Subject: [PATCH 04/15] [ruby/rubygems] Rewrite relative path expansion test to avoid mocking Plugin internals Replace the mock-heavy test that stubbed Plugin.root and used instance_variable_set with a direct YAML write/read approach, consistent with the other relative path tests. https://github.com/ruby/rubygems/commit/64730e987f Co-Authored-By: Claude Opus 4.6 (1M context) --- spec/bundler/bundler/plugin/index_spec.rb | 27 ++++++++++++----------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/spec/bundler/bundler/plugin/index_spec.rb b/spec/bundler/bundler/plugin/index_spec.rb index 2c7fb09850406f..7668c850b01e46 100644 --- a/spec/bundler/bundler/plugin/index_spec.rb +++ b/spec/bundler/bundler/plugin/index_spec.rb @@ -218,23 +218,24 @@ expect(data["load_paths"][plugin_name]).to eq([File.join(plugin_name, "lib")]) end - it "expands relative paths when the plugin root changes" do - old_index_file = index.index_file + it "expands relative paths to absolute on load" do + require_relative "../../../bundler/lib/bundler/yaml_serializer" - new_root = tmp.join("moved_plugin_root") - FileUtils.mkdir_p(new_root) - FileUtils.cp(old_index_file, new_root.join("index")) + plugin_root = Bundler::Plugin.root - Bundler::Plugin.reset! - Bundler::Plugin.instance_variable_set(:@root, nil) - allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile) - allow(Bundler::Plugin).to receive(:root).and_return(new_root) - allow(Bundler::Plugin).to receive(:local_root).and_return(new_root) + relative_index = { + "commands" => {}, + "hooks" => {}, + "load_paths" => { plugin_name => [File.join(plugin_name, "lib")] }, + "plugin_paths" => { plugin_name => plugin_name }, + "sources" => {}, + } - new_index = Index.new + File.open(index.index_file, "w") {|f| f.puts Bundler::YAMLSerializer.dump(relative_index) } - expect(new_index.plugin_path(plugin_name)).to eq(new_root.join(plugin_name)) - expect(new_index.load_paths(plugin_name)).to eq([new_root.join(plugin_name, "lib").to_s]) + 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 From 524408869f53e5fbcbe59419b717e0d134831b50 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 25 Mar 2026 15:28:00 +0900 Subject: [PATCH 05/15] [ruby/rubygems] Fix require path for yaml_serializer in plugin index specs Use `require "bundler/yaml_serializer"` instead of `require_relative` so the tests work in both the rubygems repo and ruby-core CI layouts. https://github.com/ruby/rubygems/commit/823b6dd3fb Co-Authored-By: Claude Opus 4.6 (1M context) --- spec/bundler/bundler/plugin/index_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/bundler/bundler/plugin/index_spec.rb b/spec/bundler/bundler/plugin/index_spec.rb index 7668c850b01e46..a28934269bd24a 100644 --- a/spec/bundler/bundler/plugin/index_spec.rb +++ b/spec/bundler/bundler/plugin/index_spec.rb @@ -219,7 +219,7 @@ end it "expands relative paths to absolute on load" do - require_relative "../../../bundler/lib/bundler/yaml_serializer" + require "bundler/yaml_serializer" plugin_root = Bundler::Plugin.root @@ -252,7 +252,7 @@ end it "reads legacy index files with absolute paths" do - require_relative "../../../bundler/lib/bundler/yaml_serializer" + require "bundler/yaml_serializer" plugin_root = Bundler::Plugin.root absolute_path = plugin_root.join(plugin_name).to_s From 1f885d6431f69dbb530100fb16af0a952c1771bf Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 25 Mar 2026 15:20:45 +0900 Subject: [PATCH 06/15] [ruby/rubygems] Include detailed gemspec vs. lockfile dependency discrepancies When the lockfile dependencies don't match the gemspec, display both sets of dependencies so users can easily identify which ones differ. https://github.com/ruby/rubygems/commit/c90d7784f8 --- lib/bundler/errors.rb | 38 +++++++++- lib/bundler/lazy_specification.rb | 2 +- spec/bundler/bundler/errors_spec.rb | 104 ++++++++++++++++++++++++++ spec/bundler/commands/install_spec.rb | 6 +- spec/bundler/install/failure_spec.rb | 38 ++++++++++ spec/bundler/lock/lockfile_spec.rb | 6 +- 6 files changed, 188 insertions(+), 6 deletions(-) create mode 100644 spec/bundler/bundler/errors_spec.rb diff --git a/lib/bundler/errors.rb b/lib/bundler/errors.rb index d8df4d6ec5c553..c20aa3b7dcc6fb 100644 --- a/lib/bundler/errors.rb +++ b/lib/bundler/errors.rb @@ -265,14 +265,46 @@ 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}" + msg = "Bundler found incorrect dependencies in the lockfile for #{spec.full_name}\n" + + if @actual_dependencies && @lockfile_dependencies + msg << "\n" + msg << "The gemspec for #{spec.full_name} specifies the following dependencies:\n" + if @actual_dependencies.empty? + msg << " (none)\n" + else + @actual_dependencies.sort_by(&:to_s).each do |dep| + msg << " #{dep}\n" + end + end + + msg << "\n" + msg << "However, the lockfile has the following dependencies recorded:\n" + if @lockfile_dependencies.empty? + msg << " (none)\n" + else + @lockfile_dependencies.sort_by(&:to_s).each do |dep| + msg << " #{dep}\n" + end + end + + msg << "\n" + msg << "This discrepancy may be caused by manually editing the lockfile.\n" + msg << "Please run `bundle install` to regenerate the lockfile with correct dependencies." + else + msg << "\nPlease run `bundle install` to regenerate the lockfile." + end + + msg 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/spec/bundler/bundler/errors_spec.rb b/spec/bundler/bundler/errors_spec.rb new file mode 100644 index 00000000000000..f88f8497e7e6b2 --- /dev/null +++ b/spec/bundler/bundler/errors_spec.rb @@ -0,0 +1,104 @@ +# 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 "provides a detailed error message showing the discrepancy" do + message = subject.message + + expect(message).to include("Bundler found incorrect dependencies in the lockfile for rubocop-1.82.0") + expect(message).to include("The gemspec for rubocop-1.82.0 specifies the following dependencies:") + expect(message).to include("json (>= 2.3, < 4.0)") + expect(message).to include("parallel (~> 1.10)") + expect(message).to include("parser (>= 3.3.0.2)") + expect(message).to include("However, the lockfile has the following dependencies recorded:") + expect(message).to include("json (>= 2.3, < 3.0)") + expect(message).to include("parser (>= 3.2.0.0)") + expect(message).to include("This discrepancy may be caused by manually editing the lockfile.") + expect(message).to include("Please run `bundle install` to regenerate the lockfile with correct dependencies.") + 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 that lockfile has no dependencies" do + message = subject.message + + expect(message).to include("The gemspec for rubocop-1.82.0 specifies the following dependencies:") + expect(message).to include("myrack-test (~> 1.0)") + expect(message).to include("However, the lockfile has the following dependencies recorded:") + expect(message).to include("(none)") + 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 that gemspec has no dependencies" do + message = subject.message + + expect(message).to include("The gemspec for rubocop-1.82.0 specifies the following dependencies:") + expect(message).to include("(none)") + expect(message).to include("However, the lockfile has the following dependencies recorded:") + expect(message).to include("unexpected (~> 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/commands/install_spec.rb b/spec/bundler/commands/install_spec.rb index ae651bf981c7f8..a0ad433d3ec665 100644 --- a/spec/bundler/commands/install_spec.rb +++ b/spec/bundler/commands/install_spec.rb @@ -1658,7 +1658,11 @@ 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("The gemspec for myrack_middleware-1.0 specifies the following dependencies:") + expect(err).to include("myrack (= 0.9.1)") + expect(err).to include("However, the lockfile has the following dependencies recorded:") + expect(err).to include("(none)") 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..0d260edb514a38 100644 --- a/spec/bundler/install/failure_spec.rb +++ b/spec/bundler/install/failure_spec.rb @@ -48,4 +48,42 @@ 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("The gemspec for myrack-1.0.0 specifies the following dependencies:") + expect(err).to include("myrack-test (~> 1.0)") + expect(err).to include("However, the lockfile has the following dependencies recorded:") + expect(err).to include("(none)") + expect(err).to include("This discrepancy may be caused by manually editing the lockfile.") + expect(err).to include("Please run `bundle install` to regenerate the lockfile with correct dependencies.") + end + end end diff --git a/spec/bundler/lock/lockfile_spec.rb b/spec/bundler/lock/lockfile_spec.rb index dcefe9cc2a2eff..a68a91dafed838 100644 --- a/spec/bundler/lock/lockfile_spec.rb +++ b/spec/bundler/lock/lockfile_spec.rb @@ -1608,7 +1608,11 @@ 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("The gemspec for myrack_middleware-1.0 specifies the following dependencies:") + expect(err).to include("myrack (= 0.9.1)") + expect(err).to include("However, the lockfile has the following dependencies recorded:") + expect(err).to include("(none)") expect(the_bundle).not_to include_gems "myrack_middleware 1.0" end From 573c2142adb11bc7c200f370adfd3cf65625eded Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 25 Mar 2026 15:23:59 +0900 Subject: [PATCH 07/15] [ruby/rubygems] Show only mismatched dependencies in lockfile error message Instead of listing all dependencies from both gemspec and lockfile, show only the ones that actually differ to make it easier to identify the source of the discrepancy. https://github.com/ruby/rubygems/commit/8c551a3621 Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/bundler/errors.rb | 39 +++++++++++---------------- spec/bundler/bundler/errors_spec.rb | 29 ++++++-------------- spec/bundler/commands/install_spec.rb | 5 +--- spec/bundler/install/failure_spec.rb | 8 ++---- spec/bundler/lock/lockfile_spec.rb | 5 +--- 5 files changed, 28 insertions(+), 58 deletions(-) diff --git a/lib/bundler/errors.rb b/lib/bundler/errors.rb index c20aa3b7dcc6fb..ee10123296fa3b 100644 --- a/lib/bundler/errors.rb +++ b/lib/bundler/errors.rb @@ -277,33 +277,26 @@ def message msg = "Bundler found incorrect dependencies in the lockfile for #{spec.full_name}\n" if @actual_dependencies && @lockfile_dependencies - msg << "\n" - msg << "The gemspec for #{spec.full_name} specifies the following dependencies:\n" - if @actual_dependencies.empty? - msg << " (none)\n" - else - @actual_dependencies.sort_by(&:to_s).each do |dep| - msg << " #{dep}\n" + 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 } + all_names = (actual_by_name.keys | lockfile_by_name.keys).sort + + all_names.each do |name| + actual = actual_by_name[name] + lockfile = lockfile_by_name[name] + next if actual && lockfile && actual.requirement == lockfile.requirement + + if actual && lockfile + msg << " #{name}: gemspec specifies #{actual.requirement}, lockfile has #{lockfile.requirement}\n" + elsif actual + msg << " #{name}: gemspec specifies #{actual.requirement}, not in lockfile\n" + else + msg << " #{name}: not in gemspec, lockfile has #{lockfile.requirement}\n" end end - - msg << "\n" - msg << "However, the lockfile has the following dependencies recorded:\n" - if @lockfile_dependencies.empty? - msg << " (none)\n" - else - @lockfile_dependencies.sort_by(&:to_s).each do |dep| - msg << " #{dep}\n" - end - end - - msg << "\n" - msg << "This discrepancy may be caused by manually editing the lockfile.\n" - msg << "Please run `bundle install` to regenerate the lockfile with correct dependencies." - else - msg << "\nPlease run `bundle install` to regenerate the lockfile." end + msg << "Please run `bundle install` to regenerate the lockfile." msg end diff --git a/spec/bundler/bundler/errors_spec.rb b/spec/bundler/bundler/errors_spec.rb index f88f8497e7e6b2..b62d85d32bd4b1 100644 --- a/spec/bundler/bundler/errors_spec.rb +++ b/spec/bundler/bundler/errors_spec.rb @@ -34,19 +34,12 @@ subject { described_class.new(spec, actual_dependencies, lockfile_dependencies) } - it "provides a detailed error message showing the discrepancy" do + it "shows only mismatched dependencies" do message = subject.message - expect(message).to include("Bundler found incorrect dependencies in the lockfile for rubocop-1.82.0") - expect(message).to include("The gemspec for rubocop-1.82.0 specifies the following dependencies:") - expect(message).to include("json (>= 2.3, < 4.0)") - expect(message).to include("parallel (~> 1.10)") - expect(message).to include("parser (>= 3.3.0.2)") - expect(message).to include("However, the lockfile has the following dependencies recorded:") - expect(message).to include("json (>= 2.3, < 3.0)") - expect(message).to include("parser (>= 3.2.0.0)") - expect(message).to include("This discrepancy may be caused by manually editing the lockfile.") - expect(message).to include("Please run `bundle install` to regenerate the lockfile with correct dependencies.") + expect(message).to include("json: gemspec specifies") + expect(message).to include("parser: gemspec specifies") + expect(message).not_to include("parallel") end end @@ -61,13 +54,10 @@ subject { described_class.new(spec, actual_dependencies, lockfile_dependencies) } - it "shows that lockfile has no dependencies" do + it "shows the dependency as not in lockfile" do message = subject.message - expect(message).to include("The gemspec for rubocop-1.82.0 specifies the following dependencies:") - expect(message).to include("myrack-test (~> 1.0)") - expect(message).to include("However, the lockfile has the following dependencies recorded:") - expect(message).to include("(none)") + expect(message).to include("myrack-test: gemspec specifies ~> 1.0, not in lockfile") end end @@ -82,13 +72,10 @@ subject { described_class.new(spec, actual_dependencies, lockfile_dependencies) } - it "shows that gemspec has no dependencies" do + it "shows the dependency as not in gemspec" do message = subject.message - expect(message).to include("The gemspec for rubocop-1.82.0 specifies the following dependencies:") - expect(message).to include("(none)") - expect(message).to include("However, the lockfile has the following dependencies recorded:") - expect(message).to include("unexpected (~> 1.0)") + expect(message).to include("unexpected: not in gemspec, lockfile has ~> 1.0") end end end diff --git a/spec/bundler/commands/install_spec.rb b/spec/bundler/commands/install_spec.rb index a0ad433d3ec665..86079e66b4d78b 100644 --- a/spec/bundler/commands/install_spec.rb +++ b/spec/bundler/commands/install_spec.rb @@ -1659,10 +1659,7 @@ def run expect(exitstatus).to eq(41) expect(err).to include("Bundler found incorrect dependencies in the lockfile for myrack_middleware-1.0") - expect(err).to include("The gemspec for myrack_middleware-1.0 specifies the following dependencies:") - expect(err).to include("myrack (= 0.9.1)") - expect(err).to include("However, the lockfile has the following dependencies recorded:") - expect(err).to include("(none)") + 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 0d260edb514a38..32ca4554396d05 100644 --- a/spec/bundler/install/failure_spec.rb +++ b/spec/bundler/install/failure_spec.rb @@ -78,12 +78,8 @@ 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("The gemspec for myrack-1.0.0 specifies the following dependencies:") - expect(err).to include("myrack-test (~> 1.0)") - expect(err).to include("However, the lockfile has the following dependencies recorded:") - expect(err).to include("(none)") - expect(err).to include("This discrepancy may be caused by manually editing the lockfile.") - expect(err).to include("Please run `bundle install` to regenerate the lockfile with correct dependencies.") + 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 a68a91dafed838..654ac02aa7d43a 100644 --- a/spec/bundler/lock/lockfile_spec.rb +++ b/spec/bundler/lock/lockfile_spec.rb @@ -1609,10 +1609,7 @@ G expect(err).to include("Bundler found incorrect dependencies in the lockfile for myrack_middleware-1.0") - expect(err).to include("The gemspec for myrack_middleware-1.0 specifies the following dependencies:") - expect(err).to include("myrack (= 0.9.1)") - expect(err).to include("However, the lockfile has the following dependencies recorded:") - expect(err).to include("(none)") + 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 From be659a0ef277be04a1a1a01f9486230e3e3cd650 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 25 Mar 2026 15:25:32 +0900 Subject: [PATCH 08/15] [ruby/rubygems] Use Array#join to build error message instead of string concatenation https://github.com/ruby/rubygems/commit/7c30560939 Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/bundler/errors.rb | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/bundler/errors.rb b/lib/bundler/errors.rb index ee10123296fa3b..dff5d931288706 100644 --- a/lib/bundler/errors.rb +++ b/lib/bundler/errors.rb @@ -274,30 +274,31 @@ def initialize(spec, actual_dependencies = nil, lockfile_dependencies = nil) end def message - msg = "Bundler found incorrect dependencies in the lockfile for #{spec.full_name}\n" + 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 } - all_names = (actual_by_name.keys | lockfile_by_name.keys).sort - all_names.each do |name| + (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 - msg << " #{name}: gemspec specifies #{actual.requirement}, lockfile has #{lockfile.requirement}\n" + lines << " #{name}: gemspec specifies #{actual.requirement}, lockfile has #{lockfile.requirement}" elsif actual - msg << " #{name}: gemspec specifies #{actual.requirement}, not in lockfile\n" + lines << " #{name}: gemspec specifies #{actual.requirement}, not in lockfile" else - msg << " #{name}: not in gemspec, lockfile has #{lockfile.requirement}\n" + lines << " #{name}: not in gemspec, lockfile has #{lockfile.requirement}" end end + + lines << "" end - msg << "Please run `bundle install` to regenerate the lockfile." - msg + lines << "Please run `bundle install` to regenerate the lockfile." + lines.join("\n") end status_code(41) From cb03bf43eb2339e1d81b20a2dd9783b83660007c Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Sat, 13 Sep 2025 22:37:40 +0200 Subject: [PATCH 09/15] 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. --- NEWS.md | 7 +++ depend | 1 + internal/re.h | 1 + marshal.c | 21 ++++++--- re.c | 16 ++++--- spec/ruby/core/marshal/dump_spec.rb | 22 ++++++--- spec/ruby/core/marshal/shared/load.rb | 57 ++++++++++++++++++------ spec/ruby/core/regexp/initialize_spec.rb | 18 +++++++- spec/ruby/core/regexp/shared/new.rb | 6 +++ spec/ruby/core/string/match_spec.rb | 14 ++++-- spec/ruby/optional/capi/encoding_spec.rb | 43 ++++++++++++++---- test/ruby/test_enum.rb | 10 ++--- test/ruby/test_marshal.rb | 4 +- test/ruby/test_regexp.rb | 2 +- test/ruby/test_string.rb | 5 ++- test/ruby/test_symbol.rb | 5 ++- 16 files changed, 175 insertions(+), 57 deletions(-) diff --git a/NEWS.md b/NEWS.md index ea693b710de730..11fef790ca28ba 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 @@ -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/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/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/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/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 } From 58f5d3a9346f574634043f186c6e88a05c013376 Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Tue, 24 Mar 2026 13:50:47 +0000 Subject: [PATCH 10/15] wip is this still necessary --- gc/default/default.c | 5 ----- 1 file changed, 5 deletions(-) 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; From a4c8964846e7d030755871f8751796e7aed75e00 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 25 Mar 2026 17:29:54 +0900 Subject: [PATCH 11/15] [ruby/rubygems] Skip flaky test_with_webauthn_enabled_failure on TruffleRuby https://github.com/ruby/rubygems/commit/f144a28b64 Co-Authored-By: Claude Opus 4.6 (1M context) --- test/rubygems/test_gem_commands_yank_command.rb | 1 + 1 file changed, 1 insertion(+) 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") From 89a2eeb29c201d82b10e5a6b47d48877f9c20df1 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 25 Mar 2026 11:45:17 +0100 Subject: [PATCH 12/15] [ruby/json] Fix handling of unescaped control characters preceeded by a backslash https://github.com/ruby/json/commit/b575be5302 --- ext/json/parser/parser.c | 4 +++- test/json/json_parser_test.rb | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) 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/test/json/json_parser_test.rb b/test/json/json_parser_test.rb index 2d2f065ecb8f2a..af08d18357f563 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_parsse_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")) From fe087044734ab10565ac3bb32e87b9e1cbb6ee35 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 25 Mar 2026 12:02:34 +0100 Subject: [PATCH 13/15] [ruby/json] Release 2.19.3 https://github.com/ruby/json/commit/37caa89272 --- ext/json/lib/json/version.rb | 2 +- test/json/json_parser_test.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/test/json/json_parser_test.rb b/test/json/json_parser_test.rb index af08d18357f563..67d86a0b35db28 100644 --- a/test/json/json_parser_test.rb +++ b/test/json/json_parser_test.rb @@ -183,7 +183,7 @@ def test_parse_allowed_control_chars_in_string end end - def test_parsse_control_char_and_backslash + 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) From 173b97103f3f870179070f5d371f20bf0eddbfb8 Mon Sep 17 00:00:00 2001 From: git Date: Wed, 25 Mar 2026 11:04:11 +0000 Subject: [PATCH 14/15] Update default gems list at fe087044734ab10565ac3bb32e87b9 [ci skip] --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 11fef790ca28ba..7e00b3a851c0a9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -75,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] From fda7e6750d79a0be34b87fd6cd89b71f0c6f57da Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Wed, 25 Mar 2026 12:19:35 +0100 Subject: [PATCH 15/15] Fix typo --- include/ruby/internal/core/rmatch.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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;