From d1ff1a3e5fdb3f11f9a7c5572338347247657feb Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Fri, 8 Nov 2024 16:26:27 +0200 Subject: [PATCH 1/8] Fix validation of seconds in utc offset in String format --- CHANGELOG.md | 1 + spec/ruby/core/time/at_spec.rb | 52 ++++++++++++++++++++++ spec/ruby/core/time/getlocal_spec.rb | 58 +++++++++++++++++++++++++ spec/ruby/core/time/localtime_spec.rb | 60 ++++++++++++++++++++++++++ spec/ruby/core/time/new_spec.rb | 29 ++++++++++++- spec/ruby/core/time/now_spec.rb | 31 +++++++++++++ spec/tags/core/time/new_tags.txt | 1 - src/main/ruby/truffleruby/core/type.rb | 14 +++--- 8 files changed, 238 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e73f84c36f2b..4d6a74f01e20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Bug fixes: * Fix `Module#name` called inside the `Module#const_added` callback when the module is defined in the top-level scope (#3683, @andrykonchin). * Fix duplicated calls of a `Module#const_added` callback when a module with nested modules is assigned to a constant (@andrykonchin). * Support OpenSSL 1.1-3.4 and prefer in order OpenSSL 3.0.x, 3.x and 1.1 (EOL). There was a compilation issue with OpenSSL 3.4 (#3724, @eregon). +* Fix `Time{.at,.new,.now,#getlocal,#localtime}` methods and validation of seconds in utc offset in String format (@andrykonchin). Compatibility: diff --git a/spec/ruby/core/time/at_spec.rb b/spec/ruby/core/time/at_spec.rb index 48fb3c6f5233..85bb6d7ebf05 100644 --- a/spec/ruby/core/time/at_spec.rb +++ b/spec/ruby/core/time/at_spec.rb @@ -228,6 +228,12 @@ time.utc_offset.should == -9*60*60 time.zone.should == nil time.to_i.should == @epoch_time + + time = Time.at(@epoch_time, in: "-09:00:01") + + time.utc_offset.should == -(9*60*60 + 1) + time.zone.should == nil + time.to_i.should == @epoch_time end it "could be UTC offset as a number of seconds" do @@ -280,5 +286,51 @@ -> { Time.at(@epoch_time, in: "+09:99") }.should raise_error(ArgumentError) -> { Time.at(@epoch_time, in: "ABC") }.should raise_error(ArgumentError) end + + it "raises ArgumentError if hours greater than 23" do # TODO + ruby_version_is ""..."3.1" do + -> { Time.at(@epoch_time, in: "+24:00") }.should raise_error(ArgumentError, 'utc_offset out of range') + -> { Time.at(@epoch_time, in: "+2400") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + + -> { Time.at(@epoch_time, in: "+99:00") }.should raise_error(ArgumentError, 'utc_offset out of range') + -> { Time.at(@epoch_time, in: "+9900") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + end + + ruby_version_is "3.1" do + -> { Time.at(@epoch_time, in: "+24:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.at(@epoch_time, in: "+2400") }.should raise_error(ArgumentError, "utc_offset out of range") + + -> { Time.at(@epoch_time, in: "+99:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.at(@epoch_time, in: "+9900") }.should raise_error(ArgumentError, "utc_offset out of range") + end + end + + it "raises ArgumentError if minutes greater than 59" do # TODO + ruby_version_is ""..."3.1" do + -> { Time.at(@epoch_time, in: "+00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + -> { Time.at(@epoch_time, in: "+0060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + + -> { Time.at(@epoch_time, in: "+00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + -> { Time.at(@epoch_time, in: "+0099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + end + + ruby_version_is "3.1" do + -> { Time.at(@epoch_time, in: "+00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:60') + -> { Time.at(@epoch_time, in: "+0060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0060') + + -> { Time.at(@epoch_time, in: "+00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:99') + -> { Time.at(@epoch_time, in: "+0099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0099') + end + end + + ruby_bug '#20797', ''...'3.4' do + it "raises ArgumentError if seconds greater than 59" do + -> { Time.at(@epoch_time, in: "+00:00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:60') + -> { Time.at(@epoch_time, in: "+000060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000060') + + -> { Time.at(@epoch_time, in: "+00:00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:99') + -> { Time.at(@epoch_time, in: "+000099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000099') + end + end end end diff --git a/spec/ruby/core/time/getlocal_spec.rb b/spec/ruby/core/time/getlocal_spec.rb index 926a6dbf4504..ff3e3d8dd1aa 100644 --- a/spec/ruby/core/time/getlocal_spec.rb +++ b/spec/ruby/core/time/getlocal_spec.rb @@ -59,12 +59,24 @@ t.utc_offset.should == 3600 end + it "returns a Time with a UTC offset specified as +HH:MM:SS" do + t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal("+01:00:01") + t.should == Time.new(2007, 1, 9, 13, 0, 1, 3601) + t.utc_offset.should == 3601 + end + it "returns a Time with a UTC offset specified as -HH:MM" do t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal("-01:00") t.should == Time.new(2007, 1, 9, 11, 0, 0, -3600) t.utc_offset.should == -3600 end + it "returns a Time with a UTC offset specified as -HH:MM:SS" do + t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal("-01:00:01") + t.should == Time.new(2007, 1, 9, 10, 59, 59, -3601) + t.utc_offset.should == -3601 + end + describe "with an argument that responds to #to_str" do it "coerces using #to_str" do o = mock('string') @@ -97,6 +109,52 @@ -> { t.getlocal(86400) }.should raise_error(ArgumentError) end + it "raises ArgumentError if String argument and hours greater than 23" do + ruby_version_is ""..."3.1" do + -> { Time.now.getlocal("+24:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now.getlocal("+2400") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + + -> { Time.now.getlocal("+99:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now.getlocal("+9900") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + end + + ruby_version_is "3.1" do + -> { Time.now.getlocal("+24:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now.getlocal("+2400") }.should raise_error(ArgumentError, "utc_offset out of range") + + -> { Time.now.getlocal("+99:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now.getlocal("+9900") }.should raise_error(ArgumentError, "utc_offset out of range") + end + end + + it "raises ArgumentError if String argument and minutes greater than 59" do + ruby_version_is ""..."3.1" do + -> { Time.now.getlocal("+00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + -> { Time.now.getlocal("+0060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + + -> { Time.now.getlocal("+00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + -> { Time.now.getlocal("+0099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + end + + ruby_version_is "3.1" do + -> { Time.now.getlocal("+00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:60') + -> { Time.now.getlocal("+0060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0060') + + -> { Time.now.getlocal("+00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:99') + -> { Time.now.getlocal("+0099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0099') + end + end + + ruby_bug '#20797', ''...'3.4' do + it "raises ArgumentError if String argument and seconds greater than 59" do + -> { Time.now.getlocal("+00:00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:60') + -> { Time.now.getlocal("+000060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000060') + + -> { Time.now.getlocal("+00:00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:99') + -> { Time.now.getlocal("+000099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000099') + end + end + describe "with a timezone argument" do it "returns a Time in the timezone" do zone = TimeSpecs::Timezone.new(offset: (5*3600+30*60)) diff --git a/spec/ruby/core/time/localtime_spec.rb b/spec/ruby/core/time/localtime_spec.rb index 609b6532a16c..5badba9fb265 100644 --- a/spec/ruby/core/time/localtime_spec.rb +++ b/spec/ruby/core/time/localtime_spec.rb @@ -72,6 +72,13 @@ t.utc_offset.should == 3600 end + it "returns a Time with a UTC offset specified as +HH:MM:SS" do + t = Time.gm(2007, 1, 9, 12, 0, 0) + t.localtime("+01:00:01") + t.should == Time.new(2007, 1, 9, 13, 0, 1, 3601) + t.utc_offset.should == 3601 + end + it "returns a Time with a UTC offset specified as -HH:MM" do t = Time.gm(2007, 1, 9, 12, 0, 0) t.localtime("-01:00") @@ -79,6 +86,13 @@ t.utc_offset.should == -3600 end + it "returns a Time with a UTC offset specified as -HH:MM:SS" do + t = Time.gm(2007, 1, 9, 12, 0, 0) + t.localtime("-01:00:01") + t.should == Time.new(2007, 1, 9, 10, 59, 59, -3601) + t.utc_offset.should == -3601 + end + it "returns a Time with a UTC offset specified as UTC" do t = Time.new(2007, 1, 9, 12, 0, 0, 3600) t.localtime("UTC") @@ -91,6 +105,52 @@ t.utc_offset.should == 3600 * 2 end + it "raises ArgumentError if String argument and hours greater than 23" do + ruby_version_is ""..."3.1" do + -> { Time.now.localtime("+24:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now.localtime("+2400") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + + -> { Time.now.localtime("+99:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now.localtime("+9900") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + end + + ruby_version_is "3.1" do + -> { Time.now.localtime("+24:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now.localtime("+2400") }.should raise_error(ArgumentError, "utc_offset out of range") + + -> { Time.now.localtime("+99:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now.localtime("+9900") }.should raise_error(ArgumentError, "utc_offset out of range") + end + end + + it "raises ArgumentError if String argument and minutes greater than 59" do + ruby_version_is ""..."3.1" do + -> { Time.now.localtime("+00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + -> { Time.now.localtime("+0060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + + -> { Time.now.localtime("+00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + -> { Time.now.localtime("+0099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset') + end + + ruby_version_is "3.1" do + -> { Time.now.localtime("+00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:60') + -> { Time.now.localtime("+0060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0060') + + -> { Time.now.localtime("+00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:99') + -> { Time.now.localtime("+0099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0099') + end + end + + ruby_bug '#20797', ''...'3.4' do + it "raises ArgumentError if String argument and seconds greater than 59" do + -> { Time.now.localtime("+00:00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:60') + -> { Time.now.localtime("+000060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000060') + + -> { Time.now.localtime("+00:00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:99') + -> { Time.now.localtime("+000099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000099') + end + end + platform_is_not :windows do it "changes the timezone according to the set one" do t = Time.new(2005, 2, 27, 22, 50, 0, -3600) diff --git a/spec/ruby/core/time/new_spec.rb b/spec/ruby/core/time/new_spec.rb index 24bb9fde0caf..1a743b485ef3 100644 --- a/spec/ruby/core/time/new_spec.rb +++ b/spec/ruby/core/time/new_spec.rb @@ -415,6 +415,11 @@ def zone.local_to_utc(t) time.utc_offset.should == -9*60*60 time.zone.should == nil + + time = Time.new(2000, 1, 1, 12, 0, 0, in: "-09:00:01") + + time.utc_offset.should == -(9*60*60 + 1) + time.zone.should == nil end it "could be UTC offset as a number of seconds" do @@ -659,7 +664,7 @@ def obj.to_int; 3; end -> { Time.new("2020-12-25 00:56:17 +23:61") - }.should raise_error(ArgumentError, /utc_offset|can't parse:/) + }.should raise_error(ArgumentError, /utc_offset/) ruby_bug '#20797', ''...'3.4' do -> { @@ -668,6 +673,28 @@ def obj.to_int; 3; end end end + it "raises ArgumentError if utc offset parts are not valid" do + -> { Time.new("2020-12-25 00:56:17 +24:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.new("2020-12-25 00:56:17 +2400") }.should raise_error(ArgumentError, "utc_offset out of range") + + -> { Time.new("2020-12-25 00:56:17 +99:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.new("2020-12-25 00:56:17 +9900") }.should raise_error(ArgumentError, "utc_offset out of range") + + -> { Time.new("2020-12-25 00:56:17 +00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:60') + -> { Time.new("2020-12-25 00:56:17 +0060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0060') + + -> { Time.new("2020-12-25 00:56:17 +00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:99') + -> { Time.new("2020-12-25 00:56:17 +0099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0099') + + ruby_bug '#20797', ''...'3.4' do + -> { Time.new("2020-12-25 00:56:17 +00:00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:60') + -> { Time.new("2020-12-25 00:56:17 +000060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000060') + + -> { Time.new("2020-12-25 00:56:17 +00:00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:99') + -> { Time.new("2020-12-25 00:56:17 +000099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000099') + end + end + it "raises ArgumentError if string has not ascii-compatible encoding" do -> { Time.new("2021-11-31 00:00:60 +09:00".encode("utf-32le")) diff --git a/spec/ruby/core/time/now_spec.rb b/spec/ruby/core/time/now_spec.rb index d47f00723ed8..7c2425411ac8 100644 --- a/spec/ruby/core/time/now_spec.rb +++ b/spec/ruby/core/time/now_spec.rb @@ -16,6 +16,11 @@ time.utc_offset.should == -9*60*60 time.zone.should == nil + + time = Time.now(in: "-09:00:01") + + time.utc_offset.should == -(9*60*60 + 1) + time.zone.should == nil end it "could be UTC offset as a number of seconds" do @@ -52,6 +57,32 @@ -> { Time.now(in: "+09:99") }.should raise_error(ArgumentError) -> { Time.now(in: "ABC") }.should raise_error(ArgumentError) end + + it "raises ArgumentError if String argument and hours greater than 23" do + -> { Time.now(in: "+24:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now(in: "+2400") }.should raise_error(ArgumentError, "utc_offset out of range") + + -> { Time.now(in: "+99:00") }.should raise_error(ArgumentError, "utc_offset out of range") + -> { Time.now(in: "+9900") }.should raise_error(ArgumentError, "utc_offset out of range") + end + + it "raises ArgumentError if String argument and minutes greater than 59" do + -> { Time.now(in: "+00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:60') + -> { Time.now(in: "+0060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0060') + + -> { Time.now(in: "+00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:99') + -> { Time.now(in: "+0099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0099') + end + + ruby_bug '#20797', ''...'3.4' do + it "raises ArgumentError if String argument and seconds greater than 59" do + -> { Time.now(in: "+00:00:60") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:60') + -> { Time.now(in: "+000060") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000060') + + -> { Time.now(in: "+00:00:99") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:99') + -> { Time.now(in: "+000099") }.should raise_error(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000099') + end + end end end end diff --git a/spec/tags/core/time/new_tags.txt b/spec/tags/core/time/new_tags.txt index c8e3291dfc72..04c1c547bdb5 100644 --- a/spec/tags/core/time/new_tags.txt +++ b/spec/tags/core/time/new_tags.txt @@ -23,4 +23,3 @@ fails:Time.new with a timezone argument :in keyword argument could be a timezone fails:Time.new with a timezone argument Time.new with a String argument accepts precision keyword argument and truncates specified digits of sub-second part fails:Time.new with a timezone argument Time.new with a String argument converts precision keyword argument into Integer if is not nil fails:Time.new with a timezone argument Time.new with a String argument raise TypeError is can't convert precision keyword argument into Integer -fails:Time.new with a timezone argument Time.new with a String argument raises ArgumentError if date/time parts values are not valid diff --git a/src/main/ruby/truffleruby/core/type.rb b/src/main/ruby/truffleruby/core/type.rb index dade0c975b24..433999aa776c 100644 --- a/src/main/ruby/truffleruby/core/type.rb +++ b/src/main/ruby/truffleruby/core/type.rb @@ -501,6 +501,10 @@ def self.coerce_to_utc_offset(offset) offset end + UTC_OFFSET_WITH_COLONS_PATTERN = /\A(?\+|-)(?\d\d)(?::(?\d\d)(?::(?\d\d))?)?\z/ + UTC_OFFSET_WITHOUT_COLONS_PATTERN = /\A(?\+|-)(?\d\d)(?:(?\d\d)(?:(?\d\d))?)?\z/ + UTC_OFFSET_PATTERN = /#{UTC_OFFSET_WITH_COLONS_PATTERN}|#{UTC_OFFSET_WITHOUT_COLONS_PATTERN}/ + def self.coerce_string_to_utc_offset(offset) unless offset.encoding.ascii_compatible? raise ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: ' + offset.inspect @@ -518,12 +522,10 @@ def self.coerce_string_to_utc_offset(offset) else offset = (offset.ord - 'N'.ord + 1) * -3600 # ("N"..Y) => -1, -2, ... end - elsif offset.match(/\A(\+|-)(\d\d)(?::(\d\d)(?::(\d\d))?)?\z/) && $1.to_i < 24 && $2.to_i < 60 && $3.to_i < 60 # with ":" separators - offset = $2.to_i*60*60 + $3.to_i*60 + $4.to_i - offset = -offset if $1.ord == 45 - elsif offset.match(/\A(\+|-)(\d\d)(?:(\d\d)(?:(\d\d))?)?\z/) && $1.to_i < 24 && $2.to_i < 60 && $3.to_i < 60 # without ":" separators - offset = $2.to_i*60*60 + $3.to_i*60 + $4.to_i - offset = -offset if $1.ord == 45 + elsif (m = offset.match(UTC_OFFSET_PATTERN)) && m[:minutes].to_i < 60 && m[:seconds].to_i < 60 + # ignore hours - they are validated indirectly in #coerce_to_utc_offset + offset = m[:hours].to_i*60*60 + m[:minutes].to_i*60 + m[:seconds].to_i + offset = -offset if m[:sign] == '-' else raise ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: ' + offset end From 9288e1b267853b7bb0f4d6dcfaefa39887626b99 Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Fri, 8 Nov 2024 17:27:31 +0200 Subject: [PATCH 2/8] Use ruby_version_is instead of ruby_bug for a spec for String#% that checks Ruby 3.4 changes --- spec/ruby/core/kernel/Float_spec.rb | 20 +++++++++++++++++++- spec/ruby/core/string/modulo_spec.rb | 4 +++- spec/tags/core/string/modulo_tags.txt | 5 ----- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/spec/ruby/core/kernel/Float_spec.rb b/spec/ruby/core/kernel/Float_spec.rb index 0f83cb5824e9..6cedfe06179e 100644 --- a/spec/ruby/core/kernel/Float_spec.rb +++ b/spec/ruby/core/kernel/Float_spec.rb @@ -157,6 +157,24 @@ def to_f() 1.2 end @object.send(:Float, "1\t\n").should == 1.0 end + ruby_version_is ""..."3.4" do + it "raises ArgumentError if a fractional part is missing" do + -> { @object.send(:Float, "1.") }.should raise_error(ArgumentError) + -> { @object.send(:Float, "+1.") }.should raise_error(ArgumentError) + -> { @object.send(:Float, "-1.") }.should raise_error(ArgumentError) + -> { @object.send(:Float, "1.e+0") }.should raise_error(ArgumentError) + end + end + + ruby_version_is "3.4" do + it "allows String representation without a fractional part" do + @object.send(:Float, "1.").should == 1.0 + @object.send(:Float, "+1.").should == 1.0 + @object.send(:Float, "-1.").should == -1.0 + @object.send(:Float, "1.e+0").should == 1.0 + end + end + %w(e E).each do |e| it "raises an ArgumentError if #{e} is the trailing character" do -> { @object.send(:Float, "2#{e}") }.should raise_error(ArgumentError) @@ -280,7 +298,7 @@ def to_f() 1.2 end nan2.should equal(nan) end - it "returns the identical Infinity if to_f is called and it returns Infinity" do + it "returns the identical Infinity if #to_f is called and it returns Infinity" do infinity = infinity_value (infinity_to_f = mock('Infinity')).should_receive(:to_f).once.and_return(infinity) infinity2 = @object.send(:Float, infinity_to_f) diff --git a/spec/ruby/core/string/modulo_spec.rb b/spec/ruby/core/string/modulo_spec.rb index 8e3853551f36..33c21418124c 100644 --- a/spec/ruby/core/string/modulo_spec.rb +++ b/spec/ruby/core/string/modulo_spec.rb @@ -749,9 +749,11 @@ def obj.to_s() "obj" end (format % "-10.4e-20").should == (format % -10.4e-20) (format % ".5").should == (format % 0.5) (format % "-.5").should == (format % -0.5) - ruby_bug("#20705", ""..."3.4") do + + ruby_version_is "3.4" do (format % "10.").should == (format % 10) end + # Something's strange with this spec: # it works just fine in individual mode, but not when run as part of a group (format % "10_1_0.5_5_5").should == (format % 1010.555) diff --git a/spec/tags/core/string/modulo_tags.txt b/spec/tags/core/string/modulo_tags.txt index d980d7313508..5d03579ee583 100644 --- a/spec/tags/core/string/modulo_tags.txt +++ b/spec/tags/core/string/modulo_tags.txt @@ -8,8 +8,3 @@ fails:String#% flags # applies to formats bBxX does nothing for zero argument fails:String#% other formats c displays only the first character if argument is a string of several characters fails:String#% other formats c displays no characters if argument is an empty string fails:String#% supports only the first character as argument for %c -fails:String#% behaves as if calling Kernel#Float for %e arguments, when the passed argument does not respond to #to_ary -fails:String#% behaves as if calling Kernel#Float for %E arguments, when the passed argument does not respond to #to_ary -fails:String#% behaves as if calling Kernel#Float for %f arguments, when the passed argument does not respond to #to_ary -fails:String#% behaves as if calling Kernel#Float for %g arguments, when the passed argument does not respond to #to_ary -fails:String#% behaves as if calling Kernel#Float for %G arguments, when the passed argument does not respond to #to_ary From 19cdb44d819867e935e2d01961422ef20a16ac71 Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Fri, 8 Nov 2024 19:44:42 +0200 Subject: [PATCH 3/8] Emit warning for Kernel#format when there are excessive arguments --- CHANGELOG.md | 1 + spec/tags/core/kernel/format_tags.txt | 1 - .../core/format/printf/PrintfCompiler.java | 54 ------------------ .../printf/PrintfSimpleTreeBuilder.java | 2 +- .../truffleruby/core/kernel/KernelNodes.java | 55 +++++++++++++++++-- 5 files changed, 51 insertions(+), 62 deletions(-) delete mode 100644 src/main/java/org/truffleruby/core/format/printf/PrintfCompiler.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d6a74f01e20..93fb79d335be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ Compatibility: * `Kernel#lambda` with now raises `ArgumentError` when given a non-lambda, non-literal block (#3681, @Th3-M4jor). * Add `rb_data_define()` to define Data (#3681, @andrykonchin). * Add `Refinement#target` (#3681, @andrykonchin). +* Emit warning when `Kernel#format` called with excessive arguments (@andrykonchin). Performance: diff --git a/spec/tags/core/kernel/format_tags.txt b/spec/tags/core/kernel/format_tags.txt index df82df8689df..69c38c7024d8 100644 --- a/spec/tags/core/kernel/format_tags.txt +++ b/spec/tags/core/kernel/format_tags.txt @@ -1,4 +1,3 @@ slow:Kernel.format when $VERBOSE is true warns if too many arguments are passed slow:Kernel.format when $VERBOSE is true does not warns if too many keyword arguments are passed slow:Kernel.format when $VERBOSE is true doesn't warns if keyword arguments are passed and none are used -fails:Kernel.format when $VERBOSE is true warns if too many arguments are passed diff --git a/src/main/java/org/truffleruby/core/format/printf/PrintfCompiler.java b/src/main/java/org/truffleruby/core/format/printf/PrintfCompiler.java deleted file mode 100644 index 826a4d5b2ff4..000000000000 --- a/src/main/java/org/truffleruby/core/format/printf/PrintfCompiler.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2015, 2025 Oracle and/or its affiliates. All rights reserved. This - * code is released under a tri EPL/GPL/LGPL license. You can use it, - * redistribute it and/or modify it under the terms of the: - * - * Eclipse Public License version 2.0, or - * GNU General Public License version 2, or - * GNU Lesser General Public License version 2.1. - */ -package org.truffleruby.core.format.printf; - -import java.util.List; - -import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; -import com.oracle.truffle.api.nodes.Node; -import com.oracle.truffle.api.strings.AbstractTruffleString; -import org.truffleruby.RubyLanguage; -import org.truffleruby.core.encoding.RubyEncoding; -import org.truffleruby.core.format.FormatEncoding; -import org.truffleruby.core.format.FormatRootNode; - -import com.oracle.truffle.api.RootCallTarget; -import org.truffleruby.core.string.StringSupport; - -public final class PrintfCompiler { - - private final RubyLanguage language; - private final Node currentNode; - - public PrintfCompiler(RubyLanguage language, Node currentNode) { - this.language = language; - this.currentNode = currentNode; - } - - @TruffleBoundary - public RootCallTarget compile(AbstractTruffleString tstring, RubyEncoding encoding, Object[] arguments, - boolean isDebug) { - var byteArray = tstring.getInternalByteArrayUncached(encoding.tencoding); - - final PrintfSimpleParser parser = new PrintfSimpleParser(StringSupport.bytesToChars(byteArray), arguments, - isDebug); - final List configs = parser.parse(); - final PrintfSimpleTreeBuilder builder = new PrintfSimpleTreeBuilder(language, configs, encoding); - - return new FormatRootNode( - language, - currentNode.getEncapsulatingSourceSection(), - new FormatEncoding(encoding), - builder.getNode(), - false, - false).getCallTarget(); - } - -} diff --git a/src/main/java/org/truffleruby/core/format/printf/PrintfSimpleTreeBuilder.java b/src/main/java/org/truffleruby/core/format/printf/PrintfSimpleTreeBuilder.java index eba02df5ff36..a819fe3a938f 100644 --- a/src/main/java/org/truffleruby/core/format/printf/PrintfSimpleTreeBuilder.java +++ b/src/main/java/org/truffleruby/core/format/printf/PrintfSimpleTreeBuilder.java @@ -258,7 +258,7 @@ private void buildTree() { break; default: throw new UnsupportedOperationException( - "unsupported type: " + config.getFormatType().toString()); + "unsupported type: " + config.getFormatType()); } } diff --git a/src/main/java/org/truffleruby/core/kernel/KernelNodes.java b/src/main/java/org/truffleruby/core/kernel/KernelNodes.java index 0959996a3e5c..2f9c259f4dd8 100644 --- a/src/main/java/org/truffleruby/core/kernel/KernelNodes.java +++ b/src/main/java/org/truffleruby/core/kernel/KernelNodes.java @@ -59,10 +59,14 @@ import org.truffleruby.core.encoding.RubyEncoding; import org.truffleruby.core.exception.GetBacktraceException; import org.truffleruby.core.format.BytesResult; +import org.truffleruby.core.format.FormatEncoding; import org.truffleruby.core.format.FormatExceptionTranslator; +import org.truffleruby.core.format.FormatRootNode; import org.truffleruby.core.format.exceptions.FormatException; import org.truffleruby.core.format.exceptions.InvalidFormatException; -import org.truffleruby.core.format.printf.PrintfCompiler; +import org.truffleruby.core.format.printf.PrintfSimpleParser; +import org.truffleruby.core.format.printf.PrintfSimpleTreeBuilder; +import org.truffleruby.core.format.printf.SprintfConfig; import org.truffleruby.core.hash.HashOperations; import org.truffleruby.core.hash.HashingNodes; import org.truffleruby.core.hash.RubyHash; @@ -82,6 +86,7 @@ import org.truffleruby.core.string.RubyString; import org.truffleruby.core.string.StringHelperNodes; import org.truffleruby.core.string.StringNodes; +import org.truffleruby.core.string.StringSupport; import org.truffleruby.core.support.TypeNodes; import org.truffleruby.core.support.TypeNodes.CheckFrozenNode; import org.truffleruby.core.support.TypeNodes.ObjectInstanceVariablesNode; @@ -103,6 +108,7 @@ import org.truffleruby.language.RubyNode; import org.truffleruby.language.RubyRootNode; import org.truffleruby.language.WarnNode; +import org.truffleruby.language.WarningNode; import org.truffleruby.language.arguments.RubyArguments; import org.truffleruby.language.backtrace.Backtrace; import org.truffleruby.language.backtrace.BacktraceFormatter; @@ -1658,14 +1664,17 @@ public abstract BytesResult execute(Node node, AbstractTruffleString format, Rub @Specialization( guards = { "equalNode.execute(node, format, encoding, cachedFormat, cachedEncoding)", - "isDebug == cachedIsDebug" }, + "isDebug == cachedIsDebug", + "arguments.length == cachedArgumentsLength" }, limit = "3") static BytesResult formatCached( Node node, AbstractTruffleString format, RubyEncoding encoding, Object[] arguments, boolean isDebug, + @Cached @Shared WarningNode warnNode, @Cached("isDebug") boolean cachedIsDebug, @Cached("format.asTruffleStringUncached(encoding.tencoding)") TruffleString cachedFormat, @Cached("encoding") RubyEncoding cachedEncoding, - @Cached(value = "create(compileFormat(cachedFormat, cachedEncoding, arguments, isDebug, node))", + @Cached("arguments.length") int cachedArgumentsLength, + @Cached(value = "create(compileFormat(cachedFormat, cachedEncoding, arguments, isDebug, warnNode, node))", inline = false) DirectCallNode callPackNode, @Cached StringHelperNodes.EqualSameEncodingNode equalNode) { return (BytesResult) callPackNode.call(new Object[]{ arguments, arguments.length, null }); @@ -1674,17 +1683,51 @@ static BytesResult formatCached( @Specialization(replaces = "formatCached") static BytesResult formatUncached( Node node, AbstractTruffleString format, RubyEncoding encoding, Object[] arguments, boolean isDebug, + @Cached @Shared WarningNode warnNode, @Cached(inline = false) IndirectCallNode callPackNode) { return (BytesResult) callPackNode.call( - compileFormat(format, encoding, arguments, isDebug, node), + compileFormat(format, encoding, arguments, isDebug, warnNode, node), new Object[]{ arguments, arguments.length, null }); } @TruffleBoundary static RootCallTarget compileFormat(AbstractTruffleString tstring, RubyEncoding encoding, Object[] arguments, - boolean isDebug, Node node) { + boolean isDebug, WarningNode warnNode, Node node) { try { - return new PrintfCompiler(getLanguage(node), node).compile(tstring, encoding, arguments, isDebug); + var byteArray = tstring.getInternalByteArrayUncached(encoding.tencoding); + + final PrintfSimpleParser parser = new PrintfSimpleParser(StringSupport.bytesToChars(byteArray), + arguments, + isDebug); + final List configs = parser.parse(); + final PrintfSimpleTreeBuilder builder = new PrintfSimpleTreeBuilder(getLanguage(node), configs, + encoding); + + if (warnNode.shouldWarn()) { + int modifiersCount = 0; + for (var config : configs) { + if (!config.isLiteral()) { + modifiersCount++; + } + } + + // don't check number of values passed as a Hash: + // format("%d : %f", { :foo => 1, :bar => 2 }) + boolean areReferences = arguments.length == 1 && arguments[0] instanceof RubyHash; + if (!areReferences && modifiersCount < arguments.length) { + warnNode.warningMessage( + RubyContext.get(node).getCallStack().getTopMostUserSourceSection(), + "too many arguments for format string"); + } + } + + return new FormatRootNode( + getLanguage(node), + node.getEncapsulatingSourceSection(), + new FormatEncoding(encoding), + builder.getNode(), + false, + false).getCallTarget(); } catch (InvalidFormatException e) { throw new RaiseException(getContext(node), coreExceptions(node).argumentError(e.getMessage(), node)); } From 3502b9eff772d3b3c506b45e23bcacc165f08c06 Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Fri, 8 Nov 2024 20:21:01 +0200 Subject: [PATCH 4/8] Fix Integer#ceil called on 0 --- CHANGELOG.md | 1 + spec/tags/core/integer/ceil_tags.txt | 1 - src/main/ruby/truffleruby/core/integer.rb | 2 ++ 3 files changed, 3 insertions(+), 1 deletion(-) delete mode 100644 spec/tags/core/integer/ceil_tags.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 93fb79d335be..01fcaf9502b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ Compatibility: * Add `rb_data_define()` to define Data (#3681, @andrykonchin). * Add `Refinement#target` (#3681, @andrykonchin). * Emit warning when `Kernel#format` called with excessive arguments (@andrykonchin). +* Fix `Integer#ceil` when self is 0 (@andrykonchin). Performance: diff --git a/spec/tags/core/integer/ceil_tags.txt b/spec/tags/core/integer/ceil_tags.txt deleted file mode 100644 index 8bf644844a28..000000000000 --- a/spec/tags/core/integer/ceil_tags.txt +++ /dev/null @@ -1 +0,0 @@ -fails:Integer#ceil with precision precision is negative always returns 0 when self is 0 diff --git a/src/main/ruby/truffleruby/core/integer.rb b/src/main/ruby/truffleruby/core/integer.rb index 722d9db85360..f0058d21bcbb 100644 --- a/src/main/ruby/truffleruby/core/integer.rb +++ b/src/main/ruby/truffleruby/core/integer.rb @@ -82,6 +82,8 @@ def anybits?(mask) def ceil(precision = 0) return self unless precision < 0 + return 0 if self == 0 + x = 10 ** precision.abs ((self / x) + 1) * x end From 2c27406a65e3aeeaa6a4544f610e90aba4494ac7 Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Fri, 8 Nov 2024 21:01:05 +0200 Subject: [PATCH 5/8] Fix ObjectSpace.undefine_finalizer and raise FrozenError when called for a frozen object --- CHANGELOG.md | 1 + spec/tags/core/objectspace/undefine_finalizer_tags.txt | 1 - .../org/truffleruby/core/objectspace/ObjectSpaceNodes.java | 6 +++++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01fcaf9502b8..0980b3802dfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Bug fixes: * Fix duplicated calls of a `Module#const_added` callback when a module with nested modules is assigned to a constant (@andrykonchin). * Support OpenSSL 1.1-3.4 and prefer in order OpenSSL 3.0.x, 3.x and 1.1 (EOL). There was a compilation issue with OpenSSL 3.4 (#3724, @eregon). * Fix `Time{.at,.new,.now,#getlocal,#localtime}` methods and validation of seconds in utc offset in String format (@andrykonchin). +* Fix `ObjectSpace.undefine_finalizer` and raise `FrozenError` when called for a frozen object (@andrykonchin). Compatibility: diff --git a/spec/tags/core/objectspace/undefine_finalizer_tags.txt b/spec/tags/core/objectspace/undefine_finalizer_tags.txt index 036b8d5bb9d6..7bd5a48d664b 100644 --- a/spec/tags/core/objectspace/undefine_finalizer_tags.txt +++ b/spec/tags/core/objectspace/undefine_finalizer_tags.txt @@ -1,4 +1,3 @@ slow:ObjectSpace.undefine_finalizer removes finalizers for an object slow:ObjectSpace.undefine_finalizer should not remove finalizers for a frozen object fails:ObjectSpace.undefine_finalizer should not remove finalizers for a frozen object -fails:ObjectSpace.undefine_finalizer should raise when removing finalizers for a frozen object diff --git a/src/main/java/org/truffleruby/core/objectspace/ObjectSpaceNodes.java b/src/main/java/org/truffleruby/core/objectspace/ObjectSpaceNodes.java index 09ae8520fea1..72c4901a9c9c 100644 --- a/src/main/java/org/truffleruby/core/objectspace/ObjectSpaceNodes.java +++ b/src/main/java/org/truffleruby/core/objectspace/ObjectSpaceNodes.java @@ -25,6 +25,7 @@ import org.truffleruby.core.numeric.RubyBignum; import org.truffleruby.core.proc.RubyProc; import org.truffleruby.core.string.StringUtils; +import org.truffleruby.core.support.TypeNodes; import org.truffleruby.core.symbol.RubySymbol; import org.truffleruby.language.ImmutableRubyObject; import org.truffleruby.language.NotProvided; @@ -279,9 +280,12 @@ public abstract static class UndefineFinalizerNode extends CoreMethodArrayArgume @TruffleBoundary @Specialization - Object undefineFinalizer(RubyDynamicObject object) { + Object undefineFinalizer(RubyDynamicObject object, + @Cached TypeNodes.CheckFrozenNode raiseIfFrozenNode) { final DynamicObjectLibrary objectLibrary = DynamicObjectLibrary.getUncached(); synchronized (object) { + raiseIfFrozenNode.execute(this, object); + FinalizerReference ref = (FinalizerReference) objectLibrary .getOrDefault(object, Layouts.FINALIZER_REF_IDENTIFIER, null); if (ref != null) { From 40fb2013150d81ee25313faf67d7d9e904b4f2e3 Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Thu, 12 Dec 2024 16:35:59 +0200 Subject: [PATCH 6/8] Emit a warning when a deprecated constant is being removed --- CHANGELOG.md | 1 + .../core/module/deprecate_constant_tags.txt | 1 - .../truffleruby/core/module/ModuleNodes.java | 12 ++++- .../constants/LookupConstantBaseNode.java | 28 ++-------- .../constants/WarnDeprecatedConstantNode.java | 54 +++++++++++++++++++ 5 files changed, 71 insertions(+), 25 deletions(-) delete mode 100644 spec/tags/core/module/deprecate_constant_tags.txt create mode 100644 src/main/java/org/truffleruby/language/constants/WarnDeprecatedConstantNode.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 0980b3802dfd..6ca66d88b406 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ Compatibility: * Add `Refinement#target` (#3681, @andrykonchin). * Emit warning when `Kernel#format` called with excessive arguments (@andrykonchin). * Fix `Integer#ceil` when self is 0 (@andrykonchin). +* Fix `Module#remove_const` and emit warning when constant is deprecated (@andrykonchin). Performance: diff --git a/spec/tags/core/module/deprecate_constant_tags.txt b/spec/tags/core/module/deprecate_constant_tags.txt deleted file mode 100644 index d165b14f15ef..000000000000 --- a/spec/tags/core/module/deprecate_constant_tags.txt +++ /dev/null @@ -1 +0,0 @@ -fails:Module#deprecate_constant when removing the deprecated module warns with a message diff --git a/src/main/java/org/truffleruby/core/module/ModuleNodes.java b/src/main/java/org/truffleruby/core/module/ModuleNodes.java index 65b0d1f52614..af3927d07e20 100644 --- a/src/main/java/org/truffleruby/core/module/ModuleNodes.java +++ b/src/main/java/org/truffleruby/core/module/ModuleNodes.java @@ -71,6 +71,7 @@ import org.truffleruby.core.support.TypeNodes; import org.truffleruby.core.symbol.RubySymbol; import org.truffleruby.interop.ToJavaStringNode; +import org.truffleruby.language.LazyWarnNode; import org.truffleruby.language.LexicalScope; import org.truffleruby.language.Nil; import org.truffleruby.language.RubyBaseNode; @@ -91,6 +92,7 @@ import org.truffleruby.language.constants.GetConstantNode; import org.truffleruby.language.constants.LookupConstantInterface; import org.truffleruby.language.constants.LookupConstantNode; +import org.truffleruby.language.constants.WarnDeprecatedConstantNode; import org.truffleruby.language.control.RaiseException; import org.truffleruby.language.control.ReturnID; import org.truffleruby.language.dispatch.DispatchNode; @@ -2009,7 +2011,8 @@ public abstract static class RemoveConstNode extends PrimitiveArrayArgumentsNode @Specialization Object removeConstant(RubyModule module, Object nameObject, - @Cached NameToJavaStringNode nameToJavaStringNode) { + @Cached NameToJavaStringNode nameToJavaStringNode, + @Cached LazyWarnNode lazyWarnNode) { final var name = nameToJavaStringNode.execute(this, nameObject); final RubyConstant oldConstant = module.fields.removeConstant(getContext(), this, name); if (oldConstant == null) { @@ -2020,10 +2023,17 @@ Object removeConstant(RubyModule module, Object nameObject, if (oldConstant.isAutoload() || oldConstant.isUndefined()) { return nil; } else { + if (oldConstant.isDeprecated()) { + warnDeprecatedConstant(module, name, lazyWarnNode); + } return oldConstant.getValue(); } } } + + private void warnDeprecatedConstant(RubyModule module, String name, LazyWarnNode lazyWarnNode) { + WarnDeprecatedConstantNode.warnDeprecatedConstant(this, lazyWarnNode.get(this), module, name); + } } @CoreMethod(names = "remove_method", rest = true) diff --git a/src/main/java/org/truffleruby/language/constants/LookupConstantBaseNode.java b/src/main/java/org/truffleruby/language/constants/LookupConstantBaseNode.java index eb2917a48802..7a327031d1c3 100644 --- a/src/main/java/org/truffleruby/language/constants/LookupConstantBaseNode.java +++ b/src/main/java/org/truffleruby/language/constants/LookupConstantBaseNode.java @@ -10,45 +10,27 @@ package org.truffleruby.language.constants; import com.oracle.truffle.api.nodes.Node; -import org.truffleruby.core.module.ModuleOperations; import org.truffleruby.core.module.RubyModule; import org.truffleruby.language.RubyBaseNode; import org.truffleruby.language.WarnNode; import com.oracle.truffle.api.CompilerDirectives; -import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; -import com.oracle.truffle.api.source.SourceSection; public abstract class LookupConstantBaseNode extends RubyBaseNode { - @Child private WarnNode warnNode; + @Child private WarnDeprecatedConstantNode warnDeprecatedConstantNode; protected void warnDeprecatedConstant(RubyModule module, String name) { - if (warnNode == null) { + if (warnDeprecatedConstantNode == null) { CompilerDirectives.transferToInterpreterAndInvalidate(); - warnNode = insert(new WarnNode()); + warnDeprecatedConstantNode = insert(new WarnDeprecatedConstantNode()); } - if (warnNode.shouldWarnForDeprecation()) { - warnNode.warningMessage(getSection(this), formatMessage(this, module, name)); - } + warnDeprecatedConstantNode.warnDeprecatedConstant(module, name); } protected static void warnDeprecatedConstant(Node node, WarnNode warnNode, RubyModule module, String name) { - if (warnNode.shouldWarnForDeprecation()) { - warnNode.warningMessage(getSection(node), formatMessage(node, module, name)); - } - } - - @TruffleBoundary - private static SourceSection getSection(Node node) { - return getContext(node).getCallStack() - .getTopMostUserSourceSection(getAdoptedNode(node).getEncapsulatingSourceSection()); - } - - @TruffleBoundary - private static String formatMessage(Node node, RubyModule module, String name) { - return "constant " + ModuleOperations.constantName(getContext(node), module, name) + " is deprecated"; + WarnDeprecatedConstantNode.warnDeprecatedConstant(node, warnNode, module, name); } protected int getCacheLimit() { diff --git a/src/main/java/org/truffleruby/language/constants/WarnDeprecatedConstantNode.java b/src/main/java/org/truffleruby/language/constants/WarnDeprecatedConstantNode.java new file mode 100644 index 000000000000..37badcfcda94 --- /dev/null +++ b/src/main/java/org/truffleruby/language/constants/WarnDeprecatedConstantNode.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. All rights reserved. This + * code is released under a tri EPL/GPL/LGPL license. You can use it, + * redistribute it and/or modify it under the terms of the: + * + * Eclipse Public License version 2.0, or + * GNU General Public License version 2, or + * GNU Lesser General Public License version 2.1. + */ +package org.truffleruby.language.constants; + +import com.oracle.truffle.api.nodes.Node; +import org.truffleruby.core.module.ModuleOperations; +import org.truffleruby.core.module.RubyModule; +import org.truffleruby.language.RubyBaseNode; +import org.truffleruby.language.WarnNode; +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.source.SourceSection; + +public final class WarnDeprecatedConstantNode extends RubyBaseNode { + + @Child private WarnNode warnNode; + + public void warnDeprecatedConstant(RubyModule module, String name) { + if (warnNode == null) { + CompilerDirectives.transferToInterpreterAndInvalidate(); + warnNode = insert(new WarnNode()); + } + + if (warnNode.shouldWarnForDeprecation()) { + warnNode.warningMessage(getSection(this), formatMessage(this, module, name)); + } + } + + public static void warnDeprecatedConstant(Node node, WarnNode warnNode, RubyModule module, String name) { + if (warnNode.shouldWarnForDeprecation()) { + warnNode.warningMessage(getSection(node), formatMessage(node, module, name)); + } + } + + @TruffleBoundary + private static SourceSection getSection(Node node) { + return getContext(node).getCallStack() + .getTopMostUserSourceSection(getAdoptedNode(node).getEncapsulatingSourceSection()); + } + + @TruffleBoundary + private static String formatMessage(Node node, RubyModule module, String name) { + String fullyQualifiedName = ModuleOperations.constantName(getContext(node), module, name); + return "constant " + fullyQualifiedName + " is deprecated"; + } + +} From a884e2e865453790ed9641cf628ea01ef229e3d6 Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Tue, 14 Jan 2025 17:12:05 +0200 Subject: [PATCH 7/8] Fix Integer#/ with bignum argument --- CHANGELOG.md | 1 + spec/ruby/core/integer/divide_spec.rb | 20 +++++++++++++++++++ spec/tags/core/float/floor_tags.txt | 1 - spec/tags/core/integer/floor_tags.txt | 1 - .../core/numeric/IntegerNodes.java | 15 +++----------- 5 files changed, 24 insertions(+), 14 deletions(-) delete mode 100644 spec/tags/core/float/floor_tags.txt delete mode 100644 spec/tags/core/integer/floor_tags.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ca66d88b406..292941a6510c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Bug fixes: * Support OpenSSL 1.1-3.4 and prefer in order OpenSSL 3.0.x, 3.x and 1.1 (EOL). There was a compilation issue with OpenSSL 3.4 (#3724, @eregon). * Fix `Time{.at,.new,.now,#getlocal,#localtime}` methods and validation of seconds in utc offset in String format (@andrykonchin). * Fix `ObjectSpace.undefine_finalizer` and raise `FrozenError` when called for a frozen object (@andrykonchin). +* Fix `Integer#/` when called with a bignum argument (@andrykonchin). Compatibility: diff --git a/spec/ruby/core/integer/divide_spec.rb b/spec/ruby/core/integer/divide_spec.rb index a878c4668c71..665f4d57becf 100644 --- a/spec/ruby/core/integer/divide_spec.rb +++ b/spec/ruby/core/integer/divide_spec.rb @@ -12,6 +12,17 @@ it "supports dividing negative numbers" do (-1 / 10).should == -1 + (-1 / 10**10).should == -1 + (-1 / 10**20).should == -1 + end + + it "preservers sign correctly" do + (4 / 3).should == 1 + (4 / -3).should == -2 + (-4 / 3).should == -2 + (-4 / -3).should == 1 + (0 / -3).should == 0 + (0 / 3).should == 0 end it "returns result the same class as the argument" do @@ -58,6 +69,15 @@ ((10**50) / -(10**40 + 1)).should == -10000000000 end + it "preservers sign correctly" do + (4 / bignum_value).should == 0 + (4 / -bignum_value).should == -1 + (-4 / bignum_value).should == -1 + (-4 / -bignum_value).should == 0 + (0 / bignum_value).should == 0 + (0 / -bignum_value).should == 0 + end + it "returns self divided by Float" do not_supported_on :opal do (bignum_value(88) / 4294967295.0).should be_close(4294967297.0, TOLERANCE) diff --git a/spec/tags/core/float/floor_tags.txt b/spec/tags/core/float/floor_tags.txt deleted file mode 100644 index 78ab60812d18..000000000000 --- a/spec/tags/core/float/floor_tags.txt +++ /dev/null @@ -1 +0,0 @@ -fails:Float#floor with precision precision is negative returns -(10**precision.abs) when self is negative and precision.abs is larger than the number digits of self diff --git a/spec/tags/core/integer/floor_tags.txt b/spec/tags/core/integer/floor_tags.txt deleted file mode 100644 index 01ea50f6d7e6..000000000000 --- a/spec/tags/core/integer/floor_tags.txt +++ /dev/null @@ -1 +0,0 @@ -fails:Integer#floor with precision precision is negative returns -(10**precision.abs) when self is negative and precision.abs is larger than the number digits of self diff --git a/src/main/java/org/truffleruby/core/numeric/IntegerNodes.java b/src/main/java/org/truffleruby/core/numeric/IntegerNodes.java index 1f76f8d258c6..7018e40857c8 100644 --- a/src/main/java/org/truffleruby/core/numeric/IntegerNodes.java +++ b/src/main/java/org/truffleruby/core/numeric/IntegerNodes.java @@ -427,14 +427,9 @@ Object divLongFallback(long a, long b, return a / b; } - @Specialization(guards = { "!isLongMinValue(a)" }) - int divBignum(long a, RubyBignum b) { - return 0; - } - - @Specialization(guards = { "isLongMinValue(a)" }) - int divBignumEdgeCase(long a, RubyBignum b) { - return -b.value.signum(); + @Specialization + Object divBignum(long a, RubyBignum b) { + return (a == 0 || Long.signum(a) == b.value.signum()) ? 0 : -1; } // Bignum @@ -481,10 +476,6 @@ Object divCoerced(Object a, Object b, return redoCoerced.call(a, "redo_coerced", coreSymbols().DIVIDE, b); } - protected static boolean isLongMinValue(long a) { - return a == Long.MIN_VALUE; - } - } // Defined in Java as we need to statically call #/ From 13a65424fedc52f9740c4b18d3a8c7a9cdaf2ef1 Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Fri, 17 Jan 2025 12:20:32 +0200 Subject: [PATCH 8/8] Exclude MRI test TestGemCommandsOwnerCommand#test_with_webauthn_enabled_failure and mark it as transient --- test/mri/excludes/TestGemCommandsOwnerCommand.rb | 1 + 1 file changed, 1 insertion(+) create mode 100644 test/mri/excludes/TestGemCommandsOwnerCommand.rb diff --git a/test/mri/excludes/TestGemCommandsOwnerCommand.rb b/test/mri/excludes/TestGemCommandsOwnerCommand.rb new file mode 100644 index 000000000000..bb8204d06c46 --- /dev/null +++ b/test/mri/excludes/TestGemCommandsOwnerCommand.rb @@ -0,0 +1 @@ +exclude :test_with_webauthn_enabled_failure, "transient: Expected nil to match \"ed244fbf2b1a52e012da8616c512fa47f9aa5250\"."