Skip to content

Commit

Permalink
Introduce Statement class
Browse files Browse the repository at this point in the history
  • Loading branch information
st0012 committed Aug 14, 2023
1 parent d8fb324 commit 817a2ec
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 41 deletions.
42 changes: 6 additions & 36 deletions lib/irb.rb
Original file line number Diff line number Diff line change
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
26 changes: 21 additions & 5 deletions lib/irb/ruby-lex.rb
Original file line number Diff line number Diff line change
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,31 @@ 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)
IRB::Statement.new(
code,
assignment_expression?(code),
command,
arg,
command_class
)
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 +255,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
42 changes: 42 additions & 0 deletions lib/irb/statement.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true

module IRB
class Statement
attr_reader :code, :is_assignment

def initialize(code, is_assignment, command, arg, command_class)
@code = code
@is_assignment = is_assignment
@command = command
@arg = arg
@command_class = command_class
end

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

# 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
def should_be_handled_by_debugger?
!@command_class || IRB::ExtendCommand::DebugCommand > @command_class
end

# Because IRB accepts symbol aliases, the code user inputs may not be evaluable Ruby code
# This method returns code that's evaluable by Ruby
def evaluable_code
return @code unless @command_class

# Hook command-specific transformation
if @command_class.respond_to?(:transform_args)
arg = @command_class.transform_args(@arg)
else
arg = @arg
end

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

0 comments on commit 817a2ec

Please sign in to comment.