Is the current Ruby method called via super?

2019-02-14 08:04发布

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)?

5条回答
姐就是有狂的资本
2楼-- · 2019-02-14 08:24

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.

查看更多
不美不萌又怎样
3楼-- · 2019-02-14 08:32

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"
查看更多
在下西门庆
4楼-- · 2019-02-14 08:33

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.

查看更多
霸刀☆藐视天下
5楼-- · 2019-02-14 08:38

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.

查看更多
Melony?
6楼-- · 2019-02-14 08:38

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
查看更多
登录 后发表回答