Skip to content

Commit

Permalink
Fix SourceFinder's constant evaluation issue (#869)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
st0012 authored Feb 13, 2024
1 parent c63e4c4 commit 8c16e02
Show file tree
Hide file tree
Showing 3 changed files with 29 additions and 12 deletions.
8 changes: 1 addition & 7 deletions lib/irb/cmd/edit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,7 @@ def execute(*args)
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
Expand Down
20 changes: 15 additions & 5 deletions lib/irb/source_finder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

module IRB
class SourceFinder
class EvaluationError < StandardError; end

class Source
attr_reader :file, :line
def initialize(file, line, ast_source = nil)
Expand Down Expand Up @@ -66,20 +68,19 @@ def initialize(irb_context)
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")
Expand All @@ -94,6 +95,8 @@ def find_source(signature, super_level = 0)
source = RubyVM::AbstractSyntaxTree.of(method)&.source rescue nil
Source.new(file, line, source)
end
rescue EvaluationError
nil
end

private
Expand All @@ -112,5 +115,12 @@ def method_target(owner_receiver, super_level, method, type)
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
13 changes: 13 additions & 0 deletions test/irb/cmd/test_show_source.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,19 @@ def test_show_source_with_missing_signature
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
Expand Down

0 comments on commit 8c16e02

Please sign in to comment.