Is the current Ruby method called via super?

孤者浪人 提交于 2019-11-30 05:39:48

问题


Within a method at runtime, is there a way to know if that method has been called via super in a subclass? E.g.

module SuperDetector
  def via_super?
    # what goes here?
  end
end

class Foo
  include SuperDetector

  def bar
    via_super? ? 'super!' : 'nothing special'
  end
end

class Fu < Foo
  def bar
    super
  end
end

Foo.new.bar # => "nothing special"
Fu.new.bar  # => "super!"

How could I write via_super?, or, if necessary, via_super?(:bar)?


回答1:


The ultimate mix between my other, @mudasobwa's and @sawa's answers plus recursion support:

module SuperDetector
  def self.included(clazz)
    unless clazz.instance_methods.include?(:via_super?)
      clazz.send(:define_method, :via_super?) do
        first_caller_location = caller_locations.first
        calling_method = first_caller_location.base_label

        same_origin = ->(other_location) do
          first_caller_location.lineno == other_location.lineno and
            first_caller_location.absolute_path == other_location.absolute_path
        end

        location_changed = false
        same_name_stack = caller_locations.take_while do |location|
          should_take = location.base_label == calling_method and !location_changed
          location_changed = !same_origin.call(location)
          should_take
        end

        self.kind_of?(clazz) and !same_origin.call(same_name_stack.last)
      end
    end
  end
end

The only case that wont work (AFAIK) is if you have indirect recursion in the base class, but I don't have ideas how to handle it with anything short of parsing the code.




回答2:


There is probably a better way, but the general idea is that Object#instance_of? is restricted only to the current class, rather than the hierarchy:

module SuperDetector
  def self.included(clazz)
    clazz.send(:define_method, :via_super?) do
      !self.instance_of?(clazz)
    end
  end
end

class Foo
  include SuperDetector

  def bar
    via_super? ? 'super!' : 'nothing special'
  end
end

class Fu < Foo
  def bar
    super
  end
end

Foo.new.bar # => "nothing special"
Fu.new.bar  # => "super!"


However, note that this doesn't require explicit super in the child. If the child has no such method and the parent's one is used, via_super? will still return true. I don't think there is a way to catch only the super case other than inspecting the stack trace or the code itself.


回答3:


An addendum to an excellent @ndn approach:

module SuperDetector
  def self.included(clazz)
    clazz.send(:define_method, :via_super?) do
      self.ancestors[1..-1].include?(clazz) &&
        caller.take(2).map { |m| m[/(?<=`).*?(?=')/] }.reduce(&:==)
        # or, as by @ndn: caller_locations.take(2).map(&:label).reduce(&:==)
    end unless clazz.instance_methods.include? :via_super?
  end
end

class Foo
  include SuperDetector

  def bar
    via_super? ? 'super!' : 'nothing special'
  end
end

class Fu < Foo
  def bar
    super
  end
end

puts Foo.new.bar # => "nothing special"
puts Fu.new.bar # => "super!"

Here we use Kernel#caller to make sure that the name of the method called matches the name in super class. This approach likely requires some additional tuning in case of not direct descendant (caller(2) should be changed to more sophisticated analysis,) but you probably get the point.

UPD thanks to @Stefan’s comment to the other answer, updated with unless defined to make it to work when both Foo and Fu include SuperDetector.

UPD2 using ancestors to check for super instead of straight comparison.




回答4:


Here's a simpler (almost trivial) approach, but you have to pass both, current class and method name: (I've also changed the method name from via_super? to called_via?)

module CallDetector
  def called_via?(klass, sym)
    klass == method(sym).owner
  end
end

Example usage:

class A
  include CallDetector

  def foo
    called_via?(A, :foo) ? 'nothing special' : 'super!'
  end
end

class B < A
  def foo
    super
  end
end

class C < A
end

A.new.foo # => "nothing special"
B.new.foo # => "super!"
C.new.foo # => "nothing special"



回答5:


Edit Improved, following Stefan's suggestion.

module SuperDetector
  def via_super?
    m0, m1 = caller_locations[0].base_label, caller_locations[1]&.base_label
    m0 == m1 and
    (method(m0).owner rescue nil) == (method(m1).owner rescue nil)
  end
end


来源:https://stackoverflow.com/questions/34736850/is-the-current-ruby-method-called-via-super

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!