5

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

ndnenkov
  • 35,425
  • 9
  • 72
  • 104
Mori
  • 27,279
  • 10
  • 68
  • 73

5 Answers5

4

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.
ndnenkov
  • 35,425
  • 9
  • 72
  • 104
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.

Aleksei Matiushkin
  • 119,336
  • 10
  • 100
  • 160
3

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"
Stefan
  • 109,145
  • 14
  • 143
  • 218
2

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
sawa
  • 165,429
  • 45
  • 277
  • 381
  • Wouldn't the two `method(mX).owner` calls evaluate in the current context and always be equal? (assuming `m0 == m1`) – ndnenkov Jan 12 '16 at 07:11
  • 1
    Both `m0` and `m1` are strings and since they are equal, `call_whatever_func_on_it(m0)` would be identically equal to `call_whatever_func_on_it(m1)`. Am I missing smth? – Aleksei Matiushkin Jan 12 '16 at 07:20
  • I was afraid what happens when there is a recursive call. But I haven't thought much. Maybe I can simplify it to `m0 == m1`. – sawa Jan 12 '16 at 07:26
  • @sawa, but even then, this would assume that you are calling `super` even if you are not, as long as the names are equal. (aka if `Fu` doesn't inherit from `Foo` and `Fu#bar` method's body is `Foo.new.bar`). – ndnenkov Jan 12 '16 at 07:28
  • 2
    Just comparison of `mN` won’t work because of both reason described above by @ndn and recursion. – Aleksei Matiushkin Jan 12 '16 at 07:30
  • 1
    You should use `base_label` instead of `label`. This will allow the check to work even if `super` is invoked from within a block, e.g. `def bar; -> { super }.call; end` – Stefan Jan 12 '16 at 08:19
  • Why do you have to check the method owner? – Stefan Jan 12 '16 at 08:39
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.

Community
  • 1
  • 1
ndnenkov
  • 35,425
  • 9
  • 72
  • 104