Skip to content

Commit 5a40f7d

Browse files
st0012matzbot
authored andcommitted
[ruby/irb] Encapsulate input details in Statement objects
(ruby/irb#682) * Introduce Statement class * Split Statement class for better clarity ruby/irb@65e8e68690
1 parent 0982c5f commit 5a40f7d

File tree

3 files changed

+104
-41
lines changed

3 files changed

+104
-41
lines changed

lib/irb.rb

Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -570,26 +570,19 @@ def eval_input
570570

571571
configure_io
572572

573-
@scanner.each_top_level_statement do |line, line_no, is_assignment|
573+
@scanner.each_top_level_statement do |statement, line_no|
574574
signal_status(:IN_EVAL) do
575575
begin
576576
# If the integration with debugger is activated, we need to handle certain input differently
577-
if @context.with_debugger
578-
command_class = load_command_class(line)
579-
# First, let's pass debugging command's input to debugger
580-
# Secondly, we need to let debugger evaluate non-command input
581-
# Otherwise, the expression will be evaluated in the debugger's main session thread
582-
# This is the only way to run the user's program in the expected thread
583-
if !command_class || ExtendCommand::DebugCommand > command_class
584-
return line
585-
end
577+
if @context.with_debugger && statement.should_be_handled_by_debugger?
578+
return statement.code
586579
end
587580

588-
evaluate_line(line, line_no)
581+
@context.evaluate(statement.evaluable_code, line_no)
589582

590583
# Don't echo if the line ends with a semicolon
591-
if @context.echo? && !line.match?(/;\s*\z/)
592-
if is_assignment
584+
if @context.echo? && !statement.suppresses_echo?
585+
if statement.is_assignment?
593586
if @context.echo_on_assignment?
594587
output_value(@context.echo_on_assignment? == :truncate)
595588
end
@@ -659,29 +652,6 @@ def configure_io
659652
end
660653
end
661654

662-
def evaluate_line(line, line_no)
663-
# Transform a non-identifier alias (@, $) or keywords (next, break)
664-
command, args = line.split(/\s/, 2)
665-
if original = @context.command_aliases[command.to_sym]
666-
line = line.gsub(/\A#{Regexp.escape(command)}/, original.to_s)
667-
command = original
668-
end
669-
670-
# Hook command-specific transformation
671-
command_class = ExtendCommandBundle.load_command(command)
672-
if command_class&.respond_to?(:transform_args)
673-
line = "#{command} #{command_class.transform_args(args)}"
674-
end
675-
676-
@context.evaluate(line, line_no)
677-
end
678-
679-
def load_command_class(line)
680-
command, _ = line.split(/\s/, 2)
681-
command_name = @context.command_aliases[command.to_sym]
682-
ExtendCommandBundle.load_command(command_name || command)
683-
end
684-
685655
def convert_invalid_byte_sequence(str, enc)
686656
str.force_encoding(enc)
687657
str.scrub { |c|

lib/irb/ruby-lex.rb

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
require "ripper"
88
require "jruby" if RUBY_ENGINE == "jruby"
99
require_relative "nesting_parser"
10+
require_relative "statement"
1011

1112
# :stopdoc:
1213
class RubyLex
@@ -221,16 +222,30 @@ def each_top_level_statement
221222
break unless code
222223

223224
if code != "\n"
224-
code.force_encoding(@context.io.encoding)
225-
yield code, @line_no, assignment_expression?(code)
225+
yield build_statement(code), @line_no
226226
end
227227
increase_line_no(code.count("\n"))
228228
rescue TerminateLineInput
229229
end
230230
end
231231

232-
def assignment_expression?(line)
233-
# Try to parse the line and check if the last of possibly multiple
232+
def build_statement(code)
233+
code.force_encoding(@context.io.encoding)
234+
command_or_alias, arg = code.split(/\s/, 2)
235+
# Transform a non-identifier alias (@, $) or keywords (next, break)
236+
command_name = @context.command_aliases[command_or_alias.to_sym]
237+
command = command_name || command_or_alias
238+
command_class = IRB::ExtendCommandBundle.load_command(command)
239+
240+
if command_class
241+
IRB::Statement::Command.new(code, command, arg, command_class)
242+
else
243+
IRB::Statement::Expression.new(code, assignment_expression?(code))
244+
end
245+
end
246+
247+
def assignment_expression?(code)
248+
# Try to parse the code and check if the last of possibly multiple
234249
# expressions is an assignment type.
235250

236251
# If the expression is invalid, Ripper.sexp should return nil which will
@@ -239,7 +254,7 @@ def assignment_expression?(line)
239254
# array of parsed expressions. The first element of each expression is the
240255
# expression's type.
241256
verbose, $VERBOSE = $VERBOSE, nil
242-
code = "#{RubyLex.generate_local_variables_assign_code(@context.local_variables) || 'nil;'}\n#{line}"
257+
code = "#{RubyLex.generate_local_variables_assign_code(@context.local_variables) || 'nil;'}\n#{code}"
243258
# Get the last node_type of the line. drop(1) is to ignore the local_variables_assign_code part.
244259
node_type = Ripper.sexp(code)&.dig(1)&.drop(1)&.dig(-1, 0)
245260
ASSIGNMENT_NODE_TYPES.include?(node_type)

lib/irb/statement.rb

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# frozen_string_literal: true
2+
3+
module IRB
4+
class Statement
5+
attr_reader :code
6+
7+
def is_assignment?
8+
raise NotImplementedError
9+
end
10+
11+
def suppresses_echo?
12+
raise NotImplementedError
13+
end
14+
15+
def should_be_handled_by_debugger?
16+
raise NotImplementedError
17+
end
18+
19+
def evaluable_code
20+
raise NotImplementedError
21+
end
22+
23+
class Expression < Statement
24+
def initialize(code, is_assignment)
25+
@code = code
26+
@is_assignment = is_assignment
27+
end
28+
29+
def suppresses_echo?
30+
@code.match?(/;\s*\z/)
31+
end
32+
33+
def should_be_handled_by_debugger?
34+
true
35+
end
36+
37+
def is_assignment?
38+
@is_assignment
39+
end
40+
41+
def evaluable_code
42+
@code
43+
end
44+
end
45+
46+
class Command < Statement
47+
def initialize(code, command, arg, command_class)
48+
@code = code
49+
@command = command
50+
@arg = arg
51+
@command_class = command_class
52+
end
53+
54+
def is_assignment?
55+
false
56+
end
57+
58+
def suppresses_echo?
59+
false
60+
end
61+
62+
def should_be_handled_by_debugger?
63+
IRB::ExtendCommand::DebugCommand > @command_class
64+
end
65+
66+
def evaluable_code
67+
# Hook command-specific transformation to return valid Ruby code
68+
if @command_class.respond_to?(:transform_args)
69+
arg = @command_class.transform_args(@arg)
70+
else
71+
arg = @arg
72+
end
73+
74+
[@command, arg].compact.join(' ')
75+
end
76+
end
77+
end
78+
end

0 commit comments

Comments
 (0)