Skip to content

Commit b32aee4

Browse files
authored
Pass statements to Context#evaluate (#920)
This has a few benefits: - We can keep hiding the evaluation logic inside the Context level, which has always been the convention until #824 was merged recently. - Although not an official API, gems like `debug` and `mission_control-jobs` patch `Context#evaluate` to wrap their own logic around it. This implicit contract was broken after #824, and this change restores it. In addition to the refactor, I also converted some context-level evaluation tests into integration tests, which are more robust and easier to maintain.
1 parent eb442c4 commit b32aee4

File tree

4 files changed

+76
-56
lines changed

4 files changed

+76
-56
lines changed

lib/irb.rb

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,21 +1028,7 @@ def eval_input
10281028
return statement.code
10291029
end
10301030

1031-
case statement
1032-
when Statement::EmptyInput
1033-
# Do nothing
1034-
when Statement::Expression
1035-
@context.evaluate(statement.code, line_no)
1036-
when Statement::Command
1037-
ret = statement.command_class.execute(@context, statement.arg)
1038-
# TODO: Remove this output once we have a better way to handle it
1039-
# This is to notify `debug`'s test framework that the current input has been processed
1040-
# We also need to have a way to restart/stop threads around command execution
1041-
# when being used as `debug`'s console.
1042-
# https://github.com/ruby/debug/blob/master/lib/debug/irb_integration.rb#L8-L13
1043-
puts "INTERNAL_INFO: {}" if @context.with_debugger && ENV['RUBY_DEBUG_TEST_UI'] == 'terminal'
1044-
@context.set_last_value(ret)
1045-
end
1031+
@context.evaluate(statement, line_no)
10461032

10471033
if @context.echo? && !statement.suppresses_echo?
10481034
if statement.is_assignment?

lib/irb/context.rb

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -585,31 +585,44 @@ def inspect_mode=(opt)
585585
@inspect_mode
586586
end
587587

588-
def evaluate(line, line_no) # :nodoc:
588+
def evaluate(statement, line_no) # :nodoc:
589589
@line_no = line_no
590590
result = nil
591591

592+
case statement
593+
when Statement::EmptyInput
594+
return
595+
when Statement::Expression
596+
result = evaluate_expression(statement.code, line_no)
597+
when Statement::Command
598+
result = statement.command_class.execute(self, statement.arg)
599+
end
600+
601+
set_last_value(result)
602+
end
603+
604+
def evaluate_expression(code, line_no) # :nodoc:
605+
result = nil
592606
if IRB.conf[:MEASURE] && IRB.conf[:MEASURE_CALLBACKS].empty?
593607
IRB.set_measure_callback
594608
end
595609

596610
if IRB.conf[:MEASURE] && !IRB.conf[:MEASURE_CALLBACKS].empty?
597611
last_proc = proc do
598-
result = workspace.evaluate(line, @eval_path, line_no)
612+
result = workspace.evaluate(code, @eval_path, line_no)
599613
end
600614
IRB.conf[:MEASURE_CALLBACKS].inject(last_proc) do |chain, item|
601615
_name, callback, arg = item
602616
proc do
603-
callback.(self, line, line_no, arg) do
617+
callback.(self, code, line_no, arg) do
604618
chain.call
605619
end
606620
end
607621
end.call
608622
else
609-
result = workspace.evaluate(line, @eval_path, line_no)
623+
result = workspace.evaluate(code, @eval_path, line_no)
610624
end
611-
612-
set_last_value(result)
625+
result
613626
end
614627

615628
def inspect_last_value # :nodoc:

test/irb/test_context.rb

Lines changed: 6 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -28,35 +28,6 @@ def teardown
2828
restore_encodings
2929
end
3030

31-
def test_last_value
32-
assert_nil(@context.last_value)
33-
assert_nil(@context.evaluate('_', 1))
34-
obj = Object.new
35-
@context.set_last_value(obj)
36-
assert_same(obj, @context.last_value)
37-
assert_same(obj, @context.evaluate('_', 1))
38-
end
39-
40-
def test_evaluate_with_encoding_error_without_lineno
41-
if RUBY_ENGINE == 'truffleruby'
42-
omit "Remove me after https://github.com/ruby/prism/issues/2129 is addressed and adopted in TruffleRuby"
43-
end
44-
45-
if RUBY_VERSION >= "3.4."
46-
omit "Now raises SyntaxError"
47-
end
48-
49-
assert_raise_with_message(EncodingError, /invalid symbol/) {
50-
@context.evaluate(%q[:"\xAE"], 1)
51-
# The backtrace of this invalid encoding hash doesn't contain lineno.
52-
}
53-
end
54-
55-
def test_evaluate_still_emits_warning
56-
assert_warning("(irb):1: warning: END in method; use at_exit\n") do
57-
@context.evaluate(%q[def foo; END {}; end], 1)
58-
end
59-
end
6031

