Skip to content

Commit

Permalink
[ruby/irb] Encapsulate input details in Statement objects
Browse files Browse the repository at this point in the history
(ruby/irb#682)

* Introduce Statement class

* Split Statement class for better clarity

ruby/irb@65e8e68690
  • Loading branch information
st0012 authored and matzbot committed Aug 16, 2023
1 parent 0982c5f commit 5a40f7d
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 41 deletions.
42 changes: 6 additions & 36 deletions lib/irb.rb
Expand Up @@ -570,26 +570,19 @@ def eval_input

configure_io

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

evaluate_line(line, line_no)
@context.evaluate(statement.evaluable_code, line_no)

# Don't echo if the line ends with a semicolon
if @context.echo? && !line.match?(/;\s*\z/)
if is_assignment
if @context.echo? && !statement.suppresses_echo?
if statement.is_assignment?
if @context.echo_on_assignment?
output_value(@context.echo_on_assignment? == :truncate)
end
Expand Down Expand Up @@ -659,29 +652,6 @@ def configure_io
end
end

def evaluate_line(line, line_no)
# Transform a non-identifier alias (@, $) or keywords (next, break)
command, args = line.split(/\s/, 2)
if original = @context.command_aliases[command.to_sym]
line = line.gsub(/\A#{Regexp.escape(command)}/, original.to_s)
command = original
end

# Hook command-specific transformation
command_class = ExtendCommandBundle.load_command(command)
if command_class&.respond_to?(:transform_args)
line = "#{command} #{command_class.transform_args(args)}"
end

@context.evaluate(line, line_no)
end

def load_command_class(line)
command, _ = line.split(/\s/, 2)
command_name = @context.command_aliases[command.to_sym]
ExtendCommandBundle.load_command(command_name || command)
end

def convert_invalid_byte_sequence(str, enc)
str.force_encoding(enc)
str.scrub { |c|
Expand Down
25 changes: 20 additions & 5 deletions lib/irb/ruby-lex.rb
Expand Up @@ -7,6 +7,7 @@
require "ripper"
require "jruby" if RUBY_ENGINE == "jruby"
require_relative "nesting_parser"
require_relative "statement"

# :stopdoc:
class RubyLex
Expand Down Expand Up @@ -221,16 +222,30 @@ def each_top_level_statement
break unless code

if code != "\n"
code.force_encoding(@context.io.encoding)
yield code, @line_no, assignment_expression?(code)
yield build_statement(code), @line_no
end
increase_line_no(code.count("\n"))
rescue TerminateLineInput
end
end

def assignment_expression?(line)
# Try to parse the line and check if the last of possibly multiple
def build_statement(code)
code.force_encoding(@context.io.encoding)
command_or_alias, arg = code.split(/\s/, 2)
# Transform a non-identifier alias (@, $) or keywords (next, break)
command_name = @context.command_aliases[command_or_alias.to_sym]
command = command_name || command_or_alias
command_class = IRB::ExtendCommandBundle.load_command(command)

if command_class
IRB::Statement::Command.new(code, command, arg, command_class)
else
IRB::Statement::Expression.new(code, assignment_expression?(code))
end
end

def assignment_expression?(code)
# Try to parse the code and check if the last of possibly multiple
# expressions is an assignment type.

# If the expression is invalid, Ripper.sexp should return nil which will
Expand All @@ -239,7 +254,7 @@ def assignment_expression?(line)
# array of parsed expressions. The first element of each expression is the
# expression's type.
verbose, $VERBOSE = $VERBOSE, nil
code = "#{RubyLex.generate_local_variables_assign_code(@context.local_variables) || 'nil;'}\n#{line}"
code = "#{RubyLex.generate_local_variables_assign_code(@context.local_variables) || 'nil;'}\n#{code}"
# Get the last node_type of the line. drop(1) is to ignore the local_variables_assign_code part.
node_type = Ripper.sexp(code)&.dig(1)&.drop(1)&.dig(-1, 0)
ASSIGNMENT_NODE_TYPES.include?(node_type)
Expand Down
78 changes: 78 additions & 0 deletions lib/irb/statement.rb
@@ -0,0 +1,78 @@
# frozen_string_literal: true

module IRB
class Statement
attr_reader :code

def is_assignment?
raise NotImplementedError
end

def suppresses_echo?
raise NotImplementedError
end

def should_be_handled_by_debugger?
raise NotImplementedError
end

def evaluable_code
raise NotImplementedError
end

class Expression < Statement
def initialize(code, is_assignment)
@code = code
@is_assignment = is_assignment
end

def suppresses_echo?
@code.match?(/;\s*\z/)
end

def should_be_handled_by_debugger?
true
end

def is_assignment?
@is_assignment
end

def evaluable_code
@code
end
end

class Command < Statement
def initialize(code, command, arg, command_class)
@code = code
@command = command
@arg = arg
@command_class = command_class
end

def is_assignment?
false
end

def suppresses_echo?
false
end

def should_be_handled_by_debugger?
IRB::ExtendCommand::DebugCommand > @command_class
end

def evaluable_code
# Hook command-specific transformation to return valid Ruby code
if @command_class.respond_to?(:transform_args)
arg = @command_class.transform_args(@arg)
else
arg = @arg
end

[@command, arg].compact.join(' ')
end
end
end
end

0 comments on commit 5a40f7d

Please sign in to comment.