Skip to content

Commit

Permalink
Don't use delegator to install helper methods to main object (#1031)
Browse files Browse the repository at this point in the history
IRB used delegator to install command as a method of frozen main object.
Command is not a method now. We can drop it.
  • Loading branch information
tompng authored Nov 19, 2024
1 parent 68f5cf0 commit 2f1c593
Show file tree
Hide file tree
Showing 6 changed files with 42 additions and 30 deletions.
9 changes: 7 additions & 2 deletions lib/irb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1463,16 +1463,21 @@ def truncate_prompt_main(str) # :nodoc:
end
end

def basic_object_safe_main_call(method)
main = @context.main
Object === main ? main.__send__(method) : Object.instance_method(method).bind_call(main)
end

def format_prompt(format, ltype, indent, line_no) # :nodoc:
format.gsub(/%([0-9]+)?([a-zA-Z%])/) do
case $2
when "N"
@context.irb_name
when "m"
main_str = @context.main.to_s rescue "!#{$!.class}"
main_str = basic_object_safe_main_call(:to_s) rescue "!#{$!.class}"
truncate_prompt_main(main_str)
when "M"
main_str = @context.main.inspect rescue "!#{$!.class}"
main_str = basic_object_safe_main_call(:inspect) rescue "!#{$!.class}"
truncate_prompt_main(main_str)
when "l"
ltype
Expand Down
2 changes: 1 addition & 1 deletion lib/irb/command/internal_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def ruby_args(arg)
# Use throw and catch to handle arg that includes `;`
# For example: "1, kw: (2; 3); 4" will be parsed to [[1], { kw: 3 }]
catch(:EXTRACT_RUBY_ARGS) do
@irb_context.workspace.binding.eval "IRB::Command.extract_ruby_args #{arg}"
@irb_context.workspace.binding.eval "::IRB::Command.extract_ruby_args #{arg}"
end || [[], {}]
end
end
Expand Down
3 changes: 2 additions & 1 deletion lib/irb/completion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ def eval_global_variables
end

def eval_class_constants
::Module.instance_method(:constants).bind(eval("self.class")).call
klass = ::Object.instance_method(:class).bind_call(receiver)
::Module.instance_method(:constants).bind_call(klass)
end
end
}
Expand Down
32 changes: 6 additions & 26 deletions lib/irb/workspace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
# by Keiju ISHITSUKA(keiju@ruby-lang.org)
#

require "delegate"

require_relative "helper_method"

IRB::TOPLEVEL_BINDING = binding
Expand All @@ -16,7 +14,7 @@ class WorkSpace
# set self to main if specified, otherwise
# inherit main from TOPLEVEL_BINDING.
def initialize(*main)
if main[0].kind_of?(Binding)
if Binding === main[0]
@binding = main.shift
elsif IRB.conf[:SINGLE_IRB]
@binding = TOPLEVEL_BINDING
Expand Down Expand Up @@ -70,37 +68,16 @@ def initialize(*main)
unless main.empty?
case @main
when Module
@binding = eval("IRB.conf[:__MAIN__].module_eval('binding', __FILE__, __LINE__)", @binding, __FILE__, __LINE__)
@binding = eval("::IRB.conf[:__MAIN__].module_eval('::Kernel.binding', __FILE__, __LINE__)", @binding, __FILE__, __LINE__)
else
begin
@binding = eval("IRB.conf[:__MAIN__].instance_eval('binding', __FILE__, __LINE__)", @binding, __FILE__, __LINE__)
@binding = eval("::IRB.conf[:__MAIN__].instance_eval('::Kernel.binding', __FILE__, __LINE__)", @binding, __FILE__, __LINE__)
rescue TypeError
fail CantChangeBinding, @main.inspect
end
end
end

case @main
when Object
use_delegator = @main.frozen?
else
use_delegator = true
end

if use_delegator
@main = SimpleDelegator.new(@main)
IRB.conf[:__MAIN__] = @main
@main.singleton_class.class_eval do
private
define_method(:binding, Kernel.instance_method(:binding))
define_method(:local_variables, Kernel.instance_method(:local_variables))
# Define empty method to avoid delegator warning, will be overridden.
define_method(:exit) {|*a, &b| }
define_method(:exit!) {|*a, &b| }
end
@binding = eval("IRB.conf[:__MAIN__].instance_eval('binding', __FILE__, __LINE__)", @binding, *@binding.source_location)
end

@binding.local_variable_set(:_, nil)
end

Expand All @@ -111,6 +88,9 @@ def initialize(*main)
attr_reader :main

def load_helper_methods_to_main
# Do not load helper methods to frozen objects and BasicObject
return unless Object === @main && !@main.frozen?

ancestors = class<<main;ancestors;end
main.extend ExtendCommandBundle if !ancestors.include?(ExtendCommandBundle)
main.extend HelpersContainer if !ancestors.include?(HelpersContainer)
Expand Down
19 changes: 19 additions & 0 deletions test/irb/command/test_cd.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ def foo
end
end
class BO < BasicObject
def baz
"this is baz"
end
end
binding.irb
RUBY
end
Expand All @@ -40,6 +46,19 @@ def test_cd
assert_match(/irb\(Foo\):006>/, out)
end

def test_cd_basic_object_or_frozen
out = run_ruby_file do
type "cd BO.new"
type "cd 1"
type "cd Object.new.freeze"
type "exit"
end

assert_match(/irb\(#<BO:.+\):002>/, out)
assert_match(/irb\(1\):003>/, out)
assert_match(/irb\(#<Object:.+\):004>/, out)
end

def test_cd_moves_top_level_with_no_args
out = run_ruby_file do
type "cd Foo"
Expand Down
7 changes: 7 additions & 0 deletions test/irb/test_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,13 @@ def main.inspect; to_s.inspect; end
assert_equal('irb("aaaaaaaaaaaaaaaaaaaaaaaaaaaa...)>', irb.send(:format_prompt, 'irb(%M)>', nil, 1, 1))
end

def test_prompt_main_basic_object
main = BasicObject.new
irb = IRB::Irb.new(IRB::WorkSpace.new(main), TestInputMethod.new)
assert_match(/irb\(#<BasicObject:.+\)/, irb.send(:format_prompt, 'irb(%m)>', nil, 1, 1))
assert_match(/irb\(#<BasicObject:.+\)/, irb.send(:format_prompt, 'irb(%M)>', nil, 1, 1))
end

def test_prompt_main_raise
main = Object.new
def main.to_s; raise TypeError; end
Expand Down

0 comments on commit 2f1c593

Please sign in to comment.