This commit is contained in:
@@ -0,0 +1,599 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'method_source'
|
||||
|
||||
class Pry
|
||||
class << self
|
||||
# If the given object is a `Pry::Method`, return it unaltered. If it's
|
||||
# anything else, return it wrapped in a `Pry::Method` instance.
|
||||
def Method(obj)
|
||||
if obj.is_a? Pry::Method
|
||||
obj
|
||||
else
|
||||
Pry::Method.new(obj)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This class wraps the normal `Method` and `UnboundMethod` classes
|
||||
# to provide extra functionality useful to Pry.
|
||||
class Method # rubocop:disable Metrics/ClassLength
|
||||
extend Helpers::BaseHelpers
|
||||
extend Forwardable
|
||||
|
||||
include Helpers::BaseHelpers
|
||||
include Helpers::DocumentationHelpers
|
||||
include CodeObject::Helpers
|
||||
|
||||
class << self
|
||||
# Given a string representing a method name and optionally a binding to
|
||||
# search in, find and return the requested method wrapped in a
|
||||
# `Pry::Method` instance.
|
||||
#
|
||||
# @param [String] name The name of the method to retrieve.
|
||||
# @param [Binding] target The context in which to search for the method.
|
||||
# @param [Hash] options
|
||||
# @option options [Boolean] :instance Look for an instance method if
|
||||
# `name` doesn't contain any context.
|
||||
# @option options [Boolean] :methods Look for a bound/singleton method if
|
||||
# `name` doesn't contain any context.
|
||||
# @return [Pry::Method, nil] A `Pry::Method` instance containing the
|
||||
# requested method, or `nil` if name is `nil` or no method could be
|
||||
# located matching the parameters.
|
||||
def from_str(name, target = TOPLEVEL_BINDING, options = {})
|
||||
if name.nil?
|
||||
nil
|
||||
elsif name.to_s =~ /(.+)\#(\S+)\Z/
|
||||
context = Regexp.last_match(1)
|
||||
meth_name = Regexp.last_match(2)
|
||||
from_module(target.eval(context), meth_name, target)
|
||||
elsif name.to_s =~ /(.+)(\[\])\Z/
|
||||
context = Regexp.last_match(1)
|
||||
meth_name = Regexp.last_match(2)
|
||||
from_obj(target.eval(context), meth_name, target)
|
||||
elsif name.to_s =~ /(.+)(\.|::)(\S+)\Z/
|
||||
context = Regexp.last_match(1)
|
||||
meth_name = Regexp.last_match(3)
|
||||
from_obj(target.eval(context), meth_name, target)
|
||||
elsif options[:instance]
|
||||
from_module(target.eval("self"), name, target)
|
||||
elsif options[:methods]
|
||||
from_obj(target.eval("self"), name, target)
|
||||
else
|
||||
from_str(name, target, instance: true) ||
|
||||
from_str(name, target, methods: true)
|
||||
end
|
||||
rescue Pry::RescuableException
|
||||
nil
|
||||
end
|
||||
|
||||
# Given a `Binding`, try to extract the `::Method` it originated from and
|
||||
# use it to instantiate a `Pry::Method`. Return `nil` if this isn't
|
||||
# possible.
|
||||
#
|
||||
# @param [Binding] binding
|
||||
# @return [Pry::Method, nil]
|
||||
#
|
||||
def from_binding(binding)
|
||||
meth_name = binding.eval('::Kernel.__method__')
|
||||
if [:__script__, nil].include?(meth_name)
|
||||
nil
|
||||
else
|
||||
method =
|
||||
begin
|
||||
if Object === binding.eval('self') # rubocop:disable Style/CaseEquality
|
||||
new(
|
||||
Kernel.instance_method(:method)
|
||||
.bind(binding.eval("self"))
|
||||
.call(meth_name)
|
||||
)
|
||||
else
|
||||
str = 'class << self; self; end' \
|
||||
'.instance_method(::Kernel.__method__).bind(self)'
|
||||
new(binding.eval(str))
|
||||
end
|
||||
rescue NameError, NoMethodError # rubocop:disable Lint/ShadowedException
|
||||
Disowned.new(binding.eval('self'), meth_name.to_s)
|
||||
end
|
||||
|
||||
if WeirdMethodLocator.weird_method?(method, binding)
|
||||
WeirdMethodLocator.new(method, binding).find_method || method
|
||||
else
|
||||
method
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# In order to support 2.0 Refinements we need to look up methods
|
||||
# inside the relevant Binding.
|
||||
# @param [Object] obj The owner/receiver of the method.
|
||||
# @param [Symbol] method_name The name of the method.
|
||||
# @param [Symbol] method_type The type of method: :method or :instance_method
|
||||
# @param [Binding] target The binding where the method is looked up.
|
||||
# @return [Method, UnboundMethod] The 'refined' method object.
|
||||
def lookup_method_via_binding(
|
||||
obj, method_name, method_type, target = TOPLEVEL_BINDING
|
||||
)
|
||||
Pry.current[:obj] = obj
|
||||
Pry.current[:name] = method_name
|
||||
receiver = obj.is_a?(Module) ? "Module" : "Kernel"
|
||||
target.eval(
|
||||
"::#{receiver}.instance_method(:#{method_type})" \
|
||||
".bind(Pry.current[:obj]).call(Pry.current[:name])"
|
||||
)
|
||||
ensure
|
||||
Pry.current[:obj] = Pry.current[:name] = nil
|
||||
end
|
||||
|
||||
# Given a `Class` or `Module` and the name of a method, try to
|
||||
# instantiate a `Pry::Method` containing the instance method of
|
||||
# that name. Return `nil` if no such method exists.
|
||||
#
|
||||
# @param [Class, Module] klass
|
||||
# @param [String] name
|
||||
# @param [Binding] target The binding where the method is looked up.
|
||||
# @return [Pry::Method, nil]
|
||||
def from_class(klass, name, target = TOPLEVEL_BINDING)
|
||||
new(lookup_method_via_binding(klass, name, :instance_method, target))
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
alias from_module from_class
|
||||
|
||||
# Given an object and the name of a method, try to instantiate
|
||||
# a `Pry::Method` containing the method of that name bound to
|
||||
# that object. Return `nil` if no such method exists.
|
||||
#
|
||||
# @param [Object] obj
|
||||
# @param [String] name
|
||||
# @param [Binding] target The binding where the method is looked up.
|
||||
# @return [Pry::Method, nil]
|
||||
def from_obj(obj, name, target = TOPLEVEL_BINDING)
|
||||
new(lookup_method_via_binding(obj, name, :method, target))
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
|
||||
# Get all of the instance methods of a `Class` or `Module`
|
||||
# @param [Class,Module] klass
|
||||
# @param [Boolean] include_super Whether to include methods from ancestors.
|
||||
# @return [Array[Pry::Method]]
|
||||
def all_from_class(klass, include_super = true)
|
||||
%w[public protected private].flat_map do |visibility|
|
||||
safe_send(
|
||||
klass, :"#{visibility}_instance_methods", include_super
|
||||
).map do |method_name|
|
||||
new(
|
||||
safe_send(klass, :instance_method, method_name),
|
||||
visibility: visibility.to_sym
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Get all of the methods on an `Object`
|
||||
#
|
||||
# @param [Object] obj
|
||||
#
|
||||
# @param [Boolean] include_super
|
||||
# indicates whether or not to include methods from ancestors.
|
||||
#
|
||||
# @return [Array[Pry::Method]]
|
||||
#
|
||||
def all_from_obj(obj, include_super = true)
|
||||
all_from_class(singleton_class_of(obj), include_super)
|
||||
end
|
||||
|
||||
# Get every `Class` and `Module`, in order, that will be checked when looking
|
||||
# for an instance method to call on this object.
|
||||
# @param [Object] obj
|
||||
# @return [Array[Class, Module]]
|
||||
def resolution_order(obj)
|
||||
if Class === obj # rubocop:disable Style/CaseEquality
|
||||
singleton_class_resolution_order(obj) + instance_resolution_order(Class)
|
||||
else
|
||||
klass = begin
|
||||
singleton_class_of(obj)
|
||||
rescue StandardError
|
||||
obj.class
|
||||
end
|
||||
instance_resolution_order(klass)
|
||||
end
|
||||
end
|
||||
|
||||
# Get every `Class` and `Module`, in order, that will be checked when looking
|
||||
# for methods on instances of the given `Class` or `Module`.
|
||||
# This does not treat singleton classes of classes specially.
|
||||
# @param [Class, Module] klass
|
||||
# @return [Array[Class, Module]]
|
||||
def instance_resolution_order(klass)
|
||||
# include klass in case it is a singleton class,
|
||||
([klass] + Pry::Method.safe_send(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)
|
||||
regexp =
|
||||
/^define_singleton_method\(?\s*[:\"\']#{Regexp.escape(name)}|
|
||||
^def\s*self\.#{Regexp.escape(name)}/x
|
||||
regexp =~ definition_line.strip
|
||||
end
|
||||
|
||||
def instance_method_definition?(name, definition_line)
|
||||
regexp =
|
||||
/^define_method\(?\s*[:\"\']#{Regexp.escape(name)}|
|
||||
^def\s*#{Regexp.escape(name)}/x
|
||||
regexp =~ definition_line.strip
|
||||
end
|
||||
|
||||
# Get the singleton classes of superclasses that could define methods on
|
||||
# the given class object, and any modules they include.
|
||||
# If a module is included at multiple points in the ancestry, only
|
||||
# the lowest copy will be returned.
|
||||
def singleton_class_resolution_order(klass)
|
||||
ancestors = Pry::Method.safe_send(klass, :ancestors)
|
||||
resolution_order = ancestors.grep(Class).flat_map do |anc|
|
||||
[singleton_class_of(anc), *singleton_class_of(anc).included_modules]
|
||||
end
|
||||
|
||||
resolution_order.reverse.uniq.reverse - Class.included_modules
|
||||
end
|
||||
|
||||
def singleton_class_of(obj)
|
||||
class << obj; self; end
|
||||
rescue TypeError # can't define singleton. Fixnum, Symbol, Float, ...
|
||||
obj.class
|
||||
end
|
||||
end
|
||||
|
||||
# Workaround for https://github.com/pry/pry/pull/2086
|
||||
def_delegators :@method, :owner, :parameters, :receiver
|
||||
|
||||
# A new instance of `Pry::Method` wrapping the given `::Method`,
|
||||
# `UnboundMethod`, or `Proc`.
|
||||
#
|
||||
# @param [::Method, UnboundMethod, Proc] method
|
||||
# @param [Hash] known_info Can be used to pre-cache expensive to compute stuff.
|
||||
# @return [Pry::Method]
|
||||
def initialize(method, known_info = {})
|
||||
@method = method
|
||||
@visibility = known_info[:visibility]
|
||||
end
|
||||
|
||||
# Get the name of the method as a String, regardless of the underlying
|
||||
# Method#name type.
|
||||
#
|
||||
# @return [String]
|
||||
def name
|
||||
@method.name.to_s
|
||||
end
|
||||
|
||||
# Get the owner of the method as a Pry::Module
|
||||
# @return [Pry::Module]
|
||||
def wrapped_owner
|
||||
@wrapped_owner ||= Pry::WrappedModule.new(owner)
|
||||
end
|
||||
|
||||
# Get underlying object wrapped by this Pry::Method instance
|
||||
# @return [Method, UnboundMethod, Proc]
|
||||
def wrapped
|
||||
@method
|
||||
end
|
||||
|
||||
# Is the method undefined? (aka `Disowned`)
|
||||
# @return [Boolean] false
|
||||
def undefined?
|
||||
false
|
||||
end
|
||||
|
||||
# Get the name of the method including the class on which it was defined.
|
||||
# @example
|
||||
# method(:puts).method_name
|
||||
# => "Kernel.puts"
|
||||
# @return [String]
|
||||
def name_with_owner
|
||||
"#{wrapped_owner.method_prefix}#{name}"
|
||||
end
|
||||
|
||||
# @return [String, nil] The source code of the method, or `nil` if it's unavailable.
|
||||
def source
|
||||
@source ||= case source_type
|
||||
when :c
|
||||
c_source
|
||||
when :ruby
|
||||
ruby_source
|
||||
end
|
||||
end
|
||||
|
||||
# Update the live copy of the method's source.
|
||||
def redefine(source)
|
||||
Patcher.new(self).patch_in_ram source
|
||||
Pry::Method(owner.instance_method(name))
|
||||
end
|
||||
|
||||
# Can we get the source code for this method?
|
||||
# @return [Boolean]
|
||||
def source?
|
||||
!!source
|
||||
rescue MethodSource::SourceNotFoundError
|
||||
false
|
||||
end
|
||||
|
||||
# @return [String, nil] The documentation for the method, or `nil` if it's
|
||||
# unavailable.
|
||||
def doc
|
||||
@doc ||=
|
||||
case source_type
|
||||
when :c
|
||||
info = pry_doc_info
|
||||
info.docstring if info
|
||||
when :ruby
|
||||
get_comment_content(comment)
|
||||
end
|
||||
end
|
||||
|
||||
# @return [Symbol] The source type of the method. The options are
|
||||
# `:ruby` for Ruby methods or `:c` for methods written in C.
|
||||
def source_type
|
||||
source_location.nil? ? :c : :ruby
|
||||
end
|
||||
|
||||
# @return [String, nil] The name of the file the method is defined in, or
|
||||
# `nil` if the filename is unavailable.
|
||||
def source_file
|
||||
if source_location.nil?
|
||||
if source_type == :c
|
||||
info = pry_doc_info
|
||||
info.file if info
|
||||
end
|
||||
else
|
||||
source_location.first
|
||||
end
|
||||
end
|
||||
|
||||
# @return [Fixnum, nil] The line of code in `source_file` which begins
|
||||
# the method's definition, or `nil` if that information is unavailable.
|
||||
def source_line
|
||||
source_location.nil? ? nil : source_location.last
|
||||
end
|
||||
|
||||
# @return [Range, nil] The range of lines in `source_file` which contain
|
||||
# the method's definition, or `nil` if that information is unavailable.
|
||||
def source_range
|
||||
source_location.nil? ? nil : (source_line)..(source_line + source.lines.count - 1)
|
||||
end
|
||||
|
||||
# @return [Symbol] The visibility of the method. May be `:public`,
|
||||
# `:protected`, or `:private`.
|
||||
def visibility
|
||||
@visibility ||=
|
||||
if owner.public_instance_methods.any? { |m| m.to_s == name }
|
||||
:public
|
||||
elsif owner.protected_instance_methods.any? { |m| m.to_s == name }
|
||||
:protected
|
||||
elsif owner.private_instance_methods.any? { |m| m.to_s == name }
|
||||
:private
|
||||
else
|
||||
:none
|
||||
end
|
||||
end
|
||||
|
||||
# @return [String] A representation of the method's signature, including its
|
||||
# name and parameters. Optional and "rest" parameters are marked with `*`
|
||||
# and block parameters with `&`. Keyword arguments are shown with `:`
|
||||
# If the parameter names are unavailable, they're given numbered names instead.
|
||||
# Paraphrased from `awesome_print` gem.
|
||||
def signature
|
||||
if respond_to?(:parameters)
|
||||
args = parameters.inject([]) do |args_array, (arg_type, name)|
|
||||
name ||= (arg_type == :block ? 'block' : "arg#{args_array.size + 1}")
|
||||
args_array.push(
|
||||
case arg_type
|
||||
when :req then name.to_s
|
||||
when :opt then "#{name}=?"
|
||||
when :rest then "*#{name}"
|
||||
when :block then "&#{name}"
|
||||
when :key then "#{name}:?"
|
||||
when :keyreq then "#{name}:"
|
||||
else '?'
|
||||
end
|
||||
)
|
||||
end
|
||||
else
|
||||
args = (1..arity.abs).map { |i| "arg#{i}" }
|
||||
args[-1] = "*#{args[-1]}" if arity < 0
|
||||
end
|
||||
|
||||
"#{name}(#{args.join(', ')})"
|
||||
end
|
||||
|
||||
# @return [Pry::Method, nil] The wrapped method that is called when you
|
||||
# use "super" in the body of this method.
|
||||
def super(times = 1)
|
||||
if @method.is_a?(UnboundMethod)
|
||||
sup = super_using_ancestors(Pry::Method.instance_resolution_order(owner), times)
|
||||
else
|
||||
sup = super_using_ancestors(Pry::Method.resolution_order(receiver), times)
|
||||
sup &&= sup.bind(receiver)
|
||||
end
|
||||
Pry::Method.new(sup) if sup
|
||||
end
|
||||
|
||||
# @return [String, nil] The original name the method was defined under,
|
||||
# before any aliasing, or `nil` if it can't be determined.
|
||||
def original_name
|
||||
return nil if source_type != :ruby
|
||||
|
||||
method_name_from_first_line(source.lines.first)
|
||||
end
|
||||
|
||||
# @return [Boolean] Was the method defined outside a source file?
|
||||
def dynamically_defined?
|
||||
!!(source_file && 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
|
||||
end
|
||||
|
||||
# @return [Array<String>] All known aliases for the method.
|
||||
def aliases
|
||||
owner = @method.owner
|
||||
# Avoid using `to_sym` on {Method#name}, which returns a `String`, because
|
||||
# it won't be garbage collected.
|
||||
name = @method.name
|
||||
|
||||
all_methods_to_compare = owner.instance_methods | owner.private_instance_methods
|
||||
alias_list = all_methods_to_compare.combination(2).select do |pair|
|
||||
pair.include?(name) &&
|
||||
owner.instance_method(pair.first) == owner.instance_method(pair.last)
|
||||
end.flatten
|
||||
alias_list.delete(name)
|
||||
|
||||
alias_list.map(&:to_s)
|
||||
end
|
||||
|
||||
# @return [Boolean] Is the method definitely an alias?
|
||||
def alias?
|
||||
name != original_name
|
||||
end
|
||||
|
||||
# @return [Boolean]
|
||||
def ==(other)
|
||||
return other == @method if other.is_a?(Pry::Method)
|
||||
|
||||
@method == other
|
||||
end
|
||||
|
||||
# @param [Class] klass
|
||||
# @return [Boolean]
|
||||
def is_a?(klass)
|
||||
(klass == Pry::Method) || @method.is_a?(klass)
|
||||
end
|
||||
alias kind_of? is_a?
|
||||
|
||||
# @param [String, Symbol] method_name
|
||||
# @return [Boolean]
|
||||
def respond_to?(method_name, include_all = false)
|
||||
super || @method.respond_to?(method_name, include_all)
|
||||
end
|
||||
|
||||
# Delegate any unknown calls to the wrapped method.
|
||||
def method_missing(method_name, *args, &block)
|
||||
if @method.respond_to?(method_name)
|
||||
@method.__send__(method_name, *args, &block)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def respond_to_missing?(method_name, include_private = false)
|
||||
@method.respond_to?(method_name) || super
|
||||
end
|
||||
|
||||
def comment
|
||||
Pry::Code.from_file(source_file).comment_describing(source_line)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @return [YARD::CodeObjects::MethodObject]
|
||||
# @raise [CommandError] when the method can't be found or `pry-doc` isn't installed.
|
||||
def pry_doc_info
|
||||
if defined?(PryDoc)
|
||||
Pry::MethodInfo.info_for(@method) ||
|
||||
raise(
|
||||
CommandError,
|
||||
"Cannot locate this method: #{name}. (source_location returns nil)"
|
||||
)
|
||||
else
|
||||
fail_msg = "Cannot locate this method: #{name}."
|
||||
if Helpers::Platform.mri?
|
||||
fail_msg += " Run `gem install pry-doc` to install" \
|
||||
" Ruby Core documentation," \
|
||||
" and `require 'pry-doc'` to load it.\n"
|
||||
end
|
||||
raise CommandError, fail_msg
|
||||
end
|
||||
end
|
||||
|
||||
# @param [Class, Module] ancestors The ancestors to investigate
|
||||
# @return [Method] The unwrapped super-method
|
||||
def super_using_ancestors(ancestors, times = 1)
|
||||
next_owner = owner
|
||||
times.times do
|
||||
i = ancestors.index(next_owner) + 1
|
||||
while ancestors[i] &&
|
||||
!(ancestors[i].method_defined?(name) ||
|
||||
ancestors[i].private_method_defined?(name))
|
||||
i += 1
|
||||
end
|
||||
(next_owner = ancestors[i]) || (return nil)
|
||||
end
|
||||
|
||||
begin
|
||||
safe_send(next_owner, :instance_method, name)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# @param [String] first_ln The first line of a method definition.
|
||||
# @return [String, nil]
|
||||
def method_name_from_first_line(first_ln)
|
||||
return nil if first_ln.strip !~ /^def /
|
||||
|
||||
tokens = SyntaxHighlighter.tokenize(first_ln).each_slice(2)
|
||||
tokens.each_cons(2) do |t1, t2|
|
||||
if t2.last == :method || t2.last == :ident && t1 == [".", :operator]
|
||||
return t2.first
|
||||
end
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def c_source
|
||||
info = pry_doc_info
|
||||
strip_comments_from_c_code(info.source) if info && info.source
|
||||
end
|
||||
|
||||
def ruby_source
|
||||
# Clone of `MethodSource.source_helper` that knows to use our
|
||||
# hacked version of `source_location` for our input buffer for methods
|
||||
# defined in `(pry)`.
|
||||
file, line = *source_location
|
||||
unless file
|
||||
raise SourceNotFoundError, "Could not locate source for #{name_with_owner}!"
|
||||
end
|
||||
|
||||
begin
|
||||
code = Pry::Code.from_file(file).expression_at(line)
|
||||
rescue SyntaxError => e
|
||||
raise MethodSource::SourceNotFoundError, e.message
|
||||
end
|
||||
strip_leading_whitespace(code)
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user