Skip to content

Commit

Permalink
Pass statements to Context#evaluate (#920)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
st0012 authored Apr 12, 2024
1 parent eb442c4 commit b32aee4
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 56 deletions.
16 changes: 1 addition & 15 deletions lib/irb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1028,21 +1028,7 @@ def eval_input
return statement.code
end

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

if @context.echo? && !statement.suppresses_echo?
if statement.is_assignment?
Expand Down
25 changes: 19 additions & 6 deletions lib/irb/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -585,31 +585,44 @@ def inspect_mode=(opt)
@inspect_mode
end

def evaluate(line, line_no) # :nodoc:
def evaluate(statement, line_no) # :nodoc:
@line_no = line_no
result = nil

case statement
when Statement::EmptyInput
return
when Statement::Expression
result = evaluate_expression(statement.code, line_no)
when Statement::Command
result = statement.command_class.execute(self, statement.arg)
end

set_last_value(result)
end

def evaluate_expression(code, line_no) # :nodoc:
result = nil
if IRB.conf[:MEASURE] && IRB.conf[:MEASURE_CALLBACKS].empty?
IRB.set_measure_callback
end

if IRB.conf[:MEASURE] && !IRB.conf[:MEASURE_CALLBACKS].empty?
last_proc = proc do
result = workspace.evaluate(line, @eval_path, line_no)
result = workspace.evaluate(code, @eval_path, line_no)
end
IRB.conf[:MEASURE_CALLBACKS].inject(last_proc) do |chain, item|
_name, callback, arg = item
proc do
callback.(self, line, line_no, arg) do
callback.(self, code, line_no, arg) do
chain.call
end
end
end.call
else
result = workspace.evaluate(line, @eval_path, line_no)
result = workspace.evaluate(code, @eval_path, line_no)
end

set_last_value(result)
result
end

def inspect_last_value # :nodoc:
Expand Down
41 changes: 6 additions & 35 deletions test/irb/test_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,35 +28,6 @@ def teardown
restore_encodings
end

def test_last_value
assert_nil(@context.last_value)
assert_nil(@context.evaluate('_', 1))
obj = Object.new
@context.set_last_value(obj)
assert_same(obj, @context.last_value)
assert_same(obj, @context.evaluate('_', 1))
end

def test_evaluate_with_encoding_error_without_lineno
if RUBY_ENGINE == 'truffleruby'
omit "Remove me after https://github.com/ruby/prism/issues/2129 is addressed and adopted in TruffleRuby"
end

if RUBY_VERSION >= "3.4."
omit "Now raises SyntaxError"
end

assert_raise_with_message(EncodingError, /invalid symbol/) {
@context.evaluate(%q[:"\xAE"], 1)
# The backtrace of this invalid encoding hash doesn't contain lineno.
}
end

def test_evaluate_still_emits_warning
assert_warning("(irb):1: warning: END in method; use at_exit\n") do
@context.evaluate(%q[def foo; END {}; end], 1)
end
end

def test_eval_input
verbose, $VERBOSE = $VERBOSE, nil
Expand Down Expand Up @@ -382,7 +353,7 @@ def test_omit_multiline_on_assignment
end
assert_empty err
assert_equal("=> \n#{value}\n", out)
irb.context.evaluate('A.remove_method(:inspect)', 0)
irb.context.evaluate_expression('A.remove_method(:inspect)', 0)

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

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

input.reset
irb.context.echo = false
Expand All @@ -412,7 +383,7 @@ def test_omit_multiline_on_assignment
end
assert_empty err
assert_equal("", out)
irb.context.evaluate('A.remove_method(:inspect)', 0)
irb.context.evaluate_expression('A.remove_method(:inspect)', 0)

input.reset
irb.context.echo = false
Expand All @@ -422,7 +393,7 @@ def test_omit_multiline_on_assignment
end
assert_empty err
assert_equal("", out)
irb.context.evaluate('A.remove_method(:inspect)', 0)
irb.context.evaluate_expression('A.remove_method(:inspect)', 0)

input.reset
irb.context.echo = false
Expand All @@ -432,7 +403,7 @@ def test_omit_multiline_on_assignment
end
assert_empty err
assert_equal("", out)
irb.context.evaluate('A.remove_method(:inspect)', 0)
irb.context.evaluate_expression('A.remove_method(:inspect)', 0)
end
end

Expand Down
50 changes: 50 additions & 0 deletions test/irb/test_irb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,56 @@ class Foo
assert_include output, "From: #{@ruby_file.path}:1"
end

def test_underscore_stores_last_result
write_ruby <<~'RUBY'
binding.irb
RUBY

output = run_ruby_file do
type "1 + 1"
type "_ + 10"
type "exit!"
end

assert_include output, "=> 12"
end

def test_evaluate_with_encoding_error_without_lineno
if RUBY_ENGINE == 'truffleruby'
omit "Remove me after https://github.com/ruby/prism/issues/2129 is addressed and adopted in TruffleRuby"
end

if RUBY_VERSION >= "3.4."
omit "Now raises SyntaxError"
end

write_ruby <<~'RUBY'
binding.irb
RUBY

output = run_ruby_file do
type %q[:"\xAE"]
type "exit!"
end

assert_include output, 'invalid symbol in encoding UTF-8 :"\xAE"'
# EncodingError would be wrapped with ANSI escape sequences, so we assert it separately
assert_include output, "EncodingError"
end

def test_evaluate_still_emits_warning
write_ruby <<~'RUBY'
binding.irb
RUBY

output = run_ruby_file do
type %q[def foo; END {}; end]
type "exit!"
end

assert_include output, '(irb):1: warning: END in method; use at_exit'
end

def test_symbol_aliases_dont_affect_ruby_syntax
write_ruby <<~'RUBY'
$foo = "It's a foo"
Expand Down

0 comments on commit b32aee4

Please sign in to comment.