From 0ee3c4af14586f7b833909e79dcfd6e5a14130b1 Mon Sep 17 00:00:00 2001 From: John Mair Date: Sat, 23 Jun 2012 03:33:12 +1200 Subject: [PATCH] Extract out Pry::WrappedModule::Candidate from Pry::WrappedModule & document. --- lib/pry/code.rb | 9 +- lib/pry/default_commands/introspection.rb | 46 ++-- lib/pry/helpers/command_helpers.rb | 2 +- lib/pry/module_candidate.rb | 131 +++++++++++ lib/pry/wrapped_module.rb | 273 ++++++++++------------ 5 files changed, 286 insertions(+), 175 deletions(-) create mode 100644 lib/pry/module_candidate.rb diff --git a/lib/pry/code.rb b/lib/pry/code.rb index 6d22e593..c7867a2b 100644 --- a/lib/pry/code.rb +++ b/lib/pry/code.rb @@ -69,10 +69,11 @@ class Pry # # @param [Module, Class] mod The module (or class) of interest. # @return [Code] - def from_module(mod, start_line=nil) - mod = Pry::WrappedModule(mod) - start_line ||= mod.source_line || 1 - new(mod.source, start_line, :ruby) + def from_module(mod, start_line=nil, candidate_rank=0) + candidate = Pry::WrappedModule(mod).candidate(candidate_rank) + + start_line ||= candidate.line + new(candidate.source, start_line, :ruby) end protected diff --git a/lib/pry/default_commands/introspection.rb b/lib/pry/default_commands/introspection.rb index 3795996b..c6c03221 100644 --- a/lib/pry/default_commands/introspection.rb +++ b/lib/pry/default_commands/introspection.rb @@ -27,11 +27,11 @@ class Pry render_output(code_or_doc, opts) end - def module_start_line(mod, candidate=0) + def module_start_line(mod, candidate_rank=0) if opts.present?(:'base-one') 1 else - mod.source_line_for_candidate(candidate) + mod.candidate(candidate_rank).line end end @@ -86,24 +86,19 @@ class Pry # classes on MRI. This is different to source_location, which # will return nil. if mod.yard_docs? - file_name, line = mod.source_file, nil + file_name, line = mod.yard_file, mod.yard_line else file_name, line = mod.source_location end - if mod.doc.empty? - output.puts "No documentation found." - "" - else - set_file_and_dir_locals(file_name) if !mod.yard_docs? - doc = "" - doc << mod.doc + set_file_and_dir_locals(file_name) if !mod.yard_docs? + doc = "" + doc << mod.doc - doc = Code.new(doc, module_start_line(mod), :text). - with_line_numbers(use_line_numbers?).to_s + doc = Code.new(doc, module_start_line(mod), :text). + with_line_numbers(use_line_numbers?).to_s - doc.insert(0, "\n#{Pry::Helpers::Text.bold('From:')} #{file_name} @ line #{line ? line : "N/A"}:\n\n") - end + doc.insert(0, "\n#{Pry::Helpers::Text.bold('From:')} #{file_name} @ line #{line ? line : "N/A"}:\n\n") end def all_modules @@ -112,11 +107,12 @@ class Pry doc = "" doc << "Found #{mod.number_of_candidates} candidates for `#{mod.name}` definition:\n" mod.number_of_candidates.times do |v| + candidate = mod.candidate(v) begin - doc << "\nCandidate #{v+1}/#{mod.number_of_candidates}: #{mod.source_file_for_candidate(v)} @ #{mod.source_line_for_candidate(v)}:\n\n" - dc = mod.doc_for_candidate(v) - doc << (dc.empty? ? "No documentation found.\n" : dc) - rescue Pry::RescuableException + doc << "\nCandidate #{v+1}/#{mod.number_of_candidates}: #{candidate.file} @ #{candidate.line}:\n\n" + doc << candidate.doc(false) + rescue Pry::RescuableException => ex + doc << "No documentation found.\n" next end end @@ -146,8 +142,8 @@ class Pry if opts.present?(:'base-one') 1 else - if mod.source_line_for_candidate(candidate) - mod.source_line_for_candidate(candidate) - mod.doc_for_candidate(candidate).lines.count + if mod.candidate(candidate).line + mod.candidate(candidate).line - mod.candidate(candidate).doc.lines.count else 1 end @@ -254,7 +250,8 @@ class Pry file_name, line = mod.source_location set_file_and_dir_locals(file_name) - code = Code.from_module(mod, module_start_line(mod)).with_line_numbers(use_line_numbers?).to_s + code = Code.from_module(mod, module_start_line(mod)). + with_line_numbers(use_line_numbers?).to_s result = "" result << "\n#{Pry::Helpers::Text.bold('From:')} #{file_name} @ line #{line}:\n" result << "#{Pry::Helpers::Text.bold('Number of lines:')} #{code.lines.count}\n\n" @@ -267,12 +264,15 @@ class Pry result = "" result << "Found #{mod.number_of_candidates} candidates for `#{mod.name}` definition:\n" mod.number_of_candidates.times do |v| + candidate = mod.candidate(v) begin - code = Code.new(mod.source_for_candidate(v), module_start_line(mod, v)).with_line_numbers(use_line_numbers?).to_s - result << "\nCandidate #{v+1}/#{mod.number_of_candidates}: #{mod.source_file_for_candidate(v)} @ line #{mod.source_line_for_candidate(v)}:\n" + result << "\nCandidate #{v+1}/#{mod.number_of_candidates}: #{candidate.file} @ line #{candidate.line}:\n" + code = Code.from_module(mod, module_start_line(mod, v), v). + with_line_numbers(use_line_numbers?).to_s result << "Number of lines: #{code.lines.count}\n\n" result << code rescue Pry::RescuableException + result << "\nNo code found.\n" next end end diff --git a/lib/pry/helpers/command_helpers.rb b/lib/pry/helpers/command_helpers.rb index f509409e..1f900a66 100644 --- a/lib/pry/helpers/command_helpers.rb +++ b/lib/pry/helpers/command_helpers.rb @@ -69,7 +69,7 @@ class Pry header = "\n#{Pry::Helpers::Text.bold('From:')} #{meth.source_file} " if meth.source_type == :c - header << "in Ruby Core (C Method):\n" + header << "(C Method):\n" else header << "@ line #{meth.source_line}:\n" end diff --git a/lib/pry/module_candidate.rb b/lib/pry/module_candidate.rb new file mode 100644 index 00000000..f7400baf --- /dev/null +++ b/lib/pry/module_candidate.rb @@ -0,0 +1,131 @@ +require 'pry/helpers/documentation_helpers' +require 'forwardable' + +class Pry + class WrappedModule + + # This class represents a single candidate for a module/class definition. + # It provides access to the source, documentation, line and file + # for a monkeypatch (reopening) of a class/module. All candidates + # are + class Candidate + include Pry::Helpers::DocumentationHelpers + extend Forwardable + + # @return [String] The file where the module definition is located. + attr_reader :file + + # @return [Fixnum] The line where the module definition is located. + attr_reader :line + + # Methods to delegate to associated `Pry::WrappedModule instance`. + to_delegate = [:lines_for_file, :method_candidates, :name, :wrapped, + :yard_docs?, :number_of_candidates] + + def_delegators :@wrapper, *to_delegate + private *to_delegate + + # @param [Pry::WrappedModule] wrapper The associated + # `Pry::WrappedModule` instance that owns the candidates. + # @param [Fixnum] rank The rank of the candidate to + # retrieve. Passing 0 returns 'primary candidate' (the candidate with largest + # number of methods), passing 1 retrieves candidate with + # second largest number of methods, and so on, up to + # `Pry::WrappedModule#number_of_candidates() - 1` + def initialize(wrapper, rank) + @wrapper = wrapper + + if rank > (number_of_candidates - 1) + raise CommandError, "No such module candidate. Allowed candidates range is from 0 to #{number_of_candidates - 1}" + end + + @rank = rank + @file, @line = source_location + end + + # @raise [Pry::CommandError] If source code cannot be found. + # @return [String] The source for the candidate, i.e the + # complete module/class definition. + def source + return @source if @source + + raise CommandError, "Could not locate source for #{wrapped}!" if file.nil? + + @source = strip_leading_whitespace(Pry::Code.from_file(file).expression_at(line)) + end + + # @raise [Pry::CommandError] If documentation cannot be found. + # @param [Boolean] check_yard Try to retrieve yard docs instead + # (if they exist), otherwise attempt to return the docs for the + # discovered module definition. `check_yard` is only relevant + # for the primary candidate (rank 0) candidate. + # @return [String] The documentation for the candidate. + def doc(check_yard=true) + return @doc if @doc + + docstring = if check_yard && @rank == 0 && yard_docs? + from_yard = YARD::Registry.at(name).docstring.to_s + from_yard.empty? ? nil : from_yard + elsif source_location.nil? + nil + else + Pry::Code.from_file(file).comment_describing(line) + end + + raise CommandError, "Could not locate doc for #{wrapped}!" if docstring.nil? || docstring.empty? + + @doc = process_doc(docstring) + end + + # @return [Array, nil] A `[String, Fixnum]` pair representing the + # source location (file and line) for the candidate or `nil` + # if no source location found. + def source_location + return @source_location if @source_location + + mod_type_string = wrapped.class.to_s.downcase + file, line = method_source_location + + return nil if !file.is_a?(String) + + class_regexes = [/#{mod_type_string}\s*(\w*)(::)?#{wrapped.name.split(/::/).last}/, + /(::)?#{wrapped.name.split(/::/).last}\s*?=\s*?#{wrapped.class}/, + /(::)?#{wrapped.name.split(/::/).last}\.(class|instance)_eval/] + + host_file_lines = lines_for_file(file) + + search_lines = host_file_lines[0..(line - 2)] + idx = search_lines.rindex { |v| class_regexes.any? { |r| r =~ v } } + + @source_location = [file, idx + 1] + rescue Pry::RescuableException + nil + end + + private + + # This method is used by `Candidate#source_location` as a + # starting point for the search for the candidate's definition. + # @return [Array] The source location of the base method used to + # calculate the source location of the candidate. + def method_source_location + return @method_source_location if @method_source_location + + file, line = method_candidates[@rank].source_location + + if file && RbxPath.is_core_path?(file) + file = RbxPath.convert_path_to_full(file) + end + + @method_source_location = [file, line] + end + + # @param [String] doc The raw docstring to process. + # @return [String] Process docstring markup and strip leading white space. + def process_doc(doc) + process_comment_markup(strip_leading_hash_and_whitespace_from_ruby_comments(doc), + :ruby) + end + end + end +end diff --git a/lib/pry/wrapped_module.rb b/lib/pry/wrapped_module.rb index 8ab50392..df5cd525 100644 --- a/lib/pry/wrapped_module.rb +++ b/lib/pry/wrapped_module.rb @@ -1,4 +1,4 @@ -require 'pry/helpers/documentation_helpers' +require 'pry/module_candidate' class Pry class << self @@ -14,8 +14,6 @@ class Pry end class WrappedModule - include Helpers::DocumentationHelpers - attr_reader :wrapped private :wrapped @@ -46,6 +44,7 @@ class Pry def initialize(mod) raise ArgumentError, "Tried to initialize a WrappedModule with a non-module #{mod.inspect}" unless ::Module === mod @wrapped = mod + @memoized_candidates = [] @host_file_lines = nil @source = nil @source_location = nil @@ -110,72 +109,6 @@ class Pry super || wrapped.respond_to?(method_name) end - def yard_docs? - !!(defined?(YARD) && YARD::Registry.at(name)) - end - - def process_doc(doc) - process_comment_markup(strip_leading_hash_and_whitespace_from_ruby_comments(doc), - :ruby) - end - - def doc - return @doc if @doc - - if yard_docs? - from_yard = YARD::Registry.at(name) - @doc = from_yard.docstring - elsif source_location.nil? - raise CommandError, "Can't find module's source location" - else - @doc = extract_doc_for_candidate(0) - end - - raise CommandError, "Can't find docs for module: #{name}." if !@doc - - @doc = process_doc(@doc) - end - - def doc_for_candidate(idx) - doc = extract_doc_for_candidate(idx) - raise CommandError, "Can't find docs for module: #{name}." if !doc - - process_doc(doc) - end - - # Retrieve the source for the module. - def source - @source ||= source_for_candidate(0) - end - - def source_for_candidate(idx) - file, line = module_source_location_for_candidate(idx) - raise CommandError, "Could not locate source for #{wrapped}!" if file.nil? - - strip_leading_whitespace(Pry::Code.from_file(file).expression_at(line)) - end - - def source_file - if yard_docs? - from_yard = YARD::Registry.at(name) - from_yard.file - else - source_file_for_candidate(0) - end - end - - def source_line - source_line_for_candidate(0) - end - - def source_file_for_candidate(idx) - Array(module_source_location_for_candidate(idx)).first - end - - def source_line_for_candidate(idx) - Array(module_source_location_for_candidate(idx)).last - end - # Retrieve the source location of a module. Return value is in same # format as Method#source_location. If the source location # cannot be found this method returns `nil`. @@ -184,74 +117,104 @@ class Pry # @return [Array] The source location of the # module (or class). def source_location - @source_location ||= module_source_location_for_candidate(0) - rescue Pry::RescuableException - nil + @source_location ||= primary_candidate.source_location end - # memoized lines for file - def lines_for_file(file) - @lines_for_file ||= {} + # @return [String, nil] The associated file for the module (i.e + # the primary candidate: highest ranked monkeypatch). + def file + Array(source_location).first + end - if file == Pry.eval_path - @lines_for_file[file] ||= Pry.line_buffer.drop(1) - else - @lines_for_file[file] ||= File.readlines(file) + # @return [Fixnum, nil] The associated line for the module (i.e + # the primary candidate: highest ranked monkeypatch). + def line + Array(source_location).last + end + + # Returns documentation for the module, with preference given to yard docs if + # available. This documentation is for the primary candidate, if + # you would like documentation for other candidates use + # `WrappedModule#candidate` to select the candidate you're + # interested in. + # @raise [Pry::CommandError] If documentation cannot be found. + # @return [String] The documentation for the module. + def doc + @doc ||= primary_candidate.doc + end + + # Returns the source for the module. + # This source is for the primary candidate, if + # you would like source for other candidates use + # `WrappedModule#candidate` to select the candidate you're + # interested in. + # @raise [Pry::CommandError] If source cannot be found. + # @return [String] The source for the module. + def source + @source ||= primary_candidate.source + end + + # @return [String] Return the associated file for the + # module from YARD, if one exists. + def yard_file + YARD::Registry.at(name).file if yard_docs? + end + + # @return [Fixnum] Return the associated line for the + # module from YARD, if one exists. + def yard_line + YARD::Registry.at(name).line if yard_docs? + end + + # Return a candidate for this module of specified rank. A `rank` + # of 0 is equivalent to the 'primary candidate', which is the + # module definition with the highest number of methods. A `rank` + # of 1 is the module definition with the second highest number of + # methods, and so on. Module candidates are necessary as modules + # can be reopened multiple times and in multiple places in Ruby, + # the candidate API gives you access to the module definition + # representing each of those reopenings. + # @raise [Pry::CommandError] If the `rank` is out of range. That + # is greater than `number_of_candidates - 1`. + # @param [Fixnum] rank + # @return [Pry::WrappedModule::Candidate] + def candidate(rank) + @memoized_candidates[rank] ||= Candidate.new(self, rank) + end + + + # @return [Fixnum] The number of candidate definitions for the + # current module. + def number_of_candidates + method_candidates.count + end + + # @return [Boolean] Whether YARD docs are available for this module. + def yard_docs? + !!(defined?(YARD) && YARD::Registry.at(name)) + end + + private + + # @return [Pry::WrappedModule::Candidate] The candidate of rank 0, + # that is the 'monkey patch' of this module with the highest + # number of methods. It is considered the 'canonical' definition + # for the module. + def primary_candidate + @primary_candidate ||= candidate(0) + end + + # @return [Array] The array of `Pry::Method` objects, + # there is one associated with each candidate. Each one is the 'base + # method' for a candidate and it serves as the start point for + # the search in uncovering the module definition. + def method_candidates + @method_candidates ||= all_source_locations_by_popularity.map do |group| + group.last.sort_by(&:source_line).first # best candidate for group end end - def module_source_location_for_candidate(idx) - mod_type_string = wrapped.class.to_s.downcase - file, line = method_source_location_for_candidate(idx) - - return nil if !file.is_a?(String) - - class_regexes = [/#{mod_type_string}\s*(\w*)(::)?#{wrapped.name.split(/::/).last}/, - /(::)?#{wrapped.name.split(/::/).last}\s*?=\s*?#{wrapped.class}/, - /(::)?#{wrapped.name.split(/::/).last}\.(class|instance)_eval/] - - host_file_lines = lines_for_file(file) - - search_lines = host_file_lines[0..(line - 2)] - idx = search_lines.rindex { |v| class_regexes.any? { |r| r =~ v } } - - [file, idx + 1] - end - - def extract_doc - extract_doc_for_candidate(0) - end - - def extract_doc_for_candidate(idx) - file, line_num = module_source_location_for_candidate(idx) - - Pry::Code.from_file(file).comment_describing(line_num) - end - - # FIXME: this method is also found in Pry::Method - def safe_send(obj, method, *args, &block) - (Module === obj ? Module : Object).instance_method(method).bind(obj).call(*args, &block) - end - - # FIXME: a variant of this method is also found in Pry::Method - def all_from_common(mod, method_type) - %w(public protected private).map do |visibility| - safe_send(mod, :"#{visibility}_#{method_type}s", false).select do |method_name| - if method_type == :method - safe_send(mod, method_type, method_name).owner == class << mod; self; end - else - safe_send(mod, method_type, method_name).owner == mod - end - end.map do |method_name| - Pry::Method.new(safe_send(mod, method_type, method_name), :visibility => visibility.to_sym) - end - end.flatten - end - - def all_methods_for(mod) - all_from_common(mod, :instance_method) + all_from_common(mod, :method) - end - + # A helper method. def all_source_locations_by_popularity return @all_source_locations_by_popularity if @all_source_locations_by_popularity @@ -269,25 +232,41 @@ class Pry sort_by { |k, v| -v.size } end - def method_candidates - @method_candidates ||= all_source_locations_by_popularity.map do |group| - group.last.sort_by(&:source_line).first # best candidate for group + # Return all methods (instance methods and class methods) for a + # given module. + def all_methods_for(mod) + all_from_common(mod, :instance_method) + all_from_common(mod, :method) + end + + # FIXME: a variant of this method is also found in Pry::Method + def all_from_common(mod, method_type) + %w(public protected private).map do |visibility| + safe_send(mod, :"#{visibility}_#{method_type}s", false).select do |method_name| + if method_type == :method + safe_send(mod, method_type, method_name).owner == class << mod; self; end + else + safe_send(mod, method_type, method_name).owner == mod + end + end.map do |method_name| + Pry::Method.new(safe_send(mod, method_type, method_name), :visibility => visibility.to_sym) + end + end.flatten + end + + # memoized lines for file + def lines_for_file(file) + @lines_for_file ||= {} + + if file == Pry.eval_path + @lines_for_file[file] ||= Pry.line_buffer.drop(1) + else + @lines_for_file[file] ||= File.readlines(file) end end - def number_of_candidates - method_candidates.count + # FIXME: this method is also found in Pry::Method + def safe_send(obj, method, *args, &block) + (Module === obj ? Module : Object).instance_method(method).bind(obj).call(*args, &block) end - - def method_source_location_for_candidate(idx) - file, line = method_candidates[idx].source_location - - if file && RbxPath.is_core_path?(file) - file = RbxPath.convert_path_to_full(file) - end - - [file, line] - end - end end