Is the current Ruby method called via super?
Asked Answered
T

5

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

Townswoman answered 12/1, 2016 at 6:23 Comment(0)
E
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.

Excrement answered 12/1, 2016 at 11:16 Comment(0)
E
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.
Excrement answered 12/1, 2016 at 6:33 Comment(4)
Unfortunately this doesn't work if both, parent and child include SuperDetector.Thundering
@Thundering unless instance_methods.include?Bluebill
@mudasobwa that would prevent the child from using SuperDetectorThundering
@Thundering sounds like I figured it out: check for ancestors + define method if and only it was not defined on ancestors does likely do a trick.Bluebill
T
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"
Thundering answered 12/1, 2016 at 10:43 Comment(0)
B
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.

Bluebill answered 12/1, 2016 at 6:42 Comment(7)
Pretty much - yes. Just use Kernel#caller_locations and call location on them, rather than parsing with regex.Excrement
@ndn you meant call label, right? I do not see any advantage of it, but updated :)Bluebill
The unless fails if you subclass Fu and expect SuperDetector for Fu to behave properly (aka will always return false even for methods defined in Fu).Excrement
@ndn there should be comparison against ancestors, I believe, not self against included.Bluebill
Wouldn't self.ancestors.include?(clazz) always be true?Excrement
Still don't see the point. It's equivalent to self.class != clazz.Excrement
In other words if Foo < Bar < Baz, Baz and Bar include the module, Baz has a method baz, but Bar doesn't, then calling super from Foo#baz will still return false.Excrement
I
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
Illeetvilaine answered 12/1, 2016 at 7:3 Comment(7)
Wouldn't the two method(mX).owner calls evaluate in the current context and always be equal? (assuming m0 == m1)Excrement
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?Bluebill
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.Illeetvilaine
@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).Excrement
Just comparison of mN won’t work because of both reason described above by @ndn and recursion.Bluebill
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; endThundering
Why do you have to check the method owner?Thundering
E
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.

Excrement answered 12/1, 2016 at 11:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.