Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encapsulate input details in Statement objects #682

Merged
merged 2 commits into from
Aug 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
25 changes: 20 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,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
Original file line number Diff line number Diff line change
@@ -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