From 0f8ff65c4e0a9f07821aff4849787677b4724e54 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Mon, 12 Feb 2024 21:09:24 +0000 Subject: [PATCH] Fix SourceFinder's constant evaluation issue Currently, if the signature's constant part is not defined, a NameError would be raised. ``` irb(main):001> show_source Foo (eval):1:in `': uninitialized constant Foo (NameError) Foo ^^^ from (irb):1:in `
' ``` This commit fixes the issue and simplifies the `edit` command's implementation. --- lib/irb/cmd/edit.rb | 8 +------- lib/irb/source_finder.rb | 20 +++++++++++++++----- test/irb/cmd/test_show_source.rb | 13 +++++++++++++ 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/lib/irb/cmd/edit.rb b/lib/irb/cmd/edit.rb index 2f89f83ec..dae65f3c3 100644 --- a/lib/irb/cmd/edit.rb +++ b/lib/irb/cmd/edit.rb @@ -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 diff --git a/lib/irb/source_finder.rb b/lib/irb/source_finder.rb index 26aae7643..c3a64b39e 100644 --- a/lib/irb/source_finder.rb +++ b/lib/irb/source_finder.rb @@ -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) @@ -63,20 +65,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(?[A-Z]\w*(::[A-Z]\w*)*)#(?[^ :.]+)\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((?.+)(\.|::))?(?[^ :.]+)\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") @@ -91,6 +92,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 @@ -109,5 +112,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 diff --git a/test/irb/cmd/test_show_source.rb b/test/irb/cmd/test_show_source.rb index 062ab327d..2b1c203da 100644 --- a/test/irb/cmd/test_show_source.rb +++ b/test/irb/cmd/test_show_source.rb @@ -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