require "reflexive/constantize" module Reflexive class MethodLookup def initialize(options) unless (@klass, @level, @name = options.values_at(:klass, :level, :name)).all? raise ArgumentError, "must pass :klass, :level, :name as named arguments" end @level, @name = @level.to_sym, @name.to_sym end def definitions lookup unless @lookup_done @definitions end def documentations lookup unless @lookup_done @documentations end def last_resort_lookup_used? @last_resort_lookup_used end private def lookup begin defined_method_lookup rescue NameError => e # don't swallow NameError if it's not related to the method we're looking for raise unless e.message.include?(@name.to_s) heuristic_lookup last_resort_lookup unless lookup_succeed? end @lookup_done = true end def lookup_succeed? @definitions && @definitions.size > 0 || @documentations && @documentations.size > 0 end def defined_method_lookup unbound_method = @klass.send(method_getter, @name) if unbound_method.source_location @definitions = [[@klass, @level, @name]] elsif @klass.instance_of?(Class) && @level == :class && @name == :new && (@klass.instance_method(:initialize).source_location rescue false) @definitions = [[@klass, :instance, :initialize]] elsif core_klass = unbound_method.owner if core_klass == Kernel @documentations = [[Kernel, :instance, @name]] elsif core_klass == Module || core_klass.to_s == "#" if (Module.methods(false) + Module.private_methods(false)).include?(@name) @documentations = [[Module, :class, @name]] else @documentations = [[Module, :instance, @name]] end elsif core_klass == Class if (Class.methods(false) + Class.private_methods(false)).include?(@name) @documentations = [[Class, :class, @name]] else @documentations = [[Class, :instance, @name]] end else if @level == :class if core_klass == @klass @documentations = [[core_klass, :class, @name]] elsif core_klass.to_s =~ /^#$/ # get class from singleton class @documentations = [[Reflexive.constantize($1), :class, @name]] end else @documentations = [[core_klass, @level, @name]] end end end end def heuristic_lookup if @klass.instance_of?(Module) && @level == :instance # only instance methods are inherited from modules potential_receivers = included_by_classes.select do |included_by_class| all_instance_methods(included_by_class).include?(@name) end potential_receivers += included_by_modules.select do |included_by_module| all_instance_methods(included_by_module).include?(@name) end potential_receivers.uniq.each do |receiver| # TODO heuristic lookup shouldn't assume that found method is not core method (@definitions ||= []) << [receiver, :instance, @name] end potential_class_receivers = included_by_singleton_classes.select do |included_by_singleton_class| all_class_methods(included_by_singleton_class).include?(@name) end potential_class_receivers.uniq.each do |class_receiver| (@definitions ||= []) << [class_receiver, :class, @name] end elsif @klass.instance_of?(Class) # both instance and class methods are inherited by classes potential_receivers = inherited_by_classes.select do |inherited_by_class| all_level_methods(inherited_by_class).include?(@name) end potential_receivers.uniq.each do |receiver| (@definitions ||= []) << [receiver, @level, @name] end end end def last_resort_lookup @last_resort_lookup_used = true seen = [] [Module, Class].each do |module_or_class| ObjectSpace.each_object(module_or_class) do |m| next if seen.include?(m) if all_instance_methods(m).include?(@name) (@definitions ||= []) << [m, :instance, @name] end if all_class_methods(m).include?(@name) (@definitions ||= []) << [m, :class, @name] end seen << m end end end def all_instance_methods(klass) klass.instance_methods(false) + klass.private_instance_methods(false) end def all_class_methods(klass) klass.methods(false) + klass.private_methods(false) end def all_level_methods(klass) @level == :instance ? all_instance_methods(klass) : all_class_methods(klass) end def included_by_classes included_by = [] ObjectSpace.each_object(Class) do |c| next if c == @klass ancestors = c.ancestors included_by << c if ancestors.include?(@klass) end included_by end alias inherited_by_classes included_by_classes def included_by_modules included_by = [] ObjectSpace.each_object(Module) do |m| next if m == @klass ancestors = m.ancestors included_by << m if ancestors.include?(@klass) end included_by end def included_by_singleton_classes included_by = [] ObjectSpace.each_object(Class) do |c| next if c == @klass ancestors = c.singleton_class.ancestors included_by << c if ancestors.include?(@klass) end included_by end def method_getter @level == :instance ? :instance_method : :method end def methods_getter @level == :instance ? :instance_methods : :methods end def reverse_methods_getter @level == :instance ? :methods : :instance_methods end def reverse_level @level == :instance ? :class : :instance end end end