6132
def test_eval_input
6233
verbose, $VERBOSE = $VERBOSE, nil
@@ -382,7 +353,7 @@ def test_omit_multiline_on_assignment
382353
end
383354
assert_empty err
384355
assert_equal("=> \n#{value}\n", out)
385-
irb.context.evaluate('A.remove_method(:inspect)', 0)
356+
irb.context.evaluate_expression('A.remove_method(:inspect)', 0)
386357

387358
input.reset
388359
irb.context.echo = true
@@ -392,7 +363,7 @@ def test_omit_multiline_on_assignment
392363
end
393364
assert_empty err
394365
assert_equal("=> #{value_first_line[0..(input.winsize.last - 9)]}...\n=> \n#{value}\n", out)
395-
irb.context.evaluate('A.remove_method(:inspect)', 0)
366+
irb.context.evaluate_expression('A.remove_method(:inspect)', 0)
396367

397368
input.reset
398369
irb.context.echo = true
@@ -402,7 +373,7 @@ def test_omit_multiline_on_assignment
402373
end
403374
assert_empty err
404375
assert_equal("=> \n#{value}\n=> \n#{value}\n", out)
405-
irb.context.evaluate('A.remove_method(:inspect)', 0)
376+
irb.context.evaluate_expression('A.remove_method(:inspect)', 0)
406377

407378
input.reset
408379
irb.context.echo = false
@@ -412,7 +383,7 @@ def test_omit_multiline_on_assignment
412383
end
413384
assert_empty err
414385
assert_equal("", out)
415-
irb.context.evaluate('A.remove_method(:inspect)', 0)
386+
irb.context.evaluate_expression('A.remove_method(:inspect)', 0)
416387

417388
input.reset
418389
irb.context.echo = false
@@ -422,7 +393,7 @@ def test_omit_multiline_on_assignment
422393
end
423394
assert_empty err
424395
assert_equal("", out)
425-
irb.context.evaluate('A.remove_method(:inspect)', 0)
396+
irb.context.evaluate_expression('A.remove_method(:inspect)', 0)
426397

427398
input.reset
428399
irb.context.echo = false
@@ -432,7 +403,7 @@ def test_omit_multiline_on_assignment
432403
end
433404
assert_empty err
434405
assert_equal("", out)
435-
irb.context.evaluate('A.remove_method(:inspect)', 0)
406+
irb.context.evaluate_expression('A.remove_method(:inspect)', 0)
436407
end
437408
end
438409

test/irb/test_irb.rb

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,56 @@ class Foo
4242
assert_include output, "From: #{@ruby_file.path}:1"
4343
end
4444

45+
def test_underscore_stores_last_result
46+
write_ruby <<~'RUBY'
47+
binding.irb
48+
RUBY
49+
50+
output = run_ruby_file do
51+
type "1 + 1"
52+
type "_ + 10"
53+
type "exit!"
54+
end
55+
56+
assert_include output, "=> 12"
57+
end
58+
59+
def test_evaluate_with_encoding_error_without_lineno
60+
if RUBY_ENGINE == 'truffleruby'
61+
omit "Remove me after https://github.com/ruby/prism/issues/2129 is addressed and adopted in TruffleRuby"
62+
end
63+
64+
if RUBY_VERSION >= "3.4."
65+
omit "Now raises SyntaxError"
66+
end
67+
68+
write_ruby <<~'RUBY'
69+
binding.irb
70+
RUBY
71+
72+
output = run_ruby_file do
73+
type %q[:"\xAE"]
74+
type "exit!"
75+
end
76+
77+
assert_include output, 'invalid symbol in encoding UTF-8 :"\xAE"'
78+
# EncodingError would be wrapped with ANSI escape sequences, so we assert it separately
79+
assert_include output, "EncodingError"
80+
end
81+
82+
def test_evaluate_still_emits_warning
83+
write_ruby <<~'RUBY'
84+
binding.irb
85+
RUBY
86+
87+
output = run_ruby_file do
88+
type %q[def foo; END {}; end]
89+
type "exit!"
90+
end
91+
92+
assert_include output, '(irb):1: warning: END in method; use at_exit'
93+
end
94+
4595
def test_symbol_aliases_dont_affect_ruby_syntax
4696
write_ruby <<~'RUBY'
4797
$foo = "It's a foo"

0 commit comments

Comments
 (0)