summaryrefslogtreecommitdiff
path: root/lib/irb/source_finder.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/irb/source_finder.rb')
-rw-r--r--lib/irb/source_finder.rb139
1 files changed, 139 insertions, 0 deletions
diff --git a/lib/irb/source_finder.rb b/lib/irb/source_finder.rb
new file mode 100644
index 0000000000..5d7d729d19
--- /dev/null
+++ b/lib/irb/source_finder.rb
@@ -0,0 +1,139 @@
+# frozen_string_literal: true
+
+require_relative "ruby-lex"
+
+module IRB
+ class SourceFinder
+ class EvaluationError < StandardError; end
+
+ class Source
+ attr_reader :file, :line
+ def initialize(file, line, ast_source = nil)
+ @file = file
+ @line = line
+ @ast_source = ast_source
+ end
+
+ def file_exist?
+ File.exist?(@file)
+ end
+
+ def binary_file?
+ # If the line is zero, it means that the target's source is probably in a binary file.
+ @line.zero?
+ end
+
+ def file_content
+ @file_content ||= File.read(@file)
+ end
+
+ def colorized_content
+ if !binary_file? && file_exist?
+ end_line = find_end
+ # To correctly colorize, we need to colorize full content and extract the relevant lines.
+ colored = IRB::Color.colorize_code(file_content)
+ colored.lines[@line - 1...end_line].join
+ elsif @ast_source
+ IRB::Color.colorize_code(@ast_source)
+ end
+ end
+
+ private
+
+ def find_end
+ lex = RubyLex.new
+ code = file_content
+ lines = code.lines[(@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, local_variables: [])
+ if !continue && syntax == :valid
+ return @line + lnum
+ end
+ end
+ @line
+ end
+ end
+
+ private_constant :Source
+
+ def initialize(irb_context)
+ @irb_context = irb_context
+ end
+
+ def find_source(signature, super_level = 0)
+ case signature
+ when /\A(::)?[A-Z]\w*(::[A-Z]\w*)*\z/ # ConstName, ::ConstName, ConstPath::ConstName
+ eval_receiver_or_owner(signature) # trigger autoload
+ *parts, name = signature.split('::', -1)
+ base =
+ if parts.empty? # ConstName
+ find_const_owner(name)
+ elsif parts == [''] # ::ConstName
+ Object
+ else # ConstPath::ConstName
+ eval_receiver_or_owner(parts.join('::'))
+ end
+ file, line = base.const_source_location(name)
+ when /\A(?<owner>[A-Z]\w*(::[A-Z]\w*)*)#(?<method>[^ :.]+)\z/ # Class#method
+ owner = eval_receiver_or_owner(Regexp.last_match[:owner])
+ method = Regexp.last_match[:method]
+ return unless owner.respond_to?(:instance_method)
+ method = method_target(owner, super_level, method, "owner")
+ file, line = method&.source_location
+ when /\A((?<receiver>.+)(\.|::))?(?<method>[^ :.]+)\z/ # method, receiver.method, receiver::method
+ receiver = eval_receiver_or_owner(Regexp.last_match[:receiver] || 'self')
+ method = Regexp.last_match[:method]
+ return unless receiver.respond_to?(method, true)
+ method = method_target(receiver, super_level, method, "receiver")
+ file, line = method&.source_location
+ end
+ return unless file && line
+
+ if File.exist?(file)
+ Source.new(file, line)
+ elsif method
+ # Method defined with eval, probably in IRB session
+ source = RubyVM::AbstractSyntaxTree.of(method)&.source rescue nil
+ Source.new(file, line, source)
+ end
+ rescue EvaluationError
+ nil
+ end
+
+ private
+
+ def method_target(owner_receiver, super_level, method, type)
+ case type
+ when "owner"
+ target_method = owner_receiver.instance_method(method)
+ when "receiver"
+ target_method = owner_receiver.method(method)
+ end
+ super_level.times do |s|
+ target_method = target_method.super_method if target_method
+ end
+ target_method
+ rescue NameError
+ nil
+ end
+
+ def eval_receiver_or_owner(code)
+ context_binding = @irb_context.workspace.binding
+ eval(code, context_binding)
+ rescue NameError
+ raise EvaluationError
+ end
+
+ def find_const_owner(name)
+ module_nesting = @irb_context.workspace.binding.eval('::Module.nesting')
+ module_nesting.find { |mod| mod.const_defined?(name, false) } || module_nesting.find { |mod| mod.const_defined?(name) } || Object
+ end
+ end
+end