Skip to content

Core respond_to? checks should not trigger method_missing #9048

Description

@headius

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions