Skip to content

Commit 81980d6

Browse files
committed
[GR-18163] Fix Kernel#raise and don't override cause
PullRequest: truffleruby/4518
2 parents 2a22e2f + 9390b95 commit 81980d6

File tree

5 files changed

+206
-35
lines changed

5 files changed

+206
-35
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Bug fixes:
99

1010
Compatibility:
1111

12+
* Support Timezone argument to `Time.{new,at}` and `Time#{getlocal,localtime}` (#1717, @patricklinpl, @manefz, @rwstauner).
1213
* Updated to Ruby 3.3.7 (@andrykonchin).
1314
* Implement `StringScanner#{peek_byte,scan_byte,scan_integer,named_captures}` methods (#3788, @andrykonchin).
1415
* Support String patterns in `StringScanner#{exist?,scan_until,skip_until,check_until,search_full}` methods (@andrykonchin).
@@ -18,6 +19,7 @@ Compatibility:
1819
* Support Digest plugins (#1390, @nirvdrum).
1920
* Joni has been updated from 2.2.1 to 2.2.6 (@andrykonchin).
2021
* Fix numeric coercing when `#coerce` method is not public (#3848, @andrykonchin).
22+
* Fix `Kernel#raise` and don't override `cause` at exception re-raising (#3831, @andrykonchin).
2123

2224
Performance:
2325

@@ -91,7 +93,6 @@ Compatibility:
9193
* Fix `Module#remove_const` and emit warning when constant is deprecated (@andrykonchin).
9294
* Add `Module#set_temporary_name` (#3681, @andrykonchin).
9395
* Modify `Float#round` to match MRI behavior (#3676, @andrykonchin).
94-
* Support Timezone argument to `Time.{new,at}` and `Time#{getlocal,localtime}` (#1717, @patricklinpl, @manefz, @rwstauner).
9596

9697
Performance:
9798

spec/ruby/core/kernel/raise_spec.rb

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,53 @@
4444

4545
it "raises an ArgumentError when only cause is given" do
4646
cause = StandardError.new
47-
-> { raise(cause: cause) }.should raise_error(ArgumentError)
47+
-> { raise(cause: cause) }.should raise_error(ArgumentError, "only cause is given with no arguments")
48+
end
49+
50+
it "raises an ArgumentError when only cause is given even if it has nil value" do
51+
-> { raise(cause: nil) }.should raise_error(ArgumentError, "only cause is given with no arguments")
52+
end
53+
54+
it "raises an ArgumentError when given cause is not an instance of Exception" do
55+
-> { raise "message", cause: Object.new }.should raise_error(TypeError, "exception object expected")
56+
end
57+
58+
it "doesn't raise an ArgumentError when given cause is nil" do
59+
-> { raise "message", cause: nil }.should raise_error(RuntimeError, "message")
60+
end
61+
62+
it "allows cause equal an exception" do
63+
e = RuntimeError.new("message")
64+
-> { raise e, cause: e }.should raise_error(e)
65+
end
66+
67+
it "doesn't set given cause when it equals an exception" do
68+
e = RuntimeError.new("message")
69+
70+
begin
71+
raise e, cause: e
72+
rescue
73+
end
74+
75+
e.cause.should == nil
76+
end
77+
78+
it "raises ArgumentError when exception is part of the cause chain" do
79+
-> {
80+
begin
81+
raise "Error 1"
82+
rescue => e1
83+
begin
84+
raise "Error 2"
85+
rescue => e2
86+
begin
87+
raise "Error 3"
88+
rescue => e3
89+
raise e1, cause: e3
90+
end
91+
end
92+
end
93+
}.should raise_error(ArgumentError, "circular causes")
4894
end
4995

5096
it "re-raises a rescued exception" do
@@ -62,6 +108,101 @@
62108
end
63109
end.should raise_error(StandardError, "aaa")
64110
end
111+
112+
it "re-raises a previously rescued exception without overwriting the cause" do
113+
begin
114+
begin
115+
begin
116+
begin
117+
raise "Error 1"
118+
rescue => e1
119+
raise "Error 2"
120+
end
121+
rescue => e2
122+
raise "Error 3"
123+
end
124+
rescue
125+
e2.cause.should == e1
126+
raise e2
127+
end
128+
rescue => e
129+
e.cause.should == e1
130+
end
131+
end
132+
133+
it "re-raises a previously rescued exception with overwriting the cause when it's explicitly specified with :cause option" do
134+
e4 = RuntimeError.new("Error 4")
135+
136+
begin
137+
begin
138+
begin
139+
begin
140+
raise "Error 1"
141+
rescue => e1
142+
raise "Error 2"
143+
end
144+
rescue => e2
145+
raise "Error 3"
146+
end
147+
rescue
148+
e2.cause.should == e1
149+
raise e2, cause: e4
150+
end
151+
rescue => e
152+
e.cause.should == e4
153+
end
154+
end
155+
156+
it "re-raises a previously rescued exception without overwriting the cause when it's explicitly specified with :cause option and has nil value" do
157+
begin
158+
begin
159+
begin
160+
begin
161+
raise "Error 1"
162+
rescue => e1
163+
raise "Error 2"
164+
end
165+
rescue => e2
166+
raise "Error 3"
167+
end
168+
rescue
169+
e2.cause.should == e1
170+
raise e2, cause: nil
171+
end
172+
rescue => e
173+
e.cause.should == e1
174+
end
175+
end
176+
177+
it "re-raises a previously rescued exception without setting a cause implicitly" do
178+
begin
179+
begin
180+
raise "Error 1"
181+
rescue => e1
182+
raise
183+
end
184+
rescue => e
185+
e.should == e1
186+
e.cause.should == nil
187+
end
188+
end
189+
190+
it "re-raises a previously rescued exception that has a cause without setting a cause implicitly" do
191+
begin
192+
begin
193+
raise "Error 1"
194+
rescue => e1
195+
begin
196+
raise "Error 2"
197+
rescue => e2
198+
raise
199+
end
200+
end
201+
rescue => e
202+
e.should == e2
203+
e.cause.should == e1
204+
end
205+
end
65206
end
66207

67208
describe "Kernel#raise" do

spec/ruby/shared/kernel/raise.rb

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -49,21 +49,6 @@ def initialize(data)
4949
end
5050
end
5151

52-
it "does not allow message and extra keyword arguments" do
53-
data_error = Class.new(StandardError) do
54-
attr_reader :data
55-
def initialize(data)
56-
@data = data
57-
end
58-
end
59-
60-
-> { @object.raise(data_error, {a: 1}, b: 2) }.should raise_error(StandardError) do |e|
61-
[TypeError, ArgumentError].should.include?(e.class)
62-
end
63-
64-
-> { @object.raise(data_error, {a: 1}, [], b: 2) }.should raise_error(ArgumentError)
65-
end
66-
6752
it "raises RuntimeError if no exception class is given" do
6853
-> { @object.raise }.should raise_error(RuntimeError, "")
6954
end
@@ -74,7 +59,7 @@ def initialize(data)
7459
end
7560

7661
it "raises a RuntimeError if string given" do
77-
-> { @object.raise("a bad thing") }.should raise_error(RuntimeError)
62+
-> { @object.raise("a bad thing") }.should raise_error(RuntimeError, "a bad thing")
7863
end
7964

8065
it "passes no arguments to the constructor when given only an exception class" do
@@ -86,19 +71,32 @@ def initialize
8671
end
8772

8873
it "raises a TypeError when passed a non-Exception object" do
89-
-> { @object.raise(Object.new) }.should raise_error(TypeError)
74+
-> { @object.raise(Object.new) }.should raise_error(TypeError, "exception class/object expected")
75+
-> { @object.raise(Object.new, "message") }.should raise_error(TypeError, "exception class/object expected")
76+
-> { @object.raise(Object.new, "message", []) }.should raise_error(TypeError, "exception class/object expected")
9077
end
9178

9279
it "raises a TypeError when passed true" do
93-
-> { @object.raise(true) }.should raise_error(TypeError)
80+
-> { @object.raise(true) }.should raise_error(TypeError, "exception class/object expected")
9481
end
9582

9683
it "raises a TypeError when passed false" do
97-
-> { @object.raise(false) }.should raise_error(TypeError)
84+
-> { @object.raise(false) }.should raise_error(TypeError, "exception class/object expected")
9885
end
9986

10087
it "raises a TypeError when passed nil" do
101-
-> { @object.raise(nil) }.should raise_error(TypeError)
88+
-> { @object.raise(nil) }.should raise_error(TypeError, "exception class/object expected")
89+
end
90+
91+
it "raises TypeError when passed a non-Exception object but it responds to #exception method that doesn't return an instance of Exception class" do
92+
e = Object.new
93+
def e.exception
94+
Array
95+
end
96+
97+
-> {
98+
@object.raise e
99+
}.should raise_error(TypeError, "exception object expected")
102100
end
103101

104102
it "re-raises a previously rescued exception without overwriting the backtrace" do

src/main/ruby/truffleruby/core/kernel.rb

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -686,29 +686,41 @@ def warn(*messages, uplevel: undefined, category: nil)
686686

687687
def raise(exc = undefined, msg = undefined, ctx = nil, cause: undefined, **kwargs)
688688
cause_given = !Primitive.undefined?(cause)
689-
cause = cause_given ? cause : $!
690689

691-
if Primitive.undefined?(exc) and cause
692-
raise ArgumentError, 'only cause is given with no arguments' if cause_given
693-
exc = cause
690+
if Primitive.undefined?(exc) && cause_given
691+
raise ArgumentError, 'only cause is given with no arguments'
692+
end
693+
694+
if Primitive.undefined?(exc) && $!
695+
exc = $!
694696
else
695-
unless kwargs.empty?
696-
if Primitive.undefined?(msg)
697-
msg = kwargs
698-
else
699-
raise ArgumentError, 'cannot give both message and extra keyword arguments'
697+
if Primitive.undefined?(msg) && !kwargs.empty?
698+
msg = kwargs
699+
end
700+
701+
if cause_given
702+
unless Primitive.is_a?(cause, ::Exception) || Primitive.nil?(cause)
703+
Truffle::ExceptionOperations.exception_object_expected!
700704
end
705+
else
706+
cause = $!
701707
end
702708

703709
exc = Truffle::ExceptionOperations.build_exception_for_raise(exc, msg)
704710

705711
exc.set_backtrace(ctx) if ctx
706712
Primitive.exception_capture_backtrace(exc, 1) unless Truffle::ExceptionOperations.backtrace?(exc)
707-
Primitive.exception_set_cause exc, cause unless Primitive.equal?(exc, cause)
713+
714+
if !Primitive.nil?(cause) && (cause_given || Primitive.nil?(exc.cause)) && !Primitive.equal?(cause, exc)
715+
if Truffle::ExceptionOperations.circular_cause?(cause, exc)
716+
raise ArgumentError, 'circular causes'
717+
end
718+
719+
Primitive.exception_set_cause exc, cause
720+
end
708721
end
709722

710723
Truffle::ExceptionOperations.show_exception_for_debug(exc, 1) if $DEBUG
711-
712724
Primitive.vm_raise_exception exc
713725
end
714726
module_function :raise

src/main/ruby/truffleruby/core/truffle/exception_operations.rb

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def self.build_exception_for_raise(exc, msg)
2020
exc = exc.exception msg
2121
end
2222

23-
exception_class_object_expected! unless Primitive.is_a?(exc, ::Exception)
23+
exception_object_expected! unless Primitive.is_a?(exc, ::Exception)
2424
exc
2525
elsif Primitive.is_a?(exc, ::String)
2626
::RuntimeError.exception exc
@@ -29,6 +29,14 @@ def self.build_exception_for_raise(exc, msg)
2929
end
3030
end
3131

32+
def self.circular_cause?(cause, exception)
33+
while cause && !Primitive.equal?(cause, exception)
34+
cause = cause.cause
35+
end
36+
37+
!Primitive.nil?(cause)
38+
end
39+
3240
def self.make_exception(args)
3341
case args.size
3442
when 0
@@ -50,7 +58,8 @@ def self.make_exception(args)
5058

5159
def self.call_exception(exc, *args)
5260
res = Truffle::Type.check_funcall(exc, :exception, args)
53-
raise TypeError, 'exception class/object expected' if Primitive.undefined?(res) || !Primitive.is_a?(res, Exception)
61+
raise TypeError, 'exception class/object expected' if Primitive.undefined?(res)
62+
raise TypeError, 'exception object expected' unless Primitive.is_a?(res, Exception)
5463
res
5564
end
5665

@@ -64,6 +73,16 @@ def self.exception_class_object_expected!
6473
Primitive.vm_raise_exception exc
6574
end
6675

76+
# Avoid using #raise here to prevent infinite recursion
77+
def self.exception_object_expected!
78+
exc = ::TypeError.new('exception object expected')
79+
Primitive.exception_capture_backtrace(exc, 1)
80+
81+
show_exception_for_debug(exc, 2) if $DEBUG
82+
83+
Primitive.vm_raise_exception exc
84+
end
85+
6786
def self.show_exception_for_debug(exc, uplevel)
6887
STDERR.puts "Exception: `#{Primitive.class(exc)}' at #{caller(uplevel + 1, 1)[0]} - #{exc.message}\n"
6988
end

0 commit comments

Comments
 (0)