Skip to content

Commit 9f072f2

Browse files
nirvdrumparacycle
andcommitted
Fix the exception type raised when type coercion via to_str raises a NoMethodError.
Co-authored-by: Ufuk Kayserilioglu <ufuk.kayserilioglu@shopify.com>
1 parent fbbc027 commit 9f072f2

File tree

12 files changed

+100
-83
lines changed

12 files changed

+100
-83
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Bug fixes:
4545
* Fix processing of proc rest arguments located at the beginning if there are no actual arguments (#2921, @andrykonchin).
4646
* Fix `Monitor#exit` to raise `ThreadError` when monitor not owned by the current thread (#2922, @andrykonchin).
4747
* Fix `MatchData#[]` to support negative `length` argument (#2929, @andrykonchin).
48+
* Fix the exception type raised when type coercion raises a `NoMethodError` (#2903, @paracycle, @nirvdrum).
4849

4950
Compatibility:
5051

spec/ruby/core/kernel/method_spec.rb

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
m.call.should == :defined
3030
end
3131

32-
it "can be called even if we only repond_to_missing? method, true" do
32+
it "can be called even if we only respond_to_missing? method, true" do
3333
m = KernelSpecs::RespondViaMissing.new.method(:handled_privately)
3434
m.should be_an_instance_of(Method)
3535
m.call(1, 2, 3).should == "Done handled_privately([1, 2, 3])"
@@ -58,4 +58,23 @@ def method_missing(m)
5858
m = cls.new.method(:bar)
5959
m.call.should == :bar
6060
end
61+
62+
describe "converts the given name to a String using #to_str" do
63+
it "calls #to_str to convert the given name to a String" do
64+
name = mock("method-name")
65+
name.should_receive(:to_str).and_return("hash")
66+
Object.method(name).should == Object.method(:hash)
67+
end
68+
69+
it "raises a TypeError if the given name can't be converted to a String" do
70+
-> { Object.method(nil) }.should raise_error(TypeError)
71+
-> { Object.method([]) }.should raise_error(TypeError)
72+
end
73+
74+
it "raises a NoMethodError if the given argument raises a NoMethodError during type coercion to a String" do
75+
name = mock("method-name")
76+
name.should_receive(:to_str).and_raise(NoMethodError)
77+
-> { Object.method(name) }.should raise_error(NoMethodError)
78+
end
79+
end
6180
end

spec/ruby/core/module/const_defined_spec.rb

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,23 @@
8080
ConstantSpecs::ClassA.const_defined?(:CS_CONSTX).should == false
8181
end
8282

83-
it "calls #to_str to convert the given name to a String" do
84-
name = mock("ClassA")
85-
name.should_receive(:to_str).and_return("ClassA")
86-
ConstantSpecs.const_defined?(name).should == true
83+
describe "converts the given name to a String using #to_str" do
84+
it "calls #to_str to convert the given name to a String" do
85+
name = mock("ClassA")
86+
name.should_receive(:to_str).and_return("ClassA")
87+
ConstantSpecs.const_defined?(name).should == true
88+
end
89+
90+
it "raises a TypeError if the given name can't be converted to a String" do
91+
-> { ConstantSpecs.const_defined?(nil) }.should raise_error(TypeError)
92+
-> { ConstantSpecs.const_defined?([]) }.should raise_error(TypeError)
93+
end
94+
95+
it "raises a NoMethodError if the given argument raises a NoMethodError during type coercion to a String" do
96+
name = mock("classA")
97+
name.should_receive(:to_str).and_raise(NoMethodError)
98+
-> { ConstantSpecs.const_defined?(name) }.should raise_error(NoMethodError)
99+
end
87100
end
88101

89102
it "special cases Object and checks it's included Modules" do

spec/ruby/core/string/append_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
describe "String#<<" do
66
it_behaves_like :string_concat, :<<
77
it_behaves_like :string_concat_encoding, :<<
8+
it_behaves_like :string_concat_type_coercion, :<<
89

910
it "raises an ArgumentError when given the incorrect number of arguments" do
1011
-> { "hello".send(:<<) }.should raise_error(ArgumentError)

spec/ruby/core/string/concat_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
describe "String#concat" do
66
it_behaves_like :string_concat, :concat
77
it_behaves_like :string_concat_encoding, :concat
8+
it_behaves_like :string_concat_type_coercion, :concat
89

910
it "takes multiple arguments" do
1011
str = "hello "

spec/ruby/core/string/plus_spec.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
require_relative 'shared/concat'
44

55
describe "String#+" do
6+
it_behaves_like :string_concat_encoding, :+
7+
it_behaves_like :string_concat_type_coercion, :+
8+
69
it "returns a new string containing the given string concatenated to self" do
710
("" + "").should == ""
811
("" + "Hello").should == "Hello"
@@ -31,6 +34,4 @@
3134
("hello" + StringSpecs::MyString.new("foo")).should be_an_instance_of(String)
3235
("hello" + StringSpecs::MyString.new("")).should be_an_instance_of(String)
3336
end
34-
35-
it_behaves_like :string_concat_encoding, :+
3637
end

spec/ruby/core/string/shared/concat.rb

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,6 @@
55
str.should == "hello world"
66
end
77

8-
it "converts the given argument to a String using to_str" do
9-
obj = mock('world!')
10-
obj.should_receive(:to_str).and_return("world!")
11-
a = 'hello '.send(@method, obj)
12-
a.should == 'hello world!'
13-
end
14-
15-
it "raises a TypeError if the given argument can't be converted to a String" do
16-
-> { 'hello '.send(@method, []) }.should raise_error(TypeError)
17-
-> { 'hello '.send(@method, mock('x')) }.should raise_error(TypeError)
18-
end
19-
208
it "raises a FrozenError when self is frozen" do
219
a = "hello"
2210
a.freeze
@@ -148,3 +136,23 @@
148136
end
149137
end
150138
end
139+
140+
describe :string_concat_type_coercion, shared: true do
141+
it "converts the given argument to a String using to_str" do
142+
obj = mock('world!')
143+
obj.should_receive(:to_str).and_return("world!")
144+
a = 'hello '.send(@method, obj)
145+
a.should == 'hello world!'
146+
end
147+
148+
it "raises a TypeError if the given argument can't be converted to a String" do
149+
-> { 'hello '.send(@method, []) }.should raise_error(TypeError)
150+
-> { 'hello '.send(@method, mock('x')) }.should raise_error(TypeError)
151+
end
152+
153+
it "raises a NoMethodError if the given argument raises a NoMethodError during type coercion to a String" do
154+
obj = mock('world!')
155+
obj.should_receive(:to_str).and_raise(NoMethodError)
156+
-> { 'hello '.send(@method, obj) }.should raise_error(NoMethodError)
157+
end
158+
end
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fails:Module#const_defined? converts the given name to a String using #to_str raises a NoMethodError if the given argument raises a NoMethodError during type coercion to a String

spec/truffle/always_inlined_spec.rb

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,6 @@
4444
}.should raise_error(ArgumentError) { |e| e.backtrace_locations[0].label.should == 'binding' }
4545
end
4646

47-
it "for #eval" do
48-
-> {
49-
eval(Object.new)
50-
}.should raise_error(TypeError) { |e| e.backtrace_locations[0].label.should == 'eval' }
51-
end
52-
5347
it "for #local_variables" do
5448
-> {
5549
local_variables(:wrong)
@@ -235,6 +229,26 @@
235229
}
236230
end
237231

232+
it "for #eval" do
233+
-> {
234+
eval(Object.new)
235+
}.should raise_error(TypeError) { |e|
236+
e.backtrace_locations[0].label.should == 'convert_type'
237+
}
238+
end
239+
240+
it "for Module#class_eval with Object" do
241+
-> {
242+
Module.new.class_eval(Object.new)
243+
}.should raise_error(TypeError) { |e| e.backtrace_locations[0].label.should == 'convert_type' }
244+
end
245+
246+
it "for Module#module_eval with Object" do
247+
-> {
248+
Module.new.module_eval(Object.new)
249+
}.should raise_error(TypeError) { |e| e.backtrace_locations[0].label.should == 'convert_type' }
250+
end
251+
238252
guard -> { RUBY_ENGINE != "ruby" } do
239253
it "for #respond_to?" do
240254
obj = Object.new

src/main/java/org/truffleruby/core/cast/ToStrNode.java

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,11 @@
1515
import org.truffleruby.core.string.RubyString;
1616
import org.truffleruby.core.string.ImmutableRubyString;
1717
import org.truffleruby.language.RubyBaseNodeWithExecute;
18-
import org.truffleruby.language.control.RaiseException;
1918
import org.truffleruby.language.dispatch.DispatchNode;
2019

2120
import com.oracle.truffle.api.dsl.Cached;
2221
import com.oracle.truffle.api.dsl.NodeChild;
2322
import com.oracle.truffle.api.dsl.Specialization;
24-
import com.oracle.truffle.api.profiles.BranchProfile;
25-
import org.truffleruby.language.library.RubyStringLibrary;
2623

2724
@GenerateUncached
2825
@NodeChild(value = "childNode", type = RubyBaseNodeWithExecute.class)
@@ -53,31 +50,13 @@ protected ImmutableRubyString coerceImmutableRubyString(ImmutableRubyString stri
5350

5451
@Specialization(guards = "isNotRubyString(object)")
5552
protected Object coerceObject(Object object,
56-
@Cached BranchProfile errorProfile,
57-
@Cached DispatchNode toStrNode,
58-
@Cached RubyStringLibrary libString) {
59-
final Object coerced;
60-
try {
61-
coerced = toStrNode.call(object, "to_str");
62-
} catch (RaiseException e) {
63-
errorProfile.enter();
64-
if (e.getException().getLogicalClass() == coreLibrary().noMethodErrorClass) {
65-
throw new RaiseException(
66-
getContext(),
67-
coreExceptions().typeErrorNoImplicitConversion(object, "String", this));
68-
} else {
69-
throw e;
70-
}
71-
}
72-
73-
if (libString.isRubyString(coerced)) {
74-
return coerced;
75-
} else {
76-
errorProfile.enter();
77-
throw new RaiseException(
78-
getContext(),
79-
coreExceptions().typeErrorBadCoercion(object, "String", "to_str", coerced, this));
80-
}
53+
@Cached DispatchNode toStrNode) {
54+
return toStrNode.call(
55+
coreLibrary().truffleTypeModule,
56+
"rb_convert_type",
57+
object,
58+
coreLibrary().stringClass,
59+
coreSymbols().TO_STR);
8160
}
8261

8362
@Override

0 commit comments

Comments
 (0)