1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 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
|