[ruby/irb] Fix SourceFinder's constant evaluation issue

(https://github.com/ruby/irb/pull/869)

Currently, if the signature's constant part is not defined, a NameError
would be raised.

```
irb(main):001> show_source Foo
(eval):1:in `<top (required)>': uninitialized constant Foo (NameError)

Foo
^^^
        from (irb):1:in `<main>'
```

This commit fixes the issue and simplifies the `edit` command's implementation.

https://github.com/ruby/irb/commit/8c16e029d1
This commit is contained in:
Stan Lo 2024-02-13 13:36:29 +00:00 коммит произвёл git
Родитель ec26786b1a
Коммит 2f0f95235a
3 изменённых файлов: 29 добавлений и 12 удалений

Просмотреть файл

@ -27,13 +27,7 @@ module IRB
if path.nil?
path = @irb_context.irb_path
elsif !File.exist?(path)
source =
begin
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
source = SourceFinder.new(@irb_context).find_source(path)
if source&.file_exist? && !source.binary_file?
path = source.file

Просмотреть файл

@ -4,6 +4,8 @@ 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)
@ -66,20 +68,19 @@ module IRB
end
def find_source(signature, super_level = 0)
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 }
eval_receiver_or_owner(signature) # trigger autoload
base = @irb_context.workspace.binding.receiver.yield_self { |r| r.is_a?(Module) ? r : Object }
file, line = base.const_source_location(signature)
when /\A(?<owner>[A-Z]\w*(::[A-Z]\w*)*)#(?<method>[^ :.]+)\z/ # Class#method
owner = eval(Regexp.last_match[:owner], context_binding)
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(Regexp.last_match[:receiver] || 'self', context_binding)
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")
@ -94,6 +95,8 @@ module IRB
source = RubyVM::AbstractSyntaxTree.of(method)&.source rescue nil
Source.new(file, line, source)
end
rescue EvaluationError
nil
end
private
@ -112,5 +115,12 @@ module IRB
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
end
end

Просмотреть файл

@ -52,6 +52,19 @@ module TestIRB
assert_match(%r[Couldn't locate a definition for foo], out)
end
def test_show_source_with_missing_constant
write_ruby <<~'RUBY'
binding.irb
RUBY
out = run_ruby_file do
type "show_source Foo"
type "exit"
end
assert_match(%r[Couldn't locate a definition for Foo], out)
end
def test_show_source_string
write_ruby <<~'RUBY'
binding.irb