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_s
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.
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"
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!"
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.
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
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.