This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Pry
|
||||
class Command
|
||||
class ShowInfo < Pry::ClassCommand
|
||||
extend Pry::Helpers::BaseHelpers
|
||||
|
||||
command_options shellwords: false, interpolate: false
|
||||
|
||||
def initialize(*)
|
||||
super
|
||||
|
||||
@used_super = nil
|
||||
end
|
||||
|
||||
def options(opt)
|
||||
opt.on :s, :super, "Select the 'super' method. Can be repeated to " \
|
||||
"traverse the ancestors", as: :count
|
||||
opt.on :l, "line-numbers", "Show line numbers"
|
||||
opt.on :b, "base-one", "Show line numbers but start numbering at 1 " \
|
||||
"(useful for `amend-line` and `play` commands)"
|
||||
opt.on :a, :all, "Show all definitions and monkeypatches of the " \
|
||||
"module/class"
|
||||
end
|
||||
|
||||
def process
|
||||
code_object = Pry::CodeObject.lookup(obj_name, pry_instance, super: opts[:super])
|
||||
raise CommandError, no_definition_message unless code_object
|
||||
|
||||
@original_code_object = code_object
|
||||
|
||||
if !obj_name && code_object.c_module? && !opts[:all]
|
||||
result = "You're inside an object, whose class is defined by means of " \
|
||||
"the C Ruby API.\nPry cannot display the information for this " \
|
||||
"class."
|
||||
if code_object.candidates.any?
|
||||
result += "\nHowever, you can view monkey-patches applied to this " \
|
||||
"class.\n.Just execute the same command with the '--all' " \
|
||||
"switch."
|
||||
end
|
||||
elsif show_all_modules?(code_object)
|
||||
# show all monkey patches for a module
|
||||
|
||||
result = content_and_headers_for_all_module_candidates(code_object)
|
||||
else
|
||||
# show a specific code object
|
||||
co = code_object_with_accessible_source(code_object)
|
||||
result = content_and_header_for_code_object(co)
|
||||
end
|
||||
|
||||
set_file_and_dir_locals(code_object.source_file)
|
||||
pry_instance.pager.page result
|
||||
end
|
||||
|
||||
# This method checks whether the `code_object` is a WrappedModule, if it
|
||||
# is, then it returns the first candidate (monkeypatch) with accessible
|
||||
# source (or docs). If `code_object` is not a WrappedModule (i.e a method
|
||||
# or a command) then the `code_object` itself is just returned.
|
||||
#
|
||||
# @return [Pry::WrappedModule, Pry::Method, Pry::Command]
|
||||
def code_object_with_accessible_source(code_object)
|
||||
return code_object unless code_object.is_a?(WrappedModule)
|
||||
|
||||
candidate = code_object.candidates.find(&:source)
|
||||
return candidate if candidate
|
||||
|
||||
raise CommandError, no_definition_message unless valid_superclass?(code_object)
|
||||
|
||||
@used_super = true
|
||||
code_object_with_accessible_source(code_object.super)
|
||||
end
|
||||
|
||||
def valid_superclass?(code_object)
|
||||
code_object.super && code_object.super.wrapped != Object
|
||||
end
|
||||
|
||||
def content_and_header_for_code_object(code_object)
|
||||
header(code_object) + content_for(code_object)
|
||||
end
|
||||
|
||||
def content_and_headers_for_all_module_candidates(mod)
|
||||
result = "Found #{mod.number_of_candidates} candidates for " \
|
||||
"`#{mod.name}` definition:\n"
|
||||
mod.number_of_candidates.times do |v|
|
||||
candidate = mod.candidate(v)
|
||||
begin
|
||||
result += "\nCandidate #{v + 1}/#{mod.number_of_candidates}: " \
|
||||
"#{candidate.source_file}:#{candidate.source_line}\n"
|
||||
content = content_for(candidate)
|
||||
|
||||
result += "Number of lines: #{content.lines.count}\n\n" + content
|
||||
rescue Pry::RescuableException
|
||||
result += "\nNo content found.\n"
|
||||
next
|
||||
end
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
def no_definition_message
|
||||
"Couldn't locate a definition for #{obj_name}"
|
||||
end
|
||||
|
||||
# Generate a header (meta-data information) for all the code
|
||||
# object types: methods, modules, commands, procs...
|
||||
def header(code_object)
|
||||
file_name, line_num = file_and_line_for(code_object)
|
||||
content = content_for(code_object)
|
||||
|
||||
h = "\n#{bold('From:')} #{file_name}"
|
||||
h += code_object_header(code_object, line_num)
|
||||
h += "\n#{bold('Number of lines:')} " + "#{content.lines.count}\n\n"
|
||||
if @used_super
|
||||
h += bold('** Warning:')
|
||||
h += " Cannot find code for #{@original_code_object.nonblank_name}. " \
|
||||
"Showing superclass #{code_object.nonblank_name} instead. **\n\n"
|
||||
end
|
||||
|
||||
if content.lines.none?
|
||||
h += bold('** Warning:')
|
||||
h += " Cannot find code for '#{code_object.name}' (source_location is nil)"
|
||||
end
|
||||
|
||||
h
|
||||
end
|
||||
|
||||
def code_object_header(code_object, line_num)
|
||||
if code_object.real_method_object?
|
||||
method_header(code_object, line_num)
|
||||
|
||||
# It sucks we have to test for both Pry::WrappedModule and
|
||||
# WrappedModule::Candidate, probably indicates a deep refactor needs
|
||||
# to happen in those classes.
|
||||
elsif code_object.is_a?(Pry::WrappedModule) ||
|
||||
code_object.is_a?(Pry::WrappedModule::Candidate)
|
||||
module_header(code_object, line_num)
|
||||
else
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
def method_header(code_object, line_num)
|
||||
h = ""
|
||||
h += (code_object.c_method? ? ' (C Method):' : ":#{line_num}:")
|
||||
h += method_sections(code_object)[:owner]
|
||||
h += method_sections(code_object)[:visibility]
|
||||
h += method_sections(code_object)[:signature]
|
||||
h
|
||||
end
|
||||
|
||||
def module_header(code_object, line_num)
|
||||
h = ""
|
||||
h += ":#{line_num}\n"
|
||||
h += bold(code_object.module? ? "Module" : "Class")
|
||||
h += " #{bold('name:')} #{code_object.nonblank_name}"
|
||||
|
||||
if code_object.number_of_candidates > 1
|
||||
h += bold("\nNumber of monkeypatches: ")
|
||||
h += code_object.number_of_candidates.to_s
|
||||
h += ". Use the `-a` option to display all available monkeypatches"
|
||||
end
|
||||
h
|
||||
end
|
||||
|
||||
def method_sections(code_object)
|
||||
{
|
||||
owner: "\n#{bold('Owner:')} #{code_object.owner || 'N/A'}\n",
|
||||
visibility: "#{bold('Visibility:')} #{code_object.visibility}",
|
||||
signature: "\n#{bold('Signature:')} #{code_object.signature}"
|
||||
}.merge(header_options) { |_key, old, new| (new && old).to_s }
|
||||
end
|
||||
|
||||
def header_options
|
||||
{
|
||||
owner: true,
|
||||
visibility: true,
|
||||
signature: nil
|
||||
}
|
||||
end
|
||||
|
||||
def show_all_modules?(code_object)
|
||||
code_object.is_a?(Pry::WrappedModule) && opts.present?(:all)
|
||||
end
|
||||
|
||||
def obj_name
|
||||
@obj_name ||= args.empty? ? nil : args.join(' ')
|
||||
end
|
||||
|
||||
def use_line_numbers?
|
||||
opts.present?(:b) || opts.present?(:l)
|
||||
end
|
||||
|
||||
def start_line_for(code_object)
|
||||
if opts.present?(:'base-one')
|
||||
1
|
||||
else
|
||||
code_object.source_line || 1
|
||||
end
|
||||
end
|
||||
|
||||
# takes into account possible yard docs, and returns yard_file / yard_line
|
||||
# Also adjusts for start line of comments (using start_line_for), which it
|
||||
# has to infer by subtracting number of lines of comment from start line
|
||||
# of code_object
|
||||
def file_and_line_for(code_object)
|
||||
if code_object.module_with_yard_docs?
|
||||
[code_object.yard_file, code_object.yard_line]
|
||||
else
|
||||
[code_object.source_file, start_line_for(code_object)]
|
||||
end
|
||||
end
|
||||
|
||||
def complete(input)
|
||||
if input =~ /([^ ]*)#([a-z0-9_]*)\z/
|
||||
prefix = Regexp.last_match(1)
|
||||
search = Regexp.last_match(2)
|
||||
methods =
|
||||
begin
|
||||
# rubocop:disable Security/Eval
|
||||
Pry::Method.all_from_class(binding.eval(prefix))
|
||||
# rubocop:enable Security/Eval
|
||||
rescue RescuableException
|
||||
return super
|
||||
end
|
||||
methods.map do |method|
|
||||
[prefix, method.name].join('#') if method.name.start_with?(search)
|
||||
end.compact
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user