Is the current Ruby method called via super?

后端 未结 5 1550
北海茫月
北海茫月 2021-01-05 19:15

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         


        
相关标签:
5条回答
  • 2021-01-05 19:48

    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.

    0 讨论(0)
  • 2021-01-05 19:51

    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"
    
    0 讨论(0)
  • 2021-01-05 19:52

    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.

    0 讨论(0)
  • 2021-01-05 19:53

    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
    
    0 讨论(0)
  • 2021-01-05 20:02

    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.

    0 讨论(0)
提交回复
热议问题