The following example should not trigger method_missing for either the to_str respond_to check in String#== nor by dispatching to == on obj. The equality check should return false.
class TestRespondToViaMethodMissing < Test::Unit::TestCase
class ABasicObject #:nodoc:
instance_methods.each do |m|
undef_method(m) if m.to_s !~ /(?:^__|^nil\?$|^send$|^object_id$)/
end
attr_accessor :respond_to_called
def method_missing(name, *args)
Kernel.java.lang.Thread.dumpStack
if name == :respond_to? && args[0] == :to_str
@respond_to_called = true
true
elsif name == :==
true
else
super
end
end
end
def test_respond_to_check_can_trigger_method_missing
obj = ABasicObject.new
assert_nothing_raised do
assert "string" == obj
end
end
end
This test was added in cb625fb but behavior may have changed since then. The logic in CRuby is below:
VALUE
rb_str_equal(VALUE str1, VALUE str2)
{
if (str1 == str2) return Qtrue;
if (!RB_TYPE_P(str2, T_STRING)) {
if (!rb_respond_to(str2, idTo_str)) {
return Qfalse;
}
return rb_equal(str2, str1);
}
return rb_str_eql_internal(str1, str2);
}
int
rb_respond_to(VALUE obj, ID id)
{
return rb_obj_respond_to(obj, id, FALSE);
}
int
rb_obj_respond_to(VALUE obj, ID id, int priv)
{
rb_execution_context_t *ec = GET_EC();
return rb_ec_obj_respond_to(ec, obj, id, priv);
}
int
rb_ec_obj_respond_to(rb_execution_context_t *ec, VALUE obj, ID id, int priv)
{
VALUE klass = CLASS_OF(obj);
int ret = vm_respond_to(ec, klass, obj, id, priv);
if (ret == -1) ret = basic_obj_respond_to(ec, obj, id, !priv);
return ret;
}
If the comparison object does not respond to to_str, this logic immediately returns false. The logic of vm_respond_to appears to be roughly equivalent to our RubyBasicObject.respondsTo, which only makes a call to respond_to? if it is present and has been replaced by a custom version.
Failing that, it falls back on BasicObject respond_to? logic:
static inline int
basic_obj_respond_to(rb_execution_context_t *ec, VALUE obj, ID id, int pub)
{
VALUE klass = CLASS_OF(obj);
VALUE ret;
switch (method_boundp(klass, id, pub|BOUND_RESPONDS)) {
case 2:
return FALSE;
case 0:
ret = basic_obj_respond_to_missing(ec, klass, obj, ID2SYM(id),
RBOOL(!pub));
return RTEST(ret) && !UNDEF_P(ret);
default:
return TRUE;
}
}
static VALUE
basic_obj_respond_to_missing(rb_execution_context_t *ec, VALUE klass, VALUE obj,
VALUE mid, VALUE priv)
{
VALUE defined_class, args[2];
const ID rtmid = idRespond_to_missing;
const rb_callable_method_entry_t *const cme = callable_method_entry(klass, rtmid, &defined_class);
if (!cme || METHOD_ENTRY_BASIC(cme)) return Qundef;
args[0] = mid;
args[1] = priv;
return call_method_entry(ec, defined_class, obj, rtmid, cme, 2, args, RB_NO_KEYWORDS);
}
This logic first checks if the method is "naturally" defined. If it is not, it checks if respond_to_missing? is "naturally" defined and only dispatches if it is.
All other cases are false and none of them should go through method_missing.
The current logic that we have uses RespondToCallSite, which will trigger method_missing for respond_to?, and then it also does not fail if to_str is not defined and triggers method_missing again for the flipped == call.
The String#== case just happened to come up while working on #8862 but this use of RspondToCallSite is all over the place and probably doing the wrong thing in every one of them.
The following example should not trigger
method_missingfor either theto_strrespond_to check inString#==nor by dispatching to==onobj. The equality check should returnfalse.This test was added in cb625fb but behavior may have changed since then. The logic in CRuby is below:
If the comparison object does not respond to
to_str, this logic immediately returns false. The logic ofvm_respond_toappears to be roughly equivalent to ourRubyBasicObject.respondsTo, which only makes a call torespond_to?if it is present and has been replaced by a custom version.Failing that, it falls back on
BasicObjectrespond_to?logic:This logic first checks if the method is "naturally" defined. If it is not, it checks if
respond_to_missing?is "naturally" defined and only dispatches if it is.All other cases are false and none of them should go through
method_missing.The current logic that we have uses
RespondToCallSite, which will triggermethod_missingforrespond_to?, and then it also does not fail ifto_stris not defined and triggersmethod_missingagain for the flipped==call.The
String#==case just happened to come up while working on #8862 but this use ofRspondToCallSiteis all over the place and probably doing the wrong thing in every one of them.