Permalink
Browse files

Refactor Pry::Method + add Pry::Method::WeirdMethodLocator

WeirdMethodLocator is used by Pry::Method.from_binding() to locate the
method captured by the binding when the naive approach fails (i.e method(binding.eval('__method__')).

"WeirdMethods" include methods defined on the superclass to the 'self' of the binding, as well as methods
that have subsequently been renamed/replaced.

We also move Pry::Method::Disowned to its own file (disowned.rb)
  • Loading branch information...
1 parent 0229255 commit 97f1be86b29e41e0a786cf0177d934c4c22e8572 @banister banister committed Feb 2, 2013
Showing with 298 additions and 87 deletions.
  1. +35 −80 lib/pry/method.rb
  2. +53 −0 lib/pry/method/disowned.rb
  3. +186 −0 lib/pry/method/weird_method_locator.rb
  4. +24 −7 spec/method_spec.rb
View
@@ -17,6 +17,9 @@ def Method(obj)
# This class wraps the normal `Method` and `UnboundMethod` classes
# to provide extra functionality useful to Pry.
class Method
+ require 'pry/method/weird_method_locator'
+ require 'pry/method/disowned'
+
extend Helpers::BaseHelpers
include Helpers::BaseHelpers
include RbxMethod if rbx?
@@ -82,37 +85,11 @@ def from_binding(b)
Disowned.new(b.eval('self'), meth_name.to_s)
end
- # it's possible in some cases that the method we find by this approach is a sub-method of
- # the one we're currently in, consider:
- #
- # class A; def b; binding.pry; end; end
- # class B < A; def b; super; end; end
- #
- # Given that we can normally find the source_range of methods, and that we know which
- # __FILE__ and __LINE__ the binding is at, we can hope to disambiguate these cases.
- #
- # This obviously won't work if the source is unavaiable for some reason, or if both
- # methods have the same __FILE__ and __LINE__, or if we're in rbx where b.eval('__LINE__')
- # is broken.
- #
- guess = method
-
- while guess
- # needs rescue if this is a Disowned method or a C method or something...
- # TODO: Fix up the exception handling so we don't need a bare rescue
- if (guess.source_file && guess.source_range rescue false) &&
- File.expand_path(guess.source_file) == File.expand_path(b.eval('__FILE__')) &&
- guess.source_range.include?(b.eval('__LINE__'))
- return guess
- else
- guess = guess.super
- end
+ if WeirdMethodLocator.weird_method?(method, b)
+ WeirdMethodLocator.new(method, b).get_method || method
+ else
+ method
end
-
- # Uhoh... none of the methods in the chain had the right __FILE__ and __LINE__
- # This may be caused by rbx https://github.com/rubinius/rubinius/issues/953,
- # or other unknown circumstances (TODO: we should warn the user when this happens)
- method
end
end
@@ -196,6 +173,19 @@ def instance_resolution_order(klass)
([klass] + klass.ancestors).uniq
end
+ def method_definition?(name, definition_line)
+ singleton_method_definition?(name, definition_line) ||
+ instance_method_definition?(name, definition_line)
+ end
+
+ def singleton_method_definition?(name, definition_line)
+ /^define_singleton_method\(?\s*[:\"\']#{name}|^def\s*self\.#{name}/ =~ definition_line.strip
+ end
+
+ def instance_method_definition?(name, definition_line)
+ /^define_method\(?\s*[:\"\']#{name}|^def\s*#{name}/ =~ definition_line.strip
+ end
+
private
# See all_from_class and all_from_obj.
@@ -423,6 +413,21 @@ def dynamically_defined?
!!(source_file and source_file =~ /(\(.*\))|<.*>/)
end
+ # @return [Boolean] Whether the method is unbound.
+ def unbound_method?
+ is_a?(::UnboundMethod)
+ end
+
+ # @return [Boolean] Whether the method is bound.
+ def bound_method?
+ is_a?(::Method)
+ end
+
+ # @return [Boolean] Whether the method is a singleton method.
+ def singleton_method?
+ wrapped_owner.singleton_class?
+ end
+
# @return [Boolean] Was the method defined within the Pry REPL?
def pry_method?
source_file == Pry.eval_path
@@ -542,55 +547,5 @@ def method_name_from_first_line(first_ln)
nil
end
-
- # A Disowned Method is one that's been removed from the class on which it was defined.
- #
- # e.g.
- # class C
- # def foo
- # C.send(:undefine_method, :foo)
- # Pry::Method.from_binding(binding)
- # end
- # end
- #
- # In this case we assume that the "owner" is the singleton class of the receiver.
- #
- # This occurs mainly in Sinatra applications.
- class Disowned < Method
- attr_reader :receiver, :name
-
- # Create a new Disowned method.
- #
- # @param [Object] receiver
- # @param [String] method_name
- def initialize(receiver, method_name)
- @receiver, @name = receiver, method_name
- end
-
- # Is the method undefined? (aka `Disowned`)
- # @return [Boolean] true
- def undefined?
- true
- end
-
- # Can we get the source for this method?
- # @return [Boolean] false
- def source?
- false
- end
-
- # Get the hypothesized owner of the method.
- #
- # @return [Object]
- def owner
- class << receiver; self; end
- end
-
- # Raise a more useful error message instead of trying to forward to nil.
- def method_missing(meth_name, *args, &block)
- raise "Cannot call '#{meth_name}' on an undef'd method." if method(:name).respond_to?(meth_name)
- Object.instance_method(:method_missing).bind(self).call(meth_name, *args, &block)
- end
- end
end
end
@@ -0,0 +1,53 @@
+class Pry
+ class Method
+ # A Disowned Method is one that's been removed from the class on which it was defined.
+ #
+ # e.g.
+ # class C
+ # def foo
+ # C.send(:undefine_method, :foo)
+ # Pry::Method.from_binding(binding)
+ # end
+ # end
+ #
+ # In this case we assume that the "owner" is the singleton class of the receiver.
+ #
+ # This occurs mainly in Sinatra applications.
+ class Disowned < Method
+ attr_reader :receiver, :name
+
+ # Create a new Disowned method.
+ #
+ # @param [Object] receiver
+ # @param [String] method_name
+ def initialize(receiver, method_name, binding=nil)
+ @receiver, @name = receiver, method_name
+ end
+
+ # Is the method undefined? (aka `Disowned`)
+ # @return [Boolean] true
+ def undefined?
+ true
+ end
+
+ # Can we get the source for this method?
+ # @return [Boolean] false
+ def source?
+ false
+ end
+
+ # Get the hypothesized owner of the method.
+ #
+ # @return [Object]
+ def owner
+ class << receiver; self; end
+ end
+
+ # Raise a more useful error message instead of trying to forward to nil.
+ def method_missing(meth_name, *args, &block)
+ raise "Cannot call '#{meth_name}' on an undef'd method." if method(:name).respond_to?(meth_name)
+ Object.instance_method(:method_missing).bind(self).call(meth_name, *args, &block)
+ end
+ end
+ end
+end
@@ -0,0 +1,186 @@
+class Pry
+ class Method
+
+ # This class is responsible for locating the *real* `Pry::Method`
+ # object captured by a binding.
+ #
+ # Given a `Binding` from inside a method and a 'seed' Pry::Method object,
+ # there are primarily two situations where the seed method doesn't match
+ # the Binding:
+ # 1. The Pry::Method is from a subclass 2. The Pry::Method represents a method of the same name
+ # while the original was renamed to something else. For 1. we
+ # search vertically up the inheritance chain,
+ # and for 2. we search laterally along the object's method table.
+ #
+ # When we locate the method that matches the Binding we wrap it in
+ # Pry::Method and return it, or return nil if we fail.
+ class WeirdMethodLocator
+ class << self
+
+ # Whether the given method object matches the associated binding.
+ # If the method object does not match the binding, then it's
+ # most likely not the method captured by the binding, and we
+ # must commence a search.
+ #
+ # @param [Pry::Method] method
+ # @param [Binding] b
+ # @return [Boolean]
+ def normal_method?(method, b)
+ method && (method.source_file && method.source_range rescue false) &&
+ File.expand_path(method.source_file) == File.expand_path(b.eval('__FILE__')) &&
+ method.source_range.include?(b.eval('__LINE__'))
+ end
+
+ def weird_method?(method, b)
+ !normal_method?(method, b)
+ end
+ end
+
+ attr_accessor :method
+ attr_accessor :target
+
+ # @param [Pry::Method] method The seed method.
+ # @param [Binding] target The Binding that captures the method
+ # we want to locate.
+ def initialize(method, target)
+ @method, @target = method, target
+ end
+
+ # @return [Pry::Method, nil] The Pry::Method that matches the
+ # given binding.
+ def get_method
+ find_method_in_superclass || find_renamed_method
+ end
+
+ # @return [Boolean] Whether the Pry::Method is unrecoverable
+ # This usually happens when the method captured by the Binding
+ # has been subsequently deleted.
+ def lost_method?
+ !!(get_method.nil? && renamed_method_source_location)
+ end
+
+ private
+
+ def normal_method?(method)
+ self.class.normal_method?(method, target)
+ end
+
+ def target_self
+ target.eval('self')
+ end
+
+ def target_file
+ pry_file? ? target.eval('__FILE__') : File.expand_path(target.eval('__FILE__'))
+ end
+
+ def target_line
+ target.eval('__LINE__')
+ end
+
+ def pry_file?
+ Pry.eval_path == target.eval('__FILE__')
+ end
+
+ # it's possible in some cases that the method we find by this approach is a sub-method of
+ # the one we're currently in, consider:
+ #
+ # class A; def b; binding.pry; end; end
+ # class B < A; def b; super; end; end
+ #
+ # Given that we can normally find the source_range of methods, and that we know which
+ # __FILE__ and __LINE__ the binding is at, we can hope to disambiguate these cases.
+ #
+ # This obviously won't work if the source is unavaiable for some reason, or if both
+ # methods have the same __FILE__ and __LINE__, or if we're in rbx where b.eval('__LINE__')
+ # is broken.
+ #
+ # @return [Pry::Method, nil] The Pry::Method representing the
+ # superclass method.
+ def find_method_in_superclass
+ guess = method
+
+ while guess
+ # needs rescue if this is a Disowned method or a C method or something...
+ # TODO: Fix up the exception handling so we don't need a bare rescue
+ if normal_method?(guess)
+ return guess
+ else
+ guess = guess.super
+ end
+ end
+
+ # Uhoh... none of the methods in the chain had the right __FILE__ and __LINE__
+ # This may be caused by rbx https://github.com/rubinius/rubinius/issues/953,
+ # or other unknown circumstances (TODO: we should warn the user when this happens)
+ nil
+ end
+
+ # This is the case where the name of a method has changed
+ # (via alias_method) so we locate the Method object for the
+ # renamed method.
+ #
+ # @return [Pry::Method, nil] The Pry::Method representing the
+ # renamed method
+ def find_renamed_method
+ return if !valid_file?(target_file)
+ alias_name = all_methods_for(target_self).find do |v|
+ expanded_source_location(target_self.method(v).source_location) == renamed_method_source_location
+ end
+
+ alias_name && Pry::Method(target_self.method(alias_name))
+ end
+
+ def expanded_source_location(sl)
+ return if !sl
+
+ if pry_file?
+ sl
+ else
+ [File.expand_path(sl.first), sl.last]
+ end
+ end
+
+ # Use static analysis to locate the start of the method definition.
+ # We have the `__FILE__` and `__LINE__` from the binding and the
+ # original name of the method so we search up until we find a
+ # def/define_method, etc defining a method of the appropriate name.
+ #
+ # @return [Array<String, Fixnum>] The `source_location` of the
+ # renamed method
+ def renamed_method_source_location
+ return @original_method_source_location if defined?(@original_method_source_location)
+
+ source_index = lines_for_file(target_file)[0..(target_line - 1)].rindex do |v|
+ Pry::Method.method_definition?(method.name, v)
+ end
+
+ @original_method_source_location = source_index &&
+ [target_file, index_to_line_number(source_index)]
+ end
+
+ def index_to_line_number(index)
+ # Pry.line_buffer is 0-indexed
+ pry_file? ? index : index + 1
+ end
+
+ def valid_file?(file)
+ File.exists?(file) || Pry.eval_path == file
+ end
+
+ def lines_for_file(file)
+ @lines_for_file ||= {}
+ @lines_for_file[file] ||= if Pry.eval_path == file
+ Pry.line_buffer
+ else
+ File.readlines(file)
+ end
+ end
+
+ def all_methods_for(obj)
+ obj.public_methods(false) +
+ obj.private_methods(false) +
+ obj.protected_methods(false)
+ end
+ end
+ end
+end
Oops, something went wrong.

0 comments on commit 97f1be8

Please sign in to comment.