From 9790517a0cb0cdef5b36b7dc2d61f6add31703aa Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Mon, 31 Jul 2023 20:57:29 +0100 Subject: [PATCH] Decouple `edit` and `show_source` commands (#658) * Decouple `edit` command from `show_source` 2 commands should not depend on each other. If `edit` command also needs to find a source, the source finding logic should be extracted into a separate class. * Return nil if is not an actual file path * Refactor SourceFinder --- lib/irb/cmd/edit.rb | 7 ++-- lib/irb/cmd/show_source.rb | 65 +++----------------------------------- lib/irb/source_finder.rb | 64 +++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 65 deletions(-) create mode 100644 lib/irb/source_finder.rb diff --git a/lib/irb/cmd/edit.rb b/lib/irb/cmd/edit.rb index 0103891cf..523a1dac5 100644 --- a/lib/irb/cmd/edit.rb +++ b/lib/irb/cmd/edit.rb @@ -1,5 +1,6 @@ require 'shellwords' require_relative "nop" +require_relative "../source_finder" module IRB # :stopdoc: @@ -28,17 +29,15 @@ def execute(*args) end if !File.exist?(path) - require_relative "show_source" - source = begin - ShowSource.find_source(path, @irb_context) + SourceFinder.new(@irb_context).find_source(path) rescue NameError # if user enters a path that doesn't exist, it'll cause NameError when passed here because find_source would try to evaluate it as well # in this case, we should just ignore the error end - if source && File.exist?(source.file) + if source path = source.file else puts "Can not find file: #{path}" diff --git a/lib/irb/cmd/show_source.rb b/lib/irb/cmd/show_source.rb index f17212387..10463ebf0 100644 --- a/lib/irb/cmd/show_source.rb +++ b/lib/irb/cmd/show_source.rb @@ -1,12 +1,10 @@ # frozen_string_literal: true require_relative "nop" +require_relative "../source_finder" require_relative "../color" -require_relative "../ruby-lex" module IRB - # :stopdoc: - module ExtendCommand class ShowSource < Nop category "Context" @@ -21,51 +19,6 @@ def transform_args(args) args.strip.dump end end - - def find_source(str, irb_context) - case str - when /\A[A-Z]\w*(::[A-Z]\w*)*\z/ # Const::Name - eval(str, irb_context.workspace.binding) # trigger autoload - base = irb_context.workspace.binding.receiver.yield_self { |r| r.is_a?(Module) ? r : Object } - file, line = base.const_source_location(str) - when /\A(?[A-Z]\w*(::[A-Z]\w*)*)#(?[^ :.]+)\z/ # Class#method - owner = eval(Regexp.last_match[:owner], irb_context.workspace.binding) - method = Regexp.last_match[:method] - if owner.respond_to?(:instance_method) - methods = owner.instance_methods + owner.private_instance_methods - file, line = owner.instance_method(method).source_location if methods.include?(method.to_sym) - end - when /\A((?.+)(\.|::))?(?[^ :.]+)\z/ # method, receiver.method, receiver::method - receiver = eval(Regexp.last_match[:receiver] || 'self', irb_context.workspace.binding) - method = Regexp.last_match[:method] - file, line = receiver.method(method).source_location if receiver.respond_to?(method, true) - end - if file && line - Source.new(file: file, first_line: line, last_line: find_end(file, line, irb_context)) - end - end - - private - - def find_end(file, first_line, irb_context) - return first_line unless File.exist?(file) - lex = RubyLex.new(irb_context) - lines = File.read(file).lines[(first_line - 1)..-1] - tokens = RubyLex.ripper_lex_without_warning(lines.join) - prev_tokens = [] - - # chunk with line number - tokens.chunk { |tok| tok.pos[0] }.each do |lnum, chunk| - code = lines[0..lnum].join - prev_tokens.concat chunk - continue = lex.should_continue?(prev_tokens) - syntax = lex.check_code_syntax(code) - if !continue && syntax == :valid - return first_line + lnum - end - end - first_line - end end def execute(str = nil) @@ -74,8 +27,9 @@ def execute(str = nil) return end - source = self.class.find_source(str, @irb_context) - if source && File.exist?(source.file) + source = SourceFinder.new(@irb_context).find_source(str) + + if source show_source(source) else puts "Error: Couldn't locate a definition for #{str}" @@ -85,7 +39,6 @@ def execute(str = nil) private - # @param [IRB::ExtendCommand::ShowSource::Source] source def show_source(source) puts puts "#{bold("From")}: #{source.file}:#{source.first_line}" @@ -98,16 +51,6 @@ def show_source(source) def bold(str) Color.colorize(str, [:BOLD]) end - - Source = Struct.new( - :file, # @param [String] - file name - :first_line, # @param [String] - first line - :last_line, # @param [String] - last line - keyword_init: true, - ) - private_constant :Source end end - - # :startdoc: end diff --git a/lib/irb/source_finder.rb b/lib/irb/source_finder.rb new file mode 100644 index 000000000..d196fcddc --- /dev/null +++ b/lib/irb/source_finder.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require_relative "ruby-lex" + +module IRB + class SourceFinder + Source = Struct.new( + :file, # @param [String] - file name + :first_line, # @param [String] - first line + :last_line, # @param [String] - last line + keyword_init: true, + ) + private_constant :Source + + def initialize(irb_context) + @irb_context = irb_context + end + + def find_source(signature) + context_binding = @irb_context.workspace.binding + case signature + when /\A[A-Z]\w*(::[A-Z]\w*)*\z/ # Const::Name + eval(signature, context_binding) # trigger autoload + base = context_binding.receiver.yield_self { |r| r.is_a?(Module) ? r : Object } + file, line = base.const_source_location(signature) + when /\A(?[A-Z]\w*(::[A-Z]\w*)*)#(?[^ :.]+)\z/ # Class#method + owner = eval(Regexp.last_match[:owner], context_binding) + method = Regexp.last_match[:method] + if owner.respond_to?(:instance_method) + methods = owner.instance_methods + owner.private_instance_methods + file, line = owner.instance_method(method).source_location if methods.include?(method.to_sym) + end + when /\A((?.+)(\.|::))?(?[^ :.]+)\z/ # method, receiver.method, receiver::method + receiver = eval(Regexp.last_match[:receiver] || 'self', context_binding) + method = Regexp.last_match[:method] + file, line = receiver.method(method).source_location if receiver.respond_to?(method, true) + end + if file && line && File.exist?(file) + Source.new(file: file, first_line: line, last_line: find_end(file, line)) + end + end + + private + + def find_end(file, first_line) + lex = RubyLex.new(@irb_context) + lines = File.read(file).lines[(first_line - 1)..-1] + tokens = RubyLex.ripper_lex_without_warning(lines.join) + prev_tokens = [] + + # chunk with line number + tokens.chunk { |tok| tok.pos[0] }.each do |lnum, chunk| + code = lines[0..lnum].join + prev_tokens.concat chunk + continue = lex.should_continue?(prev_tokens) + syntax = lex.check_code_syntax(code) + if !continue && syntax == :valid + return first_line + lnum + end + end + first_line + end + end +end